Problem Statement
Design a client-side Circuit Breaker framework (similar to Hystrix or Resilience4j). The circuit breaker acts as a protective wrapper around fragile remote service integration points. It monitors execution failures and prevents cascading network issues by transitions through three states: CLOSED (normal flow), OPEN (fail-fast and fallback execution), and HALF_OPEN (probing recovery).
Functional Requirements
- Support three distinct states:
CLOSED,OPEN, andHALF_OPEN. - Provide a wrapper method
execute(supplier, fallback)that routes execution or fails fast to a fallback immediately. - Transition from
CLOSEDtoOPENif consecutive execution failures exceed a configured threshold. - Transition from
OPENtoHALF_OPENafter a configured recovery cooldown timeout window. - In the
HALF_OPENstate, allow a limited number of test requests. If all test requests succeed, close the circuit; if any call fails, reopen the circuit. - Ensure thread safety for state transitions and counter evaluations.
Objects Required
CircuitBreakerState(State design pattern interface)ClosedState(Logic when remote calls operate normally)OpenState(Logic when remote calls fail fast)HalfOpenState(Logic when testing service recovery)CircuitBreaker(Context coordinator maintaining current configuration)
CircuitBreakerState Interface
We apply the **State Design Pattern** by modeling the behavior rules inside a common CircuitBreakerState interface. This keeps our code modular and prevents massive nested if-else structures.
import java.util.function.Function;
import java.util.function.Supplier;
public interface CircuitBreakerState {
<T> T execute(Supplier<T> supplier, Function<Throwable, T> fallback);
}
The execute() method is generic. It takes a functional Supplier containing the remote task logic, and a fallback handler Function that returns a rescue payload if the task raises an exception.
CircuitBreaker Context Class
The CircuitBreaker maintains configuration metrics (failure threshold, recovery duration) and delegates its core routing rules to the active polymorphic CircuitBreakerState.
import java.util.function.Function;
import java.util.function.Supplier;
public class CircuitBreaker {
private CircuitBreakerState state;
private final int failureThreshold;
private final long recoveryTimeoutMs;
private final int trialSuccessThreshold;
private int consecutiveFailures = 0;
private int consecutiveSuccesses = 0;
private long lastStateChangedTime;
public CircuitBreaker(int failureThreshold, long recoveryTimeoutMs, int trialSuccessThreshold) {
this.failureThreshold = failureThreshold;
this.recoveryTimeoutMs = recoveryTimeoutMs;
this.trialSuccessThreshold = trialSuccessThreshold;
this.lastStateChangedTime = System.currentTimeMillis();
this.state = new ClosedState(this);
}
public synchronized void setState(CircuitBreakerState state) {
this.state = state;
this.lastStateChangedTime = System.currentTimeMillis();
this.consecutiveFailures = 0;
this.consecutiveSuccesses = 0;
System.out.println("Circuit Breaker transitioned to: " + state.getClass().getSimpleName());
}
public <T> T execute(Supplier<T> supplier, Function<Throwable, T> fallback) {
return state.execute(supplier, fallback);
}
// --- State and Counter Mutators ---
public synchronized void recordSuccess() {
consecutiveSuccesses++;
consecutiveFailures = 0;
}
public synchronized void recordFailure() {
consecutiveFailures++;
consecutiveSuccesses = 0;
}
public synchronized int getConsecutiveFailures() { return consecutiveFailures; }
public synchronized int getConsecutiveSuccesses() { return consecutiveSuccesses; }
public int getFailureThreshold() { return failureThreshold; }
public long getRecoveryTimeoutMs() { return recoveryTimeoutMs; }
public int getTrialSuccessThreshold() { return trialSuccessThreshold; }
public long getLastStateChangedTime() { return lastStateChangedTime; }
}
Here is an explanation of the core operations in the CircuitBreaker class:
- The constructor configures thresholds and initiates the default state to
ClosedState. setState()changes the active state, updates timestamps, resets operational counters, and prints debugging markers. It is synchronized to prevent concurrent state transitions.execute()acts as a simple pass-through that delegates the task directly to the current state implementation.recordSuccess()andrecordFailure()manage thread-safe counter increments, which are evaluated by the state instances.
ClosedState Class
The ClosedState represents normal operating conditions. Remote tasks are attempted, and failures are recorded.
import java.util.function.Function;
import java.util.function.Supplier;
public class ClosedState implements CircuitBreakerState {
private final CircuitBreaker cb;
public ClosedState(CircuitBreaker cb) {
this.cb = cb;
}
@Override
public <T> T execute(Supplier<T> supplier, Function<Throwable, T> fallback) {
try {
T result = supplier.get();
cb.recordSuccess();
return result;
} catch (Throwable throwable) {
cb.recordFailure();
if (cb.getConsecutiveFailures() >= cb.getFailureThreshold()) {
cb.setState(new OpenState(cb));
}
return fallback.apply(throwable);
}
}
}
The execute() method executes the task. On success, it calls recordSuccess(). On exception, it records the failure. If failures exceed limits, it transitions the breaker context to OpenState and runs the recovery fallback.
OpenState Class
The OpenState prevents network requests from reaching struggling downstream dependencies. It immediately executes fallbacks, but checks timeouts on each call to see if it can attempt recovery.
import java.util.function.Function;
import java.util.function.Supplier;
public class OpenState implements CircuitBreakerState {
private final CircuitBreaker cb;
public OpenState(CircuitBreaker cb) {
this.cb = cb;
}
@Override
public <T> T execute(Supplier<T> supplier, Function<Throwable, T> fallback) {
long elapsed = System.currentTimeMillis() - cb.getLastStateChangedTime();
if (elapsed >= cb.getRecoveryTimeoutMs()) {
// Cool-down period elapsed, transition to HALF_OPEN and retry execution
cb.setState(new HalfOpenState(cb));
return cb.execute(supplier, fallback);
}
// Circuit is wide open, execute fallback directly without calling downstream service
return fallback.apply(new RuntimeException("Circuit Breaker is OPEN. Fail-fast active."));
}
}
The execute() method checks if the recovery timeout has elapsed. If so, it updates the state to HalfOpenState and calls cb.execute(...) recursively. If the cooldown is still active, it bypasses the supplier completely and fires the fallback wrapper.
HalfOpenState Class
The HalfOpenState acts as a testing phase. It permits a limited number of executions to probe the health of the downstream service.
import java.util.function.Function;
import java.util.function.Supplier;
public class HalfOpenState implements CircuitBreakerState {
private final CircuitBreaker cb;
public HalfOpenState(CircuitBreaker cb) {
this.cb = cb;
}
@Override
public <T> T execute(Supplier<T> supplier, Function<Throwable, T> fallback) {
try {
T result = supplier.get();
cb.recordSuccess();
if (cb.getConsecutiveSuccesses() >= cb.getTrialSuccessThreshold()) {
cb.setState(new ClosedState(cb));
}
return result;
} catch (Throwable throwable) {
// Any failure in HALF_OPEN resets the timer and reopens the circuit
cb.recordFailure();
cb.setState(new OpenState(cb));
return fallback.apply(throwable);
}
}
}
The execute() method attempts the execution. If it succeeds, the count updates, and if successes cross the trial threshold, it restores the state to ClosedState. If a single exception is caught, it instantly flips back to OpenState, starting the cooldown timer fresh.
Comments
Post a Comment