Skip to content

Run initiator is a context variable

A run records who started it in dag_runs.initiated_by, and a trigger records who last changed it in trigger_activations.changed_by and changed_at. The value comes from the run_initiator context variable, set with the initiated_by context manager, not from a parameter. woodglue needs this to show and audit runs on a server that runs DAGs for many users (#14).

Considered options

A initiated_by= parameter on DagRunner.run was the obvious alternative and was declined because runs are mostly not started by the code that knows the caller. A server request reaches TriggerManager.fire, a namespace node's wrapper, or a DAG node that maps a sub-DAG; each would need the parameter threaded through, and every sub-DAG run would need it copied from its parent. A context variable crosses those layers unchanged, and child runs inherit it because asyncio tasks copy the context and DagRunner runs sync nodes on executor threads with a copy of it.

The one place that loses the context is run_in_executor without a copy, which is exactly where DagRunner.run calls create_run. It therefore reads the variable before the executor hop and passes it explicitly. create_run still falls back to the variable when called directly, so callers on the event loop thread need not pass it; requiring the argument was declined as it would make every direct caller repeat what the context already says.

Initiators are opaque strings. Lythonic reserves only SCHEDULER ("scheduler"), which the poll loop sets for scheduled fires; the host owns the rest of the format (woodglue uses user ids, cli:<os user> and anonymous). initiated_by(None) deliberately clears an outer initiator.

Consequences

restart continues the same run, so the run keeps its original initiator and nobody is recorded as the restarter. replay creates a new run, attributed to its caller. A restarted_by column can be added if auditing restarts matters.

changed_by is written when an initiator is set or when the call changes the trigger's status. TriggerManager.activate_configured, which hosts call on every start, sets no initiator and skips triggers stored as disabled, so it never changes a status a person set and leaves the last deliberate change in place. It moved into lythonic from woodglue's activate_triggers so that every host gets the rule that a deactivation survives restarts. An explicit activate() of a disabled trigger with no initiator records changed_by = NULL with the new changed_at, rather than leaving the name of whoever disabled it on a state they did not set.

Existing databases gain the columns through lythonic.state.add_missing_column when DagProvenance or TriggerStore is constructed. That costs one PRAGMA table_info per table per construction, not per poll, so a schema version (PRAGMA user_version) was not worth introducing yet.

Not done, because nothing needs them yet: an initiator on trigger_events (reachable through run_id for every run that started), and filtering get_recent_runs by initiator.