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:
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user