orez 0.5.11 → 0.5.13
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/dist/cf-do/tx-journal.d.ts +6 -5
- package/dist/cf-do/tx-journal.d.ts.map +1 -1
- package/dist/cf-do/tx-journal.js +116 -27
- package/dist/cf-do/tx-journal.js.map +1 -1
- package/dist/pg-proxy-browser.d.ts +2 -0
- package/dist/pg-proxy-browser.d.ts.map +1 -1
- package/dist/pg-proxy-browser.js +56 -102
- package/dist/pg-proxy-browser.js.map +1 -1
- package/dist/pg-proxy-do-backend.d.ts +8 -0
- package/dist/pg-proxy-do-backend.d.ts.map +1 -1
- package/dist/pg-proxy-do-backend.js +27 -9
- package/dist/pg-proxy-do-backend.js.map +1 -1
- package/dist/pg-sqlite-compiler/compiler.d.ts +17 -0
- package/dist/pg-sqlite-compiler/compiler.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/compiler.js +80 -0
- package/dist/pg-sqlite-compiler/compiler.js.map +1 -0
- package/dist/pg-sqlite-compiler/index.d.ts +1 -16
- package/dist/pg-sqlite-compiler/index.d.ts.map +1 -1
- package/dist/pg-sqlite-compiler/index.js +2 -79
- package/dist/pg-sqlite-compiler/index.js.map +1 -1
- package/dist/replication/handler.d.ts +8 -7
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +147 -116
- package/dist/replication/handler.js.map +1 -1
- package/dist/worker/browser-build-config.d.ts.map +1 -1
- package/dist/worker/browser-build-config.js +1 -0
- package/dist/worker/browser-build-config.js.map +1 -1
- package/dist/worker/browser-embed.d.ts +3 -2
- package/dist/worker/browser-embed.d.ts.map +1 -1
- package/dist/worker/browser-embed.js +187 -150
- package/dist/worker/browser-embed.js.map +1 -1
- package/dist/worker/cf-instance-runtime.d.ts +62 -0
- package/dist/worker/cf-instance-runtime.d.ts.map +1 -0
- package/dist/worker/cf-instance-runtime.js +260 -0
- package/dist/worker/cf-instance-runtime.js.map +1 -0
- package/dist/worker/cf-patches.d.ts +9 -6
- package/dist/worker/cf-patches.d.ts.map +1 -1
- package/dist/worker/cf-patches.js +117 -60
- package/dist/worker/cf-patches.js.map +1 -1
- package/dist/worker/durable-object-websocket-handoff.d.ts +2 -0
- package/dist/worker/durable-object-websocket-handoff.d.ts.map +1 -1
- package/dist/worker/durable-object-websocket-handoff.js +22 -41
- package/dist/worker/durable-object-websocket-handoff.js.map +1 -1
- package/dist/worker/embed-generation.d.ts +9 -6
- package/dist/worker/embed-generation.d.ts.map +1 -1
- package/dist/worker/embed-generation.js +27 -6
- package/dist/worker/embed-generation.js.map +1 -1
- package/dist/worker/shims/fastify-websocket.d.ts +3 -0
- package/dist/worker/shims/fastify-websocket.d.ts.map +1 -0
- package/dist/worker/shims/fastify-websocket.js +3 -0
- package/dist/worker/shims/fastify-websocket.js.map +1 -0
- package/dist/worker/shims/fastify.d.ts +2 -10
- package/dist/worker/shims/fastify.d.ts.map +1 -1
- package/dist/worker/shims/fastify.js +14 -21
- package/dist/worker/shims/fastify.js.map +1 -1
- package/dist/worker/shims/postgres-browser.d.ts +1 -1
- package/dist/worker/shims/postgres-browser.d.ts.map +1 -1
- package/dist/worker/shims/postgres-browser.js +29 -20
- package/dist/worker/shims/postgres-browser.js.map +1 -1
- package/dist/worker/shims/sqlite-browser.d.ts +2 -3
- package/dist/worker/shims/sqlite-browser.d.ts.map +1 -1
- package/dist/worker/shims/sqlite-browser.js +2 -3
- package/dist/worker/shims/sqlite-browser.js.map +1 -1
- package/dist/worker/shims/sqlite.d.ts.map +1 -1
- package/dist/worker/shims/sqlite.js +56 -35
- package/dist/worker/shims/sqlite.js.map +1 -1
- package/dist/worker/shims/ws.d.ts.map +1 -1
- package/dist/worker/shims/ws.js +54 -39
- package/dist/worker/shims/ws.js.map +1 -1
- package/dist/worker/shims/zero-process-env.d.ts.map +1 -1
- package/dist/worker/shims/zero-process-env.js +25 -4
- package/dist/worker/shims/zero-process-env.js.map +1 -1
- package/dist/worker/zero-cache-embed-cf.d.ts +5 -1
- package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -1
- package/dist/worker/zero-cache-embed-cf.js +104 -137
- package/dist/worker/zero-cache-embed-cf.js.map +1 -1
- package/package.json +6 -2
|
@@ -65,55 +65,65 @@ function lsnFloorFromTime() {
|
|
|
65
65
|
const timeLsn = BigInt(Date.now()) << LSN_TIME_SHIFT;
|
|
66
66
|
return timeLsn > MIN_LSN ? timeLsn : MIN_LSN;
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
let
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
68
|
+
const DEFAULT_REPLICATION_INSTANCE = '__orez_default_replication__';
|
|
69
|
+
const replicationStates = new Map();
|
|
70
|
+
function newReplicationState(instanceId, instanceLog) {
|
|
71
|
+
return {
|
|
72
|
+
cachedColumnTypeOids: null,
|
|
73
|
+
cachedColumns: new Map(),
|
|
74
|
+
cachedExcludedColumns: null,
|
|
75
|
+
cachedTableKeyColumns: null,
|
|
76
|
+
currentLsn: lsnFloorFromTime(),
|
|
77
|
+
instanceId,
|
|
78
|
+
lastConfirmProgressAt: 0,
|
|
79
|
+
lastFeedbackAtPerf: 0,
|
|
80
|
+
lastStreamActivityAt: 0,
|
|
81
|
+
lastStreamedWatermark: 0,
|
|
82
|
+
lastWriteSignalAt: 0,
|
|
83
|
+
log: instanceLog,
|
|
84
|
+
pendingConfirmLsn: 0n,
|
|
85
|
+
processedConfirmLsn: 0n,
|
|
86
|
+
replicationWakeup: null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function replicationState(instanceId) {
|
|
90
|
+
const key = instanceId ?? DEFAULT_REPLICATION_INSTANCE;
|
|
91
|
+
let state = replicationStates.get(key);
|
|
92
|
+
if (!state) {
|
|
93
|
+
state = newReplicationState(instanceId);
|
|
94
|
+
replicationStates.set(key, state);
|
|
95
|
+
}
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
98
|
+
export function getReplicationHealth(instanceId) {
|
|
99
|
+
const state = replicationState(instanceId);
|
|
100
|
+
return {
|
|
101
|
+
lastWriteSignalAt: state.lastWriteSignalAt,
|
|
102
|
+
lastConfirmProgressAt: state.lastConfirmProgressAt,
|
|
103
|
+
lastStreamActivityAt: state.lastStreamActivityAt,
|
|
104
|
+
};
|
|
96
105
|
}
|
|
97
106
|
/** record durable consumer progress (confirm purge, fresh slot, or a
|
|
98
107
|
* completed zero reset — all mean the backlog is not stuck). */
|
|
99
|
-
export function markReplicationProgress() {
|
|
100
|
-
lastConfirmProgressAt = Date.now();
|
|
108
|
+
export function markReplicationProgress(instanceId) {
|
|
109
|
+
replicationState(instanceId).lastConfirmProgressAt = Date.now();
|
|
101
110
|
}
|
|
102
111
|
/**
|
|
103
112
|
* note a confirmed-flush LSN from the consumer's standby status update and
|
|
104
113
|
* wake the replication loop so it purges the confirmed batches.
|
|
105
114
|
*/
|
|
106
|
-
export function noteConfirmedFlushLsn(lsn) {
|
|
115
|
+
export function noteConfirmedFlushLsn(lsn, instanceId) {
|
|
116
|
+
const state = replicationState(instanceId);
|
|
107
117
|
// any standby status update — even one that doesn't advance the LSN — is
|
|
108
118
|
// proof the consumer is alive on the wire. the stale-unconfirmed reconnect
|
|
109
119
|
// must not kill a stream whose consumer is still chewing a large backlog
|
|
110
120
|
// (2026-07 CF incident: the 60s reconnect kept beating the consumer's first
|
|
111
121
|
// ack, so a 10k-row backlog re-streamed on every cycle, forever).
|
|
112
|
-
lastFeedbackAtPerf = performance.now();
|
|
113
|
-
if (lsn <= pendingConfirmLsn)
|
|
122
|
+
state.lastFeedbackAtPerf = performance.now();
|
|
123
|
+
if (lsn <= state.pendingConfirmLsn)
|
|
114
124
|
return;
|
|
115
|
-
pendingConfirmLsn = lsn;
|
|
116
|
-
|
|
125
|
+
state.pendingConfirmLsn = lsn;
|
|
126
|
+
state.replicationWakeup?.();
|
|
117
127
|
}
|
|
118
128
|
/**
|
|
119
129
|
* incremental parser for client→server traffic on a replication connection.
|
|
@@ -157,14 +167,14 @@ export function createReplicationFeedbackParser(onFlushedLsn = noteConfirmedFlus
|
|
|
157
167
|
};
|
|
158
168
|
}
|
|
159
169
|
// direct wakeup from proxy — bypasses pg_notify for instant replication
|
|
160
|
-
let _replicationWakeup = null;
|
|
161
170
|
/** signal the replication handler that changes may be available.
|
|
162
171
|
* called by the proxy after executing writes on the postgres instance. */
|
|
163
|
-
export function signalReplicationChange() {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
export function signalReplicationChange(instanceId) {
|
|
173
|
+
const state = replicationState(instanceId);
|
|
174
|
+
state.lastWriteSignalAt = Date.now();
|
|
175
|
+
state.replicationWakeup?.();
|
|
176
|
+
const globalWakeup = instanceId === undefined ? globalThis.__orez_signal_replication : undefined;
|
|
177
|
+
if (typeof globalWakeup === 'function' && globalWakeup !== state.replicationWakeup) {
|
|
168
178
|
globalWakeup();
|
|
169
179
|
}
|
|
170
180
|
}
|
|
@@ -172,37 +182,30 @@ export function signalReplicationChange() {
|
|
|
172
182
|
// zero-cache reconnects the replication stream after initial sync, and if setup
|
|
173
183
|
// takes too long (holding the mutex, blocking proxy queries), zero-cache's
|
|
174
184
|
// queries timeout and it kills the connection.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
let cachedColumnTypeOids = null;
|
|
178
|
-
function unconfirmedReconnectMs() {
|
|
179
|
-
const raw = process.env.OREZ_REPLICATION_UNCONFIRMED_RECONNECT_MS;
|
|
185
|
+
function unconfirmedReconnectMs(state) {
|
|
186
|
+
const raw = state.unconfirmedReconnectMs ?? process.env.OREZ_REPLICATION_UNCONFIRMED_RECONNECT_MS;
|
|
180
187
|
if (!raw)
|
|
181
188
|
return DEFAULT_UNCONFIRMED_RECONNECT_MS;
|
|
182
189
|
const parsed = Number(raw);
|
|
183
190
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_UNCONFIRMED_RECONNECT_MS;
|
|
184
191
|
}
|
|
185
192
|
/** reset module state (for tests) */
|
|
186
|
-
export function resetReplicationState() {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
cachedTableKeyColumns = null;
|
|
196
|
-
cachedExcludedColumns = null;
|
|
197
|
-
cachedColumnTypeOids = null;
|
|
198
|
-
cachedColumns.clear();
|
|
193
|
+
export function resetReplicationState(instanceId, env, instanceLog) {
|
|
194
|
+
const state = newReplicationState(instanceId, instanceLog);
|
|
195
|
+
const runtimeEnv = env ?? (instanceId === undefined ? process.env : undefined);
|
|
196
|
+
state.publications = runtimeEnv?.ZERO_APP_PUBLICATIONS?.trim();
|
|
197
|
+
state.unconfirmedReconnectMs = runtimeEnv?.OREZ_REPLICATION_UNCONFIRMED_RECONNECT_MS;
|
|
198
|
+
replicationStates.set(instanceId ?? DEFAULT_REPLICATION_INSTANCE, state);
|
|
199
|
+
}
|
|
200
|
+
export function deleteReplicationState(instanceId) {
|
|
201
|
+
replicationStates.delete(instanceId);
|
|
199
202
|
}
|
|
200
|
-
function nextLsn() {
|
|
203
|
+
function nextLsn(state) {
|
|
201
204
|
const floor = lsnFloorFromTime();
|
|
202
|
-
if (currentLsn < floor)
|
|
203
|
-
currentLsn = floor;
|
|
204
|
-
currentLsn += LSN_INCREMENT;
|
|
205
|
-
return currentLsn;
|
|
205
|
+
if (state.currentLsn < floor)
|
|
206
|
+
state.currentLsn = floor;
|
|
207
|
+
state.currentLsn += LSN_INCREMENT;
|
|
208
|
+
return state.currentLsn;
|
|
206
209
|
}
|
|
207
210
|
function lsnToString(lsn) {
|
|
208
211
|
const high = Number(lsn >> 32n);
|
|
@@ -356,17 +359,18 @@ function buildErrorResponse(message) {
|
|
|
356
359
|
* handle a replication query. returns response bytes or null if not handled.
|
|
357
360
|
* async because slot operations need to write to pglite.
|
|
358
361
|
*/
|
|
359
|
-
export async function handleReplicationQuery(query, db) {
|
|
362
|
+
export async function handleReplicationQuery(query, db, instanceId) {
|
|
363
|
+
const state = replicationState(instanceId);
|
|
360
364
|
const trimmed = query.trim().replace(/;$/, '').trim();
|
|
361
365
|
const upper = trimmed.toUpperCase();
|
|
362
366
|
if (upper === 'IDENTIFY_SYSTEM') {
|
|
363
|
-
const lsn = lsnToString(currentLsn);
|
|
367
|
+
const lsn = lsnToString(state.currentLsn);
|
|
364
368
|
return buildSimpleResponse(['systemid', 'timeline', 'xlogpos', 'dbname'], ['1234567890', '1', lsn, 'postgres']);
|
|
365
369
|
}
|
|
366
370
|
if (upper.startsWith('CREATE_REPLICATION_SLOT')) {
|
|
367
371
|
const match = trimmed.match(/CREATE_REPLICATION_SLOT\s+(?:"([^"]+)"|'([^']+)'|(\S+))/i);
|
|
368
372
|
const slotName = match?.[1] || match?.[2] || match?.[3] || 'zero_slot';
|
|
369
|
-
const lsn = lsnToString(nextLsn());
|
|
373
|
+
const lsn = lsnToString(nextLsn(state));
|
|
370
374
|
const snapshotName = `00000003-00000001-1`;
|
|
371
375
|
// set watermark to current DB state so replication only delivers changes
|
|
372
376
|
// that happen AFTER this point. this mirrors real postgres behavior where
|
|
@@ -375,8 +379,8 @@ export async function handleReplicationQuery(query, db) {
|
|
|
375
379
|
// on reconnect this is effectively a no-op since the watermark is already
|
|
376
380
|
// at or past the current DB state.
|
|
377
381
|
const currentWm = await getCurrentWatermark(db);
|
|
378
|
-
if (currentWm > lastStreamedWatermark) {
|
|
379
|
-
lastStreamedWatermark = currentWm;
|
|
382
|
+
if (currentWm > state.lastStreamedWatermark) {
|
|
383
|
+
state.lastStreamedWatermark = currentWm;
|
|
380
384
|
}
|
|
381
385
|
// a new slot means the consumer re-initializes from a fresh snapshot:
|
|
382
386
|
// rows up to the snapshot point are covered by the initial copy, and any
|
|
@@ -384,7 +388,7 @@ export async function handleReplicationQuery(query, db) {
|
|
|
384
388
|
await purgeConsumedChanges(db, currentWm);
|
|
385
389
|
await clearStreamedBatches(db);
|
|
386
390
|
// the log was just cut to the snapshot point — the backlog is not stuck.
|
|
387
|
-
markReplicationProgress();
|
|
391
|
+
markReplicationProgress(instanceId);
|
|
388
392
|
// persist slot so pg_replication_slots queries find it
|
|
389
393
|
await db.query(`INSERT INTO _orez._zero_replication_slots (slot_name, restart_lsn, confirmed_flush_lsn)
|
|
390
394
|
VALUES ($1, $2, $2)
|
|
@@ -425,9 +429,10 @@ export async function handleReplicationQuery(query, db) {
|
|
|
425
429
|
* start streaming replication changes to the client.
|
|
426
430
|
* this runs indefinitely until the connection is closed.
|
|
427
431
|
*/
|
|
428
|
-
export async function handleStartReplication(query, writer, db, mutex) {
|
|
432
|
+
export async function handleStartReplication(query, writer, db, mutex, instanceId) {
|
|
433
|
+
const state = replicationState(instanceId);
|
|
429
434
|
log.debug.repl('entering streaming mode');
|
|
430
|
-
lastStreamActivityAt = Date.now();
|
|
435
|
+
state.lastStreamActivityAt = Date.now();
|
|
431
436
|
// honor zero-cache's resume LSN. without this, after a page reload the
|
|
432
437
|
// in-memory currentLsn / lastStreamedWatermark are reset to defaults but
|
|
433
438
|
// changeLog persists with prior LSNs — re-streaming from BIGINT 0 makes
|
|
@@ -438,9 +443,9 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
438
443
|
// lastStreamedWatermark to the current sequence value we skip already-
|
|
439
444
|
// streamed _zero_changes rows.
|
|
440
445
|
const clientStartLsn = extractStartLsn(query);
|
|
441
|
-
if (clientStartLsn !== null && clientStartLsn > currentLsn) {
|
|
442
|
-
log.debug.repl(`advancing currentLsn ${lsnToString(currentLsn)} → ${lsnToString(clientStartLsn)} from client START_REPLICATION`);
|
|
443
|
-
currentLsn = clientStartLsn;
|
|
446
|
+
if (clientStartLsn !== null && clientStartLsn > state.currentLsn) {
|
|
447
|
+
log.debug.repl(`advancing currentLsn ${lsnToString(state.currentLsn)} → ${lsnToString(clientStartLsn)} from client START_REPLICATION`);
|
|
448
|
+
state.currentLsn = clientStartLsn;
|
|
444
449
|
}
|
|
445
450
|
// send CopyBothResponse to enter streaming mode
|
|
446
451
|
const copyBoth = new Uint8Array(1 + 4 + 1 + 2);
|
|
@@ -482,12 +487,12 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
482
487
|
// feed the bankruptcy monitor's confirm gauge, or a reconnect livelock
|
|
483
488
|
// (stream → no confirm → reconnect → repeat) looks healthy forever.
|
|
484
489
|
if (purged > 0)
|
|
485
|
-
markReplicationProgress();
|
|
490
|
+
markReplicationProgress(instanceId);
|
|
486
491
|
await clearStreamedBatches(db);
|
|
487
492
|
const resumeWm = await getStreamResumeWatermark(db);
|
|
488
|
-
if (resumeWm !== lastStreamedWatermark) {
|
|
489
|
-
log.debug.repl(`moving lastStreamedWatermark ${lastStreamedWatermark} → ${resumeWm} on reconnect`);
|
|
490
|
-
lastStreamedWatermark = resumeWm;
|
|
493
|
+
if (resumeWm !== state.lastStreamedWatermark) {
|
|
494
|
+
log.debug.repl(`moving lastStreamedWatermark ${state.lastStreamedWatermark} → ${resumeWm} on reconnect`);
|
|
495
|
+
state.lastStreamedWatermark = resumeWm;
|
|
491
496
|
}
|
|
492
497
|
}
|
|
493
498
|
catch (err) {
|
|
@@ -497,18 +502,20 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
497
502
|
mutex.release();
|
|
498
503
|
}
|
|
499
504
|
}
|
|
500
|
-
let lastWatermark = lastStreamedWatermark;
|
|
505
|
+
let lastWatermark = state.lastStreamedWatermark;
|
|
501
506
|
// use cached setup results on reconnect to avoid holding the mutex
|
|
502
507
|
// for seconds doing trigger installation + schema queries. zero-cache
|
|
503
508
|
// disconnects if its proxy queries are blocked too long by the mutex.
|
|
504
509
|
let tableKeyColumns;
|
|
505
510
|
let excludedColumns;
|
|
506
511
|
let columnTypeOids;
|
|
507
|
-
if (cachedTableKeyColumns &&
|
|
512
|
+
if (state.cachedTableKeyColumns &&
|
|
513
|
+
state.cachedExcludedColumns &&
|
|
514
|
+
state.cachedColumnTypeOids) {
|
|
508
515
|
log.debug.repl('reconnect: using cached setup (skipping mutex)');
|
|
509
|
-
tableKeyColumns = cachedTableKeyColumns;
|
|
510
|
-
excludedColumns = cachedExcludedColumns;
|
|
511
|
-
columnTypeOids = cachedColumnTypeOids;
|
|
516
|
+
tableKeyColumns = state.cachedTableKeyColumns;
|
|
517
|
+
excludedColumns = state.cachedExcludedColumns;
|
|
518
|
+
columnTypeOids = state.cachedColumnTypeOids;
|
|
512
519
|
}
|
|
513
520
|
else {
|
|
514
521
|
tableKeyColumns = new Map();
|
|
@@ -526,7 +533,7 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
526
533
|
// install change tracking triggers on shard schema tables (e.g. chat_0.clients)
|
|
527
534
|
await installTriggersOnShardTables(db);
|
|
528
535
|
// set up LISTEN + install notify triggers in one batch
|
|
529
|
-
const pubName =
|
|
536
|
+
const pubName = state.publications;
|
|
530
537
|
let tables;
|
|
531
538
|
if (pubName) {
|
|
532
539
|
const result = await db.query(`SELECT tablename FROM pg_publication_tables
|
|
@@ -647,9 +654,9 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
647
654
|
log.debug.proxy(`excluding unsupported columns: ${[...excludedColumns.entries()].map(([t, c]) => `${t}(${[...c].join(',')})`).join(', ')}`);
|
|
648
655
|
}
|
|
649
656
|
// cache for subsequent reconnects
|
|
650
|
-
cachedTableKeyColumns = tableKeyColumns;
|
|
651
|
-
cachedExcludedColumns = excludedColumns;
|
|
652
|
-
cachedColumnTypeOids = columnTypeOids;
|
|
657
|
+
state.cachedTableKeyColumns = tableKeyColumns;
|
|
658
|
+
state.cachedExcludedColumns = excludedColumns;
|
|
659
|
+
state.cachedColumnTypeOids = columnTypeOids;
|
|
653
660
|
}
|
|
654
661
|
finally {
|
|
655
662
|
mutex.release();
|
|
@@ -697,14 +704,14 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
697
704
|
});
|
|
698
705
|
};
|
|
699
706
|
const flushConfirmedBatches = async () => {
|
|
700
|
-
if (pendingConfirmLsn <= processedConfirmLsn)
|
|
707
|
+
if (state.pendingConfirmLsn <= state.processedConfirmLsn)
|
|
701
708
|
return;
|
|
702
709
|
await mutex.acquire();
|
|
703
|
-
const target = pendingConfirmLsn;
|
|
710
|
+
const target = state.pendingConfirmLsn;
|
|
704
711
|
try {
|
|
705
712
|
const purged = await confirmStreamedBatches(db, target);
|
|
706
|
-
processedConfirmLsn = target;
|
|
707
|
-
markReplicationProgress();
|
|
713
|
+
state.processedConfirmLsn = target;
|
|
714
|
+
markReplicationProgress(instanceId);
|
|
708
715
|
if (newestUnconfirmedLsn > 0n && target >= newestUnconfirmedLsn) {
|
|
709
716
|
oldestUnconfirmedAt = 0;
|
|
710
717
|
oldestUnconfirmedLsn = 0n;
|
|
@@ -741,8 +748,8 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
741
748
|
// discards the batch mappings and restarts the chew from zero — the
|
|
742
749
|
// reconnect livelock that burned $50-65/day of DO rows-written on the
|
|
743
750
|
// 2026-07 CF incident. reconnect is for a dead wire, not a slow consumer.
|
|
744
|
-
const sinceSignal = performance.now() - Math.max(oldestUnconfirmedAt, lastFeedbackAtPerf);
|
|
745
|
-
const timeoutMs = unconfirmedReconnectMs();
|
|
751
|
+
const sinceSignal = performance.now() - Math.max(oldestUnconfirmedAt, state.lastFeedbackAtPerf);
|
|
752
|
+
const timeoutMs = unconfirmedReconnectMs(state);
|
|
746
753
|
if (sinceSignal < timeoutMs)
|
|
747
754
|
return false;
|
|
748
755
|
log.repl(`unconfirmed streamed batch stale for ${sinceSignal.toFixed(0)}ms with no consumer feedback; closing replication stream for reconnect`);
|
|
@@ -751,8 +758,11 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
751
758
|
return true;
|
|
752
759
|
};
|
|
753
760
|
// register direct wakeup so the proxy can signal us immediately
|
|
754
|
-
|
|
755
|
-
globalThis.
|
|
761
|
+
state.replicationWakeup = wakeup;
|
|
762
|
+
// expose on globalThis so external code (e.g. pglite-pool) can signal
|
|
763
|
+
// without importing from this module (works across separate bundles)
|
|
764
|
+
if (instanceId === undefined)
|
|
765
|
+
globalThis.__orez_signal_replication = wakeup;
|
|
756
766
|
// also set up LISTEN as secondary signal
|
|
757
767
|
let unsubscribe = null;
|
|
758
768
|
try {
|
|
@@ -793,7 +803,7 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
793
803
|
// as its downstream commits, instead of at its own leisurely
|
|
794
804
|
// cadence — without this the first ack routinely lost the race
|
|
795
805
|
// against the stale-unconfirmed reconnect (2026-07 CF incident).
|
|
796
|
-
writer.write(encodeKeepalive(currentLsn, nowMicros(), newestUnconfirmedLsn > 0n));
|
|
806
|
+
writer.write(encodeKeepalive(state.currentLsn, nowMicros(), newestUnconfirmedLsn > 0n));
|
|
797
807
|
log.debug.repl(`idle keepalive (lastWatermark=${lastWatermark})`);
|
|
798
808
|
// re-scan for new shard schemas during idle
|
|
799
809
|
if (performance.now() - lastShardRescan > shardRescanIntervalMs) {
|
|
@@ -925,7 +935,7 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
925
935
|
log.debug.repl(`filter: ${preFilterCount} → ${changes.length} changes`);
|
|
926
936
|
if (changes.length === 0) {
|
|
927
937
|
lastWatermark = batchEnd;
|
|
928
|
-
lastStreamedWatermark = batchEnd;
|
|
938
|
+
state.lastStreamedWatermark = batchEnd;
|
|
929
939
|
// all changes were filtered out (e.g. shard internal tables).
|
|
930
940
|
// they never reach the consumer by design, so no confirmation
|
|
931
941
|
// will ever cover them — purge directly.
|
|
@@ -942,14 +952,14 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
942
952
|
queryPending = true;
|
|
943
953
|
continue;
|
|
944
954
|
}
|
|
945
|
-
await ensureMetadataForChangedTables(db, mutex, changes, tableKeyColumns, excludedColumns, columnTypeOids);
|
|
955
|
+
await ensureMetadataForChangedTables(db, mutex, changes, tableKeyColumns, excludedColumns, columnTypeOids, state);
|
|
946
956
|
// allocate the batch's commit LSN and durably record the
|
|
947
957
|
// lsn → batch-end mapping BEFORE the rows hit the wire: if the
|
|
948
958
|
// consumer commits the batch and this process dies before
|
|
949
959
|
// persisting the mapping, the reconnect reconciliation would
|
|
950
960
|
// re-stream an already-committed transaction (duplicate apply).
|
|
951
|
-
const batchLsn = nextLsn();
|
|
952
|
-
const batchEndLsn = nextLsn();
|
|
961
|
+
const batchLsn = nextLsn(state);
|
|
962
|
+
const batchEndLsn = nextLsn(state);
|
|
953
963
|
await mutex.acquire();
|
|
954
964
|
try {
|
|
955
965
|
await recordStreamedBatch(db, batchLsn, batchEnd);
|
|
@@ -958,11 +968,11 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
958
968
|
mutex.release();
|
|
959
969
|
}
|
|
960
970
|
log.debug.repl(`streaming ${changes.length} changes to writer`);
|
|
961
|
-
await streamChanges(changes, writer, sentRelations, txCounter++, batchLsn, batchEndLsn, tableKeyColumns, excludedColumns, columnTypeOids);
|
|
971
|
+
await streamChanges(changes, writer, sentRelations, txCounter++, batchLsn, batchEndLsn, tableKeyColumns, excludedColumns, columnTypeOids, state);
|
|
962
972
|
markBatchAwaitingConfirmation(batchLsn);
|
|
963
|
-
lastStreamActivityAt = Date.now();
|
|
973
|
+
state.lastStreamActivityAt = Date.now();
|
|
964
974
|
lastWatermark = batchEnd;
|
|
965
|
-
lastStreamedWatermark = batchEnd;
|
|
975
|
+
state.lastStreamedWatermark = batchEnd;
|
|
966
976
|
log.debug.repl(`streamed ok, watermark=${batchEnd}`);
|
|
967
977
|
hasStreamedOnce = true;
|
|
968
978
|
// got changes - continue immediately to check for more
|
|
@@ -972,7 +982,7 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
972
982
|
// no changes: send keepalive (reply requested while batches await
|
|
973
983
|
// confirmation — see the idle-timeout keepalive above)
|
|
974
984
|
const ts = nowMicros();
|
|
975
|
-
writer.write(encodeKeepalive(currentLsn, ts, newestUnconfirmedLsn > 0n));
|
|
985
|
+
writer.write(encodeKeepalive(state.currentLsn, ts, newestUnconfirmedLsn > 0n));
|
|
976
986
|
log.debug.repl(`idle (lastWatermark=${lastWatermark})`);
|
|
977
987
|
// next iteration will wait for signal at the top
|
|
978
988
|
}
|
|
@@ -996,8 +1006,8 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
996
1006
|
}
|
|
997
1007
|
finally {
|
|
998
1008
|
// only clear if still pointing to our wakeup (a new handler may have replaced it)
|
|
999
|
-
if (
|
|
1000
|
-
|
|
1009
|
+
if (state.replicationWakeup === wakeup) {
|
|
1010
|
+
state.replicationWakeup = null;
|
|
1001
1011
|
}
|
|
1002
1012
|
if (unsubscribe) {
|
|
1003
1013
|
await unsubscribe().catch(() => { });
|
|
@@ -1005,7 +1015,7 @@ export async function handleStartReplication(query, writer, db, mutex) {
|
|
|
1005
1015
|
}
|
|
1006
1016
|
log.repl('poll loop exited');
|
|
1007
1017
|
}
|
|
1008
|
-
async function ensureMetadataForChangedTables(db, mutex, changes, tableKeyColumns, excludedColumns, columnTypeOids) {
|
|
1018
|
+
async function ensureMetadataForChangedTables(db, mutex, changes, tableKeyColumns, excludedColumns, columnTypeOids, state) {
|
|
1009
1019
|
const missing = new Map();
|
|
1010
1020
|
for (const change of changes) {
|
|
1011
1021
|
if (tableKeyColumns.has(change.table_name) ||
|
|
@@ -1074,18 +1084,16 @@ async function ensureMetadataForChangedTables(db, mutex, changes, tableKeyColumn
|
|
|
1074
1084
|
}
|
|
1075
1085
|
log.debug.repl(`refreshed metadata for ${missing.size} late table(s): ${[...missing.keys()].join(',')}`);
|
|
1076
1086
|
for (const key of missing.keys())
|
|
1077
|
-
cachedColumns.delete(key);
|
|
1087
|
+
state.cachedColumns.delete(key);
|
|
1078
1088
|
}
|
|
1079
1089
|
finally {
|
|
1080
1090
|
mutex.release();
|
|
1081
1091
|
}
|
|
1082
1092
|
}
|
|
1083
|
-
// cache column info per table to avoid per-change allocation
|
|
1084
|
-
const cachedColumns = new Map();
|
|
1085
1093
|
async function streamChanges(changes, writer, sentRelations, txId,
|
|
1086
1094
|
// the batch's commit LSN and commit-end LSN are allocated by the caller so
|
|
1087
1095
|
// the lsn → batch-end mapping can be durably recorded before streaming.
|
|
1088
|
-
lsn, endLsn, tableKeyColumns, excludedColumns, columnTypeOids) {
|
|
1096
|
+
lsn, endLsn, tableKeyColumns, excludedColumns, columnTypeOids, state) {
|
|
1089
1097
|
const ts = nowMicros();
|
|
1090
1098
|
// collect all encoded messages into a list, then batch-write
|
|
1091
1099
|
// to minimize syscalls (each writer.write → socket.write is a syscall)
|
|
@@ -1120,7 +1128,7 @@ lsn, endLsn, tableKeyColumns, excludedColumns, columnTypeOids) {
|
|
|
1120
1128
|
if (!row)
|
|
1121
1129
|
continue;
|
|
1122
1130
|
// use cached columns or build and cache them
|
|
1123
|
-
let columns = cachedColumns.get(qualifiedKey);
|
|
1131
|
+
let columns = state.cachedColumns.get(qualifiedKey);
|
|
1124
1132
|
if (!columns) {
|
|
1125
1133
|
const keySet = tableKeyColumns.get(qualifiedKey);
|
|
1126
1134
|
const typeOids = columnTypeOids.get(qualifiedKey);
|
|
@@ -1129,7 +1137,7 @@ lsn, endLsn, tableKeyColumns, excludedColumns, columnTypeOids) {
|
|
|
1129
1137
|
typeOid: typeOids?.get(col.name) ?? col.typeOid,
|
|
1130
1138
|
isKey: keySet?.has(col.name) ?? false,
|
|
1131
1139
|
}));
|
|
1132
|
-
cachedColumns.set(qualifiedKey, columns);
|
|
1140
|
+
state.cachedColumns.set(qualifiedKey, columns);
|
|
1133
1141
|
}
|
|
1134
1142
|
// send RELATION if not yet sent
|
|
1135
1143
|
if (!sentRelations.has(qualifiedKey)) {
|
|
@@ -1174,9 +1182,32 @@ lsn, endLsn, tableKeyColumns, excludedColumns, columnTypeOids) {
|
|
|
1174
1182
|
}
|
|
1175
1183
|
// hook for arch instrumentation (soot-arch sq-write events)
|
|
1176
1184
|
const hook = globalThis.__orez_on_repl_commit;
|
|
1185
|
+
const tables = new Set(changes.map((change) => change.table_name));
|
|
1186
|
+
try {
|
|
1187
|
+
state.log?.({
|
|
1188
|
+
changes: changes.length,
|
|
1189
|
+
component: 'replication',
|
|
1190
|
+
event: 'commit',
|
|
1191
|
+
instanceId: state.instanceId,
|
|
1192
|
+
tables: [...tables],
|
|
1193
|
+
txId,
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
catch {
|
|
1197
|
+
// diagnostics cannot break replication
|
|
1198
|
+
}
|
|
1177
1199
|
if (hook) {
|
|
1178
|
-
|
|
1179
|
-
|
|
1200
|
+
try {
|
|
1201
|
+
hook({
|
|
1202
|
+
changes: changes.length,
|
|
1203
|
+
instanceId: state.instanceId,
|
|
1204
|
+
tables: [...tables],
|
|
1205
|
+
txId,
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
catch {
|
|
1209
|
+
// instrumentation cannot break replication
|
|
1210
|
+
}
|
|
1180
1211
|
}
|
|
1181
1212
|
}
|
|
1182
1213
|
function normalizeShardClientsRow(row) {
|