From 85ef35dd64c9a4e3afd640eaa6c843c3849aa943 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:53:56 -0500 Subject: [PATCH] fix(data): oom crash in stats sync --- infra/stats.ts | 4 ++- packages/stats/core/src/athena.ts | 41 +++++++++++++++++-------------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/infra/stats.ts b/infra/stats.ts index b5b0e1c600..10d37119f0 100644 --- a/infra/stats.ts +++ b/infra/stats.ts @@ -185,7 +185,9 @@ export const statSync = new sst.aws.Service("StatsSyncService", { cluster: lakeCluster, architecture: "arm64", cpu: "0.25 vCPU", - memory: "0.5 GB", + // 0.5 GB caused an OOM crash loop: every restart immediately re-ran the 4 Athena + // stats queries (~$5/pass) every ~5 minutes instead of hourly. + memory: "2 GB", image: { context: ".", dockerfile: "packages/stats/server/Dockerfile", diff --git a/packages/stats/core/src/athena.ts b/packages/stats/core/src/athena.ts index 50c356afa5..8879d52a66 100644 --- a/packages/stats/core/src/athena.ts +++ b/packages/stats/core/src/athena.ts @@ -116,28 +116,33 @@ const poll: ( const results: ( client: AwsAthenaClient, queryExecutionId: string, - nextToken?: string, ) => Effect.Effect = Effect.fn("Athena.results")(function* ( client: AwsAthenaClient, queryExecutionId: string, - nextToken?: string, ) { - const result = yield* Effect.tryPromise({ - try: () => - client.send( - new GetQueryResultsCommand({ - QueryExecutionId: queryExecutionId, - NextToken: nextToken, - MaxResults: ATHENA_PAGE_SIZE, - }), - ), - catch: (cause) => new AthenaQueryError({ message: "Failed to read Athena stats results", queryExecutionId, cause }), - }) - const columns = result.ResultSet?.ResultSetMetadata?.ColumnInfo?.map((item) => item.Name ?? "") ?? [] - const rows = (result.ResultSet?.Rows ?? []).slice(nextToken ? 0 : 1).map((row) => rowData(columns, row)) - - if (!result.NextToken) return rows - return [...rows, ...(yield* results(client, queryExecutionId, result.NextToken))] + // Accumulate pages iteratively; recursive spreads copied every previously + // fetched row per page and blew up memory on large result sets. + const rows: AthenaData[] = [] + let nextToken: string | undefined + while (true) { + const result = yield* Effect.tryPromise({ + try: () => + client.send( + new GetQueryResultsCommand({ + QueryExecutionId: queryExecutionId, + NextToken: nextToken, + MaxResults: ATHENA_PAGE_SIZE, + }), + ), + catch: (cause) => + new AthenaQueryError({ message: "Failed to read Athena stats results", queryExecutionId, cause }), + }) + const columns = result.ResultSet?.ResultSetMetadata?.ColumnInfo?.map((item) => item.Name ?? "") ?? [] + // The first page starts with the header row. + for (const row of (result.ResultSet?.Rows ?? []).slice(nextToken ? 0 : 1)) rows.push(rowData(columns, row)) + if (!result.NextToken) return rows + nextToken = result.NextToken + } }) function rowData(columns: string[], row: Row): AthenaData {