@@ -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> | |||