diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index 23af9fa16b6607..ae2a5667298196 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.Alpha4
1.2.1
1.3.5
3.0.3
@@ -3523,6 +3524,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
+ ${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 207e881efd4317..85ee3b2865600d 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 386f9406ded822..bbf26b7f7021f0 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,14 @@
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.io.IOException;
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;
@@ -60,9 +61,13 @@
import io.quarkus.deployment.builditem.ConfigurationTypeBuildItem;
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
+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.RuntimeInitializedClassBuildItem;
+import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
import io.quarkus.deployment.util.AsmUtil;
+import io.quarkus.deployment.util.ServiceUtil;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
@@ -78,7 +83,11 @@
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.resteasy.reactive.spi.ContainerRequestFilterBuildItem;
+import io.smallrye.stork.microprofile.MicroProfileConfigProvider;
+import io.smallrye.stork.spi.LoadBalancerProvider;
+import io.smallrye.stork.spi.ServiceDiscoveryProvider;
class RestClientReactiveProcessor {
@@ -92,6 +101,33 @@ void announceFeature(BuildProducer features) {
features.produce(new FeatureBuildItem(Feature.REST_CLIENT_REACTIVE));
}
+ @BuildStep
+ void runtimeInitialize(BuildProducer producer) {
+ producer.produce(
+ new RuntimeInitializedClassBuildItem("io.smallrye.stork.Stork"));
+ }
+
+ @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)) {
+ String service = "META-INF/services/" + providerClass.getName();
+
+ // find out all the implementation classes listed in the service files
+ Set implementations;
+ try {
+ implementations = ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(),
+ service);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to collect implementations of " + service, e);
+ }
+
+ services.produce(new ServiceProviderBuildItem(providerClass.getName(), implementations));
+ }
+ }
+
@BuildStep
void registerQueryParamStyleForConfig(BuildProducer configurationTypes) {
configurationTypes.produce(new ConfigurationTypeBuildItem(QueryParamStyle.class));
@@ -129,13 +165,17 @@ void registerRestClientListenerForTracing(
}
@BuildStep
- @Record(ExecutionTime.STATIC_INIT)
+ @Record(ExecutionTime.RUNTIME_INIT)
void setupAdditionalBeans(
BuildProducer additionalBeans,
- RestClientRecorder restClientRecorder) {
+ RestClientRecorder restClientRecorder,
+ SmallRyeStorkRecorder storkRecorder,
+ ShutdownContextBuildItem shutdown) {
restClientRecorder.setRestClientBuilderResolver();
additionalBeans.produce(new AdditionalBeanBuildItem(RestClient.class));
additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(HeaderContainer.class));
+
+ storkRecorder.initialize(shutdown);
}
@BuildStep
@@ -210,7 +250,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()
@@ -300,7 +340,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/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 00000000000000..72481992b87c72
--- /dev/null
+++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/stork/StorkDevModeTest.java
@@ -0,0 +1,116 @@
+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.restassured.RestAssured.when;
+import static org.hamcrest.Matchers.equalTo;
+
+import java.io.File;
+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.RegisterRestClient;
+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.test.QuarkusDevModeTest;
+
+public class StorkDevModeTest {
+
+ public static final String WIREMOCK_RESPONSE = "response from the wiremock server";
+ public static final String HELLO_WORLD = "Hello, World!";
+
+ 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
+ }
+
+ @Path("/helper")
+ public static 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();
+ }
+ }
+
+ @Path("/")
+ @RegisterRestClient(configKey = "hello2")
+ public interface HelloClient {
+ @GET
+ String hello();
+ }
+
+ @Path("/hello")
+ public static class HelloResource {
+ @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/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 00000000000000..71ee21427eab75
--- /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 00000000000000..cf02dbafc45444
--- /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 00000000000000..bf9d559041ed7e
--- /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 00000000000000..70d2bf645f6e01
--- /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 00000000000000..e85058d6a92172
--- /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 268c6b68082e6b..1bda5480fe11dc 100644
--- a/extensions/resteasy-reactive/rest-client-reactive/runtime/pom.xml
+++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/pom.xml
@@ -18,6 +18,10 @@
io.quarkus
quarkus-jaxrs-client-reactive
+
+ io.smallrye.stork
+ smallrye-stork-microprofile
+
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 c24806223c8df7..49e04e5f7a2c4b 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 e0873729c847cb..4ddbbb78a7a27c 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;
@@ -173,7 +173,7 @@ private void registerKeyStore(String keyStorePath, RestClientBuilder builder) {
try {
KeyStore keyStore = KeyStore.getInstance(keyStoreType.orElse("JKS"));
- if (!keyStorePassword.isPresent()) {
+ if (keyStorePassword.isEmpty()) {
throw new IllegalArgumentException("No password provided for keystore");
}
String password = keyStorePassword.get();
@@ -197,7 +197,7 @@ private void registerTrustStore(String trustStorePath, RestClientBuilder builder
try {
KeyStore trustStore = KeyStore.getInstance(maybeTrustStoreType.orElse("JKS"));
- if (!maybeTrustStorePassword.isPresent()) {
+ if (maybeTrustStorePassword.isEmpty()) {
throw new IllegalArgumentException("No password provided for truststore");
}
String password = maybeTrustStorePassword.get();
@@ -275,11 +275,11 @@ private void configureTimeouts(RestClientBuilder builder) {
private void configureBaseUrl(RestClientBuilder builder) {
Optional propertyOptional = getOptionalDynamicProperty(REST_URI_FORMAT, String.class);
- if (!propertyOptional.isPresent()) {
+ if (propertyOptional.isEmpty()) {
propertyOptional = getOptionalDynamicProperty(REST_URL_FORMAT, String.class);
}
if (((baseUriFromAnnotation == null) || baseUriFromAnnotation.isEmpty())
- && !propertyOptional.isPresent()) {
+ && propertyOptional.isEmpty()) {
throw new IllegalArgumentException(
String.format(
"Unable to determine the proper baseUrl/baseUri. " +
@@ -291,15 +291,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 00000000000000..42e5c6ccb061f8
--- /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/independent-projects/resteasy-reactive/client/runtime/pom.xml b/independent-projects/resteasy-reactive/client/runtime/pom.xml
index 4f2097ac8883e6..bc3c1525e6dfc5 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 00000000000000..da447c1a6b54b0
--- /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 super T> 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 62057faa31cfc0..395e96bf2a1979 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")) {
+ 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 extends HttpClientRequest> 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 07b98c3ddf9e77..f08b25457a1dfc 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 163b3a7da21d7c..f6509a21f375d7 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.Alpha4
@@ -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 b8a59a9881b896..4a0212a209e37e 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -256,6 +256,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 00000000000000..f8048197851b99
--- /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 00000000000000..f6390e407021e5
--- /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 00000000000000..92be79b449f9d2
--- /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 00000000000000..279c461f81d226
--- /dev/null
+++ b/integration-tests/rest-client-reactive-stork/src/main/resources/application.properties
@@ -0,0 +1,5 @@
+stork.hello-service.service-discovery=static
+stork.hello-service.service-discovery.1=localhost:8767
+stork.hello-service.service-discovery.2=localhost:8766
+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/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 00000000000000..f61113ab019724
--- /dev/null
+++ b/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/RestClientReactiveStorkIT.java
@@ -0,0 +1,35 @@
+package io.quarkus.it.rest.reactive.stork;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.client.WireMock;
+
+import io.quarkus.test.junit.NativeImageTest;
+
+@NativeImageTest
+public class RestClientReactiveStorkIT extends RestClientReactiveStorkTest {
+ @BeforeAll
+ public static void setUp() {
+ slowServer = new WireMockServer(options().port(8766));
+ slowServer.stubFor(WireMock.get("/hello")
+ .willReturn(aResponse().withFixedDelay(1000)
+ .withBody(SLOW_RESPONSE).withStatus(200)));
+ slowServer.start();
+
+ fastServer = new WireMockServer(options().port(8767));
+ fastServer.stubFor(WireMock.get("/hello")
+ .willReturn(aResponse().withBody(FAST_RESPONSE).withStatus(200)));
+ fastServer.start();
+ }
+
+ @AfterAll
+ public static void shutDown() {
+ slowServer.stop();
+ fastServer.stop();
+ }
+}
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 00000000000000..f32eba5c11ab19
--- /dev/null
+++ b/integration-tests/rest-client-reactive-stork/src/test/java/io/quarkus/it/rest/reactive/stork/RestClientReactiveStorkTest.java
@@ -0,0 +1,72 @@
+package io.quarkus.it.rest.reactive.stork;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+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.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.client.WireMock;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.response.Response;
+
+@QuarkusTest
+public class RestClientReactiveStorkTest {
+
+ static final String SLOW_RESPONSE = "hello, I'm a slow server";
+ static final String FAST_RESPONSE = "hello, I'm a fast server";
+ static WireMockServer slowServer;
+ static WireMockServer fastServer;
+
+ @BeforeAll
+ public static void setUp() {
+ slowServer = new WireMockServer(options().port(8766));
+ slowServer.stubFor(WireMock.get("/hello")
+ .willReturn(aResponse().withFixedDelay(1000)
+ .withBody(SLOW_RESPONSE).withStatus(200)));
+ slowServer.start();
+
+ fastServer = new WireMockServer(options().port(8767));
+ fastServer.stubFor(WireMock.get("/hello")
+ .willReturn(aResponse().withBody(FAST_RESPONSE).withStatus(200)));
+ fastServer.start();
+ }
+
+ @AfterAll
+ public static void shutDown() {
+ slowServer.stop();
+ fastServer.stop();
+ }
+
+ @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);
+ }
+}