From 7b9029dd914f2448eee9ed46f4a77947ca6f430c Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 21 Sep 2019 09:24:37 -0400 Subject: [PATCH] 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. --- src/Discord.Net.Core/DiscordConfig.cs | 18 ++++++++++++++++++ src/Discord.Net.Core/RequestOptions.cs | 12 ++++++++++++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 6 +++++- src/Discord.Net.Rest/DiscordRestClient.cs | 6 +++++- .../Net/Queue/RequestQueueBucket.cs | 7 +++++++ src/Discord.Net.Rest/Net/RateLimitInfo.cs | 3 +++ src/Discord.Net.WebSocket/BaseSocketClient.cs | 3 ++- .../DiscordSocketApiClient.cs | 5 +++-- 8 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 0945a77b6..51970a781 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -152,5 +152,23 @@ namespace Discord /// The currently set . /// public RateLimitPrecision RateLimitPrecision { get; set; } = RateLimitPrecision.Millisecond; + + /// + /// Gets or sets whether or not rate-limits should use the system clock. + /// + /// + /// If set to false, we will use the X-RateLimit-Reset-After header + /// to determine when a rate-limit expires, rather than comparing the + /// X-RateLimit-Reset timestamp to the system time. + /// + /// This should only be changed to false if the system is known to have + /// a clock that is out of sync. Relying on the Reset-After header will + /// incur network lag. + /// + /// Regardless of this property, we still rely on the system's wall-clock + /// to determine if a bucket is rate-limited; we do not use any monotonic + /// clock. Your system will still need a stable clock. + /// + public bool UseSystemClock { get; set; } = true; } } diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 3af3ded6f..6aa0eea12 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -44,6 +44,18 @@ namespace Discord /// to all actions. /// public string AuditLogReason { get; set; } + /// + /// Gets or sets whether or not this request should use the system + /// clock for rate-limiting. Defaults to true. + /// + /// + /// This property can also be set in . + /// + /// On a per-request basis, the system clock should only be disabled + /// when millisecond precision is especially important, and the + /// hosting system is known to have a desynced clock. + /// + public bool? UseSystemClock { get; set; } internal bool IgnoreState { get; set; } internal string BucketId { get; set; } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 0ca7ef7a6..ff6d17240 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -46,18 +46,20 @@ namespace Discord.API internal IRestClient RestClient { get; private set; } internal ulong? CurrentUserId { get; set; } public RateLimitPrecision RateLimitPrecision { get; private set; } + internal bool UseSystemClock { get; set; } internal JsonSerializer Serializer => _serializer; /// Unknown OAuth token type. public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, - JsonSerializer serializer = null, RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second) + JsonSerializer serializer = null, RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second, bool useSystemClock = true) { _restClientProvider = restClientProvider; UserAgent = userAgent; DefaultRetryMode = defaultRetryMode; _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; RateLimitPrecision = rateLimitPrecision; + UseSystemClock = useSystemClock; RequestQueue = new RequestQueue(); _stateLock = new SemaphoreSlim(1, 1); @@ -265,6 +267,8 @@ namespace Discord.API CheckState(); if (request.Options.RetryMode == null) request.Options.RetryMode = DefaultRetryMode; + if (request.Options.UseSystemClock == null) + request.Options.UseSystemClock = UseSystemClock; var stopwatch = Stopwatch.StartNew(); var responseStream = await RequestQueue.SendAsync(request).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 29bf89c50..4c29d1625 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -28,7 +28,11 @@ namespace Discord.Rest internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) - => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); + => new API.DiscordRestApiClient(config.RestClientProvider, + DiscordRestConfig.UserAgent, + rateLimitPrecision: config.RateLimitPrecision, + useSystemClock: config.UseSystemClock); + internal override void Dispose(bool disposing) { if (disposing) diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index d2f77cc39..72dd1642d 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -247,12 +247,19 @@ namespace Discord.Net.Queue Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)"); #endif } + else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false)) + { + resetTick = DateTimeOffset.Now.Add(info.ResetAfter.Value); + } else if (info.Reset.HasValue) { resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0); + /* millisecond precision makes this unnecessary, retaining in case of regression + if (request.Options.IsReactionBucket) resetTick = DateTimeOffset.Now.AddMilliseconds(250); + */ int diff = (int)(resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds; #if DEBUG_LIMITS diff --git a/src/Discord.Net.Rest/Net/RateLimitInfo.cs b/src/Discord.Net.Rest/Net/RateLimitInfo.cs index fb6f5e2ce..13e9e39a7 100644 --- a/src/Discord.Net.Rest/Net/RateLimitInfo.cs +++ b/src/Discord.Net.Rest/Net/RateLimitInfo.cs @@ -11,6 +11,7 @@ namespace Discord.Net public int? Remaining { get; } public int? RetryAfter { get; } public DateTimeOffset? Reset { get; } + public TimeSpan? ResetAfter { get; } public TimeSpan? Lag { get; } internal RateLimitInfo(Dictionary headers) @@ -25,6 +26,8 @@ namespace Discord.Net double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var reset) ? DateTimeOffset.FromUnixTimeMilliseconds((long)(reset * 1000)) : (DateTimeOffset?)null; RetryAfter = headers.TryGetValue("Retry-After", out temp) && int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var retryAfter) ? retryAfter : (int?)null; + ResetAfter = headers.TryGetValue("X-RateLimit-Reset-After", out temp) && + float.TryParse(temp, out var resetAfter) ? TimeSpan.FromMilliseconds((long)(resetAfter * 1000)) : (TimeSpan?)null; Lag = headers.TryGetValue("Date", out temp) && DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null; } diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 5f7c48417..548bb75bf 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -81,7 +81,8 @@ namespace Discord.WebSocket : base(config, client) => BaseConfig = config; private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) => new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, - rateLimitPrecision: config.RateLimitPrecision); + rateLimitPrecision: config.RateLimitPrecision, + useSystemClock: config.UseSystemClock); /// /// Gets a Discord application information for the logged-in user. diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index d0bc47686..88ef1134e 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -39,8 +39,9 @@ namespace Discord.API public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent, string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null, - RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second) - : base(restClientProvider, userAgent, defaultRetryMode, serializer, rateLimitPrecision) + RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second, + bool useSystemClock = true) + : base(restClientProvider, userAgent, defaultRetryMode, serializer, rateLimitPrecision, useSystemClock) { _gatewayUrl = url; if (url != null)