fix: probe for the telemetry table before creating it
PostgreSQL checks the schema CREATE privilege before the IF NOT EXISTS existence test, so an account with only table-level INSERT was denied on CREATE TABLE IF NOT EXISTS even though the table was right there and writable. The denial set _failed and the whole recorder went no-op for the process lifetime, silently: 150+ calls downstream lost their latency, token and cost rows with nothing but one warning to show for it. The probe is the direct fix. The larger fix is the criterion: structural degradation now means "provably cannot write" (pool creation failed, or the table is absent and cannot be created), not "something threw during init" -- a probe or acquire failure just skips the row and retries on the next call. SQLite stays as it is on purpose. Measured: it short-circuits the statement at parse time, so it passes even under another connection's EXCLUSIVE lock or on a read-only file. A probe there would buy nothing; the docstring now says so to keep symmetry-minded future edits away.
This commit is contained in:
@@ -257,17 +257,41 @@ class TestSQLiteColumnBackfill:
|
||||
|
||||
|
||||
class _FakePgConn:
|
||||
"""记录执行过的语句;可让 ALTER 抛错以模拟权限不足。"""
|
||||
"""记录执行过的语句;可让 ALTER/CREATE/探测抛错以模拟权限不足与抖动。
|
||||
|
||||
def __init__(self, existing: list[str], *, fail_alter: bool = False):
|
||||
`existing` 为空列表即表示**表不存在**(与真实 PG 一致: `to_regclass` 为 NULL
|
||||
时列探测必然零行),故 `fetchval` 与 `fetch` 共用同一份事实。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
existing: list[str],
|
||||
*,
|
||||
fail_alter: bool = False,
|
||||
fail_create: bool = False,
|
||||
probe_errors: int = 0,
|
||||
):
|
||||
self.existing = existing
|
||||
self.fail_alter = fail_alter
|
||||
self.fail_create = fail_create
|
||||
self.probe_errors = probe_errors
|
||||
self.statements: list[str] = []
|
||||
|
||||
async def execute(self, sql, *args):
|
||||
self.statements.append(sql)
|
||||
if sql.startswith("ALTER TABLE") and self.fail_alter:
|
||||
raise RuntimeError("must be owner of table llm_calls")
|
||||
if sql.lstrip().startswith("CREATE TABLE"):
|
||||
if self.fail_create:
|
||||
raise RuntimeError("permission denied for schema public")
|
||||
self.existing = list(_EXPECTED_COLUMNS)
|
||||
|
||||
async def fetchval(self, sql, *args):
|
||||
self.statements.append(sql)
|
||||
if self.probe_errors > 0:
|
||||
self.probe_errors -= 1
|
||||
raise RuntimeError("connection was closed in the middle of operation")
|
||||
return "llm_calls" if self.existing else None
|
||||
|
||||
async def fetch(self, sql, *args):
|
||||
self.statements.append(sql)
|
||||
@@ -338,6 +362,76 @@ class TestPostgresBackfillDiscipline:
|
||||
assert all("IF NOT EXISTS" not in s for s in altered) # 探测已确认缺列,无需再判
|
||||
|
||||
|
||||
class TestPostgresTableProbe:
|
||||
"""建表必须先探测,且"判死"只认"确定写不进去"(issue #9)。
|
||||
|
||||
实测(PostgreSQL 16.14,只有表级 SELECT/INSERT 的角色): `CREATE TABLE IF NOT
|
||||
EXISTS` 被拒 permission denied for schema,而同一连接的 `INSERT` 通过——
|
||||
PG 对 schema 的 CREATE 权限检查早于 `IF NOT EXISTS` 的存在性判断。无条件发
|
||||
DDL 会让这类最小权限部署的整个进程静默失遥测。
|
||||
"""
|
||||
|
||||
_CURRENT = [
|
||||
"call_id",
|
||||
"cost",
|
||||
"created_at",
|
||||
"cached_prompt_tokens",
|
||||
"model_reported",
|
||||
"sampling",
|
||||
"reasoning_tokens",
|
||||
]
|
||||
|
||||
def _recorder(self, conn):
|
||||
from polygateway.telemetry.postgres import PostgresRecorder
|
||||
|
||||
return PostgresRecorder("postgresql://u:p@h:5432/polygateway", pool=_FakePgPool(conn))
|
||||
|
||||
def _created(self, conn):
|
||||
return [s for s in conn.statements if s.lstrip().startswith("CREATE TABLE")]
|
||||
|
||||
async def test_existing_table_is_never_recreated(self):
|
||||
"""表已存在就一条 DDL 都不发——这是权限被拒的唯一根治办法。"""
|
||||
conn = _FakePgConn(self._CURRENT)
|
||||
await _record_minimal(self._recorder(conn))
|
||||
assert not self._created(conn)
|
||||
|
||||
async def test_create_denied_on_existing_table_keeps_recording(self):
|
||||
"""就算 DDL 仍被发出并被拒,表存在时也不得判死整个 recorder。"""
|
||||
conn = _FakePgConn(self._CURRENT, fail_create=True)
|
||||
recorder = self._recorder(conn)
|
||||
await _record_minimal(recorder) # 不得抛
|
||||
assert recorder._failed is False
|
||||
assert any(s.startswith("INSERT INTO llm_calls") for s in conn.statements)
|
||||
|
||||
async def test_missing_table_is_created_and_not_backfilled(self):
|
||||
"""表不存在→建表;新建表列已齐全,不得再发补列 ALTER。"""
|
||||
conn = _FakePgConn([])
|
||||
recorder = self._recorder(conn)
|
||||
await _record_minimal(recorder)
|
||||
assert len(self._created(conn)) == 1
|
||||
assert not [s for s in conn.statements if s.startswith("ALTER TABLE")]
|
||||
assert recorder._failed is False
|
||||
assert any(s.startswith("INSERT INTO llm_calls") for s in conn.statements)
|
||||
|
||||
async def test_create_failure_on_missing_table_degrades_to_noop(self):
|
||||
"""表确定不存在且建不出来 = 确定写不进去: 此时才允许永久 no-op。"""
|
||||
conn = _FakePgConn([], fail_create=True)
|
||||
recorder = self._recorder(conn)
|
||||
await _record_minimal(recorder) # 不得抛
|
||||
assert recorder._failed is True
|
||||
assert not [s for s in conn.statements if s.startswith("INSERT INTO llm_calls")]
|
||||
|
||||
async def test_probe_failure_is_transient_not_terminal(self):
|
||||
"""探测失败多为连接抖动: 跳过本次,下次调用必须重试,绝不永久判死。"""
|
||||
conn = _FakePgConn(self._CURRENT, probe_errors=1)
|
||||
recorder = self._recorder(conn)
|
||||
await _record_minimal(recorder, call_id="first") # 不得抛
|
||||
assert recorder._failed is False
|
||||
assert not [s for s in conn.statements if s.startswith("INSERT INTO llm_calls")]
|
||||
await _record_minimal(recorder, call_id="second")
|
||||
assert [s for s in conn.statements if s.startswith("INSERT INTO llm_calls")]
|
||||
|
||||
|
||||
class _MemoryRecorder:
|
||||
def __init__(self):
|
||||
self.rows = []
|
||||
|
||||
Reference in New Issue
Block a user