Includes solved 02-worker-pool and 09-safe-counter (mutex, atomic, sharded). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbnJj9ZqEx9nUvzkjdwfMq
55 lines
2.1 KiB
Markdown
55 lines
2.1 KiB
Markdown
# 02 — Worker Pool
|
|
|
|
The fundamental fan-out/fan-in shape. Everything else (pipelines, errgroup,
|
|
batch processors) is a decoration of this.
|
|
|
|
## The channel-closing protocol
|
|
|
|
The whole exercise is really about one question: **who closes what, and when?**
|
|
|
|
Rules worth engraving:
|
|
|
|
1. **Only the sender closes.** A receiver closing a channel = eventual panic on
|
|
the sender side (`send on closed channel`).
|
|
2. **Close is a broadcast.** Every `range` and every `v, ok := <-ch` across all
|
|
receivers unblocks. That's why `close(jobs)` cleanly retires all workers.
|
|
3. **With N senders, no single sender may close.** That's the "closer
|
|
goroutine" pattern you need here:
|
|
|
|
```go
|
|
go func() {
|
|
wg.Wait()
|
|
close(out)
|
|
}()
|
|
```
|
|
|
|
All N workers send to `out`; a dedicated goroutine waits for all of them and
|
|
then closes. This is the only race-free way to close a multi-producer channel.
|
|
|
|
4. **You don't have to close channels at all.** Channels are GC'd like anything
|
|
else. Closing is *signaling*, not resource cleanup. You close `out` here only
|
|
because the consumer ranges over it.
|
|
|
|
## The subtle leak in the naive version
|
|
|
|
If you forget `close(out)`, the consumer's `range` blocks forever — the test
|
|
catches this with a timeout. In prod this presents as goroutine count creeping
|
|
up one per request, which you'd see in `go_goroutines` on your dashboard long
|
|
before OOM. Well... ideally you would.
|
|
|
|
## Why unbuffered `out` is fine (and interesting)
|
|
|
|
With `out` unbuffered, workers block until the consumer reads → natural
|
|
backpressure, memory bounded by `workers`. Buffering `out` decouples throughput
|
|
spikes but the buffer is a queue, and queues hide latency — a very SRE
|
|
trade-off to mention out loud. (Little's law applies to your channels too.)
|
|
|
|
## Interview follow-ups to prep
|
|
|
|
- "Add context cancellation" → every `out <- fn(j)` becomes a `select` with
|
|
`ctx.Done()`, and job consumption too.
|
|
- "A job can fail" → either a Result{V, Err} envelope type, or switch to
|
|
`errgroup` and talk about fail-fast vs collect-all-errors semantics.
|
|
- "Jobs arrive faster than they're processed" → bounded jobs channel =
|
|
backpressure at the producer; discuss shedding vs queueing.
|