mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
79 lines
2.7 KiB
Python
79 lines
2.7 KiB
Python
"""Engine-level Postgres session timeouts (2026-07-29 pool-exhaustion class).
|
|||
|
|
|
||
|
|
A session parked mid-transaction on non-DB work (a git subprocess, an
|
||
|
|
asyncio lock queue) holds its row locks and pooled connection until Postgres
|
||
|
|
kills it; a statement queued on someone else's row lock must give up instead
|
||
|
|
of camping on a pool slot. ``get_engine`` passes both as asyncpg
|
||
|
|
``server_settings`` so every app connection carries them; a 0 value drops
|
||
|
|
the setting entirely (Postgres-default behavior, operator off-switch).
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import Any
|
||
|
|
from unittest.mock import MagicMock
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from roboco.db import base as db_base
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def _holder_reset(monkeypatch: pytest.MonkeyPatch) -> Any:
|
||
|
|
"""Isolate _DbHolder so the test never touches the real engine."""
|
||
|
|
monkeypatch.setattr(db_base._DbHolder, "engine", None)
|
||
|
|
monkeypatch.setattr(db_base._DbHolder, "session_factory", None)
|
||
|
|
monkeypatch.setattr(db_base._DbHolder, "loop", None)
|
||
|
|
return db_base._DbHolder
|
||
|
|
|
||
|
|
|
||
|
|
def _capture_engine_kwargs(monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]:
|
||
|
|
captured: dict[str, Any] = {}
|
||
|
|
|
||
|
|
def _fake_create(_url: str, **kwargs: Any) -> MagicMock:
|
||
|
|
captured.update(kwargs)
|
||
|
|
return MagicMock()
|
||
|
|
|
||
|
|
monkeypatch.setattr(db_base, "create_async_engine", _fake_create)
|
||
|
|
return captured
|
||
|
|
|
||
|
|
|
||
|
|
def test_engine_carries_server_side_timeouts(
|
||
|
|
monkeypatch: pytest.MonkeyPatch, _holder_reset: Any
|
||
|
|
) -> None:
|
||
|
|
monkeypatch.setattr(
|
||
|
|
db_base.settings, "database_idle_in_transaction_timeout_ms", 120_000
|
||
|
|
)
|
||
|
|
monkeypatch.setattr(db_base.settings, "database_lock_timeout_ms", 30_000)
|
||
|
|
captured = _capture_engine_kwargs(monkeypatch)
|
||
|
|
|
||
|
|
db_base.get_engine()
|
||
|
|
|
||
|
|
assert captured["connect_args"]["server_settings"] == {
|
||
|
|
"idle_in_transaction_session_timeout": "120000",
|
||
|
|
"lock_timeout": "30000",
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def test_zero_disables_each_timeout_individually(
|
||
|
|
monkeypatch: pytest.MonkeyPatch, _holder_reset: Any
|
||
|
|
) -> None:
|
||
|
|
monkeypatch.setattr(db_base.settings, "database_idle_in_transaction_timeout_ms", 0)
|
||
|
|
monkeypatch.setattr(db_base.settings, "database_lock_timeout_ms", 5_000)
|
||
|
|
captured = _capture_engine_kwargs(monkeypatch)
|
||
|
|
|
||
|
|
db_base.get_engine()
|
||
|
|
|
||
|
|
assert captured["connect_args"]["server_settings"] == {"lock_timeout": "5000"}
|
||
|
|
|
||
|
|
|
||
|
|
def test_both_zero_sends_no_server_settings(
|
||
|
|
monkeypatch: pytest.MonkeyPatch, _holder_reset: Any
|
||
|
|
) -> None:
|
||
|
|
monkeypatch.setattr(db_base.settings, "database_idle_in_transaction_timeout_ms", 0)
|
||
|
|
monkeypatch.setattr(db_base.settings, "database_lock_timeout_ms", 0)
|
||
|
|
captured = _capture_engine_kwargs(monkeypatch)
|
||
|
|
|
||
|
|
db_base.get_engine()
|
||
|
|
|
||
|
|
assert captured["connect_args"] == {}
|