Skip to content

[low] fix(usage): only clear the in-flight lock this render wrote - #624

Open
elhoim wants to merge 2 commits into
sirmalloc:mainfrom
elhoim:fix/usage-lock-owner-clear
Open

elhoim wants to merge 2 commits into
sirmalloc:mainfrom
elhoim:fix/usage-lock-owner-clear

Conversation

@elhoim

@elhoim elhoim commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

BLUF

  • Priority: low. Race between concurrent renders that fetch usage from the API.
  • Bug: when two renders fetch at the same time and one of them gets a 429, the other's successful response runs clearUsageLock(), which deletes the 429 rate-limited lock. The next render after the 180 s cache expiry then fetches again inside the server's Retry-After window.
  • Fix: clear usage.lock only while it still holds the in-flight record this render wrote (content compare).
  • Test: a new probe test writes a concurrent rate-limited lock while the request is in flight. It fails on main and passes with the fix.

Details

The lock check (readActiveUsageLock) and the in-flight write (writeUsageLock(now + 30, 'timeout')) are two separate syscalls, so two renders can both pass the check. The failing sequence:

  1. Renders A and B both see no lock and both send a request.
  2. B receives a 429 and writes { blockedUntil: now + retryAfter, error: 'rate-limited' }.
  3. A receives a 200 and runs clearUsageLock(), an unconditional rmSync, which deletes B's lock.
  4. After CACHE_MAX_AGE (180 s, shorter than the 300 s default backoff), the next render finds a stale cache and no lock, and fetches again.

I found this with a TLA+ model of the lock protocol:

  • The model with the unconditional clear violates "a render that starts after a 429 was received does not fetch before the deadline".
  • With the owner-only clear, the property holds exhaustively for 3 renders over 9 ticks (628,645 states).

A replay of the same interleaving against the built dist/ bundle (network faked, 300 ms pause injected after A's lock check) shows:

Trials where the herd formed and B got a 429 Render C fetched inside the window
Before 26 22
After 29 0

The window is narrow: two renders must pass the lock check at almost the same moment, and only sessions that need API-only fields fetch at all. So this is low priority. The fix is small and keeps #487's intent: a render still removes its own in-flight lock after a satisfying success.

Checks

  • bun run lint: clean.
  • bun run build: clean.
  • bun test: the new test passes. fetchUsageData error handling > preserves root errors… times out at 5 s on this loaded machine, identically on main.

🤖 Generated with Claude Code

elhoim and others added 2 commits September 30, 2026 12:19
A successful fetch removed usage.lock unconditionally. When two renders
fetch concurrently and one receives a 429, the other's later success
deleted the Retry-After lock, so the next render after the 180s cache
expiry fetched again inside the server's backoff window.

Clear the lock only while it still holds this render's own in-flight
record.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant