internal async Task StartAsync() { // State used within the method. This is modified by local methods too. StreamInitializationCause cause = StreamInitializationCause.WatchStarting; FirestoreClient.ListenStream underlyingStream = null; TimeSpan nextBackoff = TimeSpan.Zero; try { // This won't actually run forever. Calling Stop will cancel the cancellation token, and we'll end up with // an exception which may or may not be caught. while (true) { var serverResponse = await GetNextResponse().ConfigureAwait(false); _callbackCancellationTokenSource.Token.ThrowIfCancellationRequested(); var result = await _state.HandleResponseAsync(serverResponse, _callbackCancellationTokenSource.Token).ConfigureAwait(false); switch (result) { case WatchResponseResult.Continue: break; case WatchResponseResult.ResetStream: await CloseStreamAsync().ConfigureAwait(false); cause = StreamInitializationCause.ResetRequested; break; case WatchResponseResult.StreamHealthy: nextBackoff = TimeSpan.Zero; break; default: throw new InvalidOperationException($"Unknown result type: {result}"); } // What about other exception types? } } // Swallow cancellation exceptions unless one of the user-provided cancellation tokens has been // cancelled, in which case it's fine to let it through. catch (OperationCanceledException) when(!_callbackCancellationTokenSource.Token.IsCancellationRequested) { // We really do just swallow the exception. No need for logging. } finally { lock (_stateLock) { _networkCancellationTokenSource.Dispose(); _callbackCancellationTokenSource.Dispose(); _stopCancellationTokenRegistration.Dispose(); _finished = true; } // Make sure we clean up even if we get an exception we don't handle explicitly. await CloseStreamAsync().ConfigureAwait(false); } // Local method responsible for fetching the next response from the server stream, including // stream initialization and error handling. async Task <ListenResponse> GetNextResponse() { while (true) { try { // If we're just starting, or we've closed the stream or it broke, restart. if (underlyingStream == null) { await _scheduler.Delay(_backoffJitter.GetDelay(nextBackoff), _networkCancellationTokenSource.Token).ConfigureAwait(false); nextBackoff = _backoffSettings.NextDelay(nextBackoff); underlyingStream = _db.Client.Listen(); await underlyingStream.TryWriteAsync(CreateRequest(_state.ResumeToken)).ConfigureAwait(false); _state.OnStreamInitialization(cause); } // Wait for a response or end-of-stream var next = await underlyingStream.ResponseStream.MoveNext(_networkCancellationTokenSource.Token).ConfigureAwait(false); // If the server provided a response, return it if (next) { return(underlyingStream.ResponseStream.Current); } // Otherwise, close the current stream and restart. await CloseStreamAsync().ConfigureAwait(false); cause = StreamInitializationCause.StreamCompleted; } catch (RpcException e) when(s_transientErrorStatusCodes.Contains(e.Status.StatusCode)) { // Close the current stream, ready to create a new one. await CloseStreamAsync().ConfigureAwait(false); // Extend the back-off if necessary. if (e.Status.StatusCode == StatusCode.ResourceExhausted) { nextBackoff = _backoffSettings.NextDelay(nextBackoff); } cause = StreamInitializationCause.RpcError; } } } async Task CloseStreamAsync() { if (underlyingStream != null) { var completeTask = underlyingStream.TryWriteCompleteAsync(); // TODO: Handle exceptions from this? if (completeTask != null) { await completeTask.ConfigureAwait(false); } underlyingStream.GrpcCall.Dispose(); } underlyingStream = null; } }
internal async Task StartAsync() { // State used within the method. This is modified by local methods too. StreamInitializationCause cause = StreamInitializationCause.WatchStarting; FirestoreClient.ListenStream underlyingStream = null; IEnumerator <RetryAttempt> retryAttempts = CreateRetryAttemptSequence(); try { // This won't actually run forever. Calling Stop will cancel the cancellation token, and we'll end up with // an exception which may or may not be caught. while (true) { var serverResponse = await GetNextResponse().ConfigureAwait(false); _callbackCancellationTokenSource.Token.ThrowIfCancellationRequested(); var result = await _state.HandleResponseAsync(serverResponse, _callbackCancellationTokenSource.Token).ConfigureAwait(false); switch (result) { case WatchResponseResult.Continue: break; case WatchResponseResult.ResetStream: await CloseStreamAsync().ConfigureAwait(false); cause = StreamInitializationCause.ResetRequested; break; case WatchResponseResult.StreamHealthy: // Reset the retry backoff to zero. retryAttempts = CreateRetryAttemptSequence(); break; default: throw new InvalidOperationException($"Unknown result type: {result}"); } // What about other exception types? } } // Swallow cancellation exceptions unless one of the user-provided cancellation tokens has been // cancelled, in which case it's fine to let it through. catch (OperationCanceledException) when(!_callbackCancellationTokenSource.Token.IsCancellationRequested) { // We really do just swallow the exception. No need for logging. } finally { lock (_stateLock) { _networkCancellationTokenSource.Dispose(); _callbackCancellationTokenSource.Dispose(); _stopCancellationTokenRegistration.Dispose(); _finished = true; } // Make sure we clean up even if we get an exception we don't handle explicitly. await CloseStreamAsync().ConfigureAwait(false); } // Local method responsible for fetching the next response from the server stream, including // stream initialization and error handling. async Task <ListenResponse> GetNextResponse() { while (true) { try { // If we're just starting, or we've closed the stream or it broke, restart. if (underlyingStream == null) { await retryAttempts.Current.BackoffAsync(_networkCancellationTokenSource.Token).ConfigureAwait(false); retryAttempts.MoveNext(); underlyingStream = _db.Client.Listen(_listenCallSettings); await underlyingStream.TryWriteAsync(CreateRequest(_state.ResumeToken)).ConfigureAwait(false); _state.OnStreamInitialization(cause); } // Wait for a response or end-of-stream var next = await underlyingStream.GrpcCall.ResponseStream.MoveNext(_networkCancellationTokenSource.Token).ConfigureAwait(false); // If the server provided a response, return it if (next) { return(underlyingStream.GrpcCall.ResponseStream.Current); } // Otherwise, close the current stream and restart. await CloseStreamAsync().ConfigureAwait(false); cause = StreamInitializationCause.StreamCompleted; } catch (RpcException e) when(s_transientErrorStatusCodes.Contains(e.Status.StatusCode)) { // Close the current stream, ready to create a new one. await CloseStreamAsync().ConfigureAwait(false); // Extend the back-off if necessary. if (e.Status.StatusCode == StatusCode.ResourceExhausted) { retryAttempts.MoveNext(); } cause = StreamInitializationCause.RpcError; } } } async Task CloseStreamAsync() { if (underlyingStream != null) { try { var completeTask = underlyingStream.TryWriteCompleteAsync(); if (completeTask != null) { await completeTask.ConfigureAwait(false); } } catch (RpcException) { // Swallow gRPC errors when trying to "complete" the stream. This may be in response to the network connection // being dropped, at which point completing the stream will fail; we don't want the listener to stop at that // point. Instead, it will reconnect. } underlyingStream.GrpcCall.Dispose(); } underlyingStream = null; } // Create a new enumerator for the retry attempt sequence, starting with a backoff of zero. IEnumerator <RetryAttempt> CreateRetryAttemptSequence() { var iterator = RetryAttempt .CreateRetrySequence(_backoffSettings, _scheduler, initialBackoffOverride: TimeSpan.Zero) .GetEnumerator(); // Make sure Current is already valid iterator.MoveNext(); return(iterator); } }