1616
1717package com .google .cloud .spanner .it ;
1818
19- import static com .google .cloud .spanner .testing .EmulatorSpannerHelper .isUsingEmulator ;
2019import static com .google .common .truth .Truth .assertThat ;
2120import static org .junit .Assert .fail ;
22- import static org .junit .Assume .assumeFalse ;
2321
24- import com .google .api .gax .grpc .GrpcInterceptorProvider ;
2522import com .google .api .gax .longrunning .OperationFuture ;
2623import com .google .api .gax .paging .Page ;
27- import com .google .cloud .Timestamp ;
28- import com .google .cloud .spanner .Backup ;
2924import com .google .cloud .spanner .Database ;
3025import com .google .cloud .spanner .DatabaseAdminClient ;
3126import com .google .cloud .spanner .ErrorCode ;
3227import com .google .cloud .spanner .IntegrationTestEnv ;
3328import com .google .cloud .spanner .Options ;
3429import com .google .cloud .spanner .ParallelIntegrationTest ;
35- import com .google .cloud .spanner .Spanner ;
3630import com .google .cloud .spanner .SpannerException ;
37- import com .google .cloud .spanner .SpannerOptions ;
3831import com .google .cloud .spanner .testing .RemoteSpannerHelper ;
3932import com .google .common .collect .ImmutableList ;
4033import com .google .common .collect .Iterables ;
4134import com .google .common .collect .Iterators ;
42- import com .google .spanner .admin .database .v1 .CreateBackupMetadata ;
4335import com .google .spanner .admin .database .v1 .CreateDatabaseMetadata ;
44- import com .google .spanner .admin .database .v1 .RestoreDatabaseMetadata ;
4536import com .google .spanner .admin .database .v1 .UpdateDatabaseDdlMetadata ;
46- import io .grpc .CallOptions ;
47- import io .grpc .Channel ;
48- import io .grpc .ClientCall ;
49- import io .grpc .ClientInterceptor ;
50- import io .grpc .ForwardingClientCall .SimpleForwardingClientCall ;
51- import io .grpc .ForwardingClientCallListener .SimpleForwardingClientCallListener ;
52- import io .grpc .Metadata ;
53- import io .grpc .MethodDescriptor ;
54- import io .grpc .Status ;
5537import java .util .ArrayList ;
56- import java .util .Collections ;
5738import java .util .List ;
58- import java .util .Random ;
59- import java .util .concurrent .ExecutionException ;
6039import java .util .concurrent .TimeUnit ;
61- import java .util .concurrent .atomic .AtomicBoolean ;
62- import java .util .concurrent .atomic .AtomicInteger ;
6340import org .junit .After ;
6441import org .junit .Before ;
6542import org .junit .ClassRule ;
7249@ Category (ParallelIntegrationTest .class )
7350@ RunWith (JUnit4 .class )
7451public class ITDatabaseAdminTest {
75- private static final long DATABASE_TIMEOUT_MINUTES = 5 ;
76- private static final long BACKUP_TIMEOUT_MINUTES = 20 ;
77- private static final long RESTORE_TIMEOUT_MINUTES = 10 ;
52+ private static final long TIMEOUT_MINUTES = 5 ;
7853 @ ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv ();
7954 private DatabaseAdminClient dbAdminClient ;
8055 private RemoteSpannerHelper testHelper ;
@@ -101,7 +76,7 @@ public void databaseOperations() throws Exception {
10176 String statement1 = "CREATE TABLE T (\n " + " K STRING(MAX),\n " + ") PRIMARY KEY(K)" ;
10277 OperationFuture <Database , CreateDatabaseMetadata > op =
10378 dbAdminClient .createDatabase (instanceId , dbId , ImmutableList .of (statement1 ));
104- Database db = op .get (DATABASE_TIMEOUT_MINUTES , TimeUnit .MINUTES );
79+ Database db = op .get (TIMEOUT_MINUTES , TimeUnit .MINUTES );
10580 dbs .add (db );
10681 assertThat (db .getId ().getDatabase ()).isEqualTo (dbId );
10782
@@ -122,7 +97,7 @@ public void databaseOperations() throws Exception {
12297 String statement2 = "CREATE TABLE T2 (\n " + " K2 STRING(MAX),\n " + ") PRIMARY KEY(K2)" ;
12398 OperationFuture <?, ?> op2 =
12499 dbAdminClient .updateDatabaseDdl (instanceId , dbId , ImmutableList .of (statement2 ), null );
125- op2 .get (DATABASE_TIMEOUT_MINUTES , TimeUnit .MINUTES );
100+ op2 .get (TIMEOUT_MINUTES , TimeUnit .MINUTES );
126101 List <String > statementsInDb = dbAdminClient .getDatabaseDdl (instanceId , dbId );
127102 assertThat (statementsInDb ).containsExactly (statement1 , statement2 );
128103
@@ -143,15 +118,15 @@ public void updateDdlRetry() throws Exception {
143118 String statement1 = "CREATE TABLE T (\n " + " K STRING(MAX),\n " + ") PRIMARY KEY(K)" ;
144119 OperationFuture <Database , CreateDatabaseMetadata > op =
145120 dbAdminClient .createDatabase (instanceId , dbId , ImmutableList .of (statement1 ));
146- Database db = op .get (DATABASE_TIMEOUT_MINUTES , TimeUnit .MINUTES );
121+ Database db = op .get (TIMEOUT_MINUTES , TimeUnit .MINUTES );
147122 dbs .add (db );
148123 String statement2 = "CREATE TABLE T2 (\n " + " K2 STRING(MAX),\n " + ") PRIMARY KEY(K2)" ;
149124 OperationFuture <Void , UpdateDatabaseDdlMetadata > op1 =
150125 dbAdminClient .updateDatabaseDdl (instanceId , dbId , ImmutableList .of (statement2 ), "myop" );
151126 OperationFuture <Void , UpdateDatabaseDdlMetadata > op2 =
152127 dbAdminClient .updateDatabaseDdl (instanceId , dbId , ImmutableList .of (statement2 ), "myop" );
153- op1 .get (DATABASE_TIMEOUT_MINUTES , TimeUnit .MINUTES );
154- op2 .get (DATABASE_TIMEOUT_MINUTES , TimeUnit .MINUTES );
128+ op1 .get (TIMEOUT_MINUTES , TimeUnit .MINUTES );
129+ op2 .get (TIMEOUT_MINUTES , TimeUnit .MINUTES );
155130
156131 // Remove the progress list from the metadata before comparing, as there could be small
157132 // differences between the two in the reported progress depending on exactly when each
@@ -170,7 +145,7 @@ public void databaseOperationsViaEntity() throws Exception {
170145 String statement1 = "CREATE TABLE T (\n " + " K STRING(MAX),\n " + ") PRIMARY KEY(K)" ;
171146 OperationFuture <Database , CreateDatabaseMetadata > op =
172147 dbAdminClient .createDatabase (instanceId , dbId , ImmutableList .of (statement1 ));
173- Database db = op .get (DATABASE_TIMEOUT_MINUTES , TimeUnit .MINUTES );
148+ Database db = op .get (TIMEOUT_MINUTES , TimeUnit .MINUTES );
174149 dbs .add (db );
175150 assertThat (db .getId ().getDatabase ()).isEqualTo (dbId );
176151
@@ -179,7 +154,7 @@ public void databaseOperationsViaEntity() throws Exception {
179154
180155 String statement2 = "CREATE TABLE T2 (\n " + " K2 STRING(MAX),\n " + ") PRIMARY KEY(K2)" ;
181156 OperationFuture <?, ?> op2 = db .updateDdl (ImmutableList .of (statement2 ), null );
182- op2 .get (DATABASE_TIMEOUT_MINUTES , TimeUnit .MINUTES );
157+ op2 .get (TIMEOUT_MINUTES , TimeUnit .MINUTES );
183158 Iterable <String > statementsInDb = db .getDdl ();
184159 assertThat (statementsInDb ).containsExactly (statement1 , statement2 );
185160 db .drop ();
@@ -219,218 +194,4 @@ public void listPagination() throws Exception {
219194 }
220195 assertThat (dbIdsGot ).containsAtLeastElementsIn (dbIds );
221196 }
222-
223- private static final class InjectErrorInterceptorProvider implements GrpcInterceptorProvider {
224- final AtomicBoolean injectError = new AtomicBoolean (true );
225- final AtomicInteger getOperationCount = new AtomicInteger ();
226- final AtomicInteger methodCount = new AtomicInteger ();
227- final String methodName ;
228-
229- private InjectErrorInterceptorProvider (String methodName ) {
230- this .methodName = methodName ;
231- }
232-
233- @ Override
234- public List <ClientInterceptor > getInterceptors () {
235- ClientInterceptor interceptor =
236- new ClientInterceptor () {
237- @ Override
238- public <ReqT , RespT > ClientCall <ReqT , RespT > interceptCall (
239- MethodDescriptor <ReqT , RespT > method , CallOptions callOptions , Channel next ) {
240- if (method .getFullMethodName ().contains ("GetOperation" )) {
241- getOperationCount .incrementAndGet ();
242- }
243- if (!method .getFullMethodName ().contains (methodName )) {
244- return next .newCall (method , callOptions );
245- }
246-
247- methodCount .incrementAndGet ();
248- final AtomicBoolean errorInjected = new AtomicBoolean ();
249- final ClientCall <ReqT , RespT > clientCall = next .newCall (method , callOptions );
250-
251- return new SimpleForwardingClientCall <ReqT , RespT >(clientCall ) {
252- @ Override
253- public void start (Listener <RespT > responseListener , Metadata headers ) {
254- super .start (
255- new SimpleForwardingClientCallListener <RespT >(responseListener ) {
256- @ Override
257- public void onMessage (RespT message ) {
258- if (injectError .getAndSet (false )) {
259- errorInjected .set (true );
260- clientCall .cancel ("Cancelling call for injected error" , null );
261- } else {
262- super .onMessage (message );
263- }
264- }
265-
266- @ Override
267- public void onClose (Status status , Metadata metadata ) {
268- if (errorInjected .get ()) {
269- status = Status .UNAVAILABLE .augmentDescription ("INJECTED BY TEST" );
270- }
271- super .onClose (status , metadata );
272- }
273- },
274- headers );
275- }
276- };
277- }
278- };
279- return Collections .singletonList (interceptor );
280- }
281- }
282-
283- @ Test
284- public void testRetryNonIdempotentRpcsReturningLongRunningOperations () throws Exception {
285- assumeFalse (
286- "Querying long-running operations is not supported on the emulator" , isUsingEmulator ());
287-
288- // RPCs that return a long-running operation such as CreateDatabase, CreateBackup and
289- // RestoreDatabase are non-idempotent and can normally not be automatically retried in case of a
290- // transient failure. The client library will however automatically query the backend to check
291- // whether the corresponding operation was started or not, and if it was, it will pick up the
292- // existing operation. If no operation is found, a new RPC call will be executed to start the
293- // operation.
294-
295- List <Database > databases = new ArrayList <>();
296- List <Backup > backups = new ArrayList <>();
297- String initialDatabaseId ;
298- Timestamp initialDbCreateTime ;
299-
300- try {
301- // CreateDatabase
302- InjectErrorInterceptorProvider createDbInterceptor =
303- new InjectErrorInterceptorProvider ("CreateDatabase" );
304- SpannerOptions options =
305- testHelper .getOptions ().toBuilder ().setInterceptorProvider (createDbInterceptor ).build ();
306- try (Spanner spanner = options .getService ()) {
307- initialDatabaseId = testHelper .getUniqueDatabaseId ();
308- DatabaseAdminClient client = spanner .getDatabaseAdminClient ();
309- OperationFuture <Database , CreateDatabaseMetadata > op =
310- client .createDatabase (
311- testHelper .getInstanceId ().getInstance (),
312- initialDatabaseId ,
313- Collections .emptyList ());
314- databases .add (op .get (DATABASE_TIMEOUT_MINUTES , TimeUnit .MINUTES ));
315- // Keep track of the original create time of this database, as we will drop this database
316- // later and create another one with the exact same name. That means that the ListOperations
317- // call will return at least two CreateDatabase operations. The retry logic should always
318- // pick the last one.
319- initialDbCreateTime = op .get (DATABASE_TIMEOUT_MINUTES , TimeUnit .MINUTES ).getCreateTime ();
320- // Assert that the CreateDatabase RPC was called only once, and that the operation tracking
321- // was resumed through a GetOperation call.
322- assertThat (createDbInterceptor .methodCount .get ()).isEqualTo (1 );
323- assertThat (createDbInterceptor .getOperationCount .get ()).isAtLeast (1 );
324- }
325-
326- // CreateBackup
327- InjectErrorInterceptorProvider createBackupInterceptor =
328- new InjectErrorInterceptorProvider ("CreateBackup" );
329- options =
330- testHelper
331- .getOptions ()
332- .toBuilder ()
333- .setInterceptorProvider (createBackupInterceptor )
334- .build ();
335- try (Spanner spanner = options .getService ()) {
336- String databaseId = databases .get (0 ).getId ().getDatabase ();
337- String backupId = String .format ("test-bck-%08d" , new Random ().nextInt (100000000 ));
338- DatabaseAdminClient client = spanner .getDatabaseAdminClient ();
339- OperationFuture <Backup , CreateBackupMetadata > op =
340- client .createBackup (
341- testHelper .getInstanceId ().getInstance (),
342- backupId ,
343- databaseId ,
344- Timestamp .ofTimeSecondsAndNanos (
345- Timestamp .now ().getSeconds () + TimeUnit .SECONDS .convert (7L , TimeUnit .DAYS ), 0 ));
346- backups .add (op .get (BACKUP_TIMEOUT_MINUTES , TimeUnit .MINUTES ));
347- // Assert that the CreateBackup RPC was called only once, and that the operation tracking
348- // was resumed through a GetOperation call.
349- assertThat (createDbInterceptor .methodCount .get ()).isEqualTo (1 );
350- assertThat (createDbInterceptor .getOperationCount .get ()).isAtLeast (1 );
351- }
352-
353- // RestoreBackup
354- int attempts = 0 ;
355- while (true ) {
356- InjectErrorInterceptorProvider restoreBackupInterceptor =
357- new InjectErrorInterceptorProvider ("RestoreBackup" );
358- options =
359- testHelper
360- .getOptions ()
361- .toBuilder ()
362- .setInterceptorProvider (restoreBackupInterceptor )
363- .build ();
364- try (Spanner spanner = options .getService ()) {
365- String backupId = backups .get (0 ).getId ().getBackup ();
366- String restoredDbId = testHelper .getUniqueDatabaseId ();
367- DatabaseAdminClient client = spanner .getDatabaseAdminClient ();
368- OperationFuture <Database , RestoreDatabaseMetadata > op =
369- client .restoreDatabase (
370- testHelper .getInstanceId ().getInstance (),
371- backupId ,
372- testHelper .getInstanceId ().getInstance (),
373- restoredDbId );
374- databases .add (op .get (RESTORE_TIMEOUT_MINUTES , TimeUnit .MINUTES ));
375- // Assert that the RestoreDatabase RPC was called only once, and that the operation
376- // tracking was resumed through a GetOperation call.
377- assertThat (createDbInterceptor .methodCount .get ()).isEqualTo (1 );
378- assertThat (createDbInterceptor .getOperationCount .get ()).isAtLeast (1 );
379- break ;
380- } catch (ExecutionException e ) {
381- if (e .getCause () instanceof SpannerException
382- && ((SpannerException ) e .getCause ()).getErrorCode () == ErrorCode .FAILED_PRECONDITION
383- && e .getCause ()
384- .getMessage ()
385- .contains ("Please retry the operation once the pending restores complete" )) {
386- attempts ++;
387- if (attempts == 10 ) {
388- // Still same error after 10 attempts. Ignore.
389- break ;
390- }
391- // wait and then retry.
392- Thread .sleep (60_000L );
393- } else {
394- throw e ;
395- }
396- }
397- }
398-
399- // Create another database with the exact same name as the first database.
400- createDbInterceptor = new InjectErrorInterceptorProvider ("CreateDatabase" );
401- options =
402- testHelper .getOptions ().toBuilder ().setInterceptorProvider (createDbInterceptor ).build ();
403- try (Spanner spanner = options .getService ()) {
404- DatabaseAdminClient client = spanner .getDatabaseAdminClient ();
405- // First drop the initial database.
406- client .dropDatabase (testHelper .getInstanceId ().getInstance (), initialDatabaseId );
407- // Now re-create a database with the exact same name.
408- OperationFuture <Database , CreateDatabaseMetadata > op =
409- client .createDatabase (
410- testHelper .getInstanceId ().getInstance (),
411- initialDatabaseId ,
412- Collections .emptyList ());
413- // Check that the second database was created and has a greater creation time than the
414- // first.
415- Timestamp secondCreationTime =
416- op .get (DATABASE_TIMEOUT_MINUTES , TimeUnit .MINUTES ).getCreateTime ();
417- // TODO: Change this to greaterThan when the create time of a database is reported back by
418- // the server.
419- assertThat (secondCreationTime ).isAtLeast (initialDbCreateTime );
420- // Assert that the CreateDatabase RPC was called only once, and that the operation tracking
421- // was resumed through a GetOperation call.
422- assertThat (createDbInterceptor .methodCount .get ()).isEqualTo (1 );
423- assertThat (createDbInterceptor .getOperationCount .get ()).isAtLeast (1 );
424- }
425- } finally {
426- DatabaseAdminClient client = testHelper .getClient ().getDatabaseAdminClient ();
427- for (Database database : databases ) {
428- client .dropDatabase (
429- database .getId ().getInstanceId ().getInstance (), database .getId ().getDatabase ());
430- }
431- for (Backup backup : backups ) {
432- client .deleteBackup (backup .getInstanceId ().getInstance (), backup .getId ().getBackup ());
433- }
434- }
435- }
436197}
0 commit comments