From 4ef2fa2dff1c171bd76b8c501e8d67a0317b70a8 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 01:13:29 +0200 Subject: [PATCH] disk_folders: bound the walk at 25 s, below the tool runner's 30 s, and say findings as lines (issue 1804) --- modules/disk-load/README.md | 2 +- modules/disk-load/cmd/disk-load/folders.go | 4 ++-- modules/disk-load/cmd/disk-load/folders_test.go | 11 +++++++++-- modules/disk-load/cmd/disk-load/tools.go | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/modules/disk-load/README.md b/modules/disk-load/README.md index 7de42a52..4da17db5 100644 --- a/modules/disk-load/README.md +++ b/modules/disk-load/README.md @@ -32,7 +32,7 @@ All six only read. | `disk_top` | Over a window. `by=unit` (the default) ranks systemd units and containers by bytes moved, from their cgroups. `by=container` ranks containers by name, and `by=process` ranks processes | `iotop` | | `disk_filesystems` | Each filesystem that stores data, said once however many places it is mounted: size, used and free GiB, used % as `df` counts it, inodes used % | `df -h`, `df -i` | | `disk_devices` | Each disk without sampling: model, size, whether it spins, scheduler, queue limit, what it is built on, and since boot the GiB read and written, the busy share and the wait per read and per write | `lsblk`, `cat /proc/diskstats` | -| `disk_folders` | Which folders fill a disk, like `du -x`: the largest folders under `path` (default `/`) to `depth` levels (default 2, at most 3), `limit` of them (default 20, at most 100), with bytes and GiB. Sizes are allocated bytes, a hard link once. It stays on the path's one filesystem: a mount below it is named, never entered. It reads every folder the tool runner can read (docker's included) and no content; folders it cannot read are counted and named. After 60 s it answers what it has measured, marked `partial`, with `stopped_at` (novox/hq issue 1804) | `du -x`, `ncdu` | +| `disk_folders` | Which folders fill a disk, like `du -x`: the largest folders under `path` (default `/`) to `depth` levels (default 2, at most 3), `limit` of them (default 20, at most 100), with bytes and GiB. Sizes are allocated bytes, a hard link once. It stays on the path's one filesystem: a mount below it is named, never entered. It reads every folder the tool runner can read (docker's included) and no content; folders it cannot read are counted and named. After 25 s (the tool runner gives a call 30 s) it answers what it has measured, marked `partial`, with `stopped_at` (novox/hq issue 1804) | `du -x`, `ncdu` | | `cpu_top` | Samples a window (`seconds`, default 3, at most 10). It answers the machine's CPU in use (% of all CPUs and as CPUs busy, from `/proc/stat`), then `by=unit` (the default) ranks systemd units and containers by CPU, from their cgroups' cpu.stat, busiest first (`limit`, default 15), with the busy time no unit accounts for; `by=container` ranks containers by name | `top`, `systemd-cgtop` | ### How each number is read diff --git a/modules/disk-load/cmd/disk-load/folders.go b/modules/disk-load/cmd/disk-load/folders.go index a0fb425f..59b17e3b 100644 --- a/modules/disk-load/cmd/disk-load/folders.go +++ b/modules/disk-load/cmd/disk-load/folders.go @@ -13,7 +13,7 @@ import ( ) // FolderBudget is how long a folder walk may take before it answers what it has measured. -const FolderBudget = 60 * time.Second +const FolderBudget = 25 * time.Second // namedUnreadable is how many unreadable folders are named; the rest are only counted. const namedUnreadable = 50 @@ -130,7 +130,7 @@ func (m *Machine) Folders(path string, depth, limit int) (FolderReport, error) { if len(r.Skipped) > 0 { said = append(said, "other filesystems not entered: "+strings.Join(r.Skipped, ", ")) } - r.Said = strings.Join(said, "; ") + r.Said = said return r, nil } diff --git a/modules/disk-load/cmd/disk-load/folders_test.go b/modules/disk-load/cmd/disk-load/folders_test.go index 7a9d5ed8..b3ee6445 100644 --- a/modules/disk-load/cmd/disk-load/folders_test.go +++ b/modules/disk-load/cmd/disk-load/folders_test.go @@ -132,7 +132,7 @@ func TestUnreadableFoldersAreCountedAndNamed(t *testing.T) { if r.Unreadable != 2 || strings.Join(r.UnreadableFolders, ",") != "/var/lib/docker/overlay2/b,/var/log" { t.Errorf("%d: %v", r.Unreadable, r.UnreadableFolders) } - if r.Partial || !strings.Contains(r.Said, "2 folders could not be read") { + if r.Partial || !strings.Contains(strings.Join(r.Said, "\n"), "2 folders could not be read") { t.Errorf("%+v", r) } } @@ -144,7 +144,7 @@ func TestALongWalkAnswersPartialAndSaysWhereItStopped(t *testing.T) { now := time.Unix(0, 0) m.Clock = func() time.Time { now = now.Add(10 * time.Second); return now } r := folders(t, m, nil) - if !r.Partial || r.StoppedAt == "" || !strings.Contains(r.Said, "PARTIAL") || !strings.Contains(r.Said, r.StoppedAt) { + if !r.Partial || r.StoppedAt == "" || len(r.Said) < 2 || !strings.HasPrefix(r.Said[0], "PARTIAL") || !strings.Contains(r.Said[0], r.StoppedAt) { t.Errorf("%+v", r) } } @@ -161,3 +161,10 @@ func TestAHardLinkIsCountedOnce(t *testing.T) { t.Errorf("counted twice: %d", r.Bytes) } } + +// 1804/AC7: the bound sits below the 30 s the tool runner gives a call, so a partial answer reaches its caller. +func TestTheWalkBoundIsBelowTheToolRunnersCallLimit(t *testing.T) { + if FolderBudget >= 30*time.Second { + t.Errorf("the budget %s is not below the 30 s a call gets", FolderBudget) + } +} diff --git a/modules/disk-load/cmd/disk-load/tools.go b/modules/disk-load/cmd/disk-load/tools.go index ed759980..05b1e89e 100644 --- a/modules/disk-load/cmd/disk-load/tools.go +++ b/modules/disk-load/cmd/disk-load/tools.go @@ -141,7 +141,7 @@ func Tools(m *Machine) []stdio.Tool { Description: "Which folders fill a disk, like du -x: the largest folders under a path, measured on disk (allocated bytes, a hard link " + "once), staying on the path's one filesystem (a mount below it is named, never entered). path defaults to /, depth to 2 (at most 3), " + "limit to 20 (at most 100). Sizes in bytes and GiB. It reads every folder the tool runner can read, none of the content; folders " + - "it cannot read are counted and named. It stops after 60 s and answers what it measured so far, marked partial, with where it stopped. " + + "it cannot read are counted and named. It stops after 25 s and answers what it measured so far, marked partial, with where it stopped. " + "Replaces du. (r)", Input: map[string]any{ "path": map[string]any{"type": "string", "description": "the absolute folder to measure (default /)"},