fix(docker): a container's memory is its processes' own; file cache shown on its page (#6030)
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m29s
CI / integration (push) Successful in 1m45s
CI / publish (push) Successful in 1m43s

The agent sent Docker's working set (usage less inactive_file), which still
counts recently used file cache. A file-heavy container with no limit read
191 GB while its processes held 2.2 GB, and the host's own figure, which
already leaves cache out, disagreed with it.

Agent 1.23.0 sends mem_used_bytes / mem_used_pct (anon + shmem, so
Postgres's shared buffers count; v1: rss + shmem) and mem_cache_bytes
apart. The new names keep the two meanings apart in history and in learned
normals: docker_013 renames the columns, clears the old values and adds
mem_cache_bytes. The Docker page, swarm by-node sums, Busiest Containers,
container detail, normals and the MCP tools all read the new figure. File
cache is shown only on the container's page and in get_container.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 21:27:29 -04:00
co-authored by Claude Opus 5.5
parent 1335b9afaf
commit 56063f27a4
26 changed files with 226 additions and 99 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ NOW = datetime.now(timezone.utc)
def _c(name, **kw):
base = dict(name=name, host_id="h1", image="nginx:1", status="running", health=None, exit_code=None,
oom_killed=False, restart_count=0, cpu_pct=5.0, mem_pct=10.0, compose_project=None,
oom_killed=False, restart_count=0, cpu_pct=5.0, mem_used_pct=10.0, compose_project=None,
service_name=None, ports_json=None, started_at=NOW - timedelta(hours=2),
scraped_at=NOW - timedelta(seconds=20))
base.update(kw)
+2 -2
View File
@@ -15,8 +15,8 @@ NOW = datetime(2026, 10, 6, 12, 0, tzinfo=timezone.utc)
def _c(**kw):
base = dict(name="web", host_id="h1", image="nginx", status="running", health=None,
exit_code=None, oom_killed=False, restart_count=0, cpu_pct=1.0, mem_pct=2.0,
mem_usage_bytes=1, mem_limit_bytes=2, started_at=NOW - timedelta(hours=1),
exit_code=None, oom_killed=False, restart_count=0, cpu_pct=1.0, mem_used_pct=2.0,
mem_used_bytes=1, mem_limit_bytes=2, started_at=NOW - timedelta(hours=1),
compose_project=None, service_name=None, scraped_at=NOW - timedelta(seconds=30))
base.update(kw)
return NS(**base)
+14 -2
View File
@@ -265,7 +265,7 @@ def _ctr(**kw):
base = dict(name="plex", host_id="h1", image="plexinc/pms:1.40", status="exited", health=None,
exit_code=137, oom_killed=True, restart_count=2, compose_project="media",
service_name=None, node_id=None, scraped_at=NOW - timedelta(seconds=30),
started_at=NOW - timedelta(hours=3))
started_at=NOW - timedelta(hours=3), mem_cache_bytes=None)
base.update(kw)
return NS(**base)
@@ -312,6 +312,18 @@ async def test_a_running_swarm_task_reads_under_its_service(app):
assert "<dt>Swarm node</dt>" in page and "docker02" in page
async def test_file_cache_is_told_on_the_container_page_apart_from_its_memory(app):
"""#6030: its memory figure is what its processes hold; the file cache
charged to it is shown here, said to be given back, and is "—" while
nothing reports it."""
running = _ctr(status="running", exit_code=0, oom_killed=False, mem_cache_bytes=180 << 30)
page = await _container_page(app, running, exit_failed=False, uptime="3h")
assert "<dt>File cache</dt><dd>180.0 GiB" in page
assert "kept in memory until the host needs it back; not counted in its memory" in page
stopped = await _container_page(app, _ctr())
assert '<dt>File cache</dt><dd><span class="tone-dim">—</span></dd>' in stopped
async def test_a_service_log_line_names_its_task(app):
lines = [{"ts": NOW, "stream": "stderr", "line": "<b>boom</b>", "task": f"1 · {TASK[:10]}"}]
async with app.test_request_context("/plugins/docker/service/web/logs/lines"):
@@ -335,7 +347,7 @@ def _swarm_svc(name, placement):
def _task(name, service, node, cpu, mem, status="running"):
return NS(name=name, service_name=service, node_id=node, status=status, cpu_pct=cpu,
mem_usage_bytes=mem)
mem_used_bytes=mem)
def test_each_node_lists_what_runs_on_it_and_what_that_uses():
+2 -2
View File
@@ -81,12 +81,12 @@ def test_task_label_names_the_service_and_slot():
def test_usage_reads_cpu_in_cores_and_memory_against_a_real_limit():
gib = 1024 ** 3
c = NS(cpu_pct=150.0, mem_usage_bytes=gib, mem_limit_bytes=16 * gib, mem_pct=6.25)
c = NS(cpu_pct=150.0, mem_used_bytes=gib, mem_limit_bytes=16 * gib, mem_used_pct=6.25)
u = svc.usage(c, 4, 16 * gib)
assert u["cores"] == 1.5 and u["cpu_share"] == 37.5
# A limit of the host's whole memory is Docker saying "no limit".
assert u["mem_limit"] is None and u["mem_of"] == "the host's memory"
limited = svc.usage(NS(cpu_pct=None, mem_usage_bytes=gib, mem_limit_bytes=2 * gib, mem_pct=50.0), 4, 16 * gib)
limited = svc.usage(NS(cpu_pct=None, mem_used_bytes=gib, mem_limit_bytes=2 * gib, mem_used_pct=50.0), 4, 16 * gib)
assert limited["cores"] is None and limited["mem_limit"] == 2 * gib and limited["mem_of"] == "its limit"
# Unknown host size: cores still read, no share to grade against.
assert svc.usage(c, None, None)["cpu_share"] is None
+1 -1
View File
@@ -17,7 +17,7 @@ NOW = datetime(2026, 10, 6, 13, 30, tzinfo=timezone.utc)
def _c(host_id, name, *, service=None, node=None, status="running", cpu=1.0,
task=None, started=None):
return SimpleNamespace(host_id=host_id, name=name, service_name=service,
node_id=node, status=status, cpu_pct=cpu, mem_pct=2.0,
node_id=node, status=status, cpu_pct=cpu, mem_used_pct=2.0,
health=None, restart_count=0, container_id=name,
task_id=task, started_at=started)