StatifierOban.Timer.JobArgs (StatifierOban v0.5.0)

Copy Markdown View Source

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 uniqueness keys read 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, data and caller_context, are arbitrary terms with no JSON shape, so they ride as tagged :erlang.term_to_binary/1 payloads (StatifierOban.OpaqueTerm) and come back byte-identical. caller_context stays 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/1 reads 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

args()

@type args() :: %{optional(String.t()) => term()}

String-keyed args map as Oban stores and redelivers it.

decode_error()

@type decode_error() ::
  {:missing_field, String.t()}
  | {:invalid_field, String.t(), term()}
  | StatifierOban.OpaqueTerm.decode_error()

encode_error()

@type encode_error() ::
  {:codec_failed, String.t(), StatifierOban.OpaqueTerm.encode_error()}

Why from_effect/3 could not build an args map.

Functions

from_effect(scope, effect, codec \\ nil)

@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.

to_effect(args)

@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.