fix(seeders): self-healing TTLs for military-bases version keys (#6845 item 1) - #6871
Conversation
…tion (koala73#6764) The branch-contamination guard fetched origin on every push solely so the >20-commits check would not false-positive against a stale tracking ref. A fetch can only move origin/$BASE_REF forward and the count can only shrink, so a cached count within budget is already a guaranteed pass — the common path paid warm 2s / cold 72s of network for nothing (median push in the Canada merge campaign: 45.6s). Invert it: compute the count against the cached ref first; fetch only when the cached count exceeds the budget (to disprove a stale-ref false positive) or when the ref is not cached at all. The verdict is provably identical. Regression tests cover all three shapes: within-budget skips the fetch entirely, a stale-ref false positive fetches exactly once and then passes, and a genuinely contaminated branch still errors after trying to disprove.
…a73#6845 item 1) A run killed before atomicSwitch left military:bases:{geo,meta}:<version> written with no TTL, and nothing ever swept it — cleanupOldVersion only names the keys of the version that is currently active, so a version that never published leaked up to a 125,380-member zset plus a 125,380-field hash. The same mechanism leaked the superseded version when a run was killed inside the 30s post-publish grace: the DELs never ran and the next reseed deletes only the then-active version. Three cooperating halves: - seedGeo/seedMeta piggyback an EXPIRE on every batch: the TTL arms from the first write (EXPIRE on a missing key is a no-op) and keeps refreshing while the run is alive. 30 minutes, ~3x the 540s slot including the R2-cold worst case. - atomicSwitch PERSISTs both version keys inside the same EVAL that publishes, so a live version can never expire out from under readers and an unpublished one always can. - armSupersededCleanup EXPIREs the old pair right after the switch (never before — until the switch lands they ARE the live data), so a kill inside the grace window still reaps them after a window 5x the reader grace. A best-effort start-of-run sweep also re-arms TTLs on keys leaked by pre-TTL runs: a key counts as leaked only when it has no TTL and is not the active version's. The sweep never DELs directly — it lets Redis reap, preserving a grace-sized window even against a concurrent publish.
|
@yzxcj797 is attempting to deploy a commit to the World Monitor Team on Vercel. A member of the Team first needs to authorize it. |
Review — head
|
|
@yzxcj797 pls don't forget this one - you are churning a lot of PRs, but I want to close the older ones first |
…on violation (koala73#6764)" This reverts commit 698e727.
# Conflicts: # scripts/seed-military-bases.mjs
koala73#6871 landed the version-key TTL work on scripts/seed-military-bases.mjs and changed atomicSwitch's shape: it now takes the superseded version and returns the cleanup info the caller schedules the grace period from. This branch had added a writeActivationMarker call immediately after that same call, which is the one conflict. Resolved to main's signature, keeping the marker write after it. Everything else auto-merged. Claude-Session: https://claude.ai/code/session_01QBV6rYWJd62owNKBrkgZxa
First follow-up from #6845: a run killed before
atomicSwitchleaks its version keys permanently.Mechanism
The seeder writes
military:bases:geo:<version>/meta:<version>with no TTL, andcleanupOldVersiononly ever computes keys from the version that is currently active — a version that never published is swept by nothing, ever. The runner SIGTERMs attimeoutMsand SIGKILLs 10s later, so each such death leaks up to a 125,380-member zset plus a 125,380-field hash. The same hole leaks the superseded version when a run dies inside the 30s grace: the DELs never fire, and the next reseed deletes only the then-active version.Fix — three cooperating halves
seedGeo/seedMetapiggybackEXPIRE key 1800onto each pipeline: it arms from the first write (EXPIRE on a missing key is a no-op) and keeps refreshing while the run is alive — only a dead run lets it count down. 30 min is ~3× the 540s slot incl. the R2-cold worst case, so a slow-but-alive run can never expire its own data.atomicSwitchPERSISTs inside the publish EVAL. The version's own two keys are passed as KEYS[3]/KEYS[4] and PERSISTed atomically with the SETs — a published version can never expire, an unpublished one always can.armSupersededCleanupright after the switch. The old pair getsEXPIRE 150s(5× the reader grace) afteratomicSwitchlands — deliberately not before, because until the switch they ARE the live data and seeding can outlast any superseded TTL. A kill inside the grace window now self-heals: Redis reaps them shortly after the grace would have ended.Plus a best-effort start-of-run sweep (
sweepLeakedVersionKeys) that heals historical leaks from pre-TTL runs: SCAN both key families; a key counts as leaked only whenTTL == -1and it is not the active version's. Live seeding runs and the active version can never match. The sweep never DELs — it only re-arms the superseded TTL and lets Redis reap, preserving a grace-sized window even if a concurrent publish flipsactivemid-scan. Transport failure logs a warning and moves on; the next run sweeps again.Tests
Six new tests in
tests/seed-military-bases-version-key-ttl.test.mjs:seedGeo/seedMetaarm + refresh the EXPIRE on every batchatomicSwitchEVAL: numkeys 4, both version keys, PERSIST for both, payload lastarmSupersededCleanupEXPIREs exactly the two old keystests/bundle-runner.test.mjs's publish-contract test is updated for the 4-key EVAL shape and now also asserts the two PERSIST calls and the geo/meta key names. All green locally (6/6 new, 38/38 bundle-runner, 9/9 existing seeder tests).