Continuum v0.2 is a feature release that pays down v0.1 debt and adds the operability story (Observer, OpenTelemetry, snapshots). Public workflow code written for v0.1 keeps compiling and running unchanged. Operators and host applications have a small set of mechanical steps.
TL;DR
- Run the new migrations.
- Optionally opt into per-process repos with
Continuum.children/1. - Optionally opt into snapshots by setting
:snapshot_thresholdto a positive integer. - Read the Behavior changes section before scraping
continuum_eventsdirectly.
Workflow modules, activity modules, and the public API surface
(Continuum.start/3, signal/3,4, cancel/2, await/3, deterministic
primitives) are unchanged.
Database Migrations
v0.2 ships four new migrations on top of v0.1. They are designed to run cleanly on a fresh database and on the current local v0.1 dev/test schema. v0.1 had no public release, so there is no production-data compatibility promise: if your local v0.1 database has rows you care about, snapshot the database before running these.
Fresh installs (mix continuum.gen.migration) get the v0.2 shape directly and
do not need to run the four delta migrations.
In order:
20260601000000_partition_continuum_events— renames the existingcontinuum_eventstocontinuum_events_legacy, creates a parent tablePARTITION BY RANGE (inserted_at), creates the current month and the next three monthly partitions, copies legacy rows into the partitions, then drops the legacy table.20260601000001_create_continuum_activity_results— side table for activity idempotency keys ((activity_module, idempotency_key)PK).20260601000002_create_continuum_snapshots— opt-in history compaction table.20260601000003_add_trace_context_to_runs— nullablebyteacolumn oncontinuum_runsfor persisted W3Ctraceparentvalues.
Run them in order with mix ecto.migrate.
Behavior Changes That May Surprise Operators
continuum_events PK is now (run_id, seq, inserted_at)
Postgres partitioned tables require the partition key in the primary key.
v0.2's continuum_events is PARTITION BY RANGE (inserted_at) with
PRIMARY KEY (run_id, seq, inserted_at). Continuum still preserves the
v0.1 invariant that (run_id, seq) is globally unique per run by locking the
continuum_runs row before assigning seq. The SQL-level uniqueness was
relaxed to keep the partitioning shape clean — application code never relied
on the wider SQL guarantee.
If you have scripts that assert a unique index on (run_id, seq) directly,
update them to look for (run_id, seq, inserted_at).
signal_awaited is not journaled when a signal is already pending
In v0.1, await signal(:x) always journaled signal_awaited and then
signal_received when the signal arrived. In v0.2, when the signal is already
in the durable mailbox at the time of the await, Continuum journals only
signal_received and skips the timeout timer entirely.
Old runs that already journaled signal_awaited replay unchanged. Drift
detection still works. The change is only visible to operators scraping
continuum_events directly or aggregating event-type counts.
If a downstream dashboard counts signal_awaited rows as a proxy for "signal
arrivals", switch to signal_received (which is the actual arrival).
Helper-module determinism scan now warns
use Continuum.Workflow modules that call into helper modules which are not
stdlib-trusted, not marked use Continuum.Pure, and not listed in
config :continuum, trusted_modules: [...] now emit a compile-time warning
per untrusted module. To turn the warning into a compile error:
config :continuum, untrusted_call_severity: :errorSee guides/determinism-rules.md for the three trust mechanisms.
mix continuum.gen.migration generates the v0.2 shape
Newly generated migrations include the partitioned continuum_events, the
trace_context column on continuum_runs, the activity-results side table,
and the snapshots table. Existing migrations are untouched.
Optional: Per-Process Repos
v0.2 introduces named Continuum instances. Each instance has its own registry, run supervisor, dispatchers, timer wheel, signal router, lease heartbeater, snapshotter, and recovery process, bound to its own Ecto repo.
You do not have to opt in. The default instance (Continuum) reads
config :continuum, :repo exactly as before. To run more than one instance:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
MyApp.Repo,
MyApp.Billing.Repo
] ++ Continuum.children(name: :billing_continuum, repo: MyApp.Billing.Repo)
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
endThen pass instance: :billing_continuum to Continuum.start/3, signal/4,
cancel/2, and await/3. See guides/multi-instance.md for the full surface.
Optional: Snapshots
Snapshots are experimental in v0.2 and disabled by default. To opt in for a long-history workflow:
config :continuum,
snapshot_threshold: 200, # take a snapshot every ~200 new events
snapshot_max_size_bytes: 1_000_000:infinity (the default) disables snapshots entirely. See
guides/snapshots.md for what snapshots are, what they are not, and when to
turn them on.
Optional: OpenTelemetry
Continuum core compiles and runs without OpenTelemetry installed. To export spans, add and configure OTel in your application, then call:
{:ok, _handler_id} = Continuum.OpenTelemetry.setup()The bridge produces short continuum.run_attempt and continuum.activity_attempt
spans; resumed run attempts link back to the original trace via the new
continuum_runs.trace_context column. See guides/observability.md.
Optional: Observer
Mount the Phoenix LiveView Observer behind your existing authentication pipeline:
import Continuum.Observer.Router
scope "/admin" do
pipe_through [:browser, :authenticate_admin]
continuum_observer "/continuum", instance: :myapp_continuum
endContinuum's own dependencies do not transitively pull in Phoenix LiveView for
host apps. Add :phoenix_live_view and :phoenix_html to your app if you
mount the Observer. See guides/observer.md.
Partition Maintenance
continuum_events is now partitioned monthly. v0.2 ships three Mix tasks for
operator-driven partition maintenance — there is no runtime partition manager:
mix continuum.partitions.create [YYYY-MM]— create a single monthly partition. Idempotent.mix continuum.partitions.list— print current partitions and row counts.mix continuum.partitions.drop_old [--execute]— report partitions older than retention by default; drop them only with--execute.
retention_until on continuum_runs is opt-in and remains NULL by default.
drop_old is a no-op when no run has retention set.
What Did Not Change
- The public macro surface:
use Continuum.Workflow,activity,await signal,timer,seconds/minutes/hours/days,Continuum.now/0,today/0,uuid4/0,random/0,side_effect/1. - Activity definition (
use Continuum.Activity,idempotency_key/1). - Run lifecycle, lease/fencing semantics, drift detection, and the engine replay loop. v0.1 runs in flight at the time of upgrade resume on v0.2 code and complete the same way.
- Telemetry event names. v0.2 adds new events
(
[:continuum, :snapshot, :taken|:skipped],[:continuum, :activity, :idempotency_hit]); the v0.1 names are unchanged.