Skip to content

Commit

Permalink
fix: support Retry-After header (#305)
Browse files Browse the repository at this point in the history
* fix: support `Retry-After` header

* test: update retry tests

* refactor: jitter logic
  • Loading branch information
kukhariev committed Jun 10, 2021
1 parent 66c939d commit a71df9d
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 29 deletions.
9 changes: 8 additions & 1 deletion src/app/directive-way/directive-way.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ export class DirectiveWayComponent {
options: UploadxOptions = {
allowedTypes: 'image/*,video/*',
endpoint: `${environment.api}/files?uploadType=uploadx`,
token: localStorage.getItem('token') || 'token'
token: localStorage.getItem('token') || 'token',
retryConfig: {
maxAttempts: 30,
maxDelay: 60_000,
shouldRetry: (code, attempts) => {
return code === 504 || ((code < 400 || code >= 501) && attempts < 5);
}
}
};

cancel(uploadId?: string): void {
Expand Down
80 changes: 57 additions & 23 deletions src/uploadx/lib/retry-handler.spec.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,66 @@
import { ErrorType, RetryHandler, ShouldRetryFunction } from './retry-handler';

describe('ErrorHandler', () => {
it('ErrorHandler.kind(status)', () => {
const errorHandler = new RetryHandler();
expect(errorHandler.kind(400)).toBe(ErrorType.Fatal);
expect(errorHandler.kind(0)).toBe(ErrorType.Retryable);
expect(errorHandler.kind(500)).toBe(ErrorType.Retryable);
expect(errorHandler.kind(423)).toBe(ErrorType.Retryable);
expect(errorHandler.kind(200)).toBe(ErrorType.Retryable);
expect(errorHandler.kind(404)).toBe(ErrorType.NotFound);
expect(errorHandler.kind(401)).toBe(ErrorType.Auth);
describe('RetryHandler', () => {
let retry: RetryHandler;
beforeEach(() => {
retry = new RetryHandler();
jasmine.clock().install();
});
afterEach(() => jasmine.clock().uninstall());

it('Custom shouldRetry', () => {
it('kind(status)', () => {
expect(retry.kind(400)).toBe(ErrorType.Fatal);
expect(retry.kind(0)).toBe(ErrorType.Retryable);
expect(retry.kind(500)).toBe(ErrorType.Retryable);
expect(retry.kind(423)).toBe(ErrorType.Retryable);
expect(retry.kind(200)).toBe(ErrorType.Retryable);
expect(retry.kind(404)).toBe(ErrorType.NotFound);
expect(retry.kind(401)).toBe(ErrorType.Auth);
});

it('shouldRetry', () => {
const shouldRetry: ShouldRetryFunction = (code, attempts) => code === 500 && attempts < 2;
const errorHandler = new RetryHandler({ shouldRetry });
expect(errorHandler.kind(500)).toBe(ErrorType.Retryable);
errorHandler.attempts = 3;
expect(errorHandler.kind(500)).toBe(ErrorType.Fatal);
retry.config.shouldRetry = shouldRetry;
expect(retry.kind(500)).toBe(ErrorType.Retryable);
retry.attempts = 3;
expect(retry.kind(500)).toBe(ErrorType.Fatal);
});

it('observe(offset)', () => {
retry.config.maxAttempts = 2;
retry.observe('same value');
expect(retry.kind(500)).toBe(ErrorType.Retryable);
retry.observe('same value');
expect(retry.kind(500)).toBe(ErrorType.Retryable);
retry.observe('same value');
expect(retry.kind(500)).toBe(ErrorType.Fatal);
});

it('wait()', done => {
const wait = retry.wait();
jasmine.clock().tick(1001);
wait.then(() => {
expect(retry.kind(500)).toBe(ErrorType.Retryable);
done();
});
});

it('wait(time)', done => {
const wait = retry.wait(30_000);
jasmine.clock().tick(30_001);
wait.then(() => {
expect(retry.kind(500)).toBe(ErrorType.Retryable);
done();
});
});

it('ErrorHandler.observe(offset)', () => {
const errorHandler = new RetryHandler({ maxAttempts: 2 });
errorHandler.observe();
expect(errorHandler.kind(500)).toBe(ErrorType.Retryable);
errorHandler.observe();
expect(errorHandler.kind(500)).toBe(ErrorType.Retryable);
errorHandler.observe();
expect(errorHandler.kind(500)).toBe(ErrorType.Fatal);
it('wait cancel()', done => {
const wait = retry.wait(30_000);
jasmine.clock().tick(1);
retry.cancel();
wait.then(() => {
expect(retry.kind(500)).toBe(ErrorType.Retryable);
done();
});
});
});
8 changes: 4 additions & 4 deletions src/uploadx/lib/retry-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,17 @@ export class RetryHandler {
return ErrorType.Fatal;
}

wait(): Promise<void> {
wait(time?: number): Promise<void> {
const ms =
Math.min(2 ** (this.attempts - 1) * this.config.minDelay, this.config.maxDelay) +
Math.floor(Math.random() * this.config.minDelay);
time || Math.min(2 ** (this.attempts - 1) * this.config.minDelay, this.config.maxDelay);
const jitter = Math.floor(Math.random() * this.config.minDelay * this.attempts);
let id: ReturnType<typeof setTimeout>;
return new Promise(resolve => {
this.cancel = () => {
clearTimeout(id);
resolve();
};
id = setTimeout(this.cancel, ms);
id = setTimeout(this.cancel, ms + jitter);
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/uploadx/lib/uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export abstract class Uploader implements UploadState {
default:
this.responseStatus >= 400 && (this.offset = undefined);
this.status = 'retry';
await this.retry.wait();
await this.retry.wait(Number(this.responseHeaders['retry-after']) * 1000);
}
}
}
Expand Down

0 comments on commit a71df9d

Please sign in to comment.