/* OMG THIS CODE IS COMPLICATED
*
* Here's the core idea. We want to create a ReadAsync function that
* reads from our list of byte arrays **until it gets to the end of
* our current list**.
*
* If we're not there yet, we keep returning data, serializing access
* to the underlying position pointer (i.e. we definitely don't want
* people concurrently moving position along). If we try to read past
* the end, we return the section of data we could read and complete
* it.
*
* Here's where the tricky part comes in. If we're not Completed (i.e.
* the caller still wants to add more byte arrays in the future) and
* we're at the end of the current stream, we want to *block* the read
* (not blocking, but async blocking whatever you know what I mean),
* until somebody adds another byte[] to chew through, or if someone
* rewinds the position.
*
* If we *are* completed, we should return zero to simply complete the
* read, signalling we're at the end of the stream */
public override async Task <int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
retry:
int bytesRead = 0;
int buffersToRemove = 0;
if (isCompleted && position == maxLength)
{
return(0);
}
if (exception != null)
{
throw exception;
}
using (await readStreamLock.LockAsync().ConfigureAwait(false)) {
lock (bytes) {
foreach (var buf in bytes)
{
cancellationToken.ThrowIfCancellationRequested();
if (exception != null)
{
throw exception;
}
int toCopy = Math.Min(count, buf.Length - offsetInCurrentBuffer);
Array.ConstrainedCopy(buf, offsetInCurrentBuffer, buffer, offset, toCopy);
count -= toCopy;
offset += toCopy;
bytesRead += toCopy;
offsetInCurrentBuffer += toCopy;
if (offsetInCurrentBuffer >= buf.Length)
{
offsetInCurrentBuffer = 0;
buffersToRemove++;
}
if (count <= 0)
{
break;
}
}
// Remove buffers that we read in this operation
bytes.RemoveRange(0, buffersToRemove);
position += bytesRead;
}
}
// If we're at the end of the stream and it's not done, prepare
// the next read to park itself unless AddByteArray or Complete
// posts
if (position >= maxLength && !isCompleted)
{
lockRelease = await readStreamLock.LockAsync().ConfigureAwait(false);
}
if (bytesRead == 0 && !isCompleted)
{
// NB: There are certain race conditions where we somehow acquire
// the lock yet are at the end of the stream, and we're not completed
// yet. We should try again so that we can get stuck in the lock.
goto retry;
}
if (cancellationToken.IsCancellationRequested)
{
Interlocked.Exchange(ref lockRelease, EmptyDisposable.Instance).Dispose();
cancellationToken.ThrowIfCancellationRequested();
}
if (exception != null)
{
Interlocked.Exchange(ref lockRelease, EmptyDisposable.Instance).Dispose();
throw exception;
}
if (isCompleted && position < maxLength)
{
// NB: This solves a rare deadlock
//
// 1. ReadAsync called (waiting for lock release)
// 2. AddByteArray called (release lock)
// 3. AddByteArray called (release lock)
// 4. Complete called (release lock the last time)
// 5. ReadAsync called (lock released at this point, the method completed successfully)
// 6. ReadAsync called (deadlock on LockAsync(), because the lock is block, and there is no way to release it)
//
// Current condition forces the lock to be released in the end of 5th point
Interlocked.Exchange(ref lockRelease, EmptyDisposable.Instance).Dispose();
}
return(bytesRead);
}