Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions extensions/mega-compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,20 @@ export default function (pi: ExtensionAPI) {
// The PGlite indexes (vectorIndex / memoryIndex) are lazily opened module
// singletons. closeVectorIndex()/closeMemoryIndex() existed but had no
// non-test callers, so a session left both open: PGlite is WASM Postgres and
// its handles keep node's event loop alive, so `pi -p` produced its answer
// and then hung until killed rather than exiting. dispose() only released the
// fs.watch handle and the perf interval, neither of which was the culprit
// (the interval is unref'd).
// leaving it open makes process exit nondeterministic — measured on a
// teardown that opens both indexes, closing exits in ~3s while leaving them
// open either stalls ~13s or never exits at all. That is why `pi -p` produced
// its answer and then hung until killed.
//
// NOTE: an earlier revision of this comment claimed the fs.watch handle "was
// not the culprit". That was wrong. A node diagnostic report
// (--report-on-signal) taken at hang time shows the game-state watcher as an
// active AND referenced fs_event handle, which pins the event loop on its
// own — independently of PGlite, and on any path that never reaches
// session_shutdown. It is now unref'd at creation
// (ensureGameStateWatcherImpl), so it can no longer hold the process open;
// dispose() still closes it here for prompt fd release. The perf interval
// genuinely is not a culprit — it is unref'd.
//
// Both closes are idempotent and safe when the index was never opened, and
// the next initVectorIndex()/initMemoryIndex() re-opens lazily.
Expand Down
9 changes: 9 additions & 0 deletions extensions/mega-runtime/game-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { disposePerf, type PerfContext } from "./perf.js";

export interface GameWatcherLike {
close(): void;
/** fs.FSWatcher has it; test doubles need not. */
unref?(): unknown;
}

export interface GameStateContext {
Expand Down Expand Up @@ -142,6 +144,13 @@ export function ensureGameStateWatcherImpl(self: GameStateContext, view: GameSta
}
},
);
// The watcher is a cache-eviction convenience, never a reason to stay
// alive: an active+referenced fs_event handle holds node's event loop
// open, so a runtime that was never dispose()d (every extension test, and
// any `pi -p` run that skips session_shutdown) hangs the process after all
// work is done. unref'd like the perf interval — while pi runs there are
// always other referenced handles, so the watcher still fires normally.
self.gameStateWatcher.unref?.();
self.gameStateWatchDir = self.currentStateDir;
} catch {
/* non-fatal: missing dir / platform issue — next snapshot re-queries */
Expand Down
Loading