pgserve 1.1.3 → 1.1.5-rc.1
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 +172 -19
- package/bin/pglite-server.js +68 -7
- package/package.json +1 -1
- package/src/cluster.js +127 -10
- package/src/index.js +2 -0
- package/src/postgres.js +45 -0
- package/src/router.js +2 -1
- package/src/stats-collector.js +453 -0
- package/src/stats-dashboard.js +401 -0
- package/tests/benchmarks/runner.js +871 -50
- package/tests/benchmarks/vector-generator.js +368 -0
- package/tests/quick-bench.js +135 -0
- package/tests/stress-test.js +439 -0
package/README.md
CHANGED
|
@@ -72,6 +72,10 @@ psql postgresql://localhost:8432/myapp
|
|
|
72
72
|
<td><b>Async Replication</b></td>
|
|
73
73
|
<td>Sync to real PostgreSQL with minimal overhead</td>
|
|
74
74
|
</tr>
|
|
75
|
+
<tr>
|
|
76
|
+
<td><b>pgvector Built-in</b></td>
|
|
77
|
+
<td>Use <code>--pgvector</code> for auto-enabled vector similarity search</td>
|
|
78
|
+
</tr>
|
|
75
79
|
<tr>
|
|
76
80
|
<td><b>Cross-Platform</b></td>
|
|
77
81
|
<td>Linux x64, macOS ARM64/x64, Windows x64</td>
|
|
@@ -129,6 +133,8 @@ Options:
|
|
|
129
133
|
--no-provision Disable auto-provisioning of databases
|
|
130
134
|
--sync-to <url> Sync to real PostgreSQL (async replication)
|
|
131
135
|
--sync-databases <p> Database patterns to sync (comma-separated)
|
|
136
|
+
--pgvector Auto-enable pgvector extension on new databases
|
|
137
|
+
--max-connections <n> Max concurrent connections (default: 1000)
|
|
132
138
|
--help Show help message
|
|
133
139
|
```
|
|
134
140
|
|
|
@@ -148,6 +154,12 @@ pgserve --data /var/lib/pgserve
|
|
|
148
154
|
# Custom port
|
|
149
155
|
pgserve --port 5433
|
|
150
156
|
|
|
157
|
+
# Enable pgvector for AI/RAG applications
|
|
158
|
+
pgserve --pgvector
|
|
159
|
+
|
|
160
|
+
# RAM mode + pgvector (fastest for AI workloads)
|
|
161
|
+
pgserve --ram --pgvector
|
|
162
|
+
|
|
151
163
|
# Sync to production PostgreSQL
|
|
152
164
|
pgserve --sync-to "postgresql://user:pass@db.example.com:5432/prod"
|
|
153
165
|
```
|
|
@@ -167,6 +179,7 @@ const server = await startMultiTenantServer({
|
|
|
167
179
|
baseDir: null, // null = memory mode
|
|
168
180
|
logLevel: 'info',
|
|
169
181
|
autoProvision: true,
|
|
182
|
+
enablePgvector: true, // Auto-enable pgvector on new databases
|
|
170
183
|
syncTo: null, // Optional: PostgreSQL URL for replication
|
|
171
184
|
syncDatabases: null // Optional: patterns like "myapp,tenant_*"
|
|
172
185
|
});
|
|
@@ -258,48 +271,188 @@ pgserve --sync-to "postgresql://..." --sync-databases "myapp,tenant_*"
|
|
|
258
271
|
|
|
259
272
|
<br>
|
|
260
273
|
|
|
274
|
+
## pgvector (Vector Search)
|
|
275
|
+
|
|
276
|
+
pgvector is **built-in** — no separate installation required. Just enable it:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# Auto-enable pgvector on all new databases
|
|
280
|
+
pgserve --pgvector
|
|
281
|
+
|
|
282
|
+
# Combined with RAM mode for fastest vector operations
|
|
283
|
+
pgserve --ram --pgvector
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
When `--pgvector` is enabled, every new database automatically has the vector extension installed. No SQL setup required.
|
|
287
|
+
|
|
288
|
+
<details>
|
|
289
|
+
<summary><b>Using pgvector</b></summary>
|
|
290
|
+
|
|
291
|
+
```sql
|
|
292
|
+
-- Create table with vector column (1536 = OpenAI embedding size)
|
|
293
|
+
CREATE TABLE documents (id SERIAL, content TEXT, embedding vector(1536));
|
|
294
|
+
|
|
295
|
+
-- Insert with embedding
|
|
296
|
+
INSERT INTO documents (content, embedding) VALUES ('Hello', '[0.1, 0.2, ...]');
|
|
297
|
+
|
|
298
|
+
-- k-NN similarity search (L2 distance)
|
|
299
|
+
SELECT content FROM documents ORDER BY embedding <-> $1 LIMIT 10;
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
See [pgvector documentation](https://github.com/pgvector/pgvector) for full API reference.
|
|
303
|
+
</details>
|
|
304
|
+
|
|
305
|
+
<details>
|
|
306
|
+
<summary><b>Without --pgvector flag</b></summary>
|
|
307
|
+
|
|
308
|
+
If you don't use `--pgvector`, you can still enable pgvector manually per database:
|
|
309
|
+
|
|
310
|
+
```sql
|
|
311
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
</details>
|
|
315
|
+
|
|
316
|
+
> pgvector 0.8.1 is bundled with the PostgreSQL binaries. Supports L2 distance (`<->`), inner product (`<#>`), and cosine distance (`<=>`).
|
|
317
|
+
|
|
318
|
+
<br>
|
|
319
|
+
|
|
261
320
|
## Performance
|
|
262
321
|
|
|
263
|
-
###
|
|
322
|
+
### CRUD Benchmarks
|
|
264
323
|
|
|
265
324
|
<table>
|
|
266
325
|
<tr>
|
|
267
326
|
<th>Scenario</th>
|
|
268
327
|
<th>SQLite</th>
|
|
269
328
|
<th>PGlite</th>
|
|
270
|
-
<th>
|
|
271
|
-
<th>pgserve
|
|
329
|
+
<th>PostgreSQL</th>
|
|
330
|
+
<th>pgserve</th>
|
|
272
331
|
<th>pgserve --ram</th>
|
|
273
332
|
</tr>
|
|
274
333
|
<tr>
|
|
275
334
|
<td><b>Concurrent Writes</b> (10 agents)</td>
|
|
276
|
-
<td>
|
|
277
|
-
<td>
|
|
278
|
-
<td>
|
|
279
|
-
<td>
|
|
280
|
-
<td><b>4,
|
|
335
|
+
<td>91 qps</td>
|
|
336
|
+
<td>204 qps</td>
|
|
337
|
+
<td>1,667 qps</td>
|
|
338
|
+
<td>2,273 qps</td>
|
|
339
|
+
<td><b>4,167 qps</b> 🏆</td>
|
|
281
340
|
</tr>
|
|
282
341
|
<tr>
|
|
283
342
|
<td><b>Mixed Workload</b></td>
|
|
284
|
-
<td>
|
|
285
|
-
<td>
|
|
286
|
-
<td>
|
|
287
|
-
<td>
|
|
288
|
-
<td><b>
|
|
343
|
+
<td>383 qps</td>
|
|
344
|
+
<td>484 qps</td>
|
|
345
|
+
<td>507 qps</td>
|
|
346
|
+
<td>1,133 qps</td>
|
|
347
|
+
<td><b>2,109 qps</b> 🏆</td>
|
|
289
348
|
</tr>
|
|
290
349
|
<tr>
|
|
291
350
|
<td><b>Write Lock</b> (50 writers)</td>
|
|
292
|
-
<td>
|
|
293
|
-
<td>
|
|
294
|
-
<td>
|
|
295
|
-
<td>
|
|
351
|
+
<td>111 qps</td>
|
|
352
|
+
<td>228 qps</td>
|
|
353
|
+
<td>2,857 qps</td>
|
|
354
|
+
<td>3,030 qps</td>
|
|
296
355
|
<td><b>4,348 qps</b> 🏆</td>
|
|
297
356
|
</tr>
|
|
298
357
|
</table>
|
|
299
358
|
|
|
300
|
-
|
|
359
|
+
### Vector Benchmarks (pgvector)
|
|
360
|
+
|
|
361
|
+
<table>
|
|
362
|
+
<tr>
|
|
363
|
+
<th>Metric</th>
|
|
364
|
+
<th>PGlite</th>
|
|
365
|
+
<th>PostgreSQL</th>
|
|
366
|
+
<th>pgserve</th>
|
|
367
|
+
<th>pgserve --ram</th>
|
|
368
|
+
</tr>
|
|
369
|
+
<tr>
|
|
370
|
+
<td><b>Vector INSERT</b> (1000 × 1536-dim)</td>
|
|
371
|
+
<td>152/sec</td>
|
|
372
|
+
<td>392/sec</td>
|
|
373
|
+
<td>387/sec</td>
|
|
374
|
+
<td><b>1,082/sec</b> 🏆</td>
|
|
375
|
+
</tr>
|
|
376
|
+
<tr>
|
|
377
|
+
<td><b>k-NN Search</b> (k=10, 10k corpus)</td>
|
|
378
|
+
<td>22 qps</td>
|
|
379
|
+
<td>33 qps</td>
|
|
380
|
+
<td>31 qps</td>
|
|
381
|
+
<td>30 qps</td>
|
|
382
|
+
</tr>
|
|
383
|
+
<tr>
|
|
384
|
+
<td><b>Recall@10</b></td>
|
|
385
|
+
<td>100%</td>
|
|
386
|
+
<td>100%</td>
|
|
387
|
+
<td>100%</td>
|
|
388
|
+
<td>100%</td>
|
|
389
|
+
</tr>
|
|
390
|
+
</table>
|
|
391
|
+
|
|
392
|
+
> <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.
|
|
393
|
+
|
|
394
|
+
### Final Score
|
|
395
|
+
|
|
396
|
+
<table>
|
|
397
|
+
<tr>
|
|
398
|
+
<th>Engine</th>
|
|
399
|
+
<th>CRUD QPS</th>
|
|
400
|
+
<th>Vec QPS</th>
|
|
401
|
+
<th>Recall</th>
|
|
402
|
+
<th>P50</th>
|
|
403
|
+
<th>P99</th>
|
|
404
|
+
<th>Score</th>
|
|
405
|
+
</tr>
|
|
406
|
+
<tr>
|
|
407
|
+
<td>SQLite</td>
|
|
408
|
+
<td>195</td>
|
|
409
|
+
<td>N/A</td>
|
|
410
|
+
<td>N/A</td>
|
|
411
|
+
<td>6.3ms</td>
|
|
412
|
+
<td>17.3ms</td>
|
|
413
|
+
<td>117</td>
|
|
414
|
+
</tr>
|
|
415
|
+
<tr>
|
|
416
|
+
<td>PGlite</td>
|
|
417
|
+
<td>305</td>
|
|
418
|
+
<td>65</td>
|
|
419
|
+
<td>100%</td>
|
|
420
|
+
<td>3.3ms</td>
|
|
421
|
+
<td>7.0ms</td>
|
|
422
|
+
<td>209</td>
|
|
423
|
+
</tr>
|
|
424
|
+
<tr>
|
|
425
|
+
<td>PostgreSQL</td>
|
|
426
|
+
<td>1,677</td>
|
|
427
|
+
<td>152</td>
|
|
428
|
+
<td>100%</td>
|
|
429
|
+
<td>6.0ms</td>
|
|
430
|
+
<td>19.0ms</td>
|
|
431
|
+
<td>1,067</td>
|
|
432
|
+
</tr>
|
|
433
|
+
<tr>
|
|
434
|
+
<td>pgserve</td>
|
|
435
|
+
<td>2,145</td>
|
|
436
|
+
<td>149</td>
|
|
437
|
+
<td>100%</td>
|
|
438
|
+
<td>5.3ms</td>
|
|
439
|
+
<td>13.0ms</td>
|
|
440
|
+
<td>1,347</td>
|
|
441
|
+
</tr>
|
|
442
|
+
<tr>
|
|
443
|
+
<td><b>pgserve --ram</b></td>
|
|
444
|
+
<td><b>3,541</b></td>
|
|
445
|
+
<td><b>381</b></td>
|
|
446
|
+
<td><b>100%</b></td>
|
|
447
|
+
<td><b>3.3ms</b></td>
|
|
448
|
+
<td><b>10.7ms</b></td>
|
|
449
|
+
<td><b>2,277</b> 🏆</td>
|
|
450
|
+
</tr>
|
|
451
|
+
</table>
|
|
452
|
+
|
|
453
|
+
> <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
454
|
>
|
|
302
|
-
> Run benchmarks:
|
|
455
|
+
> Run benchmarks yourself: <code>bun tests/benchmarks/runner.js --include-vector</code>
|
|
303
456
|
|
|
304
457
|
<br>
|
|
305
458
|
|
package/bin/pglite-server.js
CHANGED
|
@@ -53,6 +53,9 @@ 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)
|
|
58
|
+
--pgvector Auto-enable pgvector extension on new databases
|
|
56
59
|
--help Show this help message
|
|
57
60
|
|
|
58
61
|
MODES:
|
|
@@ -108,7 +111,10 @@ function parseArgs() {
|
|
|
108
111
|
cluster: cpuCount > 1 && !isWindows, // Auto-enable on multi-core (disabled on Windows - no SO_REUSEPORT)
|
|
109
112
|
workers: null, // null = use CPU count
|
|
110
113
|
syncTo: null, // Sync target PostgreSQL URL
|
|
111
|
-
syncDatabases: null // Database patterns to sync (comma-separated)
|
|
114
|
+
syncDatabases: null, // Database patterns to sync (comma-separated)
|
|
115
|
+
showStats: true, // Show real-time stats dashboard (default: enabled)
|
|
116
|
+
maxConnections: 1000, // Max concurrent connections (high default for multi-tenant)
|
|
117
|
+
enablePgvector: false // Auto-enable pgvector extension on new databases
|
|
112
118
|
};
|
|
113
119
|
|
|
114
120
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -163,6 +169,22 @@ function parseArgs() {
|
|
|
163
169
|
options.syncDatabases = args[++i];
|
|
164
170
|
break;
|
|
165
171
|
|
|
172
|
+
case '--stats':
|
|
173
|
+
options.showStats = true;
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case '--no-stats':
|
|
177
|
+
options.showStats = false;
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case '--max-connections':
|
|
181
|
+
options.maxConnections = parseInt(args[++i], 10);
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case '--pgvector':
|
|
185
|
+
options.enablePgvector = true;
|
|
186
|
+
break;
|
|
187
|
+
|
|
166
188
|
case '--help':
|
|
167
189
|
case 'help':
|
|
168
190
|
printHelp();
|
|
@@ -211,7 +233,9 @@ pgserve - Embedded PostgreSQL Server
|
|
|
211
233
|
useRam: options.useRam,
|
|
212
234
|
logLevel: options.logLevel,
|
|
213
235
|
autoProvision: options.autoProvision,
|
|
214
|
-
workers: options.workers
|
|
236
|
+
workers: options.workers,
|
|
237
|
+
maxConnections: options.maxConnections,
|
|
238
|
+
enablePgvector: options.enablePgvector
|
|
215
239
|
});
|
|
216
240
|
|
|
217
241
|
// Only primary process shows full startup message
|
|
@@ -222,10 +246,11 @@ pgserve - Embedded PostgreSQL Server
|
|
|
222
246
|
Cluster started successfully!
|
|
223
247
|
|
|
224
248
|
Endpoint: postgresql://${options.host}:${options.port}/<database>
|
|
225
|
-
Mode: ${memoryMode ? '
|
|
249
|
+
Mode: ${memoryMode ? (options.useRam ? 'RAM (/dev/shm)' : 'Ephemeral (temp)') : 'Persistent'} (Cluster)
|
|
226
250
|
Workers: ${stats.workers} processes
|
|
227
251
|
Data: ${storageType}
|
|
228
252
|
Auto-create: ${options.autoProvision ? 'Enabled' : 'Disabled'}
|
|
253
|
+
pgvector: ${options.enablePgvector ? 'Enabled (auto-installed on new DBs)' : 'Disabled (use --pgvector to enable)'}
|
|
229
254
|
|
|
230
255
|
Examples:
|
|
231
256
|
postgresql://${options.host}:${options.port}/myapp
|
|
@@ -244,7 +269,9 @@ Press Ctrl+C to stop
|
|
|
244
269
|
logLevel: options.logLevel,
|
|
245
270
|
autoProvision: options.autoProvision,
|
|
246
271
|
syncTo: options.syncTo,
|
|
247
|
-
syncDatabases: options.syncDatabases
|
|
272
|
+
syncDatabases: options.syncDatabases,
|
|
273
|
+
maxConnections: options.maxConnections,
|
|
274
|
+
enablePgvector: options.enablePgvector
|
|
248
275
|
});
|
|
249
276
|
|
|
250
277
|
server = router;
|
|
@@ -258,10 +285,11 @@ Press Ctrl+C to stop
|
|
|
258
285
|
Server started successfully!
|
|
259
286
|
|
|
260
287
|
Endpoint: postgresql://${options.host}:${options.port}/<database>
|
|
261
|
-
Mode: ${memoryMode ? '
|
|
288
|
+
Mode: ${memoryMode ? (options.useRam ? 'RAM (/dev/shm)' : 'Ephemeral (temp)') : 'Persistent'}
|
|
262
289
|
Data: ${storageType}
|
|
263
290
|
PostgreSQL: Port ${router.pgPort} (internal)
|
|
264
291
|
Auto-create: ${options.autoProvision ? 'Enabled' : 'Disabled'}
|
|
292
|
+
pgvector: ${options.enablePgvector ? 'Enabled (auto-installed on new DBs)' : 'Disabled (use --pgvector to enable)'}
|
|
265
293
|
Sync: ${syncStatus}${options.syncDatabases ? ` (${options.syncDatabases})` : ''}
|
|
266
294
|
|
|
267
295
|
Examples:
|
|
@@ -272,12 +300,45 @@ Press Ctrl+C to stop
|
|
|
272
300
|
`);
|
|
273
301
|
}
|
|
274
302
|
|
|
303
|
+
// Start stats dashboard if requested (only for primary/single-process)
|
|
304
|
+
let dashboard = null;
|
|
305
|
+
if (options.showStats && !process.env.PGSERVE_WORKER) {
|
|
306
|
+
const { StatsDashboard } = await import('../src/stats-dashboard.js');
|
|
307
|
+
const { StatsCollector } = await import('../src/stats-collector.js');
|
|
308
|
+
|
|
309
|
+
// Create stats collector with appropriate sources
|
|
310
|
+
const collector = new StatsCollector({
|
|
311
|
+
router: options.cluster ? null : server,
|
|
312
|
+
pgManager: server.pgManager,
|
|
313
|
+
clusterStats: options.cluster ? () => server.getStats() : null,
|
|
314
|
+
logger: server.logger,
|
|
315
|
+
port: options.port,
|
|
316
|
+
host: options.host
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
dashboard = new StatsDashboard({
|
|
320
|
+
refreshInterval: 2000, // 2 second refresh for real-time feel
|
|
321
|
+
statsProvider: () => collector.collect()
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
dashboard.start();
|
|
325
|
+
}
|
|
326
|
+
|
|
275
327
|
// Graceful shutdown (only for primary/single-process, workers handle via IPC)
|
|
276
328
|
if (!process.env.PGSERVE_WORKER) {
|
|
277
329
|
const shutdown = async () => {
|
|
330
|
+
// Stop dashboard first to restore cursor
|
|
331
|
+
if (dashboard) {
|
|
332
|
+
dashboard.stop();
|
|
333
|
+
}
|
|
278
334
|
console.log('\nShutting down...');
|
|
279
|
-
|
|
280
|
-
|
|
335
|
+
try {
|
|
336
|
+
await server.stop();
|
|
337
|
+
console.log('Server stopped.');
|
|
338
|
+
} catch (err) {
|
|
339
|
+
console.error('Error during shutdown:', err.message);
|
|
340
|
+
// Still exit - best effort cleanup
|
|
341
|
+
}
|
|
281
342
|
process.exit(0);
|
|
282
343
|
};
|
|
283
344
|
|
package/package.json
CHANGED
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
|
|
@@ -38,12 +42,19 @@ class ClusterRouter extends EventEmitter {
|
|
|
38
42
|
this.pgPassword = options.pgPassword || 'postgres';
|
|
39
43
|
this.autoProvision = options.autoProvision !== false;
|
|
40
44
|
this.maxConnections = options.maxConnections || 1000;
|
|
45
|
+
this.enablePgvector = options.enablePgvector || false;
|
|
41
46
|
|
|
42
47
|
this.logger = createLogger({ level: options.logLevel || 'info' });
|
|
43
48
|
this.sql = null; // Bun.sql for admin queries
|
|
44
49
|
this.server = null;
|
|
45
50
|
this.connections = new Set();
|
|
46
51
|
this.setMaxListeners(this.maxConnections + 10);
|
|
52
|
+
|
|
53
|
+
// Connection stats tracking for IPC reporting
|
|
54
|
+
this.connectionStats = {
|
|
55
|
+
totalConnected: 0,
|
|
56
|
+
totalDisconnected: 0
|
|
57
|
+
};
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
/**
|
|
@@ -114,6 +125,11 @@ class ClusterRouter extends EventEmitter {
|
|
|
114
125
|
if (result.length === 0) {
|
|
115
126
|
// Use sql() helper for safe identifier escaping (like CREATE DATABASE)
|
|
116
127
|
await this.sql.unsafe(`CREATE DATABASE "${dbName.replace(/"/g, '""')}"`);
|
|
128
|
+
|
|
129
|
+
// Auto-enable pgvector extension if configured
|
|
130
|
+
if (this.enablePgvector) {
|
|
131
|
+
await this.enablePgvectorExtension(dbName);
|
|
132
|
+
}
|
|
117
133
|
}
|
|
118
134
|
} catch (error) {
|
|
119
135
|
// Ignore "already exists" (race condition between workers)
|
|
@@ -123,6 +139,41 @@ class ClusterRouter extends EventEmitter {
|
|
|
123
139
|
}
|
|
124
140
|
}
|
|
125
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Enable pgvector extension on a database
|
|
144
|
+
* Creates a temporary connection to the specific database to run CREATE EXTENSION
|
|
145
|
+
* @param {string} dbName - Database name to enable pgvector on
|
|
146
|
+
*/
|
|
147
|
+
async enablePgvectorExtension(dbName) {
|
|
148
|
+
let dbPool = null;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Create temporary connection to the specific database
|
|
152
|
+
dbPool = new SQL({
|
|
153
|
+
hostname: '127.0.0.1',
|
|
154
|
+
port: this.pgPort,
|
|
155
|
+
database: dbName,
|
|
156
|
+
username: this.pgUser,
|
|
157
|
+
password: this.pgPassword,
|
|
158
|
+
max: 1,
|
|
159
|
+
idleTimeout: 5,
|
|
160
|
+
connectionTimeout: 5,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Enable pgvector extension
|
|
164
|
+
await dbPool.unsafe('CREATE EXTENSION IF NOT EXISTS vector');
|
|
165
|
+
this.logger.info({ dbName }, 'pgvector extension enabled');
|
|
166
|
+
} catch (error) {
|
|
167
|
+
// Log but don't fail database creation - pgvector might not be available
|
|
168
|
+
this.logger.warn({ dbName, err: error.message }, 'Failed to enable pgvector extension (non-fatal)');
|
|
169
|
+
} finally {
|
|
170
|
+
// Always close the temporary connection
|
|
171
|
+
if (dbPool) {
|
|
172
|
+
await dbPool.close().catch(() => {});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
126
177
|
/**
|
|
127
178
|
* Handle socket open (Bun TCP handler)
|
|
128
179
|
*/
|
|
@@ -134,6 +185,7 @@ class ClusterRouter extends EventEmitter {
|
|
|
134
185
|
handshakeComplete: false
|
|
135
186
|
});
|
|
136
187
|
this.connections.add(socket);
|
|
188
|
+
this.connectionStats.totalConnected++;
|
|
137
189
|
}
|
|
138
190
|
|
|
139
191
|
/**
|
|
@@ -259,6 +311,19 @@ class ClusterRouter extends EventEmitter {
|
|
|
259
311
|
}
|
|
260
312
|
this.connections.delete(socket);
|
|
261
313
|
this.socketState.delete(socket);
|
|
314
|
+
this.connectionStats.totalDisconnected++;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get router stats for IPC reporting
|
|
319
|
+
*/
|
|
320
|
+
getStats() {
|
|
321
|
+
return {
|
|
322
|
+
connections: this.connections.size,
|
|
323
|
+
totalConnected: this.connectionStats.totalConnected,
|
|
324
|
+
totalDisconnected: this.connectionStats.totalDisconnected,
|
|
325
|
+
pid: process.pid
|
|
326
|
+
};
|
|
262
327
|
}
|
|
263
328
|
|
|
264
329
|
/**
|
|
@@ -286,7 +351,7 @@ class ClusterRouter extends EventEmitter {
|
|
|
286
351
|
try {
|
|
287
352
|
await this.sql.close();
|
|
288
353
|
} catch {
|
|
289
|
-
//
|
|
354
|
+
// Expected: connection may already be terminated during cleanup
|
|
290
355
|
}
|
|
291
356
|
}
|
|
292
357
|
|
|
@@ -297,6 +362,7 @@ class ClusterRouter extends EventEmitter {
|
|
|
297
362
|
}
|
|
298
363
|
}
|
|
299
364
|
|
|
365
|
+
|
|
300
366
|
/**
|
|
301
367
|
* Start pgserve in cluster mode
|
|
302
368
|
*/
|
|
@@ -307,6 +373,8 @@ export async function startClusterServer(options = {}) {
|
|
|
307
373
|
const pgPort = options.pgPort || (port + 1000);
|
|
308
374
|
|
|
309
375
|
if (cluster.isPrimary) {
|
|
376
|
+
// Port binding happens in workers via Bun.listen with reusePort
|
|
377
|
+
// If port is in use, first worker will fail with EADDRINUSE
|
|
310
378
|
console.log(`[pgserve] Cluster mode: ${numWorkers} workers`);
|
|
311
379
|
|
|
312
380
|
// PRIMARY: Start our embedded PostgreSQL (single instance)
|
|
@@ -315,7 +383,8 @@ export async function startClusterServer(options = {}) {
|
|
|
315
383
|
dataDir: options.baseDir,
|
|
316
384
|
port: pgPort,
|
|
317
385
|
logger: logger.child({ component: 'postgres' }),
|
|
318
|
-
useRam: options.useRam // Use /dev/shm for true RAM storage (Linux only)
|
|
386
|
+
useRam: options.useRam, // Use /dev/shm for true RAM storage (Linux only)
|
|
387
|
+
enablePgvector: options.enablePgvector // Auto-enable pgvector extension on new databases
|
|
319
388
|
});
|
|
320
389
|
|
|
321
390
|
await pgManager.start();
|
|
@@ -325,6 +394,7 @@ export async function startClusterServer(options = {}) {
|
|
|
325
394
|
console.log(`[pgserve] Socket: ${pgSocketPath || `TCP port ${pgPort}`}`);
|
|
326
395
|
|
|
327
396
|
const workers = new Map();
|
|
397
|
+
const workerStats = new Map(); // Track stats from each worker
|
|
328
398
|
|
|
329
399
|
// Fork workers with PostgreSQL connection info
|
|
330
400
|
for (let i = 0; i < numWorkers; i++) {
|
|
@@ -337,7 +407,9 @@ export async function startClusterServer(options = {}) {
|
|
|
337
407
|
PGSERVE_PG_USER: 'postgres',
|
|
338
408
|
PGSERVE_PG_PASSWORD: 'postgres',
|
|
339
409
|
PGSERVE_LOG_LEVEL: options.logLevel || 'info',
|
|
340
|
-
PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false'
|
|
410
|
+
PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false',
|
|
411
|
+
PGSERVE_MAX_CONNECTIONS: String(options.maxConnections || 1000),
|
|
412
|
+
PGSERVE_ENABLE_PGVECTOR: options.enablePgvector ? 'true' : 'false'
|
|
341
413
|
});
|
|
342
414
|
workers.set(worker.id, worker);
|
|
343
415
|
}
|
|
@@ -363,18 +435,26 @@ export async function startClusterServer(options = {}) {
|
|
|
363
435
|
PGSERVE_PG_USER: 'postgres',
|
|
364
436
|
PGSERVE_PG_PASSWORD: 'postgres',
|
|
365
437
|
PGSERVE_LOG_LEVEL: options.logLevel || 'info',
|
|
366
|
-
PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false'
|
|
438
|
+
PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false',
|
|
439
|
+
PGSERVE_MAX_CONNECTIONS: String(options.maxConnections || 1000),
|
|
440
|
+
PGSERVE_ENABLE_PGVECTOR: options.enablePgvector ? 'true' : 'false'
|
|
367
441
|
});
|
|
368
442
|
workers.set(newWorker.id, newWorker);
|
|
369
443
|
});
|
|
370
444
|
|
|
371
|
-
// Wait for workers to be ready
|
|
445
|
+
// Wait for workers to be ready and handle IPC messages
|
|
372
446
|
let readyCount = 0;
|
|
373
447
|
await new Promise((resolve) => {
|
|
374
448
|
cluster.on('message', (worker, message) => {
|
|
375
449
|
if (message.type === 'ready') {
|
|
376
450
|
readyCount++;
|
|
377
451
|
if (readyCount === numWorkers) resolve();
|
|
452
|
+
} else if (message.type === 'stats') {
|
|
453
|
+
// Update worker stats from IPC
|
|
454
|
+
workerStats.set(worker.id, {
|
|
455
|
+
...message.data,
|
|
456
|
+
lastUpdate: Date.now()
|
|
457
|
+
});
|
|
378
458
|
}
|
|
379
459
|
});
|
|
380
460
|
});
|
|
@@ -403,10 +483,35 @@ export async function startClusterServer(options = {}) {
|
|
|
403
483
|
await pgManager.stop();
|
|
404
484
|
console.log('[pgserve] Cluster stopped');
|
|
405
485
|
},
|
|
406
|
-
getStats: () =>
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
486
|
+
getStats: () => {
|
|
487
|
+
// Aggregate stats from all workers
|
|
488
|
+
let totalConnections = 0;
|
|
489
|
+
let totalConnected = 0;
|
|
490
|
+
let totalDisconnected = 0;
|
|
491
|
+
const activeWorkerStats = {};
|
|
492
|
+
|
|
493
|
+
for (const [id, stats] of workerStats) {
|
|
494
|
+
// Only include recent stats (within timeout window)
|
|
495
|
+
if (Date.now() - stats.lastUpdate < WORKER_STATS_TIMEOUT_MS) {
|
|
496
|
+
totalConnections += stats.connections || 0;
|
|
497
|
+
totalConnected += stats.totalConnected || 0;
|
|
498
|
+
totalDisconnected += stats.totalDisconnected || 0;
|
|
499
|
+
activeWorkerStats[id] = stats;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
workers: workers.size,
|
|
505
|
+
pids: Array.from(workers.values()).map(w => w.process.pid),
|
|
506
|
+
connections: {
|
|
507
|
+
active: totalConnections,
|
|
508
|
+
totalConnected,
|
|
509
|
+
totalDisconnected
|
|
510
|
+
},
|
|
511
|
+
workerStats: activeWorkerStats
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
pgManager
|
|
410
515
|
};
|
|
411
516
|
} else {
|
|
412
517
|
// WORKER: Only run TCP routing, connect to PRIMARY's PostgreSQL
|
|
@@ -418,7 +523,9 @@ export async function startClusterServer(options = {}) {
|
|
|
418
523
|
pgUser: process.env.PGSERVE_PG_USER || 'postgres',
|
|
419
524
|
pgPassword: process.env.PGSERVE_PG_PASSWORD || 'postgres',
|
|
420
525
|
logLevel: process.env.PGSERVE_LOG_LEVEL || 'info',
|
|
421
|
-
autoProvision: process.env.PGSERVE_AUTO_PROVISION === 'true'
|
|
526
|
+
autoProvision: process.env.PGSERVE_AUTO_PROVISION === 'true',
|
|
527
|
+
maxConnections: parseInt(process.env.PGSERVE_MAX_CONNECTIONS) || 1000,
|
|
528
|
+
enablePgvector: process.env.PGSERVE_ENABLE_PGVECTOR === 'true'
|
|
422
529
|
});
|
|
423
530
|
|
|
424
531
|
await router.start();
|
|
@@ -426,9 +533,19 @@ export async function startClusterServer(options = {}) {
|
|
|
426
533
|
// Tell PRIMARY we're ready
|
|
427
534
|
process.send({ type: 'ready' });
|
|
428
535
|
|
|
536
|
+
// Periodically send stats to PRIMARY
|
|
537
|
+
const statsInterval = setInterval(() => {
|
|
538
|
+
try {
|
|
539
|
+
process.send({ type: 'stats', data: router.getStats() });
|
|
540
|
+
} catch {
|
|
541
|
+
// Expected: IPC channel may be closed during shutdown
|
|
542
|
+
}
|
|
543
|
+
}, WORKER_STATS_REPORT_INTERVAL_MS);
|
|
544
|
+
|
|
429
545
|
// Handle shutdown
|
|
430
546
|
process.on('message', async (message) => {
|
|
431
547
|
if (message.type === 'shutdown') {
|
|
548
|
+
clearInterval(statsInterval);
|
|
432
549
|
await router.stop();
|
|
433
550
|
process.exit(0);
|
|
434
551
|
}
|
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';
|