refactor web pagination and add partitioned events, flap/timeout showcase agents

This commit is contained in:
Stanislav Rossovskii
2026-05-27 18:23:31 +04:00
parent d5f312f200
commit a2e88b4e76
46 changed files with 1600 additions and 742 deletions

View File

@@ -0,0 +1,143 @@
"""Monthly partition maintenance for `events` (PARTITION BY RANGE(received_at)).
Idempotent: creates current + N future month partitions and drops old ones beyond
the retention window. Names are derived from year/month, so they cannot inject SQL.
"""
from __future__ import annotations
import asyncio
from datetime import UTC, date, datetime
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker
from ..logging_config import get_logger
from ..settings import get_settings
log = get_logger("monlet.partitioning")
def _month_start(d: date) -> date:
return date(d.year, d.month, 1)
def _next_month(d: date) -> date:
year = d.year + (1 if d.month == 12 else 0)
month = 1 if d.month == 12 else d.month + 1
return date(year, month, 1)
def _shift_months(d: date, n: int) -> date:
y, m = d.year, d.month
total = y * 12 + (m - 1) + n
return date(total // 12, (total % 12) + 1, 1)
def _partition_name(d: date) -> str:
return f"events_p{d.year:04d}_{d.month:02d}"
async def ensure_event_partitions(engine_or_sm: AsyncEngine | async_sessionmaker) -> None:
"""Create current + N future month partitions for events. Idempotent."""
s = get_settings()
today = _month_start(datetime.now(UTC).date())
months = [_shift_months(today, i) for i in range(0, max(s.events_future_partitions, 0) + 1)]
statements: list[str] = []
for start in months:
end = _next_month(start)
# Explicit UTC qualifier — Postgres otherwise interprets bare date literals via
# the session TimeZone, which would shift partition bounds on non-UTC servers.
statements.append(
f"CREATE TABLE IF NOT EXISTS {_partition_name(start)} "
f"PARTITION OF events FOR VALUES FROM ('{start.isoformat()} UTC') "
f"TO ('{end.isoformat()} UTC');"
)
async with _connect(engine_or_sm) as conn:
for stmt in statements:
await conn.execute(text(stmt))
async def drop_old_event_partitions(engine_or_sm: AsyncEngine | async_sessionmaker) -> None:
"""Drop month partitions older than events_retention_months.
Each DROP runs in its own transaction so the ACCESS EXCLUSIVE lock on parent
`events` is held only for one partition at a time and ingest is not blocked
by a long maintenance batch.
"""
s = get_settings()
if s.events_retention_months <= 0:
return
cutoff = _shift_months(_month_start(datetime.now(UTC).date()), -s.events_retention_months)
engine = _resolve_engine(engine_or_sm)
async with engine.connect() as conn:
rows = (
await conn.execute(
text(
"""
SELECT c.relname
FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
JOIN pg_class p ON p.oid = i.inhparent
WHERE p.relname = 'events'
"""
)
)
).all()
to_drop: list[str] = []
for (name,) in rows:
if not name.startswith("events_p"):
continue
try:
y = int(name[8:12])
m = int(name[13:15])
except ValueError:
continue
if date(y, m, 1) < cutoff:
to_drop.append(name)
for name in to_drop:
async with engine.begin() as conn:
await conn.execute(text(f"DROP TABLE IF EXISTS {name};"))
log.info("event_partition_dropped", name=name)
def _resolve_engine(engine_or_sm: AsyncEngine | async_sessionmaker) -> AsyncEngine:
if isinstance(engine_or_sm, async_sessionmaker):
return engine_or_sm.kw["bind"]
return engine_or_sm
def _connect(engine_or_sm):
"""Return an async ctx for a transactional connection from either engine or sessionmaker."""
return _resolve_engine(engine_or_sm).begin()
async def run_partition_maintenance(sm: async_sessionmaker, stop_event: asyncio.Event) -> None:
"""Independent worker — runs even when MONLET_ENABLE_DETECTOR=false."""
interval = max(get_settings().partition_maintenance_interval_sec, 60)
while not stop_event.is_set():
try:
await ensure_event_partitions(sm)
await drop_old_event_partitions(sm)
except Exception as exc:
log.warning("partition_maintenance_failed", error=str(exc))
try:
await asyncio.wait_for(stop_event.wait(), timeout=interval)
except TimeoutError:
continue
# Re-export helpers for tests.
__all__ = [
"run_partition_maintenance",
"ensure_event_partitions",
"drop_old_event_partitions",
"_partition_name",
"_shift_months",
"_month_start",
]