Help us shape the future of Go! Take the 2026 Go Developer Survey

devproxy

package
v1.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 23, 2026 License: MIT Imports: 23 Imported by: 0

README

Development process proxy

cmd/devproxy owns a stable HTTP port and independently manages application generations. Build first, then submit the completed executable. The serving generation stays active while a candidate starts, and also when a build or candidate fails. This is a local development supervisor, with a mode-0600 Unix control socket.

go build -o /private/tools/devproxy ./cmd/devproxy
/private/tools/devproxy serve --listen 127.0.0.1:2052 \
  --control /private/run/app.sock

/private/tools/devproxy deploy --control /private/run/app.sock \
  --companion '["run-media-worker"]' -- /private/builds/app-123 run-server
/private/tools/devproxy status --control /private/run/app.sock

deploy sends its current directory and environment. The supervisor snapshots the executable into a private generation directory, selects an ephemeral API port, overrides PORT and OPS_PORT=0, and starts it without a shell. Each repeatable --companion is a JSON argument array for that same executable with its own port. Every companion must remain alive; the API must return 200 from /readyz. The dev-only readiness handler must echo X-Devproxy-Ready-Token when the request value equals its DEVPROXY_READY_TOKEN environment value. A 200 response without the matching token cannot promote a different listener that happened to take the port. Set application issuer/public URLs explicitly to their stable public addresses rather than deriving them from the ephemeral backend port.

After readiness, new requests go to the candidate and existing requests stay with their old generation. The proxy preserves the original Host, handles HTTP upgrades, and does not retry requests after an upstream disconnect. --drain-timeout bounds old requests, including long-lived upgraded connections (default 30s); after that they close. Then every old process group receives SIGTERM, followed by SIGKILL after --stop-timeout (default 45s). Applications should interrupt background jobs as shutdown begins, keep their database connections open until the queue acknowledges them, and drain HTTP before closing dependencies. The budget allows a 20s HTTP drain and 20s of cleanup; it is still a hard limit for a process that ignores cancellation. The readiness deadline is --ready-timeout (default 60s).

Descendants that moved to their own process group (a Docker client, say) get the same window, then SIGKILL; an escaped holder of the output pipe cannot keep a generation from stopping. A failed candidate is stopped in the background, so it never delays the next deploy. status reports the candidate in progress (deploying) and generations still stopping (stopping).

The deploy command exits after promotion and does not own the server process. An external watcher may freely stop its old deploy command while compiling a new executable. Stop the supervisor itself to stop all managed generations. A companion that dies after promotion marks status unhealthy and /readyz returns 503; it never promotes another in-flight generation implicitly.

This protects planned rebuilds and failed candidate startup. It does not keep an old generation indefinitely to recover from a later application crash, or guarantee child cleanup if the supervisor itself is forcibly killed with SIGKILL. Use a normal interrupt or SIGTERM for its graceful shutdown.

Run the subprocess and concurrency checks with:

go test -race ./devproxy ./cmd/devproxy

Documentation

Overview

Package devproxy keeps a stable development HTTP listener while complete, healthy subprocess generations replace one another behind it.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Run

func Run(ctx context.Context, cfg Config) error

Run serves until cancellation. No executable is launched until an authorized local process submits a deployment. A failed candidate leaves active intact.

func WithReadinessToken

func WithReadinessToken(next http.Handler, token string) http.Handler

WithReadinessToken proves that a readiness response came from this candidate. A supervisor supplies a random token only to its child. Normal readiness requests work unchanged; absent or incorrect tokens are never reflected.

Types

type Config

type Config struct {
	Listen, Control                         string
	ReadyTimeout, DrainTimeout, StopTimeout time.Duration
	Output                                  io.Writer
}

type Deployment

type Deployment struct {
	Executable  string     `json:"executable"`
	Args        []string   `json:"args"`
	Companions  [][]string `json:"companions,omitempty"`
	Directory   string     `json:"directory"`
	Environment []string   `json:"environment"`
}

Deployment is submitted over the private Unix socket, never over the public proxy. Companions run the same immutable executable with their own arguments.

type Result

type Result struct {
	Generation uint64 `json:"generation"`
	Port       int    `json:"port"`
}

func Deploy

func Deploy(ctx context.Context, control string, d Deployment) (Result, error)

Deploy submits one candidate to the private supervisor and waits until it has either become active or failed. It never owns the launched processes.

type Status

type Status struct {
	PID        int    `json:"pid"`
	Generation uint64 `json:"generation"`
	Port       int    `json:"port"`
	Healthy    bool   `json:"healthy"`
	// Deploying is the candidate awaiting readiness; Stopping lists retired or
	// failed generations whose processes are still being stopped.
	Deploying uint64   `json:"deploying,omitempty"`
	Stopping  []uint64 `json:"stopping,omitempty"`
}

func Inspect

func Inspect(ctx context.Context, control string) (Status, error)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL