116 lines
5.4 KiB
C#
116 lines
5.4 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace LaunchDarkly.EventSource
|
|
{
|
|
internal static class AsyncHelpers
|
|
{
|
|
// General-purpose timeout logic that uses a timed CancellationToken instead of
|
|
// Task.Delay. The cancellationToken that is passed in is for explicitly cancelling
|
|
// the operation from elsewhere; DoWithTimeout wraps this to create a token that can
|
|
// also be cancelled by a timeout, and passes that token to taskFn.
|
|
//
|
|
// This method will only work if taskFn actually responds to the cancellation token
|
|
// by throwing an OperationCanceledException. If taskFn ignores the token, timeouts
|
|
// will not happen.
|
|
internal static async Task<T> DoWithTimeout<T>(
|
|
TimeSpan timeout,
|
|
CancellationToken cancellationToken,
|
|
Func<CancellationToken, Task<T>> taskFn
|
|
)
|
|
{
|
|
using (var timeoutCancellation = new CancellationTokenSource(timeout))
|
|
{
|
|
using (var combinedCancellation = CancellationTokenSource.CreateLinkedTokenSource(
|
|
cancellationToken, timeoutCancellation.Token))
|
|
{
|
|
Task<T> task = null;
|
|
try
|
|
{
|
|
task = taskFn(combinedCancellation.Token);
|
|
var result = await task;
|
|
return result;
|
|
}
|
|
catch (AggregateException e) when (e.InnerException is OperationCanceledException)
|
|
{
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
throw e.InnerException;
|
|
}
|
|
throw new ReadTimeoutException();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
throw;
|
|
}
|
|
throw new ReadTimeoutException();
|
|
}
|
|
}
|
|
}
|
|
// We never need to call SuppressExceptions(task) in this method, because the act
|
|
// of awaiting the task - whether it completes normally or throws an exception -
|
|
// makes it "observed". The timeout isn't happening in a different task, it's
|
|
// causing this task to throw an OperationCanceledException, after which point it
|
|
// can't generate any other exceptions.
|
|
}
|
|
|
|
// Take a task that can't be terminated early with a CancellationToken, and wrap it in a task
|
|
// that can. If you cancel the wrapped task, the inner task will still continue, but we're
|
|
// assuming that in these situations we'll eventually be closing the underlying socket which
|
|
// will terminate any pending I/O.
|
|
internal static Task<T> AllowCancellation<T>(Task<T> originalTask, CancellationToken cancellationToken)
|
|
{
|
|
var cancellableTask = originalTask.ContinueWith(
|
|
completedTask => completedTask.GetAwaiter().GetResult(),
|
|
cancellationToken,
|
|
TaskContinuationOptions.ExecuteSynchronously,
|
|
TaskScheduler.Default
|
|
);
|
|
|
|
// Now, if originalTask either completes successfully or fails with an exception, anyone who
|
|
// was waiting on cancellableTask will get the result or the exception.
|
|
//
|
|
// But if cancellationToken gets cancelled, cancellableTask will stop and throw an
|
|
// OperationCancelledException, just as cancellable I/O operation normally would. The
|
|
// caller is no longer blocked - even though originalTask may still be executing.
|
|
//
|
|
// Normally it would be a problem that originalTask could still be hanging around, but
|
|
// in our case if things are getting cancelled it means the connection is going to be
|
|
// closed soon, causing any lingering I/O tasks that use that connection to fail. We
|
|
// just need to use SuppressExceptions to make sure those failures are discarded rather
|
|
// than causing an "unobserved exception" situation.
|
|
|
|
cancellableTask.ContinueWith(
|
|
completedTask =>
|
|
{
|
|
SuppressExceptions(originalTask);
|
|
},
|
|
TaskContinuationOptions.OnlyOnCanceled
|
|
);
|
|
return cancellableTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a continuation to a task so that if the task throws an uncaught exception, the exception
|
|
/// is not "unobserved" (which can be a fatal error in some versions of .NET). We must do this
|
|
/// whenever we're going to discard a task without awaiting it, if there's any possibility that
|
|
/// it could throw an exception.
|
|
/// </summary>
|
|
/// <param name="task">a task we are not going to await</param>
|
|
internal static void SuppressExceptions(Task task)
|
|
{
|
|
task.ContinueWith(
|
|
t =>
|
|
{
|
|
// Simply accessing the Exception property makes this exception observed.
|
|
var e = t.Exception;
|
|
},
|
|
TaskContinuationOptions.OnlyOnFaulted
|
|
);
|
|
}
|
|
}
|
|
}
|