Skip to content

Commit f4560e5

Browse files
authored
chore(spanner): fix mutation only case in rw mux with aborted error (#3571)
* chore(Spanner): fix mutation only case ibn rw using mux with aborted errors * chore(Spanner): fix mockspanner * chore(spanner): update logic for mutations only * chore(spanner): lint fix * chore(spanner): update logic to handle begin txn retry only for aborted case * chore(spanner): add span for abort
1 parent 68031f1 commit f4560e5

File tree

3 files changed

+221
-1
lines changed

3 files changed

+221
-1
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,19 @@ private void createTxnAsync(
326326
}
327327
res.set(null);
328328
} catch (ExecutionException e) {
329+
SpannerException spannerException = SpannerExceptionFactory.asSpannerException(e);
330+
if (spannerException.getErrorCode() == ErrorCode.ABORTED
331+
&& session.getIsMultiplexed()
332+
&& mutation != null) {
333+
// Begin transaction can return ABORTED errors. This can only happen if it included
334+
// a mutation key, which again means that this is a mutation-only transaction on a
335+
// multiplexed session.
336+
span.addAnnotation(
337+
"Transaction Creation Failed with ABORT. Retrying",
338+
e.getCause() == null ? e : e.getCause());
339+
createTxnAsync(res, mutation);
340+
return;
341+
}
329342
span.addAnnotation(
330343
"Transaction Creation Failed", e.getCause() == null ? e : e.getCause());
331344
res.setException(e.getCause() == null ? e : e.getCause());

google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1919,7 +1919,8 @@ private Transaction beginTransaction(
19191919
}
19201920
if (session.getMultiplexed()
19211921
&& options.getModeCase() == ModeCase.READ_WRITE
1922-
&& mutationKey != null) {
1922+
&& mutationKey != null
1923+
&& mutationKey != com.google.spanner.v1.Mutation.getDefaultInstance()) {
19231924
// Mutation only case in a read-write transaction.
19241925
builder.setPrecommitToken(getTransactionPrecommitToken(transactionId));
19251926
}
@@ -2023,6 +2024,14 @@ public void commit(CommitRequest request, StreamObserver<CommitResponse> respons
20232024
return;
20242025
}
20252026
sessionLastUsed.put(session.getName(), Instant.now());
2027+
if (session.getMultiplexed()
2028+
&& !request.hasPrecommitToken()
2029+
&& !request.hasSingleUseTransaction()) {
2030+
throw Status.INVALID_ARGUMENT
2031+
.withDescription(
2032+
"A Commit request for a read-write transaction on a multiplexed session must specify a precommit token.")
2033+
.asRuntimeException();
2034+
}
20262035
try {
20272036
commitExecutionTime.simulateExecutionTime(exceptions, stickyGlobalExceptions, freezeLock);
20282037
// Find or start a transaction

google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,204 @@ public void testMutationOnlyUsingAsyncTransactionManager() {
12941294
request.getPrecommitToken().getPrecommitToken());
12951295
}
12961296

1297+
private Spanner setupSpannerForAbortedBeginTransactionTests() {
1298+
// Force the BeginTransaction RPC to return Aborted the first time it is called. The exception
1299+
// is cleared after the first call, so the retry should succeed.
1300+
mockSpanner.setBeginTransactionExecutionTime(
1301+
SimulatedExecutionTime.ofException(
1302+
mockSpanner.createAbortedException(ByteString.copyFromUtf8("test"))));
1303+
1304+
return SpannerOptions.newBuilder()
1305+
.setProjectId("test-project")
1306+
.setChannelProvider(channelProvider)
1307+
.setCredentials(NoCredentials.getInstance())
1308+
.setSessionPoolOption(
1309+
SessionPoolOptions.newBuilder()
1310+
.setUseMultiplexedSession(true)
1311+
.setUseMultiplexedSessionForRW(true)
1312+
.setSkipVerifyingBeginTransactionForMuxRW(true)
1313+
.build())
1314+
.build()
1315+
.getService();
1316+
}
1317+
1318+
private void verifyMutationKeySetInBeginTransactionRequests(
1319+
List<BeginTransactionRequest> beginTransactionRequests) {
1320+
assertEquals(2, beginTransactionRequests.size());
1321+
// Verify the requests are executed using multiplexed sessions
1322+
for (BeginTransactionRequest request : beginTransactionRequests) {
1323+
assertTrue(mockSpanner.getSession(request.getSession()).getMultiplexed());
1324+
assertTrue(request.hasMutationKey());
1325+
assertTrue(request.getMutationKey().hasInsert());
1326+
}
1327+
}
1328+
1329+
private void verifyPreCommitTokenSetInCommitRequest(List<CommitRequest> commitRequests) {
1330+
assertEquals(1L, commitRequests.size());
1331+
for (CommitRequest request : commitRequests) {
1332+
assertTrue(mockSpanner.getSession(request.getSession()).getMultiplexed());
1333+
assertNotNull(request.getPrecommitToken());
1334+
assertEquals(
1335+
ByteString.copyFromUtf8("TransactionPrecommitToken"),
1336+
request.getPrecommitToken().getPrecommitToken());
1337+
}
1338+
}
1339+
1340+
// The following 4 tests validate mutation-only cases where the BeginTransaction RPC fails with an
1341+
// ABORTED or retryable error
1342+
@Test
1343+
public void testMutationOnlyCaseAbortedDuringBeginTransaction() {
1344+
// This test ensures that when a transaction containing only mutations is retried after an
1345+
// ABORT error in the BeginTransaction RPC:
1346+
// 1. The mutation key is correctly included in the BeginTransaction request.
1347+
// 2. The precommit token is properly set in the Commit request.
1348+
Spanner spanner = setupSpannerForAbortedBeginTransactionTests();
1349+
DatabaseClientImpl client =
1350+
(DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d"));
1351+
1352+
client
1353+
.readWriteTransaction()
1354+
.run(
1355+
transaction -> {
1356+
Mutation mutation =
1357+
Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build();
1358+
transaction.buffer(mutation);
1359+
return null;
1360+
});
1361+
1362+
// Verify that for mutation only case, a mutation key is set in BeginTransactionRequest.
1363+
List<BeginTransactionRequest> beginTransactionRequests =
1364+
mockSpanner.getRequestsOfType(BeginTransactionRequest.class);
1365+
verifyMutationKeySetInBeginTransactionRequests(beginTransactionRequests);
1366+
1367+
// Verify that the latest precommit token is set in the CommitRequest
1368+
List<CommitRequest> commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class);
1369+
verifyPreCommitTokenSetInCommitRequest(commitRequests);
1370+
1371+
spanner.close();
1372+
}
1373+
1374+
@Test
1375+
public void testMutationOnlyUsingTransactionManagerAbortedDuringBeginTransaction() {
1376+
// This test ensures that when a transaction containing only mutations is retried after an
1377+
// ABORT error in the BeginTransaction RPC:
1378+
// 1. The mutation key is correctly included in the BeginTransaction request.
1379+
// 2. The precommit token is properly set in the Commit request.
1380+
Spanner spanner = setupSpannerForAbortedBeginTransactionTests();
1381+
DatabaseClientImpl client =
1382+
(DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d"));
1383+
1384+
try (TransactionManager manager = client.transactionManager()) {
1385+
TransactionContext transaction = manager.begin();
1386+
while (true) {
1387+
try {
1388+
Mutation mutation =
1389+
Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build();
1390+
transaction.buffer(mutation);
1391+
manager.commit();
1392+
assertNotNull(manager.getCommitTimestamp());
1393+
break;
1394+
} catch (AbortedException e) {
1395+
transaction = manager.resetForRetry();
1396+
}
1397+
}
1398+
}
1399+
1400+
// Verify that for mutation only case, a mutation key is set in BeginTransactionRequest.
1401+
List<BeginTransactionRequest> beginTransactionRequests =
1402+
mockSpanner.getRequestsOfType(BeginTransactionRequest.class);
1403+
verifyMutationKeySetInBeginTransactionRequests(beginTransactionRequests);
1404+
1405+
// Verify that the latest precommit token is set in the CommitRequest
1406+
List<CommitRequest> commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class);
1407+
verifyPreCommitTokenSetInCommitRequest(commitRequests);
1408+
1409+
spanner.close();
1410+
}
1411+
1412+
@Test
1413+
public void testMutationOnlyUsingAsyncRunnerAbortedDuringBeginTransaction() {
1414+
// This test ensures that when a transaction containing only mutations is retried after an
1415+
// ABORT error in the BeginTransaction RPC:
1416+
// 1. The mutation key is correctly included in the BeginTransaction request.
1417+
// 2. The precommit token is properly set in the Commit request.
1418+
1419+
Spanner spanner = setupSpannerForAbortedBeginTransactionTests();
1420+
DatabaseClientImpl client =
1421+
(DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d"));
1422+
1423+
AsyncRunner runner = client.runAsync();
1424+
get(
1425+
runner.runAsync(
1426+
txn -> {
1427+
txn.buffer(
1428+
Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build());
1429+
return ApiFutures.immediateFuture(null);
1430+
},
1431+
MoreExecutors.directExecutor()));
1432+
1433+
// Verify that for mutation only case, a mutation key is set in BeginTransactionRequest.
1434+
List<BeginTransactionRequest> beginTransactionRequests =
1435+
mockSpanner.getRequestsOfType(BeginTransactionRequest.class);
1436+
verifyMutationKeySetInBeginTransactionRequests(beginTransactionRequests);
1437+
1438+
// Verify that the latest precommit token is set in the CommitRequest
1439+
List<CommitRequest> commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class);
1440+
verifyPreCommitTokenSetInCommitRequest(commitRequests);
1441+
1442+
spanner.close();
1443+
}
1444+
1445+
@Test
1446+
public void testMutationOnlyUsingTransactionManagerAsyncAbortedDuringBeginTransaction()
1447+
throws Exception {
1448+
// This test verifies that in the case of mutations-only, when a transaction is retried after an
1449+
// ABORT in BeginTransaction RPC, the mutation key is correctly set in the BeginTransaction
1450+
// request
1451+
// and precommit token is set in Commit request.
1452+
Spanner spanner = setupSpannerForAbortedBeginTransactionTests();
1453+
DatabaseClientImpl client =
1454+
(DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d"));
1455+
1456+
try (AsyncTransactionManager manager = client.transactionManagerAsync()) {
1457+
TransactionContextFuture transaction = manager.beginAsync();
1458+
while (true) {
1459+
CommitTimestampFuture commitTimestamp =
1460+
transaction
1461+
.then(
1462+
(txn, input) -> {
1463+
txn.buffer(
1464+
Mutation.newInsertBuilder("FOO")
1465+
.set("ID")
1466+
.to(1L)
1467+
.set("NAME")
1468+
.to("Bar")
1469+
.build());
1470+
return ApiFutures.immediateFuture(null);
1471+
},
1472+
MoreExecutors.directExecutor())
1473+
.commitAsync();
1474+
try {
1475+
assertThat(commitTimestamp.get()).isNotNull();
1476+
break;
1477+
} catch (AbortedException e) {
1478+
transaction = manager.resetForRetryAsync();
1479+
}
1480+
}
1481+
}
1482+
1483+
// Verify that for mutation only case, a mutation key is set in BeginTransactionRequest.
1484+
List<BeginTransactionRequest> beginTransactionRequests =
1485+
mockSpanner.getRequestsOfType(BeginTransactionRequest.class);
1486+
verifyMutationKeySetInBeginTransactionRequests(beginTransactionRequests);
1487+
1488+
// Verify that the latest precommit token is set in the CommitRequest
1489+
List<CommitRequest> commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class);
1490+
verifyPreCommitTokenSetInCommitRequest(commitRequests);
1491+
1492+
spanner.close();
1493+
}
1494+
12971495
// Tests the behavior of the server-side kill switch for read-write multiplexed sessions..
12981496
@Test
12991497
public void testInitialBeginTransactionWithRW_receivesUnimplemented_fallsBackToRegularSession() {

0 commit comments

Comments
 (0)