StatifierOban.Timer (StatifierOban v0.1.0)

Copy Markdown View Source

Consumes {:send_delayed, %Statifier.Effect.SendDelayed{}} into a uniquely-keyed job on the host's Oban instance.

This is the scheduling and cancellation half of the durable-timer recipe in statifier-ex's docs/durable-timers.md: the host reads the effect off a live session's subscriber stream (or a direct interpreter drive) and hands it here with the session scope and its StatifierOban.Config. When the job fires, StatifierOban.Timer.Worker feeds the event back through the config's StatifierOban.Timer.Delivery module, behind the mandatory run-liveness check (st-ADR-0054 decision 4) - the delivery module travels on the job's meta, so it is fixed at schedule time.

Two contract rules from statifier-ex are enforced at this door:

  • Only a nil target is schedulable (st-ADR-0055): any other target's route is resolved inside the session and does not travel on the effect, so a non-nil target is a typed error here - the caller should not have offered it. The effect stream is observational; leaving such a send to the library costs nothing.
  • delay_ms is relative, milliseconds from the moment the send was scheduled, so the fire time is computed at insert (DateTime.utc_now/0 plus the delay), never re-derived later.

Summary

Types

Why a SendDelayed effect could not be scheduled.

Functions

Consumes one %Cancel{} into cancellation of every matching timer job.

Schedules one %SendDelayed{} as one Oban job, unique per dedup key.

Types

schedule_error()

@type schedule_error() ::
  {:non_self_target, String.t()}
  | StatifierOban.Timer.Key.error()
  | Ecto.Changeset.t()

Why a SendDelayed effect could not be scheduled.

Functions

cancel(config, scope, effect)

Consumes one %Cancel{} into cancellation of every matching timer job.

Consumes the effect vocabulary's {:cancel, %Cancel{}}, never the instruction vocabulary's {:cancel_timers, ...} (st-ADR-0054). The match is the cancellation key {scope, send_id} from StatifierOban.Timer.Key, read off each stored job's args, and it may legitimately hit several jobs: spec 6.3 cancels every delayed send under a sendid, and an author-written id executed twice stores two jobs under one send_id. Returns {:ok, count} with the number of jobs cancelled; a cancel matching nothing is {:ok, 0}, a no-op rather than an error - the same shape as Timers.take/2's {[], timers} in statifier-ex.

A cancel racing job execution resolves to whichever transition commits first, and stays resolved: Oban.cancel_all_jobs/2 cancels any matched job still in a non-terminal state - an executing one is killed - while a job that already reached a terminal state keeps its outcome and is not counted. That is spec-faithful: a real-time <cancel> can lose to a timer that already fired, and sob-2hx.5's run-liveness check at delivery is the guard on that side, not this one.

The match ignores the queue, exactly as the dedup key's uniqueness does: a host that moved its timers queue can still cancel jobs stored under the old name.

schedule(config, scope, effect)

Schedules one %SendDelayed{} as one Oban job, unique per dedup key.

scope follows StatifierOban.Timer.Key: ctx.session_id for a live session, or the host's own durable run id for a process-less host - always the caller's to supply, never derived.

Inserting the same scope and effect again returns {:ok, job} with job.conflict? set and leaves exactly one stored job: the at-least-once no-op the dedup key exists for. The job is inserted into the host's :timers_queue, scheduled at now plus delay_ms, carrying the config's delivery module in its meta - meta is not part of the unique fields, so a replay under a reconfigured delivery still conflicts with the stored job (same scheduling decision) rather than inserting a second one.