Browse Source

Remove specific RequestQueue for WebSocket and other changes

The only account limit is for identify that is dealt in a different way (exclusive semaphore), so websocket queues can be shared with REST and don't need to be shared between clients anymore.

Also added the ratelimit for presence updates.
pull/1537/head
Paulo 5 years ago
parent
commit
9c46081890
9 changed files with 66 additions and 40 deletions
  1. +20
    -1
      src/Discord.Net.Rest/Entities/Gateway/GatewayLimits.cs
  2. +10
    -2
      src/Discord.Net.Rest/Net/Queue/GatewayBucket.cs
  3. +23
    -0
      src/Discord.Net.Rest/Net/Queue/RequestQueue.cs
  4. +5
    -3
      src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs
  5. +1
    -1
      src/Discord.Net.WebSocket/BaseSocketClient.cs
  6. +1
    -9
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  7. +5
    -13
      src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
  8. +1
    -9
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  9. +0
    -2
      src/Discord.Net.WebSocket/DiscordSocketConfig.cs

+ 20
- 1
src/Discord.Net.Rest/Entities/Gateway/GatewayLimits.cs View File

@@ -1,3 +1,5 @@
using System;

namespace Discord.Rest namespace Discord.Rest
{ {
/// <summary> /// <summary>
@@ -9,13 +11,28 @@ namespace Discord.Rest
/// Gets or sets the global limits for the gateway rate limiter. /// Gets or sets the global limits for the gateway rate limiter.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This property includes all the other limits, like Identify.
/// This property includes all the other limits, like Identify,
/// and it is per websocket.
/// </remarks> /// </remarks>
public GatewayLimit Global { get; set; } public GatewayLimit Global { get; set; }
/// <summary> /// <summary>
/// Gets or sets the limits of Identify requests. /// Gets or sets the limits of Identify requests.
/// </summary> /// </summary>
/// <remarks>
/// This limit is included into <see cref="Global"/> but it is
/// also per account.
/// </remarks>
public GatewayLimit Identify { get; set; } public GatewayLimit Identify { get; set; }
/// <summary>
/// Gets or sets the limits of Presence Update requests.
/// </summary>
/// <remarks>
/// Presence updates include activity (playing, watching, etc)
/// and status (online, idle, etc)
/// </remarks>
public GatewayLimit PresenceUpdate { get; set; }

public string IdentifySemaphoreName { get; set; }


/// <summary> /// <summary>
/// Initializes a new <see cref="GatewayLimits"/> with the default values. /// Initializes a new <see cref="GatewayLimits"/> with the default values.
@@ -24,6 +41,8 @@ namespace Discord.Rest
{ {
Global = new GatewayLimit(120, 60); Global = new GatewayLimit(120, 60);
Identify = new GatewayLimit(1, 5); Identify = new GatewayLimit(1, 5);
PresenceUpdate = new GatewayLimit(5, 60);
IdentifySemaphoreName = Guid.NewGuid().ToString();
} }


internal static GatewayLimits GetOrCreate(GatewayLimits limits) internal static GatewayLimits GetOrCreate(GatewayLimits limits)


+ 10
- 2
src/Discord.Net.Rest/Net/Queue/GatewayBucket.cs View File

@@ -6,12 +6,14 @@ namespace Discord.Net.Queue
public enum GatewayBucketType public enum GatewayBucketType
{ {
Unbucketed = 0, Unbucketed = 0,
Identify = 1
Identify = 1,
PresenceUpdate = 2,
} }
internal struct GatewayBucket internal struct GatewayBucket
{ {
private static ImmutableDictionary<GatewayBucketType, GatewayBucket> DefsByType; private static ImmutableDictionary<GatewayBucketType, GatewayBucket> DefsByType;
private static ImmutableDictionary<string, GatewayBucket> DefsById; private static ImmutableDictionary<string, GatewayBucket> DefsById;
private static string IdentifySemaphoreName;


static GatewayBucket() static GatewayBucket()
{ {
@@ -20,6 +22,7 @@ namespace Discord.Net.Queue


public static GatewayBucket Get(GatewayBucketType type) => DefsByType[type]; public static GatewayBucket Get(GatewayBucketType type) => DefsByType[type];
public static GatewayBucket Get(string id) => DefsById[id]; public static GatewayBucket Get(string id) => DefsById[id];
public static string GetIdentifySemaphoreName() => IdentifySemaphoreName;


public static void SetLimits(GatewayLimits limits) public static void SetLimits(GatewayLimits limits)
{ {
@@ -28,11 +31,14 @@ namespace Discord.Net.Queue
Preconditions.GreaterThan(limits.Global.Seconds, 0, nameof(limits.Global.Seconds), "Global seconds 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.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."); Preconditions.GreaterThan(limits.Identify.Seconds, 0, nameof(limits.Identify.Seconds), "Identify seconds must be greater than zero.");
Preconditions.GreaterThan(limits.PresenceUpdate.Count, 0, nameof(limits.PresenceUpdate.Count), "PresenceUpdate count must be greater than zero.");
Preconditions.GreaterThan(limits.PresenceUpdate.Seconds, 0, nameof(limits.PresenceUpdate.Seconds), "PresenceUpdate seconds must be greater than zero.");


var buckets = new[] var buckets = new[]
{ {
new GatewayBucket(GatewayBucketType.Unbucketed, "<gateway-unbucketed>", limits.Global.Count, limits.Global.Seconds), new GatewayBucket(GatewayBucketType.Unbucketed, "<gateway-unbucketed>", limits.Global.Count, limits.Global.Seconds),
new GatewayBucket(GatewayBucketType.Identify, "<gateway-identify>", limits.Identify.Count, limits.Identify.Seconds)
new GatewayBucket(GatewayBucketType.Identify, "<gateway-identify>", limits.Identify.Count, limits.Identify.Seconds),
new GatewayBucket(GatewayBucketType.PresenceUpdate, "<gateway-presenceupdate>", limits.Identify.Count, limits.Identify.Seconds),
}; };


var builder = ImmutableDictionary.CreateBuilder<GatewayBucketType, GatewayBucket>(); var builder = ImmutableDictionary.CreateBuilder<GatewayBucketType, GatewayBucket>();
@@ -44,6 +50,8 @@ namespace Discord.Net.Queue
foreach (var bucket in buckets) foreach (var bucket in buckets)
builder2.Add(bucket.Id, bucket); builder2.Add(bucket.Id, bucket);
DefsById = builder2.ToImmutable(); DefsById = builder2.ToImmutable();

IdentifySemaphoreName = limits.IdentifySemaphoreName;
} }


public GatewayBucketType Type { get; } public GatewayBucketType Type { get; }


+ 23
- 0
src/Discord.Net.Rest/Net/Queue/RequestQueue.cs View File

@@ -1,3 +1,4 @@
using Discord.Rest;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
#if DEBUG_LIMITS #if DEBUG_LIMITS
@@ -22,12 +23,15 @@ namespace Discord.Net.Queue
private CancellationTokenSource _requestCancelTokenSource; private CancellationTokenSource _requestCancelTokenSource;
private CancellationToken _requestCancelToken; //Parent token + Clear token private CancellationToken _requestCancelToken; //Parent token + Clear token
private DateTimeOffset _waitUntil; private DateTimeOffset _waitUntil;
private Semaphore _identifySemaphore;


private Task _cleanupTask; private Task _cleanupTask;


public RequestQueue() public RequestQueue()
{ {
_tokenLock = new SemaphoreSlim(1, 1); _tokenLock = new SemaphoreSlim(1, 1);
int semaphoreCount = GatewayBucket.Get(GatewayBucketType.Identify).WindowCount;
_identifySemaphore = new Semaphore(semaphoreCount, semaphoreCount, GatewayBucket.GetIdentifySemaphoreName());


_clearToken = new CancellationTokenSource(); _clearToken = new CancellationTokenSource();
_cancelTokenSource = new CancellationTokenSource(); _cancelTokenSource = new CancellationTokenSource();
@@ -120,10 +124,22 @@ namespace Discord.Net.Queue
} }
internal async Task EnterGlobalAsync(int id, WebSocketRequest request) internal async Task EnterGlobalAsync(int id, WebSocketRequest request)
{ {
//If this is a global request (unbucketed), it'll be dealt in EnterAsync
var requestBucket = GatewayBucket.Get(request.Options.BucketId); var requestBucket = GatewayBucket.Get(request.Options.BucketId);
if (requestBucket.Type == GatewayBucketType.Unbucketed) if (requestBucket.Type == GatewayBucketType.Unbucketed)
return; return;


//Identify is per-account so we won't trigger global until we can actually go for it
if (requestBucket.Type == GatewayBucketType.Identify)
{
while (!_identifySemaphore.WaitOne(0)) //To not block the thread
await Task.Delay(100, request.CancelToken);
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Acquired identify ticket");
#endif
}

//It's not a global request, so need to remove one from global (per-session)
var globalBucketType = GatewayBucket.Get(GatewayBucketType.Unbucketed); var globalBucketType = GatewayBucket.Get(GatewayBucketType.Unbucketed);
var options = RequestOptions.CreateOrClone(request.Options); var options = RequestOptions.CreateOrClone(request.Options);
options.BucketId = globalBucketType.Id; options.BucketId = globalBucketType.Id;
@@ -131,6 +147,13 @@ namespace Discord.Net.Queue
var globalBucket = GetOrCreateBucket(globalBucketType.Id, globalRequest); var globalBucket = GetOrCreateBucket(globalBucketType.Id, globalRequest);
await globalBucket.TriggerAsync(id, globalRequest); await globalBucket.TriggerAsync(id, globalRequest);
} }
internal void ReleaseIdentifySemaphore(int id)
{
_identifySemaphore.Release();
#if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Released identify ticket");
#endif
}


private RequestBucket GetOrCreateBucket(string id, IRequest request) private RequestBucket GetOrCreateBucket(string id, IRequest request)
{ {


+ 5
- 3
src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs View File

@@ -348,7 +348,7 @@ namespace Discord.Net.Queue
#if DEBUG_LIMITS #if DEBUG_LIMITS
Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms"); Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms");
#endif #endif
var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds));
var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds), request);
} }
return; return;
} }
@@ -372,12 +372,12 @@ namespace Discord.Net.Queue


if (!hasQueuedReset) if (!hasQueuedReset)
{ {
var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds));
var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds), request);
} }
} }
} }
} }
private async Task QueueReset(int id, int millis)
private async Task QueueReset(int id, int millis, IRequest request)
{ {
while (true) while (true)
{ {
@@ -391,6 +391,8 @@ namespace Discord.Net.Queue
#if DEBUG_LIMITS #if DEBUG_LIMITS
Debug.WriteLine($"[{id}] * Reset *"); Debug.WriteLine($"[{id}] * Reset *");
#endif #endif
if (request is WebSocketRequest webSocketRequest && webSocketRequest.Options.BucketId == GatewayBucket.Get(GatewayBucketType.Identify).Id)
_queue.ReleaseIdentifySemaphore(id);
_semaphore = WindowCount; _semaphore = WindowCount;
_resetTick = null; _resetTick = null;
return; return;


+ 1
- 1
src/Discord.Net.WebSocket/BaseSocketClient.cs View File

@@ -80,7 +80,7 @@ namespace Discord.WebSocket
internal BaseSocketClient(DiscordSocketConfig config, DiscordRestApiClient client) internal BaseSocketClient(DiscordSocketConfig config, DiscordRestApiClient client)
: base(config, client) => BaseConfig = config; : base(config, client) => BaseConfig = config;
private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
=> new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.WebsocketRequestQueue,
=> new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent,
rateLimitPrecision: config.RateLimitPrecision, rateLimitPrecision: config.RateLimitPrecision,
useSystemClock: config.UseSystemClock); useSystemClock: config.UseSystemClock);




+ 1
- 9
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -85,17 +85,9 @@ namespace Discord.WebSocket
RegisterEvents(_shards[i], i == 0); RegisterEvents(_shards[i], i == 0);
} }
} }

ApiClient.WebSocketRequestQueue.RateLimitTriggered += async (id, info) =>
{
if (info == null)
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
else
await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
};
} }
private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
=> new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.WebsocketRequestQueue,
=> new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent,
rateLimitPrecision: config.RateLimitPrecision); rateLimitPrecision: config.RateLimitPrecision);


internal override async Task OnLoginAsync(TokenType tokenType, string token) internal override async Task OnLoginAsync(TokenType tokenType, string token)


+ 5
- 13
src/Discord.Net.WebSocket/DiscordSocketApiClient.cs View File

@@ -37,17 +37,7 @@ namespace Discord.API


public ConnectionState ConnectionState { get; private set; } public ConnectionState ConnectionState { get; private set; }


internal RequestQueue WebSocketRequestQueue { get; }

public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent, public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent,
string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null,
RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second,
bool useSystemClock = true)
: this(restClientProvider, webSocketProvider, userAgent, null, url, defaultRetryMode, serializer, rateLimitPrecision, useSystemClock)
{
}

internal DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent, RequestQueue websocketRequestQueue,
string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null, string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null,
RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second, RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second,
bool useSystemClock = true) bool useSystemClock = true)
@@ -58,7 +48,6 @@ namespace Discord.API
_isExplicitUrl = true; _isExplicitUrl = true;
WebSocketClient = webSocketProvider(); WebSocketClient = webSocketProvider();
//WebSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+) //WebSocketClient.SetHeader("user-agent", DiscordConfig.UserAgent); (Causes issues in .NET Framework 4.6+)
WebSocketRequestQueue = websocketRequestQueue ?? new RequestQueue();


WebSocketClient.BinaryMessage += async (data, index, count) => WebSocketClient.BinaryMessage += async (data, index, count) =>
{ {
@@ -218,8 +207,9 @@ namespace Discord.API
bytes = Encoding.UTF8.GetBytes(SerializeJson(payload)); bytes = Encoding.UTF8.GetBytes(SerializeJson(payload));


options.IsGatewayBucket = true; options.IsGatewayBucket = true;
options.BucketId = GatewayBucket.Get(opCode == GatewayOpCode.Identify ? GatewayBucketType.Identify : GatewayBucketType.Unbucketed).Id;
await WebSocketRequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, bytes, true, options)).ConfigureAwait(false);
if (string.IsNullOrEmpty(options.BucketId))
options.BucketId = GatewayBucket.Get(GatewayBucketType.Unbucketed).Id;
await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, bytes, true, options)).ConfigureAwait(false);
await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false);
} }


@@ -240,6 +230,7 @@ namespace Discord.API
if (totalShards > 1) if (totalShards > 1)
msg.ShardingParams = new int[] { shardID, totalShards }; msg.ShardingParams = new int[] { shardID, totalShards };


options.BucketId = GatewayBucket.Get(GatewayBucketType.Identify).Id;
await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false);
} }
public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null) public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null)
@@ -268,6 +259,7 @@ namespace Discord.API
IsAFK = isAFK, IsAFK = isAFK,
Game = game Game = game
}; };
options.BucketId = GatewayBucket.Get(GatewayBucketType.PresenceUpdate).Id;
await SendGatewayAsync(GatewayOpCode.StatusUpdate, args, options: options).ConfigureAwait(false); await SendGatewayAsync(GatewayOpCode.StatusUpdate, args, options: options).ConfigureAwait(false);
} }
public async Task SendRequestMembersAsync(IEnumerable<ulong> guildIds, RequestOptions options = null) public async Task SendRequestMembersAsync(IEnumerable<ulong> guildIds, RequestOptions options = null)


+ 1
- 9
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -122,14 +122,6 @@ namespace Discord.WebSocket
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)); GatewayBucket.SetLimits(GatewayLimits.GetOrCreate(config.GatewayLimits));

ApiClient.WebSocketRequestQueue.RateLimitTriggered += async (id, info) =>
{
if (info == null)
await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
else
await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false);
};
} }
internal DiscordSocketClient(DiscordSocketConfig config, SemaphoreSlim groupLock, DiscordSocketClient parentClient) : this(config, CreateApiClient(config), groupLock, parentClient) { } internal DiscordSocketClient(DiscordSocketConfig config, SemaphoreSlim groupLock, DiscordSocketClient parentClient) : this(config, CreateApiClient(config), groupLock, parentClient) { }
#pragma warning restore IDISP004 #pragma warning restore IDISP004
@@ -190,7 +182,7 @@ namespace Discord.WebSocket
_largeGuilds = new ConcurrentQueue<ulong>(); _largeGuilds = new ConcurrentQueue<ulong>();
} }
private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
=> new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.WebsocketRequestQueue, config.GatewayHost,
=> new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost,
rateLimitPrecision: config.RateLimitPrecision); rateLimitPrecision: config.RateLimitPrecision);
/// <inheritdoc /> /// <inheritdoc />
internal override void Dispose(bool disposing) internal override void Dispose(bool disposing)


+ 0
- 2
src/Discord.Net.WebSocket/DiscordSocketConfig.cs View File

@@ -135,8 +135,6 @@ namespace Discord.WebSocket
/// </remarks> /// </remarks>
public GatewayLimits GatewayLimits { get; set; } = new GatewayLimits(); public GatewayLimits GatewayLimits { get; set; } = new GatewayLimits();


internal RequestQueue WebsocketRequestQueue { get; } = new RequestQueue();

/// <summary> /// <summary>
/// Initializes a default configuration. /// Initializes a default configuration.
/// </summary> /// </summary>


Loading…
Cancel
Save