Skip to content

fix(storage): share one in-flight provider initialization across concurrent first calls - #724

Open
Matthew-Selvam wants to merge 1 commit into
libredb:mainfrom
Matthew-Selvam:fix/storage-factory-init-race
Open

fix(storage): share one in-flight provider initialization across concurrent first calls#724
Matthew-Selvam wants to merge 1 commit into
libredb:mainfrom
Matthew-Selvam:fix/storage-factory-init-race

Conversation

@Matthew-Selvam

Copy link
Copy Markdown
Contributor

Summary

getStorageProvider() had an initialization race: it checked _provider && _initialized, then awaited a dynamic import() and initialize() before setting the flag. Two overlapping first requests — e.g. the page-load GET /api/storage racing a PUT /api/storage/[collection] — both fell through the check, each constructed and initialized a provider, and the second assignment overwrote the first mid-initialize. Result: initialize() ran twice, a duplicated (encrypted-wrapper) provider was created, and the orphaned provider's connections were leaked.

Fix

The in-flight initialization promise is memoized: the first caller starts exactly one build+initialize, and every concurrent caller awaits the same promise. A rejected initialize() is un-memoized so a transient DB outage doesn't become a permanent one (the next request retries from scratch instead of awaiting a dead promise forever). closeStorageProvider() also resets the memo so tests/cleanup keep working.

Test plan

  • New: concurrent first calls share one provider and one initialize()Promise.all of two first calls asserts a single instance and a single initialize()
  • New: a failed initialize is not memoized — the next call retries from scratch
  • Existing singleton/close suites in tests/isolated/factory-singleton.test.ts still pass unchanged

Found during a broader code review of the storage layer; no issue existed yet for it.

…urrent first calls

getStorageProvider() checked _provider && _initialized, then awaited a
dynamic import and initialize() before setting the flag. Two overlapping
first requests — the page-load GET /api/storage racing a PUT
/api/storage/[collection] — both fell through, each constructing and
initializing a provider; the second assignment overwrote the first
mid-initialize, duplicating initialize() and leaking the orphan's
connections.

The in-flight promise is now memoized: concurrent callers await the same
build+initialize. A rejected initialize is un-memoized so the next request
retries instead of awaiting a dead promise forever.
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cevheri cevheri added the loop:needs-moderator-action Flagged by the maintainer loop: suspicious content or a decision only a human can make label Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

loop:needs-moderator-action Flagged by the maintainer loop: suspicious content or a decision only a human can make

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants