From 144f706ccf1dff0143ce058dd74507f2ea45cfe1 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 25 Sep 2024 06:13:11 +0000 Subject: [PATCH 01/16] chore(spanner): track previous transaction id incase of retry during aborted transaction --- .../spanner/AsyncTransactionManagerImpl.java | 5 +++- .../com/google/cloud/spanner/SessionImpl.java | 19 ++++++++++--- .../cloud/spanner/TransactionManagerImpl.java | 8 ++++-- .../cloud/spanner/TransactionRunnerImpl.java | 28 ++++++++++++++++--- 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java index 8b20dd824a0..53def8f2531 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java @@ -28,6 +28,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.ByteString; /** Implementation of {@link AsyncTransactionManager}. */ final class AsyncTransactionManagerImpl @@ -77,7 +78,9 @@ public TransactionContextFutureImpl beginAsync() { private ApiFuture internalBeginAsync(boolean firstAttempt) { txnState = TransactionState.STARTED; - txn = session.newTransaction(options); + ByteString previousAbortedTransactionId = + !firstAttempt && session.getIsMultiplexed() ? txn.transactionId : null; + txn = session.newTransaction(options, previousAbortedTransactionId); if (firstAttempt) { session.setActive(this); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java index 7b9abc71a85..55c2afbc05a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java @@ -69,7 +69,8 @@ static void throwIfTransactionsPending() { } } - static TransactionOptions createReadWriteTransactionOptions(Options options) { + static TransactionOptions createReadWriteTransactionOptions( + Options options, ByteString previousAbortedTransactionId) { TransactionOptions.Builder transactionOptions = TransactionOptions.newBuilder(); if (options.withExcludeTxnFromChangeStreams() == Boolean.TRUE) { transactionOptions.setExcludeTxnFromChangeStreams(true); @@ -78,6 +79,11 @@ static TransactionOptions createReadWriteTransactionOptions(Options options) { if (options.withOptimisticLock() == Boolean.TRUE) { readWrite.setReadLockMode(TransactionOptions.ReadWrite.ReadLockMode.OPTIMISTIC); } + if (previousAbortedTransactionId != null + && previousAbortedTransactionId != com.google.protobuf.ByteString.EMPTY) { + // TODO(sriharshach): uncomment this when multiplexed session R/W proto is published + // readWrite.setMultiplexedSessionPreviousTransactionId(previousAbortedTransactionId); + } transactionOptions.setReadWrite(readWrite); return transactionOptions.build(); } @@ -427,13 +433,17 @@ public void close() { } ApiFuture beginTransactionAsync( - Options transactionOptions, boolean routeToLeader, Map channelHint) { + Options transactionOptions, + boolean routeToLeader, + Map channelHint, + ByteString previousAbortedTransactionId) { final SettableApiFuture res = SettableApiFuture.create(); final ISpan span = tracer.spanBuilder(SpannerImpl.BEGIN_TRANSACTION); final BeginTransactionRequest request = BeginTransactionRequest.newBuilder() .setSession(getName()) - .setOptions(createReadWriteTransactionOptions(transactionOptions)) + .setOptions( + createReadWriteTransactionOptions(transactionOptions, previousAbortedTransactionId)) .build(); final ApiFuture requestFuture; try (IScope ignore = tracer.withSpan(span)) { @@ -469,11 +479,12 @@ ApiFuture beginTransactionAsync( return res; } - TransactionContextImpl newTransaction(Options options) { + TransactionContextImpl newTransaction(Options options, ByteString previousAbortedTransactionId) { return TransactionContextImpl.newBuilder() .setSession(this) .setOptions(options) .setTransactionId(null) + .setPreviousAbortedTransactionId(previousAbortedTransactionId) .setOptions(options) .setTrackTransactionStarter(spanner.getOptions().isTrackTransactionStarter()) .setRpc(spanner.getRpc()) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java index 95ffd1168b2..ad71c852820 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java @@ -20,6 +20,7 @@ import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.SessionImpl.SessionTransaction; import com.google.common.base.Preconditions; +import com.google.protobuf.ByteString; /** Implementation of {@link TransactionManager}. */ final class TransactionManagerImpl implements TransactionManager, SessionTransaction { @@ -53,7 +54,7 @@ public void setSpan(ISpan span) { public TransactionContext begin() { Preconditions.checkState(txn == null, "begin can only be called once"); try (IScope s = tracer.withSpan(span)) { - txn = session.newTransaction(options); + txn = session.newTransaction(options, null); session.setActive(this); txnState = TransactionState.STARTED; return txn; @@ -102,7 +103,10 @@ public TransactionContext resetForRetry() { } try (IScope s = tracer.withSpan(span)) { boolean useInlinedBegin = txn.transactionId != null; - txn = session.newTransaction(options); + + ByteString previousAbortedTransactionId = + session.getIsMultiplexed() ? txn.transactionId : null; + txn = session.newTransaction(options, previousAbortedTransactionId); if (!useInlinedBegin) { txn.ensureTxn(); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java index c8bf6dc833b..71f6b07ad1f 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java @@ -93,6 +93,9 @@ static class Builder extends AbstractReadContext.Builder ensureTxnAsync() { private void createTxnAsync(final SettableApiFuture res) { span.addAnnotation("Creating Transaction"); final ApiFuture fut = - session.beginTransactionAsync(options, isRouteToLeader(), getTransactionChannelHint()); + session.beginTransactionAsync( + options, + isRouteToLeader(), + getTransactionChannelHint(), + previousAbortedTransactionId); fut.addListener( () -> { try { @@ -558,7 +573,9 @@ TransactionSelector getTransactionSelector() { } if (tx == null) { return TransactionSelector.newBuilder() - .setBegin(SessionImpl.createReadWriteTransactionOptions(options)) + .setBegin( + SessionImpl.createReadWriteTransactionOptions( + options, previousAbortedTransactionId)) .build(); } else { // Wait for the transaction to come available. The tx.get() call will fail with an @@ -1079,7 +1096,7 @@ public TransactionRunner allowNestedTransaction() { TransactionRunnerImpl(SessionImpl session, TransactionOption... options) { this.session = session; this.options = Options.fromTransactionOptions(options); - this.txn = session.newTransaction(this.options); + this.txn = session.newTransaction(this.options, null); this.tracer = session.getTracer(); } @@ -1117,7 +1134,10 @@ private T runInternal(final TransactionCallable txCallable) { // Do not inline the BeginTransaction during a retry if the initial attempt did not // actually start a transaction. useInlinedBegin = txn.transactionId != null; - txn = session.newTransaction(options); + + ByteString previousAbortedTransactionId = + session.getIsMultiplexed() ? txn.transactionId : null; + txn = session.newTransaction(options, previousAbortedTransactionId); } checkState( isValid, "TransactionRunner has been invalidated by a new operation on the session"); From 4e91fb3c43dc5514ffb19182bc2e786b0e86d452 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 25 Sep 2024 07:17:43 +0000 Subject: [PATCH 02/16] chore(spanner): add unit tests in TransactionManagerImplTest --- .../cloud/spanner/TransactionRunnerImpl.java | 2 +- .../spanner/TransactionManagerImplTest.java | 56 ++++++++++++++++--- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java index 71f6b07ad1f..42f5da9af7a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java @@ -209,7 +209,7 @@ public void removeListener(Runnable listener) { volatile ByteString transactionId; - final ByteString previousAbortedTransactionId; + ByteString previousAbortedTransactionId; private CommitResponse commitResponse; private final Clock clock; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java index c3fcf1c7480..8c5a1766a81 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java @@ -98,7 +98,7 @@ public void setUp() { @Test public void beginCalledTwiceFails() { - when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); assertThat(manager.begin()).isEqualTo(txn); assertThat(manager.getState()).isEqualTo(TransactionState.STARTED); IllegalStateException e = assertThrows(IllegalStateException.class, () -> manager.begin()); @@ -126,7 +126,7 @@ public void resetBeforeBeginFails() { @Test public void transactionRolledBackOnClose() { - when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); when(txn.isAborted()).thenReturn(false); manager.begin(); manager.close(); @@ -135,7 +135,7 @@ public void transactionRolledBackOnClose() { @Test public void commitSucceeds() { - when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); Timestamp commitTimestamp = Timestamp.ofTimeMicroseconds(1); CommitResponse response = new CommitResponse(commitTimestamp); when(txn.getCommitResponse()).thenReturn(response); @@ -147,7 +147,7 @@ public void commitSucceeds() { @Test public void resetAfterSuccessfulCommitFails() { - when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); manager.begin(); manager.commit(); IllegalStateException e = @@ -157,21 +157,21 @@ public void resetAfterSuccessfulCommitFails() { @Test public void resetAfterAbortSucceeds() { - when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); manager.begin(); doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "")).when(txn).commit(); assertThrows(AbortedException.class, () -> manager.commit()); assertEquals(TransactionState.ABORTED, manager.getState()); txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); - when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); assertThat(manager.resetForRetry()).isEqualTo(txn); assertThat(manager.getState()).isEqualTo(TransactionState.STARTED); } @Test public void resetAfterErrorFails() { - when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); manager.begin(); doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.UNKNOWN, "")).when(txn).commit(); SpannerException e = assertThrows(SpannerException.class, () -> manager.commit()); @@ -184,7 +184,7 @@ public void resetAfterErrorFails() { @Test public void rollbackAfterCommitFails() { - when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); manager.begin(); manager.commit(); IllegalStateException e = assertThrows(IllegalStateException.class, () -> manager.rollback()); @@ -193,7 +193,7 @@ public void rollbackAfterCommitFails() { @Test public void commitAfterRollbackFails() { - when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); manager.begin(); manager.rollback(); IllegalStateException e = assertThrows(IllegalStateException.class, () -> manager.commit()); @@ -363,4 +363,42 @@ public void inlineBegin() { assertThat(transactionsStarted.get()).isEqualTo(1); } } + + @Test + public void storePreviousTxnIdOnAbortForMultiplexedSession() { + txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); + final ByteString mockTransactionId = ByteString.copyFromUtf8("mockTransactionId"); + txn.transactionId = mockTransactionId; + when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + manager.begin(); + doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "")).when(txn).commit(); + assertThrows(AbortedException.class, () -> manager.commit()); + assertEquals(TransactionState.ABORTED, manager.getState()); + + txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); + txn.previousAbortedTransactionId = mockTransactionId; + when(session.newTransaction(Options.fromTransactionOptions(), mockTransactionId)) + .thenReturn(txn); + when(session.getIsMultiplexed()).thenReturn(true); + assertThat(manager.resetForRetry()).isEqualTo(txn); + assertThat(manager.getState()).isEqualTo(TransactionState.STARTED); + } + + @Test + public void skipTxnIdStorageOnAbortForRegularSession() { + txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); + final ByteString mockTransactionId = ByteString.copyFromUtf8("mockTransactionId"); + txn.transactionId = mockTransactionId; + when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + manager.begin(); + doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "")).when(txn).commit(); + assertThrows(AbortedException.class, () -> manager.commit()); + assertEquals(TransactionState.ABORTED, manager.getState()); + + txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); + when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + when(session.getIsMultiplexed()).thenReturn(false); + assertThat(manager.resetForRetry()).isEqualTo(txn); + assertThat(manager.getState()).isEqualTo(TransactionState.STARTED); + } } From 27ecea824e705d7cdc5f48094a568c7e52b3bad5 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 25 Sep 2024 08:04:52 +0000 Subject: [PATCH 03/16] chore(spanner): fix tests --- .../java/com/google/cloud/spanner/SessionPoolTest.java | 9 +++++---- .../google/cloud/spanner/TransactionRunnerImplTest.java | 6 ++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java index 998678e4296..c3e8d887ded 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java @@ -1495,9 +1495,10 @@ public void testSessionNotFoundReadWriteTransaction() { .build(); when(closedSession.asyncClose()) .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); - when(closedSession.newTransaction(Options.fromTransactionOptions())) + when(closedSession.newTransaction(eq(Options.fromTransactionOptions()), any())) .thenReturn(closedTransactionContext); - when(closedSession.beginTransactionAsync(any(), eq(true), any())).thenThrow(sessionNotFound); + when(closedSession.beginTransactionAsync(any(), eq(true), any(), any())) + .thenThrow(sessionNotFound); when(closedSession.getTracer()).thenReturn(tracer); TransactionRunnerImpl closedTransactionRunner = new TransactionRunnerImpl(closedSession); closedTransactionRunner.setSpan(span); @@ -1510,9 +1511,9 @@ public void testSessionNotFoundReadWriteTransaction() { when(openSession.getName()) .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-open"); final TransactionContextImpl openTransactionContext = mock(TransactionContextImpl.class); - when(openSession.newTransaction(Options.fromTransactionOptions())) + when(openSession.newTransaction(eq(Options.fromTransactionOptions()), any())) .thenReturn(openTransactionContext); - when(openSession.beginTransactionAsync(any(), eq(true), any())) + when(openSession.beginTransactionAsync(any(), eq(true), any(), any())) .thenReturn(ApiFutures.immediateFuture(ByteString.copyFromUtf8("open-txn"))); when(openSession.getTracer()).thenReturn(tracer); TransactionRunnerImpl openTransactionRunner = new TransactionRunnerImpl(openSession); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java index c647bb3642a..1fd6817ea96 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; @@ -117,7 +118,7 @@ public void setUp() { tracer = new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""), false); firstRun = true; when(session.getErrorHandler()).thenReturn(DefaultErrorHandler.INSTANCE); - when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(txn); + when(session.newTransaction(eq(Options.fromTransactionOptions()), any())).thenReturn(txn); when(session.getTracer()).thenReturn(tracer); when(rpc.executeQuery(Mockito.any(ExecuteSqlRequest.class), Mockito.anyMap(), eq(true))) .thenAnswer( @@ -343,7 +344,8 @@ private long[] batchDmlException(int status) { .setTracer(session.getTracer()) .setSpan(session.getTracer().getCurrentSpan()) .build(); - when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(transaction); + when(session.newTransaction(eq(Options.fromTransactionOptions()), any())) + .thenReturn(transaction); when(session.getName()).thenReturn(SessionId.of("p", "i", "d", "test").getName()); TransactionRunnerImpl runner = new TransactionRunnerImpl(session); runner.setSpan(span); From 66decb6374498fa2500c41b22a1650f3f3c59d59 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 25 Sep 2024 09:34:03 +0000 Subject: [PATCH 04/16] chore(spanner): fix tests --- .../google/cloud/spanner/AsyncTransactionManagerImplTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java index 08d22dd2d67..dd13c39abc8 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java @@ -16,6 +16,8 @@ package com.google.cloud.spanner; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -42,7 +44,7 @@ public void testCommitReturnsCommitStats() { when(oTspan.makeCurrent()).thenReturn(mock(Scope.class)); try (AsyncTransactionManagerImpl manager = new AsyncTransactionManagerImpl(session, span, Options.commitStats())) { - when(session.newTransaction(Options.fromTransactionOptions(Options.commitStats()))) + when(session.newTransaction(eq(Options.fromTransactionOptions(Options.commitStats())), any())) .thenReturn(transaction); when(transaction.ensureTxnAsync()).thenReturn(ApiFutures.immediateFuture(null)); Timestamp commitTimestamp = Timestamp.ofTimeMicroseconds(1); From 64a2e565ad78fe66c81497e5241b2721adc22af2 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 25 Sep 2024 09:40:04 +0000 Subject: [PATCH 05/16] chore(spanner): fix tests --- .../spanner/TransactionManagerImplTest.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java index 8c5a1766a81..35672e9a0e0 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; @@ -98,7 +99,7 @@ public void setUp() { @Test public void beginCalledTwiceFails() { - when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + when(session.newTransaction(eq(Options.fromTransactionOptions()), any())).thenReturn(txn); assertThat(manager.begin()).isEqualTo(txn); assertThat(manager.getState()).isEqualTo(TransactionState.STARTED); IllegalStateException e = assertThrows(IllegalStateException.class, () -> manager.begin()); @@ -126,7 +127,7 @@ public void resetBeforeBeginFails() { @Test public void transactionRolledBackOnClose() { - when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + when(session.newTransaction(eq(Options.fromTransactionOptions()), any())).thenReturn(txn); when(txn.isAborted()).thenReturn(false); manager.begin(); manager.close(); @@ -135,7 +136,7 @@ public void transactionRolledBackOnClose() { @Test public void commitSucceeds() { - when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + when(session.newTransaction(eq(Options.fromTransactionOptions()), any())).thenReturn(txn); Timestamp commitTimestamp = Timestamp.ofTimeMicroseconds(1); CommitResponse response = new CommitResponse(commitTimestamp); when(txn.getCommitResponse()).thenReturn(response); @@ -147,7 +148,7 @@ public void commitSucceeds() { @Test public void resetAfterSuccessfulCommitFails() { - when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + when(session.newTransaction(eq(Options.fromTransactionOptions()), any())).thenReturn(txn); manager.begin(); manager.commit(); IllegalStateException e = @@ -157,21 +158,21 @@ public void resetAfterSuccessfulCommitFails() { @Test public void resetAfterAbortSucceeds() { - when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + when(session.newTransaction(eq(Options.fromTransactionOptions()), any())).thenReturn(txn); manager.begin(); doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "")).when(txn).commit(); assertThrows(AbortedException.class, () -> manager.commit()); assertEquals(TransactionState.ABORTED, manager.getState()); txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); - when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + when(session.newTransaction(eq(Options.fromTransactionOptions()), any())).thenReturn(txn); assertThat(manager.resetForRetry()).isEqualTo(txn); assertThat(manager.getState()).isEqualTo(TransactionState.STARTED); } @Test public void resetAfterErrorFails() { - when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + when(session.newTransaction(eq(Options.fromTransactionOptions()), any())).thenReturn(txn); manager.begin(); doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.UNKNOWN, "")).when(txn).commit(); SpannerException e = assertThrows(SpannerException.class, () -> manager.commit()); @@ -184,7 +185,7 @@ public void resetAfterErrorFails() { @Test public void rollbackAfterCommitFails() { - when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + when(session.newTransaction(eq(Options.fromTransactionOptions()), any())).thenReturn(txn); manager.begin(); manager.commit(); IllegalStateException e = assertThrows(IllegalStateException.class, () -> manager.rollback()); @@ -193,7 +194,7 @@ public void rollbackAfterCommitFails() { @Test public void commitAfterRollbackFails() { - when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + when(session.newTransaction(eq(Options.fromTransactionOptions()), any())).thenReturn(txn); manager.begin(); manager.rollback(); IllegalStateException e = assertThrows(IllegalStateException.class, () -> manager.commit()); From 4035c84d2cff78394375c98ce66ac8b148da931a Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 25 Sep 2024 09:49:51 +0000 Subject: [PATCH 06/16] chore(spanner): add test comments --- .../google/cloud/spanner/TransactionManagerImplTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java index 35672e9a0e0..4c9de15ebd1 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java @@ -365,6 +365,9 @@ public void inlineBegin() { } } + // This test ensures that when a transaction is aborted in a multiplexed session, + // the transaction ID of the aborted transaction is saved during the retry when a new transaction + // is created. @Test public void storePreviousTxnIdOnAbortForMultiplexedSession() { txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); @@ -385,6 +388,9 @@ public void storePreviousTxnIdOnAbortForMultiplexedSession() { assertThat(manager.getState()).isEqualTo(TransactionState.STARTED); } + // This test ensures that when a transaction is aborted in a regular session, + // the transaction ID of the aborted transaction is not saved during the retry when a new + // transaction is created. @Test public void skipTxnIdStorageOnAbortForRegularSession() { txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); From 8cc2b32371c578ec528b53c7339ebac620706520 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Fri, 27 Sep 2024 11:37:33 +0000 Subject: [PATCH 07/16] chore(spanner): refactor code for lock order --- .../spanner/AsyncTransactionManagerImpl.java | 14 +++++++-- .../com/google/cloud/spanner/SessionImpl.java | 16 +++++----- .../cloud/spanner/TransactionManagerImpl.java | 12 +++++-- .../cloud/spanner/TransactionRunnerImpl.java | 31 ++++++++++--------- 4 files changed, 45 insertions(+), 28 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java index 53def8f2531..c0b2bbc2d50 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java @@ -78,9 +78,17 @@ public TransactionContextFutureImpl beginAsync() { private ApiFuture internalBeginAsync(boolean firstAttempt) { txnState = TransactionState.STARTED; - ByteString previousAbortedTransactionId = - !firstAttempt && session.getIsMultiplexed() ? txn.transactionId : null; - txn = session.newTransaction(options, previousAbortedTransactionId); + + // Determine the latest transactionId when using a multiplexed session. + ByteString multiplexedSessionPreviousTransactionId = null; + if (session.getIsMultiplexed() && !firstAttempt) { + // Use the current transactionId if available, otherwise fallback to the previous aborted + // transactionId. + multiplexedSessionPreviousTransactionId = + txn.transactionId != null ? txn.transactionId : txn.previousTransactionId; + } + + txn = session.newTransaction(options, multiplexedSessionPreviousTransactionId); if (firstAttempt) { session.setActive(this); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java index 55c2afbc05a..228b4900927 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java @@ -70,7 +70,7 @@ static void throwIfTransactionsPending() { } static TransactionOptions createReadWriteTransactionOptions( - Options options, ByteString previousAbortedTransactionId) { + Options options, ByteString previousTransactionId) { TransactionOptions.Builder transactionOptions = TransactionOptions.newBuilder(); if (options.withExcludeTxnFromChangeStreams() == Boolean.TRUE) { transactionOptions.setExcludeTxnFromChangeStreams(true); @@ -79,10 +79,10 @@ static TransactionOptions createReadWriteTransactionOptions( if (options.withOptimisticLock() == Boolean.TRUE) { readWrite.setReadLockMode(TransactionOptions.ReadWrite.ReadLockMode.OPTIMISTIC); } - if (previousAbortedTransactionId != null - && previousAbortedTransactionId != com.google.protobuf.ByteString.EMPTY) { + if (previousTransactionId != null + && previousTransactionId != com.google.protobuf.ByteString.EMPTY) { // TODO(sriharshach): uncomment this when multiplexed session R/W proto is published - // readWrite.setMultiplexedSessionPreviousTransactionId(previousAbortedTransactionId); + // readWrite.setMultiplexedSessionPreviousTransactionId(previousTransactionId); } transactionOptions.setReadWrite(readWrite); return transactionOptions.build(); @@ -436,14 +436,14 @@ ApiFuture beginTransactionAsync( Options transactionOptions, boolean routeToLeader, Map channelHint, - ByteString previousAbortedTransactionId) { + ByteString previousTransactionId) { final SettableApiFuture res = SettableApiFuture.create(); final ISpan span = tracer.spanBuilder(SpannerImpl.BEGIN_TRANSACTION); final BeginTransactionRequest request = BeginTransactionRequest.newBuilder() .setSession(getName()) .setOptions( - createReadWriteTransactionOptions(transactionOptions, previousAbortedTransactionId)) + createReadWriteTransactionOptions(transactionOptions, previousTransactionId)) .build(); final ApiFuture requestFuture; try (IScope ignore = tracer.withSpan(span)) { @@ -479,12 +479,12 @@ ApiFuture beginTransactionAsync( return res; } - TransactionContextImpl newTransaction(Options options, ByteString previousAbortedTransactionId) { + TransactionContextImpl newTransaction(Options options, ByteString previousTransactionId) { return TransactionContextImpl.newBuilder() .setSession(this) .setOptions(options) .setTransactionId(null) - .setPreviousAbortedTransactionId(previousAbortedTransactionId) + .setPreviousTransactionId(previousTransactionId) .setOptions(options) .setTrackTransactionStarter(spanner.getOptions().isTrackTransactionStarter()) .setRpc(spanner.getRpc()) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java index ad71c852820..c0d4cf3d607 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java @@ -104,9 +104,15 @@ public TransactionContext resetForRetry() { try (IScope s = tracer.withSpan(span)) { boolean useInlinedBegin = txn.transactionId != null; - ByteString previousAbortedTransactionId = - session.getIsMultiplexed() ? txn.transactionId : null; - txn = session.newTransaction(options, previousAbortedTransactionId); + // Determine the latest transactionId when using a multiplexed session. + ByteString multiplexedSessionPreviousTransactionId = null; + if (session.getIsMultiplexed()) { + // Use the current transactionId if available, otherwise fallback to the previous aborted + // transactionId. + multiplexedSessionPreviousTransactionId = + txn.transactionId != null ? txn.transactionId : txn.previousTransactionId; + } + txn = session.newTransaction(options, multiplexedSessionPreviousTransactionId); if (!useInlinedBegin) { txn.ensureTxn(); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java index 42f5da9af7a..31cefb881ec 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java @@ -95,7 +95,7 @@ static class Builder extends AbstractReadContext.Builder res) { span.addAnnotation("Creating Transaction"); final ApiFuture fut = session.beginTransactionAsync( - options, - isRouteToLeader(), - getTransactionChannelHint(), - previousAbortedTransactionId); + options, isRouteToLeader(), getTransactionChannelHint(), previousTransactionId); fut.addListener( () -> { try { @@ -574,8 +571,7 @@ TransactionSelector getTransactionSelector() { if (tx == null) { return TransactionSelector.newBuilder() .setBegin( - SessionImpl.createReadWriteTransactionOptions( - options, previousAbortedTransactionId)) + SessionImpl.createReadWriteTransactionOptions(options, previousTransactionId)) .build(); } else { // Wait for the transaction to come available. The tx.get() call will fail with an @@ -1135,9 +1131,16 @@ private T runInternal(final TransactionCallable txCallable) { // actually start a transaction. useInlinedBegin = txn.transactionId != null; - ByteString previousAbortedTransactionId = - session.getIsMultiplexed() ? txn.transactionId : null; - txn = session.newTransaction(options, previousAbortedTransactionId); + // Determine the latest transactionId when using a multiplexed session. + ByteString multiplexedSessionPreviousTransactionId = null; + if (session.getIsMultiplexed()) { + // Use the current transactionId if available, otherwise fallback to the previous + // transactionId. + multiplexedSessionPreviousTransactionId = + txn.transactionId != null ? txn.transactionId : txn.previousTransactionId; + } + + txn = session.newTransaction(options, multiplexedSessionPreviousTransactionId); } checkState( isValid, "TransactionRunner has been invalidated by a new operation on the session"); From 136d31e3508f871c22f65bbfa1d4fac86780c473 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Fri, 27 Sep 2024 11:39:05 +0000 Subject: [PATCH 08/16] chore(spanner): update test cases --- .../AsyncTransactionManagerImplTest.java | 65 +++++++++++ ...edSessionDatabaseClientMockServerTest.java | 109 ++++++++++++++++++ .../spanner/TransactionManagerImplTest.java | 20 +++- 3 files changed, 189 insertions(+), 5 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java index dd13c39abc8..4df5b6e9bad 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java @@ -16,14 +16,18 @@ package com.google.cloud.spanner; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.api.core.ApiFutures; import com.google.cloud.Timestamp; +import com.google.protobuf.ByteString; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Scope; import org.junit.Test; @@ -56,4 +60,65 @@ public void testCommitReturnsCommitStats() { verify(transaction).commitAsync(); } } + + @Test + public void testRetryUsesPreviousTransactionIdOnMultiplexedSession() { + // Set up mock transaction IDs + final ByteString mockTransactionId = ByteString.copyFromUtf8("mockTransactionId"); + final ByteString mockPreviousTransactionId = + ByteString.copyFromUtf8("mockPreviousTransactionId"); + + Span oTspan = mock(Span.class); + ISpan span = new OpenTelemetrySpan(oTspan); + when(oTspan.makeCurrent()).thenReturn(mock(Scope.class)); + // Mark the session as multiplexed. + when(session.getIsMultiplexed()).thenReturn(true); + + // Initialize a mock transaction with transactionId = null, previousTransactionId = null. + transaction = mock(TransactionRunnerImpl.TransactionContextImpl.class); + when(transaction.ensureTxnAsync()).thenReturn(ApiFutures.immediateFuture(null)); + when(session.newTransaction(eq(Options.fromTransactionOptions(Options.commitStats())), any())) + .thenReturn(transaction); + + // Simulate an ABORTED error being thrown when `commitAsync()` is called. + doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "")) + .when(transaction) + .commitAsync(); + + try (AsyncTransactionManagerImpl manager = + new AsyncTransactionManagerImpl(session, span, Options.commitStats())) { + manager.beginAsync(); + + // Verify that for the first transaction attempt, the `previousTransactionId` is null. + // This is because no transaction has been previously aborted at this point. + verify(session).newTransaction(Options.fromTransactionOptions(Options.commitStats()), null); + assertThrows(AbortedException.class, manager::commitAsync); + clearInvocations(session); + + // Mock the transaction object to contain transactionID=null and + // previousTransactionId=mockPreviousTransactionId + transaction.previousTransactionId = mockPreviousTransactionId; + manager.resetForRetryAsync(); + // Verify that in the first retry attempt, the `previousTransactionId` is passed to the new + // transaction. + // This allows Spanner to retry the transaction using the ID of the aborted transaction. + verify(session) + .newTransaction( + Options.fromTransactionOptions(Options.commitStats()), mockPreviousTransactionId); + assertThrows(AbortedException.class, manager::commitAsync); + clearInvocations(session); + + // Mock the transaction object to contain transactionID=mockTransactionId and + // previousTransactionId=mockPreviousTransactionId and transactionID = null + transaction.transactionId = mockTransactionId; + manager.resetForRetryAsync(); + // Verify that the current `transactionId` is used in the retry. + // This ensures the retry logic is working as expected with the latest transaction ID. + verify(session) + .newTransaction(Options.fromTransactionOptions(Options.commitStats()), mockTransactionId); + + when(transaction.rollbackAsync()).thenReturn(ApiFutures.immediateFuture(null)); + manager.closeAsync(); + } + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java index b6dff424079..243c50bd450 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java @@ -16,6 +16,9 @@ package com.google.cloud.spanner; +import static com.google.cloud.spanner.MockSpannerTestUtil.INVALID_UPDATE_STATEMENT; +import static com.google.cloud.spanner.MockSpannerTestUtil.UPDATE_COUNT; +import static com.google.cloud.spanner.MockSpannerTestUtil.UPDATE_STATEMENT; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -58,6 +61,12 @@ public class MultiplexedSessionDatabaseClientMockServerTest extends AbstractMock public static void setupResults() { mockSpanner.putStatementResults( StatementResult.query(STATEMENT, new RandomResultSetGenerator(1).generate())); + + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + mockSpanner.putStatementResult( + StatementResult.exception( + INVALID_UPDATE_STATEMENT, + Status.INVALID_ARGUMENT.withDescription("invalid statement").asRuntimeException())); } @Before @@ -467,6 +476,106 @@ public void testWriteAtLeastOnceWithExcludeTxnFromChangeStreams() { assertEquals(1L, client.multiplexedSessionDatabaseClient.getNumSessionsReleased().get()); } + // TODO(sriharshach): Uncomment test once Lock order preservation proto is published + /* + @Test + public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() throws InterruptedException { + DatabaseClientImpl client = + (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared + // after the first call, so the retry should succeed. + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofException( + mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); + Thread.sleep(10000); + TransactionRunner runner = client.readWriteTransaction(); + runner.run( + transaction -> { + try (ResultSet resultSet = + transaction.executeQuery(STATEMENT)) { + while (resultSet.next()) {} + } + return null; + }); + + List executeSqlRequests = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class); + assertEquals(2, executeSqlRequests.size()); + + // Verify the requests are executed using multiplexed sessions + for (ExecuteSqlRequest request : executeSqlRequests) { + assertTrue(mockSpanner.getSession(request.getSession()).getMultiplexed()); + } + + // Verify that the first request uses inline begin, and the previous transaction ID is set to ByteString.EMPTY + assertTrue(executeSqlRequests.get(0).hasTransaction()); + assertTrue(executeSqlRequests.get(0).getTransaction().hasBegin()); + assertTrue(executeSqlRequests.get(0).getTransaction().getBegin().hasReadWrite()); + assertNotNull(executeSqlRequests.get(0).getTransaction().getBegin().getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertThat(executeSqlRequests.get(0).getTransaction().getBegin().getReadWrite().getMultiplexedSessionPreviousTransactionId()).isEqualTo(ByteString.EMPTY); + + // Verify that the second request uses inline begin, and the previous transaction ID is set appropriately + assertTrue(executeSqlRequests.get(1).hasTransaction()); + assertTrue(executeSqlRequests.get(1).getTransaction().hasBegin()); + assertTrue(executeSqlRequests.get(1).getTransaction().getBegin().hasReadWrite()); + assertNotNull(executeSqlRequests.get(1).getTransaction().getBegin().getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertThat(executeSqlRequests.get(1).getTransaction().getBegin().getReadWrite().getMultiplexedSessionPreviousTransactionId()).isNotEqualTo(ByteString.EMPTY); + } + */ + + // TODO(sriharshach): Uncomment test once Lock order preservation proto is published + /* + @Test + public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithExplicitBegin() throws InterruptedException { + DatabaseClientImpl client = + (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared + // after the first call, so the retry should succeed. + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofException( + mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); + Thread.sleep(10000); + TransactionRunner runner = client.readWriteTransaction(); + Long updateCount = runner.run( + transaction -> { + // This update statement carries the BeginTransaction, but fails. This will + // cause the entire transaction to be retried with an explicit + // BeginTransaction RPC to ensure all statements in the transaction are + // actually executed against the same transaction. + SpannerException e = + assertThrows( + SpannerException.class, + () -> transaction.executeUpdate(INVALID_UPDATE_STATEMENT)); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + return transaction.executeUpdate(UPDATE_STATEMENT); + }); + + assertThat(updateCount).isEqualTo(1L); + List beginTransactionRequests = mockSpanner.getRequestsOfType(BeginTransactionRequest.class); + assertEquals(2, beginTransactionRequests.size()); + + // Verify the requests are executed using multiplexed sessions + for (BeginTransactionRequest request : beginTransactionRequests) { + assertTrue(mockSpanner.getSession(request.getSession()).getMultiplexed()); + } + + // Verify that explicit begin transaction is called during retry, and the previous transaction ID is set to ByteString.EMPTY + assertTrue(beginTransactionRequests.get(0).hasOptions()); + assertTrue(beginTransactionRequests.get(0).getOptions().hasReadWrite()); + assertNotNull(beginTransactionRequests.get(0).getOptions().getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertThat(beginTransactionRequests.get(0).getOptions().getReadWrite().getMultiplexedSessionPreviousTransactionId()).isEqualTo(ByteString.EMPTY); + + // The previous transaction with id (txn1) fails during commit operation with ABORTED error. + // Verify that explicit begin transaction is called during retry, and the previous transaction ID is not ByteString.EMPTY (should be set to txn1) + assertTrue(beginTransactionRequests.get(1).hasOptions()); + assertTrue(beginTransactionRequests.get(1).getOptions().hasReadWrite()); + assertNotNull(beginTransactionRequests.get(1).getOptions().getReadWrite().getMultiplexedSessionPreviousTransactionId()); + assertThat(beginTransactionRequests.get(1).getOptions().getReadWrite().getMultiplexedSessionPreviousTransactionId()).isNotEqualTo(ByteString.EMPTY); + } + */ + private void waitForSessionToBeReplaced(DatabaseClientImpl client) { assertNotNull(client.multiplexedSessionDatabaseClient); SessionReference sessionReference = diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java index 4c9de15ebd1..76a5ecb4905 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; @@ -375,17 +376,22 @@ public void storePreviousTxnIdOnAbortForMultiplexedSession() { txn.transactionId = mockTransactionId; when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); manager.begin(); + // Verify that for the first transaction attempt, the `previousTransactionId` is null. + // This is because no transaction has been previously aborted at this point. + verify(session).newTransaction(Options.fromTransactionOptions(), null); doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "")).when(txn).commit(); assertThrows(AbortedException.class, () -> manager.commit()); - assertEquals(TransactionState.ABORTED, manager.getState()); txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); - txn.previousAbortedTransactionId = mockTransactionId; + txn.previousTransactionId = mockTransactionId; when(session.newTransaction(Options.fromTransactionOptions(), mockTransactionId)) .thenReturn(txn); when(session.getIsMultiplexed()).thenReturn(true); assertThat(manager.resetForRetry()).isEqualTo(txn); - assertThat(manager.getState()).isEqualTo(TransactionState.STARTED); + // Verify that in the first retry attempt, the `previousTransactionId` is passed to the new + // transaction. + // This allows Spanner to retry the transaction using the ID of the aborted transaction. + verify(session).newTransaction(Options.fromTransactionOptions(), mockTransactionId); } // This test ensures that when a transaction is aborted in a regular session, @@ -398,14 +404,18 @@ public void skipTxnIdStorageOnAbortForRegularSession() { txn.transactionId = mockTransactionId; when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); manager.begin(); + verify(session).newTransaction(Options.fromTransactionOptions(), null); doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "")).when(txn).commit(); assertThrows(AbortedException.class, () -> manager.commit()); - assertEquals(TransactionState.ABORTED, manager.getState()); + clearInvocations(session); txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); when(session.getIsMultiplexed()).thenReturn(false); assertThat(manager.resetForRetry()).isEqualTo(txn); - assertThat(manager.getState()).isEqualTo(TransactionState.STARTED); + // Verify that in the first retry attempt, the `previousTransactionId` is not passed to the new + // transaction + // in case of regular sessions. + verify(session).newTransaction(Options.fromTransactionOptions(), null); } } From 9e42dd737e73aa0e252d9976bc60259b1d1b7d14 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Thu, 3 Oct 2024 05:59:42 +0000 Subject: [PATCH 09/16] chore(spanner): update default value of prevTransactionId to ByteString.EMPTY --- .../google/cloud/spanner/AsyncTransactionManagerImpl.java | 8 +++++--- .../com/google/cloud/spanner/TransactionManagerImpl.java | 8 +++++--- .../com/google/cloud/spanner/TransactionRunnerImpl.java | 8 +++++--- .../cloud/spanner/AsyncTransactionManagerImplTest.java | 6 +++--- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java index c0b2bbc2d50..40fc535968c 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java @@ -80,15 +80,17 @@ private ApiFuture internalBeginAsync(boolean firstAttempt) { txnState = TransactionState.STARTED; // Determine the latest transactionId when using a multiplexed session. - ByteString multiplexedSessionPreviousTransactionId = null; - if (session.getIsMultiplexed() && !firstAttempt) { + ByteString multiplexedSessionPreviousTransactionId = ByteString.EMPTY; + if (txn != null && session.getIsMultiplexed() && !firstAttempt) { // Use the current transactionId if available, otherwise fallback to the previous aborted // transactionId. multiplexedSessionPreviousTransactionId = txn.transactionId != null ? txn.transactionId : txn.previousTransactionId; } - txn = session.newTransaction(options, multiplexedSessionPreviousTransactionId); + txn = + session.newTransaction( + options, /* previousTransactionId = */ multiplexedSessionPreviousTransactionId); if (firstAttempt) { session.setActive(this); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java index c0d4cf3d607..8bdcc7a2f66 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java @@ -54,7 +54,7 @@ public void setSpan(ISpan span) { public TransactionContext begin() { Preconditions.checkState(txn == null, "begin can only be called once"); try (IScope s = tracer.withSpan(span)) { - txn = session.newTransaction(options, null); + txn = session.newTransaction(options, /* previousTransactionId = */ ByteString.EMPTY); session.setActive(this); txnState = TransactionState.STARTED; return txn; @@ -105,14 +105,16 @@ public TransactionContext resetForRetry() { boolean useInlinedBegin = txn.transactionId != null; // Determine the latest transactionId when using a multiplexed session. - ByteString multiplexedSessionPreviousTransactionId = null; + ByteString multiplexedSessionPreviousTransactionId = ByteString.EMPTY; if (session.getIsMultiplexed()) { // Use the current transactionId if available, otherwise fallback to the previous aborted // transactionId. multiplexedSessionPreviousTransactionId = txn.transactionId != null ? txn.transactionId : txn.previousTransactionId; } - txn = session.newTransaction(options, multiplexedSessionPreviousTransactionId); + txn = + session.newTransaction( + options, /* previousTransactionId = */ multiplexedSessionPreviousTransactionId); if (!useInlinedBegin) { txn.ensureTxn(); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java index 31cefb881ec..06f55ccf91a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java @@ -1092,7 +1092,7 @@ public TransactionRunner allowNestedTransaction() { TransactionRunnerImpl(SessionImpl session, TransactionOption... options) { this.session = session; this.options = Options.fromTransactionOptions(options); - this.txn = session.newTransaction(this.options, null); + this.txn = session.newTransaction(this.options, /* previousTransactionId = */ ByteString.EMPTY); this.tracer = session.getTracer(); } @@ -1132,7 +1132,7 @@ private T runInternal(final TransactionCallable txCallable) { useInlinedBegin = txn.transactionId != null; // Determine the latest transactionId when using a multiplexed session. - ByteString multiplexedSessionPreviousTransactionId = null; + ByteString multiplexedSessionPreviousTransactionId = ByteString.EMPTY; if (session.getIsMultiplexed()) { // Use the current transactionId if available, otherwise fallback to the previous // transactionId. @@ -1140,7 +1140,9 @@ private T runInternal(final TransactionCallable txCallable) { txn.transactionId != null ? txn.transactionId : txn.previousTransactionId; } - txn = session.newTransaction(options, multiplexedSessionPreviousTransactionId); + txn = + session.newTransaction( + options, /* previousTransactionId = */ multiplexedSessionPreviousTransactionId); } checkState( isValid, "TransactionRunner has been invalidated by a new operation on the session"); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java index 4df5b6e9bad..3bc9e0436ce 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java @@ -99,8 +99,8 @@ public void testRetryUsesPreviousTransactionIdOnMultiplexedSession() { // previousTransactionId=mockPreviousTransactionId transaction.previousTransactionId = mockPreviousTransactionId; manager.resetForRetryAsync(); - // Verify that in the first retry attempt, the `previousTransactionId` is passed to the new - // transaction. + // Verify that in the first retry attempt, the `previousTransactionId` + // (mockPreviousTransactionId) is passed to the new transaction. // This allows Spanner to retry the transaction using the ID of the aborted transaction. verify(session) .newTransaction( @@ -112,7 +112,7 @@ public void testRetryUsesPreviousTransactionIdOnMultiplexedSession() { // previousTransactionId=mockPreviousTransactionId and transactionID = null transaction.transactionId = mockTransactionId; manager.resetForRetryAsync(); - // Verify that the current `transactionId` is used in the retry. + // Verify that the latest `transactionId` (mockTransactionId) is used in the retry. // This ensures the retry logic is working as expected with the latest transaction ID. verify(session) .newTransaction(Options.fromTransactionOptions(Options.commitStats()), mockTransactionId); From 403f379a5f0cdce31f4db0e8ad4c1c9c1f3c6e27 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Thu, 3 Oct 2024 06:47:40 +0000 Subject: [PATCH 10/16] chore(spanner): update test --- ...edSessionDatabaseClientMockServerTest.java | 138 ++++++++++++------ 1 file changed, 90 insertions(+), 48 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java index 243c50bd450..0c50439d0a1 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java @@ -32,10 +32,12 @@ import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; import com.google.cloud.spanner.Options.RpcPriority; +import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.cloud.spanner.connection.RandomResultSetGenerator; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; +import com.google.spanner.v1.BeginTransactionRequest; import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.RequestOptions.Priority; @@ -46,6 +48,7 @@ import java.util.List; import java.util.Set; import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import org.junit.Before; import org.junit.BeforeClass; @@ -526,55 +529,94 @@ public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() thr // TODO(sriharshach): Uncomment test once Lock order preservation proto is published /* - @Test - public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithExplicitBegin() throws InterruptedException { - DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); - // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared - // after the first call, so the retry should succeed. - mockSpanner.setCommitExecutionTime( - SimulatedExecutionTime.ofException( - mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); - Thread.sleep(10000); - TransactionRunner runner = client.readWriteTransaction(); - Long updateCount = runner.run( - transaction -> { - // This update statement carries the BeginTransaction, but fails. This will - // cause the entire transaction to be retried with an explicit - // BeginTransaction RPC to ensure all statements in the transaction are - // actually executed against the same transaction. - SpannerException e = - assertThrows( - SpannerException.class, - () -> transaction.executeUpdate(INVALID_UPDATE_STATEMENT)); - assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); - return transaction.executeUpdate(UPDATE_STATEMENT); - }); - - assertThat(updateCount).isEqualTo(1L); - List beginTransactionRequests = mockSpanner.getRequestsOfType(BeginTransactionRequest.class); - assertEquals(2, beginTransactionRequests.size()); - - // Verify the requests are executed using multiplexed sessions - for (BeginTransactionRequest request : beginTransactionRequests) { - assertTrue(mockSpanner.getSession(request.getSession()).getMultiplexed()); - } - - // Verify that explicit begin transaction is called during retry, and the previous transaction ID is set to ByteString.EMPTY - assertTrue(beginTransactionRequests.get(0).hasOptions()); - assertTrue(beginTransactionRequests.get(0).getOptions().hasReadWrite()); - assertNotNull(beginTransactionRequests.get(0).getOptions().getReadWrite() - .getMultiplexedSessionPreviousTransactionId()); - assertThat(beginTransactionRequests.get(0).getOptions().getReadWrite().getMultiplexedSessionPreviousTransactionId()).isEqualTo(ByteString.EMPTY); - - // The previous transaction with id (txn1) fails during commit operation with ABORTED error. - // Verify that explicit begin transaction is called during retry, and the previous transaction ID is not ByteString.EMPTY (should be set to txn1) - assertTrue(beginTransactionRequests.get(1).hasOptions()); - assertTrue(beginTransactionRequests.get(1).getOptions().hasReadWrite()); - assertNotNull(beginTransactionRequests.get(1).getOptions().getReadWrite().getMultiplexedSessionPreviousTransactionId()); - assertThat(beginTransactionRequests.get(1).getOptions().getReadWrite().getMultiplexedSessionPreviousTransactionId()).isNotEqualTo(ByteString.EMPTY); + @Test + public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithExplicitBegin() + throws InterruptedException { + DatabaseClientImpl client = + (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared + // after the first call, so the retry should succeed. + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofException( + mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); + Thread.sleep(10000); + TransactionRunner runner = client.readWriteTransaction(); + AtomicReference validTransactionId = new AtomicReference<>(); + Long updateCount = + runner.run( + transaction -> { + // This update statement carries the BeginTransaction, but fails. This will + // cause the entire transaction to be retried with an explicit + // BeginTransaction RPC to ensure all statements in the transaction are + // actually executed against the same transaction. + TransactionContextImpl impl = (TransactionContextImpl) transaction; + if (validTransactionId.get() == null) { + // Track the first not-null transactionId. This transaction gets ABORTED during + // commit operation and gets retried. + validTransactionId.set(impl.transactionId); + } + SpannerException e = + assertThrows( + SpannerException.class, + () -> transaction.executeUpdate(INVALID_UPDATE_STATEMENT)); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + return transaction.executeUpdate(UPDATE_STATEMENT); + }); + + assertThat(updateCount).isEqualTo(1L); + List beginTransactionRequests = + mockSpanner.getRequestsOfType(BeginTransactionRequest.class); + assertEquals(2, beginTransactionRequests.size()); + + // Verify the requests are executed using multiplexed sessions + for (BeginTransactionRequest request : beginTransactionRequests) { + assertTrue(mockSpanner.getSession(request.getSession()).getMultiplexed()); } - */ + + // Verify that explicit begin transaction is called during retry, and the previous transaction + // ID is set to ByteString.EMPTY + assertTrue(beginTransactionRequests.get(0).hasOptions()); + assertTrue(beginTransactionRequests.get(0).getOptions().hasReadWrite()); + assertNotNull( + beginTransactionRequests + .get(0) + .getOptions() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertEquals( + ByteString.EMPTY, + beginTransactionRequests + .get(0) + .getOptions() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + + // The previous transaction with id (txn1) fails during commit operation with ABORTED error. + // Verify that explicit begin transaction is called during retry, and the previous transaction + // ID is not ByteString.EMPTY (should be set to txn1) + assertTrue(beginTransactionRequests.get(1).hasOptions()); + assertTrue(beginTransactionRequests.get(1).getOptions().hasReadWrite()); + assertNotNull( + beginTransactionRequests + .get(1) + .getOptions() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertNotEquals( + ByteString.EMPTY, + beginTransactionRequests + .get(1) + .getOptions() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertEquals( + validTransactionId.get(), + beginTransactionRequests + .get(1) + .getOptions() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + }*/ private void waitForSessionToBeReplaced(DatabaseClientImpl client) { assertNotNull(client.multiplexedSessionDatabaseClient); From 6101800ff22a274b230af8f2c381d968d4c89b7d Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Thu, 3 Oct 2024 07:04:00 +0000 Subject: [PATCH 11/16] chore(spanner): update test --- ...edSessionDatabaseClientMockServerTest.java | 132 ++++++++++++------ 1 file changed, 87 insertions(+), 45 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java index 0c50439d0a1..578dfbc957e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java @@ -37,7 +37,6 @@ import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; -import com.google.spanner.v1.BeginTransactionRequest; import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.RequestOptions.Priority; @@ -480,53 +479,96 @@ public void testWriteAtLeastOnceWithExcludeTxnFromChangeStreams() { } // TODO(sriharshach): Uncomment test once Lock order preservation proto is published - /* - @Test - public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() throws InterruptedException { - DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); - // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared - // after the first call, so the retry should succeed. - mockSpanner.setCommitExecutionTime( - SimulatedExecutionTime.ofException( - mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); - Thread.sleep(10000); - TransactionRunner runner = client.readWriteTransaction(); - runner.run( - transaction -> { - try (ResultSet resultSet = - transaction.executeQuery(STATEMENT)) { - while (resultSet.next()) {} - } - return null; - }); - - List executeSqlRequests = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class); - assertEquals(2, executeSqlRequests.size()); - - // Verify the requests are executed using multiplexed sessions - for (ExecuteSqlRequest request : executeSqlRequests) { - assertTrue(mockSpanner.getSession(request.getSession()).getMultiplexed()); - } +/* + @Test + public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() + throws InterruptedException { + DatabaseClientImpl client = + (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared + // after the first call, so the retry should succeed. + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofException( + mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); + Thread.sleep(10000); + TransactionRunner runner = client.readWriteTransaction(); + AtomicReference validTransactionId = new AtomicReference<>(); + runner.run( + transaction -> { + try (ResultSet resultSet = transaction.executeQuery(STATEMENT)) { + while (resultSet.next()) {} + } + + TransactionContextImpl impl = (TransactionContextImpl) transaction; + if (validTransactionId.get() == null) { + // Track the first not-null transactionId. This transaction gets ABORTED during commit + // operation and gets retried. + validTransactionId.set(impl.transactionId); + } + return null; + }); + + List executeSqlRequests = + mockSpanner.getRequestsOfType(ExecuteSqlRequest.class); + assertEquals(2, executeSqlRequests.size()); - // Verify that the first request uses inline begin, and the previous transaction ID is set to ByteString.EMPTY - assertTrue(executeSqlRequests.get(0).hasTransaction()); - assertTrue(executeSqlRequests.get(0).getTransaction().hasBegin()); - assertTrue(executeSqlRequests.get(0).getTransaction().getBegin().hasReadWrite()); - assertNotNull(executeSqlRequests.get(0).getTransaction().getBegin().getReadWrite() - .getMultiplexedSessionPreviousTransactionId()); - assertThat(executeSqlRequests.get(0).getTransaction().getBegin().getReadWrite().getMultiplexedSessionPreviousTransactionId()).isEqualTo(ByteString.EMPTY); - - // Verify that the second request uses inline begin, and the previous transaction ID is set appropriately - assertTrue(executeSqlRequests.get(1).hasTransaction()); - assertTrue(executeSqlRequests.get(1).getTransaction().hasBegin()); - assertTrue(executeSqlRequests.get(1).getTransaction().getBegin().hasReadWrite()); - assertNotNull(executeSqlRequests.get(1).getTransaction().getBegin().getReadWrite() - .getMultiplexedSessionPreviousTransactionId()); - assertThat(executeSqlRequests.get(1).getTransaction().getBegin().getReadWrite().getMultiplexedSessionPreviousTransactionId()).isNotEqualTo(ByteString.EMPTY); + // Verify the requests are executed using multiplexed sessions + for (ExecuteSqlRequest request : executeSqlRequests) { + assertTrue(mockSpanner.getSession(request.getSession()).getMultiplexed()); } - */ + // Verify that the first request uses inline begin, and the previous transaction ID is set to + // ByteString.EMPTY + assertTrue(executeSqlRequests.get(0).hasTransaction()); + assertTrue(executeSqlRequests.get(0).getTransaction().hasBegin()); + assertTrue(executeSqlRequests.get(0).getTransaction().getBegin().hasReadWrite()); + assertNotNull( + executeSqlRequests + .get(0) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertEquals( + ByteString.EMPTY, + executeSqlRequests + .get(0) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + + // Verify that the second request uses inline begin, and the previous transaction ID is set + // appropriately + assertTrue(executeSqlRequests.get(1).hasTransaction()); + assertTrue(executeSqlRequests.get(1).getTransaction().hasBegin()); + assertTrue(executeSqlRequests.get(1).getTransaction().getBegin().hasReadWrite()); + assertNotNull( + executeSqlRequests + .get(1) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertNotEquals( + ByteString.EMPTY, + executeSqlRequests + .get(1) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertEquals( + validTransactionId.get(), + executeSqlRequests + .get(1) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + } +*/ + // TODO(sriharshach): Uncomment test once Lock order preservation proto is published /* @Test From 3cb1a592fc894bce37f4a8613d3cb1b44d8b7ece Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Thu, 3 Oct 2024 07:26:06 +0000 Subject: [PATCH 12/16] chore(spanner): fix tests --- .../spanner/AsyncTransactionManagerImplTest.java | 4 ++-- ...iplexedSessionDatabaseClientMockServerTest.java | 2 +- .../cloud/spanner/TransactionManagerImplTest.java | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java index 3bc9e0436ce..e4d4ce138b2 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java @@ -89,9 +89,9 @@ public void testRetryUsesPreviousTransactionIdOnMultiplexedSession() { new AsyncTransactionManagerImpl(session, span, Options.commitStats())) { manager.beginAsync(); - // Verify that for the first transaction attempt, the `previousTransactionId` is null. + // Verify that for the first transaction attempt, the `previousTransactionId` is ByteString.EMPTY. // This is because no transaction has been previously aborted at this point. - verify(session).newTransaction(Options.fromTransactionOptions(Options.commitStats()), null); + verify(session).newTransaction(Options.fromTransactionOptions(Options.commitStats()), ByteString.EMPTY); assertThrows(AbortedException.class, manager::commitAsync); clearInvocations(session); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java index 578dfbc957e..84549fe2cc9 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java @@ -568,7 +568,7 @@ public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() .getMultiplexedSessionPreviousTransactionId()); } */ - + // TODO(sriharshach): Uncomment test once Lock order preservation proto is published /* @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java index 76a5ecb4905..b1b8c10eda9 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java @@ -374,11 +374,11 @@ public void storePreviousTxnIdOnAbortForMultiplexedSession() { txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); final ByteString mockTransactionId = ByteString.copyFromUtf8("mockTransactionId"); txn.transactionId = mockTransactionId; - when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY)).thenReturn(txn); manager.begin(); - // Verify that for the first transaction attempt, the `previousTransactionId` is null. + // Verify that for the first transaction attempt, the `previousTransactionId` is ByteString.EMPTY. // This is because no transaction has been previously aborted at this point. - verify(session).newTransaction(Options.fromTransactionOptions(), null); + verify(session).newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY); doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "")).when(txn).commit(); assertThrows(AbortedException.class, () -> manager.commit()); @@ -402,20 +402,20 @@ public void skipTxnIdStorageOnAbortForRegularSession() { txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); final ByteString mockTransactionId = ByteString.copyFromUtf8("mockTransactionId"); txn.transactionId = mockTransactionId; - when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY)).thenReturn(txn); manager.begin(); - verify(session).newTransaction(Options.fromTransactionOptions(), null); + verify(session).newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY); doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "")).when(txn).commit(); assertThrows(AbortedException.class, () -> manager.commit()); clearInvocations(session); txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); - when(session.newTransaction(Options.fromTransactionOptions(), null)).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY)).thenReturn(txn); when(session.getIsMultiplexed()).thenReturn(false); assertThat(manager.resetForRetry()).isEqualTo(txn); // Verify that in the first retry attempt, the `previousTransactionId` is not passed to the new // transaction // in case of regular sessions. - verify(session).newTransaction(Options.fromTransactionOptions(), null); + verify(session).newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY); } } From 2f0c6adc3aad6e5ad7ab9af952385b507bc1d8af Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Thu, 3 Oct 2024 07:30:24 +0000 Subject: [PATCH 13/16] chore(spanner): lint --- .../AsyncTransactionManagerImplTest.java | 6 +- ...edSessionDatabaseClientMockServerTest.java | 176 +++++++++--------- .../spanner/TransactionManagerImplTest.java | 12 +- 3 files changed, 99 insertions(+), 95 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java index e4d4ce138b2..76bac4b6ca7 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java @@ -89,9 +89,11 @@ public void testRetryUsesPreviousTransactionIdOnMultiplexedSession() { new AsyncTransactionManagerImpl(session, span, Options.commitStats())) { manager.beginAsync(); - // Verify that for the first transaction attempt, the `previousTransactionId` is ByteString.EMPTY. + // Verify that for the first transaction attempt, the `previousTransactionId` is + // ByteString.EMPTY. // This is because no transaction has been previously aborted at this point. - verify(session).newTransaction(Options.fromTransactionOptions(Options.commitStats()), ByteString.EMPTY); + verify(session) + .newTransaction(Options.fromTransactionOptions(Options.commitStats()), ByteString.EMPTY); assertThrows(AbortedException.class, manager::commitAsync); clearInvocations(session); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java index 84549fe2cc9..ae5d246b678 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java @@ -32,7 +32,6 @@ import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; import com.google.cloud.spanner.Options.RpcPriority; -import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.cloud.spanner.connection.RandomResultSetGenerator; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; @@ -47,7 +46,6 @@ import java.util.List; import java.util.Set; import java.util.UUID; -import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import org.junit.Before; import org.junit.BeforeClass; @@ -479,95 +477,95 @@ public void testWriteAtLeastOnceWithExcludeTxnFromChangeStreams() { } // TODO(sriharshach): Uncomment test once Lock order preservation proto is published -/* - @Test - public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() - throws InterruptedException { - DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); - // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared - // after the first call, so the retry should succeed. - mockSpanner.setCommitExecutionTime( - SimulatedExecutionTime.ofException( - mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); - Thread.sleep(10000); - TransactionRunner runner = client.readWriteTransaction(); - AtomicReference validTransactionId = new AtomicReference<>(); - runner.run( - transaction -> { - try (ResultSet resultSet = transaction.executeQuery(STATEMENT)) { - while (resultSet.next()) {} - } - - TransactionContextImpl impl = (TransactionContextImpl) transaction; - if (validTransactionId.get() == null) { - // Track the first not-null transactionId. This transaction gets ABORTED during commit - // operation and gets retried. - validTransactionId.set(impl.transactionId); - } - return null; - }); - - List executeSqlRequests = - mockSpanner.getRequestsOfType(ExecuteSqlRequest.class); - assertEquals(2, executeSqlRequests.size()); + /* + @Test + public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() + throws InterruptedException { + DatabaseClientImpl client = + (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared + // after the first call, so the retry should succeed. + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofException( + mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); + Thread.sleep(10000); + TransactionRunner runner = client.readWriteTransaction(); + AtomicReference validTransactionId = new AtomicReference<>(); + runner.run( + transaction -> { + try (ResultSet resultSet = transaction.executeQuery(STATEMENT)) { + while (resultSet.next()) {} + } + + TransactionContextImpl impl = (TransactionContextImpl) transaction; + if (validTransactionId.get() == null) { + // Track the first not-null transactionId. This transaction gets ABORTED during commit + // operation and gets retried. + validTransactionId.set(impl.transactionId); + } + return null; + }); + + List executeSqlRequests = + mockSpanner.getRequestsOfType(ExecuteSqlRequest.class); + assertEquals(2, executeSqlRequests.size()); + + // Verify the requests are executed using multiplexed sessions + for (ExecuteSqlRequest request : executeSqlRequests) { + assertTrue(mockSpanner.getSession(request.getSession()).getMultiplexed()); + } - // Verify the requests are executed using multiplexed sessions - for (ExecuteSqlRequest request : executeSqlRequests) { - assertTrue(mockSpanner.getSession(request.getSession()).getMultiplexed()); + // Verify that the first request uses inline begin, and the previous transaction ID is set to + // ByteString.EMPTY + assertTrue(executeSqlRequests.get(0).hasTransaction()); + assertTrue(executeSqlRequests.get(0).getTransaction().hasBegin()); + assertTrue(executeSqlRequests.get(0).getTransaction().getBegin().hasReadWrite()); + assertNotNull( + executeSqlRequests + .get(0) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertEquals( + ByteString.EMPTY, + executeSqlRequests + .get(0) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + + // Verify that the second request uses inline begin, and the previous transaction ID is set + // appropriately + assertTrue(executeSqlRequests.get(1).hasTransaction()); + assertTrue(executeSqlRequests.get(1).getTransaction().hasBegin()); + assertTrue(executeSqlRequests.get(1).getTransaction().getBegin().hasReadWrite()); + assertNotNull( + executeSqlRequests + .get(1) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertNotEquals( + ByteString.EMPTY, + executeSqlRequests + .get(1) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertEquals( + validTransactionId.get(), + executeSqlRequests + .get(1) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); } - - // Verify that the first request uses inline begin, and the previous transaction ID is set to - // ByteString.EMPTY - assertTrue(executeSqlRequests.get(0).hasTransaction()); - assertTrue(executeSqlRequests.get(0).getTransaction().hasBegin()); - assertTrue(executeSqlRequests.get(0).getTransaction().getBegin().hasReadWrite()); - assertNotNull( - executeSqlRequests - .get(0) - .getTransaction() - .getBegin() - .getReadWrite() - .getMultiplexedSessionPreviousTransactionId()); - assertEquals( - ByteString.EMPTY, - executeSqlRequests - .get(0) - .getTransaction() - .getBegin() - .getReadWrite() - .getMultiplexedSessionPreviousTransactionId()); - - // Verify that the second request uses inline begin, and the previous transaction ID is set - // appropriately - assertTrue(executeSqlRequests.get(1).hasTransaction()); - assertTrue(executeSqlRequests.get(1).getTransaction().hasBegin()); - assertTrue(executeSqlRequests.get(1).getTransaction().getBegin().hasReadWrite()); - assertNotNull( - executeSqlRequests - .get(1) - .getTransaction() - .getBegin() - .getReadWrite() - .getMultiplexedSessionPreviousTransactionId()); - assertNotEquals( - ByteString.EMPTY, - executeSqlRequests - .get(1) - .getTransaction() - .getBegin() - .getReadWrite() - .getMultiplexedSessionPreviousTransactionId()); - assertEquals( - validTransactionId.get(), - executeSqlRequests - .get(1) - .getTransaction() - .getBegin() - .getReadWrite() - .getMultiplexedSessionPreviousTransactionId()); - } -*/ + */ // TODO(sriharshach): Uncomment test once Lock order preservation proto is published /* diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java index b1b8c10eda9..676b626c293 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java @@ -374,9 +374,11 @@ public void storePreviousTxnIdOnAbortForMultiplexedSession() { txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); final ByteString mockTransactionId = ByteString.copyFromUtf8("mockTransactionId"); txn.transactionId = mockTransactionId; - when(session.newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY)).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY)) + .thenReturn(txn); manager.begin(); - // Verify that for the first transaction attempt, the `previousTransactionId` is ByteString.EMPTY. + // Verify that for the first transaction attempt, the `previousTransactionId` is + // ByteString.EMPTY. // This is because no transaction has been previously aborted at this point. verify(session).newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY); doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "")).when(txn).commit(); @@ -402,7 +404,8 @@ public void skipTxnIdStorageOnAbortForRegularSession() { txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); final ByteString mockTransactionId = ByteString.copyFromUtf8("mockTransactionId"); txn.transactionId = mockTransactionId; - when(session.newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY)).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY)) + .thenReturn(txn); manager.begin(); verify(session).newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY); doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "")).when(txn).commit(); @@ -410,7 +413,8 @@ public void skipTxnIdStorageOnAbortForRegularSession() { clearInvocations(session); txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); - when(session.newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY)).thenReturn(txn); + when(session.newTransaction(Options.fromTransactionOptions(), ByteString.EMPTY)) + .thenReturn(txn); when(session.getIsMultiplexed()).thenReturn(false); assertThat(manager.resetForRetry()).isEqualTo(txn); // Verify that in the first retry attempt, the `previousTransactionId` is not passed to the new From dd4c9444c421cfaa8493963d5942a24214e5af76 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Thu, 10 Oct 2024 14:11:10 +0000 Subject: [PATCH 14/16] chore(spanner): uncomment TODO changes --- .../com/google/cloud/spanner/SessionImpl.java | 3 +- ...edSessionDatabaseClientMockServerTest.java | 180 +++++++++--------- 2 files changed, 90 insertions(+), 93 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java index 228b4900927..60c9d45d186 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java @@ -81,8 +81,7 @@ static TransactionOptions createReadWriteTransactionOptions( } if (previousTransactionId != null && previousTransactionId != com.google.protobuf.ByteString.EMPTY) { - // TODO(sriharshach): uncomment this when multiplexed session R/W proto is published - // readWrite.setMultiplexedSessionPreviousTransactionId(previousTransactionId); + readWrite.setMultiplexedSessionPreviousTransactionId(previousTransactionId); } transactionOptions.setReadWrite(readWrite); return transactionOptions.build(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java index ae5d246b678..ee7e435b3f2 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java @@ -32,10 +32,12 @@ import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; import com.google.cloud.spanner.Options.RpcPriority; +import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.cloud.spanner.connection.RandomResultSetGenerator; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; +import com.google.spanner.v1.BeginTransactionRequest; import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.RequestOptions.Priority; @@ -46,6 +48,7 @@ import java.util.List; import java.util.Set; import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import org.junit.Before; import org.junit.BeforeClass; @@ -476,99 +479,94 @@ public void testWriteAtLeastOnceWithExcludeTxnFromChangeStreams() { assertEquals(1L, client.multiplexedSessionDatabaseClient.getNumSessionsReleased().get()); } - // TODO(sriharshach): Uncomment test once Lock order preservation proto is published - /* - @Test - public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() - throws InterruptedException { - DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); - // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared - // after the first call, so the retry should succeed. - mockSpanner.setCommitExecutionTime( - SimulatedExecutionTime.ofException( - mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); - Thread.sleep(10000); - TransactionRunner runner = client.readWriteTransaction(); - AtomicReference validTransactionId = new AtomicReference<>(); - runner.run( - transaction -> { - try (ResultSet resultSet = transaction.executeQuery(STATEMENT)) { - while (resultSet.next()) {} - } - - TransactionContextImpl impl = (TransactionContextImpl) transaction; - if (validTransactionId.get() == null) { - // Track the first not-null transactionId. This transaction gets ABORTED during commit - // operation and gets retried. - validTransactionId.set(impl.transactionId); - } - return null; - }); - - List executeSqlRequests = - mockSpanner.getRequestsOfType(ExecuteSqlRequest.class); - assertEquals(2, executeSqlRequests.size()); - - // Verify the requests are executed using multiplexed sessions - for (ExecuteSqlRequest request : executeSqlRequests) { - assertTrue(mockSpanner.getSession(request.getSession()).getMultiplexed()); - } + @Test + public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() + throws InterruptedException { + DatabaseClientImpl client = + (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared + // after the first call, so the retry should succeed. + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofException( + mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); + Thread.sleep(10000); + TransactionRunner runner = client.readWriteTransaction(); + AtomicReference validTransactionId = new AtomicReference<>(); + runner.run( + transaction -> { + try (ResultSet resultSet = transaction.executeQuery(STATEMENT)) { + while (resultSet.next()) {} + } + + TransactionContextImpl impl = (TransactionContextImpl) transaction; + if (validTransactionId.get() == null) { + // Track the first not-null transactionId. This transaction gets ABORTED during commit + // operation and gets retried. + validTransactionId.set(impl.transactionId); + } + return null; + }); + + List executeSqlRequests = + mockSpanner.getRequestsOfType(ExecuteSqlRequest.class); + assertEquals(2, executeSqlRequests.size()); - // Verify that the first request uses inline begin, and the previous transaction ID is set to - // ByteString.EMPTY - assertTrue(executeSqlRequests.get(0).hasTransaction()); - assertTrue(executeSqlRequests.get(0).getTransaction().hasBegin()); - assertTrue(executeSqlRequests.get(0).getTransaction().getBegin().hasReadWrite()); - assertNotNull( - executeSqlRequests - .get(0) - .getTransaction() - .getBegin() - .getReadWrite() - .getMultiplexedSessionPreviousTransactionId()); - assertEquals( - ByteString.EMPTY, - executeSqlRequests - .get(0) - .getTransaction() - .getBegin() - .getReadWrite() - .getMultiplexedSessionPreviousTransactionId()); - - // Verify that the second request uses inline begin, and the previous transaction ID is set - // appropriately - assertTrue(executeSqlRequests.get(1).hasTransaction()); - assertTrue(executeSqlRequests.get(1).getTransaction().hasBegin()); - assertTrue(executeSqlRequests.get(1).getTransaction().getBegin().hasReadWrite()); - assertNotNull( - executeSqlRequests - .get(1) - .getTransaction() - .getBegin() - .getReadWrite() - .getMultiplexedSessionPreviousTransactionId()); - assertNotEquals( - ByteString.EMPTY, - executeSqlRequests - .get(1) - .getTransaction() - .getBegin() - .getReadWrite() - .getMultiplexedSessionPreviousTransactionId()); - assertEquals( - validTransactionId.get(), - executeSqlRequests - .get(1) - .getTransaction() - .getBegin() - .getReadWrite() - .getMultiplexedSessionPreviousTransactionId()); + // Verify the requests are executed using multiplexed sessions + for (ExecuteSqlRequest request : executeSqlRequests) { + assertTrue(mockSpanner.getSession(request.getSession()).getMultiplexed()); } - */ - // TODO(sriharshach): Uncomment test once Lock order preservation proto is published - /* + // Verify that the first request uses inline begin, and the previous transaction ID is set to + // ByteString.EMPTY + assertTrue(executeSqlRequests.get(0).hasTransaction()); + assertTrue(executeSqlRequests.get(0).getTransaction().hasBegin()); + assertTrue(executeSqlRequests.get(0).getTransaction().getBegin().hasReadWrite()); + assertNotNull( + executeSqlRequests + .get(0) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertEquals( + ByteString.EMPTY, + executeSqlRequests + .get(0) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + + // Verify that the second request uses inline begin, and the previous transaction ID is set + // appropriately + assertTrue(executeSqlRequests.get(1).hasTransaction()); + assertTrue(executeSqlRequests.get(1).getTransaction().hasBegin()); + assertTrue(executeSqlRequests.get(1).getTransaction().getBegin().hasReadWrite()); + assertNotNull( + executeSqlRequests + .get(1) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertNotEquals( + ByteString.EMPTY, + executeSqlRequests + .get(1) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + assertEquals( + validTransactionId.get(), + executeSqlRequests + .get(1) + .getTransaction() + .getBegin() + .getReadWrite() + .getMultiplexedSessionPreviousTransactionId()); + } + @Test public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithExplicitBegin() throws InterruptedException { @@ -656,7 +654,7 @@ public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithExplicitBegin() .getOptions() .getReadWrite() .getMultiplexedSessionPreviousTransactionId()); - }*/ + } private void waitForSessionToBeReplaced(DatabaseClientImpl client) { assertNotNull(client.multiplexedSessionDatabaseClient); From 66d258f85134b43f8ef5fce90c71bff083172b11 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Mon, 14 Oct 2024 07:41:13 +0000 Subject: [PATCH 15/16] chore(spanner): lint fix and remove sleep --- .../com/google/cloud/spanner/DatabaseClientImpl.java | 2 +- ...ltiplexedSessionDatabaseClientMockServerTest.java | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index b9d1ce054d7..91edce79325 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -59,7 +59,7 @@ class DatabaseClientImpl implements DatabaseClient { /* useMultiplexedSessionBlindWrite = */ false, /* multiplexedSessionDatabaseClient = */ null, tracer, - false); + /* useMultiplexedSessionForRW = */ false); } DatabaseClientImpl( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java index 7233b0f150c..adf7ed2a403 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java @@ -752,10 +752,9 @@ public void testAsyncRunnerIsNonBlockingWithMultiplexedSession() throws Exceptio assertEquals(1L, client.multiplexedSessionDatabaseClient.getNumSessionsAcquired().get()); assertEquals(1L, client.multiplexedSessionDatabaseClient.getNumSessionsReleased().get()); } - + @Test - public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() - throws InterruptedException { + public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() { DatabaseClientImpl client = (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared @@ -763,7 +762,6 @@ public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() mockSpanner.setCommitExecutionTime( SimulatedExecutionTime.ofException( mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); - Thread.sleep(10000); TransactionRunner runner = client.readWriteTransaction(); AtomicReference validTransactionId = new AtomicReference<>(); runner.run( @@ -840,10 +838,9 @@ public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() .getReadWrite() .getMultiplexedSessionPreviousTransactionId()); } - + @Test - public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithExplicitBegin() - throws InterruptedException { + public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithExplicitBegin() { DatabaseClientImpl client = (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared @@ -851,7 +848,6 @@ public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithExplicitBegin() mockSpanner.setCommitExecutionTime( SimulatedExecutionTime.ofException( mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); - Thread.sleep(10000); TransactionRunner runner = client.readWriteTransaction(); AtomicReference validTransactionId = new AtomicReference<>(); Long updateCount = From e128921f0f42281778f2415a941f053f53f4cc04 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH Date: Wed, 16 Oct 2024 04:59:47 +0000 Subject: [PATCH 16/16] chore(spanner): make previoustransactionId field final --- .../cloud/spanner/AsyncTransactionManagerImpl.java | 2 +- .../cloud/spanner/TransactionManagerImpl.java | 2 +- .../google/cloud/spanner/TransactionRunnerImpl.java | 13 +++++++++---- .../spanner/AsyncTransactionManagerImplTest.java | 2 +- .../cloud/spanner/TransactionManagerImplTest.java | 2 +- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java index 837dd3c88d8..0057bb15bea 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java @@ -88,7 +88,7 @@ private ApiFuture internalBeginAsync(boolean firstAttempt) { // Use the current transactionId if available, otherwise fallback to the previous aborted // transactionId. multiplexedSessionPreviousTransactionId = - txn.transactionId != null ? txn.transactionId : txn.previousTransactionId; + txn.transactionId != null ? txn.transactionId : txn.getPreviousTransactionId(); } txn = diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java index 39c90846f9f..cafb27ba6b7 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java @@ -110,7 +110,7 @@ public TransactionContext resetForRetry() { // Use the current transactionId if available, otherwise fallback to the previous aborted // transactionId. multiplexedSessionPreviousTransactionId = - txn.transactionId != null ? txn.transactionId : txn.previousTransactionId; + txn.transactionId != null ? txn.transactionId : txn.getPreviousTransactionId(); } txn = session.newTransaction( diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java index 075e71a2309..48affde3558 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java @@ -209,7 +209,7 @@ public void removeListener(Runnable listener) { volatile ByteString transactionId; - ByteString previousTransactionId; + final ByteString previousTransactionId; private CommitResponse commitResponse; private final Clock clock; @@ -257,6 +257,10 @@ private void decreaseAsyncOperations() { } } + ByteString getPreviousTransactionId() { + return this.previousTransactionId; + } + @Override public void close() { // Only mark the context as closed, but do not end the tracer span, as that is done by the @@ -295,7 +299,7 @@ private void createTxnAsync(final SettableApiFuture res) { span.addAnnotation("Creating Transaction"); final ApiFuture fut = session.beginTransactionAsync( - options, isRouteToLeader(), getTransactionChannelHint(), previousTransactionId); + options, isRouteToLeader(), getTransactionChannelHint(), getPreviousTransactionId()); fut.addListener( () -> { try { @@ -571,7 +575,8 @@ TransactionSelector getTransactionSelector() { if (tx == null) { return TransactionSelector.newBuilder() .setBegin( - SessionImpl.createReadWriteTransactionOptions(options, previousTransactionId)) + SessionImpl.createReadWriteTransactionOptions( + options, getPreviousTransactionId())) .build(); } else { // Wait for the transaction to come available. The tx.get() call will fail with an @@ -1138,7 +1143,7 @@ private T runInternal(final TransactionCallable txCallable) { // Use the current transactionId if available, otherwise fallback to the previous // transactionId. multiplexedSessionPreviousTransactionId = - txn.transactionId != null ? txn.transactionId : txn.previousTransactionId; + txn.transactionId != null ? txn.transactionId : txn.getPreviousTransactionId(); } txn = diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java index 76bac4b6ca7..006a926e907 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerImplTest.java @@ -99,7 +99,7 @@ public void testRetryUsesPreviousTransactionIdOnMultiplexedSession() { // Mock the transaction object to contain transactionID=null and // previousTransactionId=mockPreviousTransactionId - transaction.previousTransactionId = mockPreviousTransactionId; + when(transaction.getPreviousTransactionId()).thenReturn(mockPreviousTransactionId); manager.resetForRetryAsync(); // Verify that in the first retry attempt, the `previousTransactionId` // (mockPreviousTransactionId) is passed to the new transaction. diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java index 676b626c293..10b13125152 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java @@ -385,7 +385,7 @@ public void storePreviousTxnIdOnAbortForMultiplexedSession() { assertThrows(AbortedException.class, () -> manager.commit()); txn = Mockito.mock(TransactionRunnerImpl.TransactionContextImpl.class); - txn.previousTransactionId = mockTransactionId; + when(txn.getPreviousTransactionId()).thenReturn(mockTransactionId); when(session.newTransaction(Options.fromTransactionOptions(), mockTransactionId)) .thenReturn(txn); when(session.getIsMultiplexed()).thenReturn(true);