@rocicorp/zero 1.11.0-canary.30 → 1.11.0-canary.32

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.
Files changed (89) hide show
  1. package/out/zero/package.js +1 -1
  2. package/out/zero/package.js.map +1 -1
  3. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  4. package/out/zero-cache/src/server/change-streamer.js +2 -1
  5. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  6. package/out/zero-cache/src/server/slot-keeper.d.ts +26 -0
  7. package/out/zero-cache/src/server/slot-keeper.d.ts.map +1 -0
  8. package/out/zero-cache/src/server/slot-keeper.js +175 -0
  9. package/out/zero-cache/src/server/slot-keeper.js.map +1 -0
  10. package/out/zero-cache/src/server/worker-urls.d.ts +1 -0
  11. package/out/zero-cache/src/server/worker-urls.d.ts.map +1 -1
  12. package/out/zero-cache/src/server/worker-urls.js +2 -1
  13. package/out/zero-cache/src/server/worker-urls.js.map +1 -1
  14. package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts +15 -5
  15. package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts.map +1 -1
  16. package/out/zero-cache/src/services/change-source/pg/change-source-init.js +21 -13
  17. package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
  18. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  19. package/out/zero-cache/src/services/change-source/pg/change-source.js +2 -1
  20. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  21. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +7 -1
  22. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  23. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +7 -4
  24. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  25. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts +35 -1
  26. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts.map +1 -1
  27. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +106 -22
  28. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
  29. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts +1 -1
  30. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts.map +1 -1
  31. package/out/zero-cache/src/services/change-source/pg/replication-slots.js +31 -9
  32. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
  33. package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
  34. package/out/zero-cache/src/services/change-source/pg/schema/init.js +15 -1
  35. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  36. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +20 -2
  37. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
  38. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +17 -2
  39. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  40. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +3 -2
  41. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts.map +1 -1
  42. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +1 -1
  43. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  44. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts.map +1 -1
  45. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +1 -1
  46. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  47. package/out/zero-cache/src/types/pg.js +1 -1
  48. package/out/zero-cache/src/types/processes.d.ts +3 -0
  49. package/out/zero-cache/src/types/processes.d.ts.map +1 -1
  50. package/out/zero-cache/src/types/processes.js.map +1 -1
  51. package/out/zero-cache/src/workers/connect-params.d.ts +6 -15
  52. package/out/zero-cache/src/workers/connect-params.d.ts.map +1 -1
  53. package/out/zero-cache/src/workers/connect-params.js +14 -15
  54. package/out/zero-cache/src/workers/connect-params.js.map +1 -1
  55. package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
  56. package/out/zero-cache/src/workers/connection.js +1 -1
  57. package/out/zero-cache/src/workers/connection.js.map +1 -1
  58. package/out/zero-client/src/client/version.js +1 -1
  59. package/out/zero-client/src/client/zero.js +2 -2
  60. package/out/zero-client/src/client/zero.js.map +1 -1
  61. package/out/zero-protocol/src/analyze-query-result.d.ts +1 -1
  62. package/out/zero-protocol/src/analyze-query-result.js.map +1 -1
  63. package/out/zero-protocol/src/connect.d.ts +9 -12
  64. package/out/zero-protocol/src/connect.d.ts.map +1 -1
  65. package/out/zero-protocol/src/connect.js +19 -16
  66. package/out/zero-protocol/src/connect.js.map +1 -1
  67. package/out/zero-protocol/src/poke.d.ts +3 -3
  68. package/out/zero-protocol/src/poke.js +1 -1
  69. package/out/zero-protocol/src/poke.js.map +1 -1
  70. package/out/zero-protocol/src/{feature-flag-enum.d.ts → protocol-flag-enum.d.ts} +1 -1
  71. package/out/zero-protocol/src/protocol-flag-enum.d.ts.map +1 -0
  72. package/out/zero-protocol/src/protocol-flag-enum.js +10 -0
  73. package/out/zero-protocol/src/protocol-flag-enum.js.map +1 -0
  74. package/out/zero-protocol/src/protocol-flag.d.ts +14 -0
  75. package/out/zero-protocol/src/protocol-flag.d.ts.map +1 -0
  76. package/out/zero-protocol/src/protocol-flag.js +8 -0
  77. package/out/zero-protocol/src/protocol-flag.js.map +1 -0
  78. package/out/zero-protocol/src/protocol-version.d.ts +3 -3
  79. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  80. package/package.json +1 -1
  81. package/out/shared/src/packed-boolean-map.d.ts +0 -7
  82. package/out/shared/src/packed-boolean-map.d.ts.map +0 -1
  83. package/out/shared/src/packed-boolean-map.js +0 -36
  84. package/out/shared/src/packed-boolean-map.js.map +0 -1
  85. package/out/zero-protocol/src/feature-flag-enum.d.ts.map +0 -1
  86. package/out/zero-protocol/src/feature-flag-enum.js +0 -10
  87. package/out/zero-protocol/src/feature-flag-enum.js.map +0 -1
  88. package/out/zero-protocol/src/feature-flag.d.ts +0 -5
  89. package/out/zero-protocol/src/feature-flag.d.ts.map +0 -1
@@ -13,6 +13,7 @@ import { dropShard, getActiveReplicas, getReplicaAtVersion, getRestoreCandidates
13
13
  import { AutoResetSignal } from "../../change-streamer/schema/tables.js";
14
14
  import { restoreReplica } from "../common/replica-restore.js";
15
15
  import { initReplica } from "../common/replica-schema.js";
16
+ import { endSession } from "./logical-replication/stream.js";
16
17
  import { claimSlotForResumption, createReplicaAndSlot } from "./replication-slots.js";
17
18
  import { ensureShardSchema } from "./schema/init.js";
18
19
  import { initialSync } from "./initial-sync.js";
@@ -23,11 +24,12 @@ import { PostgresChangeSource } from "./change-source.js";
23
24
  * replica, before streaming changes from the corresponding logical replication
24
25
  * stream.
25
26
  */
26
- async function initializePostgresChangeSource(lc, upstreamURI, shard, replicaDbFile, syncOptions, context, lagReportIntervalMs = 0, restoreOptions = {}, { epoch, slotPerReplica, backupV5, inactiveReplicaGracePeriodMs } = {
27
+ async function initializePostgresChangeSource(lc, upstreamURI, shard, replicaDbFile, syncOptions, context, lagReportIntervalMs = 0, restoreOptions = {}, { epoch, slotPerReplica, backupV5, inactiveReplicaGracePeriodMs, pgChangeLog } = {
27
28
  epoch: 0,
28
29
  slotPerReplica: false,
29
30
  backupV5: true,
30
- inactiveReplicaGracePeriodMs: DEFAULT_INACTIVE_REPLICA_GRACE_PERIOD_MS
31
+ inactiveReplicaGracePeriodMs: DEFAULT_INACTIVE_REPLICA_GRACE_PERIOD_MS,
32
+ pgChangeLog: false
31
33
  }, streamInboundTimeoutMs) {
32
34
  const db = await connectPgClient(lc, upstreamURI, "change-source-init");
33
35
  try {
@@ -39,7 +41,8 @@ async function initializePostgresChangeSource(lc, upstreamURI, shard, replicaDbF
39
41
  await purgeLock?.release();
40
42
  initialSyncedReplica = await initialSync(log, shard, tx, upstreamURI, syncOptions, context, {
41
43
  epoch,
42
- backupV5
44
+ backupV5,
45
+ pgChangeLog
43
46
  }, restoreOptions.cleanup);
44
47
  });
45
48
  const replica = new Database(lc, replicaDbFile);
@@ -52,7 +55,8 @@ async function initializePostgresChangeSource(lc, upstreamURI, shard, replicaDbF
52
55
  subscriptionState,
53
56
  changeSource: new PostgresChangeSource(lc, upstreamURI, shard, upstreamReplica, pgVersion, {
54
57
  backupPath,
55
- backupV5
58
+ backupV5,
59
+ pgChangeLog
56
60
  }, context, lagReportIntervalMs, syncOptions.textCopy, streamInboundTimeoutMs, minStartWatermark(subscriptionState.watermark, initialSyncedReplica !== void 0, slotWatermark, purgeLock)),
57
61
  destinationBackupURL: backupPath && restoreOptions.litestream?.backupURL ? new URL(backupPath, restoreOptions.litestream.backupURL).toString() : restoreOptions.litestream?.backupURL,
58
62
  replicaID: upstreamReplica.id,
@@ -108,7 +112,7 @@ async function forkOrResumeReplica(lc, sql, shard, epoch, slotFailover, replicaF
108
112
  purgeLock: null
109
113
  };
110
114
  const { restoreFrom, replicateTo, slotLSN, slotSession } = result;
111
- const slotWatermark = toStateVersionString(slotLSN);
115
+ const slotWatermark = restoreFrom.id === replicateTo.id && restoreFrom.pgChangeLog ? void 0 : toStateVersionString(slotLSN);
112
116
  cleanup?.onFailure(`replication slot ${replicateTo.slot}`, () => slotSession.destroy());
113
117
  const lock = acquirePurgeLock ? await acquirePurgeLock(slotWatermark) : null;
114
118
  const pgChangeLogBehindSlot = lock === "behind-slot";
@@ -162,10 +166,13 @@ async function forkOrResumeReplica(lc, sql, shard, epoch, slotFailover, replicaF
162
166
  * before being stored, and streamed again from the slot), but the stream
163
167
  * never starts below both. (With RMv1.5's purge lock, the change log's head
164
168
  * is at or past the slot, so it does not lower the minimum.)
165
- * * With a shared slot (RMv1), the change log's head when it was purge
166
- * locked, as the stream resumes from the head, which only advances. The
167
- * slot is not a reference: its owner acks it until it is taken over. Without
168
- * a purge lock (i.e. an empty change log), there is no floor.
169
+ * * With a shared slot (RMv1), or a resumed slot that was acked only once
170
+ * transactions were stored in the change log (RMv1.5), the change log's
171
+ * head when it was purge locked, as the stream resumes from the head, which
172
+ * only advances. The slot is not a reference: a shared slot is acked by its
173
+ * owner until it is taken over, and a resumed RMv1.5 slot can be past the
174
+ * head for acked keepalives. Without a purge lock (i.e. an empty change
175
+ * log), there is no floor.
169
176
  */
170
177
  function minStartWatermark(replicaWatermark, initialSynced, slotWatermark, purgeLock) {
171
178
  if (initialSynced) return replicaWatermark;
@@ -205,7 +212,7 @@ async function getSourceAndDestinationReplicas(lc, sql, shard, epoch, slotFailov
205
212
  if (shouldLog("replicas", PROGRESS_LOG_INTERVAL_MS)) lc.info?.(`current replicas at epoch ${epoch}`, { replicas });
206
213
  if (replicas.length === 0) {
207
214
  lc.info?.(`no suitable replicas to restore from`, { replicas });
208
- destination?.initialSession.destroy();
215
+ await endSession(destination?.initialSession);
209
216
  return;
210
217
  }
211
218
  for (const replica of replicas) {
@@ -220,7 +227,8 @@ async function getSourceAndDestinationReplicas(lc, sql, shard, epoch, slotFailov
220
227
  if (active) {
221
228
  destination ??= await createReplicaAndSlot(lc, sql, "fork-replica-session", shard, epoch, Date.now().toString(), slotFailover, {
222
229
  backupPath: null,
223
- backupV5: true
230
+ backupV5: true,
231
+ pgChangeLog: false
224
232
  }, () => promiseVoid, 2);
225
233
  forkLSN ??= await currentWalLSN(sql);
226
234
  if (toBigInt(confirmedFlushLsn) < toBigInt(forkLSN)) {
@@ -253,7 +261,7 @@ async function getSourceAndDestinationReplicas(lc, sql, shard, epoch, slotFailov
253
261
  if (!reserved) lc.warn?.(`unable to resume replica ${replica.id}@${replica.slot}`);
254
262
  else {
255
263
  lc.info?.(`resuming replica ${replica.id}@${replica.slot}`);
256
- destination?.initialSession.destroy();
264
+ await endSession(destination?.initialSession);
257
265
  return {
258
266
  restoreFrom: replica,
259
267
  replicateTo: reserved.replica,
@@ -266,7 +274,7 @@ async function getSourceAndDestinationReplicas(lc, sql, shard, epoch, slotFailov
266
274
  }
267
275
  }
268
276
  } catch (e) {
269
- destination?.initialSession.destroy();
277
+ await endSession(destination?.initialSession);
270
278
  throw e;
271
279
  }
272
280
  }
@@ -1 +1 @@
1
- {"version":3,"file":"change-source-init.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-source/pg/change-source-init.ts"],"sourcesContent":["import type {Readable} from 'node:stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../../shared/src/asserts.ts';\nimport {deepEqual} from '../../../../../shared/src/json.ts';\nimport {must} from '../../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../../shared/src/resolved-promises.ts';\nimport {sleep} from '../../../../../shared/src/sleep.ts';\nimport {Database} from '../../../../../zqlite/src/db.ts';\nimport {StatementRunner} from '../../../db/statements.ts';\nimport {connectPgClient, type PostgresDB} from '../../../types/pg.ts';\nimport {type ShardConfig, type ShardID} from '../../../types/shards.ts';\nimport {AutoResetSignal} from '../../change-streamer/schema/tables.ts';\nimport type {ReplicaConstraints} from '../../litestream/commands.ts';\nimport {\n getSubscriptionStateAndContext,\n type SubscriptionStateAndContext,\n} from '../../replicator/schema/replication-state.ts';\nimport {\n restoreReplica,\n type ConstrainingPurgeLock,\n type InitializeResult,\n type RestoreOptions,\n type ResumableBackup,\n} from '../common/replica-restore.ts';\nimport {initReplica} from '../common/replica-schema.ts';\nimport {PostgresChangeSource} from './change-source.ts';\nimport {\n initialSync,\n type InitialSyncOptions,\n type ReplicaOptions,\n type ServerContext,\n} from './initial-sync.ts';\nimport {toBigInt, toStateVersionString, type LSN} from './lsn.ts';\nimport {\n claimSlotForResumption,\n createReplicaAndSlot,\n type ReplicationSlotResult,\n} from './replication-slots.ts';\nimport {ensureShardSchema} from './schema/init.ts';\nimport * as ReplicaStage from './schema/replica-stage-enum.ts';\nimport {InitialSync, Replicate} from './schema/replica-stage-enum.ts';\nimport {\n dropShard,\n getActiveReplicas,\n getReplicaAtVersion,\n getRestoreCandidates,\n initRestoreReplica,\n internalPublicationPrefix,\n type ReplicaState,\n} from './schema/shard.ts';\n\nexport type InitializeOptions = ReplicaOptions & {\n // Create a new slot for the replica rather than taking over\n // an existing one (i.e. high-availability mode).\n slotPerReplica: boolean | undefined;\n\n inactiveReplicaGracePeriodMs: number;\n};\n\n/**\n * Initializes a Postgres change source, including the initial sync of the\n * replica, before streaming changes from the corresponding logical replication\n * stream.\n */\nexport async function initializePostgresChangeSource(\n lc: LogContext,\n upstreamURI: string,\n shard: ShardConfig,\n replicaDbFile: string,\n syncOptions: InitialSyncOptions,\n context: ServerContext,\n lagReportIntervalMs = 0,\n restoreOptions: RestoreOptions = {},\n {\n epoch,\n slotPerReplica,\n backupV5,\n inactiveReplicaGracePeriodMs,\n }: InitializeOptions = {\n epoch: 0,\n slotPerReplica: false,\n backupV5: true,\n inactiveReplicaGracePeriodMs: DEFAULT_INACTIVE_REPLICA_GRACE_PERIOD_MS,\n },\n streamInboundTimeoutMs?: number | undefined,\n): Promise<InitializeResult> {\n const db = await connectPgClient(lc, upstreamURI, 'change-source-init');\n try {\n await ensureShardSchema(\n lc,\n db,\n shard,\n syncOptions.installPartialIndexTriggers,\n );\n\n const {\n replica: restoredReplica,\n purgeLock,\n pgChangeLogBehindSlot,\n resumableBackup,\n slotWatermark,\n } = slotPerReplica\n ? await forkOrResumeReplica(\n lc,\n db,\n shard,\n epoch,\n syncOptions.replicationSlotFailover ?? false,\n replicaDbFile,\n restoreOptions,\n inactiveReplicaGracePeriodMs,\n )\n : await selectAndRestoreReplica(\n lc,\n db,\n shard,\n replicaDbFile,\n restoreOptions,\n );\n\n let initialSyncedReplica: ReplicaState | undefined;\n await initReplica(\n lc,\n `replica-${shard.appID}-${shard.shardNum}`,\n replicaDbFile,\n async (log, tx) => {\n // Falling back to initial sync abandons the restore, so release the\n // resources acquired for it, e.g. a replication slot claimed (or\n // created) for a resumed (or forked) replica, which would otherwise\n // remain active indefinitely.\n await restoreOptions.cleanup?.release();\n // The purge lock on the change-db must be released before performing\n // initial sync; if the change-db and upstream are the same db, a lock-holding\n // transaction will prevent a replication slot from being created. This awkward\n // dependency can go away with RMv2. (The purge lock is also released by\n // the cleanup when it is registered with it; releasing it is idempotent.)\n await purgeLock?.release();\n initialSyncedReplica = await initialSync(\n log,\n shard,\n tx,\n upstreamURI,\n syncOptions,\n context,\n {epoch, backupV5},\n restoreOptions.cleanup,\n );\n },\n );\n\n const replica = new Database(lc, replicaDbFile);\n const subscriptionState = getSubscriptionStateAndContext(\n new StatementRunner(replica),\n );\n replica.close();\n\n // Check that upstream is properly setup, and throw an AutoReset to re-run\n // initial sync if not.\n const {upstreamReplica, pgVersion} = await checkAndUpdateUpstream(\n lc,\n db,\n shard,\n subscriptionState,\n (initialSyncedReplica ?? restoredReplica)?.id,\n );\n\n // With litestream v5, every replication-manager backs up to its own\n // backup lineage.\n const newBackupLineage = backupV5 && !resumableBackup;\n const backupPath = initialSyncedReplica\n ? // If initial sync was performed, use that initial backupPath.\n initialSyncedReplica.backupPath\n : // Otherwise, use the lineage prepared by the restore, or a new, unique\n // path when backing up with litestream v5. This will be recorded in the\n // replicas table by the PostgresChangeSource.\n backupV5\n ? (resumableBackup?.backupPath ?? String(Date.now()))\n : (restoredReplica?.backupPath ?? null);\n\n const changeSource = new PostgresChangeSource(\n lc,\n upstreamURI,\n shard,\n upstreamReplica,\n pgVersion,\n {backupPath, backupV5},\n context,\n lagReportIntervalMs,\n syncOptions.textCopy,\n streamInboundTimeoutMs,\n minStartWatermark(\n subscriptionState.watermark,\n initialSyncedReplica !== undefined,\n slotWatermark,\n purgeLock,\n ),\n );\n\n const destinationBackupURL =\n backupPath && restoreOptions.litestream?.backupURL\n ? new URL(backupPath, restoreOptions.litestream.backupURL).toString()\n : // For legacy RMv1 replicas (on litestream-v3), backup to the same location\n restoreOptions.litestream?.backupURL;\n\n return {\n subscriptionState,\n changeSource,\n destinationBackupURL,\n // The replica this change stream belongs to. It is part of the identity\n // the SQLite change log records, because a generation (i.e.\n // `replicaVersion`) is shared by every sibling of a forked replica and so\n // cannot distinguish two siblings' logs.\n replicaID: upstreamReplica.id,\n waitForBackupBeforeServing:\n // Wait for the first backup if there was an initial sync,\n initialSyncedReplica !== undefined ||\n // or if the destination differs from where it was restored\n // (i.e. backupV5).\n backupPath !== (restoredReplica?.backupPath ?? null),\n pgChangeLogBehindSlot,\n newBackupLineage,\n walKeeper: resumableBackup?.walKeeper,\n };\n } finally {\n await db.end();\n }\n}\n\ntype RestoredReplica = {\n replica: ReplicaState | undefined;\n purgeLock: ConstrainingPurgeLock | null;\n pgChangeLogBehindSlot?: boolean | undefined;\n resumableBackup?: ResumableBackup | undefined;\n /**\n * The position (as a watermark) of the replication slot from which\n * replication resumes, when it was created (for a fork) or claimed (for a\n * resumption), i.e. while it is held by this process. Absent for a shared\n * slot (RMv1), whose position is advanced by its current owner until it is\n * taken over.\n */\n slotWatermark?: string | undefined;\n};\n\n// RMv1: Selects a replica to restore from and returns it, with the\n// intention of taking over the slot (in the ChangeSource).\nasync function selectAndRestoreReplica(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n replicaFile: string,\n {litestream, acquirePurgeLock, cleanup}: RestoreOptions,\n): Promise<RestoredReplica> {\n // The purge lock constrains the generation of the replica to restore. With\n // a shared replication slot, the change-log is resumed from its head\n // without being checked against the slot's position: the slot is still\n // being acked by the previous replication-manager, on an idle upstream past\n // the change-log's head (see PgChangeLogPurgeLocker).\n const lock = await acquirePurgeLock?.();\n assert(lock !== 'behind-slot', 'unexpected purge-lock result');\n const purgeLock = lock ?? null;\n const replicas = (await getActiveReplicas(lc, sql, shard)).filter(\n // filter to the generation specified by the purge lock, if present\n ({generation}) => generation === (purgeLock?.replicaVersion ?? generation),\n );\n if (replicas.length === 0) {\n lc.info?.(`no suitable replicas to restore from`, {replicas});\n return {replica: undefined, purgeLock};\n }\n const [replica] = replicas;\n\n if (litestream?.backupURL) {\n const constraints = restoreConstraints(purgeLock, replica);\n const {backupURL: backupBaseURL} = litestream;\n const {slot, backupPath, confirmedFlushLsn} = replica;\n const backupURL = new URL(backupPath ?? '', backupBaseURL).toString();\n lc.info?.(\n `restoring replica from ${backupURL} (${slot}@${confirmedFlushLsn})`,\n {replicas},\n );\n const resumableBackup = await restoreReplica(\n lc,\n {...litestream, backupURL}, // includes the replica's backup sub-path\n replicaFile,\n constraints,\n litestream.forkBackup\n ? {type: 'fork', baseURL: backupBaseURL}\n : undefined,\n cleanup,\n );\n return {replica, purgeLock, resumableBackup};\n }\n return {replica, purgeLock};\n}\n\n// RMv2: Restores from an active replica and creates a new replica / slot\n// to continue replication (i.e. \"fork\"). If only orphaned replicas remain,\n// claims a slot, restores the backup, and \"resumes\" replication for that\n// slot.\nasync function forkOrResumeReplica(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n epoch: number,\n slotFailover: boolean,\n replicaFile: string,\n {litestream, acquirePurgeLock, cleanup}: RestoreOptions,\n gracePeriodMs: number,\n): Promise<RestoredReplica> {\n const result = await getSourceAndDestinationReplicas(\n lc,\n sql,\n shard,\n epoch,\n slotFailover,\n gracePeriodMs,\n );\n if (!result) {\n // can't restore, must initial-sync\n return {replica: undefined, purgeLock: null};\n }\n const {restoreFrom, replicateTo, slotLSN, slotSession} = result;\n const slotWatermark = toStateVersionString(slotLSN);\n // The slot is held active by a session in this process until it is taken\n // over by the PostgresChangeSource. If initialization fails before then,\n // the session must be released, or the slot remains active indefinitely,\n // preventing the replica from being resumed (or forked) by a retry.\n cleanup?.onFailure(`replication slot ${replicateTo.slot}`, () =>\n slotSession.destroy(),\n );\n\n // Acquired only after the replication slot has been created (or claimed),\n // and only if the change-log can be resumed from the slot's position.\n //\n // For a fork, this is the case whenever the change-log is maintained (i.e.\n // by a replication-manager that acks the slot only once a transaction has\n // been stored in the change-log): the fork waits for the source replica's\n // slot to pass the position after the creation of the new replica, which is\n // a transaction (in the publication) after the new slot's position.\n //\n // For a resumed slot, the slot can also have been acked past the head of a\n // maintained change-log, for changes outside of the publication (e.g. on an\n // idle upstream). The change-log is then re-initialized unnecessarily, which\n // resets subscribers behind the replica. (This could be avoided by recording\n // the positions acked for such changes in the change-log, e.g. an\n // `ackedThrough` column, and checking the slot's position against that.\n // Resumption is uncommon, however, and involves downtime anyway.)\n const lock = acquirePurgeLock ? await acquirePurgeLock(slotWatermark) : null;\n const pgChangeLogBehindSlot = lock === 'behind-slot';\n const purgeLock = lock === 'behind-slot' ? null : lock;\n // Absent a purge lock, still constrain the restore to the source's\n // generation, so that a local replica left behind by a different\n // generation (e.g. a failed initial sync) is not mistaken for it.\n const constraints = restoreConstraints(purgeLock, restoreFrom);\n\n if (litestream?.backupURL) {\n const {backupURL: backupBaseURL} = litestream;\n const {slot, backupPath, confirmedFlushLsn} = restoreFrom;\n const backupURL = new URL(backupPath ?? '', backupBaseURL).toString();\n lc.info?.(\n `restoring replica from ${backupURL} (${slot}@${confirmedFlushLsn})`,\n {restoreFrom, replicateTo},\n );\n const resumableBackup = await restoreReplica(\n lc,\n {...litestream, backupURL}, // includes the replica's backup sub-path\n replicaFile,\n constraints,\n litestream.forkBackup\n ? {type: 'fork', baseURL: backupBaseURL}\n : undefined,\n cleanup,\n );\n return {\n replica: replicateTo,\n purgeLock,\n pgChangeLogBehindSlot,\n resumableBackup,\n slotWatermark,\n };\n }\n return {\n replica: replicateTo,\n purgeLock,\n pgChangeLogBehindSlot,\n slotWatermark,\n };\n}\n\n/**\n * The watermark below which the replication stream cannot (correctly) be\n * started, i.e. from which it would skip transactions that the change log\n * does not contain. It reflects the order in which each mode stores a\n * transaction and then acks it to the slot, such that a correctly initialized\n * stream always starts at or past it:\n *\n * * After an initial sync, the replica's watermark, which is the position\n * at which the new slot was created, and the change log is (re)initialized\n * at it.\n * * With a slot per replica (RMv1.5 and RMv2), the minimum of the restored\n * replica's watermark and the slot's position. Transactions are only acked\n * once backed up (so a skipped interval ends at or before the replica), and\n * if the change log is behind the slot it is re-initialized from the\n * replica. Note that the slot can be past the replica (acked for keepalives,\n * with nothing skipped), and the replica past the change log (forwarded\n * before being stored, and streamed again from the slot), but the stream\n * never starts below both. (With RMv1.5's purge lock, the change log's head\n * is at or past the slot, so it does not lower the minimum.)\n * * With a shared slot (RMv1), the change log's head when it was purge\n * locked, as the stream resumes from the head, which only advances. The\n * slot is not a reference: its owner acks it until it is taken over. Without\n * a purge lock (i.e. an empty change log), there is no floor.\n */\n// Exported for testing.\nexport function minStartWatermark(\n replicaWatermark: string,\n initialSynced: boolean,\n slotWatermark: string | undefined,\n purgeLock: ConstrainingPurgeLock | null,\n): string | undefined {\n if (initialSynced) {\n return replicaWatermark;\n }\n if (slotWatermark !== undefined) {\n return replicaWatermark < slotWatermark ? replicaWatermark : slotWatermark;\n }\n return purgeLock?.headWatermark;\n}\n\nfunction restoreConstraints(\n purgeLock: ConstrainingPurgeLock | null,\n {generation}: ReplicaState,\n): ReplicaConstraints {\n return purgeLock ?? {replicaVersion: generation, minWatermark: ''};\n}\n\nasync function currentWalLSN(sql: PostgresDB): Promise<LSN> {\n const [{lsn}] = await sql<{lsn: LSN}[]>`SELECT pg_current_wal_lsn() AS lsn`;\n return lsn;\n}\n\nconst REPLICA_POLL_INTERVAL_MS = 500;\n// Polling is frequent so that a fork proceeds as soon as the source replica\n// passes the fork LSN; logging is throttled by time rather than per poll.\nconst PROGRESS_LOG_INTERVAL_MS = 5_000;\nconst SLOW_PROGRESS_LOG_INTERVAL_MS = 60_000;\nconst DEFAULT_INACTIVE_REPLICA_GRACE_PERIOD_MS = 20_000;\n\n// Exported for testing.\nexport async function getSourceAndDestinationReplicas(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n epoch: number,\n slotFailover: boolean,\n gracePeriodMs: number,\n pollIntervalMs = REPLICA_POLL_INTERVAL_MS,\n): Promise<\n | {\n restoreFrom: ReplicaState;\n replicateTo: ReplicaState;\n /** The position of the slot from which replication resumes. */\n slotLSN: LSN;\n /**\n * The session holding the destination slot active until it is taken\n * over by the PostgresChangeSource. Destroying it releases the slot.\n */\n slotSession: Readable;\n }\n | undefined\n> {\n const inactiveSince = new Map<string, number>(); // tracks replica inactivity\n let destination: ReplicationSlotResult<void> | undefined;\n // The position after the creation of the destination replica.\n let forkLSN: LSN | undefined;\n const lastLogged = new Map<string, number>();\n const shouldLog = (key: string, intervalMs: number) => {\n const now = Date.now();\n const last = lastLogged.get(key);\n if (last !== undefined && now - last < intervalMs) {\n return false;\n }\n lastLogged.set(key, now);\n return true;\n };\n\n try {\n for (let i = 0; ; i++) {\n if (i > 0) {\n await sleep(pollIntervalMs);\n }\n const replicas = await getRestoreCandidates(sql, shard, epoch);\n if (shouldLog('replicas', PROGRESS_LOG_INTERVAL_MS)) {\n lc.info?.(`current replicas at epoch ${epoch}`, {replicas});\n }\n if (replicas.length === 0) {\n lc.info?.(`no suitable replicas to restore from`, {replicas});\n destination?.initialSession.destroy();\n return undefined;\n }\n for (const replica of replicas) {\n // Track inactivity to resume replicas after a grace period.\n if (replica.active) {\n inactiveSince.delete(replica.id);\n } else if (!inactiveSince.has(replica.id)) {\n inactiveSince.set(replica.id, Date.now());\n }\n\n // Note: Only `active` InitialSync replicas are returned from\n // getRestoreCandidates().\n if (replica.stage === InitialSync) {\n // Log periodically; initial-sync can be long\n if (\n shouldLog(\n `initial-sync:${replica.id}`,\n SLOW_PROGRESS_LOG_INTERVAL_MS,\n )\n ) {\n lc.info?.(`waiting for initial sync of ${replica.id}`, {replica});\n }\n break;\n }\n\n if (replica.stage === Replicate) {\n const {active, confirmedFlushLsn} = replica;\n if (active) {\n // Create a replication slot to fork the active replica.\n destination ??= await createReplicaAndSlot(\n lc,\n sql,\n 'fork-replica-session',\n shard,\n epoch,\n Date.now().toString(), // replicaID\n slotFailover,\n {\n backupPath: null, // set only after the backup has been confirmed\n backupV5: true, // RMv2 requires backupV5\n },\n () => promiseVoid,\n ReplicaStage.Restore,\n );\n // The replica row of the destination is inserted in a transaction\n // (in the publication) after the destination slot's position.\n // Waiting for the source replica's slot to pass it (rather than\n // just the destination slot's position) ensures that a source\n // replica that maintains the PG change-log has stored a\n // transaction past the destination slot's position, i.e. that the\n // change-log can be resumed from the destination slot.\n forkLSN ??= await currentWalLSN(sql);\n if (toBigInt(confirmedFlushLsn) < toBigInt(forkLSN)) {\n if (shouldLog(`fork:${replica.id}`, PROGRESS_LOG_INTERVAL_MS)) {\n lc.info?.(\n `waiting for ${replica.id}@${confirmedFlushLsn} to reach ${forkLSN} (after ${destination.slot.slot_name}@${destination.slot.consistent_point})`,\n {replica},\n );\n }\n break;\n }\n }\n\n // If the replica is past the destination LSN, fork it, regardless of\n // whether its slot is still active. Forking is preferable to resuming,\n // as resuming removes the replica from being a candidate for\n // subsequent forks, and carries the risk of stealing the slot from a\n // task attempting to reconnect.\n if (\n destination &&\n forkLSN &&\n toBigInt(confirmedFlushLsn) >= toBigInt(forkLSN)\n ) {\n lc.info?.(`forking replica ${replica.id}@${replica.slot}`, {\n replica,\n });\n const replicateTo = must(\n await initRestoreReplica(sql, shard, {\n sourceID: replica.id,\n destID: destination.replica.id,\n }),\n `replica ${destination.replica.id} was deleted`,\n );\n return {\n restoreFrom: replica,\n replicateTo,\n slotLSN: destination.slot.consistent_point,\n slotSession: destination.initialSession,\n };\n }\n }\n\n if (\n replica.stage === ReplicaStage.Restore &&\n replica.active &&\n replica.id !== destination?.replica.id\n ) {\n // Another task is restoring (e.g. resuming) this replica.\n if (\n shouldLog(`restoring:${replica.id}`, SLOW_PROGRESS_LOG_INTERVAL_MS)\n ) {\n lc.info?.(\n `waiting for replica ${replica.id}@${replica.slot} being restored by another task`,\n {replica},\n );\n }\n }\n\n if (replica.backupPath && !replica.active) {\n // An inactive replica that has a backupPath may be orphaned, or it\n // may be an active task that was temporarily disconnected and\n // attempting to reestablish a session.\n //\n // Resume the replica after a grace period to avoid stealing the slot\n // from an active task that was temporarily disconnected. Resumption\n // is a last resort only taken in the absence of alternatives.\n const now = Date.now();\n const inactiveMs = now - (inactiveSince.get(replica.id) ?? now);\n if (\n inactiveMs >= gracePeriodMs ||\n shouldLog(`inactive:${replica.id}`, PROGRESS_LOG_INTERVAL_MS)\n ) {\n lc.info?.(\n `replica ${replica.id}@${replica.slot} has been inactive for ${inactiveMs}ms`,\n {replica},\n );\n }\n if (inactiveMs >= gracePeriodMs) {\n const reserved = await claimSlotForResumption(\n lc,\n sql,\n shard,\n 'resume-replica',\n replica,\n );\n if (!reserved) {\n lc.warn?.(\n `unable to resume replica ${replica.id}@${replica.slot}`,\n );\n } else {\n lc.info?.(`resuming replica ${replica.id}@${replica.slot}`);\n // If a new replication slot was created in anticipation of forking,\n // cancel it\n destination?.initialSession.destroy();\n return {\n restoreFrom: replica,\n replicateTo: reserved.replica,\n // The slot is inactive, so its position is fixed.\n slotLSN: replica.confirmedFlushLsn,\n slotSession: reserved.reservation,\n };\n }\n }\n }\n }\n }\n } catch (e) {\n destination?.initialSession.destroy();\n throw e;\n }\n}\n\nasync function checkAndUpdateUpstream(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardConfig,\n {\n replicaVersion,\n publications: subscribed,\n initialSyncContext,\n }: SubscriptionStateAndContext,\n replicaID: string | undefined,\n) {\n const upstreamReplica = await getReplicaAtVersion(\n lc,\n sql,\n shard,\n replicaVersion,\n replicaID,\n initialSyncContext,\n );\n if (!upstreamReplica) {\n throw new AutoResetSignal(\n `No replication slot for replica at version ${replicaVersion} and id ${replicaID}}`,\n );\n }\n\n // Verify that the publications match what is being replicated.\n const requested = shard.publications.toSorted();\n const replicated = upstreamReplica.publications\n .filter(p => !p.startsWith(internalPublicationPrefix(shard)))\n .sort();\n if (!deepEqual(requested, replicated)) {\n lc.warn?.(`Dropping shard to change publications to: [${requested}]`);\n await sql.unsafe(dropShard(shard.appID, shard.shardNum));\n throw new AutoResetSignal(\n `Requested publications [${requested}] do not match configured ` +\n `publications: [${replicated}]`,\n );\n }\n\n // Sanity check: The subscription state on the replica should have the\n // same publications. This should be guaranteed by the equivalence of the\n // replicaVersion, but it doesn't hurt to verify.\n if (!deepEqual(upstreamReplica.publications, subscribed)) {\n throw new AutoResetSignal(\n `Upstream publications [${upstreamReplica.publications}] do not ` +\n `match subscribed publications [${subscribed}]`,\n );\n }\n\n // Verify that the publications exist.\n const exists = await sql`\n SELECT pubname FROM pg_publication WHERE pubname IN ${sql(subscribed)};\n `.values();\n if (exists.length !== subscribed.length) {\n throw new AutoResetSignal(\n `Upstream publications [${exists.flat()}] do not contain ` +\n `all subscribed publications [${subscribed}]`,\n );\n }\n\n const {slot} = upstreamReplica;\n const result = await sql<{restartLSN: LSN | null; walStatus: string | null}[]>\n /*sql*/ `\n SELECT restart_lsn as \"restartLSN\", wal_status as \"walStatus\" FROM pg_replication_slots\n WHERE slot_name = ${slot}`;\n if (result.length === 0) {\n throw new AutoResetSignal(`replication slot ${slot} is missing`);\n }\n const [{restartLSN, walStatus}] = result;\n if (restartLSN === null || walStatus === 'lost') {\n throw new AutoResetSignal(\n `replication slot ${slot} has been invalidated for exceeding the max_slot_wal_keep_size`,\n );\n }\n const [{pgVersion}] = await sql<{pgVersion: number}[]> /*sql*/ `\n SELECT current_setting('server_version_num')::int as \"pgVersion\"`;\n return {upstreamReplica, pgVersion};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"change-source-init.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-source/pg/change-source-init.ts"],"sourcesContent":["import type {Readable} from 'node:stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../../shared/src/asserts.ts';\nimport {deepEqual} from '../../../../../shared/src/json.ts';\nimport {must} from '../../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../../shared/src/resolved-promises.ts';\nimport {sleep} from '../../../../../shared/src/sleep.ts';\nimport {Database} from '../../../../../zqlite/src/db.ts';\nimport {StatementRunner} from '../../../db/statements.ts';\nimport {connectPgClient, type PostgresDB} from '../../../types/pg.ts';\nimport {type ShardConfig, type ShardID} from '../../../types/shards.ts';\nimport {AutoResetSignal} from '../../change-streamer/schema/tables.ts';\nimport type {ReplicaConstraints} from '../../litestream/commands.ts';\nimport {\n getSubscriptionStateAndContext,\n type SubscriptionStateAndContext,\n} from '../../replicator/schema/replication-state.ts';\nimport {\n restoreReplica,\n type ConstrainingPurgeLock,\n type InitializeResult,\n type RestoreOptions,\n type ResumableBackup,\n} from '../common/replica-restore.ts';\nimport {initReplica} from '../common/replica-schema.ts';\nimport {PostgresChangeSource} from './change-source.ts';\nimport {\n initialSync,\n type InitialSyncOptions,\n type ReplicaOptions,\n type ServerContext,\n} from './initial-sync.ts';\nimport {endSession} from './logical-replication/stream.ts';\nimport {toBigInt, toStateVersionString, type LSN} from './lsn.ts';\nimport {\n claimSlotForResumption,\n createReplicaAndSlot,\n type ReplicationSlotResult,\n} from './replication-slots.ts';\nimport {ensureShardSchema} from './schema/init.ts';\nimport * as ReplicaStage from './schema/replica-stage-enum.ts';\nimport {InitialSync, Replicate} from './schema/replica-stage-enum.ts';\nimport {\n dropShard,\n getActiveReplicas,\n getReplicaAtVersion,\n getRestoreCandidates,\n initRestoreReplica,\n internalPublicationPrefix,\n type ReplicaState,\n} from './schema/shard.ts';\n\nexport type InitializeOptions = ReplicaOptions & {\n // Create a new slot for the replica rather than taking over\n // an existing one (i.e. high-availability mode).\n slotPerReplica: boolean | undefined;\n\n inactiveReplicaGracePeriodMs: number;\n\n /**\n * Whether this replication-manager maintains the PG change log, i.e. acks\n * the replication slot only after transactions are stored in it. Recorded\n * with the replica (see `pgChangeLog` in `ReplicaState`) when its slot is\n * taken over.\n */\n pgChangeLog: boolean;\n};\n\n/**\n * Initializes a Postgres change source, including the initial sync of the\n * replica, before streaming changes from the corresponding logical replication\n * stream.\n */\nexport async function initializePostgresChangeSource(\n lc: LogContext,\n upstreamURI: string,\n shard: ShardConfig,\n replicaDbFile: string,\n syncOptions: InitialSyncOptions,\n context: ServerContext,\n lagReportIntervalMs = 0,\n restoreOptions: RestoreOptions = {},\n {\n epoch,\n slotPerReplica,\n backupV5,\n inactiveReplicaGracePeriodMs,\n pgChangeLog,\n }: InitializeOptions = {\n epoch: 0,\n slotPerReplica: false,\n backupV5: true,\n inactiveReplicaGracePeriodMs: DEFAULT_INACTIVE_REPLICA_GRACE_PERIOD_MS,\n pgChangeLog: false,\n },\n streamInboundTimeoutMs?: number | undefined,\n): Promise<InitializeResult> {\n const db = await connectPgClient(lc, upstreamURI, 'change-source-init');\n try {\n await ensureShardSchema(\n lc,\n db,\n shard,\n syncOptions.installPartialIndexTriggers,\n );\n\n const {\n replica: restoredReplica,\n purgeLock,\n pgChangeLogBehindSlot,\n resumableBackup,\n slotWatermark,\n } = slotPerReplica\n ? await forkOrResumeReplica(\n lc,\n db,\n shard,\n epoch,\n syncOptions.replicationSlotFailover ?? false,\n replicaDbFile,\n restoreOptions,\n inactiveReplicaGracePeriodMs,\n )\n : await selectAndRestoreReplica(\n lc,\n db,\n shard,\n replicaDbFile,\n restoreOptions,\n );\n\n let initialSyncedReplica: ReplicaState | undefined;\n await initReplica(\n lc,\n `replica-${shard.appID}-${shard.shardNum}`,\n replicaDbFile,\n async (log, tx) => {\n // Falling back to initial sync abandons the restore, so release the\n // resources acquired for it, e.g. a replication slot claimed (or\n // created) for a resumed (or forked) replica, which would otherwise\n // remain active indefinitely.\n await restoreOptions.cleanup?.release();\n // The purge lock on the change-db must be released before performing\n // initial sync; if the change-db and upstream are the same db, a lock-holding\n // transaction will prevent a replication slot from being created. This awkward\n // dependency can go away with RMv2. (The purge lock is also released by\n // the cleanup when it is registered with it; releasing it is idempotent.)\n await purgeLock?.release();\n initialSyncedReplica = await initialSync(\n log,\n shard,\n tx,\n upstreamURI,\n syncOptions,\n context,\n {epoch, backupV5, pgChangeLog},\n restoreOptions.cleanup,\n );\n },\n );\n\n const replica = new Database(lc, replicaDbFile);\n const subscriptionState = getSubscriptionStateAndContext(\n new StatementRunner(replica),\n );\n replica.close();\n\n // Check that upstream is properly setup, and throw an AutoReset to re-run\n // initial sync if not.\n const {upstreamReplica, pgVersion} = await checkAndUpdateUpstream(\n lc,\n db,\n shard,\n subscriptionState,\n (initialSyncedReplica ?? restoredReplica)?.id,\n );\n\n // With litestream v5, every replication-manager backs up to its own\n // backup lineage.\n const newBackupLineage = backupV5 && !resumableBackup;\n const backupPath = initialSyncedReplica\n ? // If initial sync was performed, use that initial backupPath.\n initialSyncedReplica.backupPath\n : // Otherwise, use the lineage prepared by the restore, or a new, unique\n // path when backing up with litestream v5. This will be recorded in the\n // replicas table by the PostgresChangeSource.\n backupV5\n ? (resumableBackup?.backupPath ?? String(Date.now()))\n : (restoredReplica?.backupPath ?? null);\n\n const changeSource = new PostgresChangeSource(\n lc,\n upstreamURI,\n shard,\n upstreamReplica,\n pgVersion,\n {backupPath, backupV5, pgChangeLog},\n context,\n lagReportIntervalMs,\n syncOptions.textCopy,\n streamInboundTimeoutMs,\n minStartWatermark(\n subscriptionState.watermark,\n initialSyncedReplica !== undefined,\n slotWatermark,\n purgeLock,\n ),\n );\n\n const destinationBackupURL =\n backupPath && restoreOptions.litestream?.backupURL\n ? new URL(backupPath, restoreOptions.litestream.backupURL).toString()\n : // For legacy RMv1 replicas (on litestream-v3), backup to the same location\n restoreOptions.litestream?.backupURL;\n\n return {\n subscriptionState,\n changeSource,\n destinationBackupURL,\n // The replica this change stream belongs to. It is part of the identity\n // the SQLite change log records, because a generation (i.e.\n // `replicaVersion`) is shared by every sibling of a forked replica and so\n // cannot distinguish two siblings' logs.\n replicaID: upstreamReplica.id,\n waitForBackupBeforeServing:\n // Wait for the first backup if there was an initial sync,\n initialSyncedReplica !== undefined ||\n // or if the destination differs from where it was restored\n // (i.e. backupV5).\n backupPath !== (restoredReplica?.backupPath ?? null),\n pgChangeLogBehindSlot,\n newBackupLineage,\n walKeeper: resumableBackup?.walKeeper,\n };\n } finally {\n await db.end();\n }\n}\n\ntype RestoredReplica = {\n replica: ReplicaState | undefined;\n purgeLock: ConstrainingPurgeLock | null;\n pgChangeLogBehindSlot?: boolean | undefined;\n resumableBackup?: ResumableBackup | undefined;\n /**\n * The position (as a watermark) of the replication slot from which\n * replication resumes, when it was created (for a fork) or claimed (for a\n * resumption), i.e. while it is held by this process. Absent for a shared\n * slot (RMv1), whose position is advanced by its current owner until it is\n * taken over, and for a resumed slot that was acked only once transactions\n * were stored in the PG change-log (RMv1.5), whose position may be past the\n * change-log's head for acked keepalives.\n */\n slotWatermark?: string | undefined;\n};\n\n// RMv1: Selects a replica to restore from and returns it, with the\n// intention of taking over the slot (in the ChangeSource).\nasync function selectAndRestoreReplica(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n replicaFile: string,\n {litestream, acquirePurgeLock, cleanup}: RestoreOptions,\n): Promise<RestoredReplica> {\n // The purge lock constrains the generation of the replica to restore. With\n // a shared replication slot, the change-log is resumed from its head\n // without being checked against the slot's position: the slot is still\n // being acked by the previous replication-manager, on an idle upstream past\n // the change-log's head (see PgChangeLogPurgeLocker).\n const lock = await acquirePurgeLock?.();\n assert(lock !== 'behind-slot', 'unexpected purge-lock result');\n const purgeLock = lock ?? null;\n const replicas = (await getActiveReplicas(lc, sql, shard)).filter(\n // filter to the generation specified by the purge lock, if present\n ({generation}) => generation === (purgeLock?.replicaVersion ?? generation),\n );\n if (replicas.length === 0) {\n lc.info?.(`no suitable replicas to restore from`, {replicas});\n return {replica: undefined, purgeLock};\n }\n const [replica] = replicas;\n\n if (litestream?.backupURL) {\n const constraints = restoreConstraints(purgeLock, replica);\n const {backupURL: backupBaseURL} = litestream;\n const {slot, backupPath, confirmedFlushLsn} = replica;\n const backupURL = new URL(backupPath ?? '', backupBaseURL).toString();\n lc.info?.(\n `restoring replica from ${backupURL} (${slot}@${confirmedFlushLsn})`,\n {replicas},\n );\n const resumableBackup = await restoreReplica(\n lc,\n {...litestream, backupURL}, // includes the replica's backup sub-path\n replicaFile,\n constraints,\n litestream.forkBackup\n ? {type: 'fork', baseURL: backupBaseURL}\n : undefined,\n cleanup,\n );\n return {replica, purgeLock, resumableBackup};\n }\n return {replica, purgeLock};\n}\n\n// RMv2: Restores from an active replica and creates a new replica / slot\n// to continue replication (i.e. \"fork\"). If only orphaned replicas remain,\n// claims a slot, restores the backup, and \"resumes\" replication for that\n// slot.\nasync function forkOrResumeReplica(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n epoch: number,\n slotFailover: boolean,\n replicaFile: string,\n {litestream, acquirePurgeLock, cleanup}: RestoreOptions,\n gracePeriodMs: number,\n): Promise<RestoredReplica> {\n const result = await getSourceAndDestinationReplicas(\n lc,\n sql,\n shard,\n epoch,\n slotFailover,\n gracePeriodMs,\n );\n if (!result) {\n // can't restore, must initial-sync\n return {replica: undefined, purgeLock: null};\n }\n const {restoreFrom, replicateTo, slotLSN, slotSession} = result;\n // A resumed slot (i.e. of the same replica) that was acked only once\n // transactions were stored in the PG change-log (RMv1.5) cannot have been\n // acked past a transaction missing from the change-log. Its position past\n // the change-log's head only reflects acked keepalives, so it is not\n // compared with the head (see below).\n const resumedAfterPgChangeLog =\n restoreFrom.id === replicateTo.id && restoreFrom.pgChangeLog;\n const slotWatermark = resumedAfterPgChangeLog\n ? undefined\n : toStateVersionString(slotLSN);\n // The slot is held active by a session in this process until it is taken\n // over by the PostgresChangeSource. If initialization fails before then,\n // the session must be released, or the slot remains active indefinitely,\n // preventing the replica from being resumed (or forked) by a retry.\n cleanup?.onFailure(`replication slot ${replicateTo.slot}`, () =>\n slotSession.destroy(),\n );\n\n // Acquired only after the replication slot has been created (or claimed),\n // and only if the change-log can be resumed from the slot's position.\n //\n // For a fork, this is the case whenever the change-log is maintained (i.e.\n // by a replication-manager that acks the slot only once a transaction has\n // been stored in the change-log): the fork waits for the source replica's\n // slot to pass the position after the creation of the new replica, which is\n // a transaction (in the publication) after the new slot's position.\n //\n // For a resumed slot, the slot can also have been acked past the head of a\n // maintained change-log, for keepalives (e.g. for changes outside of the\n // publication, or on an idle upstream). If the slot was acked by a\n // replication-manager that maintains the change-log, it is therefore not\n // checked against the slot's position, which would otherwise re-initialize\n // the change-log unnecessarily, resetting subscribers behind the replica.\n // Otherwise (e.g. RMv2, or unknown), it is: the change-log is then\n // re-initialized from the replica if it does not contain the replica's\n // changes.\n const lock = acquirePurgeLock ? await acquirePurgeLock(slotWatermark) : null;\n const pgChangeLogBehindSlot = lock === 'behind-slot';\n const purgeLock = lock === 'behind-slot' ? null : lock;\n // Absent a purge lock, still constrain the restore to the source's\n // generation, so that a local replica left behind by a different\n // generation (e.g. a failed initial sync) is not mistaken for it.\n const constraints = restoreConstraints(purgeLock, restoreFrom);\n\n if (litestream?.backupURL) {\n const {backupURL: backupBaseURL} = litestream;\n const {slot, backupPath, confirmedFlushLsn} = restoreFrom;\n const backupURL = new URL(backupPath ?? '', backupBaseURL).toString();\n lc.info?.(\n `restoring replica from ${backupURL} (${slot}@${confirmedFlushLsn})`,\n {restoreFrom, replicateTo},\n );\n const resumableBackup = await restoreReplica(\n lc,\n {...litestream, backupURL}, // includes the replica's backup sub-path\n replicaFile,\n constraints,\n litestream.forkBackup\n ? {type: 'fork', baseURL: backupBaseURL}\n : undefined,\n cleanup,\n );\n return {\n replica: replicateTo,\n purgeLock,\n pgChangeLogBehindSlot,\n resumableBackup,\n slotWatermark,\n };\n }\n return {\n replica: replicateTo,\n purgeLock,\n pgChangeLogBehindSlot,\n slotWatermark,\n };\n}\n\n/**\n * The watermark below which the replication stream cannot (correctly) be\n * started, i.e. from which it would skip transactions that the change log\n * does not contain. It reflects the order in which each mode stores a\n * transaction and then acks it to the slot, such that a correctly initialized\n * stream always starts at or past it:\n *\n * * After an initial sync, the replica's watermark, which is the position\n * at which the new slot was created, and the change log is (re)initialized\n * at it.\n * * With a slot per replica (RMv1.5 and RMv2), the minimum of the restored\n * replica's watermark and the slot's position. Transactions are only acked\n * once backed up (so a skipped interval ends at or before the replica), and\n * if the change log is behind the slot it is re-initialized from the\n * replica. Note that the slot can be past the replica (acked for keepalives,\n * with nothing skipped), and the replica past the change log (forwarded\n * before being stored, and streamed again from the slot), but the stream\n * never starts below both. (With RMv1.5's purge lock, the change log's head\n * is at or past the slot, so it does not lower the minimum.)\n * * With a shared slot (RMv1), or a resumed slot that was acked only once\n * transactions were stored in the change log (RMv1.5), the change log's\n * head when it was purge locked, as the stream resumes from the head, which\n * only advances. The slot is not a reference: a shared slot is acked by its\n * owner until it is taken over, and a resumed RMv1.5 slot can be past the\n * head for acked keepalives. Without a purge lock (i.e. an empty change\n * log), there is no floor.\n */\n// Exported for testing.\nexport function minStartWatermark(\n replicaWatermark: string,\n initialSynced: boolean,\n slotWatermark: string | undefined,\n purgeLock: ConstrainingPurgeLock | null,\n): string | undefined {\n if (initialSynced) {\n return replicaWatermark;\n }\n if (slotWatermark !== undefined) {\n return replicaWatermark < slotWatermark ? replicaWatermark : slotWatermark;\n }\n return purgeLock?.headWatermark;\n}\n\nfunction restoreConstraints(\n purgeLock: ConstrainingPurgeLock | null,\n {generation}: ReplicaState,\n): ReplicaConstraints {\n return purgeLock ?? {replicaVersion: generation, minWatermark: ''};\n}\n\nasync function currentWalLSN(sql: PostgresDB): Promise<LSN> {\n const [{lsn}] = await sql<{lsn: LSN}[]>`SELECT pg_current_wal_lsn() AS lsn`;\n return lsn;\n}\n\nconst REPLICA_POLL_INTERVAL_MS = 500;\n// Polling is frequent so that a fork proceeds as soon as the source replica\n// passes the fork LSN; logging is throttled by time rather than per poll.\nconst PROGRESS_LOG_INTERVAL_MS = 5_000;\nconst SLOW_PROGRESS_LOG_INTERVAL_MS = 60_000;\nconst DEFAULT_INACTIVE_REPLICA_GRACE_PERIOD_MS = 20_000;\n\n// Exported for testing.\nexport async function getSourceAndDestinationReplicas(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n epoch: number,\n slotFailover: boolean,\n gracePeriodMs: number,\n pollIntervalMs = REPLICA_POLL_INTERVAL_MS,\n): Promise<\n | {\n restoreFrom: ReplicaState;\n replicateTo: ReplicaState;\n /** The position of the slot from which replication resumes. */\n slotLSN: LSN;\n /**\n * The session holding the destination slot active until it is taken\n * over by the PostgresChangeSource. Destroying it releases the slot.\n */\n slotSession: Readable;\n }\n | undefined\n> {\n const inactiveSince = new Map<string, number>(); // tracks replica inactivity\n let destination: ReplicationSlotResult<void> | undefined;\n // The position after the creation of the destination replica.\n let forkLSN: LSN | undefined;\n const lastLogged = new Map<string, number>();\n const shouldLog = (key: string, intervalMs: number) => {\n const now = Date.now();\n const last = lastLogged.get(key);\n if (last !== undefined && now - last < intervalMs) {\n return false;\n }\n lastLogged.set(key, now);\n return true;\n };\n\n try {\n for (let i = 0; ; i++) {\n if (i > 0) {\n await sleep(pollIntervalMs);\n }\n const replicas = await getRestoreCandidates(sql, shard, epoch);\n if (shouldLog('replicas', PROGRESS_LOG_INTERVAL_MS)) {\n lc.info?.(`current replicas at epoch ${epoch}`, {replicas});\n }\n if (replicas.length === 0) {\n lc.info?.(`no suitable replicas to restore from`, {replicas});\n await endSession(destination?.initialSession);\n return undefined;\n }\n for (const replica of replicas) {\n // Track inactivity to resume replicas after a grace period.\n if (replica.active) {\n inactiveSince.delete(replica.id);\n } else if (!inactiveSince.has(replica.id)) {\n inactiveSince.set(replica.id, Date.now());\n }\n\n // Note: Only `active` InitialSync replicas are returned from\n // getRestoreCandidates().\n if (replica.stage === InitialSync) {\n // Log periodically; initial-sync can be long\n if (\n shouldLog(\n `initial-sync:${replica.id}`,\n SLOW_PROGRESS_LOG_INTERVAL_MS,\n )\n ) {\n lc.info?.(`waiting for initial sync of ${replica.id}`, {replica});\n }\n break;\n }\n\n if (replica.stage === Replicate) {\n const {active, confirmedFlushLsn} = replica;\n if (active) {\n // Create a replication slot to fork the active replica.\n destination ??= await createReplicaAndSlot(\n lc,\n sql,\n 'fork-replica-session',\n shard,\n epoch,\n Date.now().toString(), // replicaID\n slotFailover,\n {\n backupPath: null, // set only after the backup has been confirmed\n backupV5: true, // RMv2 requires backupV5\n // The slot is not acked until it is taken over, which sets\n // this. Until then, it is unknown.\n pgChangeLog: false,\n },\n () => promiseVoid,\n ReplicaStage.Restore,\n );\n // The replica row of the destination is inserted in a transaction\n // (in the publication) after the destination slot's position.\n // Waiting for the source replica's slot to pass it (rather than\n // just the destination slot's position) ensures that a source\n // replica that maintains the PG change-log has stored a\n // transaction past the destination slot's position, i.e. that the\n // change-log can be resumed from the destination slot.\n forkLSN ??= await currentWalLSN(sql);\n if (toBigInt(confirmedFlushLsn) < toBigInt(forkLSN)) {\n if (shouldLog(`fork:${replica.id}`, PROGRESS_LOG_INTERVAL_MS)) {\n lc.info?.(\n `waiting for ${replica.id}@${confirmedFlushLsn} to reach ${forkLSN} (after ${destination.slot.slot_name}@${destination.slot.consistent_point})`,\n {replica},\n );\n }\n break;\n }\n }\n\n // If the replica is past the destination LSN, fork it, regardless of\n // whether its slot is still active. Forking is preferable to resuming,\n // as resuming removes the replica from being a candidate for\n // subsequent forks, and carries the risk of stealing the slot from a\n // task attempting to reconnect.\n if (\n destination &&\n forkLSN &&\n toBigInt(confirmedFlushLsn) >= toBigInt(forkLSN)\n ) {\n lc.info?.(`forking replica ${replica.id}@${replica.slot}`, {\n replica,\n });\n const replicateTo = must(\n await initRestoreReplica(sql, shard, {\n sourceID: replica.id,\n destID: destination.replica.id,\n }),\n `replica ${destination.replica.id} was deleted`,\n );\n return {\n restoreFrom: replica,\n replicateTo,\n slotLSN: destination.slot.consistent_point,\n slotSession: destination.initialSession,\n };\n }\n }\n\n if (\n replica.stage === ReplicaStage.Restore &&\n replica.active &&\n replica.id !== destination?.replica.id\n ) {\n // Another task is restoring (e.g. resuming) this replica.\n if (\n shouldLog(`restoring:${replica.id}`, SLOW_PROGRESS_LOG_INTERVAL_MS)\n ) {\n lc.info?.(\n `waiting for replica ${replica.id}@${replica.slot} being restored by another task`,\n {replica},\n );\n }\n }\n\n if (replica.backupPath && !replica.active) {\n // An inactive replica that has a backupPath may be orphaned, or it\n // may be an active task that was temporarily disconnected and\n // attempting to reestablish a session.\n //\n // Resume the replica after a grace period to avoid stealing the slot\n // from an active task that was temporarily disconnected. Resumption\n // is a last resort only taken in the absence of alternatives.\n const now = Date.now();\n const inactiveMs = now - (inactiveSince.get(replica.id) ?? now);\n if (\n inactiveMs >= gracePeriodMs ||\n shouldLog(`inactive:${replica.id}`, PROGRESS_LOG_INTERVAL_MS)\n ) {\n lc.info?.(\n `replica ${replica.id}@${replica.slot} has been inactive for ${inactiveMs}ms`,\n {replica},\n );\n }\n if (inactiveMs >= gracePeriodMs) {\n const reserved = await claimSlotForResumption(\n lc,\n sql,\n shard,\n 'resume-replica',\n replica,\n );\n if (!reserved) {\n lc.warn?.(\n `unable to resume replica ${replica.id}@${replica.slot}`,\n );\n } else {\n lc.info?.(`resuming replica ${replica.id}@${replica.slot}`);\n // If a new replication slot was created in anticipation of forking,\n // cancel it\n await endSession(destination?.initialSession);\n return {\n restoreFrom: replica,\n replicateTo: reserved.replica,\n // The slot is inactive, so its position is fixed.\n slotLSN: replica.confirmedFlushLsn,\n slotSession: reserved.reservation,\n };\n }\n }\n }\n }\n }\n } catch (e) {\n await endSession(destination?.initialSession);\n throw e;\n }\n}\n\nasync function checkAndUpdateUpstream(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardConfig,\n {\n replicaVersion,\n publications: subscribed,\n initialSyncContext,\n }: SubscriptionStateAndContext,\n replicaID: string | undefined,\n) {\n const upstreamReplica = await getReplicaAtVersion(\n lc,\n sql,\n shard,\n replicaVersion,\n replicaID,\n initialSyncContext,\n );\n if (!upstreamReplica) {\n throw new AutoResetSignal(\n `No replication slot for replica at version ${replicaVersion} and id ${replicaID}}`,\n );\n }\n\n // Verify that the publications match what is being replicated.\n const requested = shard.publications.toSorted();\n const replicated = upstreamReplica.publications\n .filter(p => !p.startsWith(internalPublicationPrefix(shard)))\n .sort();\n if (!deepEqual(requested, replicated)) {\n lc.warn?.(`Dropping shard to change publications to: [${requested}]`);\n await sql.unsafe(dropShard(shard.appID, shard.shardNum));\n throw new AutoResetSignal(\n `Requested publications [${requested}] do not match configured ` +\n `publications: [${replicated}]`,\n );\n }\n\n // Sanity check: The subscription state on the replica should have the\n // same publications. This should be guaranteed by the equivalence of the\n // replicaVersion, but it doesn't hurt to verify.\n if (!deepEqual(upstreamReplica.publications, subscribed)) {\n throw new AutoResetSignal(\n `Upstream publications [${upstreamReplica.publications}] do not ` +\n `match subscribed publications [${subscribed}]`,\n );\n }\n\n // Verify that the publications exist.\n const exists = await sql`\n SELECT pubname FROM pg_publication WHERE pubname IN ${sql(subscribed)};\n `.values();\n if (exists.length !== subscribed.length) {\n throw new AutoResetSignal(\n `Upstream publications [${exists.flat()}] do not contain ` +\n `all subscribed publications [${subscribed}]`,\n );\n }\n\n const {slot} = upstreamReplica;\n const result = await sql<{restartLSN: LSN | null; walStatus: string | null}[]>\n /*sql*/ `\n SELECT restart_lsn as \"restartLSN\", wal_status as \"walStatus\" FROM pg_replication_slots\n WHERE slot_name = ${slot}`;\n if (result.length === 0) {\n throw new AutoResetSignal(`replication slot ${slot} is missing`);\n }\n const [{restartLSN, walStatus}] = result;\n if (restartLSN === null || walStatus === 'lost') {\n throw new AutoResetSignal(\n `replication slot ${slot} has been invalidated for exceeding the max_slot_wal_keep_size`,\n );\n }\n const [{pgVersion}] = await sql<{pgVersion: number}[]> /*sql*/ `\n SELECT current_setting('server_version_num')::int as \"pgVersion\"`;\n return {upstreamReplica, pgVersion};\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"change-source.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-source/pg/change-source.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"change-source.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-source/pg/change-source.ts"],"names":[],"mappings":"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"}
@@ -242,7 +242,8 @@ var PostgresChangeSource = class {
242
242
  SET "stage" = ${1},
243
243
  "subscriberContext" = ${this.#context},
244
244
  "backupPath" = ${this.#backupOptions.backupPath},
245
- "backupV5" = ${this.#backupOptions.backupV5}
245
+ "backupV5" = ${this.#backupOptions.backupV5},
246
+ "pgChangeLog" = ${this.#backupOptions.pgChangeLog}
246
247
  WHERE id = ${replicaID}`;
247
248
  const result = await sql`
248
249
  SELECT pg_terminate_backend(active_pid) as terminated, active_pid as pid