Cancellation Propagation — Hands-On Tasks¶
Practical exercises to build cancellation muscle memory. Each task includes a starting hint and a verification criterion. Solutions are not provided; the point is to do them yourself.
Task 1: Cancellable producer¶
Write a function produce(ctx context.Context) <-chan int that emits integers 0, 1, 2, ... forever on its returned channel. The producer must exit cleanly when ctx is cancelled.
Verification: a test that cancels after 100 ms and observes that the producer goroutine has exited.
func produce(ctx context.Context) <-chan int {
// your implementation
}
func TestProduce(t *testing.T) {
before := runtime.NumGoroutine()
ctx, cancel := context.WithCancel(context.Background())
out := produce(ctx)
<-out
<-out
cancel()
for range out {
}
after := runtime.NumGoroutine()
if after > before {
t.Errorf("goroutine leaked: %d -> %d", before, after)
}
}
Task 2: Pipeline of three stages¶
Build a pipeline:
- Stage 1: produce numbers 0..99.
- Stage 2: keep only odds.
- Stage 3: square each.
Make every stage cancellable via a shared context. Run the pipeline with a 50 ms deadline; verify it exits cleanly even if not all numbers are processed.
Verification: the pipeline returns within 100 ms; no goroutines leak.
Task 3: Cancel on first error¶
Use errgroup to run 5 tasks in parallel. One of the tasks returns an error after 100 ms; the others run for 500 ms.
Verify that:
- The total time is ~100 ms (not 500 ms).
- All 5 tasks have returned by the time
g.Wait()returns. - The returned error is the one from the first failing task.
Task 4: Cancellable HTTP handler¶
Write an HTTP handler that:
- Reads a
delayquery parameter. - Sleeps for that duration (in a cancellable way).
- Returns "OK" if the delay completes, "cancelled" if the client disconnected.
Verification: a test that cancels the request after 50 ms when the delay is 1 second; expects the handler to return promptly.
Task 5: Fan-out with bounded concurrency¶
Process a slice of 100 items in parallel, with at most 10 running concurrently. If any item errors, cancel the rest. Use errgroup.SetLimit.
Verification: timing test that the total work scales with total / limit, not with total.
Task 6: Worker pool with graceful shutdown¶
Implement:
type Pool struct { /* ... */ }
func NewPool(ctx context.Context, workers int) *Pool
func (p *Pool) Submit(ctx context.Context, job Job) error
func (p *Pool) Drain() // wait for in-flight jobs
func (p *Pool) Stop() // cancel immediately
Submitreturns immediately on success; returns error if pool is cancelled or caller's context cancels.Drainwaits for in-flight jobs to finish.Stopcancels everything and waits.
Verification: leak test using goleak.
Task 7: Deadline-aware retry¶
Implement a retry helper:
func WithRetry(ctx context.Context, fn func(context.Context) error, attempts int, base time.Duration) error
- Retries
fnup toattemptstimes. - Exponential backoff starting at
base, doubling each time. - Backoff is cancellable via
ctx. - Returns immediately if
ctxcancels.
Verification: a test that cancels mid-backoff; the function returns within milliseconds.
Task 8: Cancellable I/O wrapper¶
Write cancellableCopy(ctx context.Context, dst io.Writer, src io.Reader) error that:
- Copies from src to dst.
- Returns
ctx.Err()if the context cancels mid-copy. - For sources that do not support
SetReadDeadline, the copy may not abort immediately, but the function must return.
Hint: use a watcher goroutine that closes the source on cancel.
Task 9: Fan-in merger¶
Implement merge(ctx context.Context, ins ...<-chan int) <-chan int:
- Forwards values from all
insto the returned channel. - Closes the returned channel when all inputs close.
- Exits if
ctxcancels.
Verification: provide 3 input channels each producing 10 values; verify all 30 are emitted (in some order).
Task 10: Cancellable database query¶
Using database/sql (with any driver that supports QueryContext):
- Open a connection.
- Issue a query that takes a long time.
- Cancel the context.
- Verify the query is aborted promptly.
If you do not have a real DB, simulate with a slow driver.
Task 11: Long-poll endpoint¶
Build an HTTP handler:
- Long-polls for events from an
eventschannel. - Returns the first event, or 204 No Content if 30 seconds elapse.
- Exits if the client disconnects.
Verification: tests for all three cases (event arrives, timeout, disconnect).
Task 12: Supervisor with restart¶
Implement a supervisor that runs fn(ctx) and restarts it on error, with exponential backoff up to a cap. Exits cleanly when ctx cancels.
Verification: a test where fn always fails; verify that after ctx cancels, the supervisor exits and the failure count is bounded.
Task 13: Per-tenant context isolation¶
Implement a multi-tenant context manager:
GetTenantContext(tenant string) (context.Context, context.CancelFunc).- Each tenant has its own context derived from the app root.
- Cancelling one tenant does not affect others.
- App shutdown cancels all tenants.
Task 14: Streaming aggregator¶
Build a pipeline:
- Source emits events with
TimestampandValuefields. - Aggregator windows events into 1-second buckets, summing the values.
- Sink prints each window's sum.
Make the whole pipeline cancellable; on cancel, flush the current window if non-empty.
Task 15: HTTP server with graceful shutdown¶
Build a complete HTTP server:
- Uses
signal.NotifyContextfor SIGTERM/SIGINT. BaseContextpropagates the root context to every request.- On signal, calls
Shutdownwith a 30-second deadline. - All handlers respect
r.Context().
Verification: send SIGTERM during a handler with a 10-second delay; verify shutdown completes within ~10 seconds (waiting for the handler).
Task 16: Hierarchical cancellation¶
Build a structure where:
- A root context has children A and B.
- A has grandchildren A1 and A2.
- B has grandchild B1.
Verify:
- Cancelling A1 does not affect A2, B, or B1.
- Cancelling A affects A1 and A2 but not B.
- Cancelling root affects everyone.
Use context.WithCancel and observe <-ctx.Done() in each goroutine.
Task 17: First-result race¶
Query 3 backends in parallel; return the first successful result; cancel the others.
func firstResult(ctx context.Context, queries []func(context.Context) (Result, error)) (Result, error)
Verification: timing test that the total time is bounded by the fastest query.
Task 18: Cancellation observability¶
Add metrics to a pipeline:
- Counter of pipelines completed normally.
- Counter of pipelines cancelled.
- Histogram of cancellation latency (cancel-to-fully-stopped).
Use expvar or any metric library.
Task 19: Chaos test¶
Run a pipeline 1000 times with:
- Random duration before cancel (0-100 ms).
- Random concurrency.
- Random input sizes.
After all runs, verify runtime.NumGoroutine() is at baseline.
Task 20: Implement your own errgroup-lite¶
Without using golang.org/x/sync/errgroup, implement:
Groupstruct withGo(f func() error)andWait() error.- Returns the first error.
- Cancels a shared context on first error.
Verification: works the same as the real errgroup for basic cases.
Task 31: Batch processor with cancellation¶
Implement a batch processor:
- Collects items into batches of 100 or every 1 second, whichever comes first.
- Flushes the batch.
- On cancellation, flushes any partial batch before exiting.
Use a timer plus a buffer; respect context.
Task 32: Cancellable scheduler¶
Implement a job scheduler:
Schedule(at time.Time, fn func(context.Context))registers a job.- A goroutine wakes at each scheduled time and runs the job.
- Cancelling the scheduler cancels all pending jobs.
Use a heap or sorted slice for ordering.
Task 33: Cancellation tracing¶
Add a wrapper that traces context cancellations:
type tracedCtx struct {
context.Context
name string
}
func WithTrace(parent context.Context, name string) context.Context
When the context cancels, log the name and cause. Useful for debugging "which context cancelled?"
Task 34: Pipeline with progress reporting¶
Combine a pipeline with progress events:
- Pipeline processes N items.
- Emits progress events every 100 items.
- Cancellable; on cancel, emits a final "cancelled at K" event.
Task 35: Cancellation-safe finite state machine¶
Build a finite state machine:
- States: Init, Running, Draining, Stopped.
- Transitions: Init -> Running, Running -> Draining, Draining -> Stopped.
- Each transition validates and updates atomically.
- Cancellation triggers Drain.
Verify with a multi-goroutine test that the state never enters an invalid state.
Stretch tasks¶
For extra practice:
- Stretch A: Implement a cancellable
sync.Cond-like primitive. - Stretch B: Build a pub-sub broker with per-subscriber cancellation.
- Stretch C: Implement a connection pool with cancellable acquire.
- Stretch D: Build a rate limiter with cancellable wait.
- Stretch E: Implement a circuit breaker that does not count cancellations as failures.
Task 21: Cancellable buffered pipeline¶
Build a pipeline where stages communicate via buffered channels (capacity 10). Make sure that cancellation can interrupt:
- A producer waiting on a full buffer.
- A consumer waiting on an empty buffer.
- A mid-flight item.
Verify by injecting delays into producer/consumer and confirming cancellation works.
Task 22: Per-call deadlines in a chain¶
Implement a 3-call chain where each call has its own deadline:
- Top-level deadline: 1 second.
- Call A: 300 ms max.
- Call B: 400 ms max.
- Call C: 200 ms max.
Each call should fail with context.DeadlineExceeded if its budget is exceeded; the chain should fail with the same error.
Use context.WithTimeout at each step.
Task 23: Reload configuration without restart¶
Implement a hot-reload mechanism:
- A
Servicestruct with aRun(ctx context.Context)method. - A
Reload(newConfig Config)method that cancels the current internal context and starts a fresh one with the new config.
Verify that:
- Existing work completes during reload (graceful).
- New work uses the new config.
- No goroutines leak across reloads.
Task 24: Cancellation cascade benchmark¶
Write a benchmark that measures cancellation latency for various fan-out sizes:
- 1, 10, 100, 1000, 10000 goroutines.
- Each waits on
<-ctx.Done(). - Measure time from
cancel()to all goroutines exited.
Plot the results. They should be roughly linear in N.
Task 25: Cancellation under panic¶
Write a pipeline where one stage may panic. Implement:
- A
recoverat each goroutine boundary. - On panic, log and cancel the shared context.
- Other stages see the cancel and exit.
Verify the pipeline does not crash the test process.
Task 26: Two-source cancellation¶
A pipeline takes input from two sources. Either source closing should not cancel the pipeline (other source may still have data); but explicit cancel of the shared context should.
Test:
- Both sources close: pipeline drains and exits.
- Cancel: pipeline exits immediately.
Task 27: Cancellation observability dashboard¶
Extend Task 18 with:
- Per-stage cancellation latency.
- Cancellation reason (Canceled, DeadlineExceeded, custom cause).
- In-flight pipeline gauge.
Expose via /metrics endpoint compatible with Prometheus.
Task 28: Cancellation in connection pool¶
Build a connection pool:
Get(ctx context.Context) (*Conn, error): gets a connection, blocking if pool is full. Cancellable.Put(*Conn): returns a connection.Close(): cancels all in-flight Gets and prevents new ones.
Verify:
- Get respects context cancellation.
- Close cancels waiting Gets.
- No goroutines leak after Close.
Task 29: SSE handler with cancellation¶
Server-Sent Events handler:
- Streams events from an internal channel.
- Each event is formatted as
data: ...\n\n. - Client disconnect cancels the handler.
- A ping every 30 seconds keeps the connection alive.
Verify by testing with a client that disconnects mid-stream.
Task 30: Cancellable parallel map¶
Implement a parallel map:
func ParallelMap[T, U any](ctx context.Context, in []T, fn func(context.Context, T) (U, error)) ([]U, error)
- Processes inputs in parallel (bounded concurrency).
- Cancels remaining on first error.
- Returns the results slice and the error.
Use errgroup and a result slice with index-based assignment.
Tips¶
- Always test with
go test -race. - Use
goleakfor leak detection. - Verify both fast paths (no cancel) and cancel paths.
- Test edge cases: cancel before start, cancel mid-work, cancel after completion.
Solutions: write yourself, test thoroughly, get the muscle memory.