private async Task <TaskCompletionSource <bool> > WaitForInitialRateLimit(RateLimitBucket bucket) { while (!bucket._limitValid) { if (bucket._limitTesting == 0) { if (Interlocked.CompareExchange(ref bucket._limitTesting, 1, 0) == 0) { // if we got here when the first request was just finishing, we must not create the waiter task as it would signel ExecureRequestAsync to bypass rate limiting if (bucket._limitValid) { return(null); } // allow exactly one request to go through without having rate limits available var ratelimitsTcs = new TaskCompletionSource <bool>(); bucket._limitTestFinished = ratelimitsTcs.Task; return(ratelimitsTcs); } } // it can take a couple of cycles for the task to be allocated, so wait until it happens or we are no longer probing for the limits Task waitTask = null; while (bucket._limitTesting != 0 && (waitTask = bucket._limitTestFinished) == null) { await Task.Yield(); } if (waitTask != null) { await waitTask.ConfigureAwait(false); } // if the request failed and the response did not have rate limit headers we have allow the next request and wait again, thus this is a loop here } return(null); }
internal MultipartWebRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, IDictionary <string, string> headers = null, IDictionary <string, string> values = null, IDictionary <string, Stream> files = null) : base(client, bucket, url, method, headers) { this.Values = values != null ? new ReadOnlyDictionary <string, string>(values) : null; this.Files = files != null ? new ReadOnlyDictionary <string, Stream>(files) : null; }
internal MultipartWebRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary <string, string> headers = null, IReadOnlyDictionary <string, string> values = null, IReadOnlyCollection <DiscordMessageFile> files = null, double?ratelimit_wait_override = null) : base(client, bucket, url, method, route, headers, ratelimit_wait_override) { this.Values = values; this.Files = files.ToDictionary(x => x.FileName, x => x.Stream); }
internal MultipartWebRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, IDictionary <string, string> headers = null, IDictionary <string, string> values = null, IDictionary <string, Stream> files = null, double?ratelimit_wait_override = null) : base(client, bucket, url, method, headers, ratelimit_wait_override) { Values = values != null ? new ReadOnlyDictionary <string, string>(values) : null; Files = files != null ? new ReadOnlyDictionary <string, Stream>(files) : null; }
internal MultipartWebRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary <string, string> headers = null, IReadOnlyDictionary <string, string> values = null, IReadOnlyCollection <DiscordMessageFile> files = null, double?ratelimit_wait_override = null, bool removeFileCount = false) : base(client, bucket, url, method, route, headers, ratelimit_wait_override) { this.Values = values; this.Files = files; this._removeFileCount = removeFileCount; }
/// <summary> /// Creates a new <see cref="BaseRestRequest"/> with specified parameters. /// </summary> /// <param name="client"><see cref="DiscordClient"/> from which this request originated.</param> /// <param name="bucket">Rate limit bucket to place this request in.</param> /// <param name="url">Uri to which this request is going to be sent to.</param> /// <param name="method">Method to use for this request,</param> /// <param name="headers">Additional headers for this request.</param> internal BaseRestRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, IDictionary <string, string> headers = null) { this.Discord = client; this.RateLimitBucket = bucket; this.RequestTaskSource = new TaskCompletionSource <RestResponse>(); this.Url = url; this.Method = method; this.Headers = headers != null ? new ReadOnlyDictionary <string, string>(headers) : null; }
private void FailInitialRateLimitTest(RateLimitBucket bucket, TaskCompletionSource <bool> ratelimitTcs) { if (ratelimitTcs == null) { return; } bucket._limitValid = false; bucket._limitTestFinished = null; bucket._limitTesting = 0; // no need to wait on all the potentially waiting tasks Task.Run(() => ratelimitTcs.TrySetResult(false)); }
public RateLimitBucket GetBucket(RestRequestMethod method, string route, object route_params, out string url) { var rparams_props = route_params.GetType() .GetTypeInfo() .DeclaredProperties; var rparams = new Dictionary <string, string>(); foreach (var xp in rparams_props) { var val = xp.GetValue(route_params); if (val is string xs) { rparams[xp.Name] = xs; } else if (val is DateTime dt) { rparams[xp.Name] = dt.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture); } else if (val is DateTimeOffset dto) { rparams[xp.Name] = dto.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture); } else if (val is IFormattable xf) { rparams[xp.Name] = xf.ToString(null, CultureInfo.InvariantCulture); } else { rparams[xp.Name] = val.ToString(); } } var guild_id = rparams.ContainsKey("guild_id") ? rparams["guild_id"] : ""; var channel_id = rparams.ContainsKey("channel_id") ? rparams["channel_id"] : ""; var webhook_id = rparams.ContainsKey("webhook_id") ? rparams["webhook_id"] : ""; var id = RateLimitBucket.GenerateId(method, route, guild_id, channel_id, webhook_id); RateLimitBucket bucket = null; bucket = this.Buckets.FirstOrDefault(xb => xb.BucketId == id); if (bucket == null) { bucket = new RateLimitBucket(method, route, guild_id, channel_id, webhook_id); this.Buckets.Add(bucket); } url = RouteArgumentRegex.Replace(route, xm => rparams[xm.Groups[1].Value]); return(bucket); }
/// <summary> /// Creates a new <see cref="BaseRestRequest"/> with specified parameters. /// </summary> /// <param name="client"><see cref="DiscordClient"/> from which this request originated.</param> /// <param name="bucket">Rate limit bucket to place this request in.</param> /// <param name="url">Uri to which this request is going to be sent to.</param> /// <param name="method">Method to use for this request,</param> /// <param name="headers">Additional headers for this request.</param> /// <param name="ratelimitWaitOverride">Override for ratelimit bucket wait time.</param> internal BaseRestRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, IDictionary <string, string> headers = null, double?ratelimitWaitOverride = null) { this.Discord = client; this.RateLimitBucket = bucket; this.RequestTaskSource = new TaskCompletionSource <RestResponse>(); this.Url = url; this.Method = method; this.RateLimitWaitOverride = ratelimitWaitOverride; if (headers != null) { headers = headers.Select(x => new KeyValuePair <string, string>(x.Key, Uri.EscapeDataString(x.Value))) .ToDictionary(x => x.Key, x => x.Value); this.Headers = new ReadOnlyDictionary <string, string>(headers); } }
public RateLimitBucket GetBucket(RestRequestMethod method, string route, object route_params, out string url) { var rparams_props = route_params.GetType() .GetTypeInfo() .DeclaredProperties; var rparams = new Dictionary <string, string>(); foreach (var xp in rparams_props) { var val = xp.GetValue(route_params); if (val is string xs) { rparams[xp.Name] = xs; } else if (val is DateTime dt) { rparams[xp.Name] = dt.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture); } else if (val is DateTimeOffset dto) { rparams[xp.Name] = dto.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture); } else if (val is IFormattable xf) { rparams[xp.Name] = xf.ToString(null, CultureInfo.InvariantCulture); } else { rparams[xp.Name] = val.ToString(); } } var guild_id = rparams.ContainsKey("guild_id") ? rparams["guild_id"] : ""; var channel_id = rparams.ContainsKey("channel_id") ? rparams["channel_id"] : ""; var webhook_id = rparams.ContainsKey("webhook_id") ? rparams["webhook_id"] : ""; var id = RateLimitBucket.GenerateId(method, route, guild_id, channel_id, webhook_id); // using the GetOrAdd version with the factory has no advantages as it will allocate the delegate, closure object and bucket (if needed) instead of just the bucket var bucket = this.Buckets.GetOrAdd(id, new RateLimitBucket(method, route, guild_id, channel_id, webhook_id)); url = RouteArgumentRegex.Replace(route, xm => rparams[xm.Groups[1].Value]); return(bucket); }
private void UpdateHashCaches(BaseRestRequest request, RateLimitBucket bucket, string newHash = null) { var hashKey = RateLimitBucket.GenerateHashKey(request.Method, request.Route); if (!this.RoutesToHashes.TryGetValue(hashKey, out var oldHash)) { return; } // This is an unlimited bucket, which we don't need to keep track of. if (newHash == null) { _ = this.RoutesToHashes.TryRemove(hashKey, out _); _ = this.HashesToBuckets.TryRemove(bucket.BucketId, out _); return; } // Only update the hash once, due to a bug on Discord's end. // This will cause issues if the bucket hashes are dynamically changed from the API while running, // in which case, Dispose will need to be called to clear the caches. if (bucket._isUnlimited && newHash != oldHash) { this.Logger.LogDebug(LoggerEvents.RestHashMover, "Updating hash in {Hash}: \"{OldHash}\" -> \"{NewHash}\"", hashKey, oldHash, newHash); var bucketId = RateLimitBucket.GenerateBucketId(newHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId); _ = this.RoutesToHashes.AddOrUpdate(hashKey, newHash, (key, oldHash) => { bucket.Hash = newHash; var oldBucketId = RateLimitBucket.GenerateBucketId(oldHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId); // Remove the old unlimited bucket. _ = this.HashesToBuckets.TryRemove(oldBucketId, out _); _ = this.HashesToBuckets.AddOrUpdate(bucketId, bucket, (key, oldBucket) => bucket); return(newHash); }); } return; }
private void FailInitialRateLimitTest(RateLimitBucket bucket, TaskCompletionSource <bool> ratelimitTcs, bool resetToInitial = false) { if (ratelimitTcs == null && !resetToInitial) { return; } bucket._limitValid = false; bucket._limitTestFinished = null; bucket._limitTesting = 0; //Reset to initial values. if (resetToInitial) { bucket.Maximum = 0; bucket._remaining = 0; return; } // no need to wait on all the potentially waiting tasks Task.Run(() => ratelimitTcs.TrySetResult(false)); }
public RateLimitBucket GetBucket(RestRequestMethod method, string route, object route_params, out string url) { var rparams = route_params.GetType() .GetTypeInfo() .DeclaredProperties .ToDictionary(xp => xp.Name, xp => xp.GetValue(route_params) as string); var guild_id = rparams.ContainsKey("guild_id") ? rparams["guild_id"] : ""; var channel_id = rparams.ContainsKey("channel_id") ? rparams["channel_id"] : ""; var id = RateLimitBucket.GenerateId(method, route, guild_id, channel_id); RateLimitBucket bucket = null; bucket = this.Buckets.FirstOrDefault(xb => xb.BucketId == id); if (bucket == null) { bucket = new RateLimitBucket(method, route, guild_id, channel_id); this.Buckets.Add(bucket); } url = RouteArgumentRegex.Replace(route, xm => rparams[xm.Groups[1].Value]); return(bucket); }
// to allow proper rescheduling of the first request from a bucket private async Task ExecuteRequestAsync(BaseRestRequest request, RateLimitBucket bucket, TaskCompletionSource <bool> ratelimitTcs) { try { await this.GlobalRateLimitEvent.WaitAsync(); if (bucket == null) { bucket = request.RateLimitBucket; } if (ratelimitTcs == null) { ratelimitTcs = await this.WaitForInitialRateLimit(bucket); } if (ratelimitTcs == null) // ckeck rate limit only if we are not the probe request { var now = DateTimeOffset.UtcNow; await bucket.TryResetLimit(now); // Decrement the remaining number of requests as there can be other concurrent requests before this one finishes and has a chance to update the bucket #pragma warning disable 420 // interlocked access is always volatile if (Interlocked.Decrement(ref bucket._remaining) < 0) #pragma warning restore 420 // blaze it { request.Discord?.DebugLogger?.LogMessage(LogLevel.Debug, "REST", $"Request for bucket {bucket}. Blocking.", DateTime.Now); var delay = bucket.Reset - now; if (delay < new TimeSpan(-TimeSpan.TicksPerMinute)) { request.Discord?.DebugLogger?.LogMessage(LogLevel.Error, "REST", "Failed to retrieve ratelimits. Giving up and allowing next request for bucket.", DateTime.Now); bucket._remaining = 1; } if (delay < TimeSpan.Zero) { delay = TimeSpan.FromMilliseconds(100); } request.Discord?.DebugLogger?.LogMessage(LogLevel.Warning, "REST", $"Pre-emptive ratelimit triggered, waiting until {bucket.Reset:yyyy-MM-dd HH:mm:ss zzz} ({delay:c})", DateTime.Now); request.Discord?.DebugLogger?.LogTaskFault(Task.Delay(delay).ContinueWith(t => this.ExecuteRequestAsync(request, null, null)), LogLevel.Error, "RESET", "Error while executing request: "); return; } request.Discord?.DebugLogger?.LogMessage(LogLevel.Debug, "REST", $"Request for bucket {bucket}. Allowing.", DateTime.Now); } else { request.Discord?.DebugLogger?.LogMessage(LogLevel.Debug, "REST", $"Initial Request for bucket {bucket}. Allowing.", DateTime.Now); } var req = this.BuildRequest(request); var response = new RestResponse(); try { var res = await HttpClient.SendAsync(req, CancellationToken.None).ConfigureAwait(false); var bts = await res.Content.ReadAsByteArrayAsync().ConfigureAwait(false); var txt = Utilities.UTF8.GetString(bts, 0, bts.Length); response.Headers = res.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value)); response.Response = txt; response.ResponseCode = (int)res.StatusCode; } catch (HttpRequestException httpex) { request.Discord?.DebugLogger?.LogMessage(LogLevel.Error, "REST", $"Request to {request.Url} triggered an HttpException: {httpex.Message}", DateTime.Now); request.SetFaulted(httpex); this.FailInitialRateLimitTest(bucket, ratelimitTcs); return; } this.UpdateBucket(request, response, ratelimitTcs); Exception ex = null; switch (response.ResponseCode) { case 400: case 405: ex = new BadRequestException(request, response); break; case 401: case 403: ex = new UnauthorizedException(request, response); break; case 404: ex = new NotFoundException(request, response); break; case 429: ex = new RateLimitException(request, response); // check the limit info and requeue this.Handle429(response, out var wait, out var global); if (wait != null) { if (global) { request.Discord?.DebugLogger?.LogMessage(LogLevel.Error, "REST", "Global ratelimit hit, cooling down", DateTime.Now); try { this.GlobalRateLimitEvent.Reset(); await wait.ConfigureAwait(false); } finally { // we don't want to wait here until all the blocked requests have been run, additionally Set can never throw an exception that could be suppressed here _ = this.GlobalRateLimitEvent.SetAsync(); } request.Discord?.DebugLogger?.LogTaskFault(ExecuteRequestAsync(request, bucket, ratelimitTcs), LogLevel.Error, "REST", "Error while retrying request: "); } else { request.Discord?.DebugLogger?.LogMessage(LogLevel.Error, "REST", $"Ratelimit hit, requeueing request to {request.Url}", DateTime.Now); await wait.ConfigureAwait(false); request.Discord?.DebugLogger?.LogTaskFault(this.ExecuteRequestAsync(request, bucket, ratelimitTcs), LogLevel.Error, "REST", "Error while retrying request: "); } return; } break; } if (ex != null) { request.SetFaulted(ex); } else { request.SetCompleted(response); } } catch (Exception ex) { request.Discord?.DebugLogger?.LogMessage(LogLevel.Error, "REST", $"Request to {request.Url} triggered an {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}", DateTime.Now); // if something went wrong and we couldn't get rate limits for the first request here, allow the next request to run if (bucket != null && ratelimitTcs != null && bucket._limitTesting != 0) { this.FailInitialRateLimitTest(bucket, ratelimitTcs); } if (!request.TrySetFaulted(ex)) { throw; } } }
// to allow proper rescheduling of the first request from a bucket private async Task ExecuteRequestAsync(BaseRestRequest request, RateLimitBucket bucket, TaskCompletionSource <bool> ratelimitTcs) { if (this._disposed) { return; } HttpResponseMessage res = default; try { await this.GlobalRateLimitEvent.WaitAsync().ConfigureAwait(false); if (bucket == null) { bucket = request.RateLimitBucket; } if (ratelimitTcs == null) { ratelimitTcs = await this.WaitForInitialRateLimit(bucket).ConfigureAwait(false); } if (ratelimitTcs == null) // ckeck rate limit only if we are not the probe request { var now = DateTimeOffset.UtcNow; await bucket.TryResetLimitAsync(now).ConfigureAwait(false); // Decrement the remaining number of requests as there can be other concurrent requests before this one finishes and has a chance to update the bucket if (Interlocked.Decrement(ref bucket._remaining) < 0) { this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {Bucket} is blocked", bucket.ToString()); var delay = bucket.Reset - now; var resetDate = bucket.Reset; if (this.UseResetAfter) { delay = bucket.ResetAfter.Value; resetDate = bucket.ResetAfterOffset; } if (delay < new TimeSpan(-TimeSpan.TicksPerMinute)) { this.Logger.LogError(LoggerEvents.RatelimitDiag, "Failed to retrieve ratelimits - giving up and allowing next request for bucket"); bucket._remaining = 1; } if (delay < TimeSpan.Zero) { delay = TimeSpan.FromMilliseconds(100); } this.Logger.LogWarning(LoggerEvents.RatelimitPreemptive, "Pre-emptive ratelimit triggered - waiting until {0:yyyy-MM-dd HH:mm:ss zzz} ({1:c}).", resetDate, delay); Task.Delay(delay) .ContinueWith(_ => this.ExecuteRequestAsync(request, null, null)) .LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); return; } this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {Bucket} is allowed", bucket.ToString()); } else { this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Initial request for {Bucket} is allowed", bucket.ToString()); } var req = this.BuildRequest(request); var response = new RestResponse(); try { if (this._disposed) { return; } res = await this.HttpClient.SendAsync(req, HttpCompletionOption.ResponseContentRead, CancellationToken.None).ConfigureAwait(false); var bts = await res.Content.ReadAsByteArrayAsync().ConfigureAwait(false); var txt = Utilities.UTF8.GetString(bts, 0, bts.Length); this.Logger.LogTrace(LoggerEvents.RestRx, txt); response.Headers = res.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value), StringComparer.OrdinalIgnoreCase); response.Response = txt; response.ResponseCode = (int)res.StatusCode; } catch (HttpRequestException httpex) { this.Logger.LogError(LoggerEvents.RestError, httpex, "Request to {Url} triggered an HttpException", request.Url); request.SetFaulted(httpex); this.FailInitialRateLimitTest(request, ratelimitTcs); return; } this.UpdateBucket(request, response, ratelimitTcs); Exception ex = null; switch (response.ResponseCode) { case 400: case 405: ex = new BadRequestException(request, response); break; case 401: case 403: ex = new UnauthorizedException(request, response); break; case 404: ex = new NotFoundException(request, response); break; case 413: ex = new RequestSizeException(request, response); break; case 429: ex = new RateLimitException(request, response); // check the limit info and requeue this.Handle429(response, out var wait, out var global); if (wait != null) { if (global) { this.Logger.LogError(LoggerEvents.RatelimitHit, "Global ratelimit hit, cooling down"); try { this.GlobalRateLimitEvent.Reset(); await wait.ConfigureAwait(false); } finally { // we don't want to wait here until all the blocked requests have been run, additionally Set can never throw an exception that could be suppressed here _ = this.GlobalRateLimitEvent.SetAsync(); } this.ExecuteRequestAsync(request, bucket, ratelimitTcs) .LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request"); } else { this.Logger.LogError(LoggerEvents.RatelimitHit, "Ratelimit hit, requeueing request to {Url}", request.Url); await wait.ConfigureAwait(false); this.ExecuteRequestAsync(request, bucket, ratelimitTcs) .LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request"); } return; } break; case 500: case 502: case 503: case 504: ex = new ServerErrorException(request, response); break; } if (ex != null) { request.SetFaulted(ex); } else { request.SetCompleted(response); } } catch (Exception ex) { this.Logger.LogError(LoggerEvents.RestError, ex, "Request to {Url} triggered an exception", request.Url); // if something went wrong and we couldn't get rate limits for the first request here, allow the next request to run if (bucket != null && ratelimitTcs != null && bucket._limitTesting != 0) { this.FailInitialRateLimitTest(request, ratelimitTcs); } if (!request.TrySetFaulted(ex)) { throw; } } finally { res?.Dispose(); // Get and decrement active requests in this bucket by 1. _ = this.RequestQueue.TryGetValue(bucket.BucketId, out var count); this.RequestQueue[bucket.BucketId] = Interlocked.Decrement(ref count); // If it's 0 or less, we can remove the bucket from the active request queue, // along with any of its past routes. if (count <= 0) { foreach (var r in bucket.RouteHashes) { if (this.RequestQueue.ContainsKey(r)) { _ = this.RequestQueue.TryRemove(r, out _); } } } } }
internal RestRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, IReadOnlyDictionary <string, string> headers = null, string payload = null, double?ratelimitWaitOverride = null) : base(client, bucket, url, method, headers, ratelimitWaitOverride) { this.Payload = payload; }
internal RestRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, IDictionary <string, string> headers = null, string payload = null) : base(client, bucket, url, method, headers) { this.Payload = payload; }
public RateLimitBucket GetBucket(RestRequestMethod method, string route, object route_params, out string url) { var rparams_props = route_params.GetType() .GetTypeInfo() .DeclaredProperties; var rparams = new Dictionary <string, string>(); foreach (var xp in rparams_props) { var val = xp.GetValue(route_params); if (val is string xs) { rparams[xp.Name] = xs; } else if (val is DateTime dt) { rparams[xp.Name] = dt.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture); } else if (val is DateTimeOffset dto) { rparams[xp.Name] = dto.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture); } else { rparams[xp.Name] = val is IFormattable xf?xf.ToString(null, CultureInfo.InvariantCulture) : val.ToString(); } } var guild_id = rparams.ContainsKey("guild_id") ? rparams["guild_id"] : ""; var channel_id = rparams.ContainsKey("channel_id") ? rparams["channel_id"] : ""; var webhook_id = rparams.ContainsKey("webhook_id") ? rparams["webhook_id"] : ""; // Create a generic route (minus major params) key // ex: POST:/channels/channel_id/messages var hashKey = RateLimitBucket.GenerateHashKey(method, route); // We check if the hash is present, using our generic route (without major params) // ex: in POST:/channels/channel_id/messages, out 80c17d2f203122d936070c88c8d10f33 // If it doesn't exist, we create an unlimited hash as our initial key in the form of the hash key + the unlimited constant // and assign this to the route to hash cache // ex: this.RoutesToHashes[POST:/channels/channel_id/messages] = POST:/channels/channel_id/messages:unlimited var hash = this.RoutesToHashes.GetOrAdd(hashKey, RateLimitBucket.GenerateUnlimitedHash(method, route)); // Next we use the hash to generate the key to obtain the bucket. // ex: 80c17d2f203122d936070c88c8d10f33:guild_id:506128773926879242:webhook_id // or if unlimited: POST:/channels/channel_id/messages:unlimited:guild_id:506128773926879242:webhook_id var bucketId = RateLimitBucket.GenerateBucketId(hash, guild_id, channel_id, webhook_id); // If it's not in cache, create a new bucket and index it by its bucket id. var bucket = this.HashesToBuckets.GetOrAdd(bucketId, new RateLimitBucket(hash, guild_id, channel_id, webhook_id)); bucket.LastAttemptAt = DateTimeOffset.UtcNow; // Cache the routes for each bucket so it can be used for GC later. if (!bucket.RouteHashes.Contains(bucketId)) { bucket.RouteHashes.Add(bucketId); } // Add the current route to the request queue, which indexes the amount // of requests occurring to the bucket id. _ = this.RequestQueue.TryGetValue(bucketId, out var count); // Increment by one atomically due to concurrency this.RequestQueue[bucketId] = Interlocked.Increment(ref count); // Start bucket cleaner if not already running. if (!this._cleanerRunning) { this._cleanerRunning = true; this._bucketCleanerTokenSource = new CancellationTokenSource(); this._cleanerTask = Task.Run(this.CleanupBucketsAsync, this._bucketCleanerTokenSource.Token); this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task started."); } url = RouteArgumentRegex.Replace(route, xm => rparams[xm.Groups[1].Value]); return(bucket); }