diff --git a/src/Discord.Net.Core/Net/BucketId.cs b/src/Discord.Net.Core/Net/BucketId.cs
index 00562a0d1..27df5e0ce 100644
--- a/src/Discord.Net.Core/Net/BucketId.cs
+++ b/src/Discord.Net.Core/Net/BucketId.cs
@@ -5,41 +5,93 @@ using System.Linq;
namespace Discord.Net
{
+ ///
+ /// Represents a ratelimit bucket.
+ ///
public class BucketId : IEquatable
{
+ ///
+ /// Gets the http method used to make the request if available.
+ ///
public string HttpMethod { get; }
+ ///
+ /// Gets the endpoint that is going to be requested if available.
+ ///
public string Endpoint { get; }
- public IOrderedEnumerable> MajorParams { get; }
+ ///
+ /// Gets the major parameters of the route.
+ ///
+ public IOrderedEnumerable> MajorParameters { get; }
+ ///
+ /// Gets the hash of this bucket.
+ ///
+ ///
+ /// The hash is provided by Discord to group ratelimits.
+ ///
public string BucketHash { get; }
-
+ ///
+ /// Gets if this bucket is a hash type.
+ ///
public bool IsHashBucket { get => BucketHash != null; }
- private BucketId(string httpMethod, string endpoint, IEnumerable> majorParams, string bucketHash)
+ private BucketId(string httpMethod, string endpoint, IEnumerable> majorParameters, string bucketHash)
{
HttpMethod = httpMethod;
Endpoint = endpoint;
- MajorParams = majorParams.OrderBy(x => x.Key);
+ MajorParameters = majorParameters.OrderBy(x => x.Key);
BucketHash = bucketHash;
}
+ ///
+ /// Creates a new based on the
+ /// and .
+ ///
+ /// Http method used to make the request.
+ /// Endpoint that is going to receive requests.
+ /// Major parameters of the route of this endpoint.
+ ///
+ /// A based on the
+ /// and the with the provided data.
+ ///
public static BucketId Create(string httpMethod, string endpoint, Dictionary majorParams)
{
- Preconditions.NotNullOrWhitespace(httpMethod, nameof(httpMethod));
Preconditions.NotNullOrWhitespace(endpoint, nameof(endpoint));
majorParams ??= new Dictionary();
return new BucketId(httpMethod, endpoint, majorParams, null);
}
+ ///
+ /// Creates a new based on a
+ /// and a previous .
+ ///
+ /// Bucket hash provided by Discord.
+ /// that is going to be upgraded to a hash type.
+ ///
+ /// A based on the
+ /// and .
+ ///
public static BucketId Create(string hash, BucketId oldBucket)
{
Preconditions.NotNullOrWhitespace(hash, nameof(hash));
Preconditions.NotNull(oldBucket, nameof(oldBucket));
- return new BucketId(null, null, oldBucket.MajorParams, hash);
+ return new BucketId(null, null, oldBucket.MajorParameters, hash);
}
+ ///
+ /// Gets the string that will define this bucket as a hash based one.
+ ///
+ ///
+ /// A that defines this bucket as a hash based one.
+ ///
public string GetBucketHash()
- => IsHashBucket ? $"{BucketHash}:{string.Join("/", MajorParams.Select(x => x.Value))}" : null;
+ => IsHashBucket ? $"{BucketHash}:{string.Join("/", MajorParameters.Select(x => x.Value))}" : null;
+ ///
+ /// Gets the string that will define this bucket as an endpoint based one.
+ ///
+ ///
+ /// A that defines this bucket as an endpoint based one.
+ ///
public string GetUniqueEndpoint()
=> HttpMethod != null ? $"{HttpMethod} {Endpoint}" : Endpoint;
@@ -47,7 +99,7 @@ namespace Discord.Net
=> Equals(obj as BucketId);
public override int GetHashCode()
- => IsHashBucket ? (BucketHash, string.Join("/", MajorParams.Select(x => x.Value))).GetHashCode() : (HttpMethod, Endpoint).GetHashCode();
+ => IsHashBucket ? (BucketHash, string.Join("/", MajorParameters.Select(x => x.Value))).GetHashCode() : (HttpMethod, Endpoint).GetHashCode();
public override string ToString()
=> GetBucketHash() ?? GetUniqueEndpoint();
diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs
index 80c2e68ec..691ac77c0 100644
--- a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs
+++ b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs
@@ -125,16 +125,16 @@ namespace Discord.Net.Queue
{
await RateLimitTriggered(bucketId, info).ConfigureAwait(false);
}
- internal BucketId UpdateBucketHash(BucketId id, string discordHash)
+ internal (RequestBucket, BucketId) UpdateBucketHash(BucketId id, string discordHash)
{
if (!id.IsHashBucket)
{
var bucket = BucketId.Create(discordHash, id);
- _buckets.GetOrAdd(bucket, _buckets[id]);
+ var hashReqQueue = (RequestBucket)_buckets.GetOrAdd(bucket, _buckets[id]);
_buckets.AddOrUpdate(id, bucket, (oldBucket, oldObj) => bucket);
- return bucket;
+ return (hashReqQueue, bucket);
}
- return null;
+ return (null, null);
}
private async Task RunCleanup()
diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
index 0c06c7e04..682f8383f 100644
--- a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
+++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
@@ -19,6 +19,7 @@ namespace Discord.Net.Queue
private readonly RequestQueue _queue;
private int _semaphore;
private DateTimeOffset? _resetTick;
+ private RequestBucket _redirectBucket;
public BucketId Id { get; private set; }
public int WindowCount { get; private set; }
@@ -52,6 +53,8 @@ namespace Discord.Net.Queue
{
await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false);
await EnterAsync(id, request).ConfigureAwait(false);
+ if (_redirectBucket != null)
+ return await _redirectBucket.SendAsync(request);
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Sending...");
@@ -160,6 +163,9 @@ namespace Discord.Net.Queue
while (true)
{
+ if (_redirectBucket != null)
+ break;
+
if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested)
{
if (!isRateLimited)
@@ -216,13 +222,15 @@ namespace Discord.Net.Queue
}
}
- 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)
return;
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
bool hasQueuedReset = _resetTick != null;
if (info.Limit.HasValue && WindowCount != info.Limit.Value)
{
@@ -233,10 +241,20 @@ namespace Discord.Net.Queue
#endif
}
- if (info.Bucket != null)
- Id = _queue.UpdateBucketHash(request.Options.BucketId, info.Bucket) ?? Id;
+ if (info.Bucket != null && !redirected)
+ {
+ (RequestBucket, BucketId) hashBucket = _queue.UpdateBucketHash(request.Options.BucketId, info.Bucket);
+ if (hashBucket.Item1 is null || hashBucket.Item2 is null)
+ return;
+ if (hashBucket.Item1 == this) //this bucket got promoted to a hash queue
+ Id = hashBucket.Item2;
+ 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
+ }
+ }
- var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
DateTimeOffset? resetTick = null;
//Using X-RateLimit-Remaining causes a race condition
@@ -253,16 +271,15 @@ 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.UtcNow.Add(info.ResetAfter.Value);
- }
+ else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false))
+ {
+ resetTick = DateTimeOffset.UtcNow.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
-
+ /* millisecond precision makes this unnecessary, retaining in case of regression
if (request.Options.IsReactionBucket)
resetTick = DateTimeOffset.Now.AddMilliseconds(250);
*/