From 753f4043bb45a081b75d4e9fd6444ffd0af6dd3b Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Thu, 2 Jul 2020 01:28:20 +0200 Subject: [PATCH] Example for FT with webserver. Signed-off-by: Tomas Langer --- examples/webserver/fault-tolerance/pom.xml | 85 ++++++++ .../examples/faulttolerance/FtService.java | 168 +++++++++++++++ .../examples/faulttolerance/Main.java | 74 +++++++ .../examples/faulttolerance/package-info.java | 20 ++ .../src/main/resources/logging.properties | 22 ++ .../examples/faulttolerance/MainTest.java | 201 ++++++++++++++++++ examples/webserver/pom.xml | 1 + examples/webserver/tls/pom.xml | 5 - 8 files changed, 571 insertions(+), 5 deletions(-) create mode 100644 examples/webserver/fault-tolerance/pom.xml create mode 100644 examples/webserver/fault-tolerance/src/main/java/io/helidon/webserver/examples/faulttolerance/FtService.java create mode 100644 examples/webserver/fault-tolerance/src/main/java/io/helidon/webserver/examples/faulttolerance/Main.java create mode 100644 examples/webserver/fault-tolerance/src/main/java/io/helidon/webserver/examples/faulttolerance/package-info.java create mode 100644 examples/webserver/fault-tolerance/src/main/resources/logging.properties create mode 100644 examples/webserver/fault-tolerance/src/test/java/io/helidon/webserver/examples/faulttolerance/MainTest.java diff --git a/examples/webserver/fault-tolerance/pom.xml b/examples/webserver/fault-tolerance/pom.xml new file mode 100644 index 000000000..0ed98ab63 --- /dev/null +++ b/examples/webserver/fault-tolerance/pom.xml @@ -0,0 +1,85 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-se + 2.0.1-SNAPSHOT + ../../../applications/se/pom.xml + + + io.helidon.examples.webserver + helidon-examples-webserver-fault-tolerance + Helidon WebServer Examples FT + + + Application demonstrates Fault tolerance used in webserver. + + + + io.helidon.webserver.examples.faulttolerance.Main + + + + + io.helidon.webserver + helidon-webserver + + + io.helidon.fault-tolerance + helidon-fault-tolerance + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.webserver + helidon-webserver-test-support + test + + + io.helidon.webclient + helidon-webclient + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + \ No newline at end of file diff --git a/examples/webserver/fault-tolerance/src/main/java/io/helidon/webserver/examples/faulttolerance/FtService.java b/examples/webserver/fault-tolerance/src/main/java/io/helidon/webserver/examples/faulttolerance/FtService.java new file mode 100644 index 000000000..30442fc5c --- /dev/null +++ b/examples/webserver/fault-tolerance/src/main/java/io/helidon/webserver/examples/faulttolerance/FtService.java @@ -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 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 reactiveFailure() { + return Single.error(new RuntimeException("reactive failure")); + } + + private Single sleep(long sleepMillis) { + return async.invoke(() -> { + try { + Thread.sleep(sleepMillis); + } catch (InterruptedException e) { + } + return "Slept for " + sleepMillis + " ms"; + }); + } + + private Single reactiveData() { + return async.invoke(this::blockingData); + } + + private String blockingData() { + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + } + return "blocked for 100 millis"; + } + + private Single fallbackToMethod(Throwable e) { + return Single.just("Failed back because of " + e.getMessage()); + } + +} diff --git a/examples/webserver/fault-tolerance/src/main/java/io/helidon/webserver/examples/faulttolerance/Main.java b/examples/webserver/fault-tolerance/src/main/java/io/helidon/webserver/examples/faulttolerance/Main.java new file mode 100644 index 000000000..fb023f7e1 --- /dev/null +++ b/examples/webserver/fault-tolerance/src/main/java/io/helidon/webserver/examples/faulttolerance/Main.java @@ -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 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(); + } +} diff --git a/examples/webserver/fault-tolerance/src/main/java/io/helidon/webserver/examples/faulttolerance/package-info.java b/examples/webserver/fault-tolerance/src/main/java/io/helidon/webserver/examples/faulttolerance/package-info.java new file mode 100644 index 000000000..f19f38365 --- /dev/null +++ b/examples/webserver/fault-tolerance/src/main/java/io/helidon/webserver/examples/faulttolerance/package-info.java @@ -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; diff --git a/examples/webserver/fault-tolerance/src/main/resources/logging.properties b/examples/webserver/fault-tolerance/src/main/resources/logging.properties new file mode 100644 index 000000000..f34ee0f6b --- /dev/null +++ b/examples/webserver/fault-tolerance/src/main/resources/logging.properties @@ -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 diff --git a/examples/webserver/fault-tolerance/src/test/java/io/helidon/webserver/examples/faulttolerance/MainTest.java b/examples/webserver/fault-tolerance/src/test/java/io/helidon/webserver/examples/faulttolerance/MainTest.java new file mode 100644 index 000000000..250b6672f --- /dev/null +++ b/examples/webserver/fault-tolerance/src/test/java/io/helidon/webserver/examples/faulttolerance/MainTest.java @@ -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")); + } +} \ No newline at end of file diff --git a/examples/webserver/pom.xml b/examples/webserver/pom.xml index 4abee72a9..7f08b582b 100644 --- a/examples/webserver/pom.xml +++ b/examples/webserver/pom.xml @@ -42,5 +42,6 @@ websocket tls mutual-tls + fault-tolerance diff --git a/examples/webserver/tls/pom.xml b/examples/webserver/tls/pom.xml index b12c9c1d5..5feb4d944 100644 --- a/examples/webserver/tls/pom.xml +++ b/examples/webserver/tls/pom.xml @@ -73,11 +73,6 @@ helidon-webserver-test-support test - - io.helidon.webclient - helidon-webclient - test -