Skip to content

fix: answer STARTED only once the HTTP endpoint serves - #67

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
fix/started-means-serving
Sep 24, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
fix/started-means-serving

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Summary

Start answered STARTED as soon as uv run uvicorn was launched. The orchestrator starts a service's dependents on that status, so a dependent could dial the service seconds before anything listened. uvicorn binds its port only after the app has imported and its lifespan startup has run.

Observed in a local codefly run of a composition: the CLI reported a python-fastapi model service as running about 3 s before uvicorn listened. A dependent Go service ran its startup embedding-binding check in that window and got:

embedding binding installed but not yet verified ... ModelService call did not complete ... endpoint=http://localhost:58192

Change

  • Init records the endpoint address it already resolves (net.Address, the native address the host reaches).
  • Start launches uvicorn, then waits until an HTTP request to that address gets a response. Any status counts: what the app answers on / is its own business. The wait is bounded (3 min).
    • If the process exits first, Start fails. The message carries the process's output tail, so an import error surfaces as a failed Start with its traceback. Before, it was a STARTED that the supervisor revoked a moment later.
    • If the endpoint never answers, the process is stopped, the watcher is stopped, and Start fails.
    • If the request is abandoned (its ctx ends), Start fails.
  • The readiness signal is an HTTP response, not a TCP accept. With a container runtime, the published host port is accepted by the engine's proxy before anything listens behind it.

Classification: a fix, in the agent that produces STARTED. STARTED is the status the CLI orders dependents on (pkg/orchestration/runtime_start_policy.go: "the service started and passed its readiness check"). No core or CLI change.

A behaviour change to review: with hot reload on and an app that fails to import, uvicorn's reload supervisor keeps the port bound but serves nothing. Start now waits for it to answer, up to the bound. If you fix the source within that time, the reload serves and Start returns STARTED. If not, Start fails and stops the supervisor. Before, such a Start was STARTED immediately. TestHotReloadParentOutlivesBrokenApp still pins the supervisor behaviour, because it runs without an Init-resolved address.

Test plan

  • serve_readiness_test.go covers four cases:
    • STARTED is withheld until a late listener answers.
    • Any HTTP answer, including a redirect, counts, whether or not the address carries a scheme.
    • Exit before serving fails with the output tail and leaves no runner.
    • An endpoint that never answers is stopped and bounded, and the next Start is fresh.
  • TestStartReturnsOnceUvicornServes and TestStartFailsWhenUvicornDiesBeforeServing run against real uvicorn via uv.
  • go test -race -count=1 -skip 'Docker|SBOM|Image' ./ passes. gofmt and go vet are clean.
  • Not run locally: the Docker-backed tests (TestCreateToRunDocker*) and the image/SBOM tests. CI runs them.

Not verified here

  • End-to-end in a composition: I will run it with a dev build of this agent (codefly publish dev) through agent-overrides, and report the result on this PR's parent task, not in this PR.

🤖 Generated with Claude Code

Start returned STARTED as soon as `uv run uvicorn` was launched. The
orchestrator starts dependents on that status, so a dependent could dial
the service several seconds before uvicorn bound its port: uvicorn binds
only after the app has imported and its lifespan startup has run.

Start now waits, bounded, until an HTTP request to the endpoint address
Init resolved gets a response (any status). A process that exits first is
a failed Start carrying its output; one that never answers is stopped and
reported. A bare TCP accept is not used as the signal, because a container
runtime's published port is accepted by the engine's proxy before anything
listens behind it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 2e3f245 into main Sep 24, 2026
3 checks passed
@antoinetoussaint-byte
antoinetoussaint-byte deleted the fix/started-means-serving branch September 24, 2026 23:18
antoinetoussaint-byte added a commit that referenced this pull request Sep 25, 2026
Moves the core pin from v0.5.8 to v0.5.9 via codefly agent deps --pin
v0.5.9, so the release carrying #67 (readiness) builds on the released core.

Co-authored-by: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant