Repository navigation
Fix deferred task instances being cascade-deleted by Trigger.clean_unused on MySQL #72062
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Fixed a race in ``Trigger.clean_unused()`` on MySQL that could cascade-delete the task instance rows of tasks deferring during the cleanup sweep. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -180,6 +180,48 @@ def test_clean_unused(session, dag_maker): | |
| assert {result.id for result in results} == {trigger1.id, trigger4.id, trigger5.id, trigger6.id} | ||
|
|
||
|
|
||
| def test_clean_unused_keeps_triggers_referenced_after_candidate_select(session, create_task_instance): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be worth parameterizing this test to also cover the Asset and Callback references? The current test reproduces the reported TaskInstance case, so this would just improve coverage of the other two predicates. |
||
| """ | ||
| Tests that the MySQL two-step delete does not remove a trigger that a task | ||
| instance deferred onto after the candidate ids were selected -- that would | ||
| cascade-delete the task instance row itself. The dialect is pinned to | ||
| "mysql" so the two-step branch runs on every backend. | ||
| """ | ||
| trigger = Trigger(classpath="airflow.triggers.testing.SuccessTrigger", kwargs={}) | ||
| session.add(trigger) | ||
| session.flush() | ||
|
|
||
| task_instance = create_task_instance( | ||
| session=session, logical_date=timezone.utcnow(), state=State.SCHEDULED | ||
| ) | ||
| session.flush() | ||
|
|
||
| real_scalars = session.scalars | ||
|
|
||
| def scalars_then_defer(*args, **kwargs): | ||
| # session.scalars() executes the SELECT eagerly, so the trigger is | ||
| # already on the kill list when the task instance defers onto it. | ||
| result = real_scalars(*args, **kwargs) | ||
| task_instance.state = State.DEFERRED | ||
| task_instance.trigger_id = trigger.id | ||
| session.flush() | ||
| return result | ||
|
|
||
| with ( | ||
| patch("airflow.models.trigger.get_dialect_name", return_value="mysql"), | ||
| patch.object(session, "scalars", side_effect=scalars_then_defer), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: could we add autospec=True here and assert mock_scalars.call_count == 1? This would make the mock stricter and explicitly verify that the candidate IDs are materialized once before the concurrent defer is injected. |
||
| ): | ||
| Trigger.clean_unused(session=session) | ||
|
|
||
| assert session.scalar(select(func.count()).select_from(Trigger).where(Trigger.id == trigger.id)) == 1 | ||
| assert ( | ||
| session.scalar( | ||
| select(func.count()).select_from(TaskInstance).where(TaskInstance.id == task_instance.id) | ||
| ) | ||
| == 1 | ||
| ) | ||
|
|
||
|
|
||
| @patch.object(TriggererCallback, "handle_event") | ||
| def test_submit_event(mock_callback_handle_event, session, create_task_instance): | ||
| """ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we extract these three predicates and reuse them in both the SELECT and DELETE?
This would avoid duplicating the conditions and reduce the risk of adding a new reference type to one side but forgetting the other.