What the agent does
The agent reads how the server starts and stops today. In a Rust service like launchbadge/realworld-axum-sqlx, src/main.rs loads .env, parses Config with clap from DATABASE_URL and HMAC_KEY, builds a PgPoolOptions pool with max_connections(50), runs the embedded sqlx::migrate!() and calls http::serve. src/http/mod.rs builds the router from the users, profiles and articles modules, wraps it in a ServiceBuilder with an AddExtensionLayer for the shared ApiContext and a TraceLayer, then calls axum::Server::bind on 0.0.0.0:8080 and .serve(...) with nothing after it. Cargo.toml pins axum 0.3, tokio 1.14 with only the macros and rt-multi-thread features, sqlx 0.5 with Postgres, tower 0.4 and tower-http 0.2. There is no signal handling, no deadline, and the pool is never closed; a SIGTERM kills the process mid-request.
The fix follows the versions in the lockfile rather than the newest Axum. With Axum 0.3 the server is hyper's, and hyper's builder has with_graceful_shutdown, which takes a future and stops accepting connections when it completes while letting open ones finish. The agent adds the signal and time features to tokio in Cargo.toml, writes a shutdown_signal future in src/http/mod.rs that resolves on SIGINT or on SIGTERM through tokio::signal::unix, and passes it to the builder. After serve returns, main.rs calls db.close().await so the pool's connections end cleanly instead of being dropped, and the whole drain is wrapped in tokio::time::timeout with a deadline read from a new SHUTDOWN_TIMEOUT_SECS field on Config, with a default, so a request that never finishes cannot hold the process forever.
To make it testable, serve is split: the router and layers stay where they are, and the bind-and-serve part takes a listener and a shutdown future instead of a fixed address and a signal. The existing main passes the port and the signal future, so its behaviour is unchanged. An integration test under tests/ binds port zero, starts the server with a Postgres from DATABASE_URL, opens a request that is still in flight, triggers shutdown through a oneshot channel, and asserts that the in-flight request completes, that a new connection is refused, and that the server task returns before the deadline.
The agent runs the repository's own steps around it: Postgres in a container as the README describes, sqlx migrate run or the embedded migrator on start, cargo build, cargo test with the new integration test, and cargo clippy. It also sends a real SIGTERM to the running binary in the sandbox while a request is open and records the log lines from the TraceLayer showing the request finished before the process exited, since a unit test and a real signal are two different proofs.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Add graceful shutdown to this Axum service. Read Cargo.toml, Cargo.lock, src/main.rs, src/http/mod.rs, src/config.rs, .env.sample and README.md first and keep the Axum, hyper and tokio versions the lockfile has. Add the tokio signal and time features, a shutdown future that resolves on SIGINT or SIGTERM, and pass it to the server's with_graceful_shutdown. After serve returns, close the Postgres pool, and bound the whole drain with a timeout read from a new config field with a sensible default. Split serve so a test can pass its own listener and shutdown future without changing main's behaviour. Add an integration test that starts the server on port zero against Postgres from DATABASE_URL, holds a request in flight, triggers shutdown, and asserts the request completes, new connections are refused and the task returns before the deadline. Run cargo build, cargo test and cargo clippy, then send a real SIGTERM to the running binary with a request open and keep the log output. Open a pull request with the test output and the signal run.",
"resources": [
{
"url": "https://github.com/your-org/your-app"
}
]
}- Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
- Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
- Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.
What comes back
What comes back is a pull request on its own branch touching Cargo.toml and Cargo.lock for the two tokio features, src/config.rs for the timeout field, src/main.rs for the pool close and the deadline, src/http/mod.rs for the shutdown future and the split serve, a new integration test under tests/, and .env.sample documenting the new variable. The description explains why the change stays on Axum 0.3 and hyper's builder rather than upgrading, and what happens when the deadline is hit.
The transcript holds the Postgres start, the cargo test run with the new test's name in its output, cargo clippy, and the run where the agent started the binary, opened a request, sent SIGTERM and captured the trace logs showing the request's response line before the process ended. A reviewer reads that sequence rather than trusting the description.
Before merging, a reviewer checks that the deadline is longer than the slowest real request the service serves, that the pool close happens after the server future and not before, and that SIGINT still stops a local cargo run promptly. If the service runs under an orchestrator with its own termination grace period, a follow-up message to the same session aligns the default with it.
Run it on every service deployed on rolling restarts
A team moving several Rust services onto an orchestrator that sends SIGTERM on every deploy starts one run per repository: a POST /o/{org}/sessions with that repository in resources and this prompt, with an Idempotency-Key per repository so a retry returns the run already started.
Each run has its own sandbox, its own Postgres container and its own build cache, so several services compile and test in parallel up to the key's max_concurrent limit. A webhook_url on the key receives run.completed per turn with the session_url that holds the pull request and the signal run.
The use case this job follows, with its automation and trigger: Hand off from your terminal.
Why run it on Rush?
Proving shutdown behaviour means starting Postgres, building the binary, holding a connection open and sending a signal to a real process, then reading the logs to see which happened first. On a laptop that is a database on port 5432 and a process to remember to kill. In a Rush sandbox the agent does all of it on its own machine, keeps the logs, and the sandbox parks when the turn ends.
The pull request carries two kinds of proof: an integration test that runs in CI from now on, and the recorded signal run with its trace output. A reviewer can see the request complete before the exit instead of reasoning about futures, and the same prompt runs across several services at once, each in its own sandbox.
Frequently asked questions
Why not upgrade to the current Axum first?
Because that is a different pull request. The lockfile pins Axum 0.3, and hyper's server builder at that version already has with_graceful_shutdown, so shutdown can be added without touching the handlers, extractors and error types an upgrade would change. The description says what the equivalent looks like on newer Axum, where axum::serve takes the same kind of future.
What happens to a request that is still running at the deadline?
The tokio::time::timeout around the drain expires, the agent logs that the deadline was hit, and the process exits with the pool closed; that request's client sees a closed connection. The deadline exists so a stuck handler cannot block a deploy, and the pull request names the default so you can change it before merging.
Does the test need a real Postgres?
Yes, because the server's ApiContext holds a pool and the handlers query it. The test reads DATABASE_URL the way main does, and the agent starts Postgres in a container the way the README describes. In CI the same variable points at a service container.
Does SIGINT still work for cargo run?
Yes. The shutdown future resolves on either signal, so Ctrl-C in a terminal starts the same drain, and a second signal is not needed to leave. On a non-Unix host only Ctrl-C is wired, which the pull request notes.