From 370b1ff03ad32205e00625ebddda45894d022f57 Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: Jan 30 2026 10:41:13 +0000
Subject: [PATCH 1/2] handler disabled hosts in scheduler


Fixes https://pagure.io/koji/issue/4504

---

diff --git a/kojihub/scheduler.py b/kojihub/scheduler.py
index 9f0757c..0fd4437 100644
--- a/kojihub/scheduler.py
+++ b/kojihub/scheduler.py
@@ -348,7 +348,7 @@ class TaskScheduler(object):
             host = self.hosts.get(task['host_id'])
             if not host:
                 # not showing as ready
-                # TODO log and deal with this condition
+                # we'll address this later in check_active_tasks
                 continue
             host.setdefault('_load', 0.0)
             if not task['waiting']:
@@ -435,10 +435,18 @@ class TaskScheduler(object):
                 continue
 
             host = self.hosts.get(task['host_id'])
+
             if not host:
-                # host disabled?
-                # TODO
-                continue
+                # host most likely disabled
+                # simulate the data with get_host
+                host = kojihub.get_host(task['host_id'], strict=False)
+                if not host:
+                    # should not be possible
+                    log_both('Active task with nonexistent host', task_id=task['task_id'], level=logging.ERROR)
+                    kojihub.Task(task['task_id']).free()
+                    continue
+                host['data'] = {}
+                host['channels'] = []
 
             taskruns = runs.get(task['task_id'], [])
             if not taskruns:
@@ -455,6 +463,12 @@ class TaskScheduler(object):
                 # TODO fix
 
             if task['state'] == koji.TASK_STATES['ASSIGNED']:
+                if not host.get('enabled', True):
+                    # if the host is disabled there is no need to wait for a timeout
+                    log_both('Task assigned to disabled host', task_id=task['task_id'],
+                             host_id=host['id'])
+                    kojihub.Task(task['task_id']).free()
+                    continue
                 assign_ts = min([r['create_ts'] for r in taskruns])
                 age = time.time() - assign_ts
                 if age > self.assign_timeout:
diff --git a/tests/test_hub/test_scheduler.py b/tests/test_hub/test_scheduler.py
index 656076f..9704961 100644
--- a/tests/test_hub/test_scheduler.py
+++ b/tests/test_hub/test_scheduler.py
@@ -265,10 +265,11 @@ class TestDoSchedule(BaseTest):
         self.assertEqual(t_assigned, list(range(3,5)))
         self.assertEqual(h_used, list(range(3,5)))
 
-class TestCheckActiveRuns(BaseTest):
+
+class TestCheckActiveTasks(BaseTest):
 
     def setUp(self):
-        super(TestCheckActiveRuns, self).setUp()
+        super(TestCheckActiveTasks, self).setUp()
         self.sched = scheduler.TaskScheduler()
 
         self.get_active_runs = mock.MagicMock()
@@ -285,6 +286,7 @@ class TestCheckActiveRuns(BaseTest):
         self.log_db = mock.MagicMock()
         mock.patch('kojihub.scheduler.log_db', new=self.log_db).start()
         self.set_refusal = mock.patch('kojihub.scheduler.set_refusal').start()
+        self.get_host = mock.patch('kojihub.kojihub.get_host').start()
 
     def test_check_no_active(self):
         self.assertEqual(self.sched.active_tasks, [])  # set by init
@@ -309,6 +311,21 @@ class TestCheckActiveRuns(BaseTest):
         update = self.updates[0]
         self.assertEqual(update.table, 'scheduler_task_runs')
 
+    def test_check_nonexistent_host(self):
+        self.sched.active_tasks = [{'task_id': 99, 'host_id': 12345}]
+        self.sched.hosts = {}
+        self.get_host.return_value = None
+
+        self.sched.check_active_tasks()
+
+        self.log_db.assert_called_once_with('Active task with nonexistent host', 99, None)
+        self.get_active_runs.assert_called_once()
+        self.assertEqual(self.frees, [99])
+        self.assertEqual(self.assigns, [])
+        self.assertEqual(len(self.updates), 1)
+        update = self.updates[0]
+        self.assertEqual(update.table, 'scheduler_task_runs')
+
     def test_check_override(self):
         # 'Override task assignment' case
         self.sched.active_tasks = [{'task_id': 99, 'host_id': 23, 'state': koji.TASK_STATES['ASSIGNED']}]
@@ -350,6 +367,29 @@ class TestCheckActiveRuns(BaseTest):
         update = self.updates[0]
         self.assertEqual(update.table, 'scheduler_task_runs')
 
+    def test_check_assign_to_disabled(self):
+        # task assigned to disabled host (non-override)
+        create_ts = 1000
+        now = 1000000
+        update_ts = now   # host is checking in
+        self.sched.active_tasks = [{'task_id': 99, 'host_id': 23, 'state': koji.TASK_STATES['ASSIGNED']}]
+        self.sched.hosts = {}  # disabled host not in our list
+        self.get_host.return_value = {'id': 23, 'name': 'test host 23', 'update_ts': update_ts, 'enabled': False}
+        self.sched.get_active_runs.return_value = {99: [{'create_ts': create_ts}]}
+
+        with mock.patch('time.time', return_value=now):
+            self.sched.check_active_tasks()
+
+        self.get_active_runs.assert_called_once()
+        self.set_refusal.assert_not_called()
+        self.log_db.assert_called_once_with('Task assigned to disabled host', 99, 23)
+        # we should free such tasks
+        self.assertEqual(self.frees, [99])
+        self.assertEqual(self.assigns, [])
+        self.assertEqual(len(self.updates), 1)
+        update = self.updates[0]
+        self.assertEqual(update.table, 'scheduler_task_runs')
+
     def test_check_implicit_refusal(self):
         # 'Task assignment timeout' case
         create_ts = 1000
@@ -390,3 +430,6 @@ class TestCheckActiveRuns(BaseTest):
         self.assertEqual(len(self.updates), 1)
         update = self.updates[0]
         self.assertEqual(update.table, 'scheduler_task_runs')
+
+
+# the end

From f5aa99ff1dc4f95aa56c53fc255c8285c1d2e283 Mon Sep 17 00:00:00 2001
From: Mike McLean <mikem@redhat.com>
Date: Jan 30 2026 10:41:13 +0000
Subject: [PATCH 2/2] flake8


---

diff --git a/kojihub/scheduler.py b/kojihub/scheduler.py
index 0fd4437..7639cb9 100644
--- a/kojihub/scheduler.py
+++ b/kojihub/scheduler.py
@@ -442,7 +442,8 @@ class TaskScheduler(object):
                 host = kojihub.get_host(task['host_id'], strict=False)
                 if not host:
                     # should not be possible
-                    log_both('Active task with nonexistent host', task_id=task['task_id'], level=logging.ERROR)
+                    log_both('Active task with nonexistent host', task_id=task['task_id'],
+                             level=logging.ERROR)
                     kojihub.Task(task['task_id']).free()
                     continue
                 host['data'] = {}

