Repository navigation
fix(daemon): recover provider leases a killed daemon left rented - #4108
Conversation
The daemon kept live provider leases only in memory and persisted a lease only once it had expired and its release failed. A daemon killed with `kill -9` while it held a lease forgot the lease, so a rented cloud device stayed billed until the provider's own idle timeout. The lease registry now reports its provider leases whenever one is issued or leaves, and the daemon journals those whose provider can recover an expired lease in `live-provider-leases.json`. A daemon that starts and finds the journal moves its leases into the expired-lease recovery, which runs at startup and ends each rented device. The journal is dropped only after the leases are in the expired record, so a crash between the two keeps them in one file or the other. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Size Report
Startup median (7 runs, lower is better):
|
There was a problem hiding this comment.
All reported issues were addressed across 6 files
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
…able The journal now also holds every lease that left the registry for recovery but whose expired record has not been written, retries a failed write with the latest leases, and survives a failed removal at startup. ADR 0007 names the trigger as a daemon that dies and records the canceled-allocation gap. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Reviewed 9850f4c. The recovery design is sound, but a refused daemon start can release devices that a live daemon still uses, so this needs one change before merge. daemon-runtime.ts:368: Not blocking, and you can take or leave these: (1) The open inline threads still apply: the persist-failure drop from the live journal (#4108 (comment), P1), the swallowed journal write failure (#4108 (comment), P1, which I see as a policy call and non-blocking), the ADR gap list (#4108 (comment), P2), and the CI is green: 18 checks, none failing. There are no conflicts. I did not run the code or the new tests. I could not verify the Revyl Before merge, please move orphan adoption and the startup retry after |
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Apex SentinelVerdict: Since the last review: 8 new · 0 still open · 6 no longer raised 8 finding(s): 1 minor, 7 nit. Verdict: approve. reached max rounds (1). 🟡 Minor (1)
🔵 Nit (7)
Reply under an inline comment with |
There was a problem hiding this comment.
Apex Sentinel: 8 new findings on 8fd6e2d. The verdict and the full list are in the summary comment.
…red record Shutdown makes the journal write a pending retry was still owed, the journal keeps only recoverable leases, and both lease records share one record type and version name. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Thanks for the update. At 290a0be the journal handoff gaps from the earlier review are fixed, but the startup ordering problem is still there. A daemon that does not hold the state dir lock can still act on that dir's lease records. Not blocking, and you can take or leave it: the journal is rewritten only by A question, optional: this delta guards a handoff between two files (the unpersisted merge, On the open inline threads: no P1 or P2 thread still applies, and six lower-priority threads still apply (the startup-order one is covered by the finding above). These do not apply and can be resolved: r4428007864 because a provider lease cannot become a non-provider lease in place ( Coverage and Repo Guards & Compatibility pass at 290a0be, and two Smoke Tests jobs pass. One Smoke Tests job is still pending; nothing has failed. The daemon startup route that smoke tests boot through is in the diff, so a failure there would need attribution. This change touches daemon startup for provider leases, so it needs one Revyl run at the fixed head. The run should start a daemon, allocate a revyl lease, |
There was a problem hiding this comment.
Apex Sentinel: 6 new findings on bc75a53. The verdict and the full list are in the summary comment.
…filter A test pins that adopted leases whose expired record could not be written stay in the journal through later writes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
There was a problem hiding this comment.
Apex Sentinel: 6 new findings on e23e0b5. The verdict and the full list are in the summary comment.
…on is held The releaser is built, and used to adopt the live journal and start recovery, before the daemon acquired its registration. A second daemon started on the same state directory therefore recovered the running daemon's live leases, ending devices still in use, and only then failed its registration. Adoption and the first recovery retry now run after the registration is acquired. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… is held The releaser now touches no file in the state directory until the daemon calls recoverInherited, after its registration is acquired: that step loads the expired record, adopts the live journal and starts recovery. A daemon refused the registration leaves the live journal byte-identical. A durable expired record rewrites the journal, and ADR 0007 names both routes where the registry forgets a provider lease without a crash. Co-Authored-By: Claude Opus 5.5 <[email protected]>
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
…ock-held recovery emitDiagnostic needs a request scope, which daemon startup does not have, so the adoption report was dropped. It now goes through the startup diagnostics the daemon flushes to its log, and recovery starts next to the other recovery that runs once the registration is held. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ds untouched A host started on a held state directory is refused, and the running host's live journal stays byte-identical with no expired record created; the test fails when recovery runs before the registration. A source-order check pins recoverInherited after the refusal branch. Co-Authored-By: Claude Opus 5.5 <[email protected]>
There was a problem hiding this comment.
Apex Sentinel: 5 new findings on 9c93b9e. The verdict and the full list are in the summary comment.
…g it unhandled Co-Authored-By: Claude Opus 5.5 <[email protected]>
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
|
Thanks. The ordering you described was the real defect, and the latest commits fix it:
Also taken from your notes:
On the one-record design: I kept two files. One record would mean a migration that reads an existing Live run against the real Revyl backend. Packed from #4092 at 86c4c7f with this PR's commits on top (through c8a7626), installed through a local registry. Android device, one live
The startup-phase marker file is deleted once a startup succeeds, so the lock's own |
Startup has no diagnostics scope, so emitDiagnostic dropped the report; it now goes through emitDaemonDiagnostic like the daemon's other unscoped failures. Co-Authored-By: Claude Opus 5.5 <[email protected]>
There was a problem hiding this comment.
Apex Sentinel: 7 new findings on ecd2caf. The verdict and the full list are in the summary comment.
…aemon log The read now happens at startup, where emitDiagnostic has no scope, so the warning goes through the startup diagnostics and names the record's path; an unreadable record is left on disk for an operator. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Apex Sentinel could not review commit Apex Sentinel could not read or check out this pull request, which is usually temporary (GitHub was slow, or the branch moved while the review waited). If it keeps failing, tell whoever runs Sentinel for this repository, or check that the App still has access to it. Push a new commit or comment |
|
The code looks good at 95e6b33. The earlier ordering problem is fixed: adoption now runs only after the daemon holds its registration, and src/host.test.ts:369 proves the refused-start route through the real startup. The earlier journal and shutdown findings are fixed too. Not blocking, and fine to take or leave: an unreadable live-provider-leases.json is left in place at startup, but the next lease change or expiry rewrites it with only this daemon's leases, so the dead daemon's records are lost (provider-lease-expiry.ts:154 and :422; renaming the file aside before any write would keep the evidence, or the "left for an operator" comment can go); the test at provider-lease-expiry.test.ts:382 compares text positions in daemon-runtime.ts, which src/host.test.ts:369 already covers by behavior, so it can go; and retryPending in provider-lease-expiry.ts:51 has no production caller now, so it can leave the type and the tests can call recoverInherited. The only failing check is the review-bot job, whose own comment says it could not read the PR, so it did not run on this code. The other 18 checks pass, including the jobs that start the daemon through the changed path. There are no conflicts. I read the code and did not run it. The live Revyl kill -9 run is author-reported and covers commits through c8a7626. The two later commits change only failure-path diagnostics. I did not check that Limrun recovery is safe to repeat for a device that is already gone, or trace ADR 0007's second gap (late allocation whose provider release throws). No open P1 or P2 thread still applies. Two lower-priority resolved threads still apply and match the notes above (#4108 (comment), #4108 (comment)). These threads are fixed at this commit and already resolved: journal now kept until the expired record lands (#4108 (comment)); adoption now runs after registration (#4108 (comment)); shutdown now flushes an owed journal write (#4108 (comment)); only recoverable providers are recorded (#4108 (comment)); record type and constant renamed (#4108 (comment)); failed persist now returns before the journal is removed (#4108 (comment)); retry timer re-runs the journal write (#4108 (comment)); merge order is now commented (#4108 (comment)); adoption sits behind the registration, and src/host.test.ts:369 covers refusal (#4108 (comment)); pending set is set before the first await, with a comment (#4108 (comment)); src/host.test.ts:369 now drives the real registration refusal (#4108 (comment)); recoverInherited now has a catch (#4108 (comment)); startup failure now goes through the daemon diagnostic (#4108 (comment)). These threads do not apply, for the reason given, and are already resolved: a provider lease cannot become a non-provider lease in place (#4108 (comment)); the stateDir guard narrows the type (#4108 (comment)); the sync write runs only on rare provider lease changes (#4108 (comment)); the remaining gap is stated in ADR 0007 (#4108 (comment)); same stateDir guard (#4108 (comment)); the second timer clear is deliberate (#4108 (comment)); adoption never reads expiresAt (#4108 (comment)); same sync-write point (#4108 (comment)); a lease in both files is deduped by leaseId (#4108 (comment)); record names are unified (#4108 (comment)); same stateDir guard (#4108 (comment)); same sync-write point (#4108 (comment)); recovery uses leaseId and provider, not expiresAt (#4108 (comment)); that loop marks leases durable without writing, so it is not a copy (#4108 (comment)); production calls only recoverInherited (#4108 (comment)); a provider lease cannot lose its provider in place (#4108 (comment)); one rewrite per departing lease is a small cost on a rare path (#4108 (comment)); no provider lease can change before the servers open (#4108 (comment)); a failed adoption persist is retried from the journal (#4108 (comment)); adoption ignores expiresAt (#4108 (comment)). |
Summary
A daemon killed with
kill -9while it held a cloud provider lease forgot that lease. Live provider leases were kept only in memory, so the rented device stayed billed until the provider's idle timeout.The lease registry now reports its provider leases whenever one is issued or leaves. The daemon journals the ones that can be recovered in
live-provider-leases.json. A lease that leaves for expiry stays in the journal until its expired-lease record is written, and a failed journal write is retried. At startup, any lease in the journal moves into the existing expired-lease recovery, which ends the device. ADR 0007 records the decision and its one gap. This touches 6 files and unblocks thekill -9evidence on #4092.Validation
pnpm check:affected --runpassed all runnable checks.provider-lease-expiry.test.ts:completedat 18:25:51.🤖 Generated with Claude Code