Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Commit a76620d

Browse files
committed
Adding a way to execute a cross-partition query with compound session tokens.
1 parent 59d21b9 commit a76620d

4 files changed

Lines changed: 332 additions & 32 deletions

File tree

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ Sample code repo for Azure Cosmos DB Java SDK for SQL API. By cloning and runnin
2222

2323
## Contents
2424

25-
| File/folder | Description |
26-
|-------------------|--------------------------------------------|
27-
| `src` | Java sample source code. Many samples have 'sync' and 'async' variants |
28-
| `.gitignore` | Define what to ignore at commit time. |
29-
| `CHANGELOG.md` | List of changes to the sample. |
30-
| `CONTRIBUTING.md` | Guidelines for contributing to the sample. |
31-
| `README.md` | This README file. |
32-
| `LICENSE` | The license for the sample. |
33-
| `pom.xml` | Maven Project Object Model File
25+
| File/folder | Description |
26+
|-------------------|------------------------------------------------------------------------|
27+
| `src` | Java sample source code. Many samples have 'sync' and 'async' variants |
28+
| `.gitignore` | Define what to ignore at commit time. |
29+
| `CHANGELOG.md` | List of changes to the sample. |
30+
| `CONTRIBUTING.md` | Guidelines for contributing to the sample. |
31+
| `README.md` | This README file. |
32+
| `LICENSE` | The license for the sample. |
33+
| `pom.xml` | Maven Project Object Model File |
3434

3535
## Prerequisites
3636

src/main/java/com/azure/cosmos/examples/documentationsnippets/async/SampleDocumentationSnippetsAsync.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,7 @@ public static void MigrateJavaSDKv4CFAsync() {
794794
// ...
795795

796796
changeFeedProcessorInstance.start()
797-
.subscribeOn(Schedulers.elastic())
797+
.subscribeOn(Schedulers.boundedElastic())
798798
.subscribe();
799799

800800
// </MigrateCFAsync>

src/main/java/com/azure/cosmos/examples/queries/async/QueriesQuickstartAsync.java

Lines changed: 192 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import com.azure.cosmos.models.CosmosDatabaseRequestOptions;
1616
import com.azure.cosmos.models.CosmosDatabaseResponse;
1717
import com.azure.cosmos.models.CosmosItemRequestOptions;
18+
import com.azure.cosmos.models.CosmosItemResponse;
1819
import com.azure.cosmos.models.CosmosQueryRequestOptions;
1920
import com.azure.cosmos.models.FeedResponse;
2021
import com.azure.cosmos.models.PartitionKey;
@@ -25,14 +26,21 @@
2526
import com.fasterxml.jackson.databind.JsonNode;
2627

2728
import reactor.core.publisher.Flux;
29+
import reactor.core.publisher.Mono;
2830

2931
import org.slf4j.Logger;
3032
import org.slf4j.LoggerFactory;
3133

3234
import java.util.ArrayList;
3335
import java.util.UUID;
36+
import java.util.concurrent.ConcurrentHashMap;
37+
import java.util.concurrent.ThreadLocalRandom;
3438
import java.util.concurrent.atomic.AtomicBoolean;
3539
import java.util.concurrent.atomic.AtomicInteger;
40+
import java.util.Map;
41+
import java.util.HashMap;
42+
import java.util.List;
43+
import java.util.stream.Collectors;
3644

3745
public class QueriesQuickstartAsync {
3846

@@ -115,7 +123,11 @@ private void queriesDemo() throws Exception {
115123

116124
logger.info("Async doc create done.");
117125

126+
logger.info("Async doc create done.");
127+
118128
//execute all the below query examples asynchronously and waiting until all done
129+
130+
queryCrossPartitionAsyncUsingSessionConsistency();
119131
queryAllDocuments();
120132
queryWithPagingAndContinuationTokenAndPrintQueryCharge(new CosmosQueryRequestOptions());
121133
queryEquality();
@@ -130,7 +142,6 @@ private void queriesDemo() throws Exception {
130142
queryWithQuerySpec();
131143
parallelQueryWithPagingAndContinuationTokenAndPrintQueryCharge();
132144

133-
134145
// We are adding Thread.sleep to mimic the some business computation that can
135146
// happen while waiting for earlier processes to finish.
136147
Thread.sleep(2000);
@@ -208,7 +219,7 @@ private void createContainerIfNotExists() throws Exception {
208219
new CosmosContainerProperties(containerName, "/lastName");
209220

210221
// Provision throughput
211-
ThroughputProperties throughputProperties = ThroughputProperties.createManualThroughput(400);
222+
ThroughputProperties throughputProperties = ThroughputProperties.createManualThroughput(12000);
212223

213224
// Create container with 200 RU/s
214225
CosmosContainerResponse containerResponse = database.createContainerIfNotExists(containerProperties, throughputProperties).block();
@@ -273,9 +284,8 @@ private void queryWithPagingAndContinuationTokenAndPrintQueryCharge(CosmosQueryR
273284
for (FeedResponse<Family> page : feedResponseIterator) {
274285
logger.info(String.format("Current page number: %d", currentPageNumber));
275286
// Access all of the documents in this result page
276-
for (Family docProps : page.getResults()) {
277-
documentNumber++;
278-
}
287+
// Increment documentNumber by the number of results in this page to avoid an unused-variable warning
288+
documentNumber += page.getResults().size();
279289

280290
// Accumulate the request charge of this page
281291
requestCharge += page.getRequestCharge();
@@ -359,6 +369,7 @@ private void queryOrderBy() throws Exception {
359369
executeQueryPrintSingleResult("SELECT * FROM Families f WHERE f.LastName = 'Andersen' ORDER BY f.Children[0].Grade");
360370
}
361371

372+
@SuppressWarnings("unused")
362373
private void queryDistinct() throws Exception {
363374
logger.info("DISTINCT queries");
364375

@@ -454,6 +465,169 @@ private void queryWithQuerySpec() throws Exception {
454465
executeQueryWithQuerySpecPrintSingleResult(querySpec);
455466
}
456467

468+
// Step 1-4: Create multiple documents (some with duplicate partition keys) and collect latest session tokens per partition.
469+
// Uses a dedicated CosmosAsyncClient (writer) and ensures the latest session token per partition key overrides earlier values.
470+
private Map<String, String> upsertMultipleDocumentsCollectSessionTokens(String dbName, String containerName, int totalDocuments) {
471+
// Use a HashMap to store the latest session token per stringified partition key
472+
Map<String, String> partitionToSessionToken = new ConcurrentHashMap<>();
473+
474+
// Use a dedicated client instance for creates/upserts
475+
try (CosmosAsyncClient writerClient = new CosmosClientBuilder()
476+
.endpoint(AccountSettings.HOST)
477+
.key(AccountSettings.MASTER_KEY)
478+
// Use SESSION consistency for session token semantics
479+
.consistencyLevel(ConsistencyLevel.SESSION)
480+
.contentResponseOnWriteEnabled(true)
481+
.buildAsyncClient()) {
482+
483+
CosmosAsyncContainer writerContainer = writerClient.getDatabase(dbName).getContainer(containerName);
484+
485+
// Randomly pick a partition key that will receive 2-3 upserts
486+
int duplicateCount = ThreadLocalRandom.current().nextInt(2, 4); // 2 or 3
487+
int uniqueNeeded = totalDocuments - duplicateCount;
488+
489+
// Create a list of unique partition keys
490+
List<String> partitionKeys = java.util.stream.IntStream.range(0, uniqueNeeded)
491+
.mapToObj(i -> "LastName_pk_" + i)
492+
.collect(Collectors.toList());
493+
494+
// Choose one of existing partition keys or create a new one to be duplicated
495+
String duplicatePartitionKey;
496+
if (partitionKeys.isEmpty()) {
497+
duplicatePartitionKey = "LastName_pk_dup_0";
498+
partitionKeys.add(duplicatePartitionKey);
499+
} else {
500+
duplicatePartitionKey = partitionKeys.get(ThreadLocalRandom.current().nextInt(partitionKeys.size()));
501+
}
502+
503+
// Build the actual list of partition-key values for upserts, inserting the duplicate partition key duplicateCount times
504+
List<String> upsertPartitionKeys = new ArrayList<>(partitionKeys);
505+
for (int i = 0; i < duplicateCount; i++) {
506+
upsertPartitionKeys.add(duplicatePartitionKey);
507+
}
508+
509+
// If by any chance we have fewer than requested documents, append additional unique keys
510+
int nextSuffix = uniqueNeeded;
511+
while (upsertPartitionKeys.size() < totalDocuments) {
512+
upsertPartitionKeys.add("LastName_pk_extra_" + nextSuffix++);
513+
}
514+
515+
// Prepare upsert operations and attach callbacks to capture session tokens
516+
List<Mono<CosmosItemResponse<Family>>> upserts = new java.util.ArrayList<>();
517+
518+
for (String pkValue : upsertPartitionKeys) {
519+
Family f = new Family();
520+
f.setLastName(pkValue);
521+
f.setId(UUID.randomUUID().toString());
522+
523+
// Convert the typed response into Object to avoid compile-time dependency on CosmosItemResponse in generics.
524+
Mono<CosmosItemResponse<Family>> upsertMono = writerContainer
525+
.upsertItem(f, new PartitionKey(pkValue), new CosmosItemRequestOptions())
526+
.map(response -> {
527+
528+
String sessionTokenFromUpsert = response.getSessionToken();
529+
530+
if (sessionTokenFromUpsert == null) {
531+
logger.warn("Upsert response session token is null for partition {}", pkValue);
532+
} else {
533+
partitionToSessionToken.put(pkValue, sessionTokenFromUpsert);
534+
logger.info("Upserted doc id {} on partition {} and updated session token {}", f.getId(), pkValue, sessionTokenFromUpsert);
535+
}
536+
return response;
537+
})
538+
.doOnError(err -> logger.error("Upsert failed for partition {}. Error: {}", pkValue, err.getMessage(), err));
539+
540+
upserts.add(upsertMono);
541+
}
542+
543+
// Execute all upserts asynchronously and wait for completion. The partitionToSessionToken map will be updated
544+
// as each upsert completes (latest session token for a partition overrides previous value).
545+
Flux.mergeSequential(upserts).then().block();
546+
547+
} catch (Exception e) {
548+
logger.error("Exception while upserting documents: {}", e.getMessage(), e);
549+
}
550+
551+
return partitionToSessionToken;
552+
}
553+
554+
// Step 5: Build a compound session token by concatenating the session token values in the provided map using commas.
555+
private String buildCompoundSessionToken(Map<String, String> partitionSessionTokenMap) {
556+
if (partitionSessionTokenMap == null || partitionSessionTokenMap.isEmpty()) {
557+
return "";
558+
}
559+
560+
// The compound token is a comma-separated list of the session token values
561+
return partitionSessionTokenMap.values().stream()
562+
.filter(v -> v != null && !v.isEmpty())
563+
.collect(Collectors.joining(","));
564+
}
565+
566+
private void queryWithCompoundSessionTokenAndIteratePages(String dbName, String containerName, String compoundSessionToken) {
567+
// Use a dedicated client for queries
568+
try (CosmosAsyncClient readerClient = new CosmosClientBuilder()
569+
.endpoint(AccountSettings.HOST)
570+
.key(AccountSettings.MASTER_KEY)
571+
// Use SESSION consistency to honor session tokens
572+
.consistencyLevel(ConsistencyLevel.SESSION)
573+
.contentResponseOnWriteEnabled(false)
574+
.buildAsyncClient()) {
575+
576+
CosmosAsyncContainer readerContainer = readerClient.getDatabase(dbName).getContainer(containerName);
577+
578+
CosmosQueryRequestOptions options = new CosmosQueryRequestOptions();
579+
// Step 5: Plug in the compound session token
580+
options.setSessionToken(compoundSessionToken);
581+
582+
logger.info("Executing query with compound session token: {}", compoundSessionToken);
583+
584+
// Use explicit paging with continuation tokens similar to queryWithPagingAndContinuationTokenAndPrintQueryCharge
585+
String query = "SELECT * FROM c";
586+
int pageSize = 1; // number of docs per page
587+
String continuationToken = null;
588+
int currentPageNumber = 1;
589+
double requestCharge = 0.0;
590+
591+
do {
592+
logger.info("Receiving a set of query response pages.");
593+
logger.info("Continuation Token: {}\n", continuationToken);
594+
595+
Iterable<FeedResponse<Family>> feedResponseIterator =
596+
readerContainer.queryItems(query, options, Family.class).byPage(continuationToken, pageSize).toIterable();
597+
598+
for (FeedResponse<Family> page : feedResponseIterator) {
599+
logger.info(String.format("Current page number: %d", currentPageNumber));
600+
601+
// Log the session token associated with this FeedResponse
602+
String feedSessionToken = page.getSessionToken();
603+
logger.info("FeedResponse.sessionToken={}", feedSessionToken);
604+
605+
// Access all of the documents in this result page
606+
for (Family family : page.getResults()) {
607+
logger.info("Query result: id={}, partitionKey={}", family.getId(), family.getLastName());
608+
}
609+
610+
// Accumulate the request charge of this page
611+
requestCharge += page.getRequestCharge();
612+
613+
// Request charge so far
614+
logger.info(String.format("Total request charge so far: %f\n", requestCharge));
615+
616+
// Along with page results, get a continuation token which enables the client to "pick up where it left off"
617+
continuationToken = page.getContinuationToken();
618+
619+
currentPageNumber++;
620+
}
621+
622+
} while (continuationToken != null);
623+
624+
logger.info(String.format("Total request charge: %f\n", requestCharge));
625+
626+
} catch (Exception e) {
627+
logger.error("Exception while querying with compound session token: {}", e.getMessage(), e);
628+
}
629+
}
630+
457631
// Document delete
458632
private void deleteADocument() throws Exception {
459633
logger.info("Delete document {} by ID.", documentId);
@@ -489,4 +663,17 @@ private void shutdown() {
489663
logger.info("Done with sample.");
490664
}
491665

666+
// Full cross-partition query using a compound session token built from multiple partition-specific session tokens
667+
private void queryCrossPartitionAsyncUsingSessionConsistency() {
668+
logger.info("Starting cross-partition upserts and session-consistent query.");
669+
try {
670+
Map<String, String> partitionKeyToLastRecordedSessionToken = upsertMultipleDocumentsCollectSessionTokens(databaseName, containerName, 10);
671+
672+
String compoundSessionToken = buildCompoundSessionToken(partitionKeyToLastRecordedSessionToken);
673+
674+
queryWithCompoundSessionTokenAndIteratePages(databaseName, containerName, compoundSessionToken);
675+
} catch (Exception ex) {
676+
logger.error("Error while performing cross-partition session-consistent query: {}", ex.getMessage(), ex);
677+
}
678+
}
492679
}

0 commit comments

Comments
 (0)