Skip to content

Commit ac3c2b4

Browse files
committed
Cleaned up timeouts
1 parent 29d0340 commit ac3c2b4

File tree

3 files changed

+142
-11
lines changed

3 files changed

+142
-11
lines changed

packages/storage/src/task.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ export class UploadTask {
9494
private _metadataErrorHandler: (p1: StorageError) => void;
9595
private _resolve?: (p1: UploadTaskSnapshot) => void = undefined;
9696
private _reject?: (p1: StorageError) => void = undefined;
97+
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ // This has to be an "any" type due to incompatibility between Node and browser timers.
98+
private pendingTimeout: any = null;
9799
private _promise: Promise<UploadTaskSnapshot>;
98100

99101
private sleepTime: number;
@@ -191,8 +193,16 @@ export class UploadTask {
191193
// Happens if we miss the metadata on upload completion.
192194
this._fetchMetadata();
193195
} else {
194-
setTimeout(() => {
195-
this._continueUpload();
196+
this.pendingTimeout = setTimeout(() => {
197+
this.pendingTimeout = null;
198+
switch (this._state) {
199+
case InternalTaskState.CANCELING:
200+
this._transition(InternalTaskState.CANCELED);
201+
case InternalTaskState.PAUSING:
202+
this._transition(InternalTaskState.PAUSED);
203+
default:
204+
this._continueUpload();
205+
}
196206
}, this.sleepTime);
197207
}
198208
}
@@ -411,6 +421,10 @@ export class UploadTask {
411421
this._state = state;
412422
if (this._request !== undefined) {
413423
this._request.cancel();
424+
} else if (this.pendingTimeout) {
425+
clearTimeout(this.pendingTimeout);
426+
this.pendingTimeout = null;
427+
this.completeTransitions_();
414428
}
415429
break;
416430
case InternalTaskState.PAUSING:
@@ -419,6 +433,10 @@ export class UploadTask {
419433
this._state = state;
420434
if (this._request !== undefined) {
421435
this._request.cancel();
436+
} else if (this.pendingTimeout) {
437+
clearTimeout(this.pendingTimeout);
438+
this.pendingTimeout = null;
439+
this.completeTransitions_();
422440
}
423441
break;
424442
case InternalTaskState.RUNNING:

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

Lines changed: 116 additions & 7 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,12 @@ 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+
39+
use(sinonChai);
3640

3741
const testLocation = new Location('bucket', 'object');
3842
const smallBlob = new FbsBlob(new Uint8Array([97]));
@@ -361,7 +365,7 @@ describe('Firebase Storage > Upload Task', () => {
361365
function handleStateChange(
362366
requestHandler: RequestHandler,
363367
blob: FbsBlob
364-
): Promise<TotalState> {
368+
): { promise: Promise<TotalState>; task: UploadTask } {
365369
const storageService = storageServiceWithHandler(requestHandler);
366370
const task = new UploadTask(
367371
new Reference(storageService, testLocation),
@@ -410,7 +414,7 @@ describe('Firebase Storage > Upload Task', () => {
410414
}
411415
);
412416

413-
return deferred.promise;
417+
return { promise: deferred.promise, task };
414418
}
415419

416420
it('Calls callback sequences for small uploads correctly', () => {
@@ -422,7 +426,7 @@ describe('Firebase Storage > Upload Task', () => {
422426
it('properly times out if large blobs returns a 503 when finalizing', async () => {
423427
clock = useFakeTimers();
424428
// Kick off upload
425-
const promise = handleStateChange(
429+
const { promise } = handleStateChange(
426430
fake503ForFinalizeServerHandler(),
427431
bigBlob
428432
);
@@ -460,7 +464,10 @@ describe('Firebase Storage > Upload Task', () => {
460464
it('properly times out if large blobs returns a 503 when uploading', async () => {
461465
clock = useFakeTimers();
462466
// Kick off upload
463-
const promise = handleStateChange(fake503ForUploadServerHandler(), bigBlob);
467+
const { promise } = handleStateChange(
468+
fake503ForUploadServerHandler(),
469+
bigBlob
470+
);
464471
// Run all timers
465472
await clock.runAllAsync();
466473
const { events, progress } = await promise;
@@ -478,10 +485,112 @@ describe('Firebase Storage > Upload Task', () => {
478485
});
479486
clock.restore();
480487
});
488+
function resumeCancelSetup(): any {
489+
clock = useFakeTimers();
490+
const fakeSetTimeout = clock.setTimeout;
491+
let gotFirstEvent = false;
492+
493+
const stub = sinon.stub(global, 'setTimeout');
494+
495+
const timers: { [key: number]: any } = {};
496+
const readyToCancel = new Promise(resolve => {
497+
stub.callsFake((fn, timeout) => {
498+
// @ts-ignore
499+
const res = fakeSetTimeout(fn, timeout);
500+
//@ts-ignore
501+
timers[res.id] = {
502+
data: res,
503+
timeout
504+
};
505+
if ((!gotFirstEvent || timeout === 0) && timeout !== 600000) {
506+
clock.tick(timeout as number);
507+
} else if (timeout !== 600000) {
508+
resolve(null);
509+
}
510+
return res;
511+
});
512+
});
513+
readyToCancel.then(
514+
() => stub.restore(),
515+
() => {}
516+
);
517+
return {
518+
...handleStateChange(
519+
fake503ForUploadServerHandler(undefined, () => (gotFirstEvent = true)),
520+
bigBlob
521+
),
522+
readyToCancel
523+
};
524+
}
525+
it('properly errors with a cancel StorageError if a pending timeout remains', async () => {
526+
// Kick off upload
527+
const { readyToCancel, promise, task } = resumeCancelSetup();
528+
529+
await readyToCancel;
530+
task.cancel();
531+
532+
// Run all timers
533+
const { events, progress } = await promise;
534+
expect(events.length).to.equal(2);
535+
expect(events[0]).to.deep.equal({ type: 'resume' });
536+
expect(events[1].type).to.deep.equal('error');
537+
const canceledError = canceled();
538+
expect(events[1].data!.name).to.deep.equal(canceledError.name);
539+
expect(events[1].data!.message).to.deep.equal(canceledError.message);
540+
const blobSize = bigBlob.size();
541+
expect(progress.length).to.equal(1);
542+
expect(progress[0]).to.deep.equal({
543+
bytesTransferred: 0,
544+
totalBytes: blobSize
545+
});
546+
expect(clock.countTimers()).to.eq(0);
547+
clock.restore();
548+
});
549+
it('properly errors with a pause StorageError if a pending timeout remains', async () => {
550+
// Kick off upload
551+
const { readyToCancel, promise, task } = resumeCancelSetup();
552+
553+
await readyToCancel;
554+
555+
task.pause();
556+
expect(clock.countTimers()).to.eq(0);
557+
task.resume();
558+
await clock.runAllAsync();
559+
560+
// Run all timers
561+
const { events, progress } = await promise;
562+
expect(events.length).to.equal(4);
563+
expect(events[0]).to.deep.equal({ type: 'resume' });
564+
expect(events[1]).to.deep.equal({ type: 'pause' });
565+
expect(events[2]).to.deep.equal({ type: 'resume' });
566+
expect(events[3].type).to.deep.equal('error');
567+
const retryError = retryLimitExceeded();
568+
expect(events[3].data!.name).to.deep.equal(retryError.name);
569+
expect(events[3].data!.message).to.deep.equal(retryError.message);
570+
const blobSize = bigBlob.size();
571+
expect(progress.length).to.equal(3);
572+
expect(progress[0]).to.deep.equal({
573+
bytesTransferred: 0,
574+
totalBytes: blobSize
575+
});
576+
expect(progress[1]).to.deep.equal({
577+
bytesTransferred: 0,
578+
totalBytes: blobSize
579+
});
580+
expect(progress[2]).to.deep.equal({
581+
bytesTransferred: 0,
582+
totalBytes: blobSize
583+
});
584+
expect(clock.countTimers()).to.eq(0);
585+
clock.restore();
586+
});
481587
it('tests if small requests that respond with 500 retry correctly', async () => {
482588
clock = useFakeTimers();
483589
// Kick off upload
484-
const promise = handleStateChange(fakeOneShot503ServerHandler(), smallBlob);
590+
const { promise } = handleStateChange(
591+
fakeOneShot503ServerHandler(),
592+
smallBlob
593+
);
485594
// Run all timers
486595
await clock.runAllAsync();
487596
const { events, progress } = await promise;

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?: any
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)