diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c65c6ac074..275e5c968c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ * Speed-up page builds by [@martincostello](https://github.com/martincostello) in https://github.com/App-vNext/Polly/pull/1753 * Hedging strategy also deep-copies context for primary execution by [@martintmk](https://github.com/martintmk) in https://github.com/App-vNext/Polly/pull/1754 * [Docs] Add diagram about hedging's context and callbacks by [@peter-csala](https://github.com/peter-csala) in https://github.com/App-vNext/Polly/pull/1751 +* Resolve AOT compilation issues by [@martincostello](https://github.com/martincostello) in https://github.com/App-vNext/Polly/pull/1737 ## 8.0.0 diff --git a/Polly.sln b/Polly.sln index bf30ece2e28..7bb28d106d3 100644 --- a/Polly.sln +++ b/Polly.sln @@ -58,6 +58,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polly.Testing.Tests", "test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snippets", "src\Snippets\Snippets.csproj", "{D812B941-79B0-4E1E-BB70-4FAE345B5234}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polly.AotTest", "test\Polly.AotTest\Polly.AotTest.csproj", "{84091007-CFA5-4852-AC41-0171DF039C4E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -120,6 +122,10 @@ Global {D812B941-79B0-4E1E-BB70-4FAE345B5234}.Debug|Any CPU.Build.0 = Debug|Any CPU {D812B941-79B0-4E1E-BB70-4FAE345B5234}.Release|Any CPU.ActiveCfg = Release|Any CPU {D812B941-79B0-4E1E-BB70-4FAE345B5234}.Release|Any CPU.Build.0 = Release|Any CPU + {84091007-CFA5-4852-AC41-0171DF039C4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84091007-CFA5-4852-AC41-0171DF039C4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84091007-CFA5-4852-AC41-0171DF039C4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84091007-CFA5-4852-AC41-0171DF039C4E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -140,6 +146,7 @@ Global {9AD2D6AD-56E4-49D6-B6F1-EE975D5760B9} = {B7BF406B-B06F-4025-83E6-7219C53196A6} {D333B5CE-982D-4C11-BDAF-4217AA02306E} = {A6CC41B9-E0B9-44F8-916B-3E4A78DA3BFB} {D812B941-79B0-4E1E-BB70-4FAE345B5234} = {B7BF406B-B06F-4025-83E6-7219C53196A6} + {84091007-CFA5-4852-AC41-0171DF039C4E} = {A6CC41B9-E0B9-44F8-916B-3E4A78DA3BFB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2E5D54CD-770A-4345-B585-1848FC2EA6F4} diff --git a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.CompositeComponentBenchmark-report-github.md b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.CompositeComponentBenchmark-report-github.md index cfb46f674aa..66c1cb473ea 100644 --- a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.CompositeComponentBenchmark-report-github.md +++ b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.CompositeComponentBenchmark-report-github.md @@ -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 | 37.44 ns | 0.713 ns | 0.952 ns | - | diff --git a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.DelegatingComponentBenchmark-report-github.md b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.DelegatingComponentBenchmark-report-github.md new file mode 100644 index 00000000000..d8383548648 --- /dev/null +++ b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.DelegatingComponentBenchmark-report-github.md @@ -0,0 +1,15 @@ +``` + +BenchmarkDotNet v0.13.9+228a464e8be6c580ad9408e98f18813f6407fb5a, Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2) +12th Gen Intel Core i7-1270P, 1 CPU, 16 logical and 12 physical cores +.NET SDK 7.0.403 + [Host] : .NET 7.0.13 (7.0.1323.51816), X64 RyuJIT AVX2 + +Job=MediumRun Toolchain=InProcessEmitToolchain IterationCount=15 +LaunchCount=2 WarmupCount=10 + +``` +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|------------------------------------ |---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| +| DelegatingComponent_ExecuteCore_Jit | 29.40 ns | 0.699 ns | 1.025 ns | 1.00 | 0.00 | - | - | NA | +| DelegatingComponent_ExecuteCore_Aot | 34.65 ns | 0.627 ns | 0.919 ns | 1.18 | 0.05 | 0.0025 | 24 B | NA | diff --git a/bench/Polly.Core.Benchmarks/DelegatingComponentBenchmark.cs b/bench/Polly.Core.Benchmarks/DelegatingComponentBenchmark.cs new file mode 100644 index 00000000000..d2dec726b81 --- /dev/null +++ b/bench/Polly.Core.Benchmarks/DelegatingComponentBenchmark.cs @@ -0,0 +1,38 @@ +using Polly.Utils.Pipeline; + +namespace Polly.Core.Benchmarks; + +public class DelegatingComponentBenchmark : IAsyncDisposable +{ + private ResilienceContext? _context; + private DelegatingComponent? _component; + + [GlobalSetup] + public void Setup() + { + var first = PipelineComponent.Empty; + var second = PipelineComponent.Empty; + + _component = new DelegatingComponent(first) { Next = second }; + _context = ResilienceContextPool.Shared.Get(); + } + + public async ValueTask DisposeAsync() + { + if (_component is not null) + { + await _component.DisposeAsync().ConfigureAwait(false); + _component = null; + } + + GC.SuppressFinalize(this); + } + + [Benchmark(Baseline = true)] + public ValueTask> DelegatingComponent_ExecuteCore_Jit() + => _component!.ExecuteComponent((_, state) => Outcome.FromResultAsValueTask(state), _context!, 42); + + [Benchmark] + public ValueTask> DelegatingComponent_ExecuteCore_Aot() + => _component!.ExecuteComponentAot((_, state) => Outcome.FromResultAsValueTask(state), _context!, 42); +} diff --git a/build.cake b/build.cake index d69a2a96628..013c4260812 100644 --- a/build.cake +++ b/build.cake @@ -75,9 +75,9 @@ Task("__Clean") CleanDirectories(cleanDirectories); - foreach(var path in cleanDirectories) { EnsureDirectoryExists(path); } + foreach (var path in cleanDirectories) { EnsureDirectoryExists(path); } - foreach(var path in solutionPaths) + foreach (var path in solutionPaths) { Information("Cleaning {0}", path); @@ -93,7 +93,7 @@ Task("__Clean") Task("__RestoreNuGetPackages") .Does(() => { - foreach(var solution in solutions) + foreach (var solution in solutions) { Information("Restoring NuGet Packages for {0}", solution); DotNetRestore(solution.ToString()); @@ -103,7 +103,7 @@ Task("__RestoreNuGetPackages") Task("__BuildSolutions") .Does(() => { - foreach(var solution in solutions) + foreach (var solution in solutions) { Information("Building {0}", solution); @@ -125,6 +125,23 @@ Task("__BuildSolutions") } }); +Task("__ValidateAot") + .Does(() => +{ + var aotProject = MakeAbsolute(File("./test/Polly.AotTest/Polly.AotTest.csproj")); + var settings = new DotNetPublishSettings + { + Configuration = configuration, + Verbosity = DotNetVerbosity.Minimal, + MSBuildSettings = new DotNetMSBuildSettings + { + TreatAllWarningsAs = MSBuildTreatAllWarningsAs.Error, + }, + }; + + DotNetPublish(aotProject.ToString(), settings); +}); + Task("__RunTests") .Does(() => { @@ -137,7 +154,7 @@ Task("__RunTests") var projects = GetFiles("./test/**/*.csproj"); - foreach(var proj in projects) + foreach (var proj in projects) { DotNetTest(proj.FullPath, new DotNetTestSettings { @@ -252,6 +269,7 @@ Task("Build") .IsDependentOn("__RestoreNuGetPackages") .IsDependentOn("__ValidateDocs") .IsDependentOn("__BuildSolutions") + .IsDependentOn("__ValidateAot") .IsDependentOn("__RunTests") .IsDependentOn("__RunMutationTests") .IsDependentOn("__CreateNuGetPackages"); diff --git a/src/Polly.Core/Polly.Core.csproj b/src/Polly.Core/Polly.Core.csproj index e127f6701c6..6476f89a6eb 100644 --- a/src/Polly.Core/Polly.Core.csproj +++ b/src/Polly.Core/Polly.Core.csproj @@ -15,6 +15,7 @@ true true true + true true diff --git a/src/Polly.Core/Utils/Pipeline/CompositeComponent.cs b/src/Polly.Core/Utils/Pipeline/CompositeComponent.cs index abaa8692f4f..1a6c47a089b 100644 --- a/src/Polly.Core/Utils/Pipeline/CompositeComponent.cs +++ b/src/Polly.Core/Utils/Pipeline/CompositeComponent.cs @@ -116,37 +116,4 @@ private async ValueTask> ExecuteCoreWithTelemetry - /// A component that delegates the execution to the next component in the chain. - /// - private sealed class DelegatingComponent : PipelineComponent - { - private readonly PipelineComponent _component; - - public DelegatingComponent(PipelineComponent component) => _component = component; - - public PipelineComponent? Next { get; set; } - - internal override ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) - { - return _component.ExecuteCore( - static (context, state) => - { - if (context.CancellationToken.IsCancellationRequested) - { - return Outcome.FromExceptionAsValueTask(new OperationCanceledException(context.CancellationToken).TrySetStackTrace()); - } - - return state.Next!.ExecuteCore(state.callback, context, state.state); - }, - context, - (Next, callback, state)); - } - - public override ValueTask DisposeAsync() => default; - } } diff --git a/src/Polly.Core/Utils/Pipeline/DelegatingComponent.cs b/src/Polly.Core/Utils/Pipeline/DelegatingComponent.cs new file mode 100644 index 00000000000..1324cc8f45e --- /dev/null +++ b/src/Polly.Core/Utils/Pipeline/DelegatingComponent.cs @@ -0,0 +1,82 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Polly.Utils.Pipeline; + +/// +/// A component that delegates the execution to the next component in the chain. +/// +internal sealed class DelegatingComponent : PipelineComponent +{ + private readonly PipelineComponent _component; + + public DelegatingComponent(PipelineComponent component) => _component = component; + + public PipelineComponent? Next { get; set; } + + public override ValueTask DisposeAsync() => default; + + [ExcludeFromCodeCoverage] + internal override ValueTask> ExecuteCore( + Func>> 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> ExecuteNext( + PipelineComponent next, + Func>> callback, + ResilienceContext context, + TState state) + { + if (context.CancellationToken.IsCancellationRequested) + { + return Outcome.FromExceptionAsValueTask(new OperationCanceledException(context.CancellationToken).TrySetStackTrace()); + } + + return next.ExecuteCore(callback, context, state); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ValueTask> ExecuteComponent( + Func>> 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> ExecuteComponentAot( + Func>> 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. + return _component.ExecuteCore( + static (context, wrapper) => + { + var callback = (Func>>)wrapper.Callback; + var state = (TState)wrapper.State; + return ExecuteNext(wrapper.Next, callback, context, state); + }, + context, + new StateWrapper(Next!, callback, state!)); + } + + private readonly record struct StateWrapper(PipelineComponent Next, object Callback, object State); +#endif +} diff --git a/src/Polly.Core/Utils/Pipeline/PipelineComponent.cs b/src/Polly.Core/Utils/Pipeline/PipelineComponent.cs index 09993a66712..6eb898a424a 100644 --- a/src/Polly.Core/Utils/Pipeline/PipelineComponent.cs +++ b/src/Polly.Core/Utils/Pipeline/PipelineComponent.cs @@ -35,7 +35,7 @@ internal Outcome ExecuteCoreSync( public abstract ValueTask DisposeAsync(); - private class NullComponent : PipelineComponent + private sealed class NullComponent : PipelineComponent { internal override ValueTask> ExecuteCore(Func>> callback, ResilienceContext context, TState state) => callback(context, state); diff --git a/src/Polly.Extensions/Polly.Extensions.csproj b/src/Polly.Extensions/Polly.Extensions.csproj index a66e28d4989..429353154a5 100644 --- a/src/Polly.Extensions/Polly.Extensions.csproj +++ b/src/Polly.Extensions/Polly.Extensions.csproj @@ -13,6 +13,7 @@ true true true + true true diff --git a/src/Polly.RateLimiting/Polly.RateLimiting.csproj b/src/Polly.RateLimiting/Polly.RateLimiting.csproj index c02a013b4b3..97be9f209d4 100644 --- a/src/Polly.RateLimiting/Polly.RateLimiting.csproj +++ b/src/Polly.RateLimiting/Polly.RateLimiting.csproj @@ -13,6 +13,7 @@ true true true + true true diff --git a/test/Polly.AotTest/Polly.AotTest.csproj b/test/Polly.AotTest/Polly.AotTest.csproj new file mode 100644 index 00000000000..0f8b6725774 --- /dev/null +++ b/test/Polly.AotTest/Polly.AotTest.csproj @@ -0,0 +1,19 @@ + + + enable + Exe + true + true + net7.0 + + + + + + + + + + + + diff --git a/test/Polly.AotTest/Program.cs b/test/Polly.AotTest/Program.cs new file mode 100644 index 00000000000..6e6ab98ba6b --- /dev/null +++ b/test/Polly.AotTest/Program.cs @@ -0,0 +1 @@ +Console.WriteLine("Hello Polly!"); diff --git a/test/Polly.Core.Tests/Utils/Pipeline/DelegatingComponentTests.cs b/test/Polly.Core.Tests/Utils/Pipeline/DelegatingComponentTests.cs new file mode 100644 index 00000000000..e2500c6b66e --- /dev/null +++ b/test/Polly.Core.Tests/Utils/Pipeline/DelegatingComponentTests.cs @@ -0,0 +1,56 @@ +using Polly.Utils.Pipeline; + +namespace Polly.Core.Tests.Utils.Pipeline; + +public static class DelegatingComponentTests +{ + [Fact] + public static async Task ExecuteComponent_ReturnsCorrectResult() + { + await using var component = new CallbackComponent(); + var next = new CallbackComponent(); + var context = ResilienceContextPool.Shared.Get(); + var state = 1; + + await using var delegating = new DelegatingComponent(component) { Next = next }; + + var actual = await delegating.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 static async Task ExecuteComponentAot_ReturnsCorrectResult() + { + await using var component = new CallbackComponent(); + var next = new CallbackComponent(); + var context = ResilienceContextPool.Shared.Get(); + var state = 1; + + await using var delegating = new DelegatingComponent(component) { Next = next }; + + var actual = await delegating.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> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) => callback(context, state); + } +}