Skip to content

Commit eb09541

Browse files
committed
Add generic retryWithBackoff helper
1 parent e112be1 commit eb09541

File tree

1 file changed

+83
-0
lines changed

1 file changed

+83
-0
lines changed

src/gcp/retryWithBackoff.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { backOff, type BackoffOptions } from "exponential-backoff";
2+
3+
/** Extract an HTTP status from common firebase-tools error shapes. */
4+
export const statusOf = (err: any): number | undefined =>
5+
err?.context?.response?.statusCode ?? err?.status ?? err?.statusCode;
6+
7+
export interface HttpBackoffOptions extends Partial<BackoffOptions> {
8+
/**
9+
* Optional: custom predicate to decide if an error should be retried.
10+
* If provided, this takes precedence over `statuses`.
11+
*/
12+
shouldRetry?: (err: unknown, nextAttempt: number) => boolean;
13+
14+
/**
15+
* Optional: retry when HTTP status is in this set (e.g., [429, 503]).
16+
* Ignored if `shouldRetry` is provided.
17+
*/
18+
statuses?: readonly number[] | ReadonlySet<number>;
19+
20+
/**
21+
* Optional: called right before a retry. Use e.g. to log context.
22+
*/
23+
onRetry?: (info: {
24+
err: unknown;
25+
attempt: number;
26+
maxAttempts?: number;
27+
status?: number;
28+
}) => void;
29+
}
30+
31+
/** Base backoff knobs (env-tunable if you want to tweak in CI). */
32+
const defaultBackoff: BackoffOptions = {
33+
numOfAttempts: Number(process.env.FIREBASE_TOOLS_FUNC_RETRY_MAX ?? 9),
34+
startingDelay: Number(process.env.FIREBASE_TOOLS_FUNC_RETRY_BASE_MS ?? 60 * 1_000),
35+
timeMultiple: Number(process.env.FIREBASE_TOOLS_FUNC_RETRY_MULTIPLIER ?? 2),
36+
maxDelay: Number(process.env.FIREBASE_TOOLS_FUNC_RETRY_MAX_MS ?? 20 * 60 * 1_000),
37+
jitter: "full",
38+
delayFirstAttempt: true,
39+
};
40+
41+
/**
42+
* Generic exponential backoff wrapper for HTTP-ish operations.
43+
* - Retries when `shouldRetry` returns true OR status is in `statuses`.
44+
* - If neither is provided, nothing will retry.
45+
*/
46+
export async function withHttpBackoff<T>(
47+
thunk: () => Promise<T>,
48+
opts: HttpBackoffOptions = {},
49+
): Promise<T> {
50+
const statuses =
51+
opts.statuses instanceof Set
52+
? opts.statuses
53+
: Array.isArray(opts.statuses)
54+
? new Set<number>(opts.statuses)
55+
: undefined;
56+
57+
const options = {
58+
...defaultBackoff,
59+
...opts,
60+
};
61+
62+
return backOff(thunk, {
63+
...options,
64+
retry: (err, nextAttempt) => {
65+
const st = statusOf(err);
66+
const shouldRetry =
67+
opts.shouldRetry?.(err, nextAttempt) ??
68+
(st !== undefined && statuses ? statuses.has(st) : false);
69+
70+
if (shouldRetry) {
71+
if (opts.onRetry) {
72+
opts.onRetry({
73+
err,
74+
attempt: nextAttempt,
75+
maxAttempts: options.numOfAttempts,
76+
status: st,
77+
});
78+
}
79+
}
80+
return shouldRetry;
81+
},
82+
});
83+
}

0 commit comments

Comments
 (0)