pgserve 2.2.3 → 2.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cluster.js +64 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgserve",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
4
4
  "description": "Embedded PostgreSQL server with true concurrent connections - zero config, auto-provision databases",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/cluster.js CHANGED
@@ -395,6 +395,51 @@ class ClusterRouter extends EventEmitter {
395
395
  }
396
396
 
397
397
 
398
+ /**
399
+ * Build a `backendExited` handler for cluster mode supervision.
400
+ *
401
+ * On unexpected exit (`expected: false`) — postgres SIGKILL'd, OOM-killed,
402
+ * segfaulted, etc. — the handler:
403
+ * 1. logs the exit code,
404
+ * 2. flips `shuttingDown` so the cluster.on('exit') worker-respawn path
405
+ * no longer forks new workers,
406
+ * 3. SIGTERMs every live worker (no point routing to a dead backend), and
407
+ * 4. calls `exitFn(1)` so the parent process supervisor restarts us.
408
+ *
409
+ * On clean exit (`expected: true`, initiated by `pgManager.stop()`) the
410
+ * handler is silent — the surrounding shutdown logic handles teardown.
411
+ *
412
+ * Exported so the supervision contract can be unit-tested without spawning
413
+ * a real cluster (the integration path is already covered for single-process
414
+ * mode by tests/wrapper-supervision.test.js).
415
+ *
416
+ * @param {object} args
417
+ * @param {Map<number, {kill: (sig: string) => void}>} args.workers - live worker registry
418
+ * @param {(v: boolean) => void} args.setShuttingDown - flips outer-scope `shuttingDown`
419
+ * @param {(code: number) => void} [args.exitFn=process.exit] - test seam
420
+ * @param {(...args: unknown[]) => void} [args.log=console.error] - test seam
421
+ * @returns {(info: {code: number, expected: boolean}) => void}
422
+ */
423
+ export function buildClusterSupervisionHandler({
424
+ workers,
425
+ setShuttingDown,
426
+ exitFn = process.exit,
427
+ log = console.error,
428
+ }) {
429
+ return ({ code, expected }) => {
430
+ if (expected) return;
431
+ log(
432
+ `[pgserve] postgres backend exited unexpectedly (code=${code}) in cluster mode; ` +
433
+ `the primary is exiting so a process supervisor can restart it.`
434
+ );
435
+ setShuttingDown(true);
436
+ for (const worker of workers.values()) {
437
+ try { worker.kill('SIGTERM'); } catch { /* worker may already be dead */ }
438
+ }
439
+ exitFn(1);
440
+ };
441
+ }
442
+
398
443
  /**
399
444
  * Start pgserve in cluster mode
400
445
  */
@@ -425,9 +470,27 @@ export async function startClusterServer(options = {}) {
425
470
  console.log(`[pgserve] Embedded PostgreSQL started`);
426
471
  console.log(`[pgserve] Socket: ${pgSocketPath || `TCP port ${pgPort}`}`);
427
472
 
473
+ // Track shutdown state and worker registry early so the supervision
474
+ // handler below can tear workers down on unexpected backend death.
475
+ let shuttingDown = false;
428
476
  const workers = new Map();
429
477
  const workerStats = new Map(); // Track stats from each worker
430
478
 
479
+ // Supervision: when the embedded postgres backend dies unexpectedly
480
+ // (SIGKILL/OOM/segfault — anything other than a clean stop()), exit the
481
+ // primary so a process supervisor (`genie serve`, pm2, systemd) restarts
482
+ // the cluster cleanly with a fresh backend. Without this, the primary
483
+ // keeps running with a zombie pgManager (socketDir nulled) and every
484
+ // worker fails StartupMessages with "Connection closed" forever while
485
+ // pm2 reports the process as healthy. Mirrors the single-process fix
486
+ // in bin/postgres-server.js (PgserveDaemon.on('backendDiedUnexpectedly'))
487
+ // — pgserve#45 only protected the daemon path, not cluster mode (default
488
+ // on multi-core systems).
489
+ pgManager.on('backendExited', buildClusterSupervisionHandler({
490
+ workers,
491
+ setShuttingDown: (v) => { shuttingDown = v; },
492
+ }));
493
+
431
494
  // Fork workers with PostgreSQL connection info.
432
495
  //
433
496
  // Pass through using AUTOPG_<X> (the primary names) so workers don't
@@ -456,8 +519,7 @@ export async function startClusterServer(options = {}) {
456
519
  workers.set(worker.id, worker);
457
520
  }
458
521
 
459
- // Track shutdown state to prevent worker restart during shutdown
460
- let shuttingDown = false;
522
+ // (shuttingDown declared above with the supervision handler.)
461
523
 
462
524
  // Restart dead workers (unless shutting down)
463
525
  cluster.on('exit', (worker, code, signal) => {