mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-10-11 12:19:03 +00:00
* feat: implement Fallback design pattern with circuit breaker and service monitoring * docs: add README for Fallback design pattern implementation --------- Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
This commit is contained in:
1 parent
f1dcddfd3a
commit
7e0ddb01cd
11 files changed
+791
No files matched your search
@@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
title: "Fallback Pattern in Java: Graceful Degradation in Microservices"
|
||||||
|
shortTitle: Fallback
|
||||||
|
description: "Learn about the Fallback pattern in Java design, which ensures microservice resilience and graceful system degradation when primary dependencies fail."
|
||||||
|
category: Resilience
|
||||||
|
language: en
|
||||||
|
tag:
|
||||||
|
- Cloud distributed
|
||||||
|
- Fault tolerance
|
||||||
|
- Microservices
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intent of Fallback Design Pattern
|
||||||
|
|
||||||
|
The Fallback design pattern is a resiliency pattern used in microservices architecture to handle failures gracefully. It ensures that when a service is unavailable, fails, or times out, the system can continue to operate by providing an alternative response or executing a predefined fallback mechanism. This pattern enhances robustness and reliability by preventing cascading failures and improving the overall user experience.
|
||||||
|
|
||||||
|
## Detailed Explanation of Fallback Pattern with Real-World Examples
|
||||||
|
|
||||||
|
Real-world example
|
||||||
|
|
||||||
|
> Consider a movie streaming application like Netflix. The home page loads personalized recommendations for the logged-in user. If the recommendation microservice goes offline or is too slow, the user shouldn't see a broken page. Instead, the system falls back to a cached list of globally popular movies. While the response is degraded (not personalized), the application remains functional, providing a seamless user experience.
|
||||||
|
|
||||||
|
In plain words
|
||||||
|
|
||||||
|
> Fallback ensures that if a primary service call fails, the application falls back to a backup strategy (e.g. cached response, default value, or simplified service) rather than raising an error and failing completely.
|
||||||
|
|
||||||
|
Wikipedia says
|
||||||
|
|
||||||
|
> A fallback is a contingency option to be taken if the preferred choice is unavailable. In software, fallback mechanisms are crucial for fault tolerance, allowing systems to degrade gracefully rather than crash.
|
||||||
|
|
||||||
|
## Programmatic Example of Fallback Pattern in Java
|
||||||
|
|
||||||
|
This Java example demonstrates how the Fallback pattern can manage service failures, integrate with a Circuit Breaker, and apply timeout limits.
|
||||||
|
|
||||||
|
1. **Defining the Remote Service Interface**
|
||||||
|
|
||||||
|
The `RemoteService` interface represents any external dependency call.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public interface RemoteService {
|
||||||
|
String execute() throws Exception;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Defining the Primary Service and Fallback Service**
|
||||||
|
|
||||||
|
The `PrimaryService` simulates our main external dependency which may suffer from errors or latency. The `FallbackService` returns a cached or degraded static response.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Primary Service simulating latency and errors
|
||||||
|
var healthyPrimary = new PrimaryService("Healthy data from primary service", 10, false);
|
||||||
|
var failingPrimary = new PrimaryService("Failing service", 0, true);
|
||||||
|
var slowPrimary = new PrimaryService("Slow response from primary service", 500, false);
|
||||||
|
|
||||||
|
// Fallback Service providing degraded response
|
||||||
|
var fallback = new FallbackService("Fallback degraded/cached response");
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Monitoring Health with a Circuit Breaker**
|
||||||
|
|
||||||
|
A `SimpleCircuitBreaker` tracks the number of failures to trip the circuit to `OPEN`, bypassing the primary service immediately to avoid waiting for timeouts.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Trip after 2 failures; retry after 1 second
|
||||||
|
var circuitBreaker = new SimpleCircuitBreaker(2, 1000);
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Executing Calls with the FallbackExecutor**
|
||||||
|
|
||||||
|
The `FallbackExecutor` uses virtual threads to execute the primary service call. It applies timeouts, handles exceptions, records failures to the circuit breaker, and falls back to the fallback service as needed.
|
||||||
|
|
||||||
|
```java
|
||||||
|
try (var executor = new FallbackExecutor()) {
|
||||||
|
// Scenario 1: Healthy primary service call
|
||||||
|
String response1 = executor.execute(healthyPrimary, fallback, circuitBreaker, 100);
|
||||||
|
LOGGER.info("Response: {}", response1); // Healthy data from primary service
|
||||||
|
|
||||||
|
// Scenario 2: Failing service call triggers fallback
|
||||||
|
String response2 = executor.execute(failingPrimary, fallback, circuitBreaker, 100);
|
||||||
|
LOGGER.info("Response: {}", response2); // Fallback degraded/cached response
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use the Fallback Pattern in Java
|
||||||
|
|
||||||
|
The Fallback pattern is applicable:
|
||||||
|
|
||||||
|
* In microservices architectures where dependencies are called over the network and are prone to network partitions, timeouts, and outages.
|
||||||
|
* When returning a default, empty, or cached value is preferable to failing the entire request.
|
||||||
|
* In user-facing systems where maintaining a working UI (even with degraded features) is critical for user satisfaction.
|
||||||
|
|
||||||
|
## Real-World Applications of Fallback Pattern in Java
|
||||||
|
|
||||||
|
* [Resilience4j Fallback mechanism](https://resilience4j.readme.io/docs/fallback)
|
||||||
|
* [Netflix Hystrix Fallback](https://github.com/Netflix/Hystrix/wiki/How-To-Use#Fallback)
|
||||||
|
* Spring Cloud Circuit Breaker integrations
|
||||||
|
|
||||||
|
## Benefits and Trade-offs of Fallback Pattern
|
||||||
|
|
||||||
|
Benefits:
|
||||||
|
|
||||||
|
* **Graceful Degradation**: Improves user experience by returning partial/cached data instead of errors.
|
||||||
|
* **Cascading Failure Prevention**: Avoids blocking threads waiting on hung services.
|
||||||
|
* **Fault Tolerance**: Improves system uptime and reliability.
|
||||||
|
|
||||||
|
Trade-Offs:
|
||||||
|
|
||||||
|
* **Stale Data**: Fallback cached responses may present out-of-date information to the user.
|
||||||
|
* **Increased Complexity**: Requires writing alternative execution flows and testing fallback scenarios.
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
|
||||||
|
- [Circuit Breaker](https://github.com/iluwatar/java-design-patterns/tree/master/circuit-breaker): Restricts calls to failing services. Often wraps the primary service before fallback is triggered.
|
||||||
|
- [Retry Pattern](https://github.com/iluwatar/java-design-patterns/tree/master/retry): Retries failed calls before triggering the fallback.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
|
||||||
|
The MIT License
|
||||||
|
Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<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>com.iluwatar</groupId>
|
||||||
|
<artifactId>java-design-patterns</artifactId>
|
||||||
|
<version>1.26.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>fallback</artifactId>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.qos.logback</groupId>
|
||||||
|
<artifactId>logback-classic</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>com.iluwatar.fallback.App</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.iluwatar.fallback;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Fallback design pattern is a resiliency pattern used in microservices architecture to handle
|
||||||
|
* failures gracefully. When a service is unavailable, fails, or times out, the system responds with
|
||||||
|
* a pre-configured fallback mechanism (like cached data or a simplified response).
|
||||||
|
*
|
||||||
|
* <p>This App demonstrates: 1. Healthy calls returning standard responses. 2. Failing calls
|
||||||
|
* (throwing exception) falling back to a fallback handler. 3. Latent calls (timing out) falling
|
||||||
|
* back. 4. Circuit Breaker tripping and immediately fast-failing to the fallback handler. 5.
|
||||||
|
* Recovery after a retry duration, returning the system to a healthy CLOSED state.
|
||||||
|
*/
|
||||||
|
public class App {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for the application.
|
||||||
|
*
|
||||||
|
* @param args Command line arguments (not used)
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
try (var executor = new FallbackExecutor()) {
|
||||||
|
var healthyPrimary = new PrimaryService("Healthy data from primary service", 10, false);
|
||||||
|
var failingPrimary = new PrimaryService("Failing service", 0, true);
|
||||||
|
var slowPrimary = new PrimaryService("Slow response from primary service", 500, false);
|
||||||
|
var fallback = new FallbackService("Fallback degraded/cached response");
|
||||||
|
|
||||||
|
// Failure threshold is 2 failures, retry time period is 1 second (1000ms)
|
||||||
|
var circuitBreaker = new SimpleCircuitBreaker(2, 1000);
|
||||||
|
|
||||||
|
// Scenario 1: Healthy primary service call
|
||||||
|
LOGGER.info("Scenario 1: Executing request to healthy primary service...");
|
||||||
|
String response1 = executor.execute(healthyPrimary, fallback, circuitBreaker, 100);
|
||||||
|
LOGGER.info("Response received: {}", response1);
|
||||||
|
LOGGER.info("Circuit Breaker State: {}\n", circuitBreaker.getState());
|
||||||
|
|
||||||
|
// Scenario 2: Failing primary service call (fails and increments failure count to 1)
|
||||||
|
LOGGER.info("Scenario 2: Executing request to failing primary service (throws exception)...");
|
||||||
|
String response2 = executor.execute(failingPrimary, fallback, circuitBreaker, 100);
|
||||||
|
LOGGER.info("Response received: {}", response2);
|
||||||
|
LOGGER.info("Circuit Breaker State: {}\n", circuitBreaker.getState());
|
||||||
|
|
||||||
|
// Scenario 3: Slow primary service call (times out and increments failure count to 2,
|
||||||
|
// tripping breaker)
|
||||||
|
LOGGER.info("Scenario 3: Executing request to slow primary service (triggers timeout)...");
|
||||||
|
String response3 = executor.execute(slowPrimary, fallback, circuitBreaker, 100);
|
||||||
|
LOGGER.info("Response received: {}", response3);
|
||||||
|
LOGGER.info("Circuit Breaker State: {}\n", circuitBreaker.getState());
|
||||||
|
|
||||||
|
// Scenario 4: Fast failing when circuit is OPEN
|
||||||
|
LOGGER.info("Scenario 4: Executing request while Circuit Breaker is OPEN...");
|
||||||
|
String response4 = executor.execute(healthyPrimary, fallback, circuitBreaker, 100);
|
||||||
|
LOGGER.info("Response received: {}", response4);
|
||||||
|
LOGGER.info("Circuit Breaker State: {}\n", circuitBreaker.getState());
|
||||||
|
|
||||||
|
// Scenario 5: Recovery from OPEN state
|
||||||
|
LOGGER.info("Scenario 5: Waiting for retry period to elapse...");
|
||||||
|
try {
|
||||||
|
Thread.sleep(1100); // Wait longer than retryTimePeriodMs (1000ms)
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
LOGGER.info(
|
||||||
|
"Circuit Breaker State (should be HALF_OPEN on next check): {}",
|
||||||
|
circuitBreaker.getState());
|
||||||
|
LOGGER.info("Executing request to healthy primary service to reset breaker...");
|
||||||
|
String response5 = executor.execute(healthyPrimary, fallback, circuitBreaker, 100);
|
||||||
|
LOGGER.info("Response received: {}", response5);
|
||||||
|
LOGGER.info("Circuit Breaker State (should be CLOSED): {}\n", circuitBreaker.getState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.iluwatar.fallback;
|
||||||
|
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orchestrates primary service execution with timeouts, circuit breaker health checks, and fallback
|
||||||
|
* execution logic. Implements AutoCloseable to ensure thread pools are closed cleanly.
|
||||||
|
*/
|
||||||
|
public class FallbackExecutor implements AutoCloseable {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(FallbackExecutor.class);
|
||||||
|
private final ExecutorService executorService;
|
||||||
|
|
||||||
|
/** Constructor for FallbackExecutor. Initializes a virtual thread per task executor. */
|
||||||
|
public FallbackExecutor() {
|
||||||
|
this.executorService = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the primary service call. If it fails, times out, or the circuit breaker is open, it
|
||||||
|
* falls back to the fallback service.
|
||||||
|
*
|
||||||
|
* @param primary the primary service call to execute
|
||||||
|
* @param fallback the fallback service to call when primary fails or is bypassed
|
||||||
|
* @param circuitBreaker the circuit breaker monitoring service health
|
||||||
|
* @param timeoutMs timeout limit for the primary service in milliseconds
|
||||||
|
* @return response string
|
||||||
|
*/
|
||||||
|
public String execute(
|
||||||
|
RemoteService primary,
|
||||||
|
RemoteService fallback,
|
||||||
|
SimpleCircuitBreaker circuitBreaker,
|
||||||
|
long timeoutMs) {
|
||||||
|
|
||||||
|
// 1. Check Circuit Breaker
|
||||||
|
if (circuitBreaker.getState() == SimpleCircuitBreaker.State.OPEN) {
|
||||||
|
LOGGER.warn("Circuit is OPEN. Fast-failing and calling fallback service.");
|
||||||
|
try {
|
||||||
|
return fallback.execute();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOGGER.error("Fallback service execution failed: {}", ex.getMessage());
|
||||||
|
return "Fallback Error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Attempt service call with timeout
|
||||||
|
Callable<String> task = primary::execute;
|
||||||
|
Future<String> future = executorService.submit(task);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String result = future.get(timeoutMs, TimeUnit.MILLISECONDS);
|
||||||
|
circuitBreaker.recordSuccess();
|
||||||
|
return result;
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
LOGGER.error("Service call timed out. Triggering fallback.");
|
||||||
|
future.cancel(true); // Interrupt / cancel the task
|
||||||
|
circuitBreaker.recordFailure();
|
||||||
|
try {
|
||||||
|
return fallback.execute();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOGGER.error("Fallback service execution failed: {}", ex.getMessage());
|
||||||
|
return "Fallback Error";
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Service call failed with exception: {}. Triggering fallback.", e.getMessage());
|
||||||
|
circuitBreaker.recordFailure();
|
||||||
|
try {
|
||||||
|
return fallback.execute();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOGGER.error("Fallback service execution failed: {}", ex.getMessage());
|
||||||
|
return "Fallback Error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
executorService.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.iluwatar.fallback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A concrete implementation of the remote service representing the fallback handler. It is invoked
|
||||||
|
* when the primary service fails, returns a cached response or degrades gracefully.
|
||||||
|
*/
|
||||||
|
public class FallbackService implements RemoteService {
|
||||||
|
private final String fallbackResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for FallbackService.
|
||||||
|
*
|
||||||
|
* @param fallbackResponse the fallback response to return
|
||||||
|
*/
|
||||||
|
public FallbackService(String fallbackResponse) {
|
||||||
|
this.fallbackResponse = fallbackResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String execute() {
|
||||||
|
return fallbackResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.iluwatar.fallback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A concrete implementation of the remote service representing the primary service. It can be
|
||||||
|
* configured to simulate latency and errors to test resilience features.
|
||||||
|
*/
|
||||||
|
public class PrimaryService implements RemoteService {
|
||||||
|
private final long latencyMs;
|
||||||
|
private final String response;
|
||||||
|
private final boolean shouldThrowException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for PrimaryService.
|
||||||
|
*
|
||||||
|
* @param response the successful response to return
|
||||||
|
* @param latencyMs simulated latency in milliseconds
|
||||||
|
* @param shouldThrowException if true, the service will throw an exception
|
||||||
|
*/
|
||||||
|
public PrimaryService(String response, long latencyMs, boolean shouldThrowException) {
|
||||||
|
this.latencyMs = latencyMs;
|
||||||
|
this.response = response;
|
||||||
|
this.shouldThrowException = shouldThrowException;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String execute() throws Exception {
|
||||||
|
if (shouldThrowException) {
|
||||||
|
throw new RuntimeException("Primary service failed!");
|
||||||
|
}
|
||||||
|
if (latencyMs > 0) {
|
||||||
|
Thread.sleep(latencyMs);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.iluwatar.fallback;
|
||||||
|
|
||||||
|
/** Representation of a service (e.g. a microservice client) that might fail. */
|
||||||
|
public interface RemoteService {
|
||||||
|
/**
|
||||||
|
* Executes the service logic.
|
||||||
|
*
|
||||||
|
* @return the service response
|
||||||
|
* @throws Exception if service call fails or is interrupted
|
||||||
|
*/
|
||||||
|
String execute() throws Exception;
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.iluwatar.fallback;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/** A simplified circuit breaker implementation for tracking remote service health. */
|
||||||
|
public class SimpleCircuitBreaker {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleCircuitBreaker.class);
|
||||||
|
|
||||||
|
private final int failureThreshold;
|
||||||
|
private final long retryTimePeriodMs;
|
||||||
|
private int failureCount = 0;
|
||||||
|
private long lastFailureTime = 0;
|
||||||
|
private State state = State.CLOSED;
|
||||||
|
|
||||||
|
/** The state of the circuit breaker. */
|
||||||
|
public enum State {
|
||||||
|
CLOSED,
|
||||||
|
OPEN,
|
||||||
|
HALF_OPEN
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for SimpleCircuitBreaker.
|
||||||
|
*
|
||||||
|
* @param failureThreshold consecutive failure count threshold to trip the breaker
|
||||||
|
* @param retryTimePeriodMs time duration to wait in OPEN state before trying again (HALF_OPEN)
|
||||||
|
*/
|
||||||
|
public SimpleCircuitBreaker(int failureThreshold, long retryTimePeriodMs) {
|
||||||
|
this.failureThreshold = failureThreshold;
|
||||||
|
this.retryTimePeriodMs = retryTimePeriodMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current state of the circuit breaker after evaluating transitions.
|
||||||
|
*
|
||||||
|
* @return current state
|
||||||
|
*/
|
||||||
|
public synchronized State getState() {
|
||||||
|
evaluateState();
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void evaluateState() {
|
||||||
|
if (state == State.OPEN) {
|
||||||
|
if (System.currentTimeMillis() - lastFailureTime > retryTimePeriodMs) {
|
||||||
|
state = State.HALF_OPEN;
|
||||||
|
LOGGER.info("Circuit Breaker transitioned to HALF_OPEN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Records a successful operation, resetting the failure counter and closing the circuit. */
|
||||||
|
public synchronized void recordSuccess() {
|
||||||
|
failureCount = 0;
|
||||||
|
state = State.CLOSED;
|
||||||
|
LOGGER.info("Circuit Breaker transitioned to CLOSED (success recorded)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Records a failure, potentially tripping the circuit to OPEN if threshold is met. */
|
||||||
|
public synchronized void recordFailure() {
|
||||||
|
failureCount++;
|
||||||
|
lastFailureTime = System.currentTimeMillis();
|
||||||
|
if (state == State.CLOSED && failureCount >= failureThreshold) {
|
||||||
|
state = State.OPEN;
|
||||||
|
LOGGER.warn("Circuit Breaker transitioned to OPEN (failure threshold reached)");
|
||||||
|
} else if (state == State.HALF_OPEN) {
|
||||||
|
state = State.OPEN;
|
||||||
|
LOGGER.warn("Circuit Breaker transitioned to OPEN (failed during HALF_OPEN)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.iluwatar.fallback;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/** Test verifying that App main method runs without throwing exceptions. */
|
||||||
|
class AppTest {
|
||||||
|
@Test
|
||||||
|
void testMain() {
|
||||||
|
assertDoesNotThrow(() -> App.main(new String[] {}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||||
|
*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2022 Ilkka Seppälä
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.iluwatar.fallback;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/** Unit and integration tests for the Fallback design pattern. */
|
||||||
|
class FallbackPatternTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHealthyServiceCall() {
|
||||||
|
try (var executor = new FallbackExecutor()) {
|
||||||
|
var primary = new PrimaryService("Primary Response", 0, false);
|
||||||
|
var fallback = new FallbackService("Fallback Response");
|
||||||
|
var circuitBreaker = new SimpleCircuitBreaker(2, 1000);
|
||||||
|
|
||||||
|
String response = executor.execute(primary, fallback, circuitBreaker, 100);
|
||||||
|
assertEquals("Primary Response", response);
|
||||||
|
assertEquals(SimpleCircuitBreaker.State.CLOSED, circuitBreaker.getState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFailingServiceCall() {
|
||||||
|
try (var executor = new FallbackExecutor()) {
|
||||||
|
var primary = new PrimaryService("Primary Response", 0, true);
|
||||||
|
var fallback = new FallbackService("Fallback Response");
|
||||||
|
var circuitBreaker = new SimpleCircuitBreaker(2, 1000);
|
||||||
|
|
||||||
|
// First failure: should call fallback, state remains CLOSED since threshold is 2
|
||||||
|
String response1 = executor.execute(primary, fallback, circuitBreaker, 100);
|
||||||
|
assertEquals("Fallback Response", response1);
|
||||||
|
assertEquals(SimpleCircuitBreaker.State.CLOSED, circuitBreaker.getState());
|
||||||
|
|
||||||
|
// Second failure: should call fallback, state becomes OPEN
|
||||||
|
String response2 = executor.execute(primary, fallback, circuitBreaker, 100);
|
||||||
|
assertEquals("Fallback Response", response2);
|
||||||
|
assertEquals(SimpleCircuitBreaker.State.OPEN, circuitBreaker.getState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testTimeoutServiceCall() {
|
||||||
|
try (var executor = new FallbackExecutor()) {
|
||||||
|
// Primary takes 300ms, timeout limit is 50ms
|
||||||
|
var primary = new PrimaryService("Primary Response", 300, false);
|
||||||
|
var fallback = new FallbackService("Fallback Response");
|
||||||
|
var circuitBreaker = new SimpleCircuitBreaker(1, 1000);
|
||||||
|
|
||||||
|
String response = executor.execute(primary, fallback, circuitBreaker, 50);
|
||||||
|
assertEquals("Fallback Response", response);
|
||||||
|
assertEquals(SimpleCircuitBreaker.State.OPEN, circuitBreaker.getState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCircuitBreakerOpenFastFail() {
|
||||||
|
try (var executor = new FallbackExecutor()) {
|
||||||
|
// Configure primary service to throw exception if called
|
||||||
|
var primary = new PrimaryService("Primary Response", 0, true);
|
||||||
|
var fallback = new FallbackService("Fallback Response");
|
||||||
|
var circuitBreaker = new SimpleCircuitBreaker(1, 1000);
|
||||||
|
|
||||||
|
// Force open by registering a failure
|
||||||
|
circuitBreaker.recordFailure();
|
||||||
|
assertEquals(SimpleCircuitBreaker.State.OPEN, circuitBreaker.getState());
|
||||||
|
|
||||||
|
// Now call execute. It should short-circuit and not call the failing primary (fast-fail).
|
||||||
|
String response = executor.execute(primary, fallback, circuitBreaker, 100);
|
||||||
|
assertEquals("Fallback Response", response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCircuitBreakerRecovery() throws InterruptedException {
|
||||||
|
try (var executor = new FallbackExecutor()) {
|
||||||
|
var primary = new PrimaryService("Primary Response", 0, false);
|
||||||
|
var fallback = new FallbackService("Fallback Response");
|
||||||
|
// Failure threshold = 1, retry period = 100ms
|
||||||
|
var circuitBreaker = new SimpleCircuitBreaker(1, 100);
|
||||||
|
|
||||||
|
// Trip the breaker
|
||||||
|
circuitBreaker.recordFailure();
|
||||||
|
assertEquals(SimpleCircuitBreaker.State.OPEN, circuitBreaker.getState());
|
||||||
|
|
||||||
|
// Wait 150ms to exceed retry period
|
||||||
|
Thread.sleep(150);
|
||||||
|
|
||||||
|
// State should evaluate to HALF_OPEN
|
||||||
|
assertEquals(SimpleCircuitBreaker.State.HALF_OPEN, circuitBreaker.getState());
|
||||||
|
|
||||||
|
// Execute. Healthy primary service succeeds, state transitions to CLOSED
|
||||||
|
String response = executor.execute(primary, fallback, circuitBreaker, 100);
|
||||||
|
assertEquals("Primary Response", response);
|
||||||
|
assertEquals(SimpleCircuitBreaker.State.CLOSED, circuitBreaker.getState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -252,6 +252,7 @@
|
|||||||
<module>backpressure</module>
|
<module>backpressure</module>
|
||||||
<module>actor-model</module>
|
<module>actor-model</module>
|
||||||
<module>rate-limiting-pattern</module>
|
<module>rate-limiting-pattern</module>
|
||||||
|
<module>fallback</module>
|
||||||
<module>onion-architecture</module>
|
<module>onion-architecture</module>
|
||||||
</modules>
|
</modules>
|
||||||
<repositories>
|
<repositories>
|
||||||
|
|||||||
Reference in new issue
Block a user