feat(spanner): implement x-goog-spanner-request-id header propagation and retry tracking - #9446
thecodewreck wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements robust request ID tracking and injection across Spanner operations, including support for 64-bit process IDs, environment variable overrides, and a gRPC interceptor to automatically increment attempt numbers on retries. Feedback on these changes includes resolving the parent database from session instances to correctly extract client and channel IDs, using isNaN to avoid coercing a valid attempt number of 0 to 1, preferring the imported Status enum from @grpc/grpc-js over grpc.status, and utilizing a deep copy of configuration objects to prevent accidental mutation of user-provided headers.
d88c3f5 to
065c7f1
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request enhances the Spanner client's request ID tracking by introducing a gRPC interceptor (createRequestIdInterceptor) to automatically increment attempt numbers on retries, appending request IDs to error messages, and updating request ID injection across transactions, databases, and sessions. It also includes comprehensive test coverage for these changes. The review feedback highlights several improvement opportunities: returning a shallow copy of headers in injectRequestIDIntoHeaders when session is falsy to prevent accidental mutations of shared headers, robustly resolving actualDatabase to handle cases where session is already a Database instance, fixing a potential TypeScript compilation issue in nextNthRequest, and removing the redundant _nthClientId property in favor of the existing _clientId property.
… and retry tracking
065c7f1 to
71cd53a
Compare
|
|
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request enhances request ID tracking in the Spanner client by introducing a gRPC interceptor (createRequestIdInterceptor) to increment attempt numbers on retries, supporting 64-bit hex process IDs with environment variable overrides, and appending request IDs to error messages. It also updates various operations (like batch writes, commits, and rollbacks) to properly inject and propagate these headers. The review feedback identifies a critical bug in the newly added createRequestIdInterceptor where mutating the shared metadata object in-place during retries causes incorrect attempt numbers, and suggests caching the base request ID and initial attempt on the first call to resolve the issue.
| function createRequestIdInterceptor(config: any) { | ||
| let attemptCount = 0; | ||
| return (options: any, nextCall: any) => { | ||
| return new grpc.InterceptingCall(nextCall(options), { | ||
| start: function (metadata: grpc.Metadata, listener: any, next: any) { | ||
| attemptCount++; | ||
| const currentReqIds = metadata.get(X_GOOG_SPANNER_REQUEST_ID_HEADER); | ||
| if (currentReqIds && currentReqIds.length > 0) { | ||
| const currentReqId = String(currentReqIds[0]); | ||
| const lastDot = currentReqId.lastIndexOf('.'); | ||
| if (lastDot !== -1) { | ||
| const base = currentReqId.substring(0, lastDot); | ||
| const parsedAttempt = parseInt( | ||
| currentReqId.substring(lastDot + 1), | ||
| 10, | ||
| ); | ||
| const initialAttempt = isNaN(parsedAttempt) ? 1 : parsedAttempt; | ||
| const newAttempt = initialAttempt + (attemptCount - 1); | ||
| const newReqId = `${base}.${newAttempt}`; | ||
| metadata.set(X_GOOG_SPANNER_REQUEST_ID_HEADER, newReqId); | ||
| if (config && config.headers) { | ||
| config.headers[X_GOOG_SPANNER_REQUEST_ID_HEADER] = newReqId; | ||
| } | ||
| } | ||
| } | ||
| next(metadata, listener); | ||
| }, | ||
| }); | ||
| }; | ||
| } |
There was a problem hiding this comment.
There is a bug in createRequestIdInterceptor when google-gax retries a request. Since google-gax reuses the same metadata instance across retries, mutating the metadata in-place on the first attempt means that subsequent attempts will read the already-mutated request ID (e.g., ending in .2 instead of .1). This causes parsedAttempt to increase on each retry, leading to incorrect/exponentially growing attempt numbers (e.g., .1, .2, .4, .7 instead of .1, .2, .3, .4).
To fix this, we should parse and cache the original base request ID and initial attempt number on the very first call, and then use those cached values to compute the attempt number for all subsequent retries.
function createRequestIdInterceptor(config: any) {
let attemptCount = 0;
let baseRequestId: string | null = null;
let initialAttempt = 1;
return (options: any, nextCall: any) => {
return new grpc.InterceptingCall(nextCall(options), {
start: function (metadata: grpc.Metadata, listener: any, next: any) {
attemptCount++;
if (baseRequestId === null) {
const currentReqIds = metadata.get(X_GOOG_SPANNER_REQUEST_ID_HEADER);
if (currentReqIds && currentReqIds.length > 0) {
const currentReqId = String(currentReqIds[0]);
const lastDot = currentReqId.lastIndexOf('.');
if (lastDot !== -1) {
baseRequestId = currentReqId.substring(0, lastDot);
const parsedAttempt = parseInt(
currentReqId.substring(lastDot + 1),
10
);
initialAttempt = isNaN(parsedAttempt) ? 1 : parsedAttempt;
}
}
}
if (baseRequestId !== null) {
const newAttempt = initialAttempt + (attemptCount - 1);
const newReqId = baseRequestId + '.' + newAttempt;
metadata.set(X_GOOG_SPANNER_REQUEST_ID_HEADER, newReqId);
if (config && config.headers) {
config.headers[X_GOOG_SPANNER_REQUEST_ID_HEADER] = newReqId;
}
}
next(metadata, listener);
},
});
};
}
Description
Implements the end-to-end
x-goog-spanner-request-idheader specification across all Google Cloud Spanner Node.js client operations, enabling end-to-end gRPC RPC tracing, attempt tracking, and deterministic error correlation.Key Changes
Header Format Compliance:
<version>.<process>.<client>.<channel>.<request>.<attempt>.<version>is fixed to1.<process>generates a 64-bit random integer formatted as a 16-character hexadecimal string (%016x), matching the Go and Java implementations. Supports custom overrides viaSPANNER_PROCESS_IDandGOOGLE_CLOUD_SPANNER_PROCESS_IDenvironment variables.<client>sequentially increments perDatabase/Clientinstance in the process.<channel>defaults to0(Node.js gRPC channel pool).<request>monotonically increments per distinct user request / RPC initiated by the client.<attempt>tracks individual call attempts across retries (starting at1).Full API & Workload Coverage:
database.run), Read (table.read), direct table mutations (table.upsert,table.insert, etc.), and partitioned operations (runPartitionedUpdate,partitionRead,partitionQuery).executeBatchDml), Batch writes (batchWriteAtLeastOnce), transaction rollbacks (transaction.rollback), and transaction commits (transaction.commit) including retry tracking across aborted transaction replays.(x-goog-spanner-request-id: <reqId>)to gRPC error messages upon failure.gcp.spanner.request_id).Concurrency & Thread Safety:
<request>identifiers during high-concurrency parallel queries without duplicate IDs or race conditions.Verification & Testing
test/request_id_header.ts(12/12 passing)test/spanner.ts(15/15 passing forXGoogRequestIdsuite)Modified Files
handwritten/spanner/src/batch-transaction.tshandwritten/spanner/src/database.tshandwritten/spanner/src/index.tshandwritten/spanner/src/request_id_header.tshandwritten/spanner/src/session.tshandwritten/spanner/src/transaction.tshandwritten/spanner/test/request_id_header.tshandwritten/spanner/test/spanner.ts