docs: initial conventions from cockroachdb/cockroach
Key patterns: stopper handle, errors.Wrap, TODO(owner), 116 4-file packages, echotest golden files
This commit is contained in:
+179
@@ -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.
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
Reference in New Issue
Block a user