Live from arcstats (dirty), dmu_tx (throttled writes), each pool's txgs (bytes synced to disk) and objset kstats (bytes from programs). A landing tier — a mergerfs pool whose first branch is fast storage and whose last is a ZFS dataset — is detected from mergerfs's branch xattr; a mover status file in /run/landing-mover/ adds its queue, the file in flight and totals. Sensors zfs.write.*, zfs.<pool>.write_bps|logical_bps and zfs.<pool>.landing.* carry the history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
3.4 KiB
Go
87 lines
3.4 KiB
Go
package metrics
|
|
|
|
import "testing"
|
|
|
|
// real rows from grid's /proc/spl/kstat/zfs/tank/txgs, 2026-10-25
|
|
const txgsSample = `81 1 0x01 26 7072 20029594359 13953814329135
|
|
txg birth state ndirty nread nwritten reads writes otime qtime wtime stime
|
|
15812833 13936408791659 C 0 0 0 0 0 5119980242 2040 58385 309580
|
|
15812834 13941528771901 C 0 0 0 0 0 5119948815 8471 65826 112612
|
|
15812835 13946648720716 C 885760 0 2293760 0 367 5120100556 1704 56762 271264374
|
|
15812836 13951768821272 S 4194304 0 0 0 0 5120000000 1000 50000 0
|
|
15812837 13956888821272 O 0 0 0 0 0 0 0 0 0
|
|
`
|
|
|
|
func TestParseTxgs(t *testing.T) {
|
|
w, top := parseTxgs(txgsSample, 0)
|
|
if w != 2293760 || top != 15812835 {
|
|
t.Fatalf("from 0: written %d top %d", w, top)
|
|
}
|
|
// only txgs synced since the last look count, and an open or syncing txg never does
|
|
w, top = parseTxgs(txgsSample, 15812834)
|
|
if w != 2293760 || top != 15812835 {
|
|
t.Fatalf("after 15812834: written %d top %d", w, top)
|
|
}
|
|
w, top = parseTxgs(txgsSample, 15812835)
|
|
if w != 0 || top != 15812835 {
|
|
t.Fatalf("nothing new: written %d top %d", w, top)
|
|
}
|
|
}
|
|
|
|
func TestParseBranches(t *testing.T) {
|
|
p, m := parseBranches("/srv/landing/tank=RW:/tank=NC,50G:/mnt/old=RO\n")
|
|
if len(p) != 3 || p[0] != "/srv/landing/tank" || p[1] != "/tank" || p[2] != "/mnt/old" {
|
|
t.Fatalf("paths %q", p)
|
|
}
|
|
if m[0] != "RW" || m[1] != "NC" || m[2] != "RO" {
|
|
t.Fatalf("modes %q", m)
|
|
}
|
|
if p, _ := parseBranches("/a:/b"); len(p) != 2 {
|
|
t.Fatalf("bare paths %q", p)
|
|
}
|
|
}
|
|
|
|
func TestMountFor(t *testing.T) {
|
|
ms := []mountEntry{
|
|
{"/dev/sdj2", "/", "ext4"},
|
|
{"/dev/nvme0n1p1", "/srv/landing", "ext4"},
|
|
{"tank", "/tank", "zfs"},
|
|
{"tank/media", "/tank/media", "zfs"},
|
|
{"tank-landing", "/srv/tank", "fuse.mergerfs"},
|
|
}
|
|
cases := map[string]string{
|
|
"/srv/landing/tank": "/srv/landing", "/tank": "/tank", "/tank/media/x": "/tank/media",
|
|
"/tankfoo": "/", "/srv/landingx": "/",
|
|
}
|
|
for path, want := range cases {
|
|
if m, ok := mountFor(ms, path); !ok || m.point != want {
|
|
t.Errorf("%s: got %q want %q", path, m.point, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnescapeMount(t *testing.T) {
|
|
if got := unescapeMount(`/mnt/my\040disk`); got != "/mnt/my disk" {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestFoldZFSWrites(t *testing.T) {
|
|
mk := func(dirty uint64, bps, in, drain float64) Sample {
|
|
return Sample{ZFSWrite: &ZFSWrites{Dirty: dirty, Limit: 100, Pools: []PoolWrites{{Pool: "tank", WriteBps: bps,
|
|
Landing: &Landing{Branch: "/srv/landing/tank", InBps: in, DrainBps: drain, Mover: &Mover{State: "moving"}}}}}}
|
|
}
|
|
b := []Sample{mk(10, 100, 400, 0), mk(30, 300, 0, 60)}
|
|
got := fold(b).ZFSWrite
|
|
if got.Dirty != 20 || got.Pools[0].WriteBps != 200 || got.Pools[0].Landing.InBps != 200 || got.Pools[0].Landing.DrainBps != 30 {
|
|
t.Fatalf("folded %+v / %+v", got, got.Pools[0].Landing)
|
|
}
|
|
// the fold must not write through to the newest sample it copied from
|
|
if b[1].ZFSWrite.Pools[0].Landing.InBps != 0 || b[1].ZFSWrite.Dirty != 30 {
|
|
t.Fatalf("fold changed its input")
|
|
}
|
|
if got.Pools[0].Landing.Mover.State != "moving" {
|
|
t.Fatalf("mover state lost")
|
|
}
|
|
}
|