fix: PG init backfill rollback scope + audit-loop state upsert registration

- db: translate INSERT OR IGNORE ... SELECT to ON CONFLICT DO NOTHING
  (VALUES-only regex left SELECT form untranslated -> PG syntax error);
  register task_audit_loop_state (task_id) in _OR_REPLACE_PK so PG
  INSERT OR REPLACE becomes a real upsert instead of DO NOTHING.
- server: _backfill_default_quotas uses SAVEPOINT containment and no
  longer commits the caller's transaction, so a backfill failure cannot
  roll back init_db's migrate ALTERs.
- worker: _coord_child_rows rolls back before column-fallback retries so
  a missing-column failure on PG does not cascade into
  InFailedSqlTransaction.
- tests: test_pg_init_fix.py (translation, SQLite backfill/upsert,
  real-PG fresh-init + merge + upsert + backfill containment, skipped
  without CLOUD_TEST_PG_DSN/driver).
This commit is contained in:
host committed 2026-11-04 18:02:22 +00:00
1 parent d99853b684
commit b0347b374d
4 files changed
+381 -4

No files matched your search

+27 -3
View File
@@ -1642,8 +1642,28 @@ def _backfill_default_quotas(conn):
只补“users 里有、user_quotas 里无”的用户,已有行(含
管理员显式设为 NULL 的不限额行)一律不动。返回补写行数。
失败只回滚到保存点,绝不回滚调用方的整个事务:init_db
里 migrate 的 ALTER 与它同事务,旧版 except 里整事务
rollback 会把 ALTER 一并丢掉(2026-10-31 新鲜 PG 库实测:
init 后 tasks 缺 paused_by_user / interrupted_reason,合流
随后在 PG“语句失败即 abort 整个事务”的语义下连环报
InFailedSqlTransaction)。成功时也不再由本函数提交,提交
由调用方负责(init_db 结尾统一提交;旧版成功时提交等于
提前提交 init 的全部 DDL,改后 init 恢复原子性)。调用方
只有 init_db 两处(均以提交收尾),改动不影响其它路径。
保存点在 SQLite 与 PG 下都支持;保存点本身建不起来时
(如连接已处 abort 态)退化为整事务 rollback 再返回 0。
"""
_ensure_quotas_table(conn)
try:
conn.execute("SAVEPOINT backfill_default_quotas")
except Exception:
try:
conn.rollback()
except Exception:
pass
return 0
try:
cur = conn.execute(
"INSERT OR IGNORE INTO user_quotas(user_id,"
@@ -1652,13 +1672,17 @@ def _backfill_default_quotas(conn):
" LEFT JOIN user_quotas q ON q.user_id = u.id"
" WHERE q.user_id IS NULL",
(_default_quota_usd_micros(), time.time()))
conn.commit()
conn.execute("RELEASE SAVEPOINT backfill_default_quotas")
return cur.rowcount or 0
except Exception:
try:
conn.rollback()
conn.execute("ROLLBACK TO SAVEPOINT backfill_default_quotas")
conn.execute("RELEASE SAVEPOINT backfill_default_quotas")
except Exception:
pass
try:
conn.rollback()
except Exception:
pass
return 0