Skip to content

Commit 47aef87

Browse files
authored
Introduce ExecutionAttemptArguments (#1326)
1 parent 91e5384 commit 47aef87

33 files changed

+476
-58
lines changed

bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.MultipleStrategiesBenchmark-report-github.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ Job=MediumRun Toolchain=InProcessEmitToolchain IterationCount=15
99
LaunchCount=2 WarmupCount=10
1010

1111
```
12-
| Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio |
13-
|------------------------------------- |---------:|----------:|----------:|------:|-------:|----------:|------------:|
14-
| ExecuteStrategyPipeline_V7 | 2.269 μs | 0.0136 μs | 0.0204 μs | 1.00 | 0.1106 | 2824 B | 1.00 |
15-
| ExecuteStrategyPipeline_V8 | 1.861 μs | 0.0111 μs | 0.0155 μs | 0.82 | - | 40 B | 0.01 |
16-
| ExecuteStrategyPipeline_Telemetry_V8 | 2.402 μs | 0.0104 μs | 0.0156 μs | 1.06 | - | 40 B | 0.01 |
12+
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
13+
|------------------------------------- |---------:|----------:|----------:|------:|--------:|-------:|----------:|------------:|
14+
| ExecuteStrategyPipeline_V7 | 2.220 μs | 0.0164 μs | 0.0236 μs | 1.00 | 0.00 | 0.1106 | 2824 B | 1.00 |
15+
| ExecuteStrategyPipeline_V8 | 1.901 μs | 0.0089 μs | 0.0127 μs | 0.86 | 0.01 | - | 40 B | 0.01 |
16+
| ExecuteStrategyPipeline_Telemetry_V8 | 2.947 μs | 0.0077 μs | 0.0115 μs | 1.33 | 0.02 | - | 40 B | 0.01 |

src/Polly.Core/Hedging/Controller/HedgingController.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Polly.Hedging.Controller;
2+
using Polly.Telemetry;
23

34
namespace Polly.Hedging.Utils;
45

@@ -10,6 +11,7 @@ internal sealed class HedgingController<T>
1011
private int _rentedExecutions;
1112

1213
public HedgingController(
14+
ResilienceStrategyTelemetry telemetry,
1315
TimeProvider provider,
1416
HedgingHandler<T> handler,
1517
int maxAttempts)
@@ -20,7 +22,7 @@ public HedgingController(
2022
_executionPool = new ObjectPool<TaskExecution<T>>(() =>
2123
{
2224
Interlocked.Increment(ref _rentedExecutions);
23-
return new TaskExecution<T>(handler, pool);
25+
return new TaskExecution<T>(handler, pool, provider, telemetry);
2426
},
2527
_ =>
2628
{

src/Polly.Core/Hedging/Controller/TaskExecution.cs

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using System;
12
using Polly.Hedging.Utils;
3+
using Polly.Telemetry;
24

35
namespace Polly.Hedging.Controller;
46

@@ -21,15 +23,21 @@ internal sealed class TaskExecution<T>
2123
{
2224
private readonly ResilienceContext _cachedContext = ResilienceContext.Get();
2325
private readonly CancellationTokenSourcePool _cancellationTokenSourcePool;
26+
private readonly TimeProvider _timeProvider;
27+
private readonly ResilienceStrategyTelemetry _telemetry;
2428
private readonly HedgingHandler<T> _handler;
2529
private CancellationTokenSource? _cancellationSource;
2630
private CancellationTokenRegistration? _cancellationRegistration;
2731
private ResilienceContext? _activeContext;
32+
private long _startExecutionTimestamp;
33+
private long _stopExecutionTimestamp;
2834

29-
public TaskExecution(HedgingHandler<T> handler, CancellationTokenSourcePool cancellationTokenSourcePool)
35+
public TaskExecution(HedgingHandler<T> handler, CancellationTokenSourcePool cancellationTokenSourcePool, TimeProvider timeProvider, ResilienceStrategyTelemetry telemetry)
3036
{
3137
_handler = handler;
3238
_cancellationTokenSourcePool = cancellationTokenSourcePool;
39+
_timeProvider = timeProvider;
40+
_telemetry = telemetry;
3341
}
3442

3543
/// <summary>
@@ -56,6 +64,10 @@ public TaskExecution(HedgingHandler<T> handler, CancellationTokenSourcePool canc
5664

5765
public Action<TaskExecution<T>>? OnReset { get; set; }
5866

67+
public TimeSpan ExecutionTime => _timeProvider.GetElapsedTime(_startExecutionTimestamp, _stopExecutionTimestamp);
68+
69+
public int Attempt { get; private set; }
70+
5971
public void AcceptOutcome()
6072
{
6173
if (ExecutionTaskSafe?.IsCompleted == true)
@@ -83,8 +95,10 @@ public async ValueTask<bool> InitializeAsync<TResult, TState>(
8395
TState state,
8496
int attempt)
8597
{
98+
Attempt = attempt;
8699
Type = type;
87100
_cancellationSource = _cancellationTokenSourcePool.Get(System.Threading.Timeout.InfiniteTimeSpan);
101+
_startExecutionTimestamp = _timeProvider.GetTimestamp();
88102
Properties.Replace(snapshot.OriginalProperties);
89103

90104
if (snapshot.OriginalCancellationToken.CanBeCanceled)
@@ -109,6 +123,7 @@ public async ValueTask<bool> InitializeAsync<TResult, TState>(
109123
}
110124
catch (Exception e)
111125
{
126+
_stopExecutionTimestamp = _timeProvider.GetTimestamp();
112127
ExecutionTaskSafe = ExecuteCreateActionException<TResult>(e);
113128
return true;
114129
}
@@ -164,9 +179,12 @@ public async ValueTask ResetAsync()
164179
IsHandled = false;
165180
Properties.Clear();
166181
OnReset = null;
182+
Attempt = 0;
167183
_activeContext = null;
168184
_cachedContext.Reset();
169185
_cancellationSource = null!;
186+
_startExecutionTimestamp = 0;
187+
_stopExecutionTimestamp = 0;
170188
}
171189

172190
private async Task ExecuteSecondaryActionAsync<TResult>(Func<ValueTask<Outcome<TResult>>> action)
@@ -182,6 +200,7 @@ private async Task ExecuteSecondaryActionAsync<TResult>(Func<ValueTask<Outcome<T
182200
outcome = new Outcome<TResult>(e);
183201
}
184202

203+
_stopExecutionTimestamp = _timeProvider.GetTimestamp();
185204
await UpdateOutcomeAsync(outcome).ConfigureAwait(Context.ContinueOnCapturedContext);
186205
}
187206

@@ -203,6 +222,7 @@ private async Task ExecutePrimaryActionAsync<TResult, TState>(Func<ResilienceCon
203222
outcome = new Outcome<TResult>(e);
204223
}
205224

225+
_stopExecutionTimestamp = _timeProvider.GetTimestamp();
206226
await UpdateOutcomeAsync(outcome).ConfigureAwait(Context.ContinueOnCapturedContext);
207227
}
208228

@@ -211,6 +231,7 @@ private async Task UpdateOutcomeAsync<TResult>(Outcome<TResult> outcome)
211231
var args = new OutcomeArguments<TResult, HedgingPredicateArguments>(Context, outcome, new HedgingPredicateArguments());
212232
Outcome = outcome.AsOutcome();
213233
IsHandled = await _handler.ShouldHandle.HandleAsync(args).ConfigureAwait(Context.ContinueOnCapturedContext);
234+
TelemetryUtil.ReportExecutionAttempt(_telemetry, Context, outcome, Attempt, ExecutionTime, IsHandled);
214235
}
215236

216237
private void PrepareContext(ref ContextSnapshot snapshot)

src/Polly.Core/Hedging/HedgingResilienceStrategy.cs

+7-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace Polly.Hedging;
77

88
internal sealed class HedgingResilienceStrategy<T> : ResilienceStrategy
99
{
10+
private readonly TimeProvider _timeProvider;
1011
private readonly ResilienceStrategyTelemetry _telemetry;
1112
private readonly HedgingController<T> _controller;
1213

@@ -22,11 +23,12 @@ public HedgingResilienceStrategy(
2223
HedgingDelay = hedgingDelay;
2324
MaxHedgedAttempts = maxHedgedAttempts;
2425
HedgingDelayGenerator = hedgingDelayGenerator;
26+
_timeProvider = timeProvider;
2527
HedgingHandler = hedgingHandler;
2628
OnHedging = onHedging;
2729

2830
_telemetry = telemetry;
29-
_controller = new HedgingController<T>(timeProvider, HedgingHandler, maxHedgedAttempts);
31+
_controller = new HedgingController<T>(telemetry, timeProvider, HedgingHandler, maxHedgedAttempts);
3032
}
3133

3234
public TimeSpan HedgingDelay { get; }
@@ -75,7 +77,7 @@ private async ValueTask<Outcome<TResult>> ExecuteCoreAsync<TResult, TState>(
7577
attempt++;
7678
var continueOnCapturedContext = context.ContinueOnCapturedContext;
7779
var cancellationToken = context.CancellationToken;
78-
80+
var start = _timeProvider.GetTimestamp();
7981
if (cancellationToken.IsCancellationRequested)
8082
{
8183
return new Outcome<TResult>(new OperationCanceledException(cancellationToken).TrySetStackTrace());
@@ -97,7 +99,7 @@ private async ValueTask<Outcome<TResult>> ExecuteCoreAsync<TResult, TState>(
9799
await HandleOnHedgingAsync(
98100
context,
99101
new Outcome<TResult>(default(TResult)),
100-
new OnHedgingArguments(attempt, HasOutcome: false)).ConfigureAwait(context.ContinueOnCapturedContext);
102+
new OnHedgingArguments(attempt, HasOutcome: false, ExecutionTime: delay)).ConfigureAwait(context.ContinueOnCapturedContext);
101103
continue;
102104
}
103105

@@ -109,10 +111,11 @@ await HandleOnHedgingAsync(
109111
return outcome;
110112
}
111113

114+
var executionTime = _timeProvider.GetElapsedTime(start);
112115
await HandleOnHedgingAsync(
113116
context,
114117
outcome,
115-
new OnHedgingArguments(attempt, HasOutcome: true)).ConfigureAwait(context.ContinueOnCapturedContext);
118+
new OnHedgingArguments(attempt, HasOutcome: true, executionTime)).ConfigureAwait(context.ContinueOnCapturedContext);
116119
}
117120
}
118121

src/Polly.Core/Hedging/OnHedgingArguments.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ namespace Polly.Hedging;
88
/// Determines whether the outcome is available before loading the next hedged task.
99
/// No outcome indicates that the previous action did not finish within the hedging delay.
1010
/// </param>
11-
public record OnHedgingArguments(int Attempt, bool HasOutcome);
11+
/// <param name="ExecutionTime">
12+
/// The execution time of hedging attempt or the hedging delay
13+
/// in case the attempt was not finished in time.
14+
/// </param>
15+
public record OnHedgingArguments(int Attempt, bool HasOutcome, TimeSpan ExecutionTime);

src/Polly.Core/Retry/OnRetryArguments.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ namespace Polly.Retry;
55
/// </summary>
66
/// <param name="Attempt">The zero-based attempt number. The first attempt is 0, the second attempt is 1, and so on.</param>
77
/// <param name="RetryDelay">The delay before the next retry.</param>
8-
public record OnRetryArguments(int Attempt, TimeSpan RetryDelay);
8+
/// <param name="ExecutionTime">The execution time of this attempt.</param>
9+
public record OnRetryArguments(int Attempt, TimeSpan RetryDelay, TimeSpan ExecutionTime);

src/Polly.Core/Retry/RetryResilienceStrategy.cs

+7-2
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,15 @@ protected internal override async ValueTask<Outcome<TResult>> ExecuteCoreAsync<T
5757

5858
while (true)
5959
{
60+
var startTimestamp = _timeProvider.GetTimestamp();
6061
var outcome = await ExecuteCallbackSafeAsync(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext);
6162
var shouldRetryArgs = new OutcomeArguments<TResult, RetryPredicateArguments>(context, outcome, new RetryPredicateArguments(attempt));
63+
var handle = await ShouldRetry.HandleAsync(shouldRetryArgs).ConfigureAwait(context.ContinueOnCapturedContext);
64+
var executionTime = _timeProvider.GetElapsedTime(startTimestamp);
6265

63-
if (context.CancellationToken.IsCancellationRequested || IsLastAttempt(attempt) || !await ShouldRetry.HandleAsync(shouldRetryArgs).ConfigureAwait(context.ContinueOnCapturedContext))
66+
TelemetryUtil.ReportExecutionAttempt(_telemetry, context, outcome, attempt, executionTime, handle);
67+
68+
if (context.CancellationToken.IsCancellationRequested || IsLastAttempt(attempt) || !handle)
6469
{
6570
return outcome;
6671
}
@@ -76,7 +81,7 @@ protected internal override async ValueTask<Outcome<TResult>> ExecuteCoreAsync<T
7681
}
7782
}
7883

79-
var onRetryArgs = new OutcomeArguments<TResult, OnRetryArguments>(context, outcome, new OnRetryArguments(attempt, delay));
84+
var onRetryArgs = new OutcomeArguments<TResult, OnRetryArguments>(context, outcome, new OnRetryArguments(attempt, delay, executionTime));
8085
_telemetry.Report(RetryConstants.OnRetryEvent, onRetryArgs);
8186

8287
if (OnRetry is not null)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Polly.Telemetry;
2+
3+
public partial class ExecutionAttemptArguments
4+
{
5+
private static readonly ObjectPool<ExecutionAttemptArguments> Pool = new(() => new ExecutionAttemptArguments(), args =>
6+
{
7+
args.ExecutionTime = TimeSpan.Zero;
8+
args.Attempt = 0;
9+
args.Handled = false;
10+
});
11+
12+
internal static ExecutionAttemptArguments Get(int attempt, TimeSpan executionTime, bool handled)
13+
{
14+
var args = Pool.Get();
15+
args.Attempt = attempt;
16+
args.ExecutionTime = executionTime;
17+
args.Handled = handled;
18+
return args;
19+
}
20+
21+
internal static void Return(ExecutionAttemptArguments args) => Pool.Return(args);
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace Polly.Telemetry;
2+
3+
/// <summary>
4+
/// Arguments that encapsulate the execution attempt for retries or hedging.
5+
/// </summary>
6+
public partial class ExecutionAttemptArguments
7+
{
8+
/// <summary>
9+
/// Initializes a new instance of the <see cref="ExecutionAttemptArguments"/> class.
10+
/// </summary>
11+
/// <param name="attempt">The execution attempt.</param>
12+
/// <param name="executionTime">The execution time.</param>
13+
/// <param name="handled">Determines whether the attempt was handled by the strategy.</param>
14+
public ExecutionAttemptArguments(int attempt, TimeSpan executionTime, bool handled)
15+
{
16+
Attempt = attempt;
17+
ExecutionTime = executionTime;
18+
Handled = handled;
19+
}
20+
21+
private ExecutionAttemptArguments()
22+
{
23+
}
24+
25+
/// <summary>
26+
/// Gets the attempt number.
27+
/// </summary>
28+
public int Attempt { get; private set; }
29+
30+
/// <summary>
31+
/// Gets the execution time of the attempt.
32+
/// </summary>
33+
public TimeSpan ExecutionTime { get; private set; }
34+
35+
/// <summary>
36+
/// Gets a value indicating whether the outcome was handled by retry or hedging strategy.
37+
/// </summary>
38+
public bool Handled { get; private set; }
39+
}

src/Polly.Core/Telemetry/ResilienceStrategyTelemetry.cs

+19-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ internal ResilienceStrategyTelemetry(ResilienceTelemetrySource source, Diagnosti
1818

1919
internal ResilienceTelemetrySource TelemetrySource { get; }
2020

21+
/// <summary>
22+
/// Gets a value indicating whether telemetry is enabled.
23+
/// </summary>
24+
public bool IsEnabled => DiagnosticSource is not null;
25+
2126
/// <summary>
2227
/// Reports an event that occurred in a resilience strategy.
2328
/// </summary>
@@ -31,7 +36,7 @@ public void Report<TArgs>(string eventName, ResilienceContext context, TArgs arg
3136
Guard.NotNull(eventName);
3237
Guard.NotNull(context);
3338

34-
context.AddResilienceEvent(new ResilienceEvent(eventName));
39+
AddResilienceEvent(eventName, context, args);
3540

3641
if (DiagnosticSource is null || !DiagnosticSource.IsEnabled(eventName))
3742
{
@@ -57,7 +62,7 @@ public void Report<TArgs, TResult>(string eventName, OutcomeArguments<TResult, T
5762
{
5863
Guard.NotNull(eventName);
5964

60-
args.Context.AddResilienceEvent(new ResilienceEvent(eventName));
65+
AddResilienceEvent(eventName, args.Context, args.Arguments);
6166

6267
if (DiagnosticSource is null || !DiagnosticSource.IsEnabled(eventName))
6368
{
@@ -70,5 +75,17 @@ public void Report<TArgs, TResult>(string eventName, OutcomeArguments<TResult, T
7075

7176
TelemetryEventArguments.Return(telemetryArgs);
7277
}
78+
79+
private static void AddResilienceEvent<TArgs>(string eventName, ResilienceContext context, TArgs args)
80+
{
81+
// ExecutionAttemptArguments is not reported as resilience event because that information is already contained
82+
// in OnHedgingArguments and OnRetryArguments
83+
if (args is ExecutionAttemptArguments attempt)
84+
{
85+
return;
86+
}
87+
88+
context.AddResilienceEvent(new ResilienceEvent(eventName));
89+
}
7390
}
7491

src/Polly.Core/Telemetry/TelemetryUtil.cs

+20
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ internal static class TelemetryUtil
44
{
55
internal const string PollyDiagnosticSource = "Polly";
66

7+
internal const string ExecutionAttempt = "ExecutionAttempt";
8+
79
internal static readonly ResiliencePropertyKey<string> StrategyKey = new("Polly.StrategyKey");
810

911
public static ResilienceStrategyTelemetry CreateTelemetry(
@@ -17,4 +19,22 @@ public static ResilienceStrategyTelemetry CreateTelemetry(
1719

1820
return new ResilienceStrategyTelemetry(telemetrySource, diagnosticSource);
1921
}
22+
23+
public static void ReportExecutionAttempt<TResult>(
24+
ResilienceStrategyTelemetry telemetry,
25+
ResilienceContext context,
26+
Outcome<TResult> outcome,
27+
int attempt,
28+
TimeSpan executionTime,
29+
bool handled)
30+
{
31+
if (!telemetry.IsEnabled)
32+
{
33+
return;
34+
}
35+
36+
var attemptArgs = ExecutionAttemptArguments.Get(attempt, executionTime, handled);
37+
telemetry.Report<ExecutionAttemptArguments, TResult>(ExecutionAttempt, new(context, outcome, attemptArgs));
38+
ExecutionAttemptArguments.Return(attemptArgs);
39+
}
2040
}

src/Polly.Core/Utils/TimeProvider.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
13
namespace Polly.Utils;
24

35
#pragma warning disable S3872 // Parameter names should not duplicate the names of their methods
@@ -6,6 +8,7 @@ namespace Polly.Utils;
68
/// TEMPORARY ONLY, to be replaced with System.TimeProvider - https://github.com/dotnet/runtime/issues/36617 later.
79
/// </summary>
810
/// <remarks>We trimmed some of the API that's not relevant for us too.</remarks>
11+
[ExcludeFromCodeCoverage]
912
internal abstract class TimeProvider
1013
{
1114
private readonly double _tickFrequency;
@@ -22,7 +25,7 @@ protected TimeProvider(long timestampFrequency)
2225

2326
public long TimestampFrequency { get; }
2427

25-
public abstract long GetTimestamp();
28+
public virtual long GetTimestamp() => Environment.TickCount;
2629

2730
public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) => new((long)((endingTimestamp - startingTimestamp) * _tickFrequency));
2831

0 commit comments

Comments
 (0)