pgserve 2.0.3 → 2.0.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to `pgserve` are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
5
5
  to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 2.0.4
8
+
9
+ ### Fixed
10
+
11
+ - `_startPostgres()` now removes a stale `postmaster.pid` from the data
12
+ directory before spawning postgres. Previously, an unclean shutdown
13
+ (SIGKILL, machine reboot, OOM) left a `postmaster.pid` whose recorded
14
+ PID was no longer alive, and postgres refused to start with
15
+ `FATAL: lock file "postmaster.pid" already exists` on the next boot.
16
+ Operators had to `rm postmaster.pid` manually to recover. A live PID
17
+ is never touched, so a real concurrent postmaster still surfaces the
18
+ normal lock conflict. ([#46](https://github.com/namastexlabs/pgserve/pull/46),
19
+ fixes [#45](https://github.com/namastexlabs/pgserve/issues/45))
20
+
7
21
  ## 2.0.0 — Unreleased
8
22
 
9
23
  > The release date will replace "Unreleased" when the v2.0.0 release workflow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgserve",
3
- "version": "2.0.3",
3
+ "version": "2.0.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/postgres.js CHANGED
@@ -709,11 +709,57 @@ export class PostgresManager {
709
709
  }
710
710
  }
711
711
 
712
+ /**
713
+ * Detect and remove a stale postmaster.pid that postgres would otherwise
714
+ * refuse to start against. Stale = the PID written into the file is not
715
+ * alive on this host. Called at the top of _startPostgres so that crash
716
+ * / SIGKILL / unclean reboot recovery is automatic.
717
+ *
718
+ * Real running backends are NEVER touched — if the PID is alive we leave
719
+ * the file alone and let postgres surface its normal "lock file already
720
+ * exists" error so the operator sees the conflict.
721
+ */
722
+ async _ensureNoStalePostmasterLock() {
723
+ const pidFile = path.join(this.databaseDir, 'postmaster.pid');
724
+ let raw;
725
+ try {
726
+ raw = await fs.promises.readFile(pidFile, 'utf-8');
727
+ } catch (err) {
728
+ if (err.code === 'ENOENT') return;
729
+ throw err;
730
+ }
731
+ const firstLine = (raw.split('\n')[0] ?? '').trim();
732
+ const pid = Number.parseInt(firstLine, 10);
733
+ if (!Number.isInteger(pid) || pid <= 0) {
734
+ this.logger.warn(
735
+ { pidFile, firstLine },
736
+ 'postmaster.pid is unparseable; removing as stale'
737
+ );
738
+ await fs.promises.unlink(pidFile).catch(() => {});
739
+ return;
740
+ }
741
+ let alive = false;
742
+ try {
743
+ process.kill(pid, 0);
744
+ alive = true;
745
+ } catch (err) {
746
+ // EPERM = process exists but we can't signal it — still alive.
747
+ alive = err.code === 'EPERM';
748
+ }
749
+ if (alive) return;
750
+ this.logger.info(
751
+ { pidFile, stalePid: pid },
752
+ 'Removing stale postmaster.pid (PID not running) before postgres start'
753
+ );
754
+ await fs.promises.unlink(pidFile).catch(() => {});
755
+ }
756
+
712
757
  /**
713
758
  * Start the PostgreSQL server process
714
759
  * Uses Bun.spawn() for ~40% faster process startup
715
760
  */
716
761
  async _startPostgres() {
762
+ await this._ensureNoStalePostmasterLock();
717
763
  return new Promise((resolve, reject) => {
718
764
  // Build PostgreSQL arguments
719
765
  const pgArgs = [
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Stale postmaster.pid cleanup
3
+ *
4
+ * Verifies that PostgresManager._ensureNoStalePostmasterLock removes
5
+ * a postmaster.pid file whose recorded PID is no longer alive, and
6
+ * leaves alone a postmaster.pid whose recorded PID is alive.
7
+ *
8
+ * Regression coverage: postgres refuses to start when postmaster.pid
9
+ * exists, even if the writer crashed. After unclean shutdowns this
10
+ * required manual `rm` to recover.
11
+ */
12
+
13
+ import { PostgresManager } from '../src/postgres.js';
14
+ import { test, expect } from 'bun:test';
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import os from 'os';
18
+
19
+ function makeMgr(dataDir) {
20
+ const mgr = new PostgresManager({ dataDir });
21
+ mgr.databaseDir = dataDir;
22
+ mgr.logger = {
23
+ info: () => {},
24
+ warn: () => {},
25
+ error: () => {},
26
+ debug: () => {},
27
+ };
28
+ return mgr;
29
+ }
30
+
31
+ function makePidFile(dir, contents) {
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ const pidFile = path.join(dir, 'postmaster.pid');
34
+ fs.writeFileSync(pidFile, contents, 'utf-8');
35
+ return pidFile;
36
+ }
37
+
38
+ test('removes postmaster.pid when recorded PID is dead', async () => {
39
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-stale-'));
40
+ try {
41
+ // PID 999999999 will not exist on any sane system
42
+ const pidFile = makePidFile(dir, '999999999\n/some/data\n123\n');
43
+ const mgr = makeMgr(dir);
44
+ await mgr._ensureNoStalePostmasterLock();
45
+ expect(fs.existsSync(pidFile)).toBe(false);
46
+ } finally {
47
+ fs.rmSync(dir, { recursive: true, force: true });
48
+ }
49
+ });
50
+
51
+ test('keeps postmaster.pid when recorded PID is the current (alive) process', async () => {
52
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-alive-'));
53
+ try {
54
+ const pidFile = makePidFile(dir, `${process.pid}\n/some/data\n123\n`);
55
+ const mgr = makeMgr(dir);
56
+ await mgr._ensureNoStalePostmasterLock();
57
+ expect(fs.existsSync(pidFile)).toBe(true);
58
+ } finally {
59
+ fs.rmSync(dir, { recursive: true, force: true });
60
+ }
61
+ });
62
+
63
+ test('removes postmaster.pid when first line is unparseable', async () => {
64
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-garbage-'));
65
+ try {
66
+ const pidFile = makePidFile(dir, 'garbage\nnot-a-pid\n');
67
+ const mgr = makeMgr(dir);
68
+ await mgr._ensureNoStalePostmasterLock();
69
+ expect(fs.existsSync(pidFile)).toBe(false);
70
+ } finally {
71
+ fs.rmSync(dir, { recursive: true, force: true });
72
+ }
73
+ });
74
+
75
+ test('no-ops when postmaster.pid does not exist', async () => {
76
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-missing-'));
77
+ try {
78
+ const mgr = makeMgr(dir);
79
+ // Should resolve without throwing
80
+ await mgr._ensureNoStalePostmasterLock();
81
+ expect(fs.existsSync(path.join(dir, 'postmaster.pid'))).toBe(false);
82
+ } finally {
83
+ fs.rmSync(dir, { recursive: true, force: true });
84
+ }
85
+ });