Fix workers time columns to DOUBLE PRECISION on Postgres

SQLite REAL is 8-byte double, but Postgres REAL is 4-byte
float4 (~7 significant digits). Epoch seconds (10 digits) were
rounded to 128-second steps, so worker liveness ages were off
by up to +-64s and could even go negative (observed live on
the host: last_seen ahead of NOW() by 20s). New tables use
DOUBLE PRECISION; existing Postgres tables are upgraded only
when information_schema actually reports REAL, so the ALTER
does not run on every heartbeat (it would take an ACCESS
EXCLUSIVE lock each time). SQLite path is unchanged (its
REAL is already double precision).
Regression test asserts new tables use DOUBLE PRECISION.
This commit is contained in:
host committed 2026-10-26 05:04:59 +08:00
1 parent 3bc9d49d0c
commit e2d6a454bf
2 files changed
+64 -3

No files matched your search

+21
View File
@@ -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()
+43 -3
View File
@@ -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()