fix(selfhost): prevent maintenance queue self-pinning#2782
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | 66413b7 | Commit Preview URL Branch Preview URL |
Jul 03 2026, 09:52 PM |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2782 +/- ##
=======================================
Coverage 96.22% 96.22%
=======================================
Files 255 255
Lines 27944 27998 +54
Branches 10156 10174 +18
=======================================
+ Hits 26889 26941 +52
Misses 433 433
- Partials 622 624 +2
🚀 New features to boost your workflow:
|
|
Caution 🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥 🛑 Gittensory review result - fixes requiredReview updated: 2026-07-03 21:07:01 UTC
🛑 Suggested Action - Manual Review
Review summary Blockers
Nits — 5 non-blocking
Why this is blocked
CI checks failing
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
00089fd to
7e7cbb9
Compare
The maintenance-admission policy denied every claim with maintenance_pending_high once the lane backed up past its threshold, with no way for the backlog to shrink short of each job individually reaching the full 4h trickle ceiling -- deferred because the lane was over threshold, stuck over threshold because everything stayed deferred. Add a second, shorter age escape (MAINTENANCE_ADMISSION_DRAIN_AGE_MS) scoped only to that reason so the oldest jobs drain in a bounded trickle well before the outer backstop, while live-priority, host-load, and GitHub rate-limit admission are untouched. Also coalesce merge-triggered rag-index-repo incrementals for the same repo (bounded to 100 paths) and batch a webhook delivery's notification events into one notify-evaluate job instead of one job per event, both of which were inflating the maintenance lane unnecessarily.
7e7cbb9 to
66413b7
Compare
Summary
maintenance_pending_highis a lane-wide aggregate-count check with no feedback loop back to that count as jobs individually age out via the existing 4h trickle. Once the lane backed up past its threshold (e.g. the reported 68-pending incident: 30rag-index-repo+ 29notify-evaluate+ 9 other maintenance jobs, 0 live jobs pending, no rate-limit activity), every claim was deniedmaintenance_pending_highuntil each job individually reached the fullMAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS(4h) — deferred because the lane was over threshold, stuck over threshold because everything stayed deferred.MAINTENANCE_ADMISSION_DRAIN_AGE_MS, default 10m, clamped to never exceed the 4h backstop) scoped only to themaintenance_pending_highreason, so the oldest jobs in a backed-up lane drain in a bounded trickle (further throttled by the queue's existingQUEUE_BACKGROUND_CONCURRENCYcap) well before the outer 4h ceiling.live_pending_high,live_job_age_high, andhost_load_highare untouched hard blocks — live-review priority and host-load safety are preserved, and the drain escape itself is still blocked if host load is also high.rag-index-repoincrementals for the same repo now union into one pending row (bounded to 100 total paths) instead of piling up as separate rows, and a webhook delivery's notification events (review events + issue-watch matches) now batch into a singlenotify-evaluatejob instead of one job per event/watcher._by_reason_totaldeferral counter, and the Grafana "Runtime Pressure & Maintenance" panels described in the bug report already exist (shipped in feat(selfhost): defer maintenance work under runtime pressure #2717) and are correctly wired — so this PR's observability work adds the complementary piece: agittensory_jobs_maintenance_admission_granted_under_pressure_total{reason,job_type}counter (mirrors the existing denied-by-reason counter, but for admissions that happened despite pressure) plus a new dashboard panel and an updated alert runbook, so an operator can see the new drain escape actually firing rather than just inferring it from queue depth alone.What changed
src/selfhost/maintenance-admission.ts— newmaintenanceDrainAgeMsconfig field (envMAINTENANCE_ADMISSION_DRAIN_AGE_MS, clamped tomaxDeferAgeMs), newmaintenance_pending_high_drainreason, andisMaintenanceAdmissionGrantedUnderPressure()helper.src/selfhost/sqlite-queue.ts/src/selfhost/pg-queue.ts— record the new granted-under-pressure metric; add the rag-index-repo incremental-merge coalescing branch toenqueue()(mirrored across both backends).src/selfhost/queue-common.ts—jobCoalesceMergeKeyPrefix()/jobCoalesceMergedPayload()(bounded to 100 paths);notify-evaluate's coalesce key now derives from a batch's full sorted dedup-key set.src/types.ts/src/queue/processors.ts—notify-evaluatenow carriesevents: DetectedNotificationEvent[]; the webhook handler batches one job per delivery instead of one per event, and the processor evaluates every event in the batch.grafana/dashboards/gittensory.json/prometheus/rules/alerts.yml— new panel for the granted-under-pressure metric; updatedGittensoryMaintenanceStarvedrunbook to reference both age escapes and the new metric..env.example/apps/gittensory-ui/src/lib/selfhost-env-reference.ts— documented + regenerated for the new env var.Why
Directly reported operational incident: a self-host VPS showed 68 pending jobs, all maintenance, 0 live/due-now jobs, repeated
selfhost_queue_maintenance_admission_deferred reason=maintenance_pending_highlogs, and a dashboard that (per the report) made it look like review work was stuck — root-caused during investigation to the admission policy's aggregate-count check having no bounded-rate release valve short of the full 4h trickle, plus real (if secondary) coalescing gaps in the two job types that made up the bulk of the backlog.Validation
git diff --check— cleannpm run actionlint— passnpm run db:migrations:check— pass (105 migrations OK, next free 0103; no new migration needed, no schema change)npm run db:schema-drift:check— passnpm run selfhost:env-reference:check— pass (regeneratedselfhost-env-reference.tsfor the new env var)npm run selfhost:validate-observability— pass (dashboards + alert rules valid)npm run cf-typegen:check— pass (no wrangler.jsonc changes)npm run typecheck— passnpm run test:coverage(full, unsharded) — 7298 passed, 0 failed, 7 skipped across 375 test files; global 96.58%/95.66%/95.75%/97.14% (stmt/branch/func/line); every changed line and branch in this diff traced individually againstlcov.infoand confirmed covered (new admission-policy branches, both merge-coalescing branches in both queue backends, the notify-evaluate batching path, and the granted-under-pressure metric path all have dedicated tests)npm run test:workers— passnpm run build:mcp/npm run test:mcp-pack/npm run build:miner/npm run rees:test— passnpm run ui:openapi:check/npm run ui:openapi:settings-parity/npm run ui:version-audit— passnpm run ui:lint/npm run ui:typecheck/npm run ui:test/npm run ui:build— passnpm audit --audit-level=moderate— 0 vulnerabilitiesScope
type(scope): short summaryConventional Commit format.CONTRIBUTING.md; nosite//CNAME/GitHub Pages changes.Validation
git diff --checknpm run actionlintnpm run typechecknpm run test:coveragelocally — see Validation section above (full run, 100% of changed lines/branches individually confirmed covered againstlcov.info)npm run test:workersnpm run build:mcpnpm run test:mcp-packnpm run ui:openapi:checknpm run ui:lintnpm run ui:typechecknpm run ui:buildnpm audit --audit-level=moderateSafety
Notes
server.tsbefore writing this PR, rather than assuming the report's framing. This PR's actual gap was the admission policy's missing bounded-rate release valve, plus the two coalescing gaps and the complementary granted-under-pressure metric.pg-queue.tsis Codecov-ignored (validated by integration/smoke tests, not unit coverage percodecov.yml) — its changes are still mirrored line-for-line fromsqlite-queue.tsand covered by dedicated mocked-Poolunit tests for regression insurance, matching the existing convention for that file.