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

Commit b7c5306

Browse files
Merge pull request #76 from jeet1995/SessionConsistencyForCrossPartitionQueries
Adding a way to execute a cross-partition query with compound session tokens.
2 parents e90f538 + 1af284d commit b7c5306

4 files changed

Lines changed: 328 additions & 19 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: 188 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,20 @@
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.List;
42+
import java.util.stream.Collectors;
3643

3744
public class QueriesQuickstartAsync {
3845

@@ -116,6 +123,8 @@ private void queriesDemo() throws Exception {
116123
logger.info("Async doc create done.");
117124

118125
//execute all the below query examples asynchronously and waiting until all done
126+
127+
queryCrossPartitionAsyncUsingSessionConsistency();
119128
queryAllDocuments();
120129
queryWithPagingAndContinuationTokenAndPrintQueryCharge(new CosmosQueryRequestOptions());
121130
queryEquality();
@@ -130,7 +139,6 @@ private void queriesDemo() throws Exception {
130139
queryWithQuerySpec();
131140
parallelQueryWithPagingAndContinuationTokenAndPrintQueryCharge();
132141

133-
134142
// We are adding Thread.sleep to mimic the some business computation that can
135143
// happen while waiting for earlier processes to finish.
136144
Thread.sleep(2000);
@@ -208,7 +216,7 @@ private void createContainerIfNotExists() throws Exception {
208216
new CosmosContainerProperties(containerName, "/lastName");
209217

210218
// Provision throughput
211-
ThroughputProperties throughputProperties = ThroughputProperties.createManualThroughput(400);
219+
ThroughputProperties throughputProperties = ThroughputProperties.createManualThroughput(12000);
212220

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

280287
// Accumulate the request charge of this page
281288
requestCharge += page.getRequestCharge();
@@ -359,6 +366,7 @@ private void queryOrderBy() throws Exception {
359366
executeQueryPrintSingleResult("SELECT * FROM Families f WHERE f.LastName = 'Andersen' ORDER BY f.Children[0].Grade");
360367
}
361368

369+
@SuppressWarnings("unused")
362370
private void queryDistinct() throws Exception {
363371
logger.info("DISTINCT queries");
364372

@@ -454,6 +462,165 @@ private void queryWithQuerySpec() throws Exception {
454462
executeQueryWithQuerySpecPrintSingleResult(querySpec);
455463
}
456464

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

659+
// Steps:
660+
// 1. Create multiple documents (some with duplicate partition keys) and collect latest session tokens per partition key.
661+
// 2. Build a compound session token by concatenating the session tokens using commas present as values in partitionKeyToLastRecordedSessionToken.
662+
// 3. Perform a cross-partition query using the compound session token.
663+
private void queryCrossPartitionAsyncUsingSessionConsistency() {
664+
logger.info("Starting upserts across various logical partitions and a session-consistent query...");
665+
try {
666+
Map<String, String> partitionKeyToLastRecordedSessionToken = upsertMultipleDocumentsAndCollectSessionTokens(databaseName, containerName, 10);
667+
668+
String compoundSessionToken = buildCompoundSessionToken(partitionKeyToLastRecordedSessionToken);
669+
670+
queryWithCompoundSessionTokenAndIteratePages(databaseName, containerName, compoundSessionToken);
671+
} catch (Exception ex) {
672+
logger.error("Error while performing cross-partition session-consistent query: {}", ex.getMessage(), ex);
673+
}
674+
}
492675
}

0 commit comments

Comments
 (0)