Ejemplo n.º 1
0
        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);
        }
Ejemplo n.º 2
0
 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;
 }
Ejemplo n.º 3
0
 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);
 }
Ejemplo n.º 4
0
 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;
 }
Ejemplo n.º 5
0
 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;
 }
Ejemplo n.º 6
0
 /// <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;
 }
Ejemplo n.º 7
0
        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));
        }
Ejemplo n.º 8
0
        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);
        }
Ejemplo n.º 9
0
        /// <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);
            }
        }
Ejemplo n.º 10
0
        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);
        }
Ejemplo n.º 11
0
        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;
        }
Ejemplo n.º 12
0
        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));
        }
Ejemplo n.º 13
0
        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);
        }
Ejemplo n.º 14
0
        // 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;
                }
            }
        }
Ejemplo n.º 15
0
        // 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 _);
                        }
                    }
                }
            }
        }
Ejemplo n.º 16
0
 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;
 }
Ejemplo n.º 17
0
 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;
 }
Ejemplo n.º 18
0
        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);
        }