Repository navigation
Record trigger failures and owner on its row; tag metrics by kind - #76101
Merged
Merged
Conversation
A trigger that kept failing left no trace in the database, so no API, UI or alert could say it was failing, how often or why. Nothing on the row said who the trigger ran for either, so a view of every trigger would need a different lookup for each kind. Each failure now records on the row how many failures in a row, since when, and the error (type and message only, masked with the task SDK masker as the task runner does, cut to 1,000 characters), and a triggerer that picks the trigger up continues the count. The count resets once the trigger fires an event or stays up for two minutes; the last error is kept. Failing triggers are still re-created every couple of seconds as before, so this is one row update per failure. A deferred task's or callback's trigger also records its Dag and owner when it is created, and the migration fills them in for rows that already exist. Rows created without one afterwards, by components still on the previous version during a rolling upgrade, get it from the triggerer when it picks them up. triggers.failed and triggers.succeeded also could not tell an asset watcher from a deferred task's or a callback's trigger, so there was no metric to alert on for failing watchers alone, and nothing said how many triggers are failing right now. They are now tagged with trigger_kind, and a new triggers.failing gauge counts the triggers of each kind that failed since they last recovered. Per-trigger tags would make cardinality grow with the number of triggers, so the metrics stay per kind; which trigger is failing is on its row.
Postgres refuses a NUL character in text, so a trigger error containing one made writing the failure raise and stop the triggerer.
seanghaeli
marked this pull request as ready for review
October 30, 2026 23:35
seanghaeli
requested review from
XD-DENG,
amoghrajesh,
ashb,
dstandish,
ephraimbuddy,
hussein-awala and
potiuk
as code owners
October 30, 2026 23:35
fat-catTW
approved these changes
Oct 31, 2026
potiuk
approved these changes
Nov 1, 2026
potiuk
left a comment
Member
There was a problem hiding this comment.
Thanks — this reads well. I checked the tracker's recovery and hand-over logic, the triggerer_id-guarded row writes, the masking of last_error (connections and variables fetched through the supervisor are registered with its masker, so redact() covers them), the NUL/surrogate escaping, and the migration's backfill and downgrade. The remaining eager-loading nit only adds a lazy load for rows without an owner during a rolling upgrade, as trigger.assets was already loaded per non-task trigger before, so it doesn't need to block this.
Drafted-by: Claude Code (Claude Opus 5); reviewed by @potiuk before posting
hussein-awala
added a commit
to hussein-awala/airflow
that referenced
this pull request
Nov 1, 2026
apache#76101 sets last_event_at on the trigger row in submit_event, which no longer loads that row, so write it with an UPDATE by trigger id. The two submit_event query budgets in test_trigger.py count that UPDATE.
1 task done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A trigger that keeps failing leaves nothing in the database, so no API, UI or alert can tell, and the row doesn't say who the trigger runs for.
triggers.failedcan't tell a watcher from a task's trigger either.consecutive_failures,first_failed_at,last_failed_atandlast_errorto the trigger's row.last_erroris the exception type and message only, masked like task logs and cut to 1,000 characters. An event setslast_event_at.Trigger recoveredis logged). So a watcher that ran fine for a day and then crashed once shows one failure, not every failure since it first crashed.dag_idandowner_refwhen created; the migration adds nullable columns and fills the owner in for existing rows, and the triggerer fills it in for rows older components create during a rolling upgrade.triggers.failedandtriggers.succeededget atrigger_kindtag (one oftask,asset_watcherorcallback), by what the trigger runs for. A newtriggers.failinggauge counts failing triggers per triggerer and kind.How to review:
TriggerFailureTrackerinjobs/triggerer_job_runner.py, then the row writes,Trigger.submit_failureandclear_recorded_failuresinmodels/trigger.py.Breeze on Postgres, a watcher with expired credentials next to healthy watchers, deferred tasks and a deadline callback:
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Opus 5.5) following the guidelines