diff --git a/fallback/etc/fallback.urm.png b/fallback/etc/fallback.urm.png new file mode 100644 index 000000000000..210a48724251 Binary files /dev/null and b/fallback/etc/fallback.urm.png differ diff --git a/fallback/etc/fallback.urm.puml b/fallback/etc/fallback.urm.puml new file mode 100644 index 000000000000..82e07903b953 --- /dev/null +++ b/fallback/etc/fallback.urm.puml @@ -0,0 +1,173 @@ +@startuml +package fallback { + class App { + - TIMEOUT : int {static} + - MAX_ATTEMPTS : int {static} + - RETRY_DELAY : int {static} + - DEFAULT_API_URL : String {static} + - circuitBreaker : CircuitBreaker + - executor : ExecutorService + - fallbackService : Service + - primaryService : Service + - state : ExecutionState + - LOGGER : Logger {static} + + App() + + App(primaryService : Service, fallbackService : Service, circuitBreaker : CircuitBreaker) + + executeWithFallback() : String + - getFallbackData() : String + - updateFallbackCache(result : String) + + shutdown() + + main(args : String[]) {static} + } + + interface Service { + + getData() : String + } + + interface CircuitBreaker { + + isOpen() : boolean + + allowRequest() : boolean + + recordFailure() + + recordSuccess() + + reset() + + getState() : CircuitState + } + + class DefaultCircuitBreaker { + - RESET_TIMEOUT : long {static} + - MIN_HALF_OPEN_DURATION : Duration {static} + - state : State + - failureThreshold : int + - lastFailureTime : long + - failureTimestamps : Queue<Long> + - windowSize : Duration + - halfOpenStartTime : Instant + + DefaultCircuitBreaker(failureThreshold : int) + + isOpen() : boolean + + allowRequest() : boolean + + recordFailure() + + recordSuccess() + + reset() + + getState() : CircuitState + - transitionToHalfOpen() + - enum State { CLOSED, OPEN, HALF_OPEN } + } + + class FallbackService { + - {static} MAX_RETRIES : int = 3 + - {static} RETRY_DELAY_MS : long = 1000 + - {static} TIMEOUT : int = 2 + - {static} MIN_SUCCESS_RATE : double = 0.6 + - {static} MAX_REQUESTS_PER_MINUTE : int = 60 + - {static} LOGGER : Logger + - primaryService : Service + - fallbackService : Service + - circuitBreaker : CircuitBreaker + - executor : ExecutorService + - healthChecker : ScheduledExecutorService + - monitor : ServiceMonitor + - rateLimiter : RateLimiter + - state : ServiceState + + FallbackService(primaryService : Service, fallbackService : Service, circuitBreaker : CircuitBreaker) + + getData() : String + - executeWithTimeout(task : Callable<String>) : String + - executeFallback() : String + - updateFallbackCache(result : String) + - startHealthChecker() + + close() + + getMonitor() : ServiceMonitor + + getState() : ServiceState + - enum ServiceState { STARTING, RUNNING, DEGRADED, CLOSED } + } + + class LocalCacheService { + - cache : Cache<String, String> + - refreshExecutor : ScheduledExecutorService + - {static} CACHE_EXPIRY_MS : long = 300000 + - {static} CACHE_REFRESH_INTERVAL : Duration = Duration.ofMinutes(5) + - {static} LOGGER : Logger + + LocalCacheService() + + getData() : String + + updateCache(key : String, value : String) + + close() : void + - initializeDefaultCache() + - scheduleMaintenanceTasks() + - cleanupExpiredEntries() + - enum FallbackLevel { PRIMARY, SECONDARY, TERTIARY } + - class Cache<K, V> { + - map : ConcurrentHashMap<K, CacheEntry<V>> + - expiryMs : long + + Cache(expiryMs : long) + + get(key : K) : V + + put(key : K, value : V) + + cleanup() + - record CacheEntry<V>(value : V, expiryTime : long) { + isExpired() : boolean } + } + + + class RemoteService { + - apiUrl : String + - httpClient : HttpClient + - {static} TIMEOUT_SECONDS : int = 2 + + RemoteService(apiUrl : String, httpClient : HttpClient) + + getData() : String + } + + class ServiceMonitor { + - successCount : AtomicInteger + - fallbackCount : AtomicInteger + - errorCount : AtomicInteger + - lastSuccessTime : AtomicReference<Instant> + - lastFailureTime : AtomicReference<Instant> + - lastResponseTime : AtomicReference<Duration> + - metrics : Queue<ServiceMetric> + - fallbackWeight : double + - metricWindow : Duration + + ServiceMonitor() + + ServiceMonitor(fallbackWeight : double, metricWindow : Duration) + + recordSuccess(responseTime : Duration) + + recordFallback() + + recordError() + + getSuccessCount() : int + + getFallbackCount() : int + + getErrorCount() : int + + getLastSuccessTime() : Instant + + getLastFailureTime() : Instant + + getLastResponseTime() : Duration + + getSuccessRate() : double + + reset() + - pruneOldMetrics() + - record ServiceMetric(timestamp : Instant, type : MetricType, responseTime : Duration) + - enum MetricType { SUCCESS, FALLBACK, ERROR } + } + + class RateLimiter { + - maxRequests : int + - window : Duration + - requestTimestamps : Queue<Long> + + RateLimiter(maxRequests : int, window : Duration) + + tryAcquire() : boolean + } + + class ServiceException { + + ServiceException(message : String) + + ServiceException(message : String, cause : Throwable) + } +} + ' Relationships + App --> CircuitBreaker + App --> Service : primaryService + App --> Service : fallbackService + DefaultCircuitBreaker ..|> CircuitBreaker + LocalCacheService ..|> Service + RemoteService ..|> Service + FallbackService ..|> Service + FallbackService ..|> AutoCloseable + FallbackService --> Service : primaryService + FallbackService --> Service : fallbackService + FallbackService --> CircuitBreaker + FallbackService --> ServiceMonitor + FallbackService --> RateLimiter + ServiceException --|> Exception +} +@enduml \ No newline at end of file diff --git a/fallback/pom.xml b/fallback/pom.xml new file mode 100644 index 000000000000..23bf3df7e010 --- /dev/null +++ b/fallback/pom.xml @@ -0,0 +1,72 @@ +<?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> + + <properties> + <maven.compiler.source>17</maven.compiler.source> + <maven.compiler.target>17</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + <dependencies> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>5.8.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <version>5.8.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <version>5.8.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>4.5.1</version> + <scope>test</scope> + </dependency> + + </dependencies> +</project> \ No newline at end of file diff --git a/fallback/src/main/java/com/iluwatar/fallback/App.java b/fallback/src/main/java/com/iluwatar/fallback/App.java new file mode 100644 index 000000000000..75a08cd54700 --- /dev/null +++ b/fallback/src/main/java/com/iluwatar/fallback/App.java @@ -0,0 +1,177 @@ +package com.iluwatar.fallback; + +import java.net.http.HttpClient; +import java.time.Duration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Demonstrates the Fallback pattern with Circuit Breaker implementation. + * + * <p>This application shows how to: + * - Handle failures gracefully using fallback mechanisms. + * - Implement circuit breaker pattern to prevent cascade failures. + * - Configure timeouts and retries for resilient service calls. + * - Monitor service health and performance. + * + * <p>The app uses a primary remote service with a local cache fallback. + */ +public class App { + private static final Logger LOGGER = LoggerFactory.getLogger(App.class); + + /** Service call timeout in seconds. */ + private static final int TIMEOUT = 2; + + /** Maximum number of retry attempts. */ + private static final int MAX_ATTEMPTS = 3; + + /** Delay between retry attempts in milliseconds. */ + private static final int RETRY_DELAY = 1000; + + /** Default API endpoint for remote service. */ + private static final String DEFAULT_API_URL = "https://jsonplaceholder.typicode.com/todos"; + + /** Service execution state tracking. */ + private enum ExecutionState { + READY, RUNNING, FAILED, SHUTDOWN + } + + private final CircuitBreaker circuitBreaker; + private final ExecutorService executor; + private final Service primaryService; + private final Service fallbackService; + private volatile ExecutionState state; + + /** + * Constructs an App with default configuration. + * Creates HTTP client with timeout, remote service, and local cache fallback. + */ + public App() { + HttpClient httpClient = createHttpClient(); + this.primaryService = new RemoteService(DEFAULT_API_URL, httpClient); + this.fallbackService = new LocalCacheService(); + this.circuitBreaker = new DefaultCircuitBreaker(MAX_ATTEMPTS); + this.executor = Executors.newSingleThreadExecutor(); + this.state = ExecutionState.READY; + } + + private HttpClient createHttpClient() { + try { + return HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(TIMEOUT)) + .build(); + } catch (Exception e) { + LOGGER.warn("Failed to create custom HTTP client, using default", e); + return HttpClient.newHttpClient(); + } + } + + /** + * Constructs an App with custom services and circuit breaker. + * + * @param primaryService Primary service implementation + * @param fallbackService Fallback service implementation + * @param circuitBreaker Circuit breaker implementation + * @throws IllegalArgumentException if any parameter is null + */ + public App(Service primaryService, Service fallbackService, CircuitBreaker circuitBreaker) { + if (primaryService == null || fallbackService == null || circuitBreaker == null) { + throw new IllegalArgumentException("All services must be non-null"); + } + this.circuitBreaker = circuitBreaker; + this.executor = Executors.newSingleThreadExecutor(); + this.primaryService = primaryService; + this.fallbackService = fallbackService; + this.state = ExecutionState.READY; + } + + /** + * Executes the service with fallback mechanism. + * + * @return Result from primary or fallback service + * @throws IllegalStateException if app is shutdown + */ + public String executeWithFallback() { + if (state == ExecutionState.SHUTDOWN) { + throw new IllegalStateException("Application is shutdown"); + } + + state = ExecutionState.RUNNING; + if (circuitBreaker.isOpen()) { + LOGGER.info("Circuit breaker is open, using cached data"); + return getFallbackData(); + } + + try { + Future<String> future = executor.submit(primaryService::getData); + String result = future.get(TIMEOUT, TimeUnit.SECONDS); + circuitBreaker.recordSuccess(); + updateFallbackCache(result); + return result; + } catch (Exception e) { + LOGGER.warn("Primary service failed: {}", e.getMessage()); + circuitBreaker.recordFailure(); + state = ExecutionState.FAILED; + return getFallbackData(); + } + } + + private String getFallbackData() { + try { + return fallbackService.getData(); + } catch (Exception e) { + LOGGER.error("Fallback service failed: {}", e.getMessage()); + return "System is currently unavailable"; + } + } + + private void updateFallbackCache(String result) { + if (fallbackService instanceof LocalCacheService) { + ((LocalCacheService) fallbackService).updateCache("default", result); + } + } + + /** + * Shuts down the executor service and cleans up resources. + */ + public void shutdown() { + state = ExecutionState.SHUTDOWN; + executor.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + LOGGER.error("Shutdown interrupted", e); + } + } + + /** + * Main method demonstrating the fallback pattern. + */ + public static void main(String[] args) { + App app = new App(); + try { + for (int i = 0; i < MAX_ATTEMPTS; i++) { + try { + String result = app.executeWithFallback(); + LOGGER.info("Attempt {}: Result = {}", i + 1, result); + } catch (Exception e) { + LOGGER.error("Attempt {} failed: {}", i + 1, e.getMessage()); + } + Thread.sleep(RETRY_DELAY); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.error("Main thread interrupted", e); + } finally { + app.shutdown(); + } + } +} diff --git a/fallback/src/main/java/com/iluwatar/fallback/CircuitBreaker.java b/fallback/src/main/java/com/iluwatar/fallback/CircuitBreaker.java new file mode 100644 index 000000000000..df114d8f51be --- /dev/null +++ b/fallback/src/main/java/com/iluwatar/fallback/CircuitBreaker.java @@ -0,0 +1,48 @@ +/* + * 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; + +/** + * Interface defining the contract for a circuit breaker implementation. + * Provides methods to check circuit state and record success/failure events. + */ +public interface CircuitBreaker { + boolean isOpen(); + boolean allowRequest(); + void recordSuccess(); + void recordFailure(); + CircuitState getState(); + void reset(); + + /** + * Represents the possible states of the circuit breaker. + * CLOSED - Circuit is closed and allowing requests + * HALF_OPEN - Circuit is testing if service has recovered + * OPEN - Circuit is open and blocking requests + */ + enum CircuitState { + CLOSED, HALF_OPEN, OPEN + } +} \ No newline at end of file diff --git a/fallback/src/main/java/com/iluwatar/fallback/DefaultCircuitBreaker.java b/fallback/src/main/java/com/iluwatar/fallback/DefaultCircuitBreaker.java new file mode 100644 index 000000000000..a8a857ffaca4 --- /dev/null +++ b/fallback/src/main/java/com/iluwatar/fallback/DefaultCircuitBreaker.java @@ -0,0 +1,182 @@ +/* + * 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.time.Duration; +import java.time.Instant; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Circuit breaker implementation with three states: + * - CLOSED: Normal operation, requests flow through + * - OPEN: Failing fast, no attempts to call primary service + * - HALF_OPEN: Testing if service has recovered. + * + * <p>Features: + * - Thread-safe operation + * - Sliding window failure counting + * - Automatic state transitions + * - Configurable thresholds and timeouts + * - Recovery validation period + */ +public class DefaultCircuitBreaker implements CircuitBreaker { + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultCircuitBreaker.class); + + // Circuit breaker configuration + private static final long RESET_TIMEOUT = 5000; // 5 seconds + private static final Duration MIN_HALF_OPEN_DURATION = Duration.ofSeconds(30); + + private volatile State state; + private final int failureThreshold; + private volatile long lastFailureTime; + private final Queue<Long> failureTimestamps; + private final Duration windowSize; + private volatile Instant halfOpenStartTime; + + /** + * Constructs a DefaultCircuitBreaker with the given failure threshold. + * + * @param failureThreshold the number of failures to trigger the circuit breaker + */ + public DefaultCircuitBreaker(final int failureThreshold) { + this.failureThreshold = failureThreshold; + this.state = State.CLOSED; + this.failureTimestamps = new ConcurrentLinkedQueue<>(); + this.windowSize = Duration.ofMinutes(1); + } + + /** + * Checks if a request should be allowed through the circuit breaker. + * @return true if request should be allowed, false if it should be blocked + */ + @Override + public synchronized boolean allowRequest() { + if (state == State.CLOSED) { + return true; + } + if (state == State.OPEN) { + if (System.currentTimeMillis() - lastFailureTime > RESET_TIMEOUT) { + transitionToHalfOpen(); + return true; + } + return false; + } + // In HALF_OPEN state, allow limited testing + return true; + } + + @Override + public synchronized boolean isOpen() { + if (state == State.OPEN) { + if (System.currentTimeMillis() - lastFailureTime > RESET_TIMEOUT) { + transitionToHalfOpen(); + return false; + } + return true; + } + return false; + } + + /** + * Transitions circuit breaker to half-open state. + * Clears failure history and starts recovery monitoring. + */ + private synchronized void transitionToHalfOpen() { + state = State.HALF_OPEN; + halfOpenStartTime = Instant.now(); + failureTimestamps.clear(); + LOGGER.info("Circuit breaker transitioning to HALF_OPEN state"); + } + + /** + * Records successful operation. + * In half-open state, requires sustained success before closing circuit. + */ + @Override + public synchronized void recordSuccess() { + if (state == State.HALF_OPEN) { + if (Duration.between(halfOpenStartTime, Instant.now()) + .compareTo(MIN_HALF_OPEN_DURATION) >= 0) { + LOGGER.info("Circuit breaker recovering - transitioning to CLOSED"); + state = State.CLOSED; + } + } + failureTimestamps.clear(); + } + + @Override + public synchronized void recordFailure() { + long now = System.currentTimeMillis(); + failureTimestamps.offer(now); + + // Cleanup old timestamps outside window + while (!failureTimestamps.isEmpty() + && failureTimestamps.peek() < now - windowSize.toMillis()) { + failureTimestamps.poll(); + } + + if (failureTimestamps.size() >= failureThreshold) { + LOGGER.warn("Failure threshold reached - opening circuit breaker"); + state = State.OPEN; + lastFailureTime = now; + } + } + + @Override + public void reset() { + failureTimestamps.clear(); + lastFailureTime = 0; + state = State.CLOSED; + } + + /** + * Returns the current state of the circuit breaker mapped to the public enum. + * @return Current CircuitState value + */ + @Override + public synchronized CircuitState getState() { + if (state == State.OPEN && System.currentTimeMillis() - lastFailureTime > RESET_TIMEOUT) { + transitionToHalfOpen(); + } + return switch (state) { + case CLOSED -> CircuitState.CLOSED; + case OPEN -> CircuitState.OPEN; + case HALF_OPEN -> CircuitState.HALF_OPEN; + }; + } + + /** + * Internal states of the circuit breaker. + * Maps to the public CircuitState enum for external reporting. + */ + private enum State { + CLOSED, + OPEN, + HALF_OPEN + } +} \ No newline at end of file diff --git a/fallback/src/main/java/com/iluwatar/fallback/FallbackService.java b/fallback/src/main/java/com/iluwatar/fallback/FallbackService.java new file mode 100644 index 000000000000..e4a1b847aee0 --- /dev/null +++ b/fallback/src/main/java/com/iluwatar/fallback/FallbackService.java @@ -0,0 +1,307 @@ +/* + * 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 + * 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.time.Duration; +import java.time.Instant; +import java.util.Queue; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * FallbackService implements a resilient service pattern with circuit breaking, + * rate limiting, and fallback capabilities. It manages service degradation gracefully + * by monitoring service health and automatically switching to fallback mechanisms + * when the primary service is unavailable or performing poorly. + * + * <p>Features: + * - Circuit breaking to prevent cascading failures. + * - Rate limiting to protect from overload. + * - Automatic fallback to backup service. + * - Health monitoring and metrics collection. + * - Retry mechanism with exponential backoff. + */ +public class FallbackService implements Service, AutoCloseable { + + /** Logger for this class. */ + private static final Logger LOGGER = LoggerFactory.getLogger(FallbackService.class); + + /** Timeout in seconds for primary service calls. */ + private static final int TIMEOUT = 2; + + /** Maximum number of retry attempts for failed requests. */ + private static final int MAX_RETRIES = 3; + + /** Base delay between retries in milliseconds. */ + private static final long RETRY_DELAY_MS = 1000; + + /** Minimum success rate threshold before triggering warnings. */ + private static final double MIN_SUCCESS_RATE = 0.6; + + /** Maximum requests allowed per minute for rate limiting. */ + private static final int MAX_REQUESTS_PER_MINUTE = 60; + + /** Service state tracking. */ + private enum ServiceState { + STARTING, RUNNING, DEGRADED, CLOSED + } + + private volatile ServiceState state = ServiceState.STARTING; + + private final CircuitBreaker circuitBreaker; + private final ExecutorService executor; + private final Service primaryService; + private final Service fallbackService; + private final ServiceMonitor monitor; + private final ScheduledExecutorService healthChecker; + private final RateLimiter rateLimiter; + + /** + * Constructs a new FallbackService with the specified components. + * + * @param primaryService Main service implementation + * @param fallbackService Backup service for failover + * @param circuitBreaker Circuit breaker for failure detection + * @throws IllegalArgumentException if any parameter is null + */ + public FallbackService(Service primaryService, Service fallbackService, CircuitBreaker circuitBreaker) { + // Validate parameters + if (primaryService == null || fallbackService == null || circuitBreaker == null) { + throw new IllegalArgumentException("All service components must be non-null"); + } + + // Initialize components + this.primaryService = primaryService; + this.fallbackService = fallbackService; + this.circuitBreaker = circuitBreaker; + this.executor = Executors.newCachedThreadPool(); + this.monitor = new ServiceMonitor(); + this.healthChecker = Executors.newSingleThreadScheduledExecutor(); + this.rateLimiter = new RateLimiter(MAX_REQUESTS_PER_MINUTE, Duration.ofMinutes(1)); + + startHealthChecker(); + state = ServiceState.RUNNING; + } + + /** + * Starts the health monitoring schedule. + * Monitors service health metrics and logs warnings when thresholds are exceeded. + */ + private void startHealthChecker() { + healthChecker.scheduleAtFixedRate(() -> { + try { + if (monitor.getSuccessRate() < MIN_SUCCESS_RATE) { + LOGGER.warn("Success rate below threshold: {}", monitor.getSuccessRate()); + } + if (Duration.between(monitor.getLastSuccessTime(), Instant.now()).toMinutes() > 5) { + LOGGER.warn("No successful requests in last 5 minutes"); + } + } catch (Exception e) { + LOGGER.error("Health check failed: {}", e.getMessage()); + } + }, 1, 1, TimeUnit.MINUTES); + } + + /** + * Retrieves data from the primary service with a fallback mechanism. + * + * @return the data from the primary or fallback service + * @throws Exception if an error occurs while retrieving the data + */ + @Override + public String getData() throws Exception { + // Validate service state + if (state == ServiceState.CLOSED) { + throw new ServiceException("Service is closed"); + } + + // Apply rate limiting + if (!rateLimiter.tryAcquire()) { + state = ServiceState.DEGRADED; + LOGGER.warn("Rate limit exceeded, switching to fallback"); + monitor.recordFallback(); + return executeFallback(); + } + + // Check circuit breaker + if (!circuitBreaker.allowRequest()) { + state = ServiceState.DEGRADED; + LOGGER.warn("Circuit breaker open, switching to fallback"); + monitor.recordFallback(); + return executeFallback(); + } + + Instant start = Instant.now(); + Exception lastException = null; + + for (int attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + if (attempt > 0) { + // Exponential backoff with jitter + long delay = RETRY_DELAY_MS * (long) Math.pow(2, attempt - 1); + delay += ThreadLocalRandom.current().nextLong(delay / 2); + Thread.sleep(delay); + } + + String result = executeWithTimeout(primaryService::getData); + Duration responseTime = Duration.between(start, Instant.now()); + + circuitBreaker.recordSuccess(); + monitor.recordSuccess(responseTime); + updateFallbackCache(result); + + return result; + } catch (Exception e) { + lastException = e; + LOGGER.warn("Attempt {} failed: {}", attempt + 1, e.getMessage()); + + // Don't retry certain exceptions + if (e instanceof ServiceException + || e instanceof IllegalArgumentException) { + break; + } + + circuitBreaker.recordFailure(); + monitor.recordError(); + } + } + + monitor.recordFallback(); + if (lastException != null) { + LOGGER.error("All attempts failed. Last error: {}", lastException.getMessage()); + } + return executeFallback(); + } + + /** + * Executes a service call with timeout protection. + * @param task The service call to execute + * @return The service response + * @throws Exception if the call fails or times out + */ + private String executeWithTimeout(Callable<String> task) throws Exception { + Future<String> future = executor.submit(task); + try { + return future.get(TIMEOUT, TimeUnit.SECONDS); + } catch (TimeoutException e) { + future.cancel(true); + throw new ServiceException("Service timeout after " + TIMEOUT + " seconds", e); + } + } + + private String executeFallback() { + try { + return fallbackService.getData(); + } catch (Exception e) { + LOGGER.error("Fallback service failed: {}", e.getMessage()); + return "Service temporarily unavailable"; + } + } + + private void updateFallbackCache(String result) { + try { + if (fallbackService instanceof LocalCacheService) { + ((LocalCacheService) fallbackService).updateCache("default", result); + } + } catch (Exception e) { + LOGGER.warn("Failed to update fallback cache: {}", e.getMessage()); + } + } + + /** + * Shuts down the executor service. + */ + @Override + public void close() throws Exception { + state = ServiceState.CLOSED; + executor.shutdown(); + healthChecker.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + if (!healthChecker.awaitTermination(5, TimeUnit.SECONDS)) { + healthChecker.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + healthChecker.shutdownNow(); + Thread.currentThread().interrupt(); + throw new Exception("Failed to shutdown executors", e); + } + } + + public ServiceMonitor getMonitor() { + return monitor; + } + + /** + * Returns the current service state. + * @return Current ServiceState enum value + */ + public ServiceState getState() { + return state; + } + + /** + * Rate limiter implementation using a sliding window approach. + * Manages request rate by tracking timestamps within a rolling time window. + */ + private static class RateLimiter { + private final int maxRequests; + private final Duration window; + private final Queue<Long> requestTimestamps = new ConcurrentLinkedQueue<>(); + + RateLimiter(int maxRequests, Duration window) { + this.maxRequests = maxRequests; + this.window = window; + } + + boolean tryAcquire() { + long now = System.currentTimeMillis(); + long windowStart = now - window.toMillis(); + + // Removing expired timestamps + while (!requestTimestamps.isEmpty() && requestTimestamps.peek() < windowStart) { + requestTimestamps.poll(); + } + + if (requestTimestamps.size() < maxRequests) { + requestTimestamps.offer(now); + return true; + } + return false; + } + } +} \ No newline at end of file diff --git a/fallback/src/main/java/com/iluwatar/fallback/LocalCacheService.java b/fallback/src/main/java/com/iluwatar/fallback/LocalCacheService.java new file mode 100644 index 000000000000..8a1af9b10269 --- /dev/null +++ b/fallback/src/main/java/com/iluwatar/fallback/LocalCacheService.java @@ -0,0 +1,198 @@ +/* + * 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 ch.qos.logback.classic.Logger; +import java.time.Duration; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.slf4j.LoggerFactory; + +/** + * LocalCacheService implementation that provides cached data with fallback mechanism. + * This service maintains a local cache with multiple fallback levels and automatic + * expiration of cached entries. If the primary data source fails, the service + * falls back to cached data in order of priority. + */ +public class LocalCacheService implements Service, AutoCloseable { + + /** Cache instance for storing key-value pairs. */ + private final Cache<String, String> cache; + + /** Default cache entry expiration time in milliseconds. */ + private static final long CACHE_EXPIRY_MS = 300000; + + /** Logger instance for this class. */ + private static final Logger LOGGER = (Logger) LoggerFactory.getLogger(LocalCacheService.class); + + /** Interval for periodic cache refresh operations. */ + private static final Duration CACHE_REFRESH_INTERVAL = Duration.ofMinutes(5); + + /** Executor service for scheduling cache maintenance tasks. */ + private final ScheduledExecutorService refreshExecutor; + + /** + * Defines the fallback chain priority levels. + * Entries are tried in order from PRIMARY to TERTIARY until valid data is found. + */ + private enum FallbackLevel { + PRIMARY("default"), + SECONDARY("backup1"), + TERTIARY("backup2"); + + private final String key; + + FallbackLevel(String key) { + this.key = key; + } + } + + /** + * Constructs a new LocalCacheService with initialized cache and scheduled maintenance. + */ + public LocalCacheService() { + this.cache = new Cache<>(CACHE_EXPIRY_MS); + this.refreshExecutor = Executors.newSingleThreadScheduledExecutor(); + initializeDefaultCache(); + scheduleMaintenanceTasks(); + } + + /** + * Initializes the cache with default fallback values. + */ + private void initializeDefaultCache() { + cache.put(FallbackLevel.PRIMARY.key, "Default fallback response"); + cache.put(FallbackLevel.SECONDARY.key, "Secondary fallback response"); + cache.put(FallbackLevel.TERTIARY.key, "Tertiary fallback response"); + } + + /** + * Schedules periodic cache maintenance tasks. + */ + private void scheduleMaintenanceTasks() { + refreshExecutor.scheduleAtFixedRate( + this::cleanupExpiredEntries, + CACHE_REFRESH_INTERVAL.toMinutes(), + CACHE_REFRESH_INTERVAL.toMinutes(), + TimeUnit.MINUTES + ); + } + + /** + * Removes expired entries from the cache. + */ + private void cleanupExpiredEntries() { + try { + cache.cleanup(); + LOGGER.debug("Completed cache cleanup"); + } catch (Exception e) { + LOGGER.error("Error during cache cleanup", e); + } + } + + @Override + public void close() throws Exception { + if (refreshExecutor != null) { + refreshExecutor.shutdown(); + try { + if (!refreshExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + refreshExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + refreshExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + throw new Exception("Failed to shutdown refresh executor", e); + } + } + } + + /** + * Retrieves data using the fallback chain mechanism. + * @return The cached data from the highest priority available fallback level. + * @throws Exception if no valid data is available at any fallback level. + */ + @Override + public String getData() throws Exception { + // Try each fallback level in order of priority + for (FallbackLevel level : FallbackLevel.values()) { + String value = cache.get(level.key); + if (value != null) { + LOGGER.debug("Retrieved value from {} fallback level", level); + return value; + } + LOGGER.debug("Cache miss at {} fallback level", level); + } + throw new Exception("All fallback levels exhausted"); + } + + /** + * Updates the cached data for a specific key. + * @param key The cache key to update. + * @param value The new value to cache. + */ + public void updateCache(String key, String value) { + cache.put(key, value); + } + + /** + * Thread-safe cache implementation with entry expiration. + */ + private static class Cache<K, V> { + private final ConcurrentHashMap<K, CacheEntry<V>> map; + private final long expiryMs; + + Cache(long expiryMs) { + this.map = new ConcurrentHashMap<>(); + this.expiryMs = expiryMs; + } + + V get(K key) { + CacheEntry<V> entry = map.get(key); + if (entry != null && !entry.isExpired()) { + return entry.value; + } + return null; + } + + void put(K key, V value) { + map.put(key, new CacheEntry<>(value, System.currentTimeMillis() + expiryMs)); + } + + /** + * Removes all expired entries from the cache. + */ + void cleanup() { + map.entrySet().removeIf(entry -> entry.getValue().isExpired()); + } + + private record CacheEntry<V>(V value, long expiryTime) { + boolean isExpired() { + return System.currentTimeMillis() > expiryTime; + } + } + } +} \ No newline at end of file diff --git a/fallback/src/main/java/com/iluwatar/fallback/RemoteService.java b/fallback/src/main/java/com/iluwatar/fallback/RemoteService.java new file mode 100644 index 000000000000..1902b6c629dd --- /dev/null +++ b/fallback/src/main/java/com/iluwatar/fallback/RemoteService.java @@ -0,0 +1,63 @@ +/* + * 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.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +/** + * RemoteService implementation that makes HTTP calls to an external API. + * This service acts as the primary service in the fallback pattern. + */ +public class RemoteService implements Service { + private final String apiUrl; + private final HttpClient httpClient; + private static final int TIMEOUT_SECONDS = 2; + + public RemoteService(String apiUrl, HttpClient httpClient) { + this.apiUrl = apiUrl; + this.httpClient = httpClient; + } + + @Override + public String getData() throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(apiUrl)) + .timeout(Duration.ofSeconds(TIMEOUT_SECONDS)) + .GET() + .build(); + + HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new Exception("Remote service failed with status code: " + response.statusCode()); + } + + return response.body(); + } +} diff --git a/fallback/src/main/java/com/iluwatar/fallback/Service.java b/fallback/src/main/java/com/iluwatar/fallback/Service.java new file mode 100644 index 000000000000..2c5cbba22c8b --- /dev/null +++ b/fallback/src/main/java/com/iluwatar/fallback/Service.java @@ -0,0 +1,38 @@ +/* + * 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; + +/** + * Service interface that defines a method to retrieve data. + */ +public interface Service { + /** + * Retrieves data. + * + * @return the data + * @throws Exception if an error occurs while retrieving the data + */ + String getData() throws Exception; +} \ No newline at end of file diff --git a/fallback/src/main/java/com/iluwatar/fallback/ServiceException.java b/fallback/src/main/java/com/iluwatar/fallback/ServiceException.java new file mode 100644 index 000000000000..dcc4638dae80 --- /dev/null +++ b/fallback/src/main/java/com/iluwatar/fallback/ServiceException.java @@ -0,0 +1,33 @@ +package com.iluwatar.fallback; + +import java.io.Serial; + +/** + * Custom exception class for service-related errors in the fallback pattern. + * This exception is thrown when a service operation fails and needs to be handled + * by the fallback mechanism. + */ +public class ServiceException extends Exception { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * Constructs a new ServiceException with the specified detail message. + * + * @param message the detail message describing the error + */ + public ServiceException(String message) { + super(message); + } + + /** + * Constructs a new ServiceException with the specified detail message and cause. + * + * @param message the detail message describing the error + * @param cause the cause of the exception + */ + public ServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/fallback/src/main/java/com/iluwatar/fallback/ServiceMonitor.java b/fallback/src/main/java/com/iluwatar/fallback/ServiceMonitor.java new file mode 100644 index 000000000000..82e67d91dd22 --- /dev/null +++ b/fallback/src/main/java/com/iluwatar/fallback/ServiceMonitor.java @@ -0,0 +1,180 @@ +package com.iluwatar.fallback; + +import java.time.Duration; +import java.time.Instant; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * ServiceMonitor class provides monitoring capabilities for tracking service performance metrics. + * It maintains thread-safe counters for success, fallback, and error rates, as well as timing information. + */ +public class ServiceMonitor { + private final AtomicInteger successCount = new AtomicInteger(0); + private final AtomicInteger fallbackCount = new AtomicInteger(0); + private final AtomicInteger errorCount = new AtomicInteger(0); + private final AtomicReference<Instant> lastSuccessTime = new AtomicReference<>(Instant.now()); + private final AtomicReference<Instant> lastFailureTime = new AtomicReference<>(Instant.now()); + private final AtomicReference<Duration> lastResponseTime = new AtomicReference<>(Duration.ZERO); + + // Sliding window for metrics + private final Queue<ServiceMetric> metrics = new ConcurrentLinkedQueue<>(); + + private record ServiceMetric(Instant timestamp, MetricType type, Duration responseTime) {} + private enum MetricType { SUCCESS, FALLBACK, ERROR } + + /** + * Weight applied to fallback operations when calculating success rate. + * Values between 0.0 and 1.0: + * - 1.0 means fallbacks count as full successes + * - 0.0 means fallbacks count as failures + * - Default 0.7 indicates fallbacks are "partial successes" since they provide + * degraded but still functional service to users + */ + private final double fallbackWeight; + + /** + * Duration for which metrics should be retained. + * Metrics older than this window will be removed during pruning. + */ + private final Duration metricWindow; + + /** + * Constructs a ServiceMonitor with default configuration. + * Uses default fallback weight of 0.7 and 5-minute metric window. + */ + public ServiceMonitor() { + this(0.7, Duration.ofMinutes(5)); + } + + /** + * Constructs a ServiceMonitor with the specified fallback weight and metric window. + * + * @param fallbackWeight weight applied to fallback operations (0.0 to 1.0) + * @param metricWindow duration for which metrics should be retained + * @throws IllegalArgumentException if parameters are invalid + */ + public ServiceMonitor(double fallbackWeight, Duration metricWindow) { + if (fallbackWeight < 0.0 || fallbackWeight > 1.0) { + throw new IllegalArgumentException("Fallback weight must be between 0.0 and 1.0"); + } + if (metricWindow.isNegative() || metricWindow.isZero()) { + throw new IllegalArgumentException("Metric window must be positive"); + } + this.fallbackWeight = fallbackWeight; + this.metricWindow = metricWindow; + } + + /** + * Records a successful service operation with its response time. + * + * @param responseTime the duration of the successful operation + */ + public void recordSuccess(Duration responseTime) { + successCount.incrementAndGet(); + lastSuccessTime.set(Instant.now()); + lastResponseTime.set(responseTime); + metrics.offer(new ServiceMetric(Instant.now(), MetricType.SUCCESS, responseTime)); + pruneOldMetrics(); + } + + /** + * Records a fallback operation in the monitoring metrics. + * This method increments the fallback counter and updates metrics. + * Fallbacks are considered as degraded successes. + */ + public void recordFallback() { + fallbackCount.incrementAndGet(); + Instant now = Instant.now(); + Duration responseTime = lastResponseTime.get(); + metrics.offer(new ServiceMetric(now, MetricType.FALLBACK, responseTime)); + pruneOldMetrics(); + } + + /** + * Records an error operation in the monitoring metrics. + * This method increments the error counter, updates the last failure time, + * and adds a new metric to the sliding window. + */ + public void recordError() { + errorCount.incrementAndGet(); + Instant now = Instant.now(); + lastFailureTime.set(now); + Duration responseTime = lastResponseTime.get(); + metrics.offer(new ServiceMetric(now, MetricType.ERROR, responseTime)); + pruneOldMetrics(); + } + + public int getSuccessCount() { + return successCount.get(); + } + + public int getFallbackCount() { + return fallbackCount.get(); + } + + public int getErrorCount() { + return errorCount.get(); + } + + public Instant getLastSuccessTime() { + return lastSuccessTime.get(); + } + + public Instant getLastFailureTime() { + return lastFailureTime.get(); + } + + public Duration getLastResponseTime() { + return lastResponseTime.get(); + } + + /** + * Calculates the success rate of service operations. + * The rate is calculated as successes / total attempts, + * where both fallbacks and errors count as failures. + * + * @return the success rate as a double between 0.0 and 1.0 + */ + public double getSuccessRate() { + int successes = successCount.get(); + int fallbacks = fallbackCount.get(); + int errors = errorCount.get(); + int totalAttempts = successes + fallbacks + errors; + + if (totalAttempts == 0) { + return 0.0; + } + + // Weight fallbacks as partial successes since they provide degraded but functional service + return (successes + (fallbacks * fallbackWeight)) / totalAttempts; + } + + /** + * Resets all monitoring metrics to their initial values. + * This includes success count, error count, fallback count, and timing statistics. + */ + public void reset() { + successCount.set(0); + fallbackCount.set(0); + errorCount.set(0); + lastSuccessTime.set(Instant.now()); + lastFailureTime.set(Instant.now()); + lastResponseTime.set(Duration.ZERO); + metrics.clear(); + } + + /** + * Removes metrics that are older than the configured time window. + * This method is called automatically after each new metric is added + * to maintain a sliding window of recent metrics and prevent unbounded + * memory growth. + */ + private void pruneOldMetrics() { + Instant cutoff = Instant.now().minus(metricWindow); + metrics.removeIf(metric -> metric.timestamp.isBefore(cutoff)); + } +} + diff --git a/fallback/src/test/java/com/iluwatar/fallback/DefaultCircuitBreakerTest.java b/fallback/src/test/java/com/iluwatar/fallback/DefaultCircuitBreakerTest.java new file mode 100644 index 000000000000..4097daeee4ab --- /dev/null +++ b/fallback/src/test/java/com/iluwatar/fallback/DefaultCircuitBreakerTest.java @@ -0,0 +1,290 @@ +package com.iluwatar.fallback; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Unit tests for {@link DefaultCircuitBreaker}. + * Tests state transitions and behaviors of the circuit breaker pattern implementation. + */ +class DefaultCircuitBreakerTest { + private DefaultCircuitBreaker circuitBreaker; + + private static final int FAILURE_THRESHOLD = 3; + private static final long RESET_TIMEOUT_MS = 5000; + private static final long WAIT_BUFFER = 100; + + @BeforeEach + void setUp() { + circuitBreaker = new DefaultCircuitBreaker(FAILURE_THRESHOLD); + } + + @Nested + @DisplayName("State Transition Tests") + class StateTransitionTests { + @Test + @DisplayName("Should start in CLOSED state") + void initialStateShouldBeClosed() { + assertEquals(CircuitBreaker.CircuitState.CLOSED, circuitBreaker.getState()); + assertTrue(circuitBreaker.allowRequest()); + assertFalse(circuitBreaker.isOpen()); + } + + @Test + @DisplayName("Should transition to OPEN after threshold failures") + void shouldTransitionToOpenAfterFailures() { + // When + for (int i = 0; i < FAILURE_THRESHOLD; i++) { + circuitBreaker.recordFailure(); + } + + // Then + assertEquals(CircuitBreaker.CircuitState.OPEN, circuitBreaker.getState()); + assertFalse(circuitBreaker.allowRequest()); + assertTrue(circuitBreaker.isOpen()); + } + + @Test + @DisplayName("Should transition to HALF-OPEN after timeout") + void shouldTransitionToHalfOpenAfterTimeout() throws InterruptedException { + // Given + for (int i = 0; i < FAILURE_THRESHOLD; i++) { + circuitBreaker.recordFailure(); + } + assertTrue(circuitBreaker.isOpen()); + + // When + Thread.sleep(RESET_TIMEOUT_MS + WAIT_BUFFER); + + // Then + assertEquals(CircuitBreaker.CircuitState.HALF_OPEN, circuitBreaker.getState()); + assertTrue(circuitBreaker.allowRequest()); + assertFalse(circuitBreaker.isOpen()); + } + } + + @Nested + @DisplayName("Behavior Tests") + class BehaviorTests { + @Test + @DisplayName("Success should reset failure count") + void successShouldResetFailureCount() { + // Given + circuitBreaker.recordFailure(); + circuitBreaker.recordFailure(); + + // When + circuitBreaker.recordSuccess(); + + // Then + assertEquals(CircuitBreaker.CircuitState.CLOSED, circuitBreaker.getState()); + assertTrue(circuitBreaker.allowRequest()); + + // Additional verification + circuitBreaker.recordFailure(); + assertFalse(circuitBreaker.isOpen()); + } + + @Test + @DisplayName("Reset should return to initial state") + void resetShouldReturnToInitialState() { + // Given + for (int i = 0; i < FAILURE_THRESHOLD; i++) { + circuitBreaker.recordFailure(); + } + assertTrue(circuitBreaker.isOpen()); + + // When + circuitBreaker.reset(); + + // Then + assertEquals(CircuitBreaker.CircuitState.CLOSED, circuitBreaker.getState()); + assertTrue(circuitBreaker.allowRequest()); + assertFalse(circuitBreaker.isOpen()); + } + + @Test + @DisplayName("Should respect failure threshold boundary") + void shouldRespectFailureThresholdBoundary() { + // When/Then + for (int i = 0; i < FAILURE_THRESHOLD - 1; i++) { + circuitBreaker.recordFailure(); + assertEquals(CircuitBreaker.CircuitState.CLOSED, circuitBreaker.getState()); + assertFalse(circuitBreaker.isOpen()); + } + + circuitBreaker.recordFailure(); + assertEquals(CircuitBreaker.CircuitState.OPEN, circuitBreaker.getState()); + assertTrue(circuitBreaker.isOpen()); + } + + @Test + @DisplayName("Should allow request in HALF-OPEN state") + void shouldAllowRequestInHalfOpenState() throws InterruptedException { + // Given + for (int i = 0; i < FAILURE_THRESHOLD; i++) { + circuitBreaker.recordFailure(); + } + + // When + Thread.sleep(RESET_TIMEOUT_MS + WAIT_BUFFER); + + // Then + assertTrue(circuitBreaker.allowRequest()); + assertEquals(CircuitBreaker.CircuitState.HALF_OPEN, circuitBreaker.getState()); + } + } + + @Nested + @DisplayName("Recovery Tests") + class RecoveryTests { + @Test + @DisplayName("Should close circuit after sustained success in half-open state") + void shouldCloseAfterSustainedSuccess() throws InterruptedException { + // Given + for (int i = 0; i < FAILURE_THRESHOLD; i++) { + circuitBreaker.recordFailure(); + } + Thread.sleep(RESET_TIMEOUT_MS + WAIT_BUFFER); + assertEquals(CircuitBreaker.CircuitState.HALF_OPEN, circuitBreaker.getState()); + + // When + Thread.sleep(30000); // Wait for MIN_HALF_OPEN_DURATION + circuitBreaker.recordSuccess(); + + // Then + assertEquals(CircuitBreaker.CircuitState.CLOSED, circuitBreaker.getState()); + assertTrue(circuitBreaker.allowRequest()); + } + + @Test + @DisplayName("Should remain half-open if success duration not met") + void shouldRemainHalfOpenBeforeSuccessDuration() throws InterruptedException { + // Given + for (int i = 0; i < FAILURE_THRESHOLD; i++) { + circuitBreaker.recordFailure(); + } + Thread.sleep(RESET_TIMEOUT_MS + WAIT_BUFFER); + + // When + circuitBreaker.recordSuccess(); // Success before MIN_HALF_OPEN_DURATION + + // Then + assertEquals(CircuitBreaker.CircuitState.HALF_OPEN, circuitBreaker.getState()); + } + } + + @Nested + @DisplayName("Edge Case Tests") + class EdgeCaseTests { + @Test + @DisplayName("Should handle rapid state transitions") + void shouldHandleRapidStateTransitions() throws InterruptedException { + // Multiple rapid transitions + for (int i = 0; i < 3; i++) { + // To OPEN + for (int j = 0; j < FAILURE_THRESHOLD; j++) { + circuitBreaker.recordFailure(); + } + assertEquals(CircuitBreaker.CircuitState.OPEN, circuitBreaker.getState()); + + // To HALF_OPEN + Thread.sleep(RESET_TIMEOUT_MS + WAIT_BUFFER); + assertTrue(circuitBreaker.allowRequest()); + assertEquals(CircuitBreaker.CircuitState.HALF_OPEN, circuitBreaker.getState()); + + // Back to CLOSED + circuitBreaker.reset(); + assertEquals(CircuitBreaker.CircuitState.CLOSED, circuitBreaker.getState()); + } + } + + @Test + @DisplayName("Should handle boundary conditions for failure threshold") + void shouldHandleFailureThresholdBoundaries() { + // Given/When + for (int i = 0; i < FAILURE_THRESHOLD - 1; i++) { + circuitBreaker.recordFailure(); + // Then - Should still be closed + assertEquals(CircuitBreaker.CircuitState.CLOSED, circuitBreaker.getState()); + } + + // One more failure - Should open + circuitBreaker.recordFailure(); + assertEquals(CircuitBreaker.CircuitState.OPEN, circuitBreaker.getState()); + + // Additional failures should keep it open + circuitBreaker.recordFailure(); + assertEquals(CircuitBreaker.CircuitState.OPEN, circuitBreaker.getState()); + } + } + + @Nested + @DisplayName("Concurrency Tests") + class ConcurrencyTests { + private static final int THREAD_COUNT = 10; + private static final int OPERATIONS_PER_THREAD = 1000; + + @Test + @DisplayName("Should handle concurrent state transitions safely") + void shouldHandleConcurrentTransitions() throws InterruptedException { + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch endLatch = new CountDownLatch(THREAD_COUNT); + AtomicInteger errors = new AtomicInteger(0); + + // Create threads that will perform operations concurrently + List<Thread> threads = new ArrayList<>(); + for (int i = 0; i < THREAD_COUNT; i++) { + Thread t = new Thread(() -> { + try { + startLatch.await(); // Wait for start signal + for (int j = 0; j < OPERATIONS_PER_THREAD; j++) { + try { + if (j % 2 == 0) { + circuitBreaker.recordFailure(); + } else { + circuitBreaker.recordSuccess(); + } + circuitBreaker.allowRequest(); + circuitBreaker.getState(); + } catch (Exception e) { + errors.incrementAndGet(); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + endLatch.countDown(); + } + }); + threads.add(t); + t.start(); + } + + // Start all threads simultaneously + startLatch.countDown(); + + // Wait for all threads to complete + assertTrue(endLatch.await(30, TimeUnit.SECONDS), + "Concurrent operations should complete within timeout"); + assertEquals(0, errors.get(), "Should handle concurrent operations without errors"); + + // Verify circuit breaker is in a valid state + CircuitBreaker.CircuitState finalState = circuitBreaker.getState(); + assertTrue(EnumSet.allOf(CircuitBreaker.CircuitState.class).contains(finalState), + "Circuit breaker should be in a valid state"); + } + } +} diff --git a/fallback/src/test/java/com/iluwatar/fallback/FallbackServiceTest.java b/fallback/src/test/java/com/iluwatar/fallback/FallbackServiceTest.java new file mode 100644 index 000000000000..74694f298574 --- /dev/null +++ b/fallback/src/test/java/com/iluwatar/fallback/FallbackServiceTest.java @@ -0,0 +1,217 @@ +package com.iluwatar.fallback; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for {@link FallbackService}. + * Tests the fallback mechanism, circuit breaker integration, and service stability. + */ +class FallbackServiceTest { + @Mock private Service primaryService; + @Mock private Service fallbackService; + @Mock private CircuitBreaker circuitBreaker; + private FallbackService service; + + private static final int CONCURRENT_THREADS = 5; + private static final int REQUEST_COUNT = 100; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + service = new FallbackService(primaryService, fallbackService, circuitBreaker); + } + + @Nested + @DisplayName("Primary Service Tests") + class PrimaryServiceTests { + @Test + @DisplayName("Should use primary service when healthy") + void shouldUsePrimaryServiceWhenHealthy() throws Exception { + // Given + when(circuitBreaker.isOpen()).thenReturn(false); + when(circuitBreaker.allowRequest()).thenReturn(true); // Add this line + when(primaryService.getData()).thenReturn("success"); + doNothing().when(circuitBreaker).recordSuccess(); // Add this line + + // When + String result = service.getData(); + + // Then + assertEquals("success", result); + verify(primaryService).getData(); + verify(fallbackService, never()).getData(); + verify(circuitBreaker).recordSuccess(); + } + + @Test + @DisplayName("Should retry primary service on failure") + void shouldRetryPrimaryServiceOnFailure() throws Exception { + // Given + when(circuitBreaker.isOpen()).thenReturn(false); + when(circuitBreaker.allowRequest()).thenReturn(true); + when(primaryService.getData()) + .thenThrow(new TimeoutException()) + .thenThrow(new TimeoutException()) + .thenReturn("success"); + + // Make sure circuit breaker allows the retry attempts + doNothing().when(circuitBreaker).recordFailure(); + doNothing().when(circuitBreaker).recordSuccess(); + + // When + String result = service.getData(); + + // Then + assertEquals("success", result, "Should return success after retries"); + verify(primaryService, times(3)).getData(); + verify(circuitBreaker, times(2)).recordFailure(); + verify(circuitBreaker).recordSuccess(); + verify(fallbackService, never()).getData(); + } + } + + @Nested + @DisplayName("Fallback Service Tests") + class FallbackServiceTests { + @Test + @DisplayName("Should use fallback when circuit breaker is open") + void shouldUseFallbackWhenCircuitBreakerOpen() throws Exception { + // Given + when(circuitBreaker.isOpen()).thenReturn(true); + when(fallbackService.getData()).thenReturn("fallback"); + + // When + String result = service.getData(); + + // Then + assertEquals("fallback", result); + verify(primaryService, never()).getData(); + verify(fallbackService).getData(); + } + + @Test + @DisplayName("Should handle fallback service failure") + void shouldHandleFallbackServiceFailure() throws Exception { + // Given + when(circuitBreaker.isOpen()).thenReturn(false); + when(circuitBreaker.allowRequest()).thenReturn(true); // Add this line + when(primaryService.getData()) + .thenThrow(new TimeoutException()); + when(fallbackService.getData()) + .thenThrow(new Exception("Fallback failed")); + + // When + String result = service.getData(); + + // Then + assertEquals("Service temporarily unavailable", result); + verify(primaryService, atLeast(1)).getData(); // Changed verification + verify(fallbackService).getData(); + verify(circuitBreaker, atLeast(1)).recordFailure(); // Add verification + } + } + + @Nested + @DisplayName("Service Stability Tests") + class ServiceStabilityTests { + @Test + @DisplayName("Should handle concurrent requests") + void shouldHandleConcurrentRequests() throws Exception { + // Given + when(circuitBreaker.isOpen()).thenReturn(false); + when(primaryService.getData()).thenReturn("success"); + + // When + ExecutorService executor = Executors.newFixedThreadPool(CONCURRENT_THREADS); + CountDownLatch latch = new CountDownLatch(REQUEST_COUNT); + AtomicInteger successCount = new AtomicInteger(); + + // Submit concurrent requests + for (int i = 0; i < REQUEST_COUNT; i++) { + executor.execute(() -> { + try { + service.getData(); + successCount.incrementAndGet(); + } catch (Exception e) { + // Count as failure + } finally { + latch.countDown(); + } + }); + } + + // Then + assertTrue(latch.await(30, TimeUnit.SECONDS)); + assertTrue(successCount.get() >= 85, "Success rate should be at least 85%"); + + executor.shutdown(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + + @Test + @DisplayName("Should maintain monitoring metrics") + void shouldMaintainMonitoringMetrics() throws Exception { + // Given + when(circuitBreaker.isOpen()).thenReturn(false); + when(circuitBreaker.allowRequest()).thenReturn(true); + + // Set up primary service to succeed, then fail, then succeed + when(primaryService.getData()) + .thenReturn("success") + .thenThrow(new TimeoutException("Simulated timeout")) + .thenReturn("success"); + + // Set up fallback service + when(fallbackService.getData()).thenReturn("fallback"); + + // Configure circuit breaker behavior + doNothing().when(circuitBreaker).recordSuccess(); + doNothing().when(circuitBreaker).recordFailure(); + + // When - First call: success from primary + String result1 = service.getData(); + assertEquals("success", result1); + + // Second call: primary fails, use fallback + when(circuitBreaker.allowRequest()).thenReturn(false); // Force fallback + String result2 = service.getData(); + assertEquals("fallback", result2); + + // Third call: back to primary + when(circuitBreaker.allowRequest()).thenReturn(true); + String result3 = service.getData(); + assertEquals("success", result3); + + // Then + ServiceMonitor monitor = service.getMonitor(); + assertEquals(2, monitor.getSuccessCount()); + assertEquals(1, monitor.getErrorCount()); + assertTrue(monitor.getSuccessRate() > 0.5); + + // Verify interactions + verify(primaryService, times(3)).getData(); + verify(fallbackService, times(1)).getData(); + } + } + + @Test + @DisplayName("Should close resources properly") + void shouldCloseResourcesProperly() throws Exception { + // When + service.close(); + + // Then + assertThrows(ServiceException.class, () -> service.getData()); + } +} diff --git a/fallback/src/test/java/com/iluwatar/fallback/IntegrationTest.java b/fallback/src/test/java/com/iluwatar/fallback/IntegrationTest.java new file mode 100644 index 000000000000..f14da687bc63 --- /dev/null +++ b/fallback/src/test/java/com/iluwatar/fallback/IntegrationTest.java @@ -0,0 +1,173 @@ +package com.iluwatar.fallback; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; + +import java.net.http.HttpClient; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests for the Fallback pattern implementation. + * Tests the interaction between components and system behavior under various conditions. + */ +class IntegrationTest { + private FallbackService fallbackService; + private LocalCacheService cacheService; + private CircuitBreaker circuitBreaker; + + // Test configuration constants + private static final int CONCURRENT_REQUESTS = 50; + private static final int THREAD_POOL_SIZE = 10; + private static final Duration TEST_TIMEOUT = Duration.ofSeconds(30); + private static final String INVALID_SERVICE_URL = "http://localhost:0"; // Force failures + + @BeforeEach + void setUp() { + Service remoteService = new RemoteService(INVALID_SERVICE_URL, HttpClient.newHttpClient()); + cacheService = new LocalCacheService(); + circuitBreaker = new DefaultCircuitBreaker(3); + fallbackService = new FallbackService(remoteService, cacheService, circuitBreaker); + } + + @Nested + @DisplayName("Basic Fallback Flow Tests") + class BasicFallbackTests { + @Test + @DisplayName("Should follow complete fallback flow when primary service fails") + void testCompleteFailoverFlow() throws Exception { + // First call should try remote service, fail, and fall back to cache + String result1 = fallbackService.getData(); + assertNotNull(result1); + assertTrue(result1.contains("fallback"), "Should return fallback response"); + + // Update cache with new data + String updatedData = "updated cache data"; + cacheService.updateCache("default", updatedData); + + // Second call should use updated cache data + String result2 = fallbackService.getData(); + assertEquals(updatedData, result2, "Should return updated cache data"); + + // Verify metrics + ServiceMonitor monitor = fallbackService.getMonitor(); + assertEquals(0, monitor.getSuccessCount(), "Should have no successful primary calls"); + assertTrue(monitor.getFallbackCount() > 0, "Should have recorded fallbacks"); + assertTrue(monitor.getErrorCount() > 0, "Should have recorded errors"); + } + } + + @Nested + @DisplayName("Circuit Breaker Integration Tests") + class CircuitBreakerTests { + @Test + @DisplayName("Should properly transition circuit breaker states") + void testCircuitBreakerIntegration() throws Exception { + // Make calls to trigger circuit breaker + for (int i = 0; i < 5; i++) { + fallbackService.getData(); + } + + assertTrue(circuitBreaker.isOpen(), "Circuit breaker should be open after failures"); + + // Verify fallback behavior when circuit is open + String result = fallbackService.getData(); + assertNotNull(result); + assertTrue(result.contains("fallback"), "Should use fallback when circuit is open"); + + // Wait for circuit reset timeout + Thread.sleep(5100); + assertFalse(circuitBreaker.isOpen(), "Circuit breaker should reset after timeout"); + } + } + + @Nested + @DisplayName("Performance and Reliability Tests") + class PerformanceTests { + @Test + @DisplayName("Should maintain performance metrics") + void testPerformanceMetrics() throws Exception { + Instant startTime = Instant.now(); + + // Make sequential service calls + for (int i = 0; i < 3; i++) { + fallbackService.getData(); + } + + Duration totalDuration = Duration.between(startTime, Instant.now()); + ServiceMonitor monitor = fallbackService.getMonitor(); + + assertTrue(monitor.getLastResponseTime().compareTo(totalDuration) <= 0, + "Response time should be tracked accurately"); + assertTrue(monitor.getLastFailureTime().isAfter(monitor.getLastSuccessTime()), + "Timing of failures should be tracked"); + } + + @Test + @DisplayName("Should handle concurrent requests reliably") + void testSystemReliability() throws Exception { + AtomicInteger successCount = new AtomicInteger(); + AtomicInteger fallbackCount = new AtomicInteger(); + CountDownLatch latch = new CountDownLatch(CONCURRENT_REQUESTS); + + ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE); + try { + // Submit concurrent requests + for (int i = 0; i < CONCURRENT_REQUESTS; i++) { + executor.execute(() -> { + try { + String result = fallbackService.getData(); + if (result.contains("fallback")) { + fallbackCount.incrementAndGet(); + } else { + successCount.incrementAndGet(); + } + } catch (Exception e) { + fail("Request should not fail: " + e.getMessage()); + } finally { + latch.countDown(); + } + }); + } + + assertTrue(latch.await(TEST_TIMEOUT.toSeconds(), TimeUnit.SECONDS), + "Concurrent requests should complete within timeout"); + + // Verify system reliability + ServiceMonitor monitor = fallbackService.getMonitor(); + double reliabilityRate = (monitor.getSuccessRate() + + (double)fallbackCount.get() / CONCURRENT_REQUESTS); + assertTrue(reliabilityRate > 0.95, + String.format("Reliability rate should be > 95%%, actual: %.2f%%", + reliabilityRate * 100)); + } finally { + executor.shutdown(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS), + "Executor should shutdown cleanly"); + } + } + } + + @Nested + @DisplayName("Resource Management Tests") + class ResourceTests { + @Test + @DisplayName("Should properly clean up resources") + void testResourceCleanup() throws Exception { + // Verify service works before closing + String result = fallbackService.getData(); + assertNotNull(result, "Service should work before closing"); + + // Close service and verify it's unusable + fallbackService.close(); + assertThrows(ServiceException.class, () -> fallbackService.getData(), + "Service should throw exception after closing"); + } + } +} diff --git a/fallback/src/test/java/com/iluwatar/fallback/ServiceMonitorTest.java b/fallback/src/test/java/com/iluwatar/fallback/ServiceMonitorTest.java new file mode 100644 index 000000000000..17a8a9514c4b --- /dev/null +++ b/fallback/src/test/java/com/iluwatar/fallback/ServiceMonitorTest.java @@ -0,0 +1,163 @@ +package com.iluwatar.fallback; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import static org.junit.jupiter.api.Assertions.*; + +class ServiceMonitorTest { + private ServiceMonitor monitor; + + @BeforeEach + void setUp() { + monitor = new ServiceMonitor(); + } + + @Test + void testInitialState() { + assertEquals(0, monitor.getSuccessCount()); + assertEquals(0, monitor.getFallbackCount()); + assertEquals(0, monitor.getErrorCount()); + assertEquals(0.0, monitor.getSuccessRate()); + assertNotNull(monitor.getLastSuccessTime()); + assertNotNull(monitor.getLastFailureTime()); + assertEquals(Duration.ZERO, monitor.getLastResponseTime()); + } + + @Test + void testResponseTimeTracking() { + Duration responseTime = Duration.ofMillis(150); + monitor.recordSuccess(responseTime); + assertEquals(responseTime, monitor.getLastResponseTime()); + } + + @Test + void testLastSuccessTime() throws InterruptedException { + Instant beforeTest = Instant.now(); + Thread.sleep(1); // Add small delay + monitor.recordSuccess(Duration.ofMillis(100)); + assertTrue(monitor.getLastSuccessTime().isAfter(beforeTest)); + } + + @Test + void testLastFailureTime() throws InterruptedException { + Instant beforeTest = Instant.now(); + Thread.sleep(1); // Add small delay + monitor.recordError(); + assertTrue(monitor.getLastFailureTime().isAfter(beforeTest)); + } + + @Test + void testSuccessRateWithOnlyErrors() { + monitor.recordError(); + monitor.recordError(); + monitor.recordError(); + assertEquals(0.0, monitor.getSuccessRate()); + } + + @Test + void testSuccessRateWithOnlySuccesses() { + monitor.recordSuccess(Duration.ofMillis(100)); + monitor.recordSuccess(Duration.ofMillis(100)); + monitor.recordSuccess(Duration.ofMillis(100)); + assertEquals(1.0, monitor.getSuccessRate()); + } + + @Test + void testReset() { + monitor.recordSuccess(Duration.ofMillis(100)); + monitor.recordFallback(); + monitor.recordError(); + monitor.reset(); + + assertEquals(0, monitor.getSuccessCount()); + assertEquals(0, monitor.getFallbackCount()); + assertEquals(0, monitor.getErrorCount()); + assertEquals(0.0, monitor.getSuccessRate()); + assertEquals(Duration.ZERO, monitor.getLastResponseTime()); + assertTrue(monitor.getLastSuccessTime().isAfter(Instant.now().minusSeconds(1))); + assertTrue(monitor.getLastFailureTime().isAfter(Instant.now().minusSeconds(1))); + } + + @Test + void testConcurrentOperations() throws Exception { + int threadCount = 10; + CountDownLatch latch = new CountDownLatch(threadCount); + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + try { + monitor.recordSuccess(Duration.ofMillis(100)); + } finally { + latch.countDown(); + } + }); + } + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertEquals(threadCount, monitor.getSuccessCount()); + executor.shutdown(); + executor.awaitTermination(1, TimeUnit.SECONDS); + } + + @Test + void testMixedOperationsSuccessRate() { + // 60% success rate scenario + monitor.recordSuccess(Duration.ofMillis(100)); + monitor.recordSuccess(Duration.ofMillis(100)); + monitor.recordSuccess(Duration.ofMillis(100)); + monitor.recordError(); + monitor.recordError(); + + assertEquals(0.6, monitor.getSuccessRate(), 0.01); + assertEquals(3, monitor.getSuccessCount()); + assertEquals(2, monitor.getErrorCount()); + } + + @Test + void testBasicMetrics() { + monitor.recordSuccess(Duration.ofMillis(100)); + monitor.recordError(); + monitor.recordFallback(); + + assertEquals(1, monitor.getSuccessCount()); + assertEquals(1, monitor.getErrorCount()); + assertEquals(1, monitor.getFallbackCount()); + assertEquals(Duration.ofMillis(100), monitor.getLastResponseTime()); + } + + @Test + void testConsecutiveFailures() { + int consecutiveFailures = 5; + for (int i = 0; i < consecutiveFailures; i++) { + monitor.recordError(); + } + + assertEquals(consecutiveFailures, monitor.getErrorCount()); + monitor.recordSuccess(Duration.ofMillis(100)); + assertEquals(consecutiveFailures, monitor.getErrorCount()); + assertEquals(1, monitor.getSuccessCount()); + } + + @Test + void testResetBehavior() { + monitor.recordSuccess(Duration.ofMillis(100)); + monitor.recordError(); + monitor.recordFallback(); + + monitor.reset(); + + assertEquals(0, monitor.getSuccessCount()); + assertEquals(0, monitor.getErrorCount()); + assertEquals(0, monitor.getFallbackCount()); + assertEquals(Duration.ZERO, monitor.getLastResponseTime()); + } +} diff --git a/pom.xml b/pom.xml index 959643f79fcf..57b4f5c6ec05 100644 --- a/pom.xml +++ b/pom.xml @@ -1,436 +1,437 @@ -<?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> - <groupId>com.iluwatar</groupId> - <artifactId>java-design-patterns</artifactId> - <version>1.26.0-SNAPSHOT</version> - <packaging>pom</packaging> - <inceptionYear>2014-2022</inceptionYear> - <name>Java Design Patterns</name> - <description>Java Design Patterns</description> - <properties> - <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> - <sonar-maven-plugin.version>4.0.0.4121</sonar-maven-plugin.version> - <spring-boot.version>2.7.5</spring-boot.version> - <jacoco.version>0.8.12</jacoco.version> - <commons-dbcp.version>1.4</commons-dbcp.version> - <htmlunit.version>4.5.0</htmlunit.version> - <gson.version>2.11.0</gson.version> - <guice.version>6.0.0</guice.version> - <system-lambda.version>1.1.0</system-lambda.version> - <maven-surefire-plugin.version>3.5.1</maven-surefire-plugin.version> - <maven-checkstyle-plugin.version>3.6.0</maven-checkstyle-plugin.version> - <license-maven-plugin.version>4.6</license-maven-plugin.version> - <urm-maven-plugin.version>2.1.1</urm-maven-plugin.version> - <slf4j.version>2.0.16</slf4j.version> - <logback.version>1.5.6</logback.version> - <!-- SonarCloud --> - <sonar.host.url>https://sonarcloud.io</sonar.host.url> - <sonar.organization>iluwatar</sonar.organization> - <sonar.projectKey>iluwatar_java-design-patterns</sonar.projectKey> - <sonar.moduleKey>${project.artifactId}</sonar.moduleKey> - <sonar.projectName>Java Design Patterns</sonar.projectName> - </properties> - <modules> - <module>abstract-factory</module> - <module>collecting-parameter</module> - <module>monitor</module> - <module>builder</module> - <module>factory-method</module> - <module>prototype</module> - <module>singleton</module> - <module>adapter</module> - <module>bridge</module> - <module>composite</module> - <module>data-access-object</module> - <module>data-mapper</module> - <module>decorator</module> - <module>facade</module> - <module>flyweight</module> - <module>proxy</module> - <module>chain-of-responsibility</module> - <module>command</module> - <module>interpreter</module> - <module>iterator</module> - <module>mediator</module> - <module>memento</module> - <module>model-view-presenter</module> - <module>observer</module> - <module>state</module> - <module>strategy</module> - <module>template-method</module> - <module>version-number</module> - <module>visitor</module> - <module>double-checked-locking</module> - <module>servant</module> - <module>service-locator</module> - <module>null-object</module> - <module>event-aggregator</module> - <module>callback</module> - <module>execute-around</module> - <module>property</module> - <module>intercepting-filter</module> - <module>producer-consumer</module> - <module>pipeline</module> - <module>poison-pill</module> - <module>lazy-loading</module> - <module>service-layer</module> - <module>specification</module> - <module>tolerant-reader</module> - <module>model-view-controller</module> - <module>flux</module> - <module>double-dispatch</module> - <module>multiton</module> - <module>resource-acquisition-is-initialization</module> - <module>twin</module> - <module>private-class-data</module> - <module>object-pool</module> - <module>dependency-injection</module> - <module>front-controller</module> - <module>repository</module> - <module>async-method-invocation</module> - <module>monostate</module> - <module>step-builder</module> - <module>business-delegate</module> - <module>half-sync-half-async</module> - <module>layered-architecture</module> - <module>fluent-interface</module> - <module>reactor</module> - <module>caching</module> - <module>delegation</module> - <module>event-driven-architecture</module> - <module>microservices-api-gateway</module> - <module>factory-kit</module> - <module>feature-toggle</module> - <module>value-object</module> - <module>monad</module> - <module>mute-idiom</module> - <module>hexagonal-architecture</module> - <module>abstract-document</module> - <module>microservices-aggregrator</module> - <module>promise</module> - <module>page-controller</module> - <module>page-object</module> - <module>event-based-asynchronous</module> - <module>event-queue</module> - <module>queue-based-load-leveling</module> - <module>object-mother</module> - <module>data-bus</module> - <module>converter</module> - <module>guarded-suspension</module> - <module>balking</module> - <module>extension-objects</module> - <module>marker-interface</module> - <module>command-query-responsibility-segregation</module> - <module>event-sourcing</module> - <module>data-transfer-object</module> - <module>throttling</module> - <module>unit-of-work</module> - <module>partial-response</module> - <module>retry</module> - <module>dirty-flag</module> - <module>trampoline</module> - <module>ambassador</module> - <module>acyclic-visitor</module> - <module>collection-pipeline</module> - <module>master-worker</module> - <module>spatial-partition</module> - <module>commander</module> - <module>type-object</module> - <module>bytecode</module> - <module>leader-election</module> - <module>data-locality</module> - <module>subclass-sandbox</module> - <module>circuit-breaker</module> - <module>role-object</module> - <module>saga</module> - <module>double-buffer</module> - <module>sharding</module> - <module>game-loop</module> - <module>combinator</module> - <module>update-method</module> - <module>leader-followers</module> - <module>strangler</module> - <module>arrange-act-assert</module> - <module>transaction-script</module> - <module>registry</module> - <module>filterer</module> - <module>factory</module> - <module>separated-interface</module> - <module>special-case</module> - <module>parameter-object</module> - <module>active-object</module> - <module>model-view-viewmodel</module> - <module>composite-entity</module> - <module>table-module</module> - <module>presentation-model</module> - <module>lockable-object</module> - <module>fanout-fanin</module> - <module>domain-model</module> - <module>composite-view</module> - <module>metadata-mapping</module> - <module>service-to-worker</module> - <module>client-session</module> - <module>model-view-intent</module> - <module>currying</module> - <module>serialized-entity</module> - <module>identity-map</module> - <module>component</module> - <module>context-object</module> - <module>optimistic-offline-lock</module> - <module>curiously-recurring-template-pattern</module> - <module>microservices-log-aggregation</module> - <module>anti-corruption-layer</module> - <module>health-check</module> - <module>notification</module> - <module>single-table-inheritance</module> - <module>dynamic-proxy</module> - <module>gateway</module> - <module>serialized-lob</module> - <module>server-session</module> - <module>virtual-proxy</module> - <module>function-composition</module> - <module>microservices-distributed-tracing</module> - <module>microservices-idempotent-consumer</module> - </modules> - <repositories> - <repository> - <id>jitpack.io</id> - <url>https://jitpack.io</url> - </repository> - </repositories> - <dependencyManagement> - <dependencies> - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-dependencies</artifactId> - <version>${spring-boot.version}</version> - <type>pom</type> - <scope>import</scope> - </dependency> - <dependency> - <groupId>commons-dbcp</groupId> - <artifactId>commons-dbcp</artifactId> - <version>${commons-dbcp.version}</version> - </dependency> - <dependency> - <groupId>org.htmlunit</groupId> - <artifactId>htmlunit</artifactId> - <version>${htmlunit.version}</version> - </dependency> - <dependency> - <groupId>com.google.code.gson</groupId> - <artifactId>gson</artifactId> - <version>${gson.version}</version> - </dependency> - <dependency> - <groupId>com.google.inject</groupId> - <artifactId>guice</artifactId> - <version>${guice.version}</version> - </dependency> - <dependency> - <groupId>com.github.stefanbirkner</groupId> - <artifactId>system-lambda</artifactId> - <version>${system-lambda.version}</version> - <scope>test</scope> - </dependency> - </dependencies> - </dependencyManagement> - <dependencies> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - <version>${slf4j.version}</version> - </dependency> - <dependency> - <groupId>ch.qos.logback</groupId> - <artifactId>logback-classic</artifactId> - <version>${logback.version}</version> - </dependency> - <dependency> - <groupId>ch.qos.logback</groupId> - <artifactId>logback-core</artifactId> - <version>${logback.version}</version> - </dependency> - <dependency> - <groupId>org.projectlombok</groupId> - <artifactId>lombok</artifactId> - <scope>provided</scope> - </dependency> - </dependencies> - <build> - <pluginManagement> - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-compiler-plugin</artifactId> - <configuration> - <source>17</source> - <target>17</target> - </configuration> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-surefire-plugin</artifactId> - <version>${maven-surefire-plugin.version}</version> - </plugin> - <plugin> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-maven-plugin</artifactId> - </plugin> - <!-- Maven assembly plugin template for all child project to follow --> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-assembly-plugin</artifactId> - <executions> - <execution> - <phase>package</phase> - <goals> - <goal>single</goal> - </goals> - <configuration> - <descriptorRefs> - <descriptorRef>jar-with-dependencies</descriptorRef> - </descriptorRefs> - <!-- below two line make sure the fat jar is sharing the same name as of project name --> - <finalName>${project.artifactId}-${project.version}</finalName> - <appendAssemblyId>false</appendAssemblyId> - </configuration> - </execution> - </executions> - </plugin> - <plugin> - <groupId>org.sonarsource.scanner.maven</groupId> - <artifactId>sonar-maven-plugin</artifactId> - <version>${sonar-maven-plugin.version}</version> - </plugin> - </plugins> - </pluginManagement> - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-checkstyle-plugin</artifactId> - <version>${maven-checkstyle-plugin.version}</version> - <executions> - <execution> - <id>validate</id> - <goals> - <goal>check</goal> - </goals> - <phase>validate</phase> - <configuration> - <configLocation>google_checks.xml</configLocation> - <suppressionsLocation>checkstyle-suppressions.xml</suppressionsLocation> - - <failOnViolation>true</failOnViolation> - <violationSeverity>warning</violationSeverity> - <includeTestSourceDirectory>false</includeTestSourceDirectory> - </configuration> - </execution> - </executions> - </plugin> - <plugin> - <groupId>com.mycila</groupId> - <artifactId>license-maven-plugin</artifactId> - <version>${license-maven-plugin.version}</version> - <configuration> - <licenseSets> - <licenseSet> - <multi> - <preamble><![CDATA[This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).]]></preamble> - <header>com/mycila/maven/plugin/license/templates/MIT.txt</header> - </multi> - <excludes> - <exclude>**/README</exclude> - <exclude>src/test/resources/**</exclude> - <exclude>src/main/resources/**</exclude> - <exclude>checkstyle-suppressions.xml</exclude> - </excludes> - </licenseSet> - </licenseSets> - <properties> - <owner>Ilkka Seppälä</owner> - <email>iluwatar@gmail.com</email> - </properties> - </configuration> - <executions> - <execution> - <id>install-format</id> - <phase>install</phase> - <goals> - <goal>format</goal> - </goals> - </execution> - </executions> - </plugin> - <plugin> - <groupId>org.jacoco</groupId> - <artifactId>jacoco-maven-plugin</artifactId> - <version>${jacoco.version}</version> - <executions> - <execution> - <id>prepare-agent</id> - <goals> - <goal>prepare-agent</goal> - </goals> - </execution> - <execution> - <id>report</id> - <goals> - <goal>report</goal> - </goals> - </execution> - </executions> - </plugin> - <plugin> - <groupId>com.iluwatar.urm</groupId> - <artifactId>urm-maven-plugin</artifactId> - <version>${urm-maven-plugin.version}</version> - <configuration> - <!-- if outputDirectory is not set explicitly it will default to your build dir --> - <outputDirectory>${project.basedir}/etc</outputDirectory> - <packages> - <param>com.iluwatar</param> - </packages> - <includeMainDirectory>true</includeMainDirectory> - <includeTestDirectory>false</includeTestDirectory> - <presenter>plantuml</presenter> - </configuration> - <executions> - <execution> - <phase>process-classes</phase> - <goals> - <goal>map</goal> - </goals> - </execution> - </executions> - </plugin> - </plugins> - </build> -</project> +<?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> + <groupId>com.iluwatar</groupId> + <artifactId>java-design-patterns</artifactId> + <version>1.26.0-SNAPSHOT</version> + <packaging>pom</packaging> + <inceptionYear>2014-2022</inceptionYear> + <name>Java Design Patterns</name> + <description>Java Design Patterns</description> + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <sonar-maven-plugin.version>4.0.0.4121</sonar-maven-plugin.version> + <spring-boot.version>2.7.5</spring-boot.version> + <jacoco.version>0.8.12</jacoco.version> + <commons-dbcp.version>1.4</commons-dbcp.version> + <htmlunit.version>4.5.0</htmlunit.version> + <gson.version>2.11.0</gson.version> + <guice.version>6.0.0</guice.version> + <system-lambda.version>1.1.0</system-lambda.version> + <maven-surefire-plugin.version>3.5.1</maven-surefire-plugin.version> + <maven-checkstyle-plugin.version>3.6.0</maven-checkstyle-plugin.version> + <license-maven-plugin.version>4.6</license-maven-plugin.version> + <urm-maven-plugin.version>2.1.1</urm-maven-plugin.version> + <slf4j.version>2.0.16</slf4j.version> + <logback.version>1.5.6</logback.version> + <!-- SonarCloud --> + <sonar.host.url>https://sonarcloud.io</sonar.host.url> + <sonar.organization>iluwatar</sonar.organization> + <sonar.projectKey>iluwatar_java-design-patterns</sonar.projectKey> + <sonar.moduleKey>${project.artifactId}</sonar.moduleKey> + <sonar.projectName>Java Design Patterns</sonar.projectName> + </properties> + <modules> + <module>abstract-factory</module> + <module>collecting-parameter</module> + <module>monitor</module> + <module>builder</module> + <module>factory-method</module> + <module>prototype</module> + <module>singleton</module> + <module>adapter</module> + <module>bridge</module> + <module>composite</module> + <module>data-access-object</module> + <module>data-mapper</module> + <module>decorator</module> + <module>facade</module> + <module>flyweight</module> + <module>proxy</module> + <module>chain-of-responsibility</module> + <module>command</module> + <module>interpreter</module> + <module>iterator</module> + <module>mediator</module> + <module>memento</module> + <module>model-view-presenter</module> + <module>observer</module> + <module>state</module> + <module>strategy</module> + <module>template-method</module> + <module>version-number</module> + <module>visitor</module> + <module>double-checked-locking</module> + <module>servant</module> + <module>service-locator</module> + <module>null-object</module> + <module>event-aggregator</module> + <module>callback</module> + <module>execute-around</module> + <module>property</module> + <module>intercepting-filter</module> + <module>producer-consumer</module> + <module>pipeline</module> + <module>poison-pill</module> + <module>lazy-loading</module> + <module>service-layer</module> + <module>specification</module> + <module>tolerant-reader</module> + <module>model-view-controller</module> + <module>flux</module> + <module>double-dispatch</module> + <module>multiton</module> + <module>resource-acquisition-is-initialization</module> + <module>twin</module> + <module>private-class-data</module> + <module>object-pool</module> + <module>dependency-injection</module> + <module>front-controller</module> + <module>repository</module> + <module>async-method-invocation</module> + <module>monostate</module> + <module>step-builder</module> + <module>business-delegate</module> + <module>half-sync-half-async</module> + <module>layered-architecture</module> + <module>fluent-interface</module> + <module>reactor</module> + <module>caching</module> + <module>delegation</module> + <module>event-driven-architecture</module> + <module>microservices-api-gateway</module> + <module>factory-kit</module> + <module>feature-toggle</module> + <module>value-object</module> + <module>monad</module> + <module>mute-idiom</module> + <module>hexagonal-architecture</module> + <module>abstract-document</module> + <module>microservices-aggregrator</module> + <module>promise</module> + <module>page-controller</module> + <module>page-object</module> + <module>event-based-asynchronous</module> + <module>event-queue</module> + <module>queue-based-load-leveling</module> + <module>object-mother</module> + <module>data-bus</module> + <module>converter</module> + <module>guarded-suspension</module> + <module>balking</module> + <module>extension-objects</module> + <module>marker-interface</module> + <module>command-query-responsibility-segregation</module> + <module>event-sourcing</module> + <module>data-transfer-object</module> + <module>throttling</module> + <module>unit-of-work</module> + <module>partial-response</module> + <module>retry</module> + <module>dirty-flag</module> + <module>trampoline</module> + <module>ambassador</module> + <module>acyclic-visitor</module> + <module>collection-pipeline</module> + <module>master-worker</module> + <module>spatial-partition</module> + <module>commander</module> + <module>type-object</module> + <module>bytecode</module> + <module>leader-election</module> + <module>data-locality</module> + <module>subclass-sandbox</module> + <module>circuit-breaker</module> + <module>role-object</module> + <module>saga</module> + <module>double-buffer</module> + <module>sharding</module> + <module>game-loop</module> + <module>combinator</module> + <module>update-method</module> + <module>leader-followers</module> + <module>strangler</module> + <module>arrange-act-assert</module> + <module>transaction-script</module> + <module>registry</module> + <module>filterer</module> + <module>factory</module> + <module>separated-interface</module> + <module>special-case</module> + <module>parameter-object</module> + <module>active-object</module> + <module>model-view-viewmodel</module> + <module>composite-entity</module> + <module>table-module</module> + <module>presentation-model</module> + <module>lockable-object</module> + <module>fanout-fanin</module> + <module>domain-model</module> + <module>composite-view</module> + <module>metadata-mapping</module> + <module>service-to-worker</module> + <module>client-session</module> + <module>model-view-intent</module> + <module>currying</module> + <module>serialized-entity</module> + <module>identity-map</module> + <module>component</module> + <module>context-object</module> + <module>optimistic-offline-lock</module> + <module>curiously-recurring-template-pattern</module> + <module>microservices-log-aggregation</module> + <module>anti-corruption-layer</module> + <module>health-check</module> + <module>notification</module> + <module>single-table-inheritance</module> + <module>dynamic-proxy</module> + <module>gateway</module> + <module>serialized-lob</module> + <module>server-session</module> + <module>virtual-proxy</module> + <module>function-composition</module> + <module>microservices-distributed-tracing</module> + <module>microservices-idempotent-consumer</module> + <module>fallback</module> + </modules> + <repositories> + <repository> + <id>jitpack.io</id> + <url>https://jitpack.io</url> + </repository> + </repositories> + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-dependencies</artifactId> + <version>${spring-boot.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + <dependency> + <groupId>commons-dbcp</groupId> + <artifactId>commons-dbcp</artifactId> + <version>${commons-dbcp.version}</version> + </dependency> + <dependency> + <groupId>org.htmlunit</groupId> + <artifactId>htmlunit</artifactId> + <version>${htmlunit.version}</version> + </dependency> + <dependency> + <groupId>com.google.code.gson</groupId> + <artifactId>gson</artifactId> + <version>${gson.version}</version> + </dependency> + <dependency> + <groupId>com.google.inject</groupId> + <artifactId>guice</artifactId> + <version>${guice.version}</version> + </dependency> + <dependency> + <groupId>com.github.stefanbirkner</groupId> + <artifactId>system-lambda</artifactId> + <version>${system-lambda.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + </dependencyManagement> + <dependencies> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>${slf4j.version}</version> + </dependency> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + <version>${logback.version}</version> + </dependency> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-core</artifactId> + <version>${logback.version}</version> + </dependency> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <scope>provided</scope> + </dependency> + </dependencies> + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <source>17</source> + <target>17</target> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>${maven-surefire-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + </plugin> + <!-- Maven assembly plugin template for all child project to follow --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + <configuration> + <descriptorRefs> + <descriptorRef>jar-with-dependencies</descriptorRef> + </descriptorRefs> + <!-- below two line make sure the fat jar is sharing the same name as of project name --> + <finalName>${project.artifactId}-${project.version}</finalName> + <appendAssemblyId>false</appendAssemblyId> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.sonarsource.scanner.maven</groupId> + <artifactId>sonar-maven-plugin</artifactId> + <version>${sonar-maven-plugin.version}</version> + </plugin> + </plugins> + </pluginManagement> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-checkstyle-plugin</artifactId> + <version>${maven-checkstyle-plugin.version}</version> + <executions> + <execution> + <id>validate</id> + <goals> + <goal>check</goal> + </goals> + <phase>validate</phase> + <configuration> + <configLocation>google_checks.xml</configLocation> + <suppressionsLocation>checkstyle-suppressions.xml</suppressionsLocation> + + <failOnViolation>true</failOnViolation> + <violationSeverity>warning</violationSeverity> + <includeTestSourceDirectory>false</includeTestSourceDirectory> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>com.mycila</groupId> + <artifactId>license-maven-plugin</artifactId> + <version>${license-maven-plugin.version}</version> + <configuration> + <licenseSets> + <licenseSet> + <multi> + <preamble><![CDATA[This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).]]></preamble> + <header>com/mycila/maven/plugin/license/templates/MIT.txt</header> + </multi> + <excludes> + <exclude>**/README</exclude> + <exclude>src/test/resources/**</exclude> + <exclude>src/main/resources/**</exclude> + <exclude>checkstyle-suppressions.xml</exclude> + </excludes> + </licenseSet> + </licenseSets> + <properties> + <owner>Ilkka Seppälä</owner> + <email>iluwatar@gmail.com</email> + </properties> + </configuration> + <executions> + <execution> + <id>install-format</id> + <phase>install</phase> + <goals> + <goal>format</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <version>${jacoco.version}</version> + <executions> + <execution> + <id>prepare-agent</id> + <goals> + <goal>prepare-agent</goal> + </goals> + </execution> + <execution> + <id>report</id> + <goals> + <goal>report</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>com.iluwatar.urm</groupId> + <artifactId>urm-maven-plugin</artifactId> + <version>${urm-maven-plugin.version}</version> + <configuration> + <!-- if outputDirectory is not set explicitly it will default to your build dir --> + <outputDirectory>${project.basedir}/etc</outputDirectory> + <packages> + <param>com.iluwatar</param> + </packages> + <includeMainDirectory>true</includeMainDirectory> + <includeTestDirectory>false</includeTestDirectory> + <presenter>plantuml</presenter> + </configuration> + <executions> + <execution> + <phase>process-classes</phase> + <goals> + <goal>map</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project>