jason810496 commented on code in PR #66854:
URL: https://github.com/apache/airflow/pull/66854#discussion_r3610199153


##########
airflow-core/src/airflow/jobs/scheduler_job_runner.py:
##########
@@ -3676,6 +3682,88 @@ def _remove_unreferenced_triggers(self, *, session: 
Session = NEW_SESSION) -> No
             .execution_options(synchronize_session="fetch")
         )
 
+    @provide_session
+    def _drain_asset_event_queue(self, *, session: Session = NEW_SESSION) -> 
None:
+        """
+        Process pending asset-event registrations enqueued by the execution 
API.
+
+        The task-success path enqueues an 
:class:`~airflow.models.asset.AssetEventQueue` row when
+        synchronous asset-event registration is skipped or fails under 
database lock contention.
+        Here we claim pending rows one at a time (``FOR UPDATE SKIP LOCKED``, 
so multiple schedulers
+        do not collide), (re)run ``register_asset_changes_in_db`` to create 
the ``AssetEvent`` and
+        ``AssetDagRunQueue`` rows, and delete the queue row in the same 
transaction so it is
+        processed exactly once. A row that keeps failing is retried on later 
passes until it exceeds
+        ``asset_event_queue_max_attempts``, after which it is left in place 
(parked) and surfaced via
+        the ``asset.event_queue.failures`` metric for an operator to inspect.

Review Comment:
   I don't feel the current batch implementation is right.
   IIUC, we should fetch the current batch in single query and commit in the 
same session.
   
   Additionally, we shouldn't call the explicit `commit` or `rollback` within 
the function decorated by `provide_session`.
   
   Somehow like:
   ```python
   def _drain_asset_event_queue(self, *, session: Session = NEW_SESSION) -> 
None:
       from airflow.api_fastapi.execution_api.datamodels.asset import 
AssetProfile
   
       batch_size = conf.getint("scheduler", "asset_event_queue_batch_size", 
fallback=100)
       max_attempts = conf.getint("scheduler", 
"asset_event_queue_max_attempts", fallback=5)
   
       rows = session.scalars(
           with_row_locks(
               select(AssetEventQueue)
               .where(AssetEventQueue.attempts < max_attempts)
               .order_by(AssetEventQueue.created_at)
               .limit(batch_size),
               of=AssetEventQueue,
               session=session,
               skip_locked=True,
           )
       ).all()
   
       processed = 0
       for row in rows:
           try:
               with session.begin_nested():
                   ti = session.get(TaskInstance, row.ti_id)
                   if ti is not None:
                       task_outlets = [AssetProfile.model_validate(o) for o in 
row.task_outlets]
                       TaskInstance.register_asset_changes_in_db(
                           ti, task_outlets, row.outlet_events, session=session
                       )
                   session.delete(row)
               processed += 1
           except Exception:
               with session.begin_nested():
                   row.attempts += 1
               if row.attempts >= max_attempts:
                   self.log.error("Giving up on asset-event registration for TI 
%s after %s attempts", row.ti_id, row.attempts)
                   stats.incr("asset.event_queue.failures")
               else:
                   self.log.warning("Asset-event registration for TI %s failed; 
will retry (attempt %s)", row.ti_id, row.attempts)
   
       session.commit()
       if processed:
           stats.incr("asset.event_queue.processed", processed)
       stats.gauge("asset.event_queue.pending", 
session.scalar(select(func.count()).select_from(AssetEventQueue)) or 0)
   ```



-- 
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