"""Global hardware-access gate + pacing. Every command that touches the SAS bus — smartctl, sg_ses, zpool, lsblk, ledctl — must run inside this gate. It does two things: 1. Serializes hardware I/O (semaphore, default concurrency 1) so the poller can never fire a thundering herd at fragile SAS expanders. 2. Paces it: after every hardware op it holds the gate for POLL_DRIVE_GAP seconds, so back-to-back ops (gathered SMART, MegaRAID scans, SES, ledctl) are always spaced — not just the per-drive loop. This is a per-process gate; only the poller process calls hardware, so per-process serialization is sufficient. Created lazily so it binds to the running event loop. """ import asyncio import contextlib import logging import os logger = logging.getLogger(__name__) _sem: asyncio.Semaphore | None = None def _semaphore() -> asyncio.Semaphore: global _sem if _sem is None: n = max(1, int(os.environ.get("POLL_CONCURRENCY", "1"))) if n > 1: logger.warning( "POLL_CONCURRENCY=%d (>1) — hardware ops will run concurrently; " "only do this on backplanes known to tolerate it", n, ) _sem = asyncio.Semaphore(n) return _sem @contextlib.asynccontextmanager async def gate(): """Acquire the hardware gate for one op, then hold it for the pacing gap.""" sem = _semaphore() async with sem: try: yield finally: gap = float(os.environ.get("POLL_DRIVE_GAP", "0.5")) if gap > 0: await asyncio.sleep(gap)