예제 #1
0
        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;
            }
        }
예제 #2
0
        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);
            }
        }