Skip to content

Commit 3c76e05

Browse files
authored
Allow to dispose linked resources on pipeline disposal (#1511)
1 parent 32a3390 commit 3c76e05

File tree

13 files changed

+253
-3
lines changed

13 files changed

+253
-3
lines changed

src/Polly.Core/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ Polly.PredicateBuilder<TResult>.PredicateBuilder() -> void
159159
Polly.PredicateResult
160160
Polly.Registry.ConfigureBuilderContext<TKey>
161161
Polly.Registry.ConfigureBuilderContext<TKey>.EnableReloads(System.Func<System.Func<System.Threading.CancellationToken>!>! tokenProducerFactory) -> void
162+
Polly.Registry.ConfigureBuilderContext<TKey>.OnPipelineDisposed(System.Action! callback) -> void
162163
Polly.Registry.ConfigureBuilderContext<TKey>.PipelineKey.get -> TKey
163164
Polly.Registry.ResiliencePipelineProvider<TKey>
164165
Polly.Registry.ResiliencePipelineProvider<TKey>.ResiliencePipelineProvider() -> void

src/Polly.Core/Registry/ConfigureBuilderContext.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ internal ConfigureBuilderContext(TKey strategyKey, string builderName, string? b
3333

3434
internal Func<Func<CancellationToken>>? ReloadTokenProducer { get; private set; }
3535

36+
internal List<Action> DisposeCallbacks { get; } = new();
37+
3638
/// <summary>
3739
/// Enables dynamic reloading of the strategy retrieved from <see cref="ResiliencePipelineRegistry{TKey}"/>.
3840
/// </summary>
@@ -48,4 +50,15 @@ public void EnableReloads(Func<Func<CancellationToken>> tokenProducerFactory)
4850

4951
ReloadTokenProducer = tokenProducerFactory;
5052
}
53+
54+
/// <summary>
55+
/// Registers a callback that is called when the pipeline instance being configured is disposed.
56+
/// </summary>
57+
/// <param name="callback">The callback delegate.</param>
58+
public void OnPipelineDisposed(Action callback)
59+
{
60+
Guard.NotNull(callback);
61+
62+
DisposeCallbacks.Add(callback);
63+
}
5164
}

src/Polly.Core/Registry/RegistryPipelineComponentBuilder.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,17 @@ private Builder CreateBuilder()
6060
_configure(builder, context);
6161

6262
return new(
63-
builder.BuildPipelineComponent,
63+
() => PipelineComponentFactory.WithDisposableCallbacks(
64+
builder.BuildPipelineComponent(),
65+
context.DisposeCallbacks),
6466
context.ReloadTokenProducer,
67+
context.DisposeCallbacks,
6568
builder.TelemetryListener);
6669
}
6770

6871
private record Builder(
6972
Func<PipelineComponent> ComponentFactory,
7073
Func<Func<CancellationToken>>? ReloadTokenProducer,
74+
List<Action> DisposeCallbacks,
7175
TelemetryListener? Listener);
7276
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
namespace Polly.Utils.Pipeline;
2+
3+
internal class ComponentWithDisposeCallbacks : PipelineComponent
4+
{
5+
private readonly List<Action> _callbacks;
6+
7+
public ComponentWithDisposeCallbacks(PipelineComponent component, List<Action> callbacks)
8+
{
9+
Component = component;
10+
_callbacks = callbacks;
11+
}
12+
13+
internal PipelineComponent Component { get; }
14+
15+
public override void Dispose()
16+
{
17+
ExecuteCallbacks();
18+
19+
Component.Dispose();
20+
}
21+
22+
public override ValueTask DisposeAsync()
23+
{
24+
ExecuteCallbacks();
25+
26+
return Component.DisposeAsync();
27+
}
28+
29+
internal override ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState>(
30+
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
31+
ResilienceContext context,
32+
TState state) => Component.ExecuteCore(callback, context, state);
33+
34+
private void ExecuteCallbacks()
35+
{
36+
foreach (var callback in _callbacks)
37+
{
38+
callback();
39+
}
40+
41+
_callbacks.Clear();
42+
}
43+
}

src/Polly.Core/Utils/Pipeline/PipelineComponentFactory.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using Polly.Telemetry;
4+
45
namespace Polly.Utils.Pipeline;
56

67
internal static class PipelineComponentFactory
@@ -13,6 +14,16 @@ internal static class PipelineComponentFactory
1314

1415
public static PipelineComponent FromStrategy<T>(ResilienceStrategy<T> strategy) => new BridgeComponent<T>(strategy);
1516

17+
public static PipelineComponent WithDisposableCallbacks(PipelineComponent component, IEnumerable<Action> callbacks)
18+
{
19+
if (!callbacks.Any())
20+
{
21+
return component;
22+
}
23+
24+
return new ComponentWithDisposeCallbacks(component, callbacks.ToList());
25+
}
26+
1627
public static PipelineComponent CreateComposite(
1728
IReadOnlyList<PipelineComponent> components,
1829
ResilienceStrategyTelemetry telemetry,

src/Polly.Extensions/DependencyInjection/AddResiliencePipelineContext.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.Extensions.DependencyInjection;
33
using Microsoft.Extensions.Options;
44
using Polly.Registry;
5+
using Polly.Utils;
56

67
namespace Polly.DependencyInjection;
78

@@ -63,4 +64,15 @@ internal AddResiliencePipelineContext(ConfigureBuilderContext<TKey> registryCont
6364

6465
return name == null ? monitor.CurrentValue : monitor.Get(name);
6566
}
67+
68+
/// <summary>
69+
/// Registers a callback that is called when the pipeline instance being configured is disposed.
70+
/// </summary>
71+
/// <param name="callback">The callback delegate.</param>
72+
public void OnPipelineDisposed(Action callback)
73+
{
74+
Guard.NotNull(callback);
75+
76+
RegistryContext.OnPipelineDisposed(callback);
77+
}
6678
}

src/Polly.Extensions/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ abstract Polly.Telemetry.MeteringEnricher.Enrich<TResult, TArgs>(in Polly.Teleme
33
Polly.DependencyInjection.AddResiliencePipelineContext<TKey>
44
Polly.DependencyInjection.AddResiliencePipelineContext<TKey>.EnableReloads<TOptions>(string? name = null) -> void
55
Polly.DependencyInjection.AddResiliencePipelineContext<TKey>.GetOptions<TOptions>(string? name = null) -> TOptions
6+
Polly.DependencyInjection.AddResiliencePipelineContext<TKey>.OnPipelineDisposed(System.Action! callback) -> void
67
Polly.DependencyInjection.AddResiliencePipelineContext<TKey>.PipelineKey.get -> TKey
78
Polly.DependencyInjection.AddResiliencePipelineContext<TKey>.ServiceProvider.get -> System.IServiceProvider!
89
Polly.PollyServiceCollectionExtensions

src/Polly.Testing/ResiliencePipelineExtensions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ private static object GetStrategyInstance<T>(PipelineComponent component)
6262
return component;
6363
}
6464

65-
private static bool ShouldSkip(object instance) => instance is ReloadableComponent;
65+
private static bool ShouldSkip(object instance) => instance is ReloadableComponent || instance is ComponentWithDisposeCallbacks;
6666

6767
private static void ExpandComponents(this PipelineComponent component, List<PipelineComponent> components)
6868
{
@@ -78,6 +78,10 @@ private static void ExpandComponents(this PipelineComponent component, List<Pipe
7878
components.Add(reloadable);
7979
ExpandComponents(reloadable.Component, components);
8080
}
81+
else if (component is ComponentWithDisposeCallbacks callbacks)
82+
{
83+
ExpandComponents(callbacks.Component, components);
84+
}
8185
else
8286
{
8387
components.Add(component);

test/Polly.Core.Tests/Registry/ResiliencePipelineRegistryTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,37 @@ public void EnableReloads_Ok()
285285
tries.Should().Be(retryCount + 1);
286286
}
287287

288+
[Fact]
289+
public void EnableReloads_EnsureDisposedCallbackCalled()
290+
{
291+
// arrange
292+
var registry = new ResiliencePipelineRegistry<string>();
293+
using var changeSource = new CancellationTokenSource();
294+
var disposedCalls = 0;
295+
296+
registry.TryAddBuilder("dummy", (builder, context) =>
297+
{
298+
// this call enables dynamic reloads for the dummy strategy
299+
context.EnableReloads(() => () => changeSource.Token);
300+
context.OnPipelineDisposed(() => disposedCalls++);
301+
builder.AddTimeout(TimeSpan.FromSeconds(1));
302+
});
303+
304+
// act
305+
var strategy = registry.GetPipeline("dummy");
306+
307+
// assert
308+
disposedCalls.Should().Be(0);
309+
strategy.Execute(() => { });
310+
311+
changeSource.Cancel();
312+
disposedCalls.Should().Be(1);
313+
strategy.Execute(() => { });
314+
315+
registry.Dispose();
316+
disposedCalls.Should().Be(2);
317+
}
318+
288319
[Fact]
289320
public void EnableReloads_Generic_Ok()
290321
{
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using NSubstitute;
2+
using Polly.Utils.Pipeline;
3+
4+
namespace Polly.Core.Tests.Utils.Pipeline;
5+
6+
public class ComponentWithDisposeCallbacksTests
7+
{
8+
[InlineData(true)]
9+
[InlineData(false)]
10+
[Theory]
11+
public async Task Dispose_Ok(bool isAsync)
12+
{
13+
// Arrange
14+
var called1 = 0;
15+
var called2 = 0;
16+
17+
var callbacks = new List<Action>
18+
{
19+
() => called1++,
20+
() => called2++
21+
};
22+
var component = Substitute.For<PipelineComponent>();
23+
var sut = new ComponentWithDisposeCallbacks(component, callbacks);
24+
25+
// Act
26+
if (isAsync)
27+
{
28+
await sut.DisposeAsync();
29+
await sut.DisposeAsync();
30+
await component.Received(2).DisposeAsync();
31+
}
32+
else
33+
{
34+
sut.Dispose();
35+
#pragma warning disable S3966 // Objects should not be disposed more than once
36+
sut.Dispose();
37+
#pragma warning restore S3966 // Objects should not be disposed more than once
38+
component.Received(2).Dispose();
39+
}
40+
41+
// Assert
42+
callbacks.Should().BeEmpty();
43+
called1.Should().Be(1);
44+
called2.Should().Be(1);
45+
}
46+
}

0 commit comments

Comments
 (0)