Owns the lifetime of a vault's frozen configuration.
Encryptor.Vault.Config publishes the resolved struct into
:persistent_term and erases it again, but a :persistent_term entry has
no owner of its own: nothing removes it when the vault that wrote it goes
away. This process is that owner. It freezes on the way up and erases on the
way down, which is what keeps {:vault_not_started, vault} an honest answer
after a vault stops rather than a stale struct a later call would encrypt
against.
It is the vault supervisor's first child, so the configuration is published
before the cache or the provider starts and a provider's child_spec/1 can
read it.
The process holds no other state, and the one call it answers is a write to
the suspended set (below). :persistent_term is what the hot path reads -
lock-free and allocating nothing - and routing configuration reads through a
GenServer would put a serialization point in front of a pure function,
which is the shape ADR-0001 decision 5 exists to avoid.
:persistent_term.erase/1 triggers a global scan, which is why it happens
here, on a vault's lifecycle boundary, and never on a call path.
It owns one other thing, for the same reason and by the same argument: the
vault's suspended set, an ETS table created in init/1 beside the
freeze (ADR-0005 amendment A decision 8). A suspension is the package's
first piece of mutable per-vault state, and the record fixes its owner here
rather than in the frozen configuration, in :persistent_term, or behind a
GenServer.call - the three the paragraphs above rule out. The table dying
with this process is what makes a suspension node-local and volatile, which
amendment A decides deliberately rather than tolerates. That holds under
the default :suspension_store; under any other store the table is the
node's view of a set the store agrees, created under a name the gate does
not read and renamed into place once the store has been read, so the vault
denies every scope until then (ADR-0010 decisions 3 and 7).
The table is :protected: every process reads it, and only this one writes
it. suspend/2, reinstate/2 and a shared store's refresher hand their
writes to this process as calls, so no other process in the node can set or
lift a suspension behind the vault's back. Those writes are operator verbs
and a poll; the gate's read on every encrypt and decrypt stays a direct
table lookup on the caller's process, with no call in front of it.
Telemetry
terminate/2 emits [:encryptor, :vault, :stopped] after the erase, which
is what makes the event mean "the frozen configuration is gone" rather than
"a stop was requested" (ADR-0006 decision 3). It is reached only because
init/1 traps exits; a brutal kill takes the event with the erase it
reports.
Summary
Functions
Returns a specification to start this module under a supervisor.
Starts the owner process for a resolved configuration.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec start_link(Encryptor.Vault.Config.t()) :: GenServer.on_start()
Starts the owner process for a resolved configuration.