@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
|
@@ -292,15 +292,18 @@ var ChangeStreamerImpl = class {
|
|
|
292
292
|
trackBackup: backupConfig?.litestreamVersion === "v5"
|
|
293
293
|
});
|
|
294
294
|
const replicaFileForInitialization = opts.sqliteChangeLogCompare?.replicaFile ?? (!this.#pgChangeLogEnabled ? opts.sqliteChangeLogWriter?.replicaFile : void 0);
|
|
295
|
-
const
|
|
295
|
+
const replicaFileForReinitialization = this.#pgChangeLogEnabled && opts.pgChangeLogBehindSlot ? backupConfig?.replicaFile : void 0;
|
|
296
|
+
const replicaFile = replicaFileForInitialization ?? replicaFileForReinitialization;
|
|
297
|
+
const replicaSource = replicaFile ? replicaInitializationSource(lc, replicaFile) : void 0;
|
|
296
298
|
this.#initializer = new ChangeLogInitializer(lc, {
|
|
297
299
|
initFromPgChangeLog: this.#pgChangeLogEnabled,
|
|
298
|
-
initFromReplica:
|
|
300
|
+
initFromReplica: replicaFileForInitialization !== void 0
|
|
299
301
|
}, {
|
|
300
302
|
pgChangeLog: () => this.#storer.getStartStreamInitializationParameters(),
|
|
301
303
|
replica: () => must(replicaSource)(),
|
|
302
304
|
reconcileChangeLog: (resumeFrom, seed) => resumeFrom ? this.#changeLogWriter?.reconcile(resumeFrom) : this.#changeLogWriter?.reconcileFromLog(seed),
|
|
303
|
-
changeLog: () => this.#changeLogWriter?.connection
|
|
305
|
+
changeLog: () => this.#changeLogWriter?.connection,
|
|
306
|
+
reinitializePgChangeLog: replicaFileForReinitialization ? (params) => this.#storer.reinitializeFromReplica(params) : void 0
|
|
304
307
|
});
|
|
305
308
|
this.#sqliteCatchupOptions = opts.sqliteCatchup ? {
|
|
306
309
|
...opts.sqliteCatchup,
|
|
@@ -323,7 +326,10 @@ var ChangeStreamerImpl = class {
|
|
|
323
326
|
this.#latestStatus.lagReport = { nextSendTimeMs: lagReportInit.nextSendTimeMs };
|
|
324
327
|
this.#latestLagReportCommitTimeMs = lagReportInit.firstCommitTimeMs;
|
|
325
328
|
}
|
|
326
|
-
if (this.#pgChangeLogEnabled)
|
|
329
|
+
if (this.#pgChangeLogEnabled) this.#storer.assumeOwnershipAtHead({
|
|
330
|
+
purgeLock: this.#purgeLock,
|
|
331
|
+
requestRestart: (reason) => this.#backfills.requestRestart(reason)
|
|
332
|
+
});
|
|
327
333
|
this.#purgeLock = null;
|
|
328
334
|
const flushBytesThreshold = getDefaultHighWaterMark(false);
|
|
329
335
|
while (this.#state.shouldRun()) {
|
|
@@ -335,17 +341,19 @@ var ChangeStreamerImpl = class {
|
|
|
335
341
|
const backfillRequests = this.#backfills.startStream(this.#pgChangeLogEnabled ? logBackfillRequests : []);
|
|
336
342
|
this.#lastForwardedCommitWatermark = lastWatermark;
|
|
337
343
|
const stream = await this.#source.startStream(lastWatermark, backfillRequests);
|
|
338
|
-
if (this.#pgChangeLogEnabled) this.#storer.run().catch((e) => stream.changes.cancel(e));
|
|
339
344
|
this.#stream = stream;
|
|
345
|
+
if (!this.#state.shouldRun()) break;
|
|
346
|
+
if (this.#pgChangeLogEnabled) this.#storer.run().catch((e) => stream.changes.cancel(e));
|
|
340
347
|
if (this.#state.resetBackoff() > REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS) this.#replicationStatusPublisher.publish(this.#lc, "Replicating", `Replicating from ${lastWatermark}`);
|
|
341
348
|
watermark = null;
|
|
342
|
-
this.#acker.reset(stream.acks,
|
|
349
|
+
this.#acker.reset(stream.acks, lastWatermark);
|
|
343
350
|
for await (const change of stream.changes) {
|
|
344
351
|
this.#acker.trackDownstream(change);
|
|
345
352
|
const [type, msg] = change;
|
|
346
|
-
if (type === "status" && watermark === null && this.#
|
|
353
|
+
if (type === "status" && watermark === null && this.#mayRestart()) break;
|
|
347
354
|
switch (type) {
|
|
348
355
|
case "status":
|
|
356
|
+
if (watermark === null) this.#storer.status(change[2].watermark);
|
|
349
357
|
if (msg.lagReport && msg.lagReport.lastTimings.commitTimeMs >= this.#latestLagReportCommitTimeMs) {
|
|
350
358
|
this.#latestStatus.lagReport = msg.lagReport;
|
|
351
359
|
this.#latestLagReportCommitTimeMs = msg.lagReport.lastTimings.commitTimeMs;
|
|
@@ -389,7 +397,7 @@ var ChangeStreamerImpl = class {
|
|
|
389
397
|
}
|
|
390
398
|
const readyForMore = this.#pgChangeLogEnabled ? this.#storer.readyForMore() : void 0;
|
|
391
399
|
if (readyForMore) await promiseOrAbort(readyForMore, stream.changes.signal, this.#state.signal);
|
|
392
|
-
if (type === "commit" && this.#
|
|
400
|
+
if (type === "commit" && this.#mayRestart()) break;
|
|
393
401
|
}
|
|
394
402
|
} catch (e) {
|
|
395
403
|
err = e;
|
|
@@ -615,6 +623,7 @@ var ChangeStreamerImpl = class {
|
|
|
615
623
|
};
|
|
616
624
|
}
|
|
617
625
|
async #purgePGChangeLog() {
|
|
626
|
+
if (this.#storer.ownershipPending) return;
|
|
618
627
|
try {
|
|
619
628
|
const { backupWatermark, purgeWatermark, current } = this.#getCleanupFloor();
|
|
620
629
|
if (purgeWatermark < backupWatermark) {
|
|
@@ -652,6 +661,16 @@ var ChangeStreamerImpl = class {
|
|
|
652
661
|
if (!this.#purgeScheduler || continuation === void 0) return;
|
|
653
662
|
if (continuation === "immediate" || this.#sqlitePurgeContinuation === void 0) this.#sqlitePurgeContinuation = continuation;
|
|
654
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Whether the stream should be restarted (at a transaction boundary) for
|
|
666
|
+
* backfills. Restarts are held until ownership of the PG change log is
|
|
667
|
+
* assumed: a restarted stream would decode the WAL up to the change log's
|
|
668
|
+
* head again, and backfills cannot be stored until ownership is assumed
|
|
669
|
+
* anyway (see Storer.assumeOwnershipAtHead()).
|
|
670
|
+
*/
|
|
671
|
+
#mayRestart() {
|
|
672
|
+
return this.#backfills.restartReason !== null && !this.#storer.ownershipPending;
|
|
673
|
+
}
|
|
655
674
|
async stop(err) {
|
|
656
675
|
this.#state.stop(this.#lc, err);
|
|
657
676
|
this.#stream?.changes.cancel();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-streamer-service.js","names":["#lc","#shard","#changeDBProvider","#replicaVersion","#source","#storer","#pgChangeLogEnabled","#forwarder","#reservations","#replicationStatusPublisher","#sqliteCatchupOptions","#changeLogWriter","#purgeScheduler","#comparator","#acker","#initializer","#readRouter","#autoReset","#state","#backfills","#serving","#txCounter","#changeCounter","#transactionForwardDuration","#catchupRoutes","#reservationDemotions","#reservationConfirmDelays","#sqliteCatchup","#closeSQLiteCatchup","#purgeLock","#latestStatus","#latestLagReportCommitTimeMs","#lastForwardedCommitWatermark","#stream","#handleControlMessage","#recordForwardedTransactionBoundary","#selectSQLiteCatchup","#captureRequiredHead","#recordCatchupRoute","#confirmReservations","#backupWatermark","#requestSQLitePurge","#maybeSchedulePGPurge","#maybeScheduleSQLitePurge","#getChangeLogState","#pgPurgeScheduled","#pgPurgeRunning","#pgPurgedWatermark","#purgePGChangeLog","#sqlitePurgeScheduled","#sqlitePurgeRunning","#sqlitePurgeContinuation","#purgeSQLiteChangeLog","#getCleanupFloor","#loggedBehindWatermark","#currentTransactionCompletion","#transactionForwardStartedAt","#recordTransactionForwardDuration","#fallbackOrReject","#declineSQLiteCatchup","#openSQLiteCatchup","#changeLogUnavailableSince"],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-service.ts"],"sourcesContent":["import {getDefaultHighWaterMark} from 'node:stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {defu} from 'defu';\nimport postgres, {type Options, type PostgresType} from 'postgres';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseOrAbort} from '../../../../shared/src/promise-race.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {publishCriticalEvent} from '../../observability/events.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {min} from '../../types/lexi-version.ts';\nimport type {PostgresDB} from '../../types/pg.ts';\nimport type {ShardID} from '../../types/shards.ts';\nimport type {PreSerialized, Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {\n ChangeSource,\n ChangeStream,\n} from '../change-source/change-source.ts';\nimport {\n type ChangeStreamControl,\n type ChangeStreamData,\n type Rollback,\n} from '../change-source/protocol/current/downstream.ts';\nimport type {LitestreamVersion} from '../litestream/metrics.ts';\nimport {\n publishReplicationError,\n replicationStatusError,\n type ReplicationStatusPublisher,\n} from '../replicator/replication-status.ts';\nimport type {SubscriptionState} from '../replicator/schema/replication-state.ts';\nimport {\n DEFAULT_MAX_RETRY_DELAY_MS,\n RunningState,\n UnrecoverableError,\n} from '../running-state.ts';\nimport {BackfillStateTracker} from './backfill-state-tracker.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport {serializeChangeStreamDataWithChange} from './change-log-codec.ts';\nimport {\n ChangeLogInitializer,\n replicaInitializationSource,\n} from './change-log-initializer.ts';\nimport {\n type ChangeStreamerService,\n type Status,\n type SubscriberContext,\n type WatermarkedChange,\n} from './change-streamer.ts';\nimport * as ErrorType from './error-type-enum.ts';\nimport {Forwarder} from './forwarder.ts';\nimport {\n AutoResetSignal,\n ensureReplicationConfig,\n markResetRequired,\n} from './schema/tables.ts';\nimport type {SnapshotMessage} from './snapshot-message.ts';\nimport {SnapshotReservations} from './snapshot-reservations.ts';\nimport {\n SQLiteChangeLogCatchup,\n type SQLiteChangeLogCleanupGuard,\n} from './sqlite-change-log-catchup.ts';\nimport {\n SQLiteChangeLogComparator,\n type SQLiteChangeLogCompareOptions,\n} from './sqlite-change-log-comparator.ts';\nimport {\n SQLiteChangeLogPurgeScheduler,\n type PurgeContinuation,\n type SQLiteChangeLogPurgeSchedulerOptions,\n} from './sqlite-change-log-purge-scheduler.ts';\nimport {\n inspectSQLiteChangeLog,\n SQLiteChangeLogReadRouter,\n type ChangeLogReadRoute,\n type SQLiteChangeLogCoverage,\n} from './sqlite-change-log-read-router.ts';\nimport {SQLiteChangeLogReader} from './sqlite-change-log-reader.ts';\nimport {\n SQLiteChangeLogWriter,\n type SQLiteChangeLogWriterOptions,\n} from './sqlite-change-log-writer.ts';\nimport type {PostgresDBProvider} from './storer.ts';\nimport {\n Storer,\n type PurgeLock,\n type TuningOptions as StorerOptions,\n} from './storer.ts';\nimport {Subscriber} from './subscriber.ts';\nimport {UpstreamAcker} from './upstream-acker.ts';\n\nexport type BackupConfig = {\n backupURL: string;\n litestreamVersion: LitestreamVersion;\n /**\n * The replica being backed up. Its size is sent to subscribers as an\n * estimate of the size of their restored replica.\n */\n replicaFile?: string | undefined;\n};\n\nexport type SQLiteCatchupOptions = {\n /** The change-log database, i.e. `changeLogFileName(replicaFile)`. */\n changeLogFile: string;\n readBatchRows: number;\n barrierTimeoutMs: number;\n /**\n * Backstop for the change-log writer's ACK, which is what normally releases\n * the barrier. Defaults to the catchup coordinator's interval.\n */\n barrierPollIntervalMs?: number | undefined;\n /**\n * Optional policy hook used by focused tests. Production canary selection is\n * supplied by `sqliteChangeLogServe` and is stable by shard plus task ID.\n * Backup subscribers are rejected before either selector is invoked.\n */\n shouldUse?: ((ctx: SubscriberContext) => boolean) | undefined;\n /**\n * Overrides the purge scheduler's guard, for tests. When absent, the\n * service supplies the scheduler's real writer-serialized guard, or the\n * catchup's no-op default when purging is not configured.\n */\n cleanupGuard?: SQLiteChangeLogCleanupGuard | undefined;\n /**\n * How long the change log may be unavailable before declining to serve from\n * it is reported as a warning rather than a debug line. Defaults to\n * {@link DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS}.\n */\n notReadyWarnThresholdMs?: number | undefined;\n};\n\nexport type SQLiteChangeLogServeOptions = {\n /** Stable percentage of eligible serving tasks routed to SQLite. */\n readPercent: number;\n /**\n * Stable percentage of eligible serving tasks routed to a log that has not\n * yet aged through {@link retentionMs}. Zero keeps a reseeded log on PG for\n * that whole window; above zero it serves that share of tasks, and a\n * reservation it cannot cover is demoted rather than held pending.\n * Defaults to zero.\n */\n coldReadPercent?: number | undefined;\n /** The warm-up window a reseeded log ages through. */\n retentionMs: number;\n /** Injectable for deterministic breaker tests. */\n failureCooldownMs?: number | undefined;\n /** Injectable for deterministic warm-up and breaker tests. */\n now?: (() => number) | undefined;\n /** Injectable readiness inspection for service-level tests. */\n inspect?: (() => SQLiteChangeLogCoverage | undefined) | undefined;\n};\n\nexport type TuningOptions = StorerOptions & {\n /**\n * Keeps the legacy Postgres change log on the initialization, persistence,\n * catchup, purge, and ACK paths. Disable only after SQLite and the replica\n * backup are authoritative for all of those responsibilities.\n */\n pgChangeLogEnabled: boolean;\n flowControlConsensusTimeoutProportion: number;\n flowControlSlowSubscriberGracePeriodMs?: number | undefined;\n sqliteCatchup?: SQLiteCatchupOptions | undefined;\n /**\n * Supplied when `sqliteChangeLogMode != off`, i.e. this is the gate on the\n * change-log writer. Absent, nothing writes the log.\n */\n sqliteChangeLogWriter?:\n | Omit<\n SQLiteChangeLogWriterOptions,\n 'onCommit' | 'onDisabled' | 'onRebuilt'\n >\n | undefined;\n /**\n * Also supplied when `sqliteChangeLogMode != off`, and deliberately not\n * gated on the read path: in `write` mode no reader ever opens, and this is\n * the configuration the purge scheduler actually ships in. The scheduler\n * runs on the writer's own connection, so with the writer absent (mode\n * `off`) it is never constructed, and a leftover file on disk is left to\n * the replicator's cleanup.\n */\n sqliteChangeLogPurge?: SQLiteChangeLogPurgeSchedulerOptions | undefined;\n /**\n * Supplied in `compare` mode and later modes.\n * `replicaFile` enables the initialization comparison.\n * The other fields configure sampled catchup comparisons.\n * Both checks are advisory, and Postgres remains authoritative.\n * The comparator also requires the writer and catchup options.\n */\n sqliteChangeLogCompare?:\n | (SQLiteChangeLogCompareOptions & {replicaFile: string})\n | undefined;\n /** Supplied only in `serve` mode. A zero percentage keeps every read on PG. */\n sqliteChangeLogServe?: SQLiteChangeLogServeOptions | undefined;\n};\n\n/**\n * Performs initialization and schema migrations to initialize a ChangeStreamerImpl.\n */\nexport async function initializeStreamer(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n changeDB: PostgresDB,\n changeSource: ChangeSource,\n replicationStatusPublisher: ReplicationStatusPublisher,\n subscriptionState: SubscriptionState,\n backupConfig: BackupConfig | null,\n purgeLock: PurgeLock | null,\n autoReset: boolean,\n opts: TuningOptions,\n setTimeoutFn = setTimeout,\n): Promise<ChangeStreamerService> {\n await ensureReplicationConfig(\n lc,\n changeDB,\n subscriptionState,\n shard,\n autoReset,\n purgeLock ?? undefined,\n setTimeoutFn,\n opts.pgChangeLogEnabled,\n );\n\n // Dynamically creates connection pools that the implementation uses to\n // isolate concurrent, long-running transactions. This works around a bug in\n // the postgres.js client where connections get swapped in certain conditions.\n //\n // https://github.com/porsager/postgres/issues/1204\n const changeDBProvider: PostgresDBProvider = (\n applicationName: string,\n max: number,\n ) =>\n postgres(\n defu(\n {max, connection: {['application_name']: applicationName}},\n // ParsedOptions are technically compatible with Options, but happen\n // to not be typed that way. The postgres.js author does an equivalent\n // merge of ParsedOptions and Options here:\n // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/subscribe.js#L13\n changeDB.options as unknown as Options<Record<string, PostgresType>>,\n ),\n ) as PostgresDB;\n\n const {replicaVersion} = subscriptionState;\n return new ChangeStreamerImpl(\n lc,\n shard,\n taskID,\n discoveryAddress,\n discoveryProtocol,\n changeDBProvider,\n replicaVersion,\n changeSource,\n replicationStatusPublisher,\n backupConfig,\n purgeLock,\n autoReset,\n opts,\n setTimeoutFn,\n );\n}\n\nconst REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS = 5000;\n\n// How long the change log may be unavailable before declining to serve from it\n// stops looking like normal startup ordering. Subscriptions can arrive before\n// the stream loop's first reconcile has created and seeded the log, so early\n// declines are expected. Still declining a minute later means the log is not\n// being written at all -- `sqliteChangeLogMode=off`, a writer that has failed\n// soft, or a path mismatch -- which is worth surfacing.\nconst DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS = 60_000;\n\n/**\n * Upstream-agnostic dispatch of messages in a {@link ChangeStreamMessage} to a\n * {@link Forwarder} and {@link Storer} to execute the forward-store-ack\n * procedure described in {@link ChangeStreamer}.\n *\n * ### Subscriber Catchup\n *\n * Connecting clients first need to be \"caught up\" to the current watermark\n * (from stored change log entries) before new entries are forwarded to\n * them. This is non-trivial because the replication stream may be in the\n * middle of a pending streamed Transaction for which some entries have\n * already been forwarded but are not yet committed to the store.\n *\n *\n * ```\n * ------------------------------- - - - - - - - - - - - - - - - - - - -\n * | Historic changes in storage | Pending (streamed) tx | Next tx\n * ------------------------------- - - - - - - - - - - - - - - - - - - -\n * Replication stream\n * > > > > > > > > >\n * ^ ---> required catchup ---> ^\n * Subscriber watermark Subscription begins\n * ```\n *\n * Preemptively buffering the changes of every pending transaction\n * would be wasteful and consume too much memory for large transactions.\n *\n * Instead, the streamer synchronously dispatches changes and subscriptions\n * to the {@link Forwarder} and the {@link Storer} such that the two\n * components are aligned as to where in the stream the subscription started.\n * The two components then coordinate catchup and handoff via the\n * {@link Subscriber} object with the following algorithm:\n *\n * * If the streamer is in the middle of a pending Transaction, the\n * Subscriber is \"queued\" on both the Forwarder and the Storer. In this\n * state, new changes are *not* forwarded to the Subscriber, and catchup\n * is not yet executed.\n * * Once the commit message for the pending Transaction is processed\n * by the Storer, it begins catchup on the Subscriber (with a READONLY\n * snapshot so that it does not block subsequent storage operations).\n * This catchup is thus guaranteed to load the change log entries of\n * that last Transaction.\n * * When the Forwarder processes that same commit message, it moves the\n * Subscriber from the \"queued\" to the \"active\" set of clients such that\n * the Subscriber begins receiving new changes, starting from the next\n * Transaction.\n * * The Subscriber does not forward those changes, however, if its catchup\n * is not complete. Until then, it buffers the changes in memory.\n * * Once catchup is complete, the buffered changes are immediately sent\n * and the Subscriber henceforth forwards changes as they are received.\n *\n * In the (common) case where the streamer is not in the middle of a pending\n * transaction when a subscription begins, the Storer begins catchup\n * immediately and the Forwarder directly adds the Subscriber to its active\n * set. However, the Subscriber still buffers any forwarded messages until\n * its catchup is complete.\n *\n * ### Watermarks and ordering\n *\n * The ChangeStreamerService depends on its {@link ChangeSource} to send\n * changes in contiguous [`begin`, `data` ..., `data`, `commit`] sequences\n * in commit order. This follows Postgres's Logical Replication Protocol\n * Message Flow:\n *\n * https://www.postgresql.org/docs/16/protocol-logical-replication.html#PROTOCOL-LOGICAL-MESSAGES-FLOW\n *\n * > The logical replication protocol sends individual transactions one by one.\n * > This means that all messages between a pair of Begin and Commit messages belong to the same transaction.\n *\n * In order to correctly replay (new) and filter (old) messages to subscribers\n * at different points in the replication stream, these changes must be assigned\n * watermarks such that they preserve the order in which they were received\n * from the ChangeSource.\n *\n * A previous implementation incorrectly derived these watermarks from the Postgres\n * Log Sequence Numbers (LSN) of each message. However, LSNs from concurrent,\n * non-conflicting transactions can overlap, which can result in a `begin` message\n * with an earlier LSN arriving after a `commit` message. For example, the\n * changes for these transactions:\n *\n * ```\n * LSN: 1 2 3 4 5 6 7 8 9 10\n * tx1: begin data data data commit\n * tx2: begin data data data commit\n * ```\n *\n * will arrive as:\n *\n * ```\n * begin1, data2, data4, data6, commit8, begin3, data5, data7, data9, commit10\n * ```\n *\n * Thus, LSN of non-commit messages are not suitable for tracking the sorting\n * order of the replication stream.\n *\n * Instead, the ChangeStreamer uses the following algorithm for deterministic\n * catchup and filtering of changes:\n *\n * * A `commit` message is assigned to a watermark corresponding to its LSN.\n * These are guaranteed to be in commit order by definition.\n *\n * * `begin` and `data` messages are assigned to the watermark of the\n * preceding `commit` (the previous transaction, or the replication\n * slot's starting LSN) plus 1. This guarantees that they will be sorted\n * after the previously commit transaction even if their LSNs came before it.\n * This is referred to as the `preCommitWatermark`.\n *\n * * In the ChangeLog DB, messages have a secondary sort column `pos`, which is\n * the position of the message within its transaction, with the `begin` message\n * starting at `0`. This guarantees that `begin` and `data` messages will be\n * fetched in the original ChangeSource order during catchup.\n *\n * `begin` and `data` messages share the same watermark, but this is sufficient for\n * Subscriber filtering because subscribers only know about the `commit` watermarks\n * exposed in the `Downstream` `Commit` message. The Subscriber object thus compares\n * the internal watermarks of the incoming messages against the commit watermark of\n * the caller, updating the watermark at every `Commit` message that is forwarded.\n *\n * ### Cleanup\n *\n * As mentioned in the {@link ChangeStreamer} documentation: \"the ChangeStreamer\n * uses a combination of [the \"initial\", i.e. backup-derived watermark and] ACK\n * responses from connected subscribers to determine the watermark up\n * to which it is safe to purge old change log entries.\"\n *\n * More concretely:\n *\n * * The `initial`, backup-derived watermark is the earliest to which cleanup\n * should ever happen.\n *\n * * However, it is possible for the replica backup to be *ahead* of a connected\n * subscriber; and if a network error causes that subscriber to retry from its\n * last watermark, the change streamer must support it.\n *\n * Thus, before cleaning up to an `initial` backup-derived watermark, the change\n * streamer first confirms that all connected subscribers have also passed\n * that watermark.\n */\nclass ChangeStreamerImpl implements ChangeStreamerService {\n readonly id: string;\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #changeDBProvider: PostgresDBProvider;\n readonly #replicaVersion: string;\n readonly #source: ChangeSource;\n readonly #storer: Storer;\n readonly #pgChangeLogEnabled: boolean;\n readonly #forwarder: Forwarder;\n readonly #reservations: SnapshotReservations | undefined;\n readonly #replicationStatusPublisher: ReplicationStatusPublisher;\n readonly #sqliteCatchupOptions: SQLiteCatchupOptions | undefined;\n readonly #changeLogWriter: SQLiteChangeLogWriter | undefined;\n readonly #purgeScheduler: SQLiteChangeLogPurgeScheduler | undefined;\n readonly #comparator: SQLiteChangeLogComparator | undefined;\n readonly #acker: UpstreamAcker;\n readonly #initializer: ChangeLogInitializer;\n readonly #readRouter: SQLiteChangeLogReadRouter | undefined;\n\n readonly #autoReset: boolean;\n readonly #state: RunningState;\n\n /** Determines the BackfillRequests with which to (re)start streams. */\n readonly #backfills: BackfillStateTracker;\n\n // Starting the (Postgres) ChangeStream results in killing the previous\n // Postgres subscriber, potentially creating a gap in which the old\n // change-streamer has shut down and the new change-streamer has not yet\n // been recognized as \"healthy\" (and thus does not get any requests).\n //\n // To minimize this gap, delay starting the ChangeStream until the first\n // request from a `serving` replicator, indicating that higher level\n // load-balancing / routing logic has begun routing requests to this task.\n readonly #serving = resolver();\n\n readonly #txCounter = getOrCreateCounter(\n 'replication',\n 'transactions',\n 'Count of replicated transactions',\n );\n readonly #changeCounter = getOrCreateCounter(\n 'replication',\n 'changes',\n 'Count of replicated changes (DML or DDL statements)',\n );\n // The number the SQLite change-log commit lands in. It is labeled by whether\n // the log is being written so that the cost of putting a commit on the\n // forward path is attributable rather than inferred.\n readonly #transactionForwardDuration = getOrCreateLatencyHistogram(\n 'replication',\n 'transaction_forward_duration',\n \"Time from receiving a transaction's `begin` to forwarding its `commit`, \" +\n 'i.e. the change-streamer half of forward-to-subscriber latency.',\n );\n readonly #catchupRoutes = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.catchup_routes',\n 'Catchup subscriptions by selected source and low-cardinality reason.',\n );\n readonly #reservationDemotions = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.reservation_demotions',\n 'Snapshot reservations demoted from SQLite to PG because the change ' +\n 'log could not cover the backup being restored. With PG retired ' +\n 'these followers have no fallback, so this is the rate at which one ' +\n 'would instead have to wait for a later backup.',\n );\n readonly #reservationConfirmDelays = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.reservation_confirm_delays',\n 'Snapshot reservations whose confirmation was deferred because the ' +\n \"selected source's change-log minimum was later than the backup \" +\n 'watermark. Counted once per reservation, by that source.',\n );\n\n #latestStatus: Status;\n #latestLagReportCommitTimeMs = 0;\n #backupWatermark: string | undefined;\n #pgPurgedWatermark: string = '';\n /**\n * The floor last logged as held back by a laggard. The level-triggered\n * retry re-evaluates every {@link CLEANUP_DELAY_MS}; logging only when the\n * blocking floor moves keeps a stuck subscriber from emitting the same\n * line indefinitely.\n */\n #loggedBehindWatermark: string | undefined;\n #sqlitePurgeContinuation: PurgeContinuation | undefined;\n #purgeLock: PurgeLock | null;\n // PG and SQLite intentionally own separate level-triggered loops. Neither\n // waits for, advances, or retries the other.\n #pgPurgeScheduled = false;\n #pgPurgeRunning = false;\n #sqlitePurgeScheduled = false;\n #sqlitePurgeRunning = false;\n #stream: ChangeStream | undefined;\n #sqliteCatchup: SQLiteChangeLogCatchup | undefined;\n #changeLogUnavailableSince: number | undefined;\n #lastForwardedCommitWatermark: string | undefined;\n #transactionForwardStartedAt: number | undefined;\n #currentTransactionCompletion:\n | Resolver<ForwardedTransactionCompletion>\n | undefined;\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n changeDBProvider: PostgresDBProvider,\n replicaVersion: string,\n source: ChangeSource,\n replicationStatusPublisher: ReplicationStatusPublisher,\n backupConfig: BackupConfig | null,\n initialPurgeLock: PurgeLock | null,\n autoReset: boolean,\n opts: TuningOptions,\n setTimeoutFn = setTimeout,\n ) {\n this.id = `change-streamer`;\n this.#lc = lc.withContext('component', 'change-streamer');\n this.#backfills = new BackfillStateTracker(this.#lc);\n this.#shard = shard;\n this.#changeDBProvider = changeDBProvider;\n this.#replicaVersion = replicaVersion;\n this.#source = source;\n this.#pgChangeLogEnabled = opts.pgChangeLogEnabled;\n if (!this.#pgChangeLogEnabled) {\n assert(\n opts.sqliteChangeLogWriter &&\n opts.sqliteChangeLogPurge &&\n opts.sqliteCatchup &&\n opts.sqliteChangeLogServe?.readPercent === 100 &&\n opts.sqliteChangeLogServe.coldReadPercent === 100 &&\n backupConfig?.litestreamVersion === 'v5',\n 'disabling the PG change log requires SQLite serving at 100 percent and a v5 backup',\n );\n assert(\n opts.sqliteChangeLogCompare === undefined,\n 'SQLite change-log comparison requires the PG change log',\n );\n }\n this.#storer = new Storer(\n lc,\n shard,\n taskID,\n discoveryAddress,\n discoveryProtocol,\n changeDBProvider,\n replicaVersion,\n consumed => this.#acker.trackPgChangeLog(consumed[2].watermark),\n err => this.stop(err),\n opts,\n );\n this.#forwarder = new Forwarder(lc, {\n flowControlConsensusTimeoutProportion:\n opts.flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodMs:\n opts.flowControlSlowSubscriberGracePeriodMs,\n });\n const serveOptions = opts.sqliteChangeLogServe;\n const writerOptions = opts.sqliteChangeLogWriter;\n const catchupOptions = opts.sqliteCatchup;\n this.#readRouter =\n serveOptions && writerOptions && catchupOptions\n ? new SQLiteChangeLogReadRouter({\n shard,\n readPercent: serveOptions.readPercent,\n coldReadPercent: serveOptions.coldReadPercent,\n retentionMs: serveOptions.retentionMs,\n failureCooldownMs: serveOptions.failureCooldownMs,\n now: serveOptions.now,\n inspect:\n serveOptions.inspect ??\n (() =>\n inspectSQLiteChangeLog(\n lc,\n catchupOptions.changeLogFile,\n writerOptions.identity,\n )),\n })\n : undefined;\n this.#reservations = backupConfig\n ? new SnapshotReservations(lc, backupConfig, taskID => {\n this.#readRouter?.release(taskID);\n this.#purgeScheduler?.resume(taskID);\n })\n : undefined;\n this.#replicationStatusPublisher = replicationStatusPublisher;\n this.#changeLogWriter = opts.sqliteChangeLogWriter\n ? new SQLiteChangeLogWriter(lc, {\n ...opts.sqliteChangeLogWriter,\n onCommit: watermark => {\n this.#sqliteCatchup?.onChangeLogCommit(watermark);\n // The commit closed the log's transaction, i.e. opened a purge\n // window (§3.3).\n this.#purgeScheduler?.onWriterIdle();\n },\n // Fail-soft deletes the file, and the reader is cached here, so it\n // has to go with it: otherwise it serves an unlinked inode while\n // every new open sees nothing.\n onDisabled: () => {\n // The writer stays disabled and the file stays absent for the life\n // of this process, so unlike a transient read/barrier failure this\n // breaker never expires.\n this.#readRouter?.trip(true);\n this.#closeSQLiteCatchup();\n this.#comparator?.stop();\n },\n onRebuilt: () => {\n this.#closeSQLiteCatchup();\n // Invalidate cycles that can still read the replaced file.\n this.#comparator?.invalidate();\n },\n })\n : undefined;\n // The purge scheduler runs on the writer's own connection, which is also\n // its gate: no writer (mode `off`) means no scheduler, and a writer that\n // has not created the file yet -- or failed soft and deleted it -- makes\n // cycles skip rather than fail, so a late-appearing file is picked up\n // without a restart.\n this.#purgeScheduler =\n opts.sqliteChangeLogPurge && this.#changeLogWriter\n ? new SQLiteChangeLogPurgeScheduler(\n lc,\n () => this.#changeLogWriter?.connection,\n () => this.#forwarder.getAcks(),\n opts.sqliteChangeLogPurge,\n )\n : undefined;\n this.#acker = new UpstreamAcker({\n trackPgChangeLog: this.#pgChangeLogEnabled,\n trackBackup: backupConfig?.litestreamVersion === 'v5',\n });\n const replicaFileForInitialization =\n opts.sqliteChangeLogCompare?.replicaFile ??\n (!this.#pgChangeLogEnabled\n ? opts.sqliteChangeLogWriter?.replicaFile\n : undefined);\n const replicaSource = replicaFileForInitialization\n ? replicaInitializationSource(lc, replicaFileForInitialization)\n : undefined;\n this.#initializer = new ChangeLogInitializer(\n lc,\n {\n initFromPgChangeLog: this.#pgChangeLogEnabled,\n initFromReplica: replicaSource !== undefined,\n },\n {\n pgChangeLog: () =>\n this.#storer.getStartStreamInitializationParameters(),\n // Only reached when `initFromReplica` is set, i.e. when the option\n // that supplies the file is present.\n replica: () => must(replicaSource)(),\n // Use the Postgres resume point when it is present. Otherwise, let the\n // SQLite change log supply its own resume point.\n reconcileChangeLog: (resumeFrom, seed) =>\n resumeFrom\n ? this.#changeLogWriter?.reconcile(resumeFrom)\n : this.#changeLogWriter?.reconcileFromLog(seed),\n changeLog: () => this.#changeLogWriter?.connection,\n },\n );\n this.#sqliteCatchupOptions = opts.sqliteCatchup\n ? {\n ...opts.sqliteCatchup,\n // The real cleanup guard, in place of the catchup's no-op default:\n // registration and purge batches now serialize on one mutex.\n cleanupGuard:\n opts.sqliteCatchup.cleanupGuard ??\n this.#purgeScheduler?.cleanupGuard,\n }\n : undefined;\n // Compare mode requires the writer and catchup configuration.\n this.#comparator =\n this.#pgChangeLogEnabled &&\n opts.sqliteChangeLogCompare &&\n opts.sqliteChangeLogWriter &&\n opts.sqliteCatchup\n ? new SQLiteChangeLogComparator(\n lc,\n shard,\n opts.sqliteCatchup.changeLogFile,\n opts.sqliteChangeLogWriter.identity,\n this.#storer,\n {setTimeoutFn, ...opts.sqliteChangeLogCompare},\n )\n : undefined;\n this.#purgeLock = initialPurgeLock;\n this.#autoReset = autoReset;\n this.#state = new RunningState(this.id, undefined, setTimeoutFn);\n this.#latestStatus = {tag: 'status'};\n }\n\n async run() {\n this.#lc.info?.('starting change stream');\n\n this.#forwarder.startProgressMonitor();\n\n const lagReportInit = await this.#source.startLagReporter();\n if (lagReportInit) {\n this.#latestStatus.lagReport = {\n nextSendTimeMs: lagReportInit.nextSendTimeMs,\n };\n // Record the commit time of the initiated lag report (i.e. \"head\")\n // for the purpose of skipping over any lag reports that are re-streamed\n // by the change-source in the case of a change-streamer starting from\n // an older watermark.\n this.#latestLagReportCommitTimeMs = lagReportInit.firstCommitTimeMs;\n }\n\n // Once this change-streamer acquires \"ownership\" of the change DB,\n // it is safe to start the storer.\n if (this.#pgChangeLogEnabled) {\n await this.#storer.assumeOwnership(this.#purgeLock);\n }\n this.#purgeLock = null;\n\n // The threshold in (estimated number of) bytes to send() on subscriber\n // websockets before `await`-ing the I/O buffers to be ready for more.\n const flushBytesThreshold = getDefaultHighWaterMark(false);\n\n while (this.#state.shouldRun()) {\n let err: unknown;\n let watermark: string | null = null;\n let unflushedBytes = 0;\n try {\n // Initialization reconciles the change log for every stream\n // connection. It completes before `startStream`, so no change can\n // arrive during reconciliation.\n const {lastWatermark, backfillRequests: logBackfillRequests} =\n await this.#initializer.initialize();\n const backfillRequests = this.#backfills.startStream(\n // Backfill requests from the change-log are only necessary for\n // backwards compatibility with protocol v7 and RMv1. On RMv2,\n // all of the beginning state comes from the (backup) replica,\n // which contains the progress marks, so there is no need to\n // downgrade to the change-log version which does not have\n // progress marks.\n this.#pgChangeLogEnabled ? logBackfillRequests : [],\n );\n // SQLite catchup must not be eligible until this has been initialized\n // from the selected durable head. Commits observed only since process\n // startup are insufficient after a change-streamer restart.\n this.#lastForwardedCommitWatermark = lastWatermark;\n const stream = await this.#source.startStream(\n lastWatermark,\n backfillRequests,\n );\n if (this.#pgChangeLogEnabled) {\n this.#storer.run().catch(e => stream.changes.cancel(e));\n }\n\n this.#stream = stream;\n if (\n this.#state.resetBackoff() >\n REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS\n ) {\n // After recovering from a backoff for which a replication status\n // error was published, publish an OK status\n this.#replicationStatusPublisher.publish(\n this.#lc,\n 'Replicating',\n `Replicating from ${lastWatermark}`,\n );\n }\n watermark = null;\n\n // With the PG change log enabled, the stream resumes from what it has\n // persisted, so nothing before the stream is outstanding. Otherwise it\n // resumes from the SQLite change log's head, which the backup can\n // trail.\n this.#acker.reset(\n stream.acks,\n this.#pgChangeLogEnabled ? '' : lastWatermark,\n );\n\n for await (const change of stream.changes) {\n this.#acker.trackDownstream(change);\n\n const [type, msg] = change;\n if (\n type === 'status' &&\n watermark === null &&\n this.#backfills.restartReason !== null\n ) {\n // A restart can happen at a status message outside of a\n // transaction, e.g. when the replication stream is otherwise idle.\n // Exiting the iteration cancels the stream.\n break;\n }\n switch (type) {\n case 'status':\n if (\n msg.lagReport &&\n msg.lagReport.lastTimings.commitTimeMs >=\n this.#latestLagReportCommitTimeMs\n ) {\n // Lag reports are not stored in the cdc change log, but rather\n // only forwarded on \"live\" connections. When a new subscriber\n // is catching up, it is initialized with the #latestStatus\n // from which it can measure lag while catching up.\n this.#latestStatus.lagReport = msg.lagReport;\n this.#latestLagReportCommitTimeMs =\n msg.lagReport.lastTimings.commitTimeMs;\n this.#forwarder.sendStatus(this.#latestStatus);\n }\n continue;\n case 'control':\n await this.#handleControlMessage(msg);\n continue; // control messages are not stored/forwarded\n case 'begin':\n watermark = change[2].commitWatermark;\n break;\n case 'commit':\n if (watermark !== change[2].watermark) {\n throw new UnrecoverableError(\n `commit watermark ${change[2].watermark} does not match 'begin' watermark ${watermark}`,\n );\n }\n this.#txCounter.add(1);\n break;\n default:\n if (type === 'data') {\n this.#changeCounter.add(1);\n }\n if (watermark === null) {\n throw new UnrecoverableError(\n `${type} change (${msg.tag}) received before 'begin' message`,\n );\n }\n break;\n }\n\n this.#backfills.track(change);\n\n const serialized = serializeChangeStreamDataWithChange(change);\n const {json} = serialized;\n if (this.#pgChangeLogEnabled) {\n this.#storer.store(watermark, change, serialized);\n }\n // The SQLite change log commits at transaction boundaries, and its\n // commit for this transaction lands here -- before the forward of the\n // `commit` message, and before #recordForwardedTransactionBoundary\n // advances what #captureRequiredHead reads. No `await` separates it\n // from serialization or the optional PG enqueue above. This is the\n // assertable form of invariant 1: a synchronous SQLite commit in the\n // same loop iteration always precedes anything that can advance the\n // watermark this stream would resume from. Never throws; a write\n // failure disables the writer rather than stopping replication.\n this.#changeLogWriter?.write(change, json, serialized.change);\n const entry: WatermarkedChange = [watermark, change[1].tag, json];\n unflushedBytes += json.length;\n let flowControl: Promise<void> | undefined;\n if (unflushedBytes < flushBytesThreshold) {\n // pipeline changes until flushBytesThreshold\n this.#forwarder.forward(entry);\n } else {\n // Wait for messages to clear socket buffers to ensure that they\n // make their way to subscribers. Without this `await`, the\n // messages end up being buffered in this process, which:\n // (1) results in memory pressure and increased GC activity\n // (2) prevents subscribers from processing the messages as they\n // arrive, instead getting them in a large batch after being\n // idle while they were queued (causing further delays).\n flowControl = this.#forwarder.forwardWithFlowControl(entry);\n }\n // Both forwards send the entry and update the Forwarder's transaction\n // state synchronously. Record the boundary, and end the transaction at\n // its commit or rollback, before awaiting flow control: registrations\n // during the wait then observe the forwarded state, and a stream that\n // is interrupted during the wait does not roll back a transaction\n // that was already forwarded whole.\n this.#recordForwardedTransactionBoundary(type, entry[0]);\n if (type === 'commit' || type === 'rollback') {\n watermark = null;\n }\n if (flowControl) {\n await promiseOrAbort(\n flowControl,\n stream.changes.signal,\n this.#state.signal,\n );\n unflushedBytes = 0;\n }\n\n // Allow the PG storer to exert back pressure when it is enabled.\n const readyForMore = this.#pgChangeLogEnabled\n ? this.#storer.readyForMore()\n : undefined;\n if (readyForMore) {\n await promiseOrAbort(\n readyForMore,\n stream.changes.signal,\n this.#state.signal,\n );\n }\n\n // Restarts happen at transaction boundaries, so that the pending\n // backfills of the aligned subscribers (and the change log) are\n // consistent with the watermark from which the stream restarts.\n if (type === 'commit' && this.#backfills.restartReason !== null) {\n // Exiting the iteration cancels the stream.\n break;\n }\n }\n } catch (e) {\n err = e;\n } finally {\n this.#stream?.changes.cancel();\n this.#stream = undefined;\n }\n\n // When the change stream is interrupted, abort any pending transaction.\n if (watermark) {\n this.#lc.warn?.(`aborting interrupted transaction ${watermark}`);\n if (this.#pgChangeLogEnabled) {\n this.#storer.abort();\n }\n // Rolling back the log leaves no rows for the interrupted transaction,\n // so the next connection's reconciliation sees a head at or below its\n // resume watermark rather than a partial transaction.\n this.#changeLogWriter?.abort();\n // A rollback ends the log's open transaction without a commit\n // notification; wake any purge batch waiting for that window.\n this.#purgeScheduler?.onWriterIdle();\n this.#forwarder.forward([watermark, 'rollback', ROLLBACK_JSON]);\n this.#recordForwardedTransactionBoundary('rollback', watermark);\n this.#backfills.track(['rollback', {tag: 'rollback'}]);\n }\n\n if (this.#backfills.restartReason !== null && err === undefined) {\n // A planned restart: drain the storer and reconnect immediately.\n this.#backfills.recordRestart();\n await this.#storer.stop();\n } else {\n // Backoff and drain any pending entries in the storer before reconnecting.\n await Promise.all([\n this.#storer.stop(),\n this.#state.backoff(this.#lc, err),\n this.#state.retryDelay > REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS\n ? publishCriticalEvent(\n this.#lc,\n replicationStatusError(this.#lc, 'Replicating', err),\n )\n : promiseVoid,\n ]);\n }\n }\n\n this.#forwarder.stopProgressMonitor();\n this.#lc.info?.('ChangeStreamer stopped');\n }\n\n async #handleControlMessage(msg: ChangeStreamControl[1]) {\n this.#lc.info?.('received control message', msg);\n const {tag} = msg;\n\n switch (tag) {\n case 'reset-required':\n await markResetRequired(\n this.#changeDBProvider('change-streamer-reset', 1),\n this.#shard,\n );\n await publishReplicationError(\n this.#lc,\n 'Replicating',\n msg.message ?? 'Resync required',\n msg.errorDetails,\n );\n if (this.#autoReset) {\n this.#lc.warn?.('shutting down for auto-reset');\n await this.stop(new AutoResetSignal());\n }\n break;\n default:\n unreachable(tag);\n }\n }\n\n async subscribe(\n ctx: SubscriberContext,\n downstream: Subscription<\n string | PreSerializedBatch\n > = Subscription.create(),\n ): Promise<Source<string | PreSerialized>> {\n const {protocolVersion, id, mode, replicaVersion, watermark, wsBatched} =\n ctx;\n if (mode === 'serving') {\n this.#serving.resolve();\n }\n let cleanupSubscriber = () => {};\n\n // No subscriber's ACK advances the SQLite change log's head any more: the\n // writer runs in this process, so the barrier is notified from the commit\n // itself (see #changeLogWriter's onCommit).\n const lc = this.#lc.withContext('subscriber', id);\n const subscriber = new Subscriber(\n protocolVersion,\n id,\n mode,\n watermark,\n downstream,\n () => this.#latestStatus,\n {wsBatched, ...this.#backfills.subscriberOptions(lc, ctx)},\n );\n this.#backfills.register(subscriber, downstream);\n const removeFromForwarder = () => {\n lc.info?.(`removing subscriber ${subscriber.id}`);\n this.#forwarder.remove(subscriber);\n };\n cleanupSubscriber = removeFromForwarder;\n if (replicaVersion !== this.#replicaVersion) {\n lc.warn?.(`rejecting subscriber at replica version ${replicaVersion}`);\n subscriber.close(\n ErrorType.WrongReplicaVersion,\n `current replica version is ${\n this.#replicaVersion\n } (requested ${replicaVersion})`,\n );\n } else {\n lc.info?.(`adding subscriber ${subscriber.id}`);\n\n const catchupFromPG = () => {\n assert(\n this.#pgChangeLogEnabled,\n 'cannot catch up from a disabled PG change log',\n );\n // Keep the existing PG registration/catchup lockstep unchanged when\n // SQLite was not selected before Forwarder.add().\n cleanupSubscriber = removeFromForwarder;\n this.#forwarder.add(subscriber);\n this.#storer.catchup(subscriber, mode);\n };\n const sqliteDecision = this.#selectSQLiteCatchup(lc, ctx);\n if (!sqliteDecision) {\n catchupFromPG();\n } else if (sqliteDecision.kind === 'rejected') {\n lc.warn?.(\n `${\n sqliteDecision.terminal ? 'rejecting' : 'ending'\n } subscription for ${ctx.id}: ${sqliteDecision.message}`,\n );\n if (sqliteDecision.terminal) {\n subscriber.close(ErrorType.WatermarkTooOld, sqliteDecision.message);\n } else {\n subscriber.close();\n }\n } else {\n const {catchup, reason, coverage, logWarm} = sqliteDecision;\n cleanupSubscriber = () => catchup.remove(subscriber);\n const registration = await catchup.catchup(\n subscriber,\n () => this.#captureRequiredHead(),\n {logWarm},\n );\n switch (registration.kind) {\n case 'registered':\n lc.debug?.(\n `serving ${ctx.id} from SQLite catchup`,\n ...(coverage ? [{sqliteChangeLogCoverage: coverage}] : []),\n );\n this.#recordCatchupRoute('sqlite', reason);\n break;\n case 'uncovered':\n if (this.#pgChangeLogEnabled) {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: subscriber watermark ` +\n `${ctx.watermark} is below the SQLite change-log minimum ` +\n registration.minWatermark,\n );\n this.#recordCatchupRoute('pg', 'watermark-uncovered');\n catchupFromPG();\n } else {\n const message =\n `earliest supported watermark is ` +\n `${registration.minWatermark} (requested ${ctx.watermark})`;\n lc.warn?.(\n `rejecting subscriber at watermark ${ctx.watermark}: ` +\n `the SQLite change log starts at ` +\n `${registration.minWatermark} and the PG change log is disabled`,\n );\n this.#recordCatchupRoute('none', 'watermark-uncovered');\n subscriber.close(ErrorType.WatermarkTooOld, message);\n }\n break;\n case 'declined':\n // Registration failed before the subscriber was committed to\n // SQLite. Use PG when enabled; otherwise end this subscription so\n // it retries. The coordinator has already tripped the breaker,\n // which keeps the retry off SQLite for the cooldown; it is\n // deliberately not closed here, since closing it would abort the\n // catchups of every other subscriber it is serving.\n if (this.#pgChangeLogEnabled) {\n lc.error?.(\n `serving ${ctx.id} from PG catchup: SQLite catchup ` +\n `registration failed`,\n registration.error,\n );\n this.#recordCatchupRoute('pg', 'registration-failed');\n catchupFromPG();\n } else {\n lc.error?.(\n `ending subscription for ${ctx.id} to retry SQLite catchup: ` +\n `registration failed and the PG change log is disabled`,\n registration.error,\n );\n this.#recordCatchupRoute('none', 'registration-failed');\n subscriber.fail(registration.error);\n }\n break;\n case 'handled':\n // The coordinator closed or failed the subscriber itself, so\n // there is nothing left to route. Still counted, so that the\n // route counter sums to the number of subscriptions.\n this.#recordCatchupRoute('sqlite', 'registration-handled');\n break;\n default:\n unreachable(registration);\n }\n }\n }\n // Register the cleanup now that `cleanupSubscriber` points at the real\n // cleanup and the subscriber is registered. If `downstream` was already\n // cancelled (e.g. a client disconnect during the catchup await above),\n // `addCloseHandler` runs the cleanup immediately, and so it must not be\n // called until cleanupSubscriber is properly set up.\n downstream.addCloseHandler(() => cleanupSubscriber());\n\n // Any snapshot reservation held by this task can be closed now that\n // it is subscribed to the change stream.\n this.#reservations?.close(ctx.taskID);\n return downstream;\n }\n\n async startSnapshotReservation(\n taskID: string,\n ): Promise<Source<SnapshotMessage>> {\n if (!this.#reservations) {\n throw new Error('backups are not configured');\n }\n const downstream = this.#reservations.open(taskID);\n\n try {\n // Wait for an in-flight SQLite purge batch before reading and\n // advertising the reservation's bounds.\n await (this.#purgeScheduler?.pause(taskID) ?? promiseVoid);\n // A concurrent retry for this task may have superseded and cancelled\n // this reservation while its purge pause was settling. Only the current\n // owner may replace the task's source pin.\n if (!this.#reservations.isCurrent(taskID, downstream)) {\n return downstream;\n }\n // Pin after the purge pause has settled, so the SQLite minimum captured\n // by the router cannot move before it is advertised. #confirmReservations\n // skips an unpinned task while this await is in flight.\n this.#readRouter?.pin(taskID);\n // If a backup has been confirmed, immediately confirm the reservation.\n await this.#confirmReservations();\n return downstream;\n } catch (e) {\n // Cancel the reservation this call opened, not whatever currently\n // holds the task's slot: an overlapping retry for the same taskID may\n // have already replaced it, and a by-taskID close would tear down the\n // replacement and release its purge pause while it is advertising\n // snapshot bounds. cancel() routes through the subscription's cleanup,\n // which closes the reservation only if this instance still owns it.\n downstream.cancel();\n throw e;\n }\n }\n\n trackBackupWatermark(watermark: string) {\n this.#backupWatermark = watermark;\n this.#acker.trackBackup(watermark);\n // The durable backup floor is an independent input to each change-log\n // implementation. SQLite receives it even when the PG log has already\n // reached this watermark.\n this.#requestSQLitePurge('deferred');\n this.#maybeSchedulePGPurge();\n this.#maybeScheduleSQLitePurge();\n\n // Confirm any waiting reservations now that a backup has been confirmed.\n // Note that this is asynchronous and best effort; if it fails, the watermark\n // is still \"tracked\" and the confirmation will be retried on the next backup.\n void this.#confirmReservations().catch(e =>\n this.#lc.warn?.(`error confirming snapshot reservation`, e),\n );\n }\n\n async #confirmReservations() {\n const backupWatermark = this.#backupWatermark;\n const reservations = this.#reservations;\n if (\n backupWatermark === undefined ||\n !reservations?.confirmationsRequired()\n ) {\n return;\n }\n\n // Resolve PG bounds, when enabled, before touching any pin. Everything\n // below runs to completion without awaiting, which keeps a reservation's\n // advertised bounds and its pin in agreement: a concurrent /snapshot\n // retry for the same task replaces the reservation and re-pins it, and\n // confirming that replacement with the previous pin's bounds is exactly\n // the mismatch pinning exists to prevent.\n const pgState = this.#pgChangeLogEnabled\n ? await this.#getChangeLogState()\n : undefined;\n for (const taskID of reservations.unconfirmedTaskIDs()) {\n let route = this.#readRouter?.peek(taskID);\n // startSnapshotReservation pins only after an in-flight purge has\n // completed. A task with no pin yet is confirmed by that path instead.\n if (this.#readRouter && route === undefined) {\n continue;\n }\n\n // A transiently unavailable or broken SQLite log normally pins the\n // reservation to PG. With PG disabled, re-evaluate that choice on each\n // backup notification and keep the reservation pending until SQLite can\n // supply bounds.\n if (!this.#pgChangeLogEnabled && route?.source === 'pg') {\n this.#readRouter?.release(taskID);\n route = this.#readRouter?.pin(taskID);\n if (route?.source === 'pg') {\n if (reservations.noteConfirmationDelayed(taskID)) {\n this.#reservationConfirmDelays.add(1);\n }\n this.#lc.warn?.(\n `delaying snapshot reservation for ${taskID}: SQLite change log ` +\n `is unavailable and the PG change log is disabled`,\n );\n continue;\n }\n }\n\n if (route?.source === 'sqlite') {\n const coverage = must(\n route.coverage,\n 'a pinned SQLite route must carry its covered range',\n );\n if (\n this.#pgChangeLogEnabled &&\n coverage.minWatermark > backupWatermark\n ) {\n // A log seeded after this backup, most often. Holding the\n // reservation until a backup reaches the log's minimum would stall\n // a follower that PG can serve now, so move it -- pin included.\n this.#lc.info?.(\n `demoting ${taskID} to PG catchup: SQLite change-log minimum ` +\n `${coverage.minWatermark} is later than backupWatermark ` +\n backupWatermark,\n );\n this.#reservationDemotions.add(1);\n route = must(this.#readRouter).demote(taskID);\n }\n }\n\n const source = route?.source ?? 'pg';\n let minWatermark: string;\n if (route?.source === 'sqlite') {\n // Demotion above already moved every SQLite route the backup is\n // outside of, so this one covers it and confirms below.\n minWatermark = must(\n route.coverage,\n 'a pinned SQLite route must carry its covered range',\n ).minWatermark;\n } else {\n minWatermark = must(\n pgState,\n 'a PG reservation route requires the PG change log',\n ).minWatermark;\n }\n\n if (minWatermark <= backupWatermark) {\n reservations.confirmFor(\n taskID,\n this.#replicaVersion,\n backupWatermark,\n source,\n );\n } else {\n // PG cannot catch a restored replica up from this backup yet. Keep\n // the reservation pending until a later backup moves the durable\n // watermark into its covered range.\n if (reservations.noteConfirmationDelayed(taskID)) {\n this.#reservationConfirmDelays.add(1);\n }\n this.#lc.error?.(\n `${source} change-log minWatermark ${minWatermark} is later than ` +\n `backupWatermark ${backupWatermark}. Delaying confirmation of ` +\n `snapshot reservation until next backup.`,\n );\n }\n }\n }\n\n #maybeSchedulePGPurge(): void {\n const backupWatermark = this.#backupWatermark;\n if (\n !this.#pgChangeLogEnabled ||\n this.#pgPurgeScheduled ||\n this.#pgPurgeRunning ||\n backupWatermark === undefined ||\n this.#pgPurgedWatermark >= backupWatermark\n ) {\n return;\n }\n this.#pgPurgeScheduled = true;\n this.#state.setTimeout(() => {\n this.#pgPurgeScheduled = false;\n this.#pgPurgeRunning = true;\n return this.#purgePGChangeLog().finally(() => {\n this.#pgPurgeRunning = false;\n this.#maybeSchedulePGPurge();\n });\n }, CLEANUP_DELAY_MS);\n }\n\n #maybeScheduleSQLitePurge(): void {\n if (\n this.#sqlitePurgeScheduled ||\n this.#sqlitePurgeRunning ||\n this.#backupWatermark === undefined ||\n this.#sqlitePurgeContinuation === undefined\n ) {\n return;\n }\n const delay =\n this.#sqlitePurgeContinuation === 'immediate' ? 0 : CLEANUP_DELAY_MS;\n this.#sqlitePurgeScheduled = true;\n this.#state.setTimeout(() => {\n this.#sqlitePurgeScheduled = false;\n this.#sqlitePurgeRunning = true;\n return this.#purgeSQLiteChangeLog().finally(() => {\n this.#sqlitePurgeRunning = false;\n this.#maybeScheduleSQLitePurge();\n });\n }, delay);\n }\n\n async #getChangeLogState(): Promise<{\n replicaVersion: string;\n minWatermark: string;\n }> {\n const minWatermark = await this.#storer.getMinWatermarkForCatchup();\n if (!minWatermark) {\n this.#lc.warn?.(\n `Unexpected empty changeLog. Resync if \"Local replica watermark\" errors arise`,\n );\n }\n return {\n replicaVersion: this.#replicaVersion,\n minWatermark: minWatermark ?? this.#replicaVersion,\n };\n }\n\n #getCleanupFloor(): {\n backupWatermark: string;\n purgeWatermark: string;\n current: string[];\n } {\n const backupWatermark = this.#backupWatermark;\n assert(\n backupWatermark !== undefined,\n 'cleanup cannot run without a backup watermark',\n );\n const current = [\n ...this.#forwarder.getAcks(),\n ...(this.#reservations?.getReservedWatermarks() ?? []),\n ];\n // The cleanup delay above is the grace period for disconnected\n // subscribers to reconnect and expose their ACKs. Once it expires, an\n // empty set places no additional constraint on the confirmed backup\n // watermark and must not pin either change log indefinitely.\n return {\n backupWatermark,\n purgeWatermark: min(backupWatermark, ...current),\n current,\n };\n }\n\n async #purgePGChangeLog(): Promise<void> {\n try {\n const {backupWatermark, purgeWatermark, current} =\n this.#getCleanupFloor();\n if (purgeWatermark < backupWatermark) {\n if (this.#loggedBehindWatermark !== purgeWatermark) {\n this.#loggedBehindWatermark = purgeWatermark;\n this.#lc.info?.(\n `At least one client is behind backup ${backupWatermark}`,\n {watermarks: current},\n );\n }\n } else {\n this.#loggedBehindWatermark = undefined;\n }\n if (purgeWatermark <= this.#pgPurgedWatermark) {\n return;\n }\n this.#lc.info?.(`Purging PG changes before ${purgeWatermark} ...`);\n const start = performance.now();\n const deleted = await this.#storer.purgeRecordsBefore(purgeWatermark);\n const elapsed = (performance.now() - start).toFixed(2);\n this.#lc.info?.(\n `Purged ${deleted} PG changes before ${purgeWatermark} (${elapsed} ms)`,\n );\n this.#pgPurgedWatermark = purgeWatermark;\n } catch (e) {\n this.#lc.warn?.(`error purging the PG change log`, e);\n }\n }\n\n async #purgeSQLiteChangeLog(): Promise<void> {\n const scheduler = this.#purgeScheduler;\n if (!scheduler) {\n return;\n }\n try {\n const {backupWatermark, purgeWatermark} = this.#getCleanupFloor();\n // Consume the request before starting. A backup notification that\n // arrives during this pass records another request, which is merged with\n // the continuation returned by this pass rather than being overwritten.\n this.#sqlitePurgeContinuation = undefined;\n if (purgeWatermark < backupWatermark) {\n // Live constraint changes are not edge-triggered. Keep evaluating the\n // floor until it reaches the durable backup watermark, independently\n // of whether the PG implementation still exists.\n this.#requestSQLitePurge('deferred');\n }\n const result = await scheduler.purge(purgeWatermark);\n this.#requestSQLitePurge(result.continuation);\n } catch (e) {\n // purge() is fail-soft, but retain the coordinator's retry if an\n // unexpected caller-boundary failure escapes it.\n this.#requestSQLitePurge('deferred');\n this.#lc.warn?.(`error purging the SQLite change log`, e);\n }\n }\n\n #requestSQLitePurge(continuation: PurgeContinuation | undefined): void {\n if (!this.#purgeScheduler || continuation === undefined) {\n return;\n }\n if (\n continuation === 'immediate' ||\n this.#sqlitePurgeContinuation === undefined\n ) {\n this.#sqlitePurgeContinuation = continuation;\n }\n }\n\n async stop(err?: unknown) {\n this.#state.stop(this.#lc, err);\n this.#stream?.changes.cancel();\n this.#purgeScheduler?.stop();\n this.#comparator?.stop();\n this.#sqliteCatchup?.close();\n this.#changeLogWriter?.close();\n await Promise.allSettled([this.#storer.stop(), this.#source.stop()]);\n }\n\n #recordForwardedTransactionBoundary(\n type: ChangeStreamData[0],\n watermark: string,\n ) {\n switch (type) {\n case 'begin':\n assert(\n this.#currentTransactionCompletion === undefined,\n 'forwarded begin while a transaction is already in progress',\n );\n this.#currentTransactionCompletion =\n resolver<ForwardedTransactionCompletion>();\n this.#transactionForwardStartedAt = performance.now();\n break;\n case 'commit': {\n const completion = this.#currentTransactionCompletion;\n assert(completion, 'forwarded commit without a pending transaction');\n this.#recordTransactionForwardDuration();\n this.#lastForwardedCommitWatermark = watermark;\n this.#currentTransactionCompletion = undefined;\n completion.resolve({kind: 'committed', watermark});\n break;\n }\n case 'rollback': {\n const completion = this.#currentTransactionCompletion;\n assert(completion, 'forwarded rollback without a pending transaction');\n const committed = this.#lastForwardedCommitWatermark;\n assert(committed, 'last forwarded commit watermark is not initialized');\n this.#transactionForwardStartedAt = undefined;\n this.#currentTransactionCompletion = undefined;\n completion.resolve({kind: 'rolled-back', watermark: committed});\n break;\n }\n }\n }\n\n #recordTransactionForwardDuration() {\n const startedAt = this.#transactionForwardStartedAt;\n this.#transactionForwardStartedAt = undefined;\n if (startedAt !== undefined) {\n this.#transactionForwardDuration.recordMs(performance.now() - startedAt, {\n sqlite_change_log: this.#changeLogWriter?.enabled ? 'on' : 'off',\n });\n }\n }\n\n /**\n * Closes and forgets the cached catchup coordinator, so that a later\n * subscription re-opens the log from scratch rather than reading a handle\n * whose file is gone.\n */\n #closeSQLiteCatchup() {\n this.#sqliteCatchup?.close();\n this.#sqliteCatchup = undefined;\n }\n\n #captureRequiredHead(): string | Promise<string> {\n const committed = this.#lastForwardedCommitWatermark;\n assert(committed, 'last forwarded commit watermark is not initialized');\n return (\n this.#currentTransactionCompletion?.promise.then(\n completion => completion.watermark,\n ) ?? committed\n );\n }\n\n #selectSQLiteCatchup(\n lc: LogContext,\n ctx: SubscriberContext,\n ): SQLiteCatchupDecision | undefined {\n const opts = this.#sqliteCatchupOptions;\n if (this.#lastForwardedCommitWatermark === undefined || !opts) {\n return this.#fallbackOrReject(\n 'not-ready',\n 'SQLite catchup is not ready and the PG change log is disabled',\n );\n }\n // While the PG change log is available, backup subscribers retain its\n // existing recovery policy and cannot be moved by a canary selector.\n // Once PG is disabled, the backup replicator must use SQLite catchup: it\n // advances the canonical replica that Litestream backs up.\n if (ctx.mode === 'backup' && this.#pgChangeLogEnabled) {\n lc.info?.(`not serving backup subscriber ${ctx.id} from SQLite catchup`);\n return this.#fallbackOrReject(\n 'ineligible-mode',\n 'backup subscribers retain the PG change-log recovery policy',\n );\n }\n\n let route: ChangeLogReadRoute | undefined;\n if (this.#readRouter) {\n route = this.#readRouter.consume(ctx.taskID);\n if (route.source === 'pg') {\n if (route.reason === 'cold-log') {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: SQLite change log is still warming`,\n {sqliteChangeLogCoverage: route.coverage},\n );\n } else if (route.reason === 'log-unavailable') {\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `${opts.changeLogFile} is unavailable or incompatible`,\n );\n } else {\n lc.debug?.(\n `serving ${ctx.id} from PG catchup: SQLite route ${route.reason}`,\n ...(route.coverage\n ? [{sqliteChangeLogCoverage: route.coverage}]\n : []),\n );\n }\n return this.#fallbackOrReject(\n route.reason,\n `SQLite catchup route ${route.reason} is unavailable and the PG ` +\n `change log is disabled`,\n );\n }\n const coverage = route.coverage;\n assert(coverage, 'a SQLite route must carry its covered range');\n if (ctx.watermark < coverage.minWatermark) {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: subscriber watermark ` +\n `${ctx.watermark} is below the SQLite change-log minimum ` +\n coverage.minWatermark,\n {sqliteChangeLogCoverage: coverage},\n );\n return this.#fallbackOrReject(\n 'watermark-uncovered',\n `earliest supported watermark is ${coverage.minWatermark} ` +\n `(requested ${ctx.watermark})`,\n true,\n );\n }\n }\n\n // `shouldUse` remains as a test hook and an optional extra policy gate.\n // Production selection comes from #readRouter.\n if (!this.#readRouter && !opts.shouldUse?.(ctx)) {\n return this.#fallbackOrReject(\n 'selector',\n 'SQLite catchup was not selected and the PG change log is disabled',\n );\n }\n if (this.#readRouter && opts.shouldUse && !opts.shouldUse(ctx)) {\n return this.#fallbackOrReject(\n 'selector',\n 'SQLite catchup was not selected and the PG change log is disabled',\n );\n }\n\n const catchup =\n this.#sqliteCatchup ?? this.#openSQLiteCatchup(lc, opts, ctx);\n if (catchup) {\n return {\n kind: 'selected',\n catchup,\n reason: route?.reason ?? 'selector',\n coverage: route?.coverage,\n // Legacy focused-test selection has no warm classification.\n logWarm:\n route === undefined ? undefined : route.reason !== 'selected-cold',\n };\n } else {\n return this.#fallbackOrReject(\n 'log-unavailable',\n 'SQLite change log is unavailable and the PG change log is disabled',\n );\n }\n }\n\n #fallbackOrReject(\n reason: string,\n message: string,\n terminal = false,\n ): SQLiteCatchupRejection | undefined {\n this.#recordCatchupRoute(this.#pgChangeLogEnabled ? 'pg' : 'none', reason);\n return this.#pgChangeLogEnabled\n ? undefined\n : {kind: 'rejected', message, terminal};\n }\n\n #recordCatchupRoute(source: 'pg' | 'sqlite' | 'none', reason: string): void {\n this.#catchupRoutes.add(1, {source, reason});\n }\n\n /**\n * Opens the change log and, if it can serve, wraps it in the coordinator that\n * subsequent subscriptions reuse.\n *\n * The log may not exist yet -- the writer creates it at the stream loop's\n * first reconcile, and deletes it when it fails soft -- or may exist without\n * content, or may be unreadable. None of those can be allowed to fail a\n * subscription:\n * this is the last point at which PG catchup can be selected when it is\n * enabled -- past `Forwarder.add()` the subscriber is committed to SQLite --\n * so each declines here instead. With PG disabled, the caller ends the\n * subscription so that it retries. Neither the failure nor the reader is\n * retained, so a later subscription retries from scratch.\n */\n #openSQLiteCatchup(\n lc: LogContext,\n opts: SQLiteCatchupOptions,\n ctx: SubscriberContext,\n ): SQLiteChangeLogCatchup | undefined {\n let reader: SQLiteChangeLogReader | undefined;\n try {\n reader = new SQLiteChangeLogReader(this.#lc, opts.changeLogFile);\n // `plan()` doubles as the readiness check: it reports `not-ready` when\n // the writer has not created or seeded the stream table. The barrier\n // cannot wait its way out of that, since a log with no head can never\n // reach the required one.\n if (reader.plan(ctx.watermark).kind === 'not-ready') {\n reader.close();\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `${opts.changeLogFile} has no changes to serve yet`,\n );\n return undefined;\n }\n } catch (e) {\n // An absent file is the common case, since a readonly handle cannot\n // create one. A corrupt or truncated file lands here too, and is equally\n // not a reason to fail the subscription.\n reader?.close();\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `cannot read ${opts.changeLogFile}`,\n e,\n );\n return undefined;\n }\n this.#changeLogUnavailableSince = undefined;\n // Readiness is checked once, on the way to the cached coordinator: a log\n // with content keeps it. Purging preserves the latest transaction as a\n // catchup boundary, and `reconcileChangeLog` reseeds inside a single\n // transaction, so a reader never observes an emptied log.\n this.#sqliteCatchup = new SQLiteChangeLogCatchup(\n this.#lc,\n this.#forwarder,\n reader,\n {\n batchSize: opts.readBatchRows,\n barrierTimeoutMs: opts.barrierTimeoutMs,\n barrierPollIntervalMs: opts.barrierPollIntervalMs,\n cleanupGuard: opts.cleanupGuard,\n onFailure: failure => {\n this.#lc.warn?.(\n `temporarily disabling SQLite catchup after ${failure}`,\n );\n this.#readRouter?.trip();\n },\n },\n );\n return this.#sqliteCatchup;\n }\n\n /**\n * Reports falling back to PG catchup, tracking how long the log has been\n * unavailable so that a subscription that merely arrived before the writer's\n * first reconcile does not look like an incident.\n */\n #declineSQLiteCatchup(\n lc: LogContext,\n ctx: SubscriberContext,\n opts: SQLiteCatchupOptions,\n reason: string,\n error?: unknown,\n ): void {\n const now = Date.now();\n this.#changeLogUnavailableSince ??= now;\n const unavailableMs = now - this.#changeLogUnavailableSince;\n const threshold =\n opts.notReadyWarnThresholdMs ??\n DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS;\n lc[unavailableMs >= threshold ? 'warn' : 'debug']?.(\n (this.#pgChangeLogEnabled\n ? `serving ${ctx.id} from PG catchup: `\n : `cannot serve ${ctx.id} from SQLite catchup: `) +\n `${reason} ` +\n `(unavailable for ${unavailableMs} ms)`,\n ...(error === undefined ? [] : [error]),\n );\n }\n}\n\ntype ForwardedTransactionCompletion =\n | {kind: 'committed'; watermark: string}\n | {kind: 'rolled-back'; watermark: string};\n\ntype SQLiteCatchupSelection = {\n readonly kind: 'selected';\n readonly catchup: SQLiteChangeLogCatchup;\n readonly reason: string;\n readonly coverage: SQLiteChangeLogCoverage | undefined;\n /**\n * Whether the router classified the log as warm for this subscriber, or\n * undefined when selection did not run through the router (focused tests).\n */\n readonly logWarm: boolean | undefined;\n};\n\ntype SQLiteCatchupRejection = {\n readonly kind: 'rejected';\n readonly message: string;\n /** A terminal rejection forces the view-syncer to restore a fresh replica. */\n readonly terminal: boolean;\n};\n\ntype SQLiteCatchupDecision = SQLiteCatchupSelection | SQLiteCatchupRejection;\n\n// The delay between receiving an initial, backup-based watermark\n// and performing a check of whether to purge records before it.\n// This delay should be long enough to handle situations like the following:\n//\n// 1. `litestream restore` downloads a backup for the `replication-manager`\n// 2. `replication-manager` starts up and runs this `change-streamer`\n// 3. `zero-cache`s that are running on a different replica connect to this\n// `change-streamer` after exponential backoff retries.\n//\n// It is possible for a `zero-cache`[3] to be behind the backup restored [1].\n// This cleanup delay (30 seconds) is thus set to be a value comfortably\n// longer than the max delay for exponential backoff (10 seconds) in\n// `services/running-state.ts`. This allows the `zero-cache` [3] to reconnect\n// so that the `change-streamer` can track its progress and know when it has\n// surpassed the initial watermark of the backup [1].\nconst CLEANUP_DELAY_MS = DEFAULT_MAX_RETRY_DELAY_MS * 3;\n\nconst ROLLBACK_JSON = JSON.stringify([\n 'rollback',\n {tag: 'rollback'},\n] satisfies Rollback);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0MA,eAAsB,mBACpB,IACA,OACA,QACA,kBACA,mBACA,UACA,cACA,4BACA,mBACA,cACA,WACA,WACA,MACA,eAAe,YACiB;CAChC,MAAM,wBACJ,IACA,UACA,mBACA,OACA,WACA,aAAa,KAAA,GACb,cACA,KAAK,kBACP;CAOA,MAAM,oBACJ,iBACA,QAEA,SACE,KACE;EAAC;EAAK,YAAY,GAAE,qBAAqB,gBAAe;CAAC,GAKzD,SAAS,OACX,CACF;CAEF,MAAM,EAAC,mBAAkB;CACzB,OAAO,IAAI,mBACT,IACA,OACA,QACA,kBACA,mBACA,kBACA,gBACA,cACA,4BACA,cACA,WACA,WACA,MACA,YACF;AACF;AAEA,IAAM,8CAA8C;AAQpD,IAAM,mDAAmD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4IzD,IAAM,qBAAN,MAA0D;CACxD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;;CAGA;CAUA,WAAoB,SAAS;CAE7B,aAAsB,mBACpB,eACA,gBACA,kCACF;CACA,iBAA0B,mBACxB,eACA,WACA,qDACF;CAIA,8BAAuC,4BACrC,eACA,gCACA,yIAEF;CACA,iBAA0B,mBACxB,eACA,oCACA,sEACF;CACA,wBAAiC,mBAC/B,eACA,2CACA,qPAIF;CACA,4BAAqC,mBACnC,eACA,gDACA,2LAGF;CAEA;CACA,+BAA+B;CAC/B;CACA,qBAA6B;;;;;;;CAO7B;CACA;CACA;CAGA,oBAAoB;CACpB,kBAAkB;CAClB,wBAAwB;CACxB,sBAAsB;CACtB;CACA;CACA;CACA;CACA;CACA;CAIA,YACE,IACA,OACA,QACA,kBACA,mBACA,kBACA,gBACA,QACA,4BACA,cACA,kBACA,WACA,MACA,eAAe,YACf;EACA,KA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"}
|
|
1
|
+
{"version":3,"file":"change-streamer-service.js","names":["#lc","#shard","#changeDBProvider","#replicaVersion","#source","#storer","#pgChangeLogEnabled","#forwarder","#reservations","#replicationStatusPublisher","#sqliteCatchupOptions","#changeLogWriter","#purgeScheduler","#comparator","#acker","#initializer","#readRouter","#autoReset","#state","#backfills","#serving","#txCounter","#changeCounter","#transactionForwardDuration","#catchupRoutes","#reservationDemotions","#reservationConfirmDelays","#sqliteCatchup","#closeSQLiteCatchup","#purgeLock","#latestStatus","#latestLagReportCommitTimeMs","#lastForwardedCommitWatermark","#stream","#mayRestart","#handleControlMessage","#recordForwardedTransactionBoundary","#selectSQLiteCatchup","#captureRequiredHead","#recordCatchupRoute","#confirmReservations","#backupWatermark","#requestSQLitePurge","#maybeSchedulePGPurge","#maybeScheduleSQLitePurge","#getChangeLogState","#pgPurgeScheduled","#pgPurgeRunning","#pgPurgedWatermark","#purgePGChangeLog","#sqlitePurgeScheduled","#sqlitePurgeRunning","#sqlitePurgeContinuation","#purgeSQLiteChangeLog","#getCleanupFloor","#loggedBehindWatermark","#currentTransactionCompletion","#transactionForwardStartedAt","#recordTransactionForwardDuration","#fallbackOrReject","#declineSQLiteCatchup","#openSQLiteCatchup","#changeLogUnavailableSince"],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-service.ts"],"sourcesContent":["import {getDefaultHighWaterMark} from 'node:stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {defu} from 'defu';\nimport postgres, {type Options, type PostgresType} from 'postgres';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseOrAbort} from '../../../../shared/src/promise-race.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {publishCriticalEvent} from '../../observability/events.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {min} from '../../types/lexi-version.ts';\nimport type {PostgresDB} from '../../types/pg.ts';\nimport type {ShardID} from '../../types/shards.ts';\nimport type {PreSerialized, Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {\n ChangeSource,\n ChangeStream,\n} from '../change-source/change-source.ts';\nimport {\n type ChangeStreamControl,\n type ChangeStreamData,\n type Rollback,\n} from '../change-source/protocol/current/downstream.ts';\nimport type {LitestreamVersion} from '../litestream/metrics.ts';\nimport {\n publishReplicationError,\n replicationStatusError,\n type ReplicationStatusPublisher,\n} from '../replicator/replication-status.ts';\nimport type {SubscriptionState} from '../replicator/schema/replication-state.ts';\nimport {\n DEFAULT_MAX_RETRY_DELAY_MS,\n RunningState,\n UnrecoverableError,\n} from '../running-state.ts';\nimport {BackfillStateTracker} from './backfill-state-tracker.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport {serializeChangeStreamDataWithChange} from './change-log-codec.ts';\nimport {\n ChangeLogInitializer,\n replicaInitializationSource,\n} from './change-log-initializer.ts';\nimport {\n type ChangeStreamerService,\n type Status,\n type SubscriberContext,\n type WatermarkedChange,\n} from './change-streamer.ts';\nimport * as ErrorType from './error-type-enum.ts';\nimport {Forwarder} from './forwarder.ts';\nimport {\n AutoResetSignal,\n ensureReplicationConfig,\n markResetRequired,\n} from './schema/tables.ts';\nimport type {SnapshotMessage} from './snapshot-message.ts';\nimport {SnapshotReservations} from './snapshot-reservations.ts';\nimport {\n SQLiteChangeLogCatchup,\n type SQLiteChangeLogCleanupGuard,\n} from './sqlite-change-log-catchup.ts';\nimport {\n SQLiteChangeLogComparator,\n type SQLiteChangeLogCompareOptions,\n} from './sqlite-change-log-comparator.ts';\nimport {\n SQLiteChangeLogPurgeScheduler,\n type PurgeContinuation,\n type SQLiteChangeLogPurgeSchedulerOptions,\n} from './sqlite-change-log-purge-scheduler.ts';\nimport {\n inspectSQLiteChangeLog,\n SQLiteChangeLogReadRouter,\n type ChangeLogReadRoute,\n type SQLiteChangeLogCoverage,\n} from './sqlite-change-log-read-router.ts';\nimport {SQLiteChangeLogReader} from './sqlite-change-log-reader.ts';\nimport {\n SQLiteChangeLogWriter,\n type SQLiteChangeLogWriterOptions,\n} from './sqlite-change-log-writer.ts';\nimport type {PostgresDBProvider} from './storer.ts';\nimport {\n Storer,\n type PurgeLock,\n type TuningOptions as StorerOptions,\n} from './storer.ts';\nimport {Subscriber} from './subscriber.ts';\nimport {UpstreamAcker} from './upstream-acker.ts';\n\nexport type BackupConfig = {\n backupURL: string;\n litestreamVersion: LitestreamVersion;\n /**\n * The replica being backed up. Its size is sent to subscribers as an\n * estimate of the size of their restored replica.\n */\n replicaFile?: string | undefined;\n};\n\nexport type SQLiteCatchupOptions = {\n /** The change-log database, i.e. `changeLogFileName(replicaFile)`. */\n changeLogFile: string;\n readBatchRows: number;\n barrierTimeoutMs: number;\n /**\n * Backstop for the change-log writer's ACK, which is what normally releases\n * the barrier. Defaults to the catchup coordinator's interval.\n */\n barrierPollIntervalMs?: number | undefined;\n /**\n * Optional policy hook used by focused tests. Production canary selection is\n * supplied by `sqliteChangeLogServe` and is stable by shard plus task ID.\n * Backup subscribers are rejected before either selector is invoked.\n */\n shouldUse?: ((ctx: SubscriberContext) => boolean) | undefined;\n /**\n * Overrides the purge scheduler's guard, for tests. When absent, the\n * service supplies the scheduler's real writer-serialized guard, or the\n * catchup's no-op default when purging is not configured.\n */\n cleanupGuard?: SQLiteChangeLogCleanupGuard | undefined;\n /**\n * How long the change log may be unavailable before declining to serve from\n * it is reported as a warning rather than a debug line. Defaults to\n * {@link DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS}.\n */\n notReadyWarnThresholdMs?: number | undefined;\n};\n\nexport type SQLiteChangeLogServeOptions = {\n /** Stable percentage of eligible serving tasks routed to SQLite. */\n readPercent: number;\n /**\n * Stable percentage of eligible serving tasks routed to a log that has not\n * yet aged through {@link retentionMs}. Zero keeps a reseeded log on PG for\n * that whole window; above zero it serves that share of tasks, and a\n * reservation it cannot cover is demoted rather than held pending.\n * Defaults to zero.\n */\n coldReadPercent?: number | undefined;\n /** The warm-up window a reseeded log ages through. */\n retentionMs: number;\n /** Injectable for deterministic breaker tests. */\n failureCooldownMs?: number | undefined;\n /** Injectable for deterministic warm-up and breaker tests. */\n now?: (() => number) | undefined;\n /** Injectable readiness inspection for service-level tests. */\n inspect?: (() => SQLiteChangeLogCoverage | undefined) | undefined;\n};\n\nexport type TuningOptions = StorerOptions & {\n /**\n * Keeps the legacy Postgres change log on the initialization, persistence,\n * catchup, purge, and ACK paths. Disable only after SQLite and the replica\n * backup are authoritative for all of those responsibilities.\n */\n pgChangeLogEnabled: boolean;\n /**\n * Whether the PG change log's head was behind the position of the\n * replication slot from which replication resumes, in which case the\n * change log is re-initialized from the (restored) replica if it does not\n * contain all of the replica's changes (see `PgChangeLogPurgeLocker`).\n */\n pgChangeLogBehindSlot?: boolean | undefined;\n flowControlConsensusTimeoutProportion: number;\n flowControlSlowSubscriberGracePeriodMs?: number | undefined;\n sqliteCatchup?: SQLiteCatchupOptions | undefined;\n /**\n * Supplied when `sqliteChangeLogMode != off`, i.e. this is the gate on the\n * change-log writer. Absent, nothing writes the log.\n */\n sqliteChangeLogWriter?:\n | Omit<\n SQLiteChangeLogWriterOptions,\n 'onCommit' | 'onDisabled' | 'onRebuilt'\n >\n | undefined;\n /**\n * Also supplied when `sqliteChangeLogMode != off`, and deliberately not\n * gated on the read path: in `write` mode no reader ever opens, and this is\n * the configuration the purge scheduler actually ships in. The scheduler\n * runs on the writer's own connection, so with the writer absent (mode\n * `off`) it is never constructed, and a leftover file on disk is left to\n * the replicator's cleanup.\n */\n sqliteChangeLogPurge?: SQLiteChangeLogPurgeSchedulerOptions | undefined;\n /**\n * Supplied in `compare` mode and later modes.\n * `replicaFile` enables the initialization comparison.\n * The other fields configure sampled catchup comparisons.\n * Both checks are advisory, and Postgres remains authoritative.\n * The comparator also requires the writer and catchup options.\n */\n sqliteChangeLogCompare?:\n | (SQLiteChangeLogCompareOptions & {replicaFile: string})\n | undefined;\n /** Supplied only in `serve` mode. A zero percentage keeps every read on PG. */\n sqliteChangeLogServe?: SQLiteChangeLogServeOptions | undefined;\n};\n\n/**\n * Performs initialization and schema migrations to initialize a ChangeStreamerImpl.\n */\nexport async function initializeStreamer(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n changeDB: PostgresDB,\n changeSource: ChangeSource,\n replicationStatusPublisher: ReplicationStatusPublisher,\n subscriptionState: SubscriptionState,\n backupConfig: BackupConfig | null,\n purgeLock: PurgeLock | null,\n autoReset: boolean,\n opts: TuningOptions,\n setTimeoutFn = setTimeout,\n): Promise<ChangeStreamerService> {\n await ensureReplicationConfig(\n lc,\n changeDB,\n subscriptionState,\n shard,\n autoReset,\n purgeLock ?? undefined,\n setTimeoutFn,\n opts.pgChangeLogEnabled,\n );\n\n // Dynamically creates connection pools that the implementation uses to\n // isolate concurrent, long-running transactions. This works around a bug in\n // the postgres.js client where connections get swapped in certain conditions.\n //\n // https://github.com/porsager/postgres/issues/1204\n const changeDBProvider: PostgresDBProvider = (\n applicationName: string,\n max: number,\n ) =>\n postgres(\n defu(\n {max, connection: {['application_name']: applicationName}},\n // ParsedOptions are technically compatible with Options, but happen\n // to not be typed that way. The postgres.js author does an equivalent\n // merge of ParsedOptions and Options here:\n // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/subscribe.js#L13\n changeDB.options as unknown as Options<Record<string, PostgresType>>,\n ),\n ) as PostgresDB;\n\n const {replicaVersion} = subscriptionState;\n return new ChangeStreamerImpl(\n lc,\n shard,\n taskID,\n discoveryAddress,\n discoveryProtocol,\n changeDBProvider,\n replicaVersion,\n changeSource,\n replicationStatusPublisher,\n backupConfig,\n purgeLock,\n autoReset,\n opts,\n setTimeoutFn,\n );\n}\n\nconst REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS = 5000;\n\n// How long the change log may be unavailable before declining to serve from it\n// stops looking like normal startup ordering. Subscriptions can arrive before\n// the stream loop's first reconcile has created and seeded the log, so early\n// declines are expected. Still declining a minute later means the log is not\n// being written at all -- `sqliteChangeLogMode=off`, a writer that has failed\n// soft, or a path mismatch -- which is worth surfacing.\nconst DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS = 60_000;\n\n/**\n * Upstream-agnostic dispatch of messages in a {@link ChangeStreamMessage} to a\n * {@link Forwarder} and {@link Storer} to execute the forward-store-ack\n * procedure described in {@link ChangeStreamer}.\n *\n * ### Subscriber Catchup\n *\n * Connecting clients first need to be \"caught up\" to the current watermark\n * (from stored change log entries) before new entries are forwarded to\n * them. This is non-trivial because the replication stream may be in the\n * middle of a pending streamed Transaction for which some entries have\n * already been forwarded but are not yet committed to the store.\n *\n *\n * ```\n * ------------------------------- - - - - - - - - - - - - - - - - - - -\n * | Historic changes in storage | Pending (streamed) tx | Next tx\n * ------------------------------- - - - - - - - - - - - - - - - - - - -\n * Replication stream\n * > > > > > > > > >\n * ^ ---> required catchup ---> ^\n * Subscriber watermark Subscription begins\n * ```\n *\n * Preemptively buffering the changes of every pending transaction\n * would be wasteful and consume too much memory for large transactions.\n *\n * Instead, the streamer synchronously dispatches changes and subscriptions\n * to the {@link Forwarder} and the {@link Storer} such that the two\n * components are aligned as to where in the stream the subscription started.\n * The two components then coordinate catchup and handoff via the\n * {@link Subscriber} object with the following algorithm:\n *\n * * If the streamer is in the middle of a pending Transaction, the\n * Subscriber is \"queued\" on both the Forwarder and the Storer. In this\n * state, new changes are *not* forwarded to the Subscriber, and catchup\n * is not yet executed.\n * * Once the commit message for the pending Transaction is processed\n * by the Storer, it begins catchup on the Subscriber (with a READONLY\n * snapshot so that it does not block subsequent storage operations).\n * This catchup is thus guaranteed to load the change log entries of\n * that last Transaction.\n * * When the Forwarder processes that same commit message, it moves the\n * Subscriber from the \"queued\" to the \"active\" set of clients such that\n * the Subscriber begins receiving new changes, starting from the next\n * Transaction.\n * * The Subscriber does not forward those changes, however, if its catchup\n * is not complete. Until then, it buffers the changes in memory.\n * * Once catchup is complete, the buffered changes are immediately sent\n * and the Subscriber henceforth forwards changes as they are received.\n *\n * In the (common) case where the streamer is not in the middle of a pending\n * transaction when a subscription begins, the Storer begins catchup\n * immediately and the Forwarder directly adds the Subscriber to its active\n * set. However, the Subscriber still buffers any forwarded messages until\n * its catchup is complete.\n *\n * ### Watermarks and ordering\n *\n * The ChangeStreamerService depends on its {@link ChangeSource} to send\n * changes in contiguous [`begin`, `data` ..., `data`, `commit`] sequences\n * in commit order. This follows Postgres's Logical Replication Protocol\n * Message Flow:\n *\n * https://www.postgresql.org/docs/16/protocol-logical-replication.html#PROTOCOL-LOGICAL-MESSAGES-FLOW\n *\n * > The logical replication protocol sends individual transactions one by one.\n * > This means that all messages between a pair of Begin and Commit messages belong to the same transaction.\n *\n * In order to correctly replay (new) and filter (old) messages to subscribers\n * at different points in the replication stream, these changes must be assigned\n * watermarks such that they preserve the order in which they were received\n * from the ChangeSource.\n *\n * A previous implementation incorrectly derived these watermarks from the Postgres\n * Log Sequence Numbers (LSN) of each message. However, LSNs from concurrent,\n * non-conflicting transactions can overlap, which can result in a `begin` message\n * with an earlier LSN arriving after a `commit` message. For example, the\n * changes for these transactions:\n *\n * ```\n * LSN: 1 2 3 4 5 6 7 8 9 10\n * tx1: begin data data data commit\n * tx2: begin data data data commit\n * ```\n *\n * will arrive as:\n *\n * ```\n * begin1, data2, data4, data6, commit8, begin3, data5, data7, data9, commit10\n * ```\n *\n * Thus, LSN of non-commit messages are not suitable for tracking the sorting\n * order of the replication stream.\n *\n * Instead, the ChangeStreamer uses the following algorithm for deterministic\n * catchup and filtering of changes:\n *\n * * A `commit` message is assigned to a watermark corresponding to its LSN.\n * These are guaranteed to be in commit order by definition.\n *\n * * `begin` and `data` messages are assigned to the watermark of the\n * preceding `commit` (the previous transaction, or the replication\n * slot's starting LSN) plus 1. This guarantees that they will be sorted\n * after the previously commit transaction even if their LSNs came before it.\n * This is referred to as the `preCommitWatermark`.\n *\n * * In the ChangeLog DB, messages have a secondary sort column `pos`, which is\n * the position of the message within its transaction, with the `begin` message\n * starting at `0`. This guarantees that `begin` and `data` messages will be\n * fetched in the original ChangeSource order during catchup.\n *\n * `begin` and `data` messages share the same watermark, but this is sufficient for\n * Subscriber filtering because subscribers only know about the `commit` watermarks\n * exposed in the `Downstream` `Commit` message. The Subscriber object thus compares\n * the internal watermarks of the incoming messages against the commit watermark of\n * the caller, updating the watermark at every `Commit` message that is forwarded.\n *\n * ### Cleanup\n *\n * As mentioned in the {@link ChangeStreamer} documentation: \"the ChangeStreamer\n * uses a combination of [the \"initial\", i.e. backup-derived watermark and] ACK\n * responses from connected subscribers to determine the watermark up\n * to which it is safe to purge old change log entries.\"\n *\n * More concretely:\n *\n * * The `initial`, backup-derived watermark is the earliest to which cleanup\n * should ever happen.\n *\n * * However, it is possible for the replica backup to be *ahead* of a connected\n * subscriber; and if a network error causes that subscriber to retry from its\n * last watermark, the change streamer must support it.\n *\n * Thus, before cleaning up to an `initial` backup-derived watermark, the change\n * streamer first confirms that all connected subscribers have also passed\n * that watermark.\n */\nclass ChangeStreamerImpl implements ChangeStreamerService {\n readonly id: string;\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #changeDBProvider: PostgresDBProvider;\n readonly #replicaVersion: string;\n readonly #source: ChangeSource;\n readonly #storer: Storer;\n readonly #pgChangeLogEnabled: boolean;\n readonly #forwarder: Forwarder;\n readonly #reservations: SnapshotReservations | undefined;\n readonly #replicationStatusPublisher: ReplicationStatusPublisher;\n readonly #sqliteCatchupOptions: SQLiteCatchupOptions | undefined;\n readonly #changeLogWriter: SQLiteChangeLogWriter | undefined;\n readonly #purgeScheduler: SQLiteChangeLogPurgeScheduler | undefined;\n readonly #comparator: SQLiteChangeLogComparator | undefined;\n readonly #acker: UpstreamAcker;\n readonly #initializer: ChangeLogInitializer;\n readonly #readRouter: SQLiteChangeLogReadRouter | undefined;\n\n readonly #autoReset: boolean;\n readonly #state: RunningState;\n\n /** Determines the BackfillRequests with which to (re)start streams. */\n readonly #backfills: BackfillStateTracker;\n\n // Starting the (Postgres) ChangeStream results in killing the previous\n // Postgres subscriber, potentially creating a gap in which the old\n // change-streamer has shut down and the new change-streamer has not yet\n // been recognized as \"healthy\" (and thus does not get any requests).\n //\n // To minimize this gap, delay starting the ChangeStream until the first\n // request from a `serving` replicator, indicating that higher level\n // load-balancing / routing logic has begun routing requests to this task.\n readonly #serving = resolver();\n\n readonly #txCounter = getOrCreateCounter(\n 'replication',\n 'transactions',\n 'Count of replicated transactions',\n );\n readonly #changeCounter = getOrCreateCounter(\n 'replication',\n 'changes',\n 'Count of replicated changes (DML or DDL statements)',\n );\n // The number the SQLite change-log commit lands in. It is labeled by whether\n // the log is being written so that the cost of putting a commit on the\n // forward path is attributable rather than inferred.\n readonly #transactionForwardDuration = getOrCreateLatencyHistogram(\n 'replication',\n 'transaction_forward_duration',\n \"Time from receiving a transaction's `begin` to forwarding its `commit`, \" +\n 'i.e. the change-streamer half of forward-to-subscriber latency.',\n );\n readonly #catchupRoutes = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.catchup_routes',\n 'Catchup subscriptions by selected source and low-cardinality reason.',\n );\n readonly #reservationDemotions = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.reservation_demotions',\n 'Snapshot reservations demoted from SQLite to PG because the change ' +\n 'log could not cover the backup being restored. With PG retired ' +\n 'these followers have no fallback, so this is the rate at which one ' +\n 'would instead have to wait for a later backup.',\n );\n readonly #reservationConfirmDelays = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.reservation_confirm_delays',\n 'Snapshot reservations whose confirmation was deferred because the ' +\n \"selected source's change-log minimum was later than the backup \" +\n 'watermark. Counted once per reservation, by that source.',\n );\n\n #latestStatus: Status;\n #latestLagReportCommitTimeMs = 0;\n #backupWatermark: string | undefined;\n #pgPurgedWatermark: string = '';\n /**\n * The floor last logged as held back by a laggard. The level-triggered\n * retry re-evaluates every {@link CLEANUP_DELAY_MS}; logging only when the\n * blocking floor moves keeps a stuck subscriber from emitting the same\n * line indefinitely.\n */\n #loggedBehindWatermark: string | undefined;\n #sqlitePurgeContinuation: PurgeContinuation | undefined;\n #purgeLock: PurgeLock | null;\n // PG and SQLite intentionally own separate level-triggered loops. Neither\n // waits for, advances, or retries the other.\n #pgPurgeScheduled = false;\n #pgPurgeRunning = false;\n #sqlitePurgeScheduled = false;\n #sqlitePurgeRunning = false;\n #stream: ChangeStream | undefined;\n #sqliteCatchup: SQLiteChangeLogCatchup | undefined;\n #changeLogUnavailableSince: number | undefined;\n #lastForwardedCommitWatermark: string | undefined;\n #transactionForwardStartedAt: number | undefined;\n #currentTransactionCompletion:\n | Resolver<ForwardedTransactionCompletion>\n | undefined;\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n changeDBProvider: PostgresDBProvider,\n replicaVersion: string,\n source: ChangeSource,\n replicationStatusPublisher: ReplicationStatusPublisher,\n backupConfig: BackupConfig | null,\n initialPurgeLock: PurgeLock | null,\n autoReset: boolean,\n opts: TuningOptions,\n setTimeoutFn = setTimeout,\n ) {\n this.id = `change-streamer`;\n this.#lc = lc.withContext('component', 'change-streamer');\n this.#backfills = new BackfillStateTracker(this.#lc);\n this.#shard = shard;\n this.#changeDBProvider = changeDBProvider;\n this.#replicaVersion = replicaVersion;\n this.#source = source;\n this.#pgChangeLogEnabled = opts.pgChangeLogEnabled;\n if (!this.#pgChangeLogEnabled) {\n assert(\n opts.sqliteChangeLogWriter &&\n opts.sqliteChangeLogPurge &&\n opts.sqliteCatchup &&\n opts.sqliteChangeLogServe?.readPercent === 100 &&\n opts.sqliteChangeLogServe.coldReadPercent === 100 &&\n backupConfig?.litestreamVersion === 'v5',\n 'disabling the PG change log requires SQLite serving at 100 percent and a v5 backup',\n );\n assert(\n opts.sqliteChangeLogCompare === undefined,\n 'SQLite change-log comparison requires the PG change log',\n );\n }\n this.#storer = new Storer(\n lc,\n shard,\n taskID,\n discoveryAddress,\n discoveryProtocol,\n changeDBProvider,\n replicaVersion,\n consumed => this.#acker.trackPgChangeLog(consumed[2].watermark),\n err => this.stop(err),\n opts,\n );\n this.#forwarder = new Forwarder(lc, {\n flowControlConsensusTimeoutProportion:\n opts.flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodMs:\n opts.flowControlSlowSubscriberGracePeriodMs,\n });\n const serveOptions = opts.sqliteChangeLogServe;\n const writerOptions = opts.sqliteChangeLogWriter;\n const catchupOptions = opts.sqliteCatchup;\n this.#readRouter =\n serveOptions && writerOptions && catchupOptions\n ? new SQLiteChangeLogReadRouter({\n shard,\n readPercent: serveOptions.readPercent,\n coldReadPercent: serveOptions.coldReadPercent,\n retentionMs: serveOptions.retentionMs,\n failureCooldownMs: serveOptions.failureCooldownMs,\n now: serveOptions.now,\n inspect:\n serveOptions.inspect ??\n (() =>\n inspectSQLiteChangeLog(\n lc,\n catchupOptions.changeLogFile,\n writerOptions.identity,\n )),\n })\n : undefined;\n this.#reservations = backupConfig\n ? new SnapshotReservations(lc, backupConfig, taskID => {\n this.#readRouter?.release(taskID);\n this.#purgeScheduler?.resume(taskID);\n })\n : undefined;\n this.#replicationStatusPublisher = replicationStatusPublisher;\n this.#changeLogWriter = opts.sqliteChangeLogWriter\n ? new SQLiteChangeLogWriter(lc, {\n ...opts.sqliteChangeLogWriter,\n onCommit: watermark => {\n this.#sqliteCatchup?.onChangeLogCommit(watermark);\n // The commit closed the log's transaction, i.e. opened a purge\n // window (§3.3).\n this.#purgeScheduler?.onWriterIdle();\n },\n // Fail-soft deletes the file, and the reader is cached here, so it\n // has to go with it: otherwise it serves an unlinked inode while\n // every new open sees nothing.\n onDisabled: () => {\n // The writer stays disabled and the file stays absent for the life\n // of this process, so unlike a transient read/barrier failure this\n // breaker never expires.\n this.#readRouter?.trip(true);\n this.#closeSQLiteCatchup();\n this.#comparator?.stop();\n },\n onRebuilt: () => {\n this.#closeSQLiteCatchup();\n // Invalidate cycles that can still read the replaced file.\n this.#comparator?.invalidate();\n },\n })\n : undefined;\n // The purge scheduler runs on the writer's own connection, which is also\n // its gate: no writer (mode `off`) means no scheduler, and a writer that\n // has not created the file yet -- or failed soft and deleted it -- makes\n // cycles skip rather than fail, so a late-appearing file is picked up\n // without a restart.\n this.#purgeScheduler =\n opts.sqliteChangeLogPurge && this.#changeLogWriter\n ? new SQLiteChangeLogPurgeScheduler(\n lc,\n () => this.#changeLogWriter?.connection,\n () => this.#forwarder.getAcks(),\n opts.sqliteChangeLogPurge,\n )\n : undefined;\n this.#acker = new UpstreamAcker({\n trackPgChangeLog: this.#pgChangeLogEnabled,\n trackBackup: backupConfig?.litestreamVersion === 'v5',\n });\n const replicaFileForInitialization =\n opts.sqliteChangeLogCompare?.replicaFile ??\n (!this.#pgChangeLogEnabled\n ? opts.sqliteChangeLogWriter?.replicaFile\n : undefined);\n // A Postgres change log whose head is behind the replication slot may be\n // missing transactions that the slot will not stream (e.g. after running\n // in high-availability mode), in which case it is re-initialized from the\n // restored replica.\n const replicaFileForReinitialization =\n this.#pgChangeLogEnabled && opts.pgChangeLogBehindSlot\n ? backupConfig?.replicaFile\n : undefined;\n const replicaFile =\n replicaFileForInitialization ?? replicaFileForReinitialization;\n const replicaSource = replicaFile\n ? replicaInitializationSource(lc, replicaFile)\n : undefined;\n this.#initializer = new ChangeLogInitializer(\n lc,\n {\n initFromPgChangeLog: this.#pgChangeLogEnabled,\n initFromReplica: replicaFileForInitialization !== undefined,\n },\n {\n pgChangeLog: () =>\n this.#storer.getStartStreamInitializationParameters(),\n // Only reached when `initFromReplica` is set, i.e. when the option\n // that supplies the file is present.\n replica: () => must(replicaSource)(),\n // Use the Postgres resume point when it is present. Otherwise, let the\n // SQLite change log supply its own resume point.\n reconcileChangeLog: (resumeFrom, seed) =>\n resumeFrom\n ? this.#changeLogWriter?.reconcile(resumeFrom)\n : this.#changeLogWriter?.reconcileFromLog(seed),\n changeLog: () => this.#changeLogWriter?.connection,\n reinitializePgChangeLog: replicaFileForReinitialization\n ? params => this.#storer.reinitializeFromReplica(params)\n : undefined,\n },\n );\n this.#sqliteCatchupOptions = opts.sqliteCatchup\n ? {\n ...opts.sqliteCatchup,\n // The real cleanup guard, in place of the catchup's no-op default:\n // registration and purge batches now serialize on one mutex.\n cleanupGuard:\n opts.sqliteCatchup.cleanupGuard ??\n this.#purgeScheduler?.cleanupGuard,\n }\n : undefined;\n // Compare mode requires the writer and catchup configuration.\n this.#comparator =\n this.#pgChangeLogEnabled &&\n opts.sqliteChangeLogCompare &&\n opts.sqliteChangeLogWriter &&\n opts.sqliteCatchup\n ? new SQLiteChangeLogComparator(\n lc,\n shard,\n opts.sqliteCatchup.changeLogFile,\n opts.sqliteChangeLogWriter.identity,\n this.#storer,\n {setTimeoutFn, ...opts.sqliteChangeLogCompare},\n )\n : undefined;\n this.#purgeLock = initialPurgeLock;\n this.#autoReset = autoReset;\n this.#state = new RunningState(this.id, undefined, setTimeoutFn);\n this.#latestStatus = {tag: 'status'};\n }\n\n async run() {\n this.#lc.info?.('starting change stream');\n\n this.#forwarder.startProgressMonitor();\n\n const lagReportInit = await this.#source.startLagReporter();\n if (lagReportInit) {\n this.#latestStatus.lagReport = {\n nextSendTimeMs: lagReportInit.nextSendTimeMs,\n };\n // Record the commit time of the initiated lag report (i.e. \"head\")\n // for the purpose of skipping over any lag reports that are re-streamed\n // by the change-source in the case of a change-streamer starting from\n // an older watermark.\n this.#latestLagReportCommitTimeMs = lagReportInit.firstCommitTimeMs;\n }\n\n if (this.#pgChangeLogEnabled) {\n this.#storer.assumeOwnershipAtHead({\n purgeLock: this.#purgeLock,\n requestRestart: reason => this.#backfills.requestRestart(reason),\n });\n }\n this.#purgeLock = null;\n\n // The threshold in (estimated number of) bytes to send() on subscriber\n // websockets before `await`-ing the I/O buffers to be ready for more.\n const flushBytesThreshold = getDefaultHighWaterMark(false);\n\n while (this.#state.shouldRun()) {\n let err: unknown;\n let watermark: string | null = null;\n let unflushedBytes = 0;\n try {\n // Initialization reconciles the change log for every stream\n // connection. It completes before `startStream`, so no change can\n // arrive during reconciliation.\n const {lastWatermark, backfillRequests: logBackfillRequests} =\n await this.#initializer.initialize();\n const backfillRequests = this.#backfills.startStream(\n // Backfill requests from the change-log are only necessary for\n // backwards compatibility with protocol v7 and RMv1. On RMv2,\n // all of the beginning state comes from the (backup) replica,\n // which contains the progress marks, so there is no need to\n // downgrade to the change-log version which does not have\n // progress marks.\n this.#pgChangeLogEnabled ? logBackfillRequests : [],\n );\n // SQLite catchup must not be eligible until this has been initialized\n // from the selected durable head. Commits observed only since process\n // startup are insufficient after a change-streamer restart.\n this.#lastForwardedCommitWatermark = lastWatermark;\n const stream = await this.#source.startStream(\n lastWatermark,\n backfillRequests,\n );\n this.#stream = stream;\n if (!this.#state.shouldRun()) {\n // Stopped while the stream was being started (which stop() could not\n // cancel). The stream is cancelled on exiting the iteration.\n break;\n }\n if (this.#pgChangeLogEnabled) {\n this.#storer.run().catch(e => stream.changes.cancel(e));\n }\n\n if (\n this.#state.resetBackoff() >\n REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS\n ) {\n // After recovering from a backoff for which a replication status\n // error was published, publish an OK status\n this.#replicationStatusPublisher.publish(\n this.#lc,\n 'Replicating',\n `Replicating from ${lastWatermark}`,\n );\n }\n watermark = null;\n\n this.#acker.reset(stream.acks, lastWatermark);\n\n for await (const change of stream.changes) {\n this.#acker.trackDownstream(change);\n\n const [type, msg] = change;\n if (type === 'status' && watermark === null && this.#mayRestart()) {\n // A restart can happen at a status message outside of a\n // transaction, e.g. when the replication stream is otherwise idle.\n // Exiting the iteration cancels the stream.\n break;\n }\n switch (type) {\n case 'status':\n if (watermark === null) {\n // Allows ownership to be assumed on an idle upstream.\n this.#storer.status(change[2].watermark);\n }\n if (\n msg.lagReport &&\n msg.lagReport.lastTimings.commitTimeMs >=\n this.#latestLagReportCommitTimeMs\n ) {\n // Lag reports are not stored in the cdc change log, but rather\n // only forwarded on \"live\" connections. When a new subscriber\n // is catching up, it is initialized with the #latestStatus\n // from which it can measure lag while catching up.\n this.#latestStatus.lagReport = msg.lagReport;\n this.#latestLagReportCommitTimeMs =\n msg.lagReport.lastTimings.commitTimeMs;\n this.#forwarder.sendStatus(this.#latestStatus);\n }\n continue;\n case 'control':\n await this.#handleControlMessage(msg);\n continue; // control messages are not stored/forwarded\n case 'begin':\n watermark = change[2].commitWatermark;\n break;\n case 'commit':\n if (watermark !== change[2].watermark) {\n throw new UnrecoverableError(\n `commit watermark ${change[2].watermark} does not match 'begin' watermark ${watermark}`,\n );\n }\n this.#txCounter.add(1);\n break;\n default:\n if (type === 'data') {\n this.#changeCounter.add(1);\n }\n if (watermark === null) {\n throw new UnrecoverableError(\n `${type} change (${msg.tag}) received before 'begin' message`,\n );\n }\n break;\n }\n\n this.#backfills.track(change);\n\n const serialized = serializeChangeStreamDataWithChange(change);\n const {json} = serialized;\n if (this.#pgChangeLogEnabled) {\n this.#storer.store(watermark, change, serialized);\n }\n // The SQLite change log commits at transaction boundaries, and its\n // commit for this transaction lands here -- before the forward of the\n // `commit` message, and before #recordForwardedTransactionBoundary\n // advances what #captureRequiredHead reads. No `await` separates it\n // from serialization or the optional PG enqueue above. This is the\n // assertable form of invariant 1: a synchronous SQLite commit in the\n // same loop iteration always precedes anything that can advance the\n // watermark this stream would resume from. Never throws; a write\n // failure disables the writer rather than stopping replication.\n this.#changeLogWriter?.write(change, json, serialized.change);\n const entry: WatermarkedChange = [watermark, change[1].tag, json];\n unflushedBytes += json.length;\n let flowControl: Promise<void> | undefined;\n if (unflushedBytes < flushBytesThreshold) {\n // pipeline changes until flushBytesThreshold\n this.#forwarder.forward(entry);\n } else {\n // Wait for messages to clear socket buffers to ensure that they\n // make their way to subscribers. Without this `await`, the\n // messages end up being buffered in this process, which:\n // (1) results in memory pressure and increased GC activity\n // (2) prevents subscribers from processing the messages as they\n // arrive, instead getting them in a large batch after being\n // idle while they were queued (causing further delays).\n flowControl = this.#forwarder.forwardWithFlowControl(entry);\n }\n // Both forwards send the entry and update the Forwarder's transaction\n // state synchronously. Record the boundary, and end the transaction at\n // its commit or rollback, before awaiting flow control: registrations\n // during the wait then observe the forwarded state, and a stream that\n // is interrupted during the wait does not roll back a transaction\n // that was already forwarded whole.\n this.#recordForwardedTransactionBoundary(type, entry[0]);\n if (type === 'commit' || type === 'rollback') {\n watermark = null;\n }\n if (flowControl) {\n await promiseOrAbort(\n flowControl,\n stream.changes.signal,\n this.#state.signal,\n );\n unflushedBytes = 0;\n }\n\n // Allow the PG storer to exert back pressure when it is enabled.\n const readyForMore = this.#pgChangeLogEnabled\n ? this.#storer.readyForMore()\n : undefined;\n if (readyForMore) {\n await promiseOrAbort(\n readyForMore,\n stream.changes.signal,\n this.#state.signal,\n );\n }\n\n // Restarts happen at transaction boundaries, so that the pending\n // backfills of the aligned subscribers (and the change log) are\n // consistent with the watermark from which the stream restarts.\n if (type === 'commit' && this.#mayRestart()) {\n // Exiting the iteration cancels the stream.\n break;\n }\n }\n } catch (e) {\n err = e;\n } finally {\n this.#stream?.changes.cancel();\n this.#stream = undefined;\n }\n\n // When the change stream is interrupted, abort any pending transaction.\n if (watermark) {\n this.#lc.warn?.(`aborting interrupted transaction ${watermark}`);\n if (this.#pgChangeLogEnabled) {\n this.#storer.abort();\n }\n // Rolling back the log leaves no rows for the interrupted transaction,\n // so the next connection's reconciliation sees a head at or below its\n // resume watermark rather than a partial transaction.\n this.#changeLogWriter?.abort();\n // A rollback ends the log's open transaction without a commit\n // notification; wake any purge batch waiting for that window.\n this.#purgeScheduler?.onWriterIdle();\n this.#forwarder.forward([watermark, 'rollback', ROLLBACK_JSON]);\n this.#recordForwardedTransactionBoundary('rollback', watermark);\n this.#backfills.track(['rollback', {tag: 'rollback'}]);\n }\n\n if (this.#backfills.restartReason !== null && err === undefined) {\n // A planned restart: drain the storer and reconnect immediately.\n this.#backfills.recordRestart();\n await this.#storer.stop();\n } else {\n // Backoff and drain any pending entries in the storer before reconnecting.\n await Promise.all([\n this.#storer.stop(),\n this.#state.backoff(this.#lc, err),\n this.#state.retryDelay > REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS\n ? publishCriticalEvent(\n this.#lc,\n replicationStatusError(this.#lc, 'Replicating', err),\n )\n : promiseVoid,\n ]);\n }\n }\n\n this.#forwarder.stopProgressMonitor();\n this.#lc.info?.('ChangeStreamer stopped');\n }\n\n async #handleControlMessage(msg: ChangeStreamControl[1]) {\n this.#lc.info?.('received control message', msg);\n const {tag} = msg;\n\n switch (tag) {\n case 'reset-required':\n await markResetRequired(\n this.#changeDBProvider('change-streamer-reset', 1),\n this.#shard,\n );\n await publishReplicationError(\n this.#lc,\n 'Replicating',\n msg.message ?? 'Resync required',\n msg.errorDetails,\n );\n if (this.#autoReset) {\n this.#lc.warn?.('shutting down for auto-reset');\n await this.stop(new AutoResetSignal());\n }\n break;\n default:\n unreachable(tag);\n }\n }\n\n async subscribe(\n ctx: SubscriberContext,\n downstream: Subscription<\n string | PreSerializedBatch\n > = Subscription.create(),\n ): Promise<Source<string | PreSerialized>> {\n const {protocolVersion, id, mode, replicaVersion, watermark, wsBatched} =\n ctx;\n if (mode === 'serving') {\n this.#serving.resolve();\n }\n let cleanupSubscriber = () => {};\n\n // No subscriber's ACK advances the SQLite change log's head any more: the\n // writer runs in this process, so the barrier is notified from the commit\n // itself (see #changeLogWriter's onCommit).\n const lc = this.#lc.withContext('subscriber', id);\n const subscriber = new Subscriber(\n protocolVersion,\n id,\n mode,\n watermark,\n downstream,\n () => this.#latestStatus,\n {wsBatched, ...this.#backfills.subscriberOptions(lc, ctx)},\n );\n this.#backfills.register(subscriber, downstream);\n const removeFromForwarder = () => {\n lc.info?.(`removing subscriber ${subscriber.id}`);\n this.#forwarder.remove(subscriber);\n };\n cleanupSubscriber = removeFromForwarder;\n if (replicaVersion !== this.#replicaVersion) {\n lc.warn?.(`rejecting subscriber at replica version ${replicaVersion}`);\n subscriber.close(\n ErrorType.WrongReplicaVersion,\n `current replica version is ${\n this.#replicaVersion\n } (requested ${replicaVersion})`,\n );\n } else {\n lc.info?.(`adding subscriber ${subscriber.id}`);\n\n const catchupFromPG = () => {\n assert(\n this.#pgChangeLogEnabled,\n 'cannot catch up from a disabled PG change log',\n );\n // Keep the existing PG registration/catchup lockstep unchanged when\n // SQLite was not selected before Forwarder.add().\n cleanupSubscriber = removeFromForwarder;\n this.#forwarder.add(subscriber);\n this.#storer.catchup(subscriber, mode);\n };\n const sqliteDecision = this.#selectSQLiteCatchup(lc, ctx);\n if (!sqliteDecision) {\n catchupFromPG();\n } else if (sqliteDecision.kind === 'rejected') {\n lc.warn?.(\n `${\n sqliteDecision.terminal ? 'rejecting' : 'ending'\n } subscription for ${ctx.id}: ${sqliteDecision.message}`,\n );\n if (sqliteDecision.terminal) {\n subscriber.close(ErrorType.WatermarkTooOld, sqliteDecision.message);\n } else {\n subscriber.close();\n }\n } else {\n const {catchup, reason, coverage, logWarm} = sqliteDecision;\n cleanupSubscriber = () => catchup.remove(subscriber);\n const registration = await catchup.catchup(\n subscriber,\n () => this.#captureRequiredHead(),\n {logWarm},\n );\n switch (registration.kind) {\n case 'registered':\n lc.debug?.(\n `serving ${ctx.id} from SQLite catchup`,\n ...(coverage ? [{sqliteChangeLogCoverage: coverage}] : []),\n );\n this.#recordCatchupRoute('sqlite', reason);\n break;\n case 'uncovered':\n if (this.#pgChangeLogEnabled) {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: subscriber watermark ` +\n `${ctx.watermark} is below the SQLite change-log minimum ` +\n registration.minWatermark,\n );\n this.#recordCatchupRoute('pg', 'watermark-uncovered');\n catchupFromPG();\n } else {\n const message =\n `earliest supported watermark is ` +\n `${registration.minWatermark} (requested ${ctx.watermark})`;\n lc.warn?.(\n `rejecting subscriber at watermark ${ctx.watermark}: ` +\n `the SQLite change log starts at ` +\n `${registration.minWatermark} and the PG change log is disabled`,\n );\n this.#recordCatchupRoute('none', 'watermark-uncovered');\n subscriber.close(ErrorType.WatermarkTooOld, message);\n }\n break;\n case 'declined':\n // Registration failed before the subscriber was committed to\n // SQLite. Use PG when enabled; otherwise end this subscription so\n // it retries. The coordinator has already tripped the breaker,\n // which keeps the retry off SQLite for the cooldown; it is\n // deliberately not closed here, since closing it would abort the\n // catchups of every other subscriber it is serving.\n if (this.#pgChangeLogEnabled) {\n lc.error?.(\n `serving ${ctx.id} from PG catchup: SQLite catchup ` +\n `registration failed`,\n registration.error,\n );\n this.#recordCatchupRoute('pg', 'registration-failed');\n catchupFromPG();\n } else {\n lc.error?.(\n `ending subscription for ${ctx.id} to retry SQLite catchup: ` +\n `registration failed and the PG change log is disabled`,\n registration.error,\n );\n this.#recordCatchupRoute('none', 'registration-failed');\n subscriber.fail(registration.error);\n }\n break;\n case 'handled':\n // The coordinator closed or failed the subscriber itself, so\n // there is nothing left to route. Still counted, so that the\n // route counter sums to the number of subscriptions.\n this.#recordCatchupRoute('sqlite', 'registration-handled');\n break;\n default:\n unreachable(registration);\n }\n }\n }\n // Register the cleanup now that `cleanupSubscriber` points at the real\n // cleanup and the subscriber is registered. If `downstream` was already\n // cancelled (e.g. a client disconnect during the catchup await above),\n // `addCloseHandler` runs the cleanup immediately, and so it must not be\n // called until cleanupSubscriber is properly set up.\n downstream.addCloseHandler(() => cleanupSubscriber());\n\n // Any snapshot reservation held by this task can be closed now that\n // it is subscribed to the change stream.\n this.#reservations?.close(ctx.taskID);\n return downstream;\n }\n\n async startSnapshotReservation(\n taskID: string,\n ): Promise<Source<SnapshotMessage>> {\n if (!this.#reservations) {\n throw new Error('backups are not configured');\n }\n const downstream = this.#reservations.open(taskID);\n\n try {\n // Wait for an in-flight SQLite purge batch before reading and\n // advertising the reservation's bounds.\n await (this.#purgeScheduler?.pause(taskID) ?? promiseVoid);\n // A concurrent retry for this task may have superseded and cancelled\n // this reservation while its purge pause was settling. Only the current\n // owner may replace the task's source pin.\n if (!this.#reservations.isCurrent(taskID, downstream)) {\n return downstream;\n }\n // Pin after the purge pause has settled, so the SQLite minimum captured\n // by the router cannot move before it is advertised. #confirmReservations\n // skips an unpinned task while this await is in flight.\n this.#readRouter?.pin(taskID);\n // If a backup has been confirmed, immediately confirm the reservation.\n await this.#confirmReservations();\n return downstream;\n } catch (e) {\n // Cancel the reservation this call opened, not whatever currently\n // holds the task's slot: an overlapping retry for the same taskID may\n // have already replaced it, and a by-taskID close would tear down the\n // replacement and release its purge pause while it is advertising\n // snapshot bounds. cancel() routes through the subscription's cleanup,\n // which closes the reservation only if this instance still owns it.\n downstream.cancel();\n throw e;\n }\n }\n\n trackBackupWatermark(watermark: string) {\n this.#backupWatermark = watermark;\n this.#acker.trackBackup(watermark);\n // The durable backup floor is an independent input to each change-log\n // implementation. SQLite receives it even when the PG log has already\n // reached this watermark.\n this.#requestSQLitePurge('deferred');\n this.#maybeSchedulePGPurge();\n this.#maybeScheduleSQLitePurge();\n\n // Confirm any waiting reservations now that a backup has been confirmed.\n // Note that this is asynchronous and best effort; if it fails, the watermark\n // is still \"tracked\" and the confirmation will be retried on the next backup.\n void this.#confirmReservations().catch(e =>\n this.#lc.warn?.(`error confirming snapshot reservation`, e),\n );\n }\n\n async #confirmReservations() {\n const backupWatermark = this.#backupWatermark;\n const reservations = this.#reservations;\n if (\n backupWatermark === undefined ||\n !reservations?.confirmationsRequired()\n ) {\n return;\n }\n\n // Resolve PG bounds, when enabled, before touching any pin. Everything\n // below runs to completion without awaiting, which keeps a reservation's\n // advertised bounds and its pin in agreement: a concurrent /snapshot\n // retry for the same task replaces the reservation and re-pins it, and\n // confirming that replacement with the previous pin's bounds is exactly\n // the mismatch pinning exists to prevent.\n const pgState = this.#pgChangeLogEnabled\n ? await this.#getChangeLogState()\n : undefined;\n for (const taskID of reservations.unconfirmedTaskIDs()) {\n let route = this.#readRouter?.peek(taskID);\n // startSnapshotReservation pins only after an in-flight purge has\n // completed. A task with no pin yet is confirmed by that path instead.\n if (this.#readRouter && route === undefined) {\n continue;\n }\n\n // A transiently unavailable or broken SQLite log normally pins the\n // reservation to PG. With PG disabled, re-evaluate that choice on each\n // backup notification and keep the reservation pending until SQLite can\n // supply bounds.\n if (!this.#pgChangeLogEnabled && route?.source === 'pg') {\n this.#readRouter?.release(taskID);\n route = this.#readRouter?.pin(taskID);\n if (route?.source === 'pg') {\n if (reservations.noteConfirmationDelayed(taskID)) {\n this.#reservationConfirmDelays.add(1);\n }\n this.#lc.warn?.(\n `delaying snapshot reservation for ${taskID}: SQLite change log ` +\n `is unavailable and the PG change log is disabled`,\n );\n continue;\n }\n }\n\n if (route?.source === 'sqlite') {\n const coverage = must(\n route.coverage,\n 'a pinned SQLite route must carry its covered range',\n );\n if (\n this.#pgChangeLogEnabled &&\n coverage.minWatermark > backupWatermark\n ) {\n // A log seeded after this backup, most often. Holding the\n // reservation until a backup reaches the log's minimum would stall\n // a follower that PG can serve now, so move it -- pin included.\n this.#lc.info?.(\n `demoting ${taskID} to PG catchup: SQLite change-log minimum ` +\n `${coverage.minWatermark} is later than backupWatermark ` +\n backupWatermark,\n );\n this.#reservationDemotions.add(1);\n route = must(this.#readRouter).demote(taskID);\n }\n }\n\n const source = route?.source ?? 'pg';\n let minWatermark: string;\n if (route?.source === 'sqlite') {\n // Demotion above already moved every SQLite route the backup is\n // outside of, so this one covers it and confirms below.\n minWatermark = must(\n route.coverage,\n 'a pinned SQLite route must carry its covered range',\n ).minWatermark;\n } else {\n minWatermark = must(\n pgState,\n 'a PG reservation route requires the PG change log',\n ).minWatermark;\n }\n\n if (minWatermark <= backupWatermark) {\n reservations.confirmFor(\n taskID,\n this.#replicaVersion,\n backupWatermark,\n source,\n );\n } else {\n // PG cannot catch a restored replica up from this backup yet. Keep\n // the reservation pending until a later backup moves the durable\n // watermark into its covered range.\n if (reservations.noteConfirmationDelayed(taskID)) {\n this.#reservationConfirmDelays.add(1);\n }\n this.#lc.error?.(\n `${source} change-log minWatermark ${minWatermark} is later than ` +\n `backupWatermark ${backupWatermark}. Delaying confirmation of ` +\n `snapshot reservation until next backup.`,\n );\n }\n }\n }\n\n #maybeSchedulePGPurge(): void {\n const backupWatermark = this.#backupWatermark;\n if (\n !this.#pgChangeLogEnabled ||\n this.#pgPurgeScheduled ||\n this.#pgPurgeRunning ||\n backupWatermark === undefined ||\n this.#pgPurgedWatermark >= backupWatermark\n ) {\n return;\n }\n this.#pgPurgeScheduled = true;\n this.#state.setTimeout(() => {\n this.#pgPurgeScheduled = false;\n this.#pgPurgeRunning = true;\n return this.#purgePGChangeLog().finally(() => {\n this.#pgPurgeRunning = false;\n this.#maybeSchedulePGPurge();\n });\n }, CLEANUP_DELAY_MS);\n }\n\n #maybeScheduleSQLitePurge(): void {\n if (\n this.#sqlitePurgeScheduled ||\n this.#sqlitePurgeRunning ||\n this.#backupWatermark === undefined ||\n this.#sqlitePurgeContinuation === undefined\n ) {\n return;\n }\n const delay =\n this.#sqlitePurgeContinuation === 'immediate' ? 0 : CLEANUP_DELAY_MS;\n this.#sqlitePurgeScheduled = true;\n this.#state.setTimeout(() => {\n this.#sqlitePurgeScheduled = false;\n this.#sqlitePurgeRunning = true;\n return this.#purgeSQLiteChangeLog().finally(() => {\n this.#sqlitePurgeRunning = false;\n this.#maybeScheduleSQLitePurge();\n });\n }, delay);\n }\n\n async #getChangeLogState(): Promise<{\n replicaVersion: string;\n minWatermark: string;\n }> {\n const minWatermark = await this.#storer.getMinWatermarkForCatchup();\n if (!minWatermark) {\n this.#lc.warn?.(\n `Unexpected empty changeLog. Resync if \"Local replica watermark\" errors arise`,\n );\n }\n return {\n replicaVersion: this.#replicaVersion,\n minWatermark: minWatermark ?? this.#replicaVersion,\n };\n }\n\n #getCleanupFloor(): {\n backupWatermark: string;\n purgeWatermark: string;\n current: string[];\n } {\n const backupWatermark = this.#backupWatermark;\n assert(\n backupWatermark !== undefined,\n 'cleanup cannot run without a backup watermark',\n );\n const current = [\n ...this.#forwarder.getAcks(),\n ...(this.#reservations?.getReservedWatermarks() ?? []),\n ];\n // The cleanup delay above is the grace period for disconnected\n // subscribers to reconnect and expose their ACKs. Once it expires, an\n // empty set places no additional constraint on the confirmed backup\n // watermark and must not pin either change log indefinitely.\n return {\n backupWatermark,\n purgeWatermark: min(backupWatermark, ...current),\n current,\n };\n }\n\n async #purgePGChangeLog(): Promise<void> {\n if (this.#storer.ownershipPending) {\n // Purging before ownership would only abort on the ownership check (or\n // on this change-streamer's own purge lock).\n return;\n }\n try {\n const {backupWatermark, purgeWatermark, current} =\n this.#getCleanupFloor();\n if (purgeWatermark < backupWatermark) {\n if (this.#loggedBehindWatermark !== purgeWatermark) {\n this.#loggedBehindWatermark = purgeWatermark;\n this.#lc.info?.(\n `At least one client is behind backup ${backupWatermark}`,\n {watermarks: current},\n );\n }\n } else {\n this.#loggedBehindWatermark = undefined;\n }\n if (purgeWatermark <= this.#pgPurgedWatermark) {\n return;\n }\n this.#lc.info?.(`Purging PG changes before ${purgeWatermark} ...`);\n const start = performance.now();\n const deleted = await this.#storer.purgeRecordsBefore(purgeWatermark);\n const elapsed = (performance.now() - start).toFixed(2);\n this.#lc.info?.(\n `Purged ${deleted} PG changes before ${purgeWatermark} (${elapsed} ms)`,\n );\n this.#pgPurgedWatermark = purgeWatermark;\n } catch (e) {\n this.#lc.warn?.(`error purging the PG change log`, e);\n }\n }\n\n async #purgeSQLiteChangeLog(): Promise<void> {\n const scheduler = this.#purgeScheduler;\n if (!scheduler) {\n return;\n }\n try {\n const {backupWatermark, purgeWatermark} = this.#getCleanupFloor();\n // Consume the request before starting. A backup notification that\n // arrives during this pass records another request, which is merged with\n // the continuation returned by this pass rather than being overwritten.\n this.#sqlitePurgeContinuation = undefined;\n if (purgeWatermark < backupWatermark) {\n // Live constraint changes are not edge-triggered. Keep evaluating the\n // floor until it reaches the durable backup watermark, independently\n // of whether the PG implementation still exists.\n this.#requestSQLitePurge('deferred');\n }\n const result = await scheduler.purge(purgeWatermark);\n this.#requestSQLitePurge(result.continuation);\n } catch (e) {\n // purge() is fail-soft, but retain the coordinator's retry if an\n // unexpected caller-boundary failure escapes it.\n this.#requestSQLitePurge('deferred');\n this.#lc.warn?.(`error purging the SQLite change log`, e);\n }\n }\n\n #requestSQLitePurge(continuation: PurgeContinuation | undefined): void {\n if (!this.#purgeScheduler || continuation === undefined) {\n return;\n }\n if (\n continuation === 'immediate' ||\n this.#sqlitePurgeContinuation === undefined\n ) {\n this.#sqlitePurgeContinuation = continuation;\n }\n }\n\n /**\n * Whether the stream should be restarted (at a transaction boundary) for\n * backfills. Restarts are held until ownership of the PG change log is\n * assumed: a restarted stream would decode the WAL up to the change log's\n * head again, and backfills cannot be stored until ownership is assumed\n * anyway (see Storer.assumeOwnershipAtHead()).\n */\n #mayRestart() {\n return (\n this.#backfills.restartReason !== null && !this.#storer.ownershipPending\n );\n }\n\n async stop(err?: unknown) {\n this.#state.stop(this.#lc, err);\n this.#stream?.changes.cancel();\n this.#purgeScheduler?.stop();\n this.#comparator?.stop();\n this.#sqliteCatchup?.close();\n this.#changeLogWriter?.close();\n await Promise.allSettled([this.#storer.stop(), this.#source.stop()]);\n }\n\n #recordForwardedTransactionBoundary(\n type: ChangeStreamData[0],\n watermark: string,\n ) {\n switch (type) {\n case 'begin':\n assert(\n this.#currentTransactionCompletion === undefined,\n 'forwarded begin while a transaction is already in progress',\n );\n this.#currentTransactionCompletion =\n resolver<ForwardedTransactionCompletion>();\n this.#transactionForwardStartedAt = performance.now();\n break;\n case 'commit': {\n const completion = this.#currentTransactionCompletion;\n assert(completion, 'forwarded commit without a pending transaction');\n this.#recordTransactionForwardDuration();\n this.#lastForwardedCommitWatermark = watermark;\n this.#currentTransactionCompletion = undefined;\n completion.resolve({kind: 'committed', watermark});\n break;\n }\n case 'rollback': {\n const completion = this.#currentTransactionCompletion;\n assert(completion, 'forwarded rollback without a pending transaction');\n const committed = this.#lastForwardedCommitWatermark;\n assert(committed, 'last forwarded commit watermark is not initialized');\n this.#transactionForwardStartedAt = undefined;\n this.#currentTransactionCompletion = undefined;\n completion.resolve({kind: 'rolled-back', watermark: committed});\n break;\n }\n }\n }\n\n #recordTransactionForwardDuration() {\n const startedAt = this.#transactionForwardStartedAt;\n this.#transactionForwardStartedAt = undefined;\n if (startedAt !== undefined) {\n this.#transactionForwardDuration.recordMs(performance.now() - startedAt, {\n sqlite_change_log: this.#changeLogWriter?.enabled ? 'on' : 'off',\n });\n }\n }\n\n /**\n * Closes and forgets the cached catchup coordinator, so that a later\n * subscription re-opens the log from scratch rather than reading a handle\n * whose file is gone.\n */\n #closeSQLiteCatchup() {\n this.#sqliteCatchup?.close();\n this.#sqliteCatchup = undefined;\n }\n\n #captureRequiredHead(): string | Promise<string> {\n const committed = this.#lastForwardedCommitWatermark;\n assert(committed, 'last forwarded commit watermark is not initialized');\n return (\n this.#currentTransactionCompletion?.promise.then(\n completion => completion.watermark,\n ) ?? committed\n );\n }\n\n #selectSQLiteCatchup(\n lc: LogContext,\n ctx: SubscriberContext,\n ): SQLiteCatchupDecision | undefined {\n const opts = this.#sqliteCatchupOptions;\n if (this.#lastForwardedCommitWatermark === undefined || !opts) {\n return this.#fallbackOrReject(\n 'not-ready',\n 'SQLite catchup is not ready and the PG change log is disabled',\n );\n }\n // While the PG change log is available, backup subscribers retain its\n // existing recovery policy and cannot be moved by a canary selector.\n // Once PG is disabled, the backup replicator must use SQLite catchup: it\n // advances the canonical replica that Litestream backs up.\n if (ctx.mode === 'backup' && this.#pgChangeLogEnabled) {\n lc.info?.(`not serving backup subscriber ${ctx.id} from SQLite catchup`);\n return this.#fallbackOrReject(\n 'ineligible-mode',\n 'backup subscribers retain the PG change-log recovery policy',\n );\n }\n\n let route: ChangeLogReadRoute | undefined;\n if (this.#readRouter) {\n route = this.#readRouter.consume(ctx.taskID);\n if (route.source === 'pg') {\n if (route.reason === 'cold-log') {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: SQLite change log is still warming`,\n {sqliteChangeLogCoverage: route.coverage},\n );\n } else if (route.reason === 'log-unavailable') {\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `${opts.changeLogFile} is unavailable or incompatible`,\n );\n } else {\n lc.debug?.(\n `serving ${ctx.id} from PG catchup: SQLite route ${route.reason}`,\n ...(route.coverage\n ? [{sqliteChangeLogCoverage: route.coverage}]\n : []),\n );\n }\n return this.#fallbackOrReject(\n route.reason,\n `SQLite catchup route ${route.reason} is unavailable and the PG ` +\n `change log is disabled`,\n );\n }\n const coverage = route.coverage;\n assert(coverage, 'a SQLite route must carry its covered range');\n if (ctx.watermark < coverage.minWatermark) {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: subscriber watermark ` +\n `${ctx.watermark} is below the SQLite change-log minimum ` +\n coverage.minWatermark,\n {sqliteChangeLogCoverage: coverage},\n );\n return this.#fallbackOrReject(\n 'watermark-uncovered',\n `earliest supported watermark is ${coverage.minWatermark} ` +\n `(requested ${ctx.watermark})`,\n true,\n );\n }\n }\n\n // `shouldUse` remains as a test hook and an optional extra policy gate.\n // Production selection comes from #readRouter.\n if (!this.#readRouter && !opts.shouldUse?.(ctx)) {\n return this.#fallbackOrReject(\n 'selector',\n 'SQLite catchup was not selected and the PG change log is disabled',\n );\n }\n if (this.#readRouter && opts.shouldUse && !opts.shouldUse(ctx)) {\n return this.#fallbackOrReject(\n 'selector',\n 'SQLite catchup was not selected and the PG change log is disabled',\n );\n }\n\n const catchup =\n this.#sqliteCatchup ?? this.#openSQLiteCatchup(lc, opts, ctx);\n if (catchup) {\n return {\n kind: 'selected',\n catchup,\n reason: route?.reason ?? 'selector',\n coverage: route?.coverage,\n // Legacy focused-test selection has no warm classification.\n logWarm:\n route === undefined ? undefined : route.reason !== 'selected-cold',\n };\n } else {\n return this.#fallbackOrReject(\n 'log-unavailable',\n 'SQLite change log is unavailable and the PG change log is disabled',\n );\n }\n }\n\n #fallbackOrReject(\n reason: string,\n message: string,\n terminal = false,\n ): SQLiteCatchupRejection | undefined {\n this.#recordCatchupRoute(this.#pgChangeLogEnabled ? 'pg' : 'none', reason);\n return this.#pgChangeLogEnabled\n ? undefined\n : {kind: 'rejected', message, terminal};\n }\n\n #recordCatchupRoute(source: 'pg' | 'sqlite' | 'none', reason: string): void {\n this.#catchupRoutes.add(1, {source, reason});\n }\n\n /**\n * Opens the change log and, if it can serve, wraps it in the coordinator that\n * subsequent subscriptions reuse.\n *\n * The log may not exist yet -- the writer creates it at the stream loop's\n * first reconcile, and deletes it when it fails soft -- or may exist without\n * content, or may be unreadable. None of those can be allowed to fail a\n * subscription:\n * this is the last point at which PG catchup can be selected when it is\n * enabled -- past `Forwarder.add()` the subscriber is committed to SQLite --\n * so each declines here instead. With PG disabled, the caller ends the\n * subscription so that it retries. Neither the failure nor the reader is\n * retained, so a later subscription retries from scratch.\n */\n #openSQLiteCatchup(\n lc: LogContext,\n opts: SQLiteCatchupOptions,\n ctx: SubscriberContext,\n ): SQLiteChangeLogCatchup | undefined {\n let reader: SQLiteChangeLogReader | undefined;\n try {\n reader = new SQLiteChangeLogReader(this.#lc, opts.changeLogFile);\n // `plan()` doubles as the readiness check: it reports `not-ready` when\n // the writer has not created or seeded the stream table. The barrier\n // cannot wait its way out of that, since a log with no head can never\n // reach the required one.\n if (reader.plan(ctx.watermark).kind === 'not-ready') {\n reader.close();\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `${opts.changeLogFile} has no changes to serve yet`,\n );\n return undefined;\n }\n } catch (e) {\n // An absent file is the common case, since a readonly handle cannot\n // create one. A corrupt or truncated file lands here too, and is equally\n // not a reason to fail the subscription.\n reader?.close();\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `cannot read ${opts.changeLogFile}`,\n e,\n );\n return undefined;\n }\n this.#changeLogUnavailableSince = undefined;\n // Readiness is checked once, on the way to the cached coordinator: a log\n // with content keeps it. Purging preserves the latest transaction as a\n // catchup boundary, and `reconcileChangeLog` reseeds inside a single\n // transaction, so a reader never observes an emptied log.\n this.#sqliteCatchup = new SQLiteChangeLogCatchup(\n this.#lc,\n this.#forwarder,\n reader,\n {\n batchSize: opts.readBatchRows,\n barrierTimeoutMs: opts.barrierTimeoutMs,\n barrierPollIntervalMs: opts.barrierPollIntervalMs,\n cleanupGuard: opts.cleanupGuard,\n onFailure: failure => {\n this.#lc.warn?.(\n `temporarily disabling SQLite catchup after ${failure}`,\n );\n this.#readRouter?.trip();\n },\n },\n );\n return this.#sqliteCatchup;\n }\n\n /**\n * Reports falling back to PG catchup, tracking how long the log has been\n * unavailable so that a subscription that merely arrived before the writer's\n * first reconcile does not look like an incident.\n */\n #declineSQLiteCatchup(\n lc: LogContext,\n ctx: SubscriberContext,\n opts: SQLiteCatchupOptions,\n reason: string,\n error?: unknown,\n ): void {\n const now = Date.now();\n this.#changeLogUnavailableSince ??= now;\n const unavailableMs = now - this.#changeLogUnavailableSince;\n const threshold =\n opts.notReadyWarnThresholdMs ??\n DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS;\n lc[unavailableMs >= threshold ? 'warn' : 'debug']?.(\n (this.#pgChangeLogEnabled\n ? `serving ${ctx.id} from PG catchup: `\n : `cannot serve ${ctx.id} from SQLite catchup: `) +\n `${reason} ` +\n `(unavailable for ${unavailableMs} ms)`,\n ...(error === undefined ? [] : [error]),\n );\n }\n}\n\ntype ForwardedTransactionCompletion =\n | {kind: 'committed'; watermark: string}\n | {kind: 'rolled-back'; watermark: string};\n\ntype SQLiteCatchupSelection = {\n readonly kind: 'selected';\n readonly catchup: SQLiteChangeLogCatchup;\n readonly reason: string;\n readonly coverage: SQLiteChangeLogCoverage | undefined;\n /**\n * Whether the router classified the log as warm for this subscriber, or\n * undefined when selection did not run through the router (focused tests).\n */\n readonly logWarm: boolean | undefined;\n};\n\ntype SQLiteCatchupRejection = {\n readonly kind: 'rejected';\n readonly message: string;\n /** A terminal rejection forces the view-syncer to restore a fresh replica. */\n readonly terminal: boolean;\n};\n\ntype SQLiteCatchupDecision = SQLiteCatchupSelection | SQLiteCatchupRejection;\n\n// The delay between receiving an initial, backup-based watermark\n// and performing a check of whether to purge records before it.\n// This delay should be long enough to handle situations like the following:\n//\n// 1. `litestream restore` downloads a backup for the `replication-manager`\n// 2. `replication-manager` starts up and runs this `change-streamer`\n// 3. `zero-cache`s that are running on a different replica connect to this\n// `change-streamer` after exponential backoff retries.\n//\n// It is possible for a `zero-cache`[3] to be behind the backup restored [1].\n// This cleanup delay (30 seconds) is thus set to be a value comfortably\n// longer than the max delay for exponential backoff (10 seconds) in\n// `services/running-state.ts`. This allows the `zero-cache` [3] to reconnect\n// so that the `change-streamer` can track its progress and know when it has\n// surpassed the initial watermark of the backup [1].\nconst CLEANUP_DELAY_MS = DEFAULT_MAX_RETRY_DELAY_MS * 3;\n\nconst ROLLBACK_JSON = JSON.stringify([\n 'rollback',\n {tag: 'rollback'},\n] satisfies Rollback);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiNA,eAAsB,mBACpB,IACA,OACA,QACA,kBACA,mBACA,UACA,cACA,4BACA,mBACA,cACA,WACA,WACA,MACA,eAAe,YACiB;CAChC,MAAM,wBACJ,IACA,UACA,mBACA,OACA,WACA,aAAa,KAAA,GACb,cACA,KAAK,kBACP;CAOA,MAAM,oBACJ,iBACA,QAEA,SACE,KACE;EAAC;EAAK,YAAY,GAAE,qBAAqB,gBAAe;CAAC,GAKzD,SAAS,OACX,CACF;CAEF,MAAM,EAAC,mBAAkB;CACzB,OAAO,IAAI,mBACT,IACA,OACA,QACA,kBACA,mBACA,kBACA,gBACA,cACA,4BACA,cACA,WACA,WACA,MACA,YACF;AACF;AAEA,IAAM,8CAA8C;AAQpD,IAAM,mDAAmD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4IzD,IAAM,qBAAN,MAA0D;CACxD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;;CAGA;CAUA,WAAoB,SAAS;CAE7B,aAAsB,mBACpB,eACA,gBACA,kCACF;CACA,iBAA0B,mBACxB,eACA,WACA,qDACF;CAIA,8BAAuC,4BACrC,eACA,gCACA,yIAEF;CACA,iBAA0B,mBACxB,eACA,oCACA,sEACF;CACA,wBAAiC,mBAC/B,eACA,2CACA,qPAIF;CACA,4BAAqC,mBACnC,eACA,gDACA,2LAGF;CAEA;CACA,+BAA+B;CAC/B;CACA,qBAA6B;;;;;;;CAO7B;CACA;CACA;CAGA,oBAAoB;CACpB,kBAAkB;CAClB,wBAAwB;CACxB,sBAAsB;CACtB;CACA;CACA;CACA;CACA;CACA;CAIA,YACE,IACA,OACA,QACA,kBACA,mBACA,kBACA,gBACA,QACA,4BACA,cACA,kBACA,WACA,MACA,eAAe,YACf;EACA,KA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"}
|
|
@@ -49,6 +49,63 @@ interface PurgeLock {
|
|
|
49
49
|
export declare function setupCDCTables(lc: LogContext, db: postgres.TransactionSql, shard: ShardID): Promise<void>;
|
|
50
50
|
export declare function markResetRequired(sql: PostgresDB, shard: ShardID): Promise<void>;
|
|
51
51
|
export declare function ensureReplicationConfig(lc: LogContext, db: PostgresDB, subscriptionState: Pick<SubscriptionState, 'publications' | 'replicaVersion' | 'watermark'>, shard: ShardID, autoReset: boolean, purgeLock?: PurgeLock, setTimeoutFn?: typeof setTimeout, pgChangeLogEnabled?: boolean): Promise<void>;
|
|
52
|
+
export type ChangeLogReinitialization = {
|
|
53
|
+
/**
|
|
54
|
+
* The watermark of the replica from which the change log is re-initialized,
|
|
55
|
+
* i.e. the watermark from which the change stream will resume.
|
|
56
|
+
*/
|
|
57
|
+
watermark: string;
|
|
58
|
+
cookies: {
|
|
59
|
+
tableMetadata: readonly {
|
|
60
|
+
schema: string;
|
|
61
|
+
table: string;
|
|
62
|
+
metadata: TableMetadata;
|
|
63
|
+
}[];
|
|
64
|
+
backfilling: readonly {
|
|
65
|
+
schema: string;
|
|
66
|
+
table: string;
|
|
67
|
+
column: string;
|
|
68
|
+
backfill: BackfillID;
|
|
69
|
+
}[];
|
|
70
|
+
};
|
|
71
|
+
owner: string;
|
|
72
|
+
ownerAddress: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Re-initializes the change log from the state of the replica if it does not
|
|
76
|
+
* contain all of the replica's changes, i.e. if its `lastWatermark` is before
|
|
77
|
+
* the replica's watermark. This is the case if the change log is no longer
|
|
78
|
+
* maintained (e.g. after running in high-availability mode), or if its last
|
|
79
|
+
* owner stopped before persisting changes that it had already forwarded
|
|
80
|
+
* (and backed up). Ownership of the change log is taken in the same
|
|
81
|
+
* transaction, in which:
|
|
82
|
+
*
|
|
83
|
+
* * The `replicationState`, `changeLog`, `tableMetadata`, and `backfilling`
|
|
84
|
+
* tables are truncated. (The `replicationConfig` is kept;
|
|
85
|
+
* {@link ensureReplicationConfig} handles changes of the replica version
|
|
86
|
+
* or publications.)
|
|
87
|
+
* * The change log is seeded with an empty transaction at the replica's
|
|
88
|
+
* watermark, so that subscribers at that watermark can be caught up, while
|
|
89
|
+
* subscribers at earlier watermarks are rejected as being too old.
|
|
90
|
+
* * The `tableMetadata` and `backfilling` cookies are set from the replica.
|
|
91
|
+
*
|
|
92
|
+
* Returns `'maintained'` without modifying anything if the change log's
|
|
93
|
+
* `lastWatermark` is at or past the replica's watermark, i.e. the change log
|
|
94
|
+
* contains all of the replica's changes. Because a replication slot is only
|
|
95
|
+
* acked past a transaction once it has been backed up, this means that the
|
|
96
|
+
* change log contains every transaction up to the position of the replica's
|
|
97
|
+
* replication slot, and it can be taken over (and resumed from its head).
|
|
98
|
+
*
|
|
99
|
+
* This is called when the change log's head is behind the position of the
|
|
100
|
+
* replication slot (see `PgChangeLogPurgeLocker`). That position cannot be
|
|
101
|
+
* compared with the watermarks of transactions to determine whether the
|
|
102
|
+
* change log is maintained, as it is also advanced past positions of changes
|
|
103
|
+
* outside of the publication.
|
|
104
|
+
*
|
|
105
|
+
* @param purgeLock A purge lock held by the caller, if any, which is released
|
|
106
|
+
* before the tables are truncated.
|
|
107
|
+
*/
|
|
108
|
+
export declare function reinitializeChangeLog(lc: LogContext, db: PostgresDB, shard: ShardID, { watermark, cookies, owner, ownerAddress }: ChangeLogReinitialization, purgeLock?: PurgeLock, setTimeoutFn?: typeof setTimeout): Promise<'reinitialized' | 'maintained'>;
|
|
52
109
|
export declare const CHANGE_STREAMER_APP_NAME = "zero-change-streamer";
|
|
53
110
|
export declare class AutoResetSignal extends AbortError {
|
|
54
111
|
readonly name = "AutoResetSignal";
|