All notable changes to gen_durable are documented here. The format follows Keep a Changelog; this project is pre-1.0 and makes no backward-compatibility guarantees — there is one schema version and migrations are edited in place until the MVP settles.

0.1.8

Added

  • Rate limiting — token bucket, per step (spec §12). A step opts into a named limit by returning {:next, step, state, rate_limit: :stripe} (or {:stripe, partition} for a bucket per tenant). Configured at start: rate_limits: [stripe: [allowed: 100, period: {1, :minute}]] (burst defaults to allowed). Enforced in the picker (one statement) by a per-bucket token counter locked with FOR UPDATE — correct across nodes, and measured to cost nothing on the common path (NULL rate_limit short-circuits; the rate CTEs are never executed).
  • Weighted steps. {:next, step, state, rate_limit: :stripe, weight: 50} — a step may consume more than one budget unit. Grants take the urgency prefix whose cumulative weight fits (strict order, free head-of-line reservation). weight ≤ burst is the caller's responsibility and is not validated — a too-fat step freezes its bucket; split the step instead.
  • New tables gen_durable_rate_configs / gen_durable_rate_buckets; new gen_durable.rate_limit and gen_durable.weight columns. :next now normalizes to a 4-tuple carrying a per-transition opts map (rate_limit, weight). insert, insert_all, and schedule_childs children all carry the columns and ensure their buckets.
  • Telemetry: [:gen_durable, :rate_limit, :throttled] (a bucket granted fewer than wanted) and [:gen_durable, :rate_limit, :unknown] (a step named an unconfigured rate-limit).

Deliberately not added (settled decisions)

  • Weighted-step poison guard (weight ≤ burst): a too-fat step freezes its bucket — the caller's responsibility (split the step), consistent with the engine's "you own correctness" stance.
  • Sliding/fixed-window rate algorithms: token bucket only (its rate+burst knobs cover the spectrum; sliding-log breaks single-row locking).
  • Boot-time validation / on_unknown policy: an unknown rate-limit key stalls the row and emits [:gen_durable, :rate_limit, :unknown] — that is the chosen v1 behaviour (no fail-fast at boot, no :run/:stop/:defer knob).

0.1.7

Changed

  • Renamed partition_keyconcurrency_key (column, insert option, SQL, the gen_durable_concurrency_active index, and the [:gen_durable, :concurrency, :contended] telemetry event). Pure rename; the SQL PARTITION BY window keyword is untouched.

0.1.6

Changed

  • Dropped the :unique policy enum (:live/:global). correlation_scope (a durable_status[]) is now passed directly, defaulting to the non-terminal statuses. This also removes the surprising built-in :global behaviour where a finished instance silently swallowed signals.

0.1.5

Changed

  • Renamed the same-step outcome :replay:retry (it redoes the step with attempt += 1 after a delay; the old name read like event-sourcing replay).
  • Merged addressing and uniqueness into one correlation_key (Temporal/DBOS workflow-id model): the business key you signal/4 by is the same key the engine deduplicates on. Replaces the separate external_id (addressing) and unique_key/unique_scope (dedup). One partial unique index does both jobs.
  • Dropped the misleading "on top of GenServer" framing: an FSM is a row, not a process — there is no GenServer per instance; the runtime backbone (scheduler/reaper/GC) is a small set of GenServers.

0.1.4

Added

  • Built-in GC of terminal (done/failed) rows: GenDurable.GC, configurable :gc_interval / :gc_retention (default 1 day) / :gc_batch; [:gen_durable, :gc, :swept] telemetry. The delete is O(batch) (select ids, then DELETE … WHERE id = ANY), not O(table).

0.1.3

Changed

  • await waits on a set of signal names; the woken step sees the matched subset as ctx.awaited (full inbox in ctx.all). Consumption is by received id on progress (latecomers survive; packs can accumulate via re-await); a terminal outcome clears the whole inbox.

0.1.2

Added

  • Job form: define perform/1/perform/2 instead of step/2 for a one-shot durable job with built-in retry/backoff. Folded into GenDurable.FSM.

0.1.1

Added

  • Nested State embedded-schema adopted by convention (no state: option needed).

0.1.0

Added

  • Initial durable FSM engine: Postgres-backed, state committed before each step proceeds (at-least-once, whole-step re-execution). Steps and outcomes (:next/:retry/:await/:done/ :stop), schedule_childs fan-out + fan-in barrier (§11), durable signals/await (§5), queues with concurrency, priority, scheduling sugar, lease + reaper crash recovery, concurrency_key serialization, uniqueness, single-round-trip outcomes, feeder backpressure, graceful drain, broad telemetry, dynamic FSM resolution, library-owned migration.