diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerManualControl.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerManualControl.cs index 7229380d12b..f45ef12c198 100644 --- a/src/Polly.Core/CircuitBreaker/CircuitBreakerManualControl.cs +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerManualControl.cs @@ -1,54 +1,52 @@ +using System.Collections.Generic; + namespace Polly.CircuitBreaker; /// /// Allows manual control of the circuit-breaker. /// +/// +/// The instance of this class can be reused across multiple circuit breakers. +/// public sealed class CircuitBreakerManualControl : IDisposable { - private Action? _onDispose; - private Func? _onIsolate; - private Func? _onReset; + private readonly HashSet _onDispose = new(); + private readonly HashSet> _onIsolate = new(); + private readonly HashSet> _onReset = new(); + private bool _isolated; internal void Initialize(Func onIsolate, Func onReset, Action onDispose) { - if (_onIsolate != null) + _onDispose.Add(onDispose); + _onIsolate.Add(onIsolate); + _onReset.Add(onReset); + + if (_isolated) { - throw new InvalidOperationException($"This instance of '{nameof(CircuitBreakerManualControl)}' is already initialized and cannot be used in a different circuit-breaker strategy."); - } + var context = ResilienceContext.Get().Initialize(isSynchronous: true); - _onDispose = onDispose; - _onIsolate = onIsolate; - _onReset = onReset; + // if the control indicates that circuit breaker should be isolated, we isolate it right away + IsolateAsync(context).GetAwaiter().GetResult(); + } } - /// - /// Gets a value indicating whether the manual control is initialized. - /// - /// - /// The initialization happens when the circuit-breaker strategy is attached to this class. - /// This happens when the final strategy is created by the call. - /// - internal bool IsInitialized => _onIsolate != null; - /// /// Isolates (opens) the circuit manually, and holds it in this state until a call to is made. /// /// The resilience context. /// The instance of that represents the asynchronous execution. /// Thrown when is . - /// Thrown when manual control is not initialized. /// Thrown when calling this method after this object is disposed. - internal Task IsolateAsync(ResilienceContext context) + internal async Task IsolateAsync(ResilienceContext context) { Guard.NotNull(context); - if (_onIsolate == null) + _isolated = true; + + foreach (var action in _onIsolate) { - throw new InvalidOperationException("The circuit-breaker manual control is not initialized"); + await action(context).ConfigureAwait(context.ContinueOnCapturedContext); } - - context.Initialize(isSynchronous: false); - return _onIsolate(context); } /// @@ -56,11 +54,10 @@ internal Task IsolateAsync(ResilienceContext context) /// /// The cancellation token. /// The instance of that represents the asynchronous execution. - /// Thrown if manual control is not initialized. /// Thrown when calling this method after this object is disposed. public async Task IsolateAsync(CancellationToken cancellationToken = default) { - var context = ResilienceContext.Get(); + var context = ResilienceContext.Get().Initialize(isSynchronous: false); context.CancellationToken = cancellationToken; try @@ -79,19 +76,19 @@ public async Task IsolateAsync(CancellationToken cancellationToken = default) /// The resilience context. /// The instance of that represents the asynchronous execution. /// Thrown when is . - /// Thrown if manual control is not initialized. /// Thrown when calling this method after this object is disposed. - internal Task CloseAsync(ResilienceContext context) + internal async Task CloseAsync(ResilienceContext context) { Guard.NotNull(context); - if (_onReset == null) - { - throw new InvalidOperationException("The circuit-breaker manual control is not initialized"); - } + _isolated = false; context.Initialize(isSynchronous: false); - return _onReset(context); + + foreach (var action in _onReset) + { + await action(context).ConfigureAwait(context.ContinueOnCapturedContext); + } } /// @@ -99,7 +96,6 @@ internal Task CloseAsync(ResilienceContext context) /// /// The cancellation token. /// The instance of that represents the asynchronous execution. - /// Thrown if manual control is not initialized. /// Thrown when calling this method after this object is disposed. public async Task CloseAsync(CancellationToken cancellationToken = default) { @@ -119,5 +115,15 @@ public async Task CloseAsync(CancellationToken cancellationToken = default) /// /// Disposes the current class. /// - public void Dispose() => _onDispose?.Invoke(); + public void Dispose() + { + foreach (var action in _onDispose) + { + action(); + } + + _onDispose.Clear(); + _onIsolate.Clear(); + _onReset.Clear(); + } } diff --git a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerManualControlTests.cs b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerManualControlTests.cs index 5dc7e1a5948..5cfe45d1867 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerManualControlTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerManualControlTests.cs @@ -4,46 +4,58 @@ namespace Polly.Core.Tests.CircuitBreaker; public class CircuitBreakerManualControlTests { - [Fact] - public void Ctor_Ok() + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task IsolateAsync_NotInitialized_Ok(bool closedAfter) { using var control = new CircuitBreakerManualControl(); + await control.IsolateAsync(); + if (closedAfter) + { + await control.CloseAsync(); + } - control.IsInitialized.Should().BeFalse(); - } + var isolated = false; - [Fact] - public async Task IsolateAsync_NotInitialized_Throws() - { - using var control = new CircuitBreakerManualControl(); + control.Initialize( + c => + { + c.IsSynchronous.Should().BeTrue(); + isolated = true; + return Task.CompletedTask; + }, + _ => Task.CompletedTask, + () => { }); - await control - .Invoking(c => c.IsolateAsync(CancellationToken.None)) - .Should() - .ThrowAsync(); + isolated.Should().Be(!closedAfter); } [Fact] - public async Task ResetAsync_NotInitialized_Throws() + public async Task ResetAsync_NotInitialized_Ok() { using var control = new CircuitBreakerManualControl(); await control .Invoking(c => c.CloseAsync(CancellationToken.None)) .Should() - .ThrowAsync(); + .NotThrowAsync(); } [Fact] - public void Initialize_Twice_Throws() + public async Task Initialize_Twice_Ok() { - using var control = new CircuitBreakerManualControl(); + int called = 0; + var control = new CircuitBreakerManualControl(); control.Initialize(_ => Task.CompletedTask, _ => Task.CompletedTask, () => { }); + control.Initialize(_ => { called++; return Task.CompletedTask; }, _ => { called++; return Task.CompletedTask; }, () => { called++; }); - control - .Invoking(c => c.Initialize(_ => Task.CompletedTask, _ => Task.CompletedTask, () => { })) - .Should() - .Throw(); + await control.IsolateAsync(); + await control.CloseAsync(); + + control.Dispose(); + + called.Should().Be(3); } [Fact] diff --git a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderTests.cs b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderTests.cs index cda5b215ee7..c1846248496 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderTests.cs @@ -179,4 +179,27 @@ public void AddAdvancedCircuitBreaker_IntegrationTest() halfOpened.Should().Be(2); closed.Should().Be(1); } + + [Fact] + public async Task AddCircuitBreakers_WithIsolatedManualControl_ShouldBeIsolated() + { + var manualControl = new CircuitBreakerManualControl(); + await manualControl.IsolateAsync(); + + var strategy1 = new ResilienceStrategyBuilder() + .AddAdvancedCircuitBreaker(new() { ManualControl = manualControl }) + .Build(); + + var strategy2 = new ResilienceStrategyBuilder() + .AddAdvancedCircuitBreaker(new() { ManualControl = manualControl }) + .Build(); + + strategy1.Invoking(s => s.Execute(() => { })).Should().Throw(); + strategy2.Invoking(s => s.Execute(() => { })).Should().Throw(); + + await manualControl.CloseAsync(); + + strategy1.Execute(() => { }); + strategy2.Execute(() => { }); + } } diff --git a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs index da0e69080c8..f3db53cda4f 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs @@ -54,8 +54,6 @@ public async Task Ctor_ManualControl_EnsureAttached() _options.ManualControl = new CircuitBreakerManualControl(); var strategy = Create(); - _options.ManualControl.IsInitialized.Should().BeTrue(); - await _options.ManualControl.IsolateAsync(CancellationToken.None); strategy.Invoking(s => s.Execute(_ => 0)).Should().Throw(); diff --git a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerStateProviderTests.cs b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerStateProviderTests.cs index 325e436d14b..70d5a46ae5b 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerStateProviderTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerStateProviderTests.cs @@ -29,7 +29,7 @@ public async Task ResetAsync_NotInitialized_Throws() await control .Invoking(c => c.CloseAsync(CancellationToken.None)) .Should() - .ThrowAsync(); + .NotThrowAsync(); } [Fact]