| @@ -1,4 +1,3 @@ | |||
| using Discord.Rest; | |||
| using System.Collections.Immutable; | |||
| namespace Discord.Net.Queue | |||
| @@ -18,7 +17,8 @@ namespace Discord.Net.Queue | |||
| { | |||
| var buckets = new[] | |||
| { | |||
| new GatewayBucket(GatewayBucketType.Unbucketed, BucketId.Create(null, "<gateway-unbucketed>", null), 120, 60), | |||
| // Limit is 120/60s, but 3 will be reserved for heartbeats (2 for possible heartbeats in the same timeframe and a possible failure) | |||
| new GatewayBucket(GatewayBucketType.Unbucketed, BucketId.Create(null, "<gateway-unbucketed>", null), 117, 60), | |||
| new GatewayBucket(GatewayBucketType.Identify, BucketId.Create(null, "<gateway-identify>", null), 1, 5), | |||
| new GatewayBucket(GatewayBucketType.PresenceUpdate, BucketId.Create(null, "<gateway-presenceupdate>", null), 5, 60), | |||
| }; | |||
| @@ -206,6 +206,12 @@ namespace Discord.Net.Queue | |||
| return (null, null); | |||
| } | |||
| public void ClearGatewayBuckets() | |||
| { | |||
| foreach (var gwBucket in (GatewayBucketType[])Enum.GetValues(typeof(GatewayBucketType))) | |||
| _buckets.TryRemove(GatewayBucket.Get(gwBucket).Id, out _); | |||
| } | |||
| private async Task RunCleanup() | |||
| { | |||
| try | |||
| @@ -236,6 +242,8 @@ namespace Discord.Net.Queue | |||
| _tokenLock?.Dispose(); | |||
| _clearToken?.Dispose(); | |||
| _requestCancelTokenSource?.Dispose(); | |||
| _masterIdentifySemaphore?.Dispose(); | |||
| _identifySemaphore?.Dispose(); | |||
| } | |||
| } | |||
| } | |||
| @@ -248,8 +248,31 @@ namespace Discord.Net.Queue | |||
| { | |||
| if (!isRateLimited) | |||
| { | |||
| bool ignoreRatelimit = false; | |||
| isRateLimited = true; | |||
| await _queue.RaiseRateLimitTriggered(Id, null, $"{request.Method} {request.Endpoint}").ConfigureAwait(false); | |||
| switch (request) | |||
| { | |||
| case RestRequest restRequest: | |||
| await _queue.RaiseRateLimitTriggered(Id, null, $"{restRequest.Method} {restRequest.Endpoint}").ConfigureAwait(false); | |||
| break; | |||
| case WebSocketRequest webSocketRequest: | |||
| if (webSocketRequest.IgnoreLimit) | |||
| { | |||
| ignoreRatelimit = true; | |||
| break; | |||
| } | |||
| await _queue.RaiseRateLimitTriggered(Id, null, Id.Endpoint).ConfigureAwait(false); | |||
| break; | |||
| default: | |||
| throw new InvalidOperationException("Unknown request type"); | |||
| } | |||
| if (ignoreRatelimit) | |||
| { | |||
| #if DEBUG_LIMITS | |||
| Debug.WriteLine($"[{id}] Ignoring ratelimit"); | |||
| #endif | |||
| break; | |||
| } | |||
| } | |||
| ThrowRetryLimit(request); | |||
| @@ -11,18 +11,20 @@ namespace Discord.Net.Queue | |||
| public IWebSocketClient Client { get; } | |||
| public byte[] Data { get; } | |||
| public bool IsText { get; } | |||
| public bool IgnoreLimit { get; } | |||
| public DateTimeOffset? TimeoutAt { get; } | |||
| public TaskCompletionSource<Stream> Promise { get; } | |||
| public RequestOptions Options { get; } | |||
| public CancellationToken CancelToken { get; internal set; } | |||
| public WebSocketRequest(IWebSocketClient client, byte[] data, bool isText, RequestOptions options) | |||
| public WebSocketRequest(IWebSocketClient client, byte[] data, bool isText, bool ignoreLimit, RequestOptions options) | |||
| { | |||
| Preconditions.NotNull(options, nameof(options)); | |||
| Client = client; | |||
| Data = data; | |||
| IsText = isText; | |||
| IgnoreLimit = ignoreLimit; | |||
| Options = options; | |||
| TimeoutAt = options.Timeout.HasValue ? DateTimeOffset.UtcNow.AddMilliseconds(options.Timeout.Value) : (DateTimeOffset?)null; | |||
| Promise = new TaskCompletionSource<Stream>(); | |||
| @@ -12,7 +12,6 @@ namespace Discord.WebSocket | |||
| public partial class DiscordShardedClient : BaseSocketClient, IDiscordClient | |||
| { | |||
| private readonly DiscordSocketConfig _baseConfig; | |||
| private readonly SemaphoreSlim _connectionGroupLock; | |||
| private readonly Dictionary<int, int> _shardIdsToIndex; | |||
| private readonly bool _automaticShards; | |||
| private int[] _shardIds; | |||
| @@ -65,7 +64,6 @@ namespace Discord.WebSocket | |||
| _shardIdsToIndex = new Dictionary<int, int>(); | |||
| config.DisplayInitialLog = false; | |||
| _baseConfig = config; | |||
| _connectionGroupLock = new SemaphoreSlim(1, 1); | |||
| if (config.TotalShards == null) | |||
| _automaticShards = true; | |||
| @@ -88,7 +86,7 @@ namespace Discord.WebSocket | |||
| _shardIdsToIndex.Add(_shardIds[i], i); | |||
| var newConfig = config.Clone(); | |||
| newConfig.ShardId = _shardIds[i]; | |||
| _shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null, masterIdentifySemaphore, config.IdentifyMaxConcurrency > 1 ? null : identifySemaphores[i / config.IdentifyMaxConcurrency], config.IdentifyMaxConcurrency); | |||
| _shards[i] = new DiscordSocketClient(newConfig, i != 0 ? _shards[0] : null, masterIdentifySemaphore, config.IdentifyMaxConcurrency > 1 ? null : identifySemaphores[i / config.IdentifyMaxConcurrency], config.IdentifyMaxConcurrency); | |||
| RegisterEvents(_shards[i], i == 0); | |||
| } | |||
| } | |||
| @@ -122,7 +120,7 @@ namespace Discord.WebSocket | |||
| var newConfig = _baseConfig.Clone(); | |||
| newConfig.ShardId = _shardIds[i]; | |||
| newConfig.TotalShards = _totalShards; | |||
| _shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null, masterIdentifySemaphore, maxConcurrency > 1 ? null : identifySemaphores[i / maxConcurrency], maxConcurrency); | |||
| _shards[i] = new DiscordSocketClient(newConfig, i != 0 ? _shards[0] : null, masterIdentifySemaphore, maxConcurrency > 1 ? null : identifySemaphores[i / maxConcurrency], maxConcurrency); | |||
| RegisterEvents(_shards[i], i == 0); | |||
| } | |||
| } | |||
| @@ -418,7 +416,6 @@ namespace Discord.WebSocket | |||
| foreach (var client in _shards) | |||
| client?.Dispose(); | |||
| } | |||
| _connectionGroupLock?.Dispose(); | |||
| } | |||
| _isDisposed = true; | |||
| @@ -133,6 +133,8 @@ namespace Discord.API | |||
| if (WebSocketClient == null) | |||
| throw new NotSupportedException("This client is not configured with WebSocket support."); | |||
| RequestQueue.ClearGatewayBuckets(); | |||
| //Re-create streams to reset the zlib state | |||
| _compressed?.Dispose(); | |||
| _decompressor?.Dispose(); | |||
| @@ -210,7 +212,7 @@ namespace Discord.API | |||
| options.IsGatewayBucket = true; | |||
| if (options.BucketId == null) | |||
| options.BucketId = GatewayBucket.Get(GatewayBucketType.Unbucketed).Id; | |||
| await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, bytes, true, options)).ConfigureAwait(false); | |||
| await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, bytes, true, opCode == GatewayOpCode.Heartbeat, options)).ConfigureAwait(false); | |||
| await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); | |||
| } | |||
| @@ -26,7 +26,6 @@ namespace Discord.WebSocket | |||
| { | |||
| private readonly ConcurrentQueue<ulong> _largeGuilds; | |||
| private readonly JsonSerializer _serializer; | |||
| private readonly SemaphoreSlim _connectionGroupLock; | |||
| private readonly DiscordSocketClient _parentClient; | |||
| private readonly ConcurrentQueue<long> _heartbeatTimes; | |||
| private readonly ConnectionManager _connection; | |||
| @@ -119,10 +118,10 @@ namespace Discord.WebSocket | |||
| /// </summary> | |||
| /// <param name="config">The configuration to be used with the client.</param> | |||
| #pragma warning disable IDISP004 | |||
| public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config, new SemaphoreSlim(1, 1), null, 1), null, null) { } | |||
| internal DiscordSocketClient(DiscordSocketConfig config, SemaphoreSlim groupLock, DiscordSocketClient parentClient, SemaphoreSlim identifyMasterSemaphore, SemaphoreSlim identifySemaphore, int identifyMaxConcurrency) : this(config, CreateApiClient(config, identifyMasterSemaphore, identifySemaphore, identifyMaxConcurrency), groupLock, parentClient) { } | |||
| public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config, new SemaphoreSlim(1, 1), null, 1), null) { } | |||
| internal DiscordSocketClient(DiscordSocketConfig config, DiscordSocketClient parentClient, SemaphoreSlim identifyMasterSemaphore, SemaphoreSlim identifySemaphore, int identifyMaxConcurrency) : this(config, CreateApiClient(config, identifyMasterSemaphore, identifySemaphore, identifyMaxConcurrency), parentClient) { } | |||
| #pragma warning restore IDISP004 | |||
| private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, SemaphoreSlim groupLock, DiscordSocketClient parentClient) | |||
| private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, DiscordSocketClient parentClient) | |||
| : base(config, client) | |||
| { | |||
| ShardId = config.ShardId ?? 0; | |||
| @@ -148,7 +147,6 @@ namespace Discord.WebSocket | |||
| _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); | |||
| _nextAudioId = 1; | |||
| _connectionGroupLock = groupLock; | |||
| _parentClient = parentClient; | |||
| _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | |||
| @@ -230,35 +228,25 @@ namespace Discord.WebSocket | |||
| private async Task OnConnectingAsync() | |||
| { | |||
| if (_connectionGroupLock != null) | |||
| await _connectionGroupLock.WaitAsync(_connection.CancelToken).ConfigureAwait(false); | |||
| try | |||
| { | |||
| await _gatewayLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false); | |||
| await ApiClient.ConnectAsync().ConfigureAwait(false); | |||
| if (_sessionId != null) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Resuming").ConfigureAwait(false); | |||
| await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); | |||
| } | |||
| else | |||
| { | |||
| await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false); | |||
| await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions, gatewayIntents: _gatewayIntents).ConfigureAwait(false); | |||
| } | |||
| //Wait for READY | |||
| await _connection.WaitAsync().ConfigureAwait(false); | |||
| await _gatewayLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false); | |||
| await ApiClient.ConnectAsync().ConfigureAwait(false); | |||
| await _gatewayLogger.DebugAsync("Sending Status").ConfigureAwait(false); | |||
| await SendStatusAsync().ConfigureAwait(false); | |||
| if (_sessionId != null) | |||
| { | |||
| await _gatewayLogger.DebugAsync("Resuming").ConfigureAwait(false); | |||
| await ApiClient.SendResumeAsync(_sessionId, _lastSeq).ConfigureAwait(false); | |||
| } | |||
| finally | |||
| else | |||
| { | |||
| if (_connectionGroupLock != null) | |||
| _connectionGroupLock.Release(); | |||
| await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false); | |||
| await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions, gatewayIntents: _gatewayIntents).ConfigureAwait(false); | |||
| } | |||
| //Wait for READY | |||
| await _connection.WaitAsync().ConfigureAwait(false); | |||
| await _gatewayLogger.DebugAsync("Sending Status").ConfigureAwait(false); | |||
| await SendStatusAsync().ConfigureAwait(false); | |||
| } | |||
| private async Task OnDisconnectingAsync(Exception ex) | |||
| { | |||