|
69 | 69 | import java.util.concurrent.ScheduledExecutorService; |
70 | 70 | import java.util.concurrent.ScheduledThreadPoolExecutor; |
71 | 71 | import java.util.concurrent.atomic.AtomicBoolean; |
| 72 | +import java.util.concurrent.atomic.AtomicInteger; |
72 | 73 | import org.junit.After; |
73 | 74 | import org.junit.AfterClass; |
74 | 75 | import org.junit.Before; |
@@ -1359,6 +1360,55 @@ public void testInlinedBeginTxWithOnlyMutations() { |
1359 | 1360 | assertThat(countTransactionsStarted()).isEqualTo(1); |
1360 | 1361 | } |
1361 | 1362 |
|
| 1363 | + @Test |
| 1364 | + public void testInlinedBeginTxWithMutationsBeforeFailedSqlStatement() { |
| 1365 | + Statement insert = Statement.of("insert into foo (id) values (1)"); |
| 1366 | + Statement update = Statement.of("update foo set value='Two' where id=2"); |
| 1367 | + mockSpanner.putStatementResult(StatementResult.update(insert, 1L)); |
| 1368 | + mockSpanner.putStatementResult(StatementResult.update(update, 1L)); |
| 1369 | + // This error will be returned the first time the ExecuteSql method is called. The error is |
| 1370 | + // cleared after the first call, meaning that the second attempt will succeed. |
| 1371 | + mockSpanner.setExecuteSqlExecutionTime( |
| 1372 | + SimulatedExecutionTime.ofException(Status.ALREADY_EXISTS.asRuntimeException())); |
| 1373 | + |
| 1374 | + DatabaseClient client = |
| 1375 | + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); |
| 1376 | + AtomicInteger attempts = new AtomicInteger(); |
| 1377 | + client |
| 1378 | + .readWriteTransaction() |
| 1379 | + .run( |
| 1380 | + transaction -> { |
| 1381 | + attempts.incrementAndGet(); |
| 1382 | + // Buffer a blind write before executing a SQL statement. |
| 1383 | + transaction.buffer( |
| 1384 | + Collections.singletonList( |
| 1385 | + Mutation.newInsertBuilder("FOO").set("ID").to(1L).build())); |
| 1386 | + try { |
| 1387 | + transaction.executeUpdate(insert); |
| 1388 | + } catch (SpannerException exception) { |
| 1389 | + assertEquals(ErrorCode.ALREADY_EXISTS, exception.getErrorCode()); |
| 1390 | + // The error should only occur during the initial attempt. |
| 1391 | + assertEquals(1, attempts.get()); |
| 1392 | + } |
| 1393 | + // We need to execute one more statement in the transaction in order to force a |
| 1394 | + // retry. |
| 1395 | + assertEquals(1L, transaction.executeUpdate(update)); |
| 1396 | + return null; |
| 1397 | + }); |
| 1398 | + // The transaction should be retried once. |
| 1399 | + assertEquals(2, attempts.get()); |
| 1400 | + assertEquals(1, mockSpanner.countRequestsOfType(BeginTransactionRequest.class)); |
| 1401 | + // We get 3 ExecuteSql requests: |
| 1402 | + // 1. The initial attempt that carries a BeginTransaction option. |
| 1403 | + // 2. The retry attempt that does not use a BeginTransaction option. |
| 1404 | + // 3. The second UPDATE statement in the transaction that is only executed during the retry. |
| 1405 | + assertEquals(3, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class)); |
| 1406 | + assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class)); |
| 1407 | + CommitRequest commit = mockSpanner.getRequestsOfType(CommitRequest.class).get(0); |
| 1408 | + // The mutations should only be applied once. |
| 1409 | + assertEquals(1, commit.getMutationsCount()); |
| 1410 | + } |
| 1411 | + |
1362 | 1412 | @SuppressWarnings("resource") |
1363 | 1413 | @Test |
1364 | 1414 | public void testTransactionManagerInlinedBeginTx() { |
|
0 commit comments