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:
2026-07-23 14:44:56 +02:00
co-authored by Claude Sonnet 5
parent f6b7eb2dae
commit ac843da4ca
4 changed files with 94 additions and 12 deletions
+23 -2
View File
@@ -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.
+38
View File
@@ -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.