@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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { goMysqlBinding, goMysqlEndpoint, type GoMysqlBinding } from './go-mysql-binding.ts';
|
|
2
|
+
import * as managedOriginalInputs from '@volter/world-core/managed-original-inputs';
|
|
3
|
+
// Definition metadata is independent of database host lifetimes.
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { nativeMariaDBReference } from './mariadb-supplier.ts';
|
|
6
|
+
import { mariaDBCredentials } from './mariadb-bootstrap.ts';
|
|
7
|
+
import type { NativeMariaDBService } from './native-mariadb-backing.ts';
|
|
8
|
+
|
|
9
|
+
export interface InfraService { kind: string; hostPort: number; nativeMariaDB?: NativeMariaDBService; virtualTcpEndpoint?: { version: 1; host: string; port: number; targetPort: number }; connectionBindings?: Record<string, GoMysqlBinding & { containerPort: number }>; originalInputs?: managedOriginalInputs.OriginalInputReference; sides?: Record<string, { hostPort: number; containerPort: number }>; configuration?: Record<string, unknown>; replicaSet?: string; postgres?: { user: string; database: string; password?: string } }
|
|
10
|
+
|
|
11
|
+
/** Read the generated definition's loopback port mappings and backing-independent options.
|
|
12
|
+
* The Compose extension carries a MongoDB replica-set name. Older generated commands remain readable. */
|
|
13
|
+
export function parseInfraDefinition(text: string): InfraService[] {
|
|
14
|
+
const { parse } = createRequire(import.meta.url)('yaml') as typeof import('yaml');
|
|
15
|
+
const definition = parse(text);
|
|
16
|
+
const services: InfraService[] = [];
|
|
17
|
+
for (const [kind, value] of Object.entries(definition?.services ?? {})) {
|
|
18
|
+
const declared = value as { image?: unknown; ports?: unknown[]; command?: unknown; entrypoint?: unknown; volumes?: unknown; 'x-volter-native-server'?: unknown; 'x-volter-replica-set'?: unknown; environment?: Record<string, unknown>; 'x-volter-interfaces'?: unknown; 'x-volter-side-ports'?: unknown; 'x-volter-configuration'?: unknown; 'x-volter-original-inputs'?: unknown; 'x-volter-issued-dsn-bindings'?: unknown; 'x-volter-virtual-tcp-endpoint'?: unknown };
|
|
19
|
+
const nativeReference = declared['x-volter-native-server'] === undefined ? undefined : nativeMariaDBReference(declared['x-volter-native-server']);
|
|
20
|
+
if (nativeReference && (kind !== 'mysql' || declared['x-volter-interfaces'] !== 3 || typeof declared.image !== 'string'
|
|
21
|
+
|| !/^(?:docker\.io\/)?(?:library\/)?mariadb(?::[^@]+)?@sha256:[a-f0-9]{64}$/u.test(declared.image)))
|
|
22
|
+
throw new Error('Managed native MariaDB needs mysql, infrastructure interface3 and its exact declared image');
|
|
23
|
+
const originalInputs = declared['x-volter-original-inputs'] === undefined ? undefined : managedOriginalInputs.referenceOf(declared['x-volter-original-inputs']);
|
|
24
|
+
if (originalInputs && declared['x-volter-interfaces'] !== 2) throw new Error('Managed original inputs require infrastructure interface2');
|
|
25
|
+
const option = declared['x-volter-replica-set'];
|
|
26
|
+
if (option !== undefined && (kind !== 'mongodb' || typeof option !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(option))) {
|
|
27
|
+
throw new Error('World-managed replica-set metadata must name one valid MongoDB replica set');
|
|
28
|
+
}
|
|
29
|
+
let replicaSet = option as string | undefined;
|
|
30
|
+
if (kind === 'mongodb' && declared.command !== undefined) {
|
|
31
|
+
const command = declared.command;
|
|
32
|
+
if (!Array.isArray(command) || command.length !== 3 || command[0] !== 'mongod' || command[1] !== '--replSet'
|
|
33
|
+
|| typeof command[2] !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(command[2])) {
|
|
34
|
+
throw new Error('World-managed MongoDB command must be ["mongod", "--replSet", "<name>"] or absent for standalone');
|
|
35
|
+
}
|
|
36
|
+
if (replicaSet !== undefined && replicaSet !== command[2]) throw new Error('World-managed MongoDB command and replica-set metadata disagree');
|
|
37
|
+
replicaSet = command[2];
|
|
38
|
+
}
|
|
39
|
+
const mapped = (declared.ports ?? []).flatMap((port) => {
|
|
40
|
+
const match = typeof port === 'string' ? port.match(/^127\.0\.0\.1:(\d+):(\d+)$/) : null;
|
|
41
|
+
if ((kind === 'clickhouse' || nativeReference) && (!match || ![Number(match[1]), Number(match[2])].every(value => Number.isInteger(value) && value > 0 && value <= 65535))) throw new Error(`managed infrastructure ${kind} has an invalid loopback port`);
|
|
42
|
+
// Other kinds retain their earlier reader: unrecognized mappings are skipped.
|
|
43
|
+
return match ? [{ hostPort: Number(match[1]), containerPort: Number(match[2]) }] : [];
|
|
44
|
+
});
|
|
45
|
+
let sides: InfraService['sides'], configuration: InfraService['configuration'];
|
|
46
|
+
if (kind === 'clickhouse') {
|
|
47
|
+
if (declared['x-volter-interfaces'] !== (originalInputs ? 2 : 1) || mapped.length !== 2 || mapped[0]!.containerPort !== 8123 || mapped[1]!.containerPort !== 9000 || mapped[0]!.hostPort === mapped[1]!.hostPort)
|
|
48
|
+
throw new Error('Managed ClickHouse needs named-interface version1, HTTP8123 and native9000 mappings');
|
|
49
|
+
const raw = declared['x-volter-side-ports'] as Record<string, unknown> | undefined;
|
|
50
|
+
const native = raw?.native as { hostPort?: unknown; containerPort?: unknown } | undefined;
|
|
51
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw) || Object.keys(raw).join() !== 'native' || !native || native.hostPort !== mapped[1]!.hostPort || native.containerPort !== 9000)
|
|
52
|
+
throw new Error('Managed ClickHouse native side metadata differs from its declared port');
|
|
53
|
+
sides = { native: mapped[1]! };
|
|
54
|
+
const input = declared['x-volter-configuration'];
|
|
55
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Managed ClickHouse needs explicit operator configuration');
|
|
56
|
+
configuration = input as Record<string, unknown>;
|
|
57
|
+
} else if (mapped.length !== 1 || declared['x-volter-interfaces'] !== (nativeReference ? 3 : originalInputs ? 2 : undefined) || declared['x-volter-side-ports'] !== undefined || declared['x-volter-configuration'] !== undefined) {
|
|
58
|
+
throw new Error(`managed infrastructure ${kind} needs exactly one declared loopback port and no multiwire metadata`);
|
|
59
|
+
}
|
|
60
|
+
const ports = mapped.map(port => port.hostPort);
|
|
61
|
+
let nativeMariaDB: NativeMariaDBService | undefined;
|
|
62
|
+
if (nativeReference) {
|
|
63
|
+
if (mapped[0]!.containerPort !== 3306 || originalInputs || declared.command !== undefined || declared.entrypoint !== undefined)
|
|
64
|
+
throw new Error('Managed native MariaDB has an unconsumed port, original-input, command or entrypoint requirement');
|
|
65
|
+
const volumes = declared.volumes;
|
|
66
|
+
if (volumes !== undefined && (!Array.isArray(volumes) || volumes.length !== 1
|
|
67
|
+
|| volumes[0] !== '${VOLTER_WORLD_DATA:?}/native-mariadb-data:/var/lib/mysql'))
|
|
68
|
+
throw new Error('Managed native MariaDB has an unconsumed source mount requirement');
|
|
69
|
+
nativeMariaDB = { hostPort: ports[0]!, image: declared.image as string,
|
|
70
|
+
reference: nativeReference, credentials: mariaDBCredentials(declared.environment ?? {}) };
|
|
71
|
+
}
|
|
72
|
+
const virtual = declared['x-volter-virtual-tcp-endpoint'] as Record<string, unknown> | undefined;
|
|
73
|
+
let virtualTcpEndpoint: InfraService['virtualTcpEndpoint'];
|
|
74
|
+
if (virtual !== undefined) {
|
|
75
|
+
if (!virtual || typeof virtual !== 'object' || Array.isArray(virtual) || Object.keys(virtual).sort().join() !== 'host,port,targetPort,version'
|
|
76
|
+
|| virtual.version !== 1 || typeof virtual.host !== 'string' || !/^[a-z0-9][a-z0-9-]{0,62}\.world\.volter\.internal$/.test(virtual.host)
|
|
77
|
+
|| virtual.port !== mapped[0]!.containerPort || virtual.targetPort !== mapped[0]!.hostPort) throw new Error(`Managed ${kind} virtual TCP endpoint differs from its owned listener`);
|
|
78
|
+
virtualTcpEndpoint = { version: 1, host: virtual.host, port: Number(virtual.port), targetPort: Number(virtual.targetPort) };
|
|
79
|
+
}
|
|
80
|
+
let postgres: InfraService['postgres'];
|
|
81
|
+
if (kind === 'postgres') {
|
|
82
|
+
const environment = declared.environment ?? {};
|
|
83
|
+
const user = environment.POSTGRES_USER ?? 'postgres';
|
|
84
|
+
const database = environment.POSTGRES_DB ?? user;
|
|
85
|
+
const password = environment.POSTGRES_PASSWORD;
|
|
86
|
+
if ([user, database, password].some(value => value !== undefined && (typeof value !== 'string' || !value || /[\r\n\0]|\$\{/.test(value)))) {
|
|
87
|
+
throw new Error('World-managed PGlite requires literal POSTGRES_USER, POSTGRES_DB and POSTGRES_PASSWORD');
|
|
88
|
+
}
|
|
89
|
+
if ([user, database].some(value => Buffer.byteLength(value as string, 'utf8') > 63)) throw new Error('World-managed PGlite identifiers must fit PostgreSQL\'s 63-byte name limit');
|
|
90
|
+
postgres = { user: user as string, database: database as string, ...(password !== undefined ? { password: password as string } : {}) };
|
|
91
|
+
}
|
|
92
|
+
const typed = declared['x-volter-issued-dsn-bindings'];
|
|
93
|
+
let connectionBindings: Record<string, GoMysqlBinding & { containerPort: number }> | undefined;
|
|
94
|
+
if (typed !== undefined) {
|
|
95
|
+
if (kind !== 'mysql' || !typed || typeof typed !== 'object' || Array.isArray(typed)) throw new Error('Managed DSN bindings need a declared MySQL service');
|
|
96
|
+
connectionBindings = Object.create(null);
|
|
97
|
+
for (const [name, value] of Object.entries(typed)) {
|
|
98
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || !value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid managed DSN binding');
|
|
99
|
+
const row = value as Record<string, unknown>;
|
|
100
|
+
const parsed = goMysqlBinding({ ...row, scheme: 'mysql' }, 6, name, mapped[0]!.containerPort);
|
|
101
|
+
if (!parsed.dsnFormat || Object.keys(row).some(key => !['dsnFormat', 'endpoint', 'dsnPrefix', 'dsnProtocol', 'containerPort', 'query'].includes(key))) throw new Error(`Invalid managed DSN declaration for ${name}`);
|
|
102
|
+
connectionBindings![name] = { ...parsed as GoMysqlBinding, containerPort: mapped[0]!.containerPort };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
services.push({ kind, ...(nativeMariaDB ? { nativeMariaDB } : {}), ...(virtualTcpEndpoint ? { virtualTcpEndpoint } : {}), ...(connectionBindings ? { connectionBindings } : {}), ...(originalInputs ? { originalInputs } : {}), hostPort: ports[0]!, ...(sides ? { sides } : {}), ...(configuration ? { configuration } : {}), ...(replicaSet ? { replicaSet } : {}), ...(postgres ? { postgres } : {}) });
|
|
106
|
+
}
|
|
107
|
+
return services;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Endpoints of the declared infrastructure, published through external.discover. Only URLs
|
|
111
|
+
* whose protocol and loopback port match a declared service belong to this lifecycle. */
|
|
112
|
+
export function infraConnections(services: InfraService[], env: Record<string, string | undefined>): Record<string, string> {
|
|
113
|
+
const connections: Record<string, string> = {};
|
|
114
|
+
for (const [name, value] of Object.entries(env)) {
|
|
115
|
+
if (value === undefined) continue;
|
|
116
|
+
const typed = services.filter(service => service.connectionBindings?.[name] !== undefined);
|
|
117
|
+
if (typed.length > 1) throw new Error(`Managed DSN ${name} belongs to multiple infrastructure services`);
|
|
118
|
+
if (typed.length === 1) {
|
|
119
|
+
const service = typed[0]!, endpoint = goMysqlEndpoint(value, service.connectionBindings![name]!, service.connectionBindings![name]!.containerPort);
|
|
120
|
+
const virtual = service.virtualTcpEndpoint, binding = service.connectionBindings![name]!;
|
|
121
|
+
const expectedHost = binding.endpoint === 'default-port' && virtual ? virtual.host : '127.0.0.1';
|
|
122
|
+
const expectedPort = binding.endpoint === 'default-port' && virtual ? virtual.port : service.hostPort;
|
|
123
|
+
if (!endpoint || endpoint.host !== expectedHost || endpoint.port !== expectedPort) throw new Error(`Managed DSN ${name} does not match its owned endpoint`);
|
|
124
|
+
connections[name] = value;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const url = new URL(value);
|
|
129
|
+
if (url.hostname !== '127.0.0.1') continue;
|
|
130
|
+
const kind = url.protocol === 'postgresql:' ? 'postgres' : url.protocol.slice(0, -1);
|
|
131
|
+
if (services.some((service) => (service.kind === kind && service.hostPort === Number(url.port)) || (service.kind === 'clickhouse' && ((url.protocol === 'http:' && service.hostPort === Number(url.port)) || (url.protocol === 'clickhouse:' && service.sides?.native?.hostPort === Number(url.port)))))) connections[name] = value;
|
|
132
|
+
} catch { /* not an infrastructure endpoint */ }
|
|
133
|
+
}
|
|
134
|
+
return connections;
|
|
135
|
+
}
|
package/src/infra.ts
CHANGED
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
// that owns the World's lifetime, as every other service's recorder is, and not of a tool that exits once they
|
|
8
8
|
// serve. A process layer where a child does not outlive its parent (a browser tab's) otherwise ended the PGlite host
|
|
9
9
|
// the moment `volter-world-infra up` returned, and the World's readiness probe never saw it.
|
|
10
|
-
import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs';
|
|
10
|
+
import { accessSync, constants, existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
11
11
|
import { delimiter, dirname, join } from 'node:path';
|
|
12
|
+
import { setWorldEventsFile, worldEvent } from './process-groups.ts';
|
|
12
13
|
import { signalChild } from './process-signal.mjs';
|
|
13
14
|
import { spawn } from 'node:child_process';
|
|
14
15
|
import { fileURLToPath } from 'node:url';
|
|
15
16
|
import { containerlessDataDirectory, infraConnections, liveDatabaseBehind, liveDatabaseMigration, developmentMigratesAtRuntime, DEVELOPMENT_MIGRATION_NOTICE, parseInfraDefinition } from './pglite-backing.ts';
|
|
16
17
|
import { errorMessage } from './error-message.ts';
|
|
18
|
+
import type { ManagedProtocol } from './twin-backing.ts';
|
|
17
19
|
|
|
18
20
|
/** Whether a program is on PATH, read from the file system without starting a process: where a missing program
|
|
19
21
|
* still costs a process round trip (a browser tab's kernel), the container probe is not spawned at all. */
|
|
@@ -24,7 +26,13 @@ function onPath(name: string, env: NodeJS.ProcessEnv): boolean {
|
|
|
24
26
|
});
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export interface InfrastructureIo {
|
|
29
|
+
export interface InfrastructureIo {
|
|
30
|
+
out: (text: string) => void;
|
|
31
|
+
err: (text: string) => void;
|
|
32
|
+
/** The runtime's own in-process `up` (runtime.ts): handed the status its `status` would print, when this `up` itself
|
|
33
|
+
* proved everything that status proves (every service twin-served, each held and serving). */
|
|
34
|
+
verified?: (status: string) => void;
|
|
35
|
+
}
|
|
28
36
|
|
|
29
37
|
interface Ran { status: number | null; stdout: string; stderr: string; error?: Error }
|
|
30
38
|
|
|
@@ -122,11 +130,39 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
122
130
|
io.err('managed infrastructure: missing World lifecycle context\n');
|
|
123
131
|
return 2;
|
|
124
132
|
}
|
|
133
|
+
// What this command does to a backing's process is said in the World's own log, beside its services' events.
|
|
134
|
+
setWorldEventsFile(join(dirname(worldData), 'logs', 'events.jsonl'));
|
|
135
|
+
const phaseAsked = Date.now();
|
|
136
|
+
// Retirement follows retained ownership even if the definition or supplier
|
|
137
|
+
// can no longer be read. Other infrastructure still reports its own refusal.
|
|
138
|
+
let retainedMariaDB = false;
|
|
139
|
+
if (phase === 'down') {
|
|
140
|
+
try {
|
|
141
|
+
retainedMariaDB = existsSync(join(worldData, 'native-mariadb.owner.json'))
|
|
142
|
+
|| existsSync(join(worldData, 'native-mariadb.owner.json.spawn.json'))
|
|
143
|
+
|| existsSync(join(worldData, 'native-mariadb.starting.json'))
|
|
144
|
+
// install-db can outlive its caller before any server receipt exists.
|
|
145
|
+
// Either half of a helper pair enters retirement; a missing birth
|
|
146
|
+
// token remains an explicit uncertainty in nativeMariaDBDown.
|
|
147
|
+
|| (existsSync(worldData) && readdirSync(worldData).some(name => /^native-mariadb-tool-.*\.json(?:\.spawn\.json)?$/u.test(name)));
|
|
148
|
+
} catch (error) { io.err(`managed MariaDB ownership inventory is unreadable: ${errorMessage(error)}\n`); return 1; }
|
|
149
|
+
}
|
|
150
|
+
if (retainedMariaDB) {
|
|
151
|
+
try { await (await import('./native-mariadb-backing.ts')).nativeMariaDBDown(worldData); }
|
|
152
|
+
catch (error) { io.err(`managed MariaDB down failed: ${errorMessage(error)}\n`); return 1; }
|
|
153
|
+
}
|
|
125
154
|
const definition = join(dirname(worldConfig), 'world.infrastructure.yml');
|
|
126
155
|
if (!existsSync(definition)) {
|
|
127
156
|
io.err('managed infrastructure: declared definition is missing\n');
|
|
128
157
|
return 2;
|
|
129
158
|
}
|
|
159
|
+
const declared = parseInfraDefinition(readFileSync(definition, 'utf8'));
|
|
160
|
+
// Both the CLI and the World's in-process up enter here. Endpoint metadata cannot supply host DNS or a native
|
|
161
|
+
// default-port listener: refuse before choosing or starting any backing when the substrate carrier is absent.
|
|
162
|
+
if (phase === 'up' && declared.some(service => service.virtualTcpEndpoint) && env.VOLTER_WORLD_VIRTUAL_TCP !== '1') {
|
|
163
|
+
io.err('managed infrastructure up: virtual TCP endpoints require the substrate network carrier; native host DNS/default ports are not supplied\n');
|
|
164
|
+
return 2;
|
|
165
|
+
}
|
|
130
166
|
// A development World says so every time it comes up: its use is never silent.
|
|
131
167
|
if (phase === 'up' && developmentMigratesAtRuntime(worldConfig)) io.out(`${DEVELOPMENT_MIGRATION_NOTICE}\n`);
|
|
132
168
|
|
|
@@ -218,7 +254,7 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
218
254
|
const { unsupportedKinds } = await import('./pglite-backing.ts');
|
|
219
255
|
const { nativePostgresTools } = await import('./native-postgres-backing.ts');
|
|
220
256
|
const postgres = declared.filter(s => s.kind === 'postgres');
|
|
221
|
-
if (postgres.length !== 1 || unsupportedKinds(declared.filter(s => s.kind !== 'postgres' && !twinned.includes(s))).length) return selected;
|
|
257
|
+
if (postgres.length !== 1 || unsupportedKinds(declared.filter(s => s.kind !== 'postgres' && !s.nativeMariaDB && !twinned.includes(s))).length) return selected;
|
|
222
258
|
const native = existsSync(join(worldData!, 'native-postgres-data'));
|
|
223
259
|
const pglite = existsSync(join(worldData!, 'pglite-postgres-data'));
|
|
224
260
|
if (native && pglite) {
|
|
@@ -257,7 +293,7 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
257
293
|
io.err(`managed infrastructure up failed: ${diagnostic(errorMessage(error))}\n`);
|
|
258
294
|
return 1;
|
|
259
295
|
}
|
|
260
|
-
|
|
296
|
+
sayReady();
|
|
261
297
|
return 0;
|
|
262
298
|
}
|
|
263
299
|
if (phase === 'status') {
|
|
@@ -275,18 +311,68 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
275
311
|
return 0;
|
|
276
312
|
}
|
|
277
313
|
|
|
278
|
-
// ----
|
|
279
|
-
// A
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
// the
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
314
|
+
// ---- a kind a twin serves: the twin, containerless --------------------------
|
|
315
|
+
// A service whose kind an installed pack declares (`managedService` in pack facts; twin-backing.ts) is served by
|
|
316
|
+
// that pack on every machine, container runtime or not, unless the operator forces the container backing; the
|
|
317
|
+
// other services take the backing chosen above. Which pack is the pack's declaration, never a name here. Where no
|
|
318
|
+
// pack declares the kind, or the one that does is not installed, the kind goes to that backing too: the container
|
|
319
|
+
// where there is one, else the backing's refusal naming the kind (the vendor's own server for MongoDB).
|
|
320
|
+
// The kinds listed are those whose protocol this runtime can ask its two questions in (ManagedProtocol).
|
|
321
|
+
const mariaDB = declared.flatMap(service => service.nativeMariaDB ? [service.nativeMariaDB] : []);
|
|
322
|
+
if (phase !== 'down' && mariaDB.length && ['docker', 'pglite'].includes(env.VOLTER_WORLD_INFRA_BACKING ?? '')) {
|
|
323
|
+
io.err('managed MariaDB: explicit native selection cannot be replaced by the forced backing\n');
|
|
324
|
+
return 1;
|
|
325
|
+
}
|
|
326
|
+
// Resolve only kinds the definition actually asks for. Looking up a pack before filtering its services made a
|
|
327
|
+
// PostgreSQL-only World scan installed Redis/MongoDB packs and their CLI graphs on every infrastructure phase.
|
|
328
|
+
const kinds = (['redis', 'mongodb', 'clickhouse'] as const).filter(kind => declared.some(service => service.kind === kind));
|
|
329
|
+
const managed = env.VOLTER_WORLD_INFRA_BACKING === 'docker' || kinds.length === 0
|
|
330
|
+
? undefined : await import('./twin-backing.ts');
|
|
331
|
+
const protocols: { redis?: ManagedProtocol; mongodb?: ManagedProtocol; clickhouse?: ManagedProtocol } = {};
|
|
332
|
+
if (managed) {
|
|
333
|
+
if (kinds.includes('redis')) protocols.redis = (await import('./redis-backing.ts')).REDIS_PROTOCOL;
|
|
334
|
+
if (kinds.includes('mongodb')) protocols.mongodb = (await import('./mongodb-backing.ts')).MONGODB_PROTOCOL;
|
|
335
|
+
if (kinds.includes('clickhouse')) protocols.clickhouse = (await import('./clickhouse-backing.ts')).CLICKHOUSE_PROTOCOL;
|
|
336
|
+
}
|
|
337
|
+
const expectedTwinPackages = new Map<string, string>();
|
|
338
|
+
const twins = !managed ? [] : kinds.flatMap((kind) => {
|
|
339
|
+
const services = declared.filter((s) => s.kind === kind);
|
|
340
|
+
const beside = [dirname(fileURLToPath(import.meta.url)), dirname(worldConfig)];
|
|
341
|
+
const twin = managed.managedTwin(kind, protocols[kind]!, beside, phase !== 'down' && services.some(service => service.originalInputs !== undefined));
|
|
342
|
+
if (twin) expectedTwinPackages.set(kind, twin.package);
|
|
343
|
+
const cli = twin && managed.twinBackingCli(twin, beside);
|
|
344
|
+
return twin && (cli || phase === 'down') ? [{ twin, cli: cli ?? '', services }] : [];
|
|
345
|
+
});
|
|
346
|
+
const twinned = twins.flatMap((served) => served.services);
|
|
347
|
+
// A declared original-input carrier cannot fall through to a backing that drops it.
|
|
348
|
+
// Down still retires established owners even when a pack or source snapshot is unavailable.
|
|
349
|
+
if (phase !== 'down') {
|
|
350
|
+
try {
|
|
351
|
+
for (const service of declared) if (service.originalInputs) {
|
|
352
|
+
const consumer = twins.find(entry => entry.services.includes(service));
|
|
353
|
+
if (!managed || !consumer) throw new Error(`managed ${service.kind} has original inputs and no installed declaring consumer`);
|
|
354
|
+
managed.originalInputArguments(consumer.twin, service, worldConfig);
|
|
355
|
+
}
|
|
356
|
+
} catch (error) {
|
|
357
|
+
io.err(`managed infrastructure ${phase} refused original inputs: ${diagnostic(errorMessage(error))}\n`);
|
|
358
|
+
return 1;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
287
361
|
/** The services the backing below answers for: every one, or those the twin does not serve (by compose service name). */
|
|
288
|
-
const native = declared.filter((s) => s.kind === 'mongodb');
|
|
289
|
-
|
|
362
|
+
const native = declared.filter((s) => s.kind === 'mongodb' && !twinned.includes(s));
|
|
363
|
+
// Tab hosts expose their kernel tree on this symbol (world-store.ts). Redis/MongoDB's managed kinds need
|
|
364
|
+
// their installed declaring twins there; a missing pack is an input refusal, never a native-server attempt.
|
|
365
|
+
// PostgreSQL is separate: its available WALI server tools or PGlite still own that declared backing.
|
|
366
|
+
const inTab = (process as unknown as Record<symbol, unknown>)[Symbol.for('tabnode.run.vfs')] !== undefined;
|
|
367
|
+
if (inTab && phase !== 'down') {
|
|
368
|
+
const missing = kinds.filter(kind => !twinned.some(service => service.kind === kind)
|
|
369
|
+
&& (env.VOLTER_WORLD_INFRA_BACKING !== 'docker' || native.some(service => service.kind === kind)));
|
|
370
|
+
if (missing.length) {
|
|
371
|
+
for (const kind of missing) io.err(`managed ${kind} needs ${expectedTwinPackages.get(kind) ?? `@volter/twin-${kind}`} installed in this tab World\n`);
|
|
372
|
+
return 1;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const composed = twinned.length === 0 && native.length === 0 && mariaDB.length === 0 ? null : declared.filter((s) => !twinned.includes(s) && !native.includes(s) && !s.nativeMariaDB).map((s) => s.kind);
|
|
290
376
|
// A database's native wire and storage are its vendor server's. MongoDB never needs a Docker probe or container.
|
|
291
377
|
if (native.length > 0) {
|
|
292
378
|
const { pgliteUp, pgliteStatus, pgliteDown } = await import('./pglite-backing.ts');
|
|
@@ -305,65 +391,146 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
305
391
|
return 1;
|
|
306
392
|
}
|
|
307
393
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
394
|
+
// THE TWIN-SERVED KINDS WAIT FOR THE DATABASE'S CLUSTER PLACEMENT. A store staged with a prepared cluster is placed where its
|
|
395
|
+
// twin serves from, and only beside a database that carries the same migration (takePreparedStore). Brought up
|
|
396
|
+
// first, a twin's store was placed before the database's backing was chosen, and where that backing then
|
|
397
|
+
// declined the prepared cluster (its programs' stamp, PGlite serving a native cluster) the World had an
|
|
398
|
+
// unprepared database and a prepared store. On `up` with the native backing, its onPlaced starts the twins once
|
|
399
|
+
// the cluster is placed, beside readiness; otherwise they start after the database. The live cluster is then
|
|
400
|
+
// there to be read; `status` and `down` keep their order. The ready line is said once,
|
|
401
|
+
// after both.
|
|
402
|
+
/** What `status` prints when every declared service is twin-served: one fact, said by status and handed over by up. */
|
|
403
|
+
const twinServedStatus = () => ({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...databaseFacts() });
|
|
404
|
+
/** The native backing's status, its proof asked as status asks it: set by the native branch's `up` once it is ready. */
|
|
405
|
+
let nativeProof: (() => Promise<string | undefined>) | undefined;
|
|
406
|
+
/** What this `up` has proved, handed to the runtime's own in-process up (InfrastructureIo.verified) as the status its
|
|
407
|
+
* `status` would print, its proofs asked again now: every twin held (twinBackingHeld), and then either every service
|
|
408
|
+
* twin-served or, with no MongoDB native, the native backing's own status proof (nativeStatus: the containerless
|
|
409
|
+
* others serving, pgliteStatus, and the postmaster's, nativePostgresStatus). It does not repeat status's choice of
|
|
410
|
+
* backing (selectBacking, a `docker info` probe): the backing is the one this up just brought up. Any doubt hands
|
|
411
|
+
* nothing over, and the runtime's probe runs. */
|
|
412
|
+
const handOver = async (): Promise<void> => {
|
|
413
|
+
if (!io.verified || phase !== 'up' || native.length !== 0) return;
|
|
414
|
+
for (const { twin, services } of twins) if (!await managed!.twinBackingHeld(twin, services, worldData!)) return;
|
|
415
|
+
if (mariaDB.length && !await (await import('./native-mariadb-backing.ts')).nativeMariaDBStatus(mariaDB[0]!, worldData, worldConfig, launch)) return;
|
|
416
|
+
if (composed?.length === 0) { io.verified(`${JSON.stringify(twinServedStatus())}\n`); return; }
|
|
417
|
+
if (!nativeProof) return;
|
|
418
|
+
let text: string | undefined;
|
|
419
|
+
try { text = await nativeProof(); } catch { return; }
|
|
420
|
+
if (text) io.verified(text);
|
|
421
|
+
};
|
|
422
|
+
let twinsPending = false;
|
|
423
|
+
let readyHeld = false;
|
|
424
|
+
// THE TWINS COME UP BESIDE THE DATABASE'S READINESS (S2 R79), not after it: Twenty's redis twin began only when the
|
|
425
|
+
// postmaster answered (+1.36 s) and held up 330 ms more. 840a1ae3's rule stands: a twin's staged store is placed beside
|
|
426
|
+
// the cluster the backing chose, so the twins start no earlier than the native backing's onPlaced, said after the
|
|
427
|
+
// cluster is placed (or made by initdb) and its server spawned. Every other backing still runs them after it returns.
|
|
428
|
+
let twinsBeside: Promise<number> | undefined;
|
|
429
|
+
// A twin that fails beside the readiness wait ends that wait (its signal), so the up fails then, not at the
|
|
430
|
+
// database's readiness or deadline: the native start retires its postmaster as on any abort, and the failed-database
|
|
431
|
+
// path below brings the rest down.
|
|
432
|
+
const twinFailed = new AbortController();
|
|
433
|
+
const startTwinsBeside = (): void => {
|
|
434
|
+
if (!twinsPending || twinsBeside) return;
|
|
435
|
+
io.out('managed infrastructure: twins started beside the database\'s readiness (its cluster placed)\n');
|
|
436
|
+
twinsBeside = twinsPhase().then((code) => {
|
|
437
|
+
if (code !== 0) twinFailed.abort(new Error('a twin failed beside the database\'s readiness, so its start was ended'));
|
|
438
|
+
return code;
|
|
439
|
+
});
|
|
440
|
+
};
|
|
441
|
+
const sayReady = (): void => { if (twinsPending) readyHeld = true; else io.out('managed infrastructure ready\n'); };
|
|
442
|
+
const twinsPhase = async (): Promise<number> => {
|
|
443
|
+
if (!managed) return 0;
|
|
444
|
+
for (const { twin, cli, services } of twins) {
|
|
445
|
+
if (phase === 'up') {
|
|
446
|
+
io.out(`managed infrastructure backing: ${twin.id} twin (containerless)\n`);
|
|
447
|
+
try {
|
|
448
|
+
await managed.twinBackingUp(twin, services, worldData!, cli, launch);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
io.err(`managed infrastructure up failed: ${diagnostic(errorMessage(error))}\n`);
|
|
451
|
+
return 1;
|
|
452
|
+
}
|
|
453
|
+
} else if (phase === 'status') {
|
|
454
|
+
const ready = await managed.twinBackingStatus(twin, services, worldData!);
|
|
455
|
+
if (ready !== services.length) {
|
|
456
|
+
io.err(`managed infrastructure status failed: ${ready}/${declared.length} declared services are ready\n`);
|
|
457
|
+
return 1;
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
await managed.twinBackingDown(twin, services, worldData!);
|
|
324
461
|
}
|
|
325
|
-
} else {
|
|
326
|
-
await redisTwinDown(twinned, worldData!);
|
|
327
462
|
}
|
|
463
|
+
return 0;
|
|
464
|
+
};
|
|
465
|
+
if (phase === 'up' && twins.length > 0) twinsPending = true;
|
|
466
|
+
else { const code = await twinsPhase(); if (code !== 0) return code; }
|
|
467
|
+
const databasePhase = async (): Promise<number> => {
|
|
468
|
+
if (mariaDB.length) {
|
|
469
|
+
const { nativeMariaDBUp, nativeMariaDBStatus, nativeMariaDBDown } = await import('./native-mariadb-backing.ts');
|
|
470
|
+
if (mariaDB.length !== 1) { io.err('managed MariaDB needs one explicit native provider per World\n'); return 1; }
|
|
471
|
+
try {
|
|
472
|
+
if (phase === 'up') {
|
|
473
|
+
io.out(`managed infrastructure backing: native MariaDB ${mariaDB[0]!.reference.release}, exact declared provider\n`);
|
|
474
|
+
await nativeMariaDBUp(mariaDB[0]!, worldData, worldConfig, launch);
|
|
475
|
+
} else if (phase === 'status') {
|
|
476
|
+
if (!await nativeMariaDBStatus(mariaDB[0]!, worldData, worldConfig, launch)) {
|
|
477
|
+
io.err('managed MariaDB status: owned server, installed supply or authenticated readiness is not confirmed\n');
|
|
478
|
+
return 1;
|
|
479
|
+
}
|
|
480
|
+
} else await nativeMariaDBDown(worldData);
|
|
481
|
+
} catch (error) { io.err(`managed MariaDB ${phase} failed: ${diagnostic(errorMessage(error))}\n`); return 1; }
|
|
328
482
|
}
|
|
329
483
|
if (composed?.length === 0) {
|
|
330
|
-
if (phase === 'up')
|
|
331
|
-
else if (phase === 'status') io.out(`${JSON.stringify(
|
|
484
|
+
if (phase === 'up') sayReady();
|
|
485
|
+
else if (phase === 'status') io.out(`${JSON.stringify(twinServedStatus())}\n`);
|
|
332
486
|
else io.out('managed infrastructure stopped\n');
|
|
333
487
|
return 0;
|
|
334
488
|
}
|
|
335
489
|
|
|
490
|
+
// How long the choice of backing took, and how far into the phase it was made (S2, Twenty's World up): Twenty's up spent 220 ms between
|
|
491
|
+
// its infrastructure starting beside the host and the backing's line, none of it said.
|
|
492
|
+
const choosing = Date.now();
|
|
336
493
|
const backing = await containerlessPostgres(await selectBacking());
|
|
494
|
+
const chosen = `backing chosen in ${Date.now() - choosing} ms, ${Date.now() - phaseAsked} ms into the phase`;
|
|
337
495
|
if (typeof backing === 'number') return backing;
|
|
338
496
|
if (backing === 'pglite') return runPglite();
|
|
339
497
|
if (backing === 'native') {
|
|
340
498
|
const { nativePostgresUp, nativePostgresStatus, nativePostgresDown } = await import('./native-postgres-backing.ts');
|
|
341
499
|
const postgres = declared.filter(s => s.kind === 'postgres');
|
|
342
|
-
const others = declared.filter(s => s.kind !== 'postgres' && !twinned.includes(s));
|
|
500
|
+
const others = declared.filter(s => s.kind !== 'postgres' && !s.nativeMariaDB && !twinned.includes(s));
|
|
343
501
|
const { unsupportedKinds, pgliteUp, pgliteStatus, pgliteDown } = await import('./pglite-backing.ts');
|
|
344
502
|
if (postgres.length !== 1 || unsupportedKinds(others).length) {
|
|
345
503
|
io.err('managed infrastructure native backing requires one postgres and only supported containerless services alongside it\n');
|
|
346
504
|
return 1;
|
|
347
505
|
}
|
|
506
|
+
/** The native backing's status: its containerless others serving and the postmaster's own proof (ownership, its
|
|
507
|
+
* live group and identity, its port answering), then the status line; undefined when either does not hold. */
|
|
508
|
+
const nativeStatus = async (): Promise<string | undefined> => {
|
|
509
|
+
const ready = others.length === 0 || (await pgliteStatus(others, worldData)).ok;
|
|
510
|
+
if (!ready || !await nativePostgresStatus(worldData, signal)) return undefined;
|
|
511
|
+
postgresData = join(worldData, 'native-postgres-data');
|
|
512
|
+
return `${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...backingFact('native'), ...databaseFacts() })}\n`;
|
|
513
|
+
};
|
|
348
514
|
try {
|
|
349
515
|
if (phase === 'up') {
|
|
350
|
-
io.out(`managed infrastructure backing: native PostgreSQL (multi-session)${forcedBy('native')}\n`);
|
|
351
|
-
await nativePostgresUp(postgres, worldData, readFileSync(definition, 'utf8'), launch);
|
|
516
|
+
io.out(`managed infrastructure backing: native PostgreSQL (multi-session)${forcedBy('native')} (${chosen})\n`);
|
|
517
|
+
await nativePostgresUp(postgres, worldData, readFileSync(definition, 'utf8'), { ...launch, signal: launch.signal ? AbortSignal.any([launch.signal, twinFailed.signal]) : twinFailed.signal, onPlaced: startTwinsBeside });
|
|
352
518
|
try { await pgliteUp(others, worldData, launch); }
|
|
353
519
|
catch (error) {
|
|
354
520
|
try { await nativePostgresDown(worldData); }
|
|
355
521
|
catch (cleanup) { throw new AggregateError([error, cleanup], 'managed infrastructure startup and native cleanup failed'); }
|
|
356
522
|
throw error;
|
|
357
523
|
}
|
|
358
|
-
|
|
524
|
+
nativeProof = nativeStatus;
|
|
525
|
+
sayReady();
|
|
359
526
|
} else if (phase === 'status') {
|
|
360
|
-
const
|
|
361
|
-
if (
|
|
362
|
-
|
|
363
|
-
io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...backingFact('native'), ...databaseFacts() })}\n`);
|
|
527
|
+
const text = await nativeStatus();
|
|
528
|
+
if (text === undefined) return 1;
|
|
529
|
+
io.out(text);
|
|
364
530
|
} else {
|
|
365
531
|
await pgliteDown(others, worldData);
|
|
366
|
-
await nativePostgresDown(worldData);
|
|
532
|
+
await nativePostgresDown(worldData, 'infra down');
|
|
533
|
+
worldEvent({ event: 'world.infra.down', backing: 'native', ms: Date.now() - phaseAsked });
|
|
367
534
|
io.out('managed infrastructure stopped\n');
|
|
368
535
|
}
|
|
369
536
|
return 0;
|
|
@@ -379,7 +546,7 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
379
546
|
if (forcedBy('docker')) io.out(`managed infrastructure backing: container runtime${forcedBy('docker')}\n`);
|
|
380
547
|
const result = await run(['up', '-d', '--wait', ...only]);
|
|
381
548
|
if (result.status !== 0) return fail(result);
|
|
382
|
-
|
|
549
|
+
sayReady();
|
|
383
550
|
} else if (phase === 'status') {
|
|
384
551
|
const expected = await run(['config', '--services']);
|
|
385
552
|
if (expected.status !== 0) return fail(expected);
|
|
@@ -398,4 +565,42 @@ export async function runInfrastructure(phase: string, env: NodeJS.ProcessEnv, i
|
|
|
398
565
|
io.out('managed infrastructure stopped\n');
|
|
399
566
|
}
|
|
400
567
|
return 0;
|
|
568
|
+
};
|
|
569
|
+
let code: number;
|
|
570
|
+
try { code = await databasePhase(); }
|
|
571
|
+
catch (error) {
|
|
572
|
+
if (phase === 'up' && mariaDB.length) {
|
|
573
|
+
try {
|
|
574
|
+
if (await runInfrastructure('down', env, { out: () => {}, err: io.err }) !== 0)
|
|
575
|
+
throw new Error('managed infrastructure failed-start teardown is unconfirmed');
|
|
576
|
+
} catch (cleanup) { throw new AggregateError([error, cleanup], 'native MariaDB sibling startup and retirement failed'); }
|
|
577
|
+
}
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
if (code !== 0 && phase === 'up' && mariaDB.length) {
|
|
581
|
+
try {
|
|
582
|
+
if (await runInfrastructure('down', env, { out: () => {}, err: io.err }) !== 0)
|
|
583
|
+
io.err('managed infrastructure failed-start teardown is unconfirmed; ownership retained\n');
|
|
584
|
+
} catch (cleanup) { io.err(`managed MariaDB failed-start retirement: ${diagnostic(errorMessage(cleanup))}\n`); }
|
|
585
|
+
}
|
|
586
|
+
if (!twinsPending) { if (code === 0) await handOver(); return code; }
|
|
587
|
+
twinsPending = false;
|
|
588
|
+
if (code !== 0) {
|
|
589
|
+
// A database that failed after its twins began: they finish starting or fail (each retires its own half-started
|
|
590
|
+
// child, twinBackingUp), then come down with the rest, as when a twin fails, so no twin's process or receipt stays.
|
|
591
|
+
if (twinsBeside) {
|
|
592
|
+
await twinsBeside;
|
|
593
|
+
try { await runInfrastructure('down', env, { out: () => {}, err: io.err }, signal); } catch { /* the failure above is the one to report */ }
|
|
594
|
+
}
|
|
595
|
+
return code;
|
|
596
|
+
}
|
|
597
|
+
const twinsCode = await (twinsBeside ?? twinsPhase());
|
|
598
|
+
if (twinsCode !== 0) {
|
|
599
|
+
// The database came up and a twin did not: nothing of this `up` is left serving.
|
|
600
|
+
try { await runInfrastructure('down', env, { out: () => {}, err: io.err }, signal); } catch { /* the failure above is the one to report */ }
|
|
601
|
+
return twinsCode;
|
|
602
|
+
}
|
|
603
|
+
await handOver();
|
|
604
|
+
if (readyHeld) io.out('managed infrastructure ready\n');
|
|
605
|
+
return 0;
|
|
401
606
|
}
|