16
16
17
17
package com .google .cloud .spanner ;
18
18
19
+ import static org .junit .Assert .assertThrows ;
20
+ import static org .mockito .ArgumentMatchers .any ;
21
+ import static org .mockito .ArgumentMatchers .eq ;
22
+ import static org .mockito .Mockito .clearInvocations ;
23
+ import static org .mockito .Mockito .doThrow ;
19
24
import static org .mockito .Mockito .mock ;
20
25
import static org .mockito .Mockito .verify ;
21
26
import static org .mockito .Mockito .when ;
22
27
23
28
import com .google .api .core .ApiFutures ;
24
29
import com .google .cloud .Timestamp ;
30
+ import com .google .protobuf .ByteString ;
25
31
import io .opentelemetry .api .trace .Span ;
26
32
import io .opentelemetry .context .Scope ;
27
33
import org .junit .Test ;
@@ -42,7 +48,7 @@ public void testCommitReturnsCommitStats() {
42
48
when (oTspan .makeCurrent ()).thenReturn (mock (Scope .class ));
43
49
try (AsyncTransactionManagerImpl manager =
44
50
new AsyncTransactionManagerImpl (session , span , Options .commitStats ())) {
45
- when (session .newTransaction (Options .fromTransactionOptions (Options .commitStats ())))
51
+ when (session .newTransaction (eq ( Options .fromTransactionOptions (Options .commitStats ())), any ( )))
46
52
.thenReturn (transaction );
47
53
when (transaction .ensureTxnAsync ()).thenReturn (ApiFutures .immediateFuture (null ));
48
54
Timestamp commitTimestamp = Timestamp .ofTimeMicroseconds (1 );
@@ -54,4 +60,67 @@ public void testCommitReturnsCommitStats() {
54
60
verify (transaction ).commitAsync ();
55
61
}
56
62
}
63
+
64
+ @ Test
65
+ public void testRetryUsesPreviousTransactionIdOnMultiplexedSession () {
66
+ // Set up mock transaction IDs
67
+ final ByteString mockTransactionId = ByteString .copyFromUtf8 ("mockTransactionId" );
68
+ final ByteString mockPreviousTransactionId =
69
+ ByteString .copyFromUtf8 ("mockPreviousTransactionId" );
70
+
71
+ Span oTspan = mock (Span .class );
72
+ ISpan span = new OpenTelemetrySpan (oTspan );
73
+ when (oTspan .makeCurrent ()).thenReturn (mock (Scope .class ));
74
+ // Mark the session as multiplexed.
75
+ when (session .getIsMultiplexed ()).thenReturn (true );
76
+
77
+ // Initialize a mock transaction with transactionId = null, previousTransactionId = null.
78
+ transaction = mock (TransactionRunnerImpl .TransactionContextImpl .class );
79
+ when (transaction .ensureTxnAsync ()).thenReturn (ApiFutures .immediateFuture (null ));
80
+ when (session .newTransaction (eq (Options .fromTransactionOptions (Options .commitStats ())), any ()))
81
+ .thenReturn (transaction );
82
+
83
+ // Simulate an ABORTED error being thrown when `commitAsync()` is called.
84
+ doThrow (SpannerExceptionFactory .newSpannerException (ErrorCode .ABORTED , "" ))
85
+ .when (transaction )
86
+ .commitAsync ();
87
+
88
+ try (AsyncTransactionManagerImpl manager =
89
+ new AsyncTransactionManagerImpl (session , span , Options .commitStats ())) {
90
+ manager .beginAsync ();
91
+
92
+ // Verify that for the first transaction attempt, the `previousTransactionId` is
93
+ // ByteString.EMPTY.
94
+ // This is because no transaction has been previously aborted at this point.
95
+ verify (session )
96
+ .newTransaction (Options .fromTransactionOptions (Options .commitStats ()), ByteString .EMPTY );
97
+ assertThrows (AbortedException .class , manager ::commitAsync );
98
+ clearInvocations (session );
99
+
100
+ // Mock the transaction object to contain transactionID=null and
101
+ // previousTransactionId=mockPreviousTransactionId
102
+ when (transaction .getPreviousTransactionId ()).thenReturn (mockPreviousTransactionId );
103
+ manager .resetForRetryAsync ();
104
+ // Verify that in the first retry attempt, the `previousTransactionId`
105
+ // (mockPreviousTransactionId) is passed to the new transaction.
106
+ // This allows Spanner to retry the transaction using the ID of the aborted transaction.
107
+ verify (session )
108
+ .newTransaction (
109
+ Options .fromTransactionOptions (Options .commitStats ()), mockPreviousTransactionId );
110
+ assertThrows (AbortedException .class , manager ::commitAsync );
111
+ clearInvocations (session );
112
+
113
+ // Mock the transaction object to contain transactionID=mockTransactionId and
114
+ // previousTransactionId=mockPreviousTransactionId and transactionID = null
115
+ transaction .transactionId = mockTransactionId ;
116
+ manager .resetForRetryAsync ();
117
+ // Verify that the latest `transactionId` (mockTransactionId) is used in the retry.
118
+ // This ensures the retry logic is working as expected with the latest transaction ID.
119
+ verify (session )
120
+ .newTransaction (Options .fromTransactionOptions (Options .commitStats ()), mockTransactionId );
121
+
122
+ when (transaction .rollbackAsync ()).thenReturn (ApiFutures .immediateFuture (null ));
123
+ manager .closeAsync ();
124
+ }
125
+ }
57
126
}
0 commit comments