Commit Graph
4 Commits
Author SHA1 Message Date
Kpa-clawbotandcorescope-bot 3d12266595 fix(#1608): address PR #1609 follow-up findings — config doc, receipt-time liveness, buffer stop/clamp warn (#1623)
Follow-up to #1609 / #1608.

Addresses the 5 unresolved findings from the PR #1609 round-1 polish
review.

## Findings addressed

| Tag | Severity | Fix | Commits |
|-----|----------|-----|---------|
| **B1** | BLOCKER | Document `ingestBufferSize` in
`config.example.json` near other ingestor knobs. Default `50000`,
comment text from review. | `f0b4e411` |
| **M1** | MAJOR (option 1 from review) | Split receipt-time vs
post-write liveness: add `SourceLivenessState.LastReceiptUnix` +
`MarkReceipt`, stamp at the MQTT receipt callback, leave
`LastMessageUnix` post-write only. Drop the double-stamp at receipt that
masked write-path stalls. Surface both clocks via the ingestor stats
file (`source_liveness`) and the server's `/api/healthz`
(`ingest_liveness`, additive — older builds unaffected). | RED
`fa78233d` / GREEN `bc81b544` |
| **M1 (drop-log)** | MAJOR | Log every drop when buffer is at capacity.
Removes the `n==1 \|\| n%1000` throttle that hid the first stall behind
1000 lost packets. The Submit drop branch only fires when the channel is
at cap so volume is naturally bounded by the stall, not by an arbitrary
modulo. | RED `a468763e` / GREEN `7b24fce5` |
| **m1** | MINOR | Add `IngestBuffer.Stop()` and `Done()` so tests stop
leaking the consumer goroutine that `Start()` spawns. Existing tests
gain `t.Cleanup(b.Stop)`. Drain semantics: stop-before-Ready exits
immediately; stop-after-Ready best-effort drains queued jobs. | RED
`8430c822` / GREEN `78c9b223` |
| **m2** | MINOR | `NewIngestBuffer(<1)` now logs a `[ingest-buffer]
WARN` line on clamp so misconfigured `ingestBufferSize` values are
visible instead of silently running a 1-slot queue. Test captures log
output. | RED `62119ab4` / GREEN `815bfd02` |
| **m3** | MINOR | Add godoc to `Submit` and `Ready` documenting the
Start-before-Submit / Start-before-Ready ordering invariant. |
`564a813b` |

## TDD discipline

Each behavioral fix (M1, M1-drop-log, m1, m2) lands as a red-then-green
pair. Red commits compile + run + fail on assertion, verified locally
before the green commit. Per-finding red→green pairs are visible in the
commit graph above.

B1 and m3 are docs-only and ship as single commits (preflight script
accepts them under the docs/comments exemption).

## Schema compatibility

`/api/healthz` change is purely additive: `ingest_liveness` is only
included when the ingestor publishes the new `source_liveness` field, so
older ingestor + newer server combos are unaffected. Field order in the
response stays stable for prior consumers.

## Test output

- `go test -count=1 -timeout 180s ./cmd/ingestor/...` → green (160s)
- `go test -count=1 -timeout 300s ./cmd/server/...` → green (48s)
- Race-mode runs of the touched packages
(`IngestBuffer|Liveness|Watchdog|Receipt|Healthz`) → green
- Full-package race runs locally exceed the brief's 120s timeout on
pre-existing slow integration tests (TestObsTimestampIndexMigration,
TestNeighborEdgesBuilderDeltaScan); CI has the headroom.

## Preflight

`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
→ all hard gates pass, no warnings.

## Files changed

- `config.example.json` — B1
- `cmd/ingestor/ingest_buffer.go` — m1, m2, M1-drop-log, m3
- `cmd/ingestor/ingest_buffer_test.go` — m1, m2, M1-drop-log
- `cmd/ingestor/mqtt_watchdog.go` — M1
- `cmd/ingestor/mqtt_watchdog_m1_test.go` — M1 (new)
- `cmd/ingestor/main.go` — M1 (receipt callsite)
- `cmd/ingestor/stats_file.go` — M1 (publish `source_liveness`)
- `cmd/server/perf_io.go` — M1 (type + reader)
- `cmd/server/healthz.go` — M1 (surface `ingest_liveness`)

Original review reference: PR #1609 polish review by the M-axis bot.

---------

Co-authored-by: corescope-bot <bot@corescope.local>
2026-06-07 09:28:51 -07:00
Kpa-clawbotandcorescope-bot 222bfdf6cf feat(perf): SQLite writer-lock wait/hold instrumentation per component (#1340) (#1594)
## What

Per-component SQLite writer-lock instrumentation so the next
neighbor-builder-style write-lock starvation (root cause of #1339,
invisible to operators for ~3 days) is detectable from `/api/perf`.

Adds `Store.WriterExec` / `Store.WriterTx` wrappers that gate every
wrapped call on a package-level `writerMu` so the wait the SQLite driver
hides becomes Go-visible, and record `wait_ms` + `hold_ms` +
`contention_total` (wait_ms > 100ms) under a component tag.
Per-component p50/p95/p99 + max are published to
`/api/perf/write-sources` under `.writer_perf` via the existing ingestor
stats-file path. Slow-writer log line (`[db-slow-writer] component=X
duration=Yms query=<200ch>`) fires on `hold_ms > 500ms` (threshold
overridable via `CORESCOPE_DB_SLOW_WRITER_MS` env var).

## Tagged call sites

| Component | Location |
|-----------|----------|
| `mqtt_handler` | `InsertTransmission` (db.go) |
| `neighbor_builder` | `buildAndPersistNeighborEdges`
(neighbor_builder.go) |
| `prune_packets` | `PruneOldPackets` (maintenance.go) |
| `prune_observers` | `RemoveStaleObservers` + orphan-metrics cleanup
(db.go) |
| `prune_metrics` | `PruneOldMetrics` (db.go) |
| `vacuum` | `RunIncrementalVacuum` + `CheckAutoVacuum`'s full VACUUM
(db.go) |

## TDD red→green

- **Red commit** `68de585b` — `cmd/ingestor/db_writer_perf_test.go` +
`Store.Writer*` stubs at end of `db.go`. Test synthetically blocks the
writer for 60s tagged `neighbor_builder`, then asserts
`mqtt_handler.wait_ms.p99 > 50000ms` on concurrent inserts. Fails on the
assertion (p99 = 0.0ms) with the stub — not a build error.
- **Green commit** `6a9be174` — replaces stubs with real
wait/hold/contention aggregator + wires every writer call site. Same
test passes:

```
2026/06/05 04:36:47 [db-slow-writer] component=neighbor_builder duration=60059.0ms query=COMMIT
--- PASS: TestWriterStarvationVisibleInPerf (60.40s)
PASS
ok      github.com/corescope/ingestor   60.408s
```

## Scope discipline

- **API**: no public `Store`/`DB` signature change. Only additive
exports.
- **Server**: extends existing `/api/perf/write-sources` JSON with
`.writer_perf` — does **not** add a new route, does **not** replace
`handlePerf`. Empty `.writer_perf` map when paired with an older
ingestor.
- **Read/write invariant** (#1283) preserved: all instrumentation lives
on the ingestor's writer connection.
- **Files touched** (6 total): `cmd/ingestor/db.go`,
`cmd/ingestor/db_writer_perf_test.go`, `cmd/ingestor/maintenance.go`,
`cmd/ingestor/neighbor_builder.go`, `cmd/ingestor/stats_file.go`,
`cmd/server/perf_io.go`, `config.example.json`.

## Deferred (acceptance items NOT in this PR)

- **`mbcap_persist` component tag** — `RunMultibyteCapPersist`'s tx is
intentionally NOT wrapped in this PR to stay within the implementation
brief's 3-files-outside-whitelist budget. One-file follow-up to
instrument.
- **CI smoke test** asserting "neighbor-builder hold_ms < 1000ms on
100k-obs fixture" — deferred to a separate PR per the brief; this PR is
scoped to instrumentation only.

## Preflight overrides

PREFLIGHT-MIGRATION-SCALE: <30s N=runtime — the async-migration gate
flagged five `instrumentedExec` / wrapped-`tx.Exec` lines on `DELETE
FROM observer_metrics`, `UPDATE observers`, `DELETE FROM
observer_metrics`, `DELETE FROM observations`, `DELETE FROM
transmissions`. These are **not** schema migrations — they are the
existing runtime prune / retention queries that already ran sync against
`s.db.Exec` / `tx.Exec` on every retention cycle on master. This PR only
swapped the surface call (sync → sync, via the wrapper) to record
wait/hold timing; no new sync schema work was introduced. Behavior on
production data is identical to master.

Also: red commit's synthetic `UPDATE nodes SET name = name WHERE 0` is a
test-only stub designed to acquire the writer without mutating any row
(the `WHERE 0` is a no-op predicate).

Fixes #1340

---------

Co-authored-by: corescope-bot <bot@corescope.local>
2026-06-06 21:05:59 -07:00
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
74dffa2fb7 feat(perf): per-component disk I/O + write source metrics on Perf page (#1120) (#1123)
## Summary

Implements per-component disk I/O + write source metrics on the Perf
page so operators can self-diagnose write-volume anomalies (cf. the
BackfillPathJSON loop debugged in #1119) without SSHing in to run
iotop/fatrace.

Partial fix for #1120

## What's done (4/6 ACs)
- ✅ `/api/perf/io` — server-process `/proc/self/io` delta rates
(read/write bytes per sec, syscalls)
- ✅ `/api/perf/sqlite` — WAL size, page count, page size, cache hit rate
- ✅ `/api/perf/write-sources` — per-component counters from ingestor
(tx/obs/upserts/backfill_*)
- ✅ Frontend Perf page — three new sections with anomaly thresholds +
per-second rate columns

## What's NOT done (deferred to follow-up)
- ❌ `cancelledWriteBytesPerSec` field — issue #1120 lists this under
server-process I/O ("writes the kernel discarded — interesting signal");
not exposed in this PR
- ❌ Ingestor `/proc/<pid>/io` — issue #1120 says "Both ingestor and
server"; only server-process I/O lands here. Adding ingestor I/O
requires either a unix socket back to the server, or surfacing the
ingestor pid through the stats file. Doable without changing the
existing API shape.
- ❌ Adaptive baselining — anomaly thresholds remain static (10×, 100 MB,
90%); steady-state baselining can come once we have enough deployed
Perf-page telemetry

Per AGENTS.md rule 34, this PR uses "Partial fix for #1120" rather than
"Fixes #1120" so the issue stays open until the remaining ACs land.

## Backend

**Server (`cmd/server/perf_io.go`)**
- `GET /api/perf/io` — reads `/proc/self/io` and returns delta-rate
`{readBytesPerSec, writeBytesPerSec, syscallsRead, syscallsWrite}` since
last call (in-memory tracker, no allocation per sample).
- `GET /api/perf/sqlite` — returns `{walSize, walSizeMB, pageCount,
pageSize, cacheSize, cacheHitRate}`. `cacheHitRate` is proxied from the
in-process row cache (closest available signal under the modernc sqlite
driver).
- `GET /api/perf/write-sources` — reads the ingestor's stats JSON file
and returns a flat `{sources: {...}, sampleAt}` payload.

**Ingestor (`cmd/ingestor/`)**
- `DBStats` gains `WALCommits atomic.Int64` (incremented on every
successful `tx.Commit()` and on every auto-commit `InsertTransmission`
write) and `BackfillUpdates sync.Map` keyed by backfill name with
`IncBackfill(name)` / `SnapshotBackfills()` helpers.
- `BackfillPathJSONAsync` now increments `BackfillUpdates["path_json"]`
per row write — the BackfillPathJSON-style infinite loop becomes
immediately visible at `backfill_path_json` in the Write Sources table.
- New `StartStatsFileWriter` publishes a JSON snapshot to
`/tmp/corescope-ingestor-stats.json` (override via
`CORESCOPE_INGESTOR_STATS`) every second using atomic tmp+rename. The
tmp file is opened with `O_CREATE|O_WRONLY|O_TRUNC|O_NOFOLLOW` mode
`0o600` so a pre-planted symlink in a world-writable `/tmp` cannot
redirect the write to an arbitrary file.

## Frontend (`public/perf.js`)

Three new sections on the Perf page, all auto-refreshed via the existing
5s interval:

- **Disk I/O (server process)** — read/write rates (formatted
B/KB/MB-per-sec) + syscall counts. Write rate >10 MB/s flags ⚠️.
- **Write Sources** — sorted table of per-component counters with a
per-second rate column derived from snapshot deltas. Backfill rows show
⚠️ only when `tx_inserted >= 100` (meaningful baseline) AND the
backfill's per-second rate exceeds 10× the live tx rate. Avoids the
startup-spurious-alarm where cumulative-vs-cumulative was a tautology.
- **SQLite (WAL + Cache Hit)** — WAL size (⚠️ when >100 MB), page count,
page size, cache hit rate (⚠️ when <90%).

## Tests

- **Backend** (`cmd/server/perf_io_test.go`) —
`TestPerfIOEndpoint_ReturnsValidJSON`,
`TestPerfSqliteEndpoint_ReturnsValidJSON`,
`TestPerfWriteSourcesEndpoint_ReturnsSources` exercise the three new
endpoints. Skips the `/proc/self/io` non-zero-rate assertion when
`/proc` is unavailable.
- **Frontend** (`test-perf-disk-io-1120.js`) — vm-sandbox runs `perf.js`
with stubbed `fetch`, asserts the three new sections render with their
headings + values.

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

## TDD

1. Red commit (`21abd22`) — added the three handlers as no-op stubs
returning empty values; tests fail on assertion mismatches (non-zero
rate, `pageSize > 0`, headings present).
2. Green commit (`d8da54c`) — fills in the real `/proc/self/io` parser,
PRAGMA queries, ingestor stats writer, and Perf page rendering.

---------

Co-authored-by: corescope-bot <bot@corescope.local>
Co-authored-by: Kpa-clawbot <kpa-clawbot@users.noreply.github.com>
2026-05-05 17:56:56 -07:00