/// <summary> /// When resuming an order by query we need to filter the document producers. /// </summary> /// <param name="producer">The producer to filter down.</param> /// <param name="sortOrders">The sort orders.</param> /// <param name="continuationToken">The continuation token.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task to await on.</returns> private async Task FilterAsync( ItemProducerTree producer, SortOrder[] sortOrders, OrderByContinuationToken continuationToken, CancellationToken cancellationToken) { // When we resume a query on a partition there is a possibility that we only read a partial page from the backend // meaning that will we repeat some documents if we didn't do anything about it. // The solution is to filter all the documents that come before in the sort order, since we have already emitted them to the client. // The key is to seek until we get an order by value that matches the order by value we left off on. // Once we do that we need to seek to the correct _rid within the term, // since there might be many documents with the same order by value we left off on. foreach (ItemProducerTree tree in producer) { if (!ResourceId.TryParse(continuationToken.Rid, out ResourceId continuationRid)) { throw this.queryClient.CreateBadRequestException( $"Invalid Rid in the continuation token {continuationToken.CompositeContinuationToken.Token} for OrderBy~Context."); } Dictionary <string, ResourceId> resourceIds = new Dictionary <string, ResourceId>(); int itemToSkip = continuationToken.SkipCount; bool continuationRidVerified = false; while (true) { OrderByQueryResult orderByResult = new OrderByQueryResult(tree.Current); // Throw away documents until it matches the item from the continuation token. int cmp = 0; for (int i = 0; i < sortOrders.Length; ++i) { cmp = ItemComparer.Instance.Compare( continuationToken.OrderByItems[i].Item, orderByResult.OrderByItems[i].Item); if (cmp != 0) { cmp = sortOrders[i] != SortOrder.Descending ? cmp : -cmp; break; } } if (cmp < 0) { // We might have passed the item due to deletions and filters. break; } if (cmp == 0) { if (!resourceIds.TryGetValue(orderByResult.Rid, out ResourceId rid)) { if (!ResourceId.TryParse(orderByResult.Rid, out rid)) { throw this.queryClient.CreateBadRequestException( message: $"Invalid Rid in the continuation token {continuationToken.CompositeContinuationToken.Token} for OrderBy~Context~TryParse."); } resourceIds.Add(orderByResult.Rid, rid); } if (!continuationRidVerified) { if (continuationRid.Database != rid.Database || continuationRid.DocumentCollection != rid.DocumentCollection) { throw this.queryClient.CreateBadRequestException( message: $"Invalid Rid in the continuation token {continuationToken.CompositeContinuationToken.Token} for OrderBy~Context."); } continuationRidVerified = true; } // Once the item matches the order by items from the continuation tokens // We still need to remove all the documents that have a lower rid in the rid sort order. // If there is a tie in the sort order the documents should be in _rid order in the same direction as the first order by field. // So if it's ORDER BY c.age ASC, c.name DESC the _rids are ASC // If ti's ORDER BY c.age DESC, c.name DESC the _rids are DESC cmp = continuationRid.Document.CompareTo(rid.Document); if (sortOrders[0] == SortOrder.Descending) { cmp = -cmp; } // We might have passed the item due to deletions and filters. // We also have a skip count for JOINs if (cmp < 0 || (cmp == 0 && itemToSkip-- <= 0)) { break; } } (bool successfullyMovedNext, QueryResponseCore? failureResponse)moveNextResponse = await tree.MoveNextAsync(cancellationToken); if (!moveNextResponse.successfullyMovedNext) { if (moveNextResponse.failureResponse != null) { this.FailureResponse = moveNextResponse.failureResponse; } break; } } } }
public async Task TestGoneFromServiceScenarioAsync() { Mock <IHttpHandler> mockHttpHandler = new Mock <IHttpHandler>(MockBehavior.Strict); Uri endpoint = MockSetupsHelper.SetupSingleRegionAccount( "mockAccountInfo", consistencyLevel: ConsistencyLevel.Session, mockHttpHandler, out string primaryRegionEndpoint); string databaseName = "mockDbName"; string containerName = "mockContainerName"; string containerRid = "ccZ1ANCszwk="; Documents.ResourceId cRid = Documents.ResourceId.Parse(containerRid); MockSetupsHelper.SetupContainerProperties( mockHttpHandler: mockHttpHandler, regionEndpoint: primaryRegionEndpoint, databaseName: databaseName, containerName: containerName, containerRid: containerRid); MockSetupsHelper.SetupSinglePartitionKeyRange( mockHttpHandler, primaryRegionEndpoint, cRid, out IReadOnlyList <string> partitionKeyRanges); List <string> replicaIds1 = new List <string>() { "11111111111111111", "22222222222222222", "33333333333333333", "44444444444444444", }; HttpResponseMessage replicaSet1 = MockSetupsHelper.CreateAddresses( replicaIds1, partitionKeyRanges.First(), "eastus", cRid); // One replica changed on the refresh List <string> replicaIds2 = new List <string>() { "11111111111111111", "22222222222222222", "33333333333333333", "55555555555555555", }; HttpResponseMessage replicaSet2 = MockSetupsHelper.CreateAddresses( replicaIds2, partitionKeyRanges.First(), "eastus", cRid); bool delayCacheRefresh = true; bool delayRefreshUnblocked = false; mockHttpHandler.SetupSequence(x => x.SendAsync( It.Is <HttpRequestMessage>(r => r.RequestUri.ToString().Contains("addresses")), It.IsAny <CancellationToken>())) .Returns(Task.FromResult(replicaSet1)) .Returns(async() => { //block cache refresh to verify bad replica is not visited during refresh while (delayCacheRefresh) { await Task.Delay(TimeSpan.FromMilliseconds(20)); } delayRefreshUnblocked = true; return(replicaSet2); }); int callBack = 0; List <Documents.TransportAddressUri> urisVisited = new List <Documents.TransportAddressUri>(); Mock <Documents.TransportClient> mockTransportClient = new Mock <Documents.TransportClient>(MockBehavior.Strict); mockTransportClient.Setup(x => x.InvokeResourceOperationAsync(It.IsAny <Documents.TransportAddressUri>(), It.IsAny <Documents.DocumentServiceRequest>())) .Callback <Documents.TransportAddressUri, Documents.DocumentServiceRequest>((t, _) => urisVisited.Add(t)) .Returns(() => { callBack++; if (callBack == 1) { throw Documents.Rntbd.TransportExceptions.GetGoneException( new Uri("https://localhost:8081"), Guid.NewGuid(), new Documents.TransportException(Documents.TransportErrorCode.ConnectionBroken, null, Guid.NewGuid(), new Uri("https://localhost:8081"), "Mock", userPayload: true, payloadSent: false)); } return(Task.FromResult(new Documents.StoreResponse() { Status = 200, Headers = new Documents.Collections.StoreResponseNameValueCollection() { ActivityId = Guid.NewGuid().ToString(), LSN = "12345", PartitionKeyRangeId = "0", GlobalCommittedLSN = "12345", SessionToken = "1#12345#1=12345" }, ResponseBody = new MemoryStream() })); }); CosmosClientOptions cosmosClientOptions = new CosmosClientOptions() { ConsistencyLevel = Cosmos.ConsistencyLevel.Session, HttpClientFactory = () => new HttpClient(new HttpHandlerHelper(mockHttpHandler.Object)), TransportClientHandlerFactory = (original) => mockTransportClient.Object, }; using (CosmosClient customClient = new CosmosClient( endpoint.ToString(), Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())), cosmosClientOptions)) { try { Container container = customClient.GetContainer(databaseName, containerName); for (int i = 0; i < 20; i++) { ResponseMessage response = await container.ReadItemStreamAsync(Guid.NewGuid().ToString(), new Cosmos.PartitionKey(Guid.NewGuid().ToString())); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); } mockTransportClient.VerifyAll(); mockHttpHandler.VerifyAll(); Documents.TransportAddressUri failedReplica = urisVisited.First(); Assert.AreEqual(1, urisVisited.Count(x => x.Equals(failedReplica))); urisVisited.Clear(); delayCacheRefresh = false; do { await Task.Delay(TimeSpan.FromMilliseconds(100)); }while (!delayRefreshUnblocked); for (int i = 0; i < 20; i++) { ResponseMessage response = await container.ReadItemStreamAsync(Guid.NewGuid().ToString(), new Cosmos.PartitionKey(Guid.NewGuid().ToString())); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); } Assert.AreEqual(4, urisVisited.ToHashSet().Count()); // Clears all the setups. No network calls should be done on the next operation. mockHttpHandler.Reset(); mockTransportClient.Reset(); } finally { mockTransportClient.Setup(x => x.Dispose()); } } }