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 +14 -0
- package/package.json +1 -1
- package/src/postgres.js +46 -0
- package/tests/stale-postmaster-pid.test.js +85 -0
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
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
|
+
});
|