@rocicorp/zero 1.11.0-canary.21 → 1.11.0-canary.23
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/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
- package/out/zero-cache/src/config/normalize.js +1 -2
- package/out/zero-cache/src/config/normalize.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +0 -22
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +38 -42
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +90 -73
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js +1 -1
- package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/init-cleanup.d.ts +24 -0
- package/out/zero-cache/src/services/change-source/common/init-cleanup.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-source/common/init-cleanup.js +44 -0
- package/out/zero-cache/src/services/change-source/common/init-cleanup.js.map +1 -0
- package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts +49 -1
- package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.d.ts +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +7 -3
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts +10 -5
- package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js +65 -21
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +2 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js +3 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts +31 -0
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +100 -2
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.d.ts +6 -0
- package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js +8 -0
- package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js +3 -16
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.d.ts +17 -0
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.js +44 -1
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +7 -0
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +27 -8
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts +57 -0
- package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.js +103 -23
- package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.d.ts +86 -1
- package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js +272 -77
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts +7 -5
- package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/upstream-acker.js +9 -5
- package/out/zero-cache/src/services/change-streamer/upstream-acker.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts +3 -11
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +29 -23
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/litestream/config-v5.yml +0 -7
- package/out/zero-cache/src/services/profz.d.ts.map +1 -1
- package/out/zero-cache/src/services/profz.js +19 -1
- package/out/zero-cache/src/services/profz.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replication-status.js +1 -1
- package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/advancement-metrics.d.ts +26 -0
- package/out/zero-cache/src/services/view-syncer/advancement-metrics.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/advancement-metrics.js +26 -0
- package/out/zero-cache/src/services/view-syncer/advancement-metrics.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +10 -2
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +102 -122
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +8 -9
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +15 -3
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +50 -28
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +2 -1
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-react/src/use-query.js +12 -3
- package/out/zero-react/src/use-query.js.map +1 -1
- package/out/zql/src/ivm/array-view.d.ts.map +1 -1
- package/out/zql/src/ivm/array-view.js +21 -12
- package/out/zql/src/ivm/array-view.js.map +1 -1
- package/out/zqlite/src/db.d.ts.map +1 -1
- package/out/zqlite/src/db.js +34 -6
- package/out/zqlite/src/db.js.map +1 -1
- package/package.json +1 -1
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.d.ts +0 -34
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.d.ts.map +0 -1
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.js +0 -184
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.js.map +0 -1
|
@@ -2,6 +2,7 @@ import { assert, unreachable } from "../../../../shared/src/asserts.js";
|
|
|
2
2
|
import { promiseVoid } from "../../../../shared/src/resolved-promises.js";
|
|
3
3
|
import { AbortError } from "../../../../shared/src/abort-error.js";
|
|
4
4
|
import { parse, valita_exports } from "../../../../shared/src/valita.js";
|
|
5
|
+
import { must } from "../../../../shared/src/must.js";
|
|
5
6
|
import { BigIntJSON } from "../../../../shared/src/bigint-json.js";
|
|
6
7
|
import { cdcSchema } from "../../types/shards.js";
|
|
7
8
|
import { isDataChange, isSchemaChange } from "../change-source/protocol/current/data.js";
|
|
@@ -13,7 +14,7 @@ import "../../types/pg.js";
|
|
|
13
14
|
import { READONLY, READ_COMMITTED } from "../../db/mode-enum.js";
|
|
14
15
|
import { runTx } from "../../db/run-transaction.js";
|
|
15
16
|
import { Queue } from "../../../../shared/src/queue.js";
|
|
16
|
-
import { AutoResetSignal, markResetRequired } from "./schema/tables.js";
|
|
17
|
+
import { AutoResetSignal, markResetRequired, reinitializeChangeLog } from "./schema/tables.js";
|
|
17
18
|
import { TransactionPool, sharedSnapshot } from "../../db/transaction-pool.js";
|
|
18
19
|
import { orTimeout } from "../../types/timeout.js";
|
|
19
20
|
import { reconstructWatermarkedChange, serializeChangeStreamDataWithChange } from "./change-log-codec.js";
|
|
@@ -66,6 +67,11 @@ var Storer = class {
|
|
|
66
67
|
#replicaVersion;
|
|
67
68
|
#onCommitted;
|
|
68
69
|
#onFatal;
|
|
70
|
+
/**
|
|
71
|
+
* Set while waiting to assume ownership of the change log at its head
|
|
72
|
+
* (see {@link assumeOwnershipAtHead}).
|
|
73
|
+
*/
|
|
74
|
+
#pendingOwnership = null;
|
|
69
75
|
#queue = new Queue();
|
|
70
76
|
#backPressureThresholdBytes;
|
|
71
77
|
#statementTimeoutMs;
|
|
@@ -120,12 +126,128 @@ var Storer = class {
|
|
|
120
126
|
}
|
|
121
127
|
return result;
|
|
122
128
|
}
|
|
123
|
-
|
|
124
|
-
const db = this.#db;
|
|
125
|
-
const owner = this.#taskID;
|
|
129
|
+
#ownerAddress() {
|
|
126
130
|
const ownerAddress = this.#discoveryAddress;
|
|
127
131
|
const ownerProtocol = this.#discoveryProtocol;
|
|
128
|
-
|
|
132
|
+
return ownerProtocol === "ws" ? ownerAddress : `${ownerProtocol}://${ownerAddress}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Re-initializes a change log that is no longer maintained from the
|
|
136
|
+
* replica's state, taking ownership of it in the process (which is then
|
|
137
|
+
* assumed again at its new head, see {@link assumeOwnershipAtHead}).
|
|
138
|
+
* See {@link reinitializeChangeLog}.
|
|
139
|
+
*/
|
|
140
|
+
reinitializeFromReplica(params) {
|
|
141
|
+
return reinitializeChangeLog(this.#lc, this.#db, this.#shard, {
|
|
142
|
+
...params,
|
|
143
|
+
owner: this.#taskID,
|
|
144
|
+
ownerAddress: this.#ownerAddress()
|
|
145
|
+
}, this.#pendingOwnership?.purgeLock ?? void 0);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Prepares to assume ownership of the change log at its head, while it may
|
|
149
|
+
* still be owned and maintained by another change-streamer. This allows the
|
|
150
|
+
* previous owner to continue to serve subscribers while this change-source's
|
|
151
|
+
* upstream decodes the WAL to catch up to the head of the change log, which
|
|
152
|
+
* may be many minutes worth of transactions for large DBs.
|
|
153
|
+
*
|
|
154
|
+
* Once the change stream is started from the head of the change log:
|
|
155
|
+
*
|
|
156
|
+
* * Transactions that the change log already contains (i.e. stored by the
|
|
157
|
+
* previous owner) are not stored again, but are otherwise processed
|
|
158
|
+
* normally (e.g. for upstream acks, since they have been stored).
|
|
159
|
+
* * Ownership is assumed in the transaction that stores the first
|
|
160
|
+
* transaction past the head of the change log. The previous owner's
|
|
161
|
+
* (subsequent) transactions fail its ownership check, which makes this
|
|
162
|
+
* transaction the one immediately after the previous owner's last one.
|
|
163
|
+
* * Ownership is also assumed upon a {@link status} at or past the head of
|
|
164
|
+
* the change log (outside of a transaction), which covers an idle
|
|
165
|
+
* upstream: all transactions up to the status position have been
|
|
166
|
+
* received, and any past the head would have been stored.
|
|
167
|
+
*
|
|
168
|
+
* Subscribers are caught up from the change log as usual, as the stream
|
|
169
|
+
* (which is forwarded to them) starts from its head.
|
|
170
|
+
*
|
|
171
|
+
* Backfill transactions are synthesized by each change-streamer's change
|
|
172
|
+
* source, and thus never correspond to those stored by the previous owner.
|
|
173
|
+
* Until ownership is assumed, they are not stored, and no backfills are
|
|
174
|
+
* requested from the change log (see
|
|
175
|
+
* {@link getStartStreamInitializationParameters}). Once ownership is
|
|
176
|
+
* assumed, a restart of the change stream is requested if the change log has
|
|
177
|
+
* pending backfills or if a backfill transaction was skipped.
|
|
178
|
+
*/
|
|
179
|
+
assumeOwnershipAtHead(takeover) {
|
|
180
|
+
this.#pendingOwnership = {
|
|
181
|
+
...takeover,
|
|
182
|
+
head: "",
|
|
183
|
+
skippedBackfill: false,
|
|
184
|
+
start: performance.now()
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Whether ownership of the change log is pending, i.e. after
|
|
189
|
+
* {@link assumeOwnershipAtHead} and before ownership is assumed.
|
|
190
|
+
*/
|
|
191
|
+
get ownershipPending() {
|
|
192
|
+
return this.#pendingOwnership !== null;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Relays a status message received outside of a transaction, which is
|
|
196
|
+
* used to assume ownership of the change log on an idle upstream. A no-op
|
|
197
|
+
* unless ownership is pending.
|
|
198
|
+
*/
|
|
199
|
+
status(watermark) {
|
|
200
|
+
if (this.#pendingOwnership) this.#queue.enqueue(["status", watermark]);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Assumes ownership if the change log contains nothing past `watermark`.
|
|
204
|
+
* Otherwise, updates the known head of the change log.
|
|
205
|
+
*
|
|
206
|
+
* @param ifBefore When set, ownership is only assumed if the head of the
|
|
207
|
+
* change log is before (rather than at or before) the `watermark`,
|
|
208
|
+
* i.e. when the watermark is that of a transaction to be stored.
|
|
209
|
+
* @returns The state of the change log at which ownership is assumed (in
|
|
210
|
+
* `tx`), or `null` if ownership was not assumed.
|
|
211
|
+
*/
|
|
212
|
+
async #tryAssumeOwnership(tx, watermark, ifBefore) {
|
|
213
|
+
const pending = must(this.#pendingOwnership);
|
|
214
|
+
const [state] = await tx.processReadTask((sql) => sql`
|
|
215
|
+
SELECT "lastWatermark" FROM ${this.#cdc("replicationState")} FOR UPDATE`);
|
|
216
|
+
const head = state.lastWatermark;
|
|
217
|
+
pending.head = head > pending.head ? head : pending.head;
|
|
218
|
+
if (ifBefore ? head >= watermark : head > watermark) return null;
|
|
219
|
+
const owner = this.#taskID;
|
|
220
|
+
const ownerAddress = this.#ownerAddress();
|
|
221
|
+
tx.process((sql) => [sql`UPDATE ${this.#cdc("replicationState")} SET ${sql({
|
|
222
|
+
owner,
|
|
223
|
+
ownerAddress
|
|
224
|
+
})}`]);
|
|
225
|
+
const [{ backfilling }] = await tx.processReadTask((sql) => sql`
|
|
226
|
+
SELECT EXISTS (SELECT 1 FROM ${this.#cdc("backfilling")}) AS "backfilling"`);
|
|
227
|
+
this.#lc.info?.(`assuming ownership at ${ownerAddress} after change log head ${head}`);
|
|
228
|
+
return {
|
|
229
|
+
head,
|
|
230
|
+
backfilling
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
#ownershipAssumed({ head, backfilling }) {
|
|
234
|
+
const { purgeLock, requestRestart, skippedBackfill, start } = must(this.#pendingOwnership);
|
|
235
|
+
this.#pendingOwnership = null;
|
|
236
|
+
this.#lc.info?.(`assumed ownership of the change log at ${head} after ${(performance.now() - start).toFixed(2)} ms`);
|
|
237
|
+
purgeLock?.release();
|
|
238
|
+
if (backfilling || skippedBackfill) requestRestart(`resuming backfills after assuming ownership`);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Assumes ownership of the change log immediately, regardless of its head.
|
|
242
|
+
* This is used for in tests.
|
|
243
|
+
*
|
|
244
|
+
* A production change-streamer instead takes over the change log at its head
|
|
245
|
+
* (see {@link assumeOwnershipAtHead}).
|
|
246
|
+
*/
|
|
247
|
+
async assumeOwnership() {
|
|
248
|
+
const db = this.#db;
|
|
249
|
+
const owner = this.#taskID;
|
|
250
|
+
const addressWithProtocol = this.#ownerAddress();
|
|
129
251
|
this.#lc.info?.(`assuming ownership at ${addressWithProtocol}`);
|
|
130
252
|
const start = performance.now();
|
|
131
253
|
await this.#withTimeout("assume-ownership", db`UPDATE ${this.#cdc("replicationState")} SET ${db({
|
|
@@ -134,7 +256,6 @@ var Storer = class {
|
|
|
134
256
|
})}`);
|
|
135
257
|
const elapsed = (performance.now() - start).toFixed(2);
|
|
136
258
|
this.#lc.info?.(`assumed ownership at ${addressWithProtocol} (${elapsed} ms)`);
|
|
137
|
-
if (purgeLock) purgeLock.release();
|
|
138
259
|
}
|
|
139
260
|
/**
|
|
140
261
|
* The whole of what a stream connection starts from: where to resume, what to
|
|
@@ -180,7 +301,7 @@ var Storer = class {
|
|
|
180
301
|
], { mode: READONLY }));
|
|
181
302
|
return {
|
|
182
303
|
lastWatermark,
|
|
183
|
-
backfillRequests: parse(result, backfillRequestsSchema),
|
|
304
|
+
backfillRequests: this.#pendingOwnership ? [] : parse(result, backfillRequestsSchema),
|
|
184
305
|
cookies: {
|
|
185
306
|
tableMetadata: [...tableMetadata],
|
|
186
307
|
backfilling: [...backfilling]
|
|
@@ -404,88 +525,152 @@ var Storer = class {
|
|
|
404
525
|
else await this.#startCatchup([subscriber]);
|
|
405
526
|
continue;
|
|
406
527
|
}
|
|
528
|
+
case "status": {
|
|
529
|
+
const pending = this.#pendingOwnership;
|
|
530
|
+
if (pending && !tx && msg[1] >= pending.head) {
|
|
531
|
+
const pool = new TransactionPool(this.#lc, { mode: READ_COMMITTED });
|
|
532
|
+
pool.run(this.#inserter);
|
|
533
|
+
const assumed = await this.#tryAssumeOwnership(pool, msg[1], false);
|
|
534
|
+
if (assumed === null) {
|
|
535
|
+
pool.abort();
|
|
536
|
+
await pool.done();
|
|
537
|
+
} else {
|
|
538
|
+
pool.setDone();
|
|
539
|
+
await pool.done();
|
|
540
|
+
this.#ownershipAssumed(assumed);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
407
545
|
case "abort":
|
|
408
|
-
if (tx) {
|
|
546
|
+
if (tx && !tx.skipped) {
|
|
409
547
|
tx.pool.abort();
|
|
410
548
|
await tx.pool.done();
|
|
411
|
-
tx = null;
|
|
412
549
|
}
|
|
550
|
+
tx = null;
|
|
413
551
|
continue;
|
|
414
552
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
this.#approximateQueuedBytes -= json.length;
|
|
418
|
-
if (tag === "begin") {
|
|
419
|
-
assert(!tx, "received BEGIN in the middle of a transaction");
|
|
420
|
-
const { promise, resolve, reject } = resolver();
|
|
421
|
-
promise.catch(() => {});
|
|
422
|
-
tx = {
|
|
423
|
-
pool: new TransactionPool(this.#lc.withContext("watermark", watermark), { mode: READ_COMMITTED }),
|
|
424
|
-
preCommitWatermark: watermark,
|
|
425
|
-
pos: 0,
|
|
426
|
-
startingReplicationState: promise,
|
|
427
|
-
ack: !change.skipAck,
|
|
428
|
-
batch: [],
|
|
429
|
-
lastFlush: void 0
|
|
430
|
-
};
|
|
431
|
-
tx.pool.run(this.#inserter);
|
|
432
|
-
tx.pool.process((tx) => {
|
|
433
|
-
tx`
|
|
434
|
-
SELECT "owner" FROM ${this.#cdc("replicationState")} FOR UPDATE`.then(([result]) => resolve(result), reject);
|
|
435
|
-
return [];
|
|
436
|
-
});
|
|
437
|
-
} else {
|
|
438
|
-
assert(tx, () => `received change outside of transaction: ${json}`);
|
|
439
|
-
tx.pos++;
|
|
440
|
-
}
|
|
441
|
-
const entry = {
|
|
442
|
-
watermark: tag === "commit" ? watermark : tx.preCommitWatermark,
|
|
443
|
-
precommit: tag === "commit" ? tx.preCommitWatermark : null,
|
|
444
|
-
pos: tx.pos,
|
|
445
|
-
change: storedChange
|
|
446
|
-
};
|
|
447
|
-
if (change !== null && isSchemaChange(change)) {
|
|
448
|
-
await this.#flushChangeLog(tx);
|
|
449
|
-
tx.lastFlush = tx.pool.process((sql) => [sql`INSERT INTO ${this.#cdc("changeLog")} ${sql(entry)}`, ...this.#trackBackfillMetadata(sql, change)]);
|
|
450
|
-
} else {
|
|
451
|
-
tx.batch.push(entry);
|
|
452
|
-
if (tx.batch.length >= this.#changeLogBatchSize) {
|
|
453
|
-
const prevFlush = tx.lastFlush;
|
|
454
|
-
this.#flushChangeLog(tx);
|
|
455
|
-
await prevFlush;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
553
|
+
this.#approximateQueuedBytes -= msg[2].length;
|
|
554
|
+
tx = await this.#processChange(tx, msg);
|
|
458
555
|
this.#maybeReleaseBackPressure();
|
|
459
|
-
if (
|
|
460
|
-
this.#flushChangeLog(tx);
|
|
461
|
-
const { owner } = await tx.startingReplicationState;
|
|
462
|
-
if (owner !== this.#taskID) tx.pool.fail(new AbortError(`changeLog ownership has been assumed by ${owner}`));
|
|
463
|
-
else {
|
|
464
|
-
const lastWatermark = watermark;
|
|
465
|
-
tx.pool.process((tx) => [tx`
|
|
466
|
-
UPDATE ${this.#cdc("replicationState")} SET ${tx({ lastWatermark })}`]);
|
|
467
|
-
tx.pool.setDone();
|
|
468
|
-
}
|
|
469
|
-
await tx.pool.done();
|
|
470
|
-
if (tx.ack) this.#onCommitted([
|
|
471
|
-
"commit",
|
|
472
|
-
change,
|
|
473
|
-
{ watermark }
|
|
474
|
-
]);
|
|
475
|
-
tx = null;
|
|
476
|
-
await this.#startCatchup(catchupQueue.splice(0));
|
|
477
|
-
} else if (tag === "rollback") {
|
|
478
|
-
tx.pool.abort();
|
|
479
|
-
await tx.pool.done();
|
|
480
|
-
tx = null;
|
|
481
|
-
await this.#startCatchup(catchupQueue.splice(0));
|
|
482
|
-
}
|
|
556
|
+
if (tx === null) await this.#startCatchup(catchupQueue.splice(0));
|
|
483
557
|
}
|
|
558
|
+
this.#cancelQueueEntries(catchupQueue.splice(0).map((sub) => ["subscriber", sub]), void 0);
|
|
484
559
|
} catch (e) {
|
|
485
560
|
catchupQueue.forEach(({ subscriber }) => subscriber.fail(e));
|
|
486
561
|
throw e;
|
|
487
562
|
}
|
|
488
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* Processes a change from the queue, i.e. stores it in the change log as
|
|
566
|
+
* part of the current transaction (or skips it, see
|
|
567
|
+
* {@link assumeOwnershipAtHead}).
|
|
568
|
+
*
|
|
569
|
+
* @returns The transaction that is open after the change, or `null` if the
|
|
570
|
+
* change ended (or was outside of) a transaction.
|
|
571
|
+
*/
|
|
572
|
+
async #processChange(tx, msg) {
|
|
573
|
+
const [_, watermark, json, storedChange, change] = msg;
|
|
574
|
+
const tag = change?.tag;
|
|
575
|
+
if (tag === "begin") {
|
|
576
|
+
assert(!tx, "received BEGIN in the middle of a transaction");
|
|
577
|
+
const pending = this.#pendingOwnership;
|
|
578
|
+
if (pending) {
|
|
579
|
+
if (change.skipAck) {
|
|
580
|
+
pending.skippedBackfill = true;
|
|
581
|
+
return {
|
|
582
|
+
skipped: true,
|
|
583
|
+
ack: false
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
if (watermark <= pending.head) return {
|
|
587
|
+
skipped: true,
|
|
588
|
+
ack: true
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
const pool = new TransactionPool(this.#lc.withContext("watermark", watermark), { mode: READ_COMMITTED });
|
|
592
|
+
pool.run(this.#inserter);
|
|
593
|
+
const { promise, resolve, reject } = resolver();
|
|
594
|
+
promise.catch(() => {});
|
|
595
|
+
const assumed = pending ? await this.#tryAssumeOwnership(pool, watermark, true) : void 0;
|
|
596
|
+
if (assumed === null) {
|
|
597
|
+
pool.abort();
|
|
598
|
+
await pool.done();
|
|
599
|
+
return {
|
|
600
|
+
skipped: true,
|
|
601
|
+
ack: true
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
if (pending) resolve({ owner: this.#taskID });
|
|
605
|
+
else pool.process((tx) => {
|
|
606
|
+
tx`
|
|
607
|
+
SELECT "owner" FROM ${this.#cdc("replicationState")} FOR UPDATE`.then(([result]) => resolve(result), reject);
|
|
608
|
+
return [];
|
|
609
|
+
});
|
|
610
|
+
tx = {
|
|
611
|
+
pool,
|
|
612
|
+
preCommitWatermark: watermark,
|
|
613
|
+
pos: 0,
|
|
614
|
+
startingReplicationState: promise,
|
|
615
|
+
ack: !change.skipAck,
|
|
616
|
+
batch: [],
|
|
617
|
+
lastFlush: void 0,
|
|
618
|
+
ownershipAssumed: assumed
|
|
619
|
+
};
|
|
620
|
+
} else if (tx?.skipped) {
|
|
621
|
+
if (tag === "commit" && tx.ack) this.#onCommitted([
|
|
622
|
+
"commit",
|
|
623
|
+
change,
|
|
624
|
+
{ watermark }
|
|
625
|
+
]);
|
|
626
|
+
return tag === "commit" || tag === "rollback" ? null : tx;
|
|
627
|
+
} else {
|
|
628
|
+
assert(tx, () => `received change outside of transaction: ${json}`);
|
|
629
|
+
tx.pos++;
|
|
630
|
+
}
|
|
631
|
+
const entry = {
|
|
632
|
+
watermark: tag === "commit" ? watermark : tx.preCommitWatermark,
|
|
633
|
+
precommit: tag === "commit" ? tx.preCommitWatermark : null,
|
|
634
|
+
pos: tx.pos,
|
|
635
|
+
change: storedChange
|
|
636
|
+
};
|
|
637
|
+
if (change !== null && isSchemaChange(change)) {
|
|
638
|
+
await this.#flushChangeLog(tx);
|
|
639
|
+
tx.lastFlush = tx.pool.process((sql) => [sql`INSERT INTO ${this.#cdc("changeLog")} ${sql(entry)}`, ...this.#trackBackfillMetadata(sql, change)]);
|
|
640
|
+
} else {
|
|
641
|
+
tx.batch.push(entry);
|
|
642
|
+
if (tx.batch.length >= this.#changeLogBatchSize) {
|
|
643
|
+
const prevFlush = tx.lastFlush;
|
|
644
|
+
this.#flushChangeLog(tx);
|
|
645
|
+
await prevFlush;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (tag === "commit") {
|
|
649
|
+
this.#flushChangeLog(tx);
|
|
650
|
+
const { owner } = await tx.startingReplicationState;
|
|
651
|
+
if (owner !== this.#taskID) tx.pool.fail(new AbortError(`changeLog ownership has been assumed by ${owner}`));
|
|
652
|
+
else {
|
|
653
|
+
const lastWatermark = watermark;
|
|
654
|
+
tx.pool.process((tx) => [tx`
|
|
655
|
+
UPDATE ${this.#cdc("replicationState")} SET ${tx({ lastWatermark })}`]);
|
|
656
|
+
tx.pool.setDone();
|
|
657
|
+
}
|
|
658
|
+
await tx.pool.done();
|
|
659
|
+
const { ownershipAssumed, ack } = tx;
|
|
660
|
+
tx = null;
|
|
661
|
+
if (ownershipAssumed) this.#ownershipAssumed(ownershipAssumed);
|
|
662
|
+
if (ack) this.#onCommitted([
|
|
663
|
+
"commit",
|
|
664
|
+
change,
|
|
665
|
+
{ watermark }
|
|
666
|
+
]);
|
|
667
|
+
} else if (tag === "rollback") {
|
|
668
|
+
tx.pool.abort();
|
|
669
|
+
await tx.pool.done();
|
|
670
|
+
tx = null;
|
|
671
|
+
}
|
|
672
|
+
return tx;
|
|
673
|
+
}
|
|
489
674
|
async #startCatchup(subs) {
|
|
490
675
|
const numCatchups = subs.length;
|
|
491
676
|
if (numCatchups === 0) return;
|
|
@@ -720,8 +905,18 @@ var PurgeLocker = class {
|
|
|
720
905
|
#cdc(table) {
|
|
721
906
|
return this.#db(`${cdcSchema(this.#shard)}.${table}`);
|
|
722
907
|
}
|
|
723
|
-
async acquire() {
|
|
908
|
+
async acquire(slotWatermark) {
|
|
724
909
|
const tx = new TransactionPool(this.#lc, { mode: READ_COMMITTED }).run(this.#db);
|
|
910
|
+
if (slotWatermark !== void 0) {
|
|
911
|
+
const [{ lastWatermark }] = await tx.processReadTask((sql) => sql`
|
|
912
|
+
SELECT "lastWatermark" FROM ${this.#cdc("replicationState")}`);
|
|
913
|
+
if (lastWatermark < slotWatermark) {
|
|
914
|
+
this.#lc.info?.(`changeLog@${lastWatermark} is behind the replication slot@${slotWatermark}`);
|
|
915
|
+
tx.setDone();
|
|
916
|
+
await tx.done();
|
|
917
|
+
return "behind-slot";
|
|
918
|
+
}
|
|
919
|
+
}
|
|
725
920
|
const row = await tx.processReadTask((sql) => sql`
|
|
726
921
|
SELECT watermark FROM ${this.#cdc("changeLog")}
|
|
727
922
|
ORDER BY watermark, pos LIMIT 1
|