chore: remove project conventions from sources/
These have been promoted to standalone repos: - rodin/cockroachdb-conventions - rodin/prometheus-conventions - rodin/temporal-conventions
This commit is contained in:
@@ -1,179 +0,0 @@
|
|||||||
# 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 -->
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
# Patterns Extracted from prometheus/prometheus
|
|
||||||
|
|
||||||
## Pattern: Atomic File Operations with Suffix Convention
|
|
||||||
|
|
||||||
**Source:** `tsdb/db.go`
|
|
||||||
**Category:** storage
|
|
||||||
|
|
||||||
**What:** Use directory suffixes (`.tmp-for-creation`,
|
|
||||||
`.tmp-for-deletion`) to make multi-step file operations
|
|
||||||
crash-safe. On startup, clean up any dirs with these
|
|
||||||
suffixes (they represent incomplete operations).
|
|
||||||
|
|
||||||
**Why:** Database storage needs atomicity. If the process
|
|
||||||
crashes between creating a block and finalizing it, you
|
|
||||||
need to know the block is incomplete. The suffix convention
|
|
||||||
makes incomplete state visible at the filesystem level
|
|
||||||
without requiring a separate journal.
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
```go
|
|
||||||
const (
|
|
||||||
tmpForDeletionBlockDirSuffix = ".tmp-for-deletion"
|
|
||||||
tmpForCreationBlockDirSuffix = ".tmp-for-creation"
|
|
||||||
)
|
|
||||||
|
|
||||||
// On startup: remove any .tmp-* dirs (incomplete ops)
|
|
||||||
// On create: write to dir.tmp-for-creation, then rename
|
|
||||||
// On delete: rename to dir.tmp-for-deletion, then remove
|
|
||||||
```
|
|
||||||
|
|
||||||
**When to use:** Any system that manages files/directories
|
|
||||||
and needs crash consistency without a full WAL. Simpler
|
|
||||||
than a write-ahead log for coarse-grained operations.
|
|
||||||
|
|
||||||
**When NOT to use:** When you already have a WAL or
|
|
||||||
transaction log. Or for fine-grained operations where
|
|
||||||
rename semantics are insufficient.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pattern: DefaultOptions() Function
|
|
||||||
|
|
||||||
**Source:** `tsdb/db.go`
|
|
||||||
**Category:** configuration
|
|
||||||
|
|
||||||
**What:** Provide a `DefaultOptions()` function returning a
|
|
||||||
fully-populated config struct. Users copy and override only
|
|
||||||
what they need. No nil-means-default ambiguity.
|
|
||||||
|
|
||||||
**Why:** Large config structs (20+ fields) are unwieldy.
|
|
||||||
By providing sane defaults as a function (not a
|
|
||||||
package-level var), you avoid mutation bugs and make it
|
|
||||||
clear what "normal" looks like. Users only specify
|
|
||||||
deviations.
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
```go
|
|
||||||
func DefaultOptions() *Options {
|
|
||||||
return &Options{
|
|
||||||
WALSegmentSize: wlog.DefaultSegmentSize,
|
|
||||||
RetentionDuration: int64(15*24*time.Hour / ...),
|
|
||||||
MinBlockDuration: DefaultBlockDuration,
|
|
||||||
MaxBlockDuration: DefaultBlockDuration,
|
|
||||||
SamplesPerChunk: DefaultSamplesPerChunk,
|
|
||||||
// ... 20 more fields with sane defaults
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage:
|
|
||||||
opts := tsdb.DefaultOptions()
|
|
||||||
opts.RetentionDuration = 30 * 24 * time.Hour
|
|
||||||
db, err := tsdb.Open(dir, nil, nil, opts, nil)
|
|
||||||
```
|
|
||||||
|
|
||||||
**When to use:** Config structs with many fields where most
|
|
||||||
users want defaults. Especially when zero-value semantics
|
|
||||||
would be confusing (e.g., 0 retention = infinite? or off?).
|
|
||||||
|
|
||||||
**When NOT to use:** Small configs (3-4 fields) where
|
|
||||||
struct literal with zero-means-default is clear enough.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pattern: Scrape Loop with Aligned Timestamps
|
|
||||||
|
|
||||||
**Source:** `scrape/scrape.go`
|
|
||||||
**Category:** concurrency
|
|
||||||
|
|
||||||
**What:** Periodic scrape loops that align timestamps to
|
|
||||||
intervals with a small tolerance, enabling better storage
|
|
||||||
compression downstream.
|
|
||||||
|
|
||||||
**Why:** Time-series databases compress better when
|
|
||||||
timestamps are regular. A 2ms tolerance on alignment
|
|
||||||
means scraped data aligns to the expected grid while
|
|
||||||
accommodating real-world jitter.
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
```go
|
|
||||||
var ScrapeTimestampTolerance = 2 * time.Millisecond
|
|
||||||
var AlignScrapeTimestamps = true
|
|
||||||
|
|
||||||
// In scrape loop: if scrape finishes within tolerance
|
|
||||||
// of expected timestamp, snap to the grid
|
|
||||||
```
|
|
||||||
|
|
||||||
**When to use:** Any periodic data collection where
|
|
||||||
downstream storage benefits from timestamp regularity.
|
|
||||||
Metrics, heartbeats, polling loops.
|
|
||||||
|
|
||||||
**When NOT to use:** Event-driven data where timestamps
|
|
||||||
must reflect actual occurrence time. Audit logs, user
|
|
||||||
actions, financial transactions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pattern: Sentinel Errors with Interface Check
|
|
||||||
|
|
||||||
**Source:** `tsdb/db.go`
|
|
||||||
**Category:** error-handling
|
|
||||||
|
|
||||||
**What:** Define package-level sentinel errors with
|
|
||||||
`errors.New()` and use compile-time interface assertions
|
|
||||||
to verify implementations satisfy storage interfaces.
|
|
||||||
|
|
||||||
**Why:** `ErrNotReady` as a sentinel lets callers use
|
|
||||||
`errors.Is` for retry logic. The pattern ensures error
|
|
||||||
identity is stable across versions (not string-matched).
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
```go
|
|
||||||
var ErrNotReady = errors.New("TSDB not ready")
|
|
||||||
|
|
||||||
// Callers can reliably detect this:
|
|
||||||
if errors.Is(err, tsdb.ErrNotReady) {
|
|
||||||
// Retry later — DB is still initializing
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**When to use:** Any error that callers need to handle
|
|
||||||
programmatically (retry, fallback, special UI). Make it a
|
|
||||||
named sentinel, not a string comparison.
|
|
||||||
|
|
||||||
**When NOT to use:** Errors that are always terminal or
|
|
||||||
always logged-and-discarded. Not every error needs a name.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pattern: Compile-Time Interface Satisfaction
|
|
||||||
|
|
||||||
**Source:** `scrape/scrape.go`
|
|
||||||
**Category:** organization
|
|
||||||
|
|
||||||
**What:** Use `var _ Interface = (*Type)(nil)` to verify at
|
|
||||||
compile time that a type satisfies an interface, even if
|
|
||||||
the type is only used dynamically.
|
|
||||||
|
|
||||||
**Why:** Without this, you discover missing methods only
|
|
||||||
when the type is actually used — which might be in a
|
|
||||||
rarely-exercised code path or only in production. The
|
|
||||||
compile-time check catches it immediately.
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
```go
|
|
||||||
var _ FailureLogger = (*logging.JSONFileLogger)(nil)
|
|
||||||
// Fails at compile time if JSONFileLogger doesn't
|
|
||||||
// implement FailureLogger
|
|
||||||
```
|
|
||||||
|
|
||||||
**When to use:** Any type that implements an interface
|
|
||||||
consumed dynamically (registered in a map, stored as
|
|
||||||
interface value, passed to framework code).
|
|
||||||
|
|
||||||
**When NOT to use:** Types whose interface satisfaction is
|
|
||||||
already enforced by direct usage in the same package.
|
|
||||||
|
|
||||||
<!-- PATTERN_COMPLETE -->
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
# Patterns from Temporal (temporalio/temporal)
|
|
||||||
|
|
||||||
Source: github.com/temporalio/temporal
|
|
||||||
Analyzed: 2026-04-30
|
|
||||||
Commits: 8,958 | Contributors: 290
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Effect Buffer (Transactional Side Effects)
|
|
||||||
|
|
||||||
**Location:** `common/effect/buffer.go`
|
|
||||||
|
|
||||||
Buffer side effects during a transaction, apply after
|
|
||||||
commit, rollback (in reverse order) after failure.
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Buffer struct {
|
|
||||||
effects []func(context.Context)
|
|
||||||
cancels []func(context.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Buffer) OnAfterCommit(effect func(ctx)) {
|
|
||||||
b.effects = append(b.effects, effect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Buffer) OnAfterRollback(effect func(ctx)) {
|
|
||||||
b.cancels = append(b.cancels, effect)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Buffer) Apply(ctx context.Context) bool {
|
|
||||||
b.cancels = nil
|
|
||||||
for _, effect := range b.effects {
|
|
||||||
effect(ctx) // FIFO order
|
|
||||||
}
|
|
||||||
b.effects = nil
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Buffer) Cancel(ctx context.Context) bool {
|
|
||||||
b.effects = nil
|
|
||||||
for i := len(b.cancels) - 1; i >= 0; i-- {
|
|
||||||
b.cancels[i](ctx) // LIFO (reverse) order
|
|
||||||
}
|
|
||||||
b.cancels = nil
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**When to use:** Any operation that has side effects
|
|
||||||
(notifications, cache invalidation, external calls)
|
|
||||||
that should only execute if the primary transaction
|
|
||||||
succeeds.
|
|
||||||
|
|
||||||
**When NOT to use:** Simple operations where failure
|
|
||||||
leaves no cleanup needed. Over-engineering for
|
|
||||||
functions with a single side effect.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Soft Assertions (Log, Don't Crash)
|
|
||||||
|
|
||||||
**Location:** `common/softassert/softassert.go`
|
|
||||||
|
|
||||||
Assert invariants in production code without panicking.
|
|
||||||
Log the violation so tests catch it but production
|
|
||||||
continues serving.
|
|
||||||
|
|
||||||
```go
|
|
||||||
func That(logger log.Logger, condition bool,
|
|
||||||
staticMessage string, tags ...tag.Tag) bool {
|
|
||||||
if !condition {
|
|
||||||
logger.Error("failed assertion: "+staticMessage,
|
|
||||||
append([]tag.Tag{tag.FailedAssertion},
|
|
||||||
tags...)...)
|
|
||||||
}
|
|
||||||
return condition
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Usage at call sites:
|
|
||||||
```go
|
|
||||||
if !softassert.That(logger, obj.State == "ready",
|
|
||||||
"object not ready before dispatch") {
|
|
||||||
return // or take recovery action
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**The philosophy (from PR #7411):** "Why not panic?
|
|
||||||
Maybe in the future. For now, we're happy with finding
|
|
||||||
these failed assertions in functional tests."
|
|
||||||
|
|
||||||
**When to use:** Invariants that should always hold
|
|
||||||
but where crashing production is worse than logging.
|
|
||||||
Distributed systems where one node's invariant
|
|
||||||
violation shouldn't cascade.
|
|
||||||
|
|
||||||
**When NOT to use:** Safety-critical paths where
|
|
||||||
corruption would be worse than downtime. Better to
|
|
||||||
crash than serve corrupt data.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Type-Safe State Transitions
|
|
||||||
|
|
||||||
**Location:** `service/history/hsm/sm.go`
|
|
||||||
|
|
||||||
State machines with compile-time source state
|
|
||||||
validation and typed events.
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Transition[S comparable, SM StateMachine[S], E any] struct {
|
|
||||||
Sources []S
|
|
||||||
Destination S
|
|
||||||
apply func(SM, E) (TransitionOutput, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t Transition[S, SM, E]) Apply(sm SM, event E) (TransitionOutput, error) {
|
|
||||||
if !slices.Contains(t.Sources, sm.State()) {
|
|
||||||
return TransitionOutput{},
|
|
||||||
fmt.Errorf("%w from %v: %v",
|
|
||||||
ErrInvalidTransition, sm.State(), event)
|
|
||||||
}
|
|
||||||
sm.SetState(t.Destination)
|
|
||||||
return t.apply(sm, event)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**When to use:** Any system with defined state
|
|
||||||
lifecycles — workflow engines, order processing,
|
|
||||||
connection state, protocol implementations.
|
|
||||||
|
|
||||||
**When NOT to use:** Simple boolean flags, systems
|
|
||||||
where state changes are free-form or user-defined.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Mutable vs Immutable Context (Type-Level Access Control)
|
|
||||||
|
|
||||||
**Location:** `chasm/context.go`
|
|
||||||
|
|
||||||
Separate `Context` (read-only) from `MutableContext`
|
|
||||||
(read-write) at the type level. Functions that only
|
|
||||||
read take `Context`; functions that mutate take
|
|
||||||
`MutableContext`.
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Read-only operations
|
|
||||||
func (h *Handler) Check(ctx chasm.Context, c Component) (bool, error)
|
|
||||||
|
|
||||||
// Mutation operations
|
|
||||||
func (h *Handler) Execute(ctx chasm.MutableContext, c Component) error
|
|
||||||
```
|
|
||||||
|
|
||||||
**From PR discussion (Sushisource):** "I think I prefer
|
|
||||||
them separate, because what happens if you mutate
|
|
||||||
something and then say 'not ready'? That would be some
|
|
||||||
weird violation that shouldn't be possible, and separate
|
|
||||||
contexts enforces that at the type level."
|
|
||||||
|
|
||||||
**When to use:** Any system where read vs write access
|
|
||||||
matters — databases, caches, state machines, APIs with
|
|
||||||
both query and mutation.
|
|
||||||
|
|
||||||
**When NOT to use:** Simple CRUD where every operation
|
|
||||||
is both a read and a write.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Goroutine Handle (Safe Lifecycle)
|
|
||||||
|
|
||||||
**Location:** `common/goro/goro.go`
|
|
||||||
|
|
||||||
A handle to a goroutine that supports safe multi-stop,
|
|
||||||
context cancellation, and error collection.
|
|
||||||
|
|
||||||
```go
|
|
||||||
h := goro.NewHandle(ctx)
|
|
||||||
h.Go(func(ctx context.Context) error {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case task := <-ch:
|
|
||||||
process(task)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Safe to call multiple times:
|
|
||||||
h.Cancel()
|
|
||||||
err := h.Wait()
|
|
||||||
```
|
|
||||||
|
|
||||||
**Born from:** PR #1892, fixing a double-close panic in
|
|
||||||
the task writer. The old code used a raw `chan struct{}`
|
|
||||||
for shutdown, which panics if closed twice.
|
|
||||||
|
|
||||||
**When to use:** Any long-lived goroutine that needs
|
|
||||||
clean shutdown, error reporting, or may be stopped from
|
|
||||||
multiple places.
|
|
||||||
|
|
||||||
**When NOT to use:** Fire-and-forget goroutines, or
|
|
||||||
goroutines managed by higher-level frameworks
|
|
||||||
(errgroup, worker pools).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Dynamic Config with Type-Safe Generics
|
|
||||||
|
|
||||||
**Location:** `common/dynamicconfig/`
|
|
||||||
|
|
||||||
566 settings, each declared as a typed constant with
|
|
||||||
default value, description, and precedence resolution.
|
|
||||||
|
|
||||||
```go
|
|
||||||
var WorkflowMaxTimeout = NewNamespaceDurationSetting(
|
|
||||||
"limit.maxWorkflowTimeout",
|
|
||||||
24*365*time.Hour,
|
|
||||||
`Maximum timeout for a workflow execution.`,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Consumer side:
|
|
||||||
timeout := dc.GetDurationProperty(
|
|
||||||
dynamicconfig.WorkflowMaxTimeout,
|
|
||||||
namespace,
|
|
||||||
)()
|
|
||||||
```
|
|
||||||
|
|
||||||
Resolution order: task queue → namespace → global.
|
|
||||||
Uses `weak.Pointer` cache for GC-friendly memoization.
|
|
||||||
|
|
||||||
**When to use:** Large systems with many operational
|
|
||||||
knobs that need to change without restart. Multi-tenant
|
|
||||||
systems where different tenants need different limits.
|
|
||||||
|
|
||||||
**When NOT to use:** Simple services with <10 config
|
|
||||||
values. Static configuration is simpler and more
|
|
||||||
predictable.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Composable Predicates (Filter Algebra)
|
|
||||||
|
|
||||||
**Location:** `common/predicates/`
|
|
||||||
|
|
||||||
Generic predicate interface with And/Or/Not composition
|
|
||||||
and automatic flattening.
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Predicate[T any] interface {
|
|
||||||
Test(T) bool
|
|
||||||
Equals(Predicate[T]) bool
|
|
||||||
Size() int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage:
|
|
||||||
filter := predicates.And(
|
|
||||||
predicates.Not(isExpired),
|
|
||||||
predicates.Or(isHighPriority, isRetryable),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Flattening: `And(And(a, b), c)` → `And(a, b, c)`.
|
|
||||||
Short-circuit: `And(Empty, anything)` → `Empty`.
|
|
||||||
|
|
||||||
**When to use:** Task queues, query builders, access
|
|
||||||
control rules — anywhere filters compose.
|
|
||||||
|
|
||||||
**When NOT to use:** Single predicate checks, simple
|
|
||||||
if-statements.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Persistence Plugin Registration (init pattern)
|
|
||||||
|
|
||||||
**Location:** `common/persistence/sql/`
|
|
||||||
|
|
||||||
```go
|
|
||||||
func init() {
|
|
||||||
sql.RegisterPlugin("postgres12", &plugin{
|
|
||||||
driver: &driver.PQDriver{},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Import-driven registration. The main binary imports the
|
|
||||||
plugins it wants; each plugin's init() registers it.
|
|
||||||
|
|
||||||
**When to use:** Database drivers, encoding formats,
|
|
||||||
protocol implementations — anything with a fixed set
|
|
||||||
of implementations selected at compile time.
|
|
||||||
|
|
||||||
**When NOT to use:** When you need runtime plugin
|
|
||||||
discovery (use a factory pattern instead). When the
|
|
||||||
number of implementations is small and stable (just
|
|
||||||
use a switch).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. ShutdownOnce (CAS-Based Safe Close)
|
|
||||||
|
|
||||||
**Location:** `common/channel/shutdown_once.go`
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (c *ShutdownOnceImpl) Shutdown() {
|
|
||||||
if atomic.CompareAndSwapInt32(
|
|
||||||
&c.status, statusOpen, statusClosed) {
|
|
||||||
close(c.channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Wraps the "close of closed channel" panic with a CAS
|
|
||||||
guard. Safe to call from multiple goroutines.
|
|
||||||
|
|
||||||
**When to use:** Any shared shutdown signal where
|
|
||||||
multiple goroutines might initiate shutdown. Replaces
|
|
||||||
`sync.Once` when you also need a channel for select.
|
|
||||||
|
|
||||||
**When NOT to use:** When only one goroutine ever
|
|
||||||
triggers shutdown. When `context.WithCancel` suffices.
|
|
||||||
|
|
||||||
<!-- PATTERN_COMPLETE -->
|
|
||||||
Reference in New Issue
Block a user