Skip to main content
Version: 0.2

Worker Pool

A Pool runs a fixed number of worker goroutines that take jobs from a bounded queue. NewPool starts the workers immediately:

pool, err := worker.NewPool(worker.WithWorkers(8), worker.WithQueueSize(500))
if err != nil {
log.Fatal(err) // only invalid configuration fails
}
defer pool.Close()

The queue is the pool's backpressure. Once it is full, new submissions either wait for space or are rejected — you choose which by how you submit.

Jobs​

A job is any func(ctx context.Context) error, or any type implementing Job:

type Job interface {
Process(ctx context.Context) error
}

Use a function for one-off work. Use the interface when a job carries state, or when you want hooks and panic handlers to be able to tell jobs apart:

type ResizeImage struct {
ID string
Width int
}

func (j *ResizeImage) Process(ctx context.Context) error {
return resize(ctx, j.ID, j.Width)
}

pool.SubmitJob(&ResizeImage{ID: "img-42", Width: 800})

JobFunc adapts a function to Job when an API wants the interface: worker.JobFunc(fn).

Ways to submit​

MethodWhen the queue is fullReturnsJob's context
Submit(fn) / SubmitJob(job)waits for spaceonce enqueuedindependent
TrySubmit(fn) / TrySubmitJob(job)returns ErrPoolFullimmediatelyindependent
SubmitContext(ctx, job)waits for space or ctxonce enqueuedderived from ctx
SubmitWait(ctx, fn) / SubmitJobWait(ctx, job)waits for space or ctxwhen the job finishes, with its errorderived from ctx

Every method returns ErrPoolClosed once the pool is closing, including a submit that was already waiting for space.

Fire and forget​

Submit returns as soon as the job is queued. Its error only tells you whether it was queued; the job's own result goes to hooks and metrics:

if err := pool.Submit(sendWelcomeEmail); err != nil {
// ErrPoolClosed — the pool is shutting down
}

Load shedding​

TrySubmit never blocks. Use it where waiting is worse than dropping — an HTTP handler that would rather return 503 than hang:

switch err := pool.TrySubmit(job); {
case errors.Is(err, worker.ErrPoolFull):
http.Error(w, "busy, try again", http.StatusServiceUnavailable)
return
case err != nil: // ErrPoolClosed
http.Error(w, "shutting down", http.StatusServiceUnavailable)
return
}

Bounded waiting​

SubmitContext waits for space only as long as ctx allows. The job then runs with a context derived from ctx, so cancelling ctx after the job is queued also cancels the job. Pass a context whose lifetime matches the job, not one that ends as soon as the submit returns.

Waiting for the result​

SubmitWait queues the job and blocks until it has finished — after all retries — and returns its final error:

err := pool.SubmitWait(r.Context(), func(ctx context.Context) error {
return chargeCard(ctx, order)
})

If ctx ends first, SubmitWait returns ctx.Err() and the job's context is cancelled too, so the job can stop early.

Sizing​

Workers bound how many jobs run at once. For CPU-bound jobs, runtime.GOMAXPROCS(0) is a sensible start. For I/O-bound jobs, size by what the downstream system tolerates — the number of connections your database pool allows, or the request concurrency an API permits.

Queue size bounds how much work can pile up. A larger queue absorbs bigger spikes but holds more work in memory, all of which is lost if the process dies. A queue size of 0 is allowed: every submit then hands the job directly to an idle worker, or waits for one.

Inspecting the pool​

pool.QueueLength() // jobs waiting in the queue
pool.ActiveWorkers() // workers running a job right now
pool.IsPaused()
pool.IsClosed()
pool.Snapshot() // all counters, see Metrics

Thread safety​

Every Pool method is safe for concurrent use.