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)