All jobs

Add Graceful Shutdown to a Go Service from a Linear Issue

Label a Linear issue and an agent adds deadline-bounded graceful shutdown across your Go service's HTTP, gRPC and message servers, with tests.

Run this on RushIn a Project that has your repository.

What the agent does

The issue names the contract precisely: on SIGTERM, stop accepting new HTTP and gRPC requests, finish in-flight ones within 20 seconds, close the RabbitMQ and Postgres connections, then exit. The run starts with that text and the repository cloned. In evrone/go-clean-template the service boots in internal/app/app.go: Run() builds the Postgres pool, the use cases, and then four servers, an HTTP server on Fiber, a gRPC server, a RabbitMQ RPC server and a NATS RPC server, started together and waited on together.

The agent reads what exists before adding anything, and the template already has a shutdown skeleton: one signal.Notify watching os.Interrupt and SIGTERM, a select over the four servers' notify channels, and a shutdownServers that calls each server's Shutdown in sequence. What the issue asks for is the missing discipline around it. The HTTP wrapper's shutdown timeout is hardcoded to three seconds in pkg/httpserver/server.go, not the 20 the issue names; the gRPC server stops with a bare GracefulStop and no bound; the RabbitMQ and Postgres closes carry no ordering guarantee against in-flight work. The diff reshapes that path rather than bolting on a second one.

Concretely: the 20-second budget becomes a context deadline created once in Run, shared by every server; the HTTP server stops accepting and drains with ShutdownWithTimeout against that deadline; gRPC gets a GracefulStop wrapped so a stuck RPC cannot hold the deadline past its budget; the AMQP consumer stops its delivery loop before its connection closes; and the pgx pool closes after the servers drain, so in-flight handlers keep their database handles until their last query. Configuration stays in config/config.go with its caarlos0/env parsing, adding a shutdown timeout the .env.example documents.

Tests go where none exist today, internal/app, with the docker-compose stack the Makefile already orchestrates providing Postgres, RabbitMQ and NATS: start the app on random ports, send SIGTERM mid-request, and assert the process exits within the deadline, that an in-flight HTTP request completes, that a new one is refused, and that gRPC and the consumer wind down in order. The prompt asks for go test -race ./... and make linter-golangci, and the agent runs both, fixing what they flag before pushing. The pull request links back to the Linear issue.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "claude",
  "input": "A Linear issue labeled agent says: \"Add Graceful Shutdown to a Go Service. On SIGTERM, stop accepting new HTTP and gRPC requests, finish in-flight ones within 20 seconds, close the RabbitMQ and Postgres connections, then exit.\" Do the work in this repository, add tests for it, run go test -race ./... and make linter-golangci, and open a pull request that links back to the issue.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
  1. 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.
  2. 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.
  3. 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. The diff centres on internal/app/app.go: the single deadline, the ordered shutdown sequence, and the notify wiring, with matching changes in pkg/httpserver for the drain timeout and the gRPC and AMQP shutdown paths. config/config.go gains the shutdown timeout with a default, and .env.example documents it.

The new test file in internal/app is the part a reviewer reads first: it boots the service against the compose stack, fires SIGTERM during traffic, and asserts the exit order and the deadline. The pull description reports go test -race ./... and make linter-golangci, and the trajectory keeps their output, including any race the first run caught and the fix for it.

Before merging, the reviewer checks that the deadline is enforced as a whole rather than per server, that a slow in-flight request is allowed to finish while new ones are refused, that the Postgres pool closes last, and that the compose stack the tests rely on is the one the repository already defines. A rollback of the change is safe because no exported server API changes shape.

Run it on every Linear issue labeled agent

Set up the automation once in the console: a Linear trigger on linear.issue.labeled with the labels you choose, pointed at the Project whose repository the agent should work in. Labeling an issue starts a run with the issue as its context, and the agent opens a pull request that links back, so a triaged issue becomes reviewable code without a developer starting it by hand.

Changes made by an app integration never fire the trigger, so an agent's own pull requests cannot re-trigger it. The run works in the Project's repository with its secrets and its compose stack for integration dependencies; keep the agent label for issues described well enough to act on, and the automation's prompt tells the agent to ask on the issue when it is not.

The use case this job follows, with its automation and trigger: Turn a Linear label into a PR.

Why run it on Rush?

Graceful shutdown is easy to describe and easy to get subtly wrong: a GracefulStop without a bound, a pool closed while handlers still query, an HTTP wrapper whose hardcoded timeout ignores the budget. On Rush the agent makes the change in a sandbox with the full compose stack up, runs the race detector against real shutdowns, and comes back with the failing cases already found and fixed, recorded in the trajectory.

The pull request is reviewable proof. It links the issue, the description names the deadline semantics and the shutdown order, and the trajectory holds go test -race ./... and make linter-golangci with their output. Because the trigger fires per label, every well-described issue in the backlog gets the same treatment: one run, one branch, one pull request held for a human merge.

Frequently asked questions

Does it handle all four servers, including NATS?

Yes. The template runs four servers, HTTP on Fiber, gRPC, RabbitMQ RPC and NATS RPC, and the shutdown sequence covers each: consumers stop taking deliveries, servers stop accepting, in-flight work drains under the shared deadline, and the Postgres pool closes last.

What happens to a request that is still running at 20 seconds?

The deadline is enforced as a whole. In-flight HTTP and gRPC calls get until the context expires to finish; what is still running at that point is cut, connections close, and the process exits rather than hanging. The tests assert the process actually exits inside the budget.

My service only has HTTP and Postgres. Does the page still apply?

Yes. The same shape covers a smaller service: one deadline, stop accepting, drain in-flight, close the pool last. The four-server template here just shows the full version; the agent adapts to whatever servers your repository actually starts.

How are the tests run?

The unit and app tests run with go test -race ./... as the issue says, and the Makefile's linter-golangci runs golangci-lint. The shutdown test uses the repository's own docker-compose services for Postgres, RabbitMQ and NATS, the same stack make integration-test uses.

What if the issue's requirements conflict with the existing skeleton?

The agent reshapes the existing shutdown path rather than adding a second one, and the pull description says what changed and why, such as replacing the HTTP wrapper's hardcoded three-second timeout with the issue's 20-second budget. Review sees the reasoning beside the diff.