86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
// Default jitter range applied to each computed delay: a random multiplier in
|
|
// [50%, 100%] of the pre-jitter value, avoiding thundering-herd retries when
|
|
// multiple instances back off in lockstep.
|
|
const DEFAULT_JITTER_MIN = 0.5;
|
|
const DEFAULT_JITTER_MAX = 1.0;
|
|
|
|
export interface ExponentialBackoffOptions {
|
|
// Delay for the first retry (attempt 1). Subsequent attempts double the delay
|
|
// until `maxSeconds` is reached.
|
|
baseSeconds: number;
|
|
|
|
// Upper bound on the delay after exponential growth. The delay never exceeds
|
|
// this regardless of attempt count.
|
|
maxSeconds: number;
|
|
|
|
// Random multiplier applied to each computed delay. Defaults to
|
|
// [DEFAULT_JITTER_MIN, DEFAULT_JITTER_MAX].
|
|
jitterMin?: number;
|
|
jitterMax?: number;
|
|
}
|
|
|
|
/**
|
|
* Stateful exponential-backoff delay calculator. Holds an attempt counter,
|
|
* returns the next delay on each `next()` call, and can be `reset()` after a
|
|
* successful operation.
|
|
*
|
|
* Example:
|
|
* ```ts
|
|
* const backoff = new ExponentialBackoff({ baseSeconds: 1, maxSeconds: 300 });
|
|
* // 1st failure -> backoff.next() returns ~1s (jittered).
|
|
* // 2nd failure -> ~2s.
|
|
* // 3rd failure -> ~4s. ... -> 300s ceiling.
|
|
* // After success: backoff.reset().
|
|
* ```
|
|
*/
|
|
export class ExponentialBackoff {
|
|
private _baseSeconds = 0;
|
|
private _maxSeconds = 0;
|
|
private _jitterMin = DEFAULT_JITTER_MIN;
|
|
private _jitterMax = DEFAULT_JITTER_MAX;
|
|
private _attempts = 0;
|
|
|
|
constructor(options: ExponentialBackoffOptions) {
|
|
this.setOptions(options);
|
|
}
|
|
|
|
public setOptions(options: ExponentialBackoffOptions): void {
|
|
this._baseSeconds = options.baseSeconds;
|
|
this._maxSeconds = options.maxSeconds;
|
|
this._jitterMin = options.jitterMin ?? DEFAULT_JITTER_MIN;
|
|
this._jitterMax = options.jitterMax ?? DEFAULT_JITTER_MAX;
|
|
}
|
|
|
|
/**
|
|
* Returns the next delay in seconds and increments the attempt counter. The
|
|
* pre-jitter delay is `baseSeconds * 2^(attempts before increment)`, capped
|
|
* at `maxSeconds`. Jitter is a random multiplier in [jitterMin, jitterMax].
|
|
*/
|
|
public next(): number {
|
|
const delay = this.peek();
|
|
this._attempts += 1;
|
|
return delay;
|
|
}
|
|
|
|
/**
|
|
* Returns what the next `next()` call would return WITHOUT incrementing the
|
|
* counter. Useful for "re-arm at the same backoff level" cases (a scheduled
|
|
* retry deferred for an unrelated reason; don't compound the backoff). Note
|
|
* jitter is re-rolled each call, so two consecutive `peek()`s may return
|
|
* slightly different values for the same attempt count.
|
|
*/
|
|
public peek(): number {
|
|
const exp = Math.min(this._maxSeconds, this._baseSeconds * 2 ** this._attempts);
|
|
const jitter = this._jitterMin + Math.random() * (this._jitterMax - this._jitterMin);
|
|
return exp * jitter;
|
|
}
|
|
|
|
public reset(): void {
|
|
this._attempts = 0;
|
|
}
|
|
|
|
public getAttempts(): number {
|
|
return this._attempts;
|
|
}
|
|
}
|