Repository navigation
fix: close the downtime Onebox causes on change and deploy - #222
Merged
Merged
Conversation
Spec and implementation plan for removing downtime Onebox causes without need: Traefik restarts on every ob upgrade and on discovery repair, killed database shutdowns, rolls that retire the last routed replica, and recovery that removes serving containers before restoring the predecessor.
Separate staged configuration bytes from the Compose payload so later proxy convergence can distinguish configuration reloads from controller upgrades. Validate and serialize both SHA-256 digests as one applied identity while existing engine paths consume the serialized marker. On rollout, an existing single-digest marker differs once and follows the configuration path; subsequent applies compare the two-digest marker. This slice retains current restart and convergence behavior until later tasks.
Separate host observation from the decision to repair containers or apply configuration so compose drift can converge without restarting Traefik. Treat missing, malformed, and legacy identity markers as requiring configuration apply. Operators see no rollout behavior change in this preparatory commit: EnsureProxy and status remain unchanged. The new decision reports unchanged, container-only updates, or a Traefik restart for later integration.
Keep Traefik and its configuration mounted while converging compose-only changes or repairing discovery, so ob upgrades and discovery repair no longer drop routes. Require fresh discovery publication before advancing and invalidate the applied marker before uploads; commit it atomically only after the required publication and health gates succeed. Operators see no Traefik restart for discovery repairs or ob upgrades that only change discovery. The first apply on each host after upgrading still restarts once because the legacy one-digest marker takes the configuration path. Configuration changes continue to reload Traefik by recreation or restart. Compose assumption verified on Docker Compose 5.6.0: dependent container kept when its dependency was recreated The user verified both separate service up commands and full up behaved the same; the experiment was not rerun for this task.
Compare parsed config and Compose identities so Compose-only changes and legacy markers cannot appear synchronized. Share staging with proxy apply to keep local status identity aligned with the payload. Operators see config and Compose digest prefixes when synchronized, and the drifted halves plus ob proxy apply when divergent. JSON retains its identity fields and unreadable reads remain incomplete rather than asserting drift.
The healthy log truncated the identity at its sha256 prefix, hiding useful digest characters and the Compose identity. Operators now see the first eight hex characters of both config and Compose digests after a successful proxy rollout.
Make the managed proxy action visible before operators approve a deployment. Stage and observe the proxy without mutating the host, bind the action into the plan digest, and print it immediately after the compose diff or no-op explanation.
The 10s kill window could force PostgreSQL crash recovery or Redis AOF replay after a restart. Share a two-minute grace period across rendered runtimes, service apply, and best-effort stop before removal. Operators see every service recreated once on its first service apply after upgrading; deploy also recreates PostgreSQL services with declared extensions. As with #220, this one-time recreation installs the new container setting.
Read the published discovery document so rollout gates can confirm every expected HTTP and TCP backend before shifting traffic. Share endpoint selection with discovery and compare parsed IPs so IPv6 and multiple routes follow the published addresses. Operators receive bounded publication and withdrawal errors with discovery log or ob resume guidance. Failed, empty, malformed, and cancelled reads cannot establish routing presence or absence. This adds the routing readers; rollout integration follows separately.
Health alone does not confirm that discovery has published a replacement. Admit fresh and adopted newcomers before retiring replicas or declaring convergence, and remove a fresh newcomer if publication fails. Operators keep existing replicas serving when admission fails and can resume without losing adopted containers. Managed routed workloads wait for the published backend before the existing converge buffer and drain.
Wait for published routes to withdraw a draining replica before sending any signal or stopping it. Health alone does not prove discovery has removed the endpoint. Operators see rollout stop with a discovery error and a still-running draining replica when withdrawal cannot be confirmed, so they can fix discovery and resume safely.
…ners Keep interrupted running rolling replicas available while restoring the predecessor, then sweep the interrupted release before verification. Remove non-serving containers and transient name collisions first. Operators aborting or automatically rolling back a partial rollout retain serving capacity until predecessor restoration; failures remain retryable with the activation checkpoint intact.
A workload whose containers match the predecessor's revision but belong to the interrupted release is not restored: the sweep would remove its only containers. Recovery rolls it instead, which drains and retires them.
A matching workload revision can still belong to the interrupted release. Roll those containers into the predecessor before the exact release sweep so recovery cannot retain serving replicas and then remove them. Operators aborting or automatically rolling back keep serving capacity through restoration even when the two releases share a workload revision. Track interrupted ownership separately in the recovery fixture so a surviving container remains visible to the sweep and postcondition.
Document publication and withdrawal gates, recovery ordering, and the proxy action shown in deploy plans so operators can see when serving containers remain available and when Traefik restarts. Mark the accepted implementation complete while preserving its plan reference.
Compare the second YAML decode against io.EOF with errors.Is, and use strings.Contains in the marker-invalidation test, so just lint passes.
Withdrawal survives route changes, a failed converge retries as a converge, converge proves a fresh controller, the plan names a Traefik replacement, the config path keeps routes until the restart, and single-replica recovery keeps serving.
Check old endpoints against every published HTTP and TCP service so route removal or protocol changes cannot bypass retirement safety. A container outside the proxy network is already withdrawn, while newcomer admission still rejects its missing address. Operators can roll route changes safely: old replicas stop only after discovery removes their endpoint, and stalled discovery leaves them draining with the existing resume guidance.
The predecessor's running replica is undrained and adopted, so the roll retires the interrupted newcomer without a surge.
Preserve an interrupted rolling replica while freeing its transient name, then undrain the predecessor's running replicas before restoration admits them. Choose a slot unoccupied by any container, including stopped ones. Operators recovering a one-replica workload keep its healthy endpoint until the predecessor passes health and managed publication admission, after which the interrupted replica drains and retires through the normal roll.
Undrain recovery previously swallowed transport and fencing errors, allowing a stale runner to continue restoring predecessor replicas after losing authority. Return mutation errors immediately while retaining best-effort handling for remote command failures. Operators now receive the recovery error before further replicas or restore actions run. Cover transport failures, host-side fence rejection, and continued undrain after an ordinary remote exit.
Keep the applied config digest in an atomic pending marker so an interrupted discovery converge retries without restarting Traefik. Stop discovery before sampling its output inode, require fresh publication, and upload only Compose to avoid sending config credentials on that path. Retire live config and dynamic directories into staging so existing bind mounts keep their contents until the restarted proxy is healthy. Defer staging cleanup until a successful apply, including on retry, so a failed apply preserves the routes still mounted by Traefik. Cover the failure paths and tighten the publication and discovery fakes.
Compare the parsed live and rendered Traefik service and network definitions so a deploy plan names a container recreation instead of promising an update without a restart. Report a start when Traefik is absent, and treat an unreadable live definition conservatively. Run replacements through the existing converge path, preserving the pending config identity, Compose-only upload and fresh discovery publication. Let Compose recreate the proxy and wait for its health before committing the final identity.
State when Traefik restarts or is recreated so operators can use the proxy plan line to assess interruption. Make the routing guarantee explicit about its 3s loading assumption and call out the one-time proxy and managed-service applies required after this upgrade.
A deploy converges the proxy too, so the first deploy after upgrading restarts Traefik, and deploy recreates PostgreSQL services with declared extensions.
A failed replace stays labelled a replace, discovery is pulled before it is stopped and restarted if a later step fails, the unchanged path cleans stale staging, and rolls after a failed proxy apply are an accepted, documented limitation.
…abelled Keep a distinct pending replacement identity so moving Compose before a failed up cannot make a retry promise that Traefik will stay in place. Make the discovery image available before stopping its container, and attempt a guarded start on any later error without replacing the original failure. Clean stale staging when the final identity is already applied as well as after a successful apply. Cover failed replacement planning, pull ordering, discovery fallback and unchanged cleanup. Add the recovery surge regression for a sole interrupted newcomer and remove the unused route-specific withdrawal helper and its test.
The binary now gates rolling retirement on discovery's published routes, so the capabilities page says so.
Discovery is restored on every proxy path, the planned proxy action binds execution, a re-pulled Traefik image counts as a definition change, failed uploads are cleaned at the start of a run, and a roll admits each newcomer once. Claude-Session: https://claude.ai/code/session_01Dm55dV6FgUFE7vnWQEHCB9
…y action Bind the sealed proxy action for deploy and no-op execution, and refuse drift after read-only observation before cleanup or directory preparation. Compare running Traefik image IDs with the configured local image so a re-pulled tag is planned as a replacement. Prepare the discovery image on apply as well as converge, and recover a failed recreation by trying start and then Compose up when the container is gone. Clean abandoned uploads before preparation while retaining staged directories still mounted by Traefik until a successful apply. Claude-Session: https://claude.ai/code/session_01Dm55dV6FgUFE7vnWQEHCB9
Remember successful health and publication admission within each roll invocation instead of repeating it for every remaining replica. Record newly created replicas only after both gates succeed, and start a fresh admission set when a roll is retried so adopted replicas are still checked. Keep retirement and failure cleanup unchanged, and use the shared discovery container name in routing error guidance. Claude-Session: https://claude.ai/code/session_01Dm55dV6FgUFE7vnWQEHCB9
Withdraw single admission, which let a roll retire every old replica after an admitted newcomer failed; always check Traefik's health before the marker; pull discovery before the apply path invalidates anything. Claude-Session: https://claude.ai/code/session_01Dm55dV6FgUFE7vnWQEHCB9
A newcomer can fail after its first admission when an old replica leaves and traffic shifts. Remove the admission cache so each retirement and successful completion requires current health and publication evidence for every newcomer. Cover both a steady three-replica roll that loses NEW1 after its first retirement and a three-to-two roll whose newcomers fail before the final old replica retires. Remove the withdrawn once-only admission test. Claude-Session: https://claude.ai/code/session_01Dm55dV6FgUFE7vnWQEHCB9
A retry can keep the same Traefik container after a failed replacement. Require current health before committing the final identity on apply, converge and replace, so an unhealthy retry retains its pending marker instead of reporting success. Pull discovery for configuration applies immediately after drift validation, before changing the marker or directories. Give fallback recovery its own 30s context and neutral logs, and compare the image named by rendered Compose. Add matching-plan success controls while retaining drift refusal assertions. Claude-Session: https://claude.ai/code/session_01Dm55dV6FgUFE7vnWQEHCB9
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.
What this changes
Onebox caused downtime it did not need to when a project changed and deployed. This closes those gaps. The design is in
docs/plans/2026-10-17-zero-downtime-gaps.mdand the implementation plan indocs/superpowers/plans/2026-10-17-zero-downtime-gaps.md.Managed proxy
<config digest> <compose digest>. A pure decision picks one action:unchanged;update changed containers only, no restart;recreate Traefik (its container definition changed);restart Traefik (configuration changed).obupgrade changes only the discovery image, so it no longer restarts Traefik. Repairing a missing discovery controller or its output no longer restarts Traefik either.compose.yaml, neverconfig/(which can hold.envcredentials).config/anddynamic/aside instead of deleting them. Traefik's bind mounts follow the renamed directories, so it keeps its routes until it restarts. The retired directories are removed only after a successful apply.docker start onebox-discovery, thencompose up -d discoverywhen that container is gone.ob statussays which half of the proxy drifted.Deploy plan
proxy:line with the action above, orstart Traefikwhen Traefik is not running.Managed services
stop_grace_period: 2m0sin the rendered document.ob service applypasses--timeout 120.docker stop -t 120beforedocker rm -f.Rolling
With the managed proxy, a roll retires a replica only after every replacement is healthy and published under each of the workload's proxy services in
onebox.yml. That includes newcomers adopted on resume. It stops a replica only after its address is a server of no service in the document. A container that is not on the proxy network counts as withdrawn, so adding a route, removing one, or changing its protocol retires old replicas normally. A failed, empty or unparsable read counts as neither published nor withdrawn. A newcomer that is never published is removed and the old replicas keep serving; a replica that is never withdrawn is not stopped. Every newcomer is re-checked before each retirement and before success.Recovery
ob abortand rollback of an interrupted deploy now restore the predecessor before removing the interrupted release's serving containers:A workload counts as already restored only if none of its containers belongs to the interrupted release.
Docs
add-a-database.mdx: the shutdown window.safety-envelope.mdx: the routing gates, the proxy actions, the 3s converge-buffer assumption, and the one-time upgrade restarts.status/capabilities.mdx: the routing gates.No issue was filed first.
Closes #
Why this is correct
TestDecideProxy,TestEnsureProxyComposeOnlyChangeSkipsSwapAndRestart,TestEnsureProxyFailedConvergeRetriesAsConverge,TestEnsureProxyFailedReplaceIsPlannedAsReplace,TestEnsureProxyConvergeStopsDiscoveryBeforeSampling,TestEnsureProxyPullsDiscoveryBeforeStopping,TestEnsureProxyRestartsDiscoveryAfterFailedUp,TestEnsureProxyConfigSwapRetiresLiveDirsInsteadOfDeleting,TestTraefikDefinitionChanged.TestRollRetiresOldOnlyAfterNewcomerPublished,TestRollRemovesNewcomerNeverPublished,TestRollResumeBlocksAdoptedHealthyUnpublishedNewcomer,TestRetireOldReplicaOffProxyNetworkIsWithdrawn,TestRetireWaitsWhileOldReplicaServesARemovedRoute,TestRetireUnreadableRoutesAreNotWithdrawal.TestRecoverySingleReplicaKeepsInterruptedNewcomerServing,TestRecoverySurgesWhenOnlyInterruptedNewcomerServes,TestRestoreReleaseRolesRollsInterruptedOwnedMatchingRevision.TestRenderedServicesCarryStopGrace./code-reviewfound 10 more items:TestEnsureProxyApplyRestoresDiscoveryWhenRecreateFails,TestEnsureProxyRefusesPlannedActionDrift,TestObserveProxyFlagsRepulledTraefikImage,TestEnsureProxyRemovesFailedUploadsButKeepsRetired,TestRollAdmitsEachNewcomerOnce,TestExecuteBindsSealedProxyAction.TestRollStopsWhenAdmittedNewcomerFailsLater,TestRollKeepsLastHealthyOldWhenNewcomersFail,TestEnsureProxyRetryWaitsForHealthBeforeCommit,TestEnsureProxyProceedsWhenPlannedActionMatches,TestExecuteProceedsWhenSealedProxyActionMatches.internal/app/testdata/contract-verdicts.json: only service-document digests moved (the newstop_grace_period).proxywhen its dependencydiscoveryis recreated. Verified locally on Docker Compose 5.6.0, both withup -d discoveryand a fullup -d.go mod tidy -diff, the gofmt check,go vet ./...,go test ./...(2314 passed),golangci-lint run ./...(0 issues), docs-generate-check, env-namespace, and the site build. Together these are whatjust checkruns, plus lint.just e2e,just server-e2e) and amd64. Also not run: the check thatcompose stop discoveryleaves Traefik running. The local Docker VM had stopped, so that check is listed for the target hosts below.Effect on the safety envelope
Yes. This changes what Onebox does to a running production system.
ob proxy applyrestarts Traefik once (the legacy one-digest marker takes the configuration path).ob service applyrecreates each managed service once, to install the shutdown window.ob secrets push, recovery) can read a routing document Traefik is not serving. Runob proxy applyfirst;ob statusshows the proxy as drifted in that state.docker compose version.up -dof a recreated dependency keeps itsdepends_ondependent's container.compose stopof a dependency leaves the dependent running.docker inspect -f '{{.Image}}'of a container equalsdocker image inspect -f '{{.Id}}'of its tag, and thatup -drecreates a container after its tag is re-pulled to a new image./status/capabilitiesupdated.Checklist
just checkpasses locally (its steps run one at a time, as listed above).just checkverifies this).https://claude.ai/code/session_01Dm55dV6FgUFE7vnWQEHCB9