From c21d536ddaa3c4497bedcd5760bd62399a1b1af4 Mon Sep 17 00:00:00 2001 From: martintmk Date: Mon, 3 Apr 2023 16:54:39 +0200 Subject: [PATCH] Introduce OutcomeEvent --- .../Retry/RetryStrategyOptionsTests.cs | 3 + .../Strategy/OnRetryArgumentsTests.cs | 15 +++ .../Strategy/OutcomeEventTests.cs | 125 ++++++++++++++++++ .../Strategy/SimpleEventTests.cs | 68 ++++++++++ .../Timeout/OnTimeoutEventTests.cs | 56 +------- src/Polly.Core.Tests/Utils/TestArguments.cs | 10 ++ src/Polly.Core/Retry/OnRetryArguments.cs | 28 ++++ src/Polly.Core/Retry/OnRetryEvent.cs | 10 ++ src/Polly.Core/Retry/RetryStrategyOptions.cs | 9 ++ .../Strategy/OutcomeEvent.Handler.cs | 89 +++++++++++++ src/Polly.Core/Strategy/OutcomeEvent.cs | 99 ++++++++++++++ src/Polly.Core/Strategy/SimpleEvent.cs | 78 +++++++++++ src/Polly.Core/Timeout/OnTimeoutEvent.cs | 71 +--------- 13 files changed, 537 insertions(+), 124 deletions(-) create mode 100644 src/Polly.Core.Tests/Strategy/OnRetryArgumentsTests.cs create mode 100644 src/Polly.Core.Tests/Strategy/OutcomeEventTests.cs create mode 100644 src/Polly.Core.Tests/Strategy/SimpleEventTests.cs create mode 100644 src/Polly.Core.Tests/Utils/TestArguments.cs create mode 100644 src/Polly.Core/Retry/OnRetryArguments.cs create mode 100644 src/Polly.Core/Retry/OnRetryEvent.cs create mode 100644 src/Polly.Core/Strategy/OutcomeEvent.Handler.cs create mode 100644 src/Polly.Core/Strategy/OutcomeEvent.cs create mode 100644 src/Polly.Core/Strategy/SimpleEvent.cs diff --git a/src/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs b/src/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs index aca9027fecb..7d387798262 100644 --- a/src/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs +++ b/src/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs @@ -14,5 +14,8 @@ public void Ctor_Ok() options.RetryDelayGenerator.Should().NotBeNull(); options.RetryDelayGenerator.IsEmpty.Should().BeTrue(); + + options.OnRetry.Should().NotBeNull(); + options.OnRetry.IsEmpty.Should().BeTrue(); } } diff --git a/src/Polly.Core.Tests/Strategy/OnRetryArgumentsTests.cs b/src/Polly.Core.Tests/Strategy/OnRetryArgumentsTests.cs new file mode 100644 index 00000000000..8378b293162 --- /dev/null +++ b/src/Polly.Core.Tests/Strategy/OnRetryArgumentsTests.cs @@ -0,0 +1,15 @@ +using Polly.Retry; + +namespace Polly.Core.Tests.Strategy; + +public class OnRetryArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new OnRetryArguments(ResilienceContext.Get(), 2); + + args.Context.Should().NotBeNull(); + args.Attempt.Should().Be(2); + } +} diff --git a/src/Polly.Core.Tests/Strategy/OutcomeEventTests.cs b/src/Polly.Core.Tests/Strategy/OutcomeEventTests.cs new file mode 100644 index 00000000000..0fa72b80dfc --- /dev/null +++ b/src/Polly.Core.Tests/Strategy/OutcomeEventTests.cs @@ -0,0 +1,125 @@ +using System; +using System.Threading.Tasks; +using Polly.Strategy; + +namespace Polly.Core.Tests.Strategy; + +public class OutcomeEventTests +{ + private readonly DummyEvent _sut = new(); + + [Fact] + public void Empty_Ok() + { + _sut.IsEmpty.Should().BeTrue(); + + _sut.Add(() => { }); + + _sut.IsEmpty.Should().BeFalse(); + } + + [Fact] + public void CreateHandler_Empty_ReturnsNull() + { + _sut.CreateHandler().Should().BeNull(); + } + + public static readonly TheoryData> Data = new() + { + sut => + { + bool called = false; + sut.Add((_, _) => { called = true; }); + InvokeHandler(sut, new Outcome(0)); + called.Should().BeTrue(); + }, + sut => + { + bool called = false; + sut.Add(_ => { called = true; }); + InvokeHandler(sut, new Outcome(0)); + called.Should().BeTrue(); + }, + sut => + { + bool called = false; + sut.Add(() => { called = true; }); + InvokeHandler(sut, new Outcome(0)); + called.Should().BeTrue(); + }, + sut => + { + bool called = false; + sut.Add((_, _) => { called = true; return default; }); + InvokeHandler(sut, new Outcome(0)); + called.Should().BeTrue(); + }, + sut => + { + int calls = 0; + sut.Add((_, _) => { calls++; }); + sut.Add((_, _) => { calls++; }); + + InvokeHandler(sut, new Outcome(0)); + + calls.Should().Be(2); + }, + sut => + { + bool called = false; + sut.Add((_, _) => { called = true; return default; }); + sut.Add((_, _) => { called = true; return default; }); + InvokeHandler(sut, new Outcome(true)); + called.Should().BeFalse(); + }, + sut => + { + bool called = false; + sut.Add((_, _) => { called = true; return default; }); + InvokeHandler(sut, new Outcome(true)); + called.Should().BeFalse(); + }, + }; + + [MemberData(nameof(Data))] + [Theory] + public void InvokeCallbacks_Ok(Action callback) + { + _sut.Invoking(s => callback(s)).Should().NotThrow(); + callback(_sut); + } + + [Fact] + public void AddCallback_DifferentResultType_NotInvoked() + { + var callbacks = new List(); + + for (var i = 0; i < 10; i++) + { + var index = i; + + _sut.Add((_, _) => + { + callbacks.Add(index); + }); + + _sut.Add((_, _) => + { + callbacks.Add(index); + }); + } + + InvokeHandler(_sut, new Outcome(1)); + + callbacks.Distinct().Should().HaveCount(10); + } + + private static void InvokeHandler(DummyEvent sut, Outcome outcome) + { + sut.CreateHandler()!.Handle(outcome, new TestArguments()).AsTask().Wait(); + } + + public sealed class DummyEvent : OutcomeEvent + { + } +} diff --git a/src/Polly.Core.Tests/Strategy/SimpleEventTests.cs b/src/Polly.Core.Tests/Strategy/SimpleEventTests.cs new file mode 100644 index 00000000000..45440b7fdda --- /dev/null +++ b/src/Polly.Core.Tests/Strategy/SimpleEventTests.cs @@ -0,0 +1,68 @@ +using Polly.Timeout; + +namespace Polly.Core.Tests.Timeout; + +public class SimpleEventTests +{ + [Fact] + public async Task Add_EnsureOrdering() + { + var ev = new DummyEvent(); + List raisedEvents = new List(); + + ev.Add(() => raisedEvents.Add(1)); + ev.Add(args => raisedEvents.Add(2)); + ev.Add(args => { raisedEvents.Add(3); return default; }); + + var handler = ev.CreateHandler()!; + + await handler(new TestArguments()); + + raisedEvents.Should().HaveCount(3); + raisedEvents.Should().BeInAscendingOrder(); + } + + [Fact] + public void Add_EnsureValidation() + { + var ev = new DummyEvent(); + + Assert.Throws(() => ev.Add((Action)null!)); + Assert.Throws(() => ev.Add((Action)null!)); + Assert.Throws(() => ev.Add(null!)); + } + + [InlineData(1)] + [InlineData(2)] + [InlineData(100)] + [Theory] + public async Task CreateHandler_Ok(int count) + { + var ev = new DummyEvent(); + var events = new List(); + + for (int i = 0; i < count; i++) + { + ev.Add(() => events.Add(i)); + } + + await ev.CreateHandler()!(new TestArguments()); + + events.Should().HaveCount(count); + events.Should().BeInAscendingOrder(); + } + + [Fact] + public void CreateHandler_NoEvents_Null() + { + var ev = new DummyEvent(); + + var handler = ev.CreateHandler(); + + handler.Should().BeNull(); + } + + private class DummyEvent : SimpleEvent + { + } +} diff --git a/src/Polly.Core.Tests/Timeout/OnTimeoutEventTests.cs b/src/Polly.Core.Tests/Timeout/OnTimeoutEventTests.cs index cf5a6431596..a71b490064a 100644 --- a/src/Polly.Core.Tests/Timeout/OnTimeoutEventTests.cs +++ b/src/Polly.Core.Tests/Timeout/OnTimeoutEventTests.cs @@ -5,60 +5,8 @@ namespace Polly.Core.Tests.Timeout; public class OnTimeoutEventTests { [Fact] - public async Task Add_EnsureOrdering() + public void Ctor_Ok() { - var ev = new OnTimeoutEvent(); - List raisedEvents = new List(); - - ev.Add(() => raisedEvents.Add(1)); - ev.Add(args => raisedEvents.Add(2)); - ev.Add(args => { raisedEvents.Add(3); return default; }); - - var handler = ev.CreateHandler()!; - - await handler(TimeoutTestUtils.OnTimeoutArguments()); - - raisedEvents.Should().HaveCount(3); - raisedEvents.Should().BeInAscendingOrder(); - } - - [Fact] - public void Add_EnsureValidation() - { - var ev = new OnTimeoutEvent(); - - Assert.Throws(() => ev.Add((Action)null!)); - Assert.Throws(() => ev.Add((Action)null!)); - Assert.Throws(() => ev.Add(null!)); - } - - [InlineData(1)] - [InlineData(2)] - [InlineData(100)] - [Theory] - public async Task CreateHandler_Ok(int count) - { - var ev = new OnTimeoutEvent(); - var events = new List(); - - for (int i = 0; i < count; i++) - { - ev.Add(() => events.Add(i)); - } - - await ev.CreateHandler()!(TimeoutTestUtils.OnTimeoutArguments()); - - events.Should().HaveCount(count); - events.Should().BeInAscendingOrder(); - } - - [Fact] - public void CreateHandler_NoEvents_Null() - { - var ev = new OnTimeoutEvent(); - - var handler = ev.CreateHandler(); - - handler.Should().BeNull(); + this.Invoking(_ => new OnTimeoutEvent()).Should().NotThrow(); } } diff --git a/src/Polly.Core.Tests/Utils/TestArguments.cs b/src/Polly.Core.Tests/Utils/TestArguments.cs new file mode 100644 index 00000000000..c1d4d0166c4 --- /dev/null +++ b/src/Polly.Core.Tests/Utils/TestArguments.cs @@ -0,0 +1,10 @@ +using Polly.Strategy; + +namespace Polly.Core.Tests.Utils; + +public class TestArguments : IResilienceArguments +{ + public TestArguments() => Context = ResilienceContext.Get(); + + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Retry/OnRetryArguments.cs b/src/Polly.Core/Retry/OnRetryArguments.cs new file mode 100644 index 00000000000..af04edd63d6 --- /dev/null +++ b/src/Polly.Core/Retry/OnRetryArguments.cs @@ -0,0 +1,28 @@ +using Polly.Strategy; + +namespace Polly.Retry; + +#pragma warning disable CA1815 // Override equals and operator equals on value types + +/// +/// Represents the arguments used in for handling the retry event. +/// +public readonly struct OnRetryArguments : IResilienceArguments +{ + internal OnRetryArguments(ResilienceContext context, int attempt) + { + Attempt = attempt; + Context = context; + } + + /// + /// Gets the zero-based attempt number. + /// + /// + /// The first attempt is 0, the second attempt is 1, and so on. + /// + public int Attempt { get; } + + /// + public ResilienceContext Context { get; } +} diff --git a/src/Polly.Core/Retry/OnRetryEvent.cs b/src/Polly.Core/Retry/OnRetryEvent.cs new file mode 100644 index 00000000000..7057350dbd4 --- /dev/null +++ b/src/Polly.Core/Retry/OnRetryEvent.cs @@ -0,0 +1,10 @@ +using Polly.Strategy; + +namespace Polly.Retry; + +/// +/// This class holds the user-callbacks that are invoked on retries. +/// +public sealed class OnRetryEvent : OutcomeEvent +{ +} diff --git a/src/Polly.Core/Retry/RetryStrategyOptions.cs b/src/Polly.Core/Retry/RetryStrategyOptions.cs index da1e346ec04..570d5cfc7e8 100644 --- a/src/Polly.Core/Retry/RetryStrategyOptions.cs +++ b/src/Polly.Core/Retry/RetryStrategyOptions.cs @@ -24,4 +24,13 @@ public class RetryStrategyOptions /// [Required] public RetryDelayGenerator RetryDelayGenerator { get; set; } = new(); + + /// + /// Gets or sets the instance that is invoked when retry happens. + /// + /// + /// By default, the event is empty and no callbacks are registered. + /// + [Required] + public OnRetryEvent OnRetry { get; set; } = new(); } diff --git a/src/Polly.Core/Strategy/OutcomeEvent.Handler.cs b/src/Polly.Core/Strategy/OutcomeEvent.Handler.cs new file mode 100644 index 00000000000..3487e07f564 --- /dev/null +++ b/src/Polly.Core/Strategy/OutcomeEvent.Handler.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; + +namespace Polly.Strategy; + +#pragma warning disable CA1034 // Nested types should not be visible + +public abstract partial class OutcomeEvent +{ + /// + /// The handler for this event. + /// + public abstract class Handler + { + private protected Handler() + { + } + + /// + /// Invokes all registered callbacks. + /// + /// The result type to invoke a callback for. + /// The operation outcome. + /// The arguments passed to the registered callbacks. + /// The . + public abstract ValueTask Handle(Outcome outcome, TArgs args); + } + + private sealed class TypeHandler : Handler + { + private readonly Type _type; + private readonly object[] _callbacks; + + public TypeHandler(Type type, List callbacks) + { + _type = type; + _callbacks = callbacks.ToArray(); + } + + public override ValueTask Handle(Outcome outcome, TArgs args) + { + if (typeof(TResult) != _type) + { + return default; + } + + return HandleCoreAsync(outcome, args); + } + + private ValueTask HandleCoreAsync(Outcome outcome, TArgs args) + { + if (_callbacks.Length == 1) + { + var predicate = (Func, TArgs, ValueTask>)_callbacks[0]; + + return predicate(outcome, args); + } + + return ExecutePredicatesAsync(outcome, args); + } + + private async ValueTask ExecutePredicatesAsync(Outcome outcome, TArgs args) + { + foreach (var obj in _callbacks) + { + var predicate = (Func, TArgs, ValueTask>)obj; + + await predicate(outcome, args).ConfigureAwait(args.Context.ContinueOnCapturedContext); + } + } + } + + private sealed class TypesHandler : Handler + { + private readonly Dictionary _handlers; + + public TypesHandler(IEnumerable>> callbacks) + => _handlers = callbacks.ToDictionary(v => v.Key, v => new TypeHandler(v.Key, v.Value)); + + public override ValueTask Handle(Outcome outcome, TArgs args) + { + if (_handlers.TryGetValue(typeof(TResult), out var handler)) + { + return handler.Handle(outcome, args); + } + + return default; + } + } +} diff --git a/src/Polly.Core/Strategy/OutcomeEvent.cs b/src/Polly.Core/Strategy/OutcomeEvent.cs new file mode 100644 index 00000000000..40427a4b6ef --- /dev/null +++ b/src/Polly.Core/Strategy/OutcomeEvent.cs @@ -0,0 +1,99 @@ +using System; +using Polly.Strategy; + +namespace Polly.Strategy; + +/// +/// The base class for events that use and in the registered event callbacks. +/// +/// The type of arguments the event uses. +/// The class that implements . +public abstract partial class OutcomeEvent + where TArgs : IResilienceArguments + where TSelf : OutcomeEvent +{ + private readonly Dictionary> _callbacks = new(); + + /// + /// Gets a value indicating whether the predicate is empty. + /// + public bool IsEmpty => _callbacks.Count == 0; + + /// + /// Adds a callback for the specified result type. + /// + /// The result type to add a callback for. + /// The event callback associated with the result type. + /// The current updated instance. + public TSelf Add(Action callback) + { + Guard.NotNull(callback); + + return Add((_, _) => { callback(); return default; }); + } + + /// + /// Adds a callback for the specified result type. + /// + /// The result type to add a callback for. + /// The event callback associated with the result type. + /// The current updated instance. + public TSelf Add(Action> callback) + { + Guard.NotNull(callback); + + return Add((outcome, _) => { callback(outcome); return default; }); + } + + /// + /// Adds a callback for the specified result type. + /// + /// The result type to add a callback for. + /// The event callback associated with the result type. + /// The current updated instance. + public TSelf Add(Action, TArgs> callback) + { + Guard.NotNull(callback); + + return Add((outcome, args) => { callback(outcome, args); return default; }); + } + + /// + /// Adds a callback for the specified result type. + /// + /// The result type to add a callback for. + /// The event callback associated with the result type. + /// The current updated instance. + public TSelf Add(Func, TArgs, ValueTask> callback) + { + Guard.NotNull(callback); + + if (!_callbacks.TryGetValue(typeof(TResult), out var predicates)) + { + predicates = new List { callback }; + _callbacks.Add(typeof(TResult), predicates); + } + else + { + predicates.Add(callback); + } + + return (TSelf)this; + } + + /// + /// Creates a handler that invokes the registered event callbacks. + /// + /// Handler instance or null if no callbacks are registered. + protected internal Handler? CreateHandler() + { + var pairs = _callbacks.ToArray(); + + return pairs.Length switch + { + 0 => null, + 1 => new TypeHandler(pairs[0].Key, pairs[0].Value), + _ => new TypesHandler(pairs) + }; + } +} diff --git a/src/Polly.Core/Strategy/SimpleEvent.cs b/src/Polly.Core/Strategy/SimpleEvent.cs new file mode 100644 index 00000000000..15bd4049594 --- /dev/null +++ b/src/Polly.Core/Strategy/SimpleEvent.cs @@ -0,0 +1,78 @@ +using Polly.Strategy; + +namespace Polly.Timeout; + +/// +/// This class holds a list of callbacks that are invoked when some event occurs. +/// The callbacks are executed for all result types and do not require . +/// +/// This class supports registering multiple event callbacks. +/// The registered callbacks are executed one-by-one in the same order as they were registered. +/// The type of arguments the event uses. +/// The class that implements . +public abstract class SimpleEvent + where TArgs : IResilienceArguments + where TSelf : SimpleEvent +{ + private readonly List> _callbacks = new(); + + /// + /// Adds an asynchronous event callback. + /// + /// The specified callback. + /// This instance. + public TSelf Add(Func callback) + { + Guard.NotNull(callback); + + _callbacks.Add(callback); + return (TSelf)this; + } + + /// + /// Adds a synchronous event callback. + /// + /// The specified callback. + /// This instance. + public TSelf Add(Action callback) + { + Guard.NotNull(callback); + + _callbacks.Add(args => { callback(args); return default; }); + return (TSelf)this; + } + + /// + /// Adds a synchronous event callback. + /// + /// The specified callback. + /// This instance. + public TSelf Add(Action callback) + { + Guard.NotNull(callback); + + _callbacks.Add(_ => { callback(); return default; }); + return (TSelf)this; + } + + internal Func? CreateHandler() + { + return _callbacks.Count switch + { + 0 => null, + 1 => _callbacks[0], + _ => CreateHandler(_callbacks.ToArray()) + }; + } + + private static Func CreateHandler(Func[] callbacks) + { + return async args => + { + foreach (var callback in callbacks) + { + await callback(args).ConfigureAwait(args.Context.ContinueOnCapturedContext); + } + }; + } +} diff --git a/src/Polly.Core/Timeout/OnTimeoutEvent.cs b/src/Polly.Core/Timeout/OnTimeoutEvent.cs index d0aa0b148b4..e1825c2aacb 100644 --- a/src/Polly.Core/Timeout/OnTimeoutEvent.cs +++ b/src/Polly.Core/Timeout/OnTimeoutEvent.cs @@ -5,75 +5,6 @@ namespace Polly.Timeout; /// /// This class supports registering multiple on-timeout callbacks. /// The registered callbacks are executed one-by-one in the same order as they were registered. -public sealed class OnTimeoutEvent +public sealed class OnTimeoutEvent : SimpleEvent { - private readonly List> _events = new(); - - /// - /// Adds an asynchronous on-timeout callback. - /// - /// The specified callback. - /// This instance. - public OnTimeoutEvent Add(Func onTimeout) - { - Guard.NotNull(onTimeout); - - _events.Add(onTimeout); - return this; - } - - /// - /// Adds a synchronous on-timeout callback. - /// - /// The specified callback. - /// This instance. - public OnTimeoutEvent Add(Action onTimeout) - { - Guard.NotNull(onTimeout); - - _events.Add(args => - { - onTimeout(args); - return default; - }); - return this; - } - - /// - /// Adds a synchronous on-timeout callback. - /// - /// The specified callback. - /// This instance. - public OnTimeoutEvent Add(Action onTimeout) - { - Guard.NotNull(onTimeout); - - _events.Add(_ => - { - onTimeout(); - return default; - }); - return this; - } - - internal Func? CreateHandler() - { - return _events.Count switch - { - 0 => null, - 1 => _events[0], - _ => CreateHandler(_events.ToArray()) - }; - } - - private static Func CreateHandler(Func[] callbacks) - { - return async args => - { - foreach (var callback in callbacks) - { - await callback(args).ConfigureAwait(args.Context.ContinueOnCapturedContext); - } - }; - } }