/// <exception cref="OperationCanceledException"></exception> async Task ConnectLoop(bool resume, CancellationToken cancellationToken) { // Keep track of whether this is a resume or new session so // we can respond to the HELLO payload appropriately. isConnectionResuming = resume; log.LogVerbose($"[ConnectLoop] resume = {resume}"); handshakeCompleteEvent.Reset(); handshakeCompleteCancellationSource = new CancellationTokenSource(); while (!cancellationToken.IsCancellationRequested) { // Ensure previous socket has been closed if (socket != null) { UnsubscribeSocketEvents(); if (resume) { // Store previous sequence lastSequence = socket.Sequence; } if (socket.CanBeDisconnected) { log.LogVerbose("[ConnectLoop] Disconnecting previous socket..."); // If for some reason the socket cannot be disconnected gracefully, // DisconnectAsync will abort the socket after 5s. if (resume) { // Make sure to disconnect with a non 1000 code to ensure Discord doesn't // force us to make a new session since we are resuming. await socket.DisconnectAsync(WebSocketCloseStatus.EndpointUnavailable, "Reconnecting to resume...", cancellationToken).ConfigureAwait(false); } else { await socket.DisconnectAsync(WebSocketCloseStatus.NormalClosure, "Starting new session...", cancellationToken).ConfigureAwait(false); } } socket.Dispose(); } if (!resume) { // If not resuming, reset gateway session state. lastSequence = 0; } // Create a new socket socket = new GatewaySocket($"GatewaySocket#{shard.Id}", lastSequence, outboundPayloadRateLimiter, gameStatusUpdateRateLimiter, identifyRateLimiter); socket.OnHello = async() => { if (isDisposed) { return; } if (isConnectionResuming) { // Resume await socket.SendResumePayload(botToken, sessionId, lastSequence); } else { // Identify await socket.SendIdentifyPayload(botToken, lastShardStartConfig.GatewayLargeThreshold, shard.Id, totalShards); } }; SubscribeSocketEvents(); // Get the gateway URL if we don't have one string gatewayUrl = GatewayUrlMemoryCache.GatewayUrl; if (gatewayUrl == null) { try { log.LogVerbose("[ConnectLoop] Retrieving Gateway URL..."); gatewayUrl = await http.GetGateway().ConfigureAwait(false); GatewayUrlMemoryCache.UpdateUrl(gatewayUrl); } catch (Exception ex) when(ex is DiscordHttpApiException || ex is HttpRequestException || ex is OperationCanceledException) { log.LogError($"[ConnectLoop:GetGateway] {ex}"); log.LogError("[ConnectLoop] No gateway URL to connect with, trying again in 10s..."); await Task.Delay(10 * 1000, cancellationToken).ConfigureAwait(false); continue; } catch (Exception ex) { // This should never-ever happen, but we need to handle it just in-case. log.LogError(ex); log.LogError("[ConnectLoop] Uncaught severe error occured while getting the Gateway URL, setting state to Disconnected."); state = GatewayState.Disconnected; gatewayFailureData = new GatewayFailureData( "Failed to retrieve the Gateway URL because of an unknown error.", ShardFailureReason.Unknown, ex); handshakeCompleteEvent.Set(); OnFailure?.Invoke(this, gatewayFailureData); break; } } log.LogVerbose($"[ConnectLoop] gatewayUrl = {gatewayUrl}"); // Wait if necessary if (nextConnectionDelayMs > 0) { log.LogVerbose($"[ConnectLoop] Waiting {nextConnectionDelayMs}ms before connecting socket..."); await Task.Delay(nextConnectionDelayMs, cancellationToken).ConfigureAwait(false); nextConnectionDelayMs = 0; } try { // Attempt to connect await socket.ConnectAsync(new Uri($"{gatewayUrl}?v={GATEWAY_VERSION}&encoding=json"), cancellationToken) .ConfigureAwait(false); // At this point the socket has successfully connected log.LogVerbose("[ConnectLoop] Socket connected successfully."); break; } catch (WebSocketException wsex) { UnsubscribeSocketEvents(); // Failed to connect log.LogError("[ConnectLoop] Failed to connect: " + $"{wsex.WebSocketErrorCode} ({(int)wsex.WebSocketErrorCode}), {wsex.Message}"); // Invalidate the cached URL since we failed to connect the socket log.LogVerbose("[ConnectLoop] Invalidating Gateway URL..."); GatewayUrlMemoryCache.InvalidateUrl(); // Wait 5s then retry log.LogVerbose("[ConnectLoop] Waiting 5s before retrying..."); await Task.Delay(5000, cancellationToken).ConfigureAwait(false); } } // If the token is cancelled between the socket successfully connecting and the loop exiting, // do not throw an exception because the connection did technically complete before the cancel. if (socket == null || !socket.IsConnected) { // If the loop stopped from the token being cancelled, ensure an exception is still thrown. cancellationToken.ThrowIfCancellationRequested(); } // If this is an automatic reconnection, fire OnReconnected event if (state == GatewayState.Connected) { if (resume) { log.LogInfo("[ConnectLoop:Reconnection] Successfully started a resume."); } else { log.LogInfo("[ConnectLoop:Reconnection] Successfully started creating a new session."); } OnReconnected?.Invoke(this, new GatewayReconnectedEventArgs(!resume)); } }