diff --git a/bom/application/pom.xml b/bom/application/pom.xml index a3a2b7a8ce871..de150d47b6aff 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -55,6 +55,7 @@ 2.6.0 2.13.0 3.9.1 + 1.0.0.Alpha6 1.2.1 1.3.5 3.0.4 @@ -3533,6 +3534,21 @@ smallrye-jwt-build ${smallrye-jwt.version} + + io.smallrye.stork + smallrye-stork-load-balancer-response-time + ${smallrye-stork.version} + + + io.smallrye.stork + smallrye-stork-service-discovery-static-list + ${smallrye-stork.version} + + + io.smallrye.stork + smallrye-stork-microprofile-config + ${smallrye-stork.version} + jakarta.activation jakarta.activation-api diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/pom.xml b/extensions/resteasy-reactive/rest-client-reactive/deployment/pom.xml index 207e881efd431..85ee3b2865600 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/pom.xml +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/pom.xml @@ -56,7 +56,17 @@ jakarta.servlet-api test - + + io.smallrye.stork + smallrye-stork-load-balancer-response-time + test + + + io.smallrye.stork + smallrye-stork-service-discovery-static-list + test + + diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java index b13fce2f358b3..ce3b3429e1b7b 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java @@ -4,13 +4,13 @@ import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_CLIENT_HEADERS; import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_PROVIDER; import static io.quarkus.rest.client.reactive.deployment.DotNames.REGISTER_PROVIDERS; +import static java.util.Arrays.asList; import static org.jboss.resteasy.reactive.common.processor.EndpointIndexer.CDI_WRAPPER_SUFFIX; import static org.jboss.resteasy.reactive.common.processor.scanning.ResteasyReactiveScanner.BUILTIN_HTTP_ANNOTATIONS_TO_METHOD; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Modifier; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -54,14 +54,18 @@ import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Consume; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ConfigurationTypeBuildItem; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; +import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.util.AsmUtil; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.MethodCreator; @@ -77,9 +81,13 @@ import io.quarkus.rest.client.reactive.runtime.RestClientReactiveCDIWrapperBase; import io.quarkus.rest.client.reactive.runtime.RestClientReactiveConfig; import io.quarkus.rest.client.reactive.runtime.RestClientRecorder; +import io.quarkus.rest.client.reactive.runtime.SmallRyeStorkRecorder; import io.quarkus.restclient.config.RestClientConfigUtils; import io.quarkus.restclient.config.RestClientsConfig; import io.quarkus.resteasy.reactive.spi.ContainerRequestFilterBuildItem; +import io.smallrye.stork.microprofile.MicroProfileConfigProvider; +import io.smallrye.stork.spi.LoadBalancerProvider; +import io.smallrye.stork.spi.ServiceDiscoveryProvider; class RestClientReactiveProcessor { @@ -95,6 +103,16 @@ void announceFeature(BuildProducer features) { features.produce(new FeatureBuildItem(Feature.REST_CLIENT_REACTIVE)); } + @BuildStep + void registerServiceProviders(BuildProducer services) { + services.produce(new ServiceProviderBuildItem(io.smallrye.stork.config.ConfigProvider.class.getName(), + MicroProfileConfigProvider.class.getName())); + + for (Class providerClass : asList(LoadBalancerProvider.class, ServiceDiscoveryProvider.class)) { + services.produce(ServiceProviderBuildItem.allProvidersFromClassPath(providerClass.getName())); + } + } + @BuildStep void registerQueryParamStyleForConfig(BuildProducer configurationTypes) { configurationTypes.produce(new ConfigurationTypeBuildItem(QueryParamStyle.class)); @@ -135,14 +153,20 @@ void registerRestClientListenerForTracing( @BuildStep @Record(ExecutionTime.STATIC_INIT) - void setupAdditionalBeans( - BuildProducer additionalBeans, + void setupAdditionalBeans(BuildProducer additionalBeans, RestClientRecorder restClientRecorder) { restClientRecorder.setRestClientBuilderResolver(); additionalBeans.produce(new AdditionalBeanBuildItem(RestClient.class)); additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(HeaderContainer.class)); } + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + @Consume(RuntimeConfigSetupCompleteBuildItem.class) + void initializeStork(SmallRyeStorkRecorder storkRecorder, ShutdownContextBuildItem shutdown) { + storkRecorder.initialize(shutdown); + } + @BuildStep UnremovableBeanBuildItem makeConfigUnremovable() { return UnremovableBeanBuildItem.beanTypes(RestClientsConfig.class); @@ -220,7 +244,7 @@ void registerProvidersFromAnnotations(CombinedIndexBuildItem indexBuildItem, for (AnnotationInstance annotation : index.getAnnotations(REGISTER_PROVIDERS)) { String targetClass = annotation.target().asClass().name().toString(); annotationsByClassName.computeIfAbsent(targetClass, key -> new ArrayList<>()) - .addAll(Arrays.asList(annotation.value().asNestedArray())); + .addAll(asList(annotation.value().asNestedArray())); } try (ClassCreator classCreator = ClassCreator.builder() @@ -310,7 +334,7 @@ AdditionalBeanBuildItem registerProviderBeans(CombinedIndexBuildItem combinedInd IndexView index = combinedIndex.getIndex(); List allInstances = new ArrayList<>(index.getAnnotations(REGISTER_PROVIDER)); for (AnnotationInstance annotation : index.getAnnotations(REGISTER_PROVIDERS)) { - allInstances.addAll(Arrays.asList(annotation.value().asNestedArray())); + allInstances.addAll(asList(annotation.value().asNestedArray())); } allInstances.addAll(index.getAnnotations(REGISTER_CLIENT_HEADERS)); AdditionalBeanBuildItem.Builder builder = AdditionalBeanBuildItem.builder().setUnremovable(); diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/HelloClient.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/HelloClient.java new file mode 100644 index 0000000000000..0d1db10a04d95 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/HelloClient.java @@ -0,0 +1,13 @@ +package io.quarkus.rest.client.reactive.stork; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@Path("/") +@RegisterRestClient(configKey = "hello2") +public interface HelloClient { + @GET + String hello(); +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/HelloResource.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/HelloResource.java new file mode 100644 index 0000000000000..d53f325fad23c --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/HelloResource.java @@ -0,0 +1,15 @@ +package io.quarkus.rest.client.reactive.stork; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +@Path("/hello") +public class HelloResource { + + public static final String HELLO_WORLD = "Hello, World!"; + + @GET + public String hello() { + return HELLO_WORLD; + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/PassThroughResource.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/PassThroughResource.java new file mode 100644 index 0000000000000..efb6a781befb1 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/PassThroughResource.java @@ -0,0 +1,30 @@ +package io.quarkus.rest.client.reactive.stork; + +import java.net.URI; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/helper") +public class PassThroughResource { + + @RestClient + HelloClient client; + + @GET + public String invokeClient() { + HelloClient client = RestClientBuilder.newBuilder() + .baseUri(URI.create("stork://hello-service/hello")) + .build(HelloClient.class); + return client.hello(); + } + + @Path("/cdi") + @GET + public String invokeCdiClient() { + return client.hello(); + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/StorkDevModeTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/StorkDevModeTest.java new file mode 100644 index 0000000000000..7a90089a494c3 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/StorkDevModeTest.java @@ -0,0 +1,72 @@ +package io.quarkus.rest.client.reactive.stork; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static io.quarkus.rest.client.reactive.stork.HelloResource.HELLO_WORLD; +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.equalTo; + +import java.io.File; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; + +import io.quarkus.test.QuarkusDevModeTest; + +public class StorkDevModeTest { + + public static final String WIREMOCK_RESPONSE = "response from the wiremock server"; + + private static WireMockServer wireMockServer; + + @BeforeAll + public static void setUp() { + wireMockServer = new WireMockServer(options().port(8766)); + wireMockServer.stubFor(WireMock.get("/hello") + .willReturn(aResponse().withFixedDelay(1000) + .withBody(WIREMOCK_RESPONSE).withStatus(200))); + wireMockServer.start(); + } + + @AfterAll + public static void shutDown() { + wireMockServer.stop(); + } + + @RegisterExtension + static QuarkusDevModeTest TEST = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(PassThroughResource.class, HelloResource.class, HelloClient.class) + .addAsResource( + new File("src/test/resources/stork-dev-application.properties"), + "application.properties")); + + @Test + void shouldModifyStorkSettings() { + // @formatter:off + when() + .get("/helper") + .then() + .statusCode(200) + .body(equalTo(HELLO_WORLD)); + // @formatter:on + + TEST.modifyResourceFile("application.properties", + v -> v.replaceAll("stork.hello-service.service-discovery.1=.*", + "stork.hello-service.service-discovery.1=localhost:8766")); + // @formatter:off + when() + .get("/helper") + .then() + .statusCode(200) + .body(equalTo(WIREMOCK_RESPONSE)); + // @formatter:on + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/StorkIntegrationTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/StorkIntegrationTest.java new file mode 100644 index 0000000000000..71ee21427eab7 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/StorkIntegrationTest.java @@ -0,0 +1,61 @@ +package io.quarkus.rest.client.reactive.stork; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.rest.client.reactive.HelloClient2; +import io.quarkus.rest.client.reactive.HelloResource; +import io.quarkus.test.QuarkusUnitTest; + +public class StorkIntegrationTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(HelloClient2.class, HelloResource.class)) + .withConfigurationResource("stork-application.properties"); + + @RestClient + HelloClient2 client; + + @Test + void shouldDetermineUrlViaStork() { + String greeting = RestClientBuilder.newBuilder().baseUri(URI.create("stork://hello-service/hello")) + .build(HelloClient2.class) + .echo("black and white bird"); + assertThat(greeting).isEqualTo("hello, black and white bird"); + } + + @Test + void shouldDetermineUrlViaStorkCDI() { + String greeting = client.echo("big bird"); + assertThat(greeting).isEqualTo("hello, big bird"); + } + + @Test + @Timeout(20) + void shouldFailOnUnknownService() { + HelloClient2 client2 = RestClientBuilder.newBuilder() + .baseUri(URI.create("stork://nonexistent-service")) + .build(HelloClient2.class); + assertThatThrownBy(() -> client2.echo("foo")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @Timeout(20) + void shouldFailForServiceWithoutEndpoints() { + HelloClient2 client2 = RestClientBuilder.newBuilder() + .baseUri(URI.create("stork://service-without-endpoints")) + .build(HelloClient2.class); + assertThatThrownBy(() -> client2.echo("foo")).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/StorkResponseTimeLoadBalancerTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/StorkResponseTimeLoadBalancerTest.java new file mode 100644 index 0000000000000..cf02dbafc4544 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/StorkResponseTimeLoadBalancerTest.java @@ -0,0 +1,67 @@ +package io.quarkus.rest.client.reactive.stork; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; + +import io.quarkus.rest.client.reactive.HelloClient2; +import io.quarkus.rest.client.reactive.HelloResource; +import io.quarkus.test.QuarkusUnitTest; + +public class StorkResponseTimeLoadBalancerTest { + + private static final String SLOW_RESPONSE = "hello, I'm a slow server"; + private static WireMockServer server; + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(HelloClient2.class, HelloResource.class)) + .withConfigurationResource("stork-stat-lb.properties"); + + @BeforeAll + public static void setUp() { + server = new WireMockServer(options().port(8766)); + server.stubFor(WireMock.post("/hello/") + .willReturn(aResponse().withFixedDelay(1000) + .withBody(SLOW_RESPONSE).withStatus(200))); + server.start(); + } + + @AfterAll + public static void shutDown() { + server.shutdown(); + } + + @RestClient + HelloClient2 client; + + @Test + void shouldUseFasterService() { + Set responses = new HashSet<>(); + responses.add(client.echo("Bob")); + responses.add(client.echo("Bob")); + + assertThat(responses).contains("hello, Bob", SLOW_RESPONSE); + + // after hitting the slow endpoint, we should only use the fast one: + assertThat(client.echo("Alice")).isEqualTo("hello, Alice"); + assertThat(client.echo("Alice")).isEqualTo("hello, Alice"); + assertThat(client.echo("Alice")).isEqualTo("hello, Alice"); + } + +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/stork-application.properties b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/stork-application.properties new file mode 100644 index 0000000000000..bf9d559041ed7 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/stork-application.properties @@ -0,0 +1,5 @@ +stork.hello-service.service-discovery=static +stork.hello-service.service-discovery.1=${quarkus.http.host}:${quarkus.http.test-port} +stork.hello-service.load-balancer=least-response-time + +hello2/mp-rest/url=stork://hello-service/hello diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/stork-dev-application.properties b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/stork-dev-application.properties new file mode 100644 index 0000000000000..70d2bf645f6e0 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/stork-dev-application.properties @@ -0,0 +1,5 @@ +stork.hello-service.service-discovery=static +stork.hello-service.service-discovery.1=${quarkus.http.host}:${quarkus.http.port} +stork.hello-service.load-balancer=least-response-time + +hello2/mp-rest/url=stork://hello-service/hello diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/stork-stat-lb.properties b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/stork-stat-lb.properties new file mode 100644 index 0000000000000..e85058d6a9217 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/resources/stork-stat-lb.properties @@ -0,0 +1,5 @@ +stork.hello-service.service-discovery=static +stork.hello-service.service-discovery.1=${quarkus.http.host}:${quarkus.http.test-port} +stork.hello-service.service-discovery.2=localhost:8766 +stork.hello-service.load-balancer=least-response-time +hello2/mp-rest/url=stork://hello-service/hello diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/pom.xml b/extensions/resteasy-reactive/rest-client-reactive/runtime/pom.xml index e8483020898b9..d29f2674800cc 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/pom.xml +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/pom.xml @@ -22,6 +22,10 @@ io.quarkus quarkus-rest-client-config + + io.smallrye.stork + smallrye-stork-microprofile-config + org.eclipse.microprofile.rest.client microprofile-rest-client-api diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java index c24806223c8df..49e04e5f7a2c4 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java @@ -1,6 +1,7 @@ package io.quarkus.rest.client.reactive.runtime; import java.lang.reflect.InvocationTargetException; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.security.KeyStore; @@ -43,13 +44,17 @@ public class RestClientBuilderImpl implements RestClientBuilder { .withConfig(new ConfigurationImpl(RuntimeType.CLIENT)); private final List> exceptionMappers = new ArrayList<>(); - private URL url; + private URI uri; private boolean followRedirects; private QueryParamStyle queryParamStyle; @Override public RestClientBuilder baseUrl(URL url) { - this.url = url; + try { + this.uri = url.toURI(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Failed to convert REST client URL to URI", e); + } return this; } @@ -206,6 +211,12 @@ public RestClientBuilder register(Object component, Map, Integer> contr return this; } + @Override + public RestClientBuilder baseUri(URI uri) { + this.uri = uri; + return this; + } + private void registerMpSpecificProvider(Class componentClass) { if (ResponseExceptionMapper.class.isAssignableFrom(componentClass)) { try { @@ -231,7 +242,7 @@ public RestClientBuilder queryParamStyle(final QueryParamStyle style) { @Override public T build(Class aClass) throws IllegalStateException, RestClientDefinitionException { - if (url == null) { + if (uri == null) { // mandated by the spec throw new IllegalStateException("No URL specified. Cannot build a rest client without URL"); } @@ -263,12 +274,7 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi clientBuilder.trustAll(trustAll); ClientImpl client = clientBuilder.build(); - WebTargetImpl target; - try { - target = (WebTargetImpl) client.target(url.toURI()); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid Rest Client URL: " + url, e); - } + WebTargetImpl target = (WebTargetImpl) client.target(uri); try { return target.proxy(aClass); } catch (InvalidRestClientDefinitionException e) { diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java index 9ee52501eaf78..b3fb164f97403 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java @@ -6,8 +6,8 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; -import java.net.MalformedURLException; -import java.net.URL; +import java.net.URI; +import java.net.URISyntaxException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -298,7 +298,9 @@ private void configureTimeouts(RestClientBuilder builder) { private void configureBaseUrl(RestClientBuilder builder) { Optional propertyOptional = oneOf(clientConfigByClassName().uri, clientConfigByConfigKey().uri); + if (propertyOptional.isEmpty()) { + // mstodo is the url there string? propertyOptional = oneOf(clientConfigByClassName().url, clientConfigByConfigKey().url); } @@ -316,15 +318,9 @@ private void configureBaseUrl(RestClientBuilder builder) { String baseUrl = propertyOptional.orElse(baseUriFromAnnotation); try { - builder.baseUrl(new URL(baseUrl)); - } catch (MalformedURLException e) { + builder.baseUri(new URI(baseUrl)); + } catch (URISyntaxException e) { throw new IllegalArgumentException("The value of URL was invalid " + baseUrl, e); - } catch (Exception e) { - if ("com.oracle.svm.core.jdk.UnsupportedFeatureError".equals(e.getClass().getCanonicalName())) { - throw new IllegalArgumentException(baseUrl - + " requires SSL support but it is disabled. You probably have set quarkus.ssl.native to false."); - } - throw e; } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/SmallRyeStorkRecorder.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/SmallRyeStorkRecorder.java new file mode 100644 index 0000000000000..42e5c6ccb061f --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/SmallRyeStorkRecorder.java @@ -0,0 +1,19 @@ +package io.quarkus.rest.client.reactive.runtime; + +import io.quarkus.runtime.ShutdownContext; +import io.quarkus.runtime.annotations.Recorder; +import io.smallrye.stork.Stork; + +@Recorder +public class SmallRyeStorkRecorder { + + public void initialize(ShutdownContext shutdown) { + Stork.initialize(); + shutdown.addShutdownTask(new Runnable() { + @Override + public void run() { + Stork.shutdown(); + } + }); + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java index c8882656e9c65..f4c42ed7e56fb 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java @@ -3,7 +3,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.net.URL; +import java.net.URI; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -79,7 +79,7 @@ public void testQuarkusConfig() throws Exception { "test-client", configRoot).build(restClientBuilderMock); - Mockito.verify(restClientBuilderMock).baseUrl(new URL("http://localhost")); + Mockito.verify(restClientBuilderMock).baseUri(URI.create("http://localhost")); Mockito.verify(restClientBuilderMock).register(MyResponseFilter1.class); Mockito.verify(restClientBuilderMock).connectTimeout(100, TimeUnit.MILLISECONDS); Mockito.verify(restClientBuilderMock).readTimeout(101, TimeUnit.MILLISECONDS); diff --git a/independent-projects/resteasy-reactive/client/runtime/pom.xml b/independent-projects/resteasy-reactive/client/runtime/pom.xml index 4f2097ac8883e..bc3c1525e6dfc 100644 --- a/independent-projects/resteasy-reactive/client/runtime/pom.xml +++ b/independent-projects/resteasy-reactive/client/runtime/pom.xml @@ -14,7 +14,10 @@ RESTEasy Reactive - Client - Runtime - + + io.smallrye.stork + smallrye-stork-api + io.quarkus.resteasy.reactive resteasy-reactive-common diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/AsyncResultUni.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/AsyncResultUni.java new file mode 100644 index 0000000000000..da447c1a6b54b --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/AsyncResultUni.java @@ -0,0 +1,47 @@ +package org.jboss.resteasy.reactive.client; + +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import io.smallrye.mutiny.operators.AbstractUni; +import io.smallrye.mutiny.subscription.UniSubscriber; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public class AsyncResultUni extends AbstractUni implements Uni { + private final Consumer>> subscriptionConsumer; + + public static Uni toUni(Consumer>> subscriptionConsumer) { + return new AsyncResultUni<>(subscriptionConsumer); + } + + public AsyncResultUni(Consumer>> subscriptionConsumer) { + this.subscriptionConsumer = Infrastructure.decorate(subscriptionConsumer); + } + + @Override + public void subscribe(UniSubscriber downstream) { + AtomicBoolean terminated = new AtomicBoolean(); + downstream.onSubscribe(() -> terminated.set(true)); + + if (!terminated.get()) { + try { + subscriptionConsumer.accept(ar -> { + if (!terminated.getAndSet(true)) { + if (ar.succeeded()) { + T val = ar.result(); + downstream.onItem(val); + } else if (ar.failed()) { + downstream.onFailure(ar.cause()); + } + } + }); + } catch (Exception e) { + if (!terminated.getAndSet(true)) { + downstream.onFailure(e); + } + } + } + } +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java index 62057faa31cfc..da46aa99fb8b8 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java @@ -1,6 +1,9 @@ package org.jboss.resteasy.reactive.client.handlers; import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder; +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.ServiceInstance; +import io.smallrye.stork.Stork; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.MultiMap; @@ -18,11 +21,16 @@ import java.net.URI; import java.util.List; import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.ProcessingException; import javax.ws.rs.client.Entity; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Variant; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.client.AsyncResultUni; import org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties; import org.jboss.resteasy.reactive.client.impl.AsyncInvokerImpl; import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; @@ -32,6 +40,8 @@ import org.jboss.resteasy.reactive.common.core.Serialisers; public class ClientSendRequestHandler implements ClientRestHandler { + private static final Logger log = Logger.getLogger(ClientSendRequestHandler.class); + private final boolean followRedirects; public ClientSendRequestHandler(boolean followRedirects) { @@ -44,21 +54,19 @@ public void handle(RestClientRequestContext requestContext) { return; } requestContext.suspend(); - Future future = createRequest(requestContext); + Uni future = createRequest(requestContext); + // DNS failures happen before we send the request - future.onFailure(new Handler() { + future.subscribe().with(new Consumer<>() { @Override - public void handle(Throwable event) { - if (event instanceof IOException) { - requestContext.resume(new ProcessingException(event)); + public void accept(HttpClientRequest httpClientRequest) { + final long startTime; + + if (requestContext.getCallStatsCollector() != null) { + startTime = System.nanoTime(); } else { - requestContext.resume(event); + startTime = 0L; } - } - }); - future.onSuccess(new Handler() { - @Override - public void handle(HttpClientRequest httpClientRequest) { Future sent; if (requestContext.isMultipart()) { Promise requestPromise = Promise.promise(); @@ -90,6 +98,7 @@ public void handle(HttpClientRequest httpClientRequest) { requestPromise.complete(httpClientRequest); } catch (Throwable e) { + reportFinish(System.nanoTime() - startTime, e, requestContext); requestContext.resume(e); return; } @@ -109,16 +118,22 @@ public void handle(HttpClientRequest httpClientRequest) { } } - sent.onSuccess(new Handler() { + sent.onSuccess(new Handler<>() { @Override public void handle(HttpClientResponse clientResponse) { try { requestContext.initialiseResponse(clientResponse); + int status = clientResponse.statusCode(); + if (status >= 500 && status < 600) { + reportFinish(System.nanoTime() - startTime, new InternalServerErrorException(), requestContext); + } else { + reportFinish(System.nanoTime() - startTime, null, requestContext); + } if (!requestContext.isRegisterBodyHandler()) { clientResponse.pause(); requestContext.resume(); } else { - clientResponse.bodyHandler(new Handler() { + clientResponse.bodyHandler(new Handler<>() { @Override public void handle(Buffer buffer) { try { @@ -136,11 +151,12 @@ public void handle(Buffer buffer) { }); } } catch (Throwable t) { + reportFinish(System.nanoTime() - startTime, t, requestContext); requestContext.resume(t); } } }) - .onFailure(new Handler() { + .onFailure(new Handler<>() { @Override public void handle(Throwable failure) { if (failure instanceof IOException) { @@ -151,26 +167,87 @@ public void handle(Throwable failure) { } }); } + }, new Consumer<>() { + @Override + public void accept(Throwable event) { + if (event instanceof IOException) { + ProcessingException throwable = new ProcessingException(event); + reportFinish(0, throwable, requestContext); + requestContext.resume(throwable); + } else { + requestContext.resume(event); + reportFinish(0, event, requestContext); + } + } }); } - public Future createRequest(RestClientRequestContext state) { + private void reportFinish(long timeInNs, Throwable throwable, RestClientRequestContext requestContext) { + ServiceInstance serviceInstance = requestContext.getCallStatsCollector(); + if (serviceInstance != null) { + serviceInstance.recordResult(timeInNs, throwable); + } + } + + public Uni createRequest(RestClientRequestContext state) { HttpClient httpClient = state.getHttpClient(); URI uri = state.getUri(); - boolean isHttps = "https".equals(uri.getScheme()); - int port = uri.getPort() != -1 ? uri.getPort() : (isHttps ? 443 : 80); - RequestOptions requestOptions = new RequestOptions(); - requestOptions.setHost(uri.getHost()); - requestOptions.setPort(port); - requestOptions.setMethod(HttpMethod.valueOf(state.getHttpMethod())); - requestOptions.setURI(uri.getPath() + (uri.getQuery() == null ? "" : "?" + uri.getQuery())); - requestOptions.setFollowRedirects(followRedirects); - requestOptions.setSsl(isHttps); Object readTimeout = state.getConfiguration().getProperty(QuarkusRestClientProperties.READ_TIMEOUT); - if (readTimeout instanceof Long) { - requestOptions.setTimeout((Long) readTimeout); + Uni requestOptions; + if (uri.getScheme().startsWith(Stork.STORK)) { + boolean isHttps = "storks".equals(uri.getScheme()); + String serviceName = uri.getHost(); + Uni serviceInstance; + try { + serviceInstance = Stork.getInstance() + .getService(serviceName) + .selectServiceInstance(); + } catch (Throwable e) { + log.error("Error selecting service instance for serviceName: " + serviceName, e); + return Uni.createFrom().failure(e); + } + requestOptions = serviceInstance.onItem().transform(new Function<>() { + @Override + public RequestOptions apply(ServiceInstance serviceInstance) { + if (serviceInstance.gatherStatistics()) { + state.setCallStatsCollector(serviceInstance); + } + return new RequestOptions() + .setHost(serviceInstance.getHost()) + .setPort(serviceInstance.getPort()) + .setSsl(isHttps); + } + }); + } else { + boolean isHttps = "https".equals(uri.getScheme()); + int port = getPort(isHttps, uri.getPort()); + requestOptions = Uni.createFrom().item(new RequestOptions().setHost(uri.getHost()) + .setPort(port).setSsl(isHttps)); } - return httpClient.request(requestOptions); + + return requestOptions.onItem() + .transform(r -> r.setMethod(HttpMethod.valueOf(state.getHttpMethod())) + .setURI(uri.getPath() + (uri.getQuery() == null ? "" : "?" + uri.getQuery())) + .setFollowRedirects(followRedirects)) + .onItem().invoke(r -> { + if (readTimeout instanceof Long) { + r.setTimeout((Long) readTimeout); + } + }) + .onItem().transformToUni(new Function>() { + @Override + public Uni apply(RequestOptions options) { + return AsyncResultUni.toUni(handler -> httpClient.request(options, handler)); + } + }); + } + + private int getPort(boolean isHttps, int specifiedPort) { + return specifiedPort != -1 ? specifiedPort : defaultPort(isHttps); + } + + private int defaultPort(boolean isHttps) { + return isHttps ? 443 : 80; } private QuarkusMultipartFormUpload setMultipartHeadersAndPrepareBody(HttpClientRequest httpClientRequest, diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java index 07b98c3ddf9e7..f08b25457a1df 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/RestClientRequestContext.java @@ -1,5 +1,6 @@ package org.jboss.resteasy.reactive.client.impl; +import io.smallrye.stork.ServiceInstance; import io.vertx.core.Context; import io.vertx.core.MultiMap; import io.vertx.core.buffer.Buffer; @@ -81,6 +82,7 @@ public class RestClientRequestContext extends AbstractResteasyReactiveContext T readEntity(InputStream in, GenericType responseType, MediaType mediaType, MultivaluedMap metadata) @@ -427,4 +430,12 @@ public Map getClientFilterProperties() { public ClientRestHandler[] getAbortHandlerChainWithoutResponseFilters() { return abortHandlerChainWithoutResponseFilters; } + + public void setCallStatsCollector(ServiceInstance serviceInstance) { + this.callStatsCollector = serviceInstance; + } + + public ServiceInstance getCallStatsCollector() { + return callStatsCollector; + } } diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 47297bb36ace2..ccac8e09835e4 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -57,6 +57,7 @@ 1.0.0.Final 2.0.0.Final 2.12.5 + 1.0.0.Alpha6 @@ -268,6 +269,11 @@ commons-logging-jboss-logging ${commons-logging-jboss-logging.version} + + io.smallrye.stork + smallrye-stork-api + ${smallrye-stork.version} + org.jboss.spec.javax.xml.bind jboss-jaxb-api_2.3_spec diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 848fd3011a835..9591af5495a03 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -258,6 +258,7 @@ resteasy-reactive-kotlin rest-client-reactive rest-client-reactive-multipart + rest-client-reactive-stork packaging simple with space consul-config diff --git a/integration-tests/rest-client-reactive-stork/pom.xml b/integration-tests/rest-client-reactive-stork/pom.xml new file mode 100644 index 0000000000000..f8048197851b9 --- /dev/null +++ b/integration-tests/rest-client-reactive-stork/pom.xml @@ -0,0 +1,156 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + quarkus-integration-test-rest-client-reactive-stork + Quarkus - Integration Tests - REST Client Reactive with Stork + + + + + io.quarkus + quarkus-vertx-http + + + + io.quarkus + quarkus-rest-client-reactive-jackson + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + com.github.tomakehurst + wiremock-jre8 + test + + + jakarta.servlet + jakarta.servlet-api + test + + + io.smallrye.stork + smallrye-stork-load-balancer-response-time + + + io.smallrye.stork + smallrye-stork-service-discovery-static-list + + + + + io.quarkus + quarkus-junit5 + test + + + org.assertj + assertj-core + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + + + io.quarkus + quarkus-resteasy-reactive-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-client-reactive-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-vertx-http-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + src/main/resources + true + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + native-image + + + native + + + + native + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + + + + diff --git a/integration-tests/rest-client-reactive-stork/src/main/java/io/quarkus/it/rest/client/reactive/stork/Client.java b/integration-tests/rest-client-reactive-stork/src/main/java/io/quarkus/it/rest/client/reactive/stork/Client.java new file mode 100644 index 0000000000000..f6390e407021e --- /dev/null +++ b/integration-tests/rest-client-reactive-stork/src/main/java/io/quarkus/it/rest/client/reactive/stork/Client.java @@ -0,0 +1,14 @@ +package io.quarkus.it.rest.client.reactive.stork; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(configKey = "hello") +public interface Client { + @GET + @Consumes(MediaType.TEXT_PLAIN) + String echo(String name); +} diff --git a/integration-tests/rest-client-reactive-stork/src/main/java/io/quarkus/it/rest/client/reactive/stork/ClientCallingResource.java b/integration-tests/rest-client-reactive-stork/src/main/java/io/quarkus/it/rest/client/reactive/stork/ClientCallingResource.java new file mode 100644 index 0000000000000..92be79b449f9d --- /dev/null +++ b/integration-tests/rest-client-reactive-stork/src/main/java/io/quarkus/it/rest/client/reactive/stork/ClientCallingResource.java @@ -0,0 +1,20 @@ +package io.quarkus.it.rest.client.reactive.stork; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/client") +@ApplicationScoped +public class ClientCallingResource { + + @RestClient + Client client; + + @GET + public String passThrough() { + return client.echo("World!"); + } +} diff --git a/integration-tests/rest-client-reactive-stork/src/main/resources/application.properties b/integration-tests/rest-client-reactive-stork/src/main/resources/application.properties new file mode 100644 index 0000000000000..a4a9e5670c9f7 --- /dev/null +++ b/integration-tests/rest-client-reactive-stork/src/main/resources/application.properties @@ -0,0 +1,3 @@ +stork.hello-service.service-discovery=static +stork.hello-service.load-balancer=least-response-time +hello/mp-rest/url=stork://hello-service/hello diff --git a/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/FastWiremockServer.java b/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/FastWiremockServer.java new file mode 100644 index 0000000000000..8f3abbed8fb2a --- /dev/null +++ b/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/FastWiremockServer.java @@ -0,0 +1,25 @@ +package io.quarkus.it.rest.reactive.stork; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; + +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; + +public class FastWiremockServer extends WiremockBase { + + static final String FAST_RESPONSE = "hello, I'm a fast server"; + + @Override + int port() { + return 8766; + } + + @Override + protected Map initWireMock(WireMockServer server) { + server.stubFor(WireMock.get("/hello") + .willReturn(aResponse().withBody(FAST_RESPONSE).withStatus(200))); + return Map.of("stork.hello-service.service-discovery.1", "localhost:8766"); + } +} diff --git a/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/RestClientReactiveStorkIT.java b/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/RestClientReactiveStorkIT.java new file mode 100644 index 0000000000000..8f28c5bbf556e --- /dev/null +++ b/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/RestClientReactiveStorkIT.java @@ -0,0 +1,8 @@ +package io.quarkus.it.rest.reactive.stork; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class RestClientReactiveStorkIT extends RestClientReactiveStorkTest { + +} diff --git a/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/RestClientReactiveStorkTest.java b/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/RestClientReactiveStorkTest.java new file mode 100644 index 0000000000000..31ae527c81eff --- /dev/null +++ b/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/RestClientReactiveStorkTest.java @@ -0,0 +1,45 @@ +package io.quarkus.it.rest.reactive.stork; + +import static io.quarkus.it.rest.reactive.stork.FastWiremockServer.FAST_RESPONSE; +import static io.quarkus.it.rest.reactive.stork.SlowWiremockServer.SLOW_RESPONSE; +import static io.restassured.RestAssured.when; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.response.Response; + +@QuarkusTest +@QuarkusTestResource(SlowWiremockServer.class) +@QuarkusTestResource(FastWiremockServer.class) +public class RestClientReactiveStorkTest { + + @Test + void shouldUseFasterService() { + Set responses = new HashSet<>(); + + for (int i = 0; i < 2; i++) { + Response response = when().get("/client"); + response.then().statusCode(200); + responses.add(response.asString()); + } + + assertThat(responses).contains(FAST_RESPONSE, SLOW_RESPONSE); + + responses.clear(); + + for (int i = 0; i < 3; i++) { + Response response = when().get("/client"); + response.then().statusCode(200); + responses.add(response.asString()); + } + + // after hitting the slow endpoint, we should only use the fast one: + assertThat(responses).containsOnly(FAST_RESPONSE, FAST_RESPONSE, FAST_RESPONSE); + } +} diff --git a/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/SlowWiremockServer.java b/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/SlowWiremockServer.java new file mode 100644 index 0000000000000..a1f0705bf1ff3 --- /dev/null +++ b/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/SlowWiremockServer.java @@ -0,0 +1,26 @@ +package io.quarkus.it.rest.reactive.stork; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; + +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; + +public class SlowWiremockServer extends WiremockBase { + + static final String SLOW_RESPONSE = "hello, I'm a slow server"; + + @Override + int port() { + return 8767; + } + + @Override + protected Map initWireMock(WireMockServer server) { + server.stubFor(WireMock.get("/hello") + .willReturn(aResponse().withFixedDelay(1000) + .withBody(SLOW_RESPONSE).withStatus(200))); + return Map.of("stork.hello-service.service-discovery.2", "localhost:8767"); + } +} diff --git a/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/WiremockBase.java b/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/WiremockBase.java new file mode 100644 index 0000000000000..48758efba8438 --- /dev/null +++ b/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/WiremockBase.java @@ -0,0 +1,34 @@ +package io.quarkus.it.rest.reactive.stork; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public abstract class WiremockBase implements QuarkusTestResourceLifecycleManager { + private WireMockServer server; + + abstract Map initWireMock(WireMockServer server); + + abstract int port(); + + @Override + public Map start() { + server = new WireMockServer(options().port(port())); + + var result = initWireMock(server); + server.start(); + + return result; + } + + @Override + public void stop() { + if (server != null) { + server.stop(); + } + } +}