Skip to content

fix(api): derive the pool/records TTL from what the query actually costs (#687) - #839

Merged
defenwycke merged 1 commit into
mainfrom
fix/687-adaptive-records-ttl
Sep 6, 2026
Merged

fix(api): derive the pool/records TTL from what the query actually costs (#687)#839
defenwycke merged 1 commit into
mainfrom
fix/687-adaptive-records-ttl

Conversation

@defenwycke

Copy link
Copy Markdown
Contributor

Closes #687.

pool/records?window=month could serve an answer up to five minutes stale.

The 300s was not arbitrary — it was sized for a query that no longer exists

The TTL was chosen when the month window measured 7-20s, where recomputing
often would hang the pool page. But that cost is a property of the data,
not the query: the month window's cost is dominated by how many rows of
shares_archive still fall inside it, and the archive stopped being written at
the v56 cutover.

Measured on ghost-vm6, 2026-09-06:

shares_archive rows (total, static) 1,981,755
archive's newest row 18.7 days old
archive rows inside the 30-day window 550,321
live shares rows inside the window 55,760
month query, cold (vm1) ~0.6s — not the ~20s the constant was sized for
live shares only (the limit it converges on) 95ms

The archive is frozen, so in roughly eleven days zero archive rows fall
inside the month window and the query converges on 95ms. Holding a 95ms answer
for five minutes is staleness bought for nothing.

⚠ That decay is a coincidence of the archive being frozen, not a fix — which is
why this PR does not simply lower the constants to today's numbers. They would
be wrong again, in the other direction, in eleven days.

Hold the duty cycle constant, not the interval

adaptive_records_ttl recomputes after 60x the query's own measured duration,
clamped to a 5s floor and the existing per-window ceiling. The measured cost is
stored in the memo alongside the answer.

  • month, today: ~36s instead of 300s
  • month, after the archive ages out: ~6s
  • a node whose query regresses: backs off by itself up to the 300s ceiling —
    the protection the fixed ceiling was really providing, now automatic
  • duty cycle: capped at 1/60 (under 2%) whatever the query costs

The cost is measured after the error arm returns, so a fast failure cannot be
recorded as a cheap success and drive the TTL down.

What this deliberately does not do

  • It does not make the query cheaper. Ranking by rarity means
    reverse_hex(share_hash) over every row — a function of the column, so no
    index can serve the ORDER BY. The structural fix is an indexed display-order
    column plus a backfill of ~2M archive rows; that is a migration, not this PR.
  • It does not rank by difficulty instead. Tested and refuted: difficulty
    is the assigned vardiff target, not the achieved one. Four distinct hashes
    in the current month window carry 620606.037971378 to fifteen significant
    figures, so ordering by it returns an arbitrary one of the four rather than
    the rarest. The handler's own winning_assigned_diff naming agrees.

Tests

Four new tests, each naming the behaviour rather than the constant. They were
mutation-checked: reverting adaptive_records_ttl to the old fixed-TTL
behaviour fails a_cheap_query_is_not_cached_for_the_window_ceiling and nothing
else, so the suite encodes the fix rather than restating it.

https://claude.ai/code/session_01Td1vvfowptTTnu88qG2iym

…sts (#687)

`pool/records?window=month` could serve an answer up to five minutes stale.
The 300s TTL was not arbitrary — it was sized for a query that measured 7-20s,
where recomputing often would hang the pool page.

That cost was a property of the DATA, not the query. The month window's cost is
dominated by how many rows of `shares_archive` still fall inside it, and the
archive stopped being written at the v56 cutover. Measured on ghost-vm6 today:
the archive's newest row is 18.7 days old, 550,321 archive rows remain inside
the 30-day window against 55,760 live ones, and the query answers in ~0.6s, not
~20s. As the frozen archive ages past the 30-day boundary that term goes to
zero and the window converges on the live-shares-only cost — 95ms, measured.
A hard-coded 300s would then be five minutes of staleness bought for nothing.

So hold the DUTY CYCLE constant instead of the interval: recompute after 60x
the query's own measured duration, clamped to a 5s floor and the existing
per-window ceiling. Today that serves the month window at ~36s instead of 300s;
once the archive ages out it becomes ~6s; and a node whose query regresses
backs off by itself rather than melting — which is the protection the fixed
ceiling was really providing.

Two things this deliberately does NOT do:

- It does not make the query cheaper. Ranking by rarity means
  `reverse_hex(share_hash)` over every row, a function of the column, so no
  index can serve the `ORDER BY`. The structural fix is an indexed
  display-order column and a backfill of ~2M archive rows; it is not attempted
  here.
- It does not rank by `difficulty` instead. That was tested and refuted:
  `difficulty` is the ASSIGNED vardiff target, not the achieved one — four
  distinct hashes in the current month window share `620606.037971378` to
  fifteen significant figures, so ordering by it picks an arbitrary one of the
  four rather than the rarest.

Claude-Session: https://claude.ai/code/session_01Td1vvfowptTTnu88qG2iym
@defenwycke
defenwycke merged commit 3deddef into main Sep 6, 2026
12 checks passed
defenwycke added a commit that referenced this pull request Sep 6, 2026
Supersedes v1.11.36, which reached the canaries but not production.

v1.11.36 was cut, gated, built and rolled to all four canaries, where it soaked
cleanly. Production then refused it, correctly:

    REFUSED: 4d71781 no longer matches origin/main for: bins/pool-sv2 crates
             main has moved (a revert, or newer commits)

#839 and #840 were merged AFTER the release commit, and #839 touches `crates/`,
which builds `pool_sv2`. The guard is doing exactly its job — it is the check
that would have caught the #447 revert (#459) — so the fix is to cut a release
that matches main, not to bypass it.

The lesson is about ordering, not the guard: do not merge other PRs between
cutting a release and finishing its roll. A release SHA has to stay reachable as
main's tip for the paths it builds, for as long as the roll takes.

This release therefore carries everything from v1.11.35 and v1.11.36 plus #687
and #759.

Claude-Session: https://claude.ai/code/session_01Td1vvfowptTTnu88qG2iym
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.

pool/records: the month record can be up to 5 minutes stale on the public page

1 participant