public async IAsyncEnumerable <StreamPartition> GetPartitionsAsync(
            int maxActivePartitions,
            int maxLoadedPartitions,
            int size   = Constants.DefaultBufferSize,
            bool async = true,
            [EnumeratorCancellation] CancellationToken ct = default
            )
        {
            var activePartitionDisposalTasks = new List <Task>();

            var loadedPartitions = new Queue <StreamPartition>();

            var sourceComplete = false;

            while (!ct.IsCancellationRequested)
            {
                // try to keep as many partitions loaded as possible

                while (!sourceComplete && loadedPartitions.Count < maxLoadedPartitions)
                {
                    Task <StreamPartition> partitionTask = GetNextPartitionAsync(size, async, ct);

#pragma warning disable IDE0068 // Use recommended dispose pattern // disposal is performed by the uploader
                    StreamPartition partition =
                        async
                        ? await partitionTask.ConfigureAwait(false)
                        : partitionTask.EnsureCompleted();

#pragma warning restore IDE0068 // Use recommended dispose pattern

                    if (partition.Length == 0)
                    {
                        // we've run off the end of the source

                        if (_contentLength.HasValue)
                        {
                            // if we have a content length, then we should be able to guarantee this
                            Debug.Assert(partition.ParentPosition == _contentLength);
                        }

                        // don't yield this partition, and instead just let the load exhaust itself

                        sourceComplete = true;

                        partition.Dispose();

                        //Console.WriteLine("Source exhausted");
                        break;
                    }
                    else
                    {
                        loadedPartitions.Enqueue(partition);

                        //Console.WriteLine($"Enqueued partition {partition.ParentPosition}");
                    }
                }

                // if we already have a full set of active partitions, wait until some have completed

                activePartitionDisposalTasks.RemoveAll(t => t.IsCompleted);

                if (activePartitionDisposalTasks.Any() && activePartitionDisposalTasks.Count == maxActivePartitions)
                {
                    //Console.WriteLine("Waiting for partition to complete...");

                    if (async)
                    {
                        await Task.WhenAny(activePartitionDisposalTasks).ConfigureAwait(false);
                    }
                    else
                    {
                        Task.WaitAny(activePartitionDisposalTasks.ToArray(), ct);
                    }
                }

                // now we are assured to have room for partitions to be worked on

                if (loadedPartitions.Any())
                {
                    StreamPartition partition = loadedPartitions.Dequeue();

                    //Console.WriteLine($"Activation partition {partition.ParentPosition}");

                    activePartitionDisposalTasks.Add(partition.DisposalTask);

                    // yield it to the consumer
                    yield return(partition);
                }
                else
                {
                    // we've run out of buffered partitions, which means we've also run out of source

                    //Console.WriteLine("Partitions exhausted");

                    //Console.WriteLine("Waiting for remaining partitions to complete...");

                    if (async)
                    {
                        await Task.WhenAll(activePartitionDisposalTasks).ConfigureAwait(false);
                    }
                    else
                    {
                        Task.WaitAll(activePartitionDisposalTasks.ToArray(), ct);
                    }

                    //Console.WriteLine("All partitions complete...");
                    break;
                }
            }

            // we're out of partitions, or cancellation was requested and we need to dispose of remaining partitions

            foreach (StreamPartition partition in loadedPartitions)
            {
                //Console.WriteLine($"Disposing partition {partition.ParentPosition}");

                partition.Dispose();
            }
        }
        /// <summary>
        /// Generate a forward-only sequence of substreams based on bufferSize.
        /// </summary>
        /// <returns>StreamPartition</returns>
        private async Task <StreamPartition> GetNextPartitionAsync(Func <long, Stream> streamSource, bool disposeStream, Func <long> getStartPosition, Action <int> incrementStartPosition, int size = Constants.DefaultBufferSize, bool async = true, CancellationToken ct = default)
        {
            if (async)
            {
                await _getNextPartitionAsync_Semaphore.WaitAsync(ct).ConfigureAwait(false);
            }
            else
            {
                _getNextPartitionAsync_Semaphore.Wait();
            }

            var    startPosition = getStartPosition();
            Stream stream        = streamSource(startPosition);

            IMemoryOwner <byte> buffer;

            lock (_memoryPool)
            {
                // TODO these operations should be simplified with Memory- and Span-accepting APIs in future NET Standard
                buffer = _memoryPool.Rent(size);
            }

            //Console.WriteLine($"Rented buffer of size {size} bytes");
            //this.logger?.LogTrace($"Rented buffer of size {size} bytes");

            if (MemoryMarshal.TryGetArray <byte>(buffer.Memory, out ArraySegment <byte> segment))
            {
                var count =
                    async
                    ? await stream.ReadAsync(segment.Array, 0, segment.Count, ct).ConfigureAwait(false)
                    : stream.Read(segment.Array, 0, segment.Count);

                if (disposeStream)
                {
                    stream.Dispose();
                }

                incrementStartPosition(count);

                _getNextPartitionAsync_Semaphore.Release();

                //this.logger?.LogTrace($"Read {count} bytes");

                var partition = new StreamPartition(
                    buffer.Memory,
                    startPosition,
                    count,
                    () =>
                {
                    buffer.Dispose();
                    //Console.WriteLine($"Disposed buffer of size {size} bytes");
                    //this.logger?.LogTrace($"Disposed buffer of size {size} bytes");
                },
                    ct
                    );

                //Console.WriteLine($"Creating partition {partition.ParentPosition}");

                return(partition);
            }
            else
            {
                if (disposeStream)
                {
                    stream.Dispose();
                }

                _getNextPartitionAsync_Semaphore.Release();
                throw Errors.UnableAccessArray();
            }
        }