Where a vault's suspended set is agreed.
Encryptor.Vault.suspend/2 and Encryptor.Vault.reinstate/2 write through
a store, and the vault reads the store back into a per-node view that the
suspension gate consults on every call (ADR-0010 decisions 1 and 3). The
gate never asks the store: the hot path stays one table read on the node,
with no network round trip and no dependency on the store being up.
A vault takes its store as the :suspension_store option, a
{module, opts} pair. It defaults to
{Encryptor.Vault.Suspension.Store.Ets, []}, whose set is the view:
under it a suspension is per node and is lost when the vault restarts,
exactly as before this behaviour existed (ADR-0010 decision 4).
Writing a shared store
A store the host implements - usually over its own database - makes one
suspension reach every node that shares it, and survive restarts. Under any
store other than the default the vault runs a refresher that calls
list/1 at start and then :suspension_poll_interval milliseconds after
each call returns, so every other node honours a suspension within one poll
interval of the write (ADR-0010 decisions 5 and 6).
The callbacks and what the vault does with their answers:
init/2runs once, while the vault resolves its configuration, and performs no I/O. Its state is frozen with the configuration and handed to every other callback. An{:error, term}refuses the vault's start as{:invalid_config, :suspension_store, :init}.suspend/2andreinstate/2are idempotent. An{:error, term}, an exit or a raise leaves the view as it was, and the operator's call answers{:suspension_store_unavailable, store}with the store's term in the error's:enginefield (ADR-0010 decision 7).reinstate/2answers:okfor a selector that was never suspended.list/1answers the whole of this vault's set; order is meaningless and duplicates collapse. Until it first answers{:ok, selectors}the vault denies every scope, and after that a failed call keeps the last set it read (ADR-0010 decision 7).
Under a shared store each call is bounded, and runs on a short-lived
process of its own so the vault can stop waiting: list/1 has five
seconds, and suspend/2 and reinstate/2 have what is left of the
operator's five-second deadline. A call past its bound is killed and counts
as a store that could not answer, with {:timeout, milliseconds} in
:engine. A store that keeps per-process state - a dynamic repo, say -
reads it from its own state rather than from the process it runs on.
A store keys its set by the vault it was initialised for, so two vaults never share a set. A store's state holds no key material; the vault's configuration redacts it when inspected all the same.
Summary
Types
A key selector, as Encryptor.Error fixes it.
Callbacks
Validates the store's options for one vault and builds its state. Performs no I/O.
The whole of this vault's set.
Removes the selector from this vault's set. Idempotent, and :ok for a selector never in it.
Adds the selector to this vault's set. Idempotent.
Types
@type selector() :: Encryptor.Error.selector()
A key selector, as Encryptor.Error fixes it.
@type state() :: term()
Whatever init/2 built. Opaque to the vault.
Callbacks
Validates the store's options for one vault and builds its state. Performs no I/O.
The whole of this vault's set.
Removes the selector from this vault's set. Idempotent, and :ok for a selector never in it.
Adds the selector to this vault's set. Idempotent.