/// <summary> /// Initializes a new instance of the ItemProducerTreeComparableTask class. /// </summary> /// <param name="producer">The producer to fetch from.</param> /// <param name="taskPriorityFunction">The callback to determine the fetch priority of the document producer.</param> public ItemProducerTreeComparableTask( ItemProducerTree producer, Func <ItemProducerTree, int> taskPriorityFunction) : base(taskPriorityFunction(producer)) { this.producer = producer; }
/// <summary> /// Function that is given to all the document producers to call on once they are done fetching. /// This is so that the CosmosCrossPartitionQueryExecutionContext can aggregate metadata from them. /// </summary> /// <param name="producer">The document producer that just finished fetching.</param> /// <param name="itemsBuffered">The number of items that the producer just fetched.</param> /// <param name="resourceUnitUsage">The amount of RUs that the producer just consumed.</param> /// <param name="responseLengthBytes">The length of the response the producer just got back in bytes.</param> /// <param name="token">The cancellation token.</param> /// <remarks> /// This function is by nature a bit racy. /// A query might be fully drained but a background task is still fetching documents so this will get called after the context is done. /// </remarks> private void OnItemProducerTreeCompleteFetching( ItemProducerTree producer, int itemsBuffered, double resourceUnitUsage, long responseLengthBytes, CancellationToken token) { // Update charge and states this.requestChargeTracker.AddCharge(resourceUnitUsage); Interlocked.Add(ref this.totalBufferedItems, itemsBuffered); this.IncrementResponseLengthBytes(responseLengthBytes); // Adjust the producer page size so that we reach the optimal page size. producer.PageSize = Math.Min((long)(producer.PageSize * DynamicPageSizeAdjustmentFactor), this.actualMaxPageSize); // Adjust Max Degree Of Parallelism if necessary // (needs to wait for comparable task scheduler refactor). // Fetch again if necessary if (producer.HasMoreBackendResults) { // 4mb is the max response size long expectedResponseSize = Math.Min(producer.PageSize, 4 * 1024 * 1024); if (this.CanPrefetch && this.FreeItemSpace > expectedResponseSize) { this.TryScheduleFetch(producer); } } }
/// <summary> /// After a split you need to maintain the continuation tokens for all the child document producers until a condition is met. /// For example lets say that a document producer is at continuation X and it gets split, /// then the children each get continuation X, but since you only drain from one of them at a time you are left with the first child having /// continuation X + delta and the second child having continuation X (draw this out if you are following along). /// At this point you have the answer the question: "Which continuation token do you return to the user?". /// Let's say you return X, then when you come back to the first child you will be repeating work, thus returning some documents more than once. /// Let's say you return X + delta, then you fine when you return to the first child, but when you get to the second child you don't have a continuation token /// meaning that you will be repeating all the document for the second partition up until X and again you will be returning some documents more than once. /// Thus you have to return the continuation token for both children. /// Both this means you are returning more than 1 continuation token for the rest of the query. /// Well a naive optimization is to flush the continuation for a child partition once you are done draining from it, which isn't bad for a parallel query, /// but if you have an order by query you might not be done with a producer until the end of the query. /// The next optimization for a parallel query is to flush the continuation token the moment you start reading from a child partition. /// This works for a parallel query, but breaks for an order by query. /// The final realization is that for an order by query you are only choosing between multiple child partitions when their is a tie, /// so the key is that you can dump the continuation token the moment you come across a new order by item. /// For order by queries that is determined by the order by field and for parallel queries that is the moment you come by a new rid (which is any document, since rids are unique within a partition). /// So by passing an equality comparer to the document producers they can determine whether they are still "active". /// </summary> /// <returns> /// Returns all document producers whose continuation token you have to return. /// Only during a split will this list contain more than 1 item. /// </returns> public IEnumerable <ItemProducer> GetActiveItemProducers() { if (this.returnResultsInDeterministicOrder) { ItemProducerTree current = this.itemProducerForest.Peek().CurrentItemProducerTree; if (current.HasMoreResults && !current.IsActive) { // If the current document producer tree has more results, but isn't active. // then we still want to emit it, since it won't get picked up in the below for loop. yield return(current.Root); } foreach (ItemProducerTree itemProducerTree in this.itemProducerForest) { foreach (ItemProducer itemProducer in itemProducerTree.GetActiveItemProducers()) { yield return(itemProducer); } } } else { // Just return all item producers that have a continuation token foreach (ItemProducerTree itemProducerTree in this.itemProducerForest) { foreach (ItemProducerTree leaf in itemProducerTree) { if (leaf.HasMoreResults) { yield return(leaf.Root); } } } } }
/// <summary> /// Tries to schedule a fetch from the document producer tree. /// </summary> /// <param name="itemProducerTree">The document producer tree to schedule a fetch for.</param> /// <returns>Whether or not the fetch was successfully scheduled.</returns> private bool TryScheduleFetch(ItemProducerTree itemProducerTree) { return(this.comparableTaskScheduler.TryQueueTask( new ItemProducerTreeComparableTask( itemProducerTree, this.fetchPrioirtyFunction), default)); }
public async Task TestMoveNextWithEmptyPagesAndSplitAsync(string initialContinuationToken) { int maxPageSize = 5; List <MockPartitionResponse[]> mockResponsesScenario = MockQueryFactory.GetSplitScenarios(); foreach (MockPartitionResponse[] mockResponse in mockResponsesScenario) { Mock <CosmosQueryClient> mockQueryClient = new Mock <CosmosQueryClient>(); IList <ToDoItem> allItems = MockQueryFactory.GenerateAndMockResponse( mockQueryClient, isOrderByQuery: false, sqlQuerySpec: MockQueryFactory.DefaultQuerySpec, containerRid: MockQueryFactory.DefaultCollectionRid, initContinuationToken: initialContinuationToken, maxPageSize: maxPageSize, mockResponseForSinglePartition: mockResponse, cancellationTokenForMocks: this.cancellationToken); CosmosQueryContext context = MockQueryFactory.CreateContext( mockQueryClient.Object); ItemProducerTree itemProducerTree = new ItemProducerTree( context, MockQueryFactory.DefaultQuerySpec, mockResponse[0].PartitionKeyRange, MockItemProducerFactory.DefaultTreeProduceAsyncCompleteDelegate, new ParallelItemProducerTreeComparer(), CosmosElementEqualityComparer.Value, true, MockQueryFactory.DefaultCollectionRid, maxPageSize, initialContinuationToken: initialContinuationToken); Assert.IsTrue(itemProducerTree.HasMoreResults); List <ToDoItem> itemsRead = new List <ToDoItem>(); while ((await itemProducerTree.MoveNextAsync(this.cancellationToken)).successfullyMovedNext) { Assert.IsTrue(itemProducerTree.HasMoreResults); if (itemProducerTree.Current != null) { string jsonValue = itemProducerTree.Current.ToString(); ToDoItem item = JsonConvert.DeserializeObject <ToDoItem>(jsonValue); itemsRead.Add(item); } } Assert.IsFalse(itemProducerTree.HasMoreResults); Assert.AreEqual(allItems.Count, itemsRead.Count); List <ToDoItem> exepected = allItems.OrderBy(x => x.id).ToList(); List <ToDoItem> actual = itemsRead.OrderBy(x => x.id).ToList(); CollectionAssert.AreEqual(exepected, actual, new ToDoItemComparer()); } }
public static void DefaultTreeProduceAsyncCompleteDelegate( ItemProducerTree itemProducerTree, int numberOfDocuments, double requestCharge, long responseLengthInBytes, CancellationToken token) { return; }
public async Task TestMoveNextWithEmptyPagesAsync(string initialContinuationToken) { int maxPageSize = 5; List <MockPartitionResponse[]> mockResponses = MockQueryFactory.GetAllCombinationWithEmptyPage(); foreach (MockPartitionResponse[] mockResponse in mockResponses) { Mock <CosmosQueryClient> mockQueryClient = new Mock <CosmosQueryClient>(); IList <ToDoItem> allItems = MockQueryFactory.GenerateAndMockResponse( mockQueryClient, isOrderByQuery: false, sqlQuerySpec: MockQueryFactory.DefaultQuerySpec, containerRid: MockQueryFactory.DefaultCollectionRid, initContinuationToken: initialContinuationToken, maxPageSize: maxPageSize, mockResponseForSinglePartition: mockResponse, cancellationTokenForMocks: this.cancellationToken); CosmosQueryContext context = MockQueryFactory.CreateContext( mockQueryClient.Object); ItemProducerTree itemProducerTree = new ItemProducerTree( queryContext: context, querySpecForInit: MockQueryFactory.DefaultQuerySpec, partitionKeyRange: mockResponse[0].PartitionKeyRange, produceAsyncCompleteCallback: MockItemProducerFactory.DefaultTreeProduceAsyncCompleteDelegate, itemProducerTreeComparer: new ParallelItemProducerTreeComparer(), equalityComparer: CosmosElementEqualityComparer.Value, testSettings: new TestInjections(simulate429s: false, simulateEmptyPages: false), deferFirstPage: true, collectionRid: MockQueryFactory.DefaultCollectionRid, initialPageSize: maxPageSize, initialContinuationToken: initialContinuationToken); Assert.IsTrue(itemProducerTree.HasMoreResults); List <ToDoItem> itemsRead = new List <ToDoItem>(); while ((await itemProducerTree.TryMoveNextPageAsync(this.cancellationToken)).movedToNextPage) { while (itemProducerTree.TryMoveNextDocumentWithinPage()) { Assert.IsTrue(itemProducerTree.HasMoreResults); string jsonValue = itemProducerTree.Current.ToString(); ToDoItem item = JsonConvert.DeserializeObject <ToDoItem>(jsonValue); itemsRead.Add(item); } } Assert.IsFalse(itemProducerTree.HasMoreResults); Assert.AreEqual(allItems.Count, itemsRead.Count); CollectionAssert.AreEqual(itemsRead, allItems.ToList(), new ToDoItemComparer()); } }
public static void DefaultTreeProduceAsyncCompleteDelegate( ItemProducerTree itemProducerTree, int numberOfDocuments, double requestCharge, IReadOnlyCollection <QueryPageDiagnostics> diagnostics, long responseLengthInBytes, CancellationToken token) { return; }
public override async Task <QueryResponseCore> DrainAsync(int maxElements, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); // In order to maintain the continuation token for the user we must drain with a few constraints // 1) We fully drain from the left most partition before moving on to the next partition // 2) We drain only full pages from the document producer so we aren't left with a partial page // otherwise we would need to add to the continuation token how many items to skip over on that page. // Only drain from the leftmost (current) document producer tree ItemProducerTree currentItemProducerTree = this.PopCurrentItemProducerTree(); List <CosmosElement> results = new List <CosmosElement>(); try { (bool gotNextPage, QueryResponseCore? failureResponse) = await currentItemProducerTree.TryMoveNextPageAsync(cancellationToken); if (failureResponse != null) { return(failureResponse.Value); } if (gotNextPage) { int itemsLeftInCurrentPage = currentItemProducerTree.ItemsLeftInCurrentPage; // Only drain full pages or less if this is a top query. currentItemProducerTree.TryMoveNextDocumentWithinPage(); int numberOfItemsToDrain = Math.Min(itemsLeftInCurrentPage, maxElements); for (int i = 0; i < numberOfItemsToDrain; i++) { results.Add(currentItemProducerTree.Current); currentItemProducerTree.TryMoveNextDocumentWithinPage(); } } } finally { this.PushCurrentItemProducerTree(currentItemProducerTree); } return(QueryResponseCore.CreateSuccess( result: results, requestCharge: this.requestChargeTracker.GetAndResetCharge(), activityId: null, responseLengthBytes: this.GetAndResetResponseLengthBytes(), disallowContinuationTokenMessage: null, continuationToken: this.ContinuationToken, diagnostics: this.GetAndResetDiagnostics())); }
/// <summary> /// After a split you need to maintain the continuation tokens for all the child document producers until a condition is met. /// For example lets say that a document producer is at continuation X and it gets split, /// then the children each get continuation X, but since you only drain from one of them at a time you are left with the first child having /// continuation X + delta and the second child having continuation X (draw this out if you are following along). /// At this point you have the answer the question: "Which continuation token do you return to the user?". /// Let's say you return X, then when you come back to the first child you will be repeating work, thus returning some documents more than once. /// Let's say you return X + delta, then you fine when you return to the first child, but when you get to the second child you don't have a continuation token /// meaning that you will be repeating all the document for the second partition up until X and again you will be returning some documents more than once. /// Thus you have to return the continuation token for both children. /// Both this means you are returning more than 1 continuation token for the rest of the query. /// Well a naive optimization is to flush the continuation for a child partition once you are done draining from it, which isn't bad for a parallel query, /// but if you have an order by query you might not be done with a producer until the end of the query. /// The next optimization for a parallel query is to flush the continuation token the moment you start reading from a child partition. /// This works for a parallel query, but breaks for an order by query. /// The final realization is that for an order by query you are only choosing between multiple child partitions when their is a tie, /// so the key is that you can dump the continuation token the moment you come across a new order by item. /// For order by queries that is determined by the order by field and for parallel queries that is the moment you come by a new rid (which is any document, since rids are unique within a partition). /// So by passing an equality comparer to the document producers they can determine whether they are still "active". /// </summary> /// <returns> /// Returns all document producers whose continuation token you have to return. /// Only during a split will this list contain more than 1 item. /// </returns> public IEnumerable <ItemProducer> GetActiveItemProducers() { ItemProducerTree current = this.itemProducerForest.Peek().CurrentItemProducerTree; if (current.HasMoreResults && !current.IsActive) { // If the current document producer tree has more results, but isn't active. // then we still want to emit it, since it won't get picked up in the below for loop. yield return(current.Root); } foreach (ItemProducerTree itemProducerTree in this.itemProducerForest) { foreach (ItemProducer itemProducer in itemProducerTree.GetActiveItemProducers()) { yield return(itemProducer); } } }
/// <summary> /// Compares two document producer trees and returns an integer with the relation of which has the document that comes first in the sort order. /// </summary> /// <param name="producer1">The first document producer tree.</param> /// <param name="producer2">The second document producer tree.</param> /// <returns> /// Less than zero if the document in the first document producer comes first. /// Zero if the documents are equivalent. /// Greater than zero if the document in the second document producer comes first. /// </returns> public int Compare(ItemProducerTree producer1, ItemProducerTree producer2) { if (object.ReferenceEquals(producer1, producer2)) { return(0); } if (producer1.HasMoreResults && !producer2.HasMoreResults) { return(-1); } if (!producer1.HasMoreResults && producer2.HasMoreResults) { return(1); } if (!producer1.HasMoreResults && !producer2.HasMoreResults) { return(string.CompareOrdinal(producer1.PartitionKeyRange.MinInclusive, producer2.PartitionKeyRange.MinInclusive)); } OrderByQueryResult result1 = new OrderByQueryResult(producer1.Current); OrderByQueryResult result2 = new OrderByQueryResult(producer2.Current); // First compare the documents based on the sort order of the query. int cmp = this.CompareOrderByItems(result1.OrderByItems, result2.OrderByItems); if (cmp != 0) { // If there is no tie just return that. return(cmp); } // If there is a tie, then break the tie by picking the one from the left most partition. return(string.CompareOrdinal(producer1.PartitionKeyRange.MinInclusive, producer2.PartitionKeyRange.MinInclusive)); }
public async Task TestItemProducerTreeWithFailure() { int callBackCount = 0; Mock <CosmosQueryContext> mockQueryContext = new Mock <CosmosQueryContext>(); SqlQuerySpec sqlQuerySpec = new SqlQuerySpec("Select * from t"); PartitionKeyRange partitionKeyRange = new PartitionKeyRange { Id = "0", MinInclusive = "A", MaxExclusive = "B" }; Action <ItemProducerTree, int, double, QueryMetrics, long, CancellationToken> produceAsyncCompleteCallback = ( ItemProducerTree producer, int itemsBuffered, double resourceUnitUsage, QueryMetrics queryMetrics, long responseLengthBytes, CancellationToken token) => { callBackCount++; }; Mock <IComparer <ItemProducerTree> > comparer = new Mock <IComparer <ItemProducerTree> >(); Mock <IEqualityComparer <CosmosElement> > cosmosElementComparer = new Mock <IEqualityComparer <CosmosElement> >(); CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); IEnumerable <CosmosElement> cosmosElements = new List <CosmosElement>() { new Mock <CosmosElement>(CosmosElementType.Object).Object }; CosmosQueryResponseMessageHeaders headers = new CosmosQueryResponseMessageHeaders("TestToken", null) { ActivityId = "AA470D71-6DEF-4D61-9A08-272D8C9ABCFE", RequestCharge = 42 }; mockQueryContext.Setup(x => x.ExecuteQueryAsync(sqlQuerySpec, cancellationTokenSource.Token, It.IsAny <Action <CosmosRequestMessage> >())).Returns( Task.FromResult(QueryResponse.CreateSuccess(cosmosElements, 1, 500, headers))); ItemProducerTree itemProducerTree = new ItemProducerTree( queryContext: mockQueryContext.Object, querySpecForInit: sqlQuerySpec, partitionKeyRange: partitionKeyRange, produceAsyncCompleteCallback: produceAsyncCompleteCallback, itemProducerTreeComparer: comparer.Object, equalityComparer: cosmosElementComparer.Object, deferFirstPage: false, collectionRid: "collectionRid", initialContinuationToken: null, initialPageSize: 50); // Buffer to success responses await itemProducerTree.BufferMoreDocumentsAsync(cancellationTokenSource.Token); await itemProducerTree.BufferMoreDocumentsAsync(cancellationTokenSource.Token); // Buffer a failure mockQueryContext.Setup(x => x.ExecuteQueryAsync(sqlQuerySpec, cancellationTokenSource.Token, It.IsAny <Action <CosmosRequestMessage> >())).Returns( Task.FromResult(QueryResponse.CreateFailure(headers, HttpStatusCode.InternalServerError, null, "Error message", null))); await itemProducerTree.BufferMoreDocumentsAsync(cancellationTokenSource.Token); // First item should be a success var result = await itemProducerTree.MoveNextAsync(cancellationTokenSource.Token); Assert.IsTrue(result.successfullyMovedNext); Assert.IsNull(result.failureResponse); Assert.IsTrue(itemProducerTree.HasMoreResults); // Second item should be a success result = await itemProducerTree.MoveNextAsync(cancellationTokenSource.Token); Assert.IsTrue(result.successfullyMovedNext); Assert.IsNull(result.failureResponse); Assert.IsTrue(itemProducerTree.HasMoreResults); // Third item should be a failure result = await itemProducerTree.MoveNextAsync(cancellationTokenSource.Token); Assert.IsFalse(result.successfullyMovedNext); Assert.IsNotNull(result.failureResponse); Assert.IsFalse(itemProducerTree.HasMoreResults); // Try to buffer after failure. It should return the previous cached failure and not try to buffer again. mockQueryContext.Setup(x => x.ExecuteQueryAsync(sqlQuerySpec, cancellationTokenSource.Token, It.IsAny <Action <CosmosRequestMessage> >())). Throws(new Exception("Previous buffer failed. Operation should return original failure and not try again")); await itemProducerTree.BufferMoreDocumentsAsync(cancellationTokenSource.Token); Assert.IsFalse(result.successfullyMovedNext); Assert.IsNotNull(result.failureResponse); Assert.IsFalse(itemProducerTree.HasMoreResults); }
public async Task TestItemProducerTreeWithFailure() { int callBackCount = 0; Mock <CosmosQueryContext> mockQueryContext = new Mock <CosmosQueryContext>(); SqlQuerySpec sqlQuerySpec = new SqlQuerySpec("Select * from t"); PartitionKeyRange partitionKeyRange = new PartitionKeyRange { Id = "0", MinInclusive = "A", MaxExclusive = "B" }; ItemProducerTree.ProduceAsyncCompleteDelegate produceAsyncCompleteCallback = ( ItemProducerTree producer, int itemsBuffered, double resourceUnitUsage, QueryMetrics queryMetrics, long responseLengthBytes, CancellationToken token) => { callBackCount++; }; Mock <IComparer <ItemProducerTree> > comparer = new Mock <IComparer <ItemProducerTree> >(); Mock <IEqualityComparer <CosmosElement> > cosmosElementComparer = new Mock <IEqualityComparer <CosmosElement> >(); CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); IReadOnlyList <CosmosElement> cosmosElements = new List <CosmosElement>() { new Mock <CosmosElement>(CosmosElementType.Object).Object }; mockQueryContext.Setup(x => x.ContainerResourceId).Returns("MockCollectionRid"); mockQueryContext.Setup(x => x.ExecuteQueryAsync( sqlQuerySpec, It.IsAny <string>(), It.IsAny <PartitionKeyRangeIdentity>(), It.IsAny <bool>(), It.IsAny <int>(), cancellationTokenSource.Token)).Returns( Task.FromResult(QueryResponseCore.CreateSuccess( result: cosmosElements, requestCharge: 42, activityId: "AA470D71-6DEF-4D61-9A08-272D8C9ABCFE", queryMetrics: null, queryMetricsText: null, requestStatistics: null, responseLengthBytes: 500, disallowContinuationTokenMessage: null, continuationToken: "TestToken"))); ItemProducerTree itemProducerTree = new ItemProducerTree( queryContext: mockQueryContext.Object, querySpecForInit: sqlQuerySpec, partitionKeyRange: partitionKeyRange, produceAsyncCompleteCallback: produceAsyncCompleteCallback, itemProducerTreeComparer: comparer.Object, equalityComparer: cosmosElementComparer.Object, deferFirstPage: false, collectionRid: "collectionRid", initialContinuationToken: null, initialPageSize: 50); // Buffer to success responses await itemProducerTree.BufferMoreDocumentsAsync(cancellationTokenSource.Token); await itemProducerTree.BufferMoreDocumentsAsync(cancellationTokenSource.Token); // Buffer a failure mockQueryContext.Setup(x => x.ExecuteQueryAsync( sqlQuerySpec, It.IsAny <string>(), It.IsAny <PartitionKeyRangeIdentity>(), It.IsAny <bool>(), It.IsAny <int>(), cancellationTokenSource.Token)).Returns( Task.FromResult(QueryResponseCore.CreateFailure( statusCode: HttpStatusCode.InternalServerError, subStatusCodes: null, errorMessage: "Error message", requestCharge: 10.2, activityId: Guid.NewGuid().ToString(), queryMetricsText: null, queryMetrics: null))); await itemProducerTree.BufferMoreDocumentsAsync(cancellationTokenSource.Token); // First item should be a success (bool successfullyMovedNext, QueryResponseCore? failureResponse)result = await itemProducerTree.MoveNextAsync(cancellationTokenSource.Token); Assert.IsTrue(result.successfullyMovedNext); Assert.IsNull(result.failureResponse); Assert.IsTrue(itemProducerTree.HasMoreResults); // Second item should be a success result = await itemProducerTree.MoveNextAsync(cancellationTokenSource.Token); Assert.IsTrue(result.successfullyMovedNext); Assert.IsNull(result.failureResponse); Assert.IsTrue(itemProducerTree.HasMoreResults); // Third item should be a failure result = await itemProducerTree.MoveNextAsync(cancellationTokenSource.Token); Assert.IsFalse(result.successfullyMovedNext); Assert.IsNotNull(result.failureResponse); Assert.IsFalse(itemProducerTree.HasMoreResults); // Try to buffer after failure. It should return the previous cached failure and not try to buffer again. mockQueryContext.Setup(x => x.ExecuteQueryAsync( sqlQuerySpec, It.IsAny <string>(), It.IsAny <PartitionKeyRangeIdentity>(), It.IsAny <bool>(), It.IsAny <int>(), cancellationTokenSource.Token)). Throws(new Exception("Previous buffer failed. Operation should return original failure and not try again")); await itemProducerTree.BufferMoreDocumentsAsync(cancellationTokenSource.Token); Assert.IsFalse(result.successfullyMovedNext); Assert.IsNotNull(result.failureResponse); Assert.IsFalse(itemProducerTree.HasMoreResults); }
public async Task TestItemProducerTreeWithFailure() { int callBackCount = 0; Mock <CosmosQueryContext> mockQueryContext = new Mock <CosmosQueryContext>(); SqlQuerySpec sqlQuerySpec = new SqlQuerySpec("Select * from t"); PartitionKeyRange partitionKeyRange = new PartitionKeyRange { Id = "0", MinInclusive = "A", MaxExclusive = "B" }; void produceAsyncCompleteCallback( ItemProducerTree producer, int itemsBuffered, double resourceUnitUsage, IReadOnlyCollection <QueryPageDiagnostics> queryPageDiagnostics, long responseLengthBytes, CancellationToken token) { callBackCount++; } Mock <IComparer <ItemProducerTree> > comparer = new Mock <IComparer <ItemProducerTree> >(); Mock <IEqualityComparer <CosmosElement> > cosmosElementComparer = new Mock <IEqualityComparer <CosmosElement> >(); CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); IReadOnlyList <CosmosElement> cosmosElements = new List <CosmosElement>() { new Mock <CosmosElement>(CosmosElementType.Object).Object }; QueryPageDiagnostics diagnostics = new QueryPageDiagnostics( partitionKeyRangeId: "0", queryMetricText: "SomeRandomQueryMetricText", indexUtilizationText: null, requestDiagnostics: new PointOperationStatistics( Guid.NewGuid().ToString(), System.Net.HttpStatusCode.OK, subStatusCode: SubStatusCodes.Unknown, requestCharge: 42, errorMessage: null, method: HttpMethod.Post, requestUri: new Uri("http://localhost.com"), requestSessionToken: null, responseSessionToken: null, clientSideRequestStatistics: null), schedulingStopwatch: new SchedulingStopwatch()); IReadOnlyCollection <QueryPageDiagnostics> pageDiagnostics = new List <QueryPageDiagnostics>() { diagnostics }; mockQueryContext.Setup(x => x.ContainerResourceId).Returns("MockCollectionRid"); mockQueryContext.Setup(x => x.ExecuteQueryAsync( sqlQuerySpec, It.IsAny <string>(), It.IsAny <PartitionKeyRangeIdentity>(), It.IsAny <bool>(), It.IsAny <int>(), It.IsAny <SchedulingStopwatch>(), cancellationTokenSource.Token)).Returns( Task.FromResult(QueryResponseCore.CreateSuccess( result: cosmosElements, requestCharge: 42, activityId: "AA470D71-6DEF-4D61-9A08-272D8C9ABCFE", diagnostics: pageDiagnostics, responseLengthBytes: 500, disallowContinuationTokenMessage: null, continuationToken: "TestToken"))); ItemProducerTree itemProducerTree = new ItemProducerTree( queryContext: mockQueryContext.Object, querySpecForInit: sqlQuerySpec, partitionKeyRange: partitionKeyRange, produceAsyncCompleteCallback: produceAsyncCompleteCallback, itemProducerTreeComparer: comparer.Object, equalityComparer: cosmosElementComparer.Object, testSettings: new TestInjections(simulate429s: false, simulateEmptyPages: false), deferFirstPage: false, collectionRid: "collectionRid", initialContinuationToken: null, initialPageSize: 50); // Buffer to success responses await itemProducerTree.BufferMoreDocumentsAsync(cancellationTokenSource.Token); await itemProducerTree.BufferMoreDocumentsAsync(cancellationTokenSource.Token); diagnostics = new QueryPageDiagnostics( partitionKeyRangeId: "0", queryMetricText: null, indexUtilizationText: null, requestDiagnostics: new PointOperationStatistics( Guid.NewGuid().ToString(), System.Net.HttpStatusCode.InternalServerError, subStatusCode: SubStatusCodes.Unknown, requestCharge: 10.2, errorMessage: "Error message", method: HttpMethod.Post, requestUri: new Uri("http://localhost.com"), requestSessionToken: null, responseSessionToken: null, clientSideRequestStatistics: null), schedulingStopwatch: new SchedulingStopwatch()); pageDiagnostics = new List <QueryPageDiagnostics>() { diagnostics }; // Buffer a failure mockQueryContext.Setup(x => x.ExecuteQueryAsync( sqlQuerySpec, It.IsAny <string>(), It.IsAny <PartitionKeyRangeIdentity>(), It.IsAny <bool>(), It.IsAny <int>(), It.IsAny <SchedulingStopwatch>(), cancellationTokenSource.Token)).Returns( Task.FromResult(QueryResponseCore.CreateFailure( statusCode: HttpStatusCode.InternalServerError, subStatusCodes: null, errorMessage: "Error message", requestCharge: 10.2, activityId: Guid.NewGuid().ToString(), diagnostics: pageDiagnostics))); await itemProducerTree.BufferMoreDocumentsAsync(cancellationTokenSource.Token); // First item should be a success { (bool movedToNextPage, QueryResponseCore? failureResponse) = await itemProducerTree.TryMoveNextPageAsync(cancellationTokenSource.Token); Assert.IsTrue(movedToNextPage); Assert.IsNull(failureResponse); Assert.IsTrue(itemProducerTree.TryMoveNextDocumentWithinPage()); Assert.IsFalse(itemProducerTree.TryMoveNextDocumentWithinPage()); Assert.IsTrue(itemProducerTree.HasMoreResults); } // Second item should be a success { (bool movedToNextPage, QueryResponseCore? failureResponse) = await itemProducerTree.TryMoveNextPageAsync(cancellationTokenSource.Token); Assert.IsTrue(movedToNextPage); Assert.IsNull(failureResponse); Assert.IsTrue(itemProducerTree.TryMoveNextDocumentWithinPage()); Assert.IsFalse(itemProducerTree.TryMoveNextDocumentWithinPage()); Assert.IsTrue(itemProducerTree.HasMoreResults); } // Third item should be a failure { (bool movedToNextPage, QueryResponseCore? failureResponse) = await itemProducerTree.TryMoveNextPageAsync(cancellationTokenSource.Token); Assert.IsFalse(movedToNextPage); Assert.IsNotNull(failureResponse); Assert.IsFalse(itemProducerTree.HasMoreResults); } // Try to buffer after failure. It should return the previous cached failure and not try to buffer again. mockQueryContext.Setup(x => x.ExecuteQueryAsync( sqlQuerySpec, It.IsAny <string>(), It.IsAny <PartitionKeyRangeIdentity>(), It.IsAny <bool>(), It.IsAny <int>(), It.IsAny <SchedulingStopwatch>(), cancellationTokenSource.Token)). Throws(new Exception("Previous buffer failed. Operation should return original failure and not try again")); await itemProducerTree.BufferMoreDocumentsAsync(cancellationTokenSource.Token); Assert.IsFalse(itemProducerTree.HasMoreResults); }
public static (ItemProducerTree itemProducerTree, ReadOnlyCollection <ToDoItem> allItems) CreateTree( Mock <CosmosQueryContext> mockQueryContext = null, int[] responseMessagesPageSize = null, SqlQuerySpec sqlQuerySpec = null, PartitionKeyRange partitionKeyRange = null, string continuationToken = null, int maxPageSize = 50, bool deferFirstPage = true, string collectionRid = null, IComparer <ItemProducerTree> itemProducerTreeComparer = null, ItemProducerTree.ProduceAsyncCompleteDelegate completeDelegate = null, Action executeCallback = null, CancellationToken cancellationToken = default(CancellationToken)) { if (responseMessagesPageSize == null) { responseMessagesPageSize = DefaultResponseSizes; } if (sqlQuerySpec == null) { sqlQuerySpec = DefaultQuerySpec; } if (partitionKeyRange == null) { partitionKeyRange = DefaultPartitionKeyRange; } if (completeDelegate == null) { completeDelegate = DefaultTreeProduceAsyncCompleteDelegate; } if (itemProducerTreeComparer == null) { itemProducerTreeComparer = new ParallelItemProducerTreeComparer(); } if (mockQueryContext == null) { mockQueryContext = new Mock <CosmosQueryContext>(); } mockQueryContext.Setup(x => x.ContainerResourceId).Returns(collectionRid); // Setup a list of query responses. It generates a new continuation token for each response. This allows the mock to return the messages in the correct order. List <ToDoItem> allItems = MockSinglePartitionKeyRangeContext( mockQueryContext, responseMessagesPageSize, sqlQuerySpec, partitionKeyRange, continuationToken, maxPageSize, collectionRid, executeCallback, cancellationToken); ItemProducerTree itemProducerTree = new ItemProducerTree( mockQueryContext.Object, sqlQuerySpec, partitionKeyRange, completeDelegate, itemProducerTreeComparer, CosmosElementEqualityComparer.Value, new TestInjections(simulate429s: false, simulateEmptyPages: false), deferFirstPage, collectionRid, maxPageSize, initialContinuationToken: continuationToken); return(itemProducerTree, allItems.AsReadOnly()); }
/// <summary> /// Drains a page of documents from this context. /// </summary> /// <param name="maxElements">The maximum number of elements.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task that when awaited on return a page of documents.</returns> public override async Task <QueryResponseCore> DrainAsync(int maxElements, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); //// In order to maintain the continuation token for the user we must drain with a few constraints //// 1) We always drain from the partition, which has the highest priority item first //// 2) If multiple partitions have the same priority item then we drain from the left most first //// otherwise we would need to keep track of how many of each item we drained from each partition //// (just like parallel queries). //// Visually that look the following case where we have three partitions that are numbered and store letters. //// For teaching purposes I have made each item a tuple of the following form: //// <item stored in partition, partition number> //// So that duplicates across partitions are distinct, but duplicates within partitions are indistinguishable. //// |-------| |-------| |-------| //// | <a,1> | | <a,2> | | <a,3> | //// | <a,1> | | <b,2> | | <c,3> | //// | <a,1> | | <b,2> | | <c,3> | //// | <d,1> | | <c,2> | | <c,3> | //// | <d,1> | | <e,2> | | <f,3> | //// | <e,1> | | <h,2> | | <j,3> | //// | <f,1> | | <i,2> | | <k,3> | //// |-------| |-------| |-------| //// Now the correct drain order in this case is: //// <a,1>,<a,1>,<a,1>,<a,2>,<a,3>,<b,2>,<b,2>,<c,2>,<c,3>,<c,3>,<c,3>, //// <d,1>,<d,1>,<e,1>,<e,2>,<f,1>,<f,3>,<h,2>,<i,2>,<j,3>,<k,3> //// In more mathematical terms //// 1) <x, y> always comes before <z, y> where x < z //// 2) <i, j> always come before <i, k> where j < k List <CosmosElement> results = new List <CosmosElement>(); while (results.Count < maxElements) { // Only drain from the highest priority document producer // We need to pop and push back the document producer tree, since the priority changes according to the sort order. ItemProducerTree currentItemProducerTree = this.PopCurrentItemProducerTree(); try { if (!currentItemProducerTree.HasMoreResults) { // This means there are no more items to drain break; } OrderByQueryResult orderByQueryResult = new OrderByQueryResult(currentItemProducerTree.Current); // Only add the payload, since other stuff is garbage from the caller's perspective. results.Add(orderByQueryResult.Payload); // If we are at the beginning of the page and seeing an rid from the previous page we should increment the skip count // due to the fact that JOINs can make a document appear multiple times and across continuations, so we don't want to // surface this more than needed. More information can be found in the continuation token docs. if (this.ShouldIncrementSkipCount(currentItemProducerTree.CurrentItemProducerTree.Root)) { ++this.skipCount; } else { this.skipCount = 0; } this.previousRid = orderByQueryResult.Rid; this.previousOrderByItems = orderByQueryResult.OrderByItems; if (!currentItemProducerTree.TryMoveNextDocumentWithinPage()) { while (true) { (bool movedToNextPage, QueryResponseCore? failureResponse) = await currentItemProducerTree.TryMoveNextPageAsync(cancellationToken); if (!movedToNextPage) { if (failureResponse.HasValue) { // TODO: We can buffer this failure so that the user can still get the pages we already got. return(failureResponse.Value); } break; } if (currentItemProducerTree.IsAtBeginningOfPage) { break; } if (currentItemProducerTree.TryMoveNextDocumentWithinPage()) { break; } } } } finally { this.PushCurrentItemProducerTree(currentItemProducerTree); } } return(QueryResponseCore.CreateSuccess( result: results, requestCharge: this.requestChargeTracker.GetAndResetCharge(), activityId: null, responseLengthBytes: this.GetAndResetResponseLengthBytes(), disallowContinuationTokenMessage: null, continuationToken: this.ContinuationToken, diagnostics: this.GetAndResetDiagnostics())); }
protected async Task <TryCatch> TryInitializeAsync( string collectionRid, int initialPageSize, SqlQuerySpec querySpecForInit, IReadOnlyDictionary <PartitionKeyRange, string> targetRangeToContinuationMap, bool deferFirstPage, string filter, Func <ItemProducerTree, Task <TryCatch> > tryFilterAsync, CancellationToken cancellationToken) { if (collectionRid == null) { throw new ArgumentNullException(nameof(collectionRid)); } if (initialPageSize < 0) { throw new ArgumentOutOfRangeException(nameof(initialPageSize)); } if (querySpecForInit == null) { throw new ArgumentNullException(nameof(querySpecForInit)); } if (targetRangeToContinuationMap == null) { throw new ArgumentNullException(nameof(targetRangeToContinuationMap)); } cancellationToken.ThrowIfCancellationRequested(); List <ItemProducerTree> itemProducerTrees = new List <ItemProducerTree>(); foreach (KeyValuePair <PartitionKeyRange, string> rangeAndContinuationToken in targetRangeToContinuationMap) { PartitionKeyRange partitionKeyRange = rangeAndContinuationToken.Key; string continuationToken = rangeAndContinuationToken.Value; ItemProducerTree itemProducerTree = new ItemProducerTree( this.queryContext, querySpecForInit, partitionKeyRange, this.OnItemProducerTreeCompleteFetching, this.itemProducerForest.Comparer, this.equalityComparer, this.testSettings, deferFirstPage, collectionRid, initialPageSize, continuationToken) { Filter = filter }; // Prefetch if necessary, and populate consume queue. if (this.CanPrefetch) { this.TryScheduleFetch(itemProducerTree); } itemProducerTrees.Add(itemProducerTree); } // Using loop fission so that we can load the document producers in parallel foreach (ItemProducerTree itemProducerTree in itemProducerTrees) { if (!deferFirstPage) { while (true) { (bool movedToNextPage, QueryResponseCore? failureResponse) = await itemProducerTree.TryMoveNextPageAsync(cancellationToken); if (failureResponse.HasValue) { return(TryCatch.FromException( failureResponse.Value.CosmosException)); } if (!movedToNextPage) { break; } if (itemProducerTree.IsAtBeginningOfPage) { break; } if (itemProducerTree.TryMoveNextDocumentWithinPage()) { break; } } } if (tryFilterAsync != null) { TryCatch tryFilter = await tryFilterAsync(itemProducerTree); if (!tryFilter.Succeeded) { return(tryFilter); } } this.itemProducerForest.Enqueue(itemProducerTree); } return(TryCatch.FromResult()); }
/// <summary> /// Initializes cross partition query execution context by initializing the necessary document producers. /// </summary> /// <param name="collectionRid">The collection to drain from.</param> /// <param name="partitionKeyRanges">The partitions to target.</param> /// <param name="initialPageSize">The page size to start the document producers off with.</param> /// <param name="querySpecForInit">The query specification for the rewritten query.</param> /// <param name="targetRangeToContinuationMap">Map from partition to it's corresponding continuation token.</param> /// <param name="deferFirstPage">Whether or not we should defer the fetch of the first page from each partition.</param> /// <param name="filter">The filter to inject in the predicate.</param> /// <param name="tryFilterAsync">The callback used to filter each partition.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task to await on.</returns> protected async Task <TryCatch> TryInitializeAsync( string collectionRid, IReadOnlyList <PartitionKeyRange> partitionKeyRanges, int initialPageSize, SqlQuerySpec querySpecForInit, IReadOnlyDictionary <string, string> targetRangeToContinuationMap, bool deferFirstPage, string filter, Func <ItemProducerTree, Task <TryCatch> > tryFilterAsync, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); List <ItemProducerTree> itemProducerTrees = new List <ItemProducerTree>(); foreach (PartitionKeyRange partitionKeyRange in partitionKeyRanges) { string initialContinuationToken; if (targetRangeToContinuationMap != null) { if (!targetRangeToContinuationMap.TryGetValue(partitionKeyRange.Id, out initialContinuationToken)) { initialContinuationToken = null; } } else { initialContinuationToken = null; } ItemProducerTree itemProducerTree = new ItemProducerTree( this.queryContext, querySpecForInit, partitionKeyRange, this.OnItemProducerTreeCompleteFetching, this.itemProducerForest.Comparer as IComparer <ItemProducerTree>, this.equalityComparer, this.testSettings, deferFirstPage, collectionRid, initialPageSize, initialContinuationToken) { Filter = filter }; // Prefetch if necessary, and populate consume queue. if (this.CanPrefetch) { this.TryScheduleFetch(itemProducerTree); } itemProducerTrees.Add(itemProducerTree); } // Using loop fission so that we can load the document producers in parallel foreach (ItemProducerTree itemProducerTree in itemProducerTrees) { if (!deferFirstPage) { while (true) { (bool movedToNextPage, QueryResponseCore? failureResponse) = await itemProducerTree.TryMoveNextPageAsync(cancellationToken); if (failureResponse.HasValue) { return(TryCatch.FromException( new CosmosException( statusCode: failureResponse.Value.StatusCode, subStatusCode: (int)failureResponse.Value.SubStatusCode.GetValueOrDefault(0), message: failureResponse.Value.ErrorMessage, activityId: failureResponse.Value.ActivityId, requestCharge: failureResponse.Value.RequestCharge))); } if (!movedToNextPage) { break; } if (itemProducerTree.IsAtBeginningOfPage) { break; } if (itemProducerTree.TryMoveNextDocumentWithinPage()) { break; } } } if (tryFilterAsync != null) { TryCatch tryFilter = await tryFilterAsync(itemProducerTree); if (!tryFilter.Succeeded) { return(tryFilter); } } this.itemProducerForest.Enqueue(itemProducerTree); } return(TryCatch.FromResult()); }
/// <summary> /// Pushes a document producer back to the queue. /// </summary> public void PushCurrentItemProducerTree(ItemProducerTree itemProducerTree) { itemProducerTree.UpdatePriority(); this.itemProducerForest.Enqueue(itemProducerTree); }