Skip to content

Commit 5f55ed8

Browse files
authored
[Storage] Cleared timeouts upon pause/resume of uploads (#6667)
1 parent 48a127b commit 5f55ed8

File tree

4 files changed

+146
-21
lines changed

4 files changed

+146
-21
lines changed

.changeset/great-houses-trade.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@firebase/storage": patch
3+
---
4+
5+
Cleared retry timeouts when uploads are paused/canceled

packages/storage/src/task.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export class UploadTask {
9494
private _metadataErrorHandler: (p1: StorageError) => void;
9595
private _resolve?: (p1: UploadTaskSnapshot) => void = undefined;
9696
private _reject?: (p1: StorageError) => void = undefined;
97+
private pendingTimeout?: ReturnType<typeof setTimeout>;
9798
private _promise: Promise<UploadTaskSnapshot>;
9899

99100
private sleepTime: number;
@@ -191,7 +192,8 @@ export class UploadTask {
191192
// Happens if we miss the metadata on upload completion.
192193
this._fetchMetadata();
193194
} else {
194-
setTimeout(() => {
195+
this.pendingTimeout = setTimeout(() => {
196+
this.pendingTimeout = undefined;
195197
this._continueUpload();
196198
}, this.sleepTime);
197199
}
@@ -405,20 +407,17 @@ export class UploadTask {
405407
}
406408
switch (state) {
407409
case InternalTaskState.CANCELING:
410+
case InternalTaskState.PAUSING:
408411
// TODO(andysoto):
409412
// assert(this.state_ === InternalTaskState.RUNNING ||
410413
// this.state_ === InternalTaskState.PAUSING);
411414
this._state = state;
412415
if (this._request !== undefined) {
413416
this._request.cancel();
414-
}
415-
break;
416-
case InternalTaskState.PAUSING:
417-
// TODO(andysoto):
418-
// assert(this.state_ === InternalTaskState.RUNNING);
419-
this._state = state;
420-
if (this._request !== undefined) {
421-
this._request.cancel();
417+
} else if (this.pendingTimeout) {
418+
clearTimeout(this.pendingTimeout);
419+
this.pendingTimeout = undefined;
420+
this.completeTransitions_();
422421
}
423422
break;
424423
case InternalTaskState.RUNNING:

packages/storage/test/unit/task.test.ts

Lines changed: 127 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17-
import { assert, expect } from 'chai';
17+
import { assert, expect, use } from 'chai';
1818
import { FbsBlob } from '../../src/implementation/blob';
1919
import { Location } from '../../src/implementation/location';
2020
import { Unsubscribe } from '../../src/implementation/observer';
@@ -31,8 +31,13 @@ import {
3131
} from './testshared';
3232
import { injectTestConnection } from '../../src/platform/connection';
3333
import { Deferred } from '@firebase/util';
34-
import { retryLimitExceeded } from '../../src/implementation/error';
34+
import { canceled, retryLimitExceeded } from '../../src/implementation/error';
3535
import { SinonFakeTimers, useFakeTimers } from 'sinon';
36+
import * as sinon from 'sinon';
37+
import sinonChai from 'sinon-chai';
38+
import { DEFAULT_MAX_UPLOAD_RETRY_TIME } from '../../src/implementation/constants';
39+
40+
use(sinonChai);
3641

3742
const testLocation = new Location('bucket', 'object');
3843
const smallBlob = new FbsBlob(new Uint8Array([97]));
@@ -361,7 +366,7 @@ describe('Firebase Storage > Upload Task', () => {
361366
function handleStateChange(
362367
requestHandler: RequestHandler,
363368
blob: FbsBlob
364-
): Promise<TotalState> {
369+
): { taskPromise: Promise<TotalState>; task: UploadTask } {
365370
const storageService = storageServiceWithHandler(requestHandler);
366371
const task = new UploadTask(
367372
new Reference(storageService, testLocation),
@@ -410,7 +415,7 @@ describe('Firebase Storage > Upload Task', () => {
410415
}
411416
);
412417

413-
return deferred.promise;
418+
return { taskPromise: deferred.promise, task };
414419
}
415420

416421
it('Calls callback sequences for small uploads correctly', () => {
@@ -422,13 +427,13 @@ describe('Firebase Storage > Upload Task', () => {
422427
it('properly times out if large blobs returns a 503 when finalizing', async () => {
423428
clock = useFakeTimers();
424429
// Kick off upload
425-
const promise = handleStateChange(
430+
const { taskPromise } = handleStateChange(
426431
fake503ForFinalizeServerHandler(),
427432
bigBlob
428433
);
429434
// Run all timers
430435
await clock.runAllAsync();
431-
const { events, progress } = await promise;
436+
const { events, progress } = await taskPromise;
432437
expect(events.length).to.equal(2);
433438
expect(events[0]).to.deep.equal({ type: 'resume' });
434439
expect(events[1].type).to.deep.equal('error');
@@ -460,10 +465,13 @@ describe('Firebase Storage > Upload Task', () => {
460465
it('properly times out if large blobs returns a 503 when uploading', async () => {
461466
clock = useFakeTimers();
462467
// Kick off upload
463-
const promise = handleStateChange(fake503ForUploadServerHandler(), bigBlob);
468+
const { taskPromise } = handleStateChange(
469+
fake503ForUploadServerHandler(),
470+
bigBlob
471+
);
464472
// Run all timers
465473
await clock.runAllAsync();
466-
const { events, progress } = await promise;
474+
const { events, progress } = await taskPromise;
467475
expect(events.length).to.equal(2);
468476
expect(events[0]).to.deep.equal({ type: 'resume' });
469477
expect(events[1].type).to.deep.equal('error');
@@ -478,13 +486,122 @@ describe('Firebase Storage > Upload Task', () => {
478486
});
479487
clock.restore();
480488
});
489+
490+
/**
491+
* Starts upload, finds the first instance of an exponential backoff, and resolves `readyToCancel` when done.
492+
* @returns readyToCancel, taskPromise and task
493+
*/
494+
function resumeCancelSetup(): {
495+
readyToCancel: Promise<null>;
496+
taskPromise: Promise<TotalState>;
497+
task: UploadTask;
498+
} {
499+
clock = useFakeTimers();
500+
const fakeSetTimeout = clock.setTimeout;
501+
502+
let gotFirstEvent = false;
503+
504+
const stub = sinon.stub(global, 'setTimeout');
505+
506+
// Function that notifies when we are in the middle of an exponential backoff
507+
const readyToCancel = new Promise<null>(resolve => {
508+
stub.callsFake((fn, timeout) => {
509+
// @ts-ignore The types for `stub.callsFake` is incompatible with types of `clock.setTimeout`
510+
const res = fakeSetTimeout(fn, timeout);
511+
if (timeout !== DEFAULT_MAX_UPLOAD_RETRY_TIME) {
512+
if (!gotFirstEvent || timeout === 0) {
513+
clock.tick(timeout as number);
514+
} else {
515+
// If the timeout isn't 0 and it isn't the max upload retry time, it's most likely due to exponential backoff.
516+
resolve(null);
517+
}
518+
}
519+
return res;
520+
});
521+
});
522+
readyToCancel.then(
523+
() => stub.restore(),
524+
() => stub.restore()
525+
);
526+
return {
527+
...handleStateChange(
528+
fake503ForUploadServerHandler(undefined, () => (gotFirstEvent = true)),
529+
bigBlob
530+
),
531+
readyToCancel
532+
};
533+
}
534+
it('properly errors with a cancel StorageError if a pending timeout remains', async () => {
535+
// Kick off upload
536+
const { readyToCancel, taskPromise: promise, task } = resumeCancelSetup();
537+
538+
await readyToCancel;
539+
task.cancel();
540+
541+
const { events, progress } = await promise;
542+
expect(events.length).to.equal(2);
543+
expect(events[0]).to.deep.equal({ type: 'resume' });
544+
expect(events[1].type).to.deep.equal('error');
545+
const canceledError = canceled();
546+
expect(events[1].data!.name).to.deep.equal(canceledError.name);
547+
expect(events[1].data!.message).to.deep.equal(canceledError.message);
548+
const blobSize = bigBlob.size();
549+
expect(progress.length).to.equal(1);
550+
expect(progress[0]).to.deep.equal({
551+
bytesTransferred: 0,
552+
totalBytes: blobSize
553+
});
554+
expect(clock.countTimers()).to.eq(0);
555+
clock.restore();
556+
});
557+
it('properly errors with a pause StorageError if a pending timeout remains', async () => {
558+
// Kick off upload
559+
const { readyToCancel, taskPromise: promise, task } = resumeCancelSetup();
560+
561+
await readyToCancel;
562+
563+
task.pause();
564+
expect(clock.countTimers()).to.eq(0);
565+
task.resume();
566+
await clock.runAllAsync();
567+
568+
// Run all timers
569+
const { events, progress } = await promise;
570+
expect(events.length).to.equal(4);
571+
expect(events[0]).to.deep.equal({ type: 'resume' });
572+
expect(events[1]).to.deep.equal({ type: 'pause' });
573+
expect(events[2]).to.deep.equal({ type: 'resume' });
574+
expect(events[3].type).to.deep.equal('error');
575+
const retryError = retryLimitExceeded();
576+
expect(events[3].data!.name).to.deep.equal(retryError.name);
577+
expect(events[3].data!.message).to.deep.equal(retryError.message);
578+
const blobSize = bigBlob.size();
579+
expect(progress.length).to.equal(3);
580+
expect(progress[0]).to.deep.equal({
581+
bytesTransferred: 0,
582+
totalBytes: blobSize
583+
});
584+
expect(progress[1]).to.deep.equal({
585+
bytesTransferred: 0,
586+
totalBytes: blobSize
587+
});
588+
expect(progress[2]).to.deep.equal({
589+
bytesTransferred: 0,
590+
totalBytes: blobSize
591+
});
592+
expect(clock.countTimers()).to.eq(0);
593+
clock.restore();
594+
});
481595
it('tests if small requests that respond with 500 retry correctly', async () => {
482596
clock = useFakeTimers();
483597
// Kick off upload
484-
const promise = handleStateChange(fakeOneShot503ServerHandler(), smallBlob);
598+
const { taskPromise } = handleStateChange(
599+
fakeOneShot503ServerHandler(),
600+
smallBlob
601+
);
485602
// Run all timers
486603
await clock.runAllAsync();
487-
const { events, progress } = await promise;
604+
const { events, progress } = await taskPromise;
488605
expect(events.length).to.equal(2);
489606
expect(events[0]).to.deep.equal({ type: 'resume' });
490607
expect(events[1].type).to.deep.equal('error');

packages/storage/test/unit/testshared.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ export function fakeServerHandler(
356356

357357
/**
358358
* Responds with a 503 for finalize.
359-
* @param fakeMetadata metadata to respond with for query
359+
* @param fakeMetadata metadata to respond with for finalize
360360
* @returns a handler for requests
361361
*/
362362
export function fake503ForFinalizeServerHandler(
@@ -459,7 +459,8 @@ export function fake503ForFinalizeServerHandler(
459459
* @returns a handler for requests
460460
*/
461461
export function fake503ForUploadServerHandler(
462-
fakeMetadata: Partial<Metadata> = defaultFakeMetadata
462+
fakeMetadata: Partial<Metadata> = defaultFakeMetadata,
463+
cb?: () => void
463464
): RequestHandler {
464465
const stats: {
465466
[num: number]: {
@@ -536,6 +537,9 @@ export function fake503ForUploadServerHandler(
536537
const isUpload = commands.indexOf('upload') !== -1;
537538

538539
if (isUpload) {
540+
if (cb) {
541+
cb();
542+
}
539543
return {
540544
status: 503,
541545
body: JSON.stringify(fakeMetadata),

0 commit comments

Comments
 (0)