diff --git a/src/data-layer/fetchers/fetchBeaconChain.ts b/src/data-layer/fetchers/fetchBeaconChain.ts index e90a9fae2d4..047b49e1de6 100644 --- a/src/data-layer/fetchers/fetchBeaconChain.ts +++ b/src/data-layer/fetchers/fetchBeaconChain.ts @@ -5,11 +5,13 @@ import type { MetricReturnData, } from "@/lib/types" +import { fetchRetry } from "./fetchRetry" + export type BeaconChainData = BeaconchainEpochData & { apr: MetricReturnData } /** * Fetch beaconchain data from Beaconcha.in API. - * Combines epoch and ethstore endpoints (sequential to respect 1 req/sec limit). + * Combines epoch and ethstore endpoints sequentially. */ export async function fetchBeaconChain(): Promise { const base = "https://beaconcha.in" @@ -27,31 +29,26 @@ export async function fetchBeaconChain(): Promise { // Fetch epoch data const epochUrl = new URL("api/v1/epoch/latest", base).href - const epochResponse = await fetch(epochUrl, { headers }) + const epochResponse = await fetchRetry(epochUrl, { headers }) if (!epochResponse.ok) { const status = epochResponse.status console.warn("Beaconcha.in epoch fetch non-OK", { status, url: epochUrl }) - const error = `Beaconcha.in epoch responded with status ${status}` - throw new Error(error) + throw new Error(`Beaconcha.in epoch responded with status ${status}`) } const epochJson: EpochResponse = await epochResponse.json() const { validatorscount, eligibleether } = epochJson.data const totalEthStaked = Math.floor(eligibleether * 1e-9) - // Wait 1s to respect rate limit - await new Promise((r) => setTimeout(r, 1000)) - // Fetch ethstore data const ethstoreUrl = new URL("api/v1/ethstore/latest", base).href - const ethstoreResponse = await fetch(ethstoreUrl, { headers }) + const ethstoreResponse = await fetchRetry(ethstoreUrl, { headers }) if (!ethstoreResponse.ok) { const status = ethstoreResponse.status console.warn("Beaconcha.in ethstore fetch non-OK", { status, url: ethstoreUrl, }) - const error = `Beaconcha.in ethstore responded with status ${status}` - throw new Error(error) + throw new Error(`Beaconcha.in ethstore responded with status ${status}`) } const ethstoreJson: EthStoreResponse = await ethstoreResponse.json() const apr = ethstoreJson.data.apr diff --git a/src/data-layer/fetchers/fetchRetry.ts b/src/data-layer/fetchers/fetchRetry.ts new file mode 100644 index 00000000000..aeec72a9c8d --- /dev/null +++ b/src/data-layer/fetchers/fetchRetry.ts @@ -0,0 +1,32 @@ +/** + * Shared retry.fetch configuration for all data-layer fetchers. + * + * Uses Trigger.dev's built-in retry.fetch which automatically handles + * 429 (rate-limit) and 5xx (server error) responses with exponential backoff. + */ +import { retry } from "@trigger.dev/sdk/v3" + +const RETRY_BY_STATUS = { + "429": { + strategy: "backoff" as const, + maxAttempts: 2, + }, + "500-599": { + strategy: "backoff" as const, + maxAttempts: 2, + }, +} + +/** + * Drop-in replacement for `fetch()` with built-in 429 and 5xx retry. + * Delegates to Trigger.dev's `retry.fetch` under the hood. + */ +export function fetchRetry( + url: string | URL, + init?: RequestInit +): Promise { + return retry.fetch(url, { + ...init, + retry: { byStatus: RETRY_BY_STATUS }, + }) +} diff --git a/src/data-layer/tasks.ts b/src/data-layer/tasks.ts index 4fd81a23f1d..5b32d0e1de7 100644 --- a/src/data-layer/tasks.ts +++ b/src/data-layer/tasks.ts @@ -86,10 +86,10 @@ const DAILY: TaskDef[] = [ [KEYS.EVENTS, fetchEvents], [KEYS.DEVELOPER_TOOLS, fetchDeveloperTools], [KEYS.TRANSLATION_GLOSSARY, fetchTranslationGlossary], + [KEYS.BEACONCHAIN, fetchBeaconChain], ] const HOURLY: TaskDef[] = [ - [KEYS.BEACONCHAIN, fetchBeaconChain], [KEYS.BLOBSCAN_STATS, fetchBlobscanStats], [KEYS.ETHEREUM_MARKETCAP, fetchEthereumMarketcap], [KEYS.ETHEREUM_STABLECOINS_MCAP, fetchEthereumStablecoinsMcap], @@ -104,11 +104,7 @@ function createDataTask([key, fetchFn]: TaskDef) { return task({ id: key, retry: { - maxAttempts: 3, - factor: 2, - minTimeoutInMs: 2000, - maxTimeoutInMs: 30000, - randomize: true, + maxAttempts: 2, }, catchError: async ({ error }) => { logger.error(`[${key}] failed`, { error }) diff --git a/trigger.config.ts b/trigger.config.ts index d2285634a65..2d5c0730974 100644 --- a/trigger.config.ts +++ b/trigger.config.ts @@ -16,8 +16,8 @@ export default defineConfig({ enabledInDev: true, default: { maxAttempts: 1, - minTimeoutInMs: 1000, - maxTimeoutInMs: 10000, + minTimeoutInMs: 1_000, + maxTimeoutInMs: 30_000, factor: 2, randomize: true, },