// // StartRequestHandlerTask() // // Take any messages on our PendingRequest list and write them to GCP private Task StartRequestHandlerTask() { OnDebugMessage("Started Request Handler"); return(Task.Run(async() => { while (!Done) { var request = PendingRequests.Take(CancellationToken); OnDebugMessage("Setup listen for request"); //If the listener isn't active, start it if (DuplexStream == null || !ListenerIsActive) { // Initialize streaming call, retrieving the stream object DuplexStream = FirestoreClient.Listen(ListenSettings); ListenerIsActive = true; OnDebugMessage("Response Task Not Active, starting"); ResponseHanderTask = StartResponseHandlerTask(); } OnDebugMessage("Sending Request"); // Stream a request to the server await DuplexStream.WriteAsync(request); ActiveRequests.Enqueue(request); } OnDebugMessage("Request Handler Completed"); await DuplexStream.WriteCompleteAsync(); })); }
/// <summary>Snippet for Listen</summary> public async Task Listen() { // Snippet: Listen(CallSettings, BidirectionalStreamingSettings) // Create client FirestoreClient firestoreClient = FirestoreClient.Create(); // Initialize streaming call, retrieving the stream object FirestoreClient.ListenStream response = firestoreClient.Listen(); // Sending requests and retrieving responses can be arbitrarily interleaved // Exact sequence will depend on client/server behavior // Create task to do something with responses from server Task responseHandlerTask = Task.Run(async() => { // Note that C# 8 code can use await foreach AsyncResponseStream <ListenResponse> responseStream = response.GetResponseStream(); while (await responseStream.MoveNextAsync()) { ListenResponse responseItem = responseStream.Current; // Do something with streamed response } // The response stream has completed }); // Send requests to the server bool done = false; while (!done) { // Initialize a request ListenRequest request = new ListenRequest { Database = "", AddTarget = new Target(), RemoveTarget = 0, Labels = { { "", "" }, }, }; // Stream a request to the server await response.WriteAsync(request); // Set "done" to true when sending requests is complete } // Complete writing requests to the stream await response.WriteCompleteAsync(); // Await the response handler // This will complete once all server responses have been processed await responseHandlerTask; // End snippet }
/// <summary>Snippet for Listen</summary> public async Task Listen() { // Snippet: Listen(CallSettings,BidirectionalStreamingSettings) // Create client FirestoreClient firestoreClient = FirestoreClient.Create(); // Initialize streaming call, retrieving the stream object FirestoreClient.ListenStream duplexStream = firestoreClient.Listen(); // Sending requests and retrieving responses can be arbitrarily interleaved. // Exact sequence will depend on client/server behavior. // Create task to do something with responses from server Task responseHandlerTask = Task.Run(async() => { IAsyncEnumerator <ListenResponse> responseStream = duplexStream.ResponseStream; while (await responseStream.MoveNext()) { ListenResponse response = responseStream.Current; // Do something with streamed response } // The response stream has completed }); // Send requests to the server bool done = false; while (!done) { // Initialize a request ListenRequest request = new ListenRequest { Database = new DatabaseRootName("[PROJECT]", "[DATABASE]").ToString(), }; // Stream a request to the server await duplexStream.WriteAsync(request); // Set "done" to true when sending requests is complete } // Complete writing requests to the stream await duplexStream.WriteCompleteAsync(); // Await the response handler. // This will complete once all server responses have been processed. await responseHandlerTask; // End snippet }
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); } }