1515import com .azure .cosmos .models .CosmosDatabaseRequestOptions ;
1616import com .azure .cosmos .models .CosmosDatabaseResponse ;
1717import com .azure .cosmos .models .CosmosItemRequestOptions ;
18+ import com .azure .cosmos .models .CosmosItemResponse ;
1819import com .azure .cosmos .models .CosmosQueryRequestOptions ;
1920import com .azure .cosmos .models .FeedResponse ;
2021import com .azure .cosmos .models .PartitionKey ;
2526import com .fasterxml .jackson .databind .JsonNode ;
2627
2728import reactor .core .publisher .Flux ;
29+ import reactor .core .publisher .Mono ;
2830
2931import org .slf4j .Logger ;
3032import org .slf4j .LoggerFactory ;
3133
3234import java .util .ArrayList ;
3335import java .util .UUID ;
36+ import java .util .concurrent .ConcurrentHashMap ;
37+ import java .util .concurrent .ThreadLocalRandom ;
3438import java .util .concurrent .atomic .AtomicBoolean ;
3539import java .util .concurrent .atomic .AtomicInteger ;
40+ import java .util .Map ;
41+ import java .util .List ;
42+ import java .util .stream .Collectors ;
3643
3744public 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