fix: bound the PGlite open so a stalled init can't wedge a turn - #6
Merged
TheArchitectit merged 1 commit intoJul 29, 2026
Merged
Conversation
Both index modules cache their in-flight open in a module-level initPromise. That cache turns a single stalled open into a permanent hang: PGlite is a single-writer WASM Postgres over a shared dataDir (~/.pi/mega-compact-vector), so a second pi process on the same dir can block indefinitely. The await never settles, the dead promise is cached, and every later caller awaits it. With no timers and no sockets left, node reports "Promise resolution is still pending but the event loop has already resolved" and the pi turn awaiting it never ends. Observed in the wild: a pi session sat on one turn for 75 minutes with all threads parked in epoll/futex and zero open sockets — not slow, deadlocked. withOpenTimeout() puts a ceiling on the open (30s default, configurable via MEGACOMPACT_PGLITE_OPEN_TIMEOUT_MS; 0 restores the old unbounded behavior). On timeout the caller gets undefined and both modules degrade to the synchronous scan they already fall back to. The cached initPromise is cleared rather than left dead, and the abandoned open is disowned — if it settles later the instance is closed so an orphan can't hold the dataDir lock. Rejections deliberately propagate unchanged, so the existing corrupt-dir detection (Aborted/RuntimeError -> wipe + one retry) still runs. Only the timeout resolves to undefined.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #5, at the opposite end of the same lifecycle. #5 guards the close (headless exit hang). This guards the open.
The failure
Both index modules cache their in-flight open in a module-level
initPromise. That cache turns one stalled open into a permanent hang: PGlite is a single-writer WASM Postgres over a shared dataDir (~/.pi/mega-compact-vector), so a second pi process on the same dir can block indefinitely. Theawaitnever settles, the dead promise is cached, and every later caller awaits it. With no timers and no sockets left, node reports "Promise resolution is still pending but the event loop has already resolved" — and the pi turn awaiting it never ends.Observed in the wild: a session sat on one turn for 75+ minutes, all threads parked in
epoll_wait/futex_wait, zero open sockets, PGlite files held open. Not slow — deadlocked. #5's fix cannot help here: the session never reachessession_shutdown.The change
withOpenTimeout()puts a ceiling on the open — 30s default, configurable viaMEGACOMPACT_PGLITE_OPEN_TIMEOUT_MS,0restores the previous unbounded behavior. On timeout the caller getsundefinedand both modules degrade to the synchronous scan they already fall back to. The cachedinitPromiseis cleared rather than left dead, and the abandoned open is disowned — if it settles later the instance is closed, so an orphan can't hold the dataDir lock.Rejections deliberately propagate unchanged, so the existing corrupt-dir detection (
Aborted/RuntimeError→ wipe + one retry) still runs. Only the timeout resolves toundefined.Tests
src/store/pgOpenGuard.test.ts— 6 tests: never-settling open returns promptly, success passes through, rejection still reaches the corrupt-retry path, late-settling open is closed,0disables the guard, env parsing rejects junk.Full sweep: 70 of 76 test files pass. The 6 that don't are pre-existing and unrelated — 5 hang on a
harness()teardown leak (a zero-test run exits in 148ms), 1 fails because the@mongodb-js/zstdnative addon isn't built. I confirmedsrc/memoryOps.test.jshangs identically with these changes stashed.