fix(workbench): configure psycopg snapshot transaction

Open the psycopg transaction without unsupported SQLAlchemy-style options, then set the PostgreSQL isolation and read-only characteristics before the first query. Cover the exact snapshot endpoint path with a regression test.
This commit is contained in:
2026-09-24 09:49:01 +08:00
parent 9335bdc1ea
commit ed06bc2bfc
2 changed files with 76 additions and 1 deletions
+4 -1
View File
@@ -44,7 +44,10 @@ async def workbench_sync_snapshot(
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)), ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)),
conn: AsyncConnection = Depends(get_project_pg_connection), conn: AsyncConnection = Depends(get_project_pg_connection),
): ):
async with conn.transaction(isolation_level="repeatable read", read_only=True): async with conn.transaction():
await conn.execute(
"SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY"
)
cursor = await WorkbenchEventRepository(conn, ctx.project_id).current_cursor() cursor = await WorkbenchEventRepository(conn, ctx.project_id).current_cursor()
runtime = await workbench_runtime(ctx=ctx, conn=conn) runtime = await workbench_runtime(ctx=ctx, conn=conn)
tasks, _ = await _service(ctx, conn).list( tasks, _ = await _service(ctx, conn).list(
+72
View File
@@ -1,4 +1,5 @@
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import datetime, timezone
from types import SimpleNamespace from types import SimpleNamespace
from uuid import uuid4 from uuid import uuid4
@@ -43,6 +44,77 @@ class FakeMetadataRepository:
return self.routings return self.routings
class FakeSnapshotConnection:
def __init__(self):
self.statements = []
@asynccontextmanager
async def transaction(self):
yield
async def execute(self, statement):
self.statements.append(statement)
class FakeEventRepository:
def __init__(self, _conn, _project_id):
pass
async def current_cursor(self):
return 12
class FakeOperationalTaskService:
async def list(self, **_kwargs):
return [], None
async def list_notifications(self, **_kwargs):
return [], None
@pytest.mark.anyio
async def test_sync_snapshot_starts_psycopg_read_only_repeatable_read_transaction(monkeypatch):
project_id = uuid4()
conn = FakeSnapshotConnection()
now = datetime.now(timezone.utc)
async def fake_runtime(**_kwargs):
return {
"availability": "not_configured",
"source": "none",
"scenario_availability": "not_configured",
"artifact_availability": "not_configured",
"schedule_availability": "not_configured",
"work_order_availability": "not_configured",
"alert_availability": "not_configured",
"generated_at": now,
"conditions": [],
"alerts": [],
"scenarios": [],
"latest_analysis": None,
"work_orders": [],
}
monkeypatch.setattr(endpoint, "WorkbenchEventRepository", FakeEventRepository)
monkeypatch.setattr(endpoint, "workbench_runtime", fake_runtime)
monkeypatch.setattr(endpoint, "_service", lambda _ctx, _conn: FakeOperationalTaskService())
snapshot = await endpoint.workbench_sync_snapshot(
ctx=ProjectContext(
project_id=project_id,
project_code="project-a",
user_id=uuid4(),
project_role="member",
),
conn=conn,
)
assert snapshot.event_cursor == 12
assert conn.statements == [
"SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY"
]
@pytest.mark.anyio @pytest.mark.anyio
async def test_expired_event_cursor_requires_snapshot_reset(monkeypatch): async def test_expired_event_cursor_requires_snapshot_reset(monkeypatch):
project_id = uuid4() project_id = uuid4()