Skip to content

Instantly share code, notes, and snippets.

@codyromano
Created June 30, 2016 03:53
Show Gist options
  • Select an option

  • Save codyromano/0049cb731a78ebe0e2ad0ee670bbbcbf to your computer and use it in GitHub Desktop.

Select an option

Save codyromano/0049cb731a78ebe0e2ad0ee670bbbcbf to your computer and use it in GitHub Desktop.
interface BackoffConfig {
// How many times a process can be retried before it fails permanently
attempts?: number;
// How long a process can run per attempt
timeout?: number;
// Pause between attempts
delay?: number;
// Controls how quickly the delay will increase with each attempt
backoffExponent?: number;
// For a good explanation of decorrelated jitter, see:
// https://www.awsarchitectureblog.com/2015/03/backoff.html
jitterCap?: number;
jitterBase?: number;
}
interface BackoffAPI {
// Execute a process to be retried on failure
exec: Function;
// Callback that fires if the process succeeds
done: Function;
// Callback for when the process fails permanently
fail: Function;
}
interface BackoffCustomCallbacks {
[callbackName: string]: Function
}
class Backoff implements BackoffAPI
{
attempts: number = 1;
overrides: BackoffConfig = {};
callbacks: BackoffCustomCallbacks = {
done() {},
fail() {}
};
_jitter: number;
_status: string;
_timeout: number;
_doneArgs: Array<any>;
_failedArgs: Array<any>;
constructor(overrides = {}) {
this.overrides = overrides;
this._jitter = this.getConfig().jitterBase;
}
increaseJitter() {
let {jitterCap, jitterBase} = this.getConfig();
this._jitter = Math.min(jitterCap,
jitterBase + Math.random() * (this._jitter * 3));
}
getJitter(): number {
return this._jitter;
}
setStatus(newStatus: 'idle' | 'running' | 'done' | 'failed', ...args) {
// Status can't be changed after the process completes or fails
if (['done','failed'].indexOf(this._status) >= 0) {
return;
}
this._status = newStatus;
let {done, fail} = this.callbacks;
// Reset any existing timeouts leftover from previous run attempts
window.clearTimeout(this._timeout);
console.log(`Status: ${newStatus} | Attempt ${this.attempts}`);
switch (this._status) {
case 'failed':
this._failedArgs = args;
fail(...args);
break;
case 'done':
this._doneArgs = args;
done(...args);
break;
default:
break;
}
}
// Get the entire configuration object
getConfig(): BackoffConfig {
/* Override the default config with custom values
if custom values are provided */
return Object.assign({
attempts: 3,
timeout: 5000,
delay: 1000,
backoffExponent: 1.5,
jitterCap: 1000 * 60 * 30, // 30 minutes
jitterBase: 1
}, this.overrides);
}
// Shortcut for getting a specific numerical value from the config
config(key: string): number {
let configObj = this.getConfig();
return configObj[key];
}
getTimeout(): number {
let {delay, backoffExponent} = this.getConfig();
let timeout = delay * (Math.pow(backoffExponent,
this.attempts) / backoffExponent) + this.getJitter();
// Just for demo purposes
console.log(`Retrying in ${timeout.toFixed(2)}ms
(jitter = ${this.getJitter().toFixed(2)}ms)`);
return timeout;
}
attemptsRemain(): boolean {
return this.attempts < this.config('attempts');
}
incrementAttempts() {
this.attempts = Math.min(++this.attempts, this.config('attempts'));
}
exec(method: Function): BackoffAPI {
// There's nothing to do if the process has already been executed
if (['failed','done'].indexOf(this._status) !== -1) {
return this;
}
if (this.attemptsRemain()) {
this.setStatus('running');
this.incrementAttempts();
let markAsDone = this.setStatus.bind(this, 'done'),
forceRetry = this.exec.bind(this, method),
forceFailure = this.setStatus.bind(this, 'failed');
method(markAsDone, forceRetry, forceFailure);
this.increaseJitter();
// Retry automatically if the process times out
this._timeout = window.setTimeout(forceRetry,
this.getTimeout());
} else {
this.setStatus('failed');
}
return this;
}
done(callback: Function): BackoffAPI {
this.callbacks['done'] = callback;
if (this._status === 'done') {
callback(...this._doneArgs);
}
return this;
}
fail(errback: Function): BackoffAPI {
this.callbacks['fail'] = errback;
if (this._status === 'failed') {
errback(...this._failedArgs);
}
return this;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment