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); */