Repository: App-vNext/Polly

Issue:
BreakDurationGenerator not passed to CircuitStateController
              There is an issue with this. When you call AddCircuitBreaker(options), having specified BreakDurationGenerator. It does not pass the generate to the CircuitStateController. So the resulting circuitbreaker will use the default 5 seconds fixed duration.

_Originally posted by @adminnz in https://github.com/App-vNext/Polly/issues/1776#issuecomment-1846438825_
            


Hints:
Not sure what more to add,  the BreakDurationGenerator does not get passed to the controller.

So if you do:
```
        ResiliencePipeline = new ResiliencePipelineBuilder()
           .AddCircuitBreaker(new CircuitBreakerStrategyOptions
           {
               BreakDurationGenerator = args => ValueTask.FromResult(TimeSpan.FromMinutes(args.FailureCount)),
               MinimumThroughput = Settings.CircuitBreakerMinimumThroughput,
               FailureRatio = Settings.CircuitBreakerFailureRatio,
               SamplingDuration = Settings.CircuitBreakerSamplingDuration,
               StateProvider = CircuitBreakerStateProvider,
               OnClosed = OnCircuitClosed,
               OnOpened = OnCircuitOpened,
               OnHalfOpened = OnHalfOpened
           })
           .Build();
```

The resulting ResiliencePipline circuit breaker will not use the BreakDurationGenerator. Instead it will use the default 5 second BreakDuration.
Thanks - it was a concrete code snippet I was after so that we can use it to build up a unit test.

Patch:
diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerResiliencePipelineBuilderExtensions.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerResiliencePipelineBuilderExtensions.cs
index 189d3c5c886..37df92bb498 100644
--- a/src/Polly.Core/CircuitBreaker/CircuitBreakerResiliencePipelineBuilderExtensions.cs
+++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerResiliencePipelineBuilderExtensions.cs
@@ -85,7 +85,8 @@ internal static CircuitBreakerResilienceStrategy<TResult> CreateStrategy<TResult
             options.OnHalfOpened,
             behavior,
             context.TimeProvider,
-            context.Telemetry);
+            context.Telemetry,
+            options.BreakDurationGenerator);
 #pragma warning restore CA2000 // Dispose objects before losing scope
 
         return new CircuitBreakerResilienceStrategy<TResult>(
diff --git a/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs b/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs
index 73fc1010629..a00907ed227 100644
--- a/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs
+++ b/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs
@@ -32,7 +32,7 @@ public CircuitStateController(
         CircuitBehavior behavior,
         TimeProvider timeProvider,
         ResilienceStrategyTelemetry telemetry,
-        Func<BreakDurationGeneratorArguments, ValueTask<TimeSpan>>? breakDurationGenerator = null)
+        Func<BreakDurationGeneratorArguments, ValueTask<TimeSpan>>? breakDurationGenerator)
 #pragma warning restore S107
     {
         _breakDuration = breakDuration;
diff --git a/src/Snippets/Snippets.csproj b/src/Snippets/Snippets.csproj
index d6bf4d3ac66..6fad9e32e13 100644
--- a/src/Snippets/Snippets.csproj
+++ b/src/Snippets/Snippets.csproj
@@ -8,6 +8,7 @@
     <ProjectType>Library</ProjectType>
     <GenerateDocumentationFile>false</GenerateDocumentationFile>
     <IsPackable>false</IsPackable>
+    <IsTestProject>false</IsTestProject>
     <NoWarn>$(NoWarn);SA1123;SA1515;CA2000;CA2007;CA1303;IDE0021;IDE0017;IDE0060;CS1998;CA1064;S3257;IDE0028;CA1031;CA1848</NoWarn>
     <NoWarn>$(NoWarn);RS0016;SA1402;SA1600;RS0037;CA1062;SA1204</NoWarn>
     <RootNamespace>Snippets</RootNamespace>


Test patch:
diff --git a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResiliencePipelineBuilderTests.cs b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResiliencePipelineBuilderTests.cs
index 51c75f9e424..5cee0687360 100644
--- a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResiliencePipelineBuilderTests.cs
+++ b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResiliencePipelineBuilderTests.cs
@@ -68,12 +68,66 @@ public void AddCircuitBreaker_IntegrationTest()
         int closed = 0;
         int halfOpened = 0;
 
+        var breakDuration = TimeSpan.FromSeconds(1);
+
+        var options = new CircuitBreakerStrategyOptions
+        {
+            FailureRatio = 0.5,
+            MinimumThroughput = 10,
+            SamplingDuration = TimeSpan.FromSeconds(10),
+            BreakDuration = breakDuration,
+            ShouldHandle = args => new ValueTask<bool>(args.Outcome.Result is -1),
+            OnOpened = _ => { opened++; return default; },
+            OnClosed = _ => { closed++; return default; },
+            OnHalfOpened = (_) => { halfOpened++; return default; }
+        };
+
+        var timeProvider = new FakeTimeProvider();
+        var strategy = new ResiliencePipelineBuilder { TimeProvider = timeProvider }.AddCircuitBreaker(options).Build();
+
+        for (int i = 0; i < 10; i++)
+        {
+            strategy.Execute(_ => -1);
+        }
+
+        // Circuit opened
+        opened.Should().Be(1);
+        halfOpened.Should().Be(0);
+        closed.Should().Be(0);
+        Assert.Throws<BrokenCircuitException>(() => strategy.Execute(_ => 0));
+
+        // Circuit Half Opened
+        timeProvider.Advance(breakDuration);
+        strategy.Execute(_ => -1);
+        Assert.Throws<BrokenCircuitException>(() => strategy.Execute(_ => 0));
+        opened.Should().Be(2);
+        halfOpened.Should().Be(1);
+        closed.Should().Be(0);
+
+        // Now close it
+        timeProvider.Advance(breakDuration);
+        strategy.Execute(_ => 0);
+        opened.Should().Be(2);
+        halfOpened.Should().Be(2);
+        closed.Should().Be(1);
+    }
+
+    [Fact]
+    public void AddCircuitBreaker_IntegrationTest_WithBreakDurationGenerator()
+    {
+        int opened = 0;
+        int closed = 0;
+        int halfOpened = 0;
+
+        var breakDuration = TimeSpan.FromSeconds(1);
+
         var options = new CircuitBreakerStrategyOptions
         {
             FailureRatio = 0.5,
             MinimumThroughput = 10,
             SamplingDuration = TimeSpan.FromSeconds(10),
-            BreakDuration = TimeSpan.FromSeconds(1),
+            BreakDuration = TimeSpan.FromSeconds(30), // Intentionally long to check it isn't used
+            BreakDurationGenerator = (_) => new ValueTask<TimeSpan>(breakDuration),
             ShouldHandle = args => new ValueTask<bool>(args.Outcome.Result is -1),
             OnOpened = _ => { opened++; return default; },
             OnClosed = _ => { closed++; return default; },
@@ -95,7 +149,7 @@ public void AddCircuitBreaker_IntegrationTest()
         Assert.Throws<BrokenCircuitException>(() => strategy.Execute(_ => 0));
 
         // Circuit Half Opened
-        timeProvider.Advance(options.BreakDuration);
+        timeProvider.Advance(breakDuration);
         strategy.Execute(_ => -1);
         Assert.Throws<BrokenCircuitException>(() => strategy.Execute(_ => 0));
         opened.Should().Be(2);
@@ -103,7 +157,7 @@ public void AddCircuitBreaker_IntegrationTest()
         closed.Should().Be(0);
 
         // Now close it
-        timeProvider.Advance(options.BreakDuration);
+        timeProvider.Advance(breakDuration);
         strategy.Execute(_ => 0);
         opened.Should().Be(2);
         halfOpened.Should().Be(2);
diff --git a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs
index 03c7d7c7658..caf1eff9a0b 100644
--- a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs
+++ b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs
@@ -26,7 +26,8 @@ public CircuitBreakerResilienceStrategyTests()
             null,
             _behavior,
             _timeProvider,
-            _telemetry);
+            _telemetry,
+            null);
     }
 
     [Fact]
diff --git a/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs b/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs
index 3a7922c1162..f69f4ca4600 100644
--- a/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs
+++ b/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs
@@ -309,26 +309,22 @@ public async Task OnActionFailureAsync_EnsureCorrectBehavior(CircuitState state,
     public async Task OnActionFailureAsync_EnsureBreakDurationGeneration()
     {
         // arrange
-        using var controller = CreateController(new()
+        _options.BreakDurationGenerator = static args =>
         {
-            FailureRatio = 0,
-            MinimumThroughput = 0,
-            SamplingDuration = default,
-            BreakDuration = TimeSpan.FromMinutes(1),
-            BreakDurationGenerator = static args => new ValueTask<TimeSpan>(TimeSpan.FromMinutes(args.FailureCount)),
-            OnClosed = null,
-            OnOpened = null,
-            OnHalfOpened = null,
-            ManualControl = null,
-            StateProvider = null
-        });
+            args.FailureCount.Should().Be(1);
+            args.FailureRate.Should().Be(0.5);
+            return new ValueTask<TimeSpan>(TimeSpan.FromMinutes(42));
+        };
 
-        await TransitionToState(controller, CircuitState.Closed);
+        using var controller = CreateController();
 
-        var utcNow = DateTimeOffset.MaxValue;
+        await TransitionToState(controller, CircuitState.Closed);
 
+        var utcNow = new DateTimeOffset(2023, 12, 12, 12, 34, 56, TimeSpan.Zero);
         _timeProvider.SetUtcNow(utcNow);
+
         _circuitBehavior.FailureCount.Returns(1);
+        _circuitBehavior.FailureRate.Returns(0.5);
         _circuitBehavior.When(v => v.OnActionFailure(CircuitState.Closed, out Arg.Any<bool>()))
             .Do(x => x[1] = true);
 
@@ -337,7 +333,7 @@ public async Task OnActionFailureAsync_EnsureBreakDurationGeneration()
 
         // assert
         var blockedTill = GetBlockedTill(controller);
-        blockedTill.Should().Be(utcNow);
+        blockedTill.Should().Be(utcNow + TimeSpan.FromMinutes(42));
     }
 
     [InlineData(true)]
@@ -504,15 +500,6 @@ private async Task OpenCircuit(CircuitStateController<int> controller, Outcome<i
         _options.OnHalfOpened,
         _circuitBehavior,
         _timeProvider,
-        TestUtilities.CreateResilienceTelemetry(_telemetryListener));
-
-    private CircuitStateController<int> CreateController(CircuitBreakerStrategyOptions<int> options) => new(
-        options.BreakDuration,
-        options.OnOpened,
-        options.OnClosed,
-        options.OnHalfOpened,
-        _circuitBehavior,
-        _timeProvider,
         TestUtilities.CreateResilienceTelemetry(_telemetryListener),
-        options.BreakDurationGenerator);
+        _options.BreakDurationGenerator);
 }