Research digest 0935 — Go errors.Join cleanup-path audit + slog key standardisation (2026-05-31)¶
Scope¶
Audit of 28 fork-original Go files under cmd/ and pkg/ that use fmt.Errorf, focused on multi-step pipelines where a primary error and a cleanup error can both arise. The companion goal is to retire the "err" vs "error" slog key drift across the Phase 4b Go binaries.
Method¶
grep -rIln 'fmt.Errorf' cmd/ pkg/→ 28 files.- For each, grep for cleanup keywords (
defer …Close,os.Remove,cleanup,Cleanup) and the lossy patterns_ = X()orX() //nolint:errcheck. - Read each candidate to confirm an actual lost error (vs a deferred close on a successful path where the close-error is genuinely irrelevant).
- Cross-reference
slog.Xcall sites for the error key.
Findings — lost cleanup errors (5 files, 6 sites)¶
| File | Site | What was lost | Fix |
|---|---|---|---|
pkg/bisect/bisect.go | Run L245 | os.Remove(encodedPath) after scoreErr | errors.Join of score-err + remove-err, with errors.Is(rmErr, os.ErrNotExist) guard |
pkg/bisect/bisect.go | VMAFScoreFunc defer | bare defer os.Remove(tmpPath) | defer func that logs the remove failure via slog.Warn |
pkg/encoder/encoder.go | runEncode L179 | _ = os.Remove(outPath) after runErr | errors.Join of ffmpeg-err + remove-err |
pkg/storage/fuse_mount.go | Mount-start failure path | os.Remove(mountDir) error after cmd.Start failure | errors.Join |
pkg/storage/fuse_mount.go | Readiness-timeout path | unmount + killProcess + os.RemoveAll errors after waitForPath timeout | errors.Join over all four; unmount + killProcess updated to return their failures |
pkg/storage/http_serve.go | Readiness-timeout path | killProcess(cmd) error after waitForHTTP timeout | errors.Join of readiness-err + kill-err; killProcess updated to return its failure |
cmd/vmafx-controller/queue/queue.go | New × 3 init failure paths | db.Close() //nolint:errcheck lost the close-time error on WAL-pragma, schema, and reload failures | New closeAndJoin(db, primary) helper that joins the close failure into the primary error |
Findings — slog key drift (2 files, 3 sites)¶
| File | Site | Was | Now |
|---|---|---|---|
cmd/vmafx-node/main.go | L38 fatal | "err" | "error" |
cmd/vmafx-node/main.go | L77 probe-warn | "err" | "error" |
cmd/vmafx-node/server/server.go | L70 listener close | "err" | "error" |
Audit-verified that all other cmd/ and pkg/ slog call sites already used "error". No mixed-key file remains.
Out of scope (deferred to follow-up ADR)¶
These call sites discard errors but at process-exit / shutdown boundaries where there is no caller to receive a joined return value. A graceful- shutdown contract is needed before they can be retrofitted:
pkg/libvmaf/libvmaf.go—tmpOut.Close()immediately after create, baredefer os.Remove(tmpOut.Name()).cmd/vmafx-controller/main.go—defer scorer.Close(),defer jobQueue.Close()(process exit; no caller).cmd/vmafx-server/main.go—defer scorer.Close()(same reason).cmd/vmafx-mcp/impl.go— Python-shell-call error map uses the JSON payload key"error"for output, not aslogattribute; correct as-is.
Reproducer¶
cd /tmp/wt-errors
go test -race -count=1 ./pkg/bisect/... ./pkg/encoder/... \
./pkg/storage/... ./cmd/vmafx-controller/queue/... ./cmd/vmafx-node/...
golangci-lint run --timeout=2m ./pkg/bisect/... ./pkg/encoder/... \
./pkg/storage/... ./cmd/vmafx-controller/queue/...
Expected: all tests pass; no new lint findings in the touched files (pre-existing errcheck / unused findings in test files are unrelated and outside the touched-file gate).
Decision¶
Captured as ADR-0935.