Repository navigation
fix: Stay ready while a withdrawal installs - #517
Merged
Merged
Conversation
A member that installs an update withdrawing a context, zone or record cleared its approved configuration before installing. Readiness then failed, and every member closed its health port at the same moment until the withdrawal was verified, so the router had no healthy member. A slow resolver probe stretched that to several seconds and lost a lookup. The daemons keep answering from the old files until the new ones are live, so closing the health port bought no safety. Keep the replaced approval beside the checkpoint, and let readiness use it only while a live agent holds the activation lock. Take that lock before the first save, so there is no window without it. Restart, retention and older agents never read it, and failing closed or abandoning the lock ends it. Part of datum-cloud/enhancements#921
Take the activation lock before saving a new withdrawal, and check once more when readiness sees a withdrawal without a live installer, so no moment exists where the member reads as unapproved. Mark failing closed with its own activation phase that readiness rejects, so a member is never ready on a replaced approval while its gate runs. Test from the validate step, before the transaction takes its own lock, and cover context withdrawals and gating. Part of datum-cloud/enhancements#921
Leave the approval in place when a serving snapshot arrives and let reconciliation withdraw it right before it installs, so the activation lock is never held for an update that installs nothing; a held lock also stops crashed daemons from restarting. When a withdrawal changes no files, drop the approval without the lock as before. Write the gating phase in one step. Part of datum-cloud/enhancements#921
ecv
approved these changes
Nov 6, 2026
ecv
left a comment
Contributor
There was a problem hiding this comment.
Approve.
Readiness honours withdrawing only while an flock on activation.pending.lock is held, so a crash or hang cannot keep a member ready beyond the one-minute install grace. Restart and retention never read the new field, and every failing path that I traced either writes gating or leaves a marker that readiness rejects.
- Lock liveness is kernel flock, not a PID or lease. There is no PID reuse, a crashed agent drops it on exit, and a hung agent is bounded by
activationInstallGrace(60s from the first marker write, preserved across phase writes inwriteActivation).withdrawingis therefore honoured for at most about 60s. - TOCTOU in
servedApproval(readiness.go):installationReadinessis read before the checkpoint, and the one re-check whenActive==nil && Withdrawing!=nil && !installingcloses the save-then-lock gap. The ordering inreconcileDesired(lock, then save) is what makes that sound. restoreActiveandcanRetainActiveonly readActive, so a crash mid-withdrawal restarts throughfailClosedand writesgating.failClosedclearsWithdrawingin memory only. The disk copy survives until the nextSave, andreconcileDesiredpersists the cleared state before anyApplywhenActive==nil, so no install runs with a stalewithdrawingon disk. Every otherApplysite I found sits after that save or requiresActive != nil.- Rollback:
Loaddoes not useDisallowUnknownFields, so an older agent ignores the field and its next save drops it, and an older readiness fails onActive==nilas before. - Mutation spot-check: removing the
gatingcheck failsTestFailedWithdrawalIsNotReadyWhileGating, as claimed. Removing the!installingre-check inservedApprovalleaves the whole package green, so the claimed lock-ordering mutation guard does not cover that re-check. - The gating tests use the
validateandgatehooks, so they test the install window rather than a true concurrent save-versus-probe interleave.
Follow-ups:
- A withdrawal for a tenant or security reason now keeps every member in rotation, serving the withdrawn data, until its install finishes (seconds, at most about 60s with a stuck installer). Before, members left rotation first. This is the intended trade, but say so in the docs, and consider a withdrawal flag that opts out of it for tenant removal.
- The probe-failed gate path (
verifyResolverBindingsthentx.Apply(gated)) does not callfailClosed, so it writes nogatingphase. A member removing a failing binding now stays ready on the replaced approval for the grace window, whereas before it was unready. The body says a member running its agent's gate is never ready, so either gate that path too or narrow the claim. - Add a test that interleaves a checkpoint save with a readiness read, to cover the re-check in
servedApproval. - If
beginGatefails (disk error) while the install marker still saysinstalling, the lock is held through the up-to-5sFailClosedand readiness stays ready for that time. This needs a disk fault, so it is low priority.
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.
Requested by Scot Schuchert-Wells
Part of datum-cloud/enhancements#921
Problem
When an update withdraws a context, zone or record, every member clears its approved configuration before installing the update. Readiness then fails with
no approved configuration is active, and every member closes its health port at the same moment, because they all receive the same update. The router has no healthy member until the members verify the withdrawal, usually about a second later. If a resolver probe in the same pass is slow, that stretches to several seconds and lookups time out. Follow-up to #516, which fixed the false withdrawals but left real ones behaving this way.Fix
The daemons keep answering from the old files until the new ones are live, so closing the health port during the install gave no safety.
withdrawing, andactiveis still cleared as before.withdrawingonly while a live agent holds the activation lock. A stopped agent, an abandoned activation or a fail-closed gate ends that.gating), which readiness rejects. A member running its agent's gate is never ready, even while the agent holds the lock. The restart inside a structural transaction still runs under the installing phase; there the live canary decides, as before.withdrawing.Unchanged: restart (
restoreActive), retention (canRetainActive) and older agents never read the new field, so a crash or rollback mid-withdrawal still fails closed rather than restoring withdrawn data.Testing
TestWithdrawalKeepsReadinessWhileInstalling(record and zone-detach cases): at the validate step, before the transaction takes its own lock, readiness serves the replaced approval. Afterwards the withdrawal is approved and nothing is left pending.TestFailedWithdrawalIsNotReadyWhileGating: a failed withdrawal is not ready while its gate runs or after.TestReadinessUsesReplacedApprovalOnlyDuringLiveWithdrawal: not ready without an installer, ready with one, not ready once it is abandoned.go test ./internal/internaldns/...passes (two macOS-onlyTestReloadDNSDist*helper tests skipped, as on main). Lint is clean.