ramitkataria commented on code in PR #66350:
URL: https://github.com/apache/airflow/pull/66350#discussion_r4051362778


##########
airflow-core/src/airflow/utils/db_cleanup.py:
##########
@@ -457,10 +465,37 @@ def _do_delete(
                 delete = source_table.delete().where(
                     and_(*[col == target_table.c[col.name] for col in 
source_table.primary_key.columns])
                 )
+            # Re-apply skip_if_referenced on the DELETE to guard against a 
race where a new
+            # referencing row is created after the archive INSERT committed 
but before the DELETE
+            # runs. Without this the DELETE would violate the ON DELETE 
RESTRICT FK and fail.
+            if skip_if_referenced:
+                pk_col = source_table.c[referenced_pk_column]
+                for referencing_table_name, fk_column in skip_if_referenced:
+                    referencing = table(referencing_table_name, 
column(fk_column))
+                    delete = delete.where(
+                        ~select(literal(1))
+                        .select_from(referencing)
+                        .where(referencing.c[fk_column] == pk_col)
+                        .correlate(source_table)
+                        .exists()
+                    )
             logger.debug("delete statement:\n%s", delete.compile())
-            session.execute(delete)
+            deleted = cast("CursorResult", session.execute(delete)).rowcount

Review Comment:
   The NOT EXISTS is evaluated against the DELETE's own snapshot, right? So if 
I understand correctly: if a TI pinning this row is inserted while the DELETE 
is already running, the DELETE waits on the row's share lock, then proceeds and 
still hits the FK. So this closes the window between the archive commit and the 
DELETE starting, but not the DELETE itself.
   
   Would it make sense to also catch `IntegrityError` around the DELETE, roll 
back, warn, and `continue`? Roughly the same size as the guard and makes the 
newsfragment's "Fixed" accurate. If you'd rather keep just the guard, maybe 
reword the newsfragment to say the window is reduced?



##########
airflow-core/src/airflow/utils/db_cleanup.py:
##########
@@ -392,10 +418,32 @@ def _do_delete(
                 delete = source_table.delete().where(
                     and_(*[col == target_table.c[col.name] for col in 
source_table.primary_key.columns])
                 )
+            # Re-apply skip_if_referenced on the DELETE to guard against a 
race where a new
+            # referencing row is created after the archive INSERT committed 
but before the DELETE
+            # runs. Without this the DELETE would violate the ON DELETE 
RESTRICT FK and fail.
+            if skip_if_referenced:
+                pk_col = source_table.c[referenced_pk_column]
+                for referencing_table_name, fk_column in skip_if_referenced:
+                    referencing = table(referencing_table_name, 
column(fk_column))
+                    delete = delete.where(
+                        ~select(literal(1))
+                        .select_from(referencing)
+                        .where(referencing.c[fk_column] == pk_col)
+                        .correlate(source_table)
+                        .exists()
+                    )
             logger.debug("delete statement:\n%s", delete.compile())
-            session.execute(delete)
+            deleted = cast("CursorResult", session.execute(delete)).rowcount
             session.commit()
 
+            # A guarded DELETE (skip_if_referenced) may delete fewer rows than 
the SELECT
+            # found. That is fine: the SELECT already includes the same NOT 
EXISTS guard, so
+            # the skipped row is excluded on the next pass too, and the loop 
drains naturally.
+            # With --batch-size set, continuing here lets subsequent batches 
clean rows that
+            # were not affected by the race.
+            if deleted == 0:

Review Comment:
   What if the SELECT found 50 rows and one got pinned mid-pass, we delete 49, 
stay silent, and the pinned row sits in the committed archive looking purged. 
Maybe count the archived rows right after the CTAS commit and warn on `deleted 
< archived` instead?



##########
airflow-core/tests/unit/utils/test_db_cleanup.py:
##########
@@ -452,6 +463,27 @@ def test_cleanup_with_dag_id_filtering(self, dag_ids, 
exclude_dag_ids, expected_
                 f"Expected {expected_remaining_dag_ids} to remain, but got 
{remaining_dag_ids}"
             )
 
+    @pytest.mark.parametrize(
+        ("dag_ids", "exclude_dag_ids"),
+        [
+            pytest.param(["dag1"], None, id="include"),
+            pytest.param(None, ["dag1"], id="exclude"),
+        ],
+    )
+    def test_cleanup_dag_filtering_on_tables_without_their_own_dag_id(self, 
dag_ids, exclude_dag_ids):

Review Comment:
   I think we can probably drop this after the rebase? It covers the old scope 
on empty tables with no assertions, and `test_no_failure_warnings` in main 
already exercises these tables under both flags



##########
airflow-core/newsfragments/71732.bugfix.rst:
##########
@@ -0,0 +1 @@
+Fixed ``airflow db clean`` failing on the ``asset_event``, ``task_reschedule`` 
and ``deadline`` tables when ``--dag-ids`` or ``--exclude-dag-ids`` was used.

Review Comment:
   Should we drop this note now that #73173 has been merged?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to