| @@ -0,0 +1,23 @@ | |||
| namespace Discord.Rest | |||
| { | |||
| /// <summary> | |||
| /// Represents the limits for a gateway request. | |||
| /// </summary> | |||
| public struct GatewayLimit | |||
| { | |||
| /// <summary> | |||
| /// The maximum amount of this type of request in a time window, that is set by <see cref="Seconds"/>. | |||
| /// </summary> | |||
| public int Count { get; set; } | |||
| /// <summary> | |||
| /// The amount of seconds until the rate limiter resets the remaining requests <see cref="Count"/>. | |||
| /// </summary> | |||
| public int Seconds { get; set; } | |||
| internal GatewayLimit(int count, int seconds) | |||
| { | |||
| Count = count; | |||
| Seconds = seconds; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,29 @@ | |||
| namespace Discord.Rest | |||
| { | |||
| /// <summary> | |||
| /// Contains the rate limits for the gateway. | |||
| /// </summary> | |||
| public class GatewayLimits | |||
| { | |||
| /// <summary> | |||
| /// Gets or sets the global limits for the gateway rate limiter. | |||
| /// </summary> | |||
| /// <remarks> | |||
| /// It includes all the other limits, like Identify. | |||
| /// </remarks> | |||
| public GatewayLimit Global { get; set; } | |||
| /// <summary> | |||
| /// Gets or sets the limits of Identify requests. | |||
| /// </summary> | |||
| public GatewayLimit Identify { get; set; } | |||
| public GatewayLimits() | |||
| { | |||
| Global = new GatewayLimit(120, 60); | |||
| Identify = new GatewayLimit(1, 5); | |||
| } | |||
| internal static GatewayLimits GetOrCreate(GatewayLimits limits) | |||
| => limits ?? new GatewayLimits(); | |||
| } | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| using Discord.Rest; | |||
| using System.Collections.Immutable; | |||
| namespace Discord.Net.Queue | |||
| @@ -9,15 +10,29 @@ namespace Discord.Net.Queue | |||
| } | |||
| internal struct GatewayBucket | |||
| { | |||
| private static readonly ImmutableDictionary<GatewayBucketType, GatewayBucket> DefsByType; | |||
| private static readonly ImmutableDictionary<string, GatewayBucket> DefsById; | |||
| private static ImmutableDictionary<GatewayBucketType, GatewayBucket> DefsByType; | |||
| private static ImmutableDictionary<string, GatewayBucket> DefsById; | |||
| static GatewayBucket() | |||
| { | |||
| SetLimits(GatewayLimits.GetOrCreate(null)); | |||
| } | |||
| public static GatewayBucket Get(GatewayBucketType type) => DefsByType[type]; | |||
| public static GatewayBucket Get(string id) => DefsById[id]; | |||
| public static void SetLimits(GatewayLimits limits) | |||
| { | |||
| limits = GatewayLimits.GetOrCreate(limits); | |||
| Preconditions.GreaterThan(limits.Global.Count, 0, nameof(limits.Global.Count), "Global count must be greater than zero."); | |||
| Preconditions.GreaterThan(limits.Global.Seconds, 0, nameof(limits.Global.Seconds), "Global seconds must be greater than zero."); | |||
| Preconditions.GreaterThan(limits.Identify.Count, 0, nameof(limits.Identify.Count), "Identify count must be greater than zero."); | |||
| Preconditions.GreaterThan(limits.Identify.Seconds, 0, nameof(limits.Identify.Seconds), "Identify seconds must be greater than zero."); | |||
| var buckets = new[] | |||
| { | |||
| new GatewayBucket(GatewayBucketType.Unbucketed, "<gateway-unbucketed>", 120, 60), | |||
| new GatewayBucket(GatewayBucketType.Identify, "<gateway-identify>", 1, 5) | |||
| new GatewayBucket(GatewayBucketType.Unbucketed, "<gateway-unbucketed>", limits.Global.Count, limits.Global.Seconds), | |||
| new GatewayBucket(GatewayBucketType.Identify, "<gateway-identify>", limits.Identify.Count, limits.Identify.Seconds) | |||
| }; | |||
| var builder = ImmutableDictionary.CreateBuilder<GatewayBucketType, GatewayBucket>(); | |||
| @@ -31,13 +46,10 @@ namespace Discord.Net.Queue | |||
| DefsById = builder2.ToImmutable(); | |||
| } | |||
| public static GatewayBucket Get(GatewayBucketType type) => DefsByType[type]; | |||
| public static GatewayBucket Get(string id) => DefsById[id]; | |||
| public GatewayBucketType Type { get; } | |||
| public string Id { get; } | |||
| public int WindowCount { get; } | |||
| public int WindowSeconds { get; } | |||
| public int WindowCount { get; set; } | |||
| public int WindowSeconds { get; set; } | |||
| public GatewayBucket(GatewayBucketType type, string id, int count, int seconds) | |||
| { | |||
| @@ -103,7 +103,7 @@ namespace Discord.Net.Queue | |||
| createdTokenSource?.Dispose(); | |||
| } | |||
| internal async Task EnterGlobalAsync(int id, IRequest request) | |||
| internal async Task EnterGlobalAsync(int id, RestRequest request) | |||
| { | |||
| int millis = (int)Math.Ceiling((_waitUntil - DateTimeOffset.UtcNow).TotalMilliseconds); | |||
| if (millis > 0) | |||
| @@ -118,6 +118,19 @@ namespace Discord.Net.Queue | |||
| { | |||
| _waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0)); | |||
| } | |||
| internal async Task EnterGlobalAsync(int id, WebSocketRequest request) | |||
| { | |||
| var requestBucket = GatewayBucket.Get(request.Options.BucketId); | |||
| if (requestBucket.Type == GatewayBucketType.Unbucketed) | |||
| return; | |||
| var globalBucketType = GatewayBucket.Get(GatewayBucketType.Unbucketed); | |||
| var options = RequestOptions.CreateOrClone(request.Options); | |||
| options.BucketId = globalBucketType.Id; | |||
| var globalRequest = new WebSocketRequest(null, null, false, options); | |||
| var globalBucket = GetOrCreateBucket(globalBucketType.Id, globalRequest); | |||
| await globalBucket.TriggerAsync(id, globalRequest); | |||
| } | |||
| private RequestBucket GetOrCreateBucket(string id, IRequest request) | |||
| { | |||
| @@ -203,6 +203,15 @@ namespace Discord.Net.Queue | |||
| } | |||
| } | |||
| internal async Task TriggerAsync(int id, IRequest request) | |||
| { | |||
| #if DEBUG_LIMITS | |||
| Debug.WriteLine($"[{id}] Trigger Bucket"); | |||
| #endif | |||
| await EnterAsync(id, request).ConfigureAwait(false); | |||
| UpdateRateLimit(id, request, default(RateLimitInfo), false); | |||
| } | |||
| private async Task EnterAsync(int id, IRequest request) | |||
| { | |||
| int windowCount; | |||
| @@ -67,6 +67,7 @@ namespace Discord.WebSocket | |||
| config.DisplayInitialLog = false; | |||
| _baseConfig = config; | |||
| _connectionGroupLock = new SemaphoreSlim(1, 1); | |||
| GatewayBucket.SetLimits(GatewayLimits.GetOrCreate(config.GatewayLimits)); | |||
| if (config.TotalShards == null) | |||
| _automaticShards = true; | |||
| @@ -2,6 +2,7 @@ using Discord.API; | |||
| using Discord.API.Gateway; | |||
| using Discord.Logging; | |||
| using Discord.Net.Converters; | |||
| using Discord.Net.Queue; | |||
| using Discord.Net.Udp; | |||
| using Discord.Net.WebSockets; | |||
| using Discord.Rest; | |||
| @@ -120,6 +121,8 @@ namespace Discord.WebSocket | |||
| #pragma warning disable IDISP004 | |||
| public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config), null, null) | |||
| { | |||
| GatewayBucket.SetLimits(GatewayLimits.GetOrCreate(config.GatewayLimits)); | |||
| ApiClient.WebSocketRequestQueue.RateLimitTriggered += async (id, info) => | |||
| { | |||
| if (info == null) | |||
| @@ -125,6 +125,14 @@ namespace Discord.WebSocket | |||
| /// </summary> | |||
| public bool GuildSubscriptions { get; set; } = true; | |||
| /// <summary> | |||
| /// Gets or sets the gateway limits. | |||
| /// <note type="warning"> | |||
| /// It should only be changed for bots that have special limits provided by Discord. | |||
| /// </note> | |||
| /// </summary> | |||
| public GatewayLimits GatewayLimits { get; set; } = new GatewayLimits(); | |||
| internal RequestQueue _websocketRequestQueue; | |||
| /// <summary> | |||