* API breaking change: Specify WebSocket close code Should fix #1479 and help overall with resuming sessions. * Also try to resume on missed heartbeatstags/2.3.0
| @@ -14,7 +14,7 @@ namespace Discord.Net.WebSockets | |||||
| void SetCancelToken(CancellationToken cancelToken); | void SetCancelToken(CancellationToken cancelToken); | ||||
| Task ConnectAsync(string host); | Task ConnectAsync(string host); | ||||
| Task DisconnectAsync(); | |||||
| Task DisconnectAsync(int closeCode = 1000); | |||||
| Task SendAsync(byte[] data, int index, int count, bool isText); | Task SendAsync(byte[] data, int index, int count, bool isText); | ||||
| } | } | ||||
| @@ -40,7 +40,7 @@ namespace Discord.Net.Providers.WS4Net | |||||
| { | { | ||||
| if (disposing) | if (disposing) | ||||
| { | { | ||||
| DisconnectInternalAsync(true).GetAwaiter().GetResult(); | |||||
| DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult(); | |||||
| _lock?.Dispose(); | _lock?.Dispose(); | ||||
| _cancelTokenSource?.Dispose(); | _cancelTokenSource?.Dispose(); | ||||
| } | } | ||||
| @@ -92,19 +92,19 @@ namespace Discord.Net.Providers.WS4Net | |||||
| _waitUntilConnect.Wait(_cancelToken); | _waitUntilConnect.Wait(_cancelToken); | ||||
| } | } | ||||
| public async Task DisconnectAsync() | |||||
| public async Task DisconnectAsync(int closeCode = 1000) | |||||
| { | { | ||||
| await _lock.WaitAsync().ConfigureAwait(false); | await _lock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| await DisconnectInternalAsync().ConfigureAwait(false); | |||||
| await DisconnectInternalAsync(closeCode: closeCode).ConfigureAwait(false); | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| _lock.Release(); | _lock.Release(); | ||||
| } | } | ||||
| } | } | ||||
| private Task DisconnectInternalAsync(bool isDisposing = false) | |||||
| private Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) | |||||
| { | { | ||||
| _disconnectCancelTokenSource.Cancel(); | _disconnectCancelTokenSource.Cancel(); | ||||
| if (_client == null) | if (_client == null) | ||||
| @@ -112,7 +112,7 @@ namespace Discord.Net.Providers.WS4Net | |||||
| if (_client.State == WebSocketState.Open) | if (_client.State == WebSocketState.Open) | ||||
| { | { | ||||
| try { _client.Close(1000, ""); } | |||||
| try { _client.Close(closeCode, ""); } | |||||
| catch { } | catch { } | ||||
| } | } | ||||
| @@ -47,7 +47,7 @@ namespace Discord.API | |||||
| internal ulong? CurrentUserId { get; set; } | internal ulong? CurrentUserId { get; set; } | ||||
| public RateLimitPrecision RateLimitPrecision { get; private set; } | public RateLimitPrecision RateLimitPrecision { get; private set; } | ||||
| internal bool UseSystemClock { get; set; } | internal bool UseSystemClock { get; set; } | ||||
| internal JsonSerializer Serializer => _serializer; | internal JsonSerializer Serializer => _serializer; | ||||
| /// <exception cref="ArgumentException">Unknown OAuth token type.</exception> | /// <exception cref="ArgumentException">Unknown OAuth token type.</exception> | ||||
| @@ -164,7 +164,7 @@ namespace Discord.API | |||||
| try { _loginCancelToken?.Cancel(false); } | try { _loginCancelToken?.Cancel(false); } | ||||
| catch { } | catch { } | ||||
| await DisconnectInternalAsync().ConfigureAwait(false); | |||||
| await DisconnectInternalAsync(null).ConfigureAwait(false); | |||||
| await RequestQueue.ClearAsync().ConfigureAwait(false); | await RequestQueue.ClearAsync().ConfigureAwait(false); | ||||
| await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); | await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); | ||||
| @@ -175,7 +175,7 @@ namespace Discord.API | |||||
| } | } | ||||
| internal virtual Task ConnectInternalAsync() => Task.Delay(0); | internal virtual Task ConnectInternalAsync() => Task.Delay(0); | ||||
| internal virtual Task DisconnectInternalAsync() => Task.Delay(0); | |||||
| internal virtual Task DisconnectInternalAsync(Exception ex = null) => Task.Delay(0); | |||||
| //Core | //Core | ||||
| internal Task SendAsync(string method, Expression<Func<string>> endpointExpr, BucketIds ids, | internal Task SendAsync(string method, Expression<Func<string>> endpointExpr, BucketIds ids, | ||||
| @@ -1062,7 +1062,7 @@ namespace Discord.API | |||||
| { | { | ||||
| foreach (var roleId in args.RoleIds.Value) | foreach (var roleId in args.RoleIds.Value) | ||||
| Preconditions.NotEqual(roleId, 0, nameof(roleId)); | Preconditions.NotEqual(roleId, 0, nameof(roleId)); | ||||
| } | |||||
| } | |||||
| options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
| @@ -164,26 +164,17 @@ namespace Discord.API | |||||
| } | } | ||||
| } | } | ||||
| public async Task DisconnectAsync() | |||||
| public async Task DisconnectAsync(Exception ex = null) | |||||
| { | { | ||||
| await _stateLock.WaitAsync().ConfigureAwait(false); | await _stateLock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| await DisconnectInternalAsync().ConfigureAwait(false); | |||||
| } | |||||
| finally { _stateLock.Release(); } | |||||
| } | |||||
| public async Task DisconnectAsync(Exception ex) | |||||
| { | |||||
| await _stateLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| await DisconnectInternalAsync().ConfigureAwait(false); | |||||
| await DisconnectInternalAsync(ex).ConfigureAwait(false); | |||||
| } | } | ||||
| finally { _stateLock.Release(); } | finally { _stateLock.Release(); } | ||||
| } | } | ||||
| /// <exception cref="NotSupportedException">This client is not configured with WebSocket support.</exception> | /// <exception cref="NotSupportedException">This client is not configured with WebSocket support.</exception> | ||||
| internal override async Task DisconnectInternalAsync() | |||||
| internal override async Task DisconnectInternalAsync(Exception ex = null) | |||||
| { | { | ||||
| if (WebSocketClient == null) | if (WebSocketClient == null) | ||||
| throw new NotSupportedException("This client is not configured with WebSocket support."); | throw new NotSupportedException("This client is not configured with WebSocket support."); | ||||
| @@ -194,6 +185,9 @@ namespace Discord.API | |||||
| try { _connectCancelToken?.Cancel(false); } | try { _connectCancelToken?.Cancel(false); } | ||||
| catch { } | catch { } | ||||
| if (ex is GatewayReconnectException) | |||||
| await WebSocketClient.DisconnectAsync(4000); | |||||
| else | |||||
| await WebSocketClient.DisconnectAsync().ConfigureAwait(false); | await WebSocketClient.DisconnectAsync().ConfigureAwait(false); | ||||
| ConnectionState = ConnectionState.Disconnected; | ConnectionState = ConnectionState.Disconnected; | ||||
| @@ -264,7 +264,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); | await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); | ||||
| await ApiClient.DisconnectAsync().ConfigureAwait(false); | |||||
| await ApiClient.DisconnectAsync(ex).ConfigureAwait(false); | |||||
| //Wait for tasks to complete | //Wait for tasks to complete | ||||
| await _gatewayLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false); | await _gatewayLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false); | ||||
| @@ -511,7 +511,7 @@ namespace Discord.WebSocket | |||||
| case GatewayOpCode.Reconnect: | case GatewayOpCode.Reconnect: | ||||
| { | { | ||||
| await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); | await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); | ||||
| _connection.Error(new Exception("Server requested a reconnect")); | |||||
| _connection.Error(new GatewayReconnectException("Server requested a reconnect")); | |||||
| } | } | ||||
| break; | break; | ||||
| case GatewayOpCode.Dispatch: | case GatewayOpCode.Dispatch: | ||||
| @@ -1689,7 +1689,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true)) | if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true)) | ||||
| { | { | ||||
| _connection.Error(new Exception("Server missed last heartbeat")); | |||||
| _connection.Error(new GatewayReconnectException("Server missed last heartbeat")); | |||||
| return; | return; | ||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,22 @@ | |||||
| using System; | |||||
| namespace Discord.WebSocket | |||||
| { | |||||
| /// <summary> | |||||
| /// An exception thrown when the gateway client has been requested to | |||||
| /// reconnect. | |||||
| /// </summary> | |||||
| public class GatewayReconnectException : Exception | |||||
| { | |||||
| /// <summary> | |||||
| /// Creates a new instance of the | |||||
| /// <see cref="GatewayReconnectException"/> type. | |||||
| /// </summary> | |||||
| /// <param name="message"> | |||||
| /// The reason why the gateway has been requested to reconnect. | |||||
| /// </param> | |||||
| public GatewayReconnectException(string message) | |||||
| : base(message) | |||||
| { } | |||||
| } | |||||
| } | |||||
| @@ -44,7 +44,7 @@ namespace Discord.Net.WebSockets | |||||
| { | { | ||||
| if (disposing) | if (disposing) | ||||
| { | { | ||||
| DisconnectInternalAsync(true).GetAwaiter().GetResult(); | |||||
| DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult(); | |||||
| _disconnectTokenSource?.Dispose(); | _disconnectTokenSource?.Dispose(); | ||||
| _cancelTokenSource?.Dispose(); | _cancelTokenSource?.Dispose(); | ||||
| _lock?.Dispose(); | _lock?.Dispose(); | ||||
| @@ -94,19 +94,19 @@ namespace Discord.Net.WebSockets | |||||
| _task = RunAsync(_cancelToken); | _task = RunAsync(_cancelToken); | ||||
| } | } | ||||
| public async Task DisconnectAsync() | |||||
| public async Task DisconnectAsync(int closeCode = 1000) | |||||
| { | { | ||||
| await _lock.WaitAsync().ConfigureAwait(false); | await _lock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| await DisconnectInternalAsync().ConfigureAwait(false); | |||||
| await DisconnectInternalAsync(closeCode: closeCode).ConfigureAwait(false); | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| _lock.Release(); | _lock.Release(); | ||||
| } | } | ||||
| } | } | ||||
| private async Task DisconnectInternalAsync(bool isDisposing = false) | |||||
| private async Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) | |||||
| { | { | ||||
| try { _disconnectTokenSource.Cancel(false); } | try { _disconnectTokenSource.Cancel(false); } | ||||
| catch { } | catch { } | ||||
| @@ -117,7 +117,8 @@ namespace Discord.Net.WebSockets | |||||
| { | { | ||||
| if (!isDisposing) | if (!isDisposing) | ||||
| { | { | ||||
| try { await _client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", new CancellationToken()); } | |||||
| var status = (WebSocketCloseStatus)closeCode; | |||||
| try { await _client.CloseOutputAsync(status, "", new CancellationToken()); } | |||||
| catch { } | catch { } | ||||
| } | } | ||||
| try { _client.Dispose(); } | try { _client.Dispose(); } | ||||
| @@ -141,7 +142,7 @@ namespace Discord.Net.WebSockets | |||||
| await _lock.WaitAsync().ConfigureAwait(false); | await _lock.WaitAsync().ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| await DisconnectInternalAsync(false); | |||||
| await DisconnectInternalAsync(isDisposing: false); | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||