From b74deffb26ba8e08343aa928f6f3a561990d6e2d Mon Sep 17 00:00:00 2001 From: host Date: Mon, 26 Oct 2026 07:13:45 +0800 Subject: [PATCH] hotfix: autoscaler queue_depth NULL depends_on blinded scaler queue_depth turned a NULL depends_on into [None], and ready_tasks then required None to be in completed_ids, so such tasks never counted as ready. Worker pick_next was unaffected (_task_from_row maps NULL to []), which is why tasks still ran while the scaler always saw depth 0 and never scaled up (proven live: depth probe with 1 queued task returned 0). NULL now means no dependencies, with a regression test. --- cloud/autoscaler.py | 10 +++++++++- cloud/tests/test_autoscaler.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/cloud/autoscaler.py b/cloud/autoscaler.py index ff66f66..f8e9de9 100644 --- a/cloud/autoscaler.py +++ b/cloud/autoscaler.py @@ -65,7 +65,15 @@ def queue_depth(db_path, executor=None): deps = row["depends_on"] except (KeyError, IndexError, TypeError): deps = [] - if isinstance(deps, str): + if deps is None: + # depends_on 为 NULL(裸 SQL 插的任务、或老数据)表示 + # 无依赖,绝不能变成 [None]——那样 ready_tasks 会判定 + # 它依赖一个永远不存在的 None,任务永远不算就绪, + # scaler 也就永远看不到深度(2026-10-26 实测教训: + # worker 的 _task_from_row 对 NULL 处理正确所以能 + # 正常认领,只有 scaler 的深度计算被这个 bug 致盲)。 + deps = [] + elif isinstance(deps, str): try: deps = json.loads(deps) if deps else [] except ValueError: diff --git a/cloud/tests/test_autoscaler.py b/cloud/tests/test_autoscaler.py index d69834a..da9ec85 100644 --- a/cloud/tests/test_autoscaler.py +++ b/cloud/tests/test_autoscaler.py @@ -85,6 +85,24 @@ class QueueDepthTestCase(unittest.TestCase): _set_status(self.db_path, "A", "completed") self.assertEqual(queue_depth(self.db_path), 1) + def test_null_depends_on_counts_as_ready(self): + # 回归:depends_on 为 NULL 时必须算无依赖,不能变成 + # [None] 导致任务永远不算就绪(scaler 致盲)。 + conn = sqlite3.connect(self.db_path) + try: + now = time.time() + conn.execute( + "INSERT INTO tasks(id, project_id, prompt, status," + " idempotency_key, depends_on, priority, created_by," + " created_at, updated_at, last_heartbeat)" + " VALUES('N', 'p1', 'x', 'queued', NULL, NULL, 0," + " 'tester', ?, ?, NULL)", + (now, now)) + conn.commit() + finally: + conn.close() + self.assertEqual(queue_depth(self.db_path), 1) + def test_dependency_cycle_counts_zero(self): _insert_task(self.db_path, "A", depends_on='["B"]') _insert_task(self.db_path, "B", depends_on='["A"]')