Repository: resilience4j/resilience4j (Java)

Issue:
Allow passing custom `Clock` to `CircuitBreaker` without needing to use internal API.
Resilience4j version:

Java version: 11 (in process of upgrading to 21)

### Use Case
I want to  control the timing of various events  in unit tests for code that uses resilience4j, to verify that circuit breaking is working as expected. 

### Problem
However, while `CircuitBreakerStateMachine` accepts a `Clock` in the constructor, it is an "internal"  package and isn't very well documented, which makes me a little wary of using that API directly. 

### Attempted workarounds

1.  Try specifying a custom `currentTimestampFunction` in the config. I naively thought this would influence all time-based functionality. But it seems like it actually only influences the `CircuitBreaker.getCurrentTimestamp()` method. 

### Proposed solution

Add one or more new static methods to `CircuitBreaker` that include a `Clock` parameter, that pass the `Clock` through to the `CircuitBreakerStateMachine`. And probably also add similar methods to `CircuitBreakerRegistry`.

#### Alternate solution

Refactor CircuitBreakerStateMachine to always use `currentTimestampFunction` instead of using the clock to measure durations. Although then, the only purpose of the clock is to pass to a custom `currentTimestampFunction`.

### Additional Notes

It's weird that the default curent timestamp function uses `System.nanoTime` but the timing between state changes uses a `Clock`. I would either expect the timestamp function to be used for everything (and at that point we could probably just use a `Supplier<Long>`, instead of a function that takes a Clock), or the clock to be used for everything, and the default currentTimestampFunction to get compute the nanoseconds from the Instant given by the clock passed to it. See also https://github.com/resilience4j/resilience4j/issues/734. 

`Clock` seems intended to be used for wall time, and is potentially influenced by clock skew, so using `System.nanoTime` (probably via the  `currentTimestampFunction`) might actually be better for measuring time durations for state changes anyway.


Discussion: I just hit this myself.  Would love the be able to supply a custom `Clock` to `CircuitBreakerConfig`, because unit-testing code involving circuit breaker state transitions is currently very painful.
Hello, feel free to contribute changes.
I'm mostly busy at work and working on http://lmos-ai.org and https://lmos-ai.github.io/arc/
@RobWin done!  I totally get that OSS can take a backseat to our day jobs.  Thanks for your time :)



Fix (patch):
diff --git a/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/CircuitBreakerConfig.java b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/CircuitBreakerConfig.java
index 721d5b0cec..1c11886415 100644
--- a/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/CircuitBreakerConfig.java
+++ b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/CircuitBreakerConfig.java
@@ -56,6 +56,7 @@ public class CircuitBreakerConfig implements Serializable {
     private static final Predicate<Object> DEFAULT_RECORD_RESULT_PREDICATE = (Object object) -> false;
     private static final Function<Either<Object, Throwable>, TransitionCheckResult> DEFAULT_TRANSITION_ON_RESULT
         = any -> TransitionCheckResult.noTransition();
+    private static final Clock DEFAULT_CLOCK = Clock.systemUTC();
     // The default exception predicate counts all exceptions as failures.
 
     private transient Predicate<Throwable> recordExceptionPredicate = DEFAULT_RECORD_EXCEPTION_PREDICATE;
@@ -87,6 +88,7 @@ public class CircuitBreakerConfig implements Serializable {
         .ofSeconds(DEFAULT_SLOW_CALL_DURATION_THRESHOLD);
     private Duration maxWaitDurationInHalfOpenState = Duration
         .ofSeconds(DEFAULT_WAIT_DURATION_IN_HALF_OPEN_STATE);
+    private transient Clock clock = DEFAULT_CLOCK;
 
     private CircuitBreakerConfig() {
     }
@@ -189,6 +191,10 @@ public Duration getMaxWaitDurationInHalfOpenState() {
         return maxWaitDurationInHalfOpenState;
     }
 
+    public Clock getClock() {
+        return clock;
+    }
+
     public enum SlidingWindowType {
         TIME_BASED, COUNT_BASED
     }
@@ -319,6 +325,7 @@ public static class Builder {
         private Duration maxWaitDurationInHalfOpenState = Duration
             .ofSeconds(DEFAULT_WAIT_DURATION_IN_HALF_OPEN_STATE);
         private byte createWaitIntervalFunctionCounter = 0;
+        private Clock clock = DEFAULT_CLOCK;
 
 
         public Builder(CircuitBreakerConfig baseConfig) {
@@ -341,6 +348,7 @@ public Builder(CircuitBreakerConfig baseConfig) {
             this.maxWaitDurationInHalfOpenState = baseConfig.maxWaitDurationInHalfOpenState;
             this.writableStackTraceEnabled = baseConfig.writableStackTraceEnabled;
             this.recordResultPredicate = baseConfig.recordResultPredicate;
+            this.clock = baseConfig.clock;
         }
 
         public Builder() {
@@ -781,6 +789,22 @@ public Builder automaticTransitionFromOpenToHalfOpenEnabled(
             return this;
         }
 
+        /**
+         * Configures a custom Clock instance to use for time measurements.
+         * Default value is Clock.systemUTC().
+         *
+         * @param clock the Clock to use
+         * @return the CircuitBreakerConfig.Builder
+         */
+        public Builder clock(Clock clock) {
+            if (clock == null) {
+                this.clock = DEFAULT_CLOCK;
+            } else {
+                this.clock = clock;
+            }
+            return this;
+        }
+
         /**
          * Builds a CircuitBreakerConfig
          *
@@ -808,6 +832,7 @@ public CircuitBreakerConfig build() {
             config.currentTimestampFunction = currentTimestampFunction;
             config.timestampUnit = timestampUnit;
             config.recordResultPredicate = recordResultPredicate;
+            config.clock = clock;
             return config;
         }
 
diff --git a/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerMetrics.java b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerMetrics.java
index bf7414d3db..37ddcd756a 100644
--- a/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerMetrics.java
+++ b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerMetrics.java
@@ -43,14 +43,13 @@ class CircuitBreakerMetrics implements CircuitBreaker.Metrics {
 
     private CircuitBreakerMetrics(int slidingWindowSize,
         CircuitBreakerConfig.SlidingWindowType slidingWindowType,
-        CircuitBreakerConfig circuitBreakerConfig,
-        Clock clock) {
+        CircuitBreakerConfig circuitBreakerConfig) {
         if (slidingWindowType == CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) {
             this.metrics = new FixedSizeSlidingWindowMetrics(slidingWindowSize);
             this.minimumNumberOfCalls = Math
                 .min(circuitBreakerConfig.getMinimumNumberOfCalls(), slidingWindowSize);
         } else {
-            this.metrics = new SlidingTimeWindowMetrics(slidingWindowSize, clock);
+            this.metrics = new SlidingTimeWindowMetrics(slidingWindowSize, circuitBreakerConfig.getClock());
             this.minimumNumberOfCalls = circuitBreakerConfig.getMinimumNumberOfCalls();
         }
         this.failureRateThreshold = circuitBreakerConfig.getFailureRateThreshold();
@@ -61,33 +60,33 @@ private CircuitBreakerMetrics(int slidingWindowSize,
     }
 
     private CircuitBreakerMetrics(int slidingWindowSize,
-        CircuitBreakerConfig circuitBreakerConfig, Clock clock) {
-        this(slidingWindowSize, circuitBreakerConfig.getSlidingWindowType(), circuitBreakerConfig, clock);
+        CircuitBreakerConfig circuitBreakerConfig) {
+        this(slidingWindowSize, circuitBreakerConfig.getSlidingWindowType(), circuitBreakerConfig);
     }
 
-    static CircuitBreakerMetrics forClosed(CircuitBreakerConfig circuitBreakerConfig, Clock clock) {
+    static CircuitBreakerMetrics forClosed(CircuitBreakerConfig circuitBreakerConfig) {
         return new CircuitBreakerMetrics(circuitBreakerConfig.getSlidingWindowSize(),
-            circuitBreakerConfig, clock);
+            circuitBreakerConfig);
     }
 
     static CircuitBreakerMetrics forHalfOpen(int permittedNumberOfCallsInHalfOpenState,
-        CircuitBreakerConfig circuitBreakerConfig, Clock clock) {
+        CircuitBreakerConfig circuitBreakerConfig) {
         return new CircuitBreakerMetrics(permittedNumberOfCallsInHalfOpenState,
-            CircuitBreakerConfig.SlidingWindowType.COUNT_BASED, circuitBreakerConfig, clock);
+            CircuitBreakerConfig.SlidingWindowType.COUNT_BASED, circuitBreakerConfig);
     }
 
-    static CircuitBreakerMetrics forForcedOpen(CircuitBreakerConfig circuitBreakerConfig, Clock clock) {
+    static CircuitBreakerMetrics forForcedOpen(CircuitBreakerConfig circuitBreakerConfig) {
         return new CircuitBreakerMetrics(0, CircuitBreakerConfig.SlidingWindowType.COUNT_BASED,
-            circuitBreakerConfig, clock);
+            circuitBreakerConfig);
     }
 
-    static CircuitBreakerMetrics forDisabled(CircuitBreakerConfig circuitBreakerConfig, Clock clock) {
+    static CircuitBreakerMetrics forDisabled(CircuitBreakerConfig circuitBreakerConfig) {
         return new CircuitBreakerMetrics(0, CircuitBreakerConfig.SlidingWindowType.COUNT_BASED,
-            circuitBreakerConfig, clock);
+            circuitBreakerConfig);
     }
 
-    static CircuitBreakerMetrics forMetricsOnly(CircuitBreakerConfig circuitBreakerConfig, Clock clock) {
-        return forClosed(circuitBreakerConfig, clock);
+    static CircuitBreakerMetrics forMetricsOnly(CircuitBreakerConfig circuitBreakerConfig) {
+        return forClosed(circuitBreakerConfig);
     }
 
     /**
diff --git a/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerStateMachine.java b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerStateMachine.java
index 3db536045a..14cb4cd203 100644
--- a/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerStateMachine.java
+++ b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerStateMachine.java
@@ -71,16 +71,15 @@ public final class CircuitBreakerStateMachine implements CircuitBreaker {
      *
      * @param name                 the name of the CircuitBreaker
      * @param circuitBreakerConfig The CircuitBreaker configuration.
-     * @param clock                A Clock which can be mocked in tests.
      * @param schedulerFactory     A SchedulerFactory which can be mocked in tests.
      */
     private CircuitBreakerStateMachine(String name, CircuitBreakerConfig circuitBreakerConfig,
-        Clock clock, SchedulerFactory schedulerFactory, Map<String, String> tags) {
+        SchedulerFactory schedulerFactory, Map<String, String> tags) {
         this.name = name;
         this.circuitBreakerConfig = Objects
             .requireNonNull(circuitBreakerConfig, "Config must not be null");
         this.eventProcessor = new CircuitBreakerEventProcessor();
-        this.clock = clock;
+        this.clock = circuitBreakerConfig.getClock();
         this.stateReference = new AtomicReference<>(new ClosedState());
         this.schedulerFactory = schedulerFactory;
         this.tags = Objects.requireNonNull(tags, "Tags must not be null");
@@ -97,29 +96,7 @@ private CircuitBreakerStateMachine(String name, CircuitBreakerConfig circuitBrea
      */
     public CircuitBreakerStateMachine(String name, CircuitBreakerConfig circuitBreakerConfig,
         SchedulerFactory schedulerFactory) {
-        this(name, circuitBreakerConfig, Clock.systemUTC(), schedulerFactory, emptyMap());
-    }
-
-    /**
-     * Creates a circuitBreaker.
-     *
-     * @param name                 the name of the CircuitBreaker
-     * @param circuitBreakerConfig The CircuitBreaker configuration.
-     */
-    public CircuitBreakerStateMachine(String name, CircuitBreakerConfig circuitBreakerConfig,
-        Clock clock) {
-        this(name, circuitBreakerConfig, clock, SchedulerFactory.getInstance(), emptyMap());
-    }
-
-    /**
-     * Creates a circuitBreaker.
-     *
-     * @param name                 the name of the CircuitBreaker
-     * @param circuitBreakerConfig The CircuitBreaker configuration.
-     */
-    public CircuitBreakerStateMachine(String name, CircuitBreakerConfig circuitBreakerConfig,
-        Clock clock, Map<String, String> tags) {
-        this(name, circuitBreakerConfig, clock, SchedulerFactory.getInstance(), tags);
+        this(name, circuitBreakerConfig, schedulerFactory, emptyMap());
     }
 
     /**
@@ -129,7 +106,7 @@ public CircuitBreakerStateMachine(String name, CircuitBreakerConfig circuitBreak
      * @param circuitBreakerConfig The CircuitBreaker configuration.
      */
     public CircuitBreakerStateMachine(String name, CircuitBreakerConfig circuitBreakerConfig) {
-        this(name, circuitBreakerConfig, Clock.systemUTC());
+        this(name, circuitBreakerConfig, SchedulerFactory.getInstance(), emptyMap());
     }
 
     /**
@@ -137,11 +114,10 @@ public CircuitBreakerStateMachine(String name, CircuitBreakerConfig circuitBreak
      *
      * @param name                 the name of the CircuitBreaker
      * @param circuitBreakerConfig The CircuitBreaker configuration.
-     * @param tags                 Tags to add to the CircuitBreaker.
      */
     public CircuitBreakerStateMachine(String name, CircuitBreakerConfig circuitBreakerConfig,
         Map<String, String> tags) {
-        this(name, circuitBreakerConfig, Clock.systemUTC(), tags);
+        this(name, circuitBreakerConfig, SchedulerFactory.getInstance(), tags);
     }
 
     /**
@@ -588,7 +564,7 @@ private class ClosedState implements CircuitBreakerState {
         private final AtomicBoolean isClosed;
 
         ClosedState() {
-            this.circuitBreakerMetrics = CircuitBreakerMetrics.forClosed(getCircuitBreakerConfig(), clock);
+            this.circuitBreakerMetrics = CircuitBreakerMetrics.forClosed(getCircuitBreakerConfig());
             this.isClosed = new AtomicBoolean(true);
         }
 
@@ -828,7 +804,7 @@ private class DisabledState implements CircuitBreakerState {
 
         DisabledState() {
             this.circuitBreakerMetrics = CircuitBreakerMetrics
-                .forDisabled(getCircuitBreakerConfig(), clock);
+                .forDisabled(getCircuitBreakerConfig());
         }
 
         /**
@@ -899,7 +875,7 @@ private class MetricsOnlyState implements CircuitBreakerState {
 
         MetricsOnlyState() {
             circuitBreakerMetrics = CircuitBreakerMetrics
-                .forMetricsOnly(getCircuitBreakerConfig(), clock);
+                .forMetricsOnly(getCircuitBreakerConfig());
             isFailureRateExceeded = new AtomicBoolean(false);
             isSlowCallRateExceeded = new AtomicBoolean(false);
         }
@@ -995,7 +971,7 @@ private class ForcedOpenState implements CircuitBreakerState {
 
         ForcedOpenState(int attempts) {
             this.attempts = attempts;
-            this.circuitBreakerMetrics = CircuitBreakerMetrics.forForcedOpen(circuitBreakerConfig, clock);
+            this.circuitBreakerMetrics = CircuitBreakerMetrics.forForcedOpen(circuitBreakerConfig);
         }
 
         /**
@@ -1074,7 +1050,7 @@ private class HalfOpenState implements CircuitBreakerState {
             int permittedNumberOfCallsInHalfOpenState = circuitBreakerConfig
                 .getPermittedNumberOfCallsInHalfOpenState();
             this.circuitBreakerMetrics = CircuitBreakerMetrics
-                .forHalfOpen(permittedNumberOfCallsInHalfOpenState, getCircuitBreakerConfig(), clock);
+                .forHalfOpen(permittedNumberOfCallsInHalfOpenState, getCircuitBreakerConfig());
             this.permittedNumberOfCalls = new AtomicInteger(permittedNumberOfCallsInHalfOpenState);
             this.isHalfOpen = new AtomicBoolean(true);
             this.attempts = attempts;