The wire shape between a %Statifier.Effect.SendDelayed{} and the args
map an Oban job stores.
Oban args live as JSON, so this module owns the (de)serialization of the one effect a timer job carries, plus the scope it was scheduled under. Two rules shape it:
- Every deterministic field rides as an explicit JSON value: the dedup
pair (
scope,ordinal) at the top level - Oban's uniquenesskeysread args at the top level - and the row data ADR-0059 (statifier-ex) keeps beside the key (send_id,macrostep,microstep,round,c_index,owner), self-describing in the store during an incident. - The two host-opaque fields,
dataandcaller_context, are arbitrary terms with no JSON shape, so they ride as tagged:erlang.term_to_binary/1payloads (StatifierOban.OpaqueTerm) and come back byte-identical.caller_contextstays row data, never a key component (st-ADR-0063 decision 6). Decoding uses:safe, so a payload naming an atom the reading node has never seen decodes to a typed error rather than minting atoms.from_effect/3's optional codec runs over both fields' bytes and tags the payload with its module name (StatifierOban.OpaqueTerm.Codec);to_effect/1reads whatever tag the stored row carries, regardless of what the reading caller passed.
to_effect/1 is the exact inverse of from_effect/3 for every
%SendDelayed{} the scheduler accepts: what the fired job carries is
enough to rebuild the event and its position, per ADR-0054's
correlation rule - position is read off the stored effect, never
recomputed at delivery.
What a host may durably put in caller_context
Byte-identical is not the same as meaningful. The row outlives the node that wrote it - that is the whole point of the package - so two rules bind what a host stamps into the slot, and this module enforces neither because it never reads the term:
- Nothing node-local. A pid, a port, a reference, an ETS table id, or a monitor ref comes back as a term that decodes fine and refers to nothing. There is no error to raise; the value is simply a lie by the time it is read.
- Nothing whose atoms the reading node may not have seen. Decoding
is
:safe, so a payload naming an unknown atom is a typed decode error rather than a minted atom, and an undecodable row is cancelled ({:undecodable, _}) rather than retried. A term carrying a library's internal atoms therefore couples delivery to that library being loaded on whichever node happens to run the job.
For the tracing case both rules point the same way, and this is the
shape the family expects: serialize at schedule time to the W3C Trace
Context text form - %{"traceparent" => "00-<trace-id>-<span-id>-01"}
plus "tracestate" where the host propagates one - rather than storing
a live span context or an OTel context map. Strings and string keys
carry no atoms and nothing node-local, the encoding is fixed by a
published spec instead of a library version, and the value stays
readable in the row during an incident. Restoring it is
opentelemetry_statifier's: this package hands the term back exactly as
given (StatifierOban.Timer.Delivery.fired_event/2) and reads nothing
out of it (ADR-0006 decision 7).
Summary
Types
String-keyed args map as Oban stores and redelivers it.
Why from_effect/3 could not build an args map.
Functions
Builds the args map for a timer job from the scope and the effect.
Rebuilds the scope and the %SendDelayed{} from a job's args.
Types
String-keyed args map as Oban stores and redelivers it.
@type decode_error() :: {:missing_field, String.t()} | {:invalid_field, String.t(), term()} | StatifierOban.OpaqueTerm.decode_error()
@type encode_error() :: {:codec_failed, String.t(), StatifierOban.OpaqueTerm.encode_error()}
Why from_effect/3 could not build an args map.
Functions
@spec from_effect(String.t(), Statifier.Effect.SendDelayed.t(), module() | nil) :: {:ok, args()} | {:error, encode_error()}
Builds the args map for a timer job from the scope and the effect.
The caller has already validated the scope and ordinal via
StatifierOban.Timer.Key.dedup_key/2; this function only lays fields
out on the wire. data and caller_context are encoded through a
with, so the first codec failure short-circuits and no
partially-encoded args map is ever returned.
@spec to_effect(args()) :: {:ok, String.t(), Statifier.Effect.SendDelayed.t()} | {:error, decode_error()}
Rebuilds the scope and the %SendDelayed{} from a job's args.
Returns a typed error rather than raising: an undecodable job is a fact about the row, and the worker decides what to do with it.