C1 — Backoff policies as pure functions.
Every policy returns a plain fn of attempt → delay in milliseconds, where
attempt is the 1-based failure count ((retry-fn 1) is the delay after
the first failure). This matches the contract d-core.libs.retriable
already documents.
Delays are always long milliseconds. This is the settled decision for
this pillar: it matches d-core.libs.retriable (Thread/sleep), and it maps
onto both existing ecosystem implementations with no conversion, which is what
makes their migration provably behaviour-preserving (see
backoff-test/matches-async-proxy-compute-backoff-ms and
backoff-test/matches-http-calc-backoff-ms). java.time.Duration remains the
vocabulary of Pillar A at the boundary; this policy layer stays in ms.
:jitter accepts either a keyword or a number:
| Value | Distribution |
|---|---|
:none | no jitter — fully deterministic |
:full | uniform [0, d) — AWS "full jitter" |
:equal | uniform [d/2, d) — AWS "equal jitter" |
<number> k | uniform [d-(k·d), d+(k·d)] — symmetric/proportional |
The numeric form exists because it is exactly what
d-core-http/.../client.clj already does with :jitter 0.2, so that call
site can migrate without changing its observed delay distribution.
:rand-fn is an injection point (a 0-arg fn returning [0.0, 1.0), default
rand). It is mandatory whenever jitter is active — a jittered policy
without it would be untestable, and silently defaulting would make the
dependency invisible at the call site.
:max-ms caps the delay before jitter is applied. Consequently:
:none, :full or :equal, the observed delay never exceeds the cap
(:full is in fact strictly below it);k, the observed delay is bounded by
max-ms · (1 + k), i.e. the cap bounds the jitter centre, not the
absolute ceiling. This is deliberate: it is the behaviour of the existing
d-core-http implementation, and changing it would change the retry
envelope of a live client.exponential-backoff, linear-backoff and constant-backoff are pure: the
returned fn depends only on its attempt argument.
decorrelated-jitter is inherently stateful (its next delay depends on the
previous draw, not on attempt), so it is deliberately not an
attempt -> ms fn. It is exposed as a pure state transducer — see its
docstring — rather than bending the common contract and hiding an atom.
C1 — Backoff policies as pure functions. Every policy returns a plain fn of `attempt` → delay in milliseconds, where `attempt` is the **1-based failure count** (`(retry-fn 1)` is the delay after the first failure). This matches the contract `d-core.libs.retriable` already documents. ## Units Delays are **always `long` milliseconds**. This is the settled decision for this pillar: it matches `d-core.libs.retriable` (`Thread/sleep`), and it maps onto both existing ecosystem implementations with no conversion, which is what makes their migration provably behaviour-preserving (see `backoff-test/matches-async-proxy-compute-backoff-ms` and `backoff-test/matches-http-calc-backoff-ms`). `java.time.Duration` remains the vocabulary of Pillar A at the *boundary*; this policy layer stays in ms. ## Jitter `:jitter` accepts either a keyword or a number: | Value | Distribution | | :----------- | :-------------------------------------------------- | | `:none` | no jitter — fully deterministic | | `:full` | uniform `[0, d)` — AWS "full jitter" | | `:equal` | uniform `[d/2, d)` — AWS "equal jitter" | | `<number> k` | uniform `[d-(k·d), d+(k·d)]` — symmetric/proportional | The numeric form exists because it is exactly what `d-core-http/.../client.clj` already does with `:jitter 0.2`, so that call site can migrate without changing its observed delay distribution. `:rand-fn` is an injection point (a 0-arg fn returning `[0.0, 1.0)`, default `rand`). It is **mandatory whenever jitter is active** — a jittered policy without it would be untestable, and silently defaulting would make the dependency invisible at the call site. ## Cap semantics `:max-ms` caps the delay *before* jitter is applied. Consequently: - with `:none`, `:full` or `:equal`, the observed delay never exceeds the cap (`:full` is in fact strictly below it); - with numeric jitter `k`, the observed delay is bounded by `max-ms · (1 + k)`, i.e. the cap bounds the jitter *centre*, not the absolute ceiling. This is deliberate: it is the behaviour of the existing `d-core-http` implementation, and changing it would change the retry envelope of a live client. ## Purity `exponential-backoff`, `linear-backoff` and `constant-backoff` are pure: the returned fn depends only on its `attempt` argument. `decorrelated-jitter` is *inherently stateful* (its next delay depends on the previous draw, not on `attempt`), so it is deliberately **not** an `attempt -> ms` fn. It is exposed as a pure state transducer — see its docstring — rather than bending the common contract and hiding an atom.
Compact, high-signal stack traces (Roadmap 05, idea D1).
A failed assertion or exception usually produces a wall of java.*, clojure.lang.*,
nREPL and generated invokeStatic frames, burying the two frames that actually name your
code. This namespace is split hard between classification and printing:
frame-category and interesting-frame?
answer "is this frame mine?" from plain data, no exceptions required;clean-stacktrace returns the surviving frames (never truncates);format-stacktrace renders them, eliding the middle so the throw site and the
entry point stay visible.Detail level resolves as: explicit :detail/:max-frames opts > *stacktrace-detail*
D_CORE_STACKTRACE_DETAILenv var >:normal.
Filters are overridable per call via :keep / :drop predicates over a normalized frame
map, so nobody debugging clojure.lang internals is stuck with our taste.
Your own code is recognised by namespace prefix. d_core/d-core are built in; a different
project registers its prefix once, with no code required:
D_CORE_PROJECT_NS=my-app,my-app-workers clojure -M:test
or at runtime with register-project-ns!, or per call with :extra-project.
Compact, high-signal stack traces (Roadmap 05, idea D1).
A failed assertion or exception usually produces a wall of `java.*`, `clojure.lang.*`,
nREPL and generated `invokeStatic` frames, burying the two frames that actually name your
code. This namespace is split hard between *classification* and *printing*:
- classification is pure and table-driven: [[frame-category]] and [[interesting-frame?]]
answer "is this frame mine?" from plain data, no exceptions required;
- [[clean-stacktrace]] returns the surviving frames (never truncates);
- [[format-stacktrace]] renders them, eliding the middle so the throw site *and* the
entry point stay visible.
Detail level resolves as: explicit `:detail`/`:max-frames` opts > [[*stacktrace-detail*]]
> `D_CORE_STACKTRACE_DETAIL` env var > `:normal`.
Filters are overridable per call via `:keep` / `:drop` predicates over a normalized frame
map, so nobody debugging `clojure.lang` internals is stuck with our taste.
Your own code is recognised by namespace prefix. `d_core`/`d-core` are built in; a different
project registers its prefix once, with no code required:
D_CORE_PROJECT_NS=my-app,my-app-workers clojure -M:test
or at runtime with [[register-project-ns!]], or per call with `:extra-project`.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |