Compare commits
17 Commits
7b38ac9b2a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e75a97707d | |||
| df9c856d96 | |||
| b833d05410 | |||
| 6400d626dc | |||
| d4c919f8df | |||
| b4e3cf2824 | |||
| 74101b513c | |||
| f595b91030 | |||
| 40f024b477 | |||
| e989536bfb | |||
| d3adeaac25 | |||
| edef02ed0f | |||
| 8e77a5e321 | |||
| 10218813d3 | |||
| b33accf37c | |||
| 49a006dd18 | |||
| d28b9c8844 |
@@ -1,15 +1,58 @@
|
||||
# Elixir Patterns
|
||||
|
||||
Idiomatic Elixir patterns extracted from the [Elixir source code](https://github.com/elixir-lang/elixir) with verified file:line citations.
|
||||
**Prescriptive.** Follow these when writing Elixir code.
|
||||
|
||||
A pattern is a reusable solution to a recurring problem. Each one has:
|
||||
- **When to use** — the problem it solves
|
||||
- **When NOT to use** — where it causes harm
|
||||
- **Why** — the reasoning, not just the rule
|
||||
- **Source citations** — verified file:line from real codebases
|
||||
|
||||
These are derived from what mature Elixir codebases *actually do*, not opinions or blog posts.
|
||||
|
||||
## Structure
|
||||
|
||||
- `patterns/` — Core patterns (GenServer, error handling, data transforms, processes, testing, docs, typespecs, macros, behaviours, modules)
|
||||
- `smells/` — Anti-patterns and common mistakes the Elixir team avoids
|
||||
- `changelog/` — Daily digest of merged Elixir PRs with discussion summaries
|
||||
- `patterns/` — what to do (behaviours, GenServer, error handling, testing, typespecs, etc.)
|
||||
- `smells/` — what NOT to do (anti-patterns, common mistakes)
|
||||
- `sources/` — reference material from specific projects (Oban, elixir-lang). Study for ideas, don't copy blindly.
|
||||
|
||||
## Philosophy
|
||||
## How to use
|
||||
|
||||
These rules are derived from what the Elixir source code *actually does*, not opinions or blog posts. Every pattern cites specific files and line numbers.
|
||||
Give your agent these instructions depending on the task:
|
||||
|
||||
When unsure how to do something in Elixir, look at how Elixir core does it. This is how we define what "idiomatic" actually means.
|
||||
### Solving a problem
|
||||
|
||||
> You have access to a patterns repo containing proven solutions to recurring Elixir problems. When I describe a problem:
|
||||
>
|
||||
> 1. Identify which pattern files are relevant (read them)
|
||||
> 2. Check if my problem matches a "When to use" case
|
||||
> 3. Check if it matches a "When NOT to use" case
|
||||
> 4. If a pattern fits: suggest the approach, cite the pattern, explain why it applies here
|
||||
> 5. If no pattern fits: say so, and suggest an approach grounded in the principles you see across the patterns
|
||||
> 6. If my problem matches a smell: warn me before I make the mistake
|
||||
>
|
||||
> Never suggest something that contradicts a documented pattern without explicitly calling out the deviation and justifying it.
|
||||
|
||||
### Reviewing code
|
||||
|
||||
> You have access to a patterns repo that defines how Elixir code should be written. For each file in the diff:
|
||||
>
|
||||
> 1. Read the relevant pattern files
|
||||
> 2. Verify the code follows the documented patterns
|
||||
> 3. If it deviates: flag it with a reference to the specific pattern, section, and why it matters
|
||||
> 4. If it matches a smell: flag it as a known anti-pattern
|
||||
> 5. A deviation without justification is a finding
|
||||
>
|
||||
> Don't invent rules. Only flag what the patterns document.
|
||||
|
||||
### Evaluating a pattern
|
||||
|
||||
> Read the pattern file. Compare against how the following projects handle the same problem: [list projects]. Does the pattern hold? Are there cases where it breaks down? Should it be updated, split, or retired? File your findings as an issue.
|
||||
|
||||
## Patterns vs Conventions
|
||||
|
||||
**Pattern** = prescriptive. "When you face X, do Y." Language-scoped. Follow these.
|
||||
|
||||
**Convention** = descriptive. "Project Z does it this way." Context-specific. Study for ideas — applying another project's conventions to yours without understanding their constraints causes harm.
|
||||
|
||||
The `sources/` directory is convention material absorbed from thin repos. The `patterns/` directory is what you actually follow.
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
How behaviours are designed, implemented, and used in Elixir core and Phoenix.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Behaviour Definition with `@callback`](#1-behaviour-definition-with-callback)
|
||||
2. [`@optional_callbacks` for Extensibility](#2-optional_callbacks-for-extensibility)
|
||||
3. [`@behaviour` Declaration in `__using__`](#3-behaviour-declaration-in-__using__)
|
||||
4. [Default Implementations via `defoverridable`](#4-default-implementations-via-defoverridable)
|
||||
5. [Phoenix Channel: Behaviour + Process + Protocol](#5-phoenix-channel-behaviour--process--protocol)
|
||||
6. [Callback Documentation Pattern](#6-callback-documentation-pattern)
|
||||
7. [Phoenix.Endpoint: Behaviour as Interface Contract](#7-phoenixendpoint-behaviour-as-interface-contract)
|
||||
|
||||
---
|
||||
|
||||
## 1. Behaviour Definition with `@callback`
|
||||
|
||||
**Source:** [lib/elixir/lib/gen_server.ex#L577](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L577) (all callback definitions)
|
||||
@@ -678,4 +690,15 @@ end
|
||||
|
||||
**Why:** The more code a `use` macro generates, the harder it is to debug. If users regularly need to read the generated code to understand failures, the abstraction is leaking. Reserve heavy `use` macros for well-established patterns (GenServer, Endpoint, Channel) where the community has internalized the mental model.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
- If you need a contract that multiple modules will implement differently → define a behaviour with `@callback` (Pattern 1)
|
||||
- If most implementors will use a default for some callbacks → mark those `@optional_callbacks` (Pattern 2)
|
||||
- If your behaviour requires boilerplate setup (module attributes, compile hooks) → inject `@behaviour` inside `__using__` (Pattern 3)
|
||||
- If 90% of implementors want the same default for a callback → provide a `defoverridable` implementation (Pattern 4)
|
||||
- If the behaviour involves a running process with lifecycle configuration → combine behaviour + process + module attributes (Pattern 5)
|
||||
- If callback semantics are non-obvious (multiple return shapes, triggering conditions) → write comprehensive `@doc` with examples on each `@callback` (Pattern 6)
|
||||
- If the behaviour requires significant generated boilerplate (plugs, routing, supervision wiring) → use the `use` macro as the full interface contract (Pattern 7)
|
||||
- If there is only one implementation and no plans for more → skip the behaviour, use a plain module
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
+20
-8
@@ -2,6 +2,19 @@
|
||||
|
||||
Patterns extracted from Ecto's source code for building safe, composable data pipelines.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [`cast/4` — The External/Internal Data Boundary](#1-cast4--the-externalinternal-data-boundary)
|
||||
2. [`change/2` — Internal-Only Modifications](#2-change2--internal-only-modifications)
|
||||
3. [Validation Pipeline — Composable Validators](#3-validation-pipeline--composable-validators)
|
||||
4. [`validate_change/3` — Custom Validators](#4-validate_change3--custom-validators)
|
||||
5. [`add_error/4` — Manual Error Injection](#5-add_error4--manual-error-injection)
|
||||
6. [`put_change/3` vs `force_change/3` — Tracked vs Forced Changes](#6-put_change3-vs-force_change3--tracked-vs-forced-changes)
|
||||
7. [Constraints vs Validations — DB-level Safety](#7-constraints-vs-validations--db-level-safety)
|
||||
8. [`prepare_changes/2` — Last-Mile DB-Aware Transforms](#8-prepare_changes2--last-mile-db-aware-transforms)
|
||||
9. [`apply_action/2` — Schemaless Validation](#9-apply_action2--schemaless-validation)
|
||||
10. [`cast_assoc/3` vs `put_assoc/4` — External vs Internal Association Changes](#10-cast_assoc3-vs-put_assoc4--external-vs-internal-association-changes)
|
||||
|
||||
---
|
||||
|
||||
## 1. `cast/4` — The External/Internal Data Boundary
|
||||
@@ -315,17 +328,16 @@ end
|
||||
**Anti-pattern:** Reimplementing the "has change?" check inside the validator:
|
||||
|
||||
```elixir
|
||||
# BAD — redundant check; validate_change already handles this
|
||||
# BAD — redundant check; validate_change already skips if :sku is unchanged
|
||||
def changeset(item, params) do
|
||||
item
|
||||
|> cast(params, [:sku])
|
||||
|> validate_change(:sku, fn :sku, value ->
|
||||
if Map.has_key?(item.__meta__.changes, :sku) do
|
||||
if valid_sku?(value), do: [], else: [{:sku, "invalid format"}]
|
||||
changeset = cast(item, params, [:sku])
|
||||
|
||||
if Map.has_key?(changeset.changes, :sku) do
|
||||
value = get_change(changeset, :sku)
|
||||
if valid_sku?(value), do: changeset, else: add_error(changeset, :sku, "invalid format")
|
||||
else
|
||||
[]
|
||||
changeset
|
||||
end
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
Patterns extracted from Elixir's standard library source code.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [List-Specialized Clause Before Protocol Dispatch](#1-list-specialized-clause-before-protocol-dispatch)
|
||||
2. [Build-Then-Reverse (Cons-Cell Accumulation)](#2-build-then-reverse-cons-cell-accumulation)
|
||||
3. [Pipeline for Linear Transformations, Bare Calls for Control Flow](#3-pipeline-for-linear-transformations-bare-calls-for-control-flow)
|
||||
4. [Pipeline Ending with `|> elem(1)` (Protocol Reduce Unwrap)](#4-pipeline-ending-with--elem1-protocol-reduce-unwrap)
|
||||
5. [Private Helper Decomposition: Recursive Workers with Guards](#5-private-helper-decomposition-recursive-workers-with-guards)
|
||||
6. [Enum vs Stream Decision Pattern](#6-enum-vs-stream-decision-pattern)
|
||||
7. [Map.update vs Map.put Decision Pattern](#7-mapupdate-vs-mapput-decision-pattern)
|
||||
8. [Pattern Matching on Map Structure for Dispatch](#8-pattern-matching-on-map-structure-for-dispatch)
|
||||
9. [Delegating to Erlang BIFs with `defdelegate`](#9-delegating-to-erlang-bifs-with-defdelegate)
|
||||
10. [Reduce as the Universal Primitive](#10-reduce-as-the-universal-primitive)
|
||||
11. [Keyword Multi-Clause Guard Dispatch (String.split pattern)](#11-keyword-multi-clause-guard-dispatch-stringsplit-pattern)
|
||||
12. [Lazy Private Helpers with `defp parts_to_index`](#12-lazy-private-helpers-with-defp-parts_to_index)
|
||||
|
||||
---
|
||||
|
||||
## 1. List-Specialized Clause Before Protocol Dispatch
|
||||
@@ -1010,4 +1025,17 @@ def log(msg) when is_atom(msg), do: IO.puts(Atom.to_string(msg))
|
||||
|
||||
**Why:** When a conversion is used exactly once and the calling function already dispatches on clauses, folding the conversion into the caller's clauses reduces indirection. Named helpers shine when reused or when they name a non-obvious transformation.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
- If you accept "any enumerable" but lists are the common case → add a `when is_list` clause before protocol dispatch (Pattern 1)
|
||||
- If you are building a result list element-by-element and order matters → prepend with `[x | acc]` then reverse at the end (Pattern 2)
|
||||
- If data flows through 2+ sequential transformations → use the pipe operator (Pattern 3)
|
||||
- If you call `Enumerable.reduce/3` directly and always want the accumulated value → unwrap with `|> elem(1)` (Pattern 4)
|
||||
- If you need a recursive function with multiple termination conditions → decompose into public entry + private multi-clause worker (Pattern 5)
|
||||
- If the collection is large/infinite or you chain 3+ transforms → use Stream; otherwise use Enum (Pattern 6)
|
||||
- If the new value depends on the old value (increment, append) → use `Map.update/4`; if replacing unconditionally → use `Map.put/3` (Pattern 7)
|
||||
- If you need to branch on whether a key exists and extract the value → pattern-match with `%{^key => value}` in a `case` (Pattern 8)
|
||||
- If an Erlang function has identical semantics and argument order → use `defdelegate` (Pattern 9)
|
||||
- If you are implementing a custom iterable data structure → implement `Enumerable.reduce/3` to get the full Enum API (Pattern 10)
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
|
||||
Patterns extracted from the Elixir standard library source code.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [@moduledoc with Structured Sections](#1-moduledoc-with-structured-sections)
|
||||
2. [@doc with Sections and Examples](#2-doc-with-sections-and-examples)
|
||||
3. [@doc since: Version Annotation](#3-doc-since-version-annotation)
|
||||
4. [@doc guard: true Metadata](#4-doc-guard-true-metadata)
|
||||
5. [@doc false — Hiding from Documentation](#5-doc-false--hiding-from-documentation)
|
||||
6. [@moduledoc false — Hiding Modules](#6-moduledoc-false--hiding-modules)
|
||||
7. [Mermaid Diagrams in Documentation](#7-mermaid-diagrams-in-documentation)
|
||||
8. [Admonition Blocks in Documentation](#8-admonition-blocks-in-documentation)
|
||||
9. [@doc deprecated: Soft Deprecation](#9-doc-deprecated-soft-deprecation)
|
||||
10. [Callback Documentation Convention](#10-callback-documentation-convention)
|
||||
11. [Documentation with Link References (c: and t: prefixes)](#11-documentation-with-link-references-c-and-t-prefixes)
|
||||
|
||||
---
|
||||
|
||||
## 1. @moduledoc with Structured Sections
|
||||
@@ -201,7 +215,7 @@ def chunk(list, size), do: ...
|
||||
**Don't use this when:**
|
||||
- The function is private (use `# comments` for private function notes)
|
||||
- The function name + typespec are completely self-explanatory (e.g., `@spec pid() :: pid()`)
|
||||
- You're implementing a behaviour callback and want it hidden (`@impl true` sets `@doc false` automatically)
|
||||
- You're implementing a behaviour callback — `@impl true` sets `@doc false` automatically. Override only when the implementation has semantics the behaviour can't speak to (see [Pattern 10 — implementation-side docs](#when-to-override-doc-false-on-impl-functions))
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
@@ -442,6 +456,7 @@ def __struct__(fields), do: ...
|
||||
- The function IS part of the public API (even if you think it's "obvious")
|
||||
- You want to discourage use but still document it (use `@doc deprecated:` instead)
|
||||
- You're hiding functions because you're too lazy to document them
|
||||
- The `@impl` function has implementation-specific semantics that callers need to know (see [Pattern 10 — implementation-side docs](#when-to-override-doc-false-on-impl-functions))
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
@@ -467,6 +482,8 @@ end
|
||||
|
||||
**Why:** `@doc false` means "this function is not part of the public API." If users are expected to call it, it needs documentation. Hiding public API behind `@doc false` is a maintenance hazard — users will call undocumented functions and break on upgrades.
|
||||
|
||||
> **Note:** `@impl true` auto-sets `@doc false`, but some implementations earn their own `@doc`. See [Pattern 10 — implementation-side docs](#when-to-override-doc-false-on-impl-functions) for the rule.
|
||||
|
||||
---
|
||||
|
||||
## 6. @moduledoc false — Hiding Modules
|
||||
@@ -927,6 +944,67 @@ Called to format the value.
|
||||
|
||||
**Why:** A callback with one parameter and one return type doesn't need a full reference manual. Match documentation depth to complexity — a one-liner with good naming is better than padded sections that add no information.
|
||||
|
||||
### When to override `@doc false` on `@impl` functions
|
||||
|
||||
`@impl true` sets `@doc false` by default — the assumption is that the behaviour's `@callback` doc covers it. This is correct when the implementation is unremarkable. Override it with an explicit `@doc` when the implementation has semantics that would not be true of every conforming implementation.
|
||||
|
||||
**The test:** "Would this statement be true of *any* implementation of this callback?" If yes → the doc belongs on the `@callback` in the behaviour, not here. If no → the implementation earns its own `@doc`.
|
||||
|
||||
**Override when:**
|
||||
- The implementation makes a guarantee the behaviour doesn't promise (e.g., "always returns immediately", "never buffers", "fires at most once")
|
||||
- There's a race condition, ordering constraint, or subtle failure mode specific to this implementation
|
||||
- The implementation's behavior under edge cases differs from what the behaviour's generic contract implies
|
||||
|
||||
**Don't override when:**
|
||||
- The doc would just restate the `@callback` doc in different words
|
||||
- The doc describes what the function does rather than what's surprising about this implementation
|
||||
- The function name + behaviour doc are sufficient for an implementor or caller to understand it
|
||||
|
||||
**Example — unnecessary override (just restates the contract):**
|
||||
```elixir
|
||||
defmodule JsonSerializer do
|
||||
@behaviour Serializer
|
||||
|
||||
@doc """
|
||||
Encodes the given term to a binary.
|
||||
"""
|
||||
@impl true
|
||||
def encode(term), do: Jason.encode!(term)
|
||||
end
|
||||
```
|
||||
|
||||
**Example — justified override (implementation-specific guarantee):**
|
||||
```elixir
|
||||
defmodule Immediate do
|
||||
@behaviour Aggregation
|
||||
|
||||
@doc """
|
||||
Always returns `{:ready, [signal]}` — immediate mode fires on first signal
|
||||
without buffering or waiting for a timer.
|
||||
"""
|
||||
@impl true
|
||||
def check(%Signal{} = signal), do: {:ready, [signal]}
|
||||
end
|
||||
```
|
||||
|
||||
**Example — justified override (race condition warning):**
|
||||
```elixir
|
||||
defmodule AlpacaAdapter do
|
||||
@behaviour BrokerAdapter
|
||||
|
||||
@doc """
|
||||
Cancels an open order by broker ID. Returns `:ok` on success.
|
||||
|
||||
The order may still receive a final fill between the cancel request
|
||||
and confirmation — callers must handle the `partially_filled` → `cancelled` race.
|
||||
"""
|
||||
@impl true
|
||||
def cancel(credential, broker_order_id), do: ...
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The behaviour's `@callback` doc is the generic contract — "what any implementation must do." An implementation's `@doc` is the specific contract — "what callers of *this* implementation can rely on." When those differ meaningfully, silence (`@doc false`) hides information that would prevent bugs.
|
||||
|
||||
---
|
||||
|
||||
## 11. Documentation with Link References (c: and t: prefixes)
|
||||
@@ -1000,4 +1078,17 @@ Returns `true` if the calling process is the owner of this resource.
|
||||
|
||||
**Why:** Link references should aid navigation, not turn documentation into hypertext soup. Link types and callbacks that users might need to look up; don't link primitive types or universally known functions.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
- If the module is a primary entry point with 4+ public functions → use structured `@moduledoc` with sections (Pattern 1)
|
||||
- If a function has non-obvious behavior or edge cases → add `@doc` with sections and `## Examples` doctests (Pattern 2)
|
||||
- If adding a new public function to a versioned library → annotate with `@doc since: "X.Y.Z"` (Pattern 3)
|
||||
- If the function/macro is valid in guard clauses → add `@doc guard: true` metadata (Pattern 4)
|
||||
- If a function must be public for technical reasons but is not user-facing → use `@doc false` (Pattern 5)
|
||||
- If an entire module is purely internal implementation → use `@moduledoc false` (Pattern 6)
|
||||
- If documenting multi-component architecture (client-server, pipelines) → embed a Mermaid diagram (Pattern 7)
|
||||
- If critical information must stand out (security, breaking changes, `use` behavior) → use an admonition block (Pattern 8)
|
||||
- If a function still works but a better alternative exists → use `@doc deprecated:` for soft deprecation (Pattern 9)
|
||||
- If defining a behaviour callback with multiple return shapes → write comprehensive callback docs with trigger, params, returns, and example (Pattern 10)
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
@@ -2,6 +2,23 @@
|
||||
|
||||
Patterns extracted from Elixir's standard library source code.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [The `with` Macro — Normalized Error Clauses](#1-the-with-macro--normalized-error-clauses)
|
||||
2. [Real-World `with` — Multi-Step Fallible Operations](#2-real-world-with--multi-step-fallible-operations)
|
||||
3. [Another `with` — Error Info Extraction](#3-another-with--error-info-extraction)
|
||||
4. [`{:ok, value}` / `:error` Convention (Map.fetch)](#4-ok-value--error-convention-mapfetch)
|
||||
5. [Bang Functions: Raise on Error (`fetch!` vs `fetch`)](#5-bang-functions-raise-on-error-fetch-vs-fetch)
|
||||
6. [Exception Structure: `defexception` Fields](#6-exception-structure-defexception-fields)
|
||||
7. [Custom `exception/1` Callback for Ergonomic Raising](#7-custom-exception1-callback-for-ergonomic-raising)
|
||||
8. [`raise` Macro Internals: Compile-Time Type Resolution](#8-raise-macro-internals-compile-time-type-resolution)
|
||||
9. [Error Normalization: Erlang → Elixir Exception Translation](#9-error-normalization-erlang--elixir-exception-translation)
|
||||
10. [`blame/2` Callback: Enriching Exceptions After the Fact](#10-blame2-callback-enriching-exceptions-after-the-fact)
|
||||
11. [Guards for Type Dispatch in Error Handling](#11-guards-for-type-dispatch-in-error-handling)
|
||||
12. [The `:error` / `{:error, reason}` Convention Split](#12-the-error--error-reason-convention-split)
|
||||
13. [`reduce_while` — Early Exit Without Exceptions](#13-reduce_while--early-exit-without-exceptions)
|
||||
14. [Three-Tier Error Strategy in Map Operations](#14-three-tier-error-strategy-in-map-operations)
|
||||
|
||||
---
|
||||
|
||||
## 1. The `with` Macro — Normalized Error Clauses
|
||||
@@ -1400,4 +1417,19 @@ end
|
||||
|
||||
**Why:** The three-tier pattern only makes sense when failure is a real possibility and different callers genuinely need different responses to that failure. Don't cargo-cult it onto functions that always succeed or have a single calling context.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
- If you have 2+ sequential steps that each return a value to pattern-match → use `with` with normalized error shapes (Pattern 1)
|
||||
- If the caller only cares success vs failure (not which step failed) → use `with` + `else _ -> :error` catch-all (Pattern 2)
|
||||
- If extracting nested data from loosely-structured inputs (stacktraces, metadata) → chain pattern matching in `with` (Pattern 3)
|
||||
- If a function has exactly one failure mode obvious from context → return bare `:error` (Pattern 4)
|
||||
- If failure means a bug (preconditions guarantee success) → provide a bang variant that raises (Pattern 5)
|
||||
- If callers need to programmatically inspect error context → use `defexception` with structured fields (Pattern 6)
|
||||
- If the exception message is computed from multiple fields or requires validation → override `exception/1` (Pattern 7)
|
||||
- If wrapping an Erlang library that returns raw error atoms/tuples → normalize to Elixir exceptions at the boundary (Pattern 9)
|
||||
- If you can provide expensive but helpful context (did-you-mean suggestions) → implement `blame/2` (Pattern 10)
|
||||
- If multiple distinct failure modes exist → use `{:error, reason}` tuples; if only one → use bare `:error` (Pattern 12)
|
||||
- If you need early exit from iteration without exceptions → use `reduce_while` with `{:cont, acc}` / `{:halt, acc}` (Pattern 13)
|
||||
- If designing a module with lookup operations for different caller needs → provide three tiers: get/fetch/fetch! (Pattern 14)
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
+96
-1
@@ -2,6 +2,21 @@
|
||||
|
||||
Analysis of `lib/elixir/lib/gen_server.ex`, `lib/elixir/lib/agent.ex`, and related modules.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Pattern 1: Client/Server API Separation](#pattern-1-clientserver-api-separation)
|
||||
2. [Pattern 2: `@impl true` Annotations on All Callbacks](#pattern-2-impl-true-annotations-on-all-callbacks)
|
||||
3. [Pattern 3: Guard-Protected `start_link`](#pattern-3-guard-protected-start_link)
|
||||
4. [Pattern 4: `handle_continue` for Post-Init Work](#pattern-4-handle_continue-for-post-init-work)
|
||||
5. [Pattern 5: Timeout-Based Idle Shutdown](#pattern-5-timeout-based-idle-shutdown)
|
||||
6. [Pattern 6: Periodic Work via `Process.send_after`](#pattern-6-periodic-work-via-processsend_after)
|
||||
7. [Pattern 7: Call vs Cast Decision (Synchronous vs Asynchronous)](#pattern-7-call-vs-cast-decision-synchronous-vs-asynchronous)
|
||||
8. [Pattern 8: Default Callback Implementations with Clear Error Messages](#pattern-8-default-callback-implementations-with-clear-error-messages)
|
||||
9. [Pattern 9: `child_spec/1` Generation and Customization via `use` Options](#pattern-9-child_spec1-generation-and-customization-via-use-options)
|
||||
10. [Pattern 10: Agent as Minimal State Wrapper (GenServer Under the Hood)](#pattern-10-agent-as-minimal-state-wrapper-genserver-under-the-hood)
|
||||
11. [Pattern 11: Name Registration via `:via` Tuple](#pattern-11-name-registration-via-via-tuple)
|
||||
12. [Pattern 12: GenServer as Anti-Pattern — Don't Use Processes for Code Organization](#pattern-12-genserver-as-anti-pattern--dont-use-processes-for-code-organization)
|
||||
|
||||
---
|
||||
|
||||
## Pattern 1: Client/Server API Separation
|
||||
@@ -189,6 +204,72 @@ end
|
||||
|
||||
**Why:** `@impl true` only makes sense in the context of a declared behaviour. Using it without one causes a compiler warning, not a benefit.
|
||||
|
||||
### Minimal Callback Annotation
|
||||
|
||||
**Source:** [lib/elixir/lib/module.ex#L72](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/module.ex#L72) (`@impl` documentation)
|
||||
|
||||
**What it does:** Once `@impl true` is on a callback, two things become redundant:
|
||||
|
||||
1. **Repeating `@impl true` on subsequent clauses** — the annotation applies to the function as a whole, not individual clauses. All clauses inherit it from the first.
|
||||
2. **Adding `@spec`** — the behaviour's `@callback` already defines the type contract. Dialyzer uses the callback spec to check implementations, so a redundant `@spec` creates a second source of truth that can drift.
|
||||
|
||||
**Why:** The behaviour owns the contract. Adding `@spec init(term()) :: {:ok, map()}` on a GenServer callback just restates `@callback init(init_arg :: term) :: {:ok, state} | ...` with less information. Repeating `@impl true` on every clause is noise that misleads readers into thinking each clause is a separate function. The minimal annotation communicates "this is a callback, the behaviour defines the contract."
|
||||
|
||||
**Anti-pattern:**
|
||||
|
||||
```elixir
|
||||
# BAD — redundant @spec and @impl on every clause
|
||||
@spec handle_call(term(), GenServer.from(), map()) :: {:reply, term(), map()}
|
||||
@impl true
|
||||
def handle_call({:get, key}, _from, state) do
|
||||
{:reply, Map.get(state, key), state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:keys}, _from, state) do
|
||||
{:reply, Map.keys(state), state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:size}, _from, state) do
|
||||
{:reply, map_size(state), state}
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
|
||||
```elixir
|
||||
@impl true
|
||||
def handle_call({:get, key}, _from, state) do
|
||||
{:reply, Map.get(state, key), state}
|
||||
end
|
||||
|
||||
def handle_call({:keys}, _from, state) do
|
||||
{:reply, Map.keys(state), state}
|
||||
end
|
||||
|
||||
def handle_call({:size}, _from, state) do
|
||||
{:reply, map_size(state), state}
|
||||
end
|
||||
```
|
||||
|
||||
**When NOT to apply this:**
|
||||
|
||||
- **Non-consecutive clauses:** If clauses of the same callback are separated by other functions, the compiler may not associate them. Keep multi-clause callbacks grouped together.
|
||||
- **Intentionally narrower types:** When the implementation accepts a narrower type than the callback declares, a more specific `@spec` documents that constraint:
|
||||
|
||||
```elixir
|
||||
# Justified — documents that this init/1 only accepts keyword lists,
|
||||
# not arbitrary term() as the callback permits
|
||||
@spec init(keyword()) :: {:ok, Config.t()}
|
||||
@impl true
|
||||
def init(opts) when is_list(opts) do
|
||||
{:ok, Config.new!(opts)}
|
||||
end
|
||||
```
|
||||
|
||||
If the `@spec` would be identical to (or wider than) the `@callback`, omit it. If it's meaningfully narrower, keep it.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3: Guard-Protected `start_link`
|
||||
@@ -208,7 +289,7 @@ def start_link(default) when is_binary(default) do
|
||||
GenServer.start_link(__MODULE__, default)
|
||||
end
|
||||
|
||||
# From agent.ex:246
|
||||
# From agent.ex:279
|
||||
@spec start_link((-> term), GenServer.options()) :: on_start
|
||||
def start_link(fun, options \\ []) when is_function(fun, 0) do
|
||||
GenServer.start_link(Agent.Server, fun, options)
|
||||
@@ -1109,4 +1190,18 @@ end
|
||||
|
||||
**Why:** The pattern cuts both ways. Over-using GenServer creates bottlenecks. Under-using it means reinventing state management poorly. The litmus test: does the state need to survive between function calls? Does access need serialization? If yes, you need a process.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
- If other modules will interact with your GenServer → define a client API wrapping call/cast (Pattern 1)
|
||||
- If implementing any behaviour callback → annotate with `@impl true` (Pattern 2)
|
||||
- If `start_link` accepts arguments with a specific expected shape → add guards for fail-fast validation (Pattern 3)
|
||||
- If `init/1` does expensive work (DB, network, cache warming) → split into fast init + `handle_continue` (Pattern 4)
|
||||
- If the process is ephemeral (per-user, per-session) and should clean up when idle → use timeout-based idle shutdown (Pattern 5)
|
||||
- If you need work at regular intervals regardless of message traffic → use `Process.send_after` self-scheduling loop (Pattern 6)
|
||||
- If the caller needs confirmation or backpressure → use `call`; only use `cast` for genuine fire-and-forget (Pattern 7)
|
||||
- If the process needs non-default restart/shutdown behavior → customize via `use GenServer` options (Pattern 9)
|
||||
- If the process is purely about state (no custom messages, no timers) → use Agent instead of GenServer (Pattern 10)
|
||||
- If spawning processes dynamically with unbounded names → use `{:via, Registry, ...}` to avoid atom leaks (Pattern 11)
|
||||
- If the operation is stateless pure computation → don't use a GenServer at all, use a plain function (Pattern 12)
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
Patterns extracted from the Elixir standard library source code.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Context-Aware Macros (__CALLER__.context)](#1-context-aware-macros-__caller__context)
|
||||
2. [defguard — Macro for Guard-Safe Expressions](#2-defguard--macro-for-guard-safe-expressions)
|
||||
3. [quote + unquote for Code Generation](#3-quote--unquote-for-code-generation)
|
||||
4. [var! for Breaking Hygiene](#4-var-for-breaking-hygiene)
|
||||
5. [Macro Expanding with Macro.expand](#5-macro-expanding-with-macroexpand)
|
||||
6. [assert_no_match_or_guard_scope Pattern](#6-assert_no_match_or_guard_scope-pattern)
|
||||
7. [Protocol Definition as a Macro (defprotocol)](#7-protocol-definition-as-a-macro-defprotocol)
|
||||
8. [@fallback_to_any in Protocols](#8-fallback_to_any-in-protocols)
|
||||
9. [use/2 as Macro Injection Point](#9-use2-as-macro-injection-point)
|
||||
10. [Sigil Macros (Pattern for DSL Literals)](#10-sigil-macros-pattern-for-dsl-literals)
|
||||
11. [Pipe Operator as a Macro](#11-pipe-operator-as-a-macro)
|
||||
12. [Macro.generate_unique_arguments for Hygiene](#12-macrogenerate_unique_arguments-for-hygiene)
|
||||
|
||||
---
|
||||
|
||||
## 1. Context-Aware Macros (__CALLER__.context)
|
||||
@@ -1105,4 +1120,18 @@ end
|
||||
|
||||
**Why:** Variables created in `quote` are already hygienic by default — they can't clash with caller variables. `generate_unique_arguments` is needed when you're generating *multiple* variables dynamically (e.g., function parameters for a generated clause) where you need distinct names that also interoperate correctly.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
- If a macro must behave differently in guards vs normal code → check `__CALLER__.context` (Pattern 1)
|
||||
- If you need a reusable, compile-time-validated guard expression → use `defguard` (Pattern 2)
|
||||
- If a macro argument might have side effects or be expensive → use `quote bind_quoted:` to evaluate once (Pattern 3)
|
||||
- If a macro must reference a variable in the caller's scope → use `var!` sparingly (Pattern 4)
|
||||
- If the macro receives input that could be an alias or module attribute → expand with `Macro.expand` before branching (Pattern 5)
|
||||
- If your macro defines module-level constructs and should never appear in guards → assert context at the top (Pattern 6)
|
||||
- If you need open-ended type dispatch that external code can extend → use `defprotocol` (Pattern 7)
|
||||
- If a protocol should handle any value rather than raising on unknown types → use `@fallback_to_any true` (Pattern 8)
|
||||
- If a module needs injected behaviours, attributes, or compile hooks → use the `use/2` + `__using__/1` pattern (Pattern 9)
|
||||
- If you have compile-time-known literals that benefit from validation → define a sigil macro (Pattern 10)
|
||||
- If you need a zero-cost syntactic transformation (argument rewriting) → implement as a macro like `|>` (Pattern 11)
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
How modules are structured, named, and organized in Elixir core and Phoenix.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [One Module per Concept, Nested for Sub-Concepts](#1-one-module-per-concept-nested-for-sub-concepts)
|
||||
2. [Public API at the Top, Private Functions at the Bottom](#2-public-api-at-the-top-private-functions-at-the-bottom)
|
||||
3. [`@moduledoc false` for Internal Modules](#3-moduledoc-false-for-internal-modules)
|
||||
4. [Struct Definition Conventions](#4-struct-definition-conventions)
|
||||
5. [Selective Imports in `__using__`](#5-selective-imports-in-__using__)
|
||||
6. [Alias at Module Scope for Readability](#6-alias-at-module-scope-for-readability)
|
||||
7. [Boolean-Suffixed Fields in Structs](#7-boolean-suffixed-fields-in-structs)
|
||||
|
||||
---
|
||||
|
||||
## 1. One Module per Concept, Nested for Sub-Concepts
|
||||
|
||||
**Source:** `lib/elixir/lib/` directory structure
|
||||
@@ -590,4 +602,14 @@ defstruct [:user, :admin?, :count]
|
||||
|
||||
**Why:** The `?` suffix should only mark genuine booleans. Using it on non-boolean fields creates confusion about the field's type and breaks the convention's usefulness as a type signal.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
- If your module has grown beyond 300 lines with distinct sub-responsibilities → [One Module per Concept, Nested for Sub-Concepts](#1-one-module-per-concept-nested-for-sub-concepts)
|
||||
- If you need to decide function ordering within a module → [Public API at the Top, Private Functions at the Bottom](#2-public-api-at-the-top-private-functions-at-the-bottom)
|
||||
- If a module exists purely for internal code organization and should not appear in docs → [`@moduledoc false` for Internal Modules](#3-moduledoc-false-for-internal-modules)
|
||||
- If you need to define a struct and decide which fields are mandatory → [Struct Definition Conventions](#4-struct-definition-conventions)
|
||||
- If your `use` macro needs to set up the caller's namespace with specific functions → [Selective Imports in `__using__`](#5-selective-imports-in-__using__)
|
||||
- If multiple modules from the same parent namespace are used repeatedly → [Alias at Module Scope for Readability](#6-alias-at-module-scope-for-readability)
|
||||
- If a struct field stores a boolean value and you want self-documenting naming → [Boolean-Suffixed Fields in Structs](#7-boolean-suffixed-fields-in-structs)
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
+26
-16
@@ -2,6 +2,16 @@
|
||||
|
||||
Patterns extracted from Ecto's `Ecto.Multi` source code.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [`Multi.new() |> Multi.insert/update/delete` — Named Operation Pipeline](#1-multinew--multiinsertupdatedelete--named-operation-pipeline)
|
||||
2. [`Multi.run/3` — Arbitrary Code in a Transaction](#2-multirun3--arbitrary-code-in-a-transaction)
|
||||
3. [Dependent Operations with Function Variants](#3-dependent-operations-with-function-variants)
|
||||
4. [`Multi.merge/2` — Dynamic Transaction Composition](#4-multimerge2--dynamic-transaction-composition)
|
||||
5. [`Multi.append/2` / `Multi.prepend/2` — Static Multi Composition](#5-multiappend2--multiprepend2--static-multi-composition)
|
||||
6. [Tuple Keys — Dynamic Collections of Operations](#6-tuple-keys--dynamic-collections-of-operations)
|
||||
7. [`Multi.to_list/1` — Testing Without a Database](#7-multito_list1--testing-without-a-database)
|
||||
|
||||
---
|
||||
|
||||
## 1. `Multi.new() |> Multi.insert/update/delete` — Named Operation Pipeline
|
||||
@@ -17,18 +27,18 @@ def reset(account, params) do
|
||||
end
|
||||
|
||||
# Execute:
|
||||
case Repo.transact(PasswordManager.reset(account, params)) do
|
||||
case Repo.transaction(PasswordManager.reset(account, params)) do
|
||||
{:ok, %{account: account, log: log}} -> # success
|
||||
{:error, :account, changeset, _} -> # account step failed
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Each operation is named. On success, `Repo.transact` returns `{:ok, results_map}` where each key is the name given to that operation. On failure, it returns `{:error, failed_name, failed_value, changes_so_far}`, making it immediately clear which step aborted the transaction and why. This is more precise than a bare transaction function where you'd have to inspect the return value to guess which step failed.
|
||||
**Why:** Each operation is named. On success, `Repo.transaction` returns `{:ok, results_map}` where each key is the name given to that operation. On failure, it returns `{:error, failed_name, failed_value, changes_so_far}`, making it immediately clear which step aborted the transaction and why. This is more precise than a bare transaction function where you'd have to inspect the return value to guess which step failed.
|
||||
|
||||
**Anti-pattern:** Using an anonymous function with bare `case` statements inside a transaction, where failure attribution is implicit:
|
||||
```elixir
|
||||
# BAD — no way to know which operation failed from the return value alone
|
||||
Repo.transact(fn ->
|
||||
Repo.transaction(fn ->
|
||||
case Repo.update(Account.password_reset_changeset(account, params)) do
|
||||
{:ok, account} ->
|
||||
case Repo.insert(Log.password_reset_changeset(account, params)) do
|
||||
@@ -50,7 +60,7 @@ end)
|
||||
**Example — before:**
|
||||
```elixir
|
||||
def create_user_with_profile(params) do
|
||||
Repo.transact(fn ->
|
||||
Repo.transaction(fn ->
|
||||
case Repo.insert(User.changeset(params)) do
|
||||
{:ok, user} ->
|
||||
case Repo.insert(Profile.changeset(user, params)) do
|
||||
@@ -71,7 +81,7 @@ def create_user_with_profile(params) do
|
||||
|> Multi.insert(:profile, fn %{user: user} ->
|
||||
Profile.changeset(user, params)
|
||||
end)
|
||||
|> Repo.transact()
|
||||
|> Repo.transaction()
|
||||
end
|
||||
|
||||
# Caller:
|
||||
@@ -86,7 +96,7 @@ end
|
||||
|
||||
**Don't use this when:**
|
||||
- You have a single database operation (just call `Repo.insert/update/delete` directly)
|
||||
- Operations are simple and sequential with no branching (a plain `Repo.transact(fn -> ... end)` is more readable)
|
||||
- Operations are simple and sequential with no branching (a plain `Repo.transaction(fn -> ... end)` is more readable)
|
||||
- The overhead of building a Multi struct is not justified by the number of operations
|
||||
|
||||
**Over-application example:**
|
||||
@@ -94,7 +104,7 @@ end
|
||||
# Overkill for a single operation
|
||||
Multi.new()
|
||||
|> Multi.insert(:user, User.changeset(params))
|
||||
|> Repo.transact()
|
||||
|> Repo.transaction()
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
@@ -102,7 +112,7 @@ Multi.new()
|
||||
Repo.insert(User.changeset(params))
|
||||
```
|
||||
|
||||
**Why:** `Ecto.Multi` introduces indirection. For simple cases, calling Repo functions directly or using `Repo.transact(fn -> ... end)` is clearer. The named-pipeline form pays off when 3+ operations are involved and failure attribution matters.
|
||||
**Why:** `Ecto.Multi` introduces indirection. For simple cases, calling Repo functions directly or using `Repo.transaction(fn -> ... end)` is clearer. The named-pipeline form pays off when 3+ operations are involved and failure attribution matters.
|
||||
|
||||
---
|
||||
|
||||
@@ -144,7 +154,7 @@ end)
|
||||
**Example — before:**
|
||||
```elixir
|
||||
def register_user(params) do
|
||||
Repo.transact(fn ->
|
||||
Repo.transaction(fn ->
|
||||
{:ok, user} = Repo.insert(User.changeset(params))
|
||||
# This runs outside Multi — if it fails, user was already inserted
|
||||
case ExternalService.provision(user.id) do
|
||||
@@ -163,7 +173,7 @@ def register_user(params) do
|
||||
|> Multi.run(:provision, fn _repo, %{user: user} ->
|
||||
ExternalService.provision(user.id)
|
||||
end)
|
||||
|> Repo.transact()
|
||||
|> Repo.transaction()
|
||||
end
|
||||
```
|
||||
|
||||
@@ -496,7 +506,7 @@ Enum.reduce(accounts, Multi.new(), fn account, multi ->
|
||||
end)
|
||||
|
||||
# Error pattern-matching:
|
||||
case Repo.transact(multi) do
|
||||
case Repo.transaction(multi) do
|
||||
{:ok, results} -> Map.keys(results) # [{:account, 1}, {:account, 2}, ...]
|
||||
{:error, {:account, id}, changeset, _} -> "account #{id} failed"
|
||||
end
|
||||
@@ -547,7 +557,7 @@ def reset_passwords(accounts, params) do
|
||||
end
|
||||
|
||||
# Caller knows exactly which account failed:
|
||||
case Repo.transact(reset_passwords(accounts, params)) do
|
||||
case Repo.transaction(reset_passwords(accounts, params)) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, {:account, id}, changeset, _} ->
|
||||
Logger.error("Failed to reset account #{id}: #{inspect(changeset.errors)}")
|
||||
@@ -609,7 +619,7 @@ test "password reset changeset is valid" do
|
||||
account = %Account{password: "letmein"}
|
||||
{:ok, %{account: updated}} =
|
||||
PasswordManager.reset(account, valid_params)
|
||||
|> Repo.transact()
|
||||
|> Repo.transaction()
|
||||
|
||||
# The changeset validity was the whole point, not the DB state
|
||||
assert updated.password != account.password
|
||||
@@ -630,7 +640,7 @@ end
|
||||
test "creates user with profile" do
|
||||
{:ok, %{user: user, profile: profile}} =
|
||||
UserRegistration.multi(valid_params())
|
||||
|> Repo.transact()
|
||||
|> Repo.transaction()
|
||||
|
||||
assert user.email == "test@example.com"
|
||||
assert profile.user_id == user.id
|
||||
@@ -685,7 +695,7 @@ test "comment changeset is valid given a post" do
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `Multi.to_list/1` reflects the state of the pipeline at build time. Deferred values (function variants, `Multi.run` callbacks, `Multi.merge` functions) are not evaluated until `Repo.transact` runs them. Test those deferred pieces independently rather than trying to inspect them through `to_list`.
|
||||
**Why:** `Multi.to_list/1` reflects the state of the pipeline at build time. Deferred values (function variants, `Multi.run` callbacks, `Multi.merge` functions) are not evaluated until `Repo.transaction` runs them. Test those deferred pieces independently rather than trying to inspect them through `to_list`.
|
||||
|
||||
---
|
||||
|
||||
@@ -697,6 +707,6 @@ end
|
||||
- If you have reusable Multi fragments to combine → `Multi.append/2` or `Multi.prepend/2`
|
||||
- If you're updating a dynamic collection → tuple keys `{:operation, id}`
|
||||
- If you want to validate changesets without hitting the DB → `Multi.to_list/1` in tests
|
||||
- If operations are simple and static (no dynamic branching) → consider `Repo.transact(fn -> ... end)` instead
|
||||
- If operations are simple and static (no dynamic branching) → consider `Repo.transaction(fn -> ... end)` instead
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
|
||||
Analysis of `lib/elixir/lib/supervisor.ex`, `lib/elixir/lib/dynamic_supervisor.ex`, `lib/elixir/lib/task.ex`, `lib/elixir/lib/task/supervisor.ex`, `lib/elixir/lib/process.ex`, and `lib/elixir/lib/registry.ex`.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Pattern 1: Static vs Dynamic Supervision — Choose the Right Tool](#pattern-1-static-vs-dynamic-supervision--choose-the-right-tool)
|
||||
2. [Pattern 2: PartitionSupervisor for Scalability](#pattern-2-partitionsupervisor-for-scalability)
|
||||
3. [Pattern 3: Supervision Strategies — Choosing the Right Restart Behavior](#pattern-3-supervision-strategies--choosing-the-right-restart-behavior)
|
||||
4. [Pattern 4: Restart Intensity (`max_restarts` / `max_seconds`)](#pattern-4-restart-intensity-max_restarts--max_seconds)
|
||||
5. [Pattern 5: Restart Values — `:permanent` vs `:transient` vs `:temporary`](#pattern-5-restart-values--permanent-vs-transient-vs-temporary)
|
||||
6. [Pattern 6: Automatic Shutdown for Pipeline Supervisors](#pattern-6-automatic-shutdown-for-pipeline-supervisors)
|
||||
7. [Pattern 7: Task.async/await for Concurrent Value Computation](#pattern-7-taskasyncawait-for-concurrent-value-computation)
|
||||
8. [Pattern 8: Task.Supervisor.async_nolink for Fault-Tolerant Task Execution](#pattern-8-tasksupervisorasync_nolink-for-fault-tolerant-task-execution)
|
||||
9. [Pattern 9: Task Supervisor as DynamicSupervisor Specialization](#pattern-9-task-supervisor-as-dynamicsupervisor-specialization)
|
||||
10. [Pattern 10: Registry for Dynamic Process Naming and PubSub](#pattern-10-registry-for-dynamic-process-naming-and-pubsub)
|
||||
11. [Pattern 11: Shutdown Semantics — Graceful Termination](#pattern-11-shutdown-semantics--graceful-termination)
|
||||
12. [Pattern 12: DynamicSupervisor Internal State — Struct with Restart Tracking](#pattern-12-dynamicsupervisor-internal-state--struct-with-restart-tracking)
|
||||
13. [Pattern 13: Restart Logic with Exponential Backoff via `:try_again`](#pattern-13-restart-logic-with-exponential-backoff-via-try_again)
|
||||
14. [Pattern 14: `$ancestors` and `$callers` — Process Lineage Tracking](#pattern-14-ancestors-and-callers--process-lineage-tracking)
|
||||
15. [Pattern 15: GenServer.reply/2 for Deferred Responses](#pattern-15-genserverreply2-for-deferred-responses)
|
||||
16. [Pattern 16: Process.alias for Safe Request/Response](#pattern-16-processalias-for-safe-requestresponse)
|
||||
17. [Pattern 17: Registry Partitioning Strategies](#pattern-17-registry-partitioning-strategies)
|
||||
18. [Pattern 18: `init/1` Return Values — The Full Spectrum](#pattern-18-init1-return-values--the-full-spectrum)
|
||||
|
||||
---
|
||||
|
||||
## Pattern 1: Static vs Dynamic Supervision — Choose the Right Tool
|
||||
@@ -436,7 +457,7 @@ Supervisor.init(children,
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
# Task defaults to :temporary — intentional one-shot work
|
||||
# (from task.ex:282)
|
||||
# (from task.ex:327)
|
||||
def child_spec(arg) do
|
||||
%{
|
||||
id: Task,
|
||||
@@ -1911,4 +1932,25 @@ end
|
||||
|
||||
**Why:** `:ignore` means "this child intentionally should not run right now." `{:stop, reason}` means "this child tried to start and failed." Conflating the two hides real failures from your supervision tree.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
- If you have children known at compile time with ordering dependencies → [Pattern 1: Static vs Dynamic Supervision](#pattern-1-static-vs-dynamic-supervision--choose-the-right-tool)
|
||||
- If a single DynamicSupervisor or Task.Supervisor is a bottleneck under high spawn load → [Pattern 2: PartitionSupervisor for Scalability](#pattern-2-partitionsupervisor-for-scalability)
|
||||
- If you need to decide how a supervisor reacts when children share state or have dependencies → [Pattern 3: Supervision Strategies](#pattern-3-supervision-strategies--choosing-the-right-restart-behavior)
|
||||
- If you want to tune how many restarts are tolerated before escalation → [Pattern 4: Restart Intensity](#pattern-4-restart-intensity-max_restarts--max_seconds)
|
||||
- If different processes have different lifecycle expectations (one-shot vs permanent) → [Pattern 5: Restart Values](#pattern-5-restart-values--permanent-vs-transient-vs-temporary)
|
||||
- If a supervisor should self-terminate when its children finish their work → [Pattern 6: Automatic Shutdown](#pattern-6-automatic-shutdown-for-pipeline-supervisors)
|
||||
- If you need to compute values concurrently and the caller should crash on failure → [Pattern 7: Task.async/await](#pattern-7-taskasyncawait-for-concurrent-value-computation)
|
||||
- If a GenServer needs to spawn work that might fail without taking down the server → [Pattern 8: Task.Supervisor.async_nolink](#pattern-8-tasksupervisorasync_nolink-for-fault-tolerant-task-execution)
|
||||
- If you need supervised tasks with caller tracking, async_nolink, and streaming → [Pattern 9: Task Supervisor](#pattern-9-task-supervisor-as-dynamicsupervisor-specialization)
|
||||
- If you need to look up processes by a dynamic key without atom leaks → [Pattern 10: Registry](#pattern-10-registry-for-dynamic-process-naming-and-pubsub)
|
||||
- If processes hold external resources that need cleanup on shutdown → [Pattern 11: Shutdown Semantics](#pattern-11-shutdown-semantics--graceful-termination)
|
||||
- If you are building a custom supervisor-like process and need efficient child tracking → [Pattern 12: DynamicSupervisor Internal State](#pattern-12-dynamicsupervisor-internal-state--struct-with-restart-tracking)
|
||||
- If a child fails to start due to transient conditions and you want non-blocking retry → [Pattern 13: Restart Logic with Backoff](#pattern-13-restart-logic-with-exponential-backoff-via-try_again)
|
||||
- If you need to trace which process initiated spawned work for debugging → [Pattern 14: Process Lineage Tracking](#pattern-14-ancestors-and-callers--process-lineage-tracking)
|
||||
- If a GenServer needs to do async work before replying to a caller → [Pattern 15: GenServer.reply/2](#pattern-15-genserverreply2-for-deferred-responses)
|
||||
- If you build a custom request/response protocol with timeouts and need to prevent late replies → [Pattern 16: Process.alias](#pattern-16-processalias-for-safe-requestresponse)
|
||||
- If your Registry dispatch is slow because of wrong partitioning strategy → [Pattern 17: Registry Partitioning](#pattern-17-registry-partitioning-strategies)
|
||||
- If you need to communicate "don't start this child" or split init into fast/slow phases → [Pattern 18: init/1 Return Values](#pattern-18-init1-return-values--the-full-spectrum)
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
Patterns extracted from Ecto's query layer source code.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Named Query Functions — Composable Query Building](#1-named-query-functions--composable-query-building)
|
||||
2. [Query Piping — Schema to Query Pipeline](#2-query-piping--schema-to-query-pipeline)
|
||||
3. [Named Bindings — Position-Independent Composition](#3-named-bindings--position-independent-composition)
|
||||
4. [`dynamic/2` — Runtime-Constructed Predicates](#4-dynamic2--runtime-constructed-predicates)
|
||||
5. [`subquery/1` — Correlated Subqueries](#5-subquery1--correlated-subqueries)
|
||||
6. [`exclude/2` — Strip Clauses for Reuse](#6-exclude2--strip-clauses-for-reuse)
|
||||
7. [Bindingless Queries — Data-Driven Clauses](#7-bindingless-queries--data-driven-clauses)
|
||||
8. [`select_merge/3` — Augmenting Selects Dynamically](#8-select_merge3--augmenting-selects-dynamically)
|
||||
9. [`fragment/1` and `type/2` — Escape Hatches for DB-Specific Expressions](#9-fragment1-and-type2--escape-hatches-for-db-specific-expressions)
|
||||
|
||||
---
|
||||
|
||||
## 1. Named Query Functions — Composable Query Building
|
||||
|
||||
+13
-2
@@ -2,6 +2,17 @@
|
||||
|
||||
Patterns extracted from `lib/ecto/schema.ex` in the Ecto source.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Base Schema Module — App-Wide Schema Defaults](#1-base-schema-module--app-wide-schema-defaults)
|
||||
2. [`@primary_key false` — Composite or No Primary Key](#2-primary_key-false--composite-or-no-primary-key)
|
||||
3. [Virtual Fields — In-Memory-Only Data](#3-virtual-fields--in-memory-only-data)
|
||||
4. [`embedded_schema/1` — Schemaless Validation Structs](#4-embedded_schema1--schemaless-validation-structs)
|
||||
5. [`@timestamps_opts` — Consistent Timestamp Types](#5-timestamps_opts--consistent-timestamp-types)
|
||||
6. [Field `:source` Option — Column Name Mapping](#6-field-source-option--column-name-mapping)
|
||||
7. [`redact: true` — Protecting Sensitive Fields](#7-redact-true--protecting-sensitive-fields)
|
||||
8. [`__schema__/1` Reflection — Runtime Schema Introspection](#8-__schema__1-reflection--runtime-schema-introspection)
|
||||
|
||||
---
|
||||
|
||||
## 1. Base Schema Module — App-Wide Schema Defaults
|
||||
@@ -121,8 +132,8 @@ defmodule MyApp.Schema do
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
use Ecto.Schema
|
||||
@primary_key {:id, :integer, autogenerate: true} # Same as the default
|
||||
@foreign_key_type :integer
|
||||
@primary_key {:id, :id, autogenerate: true} # Same as the default (:id resolves to integer)
|
||||
@foreign_key_type :id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# Telemetry Patterns
|
||||
|
||||
## Pattern 1: Dedicated Telemetry Submodule
|
||||
|
||||
**When to use:** Any module that emits 2+ telemetry events, especially GenServers and pipeline stages.
|
||||
|
||||
**What it does:** Extracts all `:telemetry.execute/3` calls into a sibling `Telemetry` module. The parent module calls into the telemetry module; the telemetry module owns event names, measurements shape, and metadata contracts.
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
lib/my_app/quote_feed.ex # GenServer — state + logic
|
||||
lib/my_app/quote_feed/telemetry.ex # Telemetry — event emission + docs
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```elixir
|
||||
defmodule MyApp.QuoteFeed.Telemetry do
|
||||
@moduledoc """
|
||||
Telemetry events for QuoteFeed.
|
||||
|
||||
## Events
|
||||
|
||||
* `[:my_app, :quote_feed, :connected]` — WebSocket connected.
|
||||
Measurements: `%{system_time: integer()}`.
|
||||
Metadata: `%{url: String.t()}`.
|
||||
|
||||
* `[:my_app, :quote_feed, :tick, :received]` — Price tick received.
|
||||
Measurements: `%{latency_ms: integer()}`.
|
||||
Metadata: `%{symbol: String.t(), price: Decimal.t()}`.
|
||||
"""
|
||||
|
||||
@doc "Emit a connection event."
|
||||
def connected(url) do
|
||||
:telemetry.execute(
|
||||
[:my_app, :quote_feed, :connected],
|
||||
%{system_time: System.system_time()},
|
||||
%{url: url}
|
||||
)
|
||||
end
|
||||
|
||||
@doc "Emit a tick received event."
|
||||
def tick_received(symbol, price, latency_ms) do
|
||||
:telemetry.execute(
|
||||
[:my_app, :quote_feed, :tick, :received],
|
||||
%{latency_ms: latency_ms},
|
||||
%{symbol: symbol, price: price}
|
||||
)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Caller side:**
|
||||
```elixir
|
||||
defmodule MyApp.QuoteFeed do
|
||||
alias MyApp.QuoteFeed.Telemetry
|
||||
|
||||
def handle_info({:connected, url}, state) do
|
||||
Telemetry.connected(url)
|
||||
{:noreply, %{state | connected: true}}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why this over inline:**
|
||||
- **Discoverability:** One place lists all events a component emits. Handlers/dashboards have a single source of truth.
|
||||
- **Cohesion:** GenServer focuses on state management. Telemetry module focuses on observability contracts.
|
||||
- **Documentation:** `@moduledoc` on the telemetry module becomes the event catalog. No hunting through callbacks.
|
||||
- **Testability:** You can assert on telemetry calls without coupling to GenServer internals.
|
||||
|
||||
**When NOT to use:** A module with exactly one telemetry call (e.g., a simple function wrapping a single `:telemetry.execute`). Inline is fine there — the dedicated module adds ceremony without benefit.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2: Event Naming Convention
|
||||
|
||||
**What it does:** Event names follow a hierarchical path: `[app, context, noun, verb_past_tense]`.
|
||||
|
||||
**Examples:**
|
||||
```elixir
|
||||
[:gargoyle, :engine, :aggregation, :group, :completed]
|
||||
[:gargoyle, :market_data, :quote_feed, :connected]
|
||||
[:gargoyle, :daily_pnl, :snapshot, :backfilled]
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- App prefix first (namespace isolation)
|
||||
- Bounded context second (matches code structure)
|
||||
- Noun before verb (what happened to what)
|
||||
- Past tense verbs (events are facts — they already happened)
|
||||
- Never use generic names like `[:my_app, :event]` or `[:my_app, :metric]`
|
||||
|
||||
**Why:** Consistent naming lets you attach handlers by prefix (`[:gargoyle, :engine | _]`) and build dashboards without memorizing arbitrary event names.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3: Measurements vs Metadata Separation
|
||||
|
||||
**What it does:** Measurements are numeric values you aggregate (sum, avg, p99). Metadata is context for filtering/grouping.
|
||||
|
||||
```elixir
|
||||
# Good: clear separation
|
||||
:telemetry.execute(
|
||||
[:my_app, :request, :completed],
|
||||
%{duration_ms: 42, response_bytes: 1024}, # measurements: numbers
|
||||
%{method: :get, path: "/api/v1/users", status: 200} # metadata: dimensions
|
||||
)
|
||||
|
||||
# Bad: mixing concerns
|
||||
:telemetry.execute(
|
||||
[:my_app, :request, :completed],
|
||||
%{duration_ms: 42, method: :get}, # method isn't a measurement
|
||||
%{response_bytes: 1024, path: "/api"} # bytes should be a measurement
|
||||
)
|
||||
```
|
||||
|
||||
**Rule of thumb:** If you'd put it on a Y-axis in a graph → measurement. If you'd use it as a filter/group-by → metadata.
|
||||
@@ -2,6 +2,29 @@
|
||||
|
||||
Patterns extracted from the Elixir standard library source code — how the core team writes and organizes tests.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Module-Level Async Declaration](#1-module-level-async-declaration)
|
||||
2. [Parameterized Tests](#2-parameterized-tests)
|
||||
3. [Setup with `start_supervised/2`](#3-setup-with-start_supervised2)
|
||||
4. [Named Setup Functions (Composable Pipelines)](#4-named-setup-functions-composable-pipelines)
|
||||
5. [`on_exit` for Reversing Global Side Effects](#5-on_exit-for-reversing-global-side-effects)
|
||||
6. [Pattern Match Assertions](#6-pattern-match-assertions)
|
||||
7. [`assert_receive` / `refute_receive` for Process Communication](#7-assert_receive--refute_receive-for-process-communication)
|
||||
8. [Testing GenServers via Public API (No Internal State Inspection)](#8-testing-genservers-via-public-api-no-internal-state-inspection)
|
||||
9. [`catch_exit` for Testing Process Failures](#9-catch_exit-for-testing-process-failures)
|
||||
10. [`@tag capture_log: true` for Suppressing Expected Log Output](#10-tag-capture_log-true-for-suppressing-expected-log-output)
|
||||
11. [`capture_log` / `capture_io` for Content Assertions](#11-capture_log--capture_io-for-content-assertions)
|
||||
12. [`describe` Blocks for Logical Grouping](#12-describe-blocks-for-logical-grouping)
|
||||
13. [`ExUnit.CaseTemplate` for Shared Test Infrastructure](#13-exunitcasetemplate-for-shared-test-infrastructure)
|
||||
14. [`doctest` Integration](#14-doctest-integration)
|
||||
15. [`Process.sleep(:infinity)` as a Process Parking Pattern](#15-processsleepinfinity-as-a-process-parking-pattern)
|
||||
16. [Helper Functions for Test-Specific Behavior](#16-helper-functions-for-test-specific-behavior)
|
||||
17. [`@tag :tmp_dir` for Filesystem Tests](#17-tag-tmp_dir-for-filesystem-tests)
|
||||
18. [`assert_raise` with Message Matching](#18-assert_raise-with-message-matching)
|
||||
19. [`@moduletag` / `@describetag` for Cross-Cutting Configuration](#19-moduletag--describetag-for-cross-cutting-configuration)
|
||||
20. [Context Pattern Matching in Test Signatures](#20-context-pattern-matching-in-test-signatures)
|
||||
|
||||
---
|
||||
|
||||
## 1. Module-Level Async Declaration
|
||||
@@ -1725,4 +1748,100 @@ end
|
||||
|
||||
**Why:** Context destructuring signals "this test depends on external setup." If the test is self-contained, the pattern match is misleading — readers will look for setup that doesn't exist or isn't needed.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
- If you are creating a new test module and need to decide on concurrency → [Module-Level Async Declaration](#1-module-level-async-declaration)
|
||||
- If the same logic must work across multiple configurations or backends → [Parameterized Tests](#2-parameterized-tests)
|
||||
- If your test needs a running process with guaranteed cleanup → [Setup with `start_supervised/2`](#3-setup-with-start_supervised2)
|
||||
- If setup has multiple independent steps that different describe blocks reuse → [Named Setup Functions](#4-named-setup-functions-composable-pipelines)
|
||||
- If your test modifies global state that must be restored regardless of outcome → [`on_exit` for Reversing Global Side Effects](#5-on_exit-for-reversing-global-side-effects)
|
||||
- If you care about the shape/structure of a result but not every field → [Pattern Match Assertions](#6-pattern-match-assertions)
|
||||
- If you need to test asynchronous message delivery between processes → [`assert_receive` / `refute_receive`](#7-assert_receive--refute_receive-for-process-communication)
|
||||
- If you are testing a GenServer and want tests that survive refactoring → [Testing GenServers via Public API](#8-testing-genservers-via-public-api-no-internal-state-inspection)
|
||||
- If you need to assert on OTP exit signals (timeouts, noproc, shutdown) → [`catch_exit`](#9-catch_exit-for-testing-process-failures)
|
||||
- If tests intentionally trigger error paths that produce noisy log output → [`@tag capture_log: true`](#10-tag-capture_log-true-for-suppressing-expected-log-output)
|
||||
- If you need to verify specific log or IO content was emitted → [`capture_log` / `capture_io`](#11-capture_log--capture_io-for-content-assertions)
|
||||
- If a module tests multiple public functions and needs logical organization → [`describe` Blocks](#12-describe-blocks-for-logical-grouping)
|
||||
- If multiple test modules share the same setup/teardown infrastructure → [`ExUnit.CaseTemplate`](#13-exunitcasetemplate-for-shared-test-infrastructure)
|
||||
- If your module has `iex>` examples that should be verified automatically → [`doctest` Integration](#14-doctest-integration)
|
||||
- If you need an inert process that exists only to be observed or killed → [`Process.sleep(:infinity)`](#15-processsleepinfinity-as-a-process-parking-pattern)
|
||||
- If the same 3-5 line test pattern repeats across multiple tests → [Helper Functions](#16-helper-functions-for-test-specific-behavior)
|
||||
- If tests create or modify files and need filesystem isolation → [`@tag :tmp_dir`](#17-tag-tmp_dir-for-filesystem-tests)
|
||||
- If you need to verify both the exception type and the user-facing message → [`assert_raise` with Message Matching](#18-assert_raise-with-message-matching)
|
||||
- If tests only run on certain platforms or you want to filter subsets → [`@moduletag` / `@describetag`](#19-moduletag--describetag-for-cross-cutting-configuration)
|
||||
- If you want to make test dependencies on setup context explicit → [Context Pattern Matching](#20-context-pattern-matching-in-test-signatures)
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
## 21. Filtering Events in Async Tests
|
||||
|
||||
**Source:** Gargoyle PR #710 (flaky telemetry test fix)
|
||||
|
||||
**What it does:** Pin-matches on a unique identifier to filter events from concurrent tests.
|
||||
|
||||
**Why:** When testing telemetry, pub/sub, or any broadcast mechanism with `async: true`, events from other tests can leak into your mailbox. Without filtering, tests pass in isolation but fail randomly when run in parallel.
|
||||
|
||||
**Pattern:**
|
||||
|
||||
Wrong — receives events from other tests:
|
||||
```elixir
|
||||
test "emits telemetry on create", %{user: user} do
|
||||
:telemetry.attach("test", [:user, :created], &send_to_test/4, self())
|
||||
create_post(user)
|
||||
assert_receive {:telemetry, %{user_id: uid}}
|
||||
assert uid == user.id # Might match event from another test!
|
||||
end
|
||||
```
|
||||
|
||||
Right — pin filters to only your test's events:
|
||||
```elixir
|
||||
test "emits telemetry on create", %{user: user} do
|
||||
test_pid = self()
|
||||
expected_uid = user.id
|
||||
|
||||
:telemetry.attach("test-#{inspect(test_pid)}", [:user, :created], fn _, _, meta, pid ->
|
||||
if meta.user_id == expected_uid, do: send(pid, {:telemetry, meta})
|
||||
end, test_pid)
|
||||
|
||||
create_post(user)
|
||||
assert_receive {:telemetry, %{user_id: ^expected_uid}}
|
||||
end
|
||||
```
|
||||
|
||||
### Key Insight
|
||||
|
||||
The fix happens in two places:
|
||||
|
||||
1. **Filter at the source** — only send messages that match your test's unique identifier
|
||||
2. **Pin in the assertion** — use `^variable` to reject mismatches that slip through
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Telemetry handlers (filter by user_id, request_id, or test pid)
|
||||
- Phoenix.PubSub subscriptions (use unique topic per test)
|
||||
- GenStage/Broadway consumers (tag events with test pid)
|
||||
- Any shared message bus in async tests
|
||||
- Tests pass alone but fail with `--seed` or in CI
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- Tests run with `async: false` (no concurrent tests to leak events)
|
||||
- You control the event source and can make it test-aware by design
|
||||
- The event contains a natural unique key you already have (just pin on it)
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Overkill when you already have a unique key
|
||||
test "user update", %{user: user} do
|
||||
# user.id is already unique — just pin it directly
|
||||
assert_receive {:updated, %{id: ^user.id}}
|
||||
end
|
||||
```
|
||||
|
||||
### Decision Tree Addition
|
||||
|
||||
- If async tests randomly fail due to events from other tests → [Filtering Events in Async Tests](#21-filtering-events-in-async-tests)
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
+18
-9
@@ -2,6 +2,15 @@
|
||||
|
||||
Patterns extracted from Ecto's type system source code.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [`use Ecto.Type` — The Four-Callback Custom Type](#1-use-ectotype--the-four-callback-custom-type)
|
||||
2. [`embed_as/1` — Controlling Embedded Serialization](#2-embed_as1--controlling-embedded-serialization)
|
||||
3. [`equal?/2` — Custom Equality for Change Detection](#3-equal2--custom-equality-for-change-detection)
|
||||
4. [`Ecto.Enum` — Constrained Atom Fields](#4-ectoenum--constrained-atom-fields)
|
||||
5. [`Ecto.ParameterizedType` — Types with Options](#5-ectoparameterizedtype--types-with-options)
|
||||
6. [Schemaless Types — `{data, types}` Changesets](#6-schemaless-types--data-types-changesets)
|
||||
|
||||
---
|
||||
|
||||
## 1. `use Ecto.Type` — The Four-Callback Custom Type
|
||||
@@ -126,21 +135,21 @@ end
|
||||
|
||||
When a custom type is used inside an `embeds_one` or `embeds_many` field, Ecto calls `embed_as/1` to decide whether to pass the value through `dump/1` or treat it as its own serialized form. The callback receives the embed format (`:json` by default) and returns either `:self` or `:dump`.
|
||||
|
||||
- `:self` — the value is used as-is when exporting embedded data (dump is still called for DB storage)
|
||||
- `:dump` — `dump/1` is always called, even in embedded contexts
|
||||
- `:self` — the in-memory value is used as-is without calling `dump/1`; appropriate when the runtime representation is already JSON-compatible (scalars, plain maps)
|
||||
- `:dump` — `dump/1` is called to serialize the value before encoding; needed when the runtime representation (e.g., a struct) is not directly JSON-serializable
|
||||
|
||||
`use Ecto.Type` provides a default implementation that returns `:self`. Override it when your type must always run `dump/1` to produce its storable form, even when nested inside an embedded schema.
|
||||
`use Ecto.Type` provides a default implementation that returns `:self`. Override it to return `:dump` when your type holds an Elixir struct or other value that cannot be directly encoded to JSON.
|
||||
|
||||
```elixir
|
||||
defmodule EctoURI do
|
||||
use Ecto.Type
|
||||
|
||||
# Override to ensure dump/1 is called in embedded contexts
|
||||
def embed_as(_format), do: :self
|
||||
# Override: %URI{} is not JSON-serializable, so run dump/1 in embedded contexts
|
||||
def embed_as(_format), do: :dump
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** When Ecto builds embedded documents for export (e.g., storing a JSON blob), it needs to know whether to trust the in-memory value or to re-serialize it. If your type holds state in an Elixir struct that cannot be stored directly (like `%URI{}`), returning `:self` without a proper `dump/1` would persist the raw struct map rather than your intended shape. Overriding `embed_as/1` makes the contract explicit.
|
||||
**Why:** When Ecto builds embedded documents for export (e.g., storing a JSON blob), it needs to know whether to trust the in-memory value or to re-serialize it. If your type holds state in an Elixir struct that cannot be stored directly (like `%URI{}`), the default `:self` passes the raw struct to the JSON encoder — which either raises or produces garbage like `%{__struct__: "Elixir.URI", host: ..., ...}`. Returning `:dump` ensures `dump/1` converts it to a clean map first.
|
||||
|
||||
**Anti-pattern:** Assuming the default `:self` is correct for a type whose in-memory and storable representations differ, then debugging mysterious embedded schema corruption:
|
||||
```elixir
|
||||
@@ -190,8 +199,8 @@ defmodule EctoURI do
|
||||
def dump(%URI{} = uri), do: {:ok, Map.from_struct(uri)}
|
||||
def dump(_), do: :error
|
||||
|
||||
# Explicit: always pass through dump/1 when exporting embedded values
|
||||
def embed_as(_format), do: :self
|
||||
# Explicit: ensure dump/1 runs in embedded contexts to produce a clean map
|
||||
def embed_as(_format), do: :dump
|
||||
end
|
||||
```
|
||||
|
||||
@@ -200,7 +209,7 @@ end
|
||||
**Don't use this when:**
|
||||
- Your type is never used in embedded schemas (the callback has no effect)
|
||||
- The in-memory and storable representations are the same (plain maps, scalars)
|
||||
- You intentionally want to skip `dump/1` in embedded contexts (then return `:dump` and document why)
|
||||
- You intentionally want to skip `dump/1` in embedded contexts (the default `:self` already does this)
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
|
||||
Patterns extracted from the Elixir standard library source code.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Public Type with @typedoc](#1-public-type-with-typedoc)
|
||||
2. [Private Types with @typep](#2-private-types-with-typep)
|
||||
3. [@opaque Types (Protocol t())](#3-opaque-types-protocol-t)
|
||||
4. [Union Types in @spec Return Values](#4-union-types-in-spec-return-values)
|
||||
5. [`when` Constraints in Specs](#5-when-constraints-in-specs)
|
||||
6. [Map Types with required/optional Keys](#6-map-types-with-requiredoptional-keys)
|
||||
7. [Keyword List Types for Options](#7-keyword-list-types-for-options)
|
||||
8. [Parameterized Types (t/1)](#8-parameterized-types-t1)
|
||||
9. [Named Parameters in Specs (:: annotation)](#9-named-parameters-in-specs--annotation)
|
||||
10. [@typedoc since: Annotation](#10-typedoc-since-annotation)
|
||||
|
||||
---
|
||||
|
||||
## 1. Public Type with @typedoc
|
||||
@@ -798,4 +811,17 @@ end
|
||||
|
||||
**Why:** `since:` annotations are for library consumers checking compatibility across versions. Application code doesn't have "consumers" checking which version introduced a type — it's all deployed together.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
- If you are defining a public `@type` that appears in any `@spec` or callback → [Public Type with @typedoc](#1-public-type-with-typedoc)
|
||||
- If a type is used only internally for recursion or DRYing up repeated expressions → [Private Types with @typep](#2-private-types-with-typep)
|
||||
- If you want to hide internal representation and force consumers to use accessor functions → [@opaque Types](#3-opaque-types-protocol-t)
|
||||
- If a function can return multiple distinct shapes (tagged tuples, atoms) → [Union Types in @spec Return Values](#4-union-types-in-spec-return-values)
|
||||
- If the return type depends on the input type (generic/polymorphic function) → [`when` Constraints in Specs](#5-when-constraints-in-specs)
|
||||
- If you accept a map with a mix of mandatory and optional keys → [Map Types with required/optional Keys](#6-map-types-with-requiredoptional-keys)
|
||||
- If a function accepts a keyword list of options and you want to document valid keys → [Keyword List Types for Options](#7-keyword-list-types-for-options)
|
||||
- If you define a container type and want specs to express what element type is inside → [Parameterized Types (t/1)](#8-parameterized-types-t1)
|
||||
- If a parameter's type alone does not convey its purpose → [Named Parameters in Specs](#9-named-parameters-in-specs--annotation)
|
||||
- If you are adding a new public type to an existing library post-1.0 → [@typedoc since: Annotation](#10-typedoc-since-annotation)
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
@@ -84,7 +84,7 @@ end)
|
||||
|
||||
**What they avoid:** Tests that depend on or modify global state without cleanup.
|
||||
|
||||
**Source evidence:** `lib/mix/test/test_helper.exs:98-113` — MixTest.Case restores ALL global state in `on_exit`:
|
||||
**Source evidence:** `lib/mix/test/test_helper.exs:99-115` — MixTest.Case restores ALL global state in `on_exit`:
|
||||
- `Mix.env(:dev)`, `Mix.target(:host)`, `Mix.Task.clear()`, `Mix.Shell.Process.flush()`
|
||||
- Unloads all applications that were loaded during the test
|
||||
|
||||
@@ -587,12 +587,12 @@ end
|
||||
|
||||
**What they avoid:** ETS tables, registered names, or application env used across tests without isolation.
|
||||
|
||||
**Source evidence:** `lib/elixir/test/elixir/registry_test.exs:28-31` — Each test gets a uniquely-named Registry:
|
||||
**Source evidence:** `lib/elixir/test/elixir/registry_test.exs:29-32` — Each test gets a uniquely-named Registry:
|
||||
```elixir
|
||||
name = :"#{config.test}_#{partitions}_#{inspect(keys)}"
|
||||
```
|
||||
|
||||
`lib/elixir/test/elixir/gen_server_test.exs:166` — Uses `%{test: name}` for unique process registration.
|
||||
`lib/elixir/test/elixir/gen_server_test.exs:164` — Uses `%{test: name}` for unique process registration.
|
||||
|
||||
**Why it's bad:** Tests that share state can't run concurrently. They're order-dependent and fragile.
|
||||
|
||||
@@ -880,7 +880,7 @@ end
|
||||
|
||||
**What they avoid:** Converting untrusted strings to atoms.
|
||||
|
||||
**Source evidence:** `lib/elixir/lib/option_parser.ex:855` — Uses `String.to_existing_atom/1` with the `:switches` allowlist pattern. The only `String.to_atom/1` calls in library code are in compiler/macro contexts where the set is bounded.
|
||||
**Source evidence:** `lib/elixir/lib/option_parser.ex:859` — Uses `String.to_existing_atom/1` with the `:switches` allowlist pattern. The only `String.to_atom/1` calls in library code are in compiler/macro contexts where the set is bounded.
|
||||
|
||||
**Why it's bad:** Atoms are never garbage collected. User-controlled atom creation is a denial-of-service vector (1,048,576 atom limit by default).
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ setup do
|
||||
end
|
||||
```
|
||||
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/callbacks.ex:277-340` — `start_supervised` is designed specifically for this: guaranteed shutdown in reverse order, no leaked processes, no race conditions.
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/callbacks.ex:520-568` — `start_supervised` is designed specifically for this: guaranteed shutdown in reverse order, no leaked processes, no race conditions.
|
||||
|
||||
### When to Apply This Rule
|
||||
|
||||
@@ -236,7 +236,7 @@ test "starts a server", %{test: test_name} do
|
||||
end
|
||||
```
|
||||
|
||||
**Source:** `lib/elixir/test/elixir/registry_test.exs:28` — `name = :"#{config.test}_#{partitions}_#{inspect(keys)}"` — always derives unique names from test context.
|
||||
**Source:** `lib/elixir/test/elixir/registry_test.exs:29` — `name = :"#{config.test}_#{partitions}_#{inspect(keys)}"` — always derives unique names from test context.
|
||||
|
||||
### When to Apply This Rule
|
||||
|
||||
@@ -401,7 +401,7 @@ setup do
|
||||
end
|
||||
```
|
||||
|
||||
**Source:** `lib/logger/test/logger_test.exs:12-17` — Every Logger config change has a corresponding `on_exit` restoration. `lib/logger/test/test_helper.exs:57-62` — `capture_log` uses `after` to always restore level.
|
||||
**Source:** `lib/logger/test/logger_test.exs:12-17` — Every Logger config change has a corresponding `on_exit` restoration. `lib/logger/test/test_helper.exs:57-65` — `capture_log` uses `after` to always restore level.
|
||||
|
||||
### When to Apply This Rule
|
||||
|
||||
@@ -474,7 +474,7 @@ describe "admin users - deletion" do
|
||||
end
|
||||
```
|
||||
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/callbacks.ex:423-425` — `no_describe!` check prevents nesting.
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/callbacks.ex:433-437` — `no_describe!` check prevents nesting.
|
||||
|
||||
### When to Apply This Rule
|
||||
|
||||
@@ -628,7 +628,7 @@ test "handles process crash" do
|
||||
end
|
||||
```
|
||||
|
||||
**Source:** `lib/elixir/test/elixir/task_test.exs:297,305,315,330` — Every test that expects a linked process to crash sets `:trap_exit` first.
|
||||
**Source:** `lib/elixir/test/elixir/task_test.exs:300,308,316,327` — Every test that expects a linked process to crash sets `:trap_exit` first.
|
||||
|
||||
### When to Apply This Rule
|
||||
|
||||
@@ -866,7 +866,7 @@ test "process stops" do
|
||||
end
|
||||
```
|
||||
|
||||
**Source:** `lib/elixir/test/elixir/supervisor_test.exs:278-285` — `assert_kill` helper always uses monitor + assert_receive, never `Process.alive?` polling.
|
||||
**Source:** `lib/elixir/test/elixir/supervisor_test.exs:289-293` — `assert_kill` helper always uses monitor + assert_receive, never `Process.alive?` polling.
|
||||
|
||||
### When to Apply This Rule
|
||||
|
||||
@@ -1160,3 +1160,25 @@ end
|
||||
**Why it's OK here:** There's no conditional logic — every iteration tests the exact same behavior with a trivially predictable result. If one fails, the assertion message includes the specific value. The loop is purely for conciseness.
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
---
|
||||
|
||||
## Logger.warn/2 is Deprecated
|
||||
|
||||
**Smell:** Using `Logger.warn/2` instead of `Logger.warning/2`
|
||||
|
||||
**Why it's wrong:** `Logger.warn/2` was deprecated in Elixir 1.11. The standard function is `Logger.warning/2`.
|
||||
|
||||
**Example — broken:**
|
||||
```elixir
|
||||
Logger.warn("Connection lost: #{reason}")
|
||||
```
|
||||
|
||||
**Example — fixed:**
|
||||
```elixir
|
||||
Logger.warning("Connection lost: #{reason}")
|
||||
```
|
||||
|
||||
**Source:** [Elixir 1.11 Changelog](https://hexdocs.pm/elixir/1.11/changelog.html#logger-improvements)
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Sources
|
||||
|
||||
Reference material extracted from specific projects. Study for ideas, don't copy blindly.
|
||||
|
||||
These are **descriptive** — they document what a project does and why.
|
||||
The `patterns/` directory is **prescriptive** — it tells you what to do.
|
||||
|
||||
Patterns that prove broadly applicable get promoted from here into `patterns/`.
|
||||
The rest stays as reference for understanding how mature projects solve specific problems.
|
||||
|
||||
## Files
|
||||
|
||||
- `elixir-lang.md` — conventions from the elixir-lang/elixir source
|
||||
- `elixir-lang-analysis.md` — deeper analysis of elixir-lang source architecture
|
||||
- `oban.md` — patterns from oban-bg/oban (job processing, plugin system)
|
||||
@@ -0,0 +1,303 @@
|
||||
# Elixir Language Source: Architectural Conventions
|
||||
|
||||
How does José Valim and the Elixir core team build Elixir itself?
|
||||
What does the language source reveal about conventions that aren't
|
||||
documented anywhere else?
|
||||
|
||||
**Repo:** [elixir-lang/elixir](https://github.com/elixir-lang/elixir)
|
||||
|
||||
---
|
||||
|
||||
## 1. Repo Shape
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Size | 92M |
|
||||
| Source files | 567 .ex/.exs |
|
||||
| Erlang bootstrap | 33 .erl files |
|
||||
| Commits | 22,032 |
|
||||
| Contributors | 1,578 |
|
||||
| Test files | 208 |
|
||||
| Production files | 248 |
|
||||
| Test ratio | 1:1.2 |
|
||||
| TODOs (non-test) | 127 (all version-gated) |
|
||||
|
||||
### Organizational Philosophy
|
||||
|
||||
```
|
||||
lib/
|
||||
├── elixir/ # The language core (compiler + stdlib)
|
||||
│ ├── src/ # 33 Erlang files (bootstrap)
|
||||
│ └── lib/ # Elixir stdlib + compiler
|
||||
├── eex/ # Templating (independent OTP app)
|
||||
├── ex_unit/ # Testing framework (independent OTP app)
|
||||
├── iex/ # Interactive shell (independent OTP app)
|
||||
├── logger/ # Logging (independent OTP app)
|
||||
└── mix/ # Build tool (independent OTP app)
|
||||
```
|
||||
|
||||
Each component is a separate OTP application. They could theoretically
|
||||
be released independently. This is Elixir eating its own dog food —
|
||||
the umbrella project convention that Phoenix apps use comes directly
|
||||
from how the language itself is organized.
|
||||
|
||||
---
|
||||
|
||||
## 2. What the Codebase Values
|
||||
|
||||
### By size (what gets the most lines)
|
||||
|
||||
| Module | Lines | Role |
|
||||
|--------|-------|------|
|
||||
| `Kernel` | 7,102 | The implicit language surface |
|
||||
| `Module.Types.Descr` | 6,301 | Set-theoretic type descriptions |
|
||||
| `Enum` | 5,242 | Collection operations |
|
||||
| `String` | 3,263 | First-class string concept |
|
||||
| `Macro` | 3,102 | Metaprogramming foundation |
|
||||
| `Exception` | 2,720 | Error taxonomy |
|
||||
| `Code.Formatter` | 2,605 | Code formatting as library |
|
||||
|
||||
**The surprise:** The type system (`types/descr.ex` at 6,301 lines) is
|
||||
nearly as large as Kernel (7,102 lines). It's the newest and
|
||||
fastest-growing module — 504 commits, 96% written by José Valim. This
|
||||
is where the investment is going.
|
||||
|
||||
### By authorship (who shapes the language)
|
||||
|
||||
Type system: 396/504 commits from José, 32 from Eric Meadows-Jönsson,
|
||||
31 from Guillaume Duboc. This is auteur-driven development — one person
|
||||
holds the architectural vision for the most complex subsystem.
|
||||
|
||||
---
|
||||
|
||||
## 3. The Bootstrap Problem
|
||||
|
||||
**How does Elixir compile itself?**
|
||||
|
||||
The answer is 33 Erlang files in `lib/elixir/src/`:
|
||||
|
||||
```
|
||||
elixir_bootstrap.erl — minimal Kernel for self-compilation
|
||||
elixir_compiler.erl — the compiler entry point
|
||||
elixir_tokenizer.erl — lexer (in Erlang for speed)
|
||||
elixir_expand.erl — macro expansion
|
||||
elixir_erl.erl — Elixir AST → Erlang AST
|
||||
elixir_erl_pass.erl — code generation pass
|
||||
elixir_env.erl — compilation environment
|
||||
elixir_clauses.erl — pattern matching compilation
|
||||
```
|
||||
|
||||
**Convention:** The tokenizer and core compiler remain in Erlang
|
||||
permanently. This isn't technical debt — it's a deliberate choice.
|
||||
The tokenizer benefits from Erlang's binary pattern matching
|
||||
performance. The compiler needs to exist before Elixir does.
|
||||
|
||||
**Origin:** The bootstrap file dates to Nov 22, 2013 (commit
|
||||
`260be7c8e`: "Start porting elixir_macros to pure elixir"). Before
|
||||
this, MORE of the compiler was in Erlang. The trajectory is clear:
|
||||
minimize Erlang over time, but keep it where it provides genuine value.
|
||||
|
||||
---
|
||||
|
||||
## 4. TODO Culture: Version-Gated Deadlines
|
||||
|
||||
```elixir
|
||||
# TODO: Remove me on v2.0 — 16 occurrences
|
||||
# TODO: Deprecate me on Elixir v1.23 — 6 occurrences
|
||||
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
|
||||
# TODO: Make an error on Elixir v2.0 — 3 occurrences
|
||||
# TODO: Deprecate on Elixir v1.22 — 3 occurrences
|
||||
```
|
||||
|
||||
**Convention:** Every TODO has a version target. No "someday" TODOs
|
||||
exist. When a version ships, grep for that version's TODOs and resolve
|
||||
them all.
|
||||
|
||||
**127 total TODOs** across 567 files. Contrast with Go's 3,428 TODOs
|
||||
across 11K files — the Elixir team treats TODOs as time-bombs, not
|
||||
documentation.
|
||||
|
||||
---
|
||||
|
||||
## 5. Unique Patterns
|
||||
|
||||
### 5.1 Protocol Consolidation
|
||||
|
||||
Protocols dispatch dynamically at runtime by default (checking each
|
||||
struct's implementation). **Protocol consolidation** compiles all known
|
||||
implementations into a single dispatch module at build time.
|
||||
|
||||
From `lib/elixir/lib/protocol.ex`:
|
||||
> "Consolidation directly links the protocol to its implementations.
|
||||
> Invoking a consolidated protocol is equivalent to invoking two remote
|
||||
> functions."
|
||||
|
||||
**Convention:** Mix enables consolidation by default in production. The
|
||||
`@callback __protocol__(:consolidated?)` exists so code can check at
|
||||
runtime whether fast-path dispatch is active.
|
||||
|
||||
**When NOT to use:** Tests often disable consolidation (`consolidate_
|
||||
protocols: false`) so new protocol implementations added during tests
|
||||
are discoverable without recompilation.
|
||||
|
||||
### 5.2 Parallel Type Checker
|
||||
|
||||
`Module.ParallelChecker` (introduced July 2019, PR #9203 by Eric
|
||||
Meadows-Jönsson as "Add ExCk chunk") enables concurrent type checking
|
||||
across modules.
|
||||
|
||||
The type system itself (13,034 lines across 7 files in
|
||||
`lib/elixir/lib/module/types/`) is set-theoretic — types are sets, and
|
||||
operations are set operations (union, intersection, difference).
|
||||
|
||||
**Key files:**
|
||||
- `descr.ex` (6,301 lines) — type descriptions and set operations
|
||||
- `apply.ex` — function application typing
|
||||
- `expr.ex` — expression typing
|
||||
- `pattern.ex` — pattern match typing
|
||||
- `of.ex` — type inference
|
||||
- `helpers.ex` — shared utilities
|
||||
- `traverse.ex` — AST traversal
|
||||
|
||||
### 5.3 Code.Formatter as Library Function
|
||||
|
||||
The code formatter (2,605 lines) is a library function, not a CLI tool.
|
||||
You can call `Code.format_string!/2` from any Elixir code.
|
||||
|
||||
**Introduced:** Oct 7, 2017 (PR #6639 by José Valim). **Zero review
|
||||
comments. Merged in 1 hour.** José opened and merged his own formatter
|
||||
with no external review. This is the BDFL model — the language author
|
||||
ships foundational infrastructure by authority.
|
||||
|
||||
**Convention:** The formatter uses `Inspect.Algebra` (Wadler-Lindig
|
||||
pretty-printing) for layout decisions. It defines all operators and
|
||||
their associativity as module attributes:
|
||||
|
||||
```elixir
|
||||
@pipeline_operators [:|>, :~>>, :<<~, :~>, :<~, :<~>, :"<|>"]
|
||||
@right_new_line_before_binary_operators [:|, :when]
|
||||
@required_parens_logical_binary_operands [:|||, :||, :or, :&&&, :&&, :and]
|
||||
```
|
||||
|
||||
### 5.4 Mix Tasks as Single-File Modules
|
||||
|
||||
55 Mix tasks, each in its own file. Convention:
|
||||
- One task = one file
|
||||
- Module name determines task name: `Mix.Tasks.Deps.Clean` → `deps.clean`
|
||||
- `@shortdoc` for brief help, `@moduledoc` for full docs
|
||||
- `@recursive true` for umbrella traversal
|
||||
|
||||
### 5.5 ExUnit CaseTemplate (Extension Pattern)
|
||||
|
||||
The `ExUnit.CaseTemplate` is how Elixir's test framework supports
|
||||
extension — you define a module that `use`s `CaseTemplate`, and test
|
||||
modules `use YourModule` to inherit setup callbacks and helpers.
|
||||
|
||||
This is the same pattern Phoenix uses for `ConnCase` and `DataCase`.
|
||||
It originates from ExUnit itself — the framework demonstrates its own
|
||||
extension point.
|
||||
|
||||
### 5.6 Logger: Erlang Integration Done Right
|
||||
|
||||
PR #9333 (Sep 2019, merged Nov 2019): "Use Erlang's logger as main
|
||||
logging implementation." The Elixir Logger was rewritten to sit on top
|
||||
of Erlang's `:logger` module rather than reimplementing log dispatch.
|
||||
|
||||
**Convention:** When OTP provides infrastructure, wrap it rather than
|
||||
replace it. The compatibility layer translates Erlang log messages to
|
||||
Elixir format, but dispatch/filtering/handlers are OTP's.
|
||||
|
||||
---
|
||||
|
||||
## 6. PR Discussion Patterns
|
||||
|
||||
### JSON.Encoder (PR #14021, Dec 2024)
|
||||
|
||||
38 review comments, 13 days to merge. Key debate:
|
||||
|
||||
**sabiwara** asked: "What is the reason we went with a different API
|
||||
than Jason?" — questioning why the stdlib JSON module doesn't mirror
|
||||
the dominant community library.
|
||||
|
||||
**michalmuskala** (Jason author): "Once 1.18 is released with the new
|
||||
JSON module, I plan to make a new release of Jason with some small
|
||||
fixes and then effectively deprecate it."
|
||||
|
||||
**Lesson:** When stdlib absorbs community library functionality, the
|
||||
community library author participates in the review. Jason's author
|
||||
blessed the replacement and planned deprecation. This is how healthy
|
||||
ecosystem evolution works.
|
||||
|
||||
### Duration (PR #13385, Mar-Apr 2024)
|
||||
|
||||
75 comments + 116 review comments. The most debated PR in recent
|
||||
Elixir history.
|
||||
|
||||
**Pattern:** Community contributor (@tfiedlerdejanze) opened a PR
|
||||
adding `Date.shift/2`. José redirected to a broader `Duration` type.
|
||||
The contributor iterated through multiple designs.
|
||||
|
||||
**José's key intervention:** "I would rather prefer to pass a Duration
|
||||
to Calendar.ISO.shift_date, if we ever have such a type, rather than a
|
||||
keyword list." — refusing a simpler PR because it would lock in a
|
||||
suboptimal API before the full design was clear.
|
||||
|
||||
**Lesson:** The BDFL model means one person can say "this is the wrong
|
||||
abstraction" and redirect months of work. The PR took 33 days and
|
||||
several complete rewrites. The result was better because someone held
|
||||
the line on "solve the whole problem, not just the immediate pain."
|
||||
|
||||
### Formatter (PR #6639, Oct 2017)
|
||||
|
||||
Zero comments. Merged in 1 hour. 2,605 lines of new code.
|
||||
|
||||
**Lesson:** BDFL-driven projects can ship massive foundational changes
|
||||
with no review. José was both the author and the authority. This is the
|
||||
opposite of CockroachDB's Handle PR (2.5 months, extensive debate).
|
||||
Neither model is wrong — it depends on team structure and trust level.
|
||||
|
||||
---
|
||||
|
||||
## 7. Cross-Ecosystem Comparisons
|
||||
|
||||
| Aspect | Elixir | Go |
|
||||
|--------|--------|-----|
|
||||
| TODOs | 127, all version-gated | 3,428, all owner-attributed |
|
||||
| Formatter origin | BDFL ships in 1hr, no review | `gofmt` shipped with language |
|
||||
| Bootstrap | Erlang (33 files, permanent) | Assembly + Go (self-hosting since 1.5) |
|
||||
| Extension | 6 protocols + CaseTemplate | `internal/` packages (61 of them) |
|
||||
| Type system | Set-theoretic, 13K lines, growing | Static, mature, compile-time only |
|
||||
| Test ratio | 1:1.2 (file per file) | 1:3.3 (package-level tests) |
|
||||
| Governance | BDFL (José) | Committee (Russ Cox + team) |
|
||||
|
||||
---
|
||||
|
||||
## 8. What This Teaches
|
||||
|
||||
1. **BDFL projects can move faster on foundational infrastructure** —
|
||||
the formatter, type system, and JSON module all shipped because one
|
||||
person had authority. But Duration took 33 days because community
|
||||
contribution required iteration with the BDFL's vision.
|
||||
|
||||
2. **Version-gated TODOs are a superior cleanup strategy** for
|
||||
projects with regular release cycles. You never have to decide "is
|
||||
this worth fixing?" — the version bump forces the question.
|
||||
|
||||
3. **Keep the minimum viable bootstrap in the host language.** 33
|
||||
Erlang files is the floor, not a ceiling. The trajectory is always
|
||||
toward more Elixir, less Erlang — but the tokenizer stays in Erlang
|
||||
because binary matching is genuinely faster there.
|
||||
|
||||
4. **The type system's growth rate predicts the language's future.**
|
||||
504 commits, 96% from José, nearly as large as Kernel. Elixir's
|
||||
next 5 years will be defined by gradual typing.
|
||||
|
||||
5. **Community library authors should bless stdlib absorption.** The
|
||||
Jason → JSON.Encoder transition worked because michalmuskala
|
||||
participated in the review and planned deprecation.
|
||||
|
||||
6. **Each OTP app is an independent unit** — this convention flows
|
||||
directly into how Phoenix projects are organized. The language
|
||||
teaches its own architectural pattern by example.
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
@@ -0,0 +1,266 @@
|
||||
# Elixir Language Source: Convention Reference
|
||||
|
||||
Quick-reference for conventions extracted from the elixir-lang/elixir
|
||||
source code. Each entry: pattern name, location, example, when to use,
|
||||
when NOT to use, origin.
|
||||
|
||||
---
|
||||
|
||||
## Version-Gated TODOs
|
||||
|
||||
**Location:** Throughout `lib/`
|
||||
|
||||
```elixir
|
||||
# TODO: Remove me on v2.0
|
||||
# TODO: Deprecate me on Elixir v1.23
|
||||
# TODO: Make an error on Elixir v2.0
|
||||
```
|
||||
|
||||
**When to use:** Any backward-compatible code that should be removed at
|
||||
a known future version. Deprecation paths, compatibility shims, feature
|
||||
flags.
|
||||
|
||||
**When NOT to use:** Performance improvements ("make this faster
|
||||
someday"), refactoring desires, or anything without a clear version
|
||||
boundary. Those belong in issues, not TODOs.
|
||||
|
||||
**Origin:** Consistent throughout history. The pattern predates the
|
||||
repo's earliest commits — José's convention from the start.
|
||||
|
||||
---
|
||||
|
||||
## Independent OTP Applications
|
||||
|
||||
**Location:** `lib/` (6 top-level dirs)
|
||||
|
||||
```
|
||||
lib/elixir/mix.exs # The language core
|
||||
lib/ex_unit/mix.exs # Testing framework
|
||||
lib/mix/mix.exs # Build tool
|
||||
lib/logger/mix.exs # Logging
|
||||
lib/iex/mix.exs # Interactive shell
|
||||
lib/eex/mix.exs # Templates
|
||||
```
|
||||
|
||||
**When to use:** When components have distinct lifecycle, deployment, or
|
||||
dependency requirements. When you want components to be independently
|
||||
testable.
|
||||
|
||||
**When NOT to use:** Small projects where the overhead of multiple
|
||||
applications exceeds the organizational benefit. If components always
|
||||
deploy together and never independently, a single app is simpler.
|
||||
|
||||
**Origin:** Elixir 0.x — the language was always structured this way.
|
||||
|
||||
---
|
||||
|
||||
## Erlang for Performance-Critical Paths
|
||||
|
||||
**Location:** `lib/elixir/src/` (33 .erl files)
|
||||
|
||||
```erlang
|
||||
%% elixir_tokenizer.erl — binary pattern matching is faster in Erlang
|
||||
%% elixir_erl_pass.erl — code generation benefits from proximity to BEAM
|
||||
```
|
||||
|
||||
**When to use:** When Erlang's binary pattern matching or NIF interface
|
||||
provides measurable performance advantage. When code must exist before
|
||||
Elixir's compiler is available (bootstrap).
|
||||
|
||||
**When NOT to use:** For new features that could be written in Elixir.
|
||||
The trajectory is always toward less Erlang. Don't write new Erlang
|
||||
unless profiling proves it's necessary.
|
||||
|
||||
**Origin:** The entire language started as Erlang (2011). Bootstrap file
|
||||
(`elixir_bootstrap.erl`) formalized as distinct from "Elixir written
|
||||
in Erlang" in 2013 (commit `260be7c8e`).
|
||||
|
||||
---
|
||||
|
||||
## Protocol Consolidation
|
||||
|
||||
**Location:** `lib/elixir/lib/protocol.ex`
|
||||
|
||||
```elixir
|
||||
# In mix.exs (default for prod):
|
||||
consolidate_protocols: true
|
||||
|
||||
# Disable for tests:
|
||||
consolidate_protocols: Mix.env() != :test
|
||||
```
|
||||
|
||||
**When to use:** Always in production (it's the default). Consolidated
|
||||
protocols dispatch in two function calls instead of dynamic lookup.
|
||||
|
||||
**When NOT to use:** In tests where you define new protocol
|
||||
implementations at runtime. In dev if you're frequently recompiling
|
||||
protocol implementations.
|
||||
|
||||
**Origin:** PR adding consolidation to escript.build (2014, issue
|
||||
#2699). Later made default for Mix projects.
|
||||
|
||||
---
|
||||
|
||||
## Code.Formatter via Inspect.Algebra
|
||||
|
||||
**Location:** `lib/elixir/lib/code/formatter.ex` (2,605 lines)
|
||||
|
||||
```elixir
|
||||
# The formatter is a library function:
|
||||
Code.format_string!("def foo( x,y),do: x+y")
|
||||
# => "def foo(x, y), do: x + y"
|
||||
|
||||
# Internally uses Wadler-Lindig algebra:
|
||||
import Inspect.Algebra, except: [format: 2, surround: 3, surround: 4]
|
||||
```
|
||||
|
||||
**When to use:** When building code generation tools, macros that
|
||||
produce source, or custom formatting rules. The algebra is available
|
||||
to any Elixir code.
|
||||
|
||||
**When NOT to use:** Don't reimplement formatting logic. Use
|
||||
`Code.format_string!/2` or the `mix format` task. The algebra is for
|
||||
building formatters, not for end-user formatting.
|
||||
|
||||
**Origin:** Oct 7, 2017 (PR #6639, José Valim). Merged in 1 hour, zero
|
||||
review comments.
|
||||
|
||||
---
|
||||
|
||||
## Mix Task Convention
|
||||
|
||||
**Location:** `lib/mix/lib/mix/tasks/` (55 files)
|
||||
|
||||
```elixir
|
||||
defmodule Mix.Tasks.Deps.Clean do
|
||||
@moduledoc "Removes the given dependencies' build artifacts."
|
||||
@shortdoc "Deletes generated files and artifacts for dependencies"
|
||||
|
||||
use Mix.Task
|
||||
@recursive true
|
||||
|
||||
@impl true
|
||||
def run(args) do
|
||||
# ...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**When to use:** Any command-line operation that should be available via
|
||||
`mix <task>`. One file per task, module name determines task name.
|
||||
|
||||
**When NOT to use:** Tasks that require interactive user input (use
|
||||
escript or IEx helpers instead). Tasks that need to run without Mix
|
||||
loaded.
|
||||
|
||||
**Origin:** Part of Mix since its creation. The one-file convention
|
||||
is strict — all 55 stdlib tasks follow it.
|
||||
|
||||
---
|
||||
|
||||
## ExUnit.CaseTemplate
|
||||
|
||||
**Location:** `lib/ex_unit/lib/ex_unit/case_template.ex` (162 lines)
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.DataCase do
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
setup tags do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
|
||||
unless tags[:async], do: Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
# Usage:
|
||||
defmodule MyApp.SomeTest do
|
||||
use MyApp.DataCase, async: true
|
||||
end
|
||||
```
|
||||
|
||||
**When to use:** When multiple test modules share setup logic,
|
||||
assertions, or helper functions. The inheritance model allows
|
||||
composition of test concerns.
|
||||
|
||||
**When NOT to use:** For test helpers that don't need lifecycle
|
||||
callbacks. Simple `import` or `alias` is cleaner for utility functions
|
||||
that don't need `setup`/`setup_all`.
|
||||
|
||||
**Origin:** Commit `1f491dfbe` — "Add support to case templates." The
|
||||
pattern Phoenix adopted for `ConnCase`/`DataCase` comes directly from
|
||||
ExUnit.
|
||||
|
||||
---
|
||||
|
||||
## Logger Wrapping OTP
|
||||
|
||||
**Location:** `lib/logger/`
|
||||
|
||||
```elixir
|
||||
# Elixir's Logger dispatches to Erlang's :logger
|
||||
# Translation layer converts Erlang messages → Elixir format
|
||||
# Filters and handlers use OTP's infrastructure
|
||||
```
|
||||
|
||||
**When to use:** When OTP provides the infrastructure you need.
|
||||
Wrap it — don't replace it. Provide Elixir-idiomatic API on top.
|
||||
|
||||
**When NOT to use:** When OTP's solution has fundamental architectural
|
||||
limitations you can't work around with a wrapper (rare).
|
||||
|
||||
**Origin:** PR #9333 (Sep-Nov 2019). Rewrote Logger from custom
|
||||
dispatch to wrapping Erlang's `:logger`. ~2 month implementation.
|
||||
|
||||
---
|
||||
|
||||
## BDFL Merge Pattern
|
||||
|
||||
**Location:** Throughout git history
|
||||
|
||||
```
|
||||
PR #6639 (Formatter): 0 comments, merged in 1 hour, 2,605 new lines
|
||||
PR #14021 (JSON): 38 review comments, 13 days, community input
|
||||
PR #13385 (Duration): 75+116 comments, 33 days, community redirected
|
||||
```
|
||||
|
||||
**When to use:** Foundational infrastructure where one person holds the
|
||||
architectural vision. The BDFL can ship without review when the change
|
||||
is self-evidently correct (formatter) or redirect community work when
|
||||
the abstraction isn't right (Duration).
|
||||
|
||||
**When NOT to use:** In team-maintained projects without a clear
|
||||
authority. In projects where consensus is required for API decisions.
|
||||
The BDFL model fails when the BDFL is wrong and no one can override.
|
||||
|
||||
**Origin:** José Valim has been sole authority since Elixir's creation
|
||||
(2011). The core team exists but José has final say on language design.
|
||||
|
||||
---
|
||||
|
||||
## Type System Architecture (Set-Theoretic)
|
||||
|
||||
**Location:** `lib/elixir/lib/module/types/` (13,034 lines, 7 files)
|
||||
|
||||
```elixir
|
||||
# Types are sets. Operations are set operations.
|
||||
# descr.ex (6,301 lines) defines the algebra:
|
||||
# - Union of types
|
||||
# - Intersection of types
|
||||
# - Difference (negation types)
|
||||
# - Subtype checking via set inclusion
|
||||
```
|
||||
|
||||
**When to use:** Understanding how Elixir's gradual type system works
|
||||
internally. The set-theoretic approach means types compose naturally —
|
||||
`integer() | String.t()` is literally a set union.
|
||||
|
||||
**When NOT to use:** This is internal compiler infrastructure. Don't
|
||||
depend on `Module.Types` internals — they change frequently (504
|
||||
commits and counting).
|
||||
|
||||
**Origin:** Aug 2019 (PR #9270 by Eric Meadows-Jönsson). Originally
|
||||
just function clause exhaustiveness checking, now growing into full
|
||||
gradual typing. 96% of subsequent work by José.
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
# Patterns Extracted from oban-bg/oban
|
||||
|
||||
## Pattern: Plugin as Behaviour + GenServer
|
||||
|
||||
**Source:** `lib/oban/plugin.ex`
|
||||
**Category:** plugin
|
||||
|
||||
**What:** Define a plugin interface as a behaviour with
|
||||
`start_link/1` and `validate/1` callbacks. Plugins must be
|
||||
OTP-compliant (GenServer/Agent). The host supervises them.
|
||||
|
||||
**Why:** Extensibility without coupling. Oban can start any
|
||||
module that satisfies the behaviour — pruning, cron,
|
||||
lifeline — without knowing implementation details. The
|
||||
`validate/1` callback ensures misconfigured plugins fail at
|
||||
startup, not at runtime.
|
||||
|
||||
**Example:**
|
||||
|
||||
```elixir
|
||||
@callback start_link([option()]) :: GenServer.on_start()
|
||||
@callback validate([option()]) :: :ok | {:error, String.t()}
|
||||
@optional_callbacks [format_logger_output: 2]
|
||||
```
|
||||
|
||||
**When to use:** When your application needs a plugin
|
||||
system where third parties add behavior. The behaviour
|
||||
ensures type safety; supervision ensures fault isolation.
|
||||
|
||||
**When NOT to use:** Internal modules that you control.
|
||||
Behaviours add ceremony — if there is only one
|
||||
implementation, use a module directly.
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Structured Telemetry Spans
|
||||
|
||||
**Source:** `lib/oban/telemetry.ex`
|
||||
**Category:** telemetry
|
||||
|
||||
**What:** Emit telemetry events as spans with
|
||||
start/stop/exception structure. Every operation (job
|
||||
execution, engine calls, plugin work) follows the same
|
||||
three-event pattern with consistent metadata shapes.
|
||||
|
||||
**Why:** Uniform observability. Any monitoring tool
|
||||
(AppSignal, Datadog, custom logger) can hook into the same
|
||||
event structure. The span pattern (start → stop|exception)
|
||||
enables latency tracking, error rates, and resource usage
|
||||
measurement without custom instrumentation per feature.
|
||||
|
||||
**Example:**
|
||||
|
||||
```elixir
|
||||
# Event names follow: [:oban, :component, :action, :phase]
|
||||
[:oban, :job, :start]
|
||||
[:oban, :job, :stop] # measurements: duration, memory
|
||||
[:oban, :job, :exception] # + kind, reason, stacktrace
|
||||
|
||||
[:oban, :engine, :fetch_jobs, :start]
|
||||
[:oban, :engine, :fetch_jobs, :stop]
|
||||
[:oban, :engine, :fetch_jobs, :exception]
|
||||
```
|
||||
|
||||
**When to use:** Any library or application that wants
|
||||
observability without coupling to a specific monitoring
|
||||
backend. The pattern works for database queries, HTTP
|
||||
requests, background jobs, cache operations.
|
||||
|
||||
**When NOT to use:** Ultra-hot paths where telemetry
|
||||
overhead matters (millions of events/second). Use sampling
|
||||
or skip entirely.
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Engine Abstraction for Backend Swap
|
||||
|
||||
**Source:** `lib/oban/engine.ex`
|
||||
**Category:** engine
|
||||
|
||||
**What:** Define a behaviour (`Engine`) with callbacks for
|
||||
all database operations (insert, fetch, complete, etc.).
|
||||
Ship multiple implementations (Basic/Inline/Lite) that swap
|
||||
at config time.
|
||||
|
||||
**Why:** Different environments need different backends:
|
||||
Postgres for production, SQLite for development, inline
|
||||
(in-memory) for testing. The engine abstraction lets you
|
||||
swap without changing application code.
|
||||
|
||||
**Example:**
|
||||
|
||||
```elixir
|
||||
@callback init(conf, opts) :: {:ok, meta} | {:error, term}
|
||||
@callback insert_job(conf, changeset, opts) :: {:ok, Job.t()}
|
||||
@callback fetch_jobs(conf, meta, opts) :: {:ok, {meta, [Job.t()]}}
|
||||
@callback complete_job(conf, Job.t()) :: :ok
|
||||
```
|
||||
|
||||
**When to use:** When your system needs to support multiple
|
||||
storage backends, or when testing requires a fundamentally
|
||||
different execution model (synchronous vs async).
|
||||
|
||||
**When NOT to use:** Single-backend applications. The
|
||||
abstraction layer adds complexity that is only justified
|
||||
when you actually swap implementations.
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Keyword Validation with Reduce-While
|
||||
|
||||
**Source:** `lib/oban/validation.ex`
|
||||
**Category:** config
|
||||
|
||||
**What:** Validate keyword options by iterating with
|
||||
`Enum.reduce_while/3` and a validator function. Stop at
|
||||
first error. Return `:ok` or `{:error, reason}`.
|
||||
|
||||
**Why:** Keyword lists are the standard Elixir config
|
||||
format. Validating them procedurally (nested if/case) gets
|
||||
messy. The reduce-while + validator pattern is composable:
|
||||
each option validates independently, errors short-circuit,
|
||||
and the validator function can be swapped or extended.
|
||||
|
||||
**Example:**
|
||||
|
||||
```elixir
|
||||
def validate(opts, validator) when is_list(opts) do
|
||||
Enum.reduce_while(opts, :ok, fn opt, acc ->
|
||||
case validator.(opt) do
|
||||
:ok -> {:cont, acc}
|
||||
{:error, _} = error -> {:halt, error}
|
||||
end
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
**When to use:** Any public API that accepts keyword
|
||||
options from users. Libraries, GenServer init, plugin
|
||||
configs.
|
||||
|
||||
**When NOT to use:** Internal functions where the caller
|
||||
is trusted. Also avoid for deeply nested configs — use
|
||||
schema-based validation (NimbleOptions, Ecto embedded
|
||||
schemas) instead.
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Testing Mode Toggle
|
||||
|
||||
**Source:** `lib/oban/testing.ex`, `lib/oban/config.ex`
|
||||
**Category:** testing
|
||||
|
||||
**What:** Support a `testing:` config option that switches
|
||||
execution mode: `:disabled` (production), `:inline`
|
||||
(execute immediately in caller process), `:manual` (enqueue
|
||||
but don't execute — assert on DB state).
|
||||
|
||||
**Why:** Background job systems are inherently async, which
|
||||
makes testing hard. The mode toggle gives you: (1) inline
|
||||
for unit tests that need synchronous execution, (2) manual
|
||||
for integration tests that verify enqueueing without
|
||||
side effects.
|
||||
|
||||
**Example:**
|
||||
|
||||
```elixir
|
||||
# In test config:
|
||||
config :my_app, Oban, testing: :manual
|
||||
|
||||
# In tests:
|
||||
use Oban.Testing, repo: MyApp.Repo
|
||||
|
||||
perform_job(MyWorker, %{id: 1})
|
||||
assert_enqueued worker: MyWorker, args: %{id: 1}
|
||||
```
|
||||
|
||||
**When to use:** Any async system that needs deterministic
|
||||
testing — job queues, event buses, notification systems.
|
||||
The testing mode replaces "sleep and hope" with explicit
|
||||
control.
|
||||
|
||||
**When NOT to use:** Synchronous systems that are already
|
||||
deterministic. Also avoid if the mode toggle leaks into
|
||||
production code paths (keep it config-only, not conditional
|
||||
logic scattered through business code).
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Stopper for Goroutine Lifecycle (CockroachDB)
|
||||
|
||||
**Source:** `pkg/util/stop/stopper.go` (cockroachdb)
|
||||
**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: Atomic File Operations with Suffix Convention
|
||||
|
||||
**Source:** `tsdb/db.go` (prometheus)
|
||||
**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: Options as DefaultOptions() + Override
|
||||
|
||||
**Source:** `tsdb/db.go` (prometheus)
|
||||
**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` (prometheus)
|
||||
**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 the 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_COMPLETE -->
|
||||
Reference in New Issue
Block a user