Skip to content

Commit f0605ac

Browse files
matticoclaudejamescrosswell
authored
feat: Add exponential backoff and log deduplication to Spotlight (#5025)
* feat: Add exponential backoff and log deduplication to Spotlight transport When the Spotlight server is unreachable, the SDK now logs the error only once and implements exponential backoff (1s initial, 60s max) before retrying, per the Spotlight error handling spec. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: James Crosswell <james.crosswell@gmail.com>
1 parent b72a798 commit f0605ac

3 files changed

Lines changed: 375 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features ✨
6+
7+
- feat: Add exponential backoff and log deduplication to Spotlight transport by @mattico in [#5025](https://github.com/getsentry/sentry-dotnet/pull/5025)
8+
39
## 6.6.0
410

511
### Features ✨

src/Sentry/Http/SpotlightHttpTransport.cs

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ internal class SpotlightHttpTransport : HttpTransport
1212
private readonly HttpClient _httpClient;
1313
private readonly Uri _spotlightUrl;
1414
private readonly ISystemClock _clock;
15+
private readonly ExponentialBackoff _backoff;
1516

1617
public SpotlightHttpTransport(ITransport inner, SentryOptions options, HttpClient httpClient, Uri spotlightUrl, ISystemClock clock)
1718
: base(options, httpClient)
@@ -21,6 +22,7 @@ public SpotlightHttpTransport(ITransport inner, SentryOptions options, HttpClien
2122
_spotlightUrl = spotlightUrl;
2223
_inner = inner;
2324
_clock = clock;
25+
_backoff = new ExponentialBackoff(clock);
2426
}
2527

2628
protected internal override HttpRequestMessage CreateRequest(Envelope envelope)
@@ -38,23 +40,79 @@ public override async Task SendEnvelopeAsync(Envelope envelope, CancellationToke
3840
{
3941
var sentryTask = _inner.SendEnvelopeAsync(envelope, cancellationToken);
4042

41-
try
43+
if (_backoff.ShouldAttempt())
4244
{
43-
// Send to spotlight
44-
using var processedEnvelope = ProcessEnvelope(envelope);
45-
if (processedEnvelope.Items.Count > 0)
45+
try
4646
{
47-
using var request = CreateRequest(processedEnvelope);
48-
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
49-
await HandleResponseAsync(response, processedEnvelope, cancellationToken).ConfigureAwait(false);
47+
// Send to spotlight
48+
using var processedEnvelope = ProcessEnvelope(envelope);
49+
if (processedEnvelope.Items.Count > 0)
50+
{
51+
using var request = CreateRequest(processedEnvelope);
52+
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
53+
await HandleResponseAsync(response, processedEnvelope, cancellationToken).ConfigureAwait(false);
54+
55+
_backoff.RecordSuccess();
56+
}
57+
}
58+
catch (Exception e)
59+
{
60+
int failureCount = _backoff.RecordFailure();
61+
if (failureCount == 1)
62+
{
63+
_options.LogError(e, "Failed sending envelope to Spotlight.");
64+
}
5065
}
51-
}
52-
catch (Exception e)
53-
{
54-
_options.LogError(e, "Failed sending envelope to Spotlight.");
5566
}
5667

5768
// await the Sentry request before returning
5869
await sentryTask.ConfigureAwait(false);
5970
}
71+
72+
private class ExponentialBackoff
73+
{
74+
private static readonly TimeSpan InitialRetryDelay = TimeSpan.FromSeconds(1);
75+
private static readonly TimeSpan MaxRetryDelay = TimeSpan.FromSeconds(60);
76+
77+
private readonly ISystemClock _clock;
78+
79+
private readonly Lock _lock = new();
80+
private TimeSpan _retryDelay = InitialRetryDelay;
81+
private DateTimeOffset _retryAfter = DateTimeOffset.MinValue;
82+
private int _failureCount;
83+
84+
public ExponentialBackoff(ISystemClock clock)
85+
{
86+
_clock = clock;
87+
}
88+
89+
public bool ShouldAttempt()
90+
{
91+
lock (_lock)
92+
{
93+
return _clock.GetUtcNow() >= _retryAfter;
94+
}
95+
}
96+
97+
public int RecordFailure()
98+
{
99+
lock (_lock)
100+
{
101+
_failureCount++;
102+
_retryAfter = _clock.GetUtcNow() + _retryDelay;
103+
_retryDelay = TimeSpan.FromTicks(Math.Min(_retryDelay.Ticks * 2, MaxRetryDelay.Ticks));
104+
return _failureCount;
105+
}
106+
}
107+
108+
public void RecordSuccess()
109+
{
110+
lock (_lock)
111+
{
112+
_failureCount = 0;
113+
_retryDelay = InitialRetryDelay;
114+
_retryAfter = DateTimeOffset.MinValue;
115+
}
116+
}
117+
}
60118
}

0 commit comments

Comments
 (0)