pgserve 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/bin/pgserve-wrapper.cjs +9 -4
  2. package/bin/postgres-server.js +170 -631
  3. package/config/logrotate.d/pgserve +47 -0
  4. package/config/pgaudit.conf +31 -0
  5. package/package.json +3 -2
  6. package/scripts/audit-redaction-lint.js +349 -0
  7. package/scripts/test-npx.sh +32 -10
  8. package/src/audit/audit.js +134 -0
  9. package/src/cli-install.cjs +340 -100
  10. package/src/commands/uninstall.js +241 -0
  11. package/src/commands/verify.js +360 -0
  12. package/src/cosign/cache-token.js +328 -0
  13. package/src/cosign/schema.js +97 -0
  14. package/src/cosign/trust-list.js +81 -0
  15. package/src/cosign/verify-binary.js +277 -0
  16. package/src/index.js +11 -44
  17. package/src/lib/admin-json.js +202 -0
  18. package/src/lib/pm2-args.js +119 -0
  19. package/src/lib/runtime-json.js +181 -0
  20. package/src/lib/socket-dir.js +69 -0
  21. package/src/postgres.js +64 -5
  22. package/src/upgrade/index.js +5 -0
  23. package/src/upgrade/steps/cosign-meta-migration.js +123 -0
  24. package/src/admin-client.js +0 -223
  25. package/src/audit.js +0 -168
  26. package/src/cluster.js +0 -654
  27. package/src/control-db.js +0 -330
  28. package/src/daemon-control.js +0 -468
  29. package/src/daemon-shared.js +0 -18
  30. package/src/daemon-tcp.js +0 -297
  31. package/src/daemon.js +0 -709
  32. package/src/dashboard.js +0 -217
  33. package/src/fingerprint.js +0 -479
  34. package/src/gc.js +0 -351
  35. package/src/pg-wire.js +0 -869
  36. package/src/protocol.js +0 -389
  37. package/src/restore.js +0 -574
  38. package/src/router.js +0 -546
  39. package/src/sdk.js +0 -137
  40. package/src/stats-collector.js +0 -453
  41. package/src/stats-dashboard.js +0 -401
  42. package/src/sync.js +0 -335
  43. package/src/tenancy.js +0 -75
  44. package/src/tokens.js +0 -102
package/src/cluster.js DELETED
@@ -1,654 +0,0 @@
1
- /**
2
- * Cluster Mode for pgserve
3
- *
4
- * Architecture:
5
- * - PRIMARY process: Runs single embedded PostgreSQL instance
6
- * - WORKER processes: Only run TCP routing to PRIMARY's PostgreSQL
7
- *
8
- * This enables multi-core scaling (3-5x throughput on multi-core systems)
9
- * while maintaining a single PostgreSQL instance.
10
- */
11
-
12
- import cluster from 'cluster';
13
- import os from 'os';
14
- import { SQL } from 'bun';
15
- import { createLogger } from './logger.js';
16
- import { PostgresManager } from './postgres.js';
17
- import { extractDatabaseName } from './protocol.js';
18
- import { EventEmitter } from 'events';
19
- import { loadEffectiveConfig } from './settings-loader.cjs';
20
-
21
- // PostgreSQL protocol constants
22
- const PROTOCOL_VERSION_3 = 196608;
23
- const SSL_REQUEST_CODE = 80877103;
24
- const GSSAPI_REQUEST_CODE = 80877104;
25
- const CANCEL_REQUEST_CODE = 80877102;
26
-
27
- /**
28
- * Attempt to write a pending buffer to a target socket.
29
- * Returns remaining unwritten bytes, or null if fully flushed.
30
- */
31
- function flushPending(target, pending) {
32
- const written = target.write(pending);
33
- if (written === pending.byteLength) return null;
34
- if (written === 0) return pending;
35
- return pending.subarray(written);
36
- }
37
-
38
- // Stats collection constants
39
- const WORKER_STATS_TIMEOUT_MS = 10000; // Worker stats older than this are considered stale
40
- const WORKER_STATS_REPORT_INTERVAL_MS = 4000; // How often workers report stats to primary
41
-
42
- /**
43
- * ClusterRouter - Lightweight TCP router for worker processes
44
- * Does NOT start PostgreSQL - connects to PRIMARY's PostgreSQL via Unix socket
45
- */
46
- class ClusterRouter extends EventEmitter {
47
- constructor(options = {}) {
48
- super();
49
- this.port = options.port || 8432;
50
- this.host = options.host || '127.0.0.1';
51
- this.pgSocketPath = options.pgSocketPath; // From PRIMARY
52
- this.pgPort = options.pgPort;
53
- this.pgUser = options.pgUser || 'postgres';
54
- this.pgPassword = options.pgPassword || 'postgres';
55
- this.autoProvision = options.autoProvision !== false;
56
- this.maxConnections = options.maxConnections || 1000;
57
- this.enablePgvector = options.enablePgvector || false;
58
-
59
- this.logger = createLogger({ level: options.logLevel || 'info' });
60
- this.sql = null; // Bun.sql for admin queries
61
- this.server = null;
62
- this.connections = new Set();
63
- this.setMaxListeners(this.maxConnections + 10);
64
-
65
- // Connection stats tracking for IPC reporting
66
- this.connectionStats = {
67
- totalConnected: 0,
68
- totalDisconnected: 0
69
- };
70
- }
71
-
72
- /**
73
- * Socket state storage for Bun TCP handler model
74
- */
75
- socketState = new WeakMap();
76
-
77
- async start() {
78
- // Admin connection for auto-provisioning databases (Bun.sql)
79
- if (this.autoProvision) {
80
- // Bun.sql uses TCP connections - Unix sockets not directly supported
81
- // This is fine for admin queries (low volume, local connection)
82
- this.sql = new SQL({
83
- hostname: '127.0.0.1',
84
- port: this.pgPort,
85
- database: 'postgres',
86
- username: this.pgUser,
87
- password: this.pgPassword,
88
- max: 2, // Small pool for admin queries
89
- idleTimeout: 30,
90
- });
91
- }
92
-
93
- // Create TCP server using Bun.listen() for 2-3x throughput
94
- const router = this;
95
- const isWindows = os.platform() === 'win32';
96
- this.server = Bun.listen({
97
- hostname: this.host,
98
- port: this.port,
99
- reusePort: !isWindows, // SO_REUSEPORT for multi-worker port sharing (Linux/macOS only)
100
- socket: {
101
- data(socket, data) {
102
- router.handleSocketData(socket, data);
103
- },
104
- open(socket) {
105
- router.handleSocketOpen(socket);
106
- },
107
- close(socket) {
108
- router.handleSocketClose(socket);
109
- },
110
- error(socket, error) {
111
- router.handleSocketError(socket, error);
112
- },
113
- drain(socket) {
114
- const state = router.socketState.get(socket);
115
- if (!state) return;
116
- // Flush any pending PG→Client data
117
- if (state.pendingToClient) {
118
- state.pendingToClient = flushPending(socket, state.pendingToClient);
119
- }
120
- // If fully flushed, resume reading from PostgreSQL
121
- if (!state.pendingToClient && state.pgSocket) {
122
- state.pgSocket.resume();
123
- }
124
- }
125
- }
126
- });
127
-
128
- // Verify port actually bound (detect silent failures on Windows)
129
- if (!this.server || !this.server.port) {
130
- throw new Error(`Failed to bind to port ${this.port} - reusePort may not be supported on this platform`);
131
- }
132
-
133
- this.emit('listening');
134
- }
135
-
136
- async createDatabase(dbName) {
137
- if (!this.autoProvision || !this.sql) return;
138
-
139
- try {
140
- // Bun.sql uses tagged template literals for parameterized queries
141
- const result = await this.sql`SELECT 1 FROM pg_database WHERE datname = ${dbName}`;
142
-
143
- if (result.length === 0) {
144
- // Use sql() helper for safe identifier escaping (like CREATE DATABASE)
145
- await this.sql.unsafe(`CREATE DATABASE "${dbName.replace(/"/g, '""')}"`);
146
-
147
- // Auto-enable pgvector extension if configured
148
- if (this.enablePgvector) {
149
- await this.enablePgvectorExtension(dbName);
150
- }
151
- }
152
- } catch (error) {
153
- // Ignore "already exists" (race condition between workers)
154
- if (!error.message?.includes('already exists')) {
155
- this.logger.error({ database: dbName, err: error }, 'Failed to create database');
156
- }
157
- }
158
- }
159
-
160
- /**
161
- * Enable pgvector extension on a database
162
- * Creates a temporary connection to the specific database to run CREATE EXTENSION
163
- * @param {string} dbName - Database name to enable pgvector on
164
- */
165
- async enablePgvectorExtension(dbName) {
166
- let dbPool = null;
167
-
168
- try {
169
- // Create temporary connection to the specific database
170
- dbPool = new SQL({
171
- hostname: '127.0.0.1',
172
- port: this.pgPort,
173
- database: dbName,
174
- username: this.pgUser,
175
- password: this.pgPassword,
176
- max: 1,
177
- idleTimeout: 5,
178
- connectionTimeout: 5,
179
- });
180
-
181
- // Enable pgvector extension
182
- await dbPool.unsafe('CREATE EXTENSION IF NOT EXISTS vector');
183
- this.logger.info({ dbName }, 'pgvector extension enabled');
184
- } catch (error) {
185
- // Log but don't fail database creation - pgvector might not be available
186
- this.logger.warn({ dbName, err: error.message }, 'Failed to enable pgvector extension (non-fatal)');
187
- } finally {
188
- // Always close the temporary connection
189
- if (dbPool) {
190
- await dbPool.close().catch(() => {});
191
- }
192
- }
193
- }
194
-
195
- /**
196
- * Handle socket open (Bun TCP handler)
197
- */
198
- handleSocketOpen(socket) {
199
- this.socketState.set(socket, {
200
- buffer: null,
201
- pgSocket: null,
202
- dbName: null,
203
- handshakeComplete: false,
204
- pendingToPg: null,
205
- pendingToClient: null
206
- });
207
- this.connections.add(socket);
208
- this.connectionStats.totalConnected++;
209
- }
210
-
211
- /**
212
- * Handle socket data (Bun TCP handler)
213
- */
214
- handleSocketData(socket, data) {
215
- const state = this.socketState.get(socket);
216
- if (!state) return;
217
-
218
- // If handshake complete, forward to PostgreSQL
219
- if (state.handshakeComplete && state.pgSocket) {
220
- // If there's already pending data, append to it
221
- if (state.pendingToPg) {
222
- state.pendingToPg = Buffer.concat([state.pendingToPg, data]);
223
- return;
224
- }
225
- const written = state.pgSocket.write(data);
226
- if (written < data.byteLength) {
227
- // Partial write — buffer remainder and pause client
228
- state.pendingToPg = written === 0 ? Buffer.from(data) : Buffer.from(data.subarray(written));
229
- socket.pause();
230
- }
231
- return;
232
- }
233
-
234
- // Buffer data for startup message parsing
235
- if (state.buffer) {
236
- state.buffer = Buffer.concat([state.buffer, data]);
237
- } else {
238
- state.buffer = Buffer.from(data);
239
- }
240
-
241
- this.processStartupMessage(socket, state);
242
- }
243
-
244
- /**
245
- * Process PostgreSQL startup message and establish proxy connection
246
- */
247
- async processStartupMessage(socket, state) {
248
- const buffer = state.buffer;
249
- if (!buffer || buffer.length < 8) return;
250
-
251
- const messageLength = buffer.readUInt32BE(0);
252
- if (buffer.length < messageLength) return;
253
-
254
- const code = buffer.readUInt32BE(4);
255
-
256
- // Handle SSL/GSSAPI/Cancel requests
257
- if (code === SSL_REQUEST_CODE || code === GSSAPI_REQUEST_CODE) {
258
- socket.write(Buffer.from('N'));
259
- state.buffer = buffer.length > messageLength ? buffer.subarray(messageLength) : null;
260
- if (state.buffer) await this.processStartupMessage(socket, state);
261
- return;
262
- }
263
-
264
- if (code === CANCEL_REQUEST_CODE) {
265
- socket.end();
266
- return;
267
- }
268
-
269
- if (code !== PROTOCOL_VERSION_3) {
270
- this.logger.warn({ code }, 'Unsupported protocol version');
271
- socket.end();
272
- return;
273
- }
274
-
275
- const startupMessage = buffer.subarray(0, messageLength);
276
- const dbName = extractDatabaseName(startupMessage);
277
- state.dbName = dbName;
278
-
279
- try {
280
- await this.createDatabase(dbName);
281
-
282
- const router = this;
283
-
284
- // Shared handler for pgSocket (used by both unix and TCP paths)
285
- const pgHandler = {
286
- data(_pgSocket, pgData) {
287
- // Forward PostgreSQL response to client with backpressure
288
- if (state.pendingToClient) {
289
- state.pendingToClient = Buffer.concat([state.pendingToClient, pgData]);
290
- return;
291
- }
292
- const written = socket.write(pgData);
293
- if (written < pgData.byteLength) {
294
- state.pendingToClient = written === 0 ? Buffer.from(pgData) : Buffer.from(pgData.subarray(written));
295
- _pgSocket.pause();
296
- }
297
- },
298
- open(pgSocket) {
299
- pgSocket.write(startupMessage);
300
- state.handshakeComplete = true;
301
- },
302
- close(_pgSocket) {
303
- socket.end();
304
- },
305
- error(_pgSocket, error) {
306
- router.logger.error({ dbName, err: error }, 'PostgreSQL socket error');
307
- socket.end();
308
- },
309
- drain(_pgSocket) {
310
- // Flush any pending Client→PG data
311
- if (state.pendingToPg) {
312
- state.pendingToPg = flushPending(_pgSocket, state.pendingToPg);
313
- }
314
- // If fully flushed, resume reading from client
315
- if (!state.pendingToPg) {
316
- socket.resume();
317
- }
318
- }
319
- };
320
-
321
- if (this.pgSocketPath) {
322
- state.pgSocket = await Bun.connect({ unix: this.pgSocketPath, socket: pgHandler });
323
- } else {
324
- state.pgSocket = await Bun.connect({ hostname: '127.0.0.1', port: this.pgPort, socket: pgHandler });
325
- }
326
- } catch (error) {
327
- this.logger.error({ dbName, err: error }, 'Connection error');
328
- socket.end();
329
- }
330
- }
331
-
332
- /**
333
- * Handle socket close (Bun TCP handler)
334
- */
335
- handleSocketClose(socket) {
336
- const state = this.socketState.get(socket);
337
- if (state) {
338
- state.pendingToPg = null;
339
- state.pendingToClient = null;
340
- if (state.pgSocket) state.pgSocket.end();
341
- }
342
- this.connections.delete(socket);
343
- this.socketState.delete(socket);
344
- this.connectionStats.totalDisconnected++;
345
- }
346
-
347
- /**
348
- * Get router stats for IPC reporting
349
- */
350
- getStats() {
351
- return {
352
- connections: this.connections.size,
353
- totalConnected: this.connectionStats.totalConnected,
354
- totalDisconnected: this.connectionStats.totalDisconnected,
355
- pid: process.pid
356
- };
357
- }
358
-
359
- /**
360
- * Handle socket error (Bun TCP handler)
361
- */
362
- handleSocketError(socket, error) {
363
- const state = this.socketState.get(socket);
364
- if (error.code !== 'ECONNRESET') {
365
- this.logger.error({ err: error, dbName: state?.dbName }, 'Socket error');
366
- }
367
- if (state) {
368
- state.pendingToPg = null;
369
- state.pendingToClient = null;
370
- if (state.pgSocket) state.pgSocket.end();
371
- }
372
- this.connections.delete(socket);
373
- this.socketState.delete(socket);
374
- }
375
-
376
- async stop() {
377
- for (const socket of this.connections) {
378
- socket.end();
379
- }
380
- this.connections.clear();
381
-
382
- if (this.sql) {
383
- try {
384
- await this.sql.close();
385
- } catch {
386
- // Expected: connection may already be terminated during cleanup
387
- }
388
- }
389
-
390
- // Close TCP server (Bun.listen returns a server with stop() method)
391
- if (this.server) {
392
- this.server.stop();
393
- }
394
- }
395
- }
396
-
397
-
398
- /**
399
- * Build a `backendExited` handler for cluster mode supervision.
400
- *
401
- * On unexpected exit (`expected: false`) — postgres SIGKILL'd, OOM-killed,
402
- * segfaulted, etc. — the handler:
403
- * 1. logs the exit code,
404
- * 2. flips `shuttingDown` so the cluster.on('exit') worker-respawn path
405
- * no longer forks new workers,
406
- * 3. SIGTERMs every live worker (no point routing to a dead backend), and
407
- * 4. calls `exitFn(1)` so the parent process supervisor restarts us.
408
- *
409
- * On clean exit (`expected: true`, initiated by `pgManager.stop()`) the
410
- * handler is silent — the surrounding shutdown logic handles teardown.
411
- *
412
- * Exported so the supervision contract can be unit-tested without spawning
413
- * a real cluster (the integration path is already covered for single-process
414
- * mode by tests/wrapper-supervision.test.js).
415
- *
416
- * @param {object} args
417
- * @param {Map<number, {kill: (sig: string) => void}>} args.workers - live worker registry
418
- * @param {(v: boolean) => void} args.setShuttingDown - flips outer-scope `shuttingDown`
419
- * @param {(code: number) => void} [args.exitFn=process.exit] - test seam
420
- * @param {(...args: unknown[]) => void} [args.log=console.error] - test seam
421
- * @returns {(info: {code: number, expected: boolean}) => void}
422
- */
423
- export function buildClusterSupervisionHandler({
424
- workers,
425
- setShuttingDown,
426
- exitFn = process.exit,
427
- log = console.error,
428
- }) {
429
- return ({ code, expected }) => {
430
- if (expected) return;
431
- log(
432
- `[pgserve] postgres backend exited unexpectedly (code=${code}) in cluster mode; ` +
433
- `the primary is exiting so a process supervisor can restart it.`
434
- );
435
- setShuttingDown(true);
436
- for (const worker of workers.values()) {
437
- try { worker.kill('SIGTERM'); } catch { /* worker may already be dead */ }
438
- }
439
- exitFn(1);
440
- };
441
- }
442
-
443
- /**
444
- * Start pgserve in cluster mode
445
- */
446
- export async function startClusterServer(options = {}) {
447
- const numWorkers = options.workers || os.cpus().length;
448
- const port = options.port || 8432;
449
- const host = options.host || '127.0.0.1';
450
- const pgPort = options.pgPort || (port + 1000);
451
-
452
- if (cluster.isPrimary) {
453
- // Port binding happens in workers via Bun.listen with reusePort
454
- // If port is in use, first worker will fail with EADDRINUSE
455
- console.log(`[pgserve] Cluster mode: ${numWorkers} workers`);
456
-
457
- // PRIMARY: Start our embedded PostgreSQL (single instance)
458
- const logger = createLogger({ level: options.logLevel || 'info' });
459
- const pgManager = new PostgresManager({
460
- dataDir: options.baseDir,
461
- port: pgPort,
462
- logger: logger.child({ component: 'postgres' }),
463
- useRam: options.useRam, // Use /dev/shm for true RAM storage (Linux only)
464
- enablePgvector: options.enablePgvector // Auto-enable pgvector extension on new databases
465
- });
466
-
467
- await pgManager.start();
468
- const pgSocketPath = pgManager.getSocketPath();
469
-
470
- console.log(`[pgserve] Embedded PostgreSQL started`);
471
- console.log(`[pgserve] Socket: ${pgSocketPath || `TCP port ${pgPort}`}`);
472
-
473
- // Track shutdown state and worker registry early so the supervision
474
- // handler below can tear workers down on unexpected backend death.
475
- let shuttingDown = false;
476
- const workers = new Map();
477
- const workerStats = new Map(); // Track stats from each worker
478
-
479
- // Supervision: when the embedded postgres backend dies unexpectedly
480
- // (SIGKILL/OOM/segfault — anything other than a clean stop()), exit the
481
- // primary so a process supervisor (`genie serve`, pm2, systemd) restarts
482
- // the cluster cleanly with a fresh backend. Without this, the primary
483
- // keeps running with a zombie pgManager (socketDir nulled) and every
484
- // worker fails StartupMessages with "Connection closed" forever while
485
- // pm2 reports the process as healthy. Mirrors the single-process fix
486
- // in bin/postgres-server.js (PgserveDaemon.on('backendDiedUnexpectedly'))
487
- // — pgserve#45 only protected the daemon path, not cluster mode (default
488
- // on multi-core systems).
489
- pgManager.on('backendExited', buildClusterSupervisionHandler({
490
- workers,
491
- setShuttingDown: (v) => { shuttingDown = v; },
492
- }));
493
-
494
- // Fork workers with PostgreSQL connection info.
495
- //
496
- // Pass through using AUTOPG_<X> (the primary names) so workers don't
497
- // emit the legacy-PGSERVE deprecation log for our own internal IPC.
498
- // PGSERVE_WORKER stays as-is — it's an internal flag, not part of the
499
- // settings precedence chain.
500
- const workerEnv = () => ({
501
- PGSERVE_WORKER: 'true',
502
- AUTOPG_PORT: String(port),
503
- AUTOPG_HOST: host,
504
- AUTOPG_PG_SOCKET: pgSocketPath || '',
505
- AUTOPG_PG_PORT: String(pgPort),
506
- AUTOPG_PG_USER: 'postgres',
507
- AUTOPG_PG_PASSWORD: 'postgres',
508
- AUTOPG_LOG_LEVEL: options.logLevel || 'info',
509
- AUTOPG_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false',
510
- // max_connections is a postgres GUC, not a server-level env. Workers
511
- // read it from settings.postgres via loadEffectiveConfig; we still
512
- // ship the value here so a CLI override (e.g. `--max-connections`)
513
- // reaches the worker before the daemon writes settings.json.
514
- AUTOPG_MAX_CONNECTIONS: String(options.maxConnections || 1000),
515
- AUTOPG_ENABLE_PGVECTOR: options.enablePgvector ? 'true' : 'false',
516
- });
517
- for (let i = 0; i < numWorkers; i++) {
518
- const worker = cluster.fork(workerEnv());
519
- workers.set(worker.id, worker);
520
- }
521
-
522
- // (shuttingDown declared above with the supervision handler.)
523
-
524
- // Restart dead workers (unless shutting down)
525
- cluster.on('exit', (worker, code, signal) => {
526
- workers.delete(worker.id);
527
-
528
- if (shuttingDown) {
529
- return; // Don't restart during shutdown
530
- }
531
-
532
- console.log(`[pgserve] Worker ${worker.id} died (${signal || code}), restarting...`);
533
- const newWorker = cluster.fork(workerEnv());
534
- workers.set(newWorker.id, newWorker);
535
- });
536
-
537
- // Wait for workers to be ready and handle IPC messages
538
- let readyCount = 0;
539
- await new Promise((resolve) => {
540
- cluster.on('message', (worker, message) => {
541
- if (message.type === 'ready') {
542
- readyCount++;
543
- if (readyCount === numWorkers) resolve();
544
- } else if (message.type === 'stats') {
545
- // Update worker stats from IPC
546
- workerStats.set(worker.id, {
547
- ...message.data,
548
- lastUpdate: Date.now()
549
- });
550
- }
551
- });
552
- });
553
-
554
- console.log(`[pgserve] All ${numWorkers} workers ready`);
555
- console.log(`[pgserve] Listening on ${host}:${port}`);
556
-
557
- return {
558
- workers,
559
- pgPort,
560
- pgSocketPath,
561
- stop: async () => {
562
- console.log('[pgserve] Stopping cluster...');
563
- shuttingDown = true; // Prevent worker restart during shutdown
564
- for (const worker of workers.values()) {
565
- worker.send({ type: 'shutdown' });
566
- }
567
- await new Promise((resolve) => {
568
- const check = setInterval(() => {
569
- if (workers.size === 0) {
570
- clearInterval(check);
571
- resolve();
572
- }
573
- }, 100);
574
- });
575
- await pgManager.stop();
576
- console.log('[pgserve] Cluster stopped');
577
- },
578
- getStats: () => {
579
- // Aggregate stats from all workers
580
- let totalConnections = 0;
581
- let totalConnected = 0;
582
- let totalDisconnected = 0;
583
- const activeWorkerStats = {};
584
-
585
- for (const [id, stats] of workerStats) {
586
- // Only include recent stats (within timeout window)
587
- if (Date.now() - stats.lastUpdate < WORKER_STATS_TIMEOUT_MS) {
588
- totalConnections += stats.connections || 0;
589
- totalConnected += stats.totalConnected || 0;
590
- totalDisconnected += stats.totalDisconnected || 0;
591
- activeWorkerStats[id] = stats;
592
- }
593
- }
594
-
595
- return {
596
- workers: workers.size,
597
- pids: Array.from(workers.values()).map(w => w.process.pid),
598
- connections: {
599
- active: totalConnections,
600
- totalConnected,
601
- totalDisconnected
602
- },
603
- workerStats: activeWorkerStats
604
- };
605
- },
606
- pgManager
607
- };
608
- } else {
609
- // WORKER: Only run TCP routing, connect to PRIMARY's PostgreSQL.
610
- //
611
- // Worker config comes from loadEffectiveConfig() (defaults < file < env).
612
- // The primary fork() above sets PGSERVE_* env vars so existing supervised
613
- // installs keep working; AUTOPG_* env vars take precedence when set, and
614
- // a one-time deprecation note is emitted for legacy-only PGSERVE_* hits.
615
- const { settings } = loadEffectiveConfig();
616
- const router = new ClusterRouter({
617
- port: settings.server.port,
618
- host: settings.server.host,
619
- pgSocketPath: settings.server.pgSocketPath || null,
620
- pgPort: settings.server.pgPort,
621
- pgUser: settings.server.pgUser,
622
- pgPassword: settings.server.pgPassword,
623
- logLevel: settings.runtime.logLevel,
624
- autoProvision: settings.runtime.autoProvision,
625
- maxConnections: settings.postgres.max_connections,
626
- enablePgvector: settings.runtime.enablePgvector,
627
- });
628
-
629
- await router.start();
630
-
631
- // Tell PRIMARY we're ready
632
- process.send({ type: 'ready' });
633
-
634
- // Periodically send stats to PRIMARY
635
- const statsInterval = setInterval(() => {
636
- try {
637
- process.send({ type: 'stats', data: router.getStats() });
638
- } catch {
639
- // Expected: IPC channel may be closed during shutdown
640
- }
641
- }, WORKER_STATS_REPORT_INTERVAL_MS);
642
-
643
- // Handle shutdown
644
- process.on('message', async (message) => {
645
- if (message.type === 'shutdown') {
646
- clearInterval(statsInterval);
647
- await router.stop();
648
- process.exit(0);
649
- }
650
- });
651
-
652
- return router;
653
- }
654
- }