/// <summary>Run the pump until the inner iterator is done, an error occurs, or the cancellation token is fired</summary> public async Task PumpAsync(bool stopOnFirstError, CancellationToken cancellationToken) { if (m_state != STATE_IDLE) { // either way, we need to stop ! Exception error; if (m_state == STATE_DISPOSED) { error = new ObjectDisposedException(null, "Pump has already been disposed"); } else if (m_state >= STATE_FAILED) { error = new InvalidOperationException("Pump has already completed once"); } else { error = new InvalidOperationException("Pump is already running"); } try { m_target.OnError(ExceptionDispatchInfo.Capture(error)); } catch { m_target.OnCompleted(); } throw error; } try { LogPump("Starting pump"); while (!cancellationToken.IsCancellationRequested && m_state != STATE_DISPOSED) { LogPump("Waiting for next"); m_state = STATE_WAITING_FOR_NEXT; var current = await m_source.ReceiveAsync(cancellationToken).ConfigureAwait(false); LogPump("Received " + (current.HasValue ? "value" : current.HasFailed ? "error" : "completion") + ", publishing..."); m_state = STATE_PUBLISHING_TO_TARGET; await m_target.Publish(current, cancellationToken).ConfigureAwait(false); if (current.HasFailed && stopOnFirstError) { m_state = STATE_FAILED; LogPump("Stopping after this error"); current.ThrowForNonSuccess(); } else if (current.IsEmpty) { m_state = STATE_DONE; LogPump("Completed"); return; } } // push the cancellation on the queue, and throw throw new OperationCanceledException(cancellationToken); } catch (Exception e) { LogPump("Failed " + e.Message); switch (m_state) { case STATE_WAITING_FOR_NEXT: { // push the info to the called try { m_target.OnError(ExceptionDispatchInfo.Capture(e)); } catch (Exception x) { LogPump("Failed to notify target of error: " + x.Message); throw; } break; } case STATE_PUBLISHING_TO_TARGET: // the error comes from the target itself, push back to caller! case STATE_FAILED: // we want to notify the caller of some problem { throw; } } } finally { if (m_state != STATE_DISPOSED) { m_target.OnCompleted(); } LogPump("Stopped pump"); } }
/// <summary>Consumes all the elements of the source, and publish them to the target, one by one and in order</summary> /// <param name="source">Source that produces elements asynchronously</param> /// <param name="target">Target that consumes elements asynchronously</param> /// <param name="ct">Cancellation token</param> /// <returns>Task that completes when all the elements of the source have been published to the target, or fails if on the first error, or the token is cancelled unexpectedly</returns> /// <remarks>The pump will only read one element at a time, and wait for it to be published to the target, before reading the next element.</remarks> public static async Task PumpToAsync <T>(this IAsyncSource <T> source, IAsyncTarget <T> target, CancellationToken ct) { ct.ThrowIfCancellationRequested(); bool notifiedCompletion = false; bool notifiedError = false; try { //LogPump("Starting pump"); while (!ct.IsCancellationRequested) { //LogPump("Waiting for next"); var current = await source.ReceiveAsync(ct).ConfigureAwait(false); //LogPump("Received " + (current.HasValue ? "value" : current.Failed ? "error" : "completion") + ", publishing... " + current); if (ct.IsCancellationRequested) { // REVIEW: should we notify the target? // REVIEW: if the item is IDisposble, who will clean up? break; } // push the data/error/completion on to the target, which will triage and update its state accordingly await target.Publish(current, ct).ConfigureAwait(false); if (current.Failed) { // bounce the error back to the caller //REVIEW: SHOULD WE? We poush the error to the target, and the SAME error to the caller... who should be responsible for handling it? // => target should know about the error (to cancel something) // => caller should maybe also know that the pump failed unexpectedly.... notifiedError = true; current.ThrowForNonSuccess(); // throws an exception right here return; // should not be reached } else if (current.IsEmpty) { // the source has completed, stop the pump //LogPump("Completed"); notifiedCompletion = true; return; } } // notify cancellation if it happend while we were pumping if (ct.IsCancellationRequested) { //LogPump("We were cancelled!"); throw new OperationCanceledException(ct); } } catch (Exception e) { //LogPump("Failed: " + e); if (!notifiedCompletion && !notifiedError) { // notify the target that we crashed while fetching the next try { //LogPump("Push error down to target: " + e.Message); target.OnError(ExceptionDispatchInfo.Capture(e)); notifiedError = true; } catch (Exception x) when(!x.IsFatalError()) { //LogPump("Failed to notify target of error: " + x.Message); } } throw; } finally { if (!notifiedCompletion) { // we must be sure to complete the target if we haven't done so yet! //LogPump("Notify target of completion due to unexpected conditions"); target.OnCompleted(); } //LogPump("Stopped pump"); } }