| Index: web/inc/rpc/client.ts
|
| diff --git a/web/inc/rpc/client.ts b/web/inc/rpc/client.ts
|
| index 9d3928ff54b2de3008a4d84a11a3adcb5b94f042..3829aa87a07b730d592e706e02bacbda6cd88c18 100644
|
| --- a/web/inc/rpc/client.ts
|
| +++ b/web/inc/rpc/client.ts
|
| @@ -73,33 +73,28 @@ namespace luci {
|
| */
|
| call<T, R>(service: string, method: string, request?: T): Promise<R> {
|
| let transientRetry = new RetryIterator(this.transientRetry);
|
| - let doCall = (): Promise<R> => {
|
| - // Configure the client for this request.
|
| - this.pc.service = service;
|
| - this.pc.method = method;
|
| - this.pc.request = request;
|
| -
|
| - // Execute the configured request.
|
| - let callPromise: Promise<R> = this.pc.call().completes;
|
| - return callPromise.then((resp: any) => resp.response)
|
| - .catch((err: Error) => {
|
| - // Is this a transient error?
|
| - if (isTransientError(err)) {
|
| - let delay = transientRetry.next();
|
| - if (delay >= 0) {
|
| - console.warn(
|
| - `Transient error calling ` +
|
| - `${service}.${method} with params:`,
|
| - request, `:`, err, `; retrying after ${delay}ms.`);
|
| - return luci.sleepPromise(delay).then(doCall);
|
| - }
|
| - }
|
| + return transientRetry
|
| + .do(
|
| + () => {
|
| + // Configure the client for this request.
|
| + this.pc.service = service;
|
| + this.pc.method = method;
|
| + this.pc.request = request;
|
|
|
| - // Non-transient, throw the error.
|
| - throw err;
|
| - });
|
| - };
|
| - return doCall();
|
| + // Execute the configured request.
|
| + return this.pc.call().completes;
|
| + },
|
| + (err: Error, delay: number) => {
|
| + // Is this a transient error?
|
| + if (!isTransientError(err)) {
|
| + throw err;
|
| + }
|
| + console.warn(
|
| + `Transient error calling ` +
|
| + `${service}.${method} with params:`,
|
| + request, `:`, err, `; retrying after ${delay}ms.`);
|
| + })
|
| + .then((resp: any) => resp.response);
|
| }
|
| }
|
|
|
| @@ -216,20 +211,29 @@ namespace luci {
|
| export type Retry = {
|
| // The number of retries to perform before failing. If undefined, will retry
|
| // indefinitely.
|
| - retries: number | undefined;
|
| + retries?: number;
|
|
|
| // The amount of time to delay in between retry attempts, in milliseconds.
|
| // If undefined or < 0, no delay will be imposed.
|
| delay: number;
|
| // The maximum delay to apply, in milliseconds. If > 0 and delay scales past
|
| // "maxDelay", it will be capped at "maxDelay".
|
| - maxDelay: number | undefined;
|
| + maxDelay?: number;
|
|
|
| // delayScaling is the multiplier applied to "delay" in between retries. If
|
| // undefined or <= 1, DEFAULT_DELAY_SCALING will be used.
|
| delayScaling?: number;
|
| };
|
|
|
| + /** RetryCallback is an optional callback type used in "do". */
|
| + export type RetryCallback = (err: Error, delay: number) => void;
|
| +
|
| + /**
|
| + * Stopped is a sentinel error thrown by RetryIterator when it runs out of
|
| + * retries.
|
| + */
|
| + export const STOPPED = new Error('retry stopped');
|
| +
|
| /**
|
| * Generic exponential backoff retry delay generator.
|
| *
|
| @@ -260,12 +264,12 @@ namespace luci {
|
| * @returns the next delay, in milliseconds. If there are no more retries,
|
| * returns undefined.
|
| */
|
| - next(): number|undefined {
|
| + next(): number {
|
| // Apply retries, if they have been enabled.
|
| if (this.retries !== undefined) {
|
| if (this.retries <= 0) {
|
| // No more retries remaining.
|
| - return undefined;
|
| + throw STOPPED;
|
| }
|
| this.retries--;
|
| }
|
| @@ -277,5 +281,53 @@ namespace luci {
|
| }
|
| return delay;
|
| }
|
| +
|
| + /**
|
| + * Executes a Promise, retrying if the Promise raises an error.
|
| + *
|
| + * "do" iteratively tries to execute a Promise, generated by "gen". If that
|
| + * Promise raises an error, "do" will retry until it either runs out of
|
| + * retries, or the Promise does not return an error. Each retry, "do" will
|
| + * invoke "gen" again to generate a new Promise.
|
| + *
|
| + * An optional "onError" callback can be supplied. If it is, it will be
|
| + * invoked in between each retry. The callback may, itself, throw, in which
|
| + * case the retry loop will stop. This can be used for reporting and/or
|
| + * selective retries.
|
| + *
|
| + * @param gen Promise generator function for retries.
|
| + * @param onError optional callback to be invoked in between retries.
|
| + *
|
| + * @throws any the error generated by "gen"'s Promise, if out of retries,
|
| + * or the error raised by onError if it chooses to throw.
|
| + */
|
| + do
|
| + <T>(gen: () => Promise<T>, onError?: RetryCallback): Promise<T> {
|
| + let onErr = (err: Error): Promise<T> => {
|
| + let delay: number;
|
| + try {
|
| + delay = this.next();
|
| + } catch (e) {
|
| + if (e !== STOPPED) {
|
| + console.warn('Unexpected error generating next delay:', e);
|
| + }
|
| +
|
| + // If we could not generate another retry delay, raise the initial
|
| + // Promise's error.
|
| + throw err;
|
| + }
|
| +
|
| + if (onError) {
|
| + // Note: this may throw.
|
| + onError(err, delay);
|
| + }
|
| +
|
| + return luci.sleepPromise(delay).then(() => {
|
| + return gen().catch(onErr);
|
| + });
|
| + };
|
| +
|
| + return gen().catch(onErr);
|
| + }
|
| }
|
| }
|
|
|