Browse Source

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.
tags/2.2.0
Christopher F GitHub 5 years ago
parent
commit
7b9029dd91
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 55 additions and 5 deletions
  1. +18
    -0
      src/Discord.Net.Core/DiscordConfig.cs
  2. +12
    -0
      src/Discord.Net.Core/RequestOptions.cs
  3. +5
    -1
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  4. +5
    -1
      src/Discord.Net.Rest/DiscordRestClient.cs
  5. +7
    -0
      src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
  6. +3
    -0
      src/Discord.Net.Rest/Net/RateLimitInfo.cs
  7. +2
    -1
      src/Discord.Net.WebSocket/BaseSocketClient.cs
  8. +3
    -2
      src/Discord.Net.WebSocket/DiscordSocketApiClient.cs

+ 18
- 0
src/Discord.Net.Core/DiscordConfig.cs View File

@@ -152,5 +152,23 @@ namespace Discord
/// The currently set <see cref="RateLimitPrecision"/>. /// The currently set <see cref="RateLimitPrecision"/>.
/// </returns> /// </returns>
public RateLimitPrecision RateLimitPrecision { get; set; } = RateLimitPrecision.Millisecond; public RateLimitPrecision RateLimitPrecision { get; set; } = RateLimitPrecision.Millisecond;

/// <summary>
/// Gets or sets whether or not rate-limits should use the system clock.
/// </summary>
/// <remarks>
/// If set to <c>false</c>, 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.
/// </remarks>
public bool UseSystemClock { get; set; } = true;
} }
} }

+ 12
- 0
src/Discord.Net.Core/RequestOptions.cs View File

@@ -44,6 +44,18 @@ namespace Discord
/// to all actions. /// to all actions.
/// </remarks> /// </remarks>
public string AuditLogReason { get; set; } public string AuditLogReason { get; set; }
/// <summary>
/// Gets or sets whether or not this request should use the system
/// clock for rate-limiting. Defaults to <c>true</c>.
/// </summary>
/// <remarks>
/// This property can also be set in <see cref="DiscordConfig">.
///
/// 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.
/// </remarks>
public bool? UseSystemClock { get; set; }


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


+ 5
- 1
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -46,18 +46,20 @@ namespace Discord.API
internal IRestClient RestClient { get; private set; } internal IRestClient RestClient { get; private set; }
internal ulong? CurrentUserId { get; set; } internal ulong? CurrentUserId { get; set; }
public RateLimitPrecision RateLimitPrecision { get; private set; } public RateLimitPrecision RateLimitPrecision { get; private set; }
internal bool UseSystemClock { get; set; }
internal JsonSerializer Serializer => _serializer; internal JsonSerializer Serializer => _serializer;


/// <exception cref="ArgumentException">Unknown OAuth token type.</exception> /// <exception cref="ArgumentException">Unknown OAuth token type.</exception>
public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, 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; _restClientProvider = restClientProvider;
UserAgent = userAgent; UserAgent = userAgent;
DefaultRetryMode = defaultRetryMode; DefaultRetryMode = defaultRetryMode;
_serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() };
RateLimitPrecision = rateLimitPrecision; RateLimitPrecision = rateLimitPrecision;
UseSystemClock = useSystemClock;


RequestQueue = new RequestQueue(); RequestQueue = new RequestQueue();
_stateLock = new SemaphoreSlim(1, 1); _stateLock = new SemaphoreSlim(1, 1);
@@ -265,6 +267,8 @@ namespace Discord.API
CheckState(); CheckState();
if (request.Options.RetryMode == null) if (request.Options.RetryMode == null)
request.Options.RetryMode = DefaultRetryMode; request.Options.RetryMode = DefaultRetryMode;
if (request.Options.UseSystemClock == null)
request.Options.UseSystemClock = UseSystemClock;


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


+ 5
- 1
src/Discord.Net.Rest/DiscordRestClient.cs View File

@@ -28,7 +28,11 @@ namespace Discord.Rest
internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { } internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { }


private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) 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) internal override void Dispose(bool disposing)
{ {
if (disposing) if (disposing)


+ 7
- 0
src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs View File

@@ -247,12 +247,19 @@ namespace Discord.Net.Queue
Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)"); Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)");
#endif #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) else if (info.Reset.HasValue)
{ {
resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0); resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0);


/* millisecond precision makes this unnecessary, retaining in case of regression

if (request.Options.IsReactionBucket) if (request.Options.IsReactionBucket)
resetTick = DateTimeOffset.Now.AddMilliseconds(250); resetTick = DateTimeOffset.Now.AddMilliseconds(250);
*/


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


+ 3
- 0
src/Discord.Net.Rest/Net/RateLimitInfo.cs View File

@@ -11,6 +11,7 @@ namespace Discord.Net
public int? Remaining { get; } public int? Remaining { get; }
public int? RetryAfter { get; } public int? RetryAfter { get; }
public DateTimeOffset? Reset { get; } public DateTimeOffset? Reset { get; }
public TimeSpan? ResetAfter { get; }
public TimeSpan? Lag { get; } public TimeSpan? Lag { get; }


internal RateLimitInfo(Dictionary<string, string> headers) internal RateLimitInfo(Dictionary<string, string> 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; double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var reset) ? DateTimeOffset.FromUnixTimeMilliseconds((long)(reset * 1000)) : (DateTimeOffset?)null;
RetryAfter = headers.TryGetValue("Retry-After", out temp) && RetryAfter = headers.TryGetValue("Retry-After", out temp) &&
int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var retryAfter) ? retryAfter : (int?)null; 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) && Lag = headers.TryGetValue("Date", out temp) &&
DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null; DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null;
} }


+ 2
- 1
src/Discord.Net.WebSocket/BaseSocketClient.cs View File

@@ -81,7 +81,8 @@ namespace Discord.WebSocket
: base(config, client) => BaseConfig = config; : base(config, client) => BaseConfig = config;
private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
=> new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, => new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent,
rateLimitPrecision: config.RateLimitPrecision);
rateLimitPrecision: config.RateLimitPrecision,
useSystemClock: config.UseSystemClock);


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


+ 3
- 2
src/Discord.Net.WebSocket/DiscordSocketApiClient.cs View File

@@ -39,8 +39,9 @@ namespace Discord.API


public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent, public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent,
string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null, 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; _gatewayUrl = url;
if (url != null) if (url != null)


Loading…
Cancel
Save