fix(appsvc): one dead preview slot held /readyz at 503 forever #113
Open
iggy
wants to merge 3 commits from
agent/readyz-liveness into agent/lane-stages
pull from: agent/readyz-liveness
merge into: triform:agent/lane-stages
triform:main
triform:agent/zstd-project-check-runtime-20260804
triform:agent/lane-restart-continuity
triform:agent/diagnostic-retrigger-refresh-20260804
triform:agent/candidate-notfound-idempotent
triform:agent/lanetree-stale-remove-race
triform:agent/lane-stale-candidate-cleanup-20260804
triform:agent/lane-stale-candidate-cleanup-ci-retry-20260804
triform:agent/lane-cutover-integration-r3-20260804
triform:agent/lane-abandoned-verdict
triform:agent/lane-cutover-integration-r2-20260804
triform:agent/lane-point-retry
triform:agent/witness-scratch-recovery-20260804
triform:agent/lane-cutover-integration-20260804
triform:agent/project-timeout-finalization-linear3-20260804
triform:agent/diagnostic-cancel-backoff-20260803
triform:agent/project-timeout-finalization-linear2-20260804
triform:agent/project-timeout-finalization-linear-20260804
triform:agent/project-timeout-finalization-20260804
triform:agent/lane-red-files-evidence
triform:agent/lane-member-identities
triform:agent/lane-dispatch-artifact
triform:agent/lane-honesty
triform:agent/lane-stages
triform:agent/lsp-pipe-liveness-20260803
triform:agent/lane-point-reassert-20260803
triform:agent/lane-infra-backoff
triform:agent/analyzer-pathology-rollout-20260803
triform:agent/unattributed-witness-20260803
triform:agent/respawn-nonlatching-main
triform:agent/rejection-reason-refresh-20260803
triform:agent/rejection-reason-20260803
triform:agent/lane-compose-legs
triform:agent/overlay-response-timeout-20260803
triform:agent/diagnostic-event-origin-20260803
triform:agent/http-partial-write-20260803
triform:agent/diagnostic-absolute-uri-20260803
triform:agent/diagnostic-final-fence-20260803
triform:agent/diagnostic-freshness-rebased-20260803
triform:agent/diagnostic-freshness-20260803
triform:agent/diagnostic-timeout-20260803
triform:agent/reuse-result-protocol-20260803
triform:agent/project-check-lifecycle-fix
triform:agent/outcome-pending-race
triform:agent/red-files-attribution
triform:agent/name-the-unattributed-diagnostic
triform:agent/ra-proper-20260731
triform:agent/build-lane
triform:agent/serve-image-tag-epoch
triform:agent/ci-no-cancel-in-progress
triform:agent/witness-serial-class
triform:agent/batch-obs
triform:agent/cgls11-reopen-cap
triform:agent/coalesce-gated-witness-by-base-rebased
triform:agent/get-status-attributed
triform:agent/ra-memory-cap
triform:agent/witness-stranded-unknown
triform:agent/witness-ra-reap-handover
triform:agent/coalesce-gated-witness-by-base
triform:agent/witness-warm-target
triform:agent/witness-serial-warm
triform:agent/coalesced-emit-gated-checks-ran
triform:agent/test-flake-hardening
triform:agent/appdrv-test-isolation
triform:agent/cgls-26-attribution-guard
triform:agent/verdict-failure-class
triform:agent/appserve-clear-stale-last-red
triform:agent/witness-watchdog-25m
triform:agent/docs-ci-log-pointer
triform:agent/appserve-enospc-selfheal
triform:agent/eager-boot-warm
triform:agent/deploy-ref-parity-fsgroup
triform:feat/serial-batch-witness
triform:agent/witness-base-sha-keying
triform:agent/instances-sighup-reload
triform:agent/eager-boot-warm-basefetch
triform:agent/serveapi-base-local-no-fetch
triform:agent/preview-restart-persistence
triform:agent/hot-trunk-no-cancel
triform:agent/preview-selfserve-integrated
triform:agent/gated-checks-ran-on-deployed
triform:agent/gated-checks-ran
triform:agent/preview-rename-public-status
triform:fix/interaction-red-unknown
triform:agent/overlay-cap-128
triform:agent/per-lane-build-slot
triform:agent/runplan-per-build-reread
triform:agent/cgls11-pool-overlay-flycheck
triform:agent/cgls12-content-macro-detection
triform:agent/manifest-run-plan-on-main
triform:agent/ci-concurrency
triform:agent/macro-witness-fix
triform:agent/appserve-build-job
triform:agent/build-to-completion
triform:agent/fix-overlay-reaches-ra
triform:agent/fix-appbuild-test-manifest-hash
triform:agent/fix-serve-ra-procmacro-skew
triform:agent/fix-otlp-v1-traces-path
triform:agent/ra-stderr-visible
triform:agent/manifest-run-plan
triform:agent/self-serve-previews
triform:agent/appserve-worktree-setup
triform:agent/appserve-dockerfile-fix
triform:agent/timer-settle-unknown-cgls9
triform:agent/verdict-freshness-cgls9
triform:agent/app-serve
triform:agent/verdict-cli
triform:agent/witness-offloop
triform:agent/bounded-git
triform:agent/readyz
triform:agent/verdict-attribution
triform:agent/otlp-queue
triform:agent/infra-258-260
triform:agent/project-checks-nonblocking
triform:agent/attribution-helper
triform:release/v0.3.0
triform:agent/batch-concurrency-validation
triform:agent/project-checks-hard
triform:agent/amem-49-exporter-timeout
triform:agent/infra-54-init-success-log
triform:agent/infra-49-blocking-client
triform:agent/infra-49-simple-exporter
triform:agent/honest-verdict-and-otel
triform:agent/overlay-project-checks
triform:agent/fsn-serve-shards
triform:agent/existing-red-report
triform:agent/checks-base-pruning
triform:agent/docs-project-check-resource-discipline
triform:agent/session-handover-2026-05-25
triform:agent/builder-template-cache-repair
triform:agent/tf-multiverse-serve-service
triform:agent/serve-auth-read-image
triform:agent/cargoless-http-read-auth
triform:agent/serve-image-bump
triform:agent/push-only-ready
triform:agent/architect
triform:agent/bench-lead-m3
triform:agent/builder-infra-msrv-doc
triform:agent/builder-infra-doc-housekeeping
triform:agent/builder-infra-roadmap-integration
triform:agent/builder-infra-docs-combined
triform:agent/docs-launch-lead-roadmap-refresh
triform:agent/docs-launch-lead-prestage
triform:agent/builder-infra-update-lock
triform:agent/docs-launch-lead-w2-docs
triform:agent/builder-infra-263-disk-full
triform:agent/dev-fixer-2c
triform:agent/dev-fixer-2b
triform:agent/bench-lead-m2-on-9429462
triform:agent/docs-launch-lead-brand-coherence
triform:agent/bench-lead-m2-approx
triform:agent/docs-launch-lead-dewasm-scope
triform:agent/dev-fixer-otel-wave1
triform:agent/dev-fixer-dewasm
triform:agent/dogfood-lead-stage1-rework
triform:agent/dev-fixer-respawn-driver-reset
triform:agent/dev-fixer-pushoverlay
triform:agent/dev-fixer-healthz
triform:agent/dogfood-lead-stage1-suite
triform:agent/bench-lead-diffharness
triform:agent/dev-fixer-serve-wire
triform:agent/builder-infra-serve-image
triform:agent/docs-launch-lead-inc2spec
triform:agent/builder-infra-serve-k8s
triform:agent/builder-infra-v020
triform:agent/docs-launch-lead-e5
triform:agent/builder-infra-198
triform:agent/dev-fixer-modelr-cio
triform:agent/builder-infra-podinfra
triform:agent/bench-lead-15bench-r
triform:agent/bench-lead-15bench
triform:agent/builder-infra-14-integ
triform:agent/builder-infra-14auth-fix
triform:agent/builder-infra-tripleguard
triform:agent/builder-infra-i2fixed
triform:agent/builder-infra-i1
triform:agent/builder-infra-reset0
triform:agent/builder-infra-capcore
triform:agent/builder-infra-12
triform:agent/builder-infra-4
triform:agent/builder-infra-incr4
triform:agent/builder-infra-incr3
triform:agent/builder-infra-182
triform:agent/builder-infra-14auth
triform:agent/docs-launch-lead-e4
triform:agent/bench-lead-modelr-13c
triform:agent/docs-launch-lead-e3
triform:agent/bench-lead-modelr-13
triform:agent/dev-fixer-modelr-c
triform:agent/docs-launch-lead-e2
triform:agent/builder-infra-comp2v2
triform:agent/builder-infra-cigatefix2
triform:agent/dev-fixer-modelr-b
triform:agent/bench-lead-modelr
triform:agent/builder-infra-cigatefix
triform:agent/builder-infra-comp2
triform:agent/docs-launch-lead-modelr
triform:agent/builder-infra-modelr
triform:agent/docs-launch-lead-final
triform:agent/bench-lead
triform:agent/dev-fixer-132
triform:agent/dev-fixer-97
triform:agent/docs-launch-lead-prep
triform:agent/dev-fixer-128
triform:agent/dev-fixer-procmacro-downrank
triform:agent/dev-fixer-idle-evict
triform:agent/dev-fixer-structural-trigger
triform:agent/dev-fixer-openclosed-design
triform:agent/dev-fixer-94
triform:agent/dev-fixer-97-plan
triform:agent/docs-launch-sf
triform:agent/docs-launch-lead-d1b
triform:chore/f13a-integrate-v2
triform:agent/dev-fixer-f13a
triform:chore/cwdl-71-phase-d
triform:chore/buildid-integrate-v2
triform:agent/dev-fixer-f13b
triform:agent/buildid
triform:agent/ra-config
triform:agent/d-a2-design
triform:agent/docs-launch-lead-d1
triform:agent/docs-launch-lead
triform:agent/dev-fixer-5
triform:agent/dev-fixer-4
triform:agent/dev-fixer-3
triform:chore/launch-readiness-docs
triform:agent/dev-fixer-2
triform:agent/f8-integrate
triform:agent/integration-default
triform:agent/dev-fixer-f8
triform:agent/dev-fixer
triform:agent/builder-infra
triform:agent/v0-feature-complete
triform:agent/daemon-core-21
triform:agent/v0-final-converge2
triform:agent/ra-bench-wt
triform:agent/cli-ux-v0
triform:agent/v0-final-converge
triform:agent/build-cas-publisher
triform:agent/docs-v0
triform:agent/v0-core-converge
triform:agent/gate-locked
triform:agent/main-stabilize
triform:agent/cli-ux-wire
triform:agent/devserver-bundle
triform:agent/daemon-core-sup
triform:agent/integration-converge
triform:agent/build-cas-bundle
triform:agent/daemon-core-wt
triform:agent/build-cas
triform:agent/devserver
triform:agent/cli-ux
triform:integ/dc-ac6
triform:integ/ra-bench
triform:integ/daemon-core
triform:agent/ra-bench
triform:agent/proto-contracts
triform:agent/daemon-core
No reviewers
Labels
Clear labels
No items
No labels
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set.
Reference
triform/cargoless!113
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "agent/readyz-liveness"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The bug
/readyzrequired every ever-green instance to be currently serving. On the tf-multiverse previewmergelast built 2026-08-02 andfeature-x2026-06-24 — both long dead, neither on anyone's critical path — so the probe was pinned at 503 indefinitely. That is a wedged-red readiness signal: it delists the pod, and with it the healthydevandlane(one readinessProbe, all endpoints), and it says nothing at exactly the moment you need it to mean something.What
/readyzmeans now"The daemon can accept and serve work."
Live is a liveness test — a fresh observable state change within
STALE_AFTER_SECS(1h) — not "this slot exists and is not green". That distinction is the whole fix, and it is the lesson from the tf-multiverse watchdog that was widened to treat any busy slot as busy and was then made permanently unable to fire by six 41-day-dead slots.An instance on someone's critical path re-arms its own stamp simply by being used (a push moves its ref → its phase advances → that is a state change), so a broken
devpeople are pushing to keeps the probe red for as long as it is broken. A slot nobody has touched in weeks emits no transitions and ages out — by rule, with no per-name allowlist to keep in sync with the instance set.Both useless probes are structurally excluded
Why the stamp is trustworthy
The driver republishes every instance on every event. An unconditional re-stamp would let a busy
devkeep a deadmergeeternally fresh — the same widening failure in miniature. Sopublishdiffs each incoming row against the one it replaces and carries the old stamp forward when nothing changed. A first-seen instance is stampednow(), not0, so a hot-added preview is not born stale./readyzis also no longer latched at publish time: it is computed from the snapshot and the clock at read time, because a degraded instance goes stale by time passing, not by an event arriving — and a daemon whose control loop is wedged publishes nothing at all, precisely when a latch would freeze at its last value.Stale state never becomes the thing that reports a problem
An aged-out instance keeps its full
/approw pluslast_change_unix/idle_secs/stale, and is named inreadiness.stale_degraded./readyzanswers "is this daemon working";/appanswers "…and here is exactly what is not".Tests pin both directions
Forward (the fix): one live slot + one 41-day-dead slot reports ready; the real four-instance production shape reports ready with
merge+feature-xaged out and both named instale_degraded.Inverse (not just "always green"):
lanestill turns the probe red with two aged-out siblings present;Grounded against the running daemon (read-only)
Probed pod
cargoless-preview-9b84c69cd-mhnbqintriform-staging. It isapp-serve ready=falseright now, withlanegenuinely broken (serving_sha: null,phase=building,last_red_reason: cannot change to .../lane/worktree: No such file or directory, no transition in 19 min). Under this rule that pod correctly stays 503 — the fix does not paper over a real fault. The staleness horizon's lower bound is cited from the manifest's own cold-build budget (startupProbe: 180 × 10s= 30 min), cleared with 2× headroom.The one sharp edge is documented rather than hidden: an instance degraded and silent for over an hour (a cold rebuild past the 60-minute mark) ages out mid-recovery. Accepted deliberately — a build silent for an hour is indistinguishable from a hung one, and exempting busy pipelines would let a hung build veto forever, reintroducing the exact wedge this removes.
Validation
No local cargo (CI-only by design). Logic was validated against a Rust-faithful Python port before pushing: all 17 test bodies replayed statement-for-statement, which caught a
u64underflow in a test fixture.rustfmt --edition 2024clean.Files:
crates/cargoless-core/src/appsvc.rs, plus doc/manifest comment updates indocs/PREVIEW-STATUS.md,docs/design/D-APP-SERVE.md,deploy/cargoless-appserve.k8s.yaml.Based on
agent/lane-stages(PR #99), notmain.`/readyz` required EVERY ever-green instance to be currently serving. On the tf-multiverse preview `merge` last built 2026-08-02 and `feature-x` 2026-06-24 — both long dead, neither on anyone's critical path — so the probe was pinned at 503 indefinitely. That is a wedged-red readiness signal: it delisted the pod, and with it the *healthy* `dev` and `lane` (one readinessProbe, all endpoints), and it said nothing at exactly the moment you needed it to mean something. /readyz now means "the daemon can accept and serve work": ready <=> some instance is currently serving AND no LIVE instance is failing to serve a green it owns "Live" is a LIVENESS test — a fresh observable state change within `STALE_AFTER_SECS` (1h) — not "this slot exists and is not green". That distinction is the whole fix, and it is the lesson from the tf-multiverse watchdog that was widened to treat any busy slot as busy and was then made permanently unable to fire by six 41-day-dead slots. An instance on someone's critical path re-arms its own stamp simply by being used (a push moves its ref, which advances its phase), so a broken `dev` people are pushing to keeps the probe red for as long as it is broken. A slot nobody has touched in weeks emits no transitions and ages out — by rule, with no per-name allowlist to keep in sync with the instance set. Both useless probes are structurally excluded: - NOT always-green: "some instance is currently serving" is a fact about the present with no stamp to age, so ageing can only ever REMOVE a veto, never manufacture a green. Nothing serving => 503 however quiet the daemon has gone. - NOT wedged-red: a degraded instance vetoes only while live. The stamp is trustworthy because `publish` DIFFS each incoming row against the one it replaces and carries the old stamp forward when nothing changed. The driver republishes every instance on every event, so an unconditional re-stamp would let a busy `dev` keep a dead `merge` eternally fresh — the same widening failure in miniature. A first-seen instance is stamped now(), not 0, so a hot-added preview is not born stale. `/readyz` is no longer latched at publish time: it is computed from the snapshot AND the clock at read time, because a degraded instance goes stale by time passing, not by an event arriving — and a daemon whose control loop is wedged publishes nothing at all, precisely when a latch would freeze. Stale state never becomes the thing that reports a problem: an aged-out instance keeps its full `/app` row plus `last_change_unix` / `idle_secs` / `stale`, and is named in `readiness.stale_degraded`. /readyz answers "is this daemon working"; /app answers "...and here is exactly what is not". Tests pin both directions. Forward: one live slot + one 41-day-dead slot reports ready, and the real four-instance production shape reports ready with `merge`+`feature-x` aged out. Inverse: a daemon with nothing serving is NOT ready however ancient, an actively-transitioning broken instance never ages out across 20 near-horizon cycles, a break on a live `lane` still turns the probe red with two aged-out siblings present, and a backwards clock fails toward not-ready (saturating age => everything reads live). Plus the disarm trap itself: a dead slot's stamp survives 400 republishes driven by a busy sibling. Validated against a Rust-faithful Python port before pushing (all 17 test bodies replayed statement-for-statement). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.