AshWorkflow.Scheduler.Precise.Timeline (AshWorkflow v0.6.0)

Copy Markdown View Source

Holds the armed deadlines for AshWorkflow.Scheduler.Precise.

One timer per pending deadline, keyed by resource, work name and primary key. A record entering a step replaces every timer it already had, and a record reaching a terminal step drops them.

Start it with the resources it sweeps

{AshWorkflow.Scheduler.Precise.Timeline, resources: [MyApp.Candidate]}

The DSL selects a scheduler per resource, but this process is started once by the application, so it cannot infer which resources to recover deadlines for. Listing them is what makes the sweep possible. A resource whose workflow selects a different scheduler is ignored, so listing one is harmless.

Firing

Every fire re-reads the record and re-applies the work's match expression before running the action. That check is what makes a duplicate timer safe, and it is the same guarantee ash_oban gets by re-checking a trigger's where before a job runs. A record that has left the step, or had its deadline moved, fires nothing.

Retry

A failed attempt that has not yet reached work.retry.max_attempts re-arms a timer for the backoff delay, under the same key as the original deadline. That is what lets a record leaving the step drop a pending retry through the same drop_timers_for/2 and cancel/2 paths as any other timer. on_error only runs once the last attempt has failed, exactly as AshWorkflow.Scheduler.execute/3 decides.

Summary

Functions

Drop every timer this record has.

Returns a specification to start this module under a supervisor.

Replace every timer this record has with the deadlines of its current state.

Run every unit of work that is due now for a resource, in the calling process.

Start the timeline.

Functions

cancel(record, works, name \\ __MODULE__)

Drop every timer this record has.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

deadline_changed(record, works, name \\ __MODULE__)

Replace every timer this record has with the deadlines of its current state.

Called by AshWorkflow.Scheduler.notify_state_change/1 after a state change. Replacing rather than adding is what stops a record that moved between steps from keeping the previous step's deadlines armed.

run_due(resource, opts \\ [])

@spec run_due(
  Ash.Resource.t(),
  keyword()
) :: non_neg_integer()

Run every unit of work that is due now for a resource, in the calling process.

This is the synchronous counterpart to the timers, and it exists for tests. A timer fires from this process, which under Ecto.Adapters.SQL.Sandbox cannot see the test's transaction, so a test that waited for a timer would wait for work that could not read its own data. Calling this instead runs the same work through AshWorkflow.Scheduler.execute/3 on the test's own connection.

Work.match is what "due now" means: for a timeout it is true exactly when the deadline has passed, and for an automatic step it is true as soon as a record occupies it. So this needs no horizon and no timer.

Returns the number of records it ran work for. An automatic step that transitions into another automatic step needs another call, the same way AshOban.schedule_and_run_triggers/1 needs another pass, so loop until it returns 0.

run_due(MyApp.Candidate)
#=> 2

start_link(opts)

Start the timeline.

Options

  • :resources — the workflow resources to sweep for pending deadlines.
  • :name — the process name. Defaults to this module.
  • :leader, :look_ahead_ms, :horizon_ms, :actor, :authorize?, :tenant — see AshWorkflow.Scheduler.Precise.