Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add default Retry-After value #1537

Merged
merged 6 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Use a default value of 60 seconds if a `Retry-After` header is not present. ([#1537](https://github.com/getsentry/sentry-dotnet/pull/1537))

## 3.15.0

### Features
Expand Down
35 changes: 24 additions & 11 deletions src/Sentry/Internal/Http/RetryAfterHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ internal class RetryAfterHandler : DelegatingHandler
private readonly ISystemClock _clock;

private const HttpStatusCode TooManyRequests = (HttpStatusCode)429;
internal static readonly TimeSpan DefaultRetryAfterDelay = TimeSpan.FromSeconds(60);
mattjohnsonpint marked this conversation as resolved.
Show resolved Hide resolved

private long _retryAfterUtcTicks;
internal long RetryAfterUtcTicks => _retryAfterUtcTicks;
Expand Down Expand Up @@ -63,31 +64,43 @@ protected override async Task<HttpResponseMessage> SendAsync(

var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);

if (response.StatusCode == TooManyRequests && response.Headers != null)
if (response.StatusCode == TooManyRequests)
{
var retryAfterTimestamp = GetRetryAfterTimestamp(response);
_ = Interlocked.Exchange(ref _retryAfterUtcTicks, retryAfterTimestamp.UtcTicks);
}

return response;
}

private DateTimeOffset GetRetryAfterTimestamp(HttpResponseMessage response)
{
if (response.Headers != null)
mattjohnsonpint marked this conversation as resolved.
Show resolved Hide resolved
{
if (response.Headers.RetryAfter != null)
{
if (response.Headers.RetryAfter.Delta != null)
if (response.Headers.RetryAfter.Delta is { } delta)
{
var retryAfterUtc = _clock.GetUtcNow() + response.Headers.RetryAfter.Delta.Value;
_ = Interlocked.Exchange(ref _retryAfterUtcTicks, retryAfterUtc.UtcTicks);
return _clock.GetUtcNow() + delta;
}
else if (response.Headers.RetryAfter.Date != null)

if (response.Headers.RetryAfter.Date is { } date)
{
_ = Interlocked.Exchange(ref _retryAfterUtcTicks, response.Headers.RetryAfter.Date.Value.UtcTicks);
return date;
}
}

// Sentry was sending floating point numbers which are not handled by RetryConditionHeaderValue
// To be compatible with older versions of sentry on premise: https://github.com/getsentry/sentry/issues/7919
else if (response.Headers.TryGetValues("Retry-After", out var values)
&& double.TryParse(values?.FirstOrDefault(), out var retryAfterSeconds))
if (response.Headers.TryGetValues("Retry-After", out var values)
mattjohnsonpint marked this conversation as resolved.
Show resolved Hide resolved
&& double.TryParse(values?.FirstOrDefault(), out var retryAfterSeconds))
{
var retryAfterSpan = TimeSpan.FromSeconds(retryAfterSeconds);
_ = Interlocked.Exchange(ref _retryAfterUtcTicks, _clock.GetUtcNow().AddTicks(retryAfterSpan.Ticks).UtcTicks);
return _clock.GetUtcNow().AddTicks((long)(retryAfterSeconds * TimeSpan.TicksPerSecond));
}
}

return response;
// No retry header was sent. Use the default retry delay.
return _clock.GetUtcNow() + DefaultRetryAfterDelay;
}
}
}
8 changes: 5 additions & 3 deletions test/Sentry.Tests/Internals/Http/RetryAfterHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public async Task SendAsync_TooManyRequestsWithoutRetryAfterHeader_RetryAfterNot
var actual = await invoker.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/"), None);

Assert.Equal(expected, actual);
Assert.Equal(0, _fixture.Sut.RetryAfterUtcTicks);
Assert.Equal((_fixture.TimeReturned + RetryAfterHandler.DefaultRetryAfterDelay).UtcTicks, _fixture.Sut.RetryAfterUtcTicks);
Assert.True(_fixture.StubHandler.SendAsyncCalled);
}

Expand Down Expand Up @@ -104,7 +104,8 @@ public async Task SendAsync_TooManyRequestsWithRetryAfterHeaderFloat_RetryAfterS
var actual = await invoker.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/"), None);

Assert.Equal(expected, actual);
Assert.Equal((_fixture.TimeReturned + TimeSpan.FromSeconds(floating)).UtcTicks, _fixture.Sut.RetryAfterUtcTicks);
var expectedTime = _fixture.TimeReturned.AddTicks((long)(floating * TimeSpan.TicksPerSecond));
Assert.Equal(expectedTime.UtcTicks, _fixture.Sut.RetryAfterUtcTicks);
Assert.True(_fixture.StubHandler.SendAsyncCalled);
}

Expand All @@ -127,7 +128,8 @@ public async Task SendAsync_TooManyRequestsWithRetryAfterHeaderFloat_SecondReque
var actual = await invoker.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/"), None);

Assert.Equal(TooManyRequests, actual.StatusCode);
Assert.Equal((_fixture.TimeReturned + TimeSpan.FromSeconds(floating)).UtcTicks, _fixture.Sut.RetryAfterUtcTicks);
var expectedTime = _fixture.TimeReturned.AddTicks((long)(floating * TimeSpan.TicksPerSecond));
Assert.Equal(expectedTime.UtcTicks, _fixture.Sut.RetryAfterUtcTicks);
Assert.False(_fixture.StubHandler.SendAsyncCalled);
}

Expand Down