Skip to content

Commit 7b9029d

Browse files
authored
feature: support X-RateLimit-Reset-After (#1372)
* feature: support X-RateLimit-Reset-After Users may now optionally disable using the system clock to calculate the ratelimit duration. This may be overrided globally, via DiscordConfig, or per RequestOptions. This change has been built and tested via the integrated test suite, but has not been tested in the real world. Please verify this does not break any of the edge-case ratelimits. * patch: wire new config properties to ApiClient * patch: update Reset-After parsing precision This patch applies the changes made to parsing precision in 606dac3.
1 parent 68eb71c commit 7b9029d

File tree

8 files changed

+55
-5
lines changed

8 files changed

+55
-5
lines changed

src/Discord.Net.Core/DiscordConfig.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,5 +152,23 @@ public class DiscordConfig
152152
/// The currently set <see cref="RateLimitPrecision"/>.
153153
/// </returns>
154154
public RateLimitPrecision RateLimitPrecision { get; set; } = RateLimitPrecision.Millisecond;
155+
156+
/// <summary>
157+
/// Gets or sets whether or not rate-limits should use the system clock.
158+
/// </summary>
159+
/// <remarks>
160+
/// If set to <c>false</c>, we will use the X-RateLimit-Reset-After header
161+
/// to determine when a rate-limit expires, rather than comparing the
162+
/// X-RateLimit-Reset timestamp to the system time.
163+
///
164+
/// This should only be changed to false if the system is known to have
165+
/// a clock that is out of sync. Relying on the Reset-After header will
166+
/// incur network lag.
167+
///
168+
/// Regardless of this property, we still rely on the system's wall-clock
169+
/// to determine if a bucket is rate-limited; we do not use any monotonic
170+
/// clock. Your system will still need a stable clock.
171+
/// </remarks>
172+
public bool UseSystemClock { get; set; } = true;
155173
}
156174
}

src/Discord.Net.Core/RequestOptions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ public class RequestOptions
4444
/// to all actions.
4545
/// </remarks>
4646
public string AuditLogReason { get; set; }
47+
/// <summary>
48+
/// Gets or sets whether or not this request should use the system
49+
/// clock for rate-limiting. Defaults to <c>true</c>.
50+
/// </summary>
51+
/// <remarks>
52+
/// This property can also be set in <see cref="DiscordConfig">.
53+
///
54+
/// On a per-request basis, the system clock should only be disabled
55+
/// when millisecond precision is especially important, and the
56+
/// hosting system is known to have a desynced clock.
57+
/// </remarks>
58+
public bool? UseSystemClock { get; set; }
4759

4860
internal bool IgnoreState { get; set; }
4961
internal string BucketId { get; set; }

src/Discord.Net.Rest/DiscordRestApiClient.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,20 @@ internal class DiscordRestApiClient : IDisposable
4646
internal IRestClient RestClient { get; private set; }
4747
internal ulong? CurrentUserId { get; set; }
4848
public RateLimitPrecision RateLimitPrecision { get; private set; }
49+
internal bool UseSystemClock { get; set; }
4950

5051
internal JsonSerializer Serializer => _serializer;
5152

5253
/// <exception cref="ArgumentException">Unknown OAuth token type.</exception>
5354
public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry,
54-
JsonSerializer serializer = null, RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second)
55+
JsonSerializer serializer = null, RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second, bool useSystemClock = true)
5556
{
5657
_restClientProvider = restClientProvider;
5758
UserAgent = userAgent;
5859
DefaultRetryMode = defaultRetryMode;
5960
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() };
6061
RateLimitPrecision = rateLimitPrecision;
62+
UseSystemClock = useSystemClock;
6163

6264
RequestQueue = new RequestQueue();
6365
_stateLock = new SemaphoreSlim(1, 1);
@@ -265,6 +267,8 @@ private async Task<Stream> SendInternalAsync(string method, string endpoint, Res
265267
CheckState();
266268
if (request.Options.RetryMode == null)
267269
request.Options.RetryMode = DefaultRetryMode;
270+
if (request.Options.UseSystemClock == null)
271+
request.Options.UseSystemClock = UseSystemClock;
268272

269273
var stopwatch = Stopwatch.StartNew();
270274
var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false);

src/Discord.Net.Rest/DiscordRestClient.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ public DiscordRestClient(DiscordRestConfig config) : base(config, CreateApiClien
2828
internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { }
2929

3030
private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config)
31-
=> new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent);
31+
=> new API.DiscordRestApiClient(config.RestClientProvider,
32+
DiscordRestConfig.UserAgent,
33+
rateLimitPrecision: config.RateLimitPrecision,
34+
useSystemClock: config.UseSystemClock);
35+
3236
internal override void Dispose(bool disposing)
3337
{
3438
if (disposing)

src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,19 @@ private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bo
247247
Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)");
248248
#endif
249249
}
250+
else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false))
251+
{
252+
resetTick = DateTimeOffset.Now.Add(info.ResetAfter.Value);
253+
}
250254
else if (info.Reset.HasValue)
251255
{
252256
resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0);
253257

258+
/* millisecond precision makes this unnecessary, retaining in case of regression
259+
254260
if (request.Options.IsReactionBucket)
255261
resetTick = DateTimeOffset.Now.AddMilliseconds(250);
262+
*/
256263

257264
int diff = (int)(resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds;
258265
#if DEBUG_LIMITS

src/Discord.Net.Rest/Net/RateLimitInfo.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ internal struct RateLimitInfo
1111
public int? Remaining { get; }
1212
public int? RetryAfter { get; }
1313
public DateTimeOffset? Reset { get; }
14+
public TimeSpan? ResetAfter { get; }
1415
public TimeSpan? Lag { get; }
1516

1617
internal RateLimitInfo(Dictionary<string, string> headers)
@@ -25,6 +26,8 @@ internal RateLimitInfo(Dictionary<string, string> headers)
2526
double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var reset) ? DateTimeOffset.FromUnixTimeMilliseconds((long)(reset * 1000)) : (DateTimeOffset?)null;
2627
RetryAfter = headers.TryGetValue("Retry-After", out temp) &&
2728
int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var retryAfter) ? retryAfter : (int?)null;
29+
ResetAfter = headers.TryGetValue("X-RateLimit-Reset-After", out temp) &&
30+
float.TryParse(temp, out var resetAfter) ? TimeSpan.FromMilliseconds((long)(resetAfter * 1000)) : (TimeSpan?)null;
2831
Lag = headers.TryGetValue("Date", out temp) &&
2932
DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null;
3033
}

src/Discord.Net.WebSocket/BaseSocketClient.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ internal BaseSocketClient(DiscordSocketConfig config, DiscordRestApiClient clien
8181
: base(config, client) => BaseConfig = config;
8282
private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
8383
=> new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent,
84-
rateLimitPrecision: config.RateLimitPrecision);
84+
rateLimitPrecision: config.RateLimitPrecision,
85+
useSystemClock: config.UseSystemClock);
8586

8687
/// <summary>
8788
/// Gets a Discord application information for the logged-in user.

src/Discord.Net.WebSocket/DiscordSocketApiClient.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ internal class DiscordSocketApiClient : DiscordRestApiClient
3939

4040
public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent,
4141
string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null,
42-
RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second)
43-
: base(restClientProvider, userAgent, defaultRetryMode, serializer, rateLimitPrecision)
42+
RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second,
43+
bool useSystemClock = true)
44+
: base(restClientProvider, userAgent, defaultRetryMode, serializer, rateLimitPrecision, useSystemClock)
4445
{
4546
_gatewayUrl = url;
4647
if (url != null)

0 commit comments

Comments
 (0)