go 144 lines · 3 tabs

Graceful Drain of In-Flight Jobs Before Worker Shutdown in Go

Shared by codesnips Aug 2026
3 tabs
package worker

import "context"

func (p *Pool) Submit(job Job) error {
	// Reject fast if draining, otherwise enqueue with backpressure.
	select {
	case <-p.ctx.Done():
		return ErrDraining
	default:
	}

	select {
	case p.jobs <- job:
		return nil
	case <-p.ctx.Done():
		return ErrDraining
	}
}

func (p *Pool) Shutdown(ctx context.Context) error {
	p.cancel() // stop accepting new submissions

	p.closeOnce.Do(func() {
		close(p.jobs) // let ranging workers finish the buffer then exit
	})

	done := make(chan struct{})
	go func() {
		p.wg.Wait()
		close(done)
	}()

	select {
	case <-done:
		return nil
	case <-ctx.Done():
		return ErrDrainTimeout
	}
}
3 files · go Explain with highlit

This snippet shows how a Go worker pool drains in-flight jobs before the process exits, so that a SIGTERM from an orchestrator like Kubernetes doesn't kill work that is halfway done. The core idea is to separate two concerns: stopping the intake of new work, and waiting for already-started work to finish. Confusing these two is the most common cause of dropped jobs during a deploy.

In pool.go, the Pool owns a buffered jobs channel, a sync.WaitGroup that tracks running workers, and a context.Context that is cancelled on shutdown. Each worker in worker loops on two things: a case reading from jobs and a case on p.ctx.Done(). Crucially, when the context is cancelled the worker does not bail immediately — it keeps pulling from jobs until the channel is drained and closed, using the classic for job := range fallthrough after cancellation. This guarantees that anything already buffered still runs.

Submit respects backpressure and refuses new work once draining begins: it selects on p.ctx.Done() and returns ErrDraining so callers get a clear signal instead of blocking forever on a channel that will never be read. Shutdown performs the ordered dance — it cancels the context to reject new submissions, closes the jobs channel so ranging workers terminate once empty, then wg.Wait()s bounded by the caller's ctx. If that outer context expires first, Shutdown returns ErrDrainTimeout, which lets the caller decide between waiting longer or forcing exit.

main.go wires this to real signals with signal.NotifyContext, giving a SIGINT/SIGTERM-aware context. After the signal fires it builds a separate timeout context for the drain, so the grace period is explicit and independent of when the signal arrived. This mirrors how terminationGracePeriodSeconds works in practice.

The main trade-off is that a slow job can consume the entire grace window; the timeout path exists precisely for that. A subtle pitfall avoided here is closing jobs from a producer while workers still range over it — the code closes exactly once from Shutdown and never sends afterward, which is the only safe ordering. This pattern is what a developer reaches for whenever a task must complete atomically or not at all under deploy churn.


Related snips

Share this code

Here's the card — post it anywhere.

Graceful Drain of In-Flight Jobs Before Worker Shutdown in Go — share card
Link copied