AshWorkflow.Changes.RecordEvent (AshWorkflow v0.6.0)

Copy Markdown View Source

Records a workflow event: writes through state_entered_at and, if a transition_log is configured, appends one row describing what happened.

Replaces the five copy-pasted set_attribute(:state_entered_at, ...) call sites AshWorkflow.Transformers.AddActions used to inject — one per kind of workflow event — plus the resource-global :initial row on create. The change is always injected regardless of whether logging is configured; the log append is a single conditional at the leaf, not a fork in the transformer.

Options

  • :triggered_by (required) — one of :initial, :manual, :automatic, :timeout, :error_path, :undo.
  • :touch_state_entered_at (optional, defaults to true) — whether the change writes state_entered_at. A repeating timeout relies on that write: resetting the anchor is how its trigger re-arms. A non-repeating action timeout must not reset it, because every other deadline on the step measures after from the same attribute, so moving it would push a pending transition_to timeout out of reach.
  • :transition_name (optional) — the name recorded on the log row. Defaults to changeset.action.name, which is enough for most sites, but several of the actions AddActions generates use an internal hidden name (e.g. __on_error_process) that would be confusing in history, so those sites pass a human-facing name explicitly. An undo takes the name of the row it reverses, so the pair reads as one decision and its reversal rather than as two unrelated events.

Undo

AshWorkflow.Changes.UndoTransition puts the row being reversed into the changeset context, and this change writes its primary key to the log's undoes foreign key. That pointer is what makes an undo additive: the reversed row is left exactly as written, and both readings of history stay derivable from the same rows — see AshWorkflow.TransitionLog.effective/1.

Telemetry

This change is also where the [:ash_workflow, :transition] span is emitted, for the same reason it is where the log row is written: it runs on every action AshWorkflow generates, so one place covers a manual transition, an automatic step, a timeout, an error path, an undo and the initial create. See AshWorkflow.Telemetry for the events and their metadata.

Telling the scheduler

Entering a step is when the deadlines ahead of a record become known, so this change also calls AshWorkflow.Scheduler.notify_state_change/1. A scheduler that arms timers needs that call to be precise; a scheduler that polls implements neither runtime callback, so the call does nothing for it.

It runs in the same after_action hook as the log append and the span's stop event, which means it runs after the data layer has committed the new state. A timer armed before the commit could fire against a record still holding the old state.

Atomicity

This change implements atomic/3 rather than falling back to require_atomic? false. state_entered_at is set with the now() Ash expression so it stays part of the atomic update, and the log append is attached as an after_action hook either way — hooks run after the data layer action completes whether or not the attribute update itself was atomic, so there is nothing about appending the log row that requires opting out of atomicity. This is the "try atomic first" route the design doc calls out as the likeliest source of bugs; see test/ash_workflow/changes/record_event_test.exs for the direct atomicity test it asks for.