/// <summary>
        /// Tries to the move to the next page in the document producer.
        /// </summary>
        /// <param name="token">The cancellation token.</param>
        /// <returns>Whether the operation was successful.</returns>
        private async Task <bool> MoveNextPageAsync(CancellationToken token)
        {
            token.ThrowIfCancellationRequested();

            if (this.itemsLeftInCurrentPage != 0)
            {
                throw new InvalidOperationException("Tried to move onto the next page before finishing the first page.");
            }

            // We need to buffer pages if empty, since that how we know there are no more pages left.
            await this.BufferMoreIfEmptyAsync(token);

            if (this.bufferedPages.Count == 0)
            {
                return(false);
            }

            // Pull a FeedResponse using TryMonad (we could have buffered an exception).
            TryMonad <DocumentFeedResponse <CosmosElement> > tryMonad = await this.bufferedPages.TakeAsync(token);

            DocumentFeedResponse <CosmosElement> feedResponse = tryMonad.Match <DocumentFeedResponse <CosmosElement> >(
                onSuccess: ((page) =>
            {
                return(page);
            }),
                onError: (exceptionDispatchInfo) =>
            {
                exceptionDispatchInfo.Throw();
                return(null);
            });

            // Update the state.
            this.PreviousContinuationToken = this.currentContinuationToken;
            this.currentContinuationToken  = feedResponse.ResponseContinuation;
            this.currentPage            = feedResponse.GetEnumerator();
            this.IsAtBeginningOfPage    = true;
            this.itemsLeftInCurrentPage = feedResponse.Count;

            // Prime the enumerator,
            // so that current is pointing to the first document instead of one before.
            if (this.MoveToFirstDocumentInPage())
            {
                this.IsAtBeginningOfPage = true;
                return(true);
            }
            else
            {
                // We got an empty page
                if (this.currentContinuationToken != null)
                {
                    return(await this.MoveNextPageAsync(token));
                }

                return(false);
            }
        }
        /// <summary>
        /// Tries to the move to the next page in the document producer.
        /// </summary>
        /// <param name="token">The cancellation token.</param>
        /// <returns>Whether the operation was successful.</returns>
        private async Task <bool> MoveNextPageAsync(CancellationToken token)
        {
            token.ThrowIfCancellationRequested();

            if (this.bufferedPages.Count == 0)
            {
                return(false);
            }

            if (this.itemsLeftInCurrentPage != 0)
            {
                throw new InvalidOperationException("Tried to move onto the next page before finishing the first page.");
            }

            TryMonad <DocumentFeedResponse <CosmosElement> > tryMonad = await this.bufferedPages.TakeAsync(token);

            DocumentFeedResponse <CosmosElement> feedResponse = tryMonad.Match <DocumentFeedResponse <CosmosElement> >(
                onSuccess: ((page) =>
            {
                return(page);
            }),
                onError: (exceptionDispatchInfo) =>
            {
                exceptionDispatchInfo.Throw();
                return(null);
            });

            this.previousContinuationToken = this.currentContinuationToken;
            this.currentContinuationToken  = feedResponse.ResponseContinuation;
            this.currentPage            = feedResponse.GetEnumerator();
            this.itemsLeftInCurrentPage = feedResponse.Count;
            if (this.MoveNextDocumentWithinCurrentPage())
            {
                this.isAtBeginningOfPage = true;
                return(true);
            }
            else
            {
                return(false);
            }
        }
        /// <summary>
        /// Buffers more documents in the producer.
        /// </summary>
        /// <param name="token">The cancellation token.</param>
        /// <returns>A task to await on.</returns>
        public async Task BufferMoreDocumentsAsync(CancellationToken token)
        {
            token.ThrowIfCancellationRequested();

            try
            {
                await this.fetchSemaphore.WaitAsync();

                if (!this.HasMoreBackendResults)
                {
                    // Just NOP
                    return;
                }

                this.fetchSchedulingMetrics.Start();
                this.fetchExecutionRangeAccumulator.BeginFetchRange();
                int pageSize = (int)Math.Min(this.pageSize, (long)int.MaxValue);
                using (DocumentServiceRequest request = this.createRequestFunc(this.PartitionKeyRange, this.backendContinuationToken, pageSize))
                {
                    IDocumentClientRetryPolicy retryPolicy = this.createRetryPolicyFunc();

                    // Custom backoff and retry
                    while (true)
                    {
                        int retries = 0;
                        try
                        {
                            DocumentFeedResponse <CosmosElement> feedResponse = await this.executeRequestFunc(request, retryPolicy, token);

                            this.fetchExecutionRangeAccumulator.EndFetchRange(
                                this.PartitionKeyRange.Id,
                                feedResponse.ActivityId,
                                feedResponse.Count,
                                retries);
                            this.fetchSchedulingMetrics.Stop();
                            this.hasStartedFetching       = true;
                            this.backendContinuationToken = feedResponse.ResponseContinuation;
                            this.activityId = Guid.Parse(feedResponse.ActivityId);
                            await this.bufferedPages.AddAsync(TryMonad <DocumentFeedResponse <CosmosElement> > .FromResult(feedResponse));

                            Interlocked.Add(ref this.bufferedItemCount, feedResponse.Count);

                            QueryMetrics queryMetrics = QueryMetrics.Zero;
                            if (feedResponse.ResponseHeaders[HttpConstants.HttpHeaders.QueryMetrics] != null)
                            {
                                queryMetrics = QueryMetrics.CreateFromDelimitedStringAndClientSideMetrics(
                                    feedResponse.ResponseHeaders[HttpConstants.HttpHeaders.QueryMetrics],
                                    new ClientSideMetrics(
                                        retries,
                                        feedResponse.RequestCharge,
                                        this.fetchExecutionRangeAccumulator.GetExecutionRanges(),
                                        new List <Tuple <string, SchedulingTimeSpan> >()));
                            }

                            if (!this.HasMoreBackendResults)
                            {
                                queryMetrics = QueryMetrics.CreateWithSchedulingMetrics(
                                    queryMetrics,
                                    new List <Tuple <string, SchedulingTimeSpan> >
                                {
                                    new Tuple <string, SchedulingTimeSpan>(
                                        this.PartitionKeyRange.Id,
                                        this.fetchSchedulingMetrics.Elapsed)
                                });
                            }

                            this.produceAsyncCompleteCallback(
                                this,
                                feedResponse.Count,
                                feedResponse.RequestCharge,
                                queryMetrics,
                                feedResponse.ResponseLengthBytes,
                                token);

                            break;
                        }
                        catch (Exception exception)
                        {
                            // See if we need to retry or just throw
                            ShouldRetryResult shouldRetryResult = await retryPolicy.ShouldRetryAsync(exception, token);

                            if (!shouldRetryResult.ShouldRetry)
                            {
                                Exception exceptionToBuffer;
                                if (shouldRetryResult.ExceptionToThrow != null)
                                {
                                    exceptionToBuffer = shouldRetryResult.ExceptionToThrow;
                                }
                                else
                                {
                                    // Propagate original exception.
                                    exceptionToBuffer = exception;
                                }

                                // Buffer the exception instead of throwing, since we don't want an unobserved exception.
                                await this.bufferedPages.AddAsync(TryMonad <DocumentFeedResponse <CosmosElement> > .FromException(exceptionToBuffer));

                                // null out the backend continuation token,
                                // so that people stop trying to buffer more on this producer.
                                this.hasStartedFetching       = true;
                                this.backendContinuationToken = null;
                                break;
                            }
                            else
                            {
                                await Task.Delay(shouldRetryResult.BackoffTime);

                                retries++;
                            }
                        }
                    }
                }
            }
            finally
            {
                this.fetchSchedulingMetrics.Stop();
                this.fetchSemaphore.Release();
            }
        }
 public void SetUp()
 {
     sut = new TryMonad();
 }