fix: Different ratelimits for the same route (implement discord buckets) (#1546)
* Don't disable when there's no resetTick Sometimes Discord won't send any ratelimit headers, disabling the semaphore for endpoints that should have them. * Undo changes and change comment * Add HttpMethod to BucketIds * Add X-RateLimit-Bucket * BucketId changes - BucketId is it's own class now - Add WebhookId as a major parameter - Add shared buckets using the hash and major parameters * Add webhookId to BucketIds * Update BucketId and redirect requests * General bugfixes * Assign semaphore and follow the same standard as Reset for ResetAfter
This commit is contained in:
118
src/Discord.Net.Core/Net/BucketId.cs
Normal file
118
src/Discord.Net.Core/Net/BucketId.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Discord.Net
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a ratelimit bucket.
|
||||||
|
/// </summary>
|
||||||
|
public class BucketId : IEquatable<BucketId>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the http method used to make the request if available.
|
||||||
|
/// </summary>
|
||||||
|
public string HttpMethod { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the endpoint that is going to be requested if available.
|
||||||
|
/// </summary>
|
||||||
|
public string Endpoint { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the major parameters of the route.
|
||||||
|
/// </summary>
|
||||||
|
public IOrderedEnumerable<KeyValuePair<string, string>> MajorParameters { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the hash of this bucket.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The hash is provided by Discord to group ratelimits.
|
||||||
|
/// </remarks>
|
||||||
|
public string BucketHash { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets if this bucket is a hash type.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsHashBucket { get => BucketHash != null; }
|
||||||
|
|
||||||
|
private BucketId(string httpMethod, string endpoint, IEnumerable<KeyValuePair<string, string>> majorParameters, string bucketHash)
|
||||||
|
{
|
||||||
|
HttpMethod = httpMethod;
|
||||||
|
Endpoint = endpoint;
|
||||||
|
MajorParameters = majorParameters.OrderBy(x => x.Key);
|
||||||
|
BucketHash = bucketHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="BucketId"/> based on the
|
||||||
|
/// <see cref="HttpMethod"/> and <see cref="Endpoint"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpMethod">Http method used to make the request.</param>
|
||||||
|
/// <param name="endpoint">Endpoint that is going to receive requests.</param>
|
||||||
|
/// <param name="majorParams">Major parameters of the route of this endpoint.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="BucketId"/> based on the <see cref="HttpMethod"/>
|
||||||
|
/// and the <see cref="Endpoint"/> with the provided data.
|
||||||
|
/// </returns>
|
||||||
|
public static BucketId Create(string httpMethod, string endpoint, Dictionary<string, string> majorParams)
|
||||||
|
{
|
||||||
|
Preconditions.NotNullOrWhitespace(endpoint, nameof(endpoint));
|
||||||
|
majorParams ??= new Dictionary<string, string>();
|
||||||
|
return new BucketId(httpMethod, endpoint, majorParams, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="BucketId"/> based on a
|
||||||
|
/// <see cref="BucketHash"/> and a previous <see cref="BucketId"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hash">Bucket hash provided by Discord.</param>
|
||||||
|
/// <param name="oldBucket"><see cref="BucketId"/> that is going to be upgraded to a hash type.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="BucketId"/> based on the <see cref="BucketHash"/>
|
||||||
|
/// and <see cref="MajorParameters"/>.
|
||||||
|
/// </returns>
|
||||||
|
public static BucketId Create(string hash, BucketId oldBucket)
|
||||||
|
{
|
||||||
|
Preconditions.NotNullOrWhitespace(hash, nameof(hash));
|
||||||
|
Preconditions.NotNull(oldBucket, nameof(oldBucket));
|
||||||
|
return new BucketId(null, null, oldBucket.MajorParameters, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the string that will define this bucket as a hash based one.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="string"/> that defines this bucket as a hash based one.
|
||||||
|
/// </returns>
|
||||||
|
public string GetBucketHash()
|
||||||
|
=> IsHashBucket ? $"{BucketHash}:{string.Join("/", MajorParameters.Select(x => x.Value))}" : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the string that will define this bucket as an endpoint based one.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="string"/> that defines this bucket as an endpoint based one.
|
||||||
|
/// </returns>
|
||||||
|
public string GetUniqueEndpoint()
|
||||||
|
=> HttpMethod != null ? $"{HttpMethod} {Endpoint}" : Endpoint;
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
=> Equals(obj as BucketId);
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
=> IsHashBucket ? (BucketHash, string.Join("/", MajorParameters.Select(x => x.Value))).GetHashCode() : (HttpMethod, Endpoint).GetHashCode();
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> GetBucketHash() ?? GetUniqueEndpoint();
|
||||||
|
|
||||||
|
public bool Equals(BucketId other)
|
||||||
|
{
|
||||||
|
if (other is null)
|
||||||
|
return false;
|
||||||
|
if (ReferenceEquals(this, other))
|
||||||
|
return true;
|
||||||
|
if (GetType() != other.GetType())
|
||||||
|
return false;
|
||||||
|
return ToString() == other.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Discord.Net;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace Discord
|
namespace Discord
|
||||||
@@ -57,7 +58,7 @@ namespace Discord
|
|||||||
public bool? UseSystemClock { get; set; }
|
public bool? UseSystemClock { get; set; }
|
||||||
|
|
||||||
internal bool IgnoreState { get; set; }
|
internal bool IgnoreState { get; set; }
|
||||||
internal string BucketId { get; set; }
|
internal BucketId BucketId { get; set; }
|
||||||
internal bool IsClientBucket { get; set; }
|
internal bool IsClientBucket { get; set; }
|
||||||
internal bool IsReactionBucket { get; set; }
|
internal bool IsReactionBucket { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ namespace Discord.Rest
|
|||||||
ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
|
ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
|
||||||
{
|
{
|
||||||
if (info == null)
|
if (info == null)
|
||||||
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
|
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
|
||||||
else
|
else
|
||||||
await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
|
await _restLogger.WarningAsync($"Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
|
||||||
};
|
};
|
||||||
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
|
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace Discord.API
|
|||||||
{
|
{
|
||||||
internal class DiscordRestApiClient : IDisposable
|
internal class DiscordRestApiClient : IDisposable
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<string, Func<BucketIds, string>> _bucketIdGenerators = new ConcurrentDictionary<string, Func<BucketIds, string>>();
|
private static readonly ConcurrentDictionary<string, Func<BucketIds, BucketId>> _bucketIdGenerators = new ConcurrentDictionary<string, Func<BucketIds, BucketId>>();
|
||||||
|
|
||||||
public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } }
|
public event Func<string, string, double, Task> SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } }
|
||||||
private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>();
|
private readonly AsyncEvent<Func<string, string, double, Task>> _sentRequestEvent = new AsyncEvent<Func<string, string, double, Task>>();
|
||||||
@@ -176,9 +176,9 @@ namespace Discord.API
|
|||||||
//Core
|
//Core
|
||||||
internal Task SendAsync(string method, Expression<Func<string>> endpointExpr, BucketIds ids,
|
internal Task SendAsync(string method, Expression<Func<string>> endpointExpr, BucketIds ids,
|
||||||
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
|
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
|
||||||
=> SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), clientBucket, options);
|
=> SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
|
||||||
public async Task SendAsync(string method, string endpoint,
|
public async Task SendAsync(string method, string endpoint,
|
||||||
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
|
BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
|
||||||
{
|
{
|
||||||
options = options ?? new RequestOptions();
|
options = options ?? new RequestOptions();
|
||||||
options.HeaderOnly = true;
|
options.HeaderOnly = true;
|
||||||
@@ -190,9 +190,9 @@ namespace Discord.API
|
|||||||
|
|
||||||
internal Task SendJsonAsync(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids,
|
internal Task SendJsonAsync(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids,
|
||||||
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
|
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
|
||||||
=> SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, funcName), clientBucket, options);
|
=> SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
|
||||||
public async Task SendJsonAsync(string method, string endpoint, object payload,
|
public async Task SendJsonAsync(string method, string endpoint, object payload,
|
||||||
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
|
BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
|
||||||
{
|
{
|
||||||
options = options ?? new RequestOptions();
|
options = options ?? new RequestOptions();
|
||||||
options.HeaderOnly = true;
|
options.HeaderOnly = true;
|
||||||
@@ -205,9 +205,9 @@ namespace Discord.API
|
|||||||
|
|
||||||
internal Task SendMultipartAsync(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids,
|
internal Task SendMultipartAsync(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids,
|
||||||
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
|
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
|
||||||
=> SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options);
|
=> SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
|
||||||
public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs,
|
public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs,
|
||||||
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
|
BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
|
||||||
{
|
{
|
||||||
options = options ?? new RequestOptions();
|
options = options ?? new RequestOptions();
|
||||||
options.HeaderOnly = true;
|
options.HeaderOnly = true;
|
||||||
@@ -219,9 +219,9 @@ namespace Discord.API
|
|||||||
|
|
||||||
internal Task<TResponse> SendAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, BucketIds ids,
|
internal Task<TResponse> SendAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, BucketIds ids,
|
||||||
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class
|
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class
|
||||||
=> SendAsync<TResponse>(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), clientBucket, options);
|
=> SendAsync<TResponse>(method, GetEndpoint(endpointExpr), GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
|
||||||
public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint,
|
public async Task<TResponse> SendAsync<TResponse>(string method, string endpoint,
|
||||||
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class
|
BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class
|
||||||
{
|
{
|
||||||
options = options ?? new RequestOptions();
|
options = options ?? new RequestOptions();
|
||||||
options.BucketId = bucketId;
|
options.BucketId = bucketId;
|
||||||
@@ -232,9 +232,9 @@ namespace Discord.API
|
|||||||
|
|
||||||
internal Task<TResponse> SendJsonAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids,
|
internal Task<TResponse> SendJsonAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, object payload, BucketIds ids,
|
||||||
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class
|
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class
|
||||||
=> SendJsonAsync<TResponse>(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, funcName), clientBucket, options);
|
=> SendJsonAsync<TResponse>(method, GetEndpoint(endpointExpr), payload, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
|
||||||
public async Task<TResponse> SendJsonAsync<TResponse>(string method, string endpoint, object payload,
|
public async Task<TResponse> SendJsonAsync<TResponse>(string method, string endpoint, object payload,
|
||||||
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class
|
BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class
|
||||||
{
|
{
|
||||||
options = options ?? new RequestOptions();
|
options = options ?? new RequestOptions();
|
||||||
options.BucketId = bucketId;
|
options.BucketId = bucketId;
|
||||||
@@ -246,9 +246,9 @@ namespace Discord.API
|
|||||||
|
|
||||||
internal Task<TResponse> SendMultipartAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids,
|
internal Task<TResponse> SendMultipartAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids,
|
||||||
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
|
ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null)
|
||||||
=> SendMultipartAsync<TResponse>(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options);
|
=> SendMultipartAsync<TResponse>(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options);
|
||||||
public async Task<TResponse> SendMultipartAsync<TResponse>(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs,
|
public async Task<TResponse> SendMultipartAsync<TResponse>(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs,
|
||||||
string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
|
BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null)
|
||||||
{
|
{
|
||||||
options = options ?? new RequestOptions();
|
options = options ?? new RequestOptions();
|
||||||
options.BucketId = bucketId;
|
options.BucketId = bucketId;
|
||||||
@@ -520,7 +520,8 @@ namespace Discord.API
|
|||||||
throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content));
|
throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content));
|
||||||
options = RequestOptions.CreateOrClone(options);
|
options = RequestOptions.CreateOrClone(options);
|
||||||
|
|
||||||
return await SendJsonAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
|
var ids = new BucketIds(webhookId: webhookId);
|
||||||
|
return await SendJsonAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
|
/// <exception cref="ArgumentOutOfRangeException">Message content is too long, length must be less or equal to <see cref="DiscordConfig.MaxMessageSize"/>.</exception>
|
||||||
public async Task<Message> UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null)
|
public async Task<Message> UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null)
|
||||||
@@ -559,7 +560,8 @@ namespace Discord.API
|
|||||||
throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
|
throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content));
|
||||||
}
|
}
|
||||||
|
|
||||||
return await SendMultipartAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
|
var ids = new BucketIds(webhookId: webhookId);
|
||||||
|
return await SendMultipartAsync<Message>("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null)
|
public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null)
|
||||||
{
|
{
|
||||||
@@ -1466,21 +1468,39 @@ namespace Discord.API
|
|||||||
{
|
{
|
||||||
public ulong GuildId { get; internal set; }
|
public ulong GuildId { get; internal set; }
|
||||||
public ulong ChannelId { get; internal set; }
|
public ulong ChannelId { get; internal set; }
|
||||||
|
public ulong WebhookId { get; internal set; }
|
||||||
|
public string HttpMethod { get; internal set; }
|
||||||
|
|
||||||
internal BucketIds(ulong guildId = 0, ulong channelId = 0)
|
internal BucketIds(ulong guildId = 0, ulong channelId = 0, ulong webhookId = 0)
|
||||||
{
|
{
|
||||||
GuildId = guildId;
|
GuildId = guildId;
|
||||||
ChannelId = channelId;
|
ChannelId = channelId;
|
||||||
|
WebhookId = webhookId;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object[] ToArray()
|
internal object[] ToArray()
|
||||||
=> new object[] { GuildId, ChannelId };
|
=> new object[] { HttpMethod, GuildId, ChannelId, WebhookId };
|
||||||
|
|
||||||
|
internal Dictionary<string, string> ToMajorParametersDictionary()
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<string, string>();
|
||||||
|
if (GuildId != 0)
|
||||||
|
dict["GuildId"] = GuildId.ToString();
|
||||||
|
if (ChannelId != 0)
|
||||||
|
dict["ChannelId"] = ChannelId.ToString();
|
||||||
|
if (WebhookId != 0)
|
||||||
|
dict["WebhookId"] = WebhookId.ToString();
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
internal static int? GetIndex(string name)
|
internal static int? GetIndex(string name)
|
||||||
{
|
{
|
||||||
switch (name)
|
switch (name)
|
||||||
{
|
{
|
||||||
case "guildId": return 0;
|
case "httpMethod": return 0;
|
||||||
case "channelId": return 1;
|
case "guildId": return 1;
|
||||||
|
case "channelId": return 2;
|
||||||
|
case "webhookId": return 3;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1491,18 +1511,19 @@ namespace Discord.API
|
|||||||
{
|
{
|
||||||
return endpointExpr.Compile()();
|
return endpointExpr.Compile()();
|
||||||
}
|
}
|
||||||
private static string GetBucketId(BucketIds ids, Expression<Func<string>> endpointExpr, string callingMethod)
|
private static BucketId GetBucketId(string httpMethod, BucketIds ids, Expression<Func<string>> endpointExpr, string callingMethod)
|
||||||
{
|
{
|
||||||
|
ids.HttpMethod ??= httpMethod;
|
||||||
return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr))(ids);
|
return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr))(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Func<BucketIds, string> CreateBucketId(Expression<Func<string>> endpoint)
|
private static Func<BucketIds, BucketId> CreateBucketId(Expression<Func<string>> endpoint)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
//Is this a constant string?
|
//Is this a constant string?
|
||||||
if (endpoint.Body.NodeType == ExpressionType.Constant)
|
if (endpoint.Body.NodeType == ExpressionType.Constant)
|
||||||
return x => (endpoint.Body as ConstantExpression).Value.ToString();
|
return x => BucketId.Create(x.HttpMethod, (endpoint.Body as ConstantExpression).Value.ToString(), x.ToMajorParametersDictionary());
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
var methodCall = endpoint.Body as MethodCallExpression;
|
var methodCall = endpoint.Body as MethodCallExpression;
|
||||||
@@ -1539,7 +1560,7 @@ namespace Discord.API
|
|||||||
|
|
||||||
var mappedId = BucketIds.GetIndex(fieldName);
|
var mappedId = BucketIds.GetIndex(fieldName);
|
||||||
|
|
||||||
if(!mappedId.HasValue && rightIndex != endIndex && format.Length > rightIndex + 1 && format[rightIndex + 1] == '/') //Ignore the next slash
|
if (!mappedId.HasValue && rightIndex != endIndex && format.Length > rightIndex + 1 && format[rightIndex + 1] == '/') //Ignore the next slash
|
||||||
rightIndex++;
|
rightIndex++;
|
||||||
|
|
||||||
if (mappedId.HasValue)
|
if (mappedId.HasValue)
|
||||||
@@ -1552,7 +1573,7 @@ namespace Discord.API
|
|||||||
|
|
||||||
format = builder.ToString();
|
format = builder.ToString();
|
||||||
|
|
||||||
return x => string.Format(format, x.ToArray());
|
return x => BucketId.Create(x.HttpMethod, string.Format(format, x.ToArray()), x.ToMajorParametersDictionary());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ namespace Discord.Net.Queue
|
|||||||
internal struct ClientBucket
|
internal struct ClientBucket
|
||||||
{
|
{
|
||||||
private static readonly ImmutableDictionary<ClientBucketType, ClientBucket> DefsByType;
|
private static readonly ImmutableDictionary<ClientBucketType, ClientBucket> DefsByType;
|
||||||
private static readonly ImmutableDictionary<string, ClientBucket> DefsById;
|
private static readonly ImmutableDictionary<BucketId, ClientBucket> DefsById;
|
||||||
|
|
||||||
static ClientBucket()
|
static ClientBucket()
|
||||||
{
|
{
|
||||||
var buckets = new[]
|
var buckets = new[]
|
||||||
{
|
{
|
||||||
new ClientBucket(ClientBucketType.Unbucketed, "<unbucketed>", 10, 10),
|
new ClientBucket(ClientBucketType.Unbucketed, BucketId.Create(null, "<unbucketed>", null), 10, 10),
|
||||||
new ClientBucket(ClientBucketType.SendEdit, "<send_edit>", 10, 10)
|
new ClientBucket(ClientBucketType.SendEdit, BucketId.Create(null, "<send_edit>", null), 10, 10)
|
||||||
};
|
};
|
||||||
|
|
||||||
var builder = ImmutableDictionary.CreateBuilder<ClientBucketType, ClientBucket>();
|
var builder = ImmutableDictionary.CreateBuilder<ClientBucketType, ClientBucket>();
|
||||||
@@ -25,21 +25,21 @@ namespace Discord.Net.Queue
|
|||||||
builder.Add(bucket.Type, bucket);
|
builder.Add(bucket.Type, bucket);
|
||||||
DefsByType = builder.ToImmutable();
|
DefsByType = builder.ToImmutable();
|
||||||
|
|
||||||
var builder2 = ImmutableDictionary.CreateBuilder<string, ClientBucket>();
|
var builder2 = ImmutableDictionary.CreateBuilder<BucketId, ClientBucket>();
|
||||||
foreach (var bucket in buckets)
|
foreach (var bucket in buckets)
|
||||||
builder2.Add(bucket.Id, bucket);
|
builder2.Add(bucket.Id, bucket);
|
||||||
DefsById = builder2.ToImmutable();
|
DefsById = builder2.ToImmutable();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ClientBucket Get(ClientBucketType type) => DefsByType[type];
|
public static ClientBucket Get(ClientBucketType type) => DefsByType[type];
|
||||||
public static ClientBucket Get(string id) => DefsById[id];
|
public static ClientBucket Get(BucketId id) => DefsById[id];
|
||||||
|
|
||||||
public ClientBucketType Type { get; }
|
public ClientBucketType Type { get; }
|
||||||
public string Id { get; }
|
public BucketId Id { get; }
|
||||||
public int WindowCount { get; }
|
public int WindowCount { get; }
|
||||||
public int WindowSeconds { get; }
|
public int WindowSeconds { get; }
|
||||||
|
|
||||||
public ClientBucket(ClientBucketType type, string id, int count, int seconds)
|
public ClientBucket(ClientBucketType type, BucketId id, int count, int seconds)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
Id = id;
|
Id = id;
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ namespace Discord.Net.Queue
|
|||||||
{
|
{
|
||||||
internal class RequestQueue : IDisposable
|
internal class RequestQueue : IDisposable
|
||||||
{
|
{
|
||||||
public event Func<string, RateLimitInfo?, Task> RateLimitTriggered;
|
public event Func<BucketId, RateLimitInfo?, Task> RateLimitTriggered;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, RequestBucket> _buckets;
|
private readonly ConcurrentDictionary<BucketId, object> _buckets;
|
||||||
private readonly SemaphoreSlim _tokenLock;
|
private readonly SemaphoreSlim _tokenLock;
|
||||||
private readonly CancellationTokenSource _cancelTokenSource; //Dispose token
|
private readonly CancellationTokenSource _cancelTokenSource; //Dispose token
|
||||||
private CancellationTokenSource _clearToken;
|
private CancellationTokenSource _clearToken;
|
||||||
@@ -34,7 +34,7 @@ namespace Discord.Net.Queue
|
|||||||
_requestCancelToken = CancellationToken.None;
|
_requestCancelToken = CancellationToken.None;
|
||||||
_parentToken = CancellationToken.None;
|
_parentToken = CancellationToken.None;
|
||||||
|
|
||||||
_buckets = new ConcurrentDictionary<string, RequestBucket>();
|
_buckets = new ConcurrentDictionary<BucketId, object>();
|
||||||
|
|
||||||
_cleanupTask = RunCleanup();
|
_cleanupTask = RunCleanup();
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ namespace Discord.Net.Queue
|
|||||||
else
|
else
|
||||||
request.Options.CancelToken = _requestCancelToken;
|
request.Options.CancelToken = _requestCancelToken;
|
||||||
|
|
||||||
var bucket = GetOrCreateBucket(request.Options.BucketId, request);
|
var bucket = GetOrCreateBucket(request.Options, request);
|
||||||
var result = await bucket.SendAsync(request).ConfigureAwait(false);
|
var result = await bucket.SendAsync(request).ConfigureAwait(false);
|
||||||
createdTokenSource?.Dispose();
|
createdTokenSource?.Dispose();
|
||||||
return result;
|
return result;
|
||||||
@@ -110,14 +110,32 @@ namespace Discord.Net.Queue
|
|||||||
_waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0));
|
_waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private RequestBucket GetOrCreateBucket(string id, RestRequest request)
|
private RequestBucket GetOrCreateBucket(RequestOptions options, RestRequest request)
|
||||||
{
|
{
|
||||||
return _buckets.GetOrAdd(id, x => new RequestBucket(this, request, x));
|
var bucketId = options.BucketId;
|
||||||
|
object obj = _buckets.GetOrAdd(bucketId, x => new RequestBucket(this, request, x));
|
||||||
|
if (obj is BucketId hashBucket)
|
||||||
|
{
|
||||||
|
options.BucketId = hashBucket;
|
||||||
|
return (RequestBucket)_buckets.GetOrAdd(hashBucket, x => new RequestBucket(this, request, x));
|
||||||
}
|
}
|
||||||
internal async Task RaiseRateLimitTriggered(string bucketId, RateLimitInfo? info)
|
return (RequestBucket)obj;
|
||||||
|
}
|
||||||
|
internal async Task RaiseRateLimitTriggered(BucketId bucketId, RateLimitInfo? info)
|
||||||
{
|
{
|
||||||
await RateLimitTriggered(bucketId, info).ConfigureAwait(false);
|
await RateLimitTriggered(bucketId, info).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
internal (RequestBucket, BucketId) UpdateBucketHash(BucketId id, string discordHash)
|
||||||
|
{
|
||||||
|
if (!id.IsHashBucket)
|
||||||
|
{
|
||||||
|
var bucket = BucketId.Create(discordHash, id);
|
||||||
|
var hashReqQueue = (RequestBucket)_buckets.GetOrAdd(bucket, _buckets[id]);
|
||||||
|
_buckets.AddOrUpdate(id, bucket, (oldBucket, oldObj) => bucket);
|
||||||
|
return (hashReqQueue, bucket);
|
||||||
|
}
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RunCleanup()
|
private async Task RunCleanup()
|
||||||
{
|
{
|
||||||
@@ -126,11 +144,16 @@ namespace Discord.Net.Queue
|
|||||||
while (!_cancelTokenSource.IsCancellationRequested)
|
while (!_cancelTokenSource.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
foreach (var bucket in _buckets.Select(x => x.Value))
|
foreach (var bucket in _buckets.Where(x => x.Value is RequestBucket).Select(x => (RequestBucket)x.Value))
|
||||||
{
|
{
|
||||||
if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0)
|
if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0)
|
||||||
|
{
|
||||||
|
if (bucket.Id.IsHashBucket)
|
||||||
|
foreach (var redirectBucket in _buckets.Where(x => x.Value == bucket.Id).Select(x => (BucketId)x.Value))
|
||||||
|
_buckets.TryRemove(redirectBucket, out _); //remove redirections if hash bucket
|
||||||
_buckets.TryRemove(bucket.Id, out _);
|
_buckets.TryRemove(bucket.Id, out _);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
await Task.Delay(60000, _cancelTokenSource.Token).ConfigureAwait(false); //Runs each minute
|
await Task.Delay(60000, _cancelTokenSource.Token).ConfigureAwait(false); //Runs each minute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ namespace Discord.Net.Queue
|
|||||||
private readonly RequestQueue _queue;
|
private readonly RequestQueue _queue;
|
||||||
private int _semaphore;
|
private int _semaphore;
|
||||||
private DateTimeOffset? _resetTick;
|
private DateTimeOffset? _resetTick;
|
||||||
|
private RequestBucket _redirectBucket;
|
||||||
|
|
||||||
public string Id { get; private set; }
|
public BucketId Id { get; private set; }
|
||||||
public int WindowCount { get; private set; }
|
public int WindowCount { get; private set; }
|
||||||
public DateTimeOffset LastAttemptAt { get; private set; }
|
public DateTimeOffset LastAttemptAt { get; private set; }
|
||||||
|
|
||||||
public RequestBucket(RequestQueue queue, RestRequest request, string id)
|
public RequestBucket(RequestQueue queue, RestRequest request, BucketId id)
|
||||||
{
|
{
|
||||||
_queue = queue;
|
_queue = queue;
|
||||||
Id = id;
|
Id = id;
|
||||||
@@ -32,7 +33,7 @@ namespace Discord.Net.Queue
|
|||||||
_lock = new object();
|
_lock = new object();
|
||||||
|
|
||||||
if (request.Options.IsClientBucket)
|
if (request.Options.IsClientBucket)
|
||||||
WindowCount = ClientBucket.Get(request.Options.BucketId).WindowCount;
|
WindowCount = ClientBucket.Get(Id).WindowCount;
|
||||||
else
|
else
|
||||||
WindowCount = 1; //Only allow one request until we get a header back
|
WindowCount = 1; //Only allow one request until we get a header back
|
||||||
_semaphore = WindowCount;
|
_semaphore = WindowCount;
|
||||||
@@ -52,6 +53,8 @@ namespace Discord.Net.Queue
|
|||||||
{
|
{
|
||||||
await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false);
|
await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false);
|
||||||
await EnterAsync(id, request).ConfigureAwait(false);
|
await EnterAsync(id, request).ConfigureAwait(false);
|
||||||
|
if (_redirectBucket != null)
|
||||||
|
return await _redirectBucket.SendAsync(request);
|
||||||
|
|
||||||
#if DEBUG_LIMITS
|
#if DEBUG_LIMITS
|
||||||
Debug.WriteLine($"[{id}] Sending...");
|
Debug.WriteLine($"[{id}] Sending...");
|
||||||
@@ -160,6 +163,9 @@ namespace Discord.Net.Queue
|
|||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
if (_redirectBucket != null)
|
||||||
|
break;
|
||||||
|
|
||||||
if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested)
|
if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
if (!isRateLimited)
|
if (!isRateLimited)
|
||||||
@@ -175,7 +181,8 @@ namespace Discord.Net.Queue
|
|||||||
}
|
}
|
||||||
|
|
||||||
DateTimeOffset? timeoutAt = request.TimeoutAt;
|
DateTimeOffset? timeoutAt = request.TimeoutAt;
|
||||||
if (windowCount > 0 && Interlocked.Decrement(ref _semaphore) < 0)
|
int semaphore = Interlocked.Decrement(ref _semaphore);
|
||||||
|
if (windowCount > 0 && semaphore < 0)
|
||||||
{
|
{
|
||||||
if (!isRateLimited)
|
if (!isRateLimited)
|
||||||
{
|
{
|
||||||
@@ -210,20 +217,52 @@ namespace Discord.Net.Queue
|
|||||||
}
|
}
|
||||||
#if DEBUG_LIMITS
|
#if DEBUG_LIMITS
|
||||||
else
|
else
|
||||||
Debug.WriteLine($"[{id}] Entered Semaphore ({_semaphore}/{WindowCount} remaining)");
|
Debug.WriteLine($"[{id}] Entered Semaphore ({semaphore}/{WindowCount} remaining)");
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429)
|
private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429, bool redirected = false)
|
||||||
{
|
{
|
||||||
if (WindowCount == 0)
|
if (WindowCount == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
|
if (redirected)
|
||||||
|
{
|
||||||
|
Interlocked.Decrement(ref _semaphore); //we might still hit a real ratelimit if all tickets were already taken, can't do much about it since we didn't know they were the same
|
||||||
|
#if DEBUG_LIMITS
|
||||||
|
Debug.WriteLine($"[{id}] Decrease Semaphore");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
bool hasQueuedReset = _resetTick != null;
|
bool hasQueuedReset = _resetTick != null;
|
||||||
|
|
||||||
|
if (info.Bucket != null && !redirected)
|
||||||
|
{
|
||||||
|
(RequestBucket, BucketId) hashBucket = _queue.UpdateBucketHash(Id, info.Bucket);
|
||||||
|
if (!(hashBucket.Item1 is null) && !(hashBucket.Item2 is null))
|
||||||
|
{
|
||||||
|
if (hashBucket.Item1 == this) //this bucket got promoted to a hash queue
|
||||||
|
{
|
||||||
|
Id = hashBucket.Item2;
|
||||||
|
#if DEBUG_LIMITS
|
||||||
|
Debug.WriteLine($"[{id}] Promoted to Hash Bucket ({hashBucket.Item2})");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_redirectBucket = hashBucket.Item1; //this request should be part of another bucket, this bucket will be disabled, redirect everything
|
||||||
|
_redirectBucket.UpdateRateLimit(id, request, info, is429, redirected: true); //update the hash bucket ratelimit
|
||||||
|
#if DEBUG_LIMITS
|
||||||
|
Debug.WriteLine($"[{id}] Redirected to {_redirectBucket.Id}");
|
||||||
|
#endif
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (info.Limit.HasValue && WindowCount != info.Limit.Value)
|
if (info.Limit.HasValue && WindowCount != info.Limit.Value)
|
||||||
{
|
{
|
||||||
WindowCount = info.Limit.Value;
|
WindowCount = info.Limit.Value;
|
||||||
@@ -233,7 +272,6 @@ namespace Discord.Net.Queue
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
||||||
DateTimeOffset? resetTick = null;
|
DateTimeOffset? resetTick = null;
|
||||||
|
|
||||||
//Using X-RateLimit-Remaining causes a race condition
|
//Using X-RateLimit-Remaining causes a race condition
|
||||||
@@ -253,13 +291,15 @@ namespace Discord.Net.Queue
|
|||||||
else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false))
|
else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false))
|
||||||
{
|
{
|
||||||
resetTick = DateTimeOffset.UtcNow.Add(info.ResetAfter.Value);
|
resetTick = DateTimeOffset.UtcNow.Add(info.ResetAfter.Value);
|
||||||
|
#if DEBUG_LIMITS
|
||||||
|
Debug.WriteLine($"[{id}] Reset-After: {info.ResetAfter.Value} ({info.ResetAfter?.TotalMilliseconds} ms)");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
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
|
/* 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);
|
||||||
*/
|
*/
|
||||||
@@ -269,17 +309,17 @@ namespace Discord.Net.Queue
|
|||||||
Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)");
|
Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
else if (request.Options.IsClientBucket && request.Options.BucketId != null)
|
else if (request.Options.IsClientBucket && Id != null)
|
||||||
{
|
{
|
||||||
resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(request.Options.BucketId).WindowSeconds);
|
resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(Id).WindowSeconds);
|
||||||
#if DEBUG_LIMITS
|
#if DEBUG_LIMITS
|
||||||
Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)");
|
Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(Id).WindowSeconds * 1000} ms)");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resetTick == null)
|
if (resetTick == null)
|
||||||
{
|
{
|
||||||
WindowCount = 0; //No rate limit info, disable limits on this bucket (should only ever happen with a user token)
|
WindowCount = 0; //No rate limit info, disable limits on this bucket
|
||||||
#if DEBUG_LIMITS
|
#if DEBUG_LIMITS
|
||||||
Debug.WriteLine($"[{id}] Disabled Semaphore");
|
Debug.WriteLine($"[{id}] Disabled Semaphore");
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace Discord.Net
|
|||||||
public int? RetryAfter { get; }
|
public int? RetryAfter { get; }
|
||||||
public DateTimeOffset? Reset { get; }
|
public DateTimeOffset? Reset { get; }
|
||||||
public TimeSpan? ResetAfter { get; }
|
public TimeSpan? ResetAfter { get; }
|
||||||
|
public string Bucket { get; }
|
||||||
public TimeSpan? Lag { get; }
|
public TimeSpan? Lag { get; }
|
||||||
|
|
||||||
internal RateLimitInfo(Dictionary<string, string> headers)
|
internal RateLimitInfo(Dictionary<string, string> headers)
|
||||||
@@ -27,7 +28,8 @@ namespace Discord.Net
|
|||||||
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) &&
|
ResetAfter = headers.TryGetValue("X-RateLimit-Reset-After", out temp) &&
|
||||||
float.TryParse(temp, out var resetAfter) ? TimeSpan.FromMilliseconds((long)(resetAfter * 1000)) : (TimeSpan?)null;
|
double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var resetAfter) ? TimeSpan.FromMilliseconds((long)(resetAfter * 1000)) : (TimeSpan?)null;
|
||||||
|
Bucket = headers.TryGetValue("X-RateLimit-Bucket", out temp) ? temp : 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ namespace Discord.Webhook
|
|||||||
ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
|
ApiClient.RequestQueue.RateLimitTriggered += async (id, info) =>
|
||||||
{
|
{
|
||||||
if (info == null)
|
if (info == null)
|
||||||
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
|
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
|
||||||
else
|
else
|
||||||
await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
|
await _restLogger.WarningAsync($"Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false);
|
||||||
};
|
};
|
||||||
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
|
ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user