Files
Rodin 65a433d0c6 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).
2026-05-07 18:02:04 -07:00

5.2 KiB

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:

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:

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:

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:

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:

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.