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"/>.
/// </returns>
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.
/// </remarks>
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 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 ulong? CurrentUserId { get; set; }
public RateLimitPrecision RateLimitPrecision { get; private set; }
internal bool UseSystemClock { get; set; }
internal JsonSerializer Serializer => _serializer;

/// <exception cref="ArgumentException">Unknown OAuth token type.</exception>
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);


+ 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) { }

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)


+ 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)");
#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


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

@@ -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<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;
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;
}


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

@@ -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);

/// <summary>
/// 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,
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)


Loading…
Cancel
Save