Skip to content

fix(selfhost): fail open with a metric on a token-cache Redis error - #6364

Merged
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
galuis116:fix/redis-token-cache-fail-open-on-error
Jul 16, 2026
Merged

fix(selfhost): fail open with a metric on a token-cache Redis error#6364
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
galuis116:fix/redis-token-cache-fail-open-on-error

Conversation

@galuis116

Copy link
Copy Markdown
Contributor

Closes #6288

Summary

  • redis-token-cache.ts's get() had no try/catch around redis.get() itself, so a Redis connection failure threw uncaught with no observability signal — unlike its two sibling wrappers (redis-response-cache.ts throws with a result:"error" metric, redis-cache.ts fails open silently with no metric).
  • Verified the actual caller contract before choosing a fix: github/app.ts's readCachedTokencreateInstallationToken has no try/catch of its own around this call, so an uncaught rejection here would hard-fail GitHub App token minting on every Redis hiccup instead of just costing one extra real mint. Fail open (return null, same contract as redis-cache.ts) is the correct behavior for this specific caller — but unlike redis-cache.ts, this fix also records a result:"error" metric so the failure is never silently invisible, matching redis-response-cache.ts's observability discipline. set() and the other two Redis wrapper files are untouched — no inconsistency was found in their behavior.
  • Added a regression test simulating a Redis connection failure, verified to genuinely fail without the fix (uncaught rejection) before confirming it passes with it.

Scope

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck — root tsc --noEmit reliably OOMs on this shared sandbox regardless of what changed (reproduced repeatedly this session on a clean checkout). Scoped coverage (npx vitest run ... --coverage --coverage.include="src/selfhost/redis-token-cache.ts") confirms 100% statements/branches/functions/lines on the changed file, and the full dedicated test file plus github-app.test.ts (98 tests total) pass.
  • npm run test:coverage — not run repo-wide (same OOM risk). Scoped coverage on the changed file is 100% branches (6/6). test/unit/selfhost-redis-token-cache.test.ts (7 tests) and test/unit/github-app.test.ts (91 tests, covering the caller side) both pass.
  • npm run test:workers — N/A, no Worker-facing behavior changed beyond this file's own contract, which is now more resilient (fail-open), not less.
  • npm run build:mcp / npm run test:mcp-pack — N/A, no @loopover/mcp changes.
  • npm run ui:openapi:check / ui:lint / ui:typecheck / ui:build — N/A, no apps/loopover-ui changes.
  • npm audit --audit-level=moderate — 0 vulnerabilities.
  • New/changed behavior has tests — the new regression test was verified to genuinely catch the bug (fails on main with the fix stashed) before confirming it passes with the fix applied.

If any required check was skipped, explain why:

  • Root npm run typecheck / npm run test:coverage: reliably OOMs on this shared sandbox under memory pressure from concurrent sessions, independent of the diff. Substituted with scoped 100% coverage on the changed file plus the full caller-side test suite (98 tests, all passing).

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. — Yes: this IS a negative-path fix for GitHub App token-cache resilience, with a dedicated regression test for the Redis-failure path.
  • API/OpenAPI/MCP behavior is updated and tested where needed. — N/A.
  • 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 with screenshots. — N/A, no visible UI change.
  • Public docs/changelogs are updated where needed; changelogs are only edited for release-prep PRs. — CHANGELOG.md untouched.

Notes

  • set() in the same file also lacks a try/catch, but the issue explicitly scopes to the get() path (redis-token-cache.ts:22-27) and instructs not to touch the other files "unless a genuine inconsistency in THEIR behavior is also found" — kept this PR focused on the described get() gap rather than expanding scope.

redis-token-cache.ts's get() had no try/catch around redis.get()
itself, so a connection failure threw uncaught with no observability
signal — unlike its two sibling wrappers (redis-response-cache.ts
throws with a metric, redis-cache.ts fails open silently).

Its actual caller (github/app.ts's readCachedToken ->
createInstallationToken) has no try/catch of its own, so an uncaught
rejection here would hard-fail GitHub App token minting on every
Redis hiccup instead of costing one extra real mint. Fail open
(return null, same contract as redis-cache.ts) is the correct
behavior for this caller, but record a metric so the failure is
never silently invisible, matching redis-response-cache.ts's
observability discipline.

Closes JSONbored#6288
@galuis116
galuis116 requested a review from JSONbored as a code owner July 16, 2026 04:36
@superagent-security

Copy link
Copy Markdown
Contributor

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

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.60%. Comparing base (b7e1bec) to head (586b503).
⚠️ Report is 14 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #6364   +/-   ##
=======================================
  Coverage   95.60%   95.60%           
=======================================
  Files         599      599           
  Lines       47221    47224    +3     
  Branches    15024    15024           
=======================================
+ Hits        45147    45150    +3     
  Misses       1290     1290           
  Partials      784      784           
Flag Coverage Δ
shard-1 44.00% <0.00%> (-0.19%) ⬇️
shard-2 36.80% <0.00%> (+0.40%) ⬆️
shard-3 32.45% <0.00%> (-0.07%) ⬇️
shard-4 34.55% <0.00%> (-0.06%) ⬇️
shard-5 31.01% <0.00%> (-0.59%) ⬇️
shard-6 45.31% <100.00%> (+0.43%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/selfhost/redis-token-cache.ts 100.00% <100.00%> (ø)

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

loopover-orb Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-16 05:13:43 UTC

2 files · 1 AI reviewer · no blockers · readiness 95/100 · CI green · unknown

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This PR wraps the existing `redis.get()` call in `redis-token-cache.ts` with a try/catch that fails open (returns null) and records a `result:"error"` metric, matching the caller contract of `readCachedToken`/`createInstallationToken` which has no error handling of its own. The fix is correctly scoped to the actual bug (uncaught rejection hard-failing token minting on any Redis hiccup) and the regression test genuinely exercises the failing path via a throwing fake `get()`, confirming both the null return and the metric emission. `set()` is correctly left untouched since it's not part of the described defect.

Nits — 4 non-blocking
  • The three inline comments in redis-token-cache.ts (module header, get() fail-open block, test block) repeat the same rationale about the caller's missing try/catch almost verbatim — could be trimmed to one canonical explanation.
  • fakeRedis's getThrows option always throws the same generic Error regardless of call count, so there's no test for a transient failure that later recovers, though this is a minor coverage gap given the existing test suite's conventions.
  • Consider consolidating the repeated 'caller has no try/catch' rationale into a single comment near the try/catch block rather than restating it in the test file too.
  • If other Redis-cache wrappers get similar fail-open+metric treatment in future PRs, this file's pattern (try/catch + recordTokenCacheMetric("error")) is a reasonable template to point to.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #6288
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 ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 1920 registered-repo PR(s), 1265 merged, 54 issue(s).
Contributor context ✅ Confirmed Gittensor contributor galuis116; Gittensor profile; 1920 PR(s), 54 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: minor
Review context
  • Author: galuis116
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: not available
  • Official Gittensor activity: 1920 PR(s), 54 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Triage stale or unlinked PRs.
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.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

🟩 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 LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@loopover-orb loopover-orb Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit 0c18914 into JSONbored:main Jul 16, 2026
16 checks passed
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.

fix(selfhost): 3 Redis cache wrappers handle a connection failure 3 different ways, one recording no error signal at all

1 participant