Example for FT with webserver.

Signed-off-by: Tomas Langer <tomas.langer@oracle.com>
This commit is contained in:
Tomas Langer
2020-07-02 01:28:20 +02:00
committed by Santiago Pericasgeertsen
parent e49ca4b394
commit 753f4043bb
8 changed files with 571 additions and 5 deletions

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2020 Oracle and/or its affiliates.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.helidon.applications</groupId>
<artifactId>helidon-se</artifactId>
<version>2.0.1-SNAPSHOT</version>
<relativePath>../../../applications/se/pom.xml</relativePath>
</parent>
<groupId>io.helidon.examples.webserver</groupId>
<artifactId>helidon-examples-webserver-fault-tolerance</artifactId>
<name>Helidon WebServer Examples FT</name>
<description>
Application demonstrates Fault tolerance used in webserver.
</description>
<properties>
<mainClass>io.helidon.webserver.examples.faulttolerance.Main</mainClass>
</properties>
<dependencies>
<dependency>
<groupId>io.helidon.webserver</groupId>
<artifactId>helidon-webserver</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.fault-tolerance</groupId>
<artifactId>helidon-fault-tolerance</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.webserver</groupId>
<artifactId>helidon-webserver-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.webclient</groupId>
<artifactId>helidon-webclient</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,168 @@
/*
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.webserver.examples.faulttolerance;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
import io.helidon.common.reactive.Single;
import io.helidon.faulttolerance.Async;
import io.helidon.faulttolerance.Bulkhead;
import io.helidon.faulttolerance.CircuitBreaker;
import io.helidon.faulttolerance.Fallback;
import io.helidon.faulttolerance.Retry;
import io.helidon.faulttolerance.Timeout;
import io.helidon.webserver.Routing;
import io.helidon.webserver.ServerRequest;
import io.helidon.webserver.ServerResponse;
import io.helidon.webserver.Service;
/**
* Simple service to demonstrate fault tolerance.
*/
public class FtService implements Service {
private final Async async;
private final Bulkhead bulkhead;
private final CircuitBreaker breaker;
private final Fallback<String> fallback;
private final Retry retry;
private final Timeout timeout;
FtService() {
this.async = Async.create();
this.bulkhead = Bulkhead.builder()
.queueLength(1)
.limit(1)
.name("helidon-example-bulkhead")
.build();
this.breaker = CircuitBreaker.builder()
.volume(10)
.errorRatio(20)
.successThreshold(1)
.delay(Duration.ofSeconds(5))
.build();
this.fallback = Fallback.create(this::fallbackToMethod);
this.retry = Retry.builder()
.retryPolicy(Retry.DelayingRetryPolicy.noDelay(3))
.build();
this.timeout = Timeout.create(Duration.ofMillis(100));
}
@Override
public void update(Routing.Rules rules) {
rules.get("/async", this::async)
.get("/bulkhead/{millis}", this::bulkhead)
.get("/circuitBreaker/{success}", this::circuitBreaker)
.get("/fallback/{success}", this::fallback)
.get("/retry/{count}", this::retry)
.get("/timeout/{millis}", this::timeout);
}
private void timeout(ServerRequest request, ServerResponse response) {
long sleep = Long.parseLong(request.path().param("millis"));
timeout.invoke(() -> sleep(sleep))
.thenAccept(response::send)
.exceptionally(response::send);
}
private void retry(ServerRequest request, ServerResponse response) {
int count = Integer.parseInt(request.path().param("count"));
AtomicInteger call = new AtomicInteger(1);
AtomicInteger failures = new AtomicInteger();
retry.invoke(() -> {
int current = call.getAndIncrement();
if (current < count) {
failures.incrementAndGet();
return reactiveFailure();
}
return Single.just("calls/failures: " + current + "/" + failures.get());
}).thenAccept(response::send)
.exceptionally(response::send);
}
private void fallback(ServerRequest request, ServerResponse response) {
boolean success = "true".equalsIgnoreCase(request.path().param("success"));
if (success) {
fallback.invoke(this::reactiveData).thenAccept(response::send);
} else {
fallback.invoke(this::reactiveFailure).thenAccept(response::send);
}
}
private void circuitBreaker(ServerRequest request, ServerResponse response) {
boolean success = "true".equalsIgnoreCase(request.path().param("success"));
if (success) {
breaker.invoke(this::reactiveData)
.thenAccept(response::send)
.exceptionally(response::send);
} else {
breaker.invoke(this::reactiveFailure)
.thenAccept(response::send)
.exceptionally(response::send);
}
}
private void bulkhead(ServerRequest request, ServerResponse response) {
long sleep = Long.parseLong(request.path().param("millis"));
bulkhead.invoke(() -> sleep(sleep))
.thenAccept(response::send)
.exceptionally(response::send);
}
private void async(ServerRequest request, ServerResponse response) {
async.invoke(this::blockingData).thenApply(response::send);
}
private Single<String> reactiveFailure() {
return Single.error(new RuntimeException("reactive failure"));
}
private Single<String> sleep(long sleepMillis) {
return async.invoke(() -> {
try {
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
}
return "Slept for " + sleepMillis + " ms";
});
}
private Single<String> reactiveData() {
return async.invoke(this::blockingData);
}
private String blockingData() {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
return "blocked for 100 millis";
}
private Single<String> fallbackToMethod(Throwable e) {
return Single.just("Failed back because of " + e.getMessage());
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.webserver.examples.faulttolerance;
import java.util.concurrent.TimeoutException;
import io.helidon.common.LogConfig;
import io.helidon.common.http.Http;
import io.helidon.common.reactive.Single;
import io.helidon.faulttolerance.BulkheadException;
import io.helidon.faulttolerance.CircuitBreakerOpenException;
import io.helidon.webserver.Routing;
import io.helidon.webserver.WebServer;
/**
* Main class of Fault tolerance example.
*/
public final class Main {
// utility class
private Main() {
}
/**
* Start the example.
*
* @param args start arguments are ignored
*/
public static void main(String[] args) {
LogConfig.configureRuntime();
startServer(8079).thenRun(() -> {});
}
static Single<WebServer> startServer(int port) {
return WebServer.builder()
.routing(routing())
.port(port)
.build()
.start()
.peek(server -> {
String url = "http://localhost:" + server.port();
System.out.println("Server started on " + url);
});
}
private static Routing routing() {
return Routing.builder()
.register("/ft", new FtService())
.error(BulkheadException.class,
(req, res, ex) -> res.status(Http.Status.SERVICE_UNAVAILABLE_503).send("bulkhead"))
.error(CircuitBreakerOpenException.class,
(req, res, ex) -> res.status(Http.Status.SERVICE_UNAVAILABLE_503).send("circuit breaker"))
.error(TimeoutException.class,
(req, res, ex) -> res.status(Http.Status.REQUEST_TIMEOUT_408).send("timeout"))
.error(Throwable.class,
(req, res, ex) -> res.status(Http.Status.INTERNAL_SERVER_ERROR_500)
.send(ex.getClass().getName() + ": " + ex.getMessage()))
.build();
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Example of Fault Tolerance usage in webserver.
*/
package io.helidon.webserver.examples.faulttolerance;

View File

@@ -0,0 +1,22 @@
#
# Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
handlers=io.helidon.common.HelidonConsoleHandler
java.util.logging.SimpleFormatter.format=[%1$tc] %4$s: %2$s - %5$s %6$s%n
.level=INFO
io.helidon.microprofile.server.level=INFO
#io.helidon.faulttolerance.level=FINEST

View File

@@ -0,0 +1,201 @@
/*
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.webserver.examples.faulttolerance;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import io.helidon.common.http.Http;
import io.helidon.webclient.WebClient;
import io.helidon.webclient.WebClientResponse;
import io.helidon.webserver.WebServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
class MainTest {
private static WebServer server;
private static WebClient client;
@BeforeAll
static void initClass() throws ExecutionException, InterruptedException {
server = Main.startServer(0)
.await(10, TimeUnit.SECONDS);
client = WebClient.builder()
.baseUri("http://localhost:" + server.port() + "/ft")
.build();
}
@AfterAll
static void destroyClass() {
server.shutdown()
.await(5, TimeUnit.SECONDS);
}
@Test
void testAsync() {
String response = client.get()
.path("/async")
.request(String.class)
.await(5, TimeUnit.SECONDS);
assertThat(response, is("blocked for 100 millis"));
}
@Test
void testBulkhead() throws InterruptedException {
// bulkhead is configured for limit of 1 and queue of 1, so third
// request should fail
client.get()
.path("/bulkhead/100000")
.request()
.thenRun(() -> {});
client.get()
.path("/bulkhead/100000")
.request()
.thenRun(() -> {});
// I want to make sure the above is connected
Thread.sleep(100);
WebClientResponse third = client.get()
.path("/bulkhead/10000")
.request()
.await(1, TimeUnit.SECONDS);
// registered an error handler in Main
assertThat(third.status(), is(Http.Status.SERVICE_UNAVAILABLE_503));
assertThat(third.content().as(String.class).await(1, TimeUnit.SECONDS), is("bulkhead"));
}
@Test
void testCircuitBreaker() {
String response = client.get()
.path("/circuitBreaker/true")
.request(String.class)
.await(1, TimeUnit.SECONDS);
assertThat(response, is("blocked for 100 millis"));
// error ratio is 20% within 10 request
client.get()
.path("/circuitBreaker/false")
.request()
.await(1, TimeUnit.SECONDS);
// should work after first
response = client.get()
.path("/circuitBreaker/true")
.request(String.class)
.await(1, TimeUnit.SECONDS);
assertThat(response, is("blocked for 100 millis"));
// should open after second
client.get()
.path("/circuitBreaker/false")
.request()
.await(1, TimeUnit.SECONDS);
WebClientResponse clientResponse = client.get()
.path("/circuitBreaker/true")
.request()
.await(1, TimeUnit.SECONDS);
response = clientResponse.content().as(String.class).await(1, TimeUnit.SECONDS);
// registered an error handler in Main
assertThat(clientResponse.status(), is(Http.Status.SERVICE_UNAVAILABLE_503));
assertThat(response, is("circuit breaker"));
}
@Test
void testFallback() {
String response = client.get()
.path("/fallback/true")
.request(String.class)
.await(1, TimeUnit.SECONDS);
assertThat(response, is("blocked for 100 millis"));
response = client.get()
.path("/fallback/false")
.request(String.class)
.await(1, TimeUnit.SECONDS);
assertThat(response, is("Failed back because of reactive failure"));
}
@Test
void testRetry() {
String response = client.get()
.path("/retry/1")
.request(String.class)
.await(1, TimeUnit.SECONDS);
assertThat(response, is("calls/failures: 1/0"));
response = client.get()
.path("/retry/2")
.request(String.class)
.await(1, TimeUnit.SECONDS);
assertThat(response, is("calls/failures: 2/1"));
response = client.get()
.path("/retry/3")
.request(String.class)
.await(1, TimeUnit.SECONDS);
assertThat(response, is("calls/failures: 3/2"));
WebClientResponse clientResponse = client.get()
.path("/retry/4")
.request()
.await(1, TimeUnit.SECONDS);
response = clientResponse.content().as(String.class).await(1, TimeUnit.SECONDS);
// no error handler specified
assertThat(clientResponse.status(), is(Http.Status.INTERNAL_SERVER_ERROR_500));
assertThat(response, is("java.lang.RuntimeException: reactive failure"));
}
@Test
void testTimeout() {
String response = client.get()
.path("/timeout/50")
.request(String.class)
.await(1, TimeUnit.SECONDS);
assertThat(response, is("Slept for 50 ms"));
WebClientResponse clientResponse = client.get()
.path("/timeout/105")
.request()
.await(1, TimeUnit.SECONDS);
response = clientResponse.content().as(String.class).await(1, TimeUnit.SECONDS);
// error handler specified in Main
assertThat(clientResponse.status(), is(Http.Status.REQUEST_TIMEOUT_408));
assertThat(response, is("timeout"));
}
}

View File

@@ -42,5 +42,6 @@
<module>websocket</module>
<module>tls</module>
<module>mutual-tls</module>
<module>fault-tolerance</module>
</modules>
</project>

View File

@@ -73,11 +73,6 @@
<artifactId>helidon-webserver-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.webclient</groupId>
<artifactId>helidon-webclient</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>