chore: merge golang-conventions and prometheus-conventions into sources/
Absorbed content from rodin/golang-conventions and rodin/prometheus-conventions into a sources/ directory. Reference material — descriptive, not prescriptive. Part of taxonomy cleanup (elixir-patterns issue #4).
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
# 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 -->
|
||||
Reference in New Issue
Block a user