kibana/test/common/services/retry/retry_for_success.ts
Brandon Kobel 4584a8b570
Elastic License 2.0 (#90099)
* Updating everything except the license headers themselves

* Applying ESLint rules

* Manually replacing the stragglers
2021-02-03 18:12:39 -08:00

85 lines
2.5 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ToolingLog } from '@kbn/dev-utils';
import { inspect } from 'util';
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const returnTrue = () => true;
const defaultOnFailure = (methodName: string) => (lastError: Error) => {
throw new Error(`${methodName} timeout: ${lastError.stack || lastError.message}`);
};
/**
* Run a function and return either an error or result
* @param {Function} block
*/
async function runAttempt<T>(block: () => Promise<T>): Promise<{ result: T } | { error: Error }> {
try {
return {
result: await block(),
};
} catch (error) {
return {
// we rely on error being truthy and throwing falsy values is *allowed*
// so we cast falsy values to errors
error: error instanceof Error ? error : new Error(`${inspect(error)} thrown`),
};
}
}
interface Options<T> {
timeout: number;
methodName: string;
block: () => Promise<T>;
onFailureBlock?: () => Promise<T>;
onFailure?: ReturnType<typeof defaultOnFailure>;
accept?: (v: T) => boolean;
}
export async function retryForSuccess<T>(log: ToolingLog, options: Options<T>) {
const { timeout, methodName, block, onFailureBlock, accept = returnTrue } = options;
const { onFailure = defaultOnFailure(methodName) } = options;
const start = Date.now();
const retryDelay = 502;
let lastError;
while (true) {
if (lastError && Date.now() - start > timeout) {
await onFailure(lastError);
throw new Error('expected onFailure() option to throw an error');
} else if (lastError && onFailureBlock) {
const before = await runAttempt(onFailureBlock);
if ('error' in before) {
log.debug(`--- onRetryBlock error: ${before.error.message}`);
}
}
const attempt = await runAttempt(block);
if ('result' in attempt && accept(attempt.result)) {
return attempt.result;
}
if ('error' in attempt) {
if (lastError && lastError.message === attempt.error.message) {
log.debug(`--- ${methodName} failed again with the same message...`);
} else {
log.debug(`--- ${methodName} error: ${attempt.error.message}`);
}
lastError = attempt.error;
}
await delay(retryDelay);
}
}