Files
advanced-camera-card/src/utils/exponential-backoff.ts
T

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;
}
}