Skip to content

Commit 9d850a7

Browse files
committedMar 28, 2025
chore: wire up x-goog-spanner-request-id to all
Wires up x-goog-spanner-request-id for piecemeal addition per gRPC method, starting with BatchCreateSessions. This change involves creating TransactionOption, UpdateOption variants that allow holding the request id. Updates googleapis#3537
1 parent 9940b66 commit 9d850a7

15 files changed

+545
-40
lines changed
 

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

+13-5
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,11 @@ void initTransaction() {
458458

459459
private void initTransactionInternal(BeginTransactionRequest request) {
460460
try {
461+
XGoogSpannerRequestId reqId =
462+
session.getRequestIdCreator().nextRequestId(1 /*TODO: retrieve channelId*/, 1);
461463
Transaction transaction =
462-
rpc.beginTransaction(request, getTransactionChannelHint(), isRouteToLeader());
464+
rpc.beginTransaction(
465+
request, reqId.withOptions(getTransactionChannelHint()), isRouteToLeader());
463466
if (!transaction.hasReadTimestamp()) {
464467
throw SpannerExceptionFactory.newSpannerException(
465468
ErrorCode.INTERNAL, "Missing expected transaction.read_timestamp metadata field");
@@ -803,7 +806,8 @@ ResultSet executeQueryInternalWithOptions(
803806
tracer.createStatementAttributes(statement, options),
804807
session.getErrorHandler(),
805808
rpc.getExecuteQueryRetrySettings(),
806-
rpc.getExecuteQueryRetryableCodes()) {
809+
rpc.getExecuteQueryRetryableCodes(),
810+
session.getRequestIdCreator()) {
807811
@Override
808812
CloseableIterator<PartialResultSet> startStream(
809813
@Nullable ByteString resumeToken,
@@ -826,11 +830,13 @@ CloseableIterator<PartialResultSet> startStream(
826830
if (selector != null) {
827831
request.setTransaction(selector);
828832
}
833+
834+
this.incrementXGoogRequestIdAttempt();
829835
SpannerRpc.StreamingCall call =
830836
rpc.executeQuery(
831837
request.build(),
832838
stream.consumer(),
833-
getTransactionChannelHint(),
839+
this.xGoogRequestId.withOptions(getTransactionChannelHint()),
834840
isRouteToLeader());
835841
session.markUsed(clock.instant());
836842
stream.setCall(call, request.getTransaction().hasBegin());
@@ -1008,7 +1014,8 @@ ResultSet readInternalWithOptions(
10081014
tracer.createTableAttributes(table, readOptions),
10091015
session.getErrorHandler(),
10101016
rpc.getReadRetrySettings(),
1011-
rpc.getReadRetryableCodes()) {
1017+
rpc.getReadRetryableCodes(),
1018+
session.getRequestIdCreator()) {
10121019
@Override
10131020
CloseableIterator<PartialResultSet> startStream(
10141021
@Nullable ByteString resumeToken,
@@ -1029,11 +1036,12 @@ CloseableIterator<PartialResultSet> startStream(
10291036
builder.setTransaction(selector);
10301037
}
10311038
builder.setRequestOptions(buildRequestOptions(readOptions));
1039+
this.incrementXGoogRequestIdAttempt();
10321040
SpannerRpc.StreamingCall call =
10331041
rpc.read(
10341042
builder.build(),
10351043
stream.consumer(),
1036-
getTransactionChannelHint(),
1044+
this.xGoogRequestId.withOptions(getTransactionChannelHint()),
10371045
isRouteToLeader());
10381046
session.markUsed(clock.instant());
10391047
stream.setCall(call, /* withBeginTransaction = */ builder.getTransaction().hasBegin());

‎google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,9 @@ private List<Partition> partitionQuery(
315315

316316
final PartitionQueryRequest request = builder.build();
317317
try {
318-
PartitionResponse response = rpc.partitionQuery(request, options);
318+
XGoogSpannerRequestId reqId =
319+
this.session.requestIdCreator.nextRequestId(1 /* channelId */, 0);
320+
PartitionResponse response = rpc.partitionQuery(request, reqId.withOptions(options));
319321
ImmutableList.Builder<Partition> partitions = ImmutableList.builder();
320322
for (com.google.spanner.v1.Partition p : response.getPartitionsList()) {
321323
Partition partition =

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

+64-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
import com.google.common.util.concurrent.ListenableFuture;
2828
import com.google.spanner.v1.BatchWriteResponse;
2929
import io.opentelemetry.api.common.Attributes;
30+
import java.util.ArrayList;
31+
import java.util.Arrays;
32+
import java.util.Objects;
33+
import java.util.concurrent.atomic.AtomicInteger;
3034
import javax.annotation.Nullable;
3135

3236
class DatabaseClientImpl implements DatabaseClient {
@@ -40,6 +44,8 @@ class DatabaseClientImpl implements DatabaseClient {
4044
@VisibleForTesting final MultiplexedSessionDatabaseClient multiplexedSessionDatabaseClient;
4145
@VisibleForTesting final boolean useMultiplexedSessionPartitionedOps;
4246
@VisibleForTesting final boolean useMultiplexedSessionForRW;
47+
private final int dbId;
48+
private final AtomicInteger nthRequest;
4349

4450
final boolean useMultiplexedSessionBlindWrite;
4551

@@ -86,6 +92,18 @@ class DatabaseClientImpl implements DatabaseClient {
8692
this.tracer = tracer;
8793
this.useMultiplexedSessionForRW = useMultiplexedSessionForRW;
8894
this.commonAttributes = commonAttributes;
95+
96+
this.dbId = this.dbIdFromClientId(this.clientId);
97+
this.nthRequest = new AtomicInteger(0);
98+
}
99+
100+
private int dbIdFromClientId(String clientId) {
101+
int i = clientId.indexOf("-");
102+
String strWithValue = clientId.substring(i + 1);
103+
if (Objects.equals(strWithValue, "")) {
104+
strWithValue = "0";
105+
}
106+
return Integer.parseInt(strWithValue);
89107
}
90108

91109
@VisibleForTesting
@@ -183,8 +201,20 @@ public CommitResponse writeAtLeastOnceWithOptions(
183201
return getMultiplexedSessionDatabaseClient()
184202
.writeAtLeastOnceWithOptions(mutations, options);
185203
}
204+
205+
int nthRequest = this.nextNthRequest();
206+
int channelId = 1; /* TODO: infer the channelId from the gRPC channel of the session */
207+
XGoogSpannerRequestId reqId = XGoogSpannerRequestId.of(this.dbId, channelId, nthRequest, 0);
208+
186209
return runWithSessionRetry(
187-
session -> session.writeAtLeastOnceWithOptions(mutations, options));
210+
(session) -> {
211+
reqId.incrementAttempt();
212+
// TODO: Update the channelId depending on the session that is inferred.
213+
ArrayList<TransactionOption> allOptions = new ArrayList(Arrays.asList(options));
214+
allOptions.add(new Options.RequestIdOption(reqId));
215+
return session.writeAtLeastOnceWithOptions(
216+
mutations, allOptions.toArray(new TransactionOption[0]));
217+
});
188218
} catch (RuntimeException e) {
189219
span.setStatus(e);
190220
throw e;
@@ -193,16 +223,38 @@ public CommitResponse writeAtLeastOnceWithOptions(
193223
}
194224
}
195225

226+
private int nextNthRequest() {
227+
return this.nthRequest.incrementAndGet();
228+
}
229+
196230
@Override
197231
public ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
198232
final Iterable<MutationGroup> mutationGroups, final TransactionOption... options)
199233
throws SpannerException {
200234
ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION, commonAttributes, options);
201235
try (IScope s = tracer.withSpan(span)) {
236+
XGoogSpannerRequestId reqId =
237+
XGoogSpannerRequestId.of(this.dbId, 1 /*TODO:channelId*/, this.nextNthRequest(), 0);
238+
System.out.println("\033[35mbatchWriteAtLeastOnceReq: " + reqId.toString() + "\033[00m");
202239
if (canUseMultiplexedSessionsForRW() && getMultiplexedSessionDatabaseClient() != null) {
203-
return getMultiplexedSessionDatabaseClient().batchWriteAtLeastOnce(mutationGroups, options);
240+
reqId.incrementAttempt();
241+
ArrayList<TransactionOption> allOptions = new ArrayList(Arrays.asList(options));
242+
allOptions.add(new Options.RequestIdOption(reqId));
243+
return getMultiplexedSessionDatabaseClient()
244+
.batchWriteAtLeastOnce(mutationGroups, allOptions.toArray(new TransactionOption[0]));
204245
}
205-
return runWithSessionRetry(session -> session.batchWriteAtLeastOnce(mutationGroups, options));
246+
return runWithSessionRetry(
247+
(session) -> {
248+
reqId.incrementAttempt();
249+
ArrayList<TransactionOption> allOptions = new ArrayList(Arrays.asList(options));
250+
System.out.println(
251+
"\033[35mbatchWriteAtLeastOnce:: session.class: "
252+
+ session.getClass()
253+
+ "\033[00m");
254+
allOptions.add(new Options.RequestIdOption(reqId));
255+
return session.batchWriteAtLeastOnce(
256+
mutationGroups, allOptions.toArray(new TransactionOption[0]));
257+
});
206258
} catch (RuntimeException e) {
207259
span.setStatus(e);
208260
throw e;
@@ -350,7 +402,15 @@ private long executePartitionedUpdateWithPooledSession(
350402
final Statement stmt, final UpdateOption... options) {
351403
ISpan span = tracer.spanBuilder(PARTITION_DML_TRANSACTION, commonAttributes);
352404
try (IScope s = tracer.withSpan(span)) {
353-
return runWithSessionRetry(session -> session.executePartitionedUpdate(stmt, options));
405+
XGoogSpannerRequestId reqId =
406+
XGoogSpannerRequestId.of(this.dbId, 1 /*TODO: channelId*/, this.nextNthRequest(), 0);
407+
return runWithSessionRetry(
408+
(session) -> {
409+
reqId.incrementAttempt();
410+
ArrayList<TransactionOption> allOptions = new ArrayList(Arrays.asList(options));
411+
allOptions.add(new Options.RequestIdOption(reqId));
412+
return session.executePartitionedUpdate(stmt, allOptions.toArray(new UpdateOption[0]));
413+
});
354414
} catch (RuntimeException e) {
355415
span.setStatus(e);
356416
span.end();

‎google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java

+35
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,7 @@ void appendToOptions(Options options) {
535535
private RpcLockHint lockHint;
536536
private Boolean lastStatement;
537537
private IsolationLevel isolationLevel;
538+
private XGoogSpannerRequestId reqId;
538539

539540
// Construction is via factory methods below.
540541
private Options() {}
@@ -591,6 +592,14 @@ String pageToken() {
591592
return pageToken;
592593
}
593594

595+
boolean hasReqId() {
596+
return reqId != null;
597+
}
598+
599+
XGoogSpannerRequestId reqId() {
600+
return reqId;
601+
}
602+
594603
boolean hasFilter() {
595604
return filter != null;
596605
}
@@ -1052,4 +1061,30 @@ public boolean equals(Object o) {
10521061
return o instanceof LastStatementUpdateOption;
10531062
}
10541063
}
1064+
1065+
static final class RequestIdOption extends InternalOption
1066+
implements TransactionOption, UpdateOption {
1067+
private final XGoogSpannerRequestId reqId;
1068+
1069+
RequestIdOption(XGoogSpannerRequestId reqId) {
1070+
this.reqId = reqId;
1071+
}
1072+
1073+
@Override
1074+
void appendToOptions(Options options) {
1075+
options.reqId = this.reqId;
1076+
}
1077+
1078+
@Override
1079+
public int hashCode() {
1080+
return RequestIdOption.class.hashCode();
1081+
}
1082+
1083+
@Override
1084+
public boolean equals(Object o) {
1085+
// TODO: Examine why the precedent for LastStatementUpdateOption
1086+
// does not check against the actual value.
1087+
return o instanceof RequestIdOption;
1088+
}
1089+
}
10551090
}

‎google-cloud-spanner/src/main/java/com/google/cloud/spanner/PartitionedDmlTransaction.java

+14-2
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,20 @@ long executeStreamingPartitionedUpdate(
8181
Stopwatch stopwatch = Stopwatch.createStarted(ticker);
8282
Options options = Options.fromUpdateOptions(updateOptions);
8383

84+
XGoogSpannerRequestId reqId =
85+
session.getRequestIdCreator().nextRequestId(1 /*TODO: infer channelId*/, 0);
86+
8487
try {
8588
ExecuteSqlRequest request = newTransactionRequestFrom(statement, options);
8689

8790
while (true) {
91+
reqId.incrementAttempt();
8892
final Duration remainingTimeout = tryUpdateTimeout(timeout, stopwatch);
8993

9094
try {
9195
ServerStream<PartialResultSet> stream =
92-
rpc.executeStreamingPartitionedDml(request, session.getOptions(), remainingTimeout);
96+
rpc.executeStreamingPartitionedDml(
97+
request, reqId.withOptions(session.getOptions()), remainingTimeout);
9398

9499
for (PartialResultSet rs : stream) {
95100
if (rs.getResumeToken() != null && !rs.getResumeToken().isEmpty()) {
@@ -113,12 +118,17 @@ long executeStreamingPartitionedUpdate(
113118
LOGGER.log(
114119
Level.FINER, "Retrying PartitionedDml transaction after InternalException - EOS", e);
115120
request = resumeOrRestartRequest(resumeToken, statement, request, options);
121+
if (resumeToken.isEmpty()) {
122+
// Create a new xGoogSpannerRequestId.
123+
reqId = session.getRequestIdCreator().nextRequestId(1 /*TODO: infer channelId*/, 0);
124+
}
116125
} catch (AbortedException e) {
117126
LOGGER.log(Level.FINER, "Retrying PartitionedDml transaction after AbortedException", e);
118127
resumeToken = ByteString.EMPTY;
119128
foundStats = false;
120129
updateCount = 0L;
121130
request = newTransactionRequestFrom(statement, options);
131+
reqId = session.getRequestIdCreator().nextRequestId(1 /*TODO: infer channelId*/, 0);
122132
}
123133
}
124134
if (!foundStats) {
@@ -209,7 +219,9 @@ private ByteString initTransaction(final Options options) {
209219
.setExcludeTxnFromChangeStreams(
210220
options.withExcludeTxnFromChangeStreams() == Boolean.TRUE))
211221
.build();
212-
Transaction tx = rpc.beginTransaction(request, session.getOptions(), true);
222+
XGoogSpannerRequestId reqId =
223+
session.getRequestIdCreator().nextRequestId(1 /*TODO: infer channelId*/, 1);
224+
Transaction tx = rpc.beginTransaction(request, reqId.withOptions(session.getOptions()), true);
213225
if (tx.getId().isEmpty()) {
214226
throw SpannerExceptionFactory.newSpannerException(
215227
ErrorCode.INTERNAL,

‎google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java

+23-3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ abstract class ResumableStreamIterator extends AbstractIterator<PartialResultSet
7171
private CloseableIterator<PartialResultSet> stream;
7272
private ByteString resumeToken;
7373
private boolean finished;
74+
public XGoogSpannerRequestId xGoogRequestId;
75+
private XGoogSpannerRequestId.RequestIdCreator xGoogRequestIdCreator;
7476
/**
7577
* Indicates whether it is currently safe to retry RPCs. This will be {@code false} if we have
7678
* reached the maximum buffer size without seeing a restart token; in this case, we will drain the
@@ -85,7 +87,8 @@ protected ResumableStreamIterator(
8587
TraceWrapper tracer,
8688
ErrorHandler errorHandler,
8789
RetrySettings streamingRetrySettings,
88-
Set<Code> retryableCodes) {
90+
Set<Code> retryableCodes,
91+
XGoogSpannerRequestId.RequestIdCreator xGoogRequestIdCreator) {
8992
this(
9093
maxBufferSize,
9194
streamName,
@@ -94,7 +97,8 @@ protected ResumableStreamIterator(
9497
Attributes.empty(),
9598
errorHandler,
9699
streamingRetrySettings,
97-
retryableCodes);
100+
retryableCodes,
101+
xGoogRequestIdCreator);
98102
}
99103

100104
protected ResumableStreamIterator(
@@ -105,14 +109,16 @@ protected ResumableStreamIterator(
105109
Attributes attributes,
106110
ErrorHandler errorHandler,
107111
RetrySettings streamingRetrySettings,
108-
Set<Code> retryableCodes) {
112+
Set<Code> retryableCodes,
113+
XGoogSpannerRequestId.RequestIdCreator xGoogRequestIdCreator) {
109114
checkArgument(maxBufferSize >= 0);
110115
this.maxBufferSize = maxBufferSize;
111116
this.tracer = tracer;
112117
this.span = tracer.spanBuilderWithExplicitParent(streamName, parent, attributes);
113118
this.errorHandler = errorHandler;
114119
this.streamingRetrySettings = Preconditions.checkNotNull(streamingRetrySettings);
115120
this.retryableCodes = Preconditions.checkNotNull(retryableCodes);
121+
this.xGoogRequestIdCreator = xGoogRequestIdCreator;
116122
}
117123

118124
private ExponentialBackOff newBackOff() {
@@ -189,6 +195,14 @@ private void backoffSleep(Context context, long backoffMillis) throws SpannerExc
189195
}
190196
}
191197

198+
public void incrementXGoogRequestIdAttempt() {
199+
if (this.xGoogRequestId == null) {
200+
this.xGoogRequestId =
201+
this.xGoogRequestIdCreator.nextRequestId(1 /*TODO: infer channelId*/, 0 /*attempt*/);
202+
}
203+
this.xGoogRequestId.incrementAttempt();
204+
}
205+
192206
private enum DirectExecutor implements Executor {
193207
INSTANCE;
194208

@@ -281,6 +295,7 @@ protected PartialResultSet computeNext() {
281295
assert buffer.isEmpty() || buffer.getLast().getResumeToken().equals(resumeToken);
282296
stream = null;
283297
try (IScope s = tracer.withSpan(span)) {
298+
incrementXGoogRequestIdAttempt();
284299
long delay = spannerException.getRetryDelayInMillis();
285300
if (delay != -1) {
286301
backoffSleep(context, delay);
@@ -301,6 +316,7 @@ protected PartialResultSet computeNext() {
301316
if (++numAttemptsOnOtherChannel < errorHandler.getMaxAttempts()
302317
&& prepareIteratorForRetryOnDifferentGrpcChannel()) {
303318
stream = null;
319+
xGoogRequestId = null;
304320
continue;
305321
}
306322
}
@@ -326,6 +342,10 @@ private void startGrpcStreaming() {
326342
// When start a new stream set the Span as current to make the gRPC Span a child of
327343
// this Span.
328344
stream = checkNotNull(startStream(resumeToken, streamMessageListener));
345+
if (xGoogRequestId == null) {
346+
xGoogRequestId =
347+
this.xGoogRequestIdCreator.nextRequestId(1 /*TODO: retrieve channelId*/, 0);
348+
}
329349
stream.requestPrefetchChunks();
330350
}
331351
}

‎google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java

+33-10
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@
3131
import java.util.List;
3232
import java.util.Map;
3333
import java.util.concurrent.ScheduledExecutorService;
34+
import java.util.concurrent.atomic.AtomicInteger;
3435
import javax.annotation.concurrent.GuardedBy;
3536

3637
/** Client for creating single sessions and batches of sessions. */
37-
class SessionClient implements AutoCloseable {
38+
class SessionClient implements AutoCloseable, XGoogSpannerRequestId.RequestIdCreator {
3839
static class SessionId {
3940
private static final PathTemplate NAME_TEMPLATE =
4041
PathTemplate.create(
@@ -174,6 +175,12 @@ interface SessionConsumer {
174175
private final DatabaseId db;
175176
private final Attributes commonAttributes;
176177

178+
// SessionClient is created long before a DatabaseClientImpl is created,
179+
// as batch sessions are firstly created then later attached to each Client.
180+
private static AtomicInteger NTH_ID = new AtomicInteger(0);
181+
private final int nthId;
182+
private final AtomicInteger nthRequest;
183+
177184
@GuardedBy("this")
178185
private volatile long sessionChannelCounter;
179186

@@ -186,6 +193,8 @@ interface SessionConsumer {
186193
this.executorFactory = executorFactory;
187194
this.executor = executorFactory.get();
188195
this.commonAttributes = spanner.getTracer().createCommonAttributes(db);
196+
this.nthId = SessionClient.NTH_ID.incrementAndGet();
197+
this.nthRequest = new AtomicInteger(0);
189198
}
190199

191200
@Override
@@ -201,28 +210,38 @@ DatabaseId getDatabaseId() {
201210
return db;
202211
}
203212

213+
@Override
214+
public XGoogSpannerRequestId nextRequestId(long channelId, int attempt) {
215+
return XGoogSpannerRequestId.of(this.nthId, this.nthRequest.incrementAndGet(), channelId, 1);
216+
}
217+
204218
/** Create a single session. */
205219
SessionImpl createSession() {
206220
// The sessionChannelCounter could overflow, but that will just flip it to Integer.MIN_VALUE,
207221
// which is also a valid channel hint.
208222
final Map<SpannerRpc.Option, ?> options;
223+
final long channelId;
209224
synchronized (this) {
210225
options = optionMap(SessionOption.channelHint(sessionChannelCounter++));
226+
channelId = sessionChannelCounter;
211227
}
212228
ISpan span = spanner.getTracer().spanBuilder(SpannerImpl.CREATE_SESSION, this.commonAttributes);
213229
try (IScope s = spanner.getTracer().withSpan(span)) {
230+
XGoogSpannerRequestId reqId = this.nextRequestId(channelId, 1);
214231
com.google.spanner.v1.Session session =
215232
spanner
216233
.getRpc()
217234
.createSession(
218235
db.getName(),
219236
spanner.getOptions().getDatabaseRole(),
220237
spanner.getOptions().getSessionLabels(),
221-
options);
238+
reqId.withOptions(options));
222239
SessionReference sessionReference =
223240
new SessionReference(
224241
session.getName(), session.getCreateTime(), session.getMultiplexed(), options);
225-
return new SessionImpl(spanner, sessionReference);
242+
SessionImpl sessionImpl = new SessionImpl(spanner, sessionReference);
243+
sessionImpl.setRequestIdCreator(this);
244+
return sessionImpl;
226245
} catch (RuntimeException e) {
227246
span.setStatus(e);
228247
throw e;
@@ -273,6 +292,7 @@ SessionImpl createMultiplexedSession() {
273292
spanner,
274293
new SessionReference(
275294
session.getName(), session.getCreateTime(), session.getMultiplexed(), null));
295+
sessionImpl.setRequestIdCreator(this);
276296
span.addAnnotation(
277297
String.format("Request for %d multiplexed session returned %d session", 1, 1));
278298
return sessionImpl;
@@ -387,6 +407,8 @@ private List<SessionImpl> internalBatchCreateSessions(
387407
.spanBuilderWithExplicitParent(SpannerImpl.BATCH_CREATE_SESSIONS_REQUEST, parent);
388408
span.addAnnotation(String.format("Requesting %d sessions", sessionCount));
389409
try (IScope s = spanner.getTracer().withSpan(span)) {
410+
XGoogSpannerRequestId reqId =
411+
XGoogSpannerRequestId.of(this.nthId, this.nthRequest.incrementAndGet(), channelHint, 1);
390412
List<com.google.spanner.v1.Session> sessions =
391413
spanner
392414
.getRpc()
@@ -395,21 +417,20 @@ private List<SessionImpl> internalBatchCreateSessions(
395417
sessionCount,
396418
spanner.getOptions().getDatabaseRole(),
397419
spanner.getOptions().getSessionLabels(),
398-
options);
420+
reqId.withOptions(options));
399421
span.addAnnotation(
400422
String.format(
401423
"Request for %d sessions returned %d sessions", sessionCount, sessions.size()));
402424
span.end();
403425
List<SessionImpl> res = new ArrayList<>(sessionCount);
404426
for (com.google.spanner.v1.Session session : sessions) {
405-
res.add(
427+
SessionImpl sessionImpl =
406428
new SessionImpl(
407429
spanner,
408430
new SessionReference(
409-
session.getName(),
410-
session.getCreateTime(),
411-
session.getMultiplexed(),
412-
options)));
431+
session.getName(), session.getCreateTime(), session.getMultiplexed(), options));
432+
sessionImpl.setRequestIdCreator(this);
433+
res.add(sessionImpl);
413434
}
414435
return res;
415436
} catch (RuntimeException e) {
@@ -425,6 +446,8 @@ SessionImpl sessionWithId(String name) {
425446
synchronized (this) {
426447
options = optionMap(SessionOption.channelHint(sessionChannelCounter++));
427448
}
428-
return new SessionImpl(spanner, new SessionReference(name, options));
449+
SessionImpl sessionImpl = new SessionImpl(spanner, new SessionReference(name, options));
450+
sessionImpl.setRequestIdCreator(this);
451+
return sessionImpl;
429452
}
430453
}

‎google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java

+44-4
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,31 @@ interface SessionTransaction {
126126
private final Clock clock;
127127
private final Map<SpannerRpc.Option, ?> options;
128128
private final ErrorHandler errorHandler;
129+
private XGoogSpannerRequestId.RequestIdCreator requestIdCreator;
129130

130131
SessionImpl(SpannerImpl spanner, SessionReference sessionReference) {
131132
this(spanner, sessionReference, NO_CHANNEL_HINT);
132133
}
133134

134135
SessionImpl(SpannerImpl spanner, SessionReference sessionReference, int channelHint) {
136+
this(spanner, sessionReference, channelHint, new XGoogSpannerRequestId.NoopRequestIdCreator());
137+
}
138+
139+
SessionImpl(
140+
SpannerImpl spanner,
141+
SessionReference sessionReference,
142+
int channelHint,
143+
XGoogSpannerRequestId.RequestIdCreator requestIdCreator) {
135144
this.spanner = spanner;
136145
this.tracer = spanner.getTracer();
137146
this.sessionReference = sessionReference;
138147
this.clock = spanner.getOptions().getSessionPoolOptions().getPoolMaintainerClock();
139148
this.options = createOptions(sessionReference, channelHint);
140149
this.errorHandler = createErrorHandler(spanner.getOptions());
150+
this.requestIdCreator = requestIdCreator;
151+
if (this.requestIdCreator == null) {
152+
this.requestIdCreator = new XGoogSpannerRequestId.NoopRequestIdCreator();
153+
}
141154
}
142155

143156
static Map<SpannerRpc.Option, ?> createOptions(
@@ -288,8 +301,14 @@ public CommitResponse writeAtLeastOnceWithOptions(
288301
CommitRequest request = requestBuilder.build();
289302
ISpan span = tracer.spanBuilder(SpannerImpl.COMMIT);
290303
try (IScope s = tracer.withSpan(span)) {
304+
// TODO: Derive the channelId from the session being used currently.
305+
XGoogSpannerRequestId reqId = this.requestIdCreator.nextRequestId(1 /* channelId */, 0);
291306
return SpannerRetryHelper.runTxWithRetriesOnAborted(
292-
() -> new CommitResponse(spanner.getRpc().commit(request, getOptions())));
307+
() -> {
308+
reqId.incrementAttempt();
309+
return new CommitResponse(
310+
spanner.getRpc().commit(request, reqId.withOptions(getOptions())));
311+
});
293312
} catch (RuntimeException e) {
294313
span.setStatus(e);
295314
throw e;
@@ -325,6 +344,11 @@ public ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
325344
.setSession(getName())
326345
.addAllMutationGroups(mutationGroupsProto);
327346
RequestOptions batchWriteRequestOptions = getRequestOptions(transactionOptions);
347+
Options allOptions = Options.fromTransactionOptions(transactionOptions);
348+
XGoogSpannerRequestId reqId = allOptions.reqId();
349+
if (reqId == null) {
350+
reqId = this.requestIdCreator.nextRequestId(1 /* channelId */, 1);
351+
}
328352
if (batchWriteRequestOptions != null) {
329353
requestBuilder.setRequestOptions(batchWriteRequestOptions);
330354
}
@@ -334,7 +358,9 @@ public ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
334358
}
335359
ISpan span = tracer.spanBuilder(SpannerImpl.BATCH_WRITE);
336360
try (IScope s = tracer.withSpan(span)) {
337-
return spanner.getRpc().batchWriteAtLeastOnce(requestBuilder.build(), getOptions());
361+
return spanner
362+
.getRpc()
363+
.batchWriteAtLeastOnce(requestBuilder.build(), reqId.withOptions(getOptions()));
338364
} catch (Throwable e) {
339365
span.setStatus(e);
340366
throw SpannerExceptionFactory.newSpannerException(e);
@@ -442,7 +468,9 @@ public ApiFuture<Empty> asyncClose() {
442468
public void close() {
443469
ISpan span = tracer.spanBuilder(SpannerImpl.DELETE_SESSION);
444470
try (IScope s = tracer.withSpan(span)) {
445-
spanner.getRpc().deleteSession(getName(), getOptions());
471+
// TODO: Derive the channelId from the session being used currently.
472+
XGoogSpannerRequestId reqId = this.requestIdCreator.nextRequestId(1 /* channelId */, 0);
473+
spanner.getRpc().deleteSession(getName(), reqId.withOptions(getOptions()));
446474
} catch (RuntimeException e) {
447475
span.setStatus(e);
448476
throw e;
@@ -474,7 +502,11 @@ ApiFuture<Transaction> beginTransactionAsync(
474502
final BeginTransactionRequest request = requestBuilder.build();
475503
final ApiFuture<Transaction> requestFuture;
476504
try (IScope ignore = tracer.withSpan(span)) {
477-
requestFuture = spanner.getRpc().beginTransactionAsync(request, channelHint, routeToLeader);
505+
XGoogSpannerRequestId reqId = this.requestIdCreator.nextRequestId(1 /* channelId */, 1);
506+
requestFuture =
507+
spanner
508+
.getRpc()
509+
.beginTransactionAsync(request, reqId.withOptions(channelHint), routeToLeader);
478510
}
479511
requestFuture.addListener(
480512
() -> {
@@ -552,4 +584,12 @@ void onTransactionDone() {}
552584
TraceWrapper getTracer() {
553585
return tracer;
554586
}
587+
588+
public void setRequestIdCreator(XGoogSpannerRequestId.RequestIdCreator creator) {
589+
this.requestIdCreator = creator;
590+
}
591+
592+
public XGoogSpannerRequestId.RequestIdCreator getRequestIdCreator() {
593+
return this.requestIdCreator;
594+
}
555595
}

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

+17-4
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,11 @@ public void run() {
489489
final ApiFuture<com.google.spanner.v1.CommitResponse> commitFuture;
490490
final ISpan opSpan = tracer.spanBuilderWithExplicitParent(SpannerImpl.COMMIT, span);
491491
try (IScope ignore = tracer.withSpan(opSpan)) {
492-
commitFuture = rpc.commitAsync(commitRequest, getTransactionChannelHint());
492+
XGoogSpannerRequestId reqId =
493+
session.getRequestIdCreator().nextRequestId(1 /*TODO: channelId */, 0);
494+
reqId.incrementAttempt();
495+
commitFuture =
496+
rpc.commitAsync(commitRequest, reqId.withOptions(getTransactionChannelHint()));
493497
}
494498
session.markUsed(clock.instant());
495499
commitFuture.addListener(
@@ -916,8 +920,11 @@ private ResultSet internalExecuteUpdate(
916920
getExecuteSqlRequestBuilder(
917921
statement, queryMode, options, /* withTransactionSelector = */ true);
918922
try {
923+
XGoogSpannerRequestId reqId =
924+
session.getRequestIdCreator().nextRequestId(1 /*TODO: channelId */, 1);
919925
com.google.spanner.v1.ResultSet resultSet =
920-
rpc.executeQuery(builder.build(), getTransactionChannelHint(), isRouteToLeader());
926+
rpc.executeQuery(
927+
builder.build(), reqId.withOptions(getTransactionChannelHint()), isRouteToLeader());
921928
session.markUsed(clock.instant());
922929
if (resultSet.getMetadata().hasTransaction()) {
923930
onTransactionMetadata(
@@ -1049,8 +1056,10 @@ public long[] batchUpdate(Iterable<Statement> statements, UpdateOption... update
10491056
final ExecuteBatchDmlRequest.Builder builder =
10501057
getExecuteBatchDmlRequestBuilder(statements, options);
10511058
try {
1059+
XGoogSpannerRequestId reqId =
1060+
session.getRequestIdCreator().nextRequestId(1 /*TODO: channelId */, 1);
10521061
com.google.spanner.v1.ExecuteBatchDmlResponse response =
1053-
rpc.executeBatchDml(builder.build(), getTransactionChannelHint());
1062+
rpc.executeBatchDml(builder.build(), reqId.withOptions(getTransactionChannelHint()));
10541063
session.markUsed(clock.instant());
10551064
long[] results = new long[response.getResultSetsCount()];
10561065
for (int i = 0; i < response.getResultSetsCount(); ++i) {
@@ -1111,7 +1120,11 @@ public ApiFuture<long[]> batchUpdateAsync(
11111120
// Register the update as an async operation that must finish before the transaction may
11121121
// commit.
11131122
increaseAsyncOperations();
1114-
response = rpc.executeBatchDmlAsync(builder.build(), getTransactionChannelHint());
1123+
XGoogSpannerRequestId reqId =
1124+
session.getRequestIdCreator().nextRequestId(1 /*TODO: channelId */, 1);
1125+
response =
1126+
rpc.executeBatchDmlAsync(
1127+
builder.build(), reqId.withOptions(getTransactionChannelHint()));
11151128
session.markUsed(clock.instant());
11161129
} catch (Throwable t) {
11171130
decreaseAsyncOperations();

‎google-cloud-spanner/src/main/java/com/google/cloud/spanner/XGoogSpannerRequestId.java

+86
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,29 @@
1717
package com.google.cloud.spanner;
1818

1919
import com.google.api.core.InternalApi;
20+
import com.google.cloud.spanner.spi.v1.SpannerRpc;
2021
import com.google.common.annotations.VisibleForTesting;
22+
import io.grpc.Metadata;
2123
import java.math.BigInteger;
2224
import java.security.SecureRandom;
25+
import java.util.ArrayList;
26+
import java.util.HashMap;
27+
import java.util.List;
28+
import java.util.Map;
2329
import java.util.Objects;
30+
import java.util.regex.MatchResult;
31+
import java.util.regex.Matcher;
32+
import java.util.regex.Pattern;
2433

2534
@InternalApi
2635
public class XGoogSpannerRequestId {
2736
// 1. Generate the random process Id singleton.
2837
@VisibleForTesting
2938
static final String RAND_PROCESS_ID = XGoogSpannerRequestId.generateRandProcessId();
3039

40+
public static final Metadata.Key<String> REQUEST_HEADER_KEY =
41+
Metadata.Key.of("x-goog-spanner-request-id", Metadata.ASCII_STRING_MARSHALLER);
42+
3143
@VisibleForTesting
3244
static final long VERSION = 1; // The version of the specification being implemented.
3345

@@ -48,6 +60,26 @@ public static XGoogSpannerRequestId of(
4860
return new XGoogSpannerRequestId(nthClientId, nthChannelId, nthRequest, attempt);
4961
}
5062

63+
@VisibleForTesting
64+
static final Pattern REGEX =
65+
Pattern.compile("^(\\d)\\.([0-9a-z]{16})\\.(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)$");
66+
67+
public static XGoogSpannerRequestId of(String s) {
68+
Matcher m = XGoogSpannerRequestId.REGEX.matcher(s);
69+
if (!m.matches()) {
70+
throw new IllegalStateException(
71+
s + " does not match " + XGoogSpannerRequestId.REGEX.pattern());
72+
}
73+
74+
MatchResult mr = m.toMatchResult();
75+
76+
return new XGoogSpannerRequestId(
77+
Long.parseLong(mr.group(3)),
78+
Long.parseLong(mr.group(4)),
79+
Long.parseLong(mr.group(5)),
80+
Long.parseLong(mr.group(6)));
81+
}
82+
5183
private static String generateRandProcessId() {
5284
// Expecting to use 64-bits of randomness to avoid clashes.
5385
BigInteger bigInt = new BigInteger(64, new SecureRandom());
@@ -66,6 +98,13 @@ public String toString() {
6698
this.attempt);
6799
}
68100

101+
private boolean isGreaterThan(XGoogSpannerRequestId other) {
102+
return this.nthClientId > other.nthClientId
103+
&& this.nthChannelId > other.nthChannelId
104+
&& this.nthRequest > other.nthRequest
105+
&& this.attempt > other.attempt;
106+
}
107+
69108
@Override
70109
public boolean equals(Object other) {
71110
// instanceof for a null object returns false.
@@ -81,8 +120,55 @@ public boolean equals(Object other) {
81120
&& Objects.equals(this.attempt, otherReqId.attempt);
82121
}
83122

123+
public void incrementAttempt() {
124+
this.attempt++;
125+
}
126+
127+
@SuppressWarnings("unchecked")
128+
public Map withOptions(Map options) {
129+
Map copyOptions = new HashMap<>();
130+
copyOptions.putAll(options);
131+
copyOptions.put(SpannerRpc.Option.REQUEST_ID, this.toString());
132+
return copyOptions;
133+
}
134+
84135
@Override
85136
public int hashCode() {
86137
return Objects.hash(this.nthClientId, this.nthChannelId, this.nthRequest, this.attempt);
87138
}
139+
140+
public interface RequestIdCreator {
141+
XGoogSpannerRequestId nextRequestId(long channelId, int attempt);
142+
}
143+
144+
public static class NoopRequestIdCreator implements RequestIdCreator {
145+
NoopRequestIdCreator() {}
146+
147+
@Override
148+
public XGoogSpannerRequestId nextRequestId(long channelId, int attempt) {
149+
return XGoogSpannerRequestId.of(1, 1, 1, 0);
150+
}
151+
}
152+
153+
public static void assertMonotonicityOfIds(String prefix, List<XGoogSpannerRequestId> reqIds) {
154+
int size = reqIds.size();
155+
156+
List<String> violations = new ArrayList<>();
157+
for (int i = 1; i < size; i++) {
158+
XGoogSpannerRequestId prev = reqIds.get(i - 1);
159+
XGoogSpannerRequestId curr = reqIds.get(i);
160+
if (prev.isGreaterThan(curr)) {
161+
violations.add(String.format("#%d(%s) > #%d(%s)", i - 1, prev, i, curr));
162+
}
163+
}
164+
165+
if (violations.size() == 0) {
166+
return;
167+
}
168+
169+
throw new IllegalStateException(
170+
prefix
171+
+ " monotonicity violation:"
172+
+ String.join("\n\t", violations.toArray(new String[0])));
173+
}
88174
}

‎google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java

+19
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
import com.google.cloud.spanner.SpannerOptions;
7272
import com.google.cloud.spanner.SpannerOptions.CallContextConfigurator;
7373
import com.google.cloud.spanner.SpannerOptions.CallCredentialsProvider;
74+
import com.google.cloud.spanner.XGoogSpannerRequestId;
7475
import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStub;
7576
import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings;
7677
import com.google.cloud.spanner.admin.database.v1.stub.GrpcDatabaseAdminCallableFactory;
@@ -88,6 +89,7 @@
8889
import com.google.common.base.Supplier;
8990
import com.google.common.base.Suppliers;
9091
import com.google.common.collect.ImmutableList;
92+
import com.google.common.collect.ImmutableMap;
9193
import com.google.common.collect.ImmutableSet;
9294
import com.google.common.io.Resources;
9395
import com.google.common.util.concurrent.RateLimiter;
@@ -402,6 +404,8 @@ public GapicSpannerRpc(final SpannerOptions options) {
402404
final String emulatorHost = System.getenv("SPANNER_EMULATOR_HOST");
403405

404406
try {
407+
// TODO: make our retry settings to inject and increment
408+
// XGoogSpannerRequestId whenever a retry occurs.
405409
SpannerStubSettings spannerStubSettings =
406410
options
407411
.getSpannerStubSettings()
@@ -2018,6 +2022,8 @@ <ReqT, RespT> GrpcCallContext newCallContext(
20182022
// Set channel affinity in GAX.
20192023
context = context.withChannelAffinity(Option.CHANNEL_HINT.getLong(options).intValue());
20202024
}
2025+
String methodName = method.getFullMethodName();
2026+
context = withRequestId(context, options, methodName);
20212027
}
20222028
if (compressorName != null) {
20232029
// This sets the compressor for Client -> Server.
@@ -2041,6 +2047,7 @@ <ReqT, RespT> GrpcCallContext newCallContext(
20412047
context
20422048
.withStreamWaitTimeoutDuration(waitTimeout)
20432049
.withStreamIdleTimeoutDuration(idleTimeout);
2050+
20442051
CallContextConfigurator configurator = SpannerOptions.CALL_CONTEXT_CONFIGURATOR_KEY.get();
20452052
ApiCallContext apiCallContextFromContext = null;
20462053
if (configurator != null) {
@@ -2049,6 +2056,18 @@ <ReqT, RespT> GrpcCallContext newCallContext(
20492056
return (GrpcCallContext) context.merge(apiCallContextFromContext);
20502057
}
20512058

2059+
GrpcCallContext withRequestId(GrpcCallContext context, Map options, String methodName) {
2060+
String reqId = (String) options.get(Option.REQUEST_ID);
2061+
if (reqId == null || Objects.equals(reqId, "")) {
2062+
return context;
2063+
}
2064+
2065+
Map<String, List<String>> withReqId =
2066+
ImmutableMap.of(
2067+
XGoogSpannerRequestId.REQUEST_HEADER_KEY.name(), Collections.singletonList(reqId));
2068+
return context.withExtraHeaders(withReqId);
2069+
}
2070+
20522071
void registerResponseObserver(SpannerResponseObserver responseObserver) {
20532072
responseObservers.add(responseObserver);
20542073
}

‎google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@
7878
public interface SpannerRpc extends ServiceRpc {
7979
/** Options passed in {@link SpannerRpc} methods to control how an RPC is issued. */
8080
enum Option {
81-
CHANNEL_HINT("Channel Hint");
81+
CHANNEL_HINT("Channel Hint"),
82+
REQUEST_ID("Request Id");
8283

8384
private final String value;
8485

‎google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java

+43-1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
import io.grpc.Metadata;
106106
import io.grpc.MethodDescriptor;
107107
import io.grpc.Server;
108+
import io.grpc.ServerInterceptors;
108109
import io.grpc.Status;
109110
import io.grpc.StatusRuntimeException;
110111
import io.grpc.inprocess.InProcessServerBuilder;
@@ -119,6 +120,7 @@
119120
import java.util.Arrays;
120121
import java.util.Base64;
121122
import java.util.Collections;
123+
import java.util.HashSet;
122124
import java.util.List;
123125
import java.util.Random;
124126
import java.util.Set;
@@ -152,6 +154,7 @@ public class DatabaseClientImplTest {
152154
private static final String DATABASE_NAME =
153155
String.format(
154156
"projects/%s/instances/%s/databases/%s", TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE);
157+
private static XGoogSpannerRequestIdTest.ServerHeaderEnforcer xGoogReqIdInterceptor;
155158
private static MockSpannerServiceImpl mockSpanner;
156159
private static Server server;
157160
private static LocalChannelProvider channelProvider;
@@ -220,13 +223,27 @@ public static void startStaticServer() throws IOException {
220223
StatementResult.query(SELECT1_FROM_TABLE, MockSpannerTestUtil.SELECT1_RESULTSET));
221224
mockSpanner.setBatchWriteResult(BATCH_WRITE_RESPONSES);
222225

226+
Set<String> checkMethods =
227+
new HashSet(
228+
Arrays.asList(
229+
"google.spanner.v1.Spanner/BatchCreateSessions",
230+
"google.spanner.v1.Spanner/DeleteSession",
231+
"google.spanner.v1.Spanner/CreateSession",
232+
"google.spanner.v1.Spanner/ExecuteStreamingSql",
233+
"google.spanner.v1.Spanner/BeginTransaction",
234+
"google.spanner.v1.Spanner/ExecuteSql",
235+
"google.spanner.v1.Spanner/BatchWrite",
236+
"google.spanner.v1.Spanner/StreamingRead",
237+
"google.spanner.v1.Spanner/ExecuteBatchDml",
238+
"google.spanner.v1.Spanner/Commit"));
239+
xGoogReqIdInterceptor = new XGoogSpannerRequestIdTest.ServerHeaderEnforcer(checkMethods);
223240
executor = Executors.newSingleThreadExecutor();
224241
String uniqueName = InProcessServerBuilder.generateName();
225242
server =
226243
InProcessServerBuilder.forName(uniqueName)
227244
// We need to use a real executor for timeouts to occur.
228245
.scheduledExecutorService(new ScheduledThreadPoolExecutor(1))
229-
.addService(mockSpanner)
246+
.addService(ServerInterceptors.intercept(mockSpanner, xGoogReqIdInterceptor))
230247
.build()
231248
.start();
232249
channelProvider = LocalChannelProvider.create(uniqueName);
@@ -266,6 +283,7 @@ public void tearDown() {
266283
spanner.close();
267284
spannerWithEmptySessionPool.close();
268285
mockSpanner.reset();
286+
xGoogReqIdInterceptor.reset();
269287
mockSpanner.removeAllExecutionTimes();
270288
}
271289

@@ -1393,6 +1411,10 @@ public void testWriteAtLeastOnceAborted() {
13931411

13941412
List<CommitRequest> commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class);
13951413
assertEquals(2, commitRequests.size());
1414+
xGoogReqIdInterceptor.assertIntegrity();
1415+
System.out.println(
1416+
"\033[33mGot: " + xGoogReqIdInterceptor.accumulatedUnaryValues() + "\033[00m");
1417+
xGoogReqIdInterceptor.printAccumulatedValues();
13961418
}
13971419

13981420
@Test
@@ -5057,6 +5079,26 @@ public void testRetryOnResourceExhausted() {
50575079
}
50585080
}
50595081

5082+
@Test
5083+
public void testSelectHasXGoogRequestIdHeader() {
5084+
Statement statement =
5085+
Statement.newBuilder("select id from test where b=@p1")
5086+
.bind("p1")
5087+
.toBytesArray(
5088+
Arrays.asList(ByteArray.copyFrom("test1"), null, ByteArray.copyFrom("test2")))
5089+
.build();
5090+
mockSpanner.putStatementResult(StatementResult.query(statement, SELECT1_RESULTSET));
5091+
DatabaseClient client =
5092+
spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
5093+
try (ResultSet resultSet = client.singleUse().executeQuery(statement)) {
5094+
assertTrue(resultSet.next());
5095+
assertEquals(1L, resultSet.getLong(0));
5096+
assertFalse(resultSet.next());
5097+
} finally {
5098+
xGoogReqIdInterceptor.assertIntegrity();
5099+
}
5100+
}
5101+
50605102
@Test
50615103
public void testSessionPoolExhaustedError_containsStackTraces() {
50625104
assumeFalse(

‎google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResumableStreamIteratorTest.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ private void initWithLimit(int maxBufferSize) {
162162
new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""), false),
163163
DefaultErrorHandler.INSTANCE,
164164
SpannerStubSettings.newBuilder().executeStreamingSqlSettings().getRetrySettings(),
165-
SpannerStubSettings.newBuilder().executeStreamingSqlSettings().getRetryableCodes()) {
165+
SpannerStubSettings.newBuilder().executeStreamingSqlSettings().getRetryableCodes(),
166+
new XGoogSpannerRequestId.NoopRequestIdCreator()) {
166167
@Override
167168
AbstractResultSet.CloseableIterator<PartialResultSet> startStream(
168169
@Nullable ByteString resumeToken,

‎google-cloud-spanner/src/test/java/com/google/cloud/spanner/XGoogSpannerRequestIdTest.java

+147-4
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,29 @@
1818

1919
import static org.junit.Assert.assertEquals;
2020
import static org.junit.Assert.assertNotEquals;
21+
import static org.junit.Assert.assertNotNull;
2122
import static org.junit.Assert.assertTrue;
2223

24+
import io.grpc.Metadata;
25+
import io.grpc.MethodDescriptor.MethodType;
26+
import io.grpc.ServerCall;
27+
import io.grpc.ServerCallHandler;
28+
import io.grpc.ServerInterceptor;
29+
import io.grpc.Status;
30+
import java.util.ArrayList;
31+
import java.util.List;
32+
import java.util.Map;
33+
import java.util.Objects;
34+
import java.util.Set;
35+
import java.util.concurrent.ConcurrentHashMap;
36+
import java.util.concurrent.CopyOnWriteArrayList;
2337
import java.util.regex.Matcher;
24-
import java.util.regex.Pattern;
2538
import org.junit.Test;
2639
import org.junit.runner.RunWith;
2740
import org.junit.runners.JUnit4;
2841

2942
@RunWith(JUnit4.class)
3043
public class XGoogSpannerRequestIdTest {
31-
private static final Pattern REGEX_RAND_PROCESS_ID =
32-
Pattern.compile("1.([0-9a-z]{16})(\\.\\d+){3}\\.(\\d+)$");
3344

3445
@Test
3546
public void testEquals() {
@@ -48,7 +59,139 @@ public void testEquals() {
4859
@Test
4960
public void testEnsureHexadecimalFormatForRandProcessID() {
5061
String str = XGoogSpannerRequestId.of(1, 2, 3, 4).toString();
51-
Matcher m = XGoogSpannerRequestIdTest.REGEX_RAND_PROCESS_ID.matcher(str);
62+
Matcher m = XGoogSpannerRequestId.REGEX.matcher(str);
5263
assertTrue(m.matches());
5364
}
65+
66+
public static class ServerHeaderEnforcer implements ServerInterceptor {
67+
private Map<String, CopyOnWriteArrayList<XGoogSpannerRequestId>> unaryResults;
68+
private Map<String, CopyOnWriteArrayList<XGoogSpannerRequestId>> streamingResults;
69+
private List<String> gotValues;
70+
private Set<String> checkMethods;
71+
72+
ServerHeaderEnforcer(Set<String> checkMethods) {
73+
this.gotValues = new CopyOnWriteArrayList<String>();
74+
this.unaryResults =
75+
new ConcurrentHashMap<String, CopyOnWriteArrayList<XGoogSpannerRequestId>>();
76+
this.streamingResults =
77+
new ConcurrentHashMap<String, CopyOnWriteArrayList<XGoogSpannerRequestId>>();
78+
this.checkMethods = checkMethods;
79+
}
80+
81+
@Override
82+
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
83+
ServerCall<ReqT, RespT> call,
84+
final Metadata requestHeaders,
85+
ServerCallHandler<ReqT, RespT> next) {
86+
boolean isUnary = call.getMethodDescriptor().getType() == MethodType.UNARY;
87+
String methodName = call.getMethodDescriptor().getFullMethodName();
88+
String gotReqIdStr = requestHeaders.get(XGoogSpannerRequestId.REQUEST_HEADER_KEY);
89+
if (!this.checkMethods.contains(methodName)) {
90+
System.out.println(
91+
"\033[35mBypassing " + methodName + " but has " + gotReqIdStr + "\033[00m");
92+
return next.startCall(call, requestHeaders);
93+
}
94+
95+
Map<String, CopyOnWriteArrayList<XGoogSpannerRequestId>> saver = this.streamingResults;
96+
if (isUnary) {
97+
saver = this.unaryResults;
98+
}
99+
100+
if (Objects.equals(gotReqIdStr, null) || Objects.equals(gotReqIdStr, "")) {
101+
Status status =
102+
Status.fromCode(Status.Code.INVALID_ARGUMENT)
103+
.augmentDescription(
104+
methodName + " lacks " + XGoogSpannerRequestId.REQUEST_HEADER_KEY);
105+
call.close(status, requestHeaders);
106+
return next.startCall(call, requestHeaders);
107+
}
108+
109+
assertNotNull(gotReqIdStr);
110+
// Firstly assert and validate that at least we've got a requestId.
111+
Matcher m = XGoogSpannerRequestId.REGEX.matcher(gotReqIdStr);
112+
assertTrue(m.matches());
113+
114+
XGoogSpannerRequestId reqId = XGoogSpannerRequestId.of(gotReqIdStr);
115+
if (!saver.containsKey(methodName)) {
116+
saver.put(methodName, new CopyOnWriteArrayList<XGoogSpannerRequestId>());
117+
}
118+
119+
saver.get(methodName).add(reqId);
120+
121+
// Finally proceed with the call.
122+
return next.startCall(call, requestHeaders);
123+
}
124+
125+
public String[] accumulatedValues() {
126+
return this.gotValues.toArray(new String[0]);
127+
}
128+
129+
public void assertIntegrity() {
130+
this.unaryResults.forEach(
131+
(String method, CopyOnWriteArrayList<XGoogSpannerRequestId> values) -> {
132+
// System.out.println("\033[36munary.method: " + method + "\033[00m");
133+
XGoogSpannerRequestId.assertMonotonicityOfIds(method, values);
134+
});
135+
this.streamingResults.forEach(
136+
(String method, CopyOnWriteArrayList<XGoogSpannerRequestId> values) -> {
137+
// System.out.println("\033[36mstreaming.method: " + method + "\033[00m");
138+
XGoogSpannerRequestId.assertMonotonicityOfIds(method, values);
139+
});
140+
}
141+
142+
public static class methodAndRequestId {
143+
String method;
144+
String requestId;
145+
146+
public methodAndRequestId(String method, String requestId) {
147+
this.method = method;
148+
this.requestId = requestId;
149+
}
150+
151+
public String toString() {
152+
return "{" + this.method + ":" + this.requestId + "}";
153+
}
154+
}
155+
156+
public methodAndRequestId[] accumulatedUnaryValues() {
157+
List<methodAndRequestId> accumulated = new ArrayList();
158+
this.unaryResults.forEach(
159+
(String method, CopyOnWriteArrayList<XGoogSpannerRequestId> values) -> {
160+
for (int i = 0; i < values.size(); i++) {
161+
accumulated.add(new methodAndRequestId(method, values.get(i).toString()));
162+
}
163+
});
164+
return accumulated.toArray(new methodAndRequestId[0]);
165+
}
166+
167+
public methodAndRequestId[] accumulatedStreamingValues() {
168+
List<methodAndRequestId> accumulated = new ArrayList();
169+
this.streamingResults.forEach(
170+
(String method, CopyOnWriteArrayList<XGoogSpannerRequestId> values) -> {
171+
for (int i = 0; i < values.size(); i++) {
172+
accumulated.add(new methodAndRequestId(method, values.get(i).toString()));
173+
}
174+
});
175+
return accumulated.toArray(new methodAndRequestId[0]);
176+
}
177+
178+
public void printAccumulatedValues() {
179+
methodAndRequestId[] unary = this.accumulatedUnaryValues();
180+
System.out.println("accumulatedUnaryvalues");
181+
for (int i = 0; i < unary.length; i++) {
182+
System.out.println("\t" + unary[i].toString());
183+
}
184+
methodAndRequestId[] streaming = this.accumulatedStreamingValues();
185+
System.out.println("accumulatedStreaminvalues");
186+
for (int i = 0; i < streaming.length; i++) {
187+
System.out.println("\t" + streaming[i].toString());
188+
}
189+
}
190+
191+
public void reset() {
192+
this.gotValues.clear();
193+
this.unaryResults.clear();
194+
this.streamingResults.clear();
195+
}
196+
}
54197
}

0 commit comments

Comments
 (0)
Please sign in to comment.