Migrating from v0.1 to v0.2

Copy Markdown View Source

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

  1. Run the new migrations.
  2. Optionally opt into per-process repos with Continuum.children/1.
  3. Optionally opt into snapshots by setting :snapshot_threshold to a positive integer.
  4. Read the Behavior changes section before scraping continuum_events directly.

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:

  1. 20260601000000_partition_continuum_events — renames the existing continuum_events to continuum_events_legacy, creates a parent table PARTITION 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.
  2. 20260601000001_create_continuum_activity_results — side table for activity idempotency keys ((activity_module, idempotency_key) PK).
  3. 20260601000002_create_continuum_snapshots — opt-in history compaction table.
  4. 20260601000003_add_trace_context_to_runs — nullable bytea column on continuum_runs for persisted W3C traceparent values.

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: :error

See 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
end

Then 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
end

Continuum'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.