StatifierPersistence.Storage.InMemory (StatifierPersistence v0.15.1)

Copy Markdown View Source

The reference StatifierPersistence.Storage.Adapter: an Agent holding three maps - charts keyed by content hash, positions keyed by session id, and executions keyed by execution id.

The chart map is keyed by the content hash alone, as every adapter's is: byte-identical charts stored by two tenants are one entry, and tenant scoping is the host's own (StatifierPersistence.Storage's moduledoc).

It ships in lib/, not the test-only support/ directory, for two reasons: the conformance template this package ships in lib/ (this package's own Testing namespace) needs a reference implementation to check against from outside this repository's own test/, and a host prototyping the stepper wants an adapter with no database to stand up.

Summary

Types

This adapter's state: the three record maps init/1 starts the Agent with, plus the per-execution lock table lock_execution/3 acquires through.

Functions

Counts the executions on content_hash per stored arm (the optional StatifierPersistence.Storage.Adapter.count_executions_by_content_hash/2, ADR-0012 decision 3).

Fetches the chart stored under content_hash, or :chart_not_found.

Fetches the execution stored under execution_id, or :execution_not_found.

Fetches the position stored for session_id, or :position_not_found.

Reads the tombstone on content_hash, or nil for a hash that has none (the optional StatifierPersistence.Storage.Adapter.fetch_retired_info/2).

Starts the backing Agent and returns opts with :pid merged in - the handle every other callback expects as its first argument.

Inserts execution_record under its execution_id, refusing a duplicate with {:error, :execution_exists}.

Lists the ids of the :active executions on content_hash (the optional StatifierPersistence.Storage.Adapter.list_active_execution_ids_by_content_hash/2, ADR-0012 decision 4): what a pin source is handed as its context.

Lists the executions whose stored metadata contains every key/value pair in metadata, recursively for a nested map (the optional StatifierPersistence.Storage.Adapter.list_executions_by_metadata/2, ADR-0008 decision 5) - the same subset semantics StatifierPersistence.Storage.Ecto's jsonb @> gives, and the same ArgumentError on an empty or non-string-keyed map.

Runs fun under this adapter's per-execution mutual exclusion for execution_id (the optional StatifierPersistence.Storage.Adapter.lock_execution/3).

Retires the chart on content_hash (the optional StatifierPersistence.Storage.Adapter.retire_chart/3, ADR-0012 decisions 5 and 6).

Stores chart_record under its content_hash, idempotent on repeat writes of the same hash.

Stores position_record under its session_id, overwriting any position already stored for that session.

Declares chart retirement (the optional StatifierPersistence.Storage.Adapter.supports_chart_retirement?/1, ADR-0012 decision 6): an Agent's map has no NOT NULL to drop, so the backend limit V07 records on a database adapter has no counterpart here.

Declares the drained query (the optional StatifierPersistence.Storage.Adapter.supports_content_hash_query?/1): this adapter holds every execution record in one map and can count them.

Declares outcome support (the optional StatifierPersistence.Storage.Adapter.supports_execution_outcome?/1): this adapter keeps the blob on the execution record like every other field.

Declares metadata support (the optional StatifierPersistence.Storage.Adapter.supports_metadata?/1): this adapter stores the map with the execution record and returns it verbatim (ADR-0006 decision 3).

Declares the tree migration unit (the optional StatifierPersistence.Storage.Adapter.supports_tree_migration?/1, ADR-0015 decision 3).

Overwrites the execution stored under execution_record's execution_id with the full record, or refuses with :execution_not_found when no execution exists for the id.

Writes a tree migration's re-pins and parks as one unit (the optional StatifierPersistence.Storage.Adapter.write_tree_migration/2, ADR-0015 decision 3).

Types

state()

This adapter's state: the three record maps init/1 starts the Agent with, plus the per-execution lock table lock_execution/3 acquires through.

Functions

count_executions_by_content_hash(opts, content_hash)

Counts the executions on content_hash per stored arm (the optional StatifierPersistence.Storage.Adapter.count_executions_by_content_hash/2, ADR-0012 decision 3).

Every key is present for every hash, so an unknown one answers zeros. A fold over the execution map is what this adapter has - an Agent holds no index - so this is the reference implementation of the contract, not of the aggregate the contract exists for; the Ecto adapter is where the count is a grouped count.

children counts the durable-child linkage pins naming content_hash whose parent execution is :active or :needs_migration (ADR-0012 decision 1, as ADR-0014 decision 4 reads it): a second pass over the same map, reading each execution's reserved metadata key through StatifierPersistence.Execution.Linkage.from_metadata/1 and looking its parent up by execution id.

fetch_chart(opts, content_hash)

Fetches the chart stored under content_hash, or :chart_not_found.

A tombstoned hash answers {:error, {:chart_retired, info}} instead (ADR-0012 decision 6): the entry is still there and its blobs are nil, and an entry with nil blobs is not a chart this adapter holds.

fetch_execution(opts, execution_id)

Fetches the execution stored under execution_id, or :execution_not_found.

fetch_position(opts, session_id)

Fetches the position stored for session_id, or :position_not_found.

fetch_retired_info(opts, content_hash)

Reads the tombstone on content_hash, or nil for a hash that has none (the optional StatifierPersistence.Storage.Adapter.fetch_retired_info/2).

The Agent answers with the tombstone alone rather than the stored entry, so the reply carries no chart bytes.

init(opts)

Starts the backing Agent and returns opts with :pid merged in - the handle every other callback expects as its first argument.

insert_execution(opts, execution_record)

Inserts execution_record under its execution_id, refusing a duplicate with {:error, :execution_exists}.

The exists-check and the write run inside one Agent.get_and_update/2 call, so they are a single atomic state transition: two concurrent inserts of the same execution_id cannot both return :ok.

This adapter supports the optional metadata map (ADR-0006 decision 3): the map is stored with the record and returned by fetch_execution/2 verbatim, whatever Elixir terms it holds - an Agent has no type system to refuse one.

list_active_execution_ids_by_content_hash(opts, content_hash)

Lists the ids of the :active executions on content_hash (the optional StatifierPersistence.Storage.Adapter.list_active_execution_ids_by_content_hash/2, ADR-0012 decision 4): what a pin source is handed as its context.

A filter over the same execution map the counts fold over, in the order the map yields, which is no order a caller may rely on: the contract is a list of ids, not a sequence.

list_execution_states_by_metadata(opts, metadata)

The status projection over a metadata match (the optional StatifierPersistence.Storage.Adapter.list_execution_states_by_metadata/2).

The same containment list_executions_by_metadata/2 applies, projected down to the three StatifierPersistence.Storage.Adapter.execution_state/0 fields. There is no index to serve it from here - an Agent holds a map - so this is the reference implementation of the contract, not of the performance the contract exists for; the Ecto adapter is where the projection is a projection.

list_executions_by_metadata(opts, metadata)

Lists the executions whose stored metadata contains every key/value pair in metadata, recursively for a nested map (the optional StatifierPersistence.Storage.Adapter.list_executions_by_metadata/2, ADR-0008 decision 5) - the same subset semantics StatifierPersistence.Storage.Ecto's jsonb @> gives, and the same ArgumentError on an empty or non-string-keyed map.

lock_execution(opts, execution_id, fun)

@spec lock_execution(
  StatifierPersistence.Storage.Adapter.opts(),
  StatifierPersistence.Storage.Adapter.execution_id(),
  (-> result)
) :: {:ok, result} | {:error, StatifierPersistence.Storage.Adapter.error()}
when result: term()

Runs fun under this adapter's per-execution mutual exclusion for execution_id (the optional StatifierPersistence.Storage.Adapter.lock_execution/3).

Acquisition is an insert-if-absent on the Agent's lock table, one atomic Agent.get_and_update/2 transition; contention spins with a small bounded sleep (5ms) between attempts. The lock is released in an after block, so any exit from fun - a raise included - releases it; the raise itself propagates to the caller.

Simple and honest for a reference adapter. A production adapter should prefer its backend's native lock - the Ecto adapter implements this as a transaction-scoped advisory-plus-row lock (ADR-0004 decision 5 as amended 2026-08-22).

retire_chart(opts, content_hash, retirement)

Retires the chart on content_hash (the optional StatifierPersistence.Storage.Adapter.retire_chart/3, ADR-0012 decisions 5 and 6).

The counts and the tombstone are one Agent.get_and_update/2, which is this adapter's whole answer to the callback's atomicity contract: the Agent serves one message at a time, so nothing can be created on the hash between the counting and the write.

A refused retirement returns the state unchanged, and the refusal carries every count - the five execution arms, the durable-child pins, the position rows, and each source's own counts - while only ADR-0012 decision 1's blocking set causes one.

The tombstone keeps the entry and its content hash and nulls both blobs, which is what makes the retired arm of fetch_chart/2 answerable at all.

save_chart(opts, chart_record)

Stores chart_record under its content_hash, idempotent on repeat writes of the same hash.

A tombstoned hash is refused with {:error, {:chart_retired, info}} and is not revived (ADR-0012 decision 6). The read of the tombstone and the write are one Agent.get_and_update/2, which is this adapter's transaction.

save_position(opts, position_record)

Stores position_record under its session_id, overwriting any position already stored for that session.

supports_chart_retirement?(opts)

@spec supports_chart_retirement?(StatifierPersistence.Storage.Adapter.opts()) ::
  boolean()

Declares chart retirement (the optional StatifierPersistence.Storage.Adapter.supports_chart_retirement?/1, ADR-0012 decision 6): an Agent's map has no NOT NULL to drop, so the backend limit V07 records on a database adapter has no counterpart here.

supports_content_hash_query?(opts)

@spec supports_content_hash_query?(StatifierPersistence.Storage.Adapter.opts()) ::
  boolean()

Declares the drained query (the optional StatifierPersistence.Storage.Adapter.supports_content_hash_query?/1): this adapter holds every execution record in one map and can count them.

supports_execution_outcome?(opts)

@spec supports_execution_outcome?(StatifierPersistence.Storage.Adapter.opts()) ::
  boolean()

Declares outcome support (the optional StatifierPersistence.Storage.Adapter.supports_execution_outcome?/1): this adapter keeps the blob on the execution record like every other field.

supports_metadata?(opts)

@spec supports_metadata?(StatifierPersistence.Storage.Adapter.opts()) :: boolean()

Declares metadata support (the optional StatifierPersistence.Storage.Adapter.supports_metadata?/1): this adapter stores the map with the execution record and returns it verbatim (ADR-0006 decision 3).

supports_retired_info?(opts)

@spec supports_retired_info?(StatifierPersistence.Storage.Adapter.opts()) :: boolean()

Declares the narrow tombstone read (the optional StatifierPersistence.Storage.Adapter.supports_retired_info?/1).

supports_tree_migration?(opts)

@spec supports_tree_migration?(StatifierPersistence.Storage.Adapter.opts()) ::
  boolean()

Declares the tree migration unit (the optional StatifierPersistence.Storage.Adapter.supports_tree_migration?/1, ADR-0015 decision 3).

update_execution(opts, execution_record)

Overwrites the execution stored under execution_record's execution_id with the full record, or refuses with :execution_not_found when no execution exists for the id.

metadata is the documented exception to the full overwrite: it is write-once (ADR-0006 decision 1 grants no way to change it after create), so the stored map is carried forward and the given record's metadata is ignored. outcome_blob is the second exception: a nil in the given record carries the stored value forward, and a binary sets it.

write_tree_migration(opts, writes)

Writes a tree migration's re-pins and parks as one unit (the optional StatifierPersistence.Storage.Adapter.write_tree_migration/2, ADR-0015 decision 3).

Every write is applied to one copy of the state inside one Agent.get_and_update/2, which is this adapter's transaction: the copy replaces the state only when every write applied, and a write naming an execution that is not stored answers {:error, :execution_not_found} with the state unchanged.