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 + - 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 + - executeFallback() : String + - updateFallbackCache(result : String) + - startHealthChecker() + + close() + + getMonitor() : ServiceMonitor + + getState() : ServiceState + - enum ServiceState { STARTING, RUNNING, DEGRADED, CLOSED } + } + + class LocalCacheService { + - cache : Cache + - 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 { + - map : ConcurrentHashMap> + - expiryMs : long + + Cache(expiryMs : long) + + get(key : K) : V + + put(key : K, value : V) + + cleanup() + - record CacheEntry(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 + - lastFailureTime : AtomicReference + - lastResponseTime : AtomicReference + - metrics : Queue + - 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 + + 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 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + fallback + + + 17 + 17 + UTF-8 + + + + org.junit.jupiter + junit-jupiter-engine + 5.8.2 + test + + + org.junit.jupiter + junit-jupiter-api + 5.8.2 + test + + + org.junit.jupiter + junit-jupiter + 5.8.2 + test + + + org.mockito + mockito-core + 4.5.1 + test + + + + \ 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. + * + *

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. + * + *

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 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. + * + *

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 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. + * + *

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 task) throws Exception { + Future 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 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 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 { + private final ConcurrentHashMap> map; + private final long expiryMs; + + Cache(long expiryMs) { + this.map = new ConcurrentHashMap<>(); + this.expiryMs = expiryMs; + } + + V get(K key) { + CacheEntry 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 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 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 lastSuccessTime = new AtomicReference<>(Instant.now()); + private final AtomicReference lastFailureTime = new AtomicReference<>(Instant.now()); + private final AtomicReference lastResponseTime = new AtomicReference<>(Duration.ZERO); + + // Sliding window for metrics + private final Queue 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 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 @@ - - - - 4.0.0 - com.iluwatar - java-design-patterns - 1.26.0-SNAPSHOT - pom - 2014-2022 - Java Design Patterns - Java Design Patterns - - UTF-8 - 4.0.0.4121 - 2.7.5 - 0.8.12 - 1.4 - 4.5.0 - 2.11.0 - 6.0.0 - 1.1.0 - 3.5.1 - 3.6.0 - 4.6 - 2.1.1 - 2.0.16 - 1.5.6 - - https://sonarcloud.io - iluwatar - iluwatar_java-design-patterns - ${project.artifactId} - Java Design Patterns - - - abstract-factory - collecting-parameter - monitor - builder - factory-method - prototype - singleton - adapter - bridge - composite - data-access-object - data-mapper - decorator - facade - flyweight - proxy - chain-of-responsibility - command - interpreter - iterator - mediator - memento - model-view-presenter - observer - state - strategy - template-method - version-number - visitor - double-checked-locking - servant - service-locator - null-object - event-aggregator - callback - execute-around - property - intercepting-filter - producer-consumer - pipeline - poison-pill - lazy-loading - service-layer - specification - tolerant-reader - model-view-controller - flux - double-dispatch - multiton - resource-acquisition-is-initialization - twin - private-class-data - object-pool - dependency-injection - front-controller - repository - async-method-invocation - monostate - step-builder - business-delegate - half-sync-half-async - layered-architecture - fluent-interface - reactor - caching - delegation - event-driven-architecture - microservices-api-gateway - factory-kit - feature-toggle - value-object - monad - mute-idiom - hexagonal-architecture - abstract-document - microservices-aggregrator - promise - page-controller - page-object - event-based-asynchronous - event-queue - queue-based-load-leveling - object-mother - data-bus - converter - guarded-suspension - balking - extension-objects - marker-interface - command-query-responsibility-segregation - event-sourcing - data-transfer-object - throttling - unit-of-work - partial-response - retry - dirty-flag - trampoline - ambassador - acyclic-visitor - collection-pipeline - master-worker - spatial-partition - commander - type-object - bytecode - leader-election - data-locality - subclass-sandbox - circuit-breaker - role-object - saga - double-buffer - sharding - game-loop - combinator - update-method - leader-followers - strangler - arrange-act-assert - transaction-script - registry - filterer - factory - separated-interface - special-case - parameter-object - active-object - model-view-viewmodel - composite-entity - table-module - presentation-model - lockable-object - fanout-fanin - domain-model - composite-view - metadata-mapping - service-to-worker - client-session - model-view-intent - currying - serialized-entity - identity-map - component - context-object - optimistic-offline-lock - curiously-recurring-template-pattern - microservices-log-aggregation - anti-corruption-layer - health-check - notification - single-table-inheritance - dynamic-proxy - gateway - serialized-lob - server-session - virtual-proxy - function-composition - microservices-distributed-tracing - microservices-idempotent-consumer - - - - jitpack.io - https://jitpack.io - - - - - - org.springframework.boot - spring-boot-dependencies - ${spring-boot.version} - pom - import - - - commons-dbcp - commons-dbcp - ${commons-dbcp.version} - - - org.htmlunit - htmlunit - ${htmlunit.version} - - - com.google.code.gson - gson - ${gson.version} - - - com.google.inject - guice - ${guice.version} - - - com.github.stefanbirkner - system-lambda - ${system-lambda.version} - test - - - - - - org.slf4j - slf4j-api - ${slf4j.version} - - - ch.qos.logback - logback-classic - ${logback.version} - - - ch.qos.logback - logback-core - ${logback.version} - - - org.projectlombok - lombok - provided - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 17 - 17 - - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.apache.maven.plugins - maven-assembly-plugin - - - package - - single - - - - jar-with-dependencies - - - ${project.artifactId}-${project.version} - false - - - - - - org.sonarsource.scanner.maven - sonar-maven-plugin - ${sonar-maven-plugin.version} - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - ${maven-checkstyle-plugin.version} - - - validate - - check - - validate - - google_checks.xml - checkstyle-suppressions.xml - - true - warning - false - - - - - - com.mycila - license-maven-plugin - ${license-maven-plugin.version} - - - - - -

com/mycila/maven/plugin/license/templates/MIT.txt
- - - **/README - src/test/resources/** - src/main/resources/** - checkstyle-suppressions.xml - - - - - Ilkka Seppälä - iluwatar@gmail.com - - - - - install-format - install - - format - - - - - - org.jacoco - jacoco-maven-plugin - ${jacoco.version} - - - prepare-agent - - prepare-agent - - - - report - - report - - - - - - com.iluwatar.urm - urm-maven-plugin - ${urm-maven-plugin.version} - - - ${project.basedir}/etc - - com.iluwatar - - true - false - plantuml - - - - process-classes - - map - - - - - - - + + + + 4.0.0 + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + pom + 2014-2022 + Java Design Patterns + Java Design Patterns + + UTF-8 + 4.0.0.4121 + 2.7.5 + 0.8.12 + 1.4 + 4.5.0 + 2.11.0 + 6.0.0 + 1.1.0 + 3.5.1 + 3.6.0 + 4.6 + 2.1.1 + 2.0.16 + 1.5.6 + + https://sonarcloud.io + iluwatar + iluwatar_java-design-patterns + ${project.artifactId} + Java Design Patterns + + + abstract-factory + collecting-parameter + monitor + builder + factory-method + prototype + singleton + adapter + bridge + composite + data-access-object + data-mapper + decorator + facade + flyweight + proxy + chain-of-responsibility + command + interpreter + iterator + mediator + memento + model-view-presenter + observer + state + strategy + template-method + version-number + visitor + double-checked-locking + servant + service-locator + null-object + event-aggregator + callback + execute-around + property + intercepting-filter + producer-consumer + pipeline + poison-pill + lazy-loading + service-layer + specification + tolerant-reader + model-view-controller + flux + double-dispatch + multiton + resource-acquisition-is-initialization + twin + private-class-data + object-pool + dependency-injection + front-controller + repository + async-method-invocation + monostate + step-builder + business-delegate + half-sync-half-async + layered-architecture + fluent-interface + reactor + caching + delegation + event-driven-architecture + microservices-api-gateway + factory-kit + feature-toggle + value-object + monad + mute-idiom + hexagonal-architecture + abstract-document + microservices-aggregrator + promise + page-controller + page-object + event-based-asynchronous + event-queue + queue-based-load-leveling + object-mother + data-bus + converter + guarded-suspension + balking + extension-objects + marker-interface + command-query-responsibility-segregation + event-sourcing + data-transfer-object + throttling + unit-of-work + partial-response + retry + dirty-flag + trampoline + ambassador + acyclic-visitor + collection-pipeline + master-worker + spatial-partition + commander + type-object + bytecode + leader-election + data-locality + subclass-sandbox + circuit-breaker + role-object + saga + double-buffer + sharding + game-loop + combinator + update-method + leader-followers + strangler + arrange-act-assert + transaction-script + registry + filterer + factory + separated-interface + special-case + parameter-object + active-object + model-view-viewmodel + composite-entity + table-module + presentation-model + lockable-object + fanout-fanin + domain-model + composite-view + metadata-mapping + service-to-worker + client-session + model-view-intent + currying + serialized-entity + identity-map + component + context-object + optimistic-offline-lock + curiously-recurring-template-pattern + microservices-log-aggregation + anti-corruption-layer + health-check + notification + single-table-inheritance + dynamic-proxy + gateway + serialized-lob + server-session + virtual-proxy + function-composition + microservices-distributed-tracing + microservices-idempotent-consumer + fallback + + + + jitpack.io + https://jitpack.io + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + commons-dbcp + commons-dbcp + ${commons-dbcp.version} + + + org.htmlunit + htmlunit + ${htmlunit.version} + + + com.google.code.gson + gson + ${gson.version} + + + com.google.inject + guice + ${guice.version} + + + com.github.stefanbirkner + system-lambda + ${system-lambda.version} + test + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + ch.qos.logback + logback-core + ${logback.version} + + + org.projectlombok + lombok + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-assembly-plugin + + + package + + single + + + + jar-with-dependencies + + + ${project.artifactId}-${project.version} + false + + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${sonar-maven-plugin.version} + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + + validate + + check + + validate + + google_checks.xml + checkstyle-suppressions.xml + + true + warning + false + + + + + + com.mycila + license-maven-plugin + ${license-maven-plugin.version} + + + + + +
com/mycila/maven/plugin/license/templates/MIT.txt
+
+ + **/README + src/test/resources/** + src/main/resources/** + checkstyle-suppressions.xml + +
+
+ + Ilkka Seppälä + iluwatar@gmail.com + +
+ + + install-format + install + + format + + + +
+ + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + prepare-agent + + prepare-agent + + + + report + + report + + + + + + com.iluwatar.urm + urm-maven-plugin + ${urm-maven-plugin.version} + + + ${project.basedir}/etc + + com.iluwatar + + true + false + plantuml + + + + process-classes + + map + + + + +
+
+