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)