Skip to content

Commit 6f7edf8

Browse files
authored
Add TelemetrySource to ExecutionRejectedException (#2346)
Add `TelemetrySource` for use with `ExecutionRejectedException`.
1 parent ea15b52 commit 6f7edf8

File tree

12 files changed

+171
-43
lines changed

12 files changed

+171
-43
lines changed

src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ public ValueTask IsolateCircuitAsync(ResilienceContext context)
9595

9696
lock (_lock)
9797
{
98-
SetLastHandledOutcome_NeedsLock(Outcome.FromException<T>(new IsolatedCircuitException()));
98+
var exception = new IsolatedCircuitException();
99+
_telemetry.SetTelemetrySource(exception);
100+
SetLastHandledOutcome_NeedsLock(Outcome.FromException<T>(exception));
99101
OpenCircuitFor_NeedsLock(Outcome.FromResult<T>(default), TimeSpan.MaxValue, manual: true, context, out task);
100102
_circuitState = CircuitState.Isolated;
101103
}
@@ -123,7 +125,7 @@ public ValueTask CloseCircuitAsync(ResilienceContext context)
123125
{
124126
EnsureNotDisposed();
125127

126-
Exception? exception = null;
128+
BrokenCircuitException? exception = null;
127129
bool isHalfOpen = false;
128130

129131
Task? task = null;
@@ -157,6 +159,7 @@ public ValueTask CloseCircuitAsync(ResilienceContext context)
157159

158160
if (exception is not null)
159161
{
162+
_telemetry.SetTelemetrySource(exception);
160163
return Outcome.FromException<T>(exception);
161164
}
162165

@@ -308,11 +311,13 @@ private void SetLastHandledOutcome_NeedsLock(Outcome<T> outcome)
308311
private BrokenCircuitException CreateBrokenCircuitException()
309312
{
310313
TimeSpan retryAfter = _blockedUntil - _timeProvider.GetUtcNow();
311-
return _breakingException switch
314+
var exception = _breakingException switch
312315
{
313-
Exception exception => new BrokenCircuitException(BrokenCircuitException.DefaultMessage, retryAfter, exception),
316+
Exception ex => new BrokenCircuitException(BrokenCircuitException.DefaultMessage, retryAfter, ex),
314317
_ => new BrokenCircuitException(BrokenCircuitException.DefaultMessage, retryAfter)
315318
};
319+
_telemetry.SetTelemetrySource(exception);
320+
return exception;
316321
}
317322

318323
private void OpenCircuit_NeedsLock(Outcome<T> outcome, bool manual, ResilienceContext context, out Task? scheduledTask)

src/Polly.Core/ExecutionRejectedException.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.Runtime.Serialization;
33
#endif
44

5+
using Polly.Telemetry;
6+
57
namespace Polly;
68

79
/// <summary>
@@ -49,4 +51,9 @@ protected ExecutionRejectedException(SerializationInfo info, StreamingContext co
4951
}
5052
#endif
5153
#pragma warning restore RS0016 // Add public types and members to the declared API
54+
55+
/// <summary>
56+
/// Gets the source of the strategy which has thrown the exception, if known.
57+
/// </summary>
58+
public virtual ResilienceTelemetrySource? TelemetrySource { get; internal set; }
5259
}

src/Polly.Core/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ Polly.CircuitBreaker.BrokenCircuitException.BrokenCircuitException(string! messa
33
Polly.CircuitBreaker.BrokenCircuitException.BrokenCircuitException(string! message, System.TimeSpan retryAfter, System.Exception! inner) -> void
44
Polly.CircuitBreaker.BrokenCircuitException.BrokenCircuitException(System.TimeSpan retryAfter) -> void
55
Polly.CircuitBreaker.BrokenCircuitException.RetryAfter.get -> System.TimeSpan?
6+
virtual Polly.ExecutionRejectedException.TelemetrySource.get -> Polly.Telemetry.ResilienceTelemetrySource?
7+
Polly.Telemetry.ResilienceStrategyTelemetry.SetTelemetrySource(Polly.ExecutionRejectedException! exception) -> void

src/Polly.Core/Telemetry/ResilienceStrategyTelemetry.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.ComponentModel;
2+
13
namespace Polly.Telemetry;
24

35
/// <summary>
@@ -21,6 +23,18 @@ internal ResilienceStrategyTelemetry(ResilienceTelemetrySource source, Telemetry
2123

2224
internal ResilienceTelemetrySource TelemetrySource { get; }
2325

26+
/// <summary>
27+
/// Sets the source of the telemetry on the provided exception.
28+
/// </summary>
29+
/// <param name="exception">The to-be-set exception.</param>
30+
[EditorBrowsable(EditorBrowsableState.Never)]
31+
public void SetTelemetrySource(ExecutionRejectedException exception)
32+
{
33+
Guard.NotNull(exception);
34+
35+
exception.TelemetrySource = TelemetrySource;
36+
}
37+
2438
/// <summary>
2539
/// Reports an event that occurred in a resilience strategy.
2640
/// </summary>

src/Polly.Core/Timeout/TimeoutResilienceStrategy.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ protected internal override async ValueTask<Outcome<TResult>> ExecuteCore<TResul
7474
timeout,
7575
e);
7676

77+
_telemetry.SetTelemetrySource(timeoutException);
7778
return Outcome.FromException<TResult>(timeoutException.TrySetStackTrace());
7879
}
7980

src/Polly.RateLimiting/RateLimiterRejectedException.cs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public RateLimiterRejectedException(string message)
4444
/// <param name="message">The message that describes the error.</param>
4545
/// <param name="retryAfter">The retry after value.</param>
4646
public RateLimiterRejectedException(string message, TimeSpan retryAfter)
47-
: base(message) => RetryAfter = retryAfter;
47+
: base(message)
48+
=> RetryAfter = retryAfter;
4849

4950
/// <summary>
5051
/// Initializes a new instance of the <see cref="RateLimiterRejectedException"/> class.
@@ -63,7 +64,8 @@ public RateLimiterRejectedException(string message, Exception inner)
6364
/// <param name="retryAfter">The retry after value.</param>
6465
/// <param name="inner">The inner exception.</param>
6566
public RateLimiterRejectedException(string message, TimeSpan retryAfter, Exception inner)
66-
: base(message, inner) => RetryAfter = retryAfter;
67+
: base(message, inner)
68+
=> RetryAfter = retryAfter;
6769

6870
/// <summary>
6971
/// Gets the amount of time to wait before retrying again.
@@ -84,10 +86,10 @@ public RateLimiterRejectedException(string message, TimeSpan retryAfter, Excepti
8486
private RateLimiterRejectedException(SerializationInfo info, StreamingContext context)
8587
: base(info, context)
8688
{
87-
var value = info.GetDouble("RetryAfter");
88-
if (value >= 0.0)
89+
var retryAfter = info.GetDouble(nameof(RetryAfter));
90+
if (retryAfter >= 0.0)
8991
{
90-
RetryAfter = TimeSpan.FromSeconds(value);
92+
RetryAfter = TimeSpan.FromSeconds(retryAfter);
9193
}
9294
}
9395

@@ -96,14 +98,7 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont
9698
{
9799
Guard.NotNull(info);
98100

99-
if (RetryAfter.HasValue)
100-
{
101-
info.AddValue("RetryAfter", RetryAfter.Value.TotalSeconds);
102-
}
103-
else
104-
{
105-
info.AddValue("RetryAfter", -1.0);
106-
}
101+
info.AddValue(nameof(RetryAfter), RetryAfter.HasValue ? RetryAfter.Value.TotalSeconds : -1.0);
107102

108103
base.GetObjectData(info, context);
109104
}

src/Polly.RateLimiting/RateLimiterResilienceStrategy.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ protected override async ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState
6565
await OnLeaseRejected(new OnRateLimiterRejectedArguments(context, lease)).ConfigureAwait(context.ContinueOnCapturedContext);
6666
}
6767

68-
var exception = retryAfter.HasValue ? new RateLimiterRejectedException(retryAfter.Value) : new RateLimiterRejectedException();
68+
var exception = retryAfter is not null
69+
? new RateLimiterRejectedException(retryAfterValue)
70+
: new RateLimiterRejectedException();
71+
72+
_telemetry.SetTelemetrySource(exception);
6973

7074
return Outcome.FromException<TResult>(exception.TrySetStackTrace());
7175
}

test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ public async Task IsolateAsync_Ok()
5252
called.Should().BeTrue();
5353

5454
var outcome = await controller.OnActionPreExecuteAsync(ResilienceContextPool.Shared.Get());
55-
outcome.Value.Exception.Should().BeOfType<IsolatedCircuitException>()
56-
.And.Subject.As<IsolatedCircuitException>().RetryAfter.Should().BeNull();
55+
var exception = outcome.Value.Exception.Should().BeOfType<IsolatedCircuitException>().Subject;
56+
exception.RetryAfter.Should().BeNull();
57+
exception.TelemetrySource.Should().NotBeNull();
5758

5859
// now close it
5960
await controller.CloseCircuitAsync(ResilienceContextPool.Shared.Get());
@@ -119,8 +120,9 @@ public async Task OnActionPreExecute_CircuitOpenedByValue()
119120
using var controller = CreateController();
120121

121122
await OpenCircuit(controller, Outcome.FromResult(99));
122-
var error = (BrokenCircuitException)(await controller.OnActionPreExecuteAsync(ResilienceContextPool.Shared.Get())).Value.Exception!;
123-
error.Should().BeOfType<BrokenCircuitException>().And.Subject.As<BrokenCircuitException>().RetryAfter.Should().NotBeNull();
123+
var exception = (BrokenCircuitException)(await controller.OnActionPreExecuteAsync(ResilienceContextPool.Shared.Get())).Value.Exception!;
124+
exception.RetryAfter.Should().NotBeNull();
125+
exception.TelemetrySource.Should().NotBeNull();
124126

125127
GetBlockedTill(controller).Should().Be(_timeProvider.GetUtcNow() + _options.BreakDuration);
126128
}
@@ -149,6 +151,7 @@ await OpenCircuit(
149151
stacks.Add(e.StackTrace!);
150152
e.Message.Should().Be("The circuit is now open and is not allowing calls.");
151153
e.RetryAfter.Should().NotBeNull();
154+
e.TelemetrySource.Should().NotBeNull();
152155

153156
if (innerException)
154157
{
@@ -206,9 +209,10 @@ public async Task OnActionPreExecute_CircuitOpenedByException()
206209
using var controller = CreateController();
207210

208211
await OpenCircuit(controller, Outcome.FromException<int>(new InvalidOperationException()));
209-
var error = (BrokenCircuitException)(await controller.OnActionPreExecuteAsync(ResilienceContextPool.Shared.Get())).Value.Exception!;
210-
error.InnerException.Should().BeOfType<InvalidOperationException>();
211-
error.RetryAfter.Should().NotBeNull();
212+
var exception = (BrokenCircuitException)(await controller.OnActionPreExecuteAsync(ResilienceContextPool.Shared.Get())).Value.Exception!;
213+
exception.InnerException.Should().BeOfType<InvalidOperationException>();
214+
exception.RetryAfter.Should().NotBeNull();
215+
exception.TelemetrySource.Should().NotBeNull();
212216
}
213217

214218
[Fact]
@@ -261,9 +265,11 @@ public async Task OnActionPreExecute_HalfOpen()
261265
// act
262266
await controller.OnActionPreExecuteAsync(ResilienceContextPool.Shared.Get());
263267
var error = (await controller.OnActionPreExecuteAsync(ResilienceContextPool.Shared.Get())).Value.Exception;
264-
error.Should().BeOfType<BrokenCircuitException>().And.Subject.As<BrokenCircuitException>().RetryAfter.Should().NotBeNull();
265268

266269
// assert
270+
var exception = error.Should().BeOfType<BrokenCircuitException>().Subject;
271+
exception.RetryAfter.Should().NotBeNull();
272+
exception.TelemetrySource.Should().NotBeNull();
267273
controller.CircuitState.Should().Be(CircuitState.HalfOpen);
268274
called.Should().BeTrue();
269275
}
@@ -465,7 +471,9 @@ public async Task OnActionFailureAsync_VoidResult_EnsureBreakingExceptionNotSet(
465471
// assert
466472
controller.LastException.Should().BeNull();
467473
var outcome = await controller.OnActionPreExecuteAsync(ResilienceContextPool.Shared.Get());
468-
outcome.Value.Exception.Should().BeOfType<BrokenCircuitException>().And.Subject.As<BrokenCircuitException>().RetryAfter.Should().NotBeNull();
474+
var exception = outcome.Value.Exception.Should().BeOfType<BrokenCircuitException>().Subject;
475+
exception.RetryAfter.Should().NotBeNull();
476+
exception.TelemetrySource.Should().NotBeNull();
469477
}
470478

471479
[Fact]
@@ -501,8 +509,9 @@ public async Task Flow_Closed_HalfOpen_Open_HalfOpen_Closed()
501509
TimeSpan advanceTimeRejected = TimeSpan.FromMilliseconds(1);
502510
AdvanceTime(advanceTimeRejected);
503511
var outcome = await controller.OnActionPreExecuteAsync(ResilienceContextPool.Shared.Get());
504-
outcome.Value.Exception.Should().BeOfType<BrokenCircuitException>()
505-
.And.Subject.As<BrokenCircuitException>().RetryAfter.Should().Be(_options.BreakDuration - advanceTimeRejected);
512+
var exception = outcome.Value.Exception.Should().BeOfType<BrokenCircuitException>().Subject;
513+
exception.RetryAfter.Should().Be(_options.BreakDuration - advanceTimeRejected);
514+
exception.TelemetrySource.Should().NotBeNull();
506515

507516
// wait and try, transition to half open
508517
AdvanceTime(_options.BreakDuration + _options.BreakDuration);

test/Polly.Core.Tests/Telemetry/ResilienceStrategyTelemetryTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Polly.Telemetry;
2+
using Polly.Timeout;
23

34
namespace Polly.Core.Tests.Telemetry;
45

@@ -94,4 +95,25 @@ public void Report_NoListener_ShouldNotThrow()
9495
.Should()
9596
.NotThrow();
9697
}
98+
99+
[Fact]
100+
public void SetTelemetrySource_Ok()
101+
{
102+
var sut = new ResilienceStrategyTelemetry(_source, null);
103+
var exception = new TimeoutRejectedException();
104+
105+
sut.SetTelemetrySource(exception);
106+
107+
exception.TelemetrySource.Should().Be(_source);
108+
}
109+
110+
[Fact]
111+
public void SetTelemetrySource_ShouldThrow()
112+
{
113+
ExecutionRejectedException? exception = null;
114+
115+
_sut.Invoking(s => s.SetTelemetrySource(exception!))
116+
.Should()
117+
.Throw<ArgumentNullException>();
118+
}
97119
}

test/Polly.Core.Tests/Timeout/TimeoutResilienceStrategyTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,27 @@ public async Task Execute_Timeout_EnsureStackTrace()
173173
}
174174
}
175175

176+
[Fact]
177+
public async Task Execute_Timeout_EnsureTelemetrySource()
178+
{
179+
SetTimeout(TimeSpan.FromSeconds(2));
180+
var sut = CreateSut();
181+
182+
var outcome = await sut.ExecuteOutcomeAsync(
183+
async (c, _) =>
184+
{
185+
var delay = _timeProvider.Delay(TimeSpan.FromSeconds(4), c.CancellationToken);
186+
_timeProvider.Advance(TimeSpan.FromSeconds(2));
187+
await delay;
188+
189+
return Outcome.FromResult("dummy");
190+
},
191+
ResilienceContextPool.Shared.Get(),
192+
"state");
193+
194+
outcome.Exception.Should().BeOfType<TimeoutRejectedException>().Subject.TelemetrySource.Should().NotBeNull();
195+
}
196+
176197
[Fact]
177198
public async Task Execute_Cancelled_EnsureNoTimeout()
178199
{

0 commit comments

Comments
 (0)