Providing more context on three different approaches. My service monitors other web applications availability. So, it needs to establish lots of connections to various web sites. Some of them crash/return errors/become unresponsive.
Axis Y - number of hung tests (sessions). Drops to 0 caused by deployments/restarts.
I. (Jan 25th) After revamping a service, the initial implementation used ReadAsync with a cancellation token. This resulted in lots of tests hanging (running requests against those web sites showed that servers indeed sometimes didn't return content).
II. (Feb 17th) Deployed a change which guarded cancellation with Task.Delay. This completely fixed this issue.
private async Task<int> StreamReadWithCancellationTokenAsync(Stream stream, byte[] buffer, int count, Task cancellationDelayTask)
{
if (cancellationDelayTask.IsCanceled)
{
throw new TaskCanceledException();
}
// Stream.ReadAsync doesn't honor cancellation token. It only checks it at the beginning. The actual
// operation is not guarded. As a result if remote server never responds and connection never closed
// it will lead to this operation hanging forever.
Task<int> readBytesTask = stream.ReadAsync(
buffer,
0,
count);
await Task.WhenAny(readBytesTask, cancellationDelayTask).ConfigureAwait(false);
// Check whether cancellation task is cancelled (or completed).
if (cancellationDelayTask.IsCanceled || cancellationDelayTask.IsCompleted)
{
throw new TaskCanceledException();
}
// Means that main task completed. We use Result directly.
// If the main task failed the following line will throw an exception and
// we'll catch it above.
int readBytes = readBytesTask.Result;
return readBytes;
}
III (March 3rd) Following this StackOverflow implemented closing a stream based on timeout:
using (timeoutToken.Register(() => stream.Close()))
{
// Stream.ReadAsync doesn't honor cancellation token. It only checks it at the beginning. The actual
// operation is not guarded. As a result if a remote server never responds and connection never closed
// it will lead to this operation hanging forever.
// ReSharper disable once MethodSupportsCancellation
readBytes = await targetStream.ReadAsync(
buffer,
0,
Math.Min(responseBodyLimitInBytes - totalReadBytes, buffer.Length)).ConfigureAwait(false);
}
This implementation brought hangs back (not to the same extent as the initial approach):

Reverted back to Task.Delay solution.