|
2 | 2 | // Licensed under the MIT License. |
3 | 3 | package com.azure.cosmos.rx; |
4 | 4 |
|
| 5 | +import com.azure.cosmos.BridgeInternal; |
5 | 6 | import com.azure.cosmos.CosmosAsyncClient; |
6 | 7 | import com.azure.cosmos.CosmosAsyncContainer; |
7 | 8 | import com.azure.cosmos.CosmosAsyncDatabase; |
|
11 | 12 | import com.azure.cosmos.implementation.AsyncDocumentClient; |
12 | 13 | import com.azure.cosmos.implementation.Configs; |
13 | 14 | import com.azure.cosmos.implementation.HttpConstants; |
| 15 | +import com.azure.cosmos.implementation.PartitionKeyRange; |
14 | 16 | import com.azure.cosmos.models.CosmosContainerProperties; |
15 | 17 | import com.azure.cosmos.models.CosmosContainerRequestOptions; |
| 18 | +import com.azure.cosmos.models.CosmosContainerResponse; |
16 | 19 | import com.azure.cosmos.models.CosmosDatabaseProperties; |
17 | 20 | import com.azure.cosmos.models.CosmosPermissionProperties; |
18 | 21 | import com.azure.cosmos.models.CosmosQueryRequestOptions; |
|
26 | 29 | import com.azure.cosmos.models.SqlParameter; |
27 | 30 | import com.azure.cosmos.models.SqlQuerySpec; |
28 | 31 | import com.azure.cosmos.models.ThroughputProperties; |
| 32 | +import com.azure.cosmos.models.ThroughputResponse; |
29 | 33 | import com.azure.cosmos.models.TriggerOperation; |
30 | 34 | import com.azure.cosmos.models.TriggerType; |
31 | 35 | import com.azure.cosmos.util.CosmosPagedFlux; |
| 36 | +import com.fasterxml.jackson.databind.JsonNode; |
32 | 37 | import io.reactivex.subscribers.TestSubscriber; |
| 38 | +import org.jetbrains.annotations.NotNull; |
33 | 39 | import org.testng.annotations.BeforeClass; |
34 | 40 | import org.testng.annotations.DataProvider; |
35 | 41 | import org.testng.annotations.Factory; |
36 | 42 | import org.testng.annotations.Test; |
| 43 | +import reactor.core.publisher.Flux; |
37 | 44 |
|
38 | 45 | import java.util.ArrayList; |
| 46 | +import java.util.Arrays; |
39 | 47 | import java.util.Collections; |
40 | 48 | import java.util.Comparator; |
41 | 49 | import java.util.List; |
@@ -317,6 +325,129 @@ public void queryPlanCacheSinglePartitionParameterizedQueriesCorrectness() { |
317 | 325 |
|
318 | 326 | } |
319 | 327 |
|
| 328 | + @Test(groups = {"simple"}, timeOut = TIMEOUT * 10) |
| 329 | + public void splitQueryContinuationToken() throws Exception { |
| 330 | + String containerId = "splittestcontainer_" + UUID.randomUUID(); |
| 331 | + int itemCount = 20; |
| 332 | + |
| 333 | + //Create container |
| 334 | + CosmosContainerProperties containerProperties = new CosmosContainerProperties(containerId, "/mypk"); |
| 335 | + CosmosContainerResponse containerResponse = createdDatabase.createContainer(containerProperties).block(); |
| 336 | + CosmosAsyncContainer container = createdDatabase.getContainer(containerId); |
| 337 | + AsyncDocumentClient asyncDocumentClient = BridgeInternal.getContextClient(this.client); |
| 338 | + |
| 339 | + //Insert some documents |
| 340 | + List<TestObject> testObjects = insertDocuments(itemCount, Arrays.asList("CA", "US"), container); |
| 341 | + |
| 342 | + List<String> sortedObjects = testObjects.stream() |
| 343 | + .sorted(Comparator.comparing(TestObject::getProp)) |
| 344 | + .map(TestObject::getId) |
| 345 | + .collect(Collectors.toList()); |
| 346 | + |
| 347 | + String query = "Select * from c"; |
| 348 | + String orderByQuery = "select * from c order by c.prop"; |
| 349 | + |
| 350 | + List<PartitionKeyRange> partitionKeyRanges = getPartitionKeyRanges(containerId, asyncDocumentClient); |
| 351 | + String requestContinuation = null; |
| 352 | + String orderByRequestContinuation = null; |
| 353 | + int preferredPageSize = 15; |
| 354 | + ArrayList<TestObject> resultList = new ArrayList<>(); |
| 355 | + ArrayList<TestObject> orderByResultList = new ArrayList<>(); |
| 356 | + |
| 357 | + // Query |
| 358 | + FeedResponse<TestObject> jsonNodeFeedResponse = container |
| 359 | + .queryItems(query, new CosmosQueryRequestOptions(), TestObject.class) |
| 360 | + .byPage(preferredPageSize).blockFirst(); |
| 361 | + assert jsonNodeFeedResponse != null; |
| 362 | + resultList.addAll(jsonNodeFeedResponse.getResults()); |
| 363 | + requestContinuation = jsonNodeFeedResponse.getContinuationToken(); |
| 364 | + |
| 365 | + // Orderby query |
| 366 | + FeedResponse<TestObject> orderByFeedResponse = container |
| 367 | + .queryItems(orderByQuery, new CosmosQueryRequestOptions(), |
| 368 | + TestObject.class) |
| 369 | + .byPage(preferredPageSize).blockFirst(); |
| 370 | + assert orderByFeedResponse != null; |
| 371 | + orderByResultList.addAll(orderByFeedResponse.getResults()); |
| 372 | + orderByRequestContinuation = orderByFeedResponse.getContinuationToken(); |
| 373 | + |
| 374 | + // Scale up the throughput for a split |
| 375 | + logger.info("Scaling up throughput for split"); |
| 376 | + ThroughputProperties throughputProperties = ThroughputProperties.createManualThroughput(16000); |
| 377 | + ThroughputResponse throughputResponse = container.replaceThroughput(throughputProperties).block(); |
| 378 | + logger.info("Throughput replace request submitted for {} ", |
| 379 | + throughputResponse.getProperties().getManualThroughput()); |
| 380 | + throughputResponse = container.readThroughput().block(); |
| 381 | + |
| 382 | + |
| 383 | + // Wait for the throughput update to complete so that we get the partition split |
| 384 | + while (true) { |
| 385 | + assert throughputResponse != null; |
| 386 | + if (!throughputResponse.isReplacePending()) { |
| 387 | + break; |
| 388 | + } |
| 389 | + logger.info("Waiting for split to complete"); |
| 390 | + Thread.sleep(10 * 1000); |
| 391 | + throughputResponse = container.readThroughput().block(); |
| 392 | + } |
| 393 | + |
| 394 | + logger.info("Resuming query from the continuation"); |
| 395 | + // Read number of partitions. Should be greater than one |
| 396 | + List<PartitionKeyRange> partitionKeyRangesAfterSplit = getPartitionKeyRanges(containerId, asyncDocumentClient); |
| 397 | + assertThat(partitionKeyRangesAfterSplit.size()).isGreaterThan(partitionKeyRanges.size()) |
| 398 | + .as("Partition ranges should increase after split"); |
| 399 | + logger.info("After split num partitions = {}", partitionKeyRangesAfterSplit.size()); |
| 400 | + |
| 401 | + // Reading item to refresh cache |
| 402 | + container.readItem(testObjects.get(0).getId(), new PartitionKey(testObjects.get(0).getMypk()), |
| 403 | + JsonNode.class).block(); |
| 404 | + |
| 405 | + // Resume the query with continuation token saved above and make sure you get all the documents |
| 406 | + Flux<FeedResponse<TestObject>> feedResponseFlux = container |
| 407 | + .queryItems(query, new CosmosQueryRequestOptions(), |
| 408 | + TestObject.class) |
| 409 | + .byPage(requestContinuation, preferredPageSize); |
| 410 | + |
| 411 | + for (FeedResponse<TestObject> nodeFeedResponse : feedResponseFlux.toIterable()) { |
| 412 | + resultList.addAll(nodeFeedResponse.getResults()); |
| 413 | + } |
| 414 | + |
| 415 | + // Resume the orderby query with continuation token saved above and make sure you get all the documents |
| 416 | + Flux<FeedResponse<TestObject>> orderfeedResponseFlux = container |
| 417 | + .queryItems(orderByQuery, new CosmosQueryRequestOptions(), |
| 418 | + TestObject.class) |
| 419 | + .byPage(orderByRequestContinuation, preferredPageSize); |
| 420 | + |
| 421 | + for (FeedResponse<TestObject> nodeFeedResponse : orderfeedResponseFlux.toIterable()) { |
| 422 | + orderByResultList.addAll(nodeFeedResponse.getResults()); |
| 423 | + } |
| 424 | + |
| 425 | + List<String> sourceIds = testObjects.stream().map(obj -> obj.getId()).collect(Collectors.toList()); |
| 426 | + List<String> resultIds = resultList.stream().map(obj -> obj.getId()).collect(Collectors.toList()); |
| 427 | + List<String> orderResultIds = orderByResultList.stream().map(obj -> obj.getId()).collect(Collectors.toList()); |
| 428 | + |
| 429 | + assertThat(resultIds).containsExactlyInAnyOrderElementsOf(sourceIds) |
| 430 | + .as("Resuming query from continuation token after split validated"); |
| 431 | + |
| 432 | + assertThat(orderResultIds).containsExactlyElementsOf(sortedObjects) |
| 433 | + .as("Resuming orderby query from continuation token after split validated"); |
| 434 | + |
| 435 | + container.delete().block(); |
| 436 | + } |
| 437 | + |
| 438 | + @NotNull |
| 439 | + private List<PartitionKeyRange> getPartitionKeyRanges( |
| 440 | + String containerId, AsyncDocumentClient asyncDocumentClient) { |
| 441 | + List<PartitionKeyRange> partitionKeyRanges = new ArrayList<>(); |
| 442 | + List<FeedResponse<PartitionKeyRange>> partitionFeedResponseList = asyncDocumentClient |
| 443 | + .readPartitionKeyRanges("/dbs/" + createdDatabase.getId() |
| 444 | + + "/colls/" + containerId, |
| 445 | + new CosmosQueryRequestOptions()) |
| 446 | + .collectList().block(); |
| 447 | + partitionFeedResponseList.forEach(f -> partitionKeyRanges.addAll(f.getResults())); |
| 448 | + return partitionKeyRanges; |
| 449 | + } |
| 450 | + |
320 | 451 | private <T> List<T> queryAndGetResults(SqlQuerySpec querySpec, CosmosQueryRequestOptions options, Class<T> type) { |
321 | 452 | CosmosPagedFlux<T> queryPagedFlux = createdContainer.queryItems(querySpec, options, type); |
322 | 453 | TestSubscriber<T> testSubscriber = new TestSubscriber<>(); |
|
0 commit comments