This is an automated email from the ASF dual-hosted git repository.

ephraimbuddy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new e3089bf977c Extract DB operations from TriggerRunnerSupervisor into 
overridable methods (#65680)
e3089bf977c is described below

commit e3089bf977c1a936c1a54600dcecf1c7ea21d771
Author: Ephraim Anierobi <[email protected]>
AuthorDate: Mon Apr 27 20:56:52 2026 +0100

    Extract DB operations from TriggerRunnerSupervisor into overridable methods 
(#65680)
    
    * Extract DB operations from TriggerRunnerSupervisor into overridable 
methods
    
    Add overridable hooks on TriggerRunnerSupervisor so the DB-backed
    Trigger.* calls can be swapped for Execution API calls in a follow-up
    for AIP-92 (isolate DAG processor, callback processor, and triggerer
    from core services):
    
    - on_trigger_event / on_trigger_failure wrap Trigger.submit_event /
      Trigger.submit_failure.
    - fetch_trigger_details / fetch_non_task_trigger_ids wrap the bulk
      fetches used during update_triggers.
    - build_trigger_workloads extracts the session-scoped workload
      construction out of update_triggers, leaving update_triggers as a
      pure diff/enqueue step. As a side effect, DBDagBag, the log
      renderer, and the trigger fetches are now only created when there
      are new triggers to process, which removes a small amount of idle
      metadata-DB work on each loop iteration.
    
    Also fix the return annotation on
    Trigger.fetch_trigger_ids_with_non_task_associations from set[str]
    to set[int] to match the actual PK type.
    
    * Apply suggestion
    
    Co-authored-by: Tzu-ping Chung <[email protected]>
    
    ---------
    
    Co-authored-by: Tzu-ping Chung <[email protected]>
---
 .../src/airflow/jobs/triggerer_job_runner.py       | 78 +++++++++++++---------
 airflow-core/src/airflow/models/trigger.py         |  2 +-
 airflow-core/tests/unit/jobs/test_triggerer_job.py | 57 ++++++++++++++++
 3 files changed, 105 insertions(+), 32 deletions(-)

diff --git a/airflow-core/src/airflow/jobs/triggerer_job_runner.py 
b/airflow-core/src/airflow/jobs/triggerer_job_runner.py
index 73c0ed906e8..2d0fc2ae224 100644
--- a/airflow-core/src/airflow/jobs/triggerer_job_runner.py
+++ b/airflow-core/src/airflow/jobs/triggerer_job_runner.py
@@ -616,8 +616,8 @@ class TriggerRunnerSupervisor(WatchedSubprocess):
     def heartbeat_callback(self, session: Session | None = None) -> None:
         Stats.incr("triggerer_heartbeat", 1, 1)
 
-    def load_triggers(self):
-        """Query the database for the triggers we're supposed to be running 
and update the runner."""
+    def load_triggers(self) -> None:
+        """Assign triggers to this triggerer and update the runner with the 
IDs it should run."""
         Trigger.assign_unassigned(
             self.job.id,
             self.capacity,
@@ -633,12 +633,16 @@ class TriggerRunnerSupervisor(WatchedSubprocess):
             # Get the event and its trigger ID
             trigger_id, event = self.events.popleft()
             # Tell the model to wake up its tasks
-            Trigger.submit_event(trigger_id=trigger_id, event=event)
+            self.on_trigger_event(trigger_id=trigger_id, event=event)
             # Emit stat event
             Stats.incr("triggers.succeeded")
 
-    def clean_unused(self):
-        """Clean out unused or finished triggers."""
+    def on_trigger_event(self, trigger_id: int, event: TriggerEvent) -> None:
+        """Record that a trigger fired an event."""
+        Trigger.submit_event(trigger_id=trigger_id, event=event)
+
+    def clean_unused(self) -> None:
+        """Remove triggers that are no longer needed."""
         Trigger.clean_unused()
 
     def handle_failed_triggers(self):
@@ -649,11 +653,15 @@ class TriggerRunnerSupervisor(WatchedSubprocess):
         """
         while self.failed_triggers:
             # Tell the model to fail this trigger's deps
-            trigger_id, saved_exc = self.failed_triggers.popleft()
-            Trigger.submit_failure(trigger_id=trigger_id, exc=saved_exc)
+            trigger_id, exc = self.failed_triggers.popleft()
+            self.on_trigger_failure(trigger_id=trigger_id, exc=exc)
             # Emit stat event
             Stats.incr("triggers.failed")
 
+    def on_trigger_failure(self, trigger_id: int, exc: list[str] | None) -> 
None:
+        """Record that a trigger failed."""
+        Trigger.submit_failure(trigger_id=trigger_id, exc=exc)
+
     def emit_metrics(self):
         DualStatsManager.gauge(
             "triggers.running",
@@ -733,34 +741,22 @@ class TriggerRunnerSupervisor(WatchedSubprocess):
             timeout_after=trigger.task_instance.trigger_timeout,
         )
 
-    def update_triggers(self, requested_trigger_ids: set[int]):
-        """
-        Request that we update what triggers we're running.
+    def fetch_trigger_details(self, trigger_ids: set[int], *, session: 
Session) -> dict[int, Trigger]:
+        """Fetch trigger rows by ID."""
+        return Trigger.bulk_fetch(trigger_ids, session=session)
 
-        Works out the differences - ones to add, and ones to remove - then
-        adds them to the dequeues so the subprocess can actually mutate the 
running
-        trigger set.
-        """
+    def fetch_non_task_trigger_ids(self, *, session: Session) -> set[int]:
+        """Fetch trigger IDs associated with non-task entities."""
+        return 
Trigger.fetch_trigger_ids_with_non_task_associations(session=session)
+
+    def build_trigger_workloads(self, new_trigger_ids: set[int]) -> 
list[workloads.RunTrigger]:
+        """Build workloads for new trigger IDs."""
         dag_bag = DBDagBag()
         render_log_fname = log_filename_template_renderer()
-        known_trigger_ids = (
-            self.running_triggers.union(x[0] for x in self.events)
-            .union(self.cancelling_triggers)
-            .union(trigger[0] for trigger in self.failed_triggers)
-            .union(trigger.id for trigger in self.creating_triggers)
-        )
-        # Work out the two difference sets
-        new_trigger_ids = requested_trigger_ids - known_trigger_ids
-        cancel_trigger_ids = self.running_triggers - requested_trigger_ids
-        # Bulk-fetch new trigger records
         with create_session() as session:
-            # Bulk-fetch new trigger records
-            new_triggers = Trigger.bulk_fetch(new_trigger_ids, session=session)
-            trigger_ids_with_non_task_associations = 
Trigger.fetch_trigger_ids_with_non_task_associations(
-                session=session
-            )
+            new_triggers = self.fetch_trigger_details(new_trigger_ids, 
session=session)
+            trigger_ids_with_non_task_associations = 
self.fetch_non_task_trigger_ids(session=session)
             to_create: list[workloads.RunTrigger] = []
-            # Add in new triggers
             for new_trigger_id in new_trigger_ids:
                 # Check it didn't vanish in the meantime
                 if new_trigger_id not in new_triggers:
@@ -794,7 +790,27 @@ class TriggerRunnerSupervisor(WatchedSubprocess):
                 ):
                     to_create.append(workload)
 
-            self.creating_triggers.extend(to_create)
+        return to_create
+
+    def update_triggers(self, requested_trigger_ids: set[int]):
+        """
+        Request that we update what triggers we're running.
+
+        Works out the differences - ones to add, and ones to remove - then
+        adds them to the dequeues so the subprocess can actually mutate the 
running
+        trigger set.
+        """
+        known_trigger_ids = self.running_triggers.union(
+            (x[0] for x in self.events),
+            self.cancelling_triggers,
+            (trigger[0] for trigger in self.failed_triggers),
+            (trigger.id for trigger in self.creating_triggers),
+        )
+        # Work out the two difference sets
+        new_trigger_ids = requested_trigger_ids - known_trigger_ids
+        cancel_trigger_ids = self.running_triggers - requested_trigger_ids
+        if new_trigger_ids:
+            
self.creating_triggers.extend(self.build_trigger_workloads(new_trigger_ids))
 
         if cancel_trigger_ids:
             # Enqueue orphaned triggers for cancellation
diff --git a/airflow-core/src/airflow/models/trigger.py 
b/airflow-core/src/airflow/models/trigger.py
index 97079952885..589725ced2c 100644
--- a/airflow-core/src/airflow/models/trigger.py
+++ b/airflow-core/src/airflow/models/trigger.py
@@ -207,7 +207,7 @@ class Trigger(Base):
 
     @classmethod
     @provide_session
-    def fetch_trigger_ids_with_non_task_associations(cls, session: Session = 
NEW_SESSION) -> set[str]:
+    def fetch_trigger_ids_with_non_task_associations(cls, session: Session = 
NEW_SESSION) -> set[int]:
         """Fetch all trigger IDs actively associated with non-task entities 
like assets and callbacks."""
         from airflow.models.callback import Callback  # to avoid circular 
import: Callback -> Trigger
 
diff --git a/airflow-core/tests/unit/jobs/test_triggerer_job.py 
b/airflow-core/tests/unit/jobs/test_triggerer_job.py
index 4b047ab15e1..ef5b5a02c5c 100644
--- a/airflow-core/tests/unit/jobs/test_triggerer_job.py
+++ b/airflow-core/tests/unit/jobs/test_triggerer_job.py
@@ -1308,6 +1308,63 @@ def 
test_update_triggers_prevents_duplicate_creation_queue_entries(session, supe
     assert not any(trigger_id == trigger_orm.id for trigger_id, _ in 
supervisor.failed_triggers)
 
 
+def test_update_triggers_delegates_workload_creation(supervisor_builder, 
mocker):
+    supervisor = supervisor_builder()
+    supervisor.running_triggers = {1, 3}
+    workload = workloads.RunTrigger(id=2, classpath="some.trigger", 
encrypted_kwargs="", ti=None)
+    build_trigger_workloads = mocker.patch.object(
+        TriggerRunnerSupervisor, "build_trigger_workloads", autospec=True, 
return_value=[workload]
+    )
+
+    supervisor.update_triggers({2, 3})
+
+    build_trigger_workloads.assert_called_once_with(supervisor, {2})
+    assert list(supervisor.creating_triggers) == [workload]
+    assert supervisor.cancelling_triggers == {1}
+
+
+def test_update_triggers_uses_fetch_hooks(session, supervisor_builder, mocker):
+    trigger = TimeDeltaTrigger(datetime.timedelta(days=7))
+    _, _, trigger_orm, _ = create_trigger_in_db(session, trigger)
+
+    supervisor = supervisor_builder()
+    fetch_trigger_details_calls = []
+    fetch_non_task_trigger_ids_calls = []
+    original_fetch_trigger_details = 
TriggerRunnerSupervisor.fetch_trigger_details
+    original_fetch_non_task_trigger_ids = 
TriggerRunnerSupervisor.fetch_non_task_trigger_ids
+
+    def fetch_trigger_details(self, trigger_ids, *, session):
+        fetch_trigger_details_calls.append((trigger_ids, session))
+        return original_fetch_trigger_details(self, trigger_ids, 
session=session)
+
+    def fetch_non_task_trigger_ids(self, *, session):
+        fetch_non_task_trigger_ids_calls.append(session)
+        return original_fetch_non_task_trigger_ids(self, session=session)
+
+    mocker.patch.object(
+        TriggerRunnerSupervisor, "fetch_trigger_details", autospec=True, 
side_effect=fetch_trigger_details
+    )
+    mocker.patch.object(
+        TriggerRunnerSupervisor,
+        "fetch_non_task_trigger_ids",
+        autospec=True,
+        side_effect=fetch_non_task_trigger_ids,
+    )
+
+    supervisor.update_triggers({trigger_orm.id})
+
+    assert len(fetch_trigger_details_calls) == 1
+    assert len(fetch_non_task_trigger_ids_calls) == 1
+    assert fetch_trigger_details_calls == [({trigger_orm.id}, 
fetch_non_task_trigger_ids_calls[0])]
+    assert len(supervisor.creating_triggers) == 1
+    assert supervisor.creating_triggers[0].id == trigger_orm.id
+
+    supervisor.update_triggers({trigger_orm.id})
+
+    assert len(supervisor.creating_triggers) == 1
+    assert supervisor.creating_triggers[0].id == trigger_orm.id
+
+
 def 
test_update_triggers_prevents_duplicate_creation_queue_entries_with_multiple_triggers(
     session, supervisor_builder, dag_maker
 ):

Reply via email to