Skip to content

Commit 505ef1f

Browse files
authored
Merge branch 'storage-node-18' into node18/system-test
2 parents 1db4d38 + 106c994 commit 505ef1f

7 files changed

Lines changed: 454 additions & 26 deletions

File tree

handwritten/storage/src/bucket.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import * as http from 'http';
2929
import * as path from 'path';
3030
import {promisify} from 'util';
3131
import AsyncRetry from 'async-retry';
32+
import {randomUUID} from 'crypto';
3233
import {convertObjKeysToSnakeCase, handleContextValidation} from './util.js';
3334

3435
import {Acl, AclMetadata} from './acl.js';
@@ -38,6 +39,7 @@ import {
3839
FileOptions,
3940
CreateResumableUploadOptions,
4041
CreateWriteStreamOptions,
42+
CreateWriteStreamOptionsInternal,
4143
FileMetadata,
4244
ContextValue,
4345
} from './file.js';
@@ -4511,6 +4513,7 @@ class Bucket extends ServiceObject<Bucket, BucketMetadata> {
45114513
optionsOrCallback?: UploadOptions | UploadCallback,
45124514
callback?: UploadCallback,
45134515
): Promise<UploadResponse> | void {
4516+
const persistentInvocationId = randomUUID();
45144517
const upload = (numberOfRetries: number | undefined) => {
45154518
const returnValue = AsyncRetry(
45164519
async (bail: (err: GaxiosError | Error) => void) => {
@@ -4521,7 +4524,10 @@ class Bucket extends ServiceObject<Bucket, BucketMetadata> {
45214524
) {
45224525
newFile.storage.retryOptions.autoRetry = false;
45234526
}
4524-
const writable = newFile.createWriteStream(options);
4527+
const writable = newFile.createWriteStream({
4528+
...options,
4529+
invocationId: persistentInvocationId,
4530+
} as CreateWriteStreamOptionsInternal);
45254531
if (options.onUploadProgress) {
45264532
writable.on('progress', options.onUploadProgress);
45274533
}

handwritten/storage/src/file.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import * as resumableUpload from './resumable-upload.js';
2727
import {Writable, Readable, pipeline, Transform, PipelineSource} from 'stream';
2828
import * as zlib from 'zlib';
2929
import * as http from 'http';
30+
import {randomUUID} from 'crypto';
3031

3132
import {
3233
ExceptionMessages,
@@ -266,6 +267,14 @@ export interface CreateWriteStreamOptions extends CreateResumableUploadOptions {
266267
validation?: string | boolean;
267268
}
268269

270+
/**
271+
* @internal
272+
*/
273+
export interface CreateWriteStreamOptionsInternal
274+
extends CreateWriteStreamOptions {
275+
invocationId?: string;
276+
}
277+
269278
export interface MakeFilePrivateOptions {
270279
metadata?: FileMetadata;
271280
strict?: boolean;
@@ -2213,7 +2222,10 @@ class File extends ServiceObject<File, FileMetadata> {
22132222

22142223
writeStream.once('writing', async () => {
22152224
if (options.resumable === false) {
2216-
await this.startSimpleUpload_(fileWriteStream, options);
2225+
await this.startSimpleUpload_(
2226+
fileWriteStream,
2227+
options as CreateWriteStreamOptionsInternal,
2228+
);
22172229
} else {
22182230
await this.startResumableUpload_(fileWriteStream, options);
22192231
}
@@ -4219,13 +4231,17 @@ class File extends ServiceObject<File, FileMetadata> {
42194231
) {
42204232
maxRetries = 0;
42214233
}
4234+
const persistentInvocationId = randomUUID();
42224235
const returnValue = AsyncRetry(
42234236
async (bail: (err: Error) => void) => {
42244237
return new Promise<void>((resolve, reject) => {
42254238
if (maxRetries === 0) {
42264239
this.storage.retryOptions.autoRetry = false;
42274240
}
4228-
const writable = this.createWriteStream(options);
4241+
const writable = this.createWriteStream({
4242+
...options,
4243+
invocationId: persistentInvocationId,
4244+
} as CreateWriteStreamOptionsInternal);
42294245

42304246
if (options.onUploadProgress) {
42314247
writable.on('progress', options.onUploadProgress);
@@ -4532,7 +4548,7 @@ class File extends ServiceObject<File, FileMetadata> {
45324548
*/
45334549
startSimpleUpload_(
45344550
dup: Duplexify,
4535-
options: CreateWriteStreamOptions = {},
4551+
options: CreateWriteStreamOptionsInternal = {},
45364552
): void {
45374553
options.metadata ??= {};
45384554

@@ -4546,6 +4562,7 @@ class File extends ServiceObject<File, FileMetadata> {
45464562
uploadType: 'multipart',
45474563
},
45484564
url,
4565+
invocationId: options.invocationId,
45494566
[GCCL_GCS_CMD_KEY]: options[GCCL_GCS_CMD_KEY],
45504567
method: 'POST',
45514568
responseType: 'json',

handwritten/storage/src/storage-transport.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export interface StorageQueryParameters extends StandardStorageQueryParams {
4949

5050
export interface StorageRequestOptions extends GaxiosOptions {
5151
[GCCL_GCS_CMD_KEY]?: string;
52+
invocationId?: string;
5253
interceptors?: GaxiosInterceptor<GaxiosOptionsPrepared>[];
5354
autoPaginate?: boolean;
5455
autoPaginateVal?: boolean;
@@ -273,7 +274,10 @@ export class StorageTransport {
273274
}
274275

275276
#prepareHeaders(reqOpts: StorageRequestOptions): Record<string, string> {
276-
const headersObj = this.#buildRequestHeaders(reqOpts.headers);
277+
const headersObj = this.#buildRequestHeaders(
278+
reqOpts.headers,
279+
reqOpts.invocationId,
280+
);
277281

278282
if (reqOpts[GCCL_GCS_CMD_KEY]) {
279283
const current = headersObj.get('x-goog-api-client') || '';
@@ -318,12 +322,16 @@ export class StorageTransport {
318322
return searchParams.toString();
319323
};
320324

321-
#buildRequestHeaders(requestHeaders = {}) {
322-
const headers = new Headers(requestHeaders);
325+
#buildRequestHeaders(
326+
reqHeaders?: GaxiosOptions['headers'],
327+
invocationId?: string,
328+
) {
329+
const headers = new Headers(reqHeaders);
323330
headers.set('User-Agent', this.#getUserAgentString());
331+
const finalInvocationId = invocationId || randomUUID();
324332
headers.set(
325333
'x-goog-api-client',
326-
`${getRuntimeTrackingString()} gccl/${this.packageJson.version}-${getModuleFormat()} gccl-invocation-id/${randomUUID()}`,
334+
`${getRuntimeTrackingString()} gccl/${this.packageJson.version}-${getModuleFormat()} gccl-invocation-id/${finalInvocationId}`,
327335
);
328336
return headers;
329337
}

handwritten/storage/system-test/storage.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3436,6 +3436,42 @@ describe('storage', function () {
34363436

34373437
assert.strictEqual(called, true);
34383438
});
3439+
3440+
it('should maintain the same invocationId across the upload lifecycle', async () => {
3441+
const invocationIds: string[] = [];
3442+
3443+
const originalRequest = bucket.storageTransport.authClient.request.bind(
3444+
bucket.storageTransport.authClient,
3445+
);
3446+
3447+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3448+
bucket.storageTransport.authClient.request = async (config: any) => {
3449+
const headers = config.headers || {};
3450+
const apiHeaderKey = Object.keys(headers).find(
3451+
key => key.toLowerCase() === 'x-goog-api-client',
3452+
);
3453+
3454+
if (apiHeaderKey) {
3455+
const val = headers[apiHeaderKey];
3456+
const match = val.match(/gccl-invocation-id\/([a-f0-9-]+)/);
3457+
if (match) {
3458+
invocationIds.push(match[1]);
3459+
}
3460+
}
3461+
return originalRequest(config);
3462+
};
3463+
3464+
try {
3465+
const destination = `test-id-${Date.now()}.txt`;
3466+
await bucket.upload(FILES.big.path, {destination, resumable: false});
3467+
3468+
assert.ok(invocationIds.length >= 1);
3469+
const uniqueIds = [...new Set(invocationIds)];
3470+
assert.strictEqual(uniqueIds.length, 1);
3471+
} finally {
3472+
bucket.storageTransport.authClient.request = originalRequest;
3473+
}
3474+
});
34393475
});
34403476

34413477
describe('channels', () => {

0 commit comments

Comments
 (0)