refactor(db)!: adopt project-routed pooled databases
Reorganize WNDB by responsibility and remove legacy scheme endpoints.\n\nRoute analysis and time-series access through project pools, preserve transactional realtime replacement, and refresh GIS materialized views after writes.\n\nAdd database architecture documentation, live pooling coverage, API contract updates, and executable container verification.\n\nBREAKING CHANGE: legacy scheme APIs and flat app.native.wndb module imports are removed.
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
from contextlib import contextmanager
|
||||
|
||||
import pytest
|
||||
|
||||
from app.infra.db.timescaledb import sync_pool
|
||||
|
||||
|
||||
class _FakePool:
|
||||
def __init__(self, *, conninfo, **_kwargs):
|
||||
self.conninfo = conninfo
|
||||
self.closed = False
|
||||
self.borrowed = 0
|
||||
self.returned = 0
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
@contextmanager
|
||||
def connection(self):
|
||||
self.borrowed += 1
|
||||
try:
|
||||
yield object()
|
||||
finally:
|
||||
self.returned += 1
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_pools():
|
||||
sync_pool._pools.clear()
|
||||
sync_pool._pool_conninfo.clear()
|
||||
sync_pool._pool_borrows.clear()
|
||||
yield
|
||||
sync_pool._pools.clear()
|
||||
sync_pool._pool_conninfo.clear()
|
||||
sync_pool._pool_borrows.clear()
|
||||
|
||||
|
||||
def test_pool_reuses_same_routed_timescale_dsn(monkeypatch):
|
||||
monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
|
||||
monkeypatch.setattr(
|
||||
sync_pool,
|
||||
"get_project_timescale_pgconn_string",
|
||||
lambda *, db_name: f"dbname={db_name}",
|
||||
)
|
||||
|
||||
first = sync_pool.get_timescale_pool("tjwater_next")
|
||||
second = sync_pool.get_timescale_pool("tjwater_next")
|
||||
|
||||
assert first is second
|
||||
|
||||
|
||||
def test_pool_rebuilds_after_routing_change(monkeypatch):
|
||||
monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
|
||||
dsn = {"value": "host=old dbname=tjwater_next"}
|
||||
monkeypatch.setattr(
|
||||
sync_pool,
|
||||
"get_project_timescale_pgconn_string",
|
||||
lambda *, db_name: dsn["value"],
|
||||
)
|
||||
|
||||
old = sync_pool.get_timescale_pool("tjwater_next")
|
||||
dsn["value"] = "host=new dbname=tjwater_next"
|
||||
new = sync_pool.get_timescale_pool("tjwater_next")
|
||||
|
||||
assert old.closed is True
|
||||
assert new is not old
|
||||
|
||||
|
||||
def test_connection_is_returned_to_pool(monkeypatch):
|
||||
monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
|
||||
monkeypatch.setattr(
|
||||
sync_pool,
|
||||
"get_project_timescale_pgconn_string",
|
||||
lambda *, db_name: f"dbname={db_name}",
|
||||
)
|
||||
|
||||
pool = sync_pool.get_timescale_pool("tjwater_next")
|
||||
with sync_pool.timescale_connection("tjwater_next"):
|
||||
assert pool.borrowed == 1
|
||||
assert pool.returned == 0
|
||||
assert pool.returned == 1
|
||||
|
||||
|
||||
def test_close_removes_pool(monkeypatch):
|
||||
monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
|
||||
monkeypatch.setattr(
|
||||
sync_pool,
|
||||
"get_project_timescale_pgconn_string",
|
||||
lambda *, db_name: f"dbname={db_name}",
|
||||
)
|
||||
|
||||
pool = sync_pool.get_timescale_pool("tjwater_next")
|
||||
sync_pool.close_timescale_pool("tjwater_next")
|
||||
|
||||
assert pool.closed is True
|
||||
assert "tjwater_next" not in sync_pool._pools
|
||||
|
||||
|
||||
def test_close_all_removes_every_pool(monkeypatch):
|
||||
monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
|
||||
monkeypatch.setattr(
|
||||
sync_pool,
|
||||
"get_project_timescale_pgconn_string",
|
||||
lambda *, db_name: f"dbname={db_name}",
|
||||
)
|
||||
|
||||
first = sync_pool.get_timescale_pool("first")
|
||||
second = sync_pool.get_timescale_pool("second")
|
||||
sync_pool.close_all_timescale_pools()
|
||||
|
||||
assert first.closed is True
|
||||
assert second.closed is True
|
||||
assert sync_pool._pools == {}
|
||||
|
||||
|
||||
def test_pool_cache_evicts_least_recently_used_idle_pool(monkeypatch):
|
||||
monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
|
||||
monkeypatch.setattr(sync_pool.settings, "PROJECT_TS_CACHE_SIZE", 2)
|
||||
monkeypatch.setattr(
|
||||
sync_pool,
|
||||
"get_project_timescale_pgconn_string",
|
||||
lambda *, db_name: f"dbname={db_name}",
|
||||
)
|
||||
|
||||
first = sync_pool.get_timescale_pool("first")
|
||||
second = sync_pool.get_timescale_pool("second")
|
||||
sync_pool.get_timescale_pool("first")
|
||||
third = sync_pool.get_timescale_pool("third")
|
||||
|
||||
assert list(sync_pool._pools) == ["first", "third"]
|
||||
assert second.closed is True
|
||||
assert first.closed is False
|
||||
assert third.closed is False
|
||||
|
||||
|
||||
def test_pool_cache_does_not_evict_active_pool(monkeypatch):
|
||||
monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
|
||||
monkeypatch.setattr(sync_pool.settings, "PROJECT_TS_CACHE_SIZE", 1)
|
||||
monkeypatch.setattr(
|
||||
sync_pool,
|
||||
"get_project_timescale_pgconn_string",
|
||||
lambda *, db_name: f"dbname={db_name}",
|
||||
)
|
||||
|
||||
with sync_pool.timescale_connection("active"):
|
||||
active = sync_pool._pools["active"]
|
||||
sync_pool.get_timescale_pool("new")
|
||||
assert active.closed is False
|
||||
assert set(sync_pool._pools) == {"active", "new"}
|
||||
|
||||
assert list(sync_pool._pools) == ["new"]
|
||||
assert active.closed is True
|
||||
Reference in New Issue
Block a user