fix(automations): 审计行不再显式插 TEXT id,PG identity 下不再丢审计
旧 _audit_row 显式插 uuid 当 audit_log.id:SQLite 下能跑, 生产 PG 的 id 是 BIGINT GENERATED BY DEFAULT AS IDENTITY, 插 TEXT 直接报错、审计行全丢(best-effort 吞掉后无声无息, #156 留下的已知局限)。修法:INSERT 一律不带 id,让两端 自增/identity 生成;表不存在时先按 PG 口径建、再按 SQLite 口径建、逐个重试——automations 按设计不 import 其它 cloud 模块,所以用先插、失败再按口径重试做方言探测。 单测:test_automation_run_due 4 项(含新增 PG-like 假连接 1 项:只接受不带 id 的 INSERT,旧实现在此应失败)。
This commit is contained in:
1 parent
0c458cb2ce
commit
8afcd3253e
2 files changed
+74
-12
No files matched your search
+36
-12
@@ -566,26 +566,50 @@ def _automation_quota_exceeded(conn, owner):
|
|||||||
|
|
||||||
|
|
||||||
def _audit_row(conn, project_id, action, detail, now):
|
def _audit_row(conn, project_id, action, detail, now):
|
||||||
"""best-effort 写审计行:失败不影响自动化运行本身。"""
|
"""best-effort 写审计行:失败不影响自动化运行本身。
|
||||||
|
|
||||||
|
旧实现显式插 TEXT uuid 当 id:SQLite 下能跑,但生产 PG 的
|
||||||
|
audit_log.id 是 BIGINT GENERATED BY DEFAULT AS IDENTITY,
|
||||||
|
插 TEXT 直接报错、审计行全丢(best-effort 吞掉后无声无息)。
|
||||||
|
修法:INSERT 一律不带 id,让两端自增/identity 生成;
|
||||||
|
表不存在时先按 PG 口径建、再按 SQLite 口径建、逐个重试,
|
||||||
|
哪边建表成功就用哪边——automations 按设计不 import 其它
|
||||||
|
cloud 模块,所以用“先插、失败再按口径重试”做方言探测,
|
||||||
|
不靠 import 判断后端。
|
||||||
|
"""
|
||||||
|
params = ("system", project_id, action, detail, now)
|
||||||
try:
|
try:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS audit_log("
|
"INSERT INTO audit_log(user_id, project_id, action,"
|
||||||
"id TEXT PRIMARY KEY, "
|
" detail, created_at) VALUES(?, ?, ?, ?, ?)", params)
|
||||||
"user_id TEXT, "
|
|
||||||
"project_id TEXT, "
|
|
||||||
"action TEXT, "
|
|
||||||
"detail TEXT, "
|
|
||||||
"created_at DOUBLE PRECISION)")
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO audit_log(id, user_id, project_id, action,"
|
|
||||||
" detail, created_at) VALUES(?, 'system', ?, ?, ?, ?)",
|
|
||||||
(uuid.uuid4().hex, project_id, action, detail, now))
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
for ddl in (
|
||||||
|
"CREATE TABLE IF NOT EXISTS audit_log("
|
||||||
|
"id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, "
|
||||||
|
"user_id TEXT, project_id TEXT, action TEXT, detail TEXT, "
|
||||||
|
"created_at DOUBLE PRECISION)",
|
||||||
|
"CREATE TABLE IF NOT EXISTS audit_log("
|
||||||
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||||
|
"user_id TEXT, project_id TEXT, action TEXT, detail TEXT, "
|
||||||
|
"created_at REAL)"):
|
||||||
|
try:
|
||||||
|
conn.execute(ddl)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO audit_log(user_id, project_id, action,"
|
||||||
|
" detail, created_at) VALUES(?, ?, ?, ?, ?)", params)
|
||||||
|
conn.commit()
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
conn.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def run_due_automation(conn, automation_id, now=None):
|
def run_due_automation(conn, automation_id, now=None):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import time
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from cloud.automations import (
|
from cloud.automations import (
|
||||||
|
_audit_row,
|
||||||
create_automation,
|
create_automation,
|
||||||
ensure_automations_tables,
|
ensure_automations_tables,
|
||||||
list_automation_runs,
|
list_automation_runs,
|
||||||
@@ -48,6 +49,43 @@ def _set_quota(db_path, user_id, micros):
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
class AuditRowPgIdentityTestCase(unittest.TestCase):
|
||||||
|
"""模拟 PG:id 是 BIGINT identity,显式插 TEXT uuid 会报错。
|
||||||
|
|
||||||
|
假连接只接受不带 id 的 INSERT;旧实现(带 TEXT id)在此
|
||||||
|
假连接上应失败、新实现应成功且审计行落盘。
|
||||||
|
"""
|
||||||
|
|
||||||
|
class _PgLikeConn:
|
||||||
|
def __init__(self):
|
||||||
|
self.rows = []
|
||||||
|
self.committed = False
|
||||||
|
|
||||||
|
def execute(self, sql, params=()):
|
||||||
|
if sql.strip().upper().startswith("INSERT INTO AUDIT_LOG(ID"):
|
||||||
|
raise ValueError("column \"id\" is of type bigint")
|
||||||
|
if sql.strip().upper().startswith("INSERT INTO AUDIT_LOG"):
|
||||||
|
self.rows.append(params)
|
||||||
|
return None
|
||||||
|
raise ValueError("unexpected sql in fake PG conn")
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
self.committed = True
|
||||||
|
|
||||||
|
def rollback(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_insert_without_id_succeeds_on_pg_like_conn(self):
|
||||||
|
conn = self._PgLikeConn()
|
||||||
|
_audit_row(conn, "p1", "automation_run_created",
|
||||||
|
"auto1 -> task1", 123.0)
|
||||||
|
self.assertTrue(conn.committed)
|
||||||
|
self.assertEqual(len(conn.rows), 1)
|
||||||
|
# 参数里没有 uuid:第一位是 system,第二位是 project_id。
|
||||||
|
self.assertEqual(conn.rows[0][0], "system")
|
||||||
|
self.assertEqual(conn.rows[0][1], "p1")
|
||||||
|
|
||||||
|
|
||||||
class RunDueAutomationTestCase(unittest.TestCase):
|
class RunDueAutomationTestCase(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.tmp = tempfile.TemporaryDirectory()
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
|||||||
Reference in new issue
Block a user