Skip to content

Commit b156944

Browse files
authored
[automatic failover] Builder APIs for DatabaseConfig and CircuitBreakerConfig (CAE-1695) (#3571)
* add DatabaseConfig.Builder * healthCheckStrategySupplier now defaults to PingStrategy.DEFAULT in the builder - When using the builder without setting healthCheckStrategySupplier: Health checks will use PingStrategy.DEFAULT - When explicitly setting to null: Health checks will be disabled (as documented) - When setting to a custom supplier: Uses the custom health check strategy Example Usage: // Uses PingStrategy.DEFAULT for health checks DatabaseConfig config1 = DatabaseConfig.builder(uri) .weight(1.0f) .build(); // Explicitly disables health checks DatabaseConfig config2 = DatabaseConfig.builder(uri) .healthCheckStrategySupplier(null) .build(); // Uses custom health check strategy DatabaseConfig config3 = DatabaseConfig.builder(uri) .healthCheckStrategySupplier(customSupplier) .build(); * HealthCheckStrategySupplier.NO_HEALTH_CHECK instead null * Remove DatabaseConfig constructors // To create DatabaseConfig use provided builder DatabaseConfig config = DatabaseConfig.builder(redisURI) .weight(1.5f) .clientOptions(options) .circuitBreakerConfig(cbConfig) .healthCheckStrategySupplier(supplier) .build(); * remove redundant public modifiers * Builder for CircuitBreakerConfig // Minimal configuration with defaults CircuitBreakerConfig config = CircuitBreakerConfig.builder().build(); // Custom configuration CircuitBreakerConfig config = CircuitBreakerConfig.builder() .failureRateThreshold(25.0f) .minimumNumberOfFailures(500) .metricsWindowSize(5) .build(); // With custom tracked exceptions Set<Class<? extends Throwable>> customExceptions = new HashSet<>(); customExceptions.add(RuntimeException.class); CircuitBreakerConfig config = CircuitBreakerConfig.builder() .failureRateThreshold(15.5f) .minimumNumberOfFailures(200) .trackedExceptions(customExceptions) .metricsWindowSize(3) .build(); * enforce min window size of 2s * tracked exceptions should not be null * add convenience methods for Tracked Exceptions //Combine add and remove CircuitBreakerConfig config = CircuitBreakerConfig.builder() .addTrackedExceptions(MyCustomException.class) .removeTrackedExceptions(TimeoutException.class) .build(); // Replace all tracked exceptions Set<Class<? extends Throwable>> customExceptions = new HashSet<>(); customExceptions.add(RuntimeException.class); customExceptions.add(IOException.class); CircuitBreakerConfig config = CircuitBreakerConfig.builder() .trackedExceptions(customExceptions) .build(); * remove option to configure per database clientOptions till #3572 is resolved * Disable health checks in test configs to isolate circuit breaker testing Configure DB1, DB2, and DB3 with NO_HEALTH_CHECK to prevent health check interference when testing circuit breaker failure detection. * forma * clean up * address review comments (Copilot)
1 parent 20ea67a commit b156944

14 files changed

+962
-215
lines changed

src/main/java/io/lettuce/core/failover/CircuitBreaker.java

Lines changed: 159 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
import io.lettuce.core.RedisConnectionException;
1414
import io.lettuce.core.failover.api.CircuitBreakerStateListener;
1515
import io.lettuce.core.failover.metrics.MetricsSnapshot;
16+
import io.lettuce.core.internal.LettuceAssert;
1617

1718
/**
18-
* Circuit breaker for tracking command metrics and managing circuit breaker state. Wraps CircuitBreakerMetrics and exposes it
19-
* via {@link #getMetrics()}.
19+
* Circuit breaker for tracking command metrics and managing circuit breaker state.
20+
*
2021
* <p>
21-
* State transitions and metrics replacement are atomic and lock-free using {@link AtomicReference}. When the circuit breaker
22-
* transitions to a new state, a fresh metrics instance is created atomically.
22+
* This interface provides methods to track command metrics and manage circuit breaker state. Implementations must be
23+
* thread-safe.
24+
* </p>
2325
*
2426
* @author Ali Takavci
2527
* @since 7.1
@@ -32,52 +34,52 @@ public interface CircuitBreaker extends Closeable {
3234
*
3335
* @return an immutable snapshot of current metrics
3436
*/
35-
public MetricsSnapshot getSnapshot();
37+
MetricsSnapshot getSnapshot();
3638

3739
/**
3840
* Get the current generation of the circuit breaker. This is used to track the state and metrics of the circuit breaker at
3941
* the time of command execution.
4042
*
4143
* @return the current generation of the circuit breaker
4244
*/
43-
public CircuitBreakerGeneration getGeneration();
45+
CircuitBreakerGeneration getGeneration();
4446

4547
/**
4648
* Get the current state of the circuit breaker.
4749
*
4850
* @return the current state
4951
*/
50-
public State getCurrentState();
52+
State getCurrentState();
5153

5254
/**
5355
* Check if the circuit breaker is in the closed state.
5456
*
5557
* @return {@code true} if the circuit breaker is in the closed state
5658
*/
57-
public boolean isClosed();
59+
boolean isClosed();
5860

5961
/**
6062
* Add a listener for circuit breaker state change events.
6163
*
6264
* @param listener the listener to add, must not be {@code null}
6365
*/
64-
public void addListener(CircuitBreakerStateListener listener);
66+
void addListener(CircuitBreakerStateListener listener);
6567

6668
/**
6769
* Remove a listener for circuit breaker state change events.
6870
*
6971
* @param listener the listener to remove, must not be {@code null}
7072
*/
71-
public void removeListener(CircuitBreakerStateListener listener);
73+
void removeListener(CircuitBreakerStateListener listener);
7274

7375
@Override
74-
public void close();
76+
void close();
7577

76-
public enum State {
78+
enum State {
7779
CLOSED, OPEN
7880
}
7981

80-
public static class CircuitBreakerConfig {
82+
class CircuitBreakerConfig {
8183

8284
private final static float DEFAULT_FAILURE_RATE_THRESHOLD = 10;
8385

@@ -98,7 +100,7 @@ public static class CircuitBreakerConfig {
98100

99101
));
100102

101-
public static final CircuitBreakerConfig DEFAULT = new CircuitBreakerConfig();
103+
public static final CircuitBreakerConfig DEFAULT = builder().build();
102104

103105
private final Set<Class<? extends Throwable>> trackedExceptions;
104106

@@ -108,17 +110,26 @@ public static class CircuitBreakerConfig {
108110

109111
private final int metricsWindowSize;
110112

111-
private CircuitBreakerConfig() {
112-
this(DEFAULT_FAILURE_RATE_THRESHOLD, DEFAULT_MINIMUM_NUMBER_OF_FAILURES, DEFAULT_TRACKED_EXCEPTIONS,
113-
DEFAULT_METRICS_WINDOW_SIZE);
113+
/**
114+
* Create a new circuit breaker configuration from a builder. Use {@link #builder()} instead.
115+
*
116+
* @param builder the builder
117+
*/
118+
CircuitBreakerConfig(Builder builder) {
119+
this.trackedExceptions = new HashSet<>(builder.trackedExceptions);
120+
this.failureThreshold = builder.failureThreshold;
121+
this.minimumNumberOfFailures = builder.minimumNumberOfFailures;
122+
this.metricsWindowSize = builder.metricsWindowSize;
114123
}
115124

116-
public CircuitBreakerConfig(float failureThreshold, int minimumNumberOfFailures,
117-
Set<Class<? extends Throwable>> trackedExceptions, int metricsWindowSize) {
118-
this.trackedExceptions = trackedExceptions;
119-
this.failureThreshold = failureThreshold;
120-
this.minimumNumberOfFailures = minimumNumberOfFailures;
121-
this.metricsWindowSize = metricsWindowSize;
125+
/**
126+
* Create a new builder for {@link CircuitBreakerConfig}.
127+
*
128+
* @return a new builder
129+
* @since 7.4
130+
*/
131+
public static Builder builder() {
132+
return new Builder();
122133
}
123134

124135
public Set<Class<? extends Throwable>> getTrackedExceptions() {
@@ -143,6 +154,131 @@ public String toString() {
143154
+ ", minimumNumberOfFailures=" + minimumNumberOfFailures + ", metricsWindowSize=" + metricsWindowSize + '}';
144155
}
145156

157+
/**
158+
* Builder for {@link CircuitBreakerConfig}.
159+
*
160+
* @since 7.4
161+
*/
162+
public static class Builder {
163+
164+
private Set<Class<? extends Throwable>> trackedExceptions = DEFAULT_TRACKED_EXCEPTIONS;
165+
166+
private float failureThreshold = DEFAULT_FAILURE_RATE_THRESHOLD;
167+
168+
private int minimumNumberOfFailures = DEFAULT_MINIMUM_NUMBER_OF_FAILURES;
169+
170+
private int metricsWindowSize = DEFAULT_METRICS_WINDOW_SIZE;
171+
172+
private Builder() {
173+
}
174+
175+
/**
176+
* Set the failure rate threshold percentage (0-100). The circuit breaker will open when the failure rate exceeds
177+
* this threshold.
178+
*
179+
* @param failureThreshold the failure rate threshold percentage, must be >= 0
180+
* @return {@code this} builder
181+
*/
182+
public Builder failureRateThreshold(float failureThreshold) {
183+
this.failureThreshold = failureThreshold;
184+
return this;
185+
}
186+
187+
/**
188+
* Set the minimum number of failures required before the circuit breaker can open. This prevents the circuit from
189+
* opening due to a small number of failures.
190+
*
191+
* @param minimumNumberOfFailures the minimum number of failures, must be >= 0
192+
* @return {@code this} builder
193+
*/
194+
public Builder minimumNumberOfFailures(int minimumNumberOfFailures) {
195+
this.minimumNumberOfFailures = minimumNumberOfFailures;
196+
return this;
197+
}
198+
199+
/**
200+
* Set the exceptions to track for circuit breaker metrics. Only these exceptions (and their subclasses) will be
201+
* counted as failures. This replaces any previously configured tracked exceptions.
202+
*
203+
* @param trackedExceptions the set of exception classes to track, must not be {@code null}
204+
* @return {@code this} builder
205+
*/
206+
public Builder trackedExceptions(Set<Class<? extends Throwable>> trackedExceptions) {
207+
LettuceAssert.notNull(trackedExceptions, "Tracked exceptions must not be null");
208+
this.trackedExceptions = trackedExceptions;
209+
return this;
210+
}
211+
212+
/**
213+
* Add one or more exception classes to track for circuit breaker metrics. Only these exceptions (and their
214+
* subclasses) will be counted as failures. This adds to the existing tracked exceptions.
215+
*
216+
* @param exceptionClasses one or more exception classes to track, must not be {@code null} and must not contain
217+
* {@code null} elements
218+
* @return {@code this} builder
219+
* @since 7.4
220+
*/
221+
@SafeVarargs
222+
public final Builder addTrackedExceptions(Class<? extends Throwable>... exceptionClasses) {
223+
LettuceAssert.notNull(exceptionClasses, "Exception classes must not be null");
224+
LettuceAssert.noNullElements(exceptionClasses, "Exception classes must not contain null elements");
225+
LettuceAssert.notEmpty(exceptionClasses, "Exception classes must contain at least one element");
226+
227+
// Ensure we have a mutable copy
228+
if (this.trackedExceptions == DEFAULT_TRACKED_EXCEPTIONS) {
229+
this.trackedExceptions = new HashSet<>(DEFAULT_TRACKED_EXCEPTIONS);
230+
}
231+
232+
this.trackedExceptions.addAll(Arrays.asList(exceptionClasses));
233+
return this;
234+
}
235+
236+
/**
237+
* Remove one or more exception classes from the tracked exceptions for circuit breaker metrics.
238+
*
239+
* @param exceptionClasses one or more exception classes to remove, must not be {@code null} and must not contain
240+
* {@code null} elements
241+
* @return {@code this} builder
242+
* @since 7.4
243+
*/
244+
@SafeVarargs
245+
public final Builder removeTrackedExceptions(Class<? extends Throwable>... exceptionClasses) {
246+
LettuceAssert.notNull(exceptionClasses, "Exception classes must not be null");
247+
LettuceAssert.noNullElements(exceptionClasses, "Exception classes must not contain null elements");
248+
LettuceAssert.notEmpty(exceptionClasses, "Exception classes must contain at least one element");
249+
250+
// Ensure we have a mutable copy
251+
if (this.trackedExceptions == DEFAULT_TRACKED_EXCEPTIONS) {
252+
this.trackedExceptions = new HashSet<>(DEFAULT_TRACKED_EXCEPTIONS);
253+
}
254+
255+
Arrays.asList(exceptionClasses).forEach(this.trackedExceptions::remove);
256+
return this;
257+
}
258+
259+
/**
260+
* Set the metrics window size in seconds. Metrics are collected over this time window.
261+
*
262+
* @param metricsWindowSize the metrics window size in seconds, must be >= 2
263+
* @return {@code this} builder
264+
*/
265+
public Builder metricsWindowSize(int metricsWindowSize) {
266+
LettuceAssert.isTrue(metricsWindowSize >= 2, "Metrics window size must be at least 2 seconds");
267+
this.metricsWindowSize = metricsWindowSize;
268+
return this;
269+
}
270+
271+
/**
272+
* Build a new {@link CircuitBreakerConfig} instance.
273+
*
274+
* @return a new {@link CircuitBreakerConfig}
275+
*/
276+
public CircuitBreakerConfig build() {
277+
return new CircuitBreakerConfig(this);
278+
}
279+
280+
}
281+
146282
}
147283

148284
}

0 commit comments

Comments
 (0)