fix(share-tokens): single-flight load and serialized atomic persist (Closes #102) - #165
Conversation
|
Warning Review limit reached
Next review available in: 3 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
35fba67
into
harsharajkumar-273:main
Closes #102
Problem
backend/src/services/shareTokenStore.tshas two independent races, and the first is considerably more damaging than it looks.1.
load()can read more than once. The guard isif (cache) return cache, butcacheis only assigned after theawait fs.readFile(...)resolves. Every caller arriving during that window seesnulland starts its own read, so each ends up holding a different object. Each then adds its token to its own copy and persists it — and the last write wins, discarding every other caller's token.2.
persist()writes concurrently to one path.fs.writeFiletruncates and then writes, so two overlapping calls can leave a partially written file. That matters more than a normal torn write would, becauseload()handles a parse failure with a barecatch { cache = {} }— a corrupted file is not surfaced anywhere, it silently becomes an empty token store.Reproduction
I transpiled the current file and drove 300 concurrent
createShareTokencalls against it:All 300 callers hit the cold-cache window, so all 300 wrote single-token files over each other.
The same run against the patched file:
To be precise about scope: with a warm cache the current code is fine (301/301), because all callers then share one object. The severe path is specifically the cold-start burst — process start, or the first requests after a restart.
I should also be straight that I could not reproduce a torn write on my filesystem; ~64KB writes completed without visible interleaving. The atomic rename below removes that failure mode by construction rather than by observation.
Change
Single-flight load. The in-flight promise is memoised, not just its result, so concurrent callers await the same read and receive the same object:
Parsing is also shape-checked now — a value that isn't a plain object is treated as empty rather than returned as a
TokenCache.Atomic write.
writeAtomicwrites to a randomly-named temporary file in the target directory (sorenamestays within one filesystem), flushes the handle, then renames over the target.renameis atomic, so a reader sees either the whole old file or the whole new one — never a partial. The random suffix stops two writers colliding on the temp path, and a failed write removes its own temp file.Serialized persists. Writes chain onto a queue so only one runs at a time. The queue advances with a swallowed rejection so a single failed write doesn't wedge every later one, and
persiststill logs rather than throws — the original contract is unchanged.Scope
This addresses in-process concurrency, which is what the issue describes. It does not add a cross-process lock: two backend processes sharing
PROOFDESK_DATA_DIRcould still lose an update, though the atomic rename means they can no longer corrupt the file.docker-compose.prod.ymlruns a single backend, so that isn't currently reachable — happy to add file locking if you'd like it covered.Verification
npx tsc --noEmitinbackend/clean..tmpfiles are left behind after the run.Note:
npm testis red on cleanmainindependently of this branch.