| @@ -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; | using System.Collections.Immutable; | ||||
| namespace Discord.Net.Queue | namespace Discord.Net.Queue | ||||
| @@ -9,15 +10,29 @@ namespace Discord.Net.Queue | |||||
| } | } | ||||
| internal struct GatewayBucket | 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() | 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[] | 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>(); | var builder = ImmutableDictionary.CreateBuilder<GatewayBucketType, GatewayBucket>(); | ||||
| @@ -31,13 +46,10 @@ namespace Discord.Net.Queue | |||||
| DefsById = builder2.ToImmutable(); | DefsById = builder2.ToImmutable(); | ||||
| } | } | ||||
| public static GatewayBucket Get(GatewayBucketType type) => DefsByType[type]; | |||||
| public static GatewayBucket Get(string id) => DefsById[id]; | |||||
| public GatewayBucketType Type { get; } | public GatewayBucketType Type { get; } | ||||
| public string Id { 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) | public GatewayBucket(GatewayBucketType type, string id, int count, int seconds) | ||||
| { | { | ||||
| @@ -103,7 +103,7 @@ namespace Discord.Net.Queue | |||||
| createdTokenSource?.Dispose(); | 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); | int millis = (int)Math.Ceiling((_waitUntil - DateTimeOffset.UtcNow).TotalMilliseconds); | ||||
| if (millis > 0) | if (millis > 0) | ||||
| @@ -118,6 +118,19 @@ namespace Discord.Net.Queue | |||||
| { | { | ||||
| _waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0)); | _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) | 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) | private async Task EnterAsync(int id, IRequest request) | ||||
| { | { | ||||
| int windowCount; | int windowCount; | ||||
| @@ -67,6 +67,7 @@ namespace Discord.WebSocket | |||||
| config.DisplayInitialLog = false; | config.DisplayInitialLog = false; | ||||
| _baseConfig = config; | _baseConfig = config; | ||||
| _connectionGroupLock = new SemaphoreSlim(1, 1); | _connectionGroupLock = new SemaphoreSlim(1, 1); | ||||
| GatewayBucket.SetLimits(GatewayLimits.GetOrCreate(config.GatewayLimits)); | |||||
| if (config.TotalShards == null) | if (config.TotalShards == null) | ||||
| _automaticShards = true; | _automaticShards = true; | ||||
| @@ -2,6 +2,7 @@ using Discord.API; | |||||
| using Discord.API.Gateway; | using Discord.API.Gateway; | ||||
| using Discord.Logging; | using Discord.Logging; | ||||
| using Discord.Net.Converters; | using Discord.Net.Converters; | ||||
| using Discord.Net.Queue; | |||||
| using Discord.Net.Udp; | using Discord.Net.Udp; | ||||
| using Discord.Net.WebSockets; | using Discord.Net.WebSockets; | ||||
| using Discord.Rest; | using Discord.Rest; | ||||
| @@ -120,6 +121,8 @@ namespace Discord.WebSocket | |||||
| #pragma warning disable IDISP004 | #pragma warning disable IDISP004 | ||||
| public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config), null, null) | public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config), null, null) | ||||
| { | { | ||||
| GatewayBucket.SetLimits(GatewayLimits.GetOrCreate(config.GatewayLimits)); | |||||
| ApiClient.WebSocketRequestQueue.RateLimitTriggered += async (id, info) => | ApiClient.WebSocketRequestQueue.RateLimitTriggered += async (id, info) => | ||||
| { | { | ||||
| if (info == null) | if (info == null) | ||||
| @@ -125,6 +125,14 @@ namespace Discord.WebSocket | |||||
| /// </summary> | /// </summary> | ||||
| public bool GuildSubscriptions { get; set; } = true; | 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; | internal RequestQueue _websocketRequestQueue; | ||||
| /// <summary> | /// <summary> | ||||