Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SmallRye Stork service discovery #68

Closed
wants to merge 13 commits into from
Closed
28 changes: 26 additions & 2 deletions rest-fights/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
- [Retries](#retries)
- [Hero Client](#hero-client)
- [Villain Client](#villain-client)
- [Service Discovery and Load Balancing](#service-discovery-and-client-load-balancing)
- [Service Discovery](#service-discovery)
- [Client-side Load Balancing](#client-side-load-balancing)
- [Testing](#testing)
- [Running the Application](#running-the-application)
- [Running Locally via Docker Compose](#running-locally-via-docker-compose)
Expand Down Expand Up @@ -66,6 +69,27 @@ The [`VillainClient`](src/main/java/io/quarkus/sample/superheroes/fight/client/V
- The downstream [Villain service](../rest-villains) returns a `404` if no random [`Villain`](src/main/java/io/quarkus/sample/superheroes/fight/client/Villain.java) is found. `VillainClient` handles this case and simulates the service returning nothing.
- In the event the downstream [Villain service](../rest-heroes) returns an error, `VillainClient` adds 3 retries with a 200ms delay between each retry.

## Service Discovery and Client Load Balancing
The fight service implements service discovery and client-side load balancing when making downstream calls to the [`rest-heroes`](../rest-heroes) and [`rest-villains`](../rest-villains) services. The service discovery is implemented in Quarkus using [SmallRye Stork](https://quarkus.io/blog/smallrye-stork-intro).

Stork [integrates directly with the Quarkus REST Client Reactive](http://smallrye.io/smallrye-stork/1.1.0/quarkus). This means that there is no additional code needed in the [`HeroRestClient`](src/main/java/io/quarkus/sample/superheroes/fight/client/HeroRestClient.java) in order to take advantage of Stork's service discovery and client-side load balancing.

> You could disable Stork completely for the `HeroRestClient` by setting `quarkus.rest-client.hero-client.url` to any non-Stork URL (i.e. something that doesn't start with `stork://`).

The [`VillainClient`](src/main/java/io/quarkus/sample/superheroes/fight/client/VillainClient.java), on the other hand, is implemented by directly using the [JAX-RS client API](https://docs.oracle.com/javaee/7/tutorial/jaxrs-client001.htm) with the [RESTEasy Reactive client](https://quarkus.io/guides/resteasy-reactive#resteasy-reactive-client). Therefore, the [Stork API](http://smallrye.io/smallrye-stork/1.1.0/concepts) is used directly in order to get the same functionality available in the [`HeroRestClient`](src/main/java/io/quarkus/sample/superheroes/fight/client/HeroRestClient.java).

> Similarly. you could disable Stork completely for the `VillainClient` by setting `fight.villain.client-base-url` to any non-Stork URL (i.e. something that doesn't start with `stork://`).

### Service Discovery
In local development mode, as well as when running via Docker Compose, SmallRye Stork is configured using [static list discovery](https://github.com/smallrye/smallrye-stork/blob/main/docs/service-discovery/static-list.md). In this mode, the downstream URLs are statically defined in an address list. In [`application.properties`](src/main/resources/application.properties), see the `quarkus.stork.hero-service.service-discovery.address-list` and `quarkus.stork.villain-service.service-discovery.address-list` properties.

When [running in Kubernetes](https://quarkus.io/blog/stork-kubernetes-discovery), Stork is configured to use the [Kubernetes Service Discovery](http://smallrye.io/smallrye-stork/1.1.0/kubernetes). In this mode, Stork will read the Kubernetes `Service`s for the [`rest-heroes`](../rest-heroes) and [`rest-villains`](../rest-villains) services to obtain the instance information. Additionally, the instance information has been configured to refresh every minute. See the `rest-fights-config` ConfigMap in [the Kubernetes deployment descriptors](deploy/k8s). Look for the `quarkus.stork.*` properties within the various `ConfigMap`s.

All of the other Stork service discovery mechanisms ([Consul](http://smallrye.io/smallrye-stork/1.1.0/consul) and [Eureka](http://smallrye.io/smallrye-stork/1.1.0/eureka)) can be used simply by updating the configuration appropriately according to the Stork documentation.

### Client-Side Load Balancing
In all cases, the default load balancing algorithm used is [round robin](http://smallrye.io/smallrye-stork/1.1.0/round-robin). All of the other load balancing algorithms ([random](http://smallrye.io/smallrye-stork/1.1.0/random), [least requests](http://smallrye.io/smallrye-stork/1.1.0/least-requests), [least response time](http://smallrye.io/smallrye-stork/1.1.0/response-time), and [power of two choices](http://smallrye.io/smallrye-stork/1.1.0/power-of-two-choices)) are available on the application's classpath, so feel free to play around with them by updating the configuration appropriately according to the Stork documentation.

## Testing
This application has a full suite of tests, including an [integration test suite](src/test/java/io/quarkus/sample/superheroes/fight/rest/FightResourceIT.java).
- The test suite uses [Wiremock](http://wiremock.org/) for [mocking http calls](https://quarkus.io/guides/rest-client-reactive#using-a-mock-http-server-for-tests) (see [`HeroesVillainsWiremockServerResource`](src/test/java/io/quarkus/sample/superheroes/fight/HeroesVillainsWiremockServerResource.java)) to the downstream [Hero](../rest-heroes) and [Villain](../rest-villains) services.
Expand All @@ -92,8 +116,8 @@ By default, the application is configured with the following:
| Database password | `QUARKUS_MONGODB_CREDENTIALS_PASSWORD` | `quarkus.mongodb.credentials.password` | `superfight` |
| Kafka Bootstrap servers | `KAFKA_BOOTSTRAP_SERVERS` | `kafka.bootstrap.servers` | `PLAINTEXT://localhost:9092` |
| Apicurio Schema Registry | `MP_MESSAGING_CONNECTOR_SMALLRYE_KAFKA_APICURIO_REGISTRY_URL` | `mp.messaging.connector.smallrye-kafka.apicurio.registry.url` | `http://localhost:8086/apis/registry/v2` |
| Heroes Service URL | `quarkus.rest-client.hero-client.url` | `quarkus.rest-client.hero-client.url` | `http://localhost:8083` |
| Villains Service URL | `fight.villain.client-base-url` | `fight.villain.client-base-url` | `http://localhost:8084` |
| Heroes Service URL | `QUARKUS_REST_CLIENT_HERO_CLIENT_URL` | `quarkus.rest-client.hero-client.url` | `stork://hero-service` |
| Villains Service URL | `FIGHT_VILLAIN_CLIENT_BASE_URL` | `fight.villain.client-base-url` | `stork://villain-service` |

## Running Locally via Docker Compose
Pre-built images for this application can be found at [`quay.io/quarkus-super-heroes/rest-fights`](https://quay.io/repository/quarkus-super-heroes/rest-fights?tab=tags).
Expand Down
40 changes: 40 additions & 0 deletions rest-fights/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,46 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-liquibase-mongodb</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-service-discovery-static-list</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-service-discovery-kubernetes</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-service-discovery-eureka</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-service-discovery-consul</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-load-balancer-random</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-load-balancer-least-requests</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-load-balancer-least-response-time</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-load-balancer-power-of-two-choices</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
Expand Down
4 changes: 2 additions & 2 deletions rest-fights/src/main/docker-compose/java11.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
QUARKUS_LIQUIBASE_MONGODB_MIGRATE_AT_START: false
QUARKUS_MONGODB_CREDENTIALS_USERNAME: superfight
QUARKUS_MONGODB_CREDENTIALS_PASSWORD: superfight
QUARKUS_REST_CLIENT_HERO_CLIENT_URL: http://rest-heroes:8083
FIGHT_VILLAIN_CLIENT_BASE_URL: http://rest-villains:8084
QUARKUS_STORK_HERO_SERVICE_SERVICE_DISCOVERY_ADDRESS_LIST: rest-heroes:8083
QUARKUS_STORK_VILLAIN_SERVICE_SERVICE_DISCOVERY_ADDRESS_LIST: rest-villains:8084
MP_MESSAGING_CONNECTOR_SMALLRYE_KAFKA_APICURIO_REGISTRY_URL: http://apicurio:8086/apis/registry/v2
restart: on-failure
networks:
Expand Down
4 changes: 2 additions & 2 deletions rest-fights/src/main/docker-compose/java17.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
QUARKUS_LIQUIBASE_MONGODB_MIGRATE_AT_START: false
QUARKUS_MONGODB_CREDENTIALS_USERNAME: superfight
QUARKUS_MONGODB_CREDENTIALS_PASSWORD: superfight
QUARKUS_REST_CLIENT_HERO_CLIENT_URL: http://rest-heroes:8083
FIGHT_VILLAIN_CLIENT_BASE_URL: http://rest-villains:8084
QUARKUS_STORK_HERO_SERVICE_SERVICE_DISCOVERY_ADDRESS_LIST: rest-heroes:8083
QUARKUS_STORK_VILLAIN_SERVICE_SERVICE_DISCOVERY_ADDRESS_LIST: rest-villains:8084
MP_MESSAGING_CONNECTOR_SMALLRYE_KAFKA_APICURIO_REGISTRY_URL: http://apicurio:8086/apis/registry/v2
restart: on-failure
networks:
Expand Down
4 changes: 2 additions & 2 deletions rest-fights/src/main/docker-compose/native-java11.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
QUARKUS_LIQUIBASE_MONGODB_MIGRATE_AT_START: false
QUARKUS_MONGODB_CREDENTIALS_USERNAME: superfight
QUARKUS_MONGODB_CREDENTIALS_PASSWORD: superfight
QUARKUS_REST_CLIENT_HERO_CLIENT_URL: http://rest-heroes:8083
FIGHT_VILLAIN_CLIENT_BASE_URL: http://rest-villains:8084
QUARKUS_STORK_HERO_SERVICE_SERVICE_DISCOVERY_ADDRESS_LIST: rest-heroes:8083
QUARKUS_STORK_VILLAIN_SERVICE_SERVICE_DISCOVERY_ADDRESS_LIST: rest-villains:8084
MP_MESSAGING_CONNECTOR_SMALLRYE_KAFKA_APICURIO_REGISTRY_URL: http://apicurio:8086/apis/registry/v2
restart: on-failure
networks:
Expand Down
4 changes: 2 additions & 2 deletions rest-fights/src/main/docker-compose/native-java17.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
QUARKUS_LIQUIBASE_MONGODB_MIGRATE_AT_START: false
QUARKUS_MONGODB_CREDENTIALS_USERNAME: superfight
QUARKUS_MONGODB_CREDENTIALS_PASSWORD: superfight
QUARKUS_REST_CLIENT_HERO_CLIENT_URL: http://rest-heroes:8083
FIGHT_VILLAIN_CLIENT_BASE_URL: http://rest-villains:8084
QUARKUS_STORK_HERO_SERVICE_SERVICE_DISCOVERY_ADDRESS_LIST: rest-heroes:8083
QUARKUS_STORK_VILLAIN_SERVICE_SERVICE_DISCOVERY_ADDRESS_LIST: rest-villains:8084
MP_MESSAGING_CONNECTOR_SMALLRYE_KAFKA_APICURIO_REGISTRY_URL: http://apicurio:8086/apis/registry/v2
restart: on-failure
networks:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@
import java.util.concurrent.CompletionStage;

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;

import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
import org.jboss.resteasy.reactive.client.impl.UniInvoker;

import io.quarkus.logging.Log;
import io.quarkus.sample.superheroes.fight.config.FightConfig;

import io.smallrye.faulttolerance.api.CircuitBreakerName;
import io.smallrye.mutiny.Uni;
import io.smallrye.stork.Stork;
import io.smallrye.stork.api.ServiceInstance;

/**
* Bean to be used for interacting with the Villain service.
Expand All @@ -25,12 +29,20 @@
*/
@ApplicationScoped
public class VillainClient {
private final WebTarget villainClient;
private static final String STORK_PREFIX = "stork://";
private static final String VILLAINS_API_PATH = "/api/villains";

private final WebTargetProvider webTargetProvider;

public VillainClient(FightConfig fightConfig) {
this.villainClient = ClientBuilder.newClient()
.target(fightConfig.villain().clientBaseUrl())
.path("api/villains/");
var villainClientBaseUrl = fightConfig.villain().clientBaseUrl();

if (villainClientBaseUrl.startsWith(STORK_PREFIX)) {
this.webTargetProvider = new StorkWebTargetProvider(villainClientBaseUrl.replace(STORK_PREFIX, ""));
}
else {
this.webTargetProvider = new DefaultWebTargetProvider(villainClientBaseUrl);
}
}

/**
Expand All @@ -39,16 +51,18 @@ public VillainClient(FightConfig fightConfig) {
*/
@CircuitBreaker(requestVolumeThreshold = 8, failureRatio = 0.5, delay = 2, delayUnit = ChronoUnit.SECONDS)
@CircuitBreakerName("findRandomVillain")
CompletionStage<Villain> getRandomVillain() {
// Want the 404 handling to be part of the circuit breaker
// This means that the 404 responses aren't considered errors by the circuit breaker
return this.villainClient.path("random")
.request(MediaType.APPLICATION_JSON_TYPE)
.rx(UniInvoker.class)
.get(Villain.class)
.onFailure(Is404Exception.IS_404).recoverWithNull()
.subscribeAsCompletionStage();
}
CompletionStage<Villain> getRandomVillain() {
// Want the 404 handling to be part of the circuit breaker
// This means that the 404 responses aren't considered errors by the circuit breaker
return this.webTargetProvider.getWebTarget("/random")
.flatMap(webTarget ->
webTarget.request(MediaType.APPLICATION_JSON_TYPE)
.rx(UniInvoker.class)
.get(Villain.class)
.onFailure(Is404Exception.IS_404).recoverWithNull()
)
.subscribeAsCompletionStage();
}

/**
* Finds a random {@link Villain}. The retry logic is applied to the result of the {@link CircuitBreaker}, meaning that retries that return failures could trigger the breaker to open.
Expand All @@ -66,9 +80,69 @@ public Uni<Villain> findRandomVillain() {
* @return A "hello" from Villains
*/
public Uni<String> helloVillains() {
return this.villainClient.path("hello")
.request(MediaType.TEXT_PLAIN_TYPE)
.rx(UniInvoker.class)
.get(String.class);
return this.webTargetProvider.getWebTarget("/hello")
.flatMap(webTarget ->
webTarget.request(MediaType.TEXT_PLAIN_TYPE)
.rx(UniInvoker.class)
.get(String.class)
);
}

private static abstract class WebTargetProvider {
protected abstract Uni<WebTarget> getWebTarget(String path);
}

private static class DefaultWebTargetProvider extends WebTargetProvider {
private final WebTarget webTarget;

private DefaultWebTargetProvider(String baseUrl) {
Log.debugf("Creating Default provider for baseUrl = %s", baseUrl);
this.webTarget = ClientBuilder.newClient()
.target(baseUrl)
.path(VILLAINS_API_PATH);
}

@Override
protected Uni<WebTarget> getWebTarget(String path) {
return Uni.createFrom().item(this.webTarget.path(path));
}
}

private static class StorkWebTargetProvider extends WebTargetProvider {
private final Client villainClient = ClientBuilder.newClient();
private final String storkServiceName;

private StorkWebTargetProvider(String storkServiceName) {
Log.debugf("Creating Stork provider for service name = %s", storkServiceName);
this.storkServiceName = storkServiceName;
}

private Uni<ServiceInstance> getServiceInstance() {
return Stork.getInstance()
.getService(this.storkServiceName)
.selectInstanceAndRecordStart(true);
}

private WebTarget createWebTarget(ServiceInstance serviceInstance, String path) {
var url = String.format(
"%s://%s:%d",
serviceInstance.isSecure() ? "https" : "http",
serviceInstance.getHost(),
serviceInstance.getPort()
);

Log.debugf("Targeting Stork client for service with URL = %s", url);

return this.villainClient.target(url)
.path(VILLAINS_API_PATH)
.path(path);
}

@Override
protected Uni<WebTarget> getWebTarget(String path) {
return getServiceInstance()
.onItem().ifNotNull().transform(serviceInstance -> createWebTarget(serviceInstance, path))
.onItem().ifNull().failWith(() -> new IllegalArgumentException(String.format("Can't determine a downstream service for service name '%s'. Is one configured?", this.storkServiceName)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public Uni<Response> getFight(@Parameter(name = "id", required = true) @PathPara
return Response.ok(f).build();
})
.onItem().ifNull().continueWith(() -> {
Log.debugf("No fight found with id %d", id);
Log.debugf("No fight found with id %s", id);
return Response.status(Status.NOT_FOUND).build();
});
}
Expand Down
20 changes: 18 additions & 2 deletions rest-fights/src/main/kubernetes/knative.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: default_view
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
subjects:
- kind: ServiceAccount
name: default
---
apiVersion: v1
kind: ConfigMap
metadata:
Expand All @@ -10,8 +22,12 @@ metadata:
data:
quarkus.liquibase-mongodb.migrate-at-start: false
quarkus.mongodb.hosts: fights-db:27017
quarkus.rest-client.hero-client.url: http://rest-heroes
fight.villain.client-base-url: http://rest-villains
quarkus.stork.hero-service.service-discovery.type: kubernetes
quarkus.stork.hero-service.service-discovery.application: rest-heroes
quarkus.stork.hero-service.service-discovery.refresh-period: 1M
quarkus.stork.villain-service.service-discovery.type: kubernetes
quarkus.stork.villain-service.service-discovery.application: rest-villains
quarkus.stork.villain-service.service-discovery.refresh-period: 1M
kafka.bootstrap.servers: PLAINTEXT://fights-kafka:9092
mp.messaging.connector.smallrye-kafka.apicurio.registry.url: http://apicurio:8080/apis/registry/v2
---
Expand Down
20 changes: 18 additions & 2 deletions rest-fights/src/main/kubernetes/kubernetes.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: default_view
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
subjects:
- kind: ServiceAccount
name: default
---
apiVersion: v1
kind: ConfigMap
metadata:
Expand All @@ -10,8 +22,12 @@ metadata:
data:
quarkus.liquibase-mongodb.migrate-at-start: false
quarkus.mongodb.hosts: fights-db:27017
quarkus.rest-client.hero-client.url: http://rest-heroes
fight.villain.client-base-url: http://rest-villains
quarkus.stork.hero-service.service-discovery.type: kubernetes
quarkus.stork.hero-service.service-discovery.application: rest-heroes
quarkus.stork.hero-service.service-discovery.refresh-period: 1M
quarkus.stork.villain-service.service-discovery.type: kubernetes
quarkus.stork.villain-service.service-discovery.application: rest-villains
quarkus.stork.villain-service.service-discovery.refresh-period: 1M
kafka.bootstrap.servers: PLAINTEXT://fights-kafka:9092
mp.messaging.connector.smallrye-kafka.apicurio.registry.url: http://apicurio:8080/apis/registry/v2
---
Expand Down
Loading