Repository navigation
fix(worker): add an alarm backstop to the registry sweeper - #30
Merged
Merged
Conversation
The sweeper alarm is the one cost that needs no visitor: a bug that left something due "now" would re-run it back to back, each run billed. Following the studio playbook's alarm guardrails: - MIN_ALARM_GAP_MS (60 s): the sweeper never runs within a minute of its last run, so a loop costs at most 1,440 runs a day. Nothing needs it sooner: expiry is enforced on read, so the sweeper only reclaims storage. - ensureAlarm and the sweeper's own reschedule are merged into scheduleAlarm, the only place that sets the alarm; test/tooling/alarmCalls.test.ts (node, vitest.tooling.config.ts + tsconfig.node.json) fails CI otherwise. - No kill switch, by decision: the ceiling is low enough. Tests in test/alarms.test.ts; docs in ARCHITECTURE.md "Alarm backstops". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Christian Catalan <crcatala@gmail.com>
From review of #30: - verify-deployment.mjs waits up to 150 s (was 90 s) for the sweeper, since a purge can now come up to a minute after it's due; VERIFY_DEPLOYMENT.md and the historical measurement in ARCHITECTURE.md say so. - ARCHITECTURE.md: the 1,440/day ceiling is for the sweeper rescheduling itself; after an eviction each API call can bring a run forward once. - Tests: an API call right after a sweep still schedules a minute after it; grace period read from the config instead of hardcoded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Christian Catalan <crcatala@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
flashlink's registry runs a "sweeper" on a Durable Object alarm: a timer that wakes it to delete expired files. Each run is a billed request plus a few storage writes. If a bug ever left something due "now", the sweeper would re-run back to back with nobody using flashlink. That's the kind of loop behind the well-known $10k–$34k Durable Object bills.
What changes:
Nothing changes for users. Expired links are already refused on every download, so the sweeper only frees storage, days after expiry by default. A minute's delay is invisible.
As decided, there's no kill switch: with a ceiling this low, deploying a fix is enough.
Technical details
packages/worker/src/registry.ts:MIN_ALARM_GAP_MS = 60_000andlastWakeAt(in memory, set at the start ofalarm()).ensureAlarm()and the sweeper's own reschedule are merged intoscheduleAlarm(fired = false), which usesmax(nextDue, lastWakeAt + gap). It keeps an earlier pending alarm unless called from the sweeper, whose alarm was just used.PURGE_GRACE_SECONDS) and reaping abandoned uploads (15 min). None is user-facing:resolveenforces expiry and caps on every read;test/tooling/alarmCalls.test.ts:src/; onlyregistry.ts#scheduleAlarmmay callsetAlarm;vitest.tooling.config.ts, typechecked by the newtsconfig.node.json(Node types), and is excluded fromtsconfig.jsonand the Workers vitest config;testandtypecheckscripts run both;@types/nodeandtypescriptwere added as worker devDependencies, at the versions already in the workspace.test/helpers.tsresetStatealso clearslastWakeAt, since one registry instance serves every test.docs/ARCHITECTURE.mdgets a new "Alarm backstops" subsection under "Cost model", with the decision and the reason for no kill switch.Test plan
pnpm verify(format, typecheck, tests: worker 101 + 2 tooling, was 98; core 26; cli 364; build)test/alarms.test.ts, with Date faked inside the Durable Object and set a year ahead so real alarms never fire:nextDuestubbed to "now") runs exactly 11 times in 10 minutes, each ≥60 s apart;Each guardrail reverted in turn, and a test fails each time:
setAlarminalarm()On the live deploy: upload a file with
--ttl 1m, and withPURGE_GRACE_SECONDSsmall on a test deploy, check thatwrangler tailshows one sweeper run per due cleanup and none while idle.Review follow-up
Independent review (Grok, DeepSeek), fixed in 6c44128:
verify-deployment.mjsnow waits up to 150 s (was 90 s), and VERIFY_DEPLOYMENT.md and the old measurement in ARCHITECTURE.md say a purge can come up to a minute late.Worker tests: 102 + 2.
🤖 Generated with Claude Code