feat(health): serve healthz and readyz - #72
Conversation
A deployment has no way to tell a run that is starting from one that is stuck. The process serves /metrics and nothing else, so a probe set has nothing to gate on and a pod counts as available the moment its container starts. /healthz answers as soon as the server binds and never reads the startup sequence. /readyz refuses until the dispatcher is running. Keeping those separate is the whole point. Funding, deployment and prewarm take minutes against a cold chain. A liveness probe that reported the run dead for that window would restart the pod before it sent a transaction, then restart the next attempt at the same place, and the cause would read as a crash loop rather than a slow start. While /readyz refuses it names the phase, so a ten-minute startup shows the step it is on. Measured against the binary: healthz held 200 through a 21 second prewarm while readyz reported "prewarming accounts", then both answered once the dispatcher started. The flag and the phase are stored as one value rather than as two atomics. Two would leave a window where a reader sees the run serving while the body still names the step it left, so the status and the body would disagree about the same instant. Five mutations, five caught, including that one: split into two atomics, a reader observed a serving status carrying "funding accounts". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR SummaryLow Risk Overview
Reviewed by Cursor Bugbot for commit 137675a. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Adds a small, well-tested health package serving /healthz and /readyz on the existing metrics mux, with the ready flag and phase stored as one atomic value. The separation of liveness from readiness is correct for a run with a minutes-long startup; the only gap is that readiness is dropped on the signal path only, so a duration-bounded or error exit leaves /readyz reporting running through the shutdown/flush window.
Findings: 0 blocking | 2 non-blocking | 1 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- [suggestion]
Probes.EnterandProbes.NotReadyhave identical bodies; the distinction is documentation-only. Consider havingNotReadydelegate toEnter(or drop one) so the two cannot drift apart. - 1 suggestion(s)/nit(s) flagged inline on specific lines.
| // Out of service, still alive. The run holds the pod open for the | ||
| // post-summary scrape window, and /healthz keeps answering through it so | ||
| // the kubelet does not read that hold as a hang. | ||
| probes.NotReady("shutting down") |
There was a problem hiding this comment.
[suggestion] NotReady only runs on the signal path. When --duration expires (or a background worker fails), utils.Recv returns ctx.Err() at line 396 and returns early, so this line never executes. The run then proceeds through LogFinalStats, EmitRunSummary and the PostSummaryFlushDelay sleep (25s by default) while /readyz still answers 200 running — exactly the window readiness is meant to cover, and duration-bounded runs are the common deployment shape. Registering it once for every exit after Ready() covers all paths, e.g. defer probes.NotReady("shutting down") placed right after probes.Ready() (line 371), keeping the log line where it is.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 137675a. Configure here.
| // Out of service, still alive. The run holds the pod open for the | ||
| // post-summary scrape window, and /healthz keeps answering through it so | ||
| // the kubelet does not read that hold as a hang. | ||
| probes.NotReady("shutting down") |
There was a problem hiding this comment.
Readiness stuck after non-signal stop
Medium Severity
NotReady runs only when Recv gets a signal. A --duration timeout or a worker failure after Ready leaves /readyz at 200 running through the post-summary scrape hold, so the pod still looks serving after the dispatcher has already stopped.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 137675a. Configure here.


Stacked on #71. Base is
brandon2/amm-swap-scenario, so this branch carries theAMM scenario, the ERC721 gas fix and these endpoints together, and the image it
builds is the one to deploy.
Why
sei-loadserves/metricsand nothing else. A deployment therefore has no wayto tell a run that is still starting from one that is stuck, and a pod counts as
available the moment its container starts.
That blocks the load-generator deployment in the platform repo, whose pod spec
has a
startupProbe, areadinessProbeand alivenessProbepointing at/readyzand/healthz. Against the current binary every one of them fails.What the two endpoints mean
/healthzanswers as soon as the HTTP server binds. It never reads the startupsequence.
/readyzrefuses until the dispatcher is running, and while it refuses it namesthe phase.
Keeping them separate is the point rather than a detail. Funding, deployment and
prewarm take minutes against a cold chain. A liveness probe that reported the run
dead for that window would restart the pod before it sent a transaction, then
restart the next attempt at the same place. The run would never happen, and the
cause would read as a crash loop rather than a slow start.
Measured against the binary, polling both endpoints across a real startup:
The phase in the body is there for the operator watching that window. A
ten-minute startup that answers only
503says nothing about which step is slow.One design note
The ready flag and the phase are stored as a single value, not as two atomics.
Two would leave a window where a writer has set the flag but not yet the phase,
so a reader sees the run serving while the body still names the step it left.
The status line and the body would then disagree about the same instant.
Phases
startingdeploying contractsfunding accountsprewarming accountsrunningshutting downshutting downdrops readiness while/healthzkeeps answering. The run holdsthe pod open on purpose for that scrape window, and a liveness probe that failed
during it would kill the process before its final metrics were read.
Verification
Five mutations, five caught:
NotReadymade a no-opstatus carrying
funding accountsThe fifth is worth naming. The first version of that guard passed against the
split-atomics mutation, so its failure message claimed something it could not
detect. It was rewritten to widen the window before it was believed.
gofmt,go vetandgolangci-lint runare clean. The full suite passes, andthe health package passes under
-race.🤖 Generated with Claude Code