dashboard/shell/utils/back-off.ts

177 lines
4.8 KiB
TypeScript

type BackOffEntry = {
timeoutId?: NodeJS.Timeout,
try: number,
retries: number,
description: string,
metadata: any,
}
/**
* Helper class which handles backing off making the supplied request
*
* see `execute` for more info
*/
class BackOff {
private map: {
[id: string]: BackOffEntry
} = {};
private log(level: 'error' | 'info' | 'debug', id: string, classDescription: string, description: string, ...args: any[]) {
console[level](`BackOff... Id: "${ id }". Description: "${ description }"\nStatus: ${ classDescription }\n`, ...args); // eslint-disable-line no-console
}
/**
* Get a specific back off process
*/
getBackOff(id: string): BackOffEntry {
return this.map[id];
}
/**
* Stop ALL back off processes started since the ui was loaded
*/
resetAll() {
Object.keys(this.map).forEach((id) => {
this.reset(id);
});
}
/**
* Stop all back off process with a specific prefix
*/
resetPrefix(prefix:string) {
Object.keys(this.map).forEach((id) => {
if (id.startsWith(prefix)) {
this.reset(id);
}
});
}
/**
* Stop a back off process with a specific id
*/
reset(id: string) {
const backOff: BackOffEntry = this.map[id];
if (backOff) {
if (backOff?.timeoutId) {
this.log('info', id, 'Stopping (cancelling active back-off)', backOff.description);
clearTimeout(backOff.timeoutId);
}
this.log('debug', id, 'Reset', backOff.description);
delete this.map[id];
}
}
/**
* Call a function, but if it's recently been called delay execution aka back off
*
* This can be used in a totally disjoined asynchronous way
*
* 1. Request function A to be run
* 2. Entirely separate process requests function A to be run again
* 3. Back off process waits Xms and then runs function A again
* 4. Repeat steps 2 and 3, with an exponential increasing delay
*
* This can be called repeatedly, if the previous delay is still running new requests will be ignored
*/
async execute<T = any>({
id, description, retries = 10, delayedFn, canFn = async() => true, metadata
}: {
/**
* Unique id for the execution of this function.
*
* This will be used to delay further executions, and also to cancel it
*/
id: string,
/**
* Basic text description to use in logging
*/
description: string,
/**
* Number of executions allowed before flatly refusing to call more. Defaults to 10
*/
retries?: number,
/**
* Before calling delayedFn check if it can still run
*
* Useful for checking state after a looong delay
*/
canFn?: () => Promise<boolean>,
/**
* Call this function
* - if it's not already waiting to run
* - if it's passed canFn
* - if it hasn't been tried over `retries` amount
*
* The function will be increasingly (exponentially) delayed if it has previously been called
*/
delayedFn: () => Promise<any>,
/**
* Anything that might be important outside of this file (used with `getBackOff`)
*/
metadata?: T,
}): Promise<NodeJS.Timeout | undefined> {
const backOff: BackOffEntry = this.map[id];
const cont = await canFn();
if (!cont) {
this.log('info', id, 'Skipping (can execute fn test failed)', description);
return undefined;
} else if (backOff?.timeoutId) {
this.log('info', id, 'Skipping (previous back off process still running)', description);
return backOff.timeoutId;
} else {
const backOffTry = backOff?.try || 0;
if (backOffTry + 1 > retries) {
this.log('error', id, 'Aborting (too many retries)', description);
return undefined;
}
// First step is immediate (0.001s)
// Second and others are exponential
// 1, 2, 3, 4, 5, 6, 7, 8, 9
// 1, 4, 9, 16, 25, 36, 49, 64, 81
// 0.25s, 1s, 2.25s, 4s, 6.25s, 9s, 12.25s, 16s, 20.25s
const delay = backOffTry === 0 ? 1 : Math.pow(backOffTry, 2) * 250;
this.log('info', id, `Delaying call (attempt ${ backOffTry + 1 }, delayed by ${ delay }ms)`, description);
const timeout = setTimeout(async() => {
try {
this.log('info', id, `Executing call`, description);
await delayedFn();
} catch (e) {
// Error occurred. Don't clear the map. Next time this is called we'll back off before trying ...
this.log('error', id, 'Failed call', description, e);
}
// Unblock future calls
delete this.map[id]?.timeoutId;
}, delay);
this.map[id] = {
timeoutId: timeout,
try: backOff?.try ? backOff.try + 1 : 1,
retries,
description,
metadata
};
return timeout;
}
}
}
const backOff = new BackOff();
export default backOff;