Example #1
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;
        }
Example #2
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);
        }