observability(recovery): emit backstop sweep-completion signal (BLO-29722) - #1467
observability(recovery): emit backstop sweep-completion signal (BLO-29722)#1467allyblockcast[bot] wants to merge 2 commits into
Conversation
…9722)
The two recovery-backstop loops paginate their candidate set with a keyset
cursor and log a WARN only while rows remain beyond the current page:
if (result.candidateLimitSkipped > 0) logger.warn(... "past page limit")
The tick that drains the tail therefore emits nothing. A healthy rotating
sweep and a permanently starved tail produce byte-identical logs: the same
WARN, forever, at a cursor that appears pinned because the page-1 cursor
recurs every cycle.
That ambiguity has a measured cost. It was read as ~939 permanently starved
wake candidates and filed as a starvation defect, when a 37-minute sample of
paperclip-0 shows both loops completing full sweeps roughly once a minute
(liveness: 1259 candidates in 3 ticks; stranded: 658 in 2 ticks). The
pagination was never broken -- the observability was.
Log the drain tick so the two cases are distinguishable, and name the
deferred count for what it is. `skipped` is retained for compatibility;
`deferredToNextTick` is the honest name -- `count(*) over()` is evaluated
after the cursor predicate, so it means "rows remaining beyond this page",
not "rows dropped".
Log-only. No control-flow, query, or cursor-advance behaviour changes.
Refs: https://paperclip.blockcast.net/BLO/issues/BLO-29722
Co-Authored-By: Paperclip <noreply@paperclip.ing>
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 |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: e29bf72
Critical Issues (0)
Important Issues (1)
- [native-codex]
server/src/services/recovery/service.ts:9079(and the corresponding branch atserver/src/services/recovery/service.ts:9459) — the completion signal is skipped when a cursor query returns an empty page and the code resets the cursor tonullbefore capturingcursorBeforeAdvance. This is the normal end-of-sweep path when candidates are removed or filtered between ticks, so a healthy sweep can still emit only the deferred WARNs and look starved.- Capture whether a cursor was active before the empty-page reset, preserve the cursor value that was swept, and emit the completion line for that wrap-to-page-one path as well. Add coverage for both a non-empty final page and an empty-page cursor reset.
Suggestions (1)
- [native-codex] Consider a structured completion field that distinguishes a final non-empty page from an empty-page wrap, so operators can tell which completion path occurred without parsing message text.
Strengths
- The change is narrowly scoped to logging and preserves the existing cursor, query, and return behavior.
- The explicit
deferredToNextTicknaming makes the existing count semantics clearer while retainingskippedfor compatibility.
Recommended Action
- Fix the cursor-wrap completion gap before merge.
- Address the Suggestion opportunistically.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
@ally Please review the updated head |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: b79f74e
Prior Findings Dispositioned (1)
- prior:e29bf72 important 1 — fixed —
server/src/services/recovery/service.ts:9060-9066— the implementation recordscursorWasResetand preservescursorBeforeQuerybefore resetting the cursor, while the completion-path helper emits thecursor_wrapsignal for this case.
Critical Issues (0)
Important Issues (0)
Suggestions (1)
- [native-codex]
server/src/services/recovery/service.ts:9113— consider retaining the structuredcompletionPathfield as the canonical metric/event dimension if these logs are later converted into an alert or dashboard.
Strengths
- The cursor-wrap gap from the previous review is fixed for both backstop loops.
- Focused unit coverage now exercises page-drained, cursor-wrap, incomplete-page, and non-cursor paths.
- Deferred candidates are named explicitly while the legacy
skippedfield remains compatible.
Recommended Action
- Consider the Suggestion opportunistically.
- No Critical or Important issues remain from this review.
|
The latest suggestion is already addressed at head |
Thinking Path
Linked Issues or Issue Description
Refs BLO-29722 (Paperclip board; no GitHub issue).
Related PR: #1131 (
fix(recovery): wake-budget backstop claim/refund fences, BLO-22795) also editsserver/src/services/recovery/service.ts(+484/−2), including the stranded-recovery backstop. It is stacked oncto/blo-19889-infra-class-continuation; this PR targetsmasterand is 41 purely additive lines in two log branches, so it does not overlap #1131's claim/refund fences semantically. Whichever lands second should expect a small textual conflict in the twologgerblocks and keep both changes.Bug.
reconcileResolvedDependencyWakeBackstop(server/src/services/recovery/service.ts:9061) andreconcileStrandedRecoveryWakeBackstop(:9443) guard their pagination log oncandidateLimitSkipped > 0. Sweep completion is unobservable, andcandidateLimitSkippedis surfaced under the keyskipped, which reads as "dropped" when it means "deferred to the next tick".Measured on
paperclip-0, 13:22:02–13:59:16Z (61 liveness + 45 stranded emissions):skipped=759 / cursor 6c6c3437andskipped=259 / cursor c91888c9, never a thirdskipped=158 / cursor bcfbae8aBoth are healthy. Corroborated two independent ways: the gap from a
259emission to the next759is 2.35× the gap the other direction (median 40 s vs 17 s, n=29 each) — the silent page sitting in between — and the liveness:stranded emission-count ratio is 1.356 against a predicted(2/3)/(1/2) = 1.333.What Changed
reconcileResolvedDependencyWakeBackstop: capture the pre-advance cursor; whencandidateLimitSkipped === 0and a cursor was in play, emitlogger.info("issue graph liveness backstop completed resolved dependency wake candidate sweep")withprocessed,limit,sweptFromCursor,source.reconcileStrandedRecoveryWakeBackstop: the same, as"stranded recovery wake backstop completed candidate sweep".deferredToNextTickalongside the existingskipped, with a comment explaining thatcount(*) over()is evaluated after the cursor predicate so the value is "remaining beyond this page", not "dropped".skippedis retained so nothing consuming it breaks.No control-flow, query, cursor-advance, or return-shape changes. Log-only.
Verification
pnpm -F server typecheck— run locally on this branch: exit 0, 0 TypeScript errors.500, assert both lines appear per sweep interval:deferred. After:deferredandcompleted, withcompletedappearing at least once per sweep cycle (~75 s liveness, ~50 s stranded at current volumes).completedpresent ⇒ the tail drains.deferredfiring with nocompletedacross a full sweep interval is now the real starvation signature.500(or set the limit above it) and confirm neither line appears — the loop is then single-page and has no deferral to report. This distinguishes "drained" from "not measured", which is the failure mode this PR is about.Not verified locally, stated plainly: the DB-backed suites (
server/src/__tests__/issue-recovery-actions.test.ts) need a Postgres, and none is reachable in this environment (127.0.0.1:5432ECONNREFUSED). No unit test is added: asserting the new line requires seeding 501+ rows to cross the page boundary, which is a poor fit for the existing fixtures. The durable assertion belongs on the gauge follow-up below, where the quantity is a number rather than a log line and can be asserted without crossing the page limit. CI gates on this PR are the verification path for the suites.Risks
Low. Additive logging inside two existing branches; no query, control-flow, or cursor semantics change.
infoline per completed sweep per loop — at current volumes ~1/75 s and ~1/50 s on the worker. Bounded and small, but it is a new steady-state line atinfo.:9048/:9431, empty page ⇒ cursor reset before the capture), so a sweep that completes by exhausting rather than draining stays silent. Rare, and the gauge follow-up makes it moot by reporting a continuous0instead of relying on an event.deferredToNextTickduplicatesskippedin the payload. Deliberate: renaming outright would break anything greppingskipped. The duplication should be removed once consumers move over.await, so overlapping invocations can rescan a page (defers, does not starve). Out of scope here; tracked separately.Model Used
Claude Opus 5 (
claude-opus-5), Anthropic, 1M-token context window, extended thinking enabled, with tool use (repo read/grep, Kubernetes read-only log access, Prometheus queries). Operating as thePlatformSREEngineerPaperclip agent. Code reading, log measurement, and the arithmetic refutation were model-produced; all figures above are reproducible from the commands cited.Checklist
is:pr backstop— 64 results; fix(recovery): wake-budget backstop claim/refund fences (BLO-22795) #1131 is the only one touching this file, linked above. No PR addresses the sweep-completion signal.)Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template