Skip to content

fix(selfhost): use ownership tokens for transient PR actuation locks - #3050

Merged
JSONbored merged 1 commit into
JSONbored:mainfrom
joaovictor91123:fix/selfhost-transient-lock-ownership-tokens
Jul 4, 2026
Merged

fix(selfhost): use ownership tokens for transient PR actuation locks#3050
JSONbored merged 1 commit into
JSONbored:mainfrom
joaovictor91123:fix/selfhost-transient-lock-ownership-tokens

Conversation

@joaovictor91123

Copy link
Copy Markdown
Contributor

Summary

claimTransientLock (src/queue/processors.ts) — the shared mutex behind the per-PR actuation lock and the per-(repo, PR, head, mode) AI-review lock — used a constant lock value ("1") for every holder, and release was a blind del(). The code's own comment called this out as a KNOWN LIMITATION: if a holder ran past its TTL, a NEW claimant's live lock could be deleted by the first holder's stale finally release, reopening the exact race the mutex exists to close (a maintenance pass and a draft-dodge close, or two AI-review passes, both proceeding for the same PR/head).

Fix

  • claimTransientLock now writes a fresh random ownerToken (node:crypto's randomUUID()) per claim instead of the shared constant, returning { acquired, ownerToken }.
  • A new releaseTransientLockIfOwner releases via releaseIfValue (atomic compare-and-delete) — it only deletes the key when the caller's own token still matches what's stored, so a stale holder's late release can never delete a different, live holder's claim. A cache without releaseIfValue skips release entirely and relies on the TTL, rather than falling back to a blind del() that would reopen the same race.
  • SELFHOST_TRANSIENT_CACHE.releaseIfValue is a new optional cache-adapter method (src/env.d.ts), implemented for the Redis adapter (src/selfhost/redis-cache.ts) via a single Lua eval (get+del must be one atomic server-side step, or the check and the delete could themselves race a new claimant's write).
  • claimPrActuationLock/claimAiReviewLock now return TransientLockClaim ({ acquired, ownerToken }) instead of a bare boolean; releasePrActuationLock/releaseAiReviewLock take the caller's ownerToken and thread it through releaseTransientLockIfOwner. All four call sites (maybeRunAgentMaintenance, runAiReviewForAdvisory, maybeCloseDraftDodgeAttempt, maybeRecloseDisallowedReopen) updated to hold onto the claim result through their try/finally.

New regression tests cover: a stale holder's release not deleting a successor's live lock, release being a no-op when ownerToken is null (nothing was actually claimed), and release skipping the cache entirely when releaseIfValue isn't implemented.

Prior art / why this is safe

This exact fix was already attempted and reviewed in #2991 (closed): the AI reviewer found no blockers and confirmed the design — "directly closes the stale-holder race described in the PR... queue call sites correctly retain the returned token through their finally blocks... Redis implementation uses a single Lua eval for atomicity" — it was closed solely because codecov/patch landed at 96.15% (1 partial branch short of the 99% target) in src/queue/processors.ts. This PR reimplements the same design with full branch coverage: I split the combined null-token / cache-presence check from the closed PR's releaseTransientLockIfOwner into two separate, independently-testable if guards, and verified every branch across all eight touched functions/call sites is hit on both sides (via --coverage.include scoped to the two changed source files, checked directly against the v8 coverage JSON, not just the summary %).

Why no linked issue

Directly reproduces and fixes the exact gap #2991 (closed, coverage-only failure) already diagnosed and the AI reviewer already approved the design for; this repo's linkedIssuePolicy is preferred, not required.

Scope

  • The PR title follows type(scope): short summary Conventional Commit format, for example fix(api): restore profile access checks.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes.
  • 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

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage locally; codecov/patch requires ≥99% coverage of the lines AND branches you changed (aim for 100% on your diff so CI variance does not fail near the threshold). Global coverage is a non-blocking trend with a loose 90% backstop, not the gate.
  • 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
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries

If any required check was skipped, explain why:

  • Backend-only change, no API/OpenAPI/UI surface touched, so ui:openapi:check, ui:lint, ui:typecheck, and ui:build are not applicable.
  • npm run test:coverage was run scoped to test/unit/queue.test.ts + test/unit/ai-review-advisory.test.ts + test/unit/selfhost-redis-cache.test.ts (--coverage.include on src/queue/processors.ts + src/selfhost/redis-cache.ts): every branch across all touched functions and call sites is hit on both sides (verified directly against the v8 coverage JSON). The full unsharded suite could not be run clean in my local Windows dev environment for unrelated reasons: several test files depend on tooling not present there (Docker daemon, Python, sentry-cli), and generated-file "stale" checks (openapi.json, cf-typegen, selfhost-env-reference) false-positive on this checkout's CRLF line endings vs. the repo's LF convention — confirmed unrelated to this diff by reproducing them identically on a clean, unmodified main. Also ran the full queue.test.ts + ai-review-advisory.test.ts + selfhost-redis-cache.test.ts suites (519 tests) clean.
  • npm run test:mcp-pack hits a Windows-only spawnSync("npm", ...) resolution failure locally (no shell: true, npm resolves to npm.cmd on Windows) — unrelated to this diff, expected to run on the Linux CI runner.
  • npm run actionlint, npm run typecheck, npm run test:workers, npm run build:mcp, and npm audit all ran clean.

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 such changes.
  • API/OpenAPI/MCP behavior is updated and tested where needed. — N/A, no such changes.
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks. — N/A, no UI changes.
  • Visible UI changes include a UI Evidence section below. — N/A, no UI changes.
  • Public docs/changelogs are updated where needed. — N/A, no docs/changelog changes.

UI Evidence

N/A — no UI/frontend/docs changes.

Notes

  • New exported type: TransientLockClaim in src/queue/processors.ts.
  • New optional cache-adapter method: SELFHOST_TRANSIENT_CACHE.releaseIfValue (src/env.d.ts), implemented in src/selfhost/redis-cache.ts and the test D1 helper (test/helpers/d1.ts).

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 4, 2026
@loopover-orb

loopover-orb Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Warning

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏸️ Gittensory review result - manual review recommended

Review updated: 2026-07-04 18:07:17 UTC

7 files · 1 AI reviewer · no blockers · readiness 80/100 · CI green · clean

⏸️ Suggested Action - Manual Review

  • Touches a guarded path — held for manual review

Review summary
This change correctly moves the transient PR and AI-review locks from blind release to ownership-token release, and the Redis adapter implements the necessary compare-and-delete atomically with Lua. The production call sites now carry the returned claim through their finally blocks, so stale holders no longer delete successor locks. I do not see a reachable breaking defect in the provided diff, but there is one adapter-contract edge worth tightening.

Nits — 5 non-blocking
  • nit: src/env.d.ts:39 leaves releaseIfValue optional even when claim exists, which means a custom adapter with claim but no releaseIfValue will hold successful locks until the full TTL after every normal completion; either document that pairing as required for real adapters or make the helper fail open unless both primitives are present.
  • nit: test/unit/queue.test.ts:5169 exercises the stale-owner case by writing the successor token directly with set(), so it proves the compare-delete behavior but not the full real claim-after-expiry path; consider adding a tiny fake-cache path where the second holder obtains its token through claimPrActuationLock after simulated expiry.
  • In src/env.d.ts, clarify the cache adapter contract as "claim and releaseIfValue should be implemented together" or enforce that in claimTransientLock so future self-host adapters do not accidentally serialize work for the whole TTL.
  • In test/unit/queue.test.ts, keep the current direct-token regression but add one assertion that a mismatched owner token also leaves an AI-review lock intact, since both public release wrappers now depend on the shared helper.
  • Touches a guarded path — held for manual review — A maintainer must review and merge this change.
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ✅ No-issue rationale PR body explains why no issue is linked.
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (no linked issue context).
Validation posture ❌ 5/25 Preflight is holding this PR: the review lane is unavailable, so it is not ready for automated review.
Contributor workload ✅ 10/10 Author activity: 96 registered-repo PR(s), 52 merged, 8 issue(s).
Contributor context ✅ Confirmed Gittensor contributor joaovictor91123; Gittensor profile; 96 PR(s), 8 issue(s).
Gate result ⚠️ Not blocking Advisory; not blocking this PR.
Review context
  • Author: joaovictor91123
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: JavaScript, Python, C++, MDX, Rust, TypeScript
  • Official Gittensor activity: 96 PR(s), 8 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Await review-lane availability.
  • Refresh registry data or choose a registered active repo.
  • Link the issue being solved, or explicitly explain why this is a no-issue PR.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 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.

  • Re-run Gittensory review

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.06%. Comparing base (cafc9f4) to head (2972dfc).
⚠️ Report is 21 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3050   +/-   ##
=======================================
  Coverage   96.06%   96.06%           
=======================================
  Files         260      260           
  Lines       28685    28696   +11     
  Branches    10437    10440    +3     
=======================================
+ Hits        27556    27567   +11     
  Misses        493      493           
  Partials      636      636           
Files with missing lines Coverage Δ
src/queue/processors.ts 92.77% <100.00%> (+0.02%) ⬆️
src/selfhost/redis-cache.ts 100.00% <100.00%> (ø)
🚀 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

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Development

Successfully merging this pull request may close these issues.

2 participants