@volter/world-runtime 3.0.163 → 3.0.165
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/dist/src/ancestry.js +2 -2
- package/dist/src/app-url.d.ts +5 -1
- package/dist/src/app-url.js +14 -1
- package/dist/src/branch.js +28 -8
- package/dist/src/browser-files.d.ts +5 -0
- package/dist/src/browser-files.js +14 -0
- package/dist/src/browsers.d.ts +2 -3
- package/dist/src/browsers.js +4 -4
- package/dist/src/catalog.d.ts +2 -1
- package/dist/src/catalog.js +9 -3
- package/dist/src/changeset.js +1 -1
- package/dist/src/cli-commands.js +71 -18
- package/dist/src/cli-load-clock.d.ts +1 -0
- package/dist/src/cli-load-clock.js +2 -0
- package/dist/src/cli.js +5 -1
- package/dist/src/clickhouse-backing.d.ts +2 -0
- package/dist/src/clickhouse-backing.js +145 -0
- package/dist/src/command-lifetime.d.mts +1 -1
- package/dist/src/command-lifetime.mjs +5 -2
- package/dist/src/configs.js +1 -1
- package/dist/src/consumers.js +1 -1
- package/dist/src/covers.d.ts +4 -2
- package/dist/src/covers.js +8 -4
- package/dist/src/go-mysql-binding.d.ts +19 -0
- package/dist/src/go-mysql-binding.js +51 -0
- package/dist/src/guest-processes.d.mts +1 -1
- package/dist/src/guest-processes.mjs +22 -14
- package/dist/src/host-cli.js +8 -1
- package/dist/src/host-ready.d.ts +16 -2
- package/dist/src/host-ready.js +66 -39
- package/dist/src/host-worker.js +7 -1
- package/dist/src/host.d.ts +26 -0
- package/dist/src/host.js +93 -3
- package/dist/src/infra-definition.d.ts +35 -0
- package/dist/src/infra-definition.js +146 -0
- package/dist/src/infra.d.ts +3 -0
- package/dist/src/infra.js +375 -105
- package/dist/src/init.d.ts +46 -0
- package/dist/src/init.js +446 -19
- package/dist/src/lifecycle-record.d.ts +5 -1
- package/dist/src/lifecycle-record.js +27 -4
- package/dist/src/local-branches.js +1 -1
- package/dist/src/mariadb-bootstrap.d.ts +32 -0
- package/dist/src/mariadb-bootstrap.js +90 -0
- package/dist/src/mariadb-supplier.d.ts +23 -0
- package/dist/src/mariadb-supplier.js +215 -0
- package/dist/src/mongodb-backing.d.ts +4 -0
- package/dist/src/mongodb-backing.js +71 -1
- package/dist/src/native-mariadb-backing.d.ts +13 -0
- package/dist/src/native-mariadb-backing.js +512 -0
- package/dist/src/native-postgres-backing.d.ts +4 -10
- package/dist/src/native-postgres-backing.js +428 -43
- package/dist/src/native-server-inputs.d.ts +16 -0
- package/dist/src/native-server-inputs.js +204 -0
- package/dist/src/origin.js +3 -3
- package/dist/src/original-inputs.d.ts +14 -0
- package/dist/src/original-inputs.js +84 -0
- package/dist/src/pack-facts.d.ts +9 -1
- package/dist/src/pglite-backing.d.ts +41 -17
- package/dist/src/pglite-backing.js +213 -79
- package/dist/src/pglite-host.mjs +482 -10
- package/dist/src/process-groups.d.mts +6 -1
- package/dist/src/process-groups.d.ts +2 -1
- package/dist/src/process-groups.js +1 -1
- package/dist/src/process-groups.mjs +82 -15
- package/dist/src/process-signal.d.mts +2 -2
- package/dist/src/process-signal.mjs +29 -9
- package/dist/src/project-inspect.d.ts +12 -0
- package/dist/src/project-inspect.js +41 -4
- package/dist/src/proxy-env.d.ts +34 -0
- package/dist/src/proxy-env.js +114 -0
- package/dist/src/redirect-proxy.d.ts +1 -31
- package/dist/src/redirect-proxy.js +3 -112
- package/dist/src/redis-backing.d.ts +6 -4
- package/dist/src/redis-backing.js +20 -168
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.js +503 -218
- package/dist/src/schema.d.ts +1 -0
- package/dist/src/schema.js +5 -3
- package/dist/src/served-world.js +3 -3
- package/dist/src/twin-backing.d.ts +47 -0
- package/dist/src/twin-backing.js +343 -0
- package/dist/src/up-cli.js +56 -3
- package/package.json +3 -3
- package/src/ancestry.ts +2 -2
- package/src/app-url.ts +9 -2
- package/src/branch.ts +26 -8
- package/src/browser-files.ts +15 -0
- package/src/browsers.ts +4 -4
- package/src/catalog.ts +9 -4
- package/src/changeset.ts +1 -1
- package/src/cli-commands.ts +85 -18
- package/src/cli-load-clock.ts +2 -0
- package/src/cli.ts +5 -1
- package/src/clickhouse-backing.ts +70 -0
- package/src/command-lifetime.d.mts +1 -1
- package/src/command-lifetime.mjs +5 -2
- package/src/configs.ts +1 -1
- package/src/consumers.ts +1 -1
- package/src/covers.ts +9 -4
- package/src/go-mysql-binding.ts +49 -0
- package/src/guest-processes.d.mts +1 -1
- package/src/guest-processes.mjs +22 -14
- package/src/host-cli.ts +7 -1
- package/src/host-ready.ts +62 -31
- package/src/host-worker.ts +6 -2
- package/src/host.ts +88 -6
- package/src/infra-definition.ts +135 -0
- package/src/infra.ts +250 -45
- package/src/init.ts +417 -18
- package/src/lifecycle-record.ts +18 -4
- package/src/local-branches.ts +1 -1
- package/src/mariadb-bootstrap.ts +107 -0
- package/src/mariadb-supplier.ts +199 -0
- package/src/mongodb-backing.ts +59 -1
- package/src/native-mariadb-backing.ts +418 -0
- package/src/native-postgres-backing.ts +353 -41
- package/src/native-server-inputs.ts +169 -0
- package/src/origin.ts +3 -3
- package/src/original-inputs.ts +72 -0
- package/src/pack-facts.ts +1 -1
- package/src/pglite-backing.ts +192 -80
- package/src/pglite-host.mjs +482 -10
- package/src/process-groups.d.mts +6 -1
- package/src/process-groups.mjs +82 -15
- package/src/process-groups.ts +2 -1
- package/src/process-signal.d.mts +2 -2
- package/src/process-signal.mjs +29 -9
- package/src/project-inspect.ts +37 -4
- package/src/proxy-env.ts +128 -0
- package/src/redirect-proxy.ts +3 -125
- package/src/redis-backing.ts +19 -136
- package/src/runtime.ts +342 -107
- package/src/schema.ts +5 -4
- package/src/served-world.ts +3 -3
- package/src/twin-backing.ts +300 -0
- package/src/up-cli.ts +52 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// belong to the infrastructure service; a recipe never operates postgres itself.
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
-
import { accessSync, closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { accessSync, chmodSync, chownSync, copyFileSync, lchownSync, lstatSync, closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { connect } from 'node:net';
|
|
7
7
|
import { runtimeManifest } from "./package-manifest.js";
|
|
8
8
|
import { delimiter, dirname, join, resolve } from 'node:path';
|
|
@@ -10,8 +10,94 @@ import { commandLifetime, COMMAND_GRACE_MS, CommandRetirementError } from "./com
|
|
|
10
10
|
import { survivingOwnedGroups } from "./process-groups.js";
|
|
11
11
|
import { publishProcessReceipt, readProcessReceipt, readSpawnReceipt, spawnReceiptPath, verifySpawnIdentity, signalOwnedProcess } from "./process-receipt.js";
|
|
12
12
|
import { signalChild, childIdentity } from './process-signal.mjs';
|
|
13
|
+
import { worldEvent } from './process-groups.mjs';
|
|
13
14
|
import { errorMessage } from "./error-message.js";
|
|
14
|
-
import { PREPARED_DIRECTORY, databasePrepareOf, expectedMigration, forgetStaleMigration, keepFailureEvidence, runDatabasePrepare, takePreparedCluster } from "./pglite-backing.js";
|
|
15
|
+
import { PREPARED_DIRECTORY, PREPARE_STATE_DIRECTORY, packageScriptPath, databasePrepareOf, environmentFile, expectedMigration, forgetStaleMigration, keepFailureEvidence, migrationInputDigests, preparedStore, preparedStoreStamp, preparedWorldServices, runDatabasePrepare, takePreparedCluster } from "./pglite-backing.js";
|
|
16
|
+
/** OS identity, distinct from PostgreSQL's configured SQL role. No in-process privilege mutation. */
|
|
17
|
+
function postgresRunAs() {
|
|
18
|
+
const uid = process.getuid?.();
|
|
19
|
+
const gid = process.getgid?.();
|
|
20
|
+
if (uid === undefined || gid === undefined)
|
|
21
|
+
throw new Error('native PostgreSQL requires a numeric OS process identity');
|
|
22
|
+
if (uid !== 0)
|
|
23
|
+
return { uid, gid };
|
|
24
|
+
const accounts = readFileSync('/etc/passwd', 'utf8').split('\n').map(line => line.split(':'));
|
|
25
|
+
for (const name of ['postgres', 'nobody']) {
|
|
26
|
+
const account = accounts.find(fields => fields[0] === name && /^\d+$/.test(fields[2] ?? '') && /^\d+$/.test(fields[3] ?? '')
|
|
27
|
+
&& Number(fields[2]) > 0 && Number(fields[2]) < 0xffffffff && Number(fields[3]) > 0 && Number(fields[3]) < 0xffffffff);
|
|
28
|
+
if (account)
|
|
29
|
+
return { uid: Number(account[2]), gid: Number(account[3]) };
|
|
30
|
+
}
|
|
31
|
+
throw new Error('native PostgreSQL started by root requires an installed non-root postgres or nobody account');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The prepared cluster copied as the server's own (S2 R15): each directory, file and link made, its mode taken from
|
|
35
|
+
* the source as cpSync takes it, and given to the server's uid and gid as it is made. A cpSync and then
|
|
36
|
+
* ownPostgresCluster's walk asked the tree for every one of the cluster's files twice more (Cal in a tab: the
|
|
37
|
+
* placing step 170 ms, each ask a kernel request from up's process).
|
|
38
|
+
*/
|
|
39
|
+
function copyClusterOwned(runAs) {
|
|
40
|
+
const copy = (from, to) => {
|
|
41
|
+
const stat = lstatSync(from);
|
|
42
|
+
if (stat.isSymbolicLink()) {
|
|
43
|
+
symlinkSync(readlinkSync(from), to);
|
|
44
|
+
lchownSync(to, runAs.uid, runAs.gid);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (stat.isDirectory()) {
|
|
48
|
+
mkdirSync(to);
|
|
49
|
+
for (const name of readdirSync(from))
|
|
50
|
+
copy(join(from, name), join(to, name));
|
|
51
|
+
}
|
|
52
|
+
else if (stat.isFile())
|
|
53
|
+
copyFileSync(from, to);
|
|
54
|
+
else
|
|
55
|
+
throw new Error(`native PostgreSQL's prepared cluster holds a ${stat.isFIFO() ? 'FIFO' : 'special file'}, which is not copied: ${from}`);
|
|
56
|
+
chmodSync(to, stat.mode & 0o7777);
|
|
57
|
+
chownSync(to, runAs.uid, runAs.gid);
|
|
58
|
+
};
|
|
59
|
+
return copy;
|
|
60
|
+
}
|
|
61
|
+
/** Assign only the stopped World's cluster, including a cluster placed from preparation. Never follow links. */
|
|
62
|
+
function ownPostgresCluster(data, runAs) {
|
|
63
|
+
// What the walk did, said by the start's step (S2 R71): every entry is looked at, and only another owner's changed.
|
|
64
|
+
const counts = { walked: 0, changed: 0 };
|
|
65
|
+
const visit = (path) => {
|
|
66
|
+
const stat = lstatSync(path);
|
|
67
|
+
counts.walked += 1;
|
|
68
|
+
if (stat.uid !== runAs.uid || stat.gid !== runAs.gid) {
|
|
69
|
+
counts.changed += 1;
|
|
70
|
+
if (stat.isSymbolicLink())
|
|
71
|
+
lchownSync(path, runAs.uid, runAs.gid);
|
|
72
|
+
else
|
|
73
|
+
chownSync(path, runAs.uid, runAs.gid);
|
|
74
|
+
}
|
|
75
|
+
if (stat.isDirectory())
|
|
76
|
+
for (const name of readdirSync(path))
|
|
77
|
+
visit(join(path, name));
|
|
78
|
+
};
|
|
79
|
+
visit(data);
|
|
80
|
+
return counts;
|
|
81
|
+
}
|
|
82
|
+
/** The service may traverse its own World bookkeeping directories, without reading their private receipts. */
|
|
83
|
+
function postgresDirectorySearch(root, env, runAs) {
|
|
84
|
+
const directories = [root];
|
|
85
|
+
// The live lifecycle creates instance/data as 0700. Only its recorded instance is granted search;
|
|
86
|
+
// preparation has a different root and never changes the application's directory above it.
|
|
87
|
+
if (env.VOLTER_WORLD_INSTANCE && env.VOLTER_WORLD_DATA
|
|
88
|
+
&& resolve(env.VOLTER_WORLD_DATA) === resolve(root)
|
|
89
|
+
&& dirname(resolve(env.VOLTER_WORLD_INSTANCE)) === dirname(resolve(root)))
|
|
90
|
+
directories.push(dirname(resolve(root)));
|
|
91
|
+
for (const directory of directories) {
|
|
92
|
+
const stat = lstatSync(directory);
|
|
93
|
+
if (!stat.isDirectory())
|
|
94
|
+
throw new Error(`native PostgreSQL directory is not a directory: ${directory}`);
|
|
95
|
+
if ((stat.uid === runAs.uid && (stat.mode & 0o100)) || (stat.gid === runAs.gid && (stat.mode & 0o010)) || (stat.mode & 0o001))
|
|
96
|
+
continue;
|
|
97
|
+
chownSync(directory, stat.uid, runAs.gid);
|
|
98
|
+
chmodSync(directory, (stat.mode & 0o7777) | 0o010);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
15
101
|
const starts = new Map();
|
|
16
102
|
const wait = (ms) => new Promise(done => setTimeout(done, ms));
|
|
17
103
|
const recordPath = (root) => join(root, 'native-postgres.owner.json');
|
|
@@ -43,11 +129,73 @@ function ownership(root) {
|
|
|
43
129
|
}
|
|
44
130
|
return { present: records.some(row => row.present) || spawn.present, held: values.find(row => row.pid === spawn.value?.pid) ?? values[0], identity: spawn.value, values, errors };
|
|
45
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Whether postmaster.pid names the postmaster this lifecycle holds. The postmaster CREATES that file and then writes
|
|
134
|
+
* it, and removes it as it exits: a reader meets it empty, part written, or gone. Those are not a cluster owned by
|
|
135
|
+
* another; they are a file not written yet (false: ask again). A mismatch is four COMPLETE lines of which the pid,
|
|
136
|
+
* the data directory or the port differs, and it is refused with what was read beside what was held.
|
|
137
|
+
*/
|
|
46
138
|
function verifyOwner(held) {
|
|
47
|
-
|
|
139
|
+
let text;
|
|
140
|
+
try {
|
|
141
|
+
text = readFileSync(join(held.data, 'postmaster.pid'), 'utf8');
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const lines = text.split('\n');
|
|
147
|
+
// Lines 1-4 are pid, data directory, start time, port; each is complete when a line follows it.
|
|
148
|
+
if (lines.length < 5 || lines.slice(0, 4).some((line) => line.trim() === ''))
|
|
149
|
+
return false;
|
|
48
150
|
if (Number(lines[0]) !== held.pid || resolve(lines[1]) !== held.data || Number(lines[3]) !== held.port) {
|
|
49
|
-
|
|
151
|
+
// A FILE LEFT BY A POSTMASTER THAT IS GONE IS STALE, NOT ANOTHER OWNER. After a kill or a crash the file stays,
|
|
152
|
+
// four complete lines naming a pid with no process; the postmaster just started reads it, finds that, and
|
|
153
|
+
// writes its own. Read in between, it names neither this postmaster nor a live one: not written yet. Only a
|
|
154
|
+
// file naming a LIVE process that is not this one, or this one with another directory or port, is a mismatch.
|
|
155
|
+
const named = Number(lines[0]);
|
|
156
|
+
if (named !== held.pid && Number.isInteger(named) && named > 0) {
|
|
157
|
+
let gone = false;
|
|
158
|
+
try {
|
|
159
|
+
process.kill(named, 0);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
gone = error.code === 'ESRCH';
|
|
163
|
+
}
|
|
164
|
+
if (gone)
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`native postgres ownership does not match the cluster; teardown refused (postmaster.pid says pid ${lines[0]}, data ${lines[1]}, port ${lines[3]}; this lifecycle holds pid ${held.pid}, data ${held.data}, port ${held.port})`);
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* THE POSTMASTER'S STOP. PostgreSQL's contract for a stop is one signal to the postmaster (SIGINT, its fast
|
|
173
|
+
* shutdown; what pg_ctl sends): it ends its backends itself, writes the shutdown checkpoint and removes
|
|
174
|
+
* postmaster.pid. A signal to its GROUP reaches the checkpointer and the writers too, which are not the ones to
|
|
175
|
+
* tell. Between the signal and its exit the postmaster is writing the cluster down, and how long that takes is the
|
|
176
|
+
* size of what is dirty, so it is awaited on the lifecycle's own budget (READY_TIMEOUT_MS), never a command's
|
|
177
|
+
* grace: a SIGKILL in the middle of the checkpoint leaves a cluster with its pid file, which nothing takes. Only
|
|
178
|
+
* past that budget is the group killed, and that is said as a kill. The stop, the exit, and the time between are
|
|
179
|
+
* events in the World's log.
|
|
180
|
+
*/
|
|
181
|
+
async function stopPostmaster(pid, deliver, by, data) {
|
|
182
|
+
if (!liveGroup(pid))
|
|
183
|
+
return;
|
|
184
|
+
const asked = Date.now();
|
|
185
|
+
worldEvent({ event: 'world.service.stop', service: 'postgres', by, signal: 'SIGINT', to: 'pid', pid });
|
|
186
|
+
await deliver('SIGINT', false);
|
|
187
|
+
const deadline = asked + READY_TIMEOUT_MS;
|
|
188
|
+
while (liveGroup(pid) && Date.now() < deadline)
|
|
189
|
+
await wait(100);
|
|
190
|
+
if (!liveGroup(pid)) {
|
|
191
|
+
worldEvent({ event: 'world.service.exited', service: 'postgres', by, pid, shutdownMs: Date.now() - asked, ...(data ? { pidFileLeft: existsSync(join(data, 'postmaster.pid')) } : {}) });
|
|
192
|
+
return;
|
|
50
193
|
}
|
|
194
|
+
worldEvent({ event: 'world.service.killed', service: 'postgres', by, pid, signal: 'SIGKILL', to: 'group', afterMs: Date.now() - asked, doing: 'its shutdown had not ended within the lifecycle budget; the cluster is left with its pid file and is not taken' });
|
|
195
|
+
await deliver('SIGKILL', true);
|
|
196
|
+
const confirm = Date.now() + COMMAND_GRACE_MS;
|
|
197
|
+
while (liveGroup(pid) && Date.now() < confirm)
|
|
198
|
+
await wait(20);
|
|
51
199
|
}
|
|
52
200
|
const liveGroup = (pid) => pid > 0 && survivingOwnedGroups([pid]).length > 0;
|
|
53
201
|
function removeReceipts(root) {
|
|
@@ -73,6 +221,12 @@ function removeReceipts(root) {
|
|
|
73
221
|
if (errors.length)
|
|
74
222
|
throw new AggregateError(errors, 'native postgres retired but receipt cleanup failed');
|
|
75
223
|
}
|
|
224
|
+
async function retirePostmaster(identity, by, data) {
|
|
225
|
+
await stopPostmaster(identity.pid, (signal, group) => signalOwnedProcess(identity, signal, group, by), by, data);
|
|
226
|
+
if (liveGroup(identity.pid))
|
|
227
|
+
throw new Error(`native postgres group ${identity.pid} retirement unconfirmed; ownership retained`);
|
|
228
|
+
}
|
|
229
|
+
/** A helper this backing started (psql, createdb, initdb): it holds no state, and keeps a command's grace. */
|
|
76
230
|
async function retireIdentity(identity) {
|
|
77
231
|
const { pid } = identity;
|
|
78
232
|
for (const signal of ['SIGINT', 'SIGKILL']) {
|
|
@@ -87,21 +241,18 @@ async function retireIdentity(identity) {
|
|
|
87
241
|
throw new Error(`native postgres group ${pid} retirement unconfirmed; ownership retained`);
|
|
88
242
|
}
|
|
89
243
|
/** A retained child is ownership even before postmaster.pid or the ready receipt exists. */
|
|
90
|
-
async function retireStarted(started) {
|
|
244
|
+
async function retireStarted(started, by = 'down (this process started it)') {
|
|
91
245
|
const { child, owned } = started;
|
|
92
246
|
child.ref();
|
|
93
|
-
|
|
247
|
+
if (owned.pid > 0) {
|
|
248
|
+
await stopPostmaster(owned.pid, async (signal, group) => signalChild(child, signal, group, by)
|
|
249
|
+
|| (liveGroup(owned.pid) ? signalOwnedProcess(started.identity ?? await started.birth, signal, group, by) : false), by, owned.data);
|
|
250
|
+
}
|
|
251
|
+
const deadline = Date.now() + COMMAND_GRACE_MS;
|
|
252
|
+
while (Date.now() < deadline) {
|
|
94
253
|
if (started.closed && !liveGroup(owned.pid))
|
|
95
254
|
return;
|
|
96
|
-
|
|
97
|
-
await signalOwnedProcess(started.identity ?? await started.birth, signal, true);
|
|
98
|
-
}
|
|
99
|
-
const deadline = Date.now() + COMMAND_GRACE_MS;
|
|
100
|
-
while (Date.now() < deadline) {
|
|
101
|
-
if (started.closed && !liveGroup(owned.pid))
|
|
102
|
-
return;
|
|
103
|
-
await wait(20);
|
|
104
|
-
}
|
|
255
|
+
await wait(20);
|
|
105
256
|
}
|
|
106
257
|
throw new Error(`native postgres child ${owned.pid} retirement unconfirmed; ownership retained`);
|
|
107
258
|
}
|
|
@@ -144,12 +295,12 @@ function logTail(log) {
|
|
|
144
295
|
}
|
|
145
296
|
}
|
|
146
297
|
/** Every helper joins exit and its private group, including spawn errors and abort. */
|
|
147
|
-
async function tool(command, args, env, deadline, root, signal) {
|
|
298
|
+
async function tool(command, args, env, deadline, root, runAs, signal) {
|
|
148
299
|
signal?.throwIfAborted();
|
|
149
300
|
const remaining = deadline - Date.now();
|
|
150
301
|
if (remaining <= 0)
|
|
151
302
|
throw new Error(`native postgres readiness deadline elapsed before ${command}`);
|
|
152
|
-
const child = spawn(command, args, { env, detached: true, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
303
|
+
const child = spawn(command, args, { env, ...runAs, detached: true, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
153
304
|
const lifetime = commandLifetime(child, signal);
|
|
154
305
|
let output = '';
|
|
155
306
|
let errorOutput = '';
|
|
@@ -265,6 +416,17 @@ export function nativePostgresTools(env) {
|
|
|
265
416
|
}
|
|
266
417
|
return undefined;
|
|
267
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* How a cluster left running is made durable before its recovery: one syncfs of its file system, where PostgreSQL's
|
|
421
|
+
* default opens and fsyncs every file under the data directory. A World's postmaster is not stopped when its tab
|
|
422
|
+
* closes, so a returning visit recovers: the Umami f7b17202 pair's second visit spent 652 ms in the startup process
|
|
423
|
+
* (3,078 opens, 1,496 fsyncs, 244 directory reads), where the first's took 77 ms, and was ready at check 8, not 5
|
|
424
|
+
* (S2 L1). syncfs is Linux's (PostgreSQL builds it where the C library has it: glibc, and the musl the tab's postgres
|
|
425
|
+
* is built with); macOS has none, and an option its build lacks is refused at start, so only Linux asks for it.
|
|
426
|
+
*/
|
|
427
|
+
function recoverySync() {
|
|
428
|
+
return process.platform === 'linux' ? ['-c', 'recovery_init_sync_method=syncfs'] : [];
|
|
429
|
+
}
|
|
268
430
|
/** initdb's arguments after `-D`, the same at up and at prepare, and so in the stamp. */
|
|
269
431
|
function initdbArguments(user) {
|
|
270
432
|
return [`--username=${user}`, '--auth=trust', '--no-locale', '--encoding=UTF8'];
|
|
@@ -329,6 +491,171 @@ function nativeStampMatches(text, expected) {
|
|
|
329
491
|
* step runs against that postmaster before its stop, and a step that fails leaves no cluster. The image build runs it with the programs the tab
|
|
330
492
|
* runs, under the tab's kernel, so the cluster is the one a first boot there would write.
|
|
331
493
|
*/
|
|
494
|
+
/** Every log file under a state directory, as one text with each file's path before its lines; empty when none. */
|
|
495
|
+
function worldLogs(state) {
|
|
496
|
+
const out = [];
|
|
497
|
+
const walk = (directory) => {
|
|
498
|
+
let names;
|
|
499
|
+
try {
|
|
500
|
+
names = readdirSync(directory).sort();
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
for (const name of names) {
|
|
506
|
+
const path = join(directory, name);
|
|
507
|
+
let stats;
|
|
508
|
+
try {
|
|
509
|
+
stats = statSync(path);
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (stats.isDirectory()) {
|
|
515
|
+
if (name !== 'native-postgres-data' && name !== 'node_modules')
|
|
516
|
+
walk(path);
|
|
517
|
+
}
|
|
518
|
+
else if (stats.isFile() && /\.log$/.test(name) && stats.size <= 8 * 1024 * 1024) {
|
|
519
|
+
try {
|
|
520
|
+
out.push(`==> ${path} <==\n${readFileSync(path, 'utf8')}`);
|
|
521
|
+
}
|
|
522
|
+
catch { /* a log that cannot be read is left out */ }
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
walk(state);
|
|
527
|
+
return out.join('\n');
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* A managed store, other than the cluster, that keeps its bytes under the World's data and is staged with it: the
|
|
531
|
+
* tree of a service a twin serves, which twin-backing.ts names `<kind>-twin` (redis-twin).
|
|
532
|
+
*/
|
|
533
|
+
const stagedStore = (name) => /^[a-z0-9]+-twin$/.test(name);
|
|
534
|
+
/** The state directory a preparation with the World up boots its disposable instance in, beside the World's own. */
|
|
535
|
+
/**
|
|
536
|
+
* A cluster migrated by a step that needs the World (`runtime.database.prepare.with: "world"`): a migration that
|
|
537
|
+
* talks to a service the World supplies (a cache it flushes, a queue it registers with, a vendor it reads) cannot
|
|
538
|
+
* run against a bare postmaster. The World is brought up whole, as `run` brings it up, in a state directory of its
|
|
539
|
+
* own beside the World's (`.volter-prepare`), with no prepared cluster to take, so its managed PostgreSQL is made
|
|
540
|
+
* new by initdb; the step is the World's foreground command, in the app's tree, with the image's environment under
|
|
541
|
+
* the World's own (the names its twins issued and its infrastructure discovered); the World then goes down, which
|
|
542
|
+
* stops the postmaster cleanly. The stopped cluster is moved to the World's prepared place and stamped with the
|
|
543
|
+
* step, its inputs' digests and the twins that were up. Each other managed store that keeps its bytes under the
|
|
544
|
+
* World's data (the tree of each service a twin serves, `<kind>-twin`: the Redis twin's root) is staged beside the cluster with the same migration in a
|
|
545
|
+
* stamp of its own, since what the step wrote there is the World's prepared state as much as its tables are.
|
|
546
|
+
* Everything else that instance wrote (a twin's own state, its logs) is named in the note and removed with the
|
|
547
|
+
* state directory. A step that fails leaves no cluster, keeps the step's whole output where a failed preparation
|
|
548
|
+
* keeps its evidence, and says so.
|
|
549
|
+
*/
|
|
550
|
+
async function prepareWithWorld(step, definition, worldConfig, tools, env, launch) {
|
|
551
|
+
const appRoot = env.VOLTER_WORLD_APP_ROOT;
|
|
552
|
+
if (!appRoot)
|
|
553
|
+
throw new Error('the World declares runtime.database.prepare: VOLTER_WORLD_APP_ROOT must name the app image\'s root');
|
|
554
|
+
const root = dirname(worldConfig);
|
|
555
|
+
const { directory, stamp } = nativePreparedCluster(worldConfig);
|
|
556
|
+
const state = join(root, PREPARE_STATE_DIRECTORY);
|
|
557
|
+
rmSync(directory, { recursive: true, force: true });
|
|
558
|
+
rmSync(stamp, { force: true });
|
|
559
|
+
rmSync(state, { recursive: true, force: true });
|
|
560
|
+
// The other stores a step run with the World up may write, staged beside the cluster (architecture, "A step may
|
|
561
|
+
// need the World"): each by the name of its directory under the World's data.
|
|
562
|
+
const previouslyStaged = () => { try {
|
|
563
|
+
return readdirSync(dirname(directory)).filter(stagedStore);
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
return [];
|
|
567
|
+
} };
|
|
568
|
+
const unstage = () => { for (const name of previouslyStaged()) {
|
|
569
|
+
const store = preparedStore(worldConfig, name);
|
|
570
|
+
rmSync(store.directory, { recursive: true, force: true });
|
|
571
|
+
rmSync(store.stamp, { force: true });
|
|
572
|
+
} };
|
|
573
|
+
unstage();
|
|
574
|
+
const cwd = join(appRoot, step.cwd);
|
|
575
|
+
const { user, database } = settings(definition);
|
|
576
|
+
// The World's run reads this process's environment: the preparation's own (its PostgreSQL programs, the app's
|
|
577
|
+
// root), the image's values over it as for a step run alone, and the state directory. This process is the
|
|
578
|
+
// preparation and nothing else, so its environment is the preparation's to set.
|
|
579
|
+
const { PATH: _imagePath, ...imageValues } = env.VOLTER_WORLD_APP_ENV ? environmentFile(env.VOLTER_WORLD_APP_ENV) : {};
|
|
580
|
+
const before = { ...process.env };
|
|
581
|
+
for (const [name, value] of Object.entries({ ...env, ...imageValues, VOLTER_STATE_DIR: PREPARE_STATE_DIRECTORY }))
|
|
582
|
+
if (value !== undefined)
|
|
583
|
+
process.env[name] = value;
|
|
584
|
+
// The step is a package script's command here too, and is found where the application's own start finds it
|
|
585
|
+
// (pglite-backing.ts runDatabasePrepare): each node_modules/.bin from its working directory upward, before PATH.
|
|
586
|
+
process.env.PATH = [...packageScriptPath(cwd, appRoot), process.env.PATH ?? ''].filter(Boolean).join(delimiter);
|
|
587
|
+
delete process.env.VOLTER_WORLD_CONFIG;
|
|
588
|
+
const names = join(state, 'prepare.env');
|
|
589
|
+
let moved = false;
|
|
590
|
+
try {
|
|
591
|
+
const { runWorldOwned } = await import("./runtime.js");
|
|
592
|
+
let ran;
|
|
593
|
+
try {
|
|
594
|
+
ran = await runWorldOwned(worldConfig, step.command, { root, cwd, owner: 'database-prepare', envFile: names, forceEnvOutput: true }, launch.signal);
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
throw new Error(`the World's database prepare step (${step.command.join(' ')}), run with the World up, did not complete: ${errorMessage(error)}`, { cause: error });
|
|
598
|
+
}
|
|
599
|
+
const log = ran.outcome.log, output = log && existsSync(log) ? readFileSync(log, 'utf8') : '';
|
|
600
|
+
if (ran.exitCode !== 0) {
|
|
601
|
+
throw new Error(`the World's database prepare step (${step.command.join(' ')}), run with the World up, exited ${ran.exitCode}\n${output.trim().split('\n').slice(-40).join('\n')}${keepFailureEvidence('database-prepare-step-output', output, env)}`);
|
|
602
|
+
}
|
|
603
|
+
const data = join(ran.instance.dirs.data, 'native-postgres-data');
|
|
604
|
+
if (!existsSync(join(data, 'PG_VERSION')))
|
|
605
|
+
throw new Error(`the World came up without a native PostgreSQL cluster at ${data}: a step declared with: "world" is prepared on the native backing only`);
|
|
606
|
+
if (existsSync(join(data, 'postmaster.pid')))
|
|
607
|
+
throw new Error('the World went down and left its postmaster\'s pid file: the cluster was not stopped cleanly and is not taken');
|
|
608
|
+
const given = existsSync(names) ? readFileSync(names, 'utf8').split('\n').map((line) => /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=/.exec(line)?.[1]).filter((name) => name !== undefined) : [];
|
|
609
|
+
const world = preparedWorldServices(worldConfig);
|
|
610
|
+
mkdirSync(dirname(directory), { recursive: true });
|
|
611
|
+
renameSync(data, directory);
|
|
612
|
+
moved = true;
|
|
613
|
+
const migration = { command: [...step.command], inputs: migrationInputDigests(cwd, step.inputs), world };
|
|
614
|
+
const staged = [];
|
|
615
|
+
for (const name of readdirSync(ran.instance.dirs.data).filter(stagedStore).sort()) {
|
|
616
|
+
const from = join(ran.instance.dirs.data, name), store = preparedStore(worldConfig, name);
|
|
617
|
+
if (!statSync(from).isDirectory() || !readdirSync(from).length)
|
|
618
|
+
continue;
|
|
619
|
+
// The twin that served this root is gone when its pid file is: the World's down waits for it and removes the
|
|
620
|
+
// file (twin-backing.ts). One left behind is a twin that may still be writing, as a postmaster's is above.
|
|
621
|
+
const left = readdirSync(ran.instance.dirs.data).filter((entry) => entry.startsWith(`${name}-`) && entry.endsWith('.pid'));
|
|
622
|
+
if (left.length)
|
|
623
|
+
throw new Error(`the World went down and left ${left.join(', ')}: the ${name} was not stopped cleanly and its root is not staged`);
|
|
624
|
+
renameSync(from, store.directory);
|
|
625
|
+
writeFileSync(store.stamp, preparedStoreStamp(migration));
|
|
626
|
+
staged.push(name);
|
|
627
|
+
}
|
|
628
|
+
// Everything else the instance left under its data, by name: not staged, and gone with the state directory.
|
|
629
|
+
const notStaged = readdirSync(ran.instance.dirs.data).filter((name) => !/\.(?:log|pid|json)$/.test(name) && !/^(?:logs?|run)$/.test(name)).sort();
|
|
630
|
+
// The cluster's stamp is written LAST: a preparation killed before here leaves a cluster nothing takes, never a
|
|
631
|
+
// stamped cluster whose stores did not arrive.
|
|
632
|
+
writeFileSync(stamp, `${JSON.stringify(nativeStamp(tools, user, database, true, migration))}\n`);
|
|
633
|
+
launch.note?.(`prepared stores beside the cluster: ${staged.join(', ') || 'none'}${notStaged.length ? `; left under the instance's data and not staged: ${notStaged.join(', ')}` : ''}`);
|
|
634
|
+
launch.note?.(`database prepare step, with the World up (${Object.entries(world).map(([id, version]) => `${id}@${version}`).join(', ') || 'no twins'}; ${given.length} names given: ${given.join(', ')}): ${step.command.join(' ')}\n${output.trim()}`);
|
|
635
|
+
return directory;
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
if (moved) {
|
|
639
|
+
rmSync(directory, { recursive: true, force: true });
|
|
640
|
+
rmSync(stamp, { force: true });
|
|
641
|
+
}
|
|
642
|
+
unstage();
|
|
643
|
+
// The instance's own logs go with the state directory below, and they are where a World that did not come up
|
|
644
|
+
// says why (the co-located host's output, a managed service's log, the lifecycle log). They are kept first,
|
|
645
|
+
// whole, where a failed preparation keeps its evidence, and the error names the file.
|
|
646
|
+
const logs = worldLogs(state);
|
|
647
|
+
if (logs && error instanceof Error)
|
|
648
|
+
error.message += keepFailureEvidence('database-prepare-world-logs', logs, env);
|
|
649
|
+
throw error;
|
|
650
|
+
}
|
|
651
|
+
finally {
|
|
652
|
+
rmSync(state, { recursive: true, force: true });
|
|
653
|
+
for (const name of Object.keys(process.env))
|
|
654
|
+
if (!(name in before))
|
|
655
|
+
delete process.env[name];
|
|
656
|
+
Object.assign(process.env, before);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
332
659
|
export async function nativePrepare(service, definition, worldConfig, launch = {}) {
|
|
333
660
|
if (service.kind !== 'postgres')
|
|
334
661
|
throw new Error('only a declared PostgreSQL is prepared');
|
|
@@ -338,6 +665,9 @@ export async function nativePrepare(service, definition, worldConfig, launch = {
|
|
|
338
665
|
const tools = nativePostgresTools(env);
|
|
339
666
|
if (!tools)
|
|
340
667
|
throw new Error('native prepare requires the PostgreSQL programs: a postgres server on PATH, or VOLTER_WORLD_POSTGRES_BIN');
|
|
668
|
+
const withWorld = databasePrepareOf(worldConfig);
|
|
669
|
+
if (withWorld?.with === 'world')
|
|
670
|
+
return prepareWithWorld(withWorld, definition, worldConfig, tools, env, launch);
|
|
341
671
|
const { directory, stamp } = nativePreparedCluster(worldConfig);
|
|
342
672
|
const root = dirname(directory);
|
|
343
673
|
mkdirSync(root, { recursive: true });
|
|
@@ -380,14 +710,7 @@ export async function nativePrepare(service, definition, worldConfig, launch = {
|
|
|
380
710
|
}
|
|
381
711
|
// The clean stop: SIGINT is the postmaster's fast shutdown, which writes its checkpoint and removes postmaster.pid.
|
|
382
712
|
// Its wait is the lifecycle's own readiness budget, not a new limit; down then clears the receipts.
|
|
383
|
-
|
|
384
|
-
if (identity) {
|
|
385
|
-
await signalOwnedProcess(identity, 'SIGINT', true);
|
|
386
|
-
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
387
|
-
while (liveGroup(identity.pid) && Date.now() < deadline)
|
|
388
|
-
await wait(100);
|
|
389
|
-
}
|
|
390
|
-
await nativePostgresDown(root);
|
|
713
|
+
await nativePostgresDown(root, 'prepare (the cluster is made; its clean stop)');
|
|
391
714
|
if (stepFailure) {
|
|
392
715
|
rmSync(directory, { recursive: true, force: true });
|
|
393
716
|
throw failed(stepFailure.error);
|
|
@@ -400,10 +723,21 @@ export async function nativePrepare(service, definition, worldConfig, launch = {
|
|
|
400
723
|
writeFileSync(stamp, `${JSON.stringify(nativeStamp(tools, user, database, true, migration))}\n`);
|
|
401
724
|
return directory;
|
|
402
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* How often the start looks for the postmaster's readiness (postmaster.pid's status line, then its port). Each look
|
|
728
|
+
* is two file reads; at 100 ms a start waited up to 100 ms past a ready server (Cal in a tab: ready at check 8, 1,211
|
|
729
|
+
* ms). The postmaster says it no other way a parent can wait on: its log is a file this start does not hold open.
|
|
730
|
+
*/
|
|
731
|
+
const READY_CHECK_MS = 20;
|
|
403
732
|
export async function nativePostgresUp(services, root, definition, launch) {
|
|
404
733
|
if (services.length !== 1 || services[0].kind !== 'postgres')
|
|
405
734
|
throw new Error('native postgres requires one declared postgres service');
|
|
406
735
|
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
736
|
+
// EACH STEP OF THIS START SAYS ITS OWN TIME AND WHAT IT WAITED ON, as a note the World's up prints on its own
|
|
737
|
+
// clock. In a tab the whole start took ten seconds of a World's twelve (Twenty: backing chosen at +1.29 s, ready at
|
|
738
|
+
// +11.72 s) and one line, the cluster's placing, said where any of it went.
|
|
739
|
+
let stepBegan = performance.now();
|
|
740
|
+
const step = (what) => { const now = performance.now(); launch.note?.(`native PostgreSQL: ${what}, ${Math.round(now - stepBegan)} ms`); stepBegan = now; };
|
|
407
741
|
const env = { ...(launch.env ?? process.env), LC_ALL: launch.env?.LC_ALL || 'C' };
|
|
408
742
|
const binaries = nativePostgresTools(env);
|
|
409
743
|
if (!binaries)
|
|
@@ -428,12 +762,19 @@ export async function nativePostgresUp(services, root, definition, launch) {
|
|
|
428
762
|
const service = services[0];
|
|
429
763
|
if (await listening(service.hostPort, deadline, launch.signal))
|
|
430
764
|
throw new Error(`native postgres port ${service.hostPort} is already occupied`);
|
|
765
|
+
step(`an earlier start's programs retired and port ${service.hostPort} found free (one connection refused)`);
|
|
431
766
|
const data = resolve(root, 'native-postgres-data');
|
|
432
767
|
const { user, database } = settings(definition);
|
|
433
768
|
// An empty data directory takes the World's prepared cluster when its stamp matches (ADR 0018's placement; the
|
|
434
769
|
// cluster was made by these same programs at image build). initdb then finds PG_VERSION, and the database exists.
|
|
435
770
|
forgetStaleMigration(data);
|
|
436
771
|
const prepared = env.VOLTER_WORLD_CONFIG ? nativePreparedCluster(env.VOLTER_WORLD_CONFIG) : undefined;
|
|
772
|
+
// Placed this start under a stamp that names its database: the cluster holds it (S2 R15).
|
|
773
|
+
let placedDatabase = false;
|
|
774
|
+
// Copied as the server's own, every file already its: the walk below would find nothing to change.
|
|
775
|
+
let ownedAsCopied = false;
|
|
776
|
+
// Moved in whole this start, as the image laid it (S2 B2): owned by the owner the image names, not to be walked.
|
|
777
|
+
let movedAsLaid = false;
|
|
437
778
|
// The check reads two files' metadata and the stamp; it runs only when a prepared cluster is there to take.
|
|
438
779
|
if (prepared && existsSync(join(prepared.directory, 'PG_VERSION')) && !existsSync(join(data, 'PG_VERSION'))) {
|
|
439
780
|
// A migrated cluster is the app's only for the step inputs it was migrated from (pglite-backing.ts, expectedMigration).
|
|
@@ -442,14 +783,34 @@ export async function nativePostgresUp(services, root, definition, launch) {
|
|
|
442
783
|
launch.note?.(wanted.note);
|
|
443
784
|
else {
|
|
444
785
|
const expected = nativeStamp(binaries, user, database, false, wanted.migration);
|
|
445
|
-
const placed = takePreparedCluster(prepared, text => nativeStampMatches(text, expected), data, 'other PostgreSQL programs, initdb arguments, identity or migration', wanted.migration);
|
|
786
|
+
const placed = takePreparedCluster(prepared, text => nativeStampMatches(text, expected), data, 'other PostgreSQL programs, initdb arguments, identity or migration', wanted.migration, copyClusterOwned(postgresRunAs()));
|
|
787
|
+
ownedAsCopied = placed.copied === true;
|
|
788
|
+
movedAsLaid = placed.placed && placed.copied !== true;
|
|
446
789
|
if (placed.note)
|
|
447
790
|
launch.note?.(placed.note);
|
|
791
|
+
placedDatabase = placed.placed;
|
|
448
792
|
}
|
|
449
793
|
}
|
|
794
|
+
// The placement's own parts, each a step (S2 R71: Cal's 195-206 ms was one line): the stamp's check and the move,
|
|
795
|
+
// then the server's directories and ownership with what the walk looked at and changed.
|
|
796
|
+
step(placedDatabase ? 'the prepared cluster\'s stamp read and matched, and the cluster moved into place' : 'the prepared cluster looked for (none placed)');
|
|
450
797
|
mkdirSync(data, { recursive: true, mode: 0o700 });
|
|
798
|
+
const runAs = postgresRunAs();
|
|
799
|
+
postgresDirectorySearch(root, env, runAs);
|
|
800
|
+
// A cluster this start moved in whole from the prepared tree is owned as the image laid it. Where the page says the
|
|
801
|
+
// image laid every entry of that cluster with one owner, and that owner is the server's (VOLTER_WORLD_PREPARED_CLUSTER_
|
|
802
|
+
// OWNER, given only where its open placed the World's tree with the prepared project), the walk could change nothing
|
|
803
|
+
// and is not run (S2 B2: 134-206 ms of lstats on Cal and Twenty). Any other cluster, or no such word, is walked.
|
|
804
|
+
const laid = /^(\d+):(\d+)$/u.exec(env.VOLTER_WORLD_PREPARED_CLUSTER_OWNER ?? '');
|
|
805
|
+
const ownedAsLaid = movedAsLaid && laid !== null && Number(laid[1]) === runAs.uid && Number(laid[2]) === runAs.gid;
|
|
806
|
+
const owned = ownedAsCopied || ownedAsLaid ? undefined : ownPostgresCluster(data, runAs);
|
|
807
|
+
launch.note?.(`native PostgreSQL: child uid ${runAs.uid}, gid ${runAs.gid}; cluster ${data}`);
|
|
808
|
+
step(ownedAsCopied ? 'the prepared cluster looked for and copied as the server\'s own'
|
|
809
|
+
: ownedAsLaid ? `the prepared cluster looked for and placed where one matches, laid as the server's own (${runAs.uid}:${runAs.gid}): not walked`
|
|
810
|
+
: `the prepared cluster looked for and placed where one matches, its files made the server's (${owned.walked} entries walked, ${owned.changed} changed)`);
|
|
451
811
|
if (!existsSync(join(data, 'PG_VERSION'))) {
|
|
452
|
-
await tool(join(binaries, 'initdb'), ['-D', data, ...initdbArguments(user)], env, deadline, root, launch.signal);
|
|
812
|
+
await tool(join(binaries, 'initdb'), ['-D', data, ...initdbArguments(user)], env, deadline, root, runAs, launch.signal);
|
|
813
|
+
step('initdb run to its end (no prepared cluster was placed)');
|
|
453
814
|
}
|
|
454
815
|
launch.signal?.throwIfAborted();
|
|
455
816
|
const binary = join(binaries, 'postgres');
|
|
@@ -463,7 +824,7 @@ export async function nativePostgresUp(services, root, definition, launch) {
|
|
|
463
824
|
const fd = openSync(log, 'a');
|
|
464
825
|
let child;
|
|
465
826
|
try {
|
|
466
|
-
child = spawn(binary, ['-D', data, '-h', '127.0.0.1', '-p', String(service.hostPort), '-k', ''], { env, detached: true, windowsHide: true, stdio: ['ignore', fd, fd] });
|
|
827
|
+
child = spawn(binary, ['-D', data, '-h', '127.0.0.1', '-p', String(service.hostPort), '-k', '', ...recoverySync()], { env, ...runAs, detached: true, windowsHide: true, stdio: ['ignore', fd, fd] });
|
|
467
828
|
current.pid = child.pid ?? 0;
|
|
468
829
|
started = { child, owned: current, closed: false };
|
|
469
830
|
const observed = started;
|
|
@@ -487,7 +848,13 @@ export async function nativePostgresUp(services, root, definition, launch) {
|
|
|
487
848
|
if (!child.pid)
|
|
488
849
|
throw new Error('native postgres spawn failed');
|
|
489
850
|
publishProcessReceipt(recordPath(root), JSON.stringify(current) + '\n');
|
|
851
|
+
step('the postmaster spawned, its identity captured and its receipts written');
|
|
852
|
+
// The cluster is chosen and placed (takePreparedCluster's move or copy, its migration record written; or initdb's)
|
|
853
|
+
// and its server spawned: what a twin's staged store is placed beside is there now (S2 R79), before readiness.
|
|
854
|
+
launch.onPlaced?.();
|
|
855
|
+
let checks = 0;
|
|
490
856
|
for (;;) {
|
|
857
|
+
checks += 1;
|
|
491
858
|
launch.signal?.throwIfAborted();
|
|
492
859
|
if (started.error)
|
|
493
860
|
throw started.error;
|
|
@@ -495,19 +862,33 @@ export async function nativePostgresUp(services, root, definition, launch) {
|
|
|
495
862
|
throw new Error(`native postgres exited before readiness; see ${log}${logTail(log)}`);
|
|
496
863
|
if (Date.now() >= deadline)
|
|
497
864
|
throw new Error(`native postgres did not become ready; see ${log}${logTail(log)}`);
|
|
498
|
-
if (existsSync(join(data, 'postmaster.pid'))) {
|
|
499
|
-
|
|
500
|
-
|
|
865
|
+
if (existsSync(join(data, 'postmaster.pid')) && verifyOwner(current)) {
|
|
866
|
+
// Read again for its status line; gone or shorter between the two reads is not ready yet.
|
|
867
|
+
let lines = [];
|
|
868
|
+
try {
|
|
869
|
+
lines = readFileSync(join(data, 'postmaster.pid'), 'utf8').split('\n');
|
|
870
|
+
}
|
|
871
|
+
catch { /* removed since the check above */ }
|
|
501
872
|
if (lines[7]?.trim() === 'ready' && await listening(service.hostPort, deadline, launch.signal))
|
|
502
873
|
break;
|
|
503
874
|
}
|
|
504
|
-
await wait(
|
|
875
|
+
await wait(READY_CHECK_MS);
|
|
505
876
|
}
|
|
506
|
-
|
|
877
|
+
step(`the postmaster ready: postmaster.pid says so and its port answers, at check ${checks} (a check every ${READY_CHECK_MS} ms)`);
|
|
878
|
+
// A cluster placed this start was matched on a stamp that names this database (nativeStampMatches), and made with
|
|
879
|
+
// it by these same programs at image build: asking the server again was a psql process of its own (Cal in a tab:
|
|
880
|
+
// 327 ms of a 2.3 s World up). A cluster from an earlier start, or initdb's, is still asked, and given the database
|
|
881
|
+
// where it lacks it.
|
|
882
|
+
if (placedDatabase && database !== 'postgres')
|
|
883
|
+
step(`database ${database} is the placed cluster's, by its stamp: not asked`);
|
|
884
|
+
else if (database !== 'postgres') {
|
|
507
885
|
const sql = `SELECT 1 FROM pg_database WHERE datname = '${database.replaceAll("'", "''")}'`;
|
|
508
|
-
const output = await tool(join(binaries, 'psql'), ['-h', '127.0.0.1', '-p', String(service.hostPort), '-U', user, '-d', 'postgres', '-tAc', sql], env, deadline, root, launch.signal);
|
|
509
|
-
|
|
510
|
-
|
|
886
|
+
const output = await tool(join(binaries, 'psql'), ['-h', '127.0.0.1', '-p', String(service.hostPort), '-U', user, '-d', 'postgres', '-tAc', sql], env, deadline, root, runAs, launch.signal);
|
|
887
|
+
step(`psql run to its end, asking whether database ${database} is there`);
|
|
888
|
+
if (!output.trim()) {
|
|
889
|
+
await tool(join(binaries, 'createdb'), ['-h', '127.0.0.1', '-p', String(service.hostPort), '-U', user, '--', database], env, deadline, root, runAs, launch.signal);
|
|
890
|
+
step(`createdb run to its end`);
|
|
891
|
+
}
|
|
511
892
|
}
|
|
512
893
|
launch.signal?.throwIfAborted();
|
|
513
894
|
if (child.exitCode !== null || child.signalCode !== null)
|
|
@@ -520,7 +901,7 @@ export async function nativePostgresUp(services, root, definition, launch) {
|
|
|
520
901
|
const failures = [error];
|
|
521
902
|
try {
|
|
522
903
|
if (started)
|
|
523
|
-
await retireStarted(started);
|
|
904
|
+
await retireStarted(started, 'up (startup failed)');
|
|
524
905
|
removeReceipts(root);
|
|
525
906
|
starts.delete(root);
|
|
526
907
|
}
|
|
@@ -532,7 +913,9 @@ export async function nativePostgresUp(services, root, definition, launch) {
|
|
|
532
913
|
throw error;
|
|
533
914
|
}
|
|
534
915
|
}
|
|
535
|
-
|
|
916
|
+
/** The native postmaster's status proof: its ownership known, its held group live, its spawn identity and owner
|
|
917
|
+
* verified, and its port answering. `signal` ends the port's wait with the boot that asks (an up's hand-over). */
|
|
918
|
+
export async function nativePostgresStatus(root, signal) {
|
|
536
919
|
const state = ownership(root);
|
|
537
920
|
if (state.errors.length)
|
|
538
921
|
throw new AggregateError(state.errors, 'native postgres ownership uncertain; retry down');
|
|
@@ -540,8 +923,9 @@ export async function nativePostgresStatus(root) {
|
|
|
540
923
|
return false;
|
|
541
924
|
if (state.identity)
|
|
542
925
|
await verifySpawnIdentity(state.identity);
|
|
543
|
-
verifyOwner(state.held)
|
|
544
|
-
|
|
926
|
+
if (!verifyOwner(state.held))
|
|
927
|
+
return false;
|
|
928
|
+
return listening(state.held.port, Date.now() + READY_TIMEOUT_MS, signal);
|
|
545
929
|
}
|
|
546
930
|
async function retireTools(root) {
|
|
547
931
|
if (!existsSync(root))
|
|
@@ -570,18 +954,19 @@ async function retireTools(root) {
|
|
|
570
954
|
rmSync(spawnReceiptPath(receipt), { force: true });
|
|
571
955
|
}
|
|
572
956
|
}
|
|
573
|
-
export async function nativePostgresDown(root) {
|
|
957
|
+
export async function nativePostgresDown(root, by = 'down') {
|
|
574
958
|
const started = starts.get(root);
|
|
575
959
|
const state = ownership(root);
|
|
960
|
+
const data = resolve(root, 'native-postgres-data');
|
|
576
961
|
if (state.errors.length)
|
|
577
962
|
process.stderr.write(`${errorMessage(new AggregateError(state.errors, 'native postgres ownership uncertain; retrying retirement through established identity'))}\n`);
|
|
578
963
|
if (started) {
|
|
579
|
-
await retireStarted(started);
|
|
964
|
+
await retireStarted(started, by);
|
|
580
965
|
}
|
|
581
966
|
else if (state.identity) {
|
|
582
967
|
if (liveGroup(state.identity.pid)) {
|
|
583
968
|
await verifySpawnIdentity(state.identity);
|
|
584
|
-
await
|
|
969
|
+
await retirePostmaster(state.identity, by, data);
|
|
585
970
|
}
|
|
586
971
|
}
|
|
587
972
|
else if (state.held && state.held.pid > 0) {
|