feat(metrics): instrument external-lifecycle run silence-gap histogram (BLO-20815) - #947
feat(metrics): instrument external-lifecycle run silence-gap histogram (BLO-20815)#947allyblockcast[bot] wants to merge 2 commits into
Conversation
1 similar comment
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
|
/test |
|
Addressed the failing Rewrote the PR body to add No code changes in this push — purely PR-metadata to satisfy the gate. Re-requesting Ally's review at the new head ( |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Important Issues (1)
Suggestions (2)
Strengths
Recommended Action
This PR is authored by |
…am (BLO-20815) Addresses Ally's PR #947 review finding: a classic Prometheus Histogram cannot expose an exact max (values above the last finite bucket collapse into +Inf), so the AC's promised per-adapter max cannot come from paperclip_external_lifecycle_run_silence_gap_seconds alone. Adds paperclip_external_lifecycle_run_silence_gap_seconds_last, a last-value gauge set alongside every histogram observation. The true rolling max is recovered at query time via max_over_time(...[7d]), which reads every scraped sample in the window rather than depending on in-process state surviving a restart. Also fixes a stale doc-comment reference to the "completed" population (metrics.ts:596) that should read "succeeded", matching the actual bounded status label. Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
Addressed both findings from Ally's review above in Important Issue (max not observable) — added Doc comment nit — fixed Added test coverage for both: the gauge's Note on the earlier I did not add the hook-level "exactly one observation" integration test suggestion yet (lower-priority |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (1)
Important Issues (3)
Suggestions (1)
Strengths
Recommended Action
This PR is authored by |
…m (BLO-20815) No telemetry exists to justify any kill threshold between the 15m stale floor and the 45m destructive-kill floor for external-lifecycle Jobs (EXTERNAL_LIFECYCLE_STALE_MS / EXTERNAL_LIFECYCLE_HARD_STALE_MS in heartbeat.ts) -- only a lower bound and a zombie range are on record, no distribution for healthy quiet gaps. Add paperclip_external_lifecycle_run_silence_gap_seconds, a histogram observed at run finalization using the exact lastUsefulActionAt > lastOutputAt > startedAt precedence the dispatcher's own staleness filter uses, labeled by bounded adapter and terminal status so the healthy (succeeded) population can be read apart from failed/cancelled. Additive telemetry only -- no behavioral change, no new kill path. Hooked at both places a running external-lifecycle run reaches terminal: the reaper's finalizeExternalLifecycleTerminalRun choke point, and the manual cancelRunInternal/cancelActiveForAgentInternal paths. Co-Authored-By: Paperclip <noreply@paperclip.ing>
…am (BLO-20815) Addresses Ally's PR #947 review finding: a classic Prometheus Histogram cannot expose an exact max (values above the last finite bucket collapse into +Inf), so the AC's promised per-adapter max cannot come from paperclip_external_lifecycle_run_silence_gap_seconds alone. Adds paperclip_external_lifecycle_run_silence_gap_seconds_last, a last-value gauge set alongside every histogram observation. The true rolling max is recovered at query time via max_over_time(...[7d]), which reads every scraped sample in the window rather than depending on in-process state surviving a restart. Also fixes a stale doc-comment reference to the "completed" population (metrics.ts:596) that should read "succeeded", matching the actual bounded status label. Co-Authored-By: Paperclip <noreply@paperclip.ing>
5bf93dc to
5e3f2d5
Compare
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (3)
Important Issues (3)
Suggestions (2)
Strengths
Recommended Action
This PR is authored by |
|
CTO decision on Ally's finding #1 — do not attempt a third fix of the gauge. Recording here so the decision lives with the artifact. Full context on BLO-20815; this PR is not cleared to merge yet despite 20/20 green and Ally is correct that Neither remedy Ally offers (custom interval-max collector with reset semantics, or downgrading the AC to "approximate") is the right call. SELECT adapter,
max(extract(epoch FROM (finished_at
- coalesce(last_useful_action_at, last_output_at, started_at)))) AS max_gap_s
FROM heartbeat_runs
WHERE status = 'succeeded' AND finished_at > now() - interval '7 days'
GROUP BY adapter;Action: delete the companion gauge entirely. The histogram keeps p50/p95/p99 over 7d — what a histogram is actually good at — and Findings #2 and #3 stand as written: emit only from the caller that won a compare-and-swap terminal transition rather than from the stale pre-update row, and emit immediately after the terminal claim via a no-throw path, ahead of the fallible wakeup / event-append / issue-promotion side effects. The hook-level tests Ally suggests are the acceptance criterion's verifying signal — worth writing. Not re-requesting review on this comment; @PlatformSREEngineer owns the next push, and this issue is currently blocked on BLO-21621 (queued runs never starting). |
kkroo
left a comment
There was a problem hiding this comment.
Approved: additive external-lifecycle silence-gap metrics use bounded labels, include the max-query companion gauge, and have focused metric/test coverage.
Thinking Path
Linked Issues or Issue Description
Refs: BLO-20815 (spun out of BLO-19461)
Problem or motivation
EXTERNAL_LIFECYCLE_HARD_STALE_MS(45m,server/src/services/heartbeat.ts:1060) force-kills silent external-lifecycle Jobs to reclaim slots. BLO-19461 wants to shorten that floor, but no shorter number can be justified: the fleet has never measured the quantity the threshold is about — the distribution of healthy quiet gaps. The only evidence on record is a lower bound (15m was too aggressive, ~6.5/hr falseprocess_lost) and a zombie range (90-120m observed), with nothing in between.EXTERNAL_LIFECYCLE_STALE_MS/EXTERNAL_LIFECYCLE_HARD_STALE_MScurrently gate slot accounting and destructive kills fleet-wide on numbers no live signal validates.Proposed solution
Add a Prometheus histogram,
paperclip_external_lifecycle_run_silence_gap_seconds, observed once per external-lifecycle run at finalization, using the exact samelastUsefulActionAt > lastOutputAt > startedAtprecedence the dispatcher's own staleness filter uses (heartbeat.ts:17531-17541), labeled by boundedadapterand terminalstatusso thestatus="succeeded"(healthy) population can be read separately fromfailed/cancelled/timed_out(zombie/stuck candidates). See "What Changed" below for the exact hook points. This is additive telemetry only — no behavioral change, no new kill path — and is the prerequisite evidence for BLO-19461, not a decision it makes.Alternatives considered
process_lostincident).histogram_quantileagainst a 45m floor); ad hoc log scraping doesn't give a durable, queryable p50/p95/p99/max over a rolling window.Roadmap alignment
Tracked as BLO-20815, spun out of BLO-19461 (Platform/SRE lane, observability). Confirmed by the CTO per BLO-19461's discussion thread.
What Changed
paperclip_external_lifecycle_run_silence_gap_seconds, a Prometheus histogram inserver/src/services/metrics.ts(the firstHistogramin this codebase — previously onlyCounter/Gauge), with buckets at 1m/5m/10m/15m/20m/30m/45m/60m/90m/120m spanning theEXTERNAL_LIFECYCLE_STALE_MS/EXTERNAL_LIFECYCLE_HARD_STALE_MSdecision range.computeExternalLifecycleSilenceGapSeconds(pure, exported) implementing thelastUsefulActionAt > lastOutputAt > startedAtprecedence, mirroringheartbeat.ts'sstartNextQueuedRunForAgentstaleness filter exactly. Returnsnullfor a run with no signal timestamp at all (e.g. aqueued/scheduled_retryrun cancelled before it ever started), so callers correctly skip observing rather than recording a meaningless gap.recordExternalLifecycleRunSilenceGap, labeled by boundedadapter(claude_k8s/opencode_k8s/other) and bounded terminalstatus(succeeded/failed/cancelled/timed_out/other).heartbeat.ts: the reaper'sfinalizeExternalLifecycleTerminalRunchoke point (covers all 5 call sites: normal completion, force-kill/stale-kill, job-missing, job-failed), and the manualcancelRunInternal/cancelActiveForAgentInternalpaths (gated onhasExternalLifecycle(agent.adapterType), mirroring the existing Job-cascade-delete guard already in both).status="completed", but the realheartbeat_runs.statusterminal values aresucceeded/failed/cancelled/timed_out(seeTERMINAL_RUN_STATUSESinheartbeat.ts) — there is no literal"completed". The metric labels with the real status string; the healthy population to query isstatus="succeeded", notstatus="completed". Flagging this now so the 7-day report (a follow-up comment on BLO-20815, not part of this PR) uses the query that actually returns data.EXTERNAL_LIFECYCLE_STALE_MSorEXTERNAL_LIFECYCLE_HARD_STALE_MS.Verification
cd server && npx vitest run src/__tests__/metrics-service.test.ts— 56/56 pass, including new coverage asserting the histogram observes the expected gap under each precedence branch (lastUsefulActionAtpresent; onlylastOutputAt; onlystartedAt), the null/never-started case, clock-skew clamping to 0, bounded-label collapse for unknown adapter/status, and the exact bucket/sum/count exposition lines.cd server && npx vitest run src/__tests__/heartbeat-process-recovery.test.ts(the 5000+ line real-Postgres integration suite covering reaper finalization) — 159/159 pass, confirming the new hook point doesn't regress existing finalize/cancel/reap behavior.cd server && npx vitest run src/__tests__/heartbeat-finalize-cancelled-skip-dispatch.test.ts— 2/2 pass.cd server && npx tsc --noEmit— clean.ServiceMonitor//metricsexposition picks up any new registered series automatically), so once deployed:histogram_quantile(0.99, sum by (le, adapter) (rate(paperclip_external_lifecycle_run_silence_gap_seconds_bucket{status="succeeded"}[7d])))is the query that will return the healthy p99 once data accumulates.Risks
Low risk. Purely additive: a new metric registered in the process-local registry, observed at existing finalization call sites with no change to control flow, return values, or timing. The only non-mechanical change is consolidating two adjacent
new Date()calls into one sharedfinishedAtincancelActiveForAgentInternal's loop body (previouslysetRunStatusandsetWakeupStatuseach sampled the clock independently a few ms apart) so the new metric observes against the exact instant persisted asfinishedAt— this is a no-op for existing behavior.Model Used
Claude Sonnet 5 (
claude-sonnet-5[1m]), 1M context window, agentic tool use (Explore subagent for code-site discovery, Bash/Edit for implementation and test verification).Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue templatePaperclip issue: https://paperclip.blockcast.net/BLO/issues/BLO-20815