Skip to content
This repository was archived by the owner on Mar 3, 2026. It is now read-only.

Commit 1e72586

Browse files
shaffeeullahgcf-owl-bot[bot]ddelgrosso1
authored
fix: resumable uploads should respect autoRetry & multipart uploads should correctly use preconditions (#1779)
* chore: began refactoring gcs-resumable-upload * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: consistent defaults * fixed tests * gcs-resumable-upload should use retry interface from storage * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * refactored code * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fixed tests * create shallow copy of retry options before passing to resumable upload * fix unit test with retry options and resumable upload * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * trying to fix build issues that i cannot repro * tried changing paginator version * changed version back * tried changing paginator version * fix resumable upload conformance test * fixed bug in preconditions * update paginator to 3.0.7 * make instanceRetryValue private Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com> Co-authored-by: Denis DelGrosso <ddelgrosso@google.com>
1 parent 6ce9a2a commit 1e72586

9 files changed

Lines changed: 190 additions & 155 deletions

File tree

conformance-test/libraryMethods.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ export async function bucketSetStorageClass(bucket: Bucket) {
179179
}
180180

181181
export async function bucketUploadResumable(bucket: Bucket) {
182+
if (bucket.instancePreconditionOpts) {
183+
bucket.instancePreconditionOpts.ifGenerationMatch = 0;
184+
}
182185
await bucket.upload(
183186
path.join(
184187
__dirname,
@@ -189,14 +192,10 @@ export async function bucketUploadResumable(bucket: Bucket) {
189192
}
190193

191194
export async function bucketUploadMultipart(bucket: Bucket) {
192-
// If we are using a precondition we must make sure the file exists and the metageneration matches that provided as a query parameter
193-
bucket = new Bucket(bucket.storage, bucket.name, {
194-
preconditionOpts: {
195-
ifMetagenerationMatch: 1,
196-
},
197-
});
198-
const fileToSave = bucket.file('retryStrategyTestData.json');
199-
await fileToSave.save('fileToSave contents');
195+
if (bucket.instancePreconditionOpts) {
196+
delete bucket.instancePreconditionOpts.ifMetagenerationMatch;
197+
bucket.instancePreconditionOpts.ifGenerationMatch = 0;
198+
}
200199
await bucket.upload(
201200
path.join(
202201
__dirname,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
},
5252
"dependencies": {
5353
"@google-cloud/common": "^3.8.1",
54-
"@google-cloud/paginator": "^3.0.0",
54+
"@google-cloud/paginator": "^3.0.7",
5555
"@google-cloud/promisify": "^2.0.0",
5656
"abort-controller": "^3.0.0",
5757
"arrify": "^2.0.0",

src/bucket.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ class Bucket extends ServiceObject {
637637
signer?: URLSigner;
638638

639639
private instanceRetryValue?: boolean;
640-
private instancePreconditionOpts?: PreconditionOptions;
640+
instancePreconditionOpts?: PreconditionOptions;
641641

642642
constructor(storage: Storage, name: string, options?: BucketOptions) {
643643
options = options || {};
@@ -3991,11 +3991,12 @@ class Bucket extends ServiceObject {
39913991
options
39923992
);
39933993

3994-
// Do not retry if precondition option ifMetagenerationMatch is not set
3994+
// Do not retry if precondition option ifGenerationMatch is not set
3995+
// because this is a file operation
39953996
let maxRetries = this.storage.retryOptions.maxRetries;
39963997
if (
3997-
(options?.preconditionOpts?.ifMetagenerationMatch === undefined &&
3998-
this.instancePreconditionOpts?.ifMetagenerationMatch === undefined &&
3998+
(options?.preconditionOpts?.ifGenerationMatch === undefined &&
3999+
this.instancePreconditionOpts?.ifGenerationMatch === undefined &&
39994000
this.storage.retryOptions.idempotencyStrategy ===
40004001
IdempotencyStrategy.RetryConditional) ||
40014002
this.storage.retryOptions.idempotencyStrategy ===

src/file.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3982,7 +3982,7 @@ class File extends ServiceObject<File> {
39823982
public: options.public,
39833983
uri: options.uri,
39843984
userProject: options.userProject || this.userProject,
3985-
retryOptions: retryOptions,
3985+
retryOptions: {...retryOptions},
39863986
params: options?.preconditionOpts || this.instancePreconditionOpts,
39873987
chunkSize: options?.chunkSize,
39883988
});

src/gcs-resumable-upload/index.ts

Lines changed: 26 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,12 @@ import * as Pumpify from 'pumpify';
2828
import {Duplex, PassThrough, Readable} from 'stream';
2929
import * as streamEvents from 'stream-events';
3030
import retry = require('async-retry');
31+
import {RetryOptions, PreconditionOptions} from '../storage';
3132

3233
const NOT_FOUND_STATUS_CODE = 404;
3334
const TERMINATED_UPLOAD_STATUS_CODE = 410;
3435
const RESUMABLE_INCOMPLETE_STATUS_CODE = 308;
35-
const RETRY_LIMIT = 5;
3636
const DEFAULT_API_ENDPOINT_REGEX = /.*\.googleapis\.com/;
37-
const MAX_RETRY_DELAY = 64;
38-
const RETRY_DELAY_MULTIPLIER = 2;
39-
const MAX_TOTAL_RETRY_TIMEOUT = 600;
40-
const AUTO_RETRY_VALUE = true;
4137

4238
export const PROTOCOL_REGEX = /^(\w*):\/\//;
4339

@@ -60,12 +56,8 @@ export type PredefinedAcl =
6056
| 'projectPrivate'
6157
| 'publicRead';
6258

63-
export interface QueryParameters {
59+
export interface QueryParameters extends PreconditionOptions {
6460
contentEncoding?: string;
65-
ifGenerationMatch?: number;
66-
ifGenerationNotMatch?: number;
67-
ifMetagenerationMatch?: number;
68-
ifMetagenerationNotMatch?: number;
6961
kmsKeyName?: string;
7062
predefinedAcl?: PredefinedAcl;
7163
projection?: 'full' | 'noAcl';
@@ -207,7 +199,7 @@ export interface UploadConfig {
207199
/**
208200
* Configuration options for retrying retryable errors.
209201
*/
210-
retryOptions?: RetryOptions;
202+
retryOptions: RetryOptions;
211203
}
212204

213205
export interface ConfigMetadata {
@@ -225,15 +217,6 @@ export interface ConfigMetadata {
225217
contentType?: string;
226218
}
227219

228-
export interface RetryOptions {
229-
retryDelayMultiplier?: number;
230-
totalTimeout?: number;
231-
maxRetryDelay?: number;
232-
autoRetry?: boolean;
233-
maxRetries?: number;
234-
retryableErrorFn?: (err: ApiError) => boolean;
235-
}
236-
237220
export interface GoogleInnerError {
238221
reason?: string;
239222
}
@@ -279,12 +262,8 @@ export class Upload extends Pumpify {
279262
numBytesWritten = 0;
280263
numRetries = 0;
281264
contentLength: number | '*';
282-
retryLimit: number = RETRY_LIMIT;
283-
maxRetryDelay: number = MAX_RETRY_DELAY;
284-
retryDelayMultiplier: number = RETRY_DELAY_MULTIPLIER;
285-
maxRetryTotalTimeout: number = MAX_TOTAL_RETRY_TIMEOUT;
265+
retryOptions: RetryOptions;
286266
timeOfFirstRequest: number;
287-
retryableErrorFn?: (err: ApiError) => boolean;
288267
private upstreamChunkBuffer: Buffer = Buffer.alloc(0);
289268
private chunkBufferEncoding?: BufferEncoding = undefined;
290269
private numChunksReadInRequest = 0;
@@ -340,6 +319,7 @@ export class Upload extends Pumpify {
340319
this.params = cfg.params || {};
341320
this.userProject = cfg.userProject;
342321
this.chunkSize = cfg.chunkSize;
322+
this.retryOptions = cfg.retryOptions;
343323

344324
if (cfg.key) {
345325
/**
@@ -363,32 +343,16 @@ export class Upload extends Pumpify {
363343
configPath,
364344
});
365345

366-
const autoRetry = cfg?.retryOptions?.autoRetry || AUTO_RETRY_VALUE;
346+
const autoRetry = cfg.retryOptions.autoRetry;
367347
this.uriProvidedManually = !!cfg.uri;
368348
this.uri = cfg.uri || this.get('uri');
369349
this.numBytesWritten = 0;
370350
this.numRetries = 0; //counter for number of retries currently executed
371-
372-
if (autoRetry && cfg?.retryOptions?.maxRetries !== undefined) {
373-
this.retryLimit = cfg.retryOptions.maxRetries;
374-
} else if (!autoRetry) {
375-
this.retryLimit = 0;
376-
}
377-
378-
if (cfg?.retryOptions?.maxRetryDelay !== undefined) {
379-
this.maxRetryDelay = cfg.retryOptions.maxRetryDelay;
380-
}
381-
382-
if (cfg?.retryOptions?.retryDelayMultiplier !== undefined) {
383-
this.retryDelayMultiplier = cfg.retryOptions.retryDelayMultiplier;
384-
}
385-
386-
if (cfg?.retryOptions?.totalTimeout !== undefined) {
387-
this.maxRetryTotalTimeout = cfg.retryOptions.totalTimeout;
351+
if (!autoRetry) {
352+
cfg.retryOptions.maxRetries = 0;
388353
}
389354

390355
this.timeOfFirstRequest = Date.now();
391-
this.retryableErrorFn = cfg?.retryOptions?.retryableErrorFn;
392356

393357
const contentLength = cfg.metadata
394358
? Number(cfg.metadata.contentLength)
@@ -646,9 +610,8 @@ export class Upload extends Pumpify {
646610
],
647611
};
648612
if (
649-
this.retryLimit > 0 &&
650-
this.retryableErrorFn &&
651-
this.retryableErrorFn!(apiError as ApiError)
613+
this.retryOptions.maxRetries! > 0 &&
614+
this.retryOptions.retryableErrorFn!(apiError as ApiError)
652615
) {
653616
throw e;
654617
} else {
@@ -657,10 +620,10 @@ export class Upload extends Pumpify {
657620
}
658621
},
659622
{
660-
retries: this.retryLimit,
661-
factor: this.retryDelayMultiplier,
662-
maxTimeout: this.maxRetryDelay! * 1000, //convert to milliseconds
663-
maxRetryTime: this.maxRetryTotalTimeout! * 1000, //convert to milliseconds
623+
retries: this.retryOptions.maxRetries,
624+
factor: this.retryOptions.retryDelayMultiplier,
625+
maxTimeout: this.retryOptions.maxRetryDelay! * 1000, //convert to milliseconds
626+
maxRetryTime: this.retryOptions.totalTimeout! * 1000, //convert to milliseconds
664627
}
665628
);
666629

@@ -1055,14 +1018,11 @@ export class Upload extends Pumpify {
10551018
*/
10561019
private onResponse(resp: GaxiosResponse) {
10571020
if (
1058-
(this.retryableErrorFn &&
1059-
this.retryableErrorFn({
1060-
code: resp.status,
1061-
message: resp.statusText,
1062-
name: resp.statusText,
1063-
})) ||
1064-
resp.status === NOT_FOUND_STATUS_CODE ||
1065-
this.isServerErrorResponse(resp.status)
1021+
this.retryOptions.retryableErrorFn!({
1022+
code: resp.status,
1023+
message: resp.statusText,
1024+
name: resp.statusText,
1025+
})
10661026
) {
10671027
this.attemptDelayedRetry(resp);
10681028
return false;
@@ -1076,7 +1036,7 @@ export class Upload extends Pumpify {
10761036
* @param resp GaxiosResponse object from previous attempt
10771037
*/
10781038
private attemptDelayedRetry(resp: GaxiosResponse) {
1079-
if (this.numRetries < this.retryLimit) {
1039+
if (this.numRetries < this.retryOptions.maxRetries!) {
10801040
if (
10811041
resp.status === NOT_FOUND_STATUS_CODE &&
10821042
this.numChunksReadInRequest === 0
@@ -1120,10 +1080,13 @@ export class Upload extends Pumpify {
11201080
private getRetryDelay(): number {
11211081
const randomMs = Math.round(Math.random() * 1000);
11221082
const waitTime =
1123-
Math.pow(this.retryDelayMultiplier, this.numRetries) * 1000 + randomMs;
1083+
Math.pow(this.retryOptions.retryDelayMultiplier!, this.numRetries) *
1084+
1000 +
1085+
randomMs;
11241086
const maxAllowableDelayMs =
1125-
this.maxRetryTotalTimeout * 1000 - (Date.now() - this.timeOfFirstRequest);
1126-
const maxRetryDelayMs = this.maxRetryDelay * 1000;
1087+
this.retryOptions.totalTimeout! * 1000 -
1088+
(Date.now() - this.timeOfFirstRequest);
1089+
const maxRetryDelayMs = this.retryOptions.maxRetryDelay! * 1000;
11271090

11281091
return Math.min(waitTime, maxRetryDelayMs, maxAllowableDelayMs);
11291092
}
@@ -1147,16 +1110,6 @@ export class Upload extends Pumpify {
11471110
public isSuccessfulResponse(status: number): boolean {
11481111
return status >= 200 && status < 300;
11491112
}
1150-
1151-
/**
1152-
* Check if a given status code is 5xx
1153-
*
1154-
* @param status The status code to check
1155-
* @returns if the status is 5xx
1156-
*/
1157-
public isServerErrorResponse(status: number): boolean {
1158-
return status >= 500 && status < 600;
1159-
}
11601113
}
11611114

11621115
export function upload(cfg: UploadConfig) {

src/storage.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -205,42 +205,37 @@ export const PROTOCOL_REGEX = /^(\w*):\/\//;
205205
* Default behavior: Automatically retry retriable server errors.
206206
*
207207
* @const {boolean}
208-
* @private
209208
*/
210-
const AUTO_RETRY_DEFAULT = true;
209+
export const AUTO_RETRY_DEFAULT = true;
211210

212211
/**
213212
* Default behavior: Only attempt to retry retriable errors 3 times.
214213
*
215214
* @const {number}
216-
* @private
217215
*/
218-
const MAX_RETRY_DEFAULT = 3;
216+
export const MAX_RETRY_DEFAULT = 3;
219217

220218
/**
221219
* Default behavior: Wait twice as long as previous retry before retrying.
222220
*
223221
* @const {number}
224-
* @private
225222
*/
226-
const RETRY_DELAY_MULTIPLIER_DEFAULT = 2;
223+
export const RETRY_DELAY_MULTIPLIER_DEFAULT = 2;
227224

228225
/**
229226
* Default behavior: If the operation doesn't succeed after 600 seconds,
230227
* stop retrying.
231228
*
232229
* @const {number}
233-
* @private
234230
*/
235-
const TOTAL_TIMEOUT_DEFAULT = 600;
231+
export const TOTAL_TIMEOUT_DEFAULT = 600;
236232

237233
/**
238234
* Default behavior: Wait no more than 64 seconds between retries.
239235
*
240236
* @const {number}
241-
* @private
242237
*/
243-
const MAX_RETRY_DELAY_DEFAULT = 64;
238+
export const MAX_RETRY_DELAY_DEFAULT = 64;
244239

245240
/**
246241
* Default behavior: Retry conditionally idempotent operations if correct preconditions are set.
@@ -254,11 +249,10 @@ const IDEMPOTENCY_STRATEGY_DEFAULT = IdempotencyStrategy.RetryConditional;
254249
* Returns true if the API request should be retried, given the error that was
255250
* given the first time the request was attempted.
256251
* @const
257-
* @private
258252
* @param {error} err - The API error to check if it is appropriate to retry.
259253
* @return {boolean} True if the API request should be retried, false otherwise.
260254
*/
261-
const RETRYABLE_ERR_FN_DEFAULT = function (err?: ApiError) {
255+
export const RETRYABLE_ERR_FN_DEFAULT = function (err?: ApiError) {
262256
if (err) {
263257
if ([408, 429, 500, 502, 503, 504].indexOf(err.code!) !== -1) {
264258
return true;

0 commit comments

Comments
 (0)