Skip to content

Commit 07b777d

Browse files
authored
chore(spanner): handle server side kill switch of R/W multiplexed sessions (#3441)
* chore(spanner): handle server side kill switch for multiplexed sessions with read-write * chore(spanner): lint fix * chore(spanner): add error message * chore(spanner): fix unit tests due to backgroung BeginTransaction RPC * chore(spanner): lint fix * chore(spanner): handle mock spanner logic * chore(spanner): testcase fix * chore(spanner): do not register backgroung begin txn in mock spanner * chore(spanner): revert unit test changes * chore(spanner): verify error message of the exeception
1 parent 11ead4e commit 07b777d

File tree

4 files changed

+417
-5
lines changed

4 files changed

+417
-5
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ DatabaseClient getMultiplexedSession() {
9292

9393
@VisibleForTesting
9494
DatabaseClient getMultiplexedSessionForRW() {
95-
if (this.useMultiplexedSessionForRW) {
95+
if (canUseMultiplexedSessionsForRW()) {
9696
return getMultiplexedSession();
9797
}
9898
return getSession();
@@ -107,6 +107,12 @@ private boolean canUseMultiplexedSessions() {
107107
&& this.multiplexedSessionDatabaseClient.isMultiplexedSessionsSupported();
108108
}
109109

110+
private boolean canUseMultiplexedSessionsForRW() {
111+
return this.useMultiplexedSessionForRW
112+
&& this.multiplexedSessionDatabaseClient != null
113+
&& this.multiplexedSessionDatabaseClient.isMultiplexedSessionsForRWSupported();
114+
}
115+
110116
@Override
111117
public Dialect getDialect() {
112118
return pool.getDialect();
@@ -129,7 +135,7 @@ public CommitResponse writeWithOptions(
129135
throws SpannerException {
130136
ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION, options);
131137
try (IScope s = tracer.withSpan(span)) {
132-
if (this.useMultiplexedSessionForRW && getMultiplexedSessionDatabaseClient() != null) {
138+
if (canUseMultiplexedSessionsForRW() && getMultiplexedSessionDatabaseClient() != null) {
133139
return getMultiplexedSessionDatabaseClient().writeWithOptions(mutations, options);
134140
}
135141
return runWithSessionRetry(session -> session.writeWithOptions(mutations, options));

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

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner;
1818

1919
import static com.google.cloud.spanner.SessionImpl.NO_CHANNEL_HINT;
20+
import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException;
2021

2122
import com.google.api.core.ApiFuture;
2223
import com.google.api.core.ApiFutures;
@@ -27,6 +28,10 @@
2728
import com.google.cloud.spanner.SpannerException.ResourceNotFoundException;
2829
import com.google.common.annotations.VisibleForTesting;
2930
import com.google.common.base.Preconditions;
31+
import com.google.common.util.concurrent.MoreExecutors;
32+
import com.google.spanner.v1.BeginTransactionRequest;
33+
import com.google.spanner.v1.RequestOptions;
34+
import com.google.spanner.v1.Transaction;
3035
import java.time.Clock;
3136
import java.time.Duration;
3237
import java.time.Instant;
@@ -92,6 +97,10 @@ void onError(SpannerException spannerException) {
9297
// synchronizing, as it does not really matter exactly which error is set.
9398
this.client.resourceNotFoundException.set((ResourceNotFoundException) spannerException);
9499
}
100+
// Mark multiplexed sessions for RW as unimplemented and fall back to regular sessions if
101+
// UNIMPLEMENTED with error message "Transaction type read_write not supported with
102+
// multiplexed sessions" is returned.
103+
this.client.maybeMarkUnimplementedForRW(spannerException);
95104
}
96105

97106
@Override
@@ -164,6 +173,12 @@ public void close() {
164173
/** The current multiplexed session that is used by this client. */
165174
private final AtomicReference<ApiFuture<SessionReference>> multiplexedSessionReference;
166175

176+
/**
177+
* The Transaction response returned by the BeginTransaction request with read-write when a
178+
* multiplexed session is created during client initialization.
179+
*/
180+
private final SettableApiFuture<Transaction> readWriteBeginTransactionReferenceFuture;
181+
167182
/** The expiration date/time of the current multiplexed session. */
168183
private final AtomicReference<Instant> expirationDate;
169184

@@ -190,6 +205,12 @@ public void close() {
190205
*/
191206
private final AtomicBoolean unimplemented = new AtomicBoolean(false);
192207

208+
/**
209+
* This flag is set to true if the server return UNIMPLEMENTED when a read-write transaction is
210+
* executed on a multiplexed session. TODO: Remove once this is guaranteed to be available.
211+
*/
212+
@VisibleForTesting final AtomicBoolean unimplementedForRW = new AtomicBoolean(false);
213+
193214
MultiplexedSessionDatabaseClient(SessionClient sessionClient) {
194215
this(sessionClient, Clock.systemUTC());
195216
}
@@ -217,6 +238,7 @@ public void close() {
217238
this.tracer = sessionClient.getSpanner().getTracer();
218239
final SettableApiFuture<SessionReference> initialSessionReferenceFuture =
219240
SettableApiFuture.create();
241+
this.readWriteBeginTransactionReferenceFuture = SettableApiFuture.create();
220242
this.multiplexedSessionReference = new AtomicReference<>(initialSessionReferenceFuture);
221243
this.sessionClient.asyncCreateMultiplexedSession(
222244
new SessionConsumer() {
@@ -226,6 +248,16 @@ public void onSessionReady(SessionImpl session) {
226248
// only start the maintainer if we actually managed to create a session in the first
227249
// place.
228250
maintainer.start();
251+
252+
// initiate a begin transaction request to verify if read-write transactions are
253+
// supported using multiplexed sessions.
254+
if (sessionClient
255+
.getSpanner()
256+
.getOptions()
257+
.getSessionPoolOptions()
258+
.getUseMultiplexedSessionForRW()) {
259+
verifyBeginTransactionWithRWOnMultiplexedSessionAsync(session.getName());
260+
}
229261
}
230262

231263
@Override
@@ -267,6 +299,70 @@ private void maybeMarkUnimplemented(Throwable t) {
267299
}
268300
}
269301

302+
private void maybeMarkUnimplementedForRW(SpannerException spannerException) {
303+
if (spannerException.getErrorCode() == ErrorCode.UNIMPLEMENTED
304+
&& verifyErrorMessage(
305+
spannerException,
306+
"Transaction type read_write not supported with multiplexed sessions")) {
307+
unimplementedForRW.set(true);
308+
}
309+
}
310+
311+
private boolean verifyErrorMessage(SpannerException spannerException, String message) {
312+
if (spannerException.getCause() == null) {
313+
return false;
314+
}
315+
if (spannerException.getCause().getMessage() == null) {
316+
return false;
317+
}
318+
return spannerException.getCause().getMessage().contains(message);
319+
}
320+
321+
private void verifyBeginTransactionWithRWOnMultiplexedSessionAsync(String sessionName) {
322+
// TODO: Remove once this is guaranteed to be available.
323+
// annotate the explict BeginTransactionRequest with a transaction tag
324+
// "multiplexed-rw-background-begin-txn" to avoid storing this request on mock spanner.
325+
// this is to safeguard other mock spanner tests whose BeginTransaction request count will
326+
// otherwise increase by 1. Modifying the unit tests do not seem valid since this code is
327+
// temporary and will be removed once the read-write on multiplexed session looks stable at
328+
// backend.
329+
BeginTransactionRequest.Builder requestBuilder =
330+
BeginTransactionRequest.newBuilder()
331+
.setSession(sessionName)
332+
.setOptions(
333+
SessionImpl.createReadWriteTransactionOptions(
334+
Options.fromTransactionOptions(), /* previousTransactionId = */ null))
335+
.setRequestOptions(
336+
RequestOptions.newBuilder()
337+
.setTransactionTag("multiplexed-rw-background-begin-txn")
338+
.build());
339+
final BeginTransactionRequest request = requestBuilder.build();
340+
final ApiFuture<Transaction> requestFuture;
341+
requestFuture =
342+
sessionClient
343+
.getSpanner()
344+
.getRpc()
345+
.beginTransactionAsync(request, /* options = */ null, /* routeToLeader = */ true);
346+
requestFuture.addListener(
347+
() -> {
348+
try {
349+
Transaction txn = requestFuture.get();
350+
if (txn.getId().isEmpty()) {
351+
throw newSpannerException(
352+
ErrorCode.INTERNAL, "Missing id in transaction\n" + sessionName);
353+
}
354+
readWriteBeginTransactionReferenceFuture.set(txn);
355+
} catch (Exception e) {
356+
SpannerException spannerException = SpannerExceptionFactory.newSpannerException(e);
357+
// Mark multiplexed sessions for RW as unimplemented and fall back to regular sessions
358+
// if UNIMPLEMENTED is returned.
359+
maybeMarkUnimplementedForRW(spannerException);
360+
readWriteBeginTransactionReferenceFuture.setException(e);
361+
}
362+
},
363+
MoreExecutors.directExecutor());
364+
}
365+
270366
boolean isValid() {
271367
return resourceNotFoundException.get() == null;
272368
}
@@ -283,6 +379,10 @@ boolean isMultiplexedSessionsSupported() {
283379
return !this.unimplemented.get();
284380
}
285381

382+
boolean isMultiplexedSessionsForRWSupported() {
383+
return !this.unimplementedForRW.get();
384+
}
385+
286386
void close() {
287387
synchronized (this) {
288388
if (!this.isClosed) {
@@ -308,6 +408,17 @@ SessionReference getCurrentSessionReference() {
308408
}
309409
}
310410

411+
@VisibleForTesting
412+
Transaction getReadWriteBeginTransactionReference() {
413+
try {
414+
return this.readWriteBeginTransactionReferenceFuture.get();
415+
} catch (ExecutionException executionException) {
416+
throw SpannerExceptionFactory.asSpannerException(executionException.getCause());
417+
} catch (InterruptedException interruptedException) {
418+
throw SpannerExceptionFactory.propagateInterrupt(interruptedException);
419+
}
420+
}
421+
311422
/**
312423
* Returns true if the multiplexed session has been created. This client can be used before the
313424
* session has been created, and will in that case use a delayed transaction that contains a

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1872,7 +1872,17 @@ private Transaction getTemporaryTransactionOrNull(TransactionSelector tx) {
18721872
@Override
18731873
public void beginTransaction(
18741874
BeginTransactionRequest request, StreamObserver<Transaction> responseObserver) {
1875-
requests.add(request);
1875+
// TODO: Remove once this is guaranteed to be available.
1876+
// Skip storing the explicit BeginTransactionRequest used to verify read-write transaction
1877+
// server availability on multiplexed sessions.
1878+
// This code will be removed once read-write multiplexed sessions are stable on the backend,
1879+
// hence the temporary trade-off.
1880+
if (!request
1881+
.getRequestOptions()
1882+
.getTransactionTag()
1883+
.equals("multiplexed-rw-background-begin-txn")) {
1884+
requests.add(request);
1885+
}
18761886
Preconditions.checkNotNull(request.getSession());
18771887
Session session = getSession(request.getSession());
18781888
if (session == null) {

0 commit comments

Comments
 (0)