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:
1 parent
3bc9d49d0c
commit
e2d6a454bf
2 files changed
+64
-3
No files matched your search
@@ -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
@@ -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()
|
||||
|
||||
|
||||
Reference in new issue
Block a user