orez 0.0.38 → 0.0.40
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 +16 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +29 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/mutex.d.ts.map +1 -1
- package/dist/mutex.js +13 -2
- package/dist/mutex.js.map +1 -1
- package/dist/pg-proxy.d.ts +3 -2
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +336 -167
- package/dist/pg-proxy.js.map +1 -1
- package/package.json +2 -3
- package/src/cli.ts +29 -4
- package/src/index.ts +10 -2
- package/src/mutex.ts +12 -2
- package/src/pg-proxy.ts +404 -187
package/dist/pg-proxy.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* tcp proxy that makes pglite speak postgresql wire protocol.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* handles the postgresql wire protocol directly using raw tcp sockets,
|
|
5
|
+
* avoiding pg-gateway's Duplex.toWeb() which deadlocks under concurrent
|
|
6
|
+
* connections with large responses.
|
|
6
7
|
*
|
|
7
8
|
* regular connections: forwarded to pglite via execProtocolRaw()
|
|
8
9
|
* replication connections: intercepted, replication protocol faked
|
|
@@ -12,7 +13,6 @@
|
|
|
12
13
|
* query interleaving that causes CVR concurrent modification errors.
|
|
13
14
|
*/
|
|
14
15
|
import { createServer } from 'node:net';
|
|
15
|
-
import { fromNodeSocket } from 'pg-gateway/node';
|
|
16
16
|
import { log } from './log.js';
|
|
17
17
|
import { Mutex } from './mutex.js';
|
|
18
18
|
import { handleReplicationQuery, handleStartReplication } from './replication/handler.js';
|
|
@@ -55,6 +55,7 @@ const QUERY_REWRITES = [
|
|
|
55
55
|
// parameter status messages sent during connection handshake
|
|
56
56
|
// pg_restore and other tools read these to determine server capabilities
|
|
57
57
|
const SERVER_PARAMS = [
|
|
58
|
+
['server_version', '16.4'],
|
|
58
59
|
['server_encoding', 'UTF8'],
|
|
59
60
|
['client_encoding', 'UTF8'],
|
|
60
61
|
['DateStyle', 'ISO, MDY'],
|
|
@@ -63,7 +64,10 @@ const SERVER_PARAMS = [
|
|
|
63
64
|
['TimeZone', 'UTC'],
|
|
64
65
|
['IntervalStyle', 'postgres'],
|
|
65
66
|
];
|
|
66
|
-
//
|
|
67
|
+
// queries to intercept and return no-op success (synthetic SET response)
|
|
68
|
+
// pglite rejects SET TRANSACTION if any query (e.g. SET search_path) ran first
|
|
69
|
+
const NOOP_QUERY_PATTERNS = [/^\s*SET\s+TRANSACTION\b/i, /^\s*SET\s+SESSION\b/i];
|
|
70
|
+
// ── wire protocol helpers ──
|
|
67
71
|
function buildParameterStatus(name, value) {
|
|
68
72
|
const encoder = new TextEncoder();
|
|
69
73
|
const nameBytes = encoder.encode(name);
|
|
@@ -81,12 +85,58 @@ function buildParameterStatus(name, value) {
|
|
|
81
85
|
buf[pos] = 0;
|
|
82
86
|
return buf;
|
|
83
87
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
function buildAuthOk() {
|
|
89
|
+
const buf = new Uint8Array(9);
|
|
90
|
+
buf[0] = 0x52; // 'R' AuthenticationOk
|
|
91
|
+
new DataView(buf.buffer).setInt32(1, 8);
|
|
92
|
+
new DataView(buf.buffer).setInt32(5, 0); // auth ok
|
|
93
|
+
return buf;
|
|
94
|
+
}
|
|
95
|
+
function buildAuthCleartextPassword() {
|
|
96
|
+
const buf = new Uint8Array(9);
|
|
97
|
+
buf[0] = 0x52; // 'R'
|
|
98
|
+
new DataView(buf.buffer).setInt32(1, 8);
|
|
99
|
+
new DataView(buf.buffer).setInt32(5, 3); // cleartext password
|
|
100
|
+
return buf;
|
|
101
|
+
}
|
|
102
|
+
function buildBackendKeyData() {
|
|
103
|
+
const buf = new Uint8Array(13);
|
|
104
|
+
buf[0] = 0x4b; // 'K'
|
|
105
|
+
new DataView(buf.buffer).setInt32(1, 12);
|
|
106
|
+
new DataView(buf.buffer).setInt32(5, process.pid);
|
|
107
|
+
new DataView(buf.buffer).setInt32(9, 0);
|
|
108
|
+
return buf;
|
|
109
|
+
}
|
|
110
|
+
function buildReadyForQuery(status = 0x49) {
|
|
111
|
+
const buf = new Uint8Array(6);
|
|
112
|
+
buf[0] = 0x5a; // 'Z'
|
|
113
|
+
new DataView(buf.buffer).setInt32(1, 5);
|
|
114
|
+
buf[5] = status; // 'I' = idle
|
|
115
|
+
return buf;
|
|
116
|
+
}
|
|
117
|
+
function buildErrorResponse(message) {
|
|
118
|
+
const encoder = new TextEncoder();
|
|
119
|
+
const msgBytes = encoder.encode(message);
|
|
120
|
+
// S(ERROR) + C(code) + M(message) + terminator
|
|
121
|
+
const sField = new Uint8Array([0x53, ...encoder.encode('ERROR'), 0]);
|
|
122
|
+
const cField = new Uint8Array([0x43, ...encoder.encode('08006'), 0]);
|
|
123
|
+
const mField = new Uint8Array([0x4d, ...msgBytes, 0]);
|
|
124
|
+
const terminator = new Uint8Array([0]);
|
|
125
|
+
const bodyLen = 4 + sField.length + cField.length + mField.length + terminator.length;
|
|
126
|
+
const buf = new Uint8Array(1 + bodyLen);
|
|
127
|
+
buf[0] = 0x45; // 'E'
|
|
128
|
+
new DataView(buf.buffer).setInt32(1, bodyLen);
|
|
129
|
+
let pos = 5;
|
|
130
|
+
buf.set(sField, pos);
|
|
131
|
+
pos += sField.length;
|
|
132
|
+
buf.set(cField, pos);
|
|
133
|
+
pos += cField.length;
|
|
134
|
+
buf.set(mField, pos);
|
|
135
|
+
pos += mField.length;
|
|
136
|
+
buf.set(terminator, pos);
|
|
137
|
+
return buf;
|
|
138
|
+
}
|
|
139
|
+
// ── query helpers ──
|
|
90
140
|
function extractParseQuery(data) {
|
|
91
141
|
if (data[0] !== 0x50)
|
|
92
142
|
return null;
|
|
@@ -99,9 +149,6 @@ function extractParseQuery(data) {
|
|
|
99
149
|
offset++;
|
|
100
150
|
return new TextDecoder().decode(data.subarray(queryStart, offset));
|
|
101
151
|
}
|
|
102
|
-
/**
|
|
103
|
-
* rebuild a Parse message with a modified query string.
|
|
104
|
-
*/
|
|
105
152
|
function rebuildParseMessage(data, newQuery) {
|
|
106
153
|
let offset = 5;
|
|
107
154
|
while (offset < data.length && data[offset] !== 0)
|
|
@@ -129,9 +176,6 @@ function rebuildParseMessage(data, newQuery) {
|
|
|
129
176
|
result.set(suffix, pos);
|
|
130
177
|
return result;
|
|
131
178
|
}
|
|
132
|
-
/**
|
|
133
|
-
* rebuild a Simple Query message with a modified query string.
|
|
134
|
-
*/
|
|
135
179
|
function rebuildSimpleQuery(newQuery) {
|
|
136
180
|
const encoder = new TextEncoder();
|
|
137
181
|
const queryBytes = encoder.encode(newQuery + '\0');
|
|
@@ -141,9 +185,6 @@ function rebuildSimpleQuery(newQuery) {
|
|
|
141
185
|
buf.set(queryBytes, 5);
|
|
142
186
|
return buf;
|
|
143
187
|
}
|
|
144
|
-
/**
|
|
145
|
-
* intercept and rewrite query messages to make pglite look like real postgres.
|
|
146
|
-
*/
|
|
147
188
|
function interceptQuery(data) {
|
|
148
189
|
const msgType = data[0];
|
|
149
190
|
if (msgType === 0x51) {
|
|
@@ -183,9 +224,6 @@ function interceptQuery(data) {
|
|
|
183
224
|
}
|
|
184
225
|
return data;
|
|
185
226
|
}
|
|
186
|
-
/**
|
|
187
|
-
* check if a query should be intercepted as a no-op.
|
|
188
|
-
*/
|
|
189
227
|
function isNoopQuery(data) {
|
|
190
228
|
let query = null;
|
|
191
229
|
if (data[0] === 0x51) {
|
|
@@ -200,9 +238,6 @@ function isNoopQuery(data) {
|
|
|
200
238
|
return false;
|
|
201
239
|
return NOOP_QUERY_PATTERNS.some((p) => p.test(query));
|
|
202
240
|
}
|
|
203
|
-
/**
|
|
204
|
-
* build a synthetic "SET" command complete response.
|
|
205
|
-
*/
|
|
206
241
|
function buildSetCompleteResponse() {
|
|
207
242
|
const encoder = new TextEncoder();
|
|
208
243
|
const tag = encoder.encode('SET\0');
|
|
@@ -219,18 +254,12 @@ function buildSetCompleteResponse() {
|
|
|
219
254
|
result.set(rfq, cc.length);
|
|
220
255
|
return result;
|
|
221
256
|
}
|
|
222
|
-
/**
|
|
223
|
-
* build a synthetic ParseComplete response for extended protocol no-ops.
|
|
224
|
-
*/
|
|
225
257
|
function buildParseCompleteResponse() {
|
|
226
258
|
const pc = new Uint8Array(5);
|
|
227
259
|
pc[0] = 0x31; // ParseComplete
|
|
228
260
|
new DataView(pc.buffer).setInt32(1, 4);
|
|
229
261
|
return pc;
|
|
230
262
|
}
|
|
231
|
-
/**
|
|
232
|
-
* strip ReadyForQuery messages from a response buffer.
|
|
233
|
-
*/
|
|
234
263
|
function stripReadyForQuery(data) {
|
|
235
264
|
if (data.length === 0)
|
|
236
265
|
return data;
|
|
@@ -260,6 +289,257 @@ function stripReadyForQuery(data) {
|
|
|
260
289
|
}
|
|
261
290
|
return result;
|
|
262
291
|
}
|
|
292
|
+
// ── socket write with backpressure ──
|
|
293
|
+
function socketWrite(socket, data) {
|
|
294
|
+
if (data.length === 0 || socket.destroyed)
|
|
295
|
+
return Promise.resolve();
|
|
296
|
+
return new Promise((resolve, reject) => {
|
|
297
|
+
const ok = socket.write(data, (err) => (err ? reject(err) : resolve()));
|
|
298
|
+
// if buffer is full, the callback still fires when flushed
|
|
299
|
+
if (!ok)
|
|
300
|
+
void 0;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// ── startup handshake ──
|
|
304
|
+
// parse startup message from raw bytes.
|
|
305
|
+
// handles SSLRequest (8 bytes, code 80877103) and StartupMessage.
|
|
306
|
+
function parseStartupMessage(buf) {
|
|
307
|
+
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
308
|
+
const len = dv.getInt32(0);
|
|
309
|
+
const code = dv.getInt32(4);
|
|
310
|
+
// SSL request: length=8, code=80877103
|
|
311
|
+
if (len === 8 && code === 80877103) {
|
|
312
|
+
return { isSSL: true, params: {} };
|
|
313
|
+
}
|
|
314
|
+
// startup message: length, protocol(196608=3.0), then key=value pairs
|
|
315
|
+
const params = {};
|
|
316
|
+
let offset = 8;
|
|
317
|
+
while (offset < len) {
|
|
318
|
+
const keyStart = offset;
|
|
319
|
+
while (offset < buf.length && buf[offset] !== 0)
|
|
320
|
+
offset++;
|
|
321
|
+
const key = buf.subarray(keyStart, offset).toString();
|
|
322
|
+
offset++;
|
|
323
|
+
if (!key)
|
|
324
|
+
break; // double-null = end of params
|
|
325
|
+
const valStart = offset;
|
|
326
|
+
while (offset < buf.length && buf[offset] !== 0)
|
|
327
|
+
offset++;
|
|
328
|
+
params[key] = buf.subarray(valStart, offset).toString();
|
|
329
|
+
offset++;
|
|
330
|
+
}
|
|
331
|
+
return { isSSL: false, params };
|
|
332
|
+
}
|
|
333
|
+
// read exactly `n` bytes from socket
|
|
334
|
+
function readBytes(socket, n) {
|
|
335
|
+
return new Promise((resolve, reject) => {
|
|
336
|
+
let collected = Buffer.alloc(0);
|
|
337
|
+
const onData = (chunk) => {
|
|
338
|
+
collected = Buffer.concat([collected, chunk]);
|
|
339
|
+
if (collected.length >= n) {
|
|
340
|
+
socket.removeListener('data', onData);
|
|
341
|
+
socket.removeListener('error', onError);
|
|
342
|
+
socket.removeListener('close', onClose);
|
|
343
|
+
socket.pause();
|
|
344
|
+
resolve(collected);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const onError = (err) => {
|
|
348
|
+
socket.removeListener('data', onData);
|
|
349
|
+
socket.removeListener('close', onClose);
|
|
350
|
+
reject(err);
|
|
351
|
+
};
|
|
352
|
+
const onClose = () => {
|
|
353
|
+
socket.removeListener('data', onData);
|
|
354
|
+
socket.removeListener('error', onError);
|
|
355
|
+
reject(new Error('socket closed'));
|
|
356
|
+
};
|
|
357
|
+
socket.on('data', onData);
|
|
358
|
+
socket.on('error', onError);
|
|
359
|
+
socket.on('close', onClose);
|
|
360
|
+
socket.resume();
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
// perform the startup handshake (SSL negotiation, auth, parameter status)
|
|
364
|
+
async function performHandshake(socket, config) {
|
|
365
|
+
// read initial message length (first 4 bytes)
|
|
366
|
+
let buf = await readBytes(socket, 8);
|
|
367
|
+
// check for SSL request
|
|
368
|
+
const startup = parseStartupMessage(buf);
|
|
369
|
+
if (startup.isSSL) {
|
|
370
|
+
// reject SSL, client will reconnect without it
|
|
371
|
+
socket.write(Buffer.from('N'));
|
|
372
|
+
buf = await readBytes(socket, 8);
|
|
373
|
+
}
|
|
374
|
+
// now we have startup message header - read the rest if needed
|
|
375
|
+
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
376
|
+
const msgLen = dv.getInt32(0);
|
|
377
|
+
if (buf.length < msgLen) {
|
|
378
|
+
const rest = await readBytes(socket, msgLen - buf.length);
|
|
379
|
+
buf = Buffer.concat([buf, rest]);
|
|
380
|
+
}
|
|
381
|
+
const { params } = parseStartupMessage(buf);
|
|
382
|
+
// request cleartext password
|
|
383
|
+
socket.write(buildAuthCleartextPassword());
|
|
384
|
+
// read password message: type(1) + len(4) + password + null
|
|
385
|
+
const pwBuf = await readBytes(socket, 5);
|
|
386
|
+
const pwDv = new DataView(pwBuf.buffer, pwBuf.byteOffset, pwBuf.byteLength);
|
|
387
|
+
const pwLen = pwDv.getInt32(1);
|
|
388
|
+
let fullPwBuf = pwBuf;
|
|
389
|
+
if (fullPwBuf.length < 1 + pwLen) {
|
|
390
|
+
const rest = await readBytes(socket, 1 + pwLen - fullPwBuf.length);
|
|
391
|
+
fullPwBuf = Buffer.concat([fullPwBuf, rest]);
|
|
392
|
+
}
|
|
393
|
+
const password = fullPwBuf.subarray(5, 1 + pwLen - 1).toString();
|
|
394
|
+
// validate credentials
|
|
395
|
+
if (params.user !== config.pgUser || password !== config.pgPassword) {
|
|
396
|
+
socket.write(buildErrorResponse('authentication failed'));
|
|
397
|
+
socket.write(buildReadyForQuery());
|
|
398
|
+
socket.destroy();
|
|
399
|
+
throw new Error('auth failed');
|
|
400
|
+
}
|
|
401
|
+
// auth ok
|
|
402
|
+
socket.write(buildAuthOk());
|
|
403
|
+
// send parameter status messages
|
|
404
|
+
for (const [name, value] of SERVER_PARAMS) {
|
|
405
|
+
socket.write(buildParameterStatus(name, value));
|
|
406
|
+
}
|
|
407
|
+
// backend key data
|
|
408
|
+
socket.write(buildBackendKeyData());
|
|
409
|
+
// ready for query
|
|
410
|
+
socket.write(buildReadyForQuery());
|
|
411
|
+
return { params };
|
|
412
|
+
}
|
|
413
|
+
// ── message loop ──
|
|
414
|
+
// process messages from a connected, authenticated client.
|
|
415
|
+
// uses callback-based 'data' events instead of async iterators
|
|
416
|
+
// for reliable behavior across runtimes (node.js, bun).
|
|
417
|
+
function messageLoop(socket, db, mutex, isReplicationConnection, replicationDb, replicationMutex) {
|
|
418
|
+
return new Promise((resolve, reject) => {
|
|
419
|
+
let buffer = Buffer.alloc(0);
|
|
420
|
+
let processing = false;
|
|
421
|
+
async function processBuffer() {
|
|
422
|
+
if (processing)
|
|
423
|
+
return;
|
|
424
|
+
processing = true;
|
|
425
|
+
socket.pause();
|
|
426
|
+
try {
|
|
427
|
+
while (buffer.length >= 5) {
|
|
428
|
+
const msgType = buffer[0];
|
|
429
|
+
const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
430
|
+
const msgLen = dv.getInt32(1);
|
|
431
|
+
const totalLen = 1 + msgLen;
|
|
432
|
+
if (buffer.length < totalLen)
|
|
433
|
+
break; // need more data
|
|
434
|
+
// copy message out before modifying buffer
|
|
435
|
+
const message = new Uint8Array(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + totalLen));
|
|
436
|
+
buffer = buffer.subarray(totalLen);
|
|
437
|
+
// handle Terminate message
|
|
438
|
+
if (msgType === 0x58) {
|
|
439
|
+
resolve();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
// handle replication connections
|
|
443
|
+
if (isReplicationConnection) {
|
|
444
|
+
await handleReplicationMsg(message, socket, replicationDb, replicationMutex);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
// handle regular messages
|
|
448
|
+
await handleRegularMessage(message, socket, db, mutex);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
reject(err);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
processing = false;
|
|
456
|
+
socket.resume();
|
|
457
|
+
}
|
|
458
|
+
socket.on('data', (chunk) => {
|
|
459
|
+
buffer = buffer.length > 0 ? Buffer.concat([buffer, chunk]) : chunk;
|
|
460
|
+
processBuffer();
|
|
461
|
+
});
|
|
462
|
+
socket.on('end', () => resolve());
|
|
463
|
+
socket.on('error', (err) => reject(err));
|
|
464
|
+
socket.on('close', () => resolve());
|
|
465
|
+
socket.resume();
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
async function handleRegularMessage(data, socket, db, mutex) {
|
|
469
|
+
// check for no-op queries
|
|
470
|
+
if (isNoopQuery(data)) {
|
|
471
|
+
if (data[0] === 0x51) {
|
|
472
|
+
await socketWrite(socket, buildSetCompleteResponse());
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
else if (data[0] === 0x50) {
|
|
476
|
+
await socketWrite(socket, buildParseCompleteResponse());
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// intercept and rewrite queries
|
|
481
|
+
data = interceptQuery(data);
|
|
482
|
+
// serialize pglite access
|
|
483
|
+
await mutex.acquire();
|
|
484
|
+
let result;
|
|
485
|
+
try {
|
|
486
|
+
result = await db.execProtocolRaw(data, { throwOnError: false });
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
mutex.release();
|
|
490
|
+
throw err;
|
|
491
|
+
}
|
|
492
|
+
// strip ReadyForQuery from non-Sync/non-SimpleQuery responses
|
|
493
|
+
if (data[0] !== 0x53 && data[0] !== 0x51) {
|
|
494
|
+
result = stripReadyForQuery(result);
|
|
495
|
+
}
|
|
496
|
+
mutex.release();
|
|
497
|
+
// write response directly to socket
|
|
498
|
+
await socketWrite(socket, result);
|
|
499
|
+
}
|
|
500
|
+
async function handleReplicationMsg(data, socket, db, mutex) {
|
|
501
|
+
if (data[0] !== 0x51)
|
|
502
|
+
return;
|
|
503
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
504
|
+
const len = view.getInt32(1);
|
|
505
|
+
const query = new TextDecoder().decode(data.subarray(5, 1 + len - 1)).replace(/\0$/, '');
|
|
506
|
+
const upper = query.trim().toUpperCase();
|
|
507
|
+
log.debug.proxy(`repl query: ${query.slice(0, 200)}`);
|
|
508
|
+
if (upper.startsWith('START_REPLICATION')) {
|
|
509
|
+
const writer = {
|
|
510
|
+
write(chunk) {
|
|
511
|
+
if (!socket.destroyed) {
|
|
512
|
+
socket.write(chunk);
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
// drain incoming standby status updates
|
|
517
|
+
socket.on('data', (_chunk) => { });
|
|
518
|
+
socket.on('close', () => socket.destroy());
|
|
519
|
+
// this runs indefinitely until the socket closes
|
|
520
|
+
await handleStartReplication(query, writer, db, mutex).catch((err) => {
|
|
521
|
+
log.debug.proxy(`replication stream ended: ${err}`);
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
// handle replication queries + fallthrough to pglite
|
|
526
|
+
await mutex.acquire();
|
|
527
|
+
try {
|
|
528
|
+
const response = await handleReplicationQuery(query, db);
|
|
529
|
+
if (response) {
|
|
530
|
+
await socketWrite(socket, response);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
// apply query rewrites before forwarding
|
|
534
|
+
data = interceptQuery(data);
|
|
535
|
+
const result = await db.execProtocolRaw(data, { throwOnError: false });
|
|
536
|
+
await socketWrite(socket, result);
|
|
537
|
+
}
|
|
538
|
+
finally {
|
|
539
|
+
mutex.release();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// ── main entry point ──
|
|
263
543
|
export async function startPgProxy(dbInput, config) {
|
|
264
544
|
// normalize input: single PGlite instance = use it for all databases (backwards compat for tests)
|
|
265
545
|
const instances = 'postgres' in dbInput
|
|
@@ -271,7 +551,6 @@ export async function startPgProxy(dbInput, config) {
|
|
|
271
551
|
cvr: new Mutex(),
|
|
272
552
|
cdb: new Mutex(),
|
|
273
553
|
};
|
|
274
|
-
// helper to get instance + mutex for a database name
|
|
275
554
|
function getDbContext(dbName) {
|
|
276
555
|
if (dbName === 'zero_cvr')
|
|
277
556
|
return { db: instances.cvr, mutex: mutexes.cvr };
|
|
@@ -280,105 +559,39 @@ export async function startPgProxy(dbInput, config) {
|
|
|
280
559
|
return { db: instances.postgres, mutex: mutexes.postgres };
|
|
281
560
|
}
|
|
282
561
|
const server = createServer(async (socket) => {
|
|
283
|
-
// prevent idle timeouts from killing connections
|
|
284
562
|
socket.setKeepAlive(true, 30000);
|
|
285
563
|
socket.setTimeout(0);
|
|
564
|
+
socket.setNoDelay(true);
|
|
286
565
|
let dbName = 'postgres';
|
|
287
566
|
let isReplicationConnection = false;
|
|
288
|
-
// clean up pglite transaction state when a client disconnects
|
|
289
|
-
socket.on('close', async () => {
|
|
290
|
-
const { db, mutex } = getDbContext(dbName);
|
|
291
|
-
await mutex.acquire();
|
|
292
|
-
try {
|
|
293
|
-
await db.exec('ROLLBACK');
|
|
294
|
-
}
|
|
295
|
-
catch {
|
|
296
|
-
// no transaction to rollback
|
|
297
|
-
}
|
|
298
|
-
finally {
|
|
299
|
-
mutex.release();
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
567
|
try {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
async onStartup(state) {
|
|
324
|
-
const params = state.clientParams;
|
|
325
|
-
if (params?.replication === 'database') {
|
|
326
|
-
isReplicationConnection = true;
|
|
327
|
-
}
|
|
328
|
-
dbName = params?.database || 'postgres';
|
|
329
|
-
log.debug.proxy(`connection: db=${dbName} user=${params?.user} replication=${params?.replication || 'none'}`);
|
|
330
|
-
const { db } = getDbContext(dbName);
|
|
331
|
-
await db.waitReady;
|
|
332
|
-
},
|
|
333
|
-
async onMessage(data, state) {
|
|
334
|
-
if (!state.isAuthenticated)
|
|
335
|
-
return;
|
|
336
|
-
// handle replication connections (always go to postgres instance)
|
|
337
|
-
if (isReplicationConnection) {
|
|
338
|
-
if (data[0] === 0x51) {
|
|
339
|
-
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
340
|
-
const len = view.getInt32(1);
|
|
341
|
-
const query = new TextDecoder()
|
|
342
|
-
.decode(data.subarray(5, 1 + len - 1))
|
|
343
|
-
.replace(/\0$/, '');
|
|
344
|
-
log.debug.proxy(`repl query: ${query.slice(0, 200)}`);
|
|
345
|
-
}
|
|
346
|
-
return handleReplicationMessage(data, socket, instances.postgres, mutexes.postgres, connection);
|
|
347
|
-
}
|
|
348
|
-
// check for no-op queries
|
|
349
|
-
if (isNoopQuery(data)) {
|
|
350
|
-
if (data[0] === 0x51) {
|
|
351
|
-
return buildSetCompleteResponse();
|
|
352
|
-
}
|
|
353
|
-
else if (data[0] === 0x50) {
|
|
354
|
-
return buildParseCompleteResponse();
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
// intercept and rewrite queries
|
|
358
|
-
data = interceptQuery(data);
|
|
359
|
-
// message-level locking on the connection's pglite instance
|
|
360
|
-
const { db, mutex } = getDbContext(dbName);
|
|
361
|
-
await mutex.acquire();
|
|
362
|
-
let result;
|
|
363
|
-
try {
|
|
364
|
-
result = await db.execProtocolRaw(data, {
|
|
365
|
-
throwOnError: false,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
catch (err) {
|
|
369
|
-
mutex.release();
|
|
370
|
-
throw err;
|
|
371
|
-
}
|
|
372
|
-
// strip ReadyForQuery from non-Sync/non-SimpleQuery responses
|
|
373
|
-
if (data[0] !== 0x53 && data[0] !== 0x51) {
|
|
374
|
-
result = stripReadyForQuery(result);
|
|
375
|
-
}
|
|
376
|
-
mutex.release();
|
|
377
|
-
return result;
|
|
378
|
-
},
|
|
568
|
+
// perform startup handshake
|
|
569
|
+
const { params } = await performHandshake(socket, config);
|
|
570
|
+
dbName = params.database || 'postgres';
|
|
571
|
+
isReplicationConnection = params.replication === 'database';
|
|
572
|
+
log.debug.proxy(`connection: db=${dbName} user=${params.user} replication=${params.replication || 'none'}`);
|
|
573
|
+
const { db } = getDbContext(dbName);
|
|
574
|
+
await db.waitReady;
|
|
575
|
+
// clean up pglite transaction state when client disconnects
|
|
576
|
+
socket.on('close', async () => {
|
|
577
|
+
const { db: closeDb, mutex: closeMutex } = getDbContext(dbName);
|
|
578
|
+
await closeMutex.acquire();
|
|
579
|
+
try {
|
|
580
|
+
await closeDb.exec('ROLLBACK');
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
// no transaction to rollback
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
closeMutex.release();
|
|
587
|
+
}
|
|
379
588
|
});
|
|
589
|
+
// enter message processing loop
|
|
590
|
+
const { db: msgDb, mutex: msgMutex } = getDbContext(dbName);
|
|
591
|
+
await messageLoop(socket, msgDb, msgMutex, isReplicationConnection, instances.postgres, mutexes.postgres);
|
|
380
592
|
}
|
|
381
593
|
catch (err) {
|
|
594
|
+
// connection error during handshake or message loop
|
|
382
595
|
if (!socket.destroyed) {
|
|
383
596
|
socket.destroy();
|
|
384
597
|
}
|
|
@@ -392,48 +605,4 @@ export async function startPgProxy(dbInput, config) {
|
|
|
392
605
|
server.on('error', reject);
|
|
393
606
|
});
|
|
394
607
|
}
|
|
395
|
-
async function handleReplicationMessage(data, socket, db, mutex, connection) {
|
|
396
|
-
if (data[0] !== 0x51)
|
|
397
|
-
return undefined;
|
|
398
|
-
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
399
|
-
const len = view.getInt32(1);
|
|
400
|
-
const query = new TextDecoder().decode(data.subarray(5, 1 + len - 1)).replace(/\0$/, '');
|
|
401
|
-
const upper = query.trim().toUpperCase();
|
|
402
|
-
// check if this is a START_REPLICATION command
|
|
403
|
-
if (upper.startsWith('START_REPLICATION')) {
|
|
404
|
-
await connection.detach();
|
|
405
|
-
const writer = {
|
|
406
|
-
write(chunk) {
|
|
407
|
-
if (!socket.destroyed) {
|
|
408
|
-
socket.write(chunk);
|
|
409
|
-
}
|
|
410
|
-
},
|
|
411
|
-
};
|
|
412
|
-
// drain incoming standby status updates
|
|
413
|
-
socket.on('data', (_chunk) => { });
|
|
414
|
-
socket.on('close', () => {
|
|
415
|
-
socket.destroy();
|
|
416
|
-
});
|
|
417
|
-
handleStartReplication(query, writer, db, mutex).catch((err) => {
|
|
418
|
-
log.debug.proxy(`replication stream ended: ${err}`);
|
|
419
|
-
});
|
|
420
|
-
return undefined;
|
|
421
|
-
}
|
|
422
|
-
// handle replication queries + fallthrough to pglite, all under mutex
|
|
423
|
-
await mutex.acquire();
|
|
424
|
-
try {
|
|
425
|
-
const response = await handleReplicationQuery(query, db);
|
|
426
|
-
if (response)
|
|
427
|
-
return response;
|
|
428
|
-
// apply query rewrites before forwarding
|
|
429
|
-
data = interceptQuery(data);
|
|
430
|
-
// fall through to pglite for unrecognized queries
|
|
431
|
-
return await db.execProtocolRaw(data, {
|
|
432
|
-
throwOnError: false,
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
finally {
|
|
436
|
-
mutex.release();
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
608
|
//# sourceMappingURL=pg-proxy.js.map
|