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 .HashMap ;
42+ import java .util .List ;
43+ import java .util .stream .Collectors ;
3644
3745public 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