diff --git a/cloud/tests/test_cloud_dispatch_pr.py b/cloud/tests/test_cloud_dispatch_pr.py index 7427e34..e3cd22d 100644 --- a/cloud/tests/test_cloud_dispatch_pr.py +++ b/cloud/tests/test_cloud_dispatch_pr.py @@ -458,5 +458,26 @@ class ProjectTaskFieldTestCase(_ServerCase): self.assertEqual(body["executor"], "docker") +class WorkerTablePrecisionTestCase(unittest.TestCase): + def test_workers_time_columns_are_double_precision(self): + # 回归:2026-10-26 主机实测,workers.last_seen 用 REAL 时, + # Postgres 的 float4 把 epoch 秒舍入到 128 秒一档,存活判断 + # 误差可达 ±64 秒、甚至出现负 age。新表必须是 DOUBLE PRECISION。 + import sqlite3 as _sqlite3 + tmp = tempfile.TemporaryDirectory() + db_path = os.path.join(tmp.name, "w.db") + from cloud.workers import ensure_workers_table + ensure_workers_table(db_path) + conn = _sqlite3.connect(db_path) + try: + rows = conn.execute("PRAGMA table_info(workers)").fetchall() + finally: + conn.close() + tmp.cleanup() + by_name = {r[1]: r[2] for r in rows} + self.assertIn("DOUBLE", by_name["last_seen"]) + self.assertIn("DOUBLE", by_name["created_at"]) + + if __name__ == "__main__": unittest.main() diff --git a/cloud/workers.py b/cloud/workers.py index fb6a789..73d16f9 100644 --- a/cloud/workers.py +++ b/cloud/workers.py @@ -42,7 +42,19 @@ def alive_threshold_seconds(override=None): def ensure_workers_table(db_path): - """建 workers 表(不存在时建,已存在时不动)。""" + """建 workers 表(不存在时建,已存在时不动,老库补精度)。 + + 注意:last_seen/created_at 必须是 DOUBLE PRECISION,不能用 REAL。 + SQLite 的 REAL 是 8 字节双精度,没问题;但 Postgres 的 REAL 是 + 4 字节 float4,只能存约 7 位有效数字,epoch 秒(10 位)会被 + 舍入到 128 秒一档,导致存活判断误差可达 ±64 秒、甚至出现负 age + (2026-10-26 主机实测:last_seen 比 NOW() 还大 20 秒)。 + 新表直接用 DOUBLE PRECISION;老库(已是 REAL 列)用 ALTER + TYPE 升到 DOUBLE PRECISION(float4 -> float8 无损,已存的舍入值 + 无法恢复,但之后的心跳不再舍入)。SQLite 下 ALTER TYPE 语法 + 不支持,靠 try/except 跳过——SQLite 的 REAL 本来就是双精度, + 不需要升。 + """ conn = _db_connect(str(db_path)) try: conn.execute( @@ -50,10 +62,38 @@ def ensure_workers_table(db_path): "worker_id TEXT PRIMARY KEY, " "executor TEXT, " "capacity INTEGER, " - "last_seen REAL, " - "created_at REAL)" + "last_seen DOUBLE PRECISION, " + "created_at DOUBLE PRECISION)" ) conn.commit() + # 老库升精度只在确实是 Postgres REAL 列时做一次:每次心跳都 + # ALTER 会反复拿 ACCESS EXCLUSIVE 锁,没必要。先查 + # information_schema(SQLite 下这张表不存在,查询失败即跳过, + # SQLite 的 REAL 本来就是双精度,不需要升)。 + for col in ("last_seen", "created_at"): + try: + row = conn.execute( + "SELECT data_type FROM information_schema.columns " + "WHERE table_name = 'workers' AND column_name = ?", + (col,)).fetchone() + except Exception: + try: + conn.rollback() + except Exception: + pass + continue + if not row or row[0] != "real": + continue + try: + conn.execute( + "ALTER TABLE workers ALTER COLUMN %s " + "TYPE DOUBLE PRECISION" % col) + conn.commit() + except Exception: + try: + conn.rollback() + except Exception: + pass finally: conn.close()