From 640fcf71514d6c8d718e1f4205fe9f8d6d1e9592 Mon Sep 17 00:00:00 2001 From: Rodin Date: Thu, 30 Apr 2026 11:45:32 -0700 Subject: [PATCH] docs: initial conventions from cockroachdb/cockroach Key patterns: stopper handle, errors.Wrap, TODO(owner), 116 4-file packages, echotest golden files --- conventions.md | 179 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 conventions.md diff --git a/conventions.md b/conventions.md new file mode 100644 index 0000000..6d822df --- /dev/null +++ b/conventions.md @@ -0,0 +1,179 @@ +# Patterns Extracted from cockroachdb/cockroach + +## Pattern: Stopper for Goroutine Lifecycle + +**Source:** `pkg/util/stop/stopper.go` +**Category:** concurrency + +**What:** A dedicated struct that manages the lifecycle of +all goroutines in a component: tracks active tasks, refuses +new work during shutdown (quiesce), waits for completion, +then runs closers. + +**Why:** In distributed systems, clean shutdown is critical. +You need to: (1) stop accepting new work, (2) finish +in-flight work, (3) release resources in order. The Stopper +centralizes this instead of scattering shutdown logic across +every goroutine. + +**Example:** + +```go +type Stopper struct { + quiescer chan struct{} // closed when quiescing + stopped chan struct{} // closed when fully stopped + mu struct { + syncutil.RWMutex + _numTasks int32 + quiescing, stopping bool + closers []Closer + } +} + +// RunAsyncTask refuses new work during quiesce +func (s *Stopper) RunAsyncTask(ctx context.Context, + taskName string, f func(context.Context)) error { + if !s.addTask() { + return ErrUnavailable + } + go func() { + defer s.decTask() + f(ctx) + }() + return nil +} +``` + +**When to use:** Any server or subsystem that spawns +goroutines and needs graceful shutdown. Especially in +long-running services where leaked goroutines cause +resource exhaustion. + +**When NOT to use:** Simple programs with a single main +goroutine. Or when `errgroup` with context cancellation +suffices for the shutdown coordination. + +--- + +## Pattern: Tracked Lifecycle with Leak Detection + +**Source:** `pkg/util/stop/stopper.go` +**Category:** testing + +**What:** Register every Stopper instance in a global +tracker. In tests, call `PrintLeakedStoppers(t)` to detect +any Stopper that was created but never stopped — indicating +a resource leak. + +**Why:** Distributed systems have complex lifecycle graphs. +A forgot-to-stop bug silently leaks goroutines and +connections. The tracker makes leaks fail-loud in tests +without requiring careful manual cleanup. + +**Example:** + +```go +var trackedStoppers struct { + syncutil.Mutex + stoppers []stopperWithStack +} + +func register(s *Stopper) { + trackedStoppers.Lock() + trackedStoppers.stoppers = append(...) + trackedStoppers.Unlock() +} + +func PrintLeakedStoppers(t testing.TB) { + for _, tracked := range trackedStoppers.stoppers { + t.Errorf("leaked stopper, created at:\n%s", + tracked.createdAt) + } +} +``` + +**When to use:** Any resource that must be explicitly +closed/stopped and where forgetting to do so causes silent +degradation. + +**When NOT to use:** Resources with finalizers or GC-safe +cleanup. Adds global state — only for testing. + +--- + +## Pattern: Quiesce Then Stop (Two-Phase Shutdown) + +**Source:** `pkg/util/stop/stopper.go` +**Category:** concurrency + +**What:** Shutdown has two explicit phases: (1) Quiesce — +refuse new work, wait for in-flight to finish; (2) Stop — +run closers, signal done. Components observe +`ShouldQuiesce` channel alongside context. + +**Why:** One-phase shutdown (just cancel context) loses +in-flight work. Two-phase gives running tasks time to +complete while preventing new work from starting. The +explicit channel (vs just context) lets components +distinguish "winding down" from "dead." + +**Example:** + +```go +func worker(s *Stopper, ctx context.Context) { + for { + select { + case <-s.ShouldQuiesce(): + return // graceful: finish current, exit + case <-ctx.Done(): + return // hard cancel + case work := <-workChan: + process(work) + } + } +} +``` + +**When to use:** Servers handling requests where you want +zero-downtime deploys (drain then stop). Load balancers, +RPC servers, queue consumers. + +**When NOT to use:** Batch jobs or CLIs where immediate +exit is fine. + +--- + +## Pattern: CloserFn Adapter + +**Source:** `pkg/util/stop/stopper.go` +**Category:** concurrency + +**What:** Define a `Closer` interface with one method +(`Close()`), plus a `CloserFn` type that adapts any +function into a Closer. + +**Why:** The adapter pattern (like `http.HandlerFunc`) +avoids forcing users to define a struct just to implement +a one-method interface. Cleanup functions can be registered +directly. + +**Example:** + +```go +type Closer interface { Close() } +type CloserFn func() +func (f CloserFn) Close() { f() } + +// Usage: +stopper.AddCloser(stop.CloserFn(func() { + conn.Close() +})) +``` + +**When to use:** Any one-method interface where callers +often have a simple function they want to register. + +**When NOT to use:** Interfaces with >1 method, or when +the implementation needs state beyond a closure. + +