Files
meshcore-analyzer/cmd/server/perf_io_freshness_test.go
T
Kpa-clawbot f4cf2acbc0 perf: cancelled writes + ingestor I/O + threshold tests (#1120 follow-up) (#1167)
Red commit: e964ec9c46 (CI run: pending —
workflow only triggers on PR open)

Partial fix for #1120 — finishes the four follow-up items left open
after PR #1123 (cancelled writes, ingestor I/O, threshold-flag tests,
docs).

## What's done

- **`cancelledWriteBytesPerSec`** — server `/proc/self/io` parser
handles `cancelled_write_bytes`; `/api/perf/io` exposes the per-second
rate; Perf page renders it next to Read/Write with ⚠️ when sustained >1
MB/s.
- **Ingestor `/proc/<pid>/io`** — `cmd/ingestor/stats_file.go` samples
its own `/proc/self/io` each tick and includes `procIO` in the snapshot.
The server's `/api/perf/io` reads it and surfaces `.ingestor`. Frontend
renders an `Ingestor process` Disk I/O block alongside the existing
`server process` block (issue mockup: "Both ingestor and server").
- **Threshold + anomaly tests** — `test-perf-disk-io-1120.js` now
asserts ⚠️ fires/suppresses on WAL>100MB, cache_hit<90%, and the
backfill-rate-vs-tx-rate guard with the `tx_inserted >= 100` baseline
floor. Drops the tautological `|| ... === false` short-circuits flagged
in MINOR m4.
- **Docs (m8)** — `config.example.json` adds `_comment_ingestorStats`
(env var, default path, shared-tmp security note);
`cmd/ingestor/README.md` adds `CORESCOPE_INGESTOR_STATS` to the env-var
table plus a `Stats file` section.

## What's NOT done (deferred)

m1 sync.Map → map+RWMutex, m2 perfIOMu rate caching, m3 negative
cacheSize translation, m5 deterministic-write test, m7 ctx-aware
shutdown — pure polish; will file a follow-up issue if the operator
wants them tracked.

## TDD

- Red: `e964ec9` — adds failing tests + stub field/handler shape
(cancelled missing from struct, ingestor stub returns nil, ingestor
procIO absent).
- Green: `1240703` — wires up the parser case, ingestor sampler,
frontend rendering, docs.

E2E assertion added: test-perf-disk-io-1120.js:108

---------

Co-authored-by: clawbot <clawbot@users.noreply.github.com>
Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local>
Co-authored-by: Kpa-clawbot <bot@kpa-clawbot>
2026-05-08 16:29:23 -07:00

126 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
)
// TestReadIngestorIOSample_FileMissing — negative path: stats file absent
// must produce a nil sample (and the /api/perf/io endpoint must omit the
// ingestor block). Issue #1167 must-fix #4.
func TestReadIngestorIOSample_FileMissing(t *testing.T) {
t.Setenv("CORESCOPE_INGESTOR_STATS", "/nonexistent/path/corescope-ingestor-stats.json")
if got := readIngestorIOSample(); got != nil {
t.Fatalf("expected nil for missing file, got %+v", got)
}
_, router := setupTestServer(t)
req := httptest.NewRequest("GET", "/api/perf/io", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
var body map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if _, ok := body["ingestor"]; ok {
t.Errorf("expected NO ingestor block when stats file missing, got: %v", body["ingestor"])
}
}
// TestReadIngestorIOSample_Unparseable — negative path: malformed JSON must
// produce nil. Issue #1167 must-fix #4.
func TestReadIngestorIOSample_Unparseable(t *testing.T) {
dir := t.TempDir()
statsPath := filepath.Join(dir, "ingestor-stats.json")
if err := os.WriteFile(statsPath, []byte("{not json"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("CORESCOPE_INGESTOR_STATS", statsPath)
if got := readIngestorIOSample(); got != nil {
t.Fatalf("expected nil for unparseable JSON, got %+v", got)
}
}
// TestReadIngestorIOSample_StaleBeyondThreshold — freshness guard: a snapshot
// whose sampledAt is older than the staleness threshold (5×default writer
// interval = 5s; we use 5 minutes here for clear margin) MUST be dropped, not
// served as live ingestor I/O. Issue #1167 must-fix #1.
func TestReadIngestorIOSample_StaleBeyondThreshold(t *testing.T) {
dir := t.TempDir()
statsPath := filepath.Join(dir, "ingestor-stats.json")
staleAt := time.Now().UTC().Add(-5 * time.Minute).Format(time.RFC3339)
stub := `{
"sampledAt": "` + staleAt + `",
"tx_inserted": 0,
"backfillUpdates": {},
"procIO": {
"readBytesPerSec": 100,
"writeBytesPerSec": 200,
"cancelledWriteBytesPerSec": 0,
"syscallsRead": 5,
"syscallsWrite": 6,
"sampledAt": "` + staleAt + `"
}
}`
if err := os.WriteFile(statsPath, []byte(stub), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("CORESCOPE_INGESTOR_STATS", statsPath)
if got := readIngestorIOSample(); got != nil {
t.Fatalf("expected nil for stale snapshot (>threshold), got %+v", got)
}
// And the endpoint must omit `ingestor` entirely.
_, router := setupTestServer(t)
req := httptest.NewRequest("GET", "/api/perf/io", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
var body map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if _, ok := body["ingestor"]; ok {
t.Errorf("stale ingestor must be dropped, got: %v", body["ingestor"])
}
}
// TestReadIngestorIOSample_FreshIsServed — positive path: a snapshot with
// sampledAt <threshold old MUST still be served. Companion to the freshness
// guard test above. Issue #1167 must-fix #1.
func TestReadIngestorIOSample_FreshIsServed(t *testing.T) {
dir := t.TempDir()
statsPath := filepath.Join(dir, "ingestor-stats.json")
freshAt := time.Now().UTC().Format(time.RFC3339)
stub := `{
"sampledAt": "` + freshAt + `",
"tx_inserted": 0,
"backfillUpdates": {},
"procIO": {
"readBytesPerSec": 100,
"writeBytesPerSec": 200,
"cancelledWriteBytesPerSec": 0,
"syscallsRead": 5,
"syscallsWrite": 6,
"sampledAt": "` + freshAt + `"
}
}`
if err := os.WriteFile(statsPath, []byte(stub), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("CORESCOPE_INGESTOR_STATS", statsPath)
got := readIngestorIOSample()
if got == nil {
t.Fatalf("expected non-nil for fresh snapshot, got nil")
}
if got.WriteBytesPerSec != 200 {
t.Errorf("expected writeBytesPerSec=200, got %v", got.WriteBytesPerSec)
}
}