diff --git a/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs b/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs index a00907ed22..e233337088 100644 --- a/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs +++ b/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs @@ -20,7 +20,7 @@ internal sealed class CircuitStateController : IDisposable private DateTimeOffset _blockedUntil; private CircuitState _circuitState = CircuitState.Closed; private Outcome? _lastOutcome; - private BrokenCircuitException _breakingException = new(); + private Exception? _breakingException; private bool _disposed; #pragma warning disable S107 @@ -139,8 +139,8 @@ public ValueTask CloseCircuitAsync(ResilienceContext context) exception = _circuitState switch { - CircuitState.Open => _breakingException, - CircuitState.HalfOpen when !isHalfOpen => _breakingException, + CircuitState.Open => CreateBrokenCircuitException(), + CircuitState.HalfOpen when !isHalfOpen => CreateBrokenCircuitException(), CircuitState.Isolated => new IsolatedCircuitException(), _ => null }; @@ -302,17 +302,15 @@ private bool PermitHalfOpenCircuitTest_NeedsLock() private void SetLastHandledOutcome_NeedsLock(Outcome outcome) { _lastOutcome = outcome; - - if (outcome.Exception is Exception exception) - { - _breakingException = new BrokenCircuitException(BrokenCircuitException.DefaultMessage, exception); - } - else - { - _breakingException = new BrokenCircuitException(BrokenCircuitException.DefaultMessage); - } + _breakingException = outcome.Exception; } + private BrokenCircuitException CreateBrokenCircuitException() => _breakingException switch + { + Exception exception => new BrokenCircuitException(BrokenCircuitException.DefaultMessage, exception), + _ => new BrokenCircuitException(BrokenCircuitException.DefaultMessage) + }; + private void OpenCircuit_NeedsLock(Outcome outcome, bool manual, ResilienceContext context, out Task? scheduledTask) { OpenCircuitFor_NeedsLock(outcome, _breakDuration, manual, context, out scheduledTask); diff --git a/test/Polly.Core.Tests/CircuitBreaker/BrokenCircuitExceptionTests.cs b/test/Polly.Core.Tests/CircuitBreaker/BrokenCircuitExceptionTests.cs index 3fb989e7a5..7b69c98705 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/BrokenCircuitExceptionTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/BrokenCircuitExceptionTests.cs @@ -7,6 +7,7 @@ public class BrokenCircuitExceptionTests [Fact] public void Ctor_Ok() { + new BrokenCircuitException().Message.Should().Be("The circuit is now open and is not allowing calls."); new BrokenCircuitException("Dummy.").Message.Should().Be("Dummy."); new BrokenCircuitException("Dummy.", new InvalidOperationException()).Message.Should().Be("Dummy."); new BrokenCircuitException("Dummy.", new InvalidOperationException()).InnerException.Should().BeOfType(); diff --git a/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs b/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs index f69f4ca460..c7b086b078 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs @@ -124,6 +124,44 @@ public async Task OnActionPreExecute_CircuitOpenedByValue() GetBlockedTill(controller).Should().Be(_timeProvider.GetUtcNow() + _options.BreakDuration); } + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task OnActionPreExecute_CircuitOpened_EnsureExceptionStackTraceDoesNotGrow(bool innerException) + { + var stacks = new List(); + var context = ResilienceContextPool.Shared.Get(); + using var controller = CreateController(); + + await OpenCircuit( + controller, + innerException ? Outcome.FromException(new InvalidOperationException()) : Outcome.FromResult(99)); + + for (int i = 0; i < 100; i++) + { + try + { + (await controller.OnActionPreExecuteAsync(context)).Value.ThrowIfException(); + } + catch (BrokenCircuitException e) + { + stacks.Add(e.StackTrace!); + e.Message.Should().Be("The circuit is now open and is not allowing calls."); + + if (innerException) + { + e.InnerException.Should().BeOfType(); + } + else + { + e.InnerException.Should().BeNull(); + } + } + } + + stacks.Distinct().Should().HaveCount(1); + } + [Fact] public async Task HalfOpen_EnsureBreakDuration() {