"""Shallow readiness with a dedicated short-timeout engine. The shared application engine bakes in 10s/30s timeouts, which are far too long for a probe that load balancers poll frequently. This builds a separate NullPool engine with a 1s connect and 2s read timeout (plus a server-side MAX_EXECUTION_TIME cap) so a stalled DB surfaces as not-ready fast instead of hanging the probe. A short TTL cache keeps a burst of probes from each opening a fresh connection. """ import logging import threading import time from sqlalchemy import create_engine, text from sqlalchemy.engine import URL, Engine from sqlalchemy.pool import NullPool from core.config import Config logger = logging.getLogger(__name__) _CACHE_AT = 'at' _CACHE_OK = 'ok' _CACHE: dict[str, float] = {_CACHE_AT: 0.0, _CACHE_OK: 0.0} _LOCK = threading.Lock() _TTL = 3.0 _engine: Engine | None = None def _ready_engine() -> Engine: """Build (once) the dedicated short-timeout engine used only by the readiness probe.""" global _engine if _engine is None: url = URL.create( 'mysql+pymysql', username=Config.MYSQL_DB_USER, password=Config.MYSQL_DB_PASS, host=Config.MYSQL_DB_HOST, port=int(Config.MYSQL_DB_PORT), database=Config.MYSQL_DB_NAME, ) _engine = create_engine( url, connect_args={'connect_timeout': 1, 'read_timeout': 2}, poolclass=NullPool, pool_pre_ping=False, ) return _engine def _probe() -> None: """Run a trivial server-capped query; raises if the DB is unreachable or slow.""" with _ready_engine().connect() as conn: conn.execute(text('SELECT /*+ MAX_EXECUTION_TIME(2000) */ 1')) def is_ready() -> bool: """Return whether the DB answered a shallow probe, served from a short TTL cache.""" now = time.monotonic() with _LOCK: if now - _CACHE[_CACHE_AT] < _TTL: return bool(_CACHE[_CACHE_OK]) ok = True try: _probe() except Exception: ok = False logger.debug('readiness probe failed', exc_info=True) with _LOCK: # Stamp at completion, not entry: a slow probe must not write an already-aged timestamp # that the next caller immediately treats as expired (defeating the cache when it is most # needed). _CACHE[_CACHE_AT] = time.monotonic() _CACHE[_CACHE_OK] = float(ok) return ok