![[17.0.0.3 and later]](../ng_v17003plus.gif)
Improving microservice resilience in Liberty
You can use the MicroProfile Fault Tolerance feature to make service invocation more resilient. This feature is an implementation of Eclipse Microprofile Fault Tolerance Specification 1.0. The feature uses the Failsafe open source library to provide a programming model to support resilient microservices through patterns that include retries, circuit breakers, bulkheads, timeouts, and fallbacks.
Before you begin
Procedure
- Add the mpFaultTolerance-1.0 feature to a featureManager
element in your server.xml file.
<featureManager> <feature>mpFaultTolerance-1.0</feature> </featureManager>
- Use a code snippet to improve microservice resilience. A fault tolerance circuit breaker provides a way for systems to fail-fast. It temporarily disables the running of a service to prevent the service from overloading a system.A fault tolerance RetryPolicy provides a way to configure when to retry services. If the main service fails, a fault tolerance fallback can specify a service to use.A fault tolerance bulkhead limits the number of concurrent calls to a service. The bulkhead limits the amount of system resource that service invocations can use. The code snippet requires that the Liberty concurrent-1.0 feature is specified in the server.xml file in addition to the mpFaultTolerance-1.0 feature.
Circuit breaker code snippet 1: Create a CircuitBreakerBean with configured CircuitBreaker and Timeout.
@RequestScoped public class CircuitBreakerBean { private int executionCounterA = 0; // The combined effect of the specified requestVolumeThreshold and failureRatio is that 3 // failures will trigger the circuit to open. // After a 1 second delay the Circuit will allow fresh attempts to invoke the service. @CircuitBreaker(delay = 1, delayUnit = ChronoUnit.SECONDS, requestVolumeThreshold = 3, failureRatio = 1.0) // A service is considered to have timed out after 3 seconds @Timeout(value = 3, unit = ChronoUnit.SECONDS) public String serviceA() { executionCounterA++; if (executionCounterA <= 3) { //Sleep for 10 secs to force a timeout try { Thread.sleep(10000); } catch (InterruptedException e) { System.out.println("serviceA interrupted"); } }
CircuitBreaker code snippet 2: Using the CircuitBreakerBean.
@Inject CircuitBreakerBean bean; // FaultTolerance bean with circuit breaker, should fail 3 times for (int i = 0; i < 3; i++) { try { bean.serviceA(); throw new AssertionError("TimeoutException not caught"); } catch (TimeoutException e) { //expected } } // The CircuitBreaker should be open, so calling serviceA should generate a // CircuitBreakerOpenException. try { bean.serviceA(); throw new AssertionError("CircuitBreakerOpenException not caught"); } catch (CircuitBreakerOpenException e) { //expected } //allow time for the circuit to re-close Thread.sleep(3000); // The CircuitBreaker should be closed and serviceA should now succeed. String res = bean.serviceA(); if (!"serviceA: 4".equals(res)) { throw new AssertionError("Bad Result: " + res); }
Fallback and Retry code snippet 1: FTServiceBean with configured FallbackHandler and Retry policy.
@RequestScoped public class FTServiceBean { // Annotate serviceA with a named FallbackHandler and a Retry policy specifying the // number of retries. @Retry(maxRetries = 2) @Fallback(StringFallbackHandler.class) public String serviceA() { throw new RuntimeException("Connection failed"); return null; } }
Fallback and Retry code snippet 2: A FallbackHandler, the code that is driven if the main service fails.
@Dependent public class StringFallbackHandler implements FallbackHandler<String> { @Override public String handle(ExecutionContext context) { return "fallback for " + context.getMethod().getName(); } }
Fallback and Retry code snippet 3: Using the FTServiceBean.
private @Inject FTServiceBean ftServiceBean; try { // Call serviceA, which will be retried twice in the event of failure, after which // the FallbackHandler will be driven. String result = ftServiceBean.serviceA(); if(!result.contains("serviceA")) throw new AssertionError("The message should be \"fallback for serviceA\""); } catch(RuntimeException ex) { throw new AssertionError("serviceA should not throw a RuntimeException"); }
Bulkhead code snippet 1: Create a BulkheadBean with configured Bulkhead.
@RequestScoped @Asynchronous public class BulkheadBean { private final AtomicInteger connectATokens = new AtomicInteger(0); // Configure a Bulkhead that supports at most 2 concurrent threads. @Bulkhead(maxThreads = 2) public Future<Boolean> connectA(String data) throws InterruptedException { System.out.println("connectA starting " + data); int token = connectATokens.incrementAndGet(); try { if (token > 2) { throw new RuntimeException("Too many threads in connectA[" + data + "]: " + token); } Thread.sleep(5000); return CompletableFuture.completedFuture(Boolean.TRUE); } finally { connectATokens.decrementAndGet(); System.out.println("connectA complete " + data); } } }
Bulkhead code snippet 2: Use the BulkheadBean.
@Inject BulkheadBean bean; // connectA has a poolSize of 2 // The first two calls to connectA should be run straight away, in parallel, each around // 5 seconds Future<Boolean> future1 = bean.connectA("One"); Thread.sleep(100); Future<Boolean> future2 = bean.connectA("Two"); Thread.sleep(100); // The next two calls to connectA should wait until the first 2 have finished Future<Boolean> future3 = bean.connectA("Three"); Thread.sleep(100); Future<Boolean> future4 = bean.connectA("Four"); Thread.sleep(100); //total time should be just over 10s Thread.sleep(11000); if (!future1.get(1000, TimeUnit.MILLISECONDS)) { throw new AssertionError("Future1 did not complete properly"); } if (!future2.get(1000, TimeUnit.MILLISECONDS)) { throw new AssertionError("Future2 did not complete properly"); } if (!future3.get(1000, TimeUnit.MILLISECONDS)) { throw new AssertionError("Future3 did not complete properly"); } if (!future4.get(1000, TimeUnit.MILLISECONDS)) { throw new AssertionError("Future4 did not complete properly"); }

File name: twlp_microprofile_fault_tolerance.html