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 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 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 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 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();
+ }
+ }
+}