The behaviour a host implements so a pin this package cannot see still refuses a retirement.
ADR-0012 decision 1 names four things that pin a content hash. Three of them are rows in this package's own tables and it counts them itself. The fourth is everything else a host knows about: a pending timer, an address row, a queue entry - state held outside this package, in a package this one does not depend on and must not learn about. A pin source is how that state gets a vote.
The callback
pins/2 is handed a content hash and a context, and answers the source's
own named counts as a map of atom to non-negative integer. The names are the
source's to choose; they travel back to the caller under the source module's
name, so a refusal says which source objected and what it was counting.
The context carries :execution_ids, the ids of the :active executions on
that hash, which the retire call already holds. It is there because a source
such as a timer queue knows executions and never knows content hashes:
handing over the ids is what lets such a source answer without learning this
package's key. A source that does know hashes can ignore the context
entirely.
A source that cannot answer is a refusal, never a zero
A source may raise, and raising is the supported way to say "I could not
answer". "The source could not answer" and "the source answered zero" are
different facts, and collapsing them would retire a pinned chart, so
collect/3 turns a raise into {:error, {module, reason}} and the
host-facing retire door stops there rather than retiring on a count it never
took. Returning anything that is not a map of atom to non-negative integer
is the same kind of failure and gets the same answer.
A source that throws, or exits - a GenServer.call/3 that times out inside
it is the usual case - has not answered either, and is refused the same
way. Each failure keeps its own reason, so a host can tell them apart: see
reason/0.
This package ships no source
There is no implementation of this behaviour in lib/, and mix.exs gains
no dependency for one. A host passes the list of source modules in at the
retire call, and each module is one the host owns.
A migration asks too
StatifierPersistence.Executions.migrate/4 asks the sources a host passes
in its pin_sources: option whether the one execution it migrates has a
pending timer (ADR-0013 decision 6). It hands over the plan's from hash
and that execution's id, alone, in :execution_ids; the execution may be
:active or parked in :needs_migration. A source that cannot answer
refuses the migration exactly as it refuses a retirement.
Two hosts
A durable timer queue, over an advertising chart that waits for a click after an impression:
defmodule MyApp.TimerPins do
@behaviour StatifierPersistence.PinSource
@impl true
def pins(_content_hash, %{execution_ids: execution_ids}) do
%{pending_timers: MyApp.Timers.count_scheduled_for(execution_ids)}
end
endAn address table, over the same chart, where an impression id is the address an incoming click is routed to:
defmodule MyApp.AddressPins do
@behaviour StatifierPersistence.PinSource
@impl true
def pins(content_hash, _context) do
%{addresses: MyApp.Addresses.count_for_chart(content_hash)}
end
endBoth modules are the host's. Neither is in this package, and this package names neither except as prose.
Summary
Types
What a source is told besides the content hash.
A source's own named counts.
Why a source did not answer.
Callbacks
Answers this source's named counts for content_hash.
Functions
Calls each source in order and collects its counts under its module name.
Types
@type context() :: %{execution_ids: [String.t()]}
What a source is told besides the content hash.
:execution_ids holds the ids of the :active executions on the hash,
or, when a migration asks, the id of the one execution it migrates.
@type counts() :: %{required(atom()) => non_neg_integer()}
A source's own named counts.
@type reason() :: {:raised, Exception.t()} | {:thrown, term()} | {:exited, term()} | {:invalid_return, term()}
Why a source did not answer.
{:raised, exception}-pins/2raisedexception.{:thrown, value}-pins/2threwvalue.{:exited, reason}-pins/2exited withreason, as aGenServer.call/3that times out does.{:invalid_return, value}-pins/2returnedvalue, which is not a map of atom to non-negative integer.
Callbacks
Functions
@spec collect([module()], String.t(), context()) :: {:ok, %{required(module()) => counts()}} | {:error, {module(), reason()}}
Calls each source in order and collects its counts under its module name.
Answers {:ok, %{module => counts}} when every source answered, and
{:error, {module, reason}} at the first source that raised, threw, exited
or returned anything but a map of atom to non-negative integer, where
reason is {:raised, exception}, {:thrown, value}, {:exited, reason}
or {:invalid_return, value} (reason/0). An empty source list answers
{:ok, %{}}.
The first failure stops the walk: the retirement is already refused, and the remaining sources' counts cannot change that.