Skip to content

feat(selfhost): defer maintenance work under runtime pressure#2717

Merged
JSONbored merged 1 commit into
mainfrom
feat/selfhost-maintenance-backpressure
Jul 3, 2026
Merged

feat(selfhost): defer maintenance work under runtime pressure#2717
JSONbored merged 1 commit into
mainfrom
feat/selfhost-maintenance-backpressure

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

  • Adds a claim-time maintenance-admission policy (src/selfhost/maintenance-admission.ts) so periodic maintenance sweeps — contributor evidence, RAG re-indexing, drift scans, burden forecasts, product rollups, notifications, etc. — always yield to live webhook/review work and to host CPU pressure, instead of competing with it on equal footing. Wired into both queue backends (sqlite-queue.ts, pg-queue.ts) at the exact point they already handle GitHub rate-limit admission, using the same defer-with-jitter pattern. A denied job is pushed back to pending with a jittered future run_after; its original enqueue time is preserved so a trickle ceiling (MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS, default 4h) force-admits it under sustained pressure — maintenance can slow down but never starves forever.
  • Signals: live-priority pending count + oldest live job age, maintenance-lane pending count, and (best-effort, Node-only) 1-minute host load average normalized per CPU core (src/selfhost/host-pressure.ts). All thresholds are env-configurable with protective defaults; existing coalescing (jobCoalesceKey) already collapses duplicate maintenance requests, so this is additive, not a rewrite of the queue.
  • Classifies a bare 403 on the branch-protection probe (fetchRequiredStatusContexts) as a permission gap, not GitHub rate-limit exhaustion — reuses GitHubApiError.rateLimited (already correctly computed) and adds a dedicated gittensory_github_branch_protection_permission_denied_total counter so a dashboard can tell "GitHub is throttling us" apart from "this token can't read branch protection" (the common case for installations/forks). The 403/404 negative-cache and rate-limit classification themselves were already correct — this only adds the missing observability.
  • Observability: new Grafana panels + Prometheus alerts for live vs. maintenance queue depth, oldest job age per lane, host load, and branch-protection permission-denied count, so a slowdown is diagnosable as app-queue vs. CI-pending vs. AI-latency vs. GitHub-limits vs. host-CPU at a glance.
  • Docs: documents the three self-host runner deployment modes (app-only VPS + GitHub-hosted CI recommended default / separate dedicated runner host / co-located --profile runners) in .env.example and docker-compose.yml, addressing the production incident this PR is a response to (3 uncapped CI-runner containers starving the review app on an 8-vCPU box).
  • Adds scripts/docker-prune.sh: a safe, read-only-by-default Docker build-cache/image prune script (never touches volumes/application data) plus disk-pressure guidance.

Why no linked issue: this is a direct, self-scoped operational-hardening task (repo-owner-authored) responding to a live production incident on our own self-host VPS, not a community-issue-driven contribution — see Notes below for the incident detail.

Scope

  • The PR title follows type(scope): short summary Conventional Commit format.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes — every change here is one coherent self-host runtime-pressure feature (policy + its tests + its observability + its docs).
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • I linked an issue, or this is small enough that the summary explains why an issue is not needed.

Validation

All commands below were run locally and are portable (no repo-specific secrets required):

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage locally (unsharded) — 96.63% statements / 95.68% branches project-wide; the new/changed src/** files (maintenance-admission.ts, host-pressure.ts, sqlite-queue.ts, backfill.ts's changed lines) are at 100% line/branch coverage on their own diffs (verified with --coverage.include scoped runs). pg-queue.ts and src/server.ts are Codecov-ignored per codecov.yml (Postgres path validated by the real-Postgres integration test; server.ts by the Docker boot smoke test) but still have unit tests for parity/correctness.
  • npm run test:workers
  • npm run build:mcp
  • npm run test:mcp-pack
  • npm run ui:openapi:check
  • npm run ui:lint
  • npm run ui:typecheck
  • npm run ui:build
  • npm audit --audit-level=moderate — 0 vulnerabilities
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries
  • npm run test:ci (the full local gate, all steps above plus db:migrations:check, db:schema-drift:check, selfhost:env-reference:check, selfhost:validate-observability, cf-typegen:check, ui:openapi:settings-parity, ui:version-audit, ui:test, build:miner, rees:test) — fully green, 7058 tests passed.
  • npm run selfhost:env-reference (regenerated apps/gittensory-ui/src/lib/selfhost-env-reference.ts after adding the new MAINTENANCE_ADMISSION_* env vars to .env.example)
  • node scripts/validate-observability-configs.mjs (new Grafana panels + Prometheus alert rules validated)

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests. — N/A, no auth/session/CORS changes.
  • API/OpenAPI/MCP behavior is updated and tested where needed. — N/A, no public API/OpenAPI/MCP surface changes.
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks. — N/A, no apps/gittensory-ui UI changes.
  • Visible UI changes include a UI Evidence section — N/A, no visible UI changes (Grafana dashboard JSON and shell/doc comments are not app UI).
  • Public docs/changelogs are updated where needed (.env.example, docker-compose.yml, docker-compose.override.yml.example); CHANGELOG.md itself is untouched (not a release-prep PR).

Notes

  • Production context: our self-host VPS was redeployed and the app was healthy (queue pending ~27 draining to ~22, dead-queue 0), but everything felt slow under load. Root cause was host pressure from co-located CI runner containers (330–470% CPU each, host load ~9–11), not the app itself — this PR is the operational hardening response: an application-level admission policy so maintenance work always yields under pressure, plus documentation steering runners off the review VPS by default, plus disk hygiene for the ~83GB images / ~73GB build cache that had accumulated.
  • Scope note: docker-compose.yml, docker-compose.override.yml.example, .env.example, and grafana/dashboards/gittensory.json are outside this repo's wantedPaths gate manifest (src/, packages/, test/, migrations/, scripts/, review-enrichment/, .github/workflows/, wrangler.jsonc, apps/gittensory-ui/). Touching them is intentional and necessary here (runner-isolation docs and observability dashboards can't be delivered without them) — flagging for owner-review awareness since the automated gate treats out-of-scope paths as advisory rather than a hard requirement for owner-authored PRs.
  • Deliberately did not add an AI-provider-failure-rate signal or a rewrite of build-contributor-evidence into incremental computation — the former has no existing counter to build on (out of scope for a focused PR), and the latter is high-risk/high-effort for uncertain benefit given the admission policy already prevents it from running during a pressure spike, and its own scheduled cadence (every 6h, isFullSyncWindow in src/index.ts) is already conservative.

Adds a claim-time admission policy so periodic maintenance sweeps
(contributor evidence, RAG indexing, drift scans, product rollups,
notifications...) always yield to live webhook/review work and to
host CPU pressure, instead of competing with it on equal footing.
Denied jobs are deferred with jitter and force-admitted via a
trickle ceiling so sustained load can slow maintenance down but
never starve it forever.

Also: classifies a bare 403 on the branch-protection probe as a
permission gap (not GitHub rate-limit exhaustion) with its own
metric, adds Grafana panels + Prometheus alerts for live vs.
maintenance queue pressure, documents the three self-host runner
deployment modes, and adds a safe Docker build-cache/image prune
script (never touches volumes).
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
gittensory-ui b27b121 Commit Preview URL

Branch Preview URL
Jul 03 2026, 07:30 AM

@JSONbored JSONbored self-assigned this Jul 3, 2026
@JSONbored
JSONbored merged commit c8706d5 into main Jul 3, 2026
11 checks passed
@JSONbored
JSONbored deleted the feat/selfhost-maintenance-backpressure branch July 3, 2026 07:39
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.13%. Comparing base (a65af03) to head (b27b121).
⚠️ Report is 9 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #2717    +/-   ##
========================================
  Coverage   96.12%   96.13%            
========================================
  Files         240      243     +3     
  Lines       26988    27128   +140     
  Branches     9793     9858    +65     
========================================
+ Hits        25943    26080   +137     
  Misses        433      433            
- Partials      612      615     +3     
Files with missing lines Coverage Δ
src/github/backfill.ts 97.00% <100.00%> (+0.03%) ⬆️
src/selfhost/host-pressure.ts 100.00% <100.00%> (ø)
src/selfhost/maintenance-admission.ts 100.00% <100.00%> (ø)
src/selfhost/sqlite-queue.ts 99.38% <100.00%> (+0.07%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant