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:
co-authored by
Claude Opus 4.8
parent
cf046648bd
commit
5b59f5e8f9
@@ -184,7 +184,10 @@ def containers_for_stack(stack_id: str) -> list[ContainerInfo]:
|
||||
id=c.id,
|
||||
name=c.name,
|
||||
service=c.labels.get(SERVICE_LABEL, c.name),
|
||||
image=(c.image.tags[0] if c.image and c.image.tags else attrs.get("Config", {}).get("Image", "")),
|
||||
# Use the configured image name from the inspect we already have;
|
||||
# reading c.image.tags would trigger a separate image-inspect call
|
||||
# per container.
|
||||
image=attrs.get("Config", {}).get("Image", "") or attrs.get("Image", ""),
|
||||
state=state.get("Status", c.status),
|
||||
status=attrs.get("State", {}).get("Status", c.status),
|
||||
health=health,
|
||||
@@ -207,21 +210,14 @@ def clear_busy(stack_id: str) -> None:
|
||||
_BUSY.discard(stack_id)
|
||||
|
||||
|
||||
def compute_status(stack_id: str) -> str:
|
||||
if stack_id in _BUSY:
|
||||
return "updating"
|
||||
try:
|
||||
containers = containers_for_stack(stack_id)
|
||||
except DockerError:
|
||||
return "unknown"
|
||||
if not containers:
|
||||
def is_busy(stack_id: str) -> bool:
|
||||
return stack_id in _BUSY
|
||||
|
||||
|
||||
def _status_from_states(states: list[str]) -> str:
|
||||
if not states:
|
||||
return "stopped"
|
||||
states = [c.state for c in containers]
|
||||
if any(s in ("dead",) for s in states):
|
||||
return "error"
|
||||
if any(
|
||||
c.state == "exited" and _nonzero_exit(c) for c in containers
|
||||
):
|
||||
if any(s == "dead" for s in states):
|
||||
return "error"
|
||||
running = [s for s in states if s == "running"]
|
||||
if len(running) == len(states):
|
||||
@@ -231,10 +227,44 @@ def compute_status(stack_id: str) -> str:
|
||||
return "stopped"
|
||||
|
||||
|
||||
def _nonzero_exit(c: ContainerInfo) -> bool:
|
||||
# We only have the textual state here; treat plain "exited" as stopped, not
|
||||
# an error unless health says otherwise. Detailed exit codes handled in detail view.
|
||||
return False
|
||||
def compute_status(stack_id: str, containers: Optional[list[ContainerInfo]] = None) -> str:
|
||||
"""Status for one stack. Pass already-fetched ``containers`` to avoid a
|
||||
redundant Docker round-trip (the detail view already has them)."""
|
||||
if stack_id in _BUSY:
|
||||
return "updating"
|
||||
try:
|
||||
if containers is None:
|
||||
containers = containers_for_stack(stack_id)
|
||||
except DockerError:
|
||||
return "unknown"
|
||||
return _status_from_states([c.state for c in containers])
|
||||
|
||||
|
||||
def stack_status_summaries() -> dict[str, dict]:
|
||||
"""One cheap Docker call → ``{project: {status, total, running}}`` for every
|
||||
stack at once.
|
||||
|
||||
Uses the low-level container *summary* list (``GET /containers/json``, no
|
||||
per-container inspect) grouped by the compose project label. This is far
|
||||
cheaper than calling :func:`containers_for_stack` (which full-inspects every
|
||||
container) once per stack for the overview — turning the stacks list from
|
||||
O(stacks × containers) Docker round-trips into a single one.
|
||||
"""
|
||||
client = get_client()
|
||||
raw = safe_call(client.api.containers, all=True)
|
||||
by_project: dict[str, list[str]] = {}
|
||||
for c in raw:
|
||||
project = (c.get("Labels") or {}).get(COMPOSE_LABEL)
|
||||
if project:
|
||||
by_project.setdefault(project, []).append(c.get("State", ""))
|
||||
return {
|
||||
project: {
|
||||
"status": _status_from_states(states),
|
||||
"total": len(states),
|
||||
"running": sum(1 for s in states if s == "running"),
|
||||
}
|
||||
for project, states in by_project.items()
|
||||
}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Reference in New Issue
Block a user