Perf: serve stacks list from one Docker call; registry host → 10.10.6.10 (0.21.6)

Stacks overview and detail were doing an N+1 inspect storm: list_stacks
called containers_for_stack AND compute_status (which re-fetched) per
stack, and containers.list(sparse=False) full-inspects every container
plus c.image triggered an image-inspect each. For N stacks that was
~2N*(1 list + M inspects + M image-inspects) sequential socket round
trips (~1s for just 2 stacks, growing linearly).

- compose_service.stack_status_summaries(): one low-level
  api.containers(all=True) summary call grouped by compose project label
  → whole list served in a single Docker round-trip (~10x faster).
- compute_status() takes optional pre-fetched containers; get_stack and
  _stack_summary no longer double-fetch.
- containers_for_stack() reads the image name from the inspect it already
  has instead of c.image (drops the per-container image-inspect).
- Same batching applied to the agent's stack list/detail.

Also: Forgejo (registry + git) moved to 10.10.6.10:3020 — updated image
refs in docker-compose.yml, agent/Dockerfile, agent/docker-compose.yml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-08 18:53:12 +00:00
co-authored by Claude Opus 4.8
parent cf046648bd
commit 5b59f5e8f9
8 changed files with 120 additions and 52 deletions
+35 -13
View File
@@ -58,20 +58,37 @@ def _get_stack_or_404(session: Session, stack_id: str) -> Stack:
return stack
def _stack_summary(stack: Stack) -> dict:
try:
containers = compose_service.containers_for_stack(stack.id)
status = compose_service.compute_status(stack.id)
except DockerError:
containers = []
status = "unknown"
def _stack_summary(stack: Stack, summaries: dict | None = None) -> dict:
"""Build a list-row summary.
Pass ``summaries`` (from :func:`compose_service.stack_status_summaries`) to
serve the whole stacks list from a single Docker call. Without it (single
create/update/clone responses), fall back to one direct query for this stack.
"""
if summaries is None:
try:
containers = compose_service.containers_for_stack(stack.id)
total = len(containers)
running = sum(1 for c in containers if c.state == "running")
status = compose_service.compute_status(stack.id, containers)
except DockerError:
total = running = 0
status = "unknown"
else:
info = summaries.get(stack.id)
total = info["total"] if info else 0
running = info["running"] if info else 0
if compose_service.is_busy(stack.id):
status = "updating"
else:
status = info["status"] if info else "stopped"
return {
"id": stack.id,
"name": stack.name,
"description": stack.description,
"status": status,
"service_count": len(containers),
"running_count": sum(1 for c in containers if c.state == "running"),
"service_count": total,
"running_count": running,
"created_at": stack.created_at,
"updated_at": stack.updated_at,
}
@@ -89,7 +106,11 @@ def list_stacks(
) -> list[dict]:
sync_discovered_stacks(session)
stacks = session.exec(select(Stack)).all()
return [_stack_summary(s) for s in stacks]
try:
summaries = compose_service.stack_status_summaries()
except DockerError:
summaries = {}
return [_stack_summary(s, summaries) for s in stacks]
@router.post("", status_code=201)
@@ -130,9 +151,10 @@ def get_stack(
) -> dict:
stack = _get_stack_or_404(session, stack_id)
try:
containers = [asdict(c) for c in compose_service.containers_for_stack(stack_id)]
status = compose_service.compute_status(stack_id)
except DockerError as exc:
raw = compose_service.containers_for_stack(stack_id)
containers = [asdict(c) for c in raw]
status = compose_service.compute_status(stack_id, raw)
except DockerError:
containers = []
status = "unknown"
return {