Skip to content

feat(remote): say why a statistics refresh was skipped - #230

Merged
veksen merged 1 commit into
mainfrom
feat-refresh-decision-logging
Aug 12, 2026
Merged

feat(remote): say why a statistics refresh was skipped#230
veksen merged 1 commit into
mainfrom
feat-refresh-decision-logging

Conversation

@veksen

@veksen veksen commented Aug 12, 2026

Copy link
Copy Markdown
Member

Follow-up to #229. Same shape: observability only, no behaviour change.

Goal

The site project went five days without a production-statistics capture. #229 made the relay connection explain itself; this makes the statistics refresh do the same, so the next gap is diagnosable from one poll cycle instead of from inference.

What

Before: refreshStatsIfStale runs on every 60-second schema poll and has four ways to decline, all silent. An analyzer that had not captured statistics for days produced exactly the same log output as one refreshing on schedule.

After: each early return says which guard stopped it, and carries the number that matters.

Statistics refresh skipped: no drift (closest was public.project_queries at 42% of 50%), and 6.7h until the daily floor
Statistics refresh skipped: a refresh has been in flight for 4.2h; nothing else can start while it is
Statistics refresh skipped: backed off for another 11m after a failed refresh
Statistics refresh skipped: no drift baseline, so nothing can trigger a dump

How

Read src/remote/stats-drift.ts first. detectDrift's not-drifted verdict now carries closest, the table that came nearest the ratio without reaching it. A refresh that never fires reads the same from outside whether the database is quiet or one table is sitting just under the threshold, and those call for opposite fixes. On the project that went quiet the closest table was at 42% of a 50% ratio, which is the whole explanation and was invisible.

Then refreshStatsIfStale in src/remote/remote.ts. The first guard is split so "no baseline" and "already refreshing" report separately, and refreshingSince lets the second one say how long — a refresh that never settles wedges that flag for the life of the process, which is a real failure mode with no current symptom.

noteSkippedRefresh handles volume. Reporting every poll would add 1,440 lines a day and bury the log, since log.debug is not level-gated here. Reporting only changes would hide a steady state from any window that opens after it settled, which is the position the five-day gap left us in. So: on change, then on a 30-minute heartbeat.

Tests

Two cases in src/remote/stats-drift.test.ts cover closest — the table nearest the ratio is named with how far it moved, and no table is named when none was eligible for Size Drift.

The log lines themselves are not asserted. They have no callers and no return value, and a test that pins their wording would only restate the string.

Full suite passes: 442 tests across 43 files. npm run typecheck and npm run build are clean.

What to look for once deployed

The 2026-08-11 logs show a container that had been up four days, with the daily floor overdue by three, producing no refresh at all. Whether that was a wedged in-flight refresh, an unset baseline, or a schema poll that never fired is still open, and these lines separate the three.

The statistics refresh runs on every 60s schema poll and has four ways to
decline, all of them silent. A project that had not captured statistics for
five days looked identical in the logs to one refreshing on schedule, and the
logs from that window cannot tell us which guard was responsible.

Each early return now reports itself: no baseline, a refresh already in flight
and for how long, a backoff and how much is left, or no drift and how long
until the daily floor.

The no-drift line carries the near miss. detectDrift returns the table that
came closest to the ratio without reaching it, because a refresh that never
fires reads the same whether the database is quiet or one table is sitting just
under the threshold, and those want opposite fixes. On the project that went
quiet, the closest table was at 42% of a 50% ratio.

Reporting every poll would bury the log, and reporting only changes would hide
a steady state from any window that opens after it settled, which is the
position the five-day gap left us in. Lines are emitted on change and then on a
30-minute heartbeat.

Co-Authored-By: Claude <noreply@anthropic.com>

@github-actions github-actions Bot 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.

Query Doctor — 6 successful checks

 Cost regression — No query went up more than 5%
 Untested data access — No changed data-access file without a test
 New query — No new queries
 New query with index recommendation — No new query ships an index recommendation
 Schema drift — No schema changes
 High-value nudge — No index or rewrite past the threshold


More details via MCP → get_ci_run({ runId: "019ff79b-5dc4-7eec-8ef0-c8fc3249bf31" }) · view run · docs
3 queries read against main on assumed statistics of 10,000,000 rows per table. Sync production stats for costs measured against your real data.

@veksen
veksen merged commit 4b2273b into main Aug 12, 2026
6 checks passed
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.

1 participant