Skip to content

feat(worker): rebuild global stats from logs - #3552

Closed
steebchen wants to merge 1 commit into
mainfrom
fix/global-stats-rebuild
Closed

steebchen wants to merge 1 commit into
mainfrom
fix/global-stats-rebuild

Conversation

@steebchen

@steebchen steebchen commented Aug 11, 2026 •

Copy link
Copy Markdown
Member

Why this is possible

The mode/kind migration (1786136603_warm_omega_flight.sql) left a large slice of global_model_stats / global_source_stats with used_mode and org_kind set to unknown, so the admin Global Stats page can't make Credits + BYOK add up to All and shows almost no organization-kind attribution.

The migration's note says recovering that means re-reading raw logs, which retention has pruned. That turns out to be wrong. cleanupExpiredLogData (apps/worker/src/worker.ts:740) is an UPDATE, not a DELETE — after 30 days it nulls the verbose payload columns and sets data_retention_cleaned_up:

messages, content, reasoningContent, tools, toolChoice, toolResults,
customHeaders, rawRequest, rawResponse, upstreamRequest, upstreamResponse,
userAgent, gatewayContentFilterResponse, responsesApiData

No production code path deletes log rows at all — not the retention job, not account deletion. And not one column the aggregator reads is in that list: used_model, used_provider, used_mode, source, organization_id, created_at, every token column, every cost column, has_error, cached, streamed, unified_finish_reason.

Confirmed against production: the earliest log row falls inside the first global_model_stats day bucket, so there is no stats history predating the logs. A rebuild is lossless.

That makes re-deriving from source strictly better than a redistribution backfill — it yields an exact used_mode and org_kind for every row, with no proration and no scaling.

Design

Each day goes through the aggregator's own recomputeDayFully(), which already deletes that day's rows and re-aggregates its 24 hours. Reusing it means no new aggregation logic to keep in sync — the only production change is making that function exported.

  • Newest day first. The recent days people actually look at are correct within minutes rather than at the end of a multi-hour run.
  • No truncate, no watermark change. The table is never fully empty; each day flips atomically from wrong to right. Truncating and letting the incremental walker rebuild would leave the dashboard blank for the whole run and rebuild oldest-first, so the most-viewed days would come back last — plus resetting global_aggregation_state without also raising GLOBAL_STATS_INITIAL_LOOKBACK_DAYS (default 30) would silently rebuild only one month.
  • Today and yesterday are skipped. The incremental walker owns today, the safety net wipes-and-recomputes yesterday; racing either risks interleaving a delete with an insert and double-counting a day. --to is clamped accordingly.
  • Idempotent and resumable. Re-running any day is safe; a crashed run continues with --from/--to.

Usage

Dry run by default — reports the plan and how many rows are currently unattributed.

pnpm --filter worker rebuild-global-stats                       # dry run
pnpm --filter worker rebuild-global-stats --commit --limit=7    # 7 newest days, then eyeball the page
pnpm --filter worker rebuild-global-stats --commit              # the rest
pnpm --filter worker rebuild-global-stats --commit --from=2025-05-20 --to=2026-08-01
pnpm --filter worker rebuild-global-stats --commit --oldest-first

Verification

Driven end to end against a local stack seeded with real log rows in the exact shape the migration collapsed — 100 logs on one model and one UTC day, 60 credits/PAYG and 40 BYOK/DevPass, plus a bogus pre-existing unknown/unknown aggregate row.

After the rebuild:

 used_mode | org_kind | request_count | total_tokens | cost
-----------+----------+---------------+--------------+-------
 api-keys  | devpass  |            40 |         4800 | 20.00
 credits   | default  |            60 |         7200 | 30.00

 TOTAL: 100 requests / 50.00 / 12000 tokens   ← matches `log` exactly

Exact, not approximated. Also verified:

  • Idempotent — a second --commit run produces identical rows.
  • global_source_stats rebuilt with the same attribution, including a source value containing spaces and a slash.
  • --to clamping — a future --to is clamped to today-2 with an explanatory message.
  • --limit — restricts to the N newest days.

apps/worker suite: global-stats-aggregator.spec.ts passes 5/5. Four sync-models.spec.ts tests time out at 30s in my environment; they passed earlier in the same session at 5–11s each and are unaffected by this change (which adds an export and a new file), so I've treated them as environment flake rather than a regression.

pnpm format and pnpm build clean.

Operational notes

  • Back up the two tables first — they're small, and the rebuild is delete-then-insert per day.
  • Each day is 24 hours × 2 GROUP BY scans over log. The model query rides log_created_at_used_model_used_provider_idx; the source query groups on source, which isn't in that index. Run it off-peak.
  • The rebuild recomputes totals, not just attribution. The global tables currently carry marginally more traffic than the project hourly aggregates, so expect the headline to shift slightly — the rebuilt figure is whatever log actually says.

Relationship to the other PRs

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a worker command to rebuild global model and source statistics from retained logs.
    • Supports date ranges, commit and dry-run modes, result ordering, processing limits, progress reporting, and resumable execution.
    • Automatically excludes recent days that may still be receiving data and safely handles empty or invalid ranges.
  • Improvements

    • Enabled full recomputation of selected days for reliable statistics recovery.

The mode/kind migration (1786136603_warm_omega_flight.sql) left a large
slice of global_model_stats / global_source_stats as used_mode and
org_kind 'unknown', so the admin Global Stats page cannot make Credits +
BYOK add up to All and shows almost no organization-kind attribution.

Rebuilding from the source data fixes it exactly, because data retention
never deletes log rows — cleanupExpiredLogData only nulls the verbose
payload columns (messages, content, raw/upstream request and response,
tools, customHeaders, userAgent, responsesApiData). Every column the
aggregator reads survives, so the rebuild is lossless and needs no
proration or scaling.

Each day goes through the aggregator's own recomputeDayFully(), which
deletes that day and re-aggregates its 24 hours, so the script is
idempotent and resumable. Newest day first, so the recent days people
actually look at are correct within minutes rather than at the end of the
run, and the table is never fully empty. Today and yesterday are skipped:
the incremental walker owns today and the safety net owns yesterday, and
racing either risks interleaving a delete with an insert.

Dry run by default; --commit to apply.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 11, 2026 23:17
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The worker now includes a CLI script and npm command to rebuild global model and source statistics from retained log rows. The script supports date planning, dry runs, limits, ordering, progress reporting, and committed recomputation.

Changes

Global statistics rebuild

Layer / File(s) Summary
Rebuild planning, execution, and command wiring
apps/worker/src/scripts/rebuild-global-stats.ts, apps/worker/src/services/global-stats-aggregator.ts, apps/worker/package.json
The worker parses rebuild options, resolves and protects date ranges, supports dry-run and limited ordered execution, reports progress, and invokes exported recomputeDayFully. The npm script runs the rebuild with tsx and the shared environment file.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant rebuild_global_stats
  participant RetainedLogRows
  participant recomputeDayFully
  CLI->>rebuild_global_stats: Provide rebuild flags
  rebuild_global_stats->>RetainedLogRows: Resolve dates and attribution counts
  rebuild_global_stats->>recomputeDayFully: Recompute selected days
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a worker feature to rebuild global statistics from logs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/global-stats-rebuild

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/worker/src/scripts/rebuild-global-stats.ts (1)

138-149: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Report the dry-run counts for the planned range.

The dry-run query counts unattributed rows across the whole table. The planned range can be a subset, for example when --from, --to, or --limit is set. The printed numbers then do not describe the work the run would perform.

Add a day_timestamp filter for the planned range.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/worker/src/scripts/rebuild-global-stats.ts` around lines 138 - 149,
Update the dry-run query in the !commit branch of the rebuild flow to apply the
same planned-range day_timestamp bounds used by the rebuild, including --from,
--to, and any --limit-derived endpoint. Keep the existing unattributed-row
conditions and report counts only for rows that the run would process.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/worker/src/scripts/rebuild-global-stats.ts`:
- Line 93: Validate every CLI date and numeric flag before using it: in the
limit parsing around apps/worker/src/scripts/rebuild-global-stats.ts lines
93-93, parse --limit once and reject values that are not positive integers, then
update the day selection around line 132 to use the undefined check so a zero
limit does not select all days. In the --from/--to handling at lines 102-118,
reject Invalid Date results before clamp and range validation proceed.
- Around line 153-158: Register SIGINT and SIGTERM handlers in the standalone
script before the loop that invokes recomputeDayFully, using the existing
stop-request mechanism so signals set the requested-stop state instead of
terminating immediately. Preserve the loop’s current halted-after-completion
behavior and cleanup any handlers when the script finishes.
- Around line 78-88: Update the least(...) start-boundary query in the
rebuild-global-stats flow to interpret the timestamp without time zone values as
UTC before node-postgres converts them to Date objects. Apply AT TIME ZONE 'UTC'
to the SQL expression (or parse an explicitly Z-suffixed text value), then
preserve the existing floorToUTCDay handling and null behavior.

---

Nitpick comments:
In `@apps/worker/src/scripts/rebuild-global-stats.ts`:
- Around line 138-149: Update the dry-run query in the !commit branch of the
rebuild flow to apply the same planned-range day_timestamp bounds used by the
rebuild, including --from, --to, and any --limit-derived endpoint. Keep the
existing unattributed-row conditions and report counts only for rows that the
run would process.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3af539e7-c876-47a8-9364-5eeb2c6f6e50

📥 Commits

Reviewing files that changed from the base of the PR and between 022ea99 and e87b616.

📒 Files selected for processing (3)
  • apps/worker/package.json
  • apps/worker/src/scripts/rebuild-global-stats.ts
  • apps/worker/src/services/global-stats-aggregator.ts

Comment on lines +78 to +88
const result = await db.execute(sql`
select least(
(select created_at from log order by created_at limit 1),
(select min(day_timestamp) from global_model_stats),
(select min(day_timestamp) from global_source_stats)
) as start
`);
const start = (result.rows[0] as { start: Date | string | null } | undefined)
?.start;
return start ? floorToUTCDay(new Date(start)) : null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check the column types for log.created_at, global_model_stats.day_timestamp, and global_source_stats.day_timestamp.

fd -e ts -e sql . --exec rg -n -C3 'day_timestamp|dayTimestamp' {} \; | head -60

echo "---- schema definitions ----"
ast-grep run --pattern 'timestamp($$$)' --lang typescript 2>/dev/null | head -40

Repository: theopenco/llmgateway

Length of output: 4960


🏁 Script executed:

#!/bin/bash
set -e

echo "---- rebuild script ----"
sed -n '1,130p' apps/worker/src/scripts/rebuild-global-stats.ts

echo "---- schema declarations ----"
rg -n -C4 'createdAt|dayTimestamp|globalModelStats|globalSourceStats' packages/db/src/schema.ts

echo "---- migrations ----"
rg -n -i -C3 'global_model_stats|global_source_stats|created_at.*timestamp|day_timestamp.*timestamp' packages/db drizzle migrations 2>/dev/null | head -240

echo "---- timestamp configuration and PostgreSQL clients ----"
rg -n -C3 'timestamp\(|withTimezone|parseInputDatesAsUTC|pg-types|node-postgres|timezone|TZ' packages apps package.json pnpm-lock.yaml 2>/dev/null | head -240

Repository: theopenco/llmgateway

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -e

echo "---- exact global-stats migrations ----"
rg -l -i 'global_model_stats|global_source_stats' packages/db/migrations | sort | while read -r f; do
	echo "### $f"
	rg -n -i -C5 'global_model_stats|global_source_stats' "$f"
done

echo "---- log table creation ----"
rg -l -n 'CREATE TABLE "log"|created_at.*timestamp' packages/db/migrations | while read -r f; do
	if rg -q 'CREATE TABLE "log"' "$f"; then
		echo "### $f"
		rg -n -C8 'CREATE TABLE "log"|created_at.*timestamp' "$f"
	fi
done

echo "---- database client and timestamp options ----"
fd -i 'package.json|*.ts' packages/db apps/worker --exec rg -n -C5 'drizzle\(|new Pool|Pool\(|parseInputDatesAsUTC|pg-types|timestamp|DATABASE_URL' {} \; | head -240

Repository: theopenco/llmgateway

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -e

echo "---- exact global-stats migrations ----"
rg -l -i 'global_model_stats|global_source_stats' packages/db/migrations | sort | while read -r f; do
	echo "### $f"
	rg -n -i -C5 'global_model_stats|global_source_stats' "$f"
done

echo "---- log table creation ----"
rg -l 'CREATE TABLE "log"' packages/db/migrations | while read -r f; do
	echo "### $f"
	rg -n -C8 'CREATE TABLE "log"|created_at.*timestamp' "$f"
done

echo "---- database client and timestamp options ----"
fd -i 'package.json|*.ts' packages/db apps/worker --exec rg -n -C5 'drizzle\(|new Pool|Pool\(|parseInputDatesAsUTC|pg-types|timestamp|DATABASE_URL' {} \; | head -240

Repository: theopenco/llmgateway

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -e

echo "---- migration files containing table creation ----"
rg -l --glob '*.sql' 'CREATE TABLE "global_model_stats"|CREATE TABLE "global_source_stats"|CREATE TABLE "log"' packages/db/migrations | sort

echo "---- exact CREATE TABLE statements ----"
rg -n --glob '*.sql' -C12 'CREATE TABLE "global_model_stats"|CREATE TABLE "global_source_stats"|CREATE TABLE "log"' packages/db/migrations

echo "---- snapshot type records for the three columns ----"
python3 - <<'PY'
import json
from pathlib import Path

targets = {
	("log", "created_at"),
	("global_model_stats", "day_timestamp"),
	("global_source_stats", "day_timestamp"),
}
for path in sorted(Path("packages/db/migrations/meta").glob("*_snapshot.json")):
	try:
		data = json.loads(path.read_text())
	except Exception:
		continue
	for item in data.get("tables", {}).values():
		if not isinstance(item, dict):
			continue
		table = item.get("name")
		if table not in {t[0] for t in targets}:
			continue
		for name, col in item.get("columns", {}).items():
			if (table, name) in targets:
				print(path.name, table, name, col.get("dataType"), col.get("columnType"), col.get("type"))
PY

echo "---- client configuration and dependency versions ----"
rg -n -C6 --glob '*.ts' --glob 'package.json' --glob 'pnpm-lock.yaml' \
	'drizzle\(|new Pool|Pool\(|parseInputDatesAsUTC|pg-types|node-postgres|DATABASE_URL' \
	packages/db apps/worker package.json pnpm-lock.yaml

Repository: theopenco/llmgateway

Length of output: 45016


🌐 Web query:

node-postgres timestamp without time zone parses local timezone Date parser

💡 Result:

By default, node-postgres parses PostgreSQL TIMESTAMP WITHOUT TIME ZONE columns as JavaScript Date objects based on the local time zone of the Node.js process [1][2]. The driver uses the postgres-date library to handle this conversion [3]. Because TIMESTAMP WITHOUT TIME ZONE lacks timezone information, the parser treats the input string as a local time when constructing the Date object [4][3]. This can lead to unexpected behavior if your application expects UTC or if your Node.js server and database server operate in different time zones [1][5]. To change this behavior, you can override the default type parser provided by the pg library [1][6]. Common approaches include: 1. Returning the raw string: If you want to avoid automatic Date conversion entirely, you can disable the parser [7][5]. const { types } = require('pg'); const TIMESTAMP_OID = 1114; types.setTypeParser(TIMESTAMP_OID, (timestamp) => timestamp); 2. Forcing UTC interpretation: If you need a Date object but want it interpreted as UTC, you can append a Z (UTC indicator) to the string before passing it to the Date constructor [8]. const { types } = require('pg'); const TIMESTAMP_OID = 1114; types.setTypeParser(TIMESTAMP_OID, (stringValue) => { return new Date(${stringValue.replace(' ', 'T')}Z); }); Note: If you are experiencing issues with time offsets, it is often recommended to use the TIMESTAMPTZ (timestamp with time zone) data type in PostgreSQL, which stores the value in UTC and avoids these ambiguity issues [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -e

node - <<'JS'
const value = "2025-05-20 00:00:00";

for (const tz of ["UTC", "America/Los_Angeles", "Asia/Tokyo"]) {
	const parsed = require("child_process")
		.execFileSync(process.execPath, ["-e", `
			const d = new Date(${JSON.stringify(value)});
			const floored = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
			console.log(JSON.stringify({
				tz: process.env.TZ,
				parsed: d.toISOString(),
				floored: floored.toISOString()
			}));
		`], { env: { ...process.env, TZ: tz } })
		.toString()
		.trim();
	console.log(parsed);
}
JS

Repository: theopenco/llmgateway

Length of output: 439


Interpret the timestamp boundary as UTC. The three columns are timestamp without time zone values, and node-postgres parses them as local-time Date objects. Under TZ=Asia/Tokyo, floorToUTCDay can select the previous day. Convert the least(...) expression with AT TIME ZONE 'UTC', or parse text with an explicit Z; a bare text cast is insufficient.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/worker/src/scripts/rebuild-global-stats.ts` around lines 78 - 88, Update
the least(...) start-boundary query in the rebuild-global-stats flow to
interpret the timestamp without time zone values as UTC before node-postgres
converts them to Date objects. Apply AT TIME ZONE 'UTC' to the SQL expression
(or parse an explicitly Z-suffixed text value), then preserve the existing
floorToUTCDay handling and null behavior.

async function main(): Promise<void> {
const commit = hasFlag("commit");
const oldestFirst = hasFlag("oldest-first");
const limit = parseFlag("limit") ? Number(parseFlag("limit")) : undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate all CLI flags before use. The script converts process.argv values into numbers and dates without validation. Malformed input then propagates as NaN or Invalid Date, which either silently rebuilds nothing, rebuilds everything, or throws RangeError during logging.

  • apps/worker/src/scripts/rebuild-global-stats.ts#L93-L93: parse --limit once and reject values that are not positive integers; also change line 132 to limit === undefined ? days : days.slice(0, limit) so --limit=0 does not select every day.
  • apps/worker/src/scripts/rebuild-global-stats.ts#L102-L118: reject --from and --to values that produce an Invalid Date before the clamp and range checks run.
📍 Affects 1 file
  • apps/worker/src/scripts/rebuild-global-stats.ts#L93-L93 (this comment)
  • apps/worker/src/scripts/rebuild-global-stats.ts#L102-L118
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/worker/src/scripts/rebuild-global-stats.ts` at line 93, Validate every
CLI date and numeric flag before using it: in the limit parsing around
apps/worker/src/scripts/rebuild-global-stats.ts lines 93-93, parse --limit once
and reject values that are not positive integers, then update the day selection
around line 132 to use the undefined check so a zero limit does not select all
days. In the --from/--to handling at lines 102-118, reject Invalid Date results
before clamp and range validation proceed.

Comment on lines +153 to +158
for (const day of planned) {
const completed = await recomputeDayFully(day);
if (!completed) {
console.log(`Stop requested — halted after ${done} day(s).`);
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find where the stop flag used by isStopRequested is set.

rg -nP --type=ts -C5 '\b(isStopRequested|requestStop|stopRequested)\b'

Repository: theopenco/llmgateway

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate file ---'
wc -l apps/worker/src/scripts/rebuild-global-stats.ts
sed -n '1,220p' apps/worker/src/scripts/rebuild-global-stats.ts

printf '%s\n' '--- stop-related definitions and usages ---'
rg -n -C4 'isStopRequested|requestStop|stopRequested|SIGINT|SIGTERM|shutdown|graceful' . \
	-g '!node_modules' -g '!dist' -g '!build'

Repository: theopenco/llmgateway

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

service_file="$(fd -t f 'global-stats-aggregator' . | head -n 1)"
printf 'service_file=%s\n' "$service_file"
wc -l "$service_file"
ast-grep outline "$service_file"
printf '%s\n' '--- recomputeDayFully and direct stop references ---'
rg -n -C12 'recomputeDayFully|isStopRequested|requestStop|stopRequested' "$service_file"
printf '%s\n' '--- all exact symbol references, excluding generated/dependency files ---'
rg -n --hidden -g '!node_modules' -g '!dist' -g '!build' -g '!pnpm-lock.yaml' '\bisStopRequested\b|\brequestStop\b|\bstopRequested\b' .

Repository: theopenco/llmgateway

Length of output: 486


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate service files ---'
fd -t f 'global-stats-aggregator' apps/worker

printf '%s\n' '--- implementation references ---'
rg -n -C16 'recomputeDayFully|isStopRequested|requestStop|stopRequested' apps/worker/src \
	-g '*.ts' -g '!*.spec.ts'

Repository: theopenco/llmgateway

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- shutdown state and signal registration ---'
cat -n apps/worker/src/shutdown.ts | sed -n '1,140p'
printf '%s\n' '--- process signal handlers in worker sources ---'
rg -n -C12 "process\.on\(['\"](SIGINT|SIGTERM)|requestStop\(" apps/worker/src \
	-g '*.ts' -g '!*.spec.ts'
printf '%s\n' '--- worker entrypoint imports and startup/shutdown section ---'
rg -n -C10 'from "./shutdown\.js"|from "`@/shutdown`\.js"|process\.on|startWorker|main\(' apps/worker/src/worker.ts apps/worker/src/index.ts apps/worker/src/main.ts 2>/dev/null || true

Repository: theopenco/llmgateway

Length of output: 11347


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

script = Path("apps/worker/src/scripts/rebuild-global-stats.ts").read_text()
aggregator = Path("apps/worker/src/services/global-stats-aggregator.ts").read_text()
shutdown = Path("apps/worker/src/shutdown.ts").read_text()
worker_entry = Path("apps/worker/src/index.ts").read_text()

checks = {
	"script imports recomputeDayFully": "recomputeDayFully" in script,
	"aggregator imports isStopRequested": 'import { isStopRequested } from "`@/shutdown.js`";' in aggregator,
	"recomputeDayFully checks isStopRequested": bool(
		re.search(r"export async function recomputeDayFully[\s\S]*?isStopRequested\(\)", aggregator)
	),
	"shutdown initializes shouldStop=false": "let shouldStop = false;" in shutdown,
	"standalone script has SIGINT handler": bool(
		re.search(r"process\.on\(\s*[\"']SIGINT[\"']", script)
	),
	"standalone script has SIGTERM handler": bool(
		re.search(r"process\.on\(\s*[\"']SIGTERM[\"']", script)
	),
	"worker entrypoint has SIGINT handler": bool(
		re.search(r"process\.on\(\s*[\"']SIGINT[\"']", worker_entry)
	),
	"worker entrypoint has SIGTERM handler": bool(
		re.search(r"process\.on\(\s*[\"']SIGTERM[\"']", worker_entry)
	),
}
for name, result in checks.items():
	print(f"{name}: {result}")
PY

Repository: theopenco/llmgateway

Length of output: 505


Register signal handlers for the standalone script.

recomputeDayFully() checks isStopRequested(), but this script does not register SIGINT or SIGTERM handlers. A signal can terminate the process during a day rebuild and leave that day partially rebuilt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/worker/src/scripts/rebuild-global-stats.ts` around lines 153 - 158,
Register SIGINT and SIGTERM handlers in the standalone script before the loop
that invokes recomputeDayFully, using the existing stop-request mechanism so
signals set the requested-stop state instead of terminating immediately.
Preserve the loop’s current halted-after-completion behavior and cleanup any
handlers when the script finishes.

@steebchen

Copy link
Copy Markdown
Member Author

Superseded: going with a plain truncate + watermark reset in production and letting the incremental walker rebuild, rather than adding a script for it.

@steebchen steebchen closed this Aug 12, 2026
@steebchen
steebchen deleted the fix/global-stats-rebuild branch August 12, 2026 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants