Skip to content

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

Closed
RealDiligent wants to merge 3 commits into
JSONbored:mainfrom
RealDiligent:fix/transient-lock-ownership-release
Closed

fix(selfhost): use ownership tokens for transient PR actuation locks#3153
RealDiligent wants to merge 3 commits into
JSONbored:mainfrom
RealDiligent:fix/transient-lock-ownership-release

Conversation

@RealDiligent

Copy link
Copy Markdown
Contributor

Summary

Fixes a production race in transient PR actuation locks where a stale holder's finally block could blind-del() a successor's live lock after TTL expiry, reopening merge/close actuation races (#2129/#2135).

Problem

claimTransientLock() stored a constant "1" as the lock value. releasePrActuationLock / releaseAiReviewLock used unconditional del(key). If holder A ran past the TTL, holder B claimed the lock, and A's stale cleanup ran afterward, A deleted B's lock — restoring the exact cross-worker race the mutex was meant to prevent.

Root cause

Lock release was not ownership-aware. There was no compare-and-delete primitive on the transient cache adapter, and release always deleted by key regardless of who currently held the lock.

Implementation

  • Add releaseIfValue(key, value) to the Redis transient cache (Lua compare-and-delete).
  • Extend SELFHOST_TRANSIENT_CACHE with optional releaseIfValue.
  • claimTransientLock() now generates a UUID owner token and returns { acquired, ownerToken }.
  • releaseTransientLockIfOwner() releases only when the token matches; skips blind del() when releaseIfValue is unavailable (TTL backstop).
  • Update all actuation and AI-review lock call sites to pass through owner tokens.

Testing performed

  • npm run typecheck
  • Lock-related unit tests in test/unit/queue.test.ts (including stale-holder regression, no-releaseIfValue adapter, and releaseIfValue error path)
  • test/unit/selfhost-redis-cache.test.ts (releaseIfValue Lua path)
  • Updated test/unit/ai-review-advisory.test.ts for new lock API
  • Rebased on latest upstream/main (62059b0f)

Compatibility

  • Exported API change: claimPrActuationLock, claimAiReviewLock return TransientLockClaim instead of boolean; release helpers take ownerToken: string | null. These are internal/selfhost exports used only within this repo's call sites (all updated).
  • Adapters without releaseIfValue retain prior fail-open claim behavior but no longer perform unsafe blind release — locks expire via TTL instead.

Why this approach

Compare-and-delete is the minimal correct fix for Redis-style transient locks without introducing a heavier per-PR Durable Object. It directly addresses the documented KNOWN LIMITATION while preserving existing TTL crash-safety semantics.

Supersedes closed #2991 (same change, rebased + additional codecov branch coverage).

Scope

  • The PR title follows Conventional Commit format.
  • Focused change; no unrelated modifications.
  • Follows CONTRIBUTING.md.
  • Tests cover new branches, fallback paths, and the stale-holder regression.

Validation

  • npm run typecheck
  • Targeted lock/coverage unit tests locally
  • Full npm run test:ci pending CI (Windows-local full suite has environment-specific failures unrelated to this change)

Safety

  • No secrets or credentials in the diff.
  • Negative-path tests for stale holder, missing releaseIfValue, and release errors.

Notes

Prior PR #2991 reached maintainer approval but was closed when codecov/patch reported 96.15% on the changed hunk. This revision adds explicit tests for the intentionally skipped blind-del() path and the best-effort release error handler.

RealDiligent and others added 2 commits July 4, 2026 17:03
Per-PR actuation and AI-review mutexes claimed Redis keys with a constant
value and released via blind del(). A holder running past the TTL could
delete a successor's live lock in finally, reopening merge/close races the
mutex exists to prevent (JSONbored#2129/JSONbored#2135).

Store a per-holder UUID at claim time and release with compare-and-delete
(releaseIfValue) on the Redis cache adapter. Skip release when fail-open
(no cache) or when the adapter lacks compare-and-delete (TTL backstop).

Co-authored-by: Cursor <cursoragent@cursor.com>
Add regression tests for caches without releaseIfValue and for
releaseIfValue failures so stale-holder protection branches are fully
exercised.

Co-authored-by: Cursor <cursoragent@cursor.com>
@RealDiligent
RealDiligent requested a review from JSONbored as a code owner July 4, 2026 17:21
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@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 (62059b0) to head (29cf2d0).
⚠️ Report is 97 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3153   +/-   ##
=======================================
  Coverage   96.06%   96.06%           
=======================================
  Files         260      260           
  Lines       28684    28696   +12     
  Branches    10437    10440    +3     
=======================================
+ Hits        27555    27567   +12     
  Misses        493      493           
  Partials      636      636           
Files with missing lines Coverage Δ
src/queue/processors.ts 92.77% <100.00%> (+0.03%) ⬆️
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.

@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

Caution

🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥

🛑 Gittensory review result - reject/close recommended

Review updated: 2026-07-04 17:35:02 UTC

7 files · 1 AI reviewer · 1 blocker · readiness 80/100 · CI pending · blocked

🛑 Suggested Action - Reject/Close

  • AI reviewers agree on a likely critical defect: src/queue/processors.ts:3388 makes `releaseTransientLockIfOwner` skip release whenever a cache adapter has `claim` but no `releaseIfValue`, so any existing self-host adapter matching the still-legal `SELFHOST_TRANSIENT_CACHE` shape in `src/env.d.ts` will keep PR actuation locks for 600 seconds and AI-review locks for 1800 seconds after normal successful work
  • either make `releaseIfValue` required for adapters that expose `claim`, lower/partition the compatibility TTL behavior, or change the interface so startup/config rejects `claim` without ownership-aware release instead of silently blocking follow-up work. ```ts if (cache?.claim && !cache.releaseIfValue) { throw new Error("SELFHOST_TRANSIENT_CACHE.claim requires releaseIfValue for ownership-aware locks")
  • } ``` — Resolve the flagged defect, or override if the AI reviewers are mistaken, then re-run the gate.

Review summary
This change correctly moves transient PR and AI-review locks from blind delete semantics to owner-token compare-and-delete, and the Redis adapter implements the needed atomic release primitive. The main remaining risk is the compatibility path: adapters that implement `claim` but not `releaseIfValue` now hold every successfully acquired lock until the full TTL, which is a reachable behavior change for the optional interface described in `env.d.ts`. The Redis path itself is coherent and the new regression tests cover the stale-holder overwrite scenario.

Blockers

  • src/queue/processors.ts:3388 makes `releaseTransientLockIfOwner` skip release whenever a cache adapter has `claim` but no `releaseIfValue`, so any existing self-host adapter matching the still-legal `SELFHOST_TRANSIENT_CACHE` shape in `src/env.d.ts` will keep PR actuation locks for 600 seconds and AI-review locks for 1800 seconds after normal successful work; either make `releaseIfValue` required for adapters that expose `claim`, lower/partition the compatibility TTL behavior, or change the interface so startup/config rejects `claim` without ownership-aware release instead of silently blocking follow-up work. ```ts if (cache?.claim && !cache.releaseIfValue) { throw new Error("SELFHOST_TRANSIENT_CACHE.claim requires releaseIfValue for ownership-aware locks"); } ```
Nits — 5 non-blocking
  • test/unit/queue.test.ts:5156 validates the no-`releaseIfValue` path by asserting `del` is not called, but it does not assert the practical follow-up claim remains denied until TTL; add that assertion if you intentionally keep the compatibility behavior so the cost is explicit.
  • src/selfhost/redis-cache.ts:27 inlines the Lua script inside `releaseIfValue`; extracting it to a named constant would make the ownership-release contract easier to audit and reuse in future adapter tests.
  • src/env.d.ts:36 should make the adapter contract explicit: `claim` plus missing `releaseIfValue` is not just less capable, it changes successful releases into TTL-only holds.
  • test/unit/queue.test.ts:5156 should include a second `claimPrActuationLock` after release on the no-`releaseIfValue` adapter to document whether the lock intentionally remains held.
  • src/selfhost/redis-cache.ts:27 can use a module-level `RELEASE_IF_VALUE_SCRIPT` constant so the exact compare-and-delete primitive is not buried in a method body.

Why this is blocked

  • src/queue/processors.ts:3388 makes `releaseTransientLockIfOwner` skip release whenever a cache adapter has `claim` but no `releaseIfValue`, so any existing self-host adapter matching the still-legal `SELFHOST_TRANSIENT_CACHE` shape in `src/env.d.ts` will keep PR actuation locks for 600 seconds and AI-review locks for 1800 seconds after normal successful work; either make `releaseIfValue` required for adapters that expose `claim`, lower/partition the compatibility TTL behavior, or change the interface so startup/config rejects `claim` without ownership-aware release instead of silently blocking follow-up work. ```ts if (cache?.claim && !cache.releaseIfValue) { throw new Error("SELFHOST_TRANSIENT_CACHE.claim requires releaseIfValue for ownership-aware locks"); } ```
Signal Result Evidence
Code review ❌ 1 blocker 1 reviewer
Linked issue ✅ Linked #2991
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 (1 linked issue).
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: 124 registered-repo PR(s), 14 merged, 0 issue(s).
Contributor context ✅ Confirmed Gittensor contributor RealDiligent; Gittensor profile; 124 PR(s), 0 issue(s).
Gate result ❌ Blocking Repo-configured hard blocker found.
Review context
  • Author: RealDiligent
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: not available
  • Official Gittensor activity: 124 PR(s), 0 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Await review-lane availability.
  • Refresh registry data or choose a registered active repo.
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

…behavior

Co-authored-by: Cursor <cursoragent@cursor.com>
@loopover-orb

loopover-orb Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Gittensory is closing this pull request on the maintainer's behalf (AI reviewers agree on a likely critical defect: src/queue/processors.ts:3388 makes `releaseTransientLockIfOwner` skip release whenever a cache adapter has `claim` but no `releaseIfValue`, so any existing self-host adapter matching the still-legal `SELFHOST_TRANSIENT_CACHE` shape in `src/env.d.ts` will keep PR actuation locks for 600 seconds and AI-review locks for 1800 seconds after normal successful work; either make `releaseIfValue` required for adapters that expose `claim`, lower/partition the compatibility TTL behavior, or change the interface so startup/config rejects `claim` without ownership-aware release instead of silently blocking follow-up work. ```ts if (cache?.claim && !cache.releaseIfValue) { throw new Error("SELFHOST_TRANSIENT_CACHE.claim requires releaseIfValue for ownership-aware locks"); } ```). This is an automated maintenance action — to pursue this change, please open a new pull request with the issues resolved. Closed PRs may be analyzed later to improve review accuracy, but they are not automatically reopened or re-reviewed.

@loopover-orb loopover-orb Bot closed this Jul 4, 2026
RealDiligent added a commit to RealDiligent/gittensory that referenced this pull request Jul 4, 2026
Ownership-token release fixed stale-holder blind del() (JSONbored#2129), but skipping
release when releaseIfValue was absent pinned locks for 600s/1800s after normal
work on misconfigured adapters (JSONbored#3153).

- Boot: assertSelfhostTransientCacheOwnershipRelease() in server.ts
- Runtime: fail open without calling claim() when releaseIfValue is missing
- Tests: stale-holder regressions for both lock namespaces, boot guard, JSONbored#3153 path

Co-authored-by: Cursor <cursoragent@cursor.com>
JSONbored pushed a commit that referenced this pull request Jul 4, 2026
…rs (#3164)

Ownership-token release fixed stale-holder blind del() (#2129), but skipping
release when releaseIfValue was absent pinned locks for 600s/1800s after normal
work on misconfigured adapters (#3153).

- Boot: assertSelfhostTransientCacheOwnershipRelease() in server.ts
- Runtime: fail open without calling claim() when releaseIfValue is missing
- Tests: stale-holder regressions for both lock namespaces, boot guard, #3153 path

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant