perf(monitoring): stop blocking event loop with synchronous Docker calls
Customer search and detail loads were intermittently slow because every customer-table render (including each search keystroke) triggered /monitoring/customers/local-update-status, which looped synchronously over all customers doing blocking `docker inspect` subprocess calls on the event loop — stalling all other in-flight requests, including search itself. - Offload per-service image/container inspection to the thread pool and run checks concurrently instead of sequentially (image_service, docker_service) - Reuse a single Docker SDK client instead of reconnecting per customer - Cache local-update-status results for 20s since the underlying data only changes after an image pull, not on every keystroke - Parallelize /monitoring/customers container status lookups Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -27,13 +27,24 @@ async def _run_cmd(cmd: list[str], timeout: int = 120) -> subprocess.CompletedPr
|
||||
)
|
||||
|
||||
|
||||
_client: Optional[docker.DockerClient] = None
|
||||
|
||||
|
||||
def _get_client() -> docker.DockerClient:
|
||||
"""Return a Docker client connected via the Unix socket.
|
||||
"""Return a shared Docker client connected via the Unix socket.
|
||||
|
||||
The client is created once and reused — creating a new client per call
|
||||
(as `docker.from_env()` does) re-negotiates the API version and opens a
|
||||
fresh connection every time, which is wasteful when called once per
|
||||
customer in a loop.
|
||||
|
||||
Returns:
|
||||
docker.DockerClient instance.
|
||||
"""
|
||||
return docker.from_env()
|
||||
global _client
|
||||
if _client is None:
|
||||
_client = docker.from_env()
|
||||
return _client
|
||||
|
||||
|
||||
async def compose_up(
|
||||
@@ -212,6 +223,16 @@ def get_container_status(container_prefix: str) -> list[dict[str, Any]]:
|
||||
return results
|
||||
|
||||
|
||||
async def get_container_status_async(container_prefix: str) -> list[dict[str, Any]]:
|
||||
"""Thread-offloaded wrapper around get_container_status().
|
||||
|
||||
Use this when checking status for multiple customers so the Docker SDK
|
||||
calls run in the thread pool instead of blocking the event loop.
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(None, get_container_status, container_prefix)
|
||||
|
||||
|
||||
def get_container_logs(container_name: str, tail: int = 200) -> str:
|
||||
"""Retrieve recent logs from a container.
|
||||
|
||||
|
||||
@@ -211,6 +211,44 @@ async def pull_all_images(config) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
async def get_customer_container_image_status_async(container_prefix: str, config) -> dict[str, Any]:
|
||||
"""Async, thread-offloaded version of get_customer_container_image_status().
|
||||
|
||||
Runs the per-service `docker inspect` subprocess calls concurrently in the
|
||||
thread pool instead of sequentially blocking the event loop — use this
|
||||
whenever checking status for multiple customers (e.g. dashboard/search
|
||||
badge refresh, monitoring overview).
|
||||
|
||||
Returns:
|
||||
services: dict mapping service name to status info
|
||||
needs_update: True if any service has a different image ID than locally stored
|
||||
"""
|
||||
service_images = {
|
||||
"management": config.netbird_management_image,
|
||||
"signal": config.netbird_signal_image,
|
||||
"relay": config.netbird_relay_image,
|
||||
"dashboard": config.netbird_dashboard_image,
|
||||
}
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
async def _check(svc: str, image: str) -> tuple[str, dict[str, Any]]:
|
||||
container_name = f"{container_prefix}-{svc}"
|
||||
container_id, local_id = await asyncio.gather(
|
||||
loop.run_in_executor(None, get_container_image_id, container_name),
|
||||
loop.run_in_executor(None, get_local_image_id, image),
|
||||
)
|
||||
if container_id and local_id:
|
||||
up_to_date = container_id == local_id
|
||||
else:
|
||||
up_to_date = None # container not running or image not pulled
|
||||
return svc, {"container": container_name, "image": image, "up_to_date": up_to_date}
|
||||
|
||||
pairs = await asyncio.gather(*[_check(svc, image) for svc, image in service_images.items()])
|
||||
services = dict(pairs)
|
||||
needs_update = any(s["up_to_date"] is False for s in services.values())
|
||||
return {"services": services, "needs_update": needs_update}
|
||||
|
||||
|
||||
def get_customer_container_image_status(container_prefix: str, config) -> dict[str, Any]:
|
||||
"""Check which service containers are running outdated local images.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user