pgserve 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -260,46 +260,140 @@ pgserve --sync-to "postgresql://..." --sync-databases "myapp,tenant_*"
260
260
 
261
261
  ## Performance
262
262
 
263
- ### Benchmark Results
263
+ ### CRUD Benchmarks
264
264
 
265
265
  <table>
266
266
  <tr>
267
267
  <th>Scenario</th>
268
268
  <th>SQLite</th>
269
269
  <th>PGlite</th>
270
- <th>pgserve (Node)</th>
271
- <th>pgserve (Bun)</th>
270
+ <th>PostgreSQL</th>
271
+ <th>pgserve</th>
272
272
  <th>pgserve --ram</th>
273
273
  </tr>
274
274
  <tr>
275
275
  <td><b>Concurrent Writes</b> (10 agents)</td>
276
- <td>85 qps</td>
277
- <td>211 qps</td>
278
- <td>833 qps</td>
279
- <td>1,754 qps</td>
280
- <td><b>4,000 qps</b> 🏆</td>
276
+ <td>91 qps</td>
277
+ <td>204 qps</td>
278
+ <td>1,667 qps</td>
279
+ <td>2,273 qps</td>
280
+ <td><b>4,167 qps</b> 🏆</td>
281
281
  </tr>
282
282
  <tr>
283
283
  <td><b>Mixed Workload</b></td>
284
- <td>339 qps</td>
285
- <td>505 qps</td>
286
- <td>1,017 qps</td>
287
- <td>999 qps</td>
288
- <td><b>1,986 qps</b> 🏆</td>
284
+ <td>383 qps</td>
285
+ <td>484 qps</td>
286
+ <td>507 qps</td>
287
+ <td>1,133 qps</td>
288
+ <td><b>2,109 qps</b> 🏆</td>
289
289
  </tr>
290
290
  <tr>
291
291
  <td><b>Write Lock</b> (50 writers)</td>
292
- <td>86 qps</td>
293
- <td>168 qps</td>
294
- <td>488 qps</td>
295
- <td>1,176 qps</td>
292
+ <td>111 qps</td>
293
+ <td>228 qps</td>
294
+ <td>2,857 qps</td>
295
+ <td>3,030 qps</td>
296
296
  <td><b>4,348 qps</b> 🏆</td>
297
297
  </tr>
298
298
  </table>
299
299
 
300
- > pgserve uses Bun runtime internally for 2x throughput. **RAM mode adds another 2-4x** on Linux.
300
+ ### Vector Benchmarks (pgvector)
301
+
302
+ <table>
303
+ <tr>
304
+ <th>Metric</th>
305
+ <th>PGlite</th>
306
+ <th>PostgreSQL</th>
307
+ <th>pgserve</th>
308
+ <th>pgserve --ram</th>
309
+ </tr>
310
+ <tr>
311
+ <td><b>Vector INSERT</b> (1000 × 1536-dim)</td>
312
+ <td>152/sec</td>
313
+ <td>392/sec</td>
314
+ <td>387/sec</td>
315
+ <td><b>1,082/sec</b> 🏆</td>
316
+ </tr>
317
+ <tr>
318
+ <td><b>k-NN Search</b> (k=10, 10k corpus)</td>
319
+ <td>22 qps</td>
320
+ <td>33 qps</td>
321
+ <td>31 qps</td>
322
+ <td>30 qps</td>
323
+ </tr>
324
+ <tr>
325
+ <td><b>Recall@10</b></td>
326
+ <td>100%</td>
327
+ <td>100%</td>
328
+ <td>100%</td>
329
+ <td>100%</td>
330
+ </tr>
331
+ </table>
332
+
333
+ > <b>Why pgserve wins on writes:</b> RAM mode uses <code>/dev/shm</code> (tmpfs), eliminating fsync latency. Vector search is CPU-bound, so RAM mode shows minimal benefit there.
334
+
335
+ ### Final Score
336
+
337
+ <table>
338
+ <tr>
339
+ <th>Engine</th>
340
+ <th>CRUD QPS</th>
341
+ <th>Vec QPS</th>
342
+ <th>Recall</th>
343
+ <th>P50</th>
344
+ <th>P99</th>
345
+ <th>Score</th>
346
+ </tr>
347
+ <tr>
348
+ <td>SQLite</td>
349
+ <td>195</td>
350
+ <td>N/A</td>
351
+ <td>N/A</td>
352
+ <td>6.3ms</td>
353
+ <td>17.3ms</td>
354
+ <td>117</td>
355
+ </tr>
356
+ <tr>
357
+ <td>PGlite</td>
358
+ <td>305</td>
359
+ <td>65</td>
360
+ <td>100%</td>
361
+ <td>3.3ms</td>
362
+ <td>7.0ms</td>
363
+ <td>209</td>
364
+ </tr>
365
+ <tr>
366
+ <td>PostgreSQL</td>
367
+ <td>1,677</td>
368
+ <td>152</td>
369
+ <td>100%</td>
370
+ <td>6.0ms</td>
371
+ <td>19.0ms</td>
372
+ <td>1,067</td>
373
+ </tr>
374
+ <tr>
375
+ <td>pgserve</td>
376
+ <td>2,145</td>
377
+ <td>149</td>
378
+ <td>100%</td>
379
+ <td>5.3ms</td>
380
+ <td>13.0ms</td>
381
+ <td>1,347</td>
382
+ </tr>
383
+ <tr>
384
+ <td><b>pgserve --ram</b></td>
385
+ <td><b>3,541</b></td>
386
+ <td><b>381</b></td>
387
+ <td><b>100%</b></td>
388
+ <td><b>3.3ms</b></td>
389
+ <td><b>10.7ms</b></td>
390
+ <td><b>2,277</b> 🏆</td>
391
+ </tr>
392
+ </table>
393
+
394
+ > <b>Methodology:</b> Recall@k measured against brute-force ground truth (industry standard). PostgreSQL baseline is Docker <code>pgvector/pgvector:pg17</code>. RAM mode available on Linux and WSL2.
301
395
  >
302
- > Run benchmarks: `bun run bench` (contributors only)
396
+ > Run benchmarks yourself: <code>bun tests/benchmarks/runner.js --include-vector</code>
303
397
 
304
398
  <br>
305
399
 
@@ -53,6 +53,8 @@ OPTIONS:
53
53
  --no-provision Disable auto-provisioning of databases
54
54
  --sync-to <url> Sync to real PostgreSQL (async replication)
55
55
  --sync-databases Database patterns to sync (comma-separated, e.g. "myapp,tenant_*")
56
+ --no-stats Disable real-time stats dashboard (enabled by default)
57
+ --max-connections Max concurrent connections (default: 1000)
56
58
  --help Show this help message
57
59
 
58
60
  MODES:
@@ -108,7 +110,9 @@ function parseArgs() {
108
110
  cluster: cpuCount > 1 && !isWindows, // Auto-enable on multi-core (disabled on Windows - no SO_REUSEPORT)
109
111
  workers: null, // null = use CPU count
110
112
  syncTo: null, // Sync target PostgreSQL URL
111
- syncDatabases: null // Database patterns to sync (comma-separated)
113
+ syncDatabases: null, // Database patterns to sync (comma-separated)
114
+ showStats: true, // Show real-time stats dashboard (default: enabled)
115
+ maxConnections: 1000 // Max concurrent connections (high default for multi-tenant)
112
116
  };
113
117
 
114
118
  for (let i = 0; i < args.length; i++) {
@@ -163,6 +167,18 @@ function parseArgs() {
163
167
  options.syncDatabases = args[++i];
164
168
  break;
165
169
 
170
+ case '--stats':
171
+ options.showStats = true;
172
+ break;
173
+
174
+ case '--no-stats':
175
+ options.showStats = false;
176
+ break;
177
+
178
+ case '--max-connections':
179
+ options.maxConnections = parseInt(args[++i], 10);
180
+ break;
181
+
166
182
  case '--help':
167
183
  case 'help':
168
184
  printHelp();
@@ -211,7 +227,8 @@ pgserve - Embedded PostgreSQL Server
211
227
  useRam: options.useRam,
212
228
  logLevel: options.logLevel,
213
229
  autoProvision: options.autoProvision,
214
- workers: options.workers
230
+ workers: options.workers,
231
+ maxConnections: options.maxConnections
215
232
  });
216
233
 
217
234
  // Only primary process shows full startup message
@@ -222,7 +239,7 @@ pgserve - Embedded PostgreSQL Server
222
239
  Cluster started successfully!
223
240
 
224
241
  Endpoint: postgresql://${options.host}:${options.port}/<database>
225
- Mode: ${memoryMode ? 'In-memory (ephemeral)' : 'Persistent'} (Cluster)
242
+ Mode: ${memoryMode ? (options.useRam ? 'RAM (/dev/shm)' : 'Ephemeral (temp)') : 'Persistent'} (Cluster)
226
243
  Workers: ${stats.workers} processes
227
244
  Data: ${storageType}
228
245
  Auto-create: ${options.autoProvision ? 'Enabled' : 'Disabled'}
@@ -244,7 +261,8 @@ Press Ctrl+C to stop
244
261
  logLevel: options.logLevel,
245
262
  autoProvision: options.autoProvision,
246
263
  syncTo: options.syncTo,
247
- syncDatabases: options.syncDatabases
264
+ syncDatabases: options.syncDatabases,
265
+ maxConnections: options.maxConnections
248
266
  });
249
267
 
250
268
  server = router;
@@ -258,7 +276,7 @@ Press Ctrl+C to stop
258
276
  Server started successfully!
259
277
 
260
278
  Endpoint: postgresql://${options.host}:${options.port}/<database>
261
- Mode: ${memoryMode ? 'In-memory (ephemeral)' : 'Persistent'}
279
+ Mode: ${memoryMode ? (options.useRam ? 'RAM (/dev/shm)' : 'Ephemeral (temp)') : 'Persistent'}
262
280
  Data: ${storageType}
263
281
  PostgreSQL: Port ${router.pgPort} (internal)
264
282
  Auto-create: ${options.autoProvision ? 'Enabled' : 'Disabled'}
@@ -272,12 +290,45 @@ Press Ctrl+C to stop
272
290
  `);
273
291
  }
274
292
 
293
+ // Start stats dashboard if requested (only for primary/single-process)
294
+ let dashboard = null;
295
+ if (options.showStats && !process.env.PGSERVE_WORKER) {
296
+ const { StatsDashboard } = await import('../src/stats-dashboard.js');
297
+ const { StatsCollector } = await import('../src/stats-collector.js');
298
+
299
+ // Create stats collector with appropriate sources
300
+ const collector = new StatsCollector({
301
+ router: options.cluster ? null : server,
302
+ pgManager: server.pgManager,
303
+ clusterStats: options.cluster ? () => server.getStats() : null,
304
+ logger: server.logger,
305
+ port: options.port,
306
+ host: options.host
307
+ });
308
+
309
+ dashboard = new StatsDashboard({
310
+ refreshInterval: 2000, // 2 second refresh for real-time feel
311
+ statsProvider: () => collector.collect()
312
+ });
313
+
314
+ dashboard.start();
315
+ }
316
+
275
317
  // Graceful shutdown (only for primary/single-process, workers handle via IPC)
276
318
  if (!process.env.PGSERVE_WORKER) {
277
319
  const shutdown = async () => {
320
+ // Stop dashboard first to restore cursor
321
+ if (dashboard) {
322
+ dashboard.stop();
323
+ }
278
324
  console.log('\nShutting down...');
279
- await server.stop();
280
- console.log('Server stopped.');
325
+ try {
326
+ await server.stop();
327
+ console.log('Server stopped.');
328
+ } catch (err) {
329
+ console.error('Error during shutdown:', err.message);
330
+ // Still exit - best effort cleanup
331
+ }
281
332
  process.exit(0);
282
333
  };
283
334
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgserve",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Embedded PostgreSQL server with true concurrent connections - zero config, auto-provision databases",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/cluster.js CHANGED
@@ -23,6 +23,10 @@ const SSL_REQUEST_CODE = 80877103;
23
23
  const GSSAPI_REQUEST_CODE = 80877104;
24
24
  const CANCEL_REQUEST_CODE = 80877102;
25
25
 
26
+ // Stats collection constants
27
+ const WORKER_STATS_TIMEOUT_MS = 10000; // Worker stats older than this are considered stale
28
+ const WORKER_STATS_REPORT_INTERVAL_MS = 4000; // How often workers report stats to primary
29
+
26
30
  /**
27
31
  * ClusterRouter - Lightweight TCP router for worker processes
28
32
  * Does NOT start PostgreSQL - connects to PRIMARY's PostgreSQL via Unix socket
@@ -44,6 +48,12 @@ class ClusterRouter extends EventEmitter {
44
48
  this.server = null;
45
49
  this.connections = new Set();
46
50
  this.setMaxListeners(this.maxConnections + 10);
51
+
52
+ // Connection stats tracking for IPC reporting
53
+ this.connectionStats = {
54
+ totalConnected: 0,
55
+ totalDisconnected: 0
56
+ };
47
57
  }
48
58
 
49
59
  /**
@@ -134,6 +144,7 @@ class ClusterRouter extends EventEmitter {
134
144
  handshakeComplete: false
135
145
  });
136
146
  this.connections.add(socket);
147
+ this.connectionStats.totalConnected++;
137
148
  }
138
149
 
139
150
  /**
@@ -259,6 +270,19 @@ class ClusterRouter extends EventEmitter {
259
270
  }
260
271
  this.connections.delete(socket);
261
272
  this.socketState.delete(socket);
273
+ this.connectionStats.totalDisconnected++;
274
+ }
275
+
276
+ /**
277
+ * Get router stats for IPC reporting
278
+ */
279
+ getStats() {
280
+ return {
281
+ connections: this.connections.size,
282
+ totalConnected: this.connectionStats.totalConnected,
283
+ totalDisconnected: this.connectionStats.totalDisconnected,
284
+ pid: process.pid
285
+ };
262
286
  }
263
287
 
264
288
  /**
@@ -286,7 +310,7 @@ class ClusterRouter extends EventEmitter {
286
310
  try {
287
311
  await this.sql.close();
288
312
  } catch {
289
- // Ignore - connection may already be terminated
313
+ // Expected: connection may already be terminated during cleanup
290
314
  }
291
315
  }
292
316
 
@@ -297,6 +321,7 @@ class ClusterRouter extends EventEmitter {
297
321
  }
298
322
  }
299
323
 
324
+
300
325
  /**
301
326
  * Start pgserve in cluster mode
302
327
  */
@@ -307,6 +332,8 @@ export async function startClusterServer(options = {}) {
307
332
  const pgPort = options.pgPort || (port + 1000);
308
333
 
309
334
  if (cluster.isPrimary) {
335
+ // Port binding happens in workers via Bun.listen with reusePort
336
+ // If port is in use, first worker will fail with EADDRINUSE
310
337
  console.log(`[pgserve] Cluster mode: ${numWorkers} workers`);
311
338
 
312
339
  // PRIMARY: Start our embedded PostgreSQL (single instance)
@@ -325,6 +352,7 @@ export async function startClusterServer(options = {}) {
325
352
  console.log(`[pgserve] Socket: ${pgSocketPath || `TCP port ${pgPort}`}`);
326
353
 
327
354
  const workers = new Map();
355
+ const workerStats = new Map(); // Track stats from each worker
328
356
 
329
357
  // Fork workers with PostgreSQL connection info
330
358
  for (let i = 0; i < numWorkers; i++) {
@@ -337,7 +365,8 @@ export async function startClusterServer(options = {}) {
337
365
  PGSERVE_PG_USER: 'postgres',
338
366
  PGSERVE_PG_PASSWORD: 'postgres',
339
367
  PGSERVE_LOG_LEVEL: options.logLevel || 'info',
340
- PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false'
368
+ PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false',
369
+ PGSERVE_MAX_CONNECTIONS: String(options.maxConnections || 1000)
341
370
  });
342
371
  workers.set(worker.id, worker);
343
372
  }
@@ -363,18 +392,25 @@ export async function startClusterServer(options = {}) {
363
392
  PGSERVE_PG_USER: 'postgres',
364
393
  PGSERVE_PG_PASSWORD: 'postgres',
365
394
  PGSERVE_LOG_LEVEL: options.logLevel || 'info',
366
- PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false'
395
+ PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false',
396
+ PGSERVE_MAX_CONNECTIONS: String(options.maxConnections || 1000)
367
397
  });
368
398
  workers.set(newWorker.id, newWorker);
369
399
  });
370
400
 
371
- // Wait for workers to be ready
401
+ // Wait for workers to be ready and handle IPC messages
372
402
  let readyCount = 0;
373
403
  await new Promise((resolve) => {
374
404
  cluster.on('message', (worker, message) => {
375
405
  if (message.type === 'ready') {
376
406
  readyCount++;
377
407
  if (readyCount === numWorkers) resolve();
408
+ } else if (message.type === 'stats') {
409
+ // Update worker stats from IPC
410
+ workerStats.set(worker.id, {
411
+ ...message.data,
412
+ lastUpdate: Date.now()
413
+ });
378
414
  }
379
415
  });
380
416
  });
@@ -403,10 +439,35 @@ export async function startClusterServer(options = {}) {
403
439
  await pgManager.stop();
404
440
  console.log('[pgserve] Cluster stopped');
405
441
  },
406
- getStats: () => ({
407
- workers: workers.size,
408
- pids: Array.from(workers.values()).map(w => w.process.pid)
409
- })
442
+ getStats: () => {
443
+ // Aggregate stats from all workers
444
+ let totalConnections = 0;
445
+ let totalConnected = 0;
446
+ let totalDisconnected = 0;
447
+ const activeWorkerStats = {};
448
+
449
+ for (const [id, stats] of workerStats) {
450
+ // Only include recent stats (within timeout window)
451
+ if (Date.now() - stats.lastUpdate < WORKER_STATS_TIMEOUT_MS) {
452
+ totalConnections += stats.connections || 0;
453
+ totalConnected += stats.totalConnected || 0;
454
+ totalDisconnected += stats.totalDisconnected || 0;
455
+ activeWorkerStats[id] = stats;
456
+ }
457
+ }
458
+
459
+ return {
460
+ workers: workers.size,
461
+ pids: Array.from(workers.values()).map(w => w.process.pid),
462
+ connections: {
463
+ active: totalConnections,
464
+ totalConnected,
465
+ totalDisconnected
466
+ },
467
+ workerStats: activeWorkerStats
468
+ };
469
+ },
470
+ pgManager
410
471
  };
411
472
  } else {
412
473
  // WORKER: Only run TCP routing, connect to PRIMARY's PostgreSQL
@@ -418,7 +479,8 @@ export async function startClusterServer(options = {}) {
418
479
  pgUser: process.env.PGSERVE_PG_USER || 'postgres',
419
480
  pgPassword: process.env.PGSERVE_PG_PASSWORD || 'postgres',
420
481
  logLevel: process.env.PGSERVE_LOG_LEVEL || 'info',
421
- autoProvision: process.env.PGSERVE_AUTO_PROVISION === 'true'
482
+ autoProvision: process.env.PGSERVE_AUTO_PROVISION === 'true',
483
+ maxConnections: parseInt(process.env.PGSERVE_MAX_CONNECTIONS) || 1000
422
484
  });
423
485
 
424
486
  await router.start();
@@ -426,9 +488,19 @@ export async function startClusterServer(options = {}) {
426
488
  // Tell PRIMARY we're ready
427
489
  process.send({ type: 'ready' });
428
490
 
491
+ // Periodically send stats to PRIMARY
492
+ const statsInterval = setInterval(() => {
493
+ try {
494
+ process.send({ type: 'stats', data: router.getStats() });
495
+ } catch {
496
+ // Expected: IPC channel may be closed during shutdown
497
+ }
498
+ }, WORKER_STATS_REPORT_INTERVAL_MS);
499
+
429
500
  // Handle shutdown
430
501
  process.on('message', async (message) => {
431
502
  if (message.type === 'shutdown') {
503
+ clearInterval(statsInterval);
432
504
  await router.stop();
433
505
  process.exit(0);
434
506
  }
package/src/index.js CHANGED
@@ -11,6 +11,8 @@ export { PostgresManager } from './postgres.js';
11
11
  export { SyncManager } from './sync.js';
12
12
  export { RestoreManager } from './restore.js';
13
13
  export { Dashboard } from './dashboard.js';
14
+ export { StatsCollector } from './stats-collector.js';
15
+ export { StatsDashboard } from './stats-dashboard.js';
14
16
 
15
17
  // Default export
16
18
  export { startMultiTenantServer as default } from './router.js';
package/src/postgres.js CHANGED
@@ -660,6 +660,7 @@ export class PostgresManager {
660
660
  this.binaries.postgres,
661
661
  '-D', this.databaseDir,
662
662
  '-p', this.port.toString(),
663
+ '-c', 'max_connections=1000', // Support high connection counts for stress testing
663
664
  ];
664
665
 
665
666
  // Enable Unix socket for faster local connections (Linux/macOS)