Bläddra i källkod

fix(data): oom crash in stats sync

Adam 1 månad sedan
förälder
incheckning
85ef35dd64
2 ändrade filer med 26 tillägg och 19 borttagningar
  1. 3 1
      infra/stats.ts
  2. 23 18
      packages/stats/core/src/athena.ts

+ 3 - 1
infra/stats.ts

@@ -185,7 +185,9 @@ export const statSync = new sst.aws.Service("StatsSyncService", {
   cluster: lakeCluster,
   cluster: lakeCluster,
   architecture: "arm64",
   architecture: "arm64",
   cpu: "0.25 vCPU",
   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: {
   image: {
     context: ".",
     context: ".",
     dockerfile: "packages/stats/server/Dockerfile",
     dockerfile: "packages/stats/server/Dockerfile",

+ 23 - 18
packages/stats/core/src/athena.ts

@@ -116,28 +116,33 @@ const poll: (
 const results: (
 const results: (
   client: AwsAthenaClient,
   client: AwsAthenaClient,
   queryExecutionId: string,
   queryExecutionId: string,
-  nextToken?: string,
 ) => Effect.Effect<AthenaData[], AthenaQueryError> = Effect.fn("Athena.results")(function* (
 ) => Effect.Effect<AthenaData[], AthenaQueryError> = Effect.fn("Athena.results")(function* (
   client: AwsAthenaClient,
   client: AwsAthenaClient,
   queryExecutionId: string,
   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 {
 function rowData(columns: string[], row: Row): AthenaData {