1818import static com .google .datastore .v1 .client .DatastoreHelper .makeFilter ;
1919import static com .google .datastore .v1 .client .DatastoreHelper .makeValue ;
2020
21+ import com .google .api .core .ApiClock ;
22+ import com .google .api .core .ApiFuture ;
23+ import com .google .api .core .NanoClock ;
24+ import com .google .api .gax .retrying .BasicResultRetryAlgorithm ;
25+ import com .google .api .gax .retrying .DirectRetryingExecutor ;
26+ import com .google .api .gax .retrying .ExponentialRetryAlgorithm ;
27+ import com .google .api .gax .retrying .ResultRetryAlgorithmWithContext ;
28+ import com .google .api .gax .retrying .RetryAlgorithm ;
29+ import com .google .api .gax .retrying .RetrySettings ;
30+ import com .google .api .gax .retrying .RetryingFuture ;
2131import com .google .common .truth .Truth ;
2232import com .google .datastore .v1 .Filter ;
2333import com .google .datastore .v1 .KindExpression ;
2737import com .google .datastore .v1 .client .Datastore ;
2838import com .google .datastore .v1 .client .DatastoreException ;
2939import com .google .datastore .v1 .client .DatastoreHelper ;
40+ import com .google .rpc .Code ;
3041import java .io .IOException ;
3142import java .security .GeneralSecurityException ;
43+ import java .time .Duration ;
3244import java .util .List ;
45+ import java .util .concurrent .Callable ;
46+ import java .util .concurrent .ExecutionException ;
3347import org .junit .Before ;
3448import org .junit .Test ;
3549
@@ -48,7 +62,7 @@ public void setUp() throws GeneralSecurityException, IOException {
4862 }
4963
5064 @ Test
51- public void testQuerySplitterWithDefaultDb () throws DatastoreException {
65+ public void testQuerySplitterWithDefaultDb () throws Exception {
5266 Filter propertyFilter =
5367 makeFilter ("foo" , PropertyFilter .Operator .EQUAL , makeValue ("value" )).build ();
5468 Query query =
@@ -59,8 +73,7 @@ public void testQuerySplitterWithDefaultDb() throws DatastoreException {
5973
6074 PARTITION = PartitionId .newBuilder ().setProjectId (PROJECT_ID ).build ();
6175
62- List <Query > splits =
63- DatastoreHelper .getQuerySplitter ().getSplits (query , PARTITION , 2 , DATASTORE );
76+ List <Query > splits = getSplitsWithRetry (query , PARTITION , 2 , DATASTORE );
6477 Truth .assertThat (splits ).isNotEmpty ();
6578 splits .forEach (
6679 split -> {
@@ -70,7 +83,7 @@ public void testQuerySplitterWithDefaultDb() throws DatastoreException {
7083 }
7184
7285 @ Test
73- public void testQuerySplitterWithDb () throws DatastoreException {
86+ public void testQuerySplitterWithDb () throws Exception {
7487 Filter propertyFilter =
7588 makeFilter ("foo" , PropertyFilter .Operator .EQUAL , makeValue ("value" )).build ();
7689 Query query =
@@ -81,8 +94,7 @@ public void testQuerySplitterWithDb() throws DatastoreException {
8194
8295 PARTITION = PartitionId .newBuilder ().setProjectId (PROJECT_ID ).setDatabaseId ("test-db" ).build ();
8396
84- List <Query > splits =
85- DatastoreHelper .getQuerySplitter ().getSplits (query , PARTITION , 2 , DATASTORE );
97+ List <Query > splits = getSplitsWithRetry (query , PARTITION , 2 , DATASTORE );
8698
8799 Truth .assertThat (splits ).isNotEmpty ();
88100 splits .forEach (
@@ -91,4 +103,85 @@ public void testQuerySplitterWithDb() throws DatastoreException {
91103 Truth .assertThat (split .getFilter ()).isEqualTo (propertyFilter );
92104 });
93105 }
106+
107+ /**
108+ * A generic helper method that executes a {@link Callable} with retries using the GAX retrying
109+ * framework.
110+ *
111+ * <p>It configures a {@link DirectRetryingExecutor} with the provided {@link RetrySettings} and
112+ * the custom {@link ResultRetryAlgorithmWithContext}.
113+ *
114+ * @param callable the action to execute
115+ * @param retrySettings the retry configuration (backoff, max attempts, timeouts)
116+ * @param resultRetryAlgorithm the algorithm to determine if a failed attempt should be retried
117+ * @return the result of the callable execution
118+ * @throws Exception if the execution fails after all retry attempts.
119+ */
120+ private static <V > V runWithRetry (
121+ Callable <V > callable ,
122+ RetrySettings retrySettings ,
123+ ResultRetryAlgorithmWithContext <V > resultRetryAlgorithm )
124+ throws Exception {
125+ ApiClock clock = NanoClock .getDefaultClock ();
126+ // We must wrap the result algorithm and timed algorithm into a RetryAlgorithm
127+ // as required by DirectRetryingExecutor.
128+ RetryAlgorithm <V > retryAlgorithm =
129+ new RetryAlgorithm <>(
130+ resultRetryAlgorithm , new ExponentialRetryAlgorithm (retrySettings , clock ));
131+
132+ DirectRetryingExecutor <V > executor = new DirectRetryingExecutor <>(retryAlgorithm );
133+ RetryingFuture <V > future = executor .createFuture (callable );
134+
135+ ApiFuture <V > submittedFuture = executor .submit (future );
136+
137+ try {
138+ return submittedFuture .get ();
139+ } catch (ExecutionException e ) {
140+ Throwable cause = e .getCause ();
141+ // submittedFuture.get() wraps any exception thrown during execution in an ExecutionException.
142+ // We unwrap and rethrow the actual cause (Exception or Error) directly so that test failures
143+ // report the root cause (e.g., DatastoreException or AssertionError) instead of the wrapper.
144+ if (cause instanceof Exception ) {
145+ throw (Exception ) cause ;
146+ }
147+ if (cause instanceof Error ) {
148+ throw (Error ) cause ;
149+ }
150+ throw e ;
151+ } catch (InterruptedException e ) {
152+ // Restore the interrupted status before rethrowing, as per Java concurrency best practices.
153+ Thread .currentThread ().interrupt ();
154+ throw e ;
155+ }
156+ }
157+
158+ // This low-level Datastore client (proto-over-HTTP) does not have built-in retry logic
159+ // (unlike the high-level google-cloud-datastore gRPC client). We must explicitly retry
160+ // here to handle transient backend errors (such as Code.INTERNAL auth issues).
161+ // We reuse GAX retrying utilities here in the test to implement this backoff/retry.
162+ private static List <Query > getSplitsWithRetry (
163+ Query query , PartitionId partition , int numSplits , Datastore datastore ) throws Exception {
164+ // Fail fast configuration to avoid long wait times during test failures
165+ RetrySettings retrySettings =
166+ RetrySettings .newBuilder ()
167+ .setMaxAttempts (3 )
168+ .setInitialRetryDelayDuration (Duration .ofMillis (200 ))
169+ .setRetryDelayMultiplier (1.5 )
170+ .setMaxRetryDelayDuration (Duration .ofMillis (500 ))
171+ .setTotalTimeoutDuration (Duration .ofSeconds (2 ))
172+ .build ();
173+ return runWithRetry (
174+ () -> DatastoreHelper .getQuerySplitter ().getSplits (query , partition , numSplits , datastore ),
175+ retrySettings ,
176+ new BasicResultRetryAlgorithm <List <Query >>() {
177+ @ Override
178+ public boolean shouldRetry (Throwable prevThrowable , List <Query > prevResult ) {
179+ if (prevThrowable instanceof DatastoreException ) {
180+ DatastoreException de = (DatastoreException ) prevThrowable ;
181+ return de .getCode () == Code .INTERNAL ;
182+ }
183+ return false ;
184+ }
185+ });
186+ }
94187}
0 commit comments