Skip to content

Commit

Permalink
Introduce AoT and non-AoT branches
Browse files Browse the repository at this point in the history
- Re-introduce branching for handling AoT and non-AoT to remove the allocation impact for non-AoT scenarios.
- Refactor the code to allow for unit testing both branches and increasing code re-use.
  • Loading branch information
martincostello committed Oct 29, 2023
1 parent 24857b0 commit 671b450
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ LaunchCount=2 WarmupCount=10
```
| Method | Mean | Error | StdDev | Allocated |
|------------------------------- |---------:|---------:|---------:|----------:|
| CompositeComponent_ExecuteCore | 44.37 ns | 1.994 ns | 2.923 ns | - |
| CompositeComponent_ExecuteCore | 41.37 ns | 1.722 ns | 2.413 ns | - |
56 changes: 48 additions & 8 deletions src/Polly.Core/Utils/Pipeline/CompositeComponent.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Polly.Telemetry;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Polly.Telemetry;

namespace Polly.Utils.Pipeline;

Expand Down Expand Up @@ -120,7 +122,7 @@ private async ValueTask<Outcome<TResult>> ExecuteCoreWithTelemetry<TResult, TSta
/// <summary>
/// A component that delegates the execution to the next component in the chain.
/// </summary>
private sealed class DelegatingComponent : PipelineComponent
internal sealed class DelegatingComponent : PipelineComponent
{
private readonly PipelineComponent _component;

Expand All @@ -130,11 +132,53 @@ private sealed class DelegatingComponent : PipelineComponent

public override ValueTask DisposeAsync() => default;

[ExcludeFromCodeCoverage]
internal override ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState>(
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
ResilienceContext context,
TState state)
{
#if NET6_0_OR_GREATER
return RuntimeFeature.IsDynamicCodeSupported ? ExecuteComponent(callback, context, state) : ExecuteComponentAot(callback, context, state);
#else
return ExecuteComponent(callback, context, state);
#endif
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ValueTask<Outcome<TResult>> ExecuteNext<TResult, TState>(
PipelineComponent next,
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
ResilienceContext context,
TState state)
{
if (context.CancellationToken.IsCancellationRequested)
{
return Outcome.FromExceptionAsValueTask<TResult>(new OperationCanceledException(context.CancellationToken).TrySetStackTrace());
}

return next.ExecuteCore(callback, context, state);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ValueTask<Outcome<TResult>> ExecuteComponent<TResult, TState>(
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
ResilienceContext context,
TState state)
{
return _component.ExecuteCore(
static (context, state) => ExecuteNext(state.Next!, state.callback, context, state.state),
context,
(Next, callback, state));
}

#if NET6_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ValueTask<Outcome<TResult>> ExecuteComponentAot<TResult, TState>(
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
ResilienceContext context,
TState state)
{
// Custom state object is used to cast the callback and state to prevent infinite
// generic type recursion warning IL3054 when referenced in a native AoT application.
// See https://github.com/App-vNext/Polly/issues/1732 for further context.
Expand All @@ -143,12 +187,7 @@ internal override ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState>(
{
var callback = (Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>>)wrapper.Callback;
var state = (TState)wrapper.State;
if (context.CancellationToken.IsCancellationRequested)
{
return Outcome.FromExceptionAsValueTask<TResult>(new OperationCanceledException(context.CancellationToken).TrySetStackTrace());
}

return wrapper.Next.ExecuteCore(callback, context, state);
return ExecuteNext(wrapper.Next, callback, context, state);
},
context,
new StateWrapper(Next!, callback, state!));
Expand All @@ -167,5 +206,6 @@ public StateWrapper(PipelineComponent next, object callback, object state)
public object Callback;
public object State;
}
#endif
}
}
2 changes: 1 addition & 1 deletion src/Polly.Core/Utils/Pipeline/PipelineComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ internal Outcome<TResult> ExecuteCoreSync<TResult, TState>(

public abstract ValueTask DisposeAsync();

private class NullComponent : PipelineComponent
private sealed class NullComponent : PipelineComponent
{
internal override ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState>(Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback, ResilienceContext context, TState state)
=> callback(context, state);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,62 @@ public async Task DisposeAsync_EnsureInnerComponentsDisposed()
await b.Received(1).DisposeAsync();
}

[Fact]
public async Task ExecuteComponent_ReturnsCorrectResult()
{
var component = new CallbackComponent();
var next = new CallbackComponent();
var context = ResilienceContextPool.Shared.Get();
var state = 1;

var composite = new CompositeComponent.DelegatingComponent(component)
{
Next = next,
};

var actual = await composite.ExecuteComponent(
async static (_, state) => await Outcome.FromResultAsValueTask(state + 1),
context,
state);

actual.Should().NotBeNull();
actual.Result.Should().Be(2);
}

#if NET6_0_OR_GREATER
[Fact]
public async Task ExecuteComponentAot_ReturnsCorrectResult()
{
var component = new CallbackComponent();
var next = new CallbackComponent();
var context = ResilienceContextPool.Shared.Get();
var state = 1;

var composite = new CompositeComponent.DelegatingComponent(component)
{
Next = next,
};

var actual = await composite.ExecuteComponentAot(
async static (_, state) => await Outcome.FromResultAsValueTask(state + 1),
context,
state);

actual.Should().NotBeNull();
actual.Result.Should().Be(2);
}
#endif

private sealed class CallbackComponent : PipelineComponent
{
public override ValueTask DisposeAsync() => default;

internal override ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState>(
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
ResilienceContext context,
TState state) => callback(context, state);
}

private CompositeComponent CreateSut(PipelineComponent[] components, TimeProvider? timeProvider = null)
{
return (CompositeComponent)PipelineComponentFactory.CreateComposite(components, _telemetry, timeProvider ?? Substitute.For<TimeProvider>());
Expand Down

0 comments on commit 671b450

Please sign in to comment.