fix: a bare script now exits cleanly after close() — release every process keep-alive

A consumer report on the GA pair: any minimal add/find/close script hangs
forever. Root cause was FOUR ref'd keep-alives brainy never released:

1+2. The global SIGTERM/SIGINT shutdown hooks (registered once at init) were
   anonymous and never removed — a process.on signal listener holds a ref'd
   signal handle. They are now named statics, deregistered when the LAST live
   instance closes (Brainy.instances also stopped leaking closed instances);
   a later init re-registers them.
3. UnifiedCache.startFairnessMonitor() created an interval it never stored,
   cleared, or unref'd — unclearable by construction. Now stored + unref'd
   (same posture as the existing memory-pressure timer): a cache monitor must
   never keep the host process alive.
4. brainy.close() never called VirtualFileSystem.close(), stranding the VFS
   background-maintenance interval and the PathResolver maintenance interval.
   close() now shuts the VFS down.

Proof: a bare script (init/add/close, no process.exit) exits 1 ms after
close() returns; before the fix it hung until killed. 3 new lifecycle tests
pin the hook semantics (once globally, survive while other instances live,
removed on last close, re-register after).
This commit is contained in:
David Snelling 2026-07-02 16:39:16 -07:00
parent ef2022ffd3
commit c540d63b69
3 changed files with 146 additions and 11 deletions

View file

@ -79,6 +79,7 @@ export class UnifiedCache implements CacheProvider {
private readonly memoryInfo: MemoryInfo
private readonly allocationStrategy: CacheAllocationStrategy
private memoryPressureCheckTimer: NodeJS.Timeout | null = null
private fairnessMonitorTimer: NodeJS.Timeout | null = null
private lastMemoryWarning = 0
constructor(config: UnifiedCacheConfig = {}) {
@ -313,12 +314,19 @@ export class UnifiedCache implements CacheProvider {
}
/**
* Fairness monitoring - prevent one type from hogging cache
* Fairness monitoring - prevent one type from hogging cache.
*
* The interval is unref'd: a background cache monitor must never keep the
* host process alive (this exact timer made bare scripts hang after
* `brain.close()` the loop could not drain while it stayed ref'd).
*/
private startFairnessMonitor(): void {
setInterval(() => {
this.fairnessMonitorTimer = setInterval(() => {
this.checkFairness()
}, this.config.fairnessCheckInterval!)
if (this.fairnessMonitorTimer.unref) {
this.fairnessMonitorTimer.unref()
}
}
private checkFairness(): void {