Continuum (continuum v0.6.2)

Copy Markdown View Source

OTP-native durable execution engine for Elixir.

Continuum lets you write a multi-step business process as straight-line Elixir code. The process survives crashes, node restarts, and partitions: the engine journals each effect to Postgres and replays the workflow's history through the same orchestration code on resume.

See Continuum.Workflow for the workflow DSL and Continuum.Activity for activities (the only place side effects are allowed inside a workflow).

Public API

Summary

Functions

Block until the run completes. Test/synchronous use only.

Cancel a running workflow.

Returns runtime child specs for a Continuum instance.

Drains one Continuum runtime without stopping its supervisor.

Returns true after a successful bounded drain has completed.

Load one durable run by id.

Whether we are currently executing inside a workflow process. Useful in helper modules that branch on context.

The current wall-clock time, journaled and replayed deterministically.

Journaled patch marker for in-place, backward-compatible workflow changes.

Query durable runs with a closed, structured query spec.

Query durable runs for a named Continuum instance.

A pseudo-random float in [0, 1), journaled and replayed deterministically.

Returns the local lifecycle and claim-readiness state for one runtime.

Returns true only while the local runtime accepts new run claims.

Merge JSON-encodable search attributes into a durable run row.

General-purpose escape hatch for an impure read whose result must be journaled and replayed.

Deliver a signal to a running workflow. Signal payloads must not contain PIDs, references, ports, or functions.

Deliver a signal to a running workflow, selecting a Continuum instance with :instance. Signal payloads follow the same durable-term restrictions as workflow inputs.

Start a new workflow run.

The current UTC date, journaled and replayed deterministically.

Recover an activity's raw return value from a compensation handle.

A v4 UUID, journaled and replayed deterministically.

Types

input()

@type input() :: term()

run_id()

@type run_id() :: binary()

workflow_module()

@type workflow_module() :: module()

Functions

await(run_id, timeout \\ 5000, opts \\ [])

@spec await(run_id(), timeout(), keyword()) :: {:ok, map()} | {:error, term()}

Block until the run completes. Test/synchronous use only.

Failed workflows return a %Continuum.RunFailure{} in the result map's :error field. Diagnostic stacktraces are intentionally omitted; use get_run/2 to inspect the separate :error_stacktrace field.

cancel(run_id, opts \\ [])

@spec cancel(
  run_id(),
  keyword()
) :: :ok | {:error, term()}

Cancel a running workflow.

Cancelling the root of a continue_as_new chain cancels the live tip. When the run's engine is alive on another reachable node, the cancel is forwarded to it. If the owner holds a live lease but no engine can be reached (partition, overload), the request is recorded durably and honored by the owning engine on its next lease heartbeat — the call returns {:error, :owned_elsewhere} so the caller knows cancellation is pending rather than complete.

children(opts \\ [])

@spec children(keyword()) :: [Supervisor.child_spec()]

Returns runtime child specs for a Continuum instance.

For the default instance, add these children to the host application's supervision tree after its Ecto repo:

children =
  [
    MyApp.Repo
  ] ++ Continuum.children()

Named instances additionally own their isolated PubSub, registry, and run supervisor:

children =
  [
    MyApp.Repo,
    Continuum.children(name: :billing_continuum, repo: MyApp.Repo)
  ]

Continuum.Application owns the default instance's Repo-independent base processes. Continuum.children() returns only its Repo-dependent runtime children, so it does not duplicate those process names.

A named instance given a :repo uses the Postgres journal for every run started, signalled, cancelled, or awaited through it; pass :journal to override. The default instance follows config :continuum, :journal.

Child-specific options may be passed with :workflow_modules, :activity_executor, :activity_max_concurrency, :heartbeater, :run_supervisor, :activity_supervisor, :recovery, :dispatcher, :activity_dispatcher, :timer_wheel, :signal_router, :snapshotter, and :partition_maintainer. Partition maintenance is disabled unless :partition_maintainer is true or an option list because it requires runtime DDL privileges. Passing false for a child omits it from the returned list.

drain(opts \\ [])

@spec drain(keyword()) :: {:ok, map()} | {:error, term()}

Drains one Continuum runtime without stopping its supervisor.

Readiness becomes false before new run claims are paused. Locally owned workflow engines then get the configured heartbeater drain deadline to release their leases; overdue engines are stopped and fenced. Repeated calls are idempotent and return the same completed drain summary.

Pass instance: name for a named runtime and timeout: milliseconds to override its configured :drain_timeout_ms for this drain.

drained?(opts \\ [])

@spec drained?(keyword()) :: boolean()

Returns true after a successful bounded drain has completed.

get_run(run_id, opts \\ [])

@spec get_run(
  run_id(),
  keyword()
) :: {:ok, map()} | {:error, :not_found | term()}

Load one durable run by id.

in_workflow?()

@spec in_workflow?() :: boolean()

Whether we are currently executing inside a workflow process. Useful in helper modules that branch on context.

now()

(macro)

The current wall-clock time, journaled and replayed deterministically.

patched?(patch_name)

(since 0.3.0) (macro)

Journaled patch marker for in-place, backward-compatible workflow changes.

def run(input) do
  if Continuum.patched?(:add_fraud_check_v2) do
    activity FraudCheck.v2(input)
  else
    activity FraudCheck.v1(input)
  end
end

Inside a workflow the first decision for patched?(name) is memoized for the run. A live decision journals a patched event with value: true; histories recorded before the patch line existed decide false without consuming an event. Every later call with the same name returns that first decision, including calls at another source line or after a suspension.

Outside a workflow process (test setup, ordinary code) it returns false.

Like now/0 and uuid4/0, this is a macro so it captures __CALLER__ for a stable command identity; modules that call it must require Continuum (use Continuum.Workflow does this for you).

query(opts \\ [])

@spec query(keyword()) :: {:ok, map()} | {:error, term()}

Query durable runs with a closed, structured query spec.

See Continuum.Query for supported :where, :order_by, and pagination options. Querying requires a Postgres-backed Continuum instance.

query(instance, opts)

@spec query(
  atom() | Continuum.Runtime.Instance.t(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Query durable runs for a named Continuum instance.

random()

(macro)

A pseudo-random float in [0, 1), journaled and replayed deterministically.

readiness(opts \\ [])

@spec readiness(keyword()) :: map()

Returns the local lifecycle and claim-readiness state for one runtime.

The state is one of :ready, :draining, :drained, :degraded, or :not_started. A degraded drain is not ready and reports a non-zero unreleased_count in its last drain summary.

ready?(opts \\ [])

@spec ready?(keyword()) :: boolean()

Returns true only while the local runtime accepts new run claims.

set_attributes(run_id, attributes, opts \\ [])

@spec set_attributes(run_id(), map(), keyword()) :: :ok | {:error, term()}

Merge JSON-encodable search attributes into a durable run row.

This is external metadata. It is not journaled and workflow code cannot read it during replay.

side_effect(fun)

(macro)

General-purpose escape hatch for an impure read whose result must be journaled and replayed.

The function is called once on first execution; its return value is journaled and returned on every subsequent replay. Return values must be replay-safe across processes and nodes. PIDs, references, ports, and functions are rejected recursively with a path-aware Continuum.DurableTermError.

This is a macro so Continuum can capture the source call site for a stable command identity. Workflow modules that use Continuum.Workflow already require Continuum; other modules must require Continuum before calling it.

Helper-module caveat

Command identity includes the call site's module and line. Inside a workflow module that is safe: any edit changes the version hash and in-flight runs keep resuming through the old version's entrypoint. A Continuum.Pure helper module has no such protection — editing a helper so that a side_effect call moves to a different line changes its command identity and in-flight runs replaying through it raise Continuum.ReplayDriftError on the next deploy. Prefer keeping side_effect calls in the workflow module itself.

signal(run_id, name, payload)

@spec signal(run_id(), atom(), term()) :: :ok | {:error, term()}

Deliver a signal to a running workflow. Signal payloads must not contain PIDs, references, ports, or functions.

signal(run_id, name, payload, opts)

@spec signal(run_id(), atom(), term(), keyword()) :: :ok | {:error, term()}

Deliver a signal to a running workflow, selecting a Continuum instance with :instance. Signal payloads follow the same durable-term restrictions as workflow inputs.

start(workflow_module, input, opts \\ [])

@spec start(workflow_module(), input(), keyword()) ::
  {:ok, run_id()} | {:error, term()}

Start a new workflow run.

Options include :instance for selecting a named Continuum instance, :namespace for soft tenant scoping of list/query paths, :trace_context for persisting an opaque W3C traceparent binary that observability integrations can use to link resumed run attempts, and :attributes for JSON-encodable search metadata stored on the run row.

Inputs cross a journal boundary and must not contain PIDs, references, ports, or functions. Continuum.DurableTermError reports the path to an invalid nested value before the run is inserted.

today()

(macro)

The current UTC date, journaled and replayed deterministically.

unwrap(other)

(since 0.3.0)
@spec unwrap(term()) :: term()

Recover an activity's raw return value from a compensation handle.

When an activity/2 call uses compensate:, a success is returned as {:ok, %Continuum.ActivityRef{}} rather than a bare term. unwrap/1 peels the ref back to the activity's raw return:

  • unwrap(%Continuum.ActivityRef{raw_result: raw})raw
  • unwrap({:ok, %Continuum.ActivityRef{} = ref})ref.raw_result
  • unwrap(other)other (activities without compensate: are unchanged)

uuid4()

(macro)

A v4 UUID, journaled and replayed deterministically.