/// <summary> /// Creates a connection (aka transport), if there isn't an active one. /// </summary> public void RequestConnection() { lock (Lock) { switch (_state) { case ConnectivityState.Idle: SubchannelLog.ConnectionRequested(_logger, Id); // Only start connecting underlying transport if in an idle state. UpdateConnectivityState(ConnectivityState.Connecting, "Connection requested."); break; case ConnectivityState.Connecting: case ConnectivityState.Ready: case ConnectivityState.TransientFailure: SubchannelLog.ConnectionRequestedInNonIdleState(_logger, Id, _state); // We're already attempting to connect to the transport. // If the connection is waiting in a delayed backoff then interrupt // the delay and immediately retry connection. _delayInterruptTcs?.TrySetResult(null); return; case ConnectivityState.Shutdown: throw new InvalidOperationException($"Subchannel id '{Id}' has been shutdown."); default: throw new ArgumentOutOfRangeException("state", _state, "Unexpected state."); } } _ = ConnectTransportAsync(); }
internal Subchannel(ConnectionManager manager, IReadOnlyList <BalancerAddress> addresses) { Lock = new object(); _logger = manager.LoggerFactory.CreateLogger(GetType()); Id = manager.GetNextId(); _addresses = addresses.ToList(); _manager = manager; Attributes = new BalancerAttributes(); SubchannelLog.SubchannelCreated(_logger, Id, addresses); }
internal void RaiseStateChanged(ConnectivityState state, Status status) { SubchannelLog.SubchannelStateChanged(_logger, Id, state, status); if (_stateChangedRegistrations.Count > 0) { var subchannelState = new SubchannelState(state, status); foreach (var registration in _stateChangedRegistrations) { registration.Invoke(subchannelState); } } }
/// <summary> /// Replaces the existing addresses used with this <see cref="Subchannel"/>. /// <para> /// If the subchannel has an active connection and the new addresses contain the connected address /// then the connection is reused. Otherwise the subchannel will reconnect. /// </para> /// </summary> /// <param name="addresses"></param> public void UpdateAddresses(IReadOnlyList <BalancerAddress> addresses) { var requireReconnect = false; lock (Lock) { if (_addresses.SequenceEqual(addresses, BalancerAddressEqualityComparer.Instance)) { // Don't do anything if new addresses match existing addresses. return; } _addresses.Clear(); _addresses.AddRange(addresses); switch (_state) { case ConnectivityState.Idle: break; case ConnectivityState.Connecting: case ConnectivityState.TransientFailure: SubchannelLog.AddressesUpdatedWhileConnecting(_logger, Id); requireReconnect = true; break; case ConnectivityState.Ready: // Transport uses the subchannel lock but take copy in an abundance of caution. var currentAddress = CurrentAddress; if (currentAddress != null && !_addresses.Contains(currentAddress)) { requireReconnect = true; SubchannelLog.ConnectedAddressNotInUpdatedAddresses(_logger, Id, currentAddress); } break; case ConnectivityState.Shutdown: throw new InvalidOperationException($"Subchannel id '{Id}' has been shutdown."); default: throw new ArgumentOutOfRangeException("state", _state, "Unexpected state."); } } if (requireReconnect) { _connectCts?.Cancel(); Transport.Disconnect(); RequestConnection(); } }
private async Task ConnectTransportAsync() { // There shouldn't be a previous connect in progress, but cancel the CTS to ensure they're no longer running. _connectCts?.Cancel(); _connectCts = new CancellationTokenSource(); var backoffPolicy = _manager.BackoffPolicyFactory.Create(); try { SubchannelLog.ConnectingTransport(_logger, Id); for (var attempt = 0; ; attempt++) { lock (Lock) { if (_state == ConnectivityState.Shutdown) { return; } } if (await Transport.TryConnectAsync(_connectCts.Token).ConfigureAwait(false)) { return; } _connectCts.Token.ThrowIfCancellationRequested(); _delayInterruptTcs = new TaskCompletionSource <object?>(TaskCreationOptions.RunContinuationsAsynchronously); var delayCts = new CancellationTokenSource(); var backoffTicks = backoffPolicy.NextBackoff().Ticks; // Task.Delay supports up to Int32.MaxValue milliseconds. // Note that even if the maximum backoff is configured to this maximum, the jitter could push it over the limit. // Force an upper bound here to ensure an unsupported backoff is never used. backoffTicks = Math.Min(backoffTicks, TimeSpan.TicksPerMillisecond * int.MaxValue); var backkoff = TimeSpan.FromTicks(backoffTicks); SubchannelLog.StartingConnectBackoff(_logger, Id, backkoff); var completedTask = await Task.WhenAny(Task.Delay(backkoff, delayCts.Token), _delayInterruptTcs.Task).ConfigureAwait(false); if (completedTask != _delayInterruptTcs.Task) { // Task.Delay won. Check CTS to see if it won because of cancellation. delayCts.Token.ThrowIfCancellationRequested(); } else { SubchannelLog.ConnectBackoffInterrupted(_logger, Id); // Delay interrupt was triggered. Reset back-off. backoffPolicy = _manager.BackoffPolicyFactory.Create(); // Cancel the Task.Delay that's no longer needed. // https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/519ef7d231c01116f02bc04354816a735f2a36b6/AsyncGuidance.md#using-a-timeout delayCts.Cancel(); } } } catch (OperationCanceledException) { SubchannelLog.ConnectCanceled(_logger, Id); } catch (Exception ex) { SubchannelLog.ConnectError(_logger, Id, ex); UpdateConnectivityState(ConnectivityState.TransientFailure, "Error connecting to subchannel."); } }
private async Task ConnectTransportAsync() { // There shouldn't be a previous connect in progress, but cancel the CTS to ensure they're no longer running. _connectCts?.Cancel(); _connectCts = new CancellationTokenSource(); const int InitialBackOffMs = 1000; try { SubchannelLog.ConnectingTransport(_logger, Id); var backoffMs = InitialBackOffMs; for (var attempt = 0; ; attempt++) { lock (Lock) { if (_state == ConnectivityState.Shutdown) { return; } } if (await Transport.TryConnectAsync(_connectCts.Token).ConfigureAwait(false)) { return; } _connectCts.Token.ThrowIfCancellationRequested(); _delayInterruptTcs = new TaskCompletionSource <object?>(TaskCreationOptions.RunContinuationsAsynchronously); var delayCts = new CancellationTokenSource(); var delay = TimeSpan.FromMilliseconds(backoffMs); SubchannelLog.StartingConnectBackoff(_logger, Id, delay); var completedTask = await Task.WhenAny(Task.Delay(delay, delayCts.Token), _delayInterruptTcs.Task).ConfigureAwait(false); if (completedTask != _delayInterruptTcs.Task) { // Task.Delay won. Check CTS to see if it won because of cancellation. delayCts.Token.ThrowIfCancellationRequested(); } else { SubchannelLog.ConnectBackoffInterrupted(_logger, Id); // Delay interrupt was triggered. Reset back-off. backoffMs = InitialBackOffMs; // Cancel the Task.Delay that's no longer needed. // https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/519ef7d231c01116f02bc04354816a735f2a36b6/AsyncGuidance.md#using-a-timeout delayCts.Cancel(); } // Exponential backoff with max. backoffMs = (int)Math.Min(backoffMs * 1.6, 1000 * 120); } } catch (OperationCanceledException) { SubchannelLog.ConnectCanceled(_logger, Id); } catch (Exception ex) { SubchannelLog.ConnectError(_logger, Id, ex); UpdateConnectivityState(ConnectivityState.TransientFailure, "Error connecting to subchannel."); } }