Skip to content

Commit 7a53b75

Browse files
authored
chore: add a random hint for multi-use transactions when they are use… (#3058)
1 parent 303cc15 commit 7a53b75

File tree

4 files changed

+390
-3
lines changed

4 files changed

+390
-3
lines changed

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

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.cloud.spanner;
1818

19+
import static com.google.cloud.spanner.SessionClient.optionMap;
1920
import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException;
2021
import static com.google.common.base.Preconditions.checkArgument;
2122
import static com.google.common.base.Preconditions.checkNotNull;
@@ -32,6 +33,7 @@
3233
import com.google.cloud.spanner.AsyncResultSet.ReadyCallback;
3334
import com.google.cloud.spanner.Options.QueryOption;
3435
import com.google.cloud.spanner.Options.ReadOption;
36+
import com.google.cloud.spanner.SessionClient.SessionOption;
3537
import com.google.cloud.spanner.SessionImpl.SessionTransaction;
3638
import com.google.cloud.spanner.spi.v1.SpannerRpc;
3739
import com.google.common.annotations.VisibleForTesting;
@@ -52,6 +54,7 @@
5254
import com.google.spanner.v1.TransactionOptions;
5355
import com.google.spanner.v1.TransactionSelector;
5456
import java.util.Map;
57+
import java.util.concurrent.ThreadLocalRandom;
5558
import java.util.concurrent.atomic.AtomicLong;
5659
import javax.annotation.Nullable;
5760
import javax.annotation.concurrent.GuardedBy;
@@ -180,9 +183,15 @@ static Builder newBuilder() {
180183
@GuardedBy("lock")
181184
private boolean used;
182185

186+
private final Map<SpannerRpc.Option, ?> channelHint;
187+
183188
private SingleReadContext(Builder builder) {
184189
super(builder);
185190
this.bound = builder.bound;
191+
// single use transaction have a single RPC and hence there is no need
192+
// of a channel hint. GAX will automatically choose a hint when used
193+
// with a multiplexed session.
194+
this.channelHint = getChannelHintOptions(session.getOptions(), null);
186195
}
187196

188197
@Override
@@ -209,6 +218,11 @@ TransactionSelector getTransactionSelector() {
209218
.setSingleUse(TransactionOptions.newBuilder().setReadOnly(bound.toProto()))
210219
.build();
211220
}
221+
222+
@Override
223+
Map<SpannerRpc.Option, ?> getTransactionChannelHint() {
224+
return channelHint;
225+
}
212226
}
213227

214228
private static void assertTimestampAvailable(boolean available) {
@@ -217,6 +231,7 @@ private static void assertTimestampAvailable(boolean available) {
217231

218232
static class SingleUseReadOnlyTransaction extends SingleReadContext
219233
implements ReadOnlyTransaction {
234+
220235
@GuardedBy("lock")
221236
private Timestamp timestamp;
222237

@@ -300,6 +315,8 @@ static Builder newBuilder() {
300315
@GuardedBy("txnLock")
301316
private ByteString transactionId;
302317

318+
private final Map<SpannerRpc.Option, ?> channelHint;
319+
303320
MultiUseReadOnlyTransaction(Builder builder) {
304321
super(builder);
305322
checkArgument(
@@ -318,6 +335,14 @@ static Builder newBuilder() {
318335
this.timestamp = builder.timestamp;
319336
this.transactionId = builder.transactionId;
320337
}
338+
this.channelHint =
339+
getChannelHintOptions(
340+
session.getOptions(), ThreadLocalRandom.current().nextLong(Long.MAX_VALUE));
341+
}
342+
343+
@Override
344+
public Map<SpannerRpc.Option, ?> getTransactionChannelHint() {
345+
return channelHint;
321346
}
322347

323348
@Override
@@ -380,7 +405,7 @@ void initTransaction() {
380405
.setOptions(options)
381406
.build();
382407
Transaction transaction =
383-
rpc.beginTransaction(request, session.getOptions(), isRouteToLeader());
408+
rpc.beginTransaction(request, getTransactionChannelHint(), isRouteToLeader());
384409
if (!transaction.hasReadTimestamp()) {
385410
throw SpannerExceptionFactory.newSpannerException(
386411
ErrorCode.INTERNAL, "Missing expected transaction.read_timestamp metadata field");
@@ -727,7 +752,10 @@ CloseableIterator<PartialResultSet> startStream(@Nullable ByteString resumeToken
727752
}
728753
SpannerRpc.StreamingCall call =
729754
rpc.executeQuery(
730-
request.build(), stream.consumer(), session.getOptions(), isRouteToLeader());
755+
request.build(),
756+
stream.consumer(),
757+
getTransactionChannelHint(),
758+
isRouteToLeader());
731759
session.markUsed(clock.instant());
732760
call.request(prefetchChunks);
733761
stream.setCall(call, request.getTransaction().hasBegin());
@@ -738,6 +766,16 @@ CloseableIterator<PartialResultSet> startStream(@Nullable ByteString resumeToken
738766
stream, this, options.hasDecodeMode() ? options.decodeMode() : defaultDecodeMode);
739767
}
740768

769+
Map<SpannerRpc.Option, ?> getChannelHintOptions(
770+
Map<SpannerRpc.Option, ?> channelHintForSession, Long channelHintForTransaction) {
771+
if (channelHintForSession != null) {
772+
return channelHintForSession;
773+
} else if (channelHintForTransaction != null) {
774+
return optionMap(SessionOption.channelHint(channelHintForTransaction));
775+
}
776+
return null;
777+
}
778+
741779
/**
742780
* Called before any read or query is started to perform state checks and initializations.
743781
* Subclasses should call {@code super.beforeReadOrQuery()} if overriding.
@@ -782,6 +820,12 @@ public void close() {
782820
@Nullable
783821
abstract TransactionSelector getTransactionSelector();
784822

823+
/**
824+
* Channel hint to be used for a transaction. This enables soft-stickiness per transaction by
825+
* ensuring all RPCs within a transaction land up on the same channel.
826+
*/
827+
abstract Map<SpannerRpc.Option, ?> getTransactionChannelHint();
828+
785829
/**
786830
* Returns the transaction tag for this {@link AbstractReadContext} or <code>null</code> if this
787831
* {@link AbstractReadContext} does not have a transaction tag.
@@ -872,7 +916,10 @@ CloseableIterator<PartialResultSet> startStream(@Nullable ByteString resumeToken
872916
builder.setRequestOptions(buildRequestOptions(readOptions));
873917
SpannerRpc.StreamingCall call =
874918
rpc.read(
875-
builder.build(), stream.consumer(), session.getOptions(), isRouteToLeader());
919+
builder.build(),
920+
stream.consumer(),
921+
getTransactionChannelHint(),
922+
isRouteToLeader());
876923
session.markUsed(clock.instant());
877924
call.request(prefetchChunks);
878925
stream.setCall(call, /* withBeginTransaction = */ builder.getTransaction().hasBegin());

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import com.google.cloud.spanner.Options.TransactionOption;
3131
import com.google.cloud.spanner.Options.UpdateOption;
3232
import com.google.cloud.spanner.SessionImpl.SessionTransaction;
33+
import com.google.cloud.spanner.spi.v1.SpannerRpc;
34+
import com.google.cloud.spanner.spi.v1.SpannerRpc.Option;
3335
import com.google.common.annotations.VisibleForTesting;
3436
import com.google.common.base.Preconditions;
3537
import com.google.common.collect.ImmutableMap;
@@ -51,11 +53,13 @@
5153
import com.google.spanner.v1.TransactionSelector;
5254
import java.util.ArrayList;
5355
import java.util.List;
56+
import java.util.Map;
5457
import java.util.Queue;
5558
import java.util.concurrent.Callable;
5659
import java.util.concurrent.ConcurrentLinkedQueue;
5760
import java.util.concurrent.ExecutionException;
5861
import java.util.concurrent.Executor;
62+
import java.util.concurrent.ThreadLocalRandom;
5963
import java.util.concurrent.TimeUnit;
6064
import java.util.concurrent.TimeoutException;
6165
import java.util.concurrent.atomic.AtomicInteger;
@@ -198,13 +202,18 @@ public void removeListener(Runnable listener) {
198202
private CommitResponse commitResponse;
199203
private final Clock clock;
200204

205+
private final Map<SpannerRpc.Option, ?> channelHint;
206+
201207
private TransactionContextImpl(Builder builder) {
202208
super(builder);
203209
this.transactionId = builder.transactionId;
204210
this.trackTransactionStarter = builder.trackTransactionStarter;
205211
this.options = builder.options;
206212
this.finishedAsyncOperations.set(null);
207213
this.clock = builder.clock;
214+
this.channelHint =
215+
getChannelHintOptions(
216+
session.getOptions(), ThreadLocalRandom.current().nextLong(Long.MAX_VALUE));
208217
}
209218

210219
@Override
@@ -559,6 +568,11 @@ TransactionSelector getTransactionSelector() {
559568
return TransactionSelector.newBuilder().setId(transactionId).build();
560569
}
561570

571+
@Override
572+
Map<Option, ?> getTransactionChannelHint() {
573+
return channelHint;
574+
}
575+
562576
@Override
563577
public void onTransactionMetadata(Transaction transaction, boolean shouldIncludeId) {
564578
Preconditions.checkNotNull(transaction);

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.google.api.gax.core.ExecutorProvider;
2626
import com.google.cloud.spanner.Options.RpcPriority;
2727
import com.google.cloud.spanner.spi.v1.SpannerRpc;
28+
import com.google.cloud.spanner.spi.v1.SpannerRpc.Option;
2829
import com.google.spanner.v1.DirectedReadOptions;
2930
import com.google.spanner.v1.DirectedReadOptions.IncludeReplicas;
3031
import com.google.spanner.v1.DirectedReadOptions.ReplicaSelection;
@@ -39,6 +40,7 @@
3940
import java.util.Collection;
4041
import java.util.Collections;
4142
import java.util.List;
43+
import java.util.Map;
4244
import org.junit.Before;
4345
import org.junit.Test;
4446
import org.junit.runner.RunWith;
@@ -98,6 +100,11 @@ private final class TestReadContext extends AbstractReadContext {
98100
TransactionSelector getTransactionSelector() {
99101
return TransactionSelector.getDefaultInstance();
100102
}
103+
104+
@Override
105+
Map<SpannerRpc.Option, ?> getTransactionChannelHint() {
106+
return null;
107+
}
101108
}
102109

103110
private final class TestReadContextWithTag extends AbstractReadContext {
@@ -110,6 +117,11 @@ TransactionSelector getTransactionSelector() {
110117
return TransactionSelector.getDefaultInstance();
111118
}
112119

120+
@Override
121+
Map<Option, ?> getTransactionChannelHint() {
122+
return null;
123+
}
124+
113125
String getTransactionTag() {
114126
return "app=spanner,env=test";
115127
}

0 commit comments

Comments
 (0)