StatifierPersistence.Run.Linkage (StatifierPersistence v0.4.0)

Copy Markdown View Source

A durable subchart child's parent linkage: the reserved, package-owned namespace inside a run's metadata (ADR-0008 decision 2).

ADR-0006 decision 1 says this package never reads a metadata key to make a decision. ADR-0008 decision 2 narrows that, and narrows it exactly this far: linkage lives under one reserved top-level key - "statifier_persistence", this module's reserved_key/0 - this package reads only that key, and everything outside it stays as opaque as it was: never read, never validated beyond shape, never merged into a blob. ADR-0006 decision 2 is untouched: every value here is an identity (a run id, an invocation id, a content hash) and never personal data.

The stored shape, under the reserved key:

%{
  "parent_run_id" => "run_42",
  "invoke_id" => "call",
  "child_index" => 0,
  "content_hash" => "sha256:..."
}

content_hash is the mandatory pin ADR-0008 decision 2 hardens into contract: the same hash Statifier.Machine.identity/1 produces for the child's own chart, recorded a second time where the parent-child relationship can see it. A child is resumed by whatever node picks it up, and this is what stands between "resumed the workflow you started" and "resumed a different workflow that happens to share an id" - without the pin, a reused or collided run id would let a node silently guard a child's own load against the wrong chart's identity while still believing it is answering the right parent.

child_index is 0 for every child this version creates. It is recorded anyway because ADR-0008 decision 7 requires the linkage not to assume one child per invocation; nothing here fans out.

Summary

Types

The reserved top-level metadata key this module owns.

t()

Functions

Derives a child run id from its parent and invocation - the single definition site of the id shape (see the plan's "Implementation Approach")

Reads a linkage back out of a stored run's metadata.

The same containment map, narrowed to one invocation - what a cancel of a single invocation walks, rather than every child a parent has ever started.

Builds a linkage struct from its four values. Does not derive a child run id and does not touch storage - child_run_id/3 and to_metadata/1 do that separately, so a caller that only needs the id shape never has to fabricate a content_hash it does not yet have.

The containment map a cascade queries Storage.list_runs_by_metadata/2 with to find every child of parent_run_id, whatever invocation started it and whatever its child_index.

The one definition site for the reserved metadata namespace.

The reserved-namespace metadata map for linkage, string keys and JSON-representable values only, so the Ecto adapter's jsonb check passes and the map is safe to merge into a metadata: option unchanged.

Types

reserved_key()

@type reserved_key() :: String.t()

The reserved top-level metadata key this module owns.

t()

@type t() :: %StatifierPersistence.Run.Linkage{
  child_index: non_neg_integer(),
  content_hash: StatifierPersistence.Storage.Adapter.content_hash(),
  invoke_id: String.t(),
  parent_run_id: StatifierPersistence.Storage.Adapter.run_id()
}

Functions

child_run_id(parent_run_id, invoke_id, child_index)

@spec child_run_id(
  parent_run_id :: StatifierPersistence.Storage.Adapter.run_id(),
  invoke_id :: String.t(),
  child_index :: non_neg_integer()
) :: StatifierPersistence.Storage.Adapter.run_id()

Derives a child run id from its parent and invocation - the single definition site of the id shape (see the plan's "Implementation Approach"):

parent_run_id <> "/" <> invoke_id <> "/" <> Integer.to_string(child_index)

Deterministic in every input, which buys idempotency across ADR-0004 decision 3's at-least-once re-drive: the same crash-and-retry recomputes the same id, so the adapter's atomic :run_exists refusal is what answers the re-drive rather than a second child being created. The result strictly extends parent_run_id as a string, which is the acyclicity property the cascade in Phase 5 rests on: no run can be its own descendant, because every descendant's id is strictly longer than its ancestor's.

from_metadata(metadata)

@spec from_metadata(StatifierPersistence.Storage.Adapter.metadata()) ::
  {:ok, t()} | :no_linkage

Reads a linkage back out of a stored run's metadata.

:no_linkage for a run whose metadata carries none - a %{} metadata map, a host map with unrelated keys, or an adapter that does not store metadata at all. Not an {:error, _}: having no parent is an ordinary property of a run, not a failure, and every run this package has ever created before this feature has none.

invocation_match(parent_run_id, invoke_id)

@spec invocation_match(
  parent_run_id :: StatifierPersistence.Storage.Adapter.run_id(),
  invoke_id :: String.t()
) :: StatifierPersistence.Storage.Adapter.metadata()

The same containment map, narrowed to one invocation - what a cancel of a single invocation walks, rather than every child a parent has ever started.

new(parent_run_id, invoke_id, child_index, content_hash)

@spec new(
  parent_run_id :: StatifierPersistence.Storage.Adapter.run_id(),
  invoke_id :: String.t(),
  child_index :: non_neg_integer(),
  content_hash :: StatifierPersistence.Storage.Adapter.content_hash()
) :: t()

Builds a linkage struct from its four values. Does not derive a child run id and does not touch storage - child_run_id/3 and to_metadata/1 do that separately, so a caller that only needs the id shape never has to fabricate a content_hash it does not yet have.

parent_match(parent_run_id)

The containment map a cascade queries Storage.list_runs_by_metadata/2 with to find every child of parent_run_id, whatever invocation started it and whatever its child_index.

reserved_key()

@spec reserved_key() :: reserved_key()

The one definition site for the reserved metadata namespace.

to_metadata(linkage)

The reserved-namespace metadata map for linkage, string keys and JSON-representable values only, so the Ecto adapter's jsonb check passes and the map is safe to merge into a metadata: option unchanged.