Skip to content

Stop airflow db clean deleting triggers that are still in use - #71339

Open
steveahnahn wants to merge 2 commits into
apache:mainfrom
steveahnahn:db-clean-trigger-skip-referenced
Open

Stop airflow db clean deleting triggers that are still in use#71339
steveahnahn wants to merge 2 commits into
apache:mainfrom
steveahnahn:db-clean-trigger-skip-referenced

Conversation

@steveahnahn

Copy link
Copy Markdown
Contributor

Why

airflow db clean selects trigger rows by age alone, with no check for rows still in use.

The triggerer already removes every unreferenced trigger on each loop, so an old trigger row that survives is almost always still doing work. Three foreign keys point at trigger, and cleaning by age hits all three:

referencing table ON DELETE effect of the delete
task_instance CASCADE a running deferred task is deleted
asset_watcher CASCADE an event-driven watcher is deleted
callback no rule the delete fails and stops the command

Only the trigger rows are copied to the archive table, so the cascade-deleted task instance reaches no archive and db export-archived cannot recover it. Asset watchers are created once and reused, so their created_date ages past any retention window and every clean removes them until the next Dag parse recreates them.

Verified on Postgres. Four old triggers, three of them in use, and the cleanup reports Found 4 rows meeting deletion criteria:

before after
deferred task_instance rows 0 1
asset_watcher rows 0 1

dag_version already solves this with skip_if_referenced (#68339). This applies the same guard to trigger, so only genuinely orphaned rows are purged.

MySQL

trigger is a reserved word on MySQL, and the archive step interpolated the table name into CREATE TABLE ... LIKE unquoted. That is a syntax error, so the trigger table could never be cleaned on MySQL at all. ProgrammingError is suppressed per table, so it surfaced only as a warning.

The two changes ship together because they share a code path: the archive table is always created, so the reference guard alone would still fail on MySQL.

Tests

Both new tests fail without the change and pass with it. Full file: 74 pass on Postgres, 73 on MySQL, 72 on SQLite.

related: #56192


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 5)

Generated-by: Claude Code (Opus 5) following the guidelines

The triggerer already removes every unreferenced trigger on each loop, so an
old trigger row that survives is almost always still doing work. Cleaning that
row by age took a running deferred task or an event-driven asset watcher with
it through ON DELETE CASCADE, and neither reaches an archive table, so
db export-archived cannot recover them. A trigger held by a callback instead
failed the foreign key and stopped the whole command.

trigger is also a reserved word on MySQL, and the archive DDL interpolated the
table name unquoted, so the table could never be cleaned there at all.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant