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