pgserve 0.1.4 → 1.0.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/.claude/settings.local.json +11 -0
- package/README.md +281 -256
- package/bin/pglite-server.js +212 -395
- package/package.json +13 -10
- package/src/cluster.js +322 -0
- package/src/index.js +8 -171
- package/src/postgres.js +479 -0
- package/src/protocol.js +49 -10
- package/src/router.js +117 -114
- package/src/sync.js +344 -0
- package/tests/benchmarks/runner.js +300 -155
- package/tests/sync-perf-test.js +150 -0
- package/src/detector.js +0 -105
- package/src/pool.js +0 -320
- package/src/ports.js +0 -114
- package/src/registry.js +0 -134
- package/src/server.js +0 -265
package/src/router.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Multi-Tenant Router (
|
|
2
|
+
* Multi-Tenant Router (TCP Proxy to Embedded PostgreSQL)
|
|
3
3
|
*
|
|
4
|
-
* Single TCP server that routes connections to
|
|
5
|
-
*
|
|
4
|
+
* Single TCP server that routes connections to an embedded PostgreSQL instance.
|
|
5
|
+
* Extracts database name from PostgreSQL startup message, auto-creates database,
|
|
6
|
+
* then proxies the connection to real PostgreSQL.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
8
|
+
* Features:
|
|
9
|
+
* - TRUE concurrent connections (native PostgreSQL)
|
|
10
|
+
* - Auto-provision databases on first connection
|
|
11
|
+
* - Zero configuration required
|
|
12
|
+
* - Memory mode (default) or persistent storage
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import net from 'net';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
16
|
+
import { PostgresManager } from './postgres.js';
|
|
17
|
+
import { SyncManager } from './sync.js';
|
|
17
18
|
import { extractDatabaseNameFromSocket } from './protocol.js';
|
|
18
19
|
import { EventEmitter } from 'events';
|
|
19
20
|
import pino from 'pino';
|
|
@@ -24,13 +25,15 @@ import pino from 'pino';
|
|
|
24
25
|
export class MultiTenantRouter extends EventEmitter {
|
|
25
26
|
constructor(options = {}) {
|
|
26
27
|
super();
|
|
27
|
-
this.port = options.port ||
|
|
28
|
+
this.port = options.port || 5432;
|
|
28
29
|
this.host = options.host || '127.0.0.1';
|
|
29
|
-
this.baseDir = options.baseDir ||
|
|
30
|
-
this.memoryMode = options.
|
|
31
|
-
this.
|
|
30
|
+
this.baseDir = options.baseDir || null; // null = memory mode
|
|
31
|
+
this.memoryMode = !options.baseDir;
|
|
32
|
+
this.maxConnections = options.maxConnections || 1000;
|
|
32
33
|
this.autoProvision = options.autoProvision !== false;
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
// Internal PostgreSQL port (different from router port)
|
|
36
|
+
this.pgPort = options.pgPort || (this.port + 1000);
|
|
34
37
|
|
|
35
38
|
// Pino logger (ultra-fast structured logging)
|
|
36
39
|
const logLevel = options.logLevel || 'info';
|
|
@@ -42,13 +45,19 @@ export class MultiTenantRouter extends EventEmitter {
|
|
|
42
45
|
} : undefined
|
|
43
46
|
});
|
|
44
47
|
|
|
45
|
-
//
|
|
46
|
-
this.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
// Sync options (async replication to real PostgreSQL)
|
|
49
|
+
this.syncTo = options.syncTo || null;
|
|
50
|
+
this.syncDatabases = options.syncDatabases
|
|
51
|
+
? options.syncDatabases.split(',').map(s => s.trim())
|
|
52
|
+
: [];
|
|
53
|
+
this.syncManager = null;
|
|
54
|
+
|
|
55
|
+
// PostgreSQL manager (with sync flag if needed)
|
|
56
|
+
this.pgManager = new PostgresManager({
|
|
57
|
+
dataDir: this.baseDir,
|
|
58
|
+
port: this.pgPort,
|
|
59
|
+
logger: this.logger.child({ component: 'postgres' }),
|
|
60
|
+
syncEnabled: !!this.syncTo // Enable logical replication if sync is configured
|
|
52
61
|
});
|
|
53
62
|
|
|
54
63
|
// TCP server
|
|
@@ -56,21 +65,7 @@ export class MultiTenantRouter extends EventEmitter {
|
|
|
56
65
|
this.connections = new Set();
|
|
57
66
|
|
|
58
67
|
// Performance: Reduce event listener overhead
|
|
59
|
-
this.setMaxListeners(this.
|
|
60
|
-
|
|
61
|
-
// Forward pool events (optimized logging)
|
|
62
|
-
this.pool.on('instance-created', (dbName) => {
|
|
63
|
-
this.logger.info({ dbName }, 'Database created');
|
|
64
|
-
this.emit('database-created', dbName);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
this.pool.on('instance-locked', (dbName) => {
|
|
68
|
-
this.logger.debug({ dbName }, 'Database locked');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
this.pool.on('instance-unlocked', (dbName) => {
|
|
72
|
-
this.logger.debug({ dbName }, 'Database unlocked');
|
|
73
|
-
});
|
|
68
|
+
this.setMaxListeners(this.maxConnections + 10);
|
|
74
69
|
}
|
|
75
70
|
|
|
76
71
|
/**
|
|
@@ -84,16 +79,6 @@ export class MultiTenantRouter extends EventEmitter {
|
|
|
84
79
|
// Enable TCP keepalive (detect dead connections)
|
|
85
80
|
socket.setKeepAlive(true, 60000); // 60s initial delay
|
|
86
81
|
|
|
87
|
-
// Increase socket buffer sizes for better throughput
|
|
88
|
-
// Note: These are hints to OS, actual values may differ
|
|
89
|
-
try {
|
|
90
|
-
socket.setRecvBufferSize && socket.setRecvBufferSize(128 * 1024); // 128KB
|
|
91
|
-
socket.setSendBufferSize && socket.setSendBufferSize(128 * 1024); // 128KB
|
|
92
|
-
} catch (err) {
|
|
93
|
-
// Ignore if not supported
|
|
94
|
-
this.logger.debug({ err }, 'Could not set socket buffer sizes');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
82
|
// Prevent socket timeout during long-running queries
|
|
98
83
|
socket.setTimeout(0);
|
|
99
84
|
}
|
|
@@ -102,19 +87,39 @@ export class MultiTenantRouter extends EventEmitter {
|
|
|
102
87
|
* Start multi-tenant router
|
|
103
88
|
*/
|
|
104
89
|
async start() {
|
|
90
|
+
// Start PostgreSQL first
|
|
91
|
+
await this.pgManager.start();
|
|
92
|
+
|
|
93
|
+
// Initialize SyncManager if configured (async replication)
|
|
94
|
+
if (this.syncTo) {
|
|
95
|
+
this.syncManager = new SyncManager({
|
|
96
|
+
targetUrl: this.syncTo,
|
|
97
|
+
databases: this.syncDatabases,
|
|
98
|
+
sourcePort: this.pgPort,
|
|
99
|
+
sourceSocketPath: this.pgManager.getSocketPath(),
|
|
100
|
+
logLevel: this.logger.level
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Wire SyncManager to PostgresManager for database creation hooks
|
|
104
|
+
this.pgManager.setSyncManager(this.syncManager);
|
|
105
|
+
|
|
106
|
+
// Initialize sync connections (non-blocking, runs in background)
|
|
107
|
+
this.syncManager.initialize(this.pgManager)
|
|
108
|
+
.then(() => this.logger.info('Sync manager initialized'))
|
|
109
|
+
.catch(err => this.logger.warn({ err: err.message }, 'Sync manager initialization failed (non-fatal)'));
|
|
110
|
+
}
|
|
111
|
+
|
|
105
112
|
return new Promise((resolve, reject) => {
|
|
106
|
-
// Create TCP server
|
|
113
|
+
// Create TCP server
|
|
107
114
|
this.server = net.createServer({
|
|
108
|
-
// Performance: Allow half-open sockets (faster cleanup)
|
|
109
115
|
allowHalfOpen: false,
|
|
110
|
-
// Performance: Pause on connect (manual resume after setup)
|
|
111
116
|
pauseOnConnect: true
|
|
112
117
|
}, async (socket) => {
|
|
113
118
|
await this.handleConnection(socket);
|
|
114
119
|
});
|
|
115
120
|
|
|
116
|
-
// Set max connections
|
|
117
|
-
this.server.maxConnections = this.
|
|
121
|
+
// Set max connections
|
|
122
|
+
this.server.maxConnections = this.maxConnections;
|
|
118
123
|
|
|
119
124
|
// Error handling
|
|
120
125
|
this.server.on('error', (error) => {
|
|
@@ -124,13 +129,16 @@ export class MultiTenantRouter extends EventEmitter {
|
|
|
124
129
|
|
|
125
130
|
// Start listening
|
|
126
131
|
this.server.listen(this.port, this.host, () => {
|
|
132
|
+
const socketPath = this.pgManager.getSocketPath();
|
|
127
133
|
this.logger.info({
|
|
128
134
|
host: this.host,
|
|
129
135
|
port: this.port,
|
|
136
|
+
pgPort: this.pgPort,
|
|
137
|
+
pgSocketPath: socketPath || '(TCP)',
|
|
130
138
|
baseDir: this.memoryMode ? '(in-memory)' : this.baseDir,
|
|
131
139
|
memoryMode: this.memoryMode,
|
|
132
140
|
autoProvision: this.autoProvision,
|
|
133
|
-
|
|
141
|
+
maxConnections: this.maxConnections
|
|
134
142
|
}, 'Multi-tenant router started');
|
|
135
143
|
|
|
136
144
|
this.emit('listening');
|
|
@@ -140,91 +148,81 @@ export class MultiTenantRouter extends EventEmitter {
|
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
/**
|
|
143
|
-
* Handle incoming connection (
|
|
151
|
+
* Handle incoming connection (TCP Proxy)
|
|
152
|
+
* OPTIMIZED: Removed hot path logging for performance
|
|
144
153
|
*/
|
|
145
154
|
async handleConnection(socket) {
|
|
146
|
-
const connId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
147
|
-
const startTime = Date.now();
|
|
148
|
-
|
|
149
|
-
// Optimize socket BEFORE any I/O
|
|
150
|
-
this.optimizeSocket(socket);
|
|
151
|
-
|
|
152
155
|
// Track connection
|
|
153
156
|
this.connections.add(socket);
|
|
154
157
|
|
|
155
|
-
//
|
|
156
|
-
|
|
158
|
+
// Optimize socket BEFORE any I/O
|
|
159
|
+
this.optimizeSocket(socket);
|
|
157
160
|
|
|
158
161
|
let dbName = null;
|
|
159
|
-
let
|
|
162
|
+
let pgSocket = null;
|
|
160
163
|
|
|
161
164
|
try {
|
|
162
165
|
// Extract database name from PostgreSQL handshake
|
|
163
|
-
this.logger.debug({ connId }, 'Reading startup message');
|
|
164
166
|
const { dbName: extractedDbName, buffered } = await extractDatabaseNameFromSocket(socket);
|
|
165
167
|
dbName = extractedDbName;
|
|
166
168
|
|
|
167
|
-
|
|
169
|
+
// Auto-provision database if needed
|
|
170
|
+
if (this.autoProvision) {
|
|
171
|
+
await this.pgManager.createDatabase(dbName);
|
|
172
|
+
}
|
|
168
173
|
|
|
169
|
-
//
|
|
170
|
-
const
|
|
174
|
+
// Connect to real PostgreSQL (prefer Unix socket for speed)
|
|
175
|
+
const socketPath = this.pgManager.getSocketPath();
|
|
176
|
+
if (socketPath) {
|
|
177
|
+
// Unix socket connection (Linux/macOS) - ~30% faster than TCP
|
|
178
|
+
pgSocket = net.connect({ path: socketPath });
|
|
179
|
+
} else {
|
|
180
|
+
// TCP fallback (Windows)
|
|
181
|
+
pgSocket = net.connect({ host: '127.0.0.1', port: this.pgPort });
|
|
182
|
+
}
|
|
171
183
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
routingTimeMs: routingTime
|
|
178
|
-
}, 'Routed to database');
|
|
184
|
+
// Wait for PostgreSQL connection
|
|
185
|
+
await new Promise((resolve, reject) => {
|
|
186
|
+
pgSocket.once('connect', resolve);
|
|
187
|
+
pgSocket.once('error', reject);
|
|
188
|
+
});
|
|
179
189
|
|
|
180
|
-
|
|
181
|
-
socket.unshift(buffered);
|
|
190
|
+
this.optimizeSocket(pgSocket);
|
|
182
191
|
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
db: instance.db,
|
|
186
|
-
closeOnDetach: true,
|
|
187
|
-
inspect: this.inspect
|
|
188
|
-
});
|
|
192
|
+
// Send the buffered startup message to PostgreSQL
|
|
193
|
+
pgSocket.write(buffered);
|
|
189
194
|
|
|
190
|
-
//
|
|
191
|
-
|
|
195
|
+
// Resume client socket (was paused on connect)
|
|
196
|
+
socket.resume();
|
|
192
197
|
|
|
193
|
-
|
|
198
|
+
// Bidirectional pipe (TRUE proxy)
|
|
199
|
+
socket.pipe(pgSocket);
|
|
200
|
+
pgSocket.pipe(socket);
|
|
194
201
|
|
|
195
|
-
// Handle
|
|
202
|
+
// Handle cleanup - optimized: single handler, no logging in hot path
|
|
196
203
|
const cleanup = () => {
|
|
197
|
-
this.logger.debug({ dbName, connId }, 'Connection closed');
|
|
198
|
-
if (handler) {
|
|
199
|
-
handler.detach();
|
|
200
|
-
}
|
|
201
204
|
this.connections.delete(socket);
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
if (pgSocket && !pgSocket.destroyed) pgSocket.destroy();
|
|
206
|
+
if (socket && !socket.destroyed) socket.destroy();
|
|
204
207
|
};
|
|
205
208
|
|
|
206
209
|
socket.once('close', cleanup);
|
|
207
|
-
socket.once('error',
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
socket.once('error', cleanup);
|
|
211
|
+
pgSocket.once('close', () => {
|
|
212
|
+
if (socket && !socket.destroyed) socket.destroy();
|
|
210
213
|
});
|
|
214
|
+
pgSocket.once('error', cleanup);
|
|
211
215
|
|
|
212
|
-
this.emit('connection', { dbName, socket
|
|
216
|
+
this.emit('connection', { dbName, socket });
|
|
213
217
|
} catch (error) {
|
|
214
|
-
|
|
218
|
+
// Only log actual errors
|
|
219
|
+
this.logger.error({ dbName, err: error }, 'Connection error');
|
|
215
220
|
|
|
216
221
|
// Cleanup
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
handler.detach();
|
|
220
|
-
} catch (detachErr) {
|
|
221
|
-
this.logger.debug({ err: detachErr }, 'Error detaching handler');
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
socket.destroy(); // Force close on error
|
|
222
|
+
if (pgSocket && !pgSocket.destroyed) pgSocket.destroy();
|
|
223
|
+
socket.destroy();
|
|
226
224
|
this.connections.delete(socket);
|
|
227
|
-
this.emit('connection-error', { error, dbName
|
|
225
|
+
this.emit('connection-error', { error, dbName });
|
|
228
226
|
}
|
|
229
227
|
}
|
|
230
228
|
|
|
@@ -237,7 +235,7 @@ export class MultiTenantRouter extends EventEmitter {
|
|
|
237
235
|
// Close all connections gracefully
|
|
238
236
|
const activeConns = this.connections.size;
|
|
239
237
|
for (const socket of this.connections) {
|
|
240
|
-
socket.end();
|
|
238
|
+
socket.end();
|
|
241
239
|
}
|
|
242
240
|
this.connections.clear();
|
|
243
241
|
|
|
@@ -248,12 +246,16 @@ export class MultiTenantRouter extends EventEmitter {
|
|
|
248
246
|
});
|
|
249
247
|
}
|
|
250
248
|
|
|
251
|
-
//
|
|
252
|
-
|
|
249
|
+
// Stop SyncManager first (before PostgreSQL)
|
|
250
|
+
if (this.syncManager) {
|
|
251
|
+
await this.syncManager.stop();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Stop PostgreSQL
|
|
255
|
+
await this.pgManager.stop();
|
|
253
256
|
|
|
254
257
|
this.logger.info({
|
|
255
|
-
activeConnections: activeConns
|
|
256
|
-
closedInstances: this.pool.instances.size
|
|
258
|
+
activeConnections: activeConns
|
|
257
259
|
}, 'Router stopped');
|
|
258
260
|
|
|
259
261
|
this.emit('stopped');
|
|
@@ -266,8 +268,9 @@ export class MultiTenantRouter extends EventEmitter {
|
|
|
266
268
|
return {
|
|
267
269
|
port: this.port,
|
|
268
270
|
host: this.host,
|
|
271
|
+
pgPort: this.pgPort,
|
|
269
272
|
activeConnections: this.connections.size,
|
|
270
|
-
|
|
273
|
+
postgres: this.pgManager.getStats()
|
|
271
274
|
};
|
|
272
275
|
}
|
|
273
276
|
|
|
@@ -275,7 +278,7 @@ export class MultiTenantRouter extends EventEmitter {
|
|
|
275
278
|
* List all databases
|
|
276
279
|
*/
|
|
277
280
|
listDatabases() {
|
|
278
|
-
return this.
|
|
281
|
+
return this.pgManager.getStats().databases;
|
|
279
282
|
}
|
|
280
283
|
}
|
|
281
284
|
|