Files
worker/docs/adr/ADR-003-channel-task-queue.md
Rene Nochebuena 631c98396e docs(worker): correct tier from 2 to 3 and fix dependency tier refs
worker depends on launcher (now correctly Tier 2) and logz (Tier 1),
placing it at Tier 3. The previous docs cited launcher as Tier 1 and
logz as Tier 0, both of which were wrong.
2026-03-19 13:13:41 +00:00

2.1 KiB

ADR-003: Channel-Based Buffered Task Queue

Status: Accepted Date: 2026-03-18

Context

A worker pool requires a mechanism to hand off work from callers to goroutines. Common options include a mutex-protected slice, a ring buffer, or a Go channel. The pool must support multiple concurrent producers (callers of Dispatch) and multiple concurrent consumers (worker goroutines), while providing a simple backpressure signal when capacity is exhausted.

Decision

The task queue is a buffered chan Task with capacity Config.BufferSize (env WORKER_BUFFER_SIZE, default 100). All worker goroutines receive from the same channel using for task := range w.taskQueue. Producers call Dispatch which uses a non-blocking select with a default branch:

select {
case w.taskQueue <- task:
    return true
default:
    // queue full — log and return false
    return false
}

Dispatch returns bool: true if the task was enqueued, false if the queue was full. The caller decides what to do with a rejected task (retry, log, discard).

Closing the channel in OnStop is the drain signal: range over a closed channel drains buffered items and then exits naturally, so no separate "stop" message is needed.

Consequences

  • The channel scheduler distributes tasks across all PoolSize goroutines without any additional synchronisation code.
  • Backpressure is explicit: a full queue returns false rather than blocking the caller or growing unboundedly. Callers that must not drop tasks should implement retry logic at their layer.
  • Channel capacity is fixed at construction time. There is no dynamic resizing; if workload consistently fills the buffer, BufferSize or PoolSize must be tuned in config.
  • Closing the channel is a one-way signal: once OnStop closes it, Dispatch must not be called again. This is safe in practice because launcher ensures OnStop is only called after the application has stopped dispatching work, but there is no runtime guard against misuse.
  • The for range pattern requires no sentinel values and is idiomatic Go for fan-out worker pools.