@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
@@ -1 +1 @@
1
- {"version":3,"file":"stream.js","names":["#lc","#start","#restartLSN","#confirmedFlushLSN","#startTime","#done","#first"],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/logical-replication/stream.ts"],"sourcesContent":["import {\n PG_ADMIN_SHUTDOWN,\n PG_OBJECT_IN_USE,\n PG_OBJECT_NOT_IN_PREREQUISITE_STATE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {defu} from 'defu';\nimport postgres, {type Options, type PostgresType} from 'postgres';\nimport {sleep} from '../../../../../../shared/src/sleep.ts';\nimport {getTypeParsers} from '../../../../db/pg-type-parser.ts';\nimport {getOrCreateHistogram} from '../../../../observability/metrics.ts';\nimport {type PostgresDB} from '../../../../types/pg.ts';\nimport {id, lit} from '../../../../types/sql.ts';\nimport {pipe, type Sink, type Source} from '../../../../types/streams.ts';\nimport {Subscription} from '../../../../types/subscription.ts';\nimport {AutoResetSignal} from '../../../change-streamer/schema/tables.ts';\nimport {fromBigInt, toBigInt, type LSN} from '../lsn.ts';\nimport {PgoutputParser} from './pgoutput-parser.ts';\nimport type {Message} from './pgoutput.types.ts';\n\nconst DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE = 5;\n\nexport type StreamMessage = [\n lsn: bigint,\n Message | {tag: 'keepalive'; shouldRespond: boolean},\n];\n\n// Expose the `queued` variable of the Subscription to allow\n// the change-source to determine if there are more messages\n// immediately available.\ntype SourceWithPendingQueue<T> = Source<T> & {\n queued: number;\n};\n\nexport function createReplicationSessionFor(\n db: PostgresDB,\n applicationName: string,\n) {\n return postgres(\n defu(\n {\n max: 1,\n ['fetch_types']: false, // Necessary for the streaming protocol\n ['idle_timeout']: null,\n ['max_lifetime']: null as unknown as number,\n connection: {\n ['application_name']: applicationName,\n replication: 'database', // https://www.postgresql.org/docs/current/protocol-replication.html\n },\n },\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 db.options as unknown as Options<Record<string, PostgresType>>,\n ),\n );\n}\n\nexport async function subscribe(\n lc: LogContext,\n db: PostgresDB,\n slot: string,\n publications: string[],\n lsn: bigint,\n retriesIfReplicationSlotActive = DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE,\n applicationName = 'zero-replicator',\n inboundTimeoutOverrideMs?: number | undefined,\n): Promise<{\n messages: SourceWithPendingQueue<StreamMessage>;\n acks: Sink<bigint>;\n}> {\n const session = createReplicationSessionFor(db, applicationName);\n\n // Postgres will send keepalives before timing out a wal_sender. It is possible that\n // these keepalives are not received if there is back-pressure in the replication\n // stream. To keep the connection alive, explicitly send keepalives if none have been\n // sent within the last 75% of the wal_sender_timeout.\n //\n // https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT\n const [{walSenderTimeoutMs}] = await session<\n {walSenderTimeoutMs: number}[]\n >`SELECT EXTRACT(EPOCH FROM (setting || unit)::interval) * 1000 \n AS \"walSenderTimeoutMs\" FROM pg_settings\n WHERE name = 'wal_sender_timeout'`.simple();\n const {\n enabled: livenessEnabled,\n manualKeepaliveTimeout,\n inboundTimeoutMs,\n timerIntervalMs,\n } = computeLivenessTimings(walSenderTimeoutMs, inboundTimeoutOverrideMs);\n lc.info?.(\n livenessEnabled\n ? `wal_sender_timeout: ${walSenderTimeoutMs}ms. ` +\n `Ensuring manual keepalives at least every ${manualKeepaliveTimeout}ms. ` +\n `Inbound timeout: ${inboundTimeoutMs}ms` +\n (inboundTimeoutMs === walSenderTimeoutMs * 2\n ? ''\n : ' (configured override)')\n : `wal_sender_timeout is disabled (0); skipping manual keepalives and ` +\n `inbound liveness detection.`,\n );\n\n const decode = await InitialDecodeTracker.start(lc, session, slot, lsn);\n const [readable, writable] = await startReplicationStream(\n lc,\n session,\n slot,\n publications,\n lsn,\n retriesIfReplicationSlotActive + 1,\n );\n\n let lastAckTime = Date.now();\n function sendAck(lsn: bigint) {\n writable.write(makeAck(lsn));\n lastAckTime = Date.now();\n }\n\n // Tear down the readable to force a reconnect if the inbound half of the\n // connection goes silent (our outbound acks keep flowing into the void,\n // neither side errors, and the change-source would otherwise sit there\n // forever). See computeLivenessTimings for the derivation of these\n // thresholds — and why the whole timer is disabled when wal_sender_timeout\n // is 0.\n let lastReceivedTime = Date.now();\n\n const livenessTimer = livenessEnabled\n ? setInterval(() => {\n const now = Date.now();\n if (now - lastAckTime > manualKeepaliveTimeout) {\n sendAck(0n);\n lc.debug?.(`sent manual keepalive`);\n }\n const sinceLastReceived = now - lastReceivedTime;\n const {resetWindow, timedOut} = evaluateInboundLiveness({\n sinceLastReceived,\n inboundTimeoutMs,\n queued: messages.queued,\n });\n if (resetWindow) {\n // Downstream back-pressure is preventing us from consuming (and thus\n // receiving) upstream messages. Keep the inbound window fresh so that\n // when the backlog finally drains we grant a full inboundTimeoutMs\n // before concluding the connection is dead — otherwise a long stall\n // would leave lastReceivedTime stale and fire this watchdog on the\n // first post-drain tick, tearing down a healthy connection.\n lastReceivedTime = now;\n } else if (timedOut && !readable.destroyed) {\n lc.warn?.(\n `no message received from ${db.options.host} in ${sinceLastReceived}ms ` +\n `(> inbound timeout ${inboundTimeoutMs}ms). Destroying ` +\n `replication stream to force reconnect.`,\n );\n readable.destroy(\n new Error(\n `replication stream inbound timeout after ${sinceLastReceived}ms`,\n ),\n );\n }\n }, timerIntervalMs)\n : undefined;\n\n let destroyed = false;\n const typeParsers = await getTypeParsers(db, {returnJsonAsString: true});\n const parser = new PgoutputParser(typeParsers);\n const messages = Subscription.create<StreamMessage>({\n cleanup: () => {\n destroyed = true;\n readable.destroyed || readable.destroy();\n clearInterval(livenessTimer);\n return session.end();\n },\n });\n\n readable.once(\n 'close',\n () =>\n // Only log a warning if the stream was not manually closed.\n destroyed || lc.warn?.(`replication stream closed by ${db.options.host}`),\n );\n readable.once(\n 'error',\n e =>\n // Don't log the shutdown signal. This is the expected way for upstream\n // to close the connection (and will be logged downstream).\n (e instanceof postgres.PostgresError && e.code === PG_ADMIN_SHUTDOWN) ||\n lc.warn?.(`error from ${db.options.host}`, e),\n );\n\n pipe({\n source: readable,\n sink: messages,\n parse: buffer => {\n lastReceivedTime = Date.now();\n const msg = parseStreamMessage(lc, buffer, parser);\n if (msg) {\n decode.observe(msg, lastReceivedTime);\n }\n return msg;\n },\n // Allow a small buffer of messages to be queued in the subscription so\n // that the change-source loop can check the queue to determine if more\n // messages are immediately available.\n bufferMessages: 5,\n });\n\n return {\n messages,\n acks: {push: sendAck},\n };\n}\n\nconst INITIAL_DECODE_HISTOGRAM_BOUNDARIES_S = [\n 0.1, 0.5, 1, 2, 5, 10, 30, 60, 120, 300, 600, 1200, 2400, 3600, 7200,\n];\n\nfunction initialDecodeHistogram() {\n return getOrCreateHistogram('replication', 'stream_initial_decode', {\n description:\n 'Time from START_REPLICATION until upstream has decoded the WAL ' +\n \"between the slot's restart_lsn and the start of the stream\",\n unit: 's',\n bucketBoundaries: INITIAL_DECODE_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\n/**\n * Measures how long upstream takes to decode the WAL between the slot's\n * `restart_lsn` and the start of the stream, during which the stream\n * delivers nothing but keepalives. This span can be large, e.g. for a slot\n * that was created (or last acked) long before the requested start LSN.\n *\n * The walsender streams from `max(lsn, confirmed_flush_lsn)`, but must\n * decode from `restart_lsn` to rebuild its transaction state. The decode is\n * considered complete upon the first message at or past the start:\n * * any XLogData message, since only transactions committed after the\n * start are sent, or\n * * a keepalive at or past the start. Keepalives report the decoding\n * position, except for the first one of the stream, which reports the\n * slot's `confirmed_flush_lsn` and is thus ignored if it is at or past the\n * start.\n *\n * Note that on an idle upstream, a walsender whose start is already\n * confirmed may not send a keepalive until other WAL is written or the\n * periodic keepalive (`wal_sender_timeout / 2`) is due, so the measured\n * time is an upper bound when completed by a keepalive.\n */\nexport class InitialDecodeTracker {\n static async start(\n lc: LogContext,\n session: postgres.Sql,\n slot: string,\n lsn: bigint,\n ): Promise<InitialDecodeTracker> {\n let restartLSN: bigint | undefined;\n let confirmedFlushLSN: bigint | undefined;\n try {\n // Replication sessions only support the simple query protocol, so the\n // slot name is inlined as a literal.\n const [row] = await session\n .unsafe<{restartLSN: LSN | null; confirmedFlushLSN: LSN | null}[]>(\n `SELECT restart_lsn AS \"restartLSN\",\n confirmed_flush_lsn AS \"confirmedFlushLSN\"\n FROM pg_replication_slots\n WHERE slot_name = ${lit(slot)}`,\n )\n .simple();\n restartLSN = row?.restartLSN ? toBigInt(row.restartLSN) : undefined;\n confirmedFlushLSN = row?.confirmedFlushLSN\n ? toBigInt(row.confirmedFlushLSN)\n : undefined;\n } catch (e) {\n lc.warn?.(`unable to read position of replication slot ${slot}`, e);\n }\n return new InitialDecodeTracker(\n lc,\n lsn,\n restartLSN,\n confirmedFlushLSN,\n Date.now(),\n );\n }\n\n readonly #lc: LogContext;\n readonly #start: bigint;\n readonly #restartLSN: bigint | undefined;\n readonly #confirmedFlushLSN: bigint | undefined;\n readonly #startTime: number;\n #first = true;\n #done = false;\n\n constructor(\n lc: LogContext,\n lsn: bigint,\n restartLSN: bigint | undefined,\n confirmedFlushLSN: bigint | undefined,\n startTime: number,\n ) {\n // The walsender silently starts at confirmed_flush_lsn if the\n // requested lsn is behind it.\n this.#start =\n confirmedFlushLSN !== undefined && confirmedFlushLSN > lsn\n ? confirmedFlushLSN\n : lsn;\n this.#lc = lc.withContext('decodeStart', fromBigInt(this.#start));\n this.#restartLSN = restartLSN;\n this.#confirmedFlushLSN = confirmedFlushLSN;\n this.#startTime = startTime;\n\n if (restartLSN !== undefined) {\n this.#lc.info?.(\n `decoding ${this.#start - restartLSN} bytes of WAL from ` +\n `restart_lsn ${fromBigInt(restartLSN)} to the start of the stream`,\n );\n }\n }\n\n /**\n * Observes a received message, returning the elapsed decode time (ms) if\n * this message completed the initial decode.\n */\n observe([lsn, msg]: StreamMessage, now: number): number | undefined {\n if (this.#done) {\n return undefined;\n }\n const first = this.#first;\n this.#first = false;\n if (msg.tag === 'keepalive') {\n if (\n lsn < this.#start ||\n (first && lsn === this.#confirmedFlushLSN) // reports confirmed_flush\n ) {\n return undefined;\n }\n }\n this.#done = true;\n const elapsed = now - this.#startTime;\n initialDecodeHistogram().recordMs(elapsed);\n this.#lc.info?.(\n `upstream decoded the WAL to the start of the stream in ` +\n `${elapsed} ms (${msg.tag} at ${fromBigInt(lsn)}` +\n (this.#restartLSN === undefined\n ? ')'\n : `, ${this.#start - this.#restartLSN} bytes from restart_lsn)`),\n {elapsed},\n );\n return elapsed;\n }\n}\n\n/**\n * Derives the timings for the replication liveness timer from the server's\n * `wal_sender_timeout` (in milliseconds).\n *\n * Postgres treats `wal_sender_timeout = 0` as \"disabled\": it will neither\n * terminate an idle wal sender nor emit the periodic keepalives it otherwise\n * sends at roughly `wal_sender_timeout / 2`. Every timing below is derived from\n * that interval, so a disabled timeout disables the liveness machinery entirely\n * (`enabled: false`). Otherwise the derived thresholds would all collapse to 0,\n * inverting the intended meaning: the inbound watchdog would fire on the very\n * first tick and destroy the stream, while the timer itself would busy-spin at\n * a 0ms interval — together producing a continuous reconnect storm.\n *\n * An `inboundTimeoutOverrideMs` (when positive) replaces the default\n * `2x wal_sender_timeout` inbound threshold. The two timeouts protect\n * different parties with asymmetric false-positive costs: `wal_sender_timeout`\n * is the server reaping dead clients (a false positive there costs a cheap\n * reconnect), while the inbound watchdog tears down the stream and replays the\n * in-flight transaction — expensive, and self-defeating when the wal sender is\n * legitimately silent for long stretches (e.g. decoding through WAL from\n * unpublished tables, or assembling a large transaction that pgoutput only\n * sends at commit). Operators who don't own the server setting (e.g. managed\n * or operator-provisioned Postgres with an aggressive cluster-wide\n * `wal_sender_timeout`) can widen the client-side fuse independently.\n *\n * The override has no effect when `wal_sender_timeout` is disabled (0): the\n * server then sends no periodic keepalives, so inbound silence is normal even\n * on an idle healthy connection and any silence-based watchdog would\n * false-positive.\n *\n * https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT\n */\nexport function computeLivenessTimings(\n walSenderTimeoutMs: number,\n inboundTimeoutOverrideMs?: number | undefined,\n): {\n enabled: boolean;\n manualKeepaliveTimeout: number;\n inboundTimeoutMs: number;\n timerIntervalMs: number;\n} {\n // Guard with `!(x > 0)` so that 0, negative, and NaN values all disable the\n // timer rather than producing degenerate (zero / NaN) thresholds.\n if (!(walSenderTimeoutMs > 0)) {\n return {\n enabled: false,\n manualKeepaliveTimeout: 0,\n inboundTimeoutMs: 0,\n timerIntervalMs: 0,\n };\n }\n // Send a manual keepalive if nothing has been sent in the last 75% of the\n // wal_sender_timeout, so Postgres never times us out under back-pressure.\n const manualKeepaliveTimeout = Math.floor(walSenderTimeoutMs * 0.75);\n return {\n enabled: true,\n manualKeepaliveTimeout,\n // Force a reconnect after 2x wal_sender_timeout — i.e. two consecutive\n // missed keepalives — without any inbound message, to avoid spurious\n // reconnects from a single delayed keepalive (PG scheduling jitter under\n // load, our own event-loop stalls, etc.). A 0 / negative / NaN override\n // falls back to the default rather than degenerating the threshold.\n inboundTimeoutMs:\n inboundTimeoutOverrideMs !== undefined && inboundTimeoutOverrideMs > 0\n ? inboundTimeoutOverrideMs\n : walSenderTimeoutMs * 2,\n // Poll at 1/5th of the manual keepalive interval.\n timerIntervalMs: manualKeepaliveTimeout / 5,\n };\n}\n\n/**\n * Pure decision for the inbound-liveness watchdog, given the time since the\n * last inbound message, the timeout threshold, and the number of messages\n * currently queued downstream (see {@link SourceWithPendingQueue.queued}).\n *\n * A non-empty downstream queue means we have received data we haven't yet\n * consumed: the reason we aren't receiving *more* is our own back-pressure, not\n * upstream silence. In that case we `resetWindow` (treat the backlog as\n * liveness evidence) rather than counting the stall against the connection.\n * This both suppresses false timeouts during back-pressure and — crucially —\n * keeps `lastReceivedTime` fresh so the drain transition (queue emptying after\n * a long stall) doesn't fire the watchdog against a stale timestamp on the very\n * next tick.\n *\n * Only when the queue is empty (consumption has caught up, so reading — and\n * thus keepalives — should be flowing again) does an over-threshold gap\n * indicate a genuinely dead inbound half.\n */\nexport function evaluateInboundLiveness({\n sinceLastReceived,\n inboundTimeoutMs,\n queued,\n}: {\n sinceLastReceived: number;\n inboundTimeoutMs: number;\n queued: number;\n}): {resetWindow: boolean; timedOut: boolean} {\n if (queued > 0) {\n return {resetWindow: true, timedOut: false};\n }\n return {resetWindow: false, timedOut: sinceLastReceived > inboundTimeoutMs};\n}\n\n/**\n * Formats publication names for the START_REPLICATION command.\n * The replication protocol expects format: publication_names 'pub1,pub2'\n * Each name is escaped to handle any quotes that may have passed validation.\n */\nfunction formatPublicationNames(publications: string[]): string {\n // lit() returns 'escaped_name' with surrounding quotes\n // We strip the quotes since the outer quotes are in the template\n return publications.map(p => lit(p).slice(1, -1)).join(',');\n}\n\nasync function startReplicationStream(\n lc: LogContext,\n session: postgres.Sql,\n slot: string,\n publications: string[],\n lsn: bigint,\n maxAttempts: number,\n) {\n for (let i = 0; i < maxAttempts; i++) {\n try {\n const stream = session\n .unsafe(\n `START_REPLICATION SLOT ${id(slot)} LOGICAL ${fromBigInt(lsn)} (\n proto_version '1',\n publication_names '${formatPublicationNames(publications)}',\n messages 'true'\n )`,\n )\n .execute();\n return await Promise.all([stream.readable(), stream.writable()]);\n } catch (e) {\n if (e instanceof postgres.PostgresError) {\n // error: replication slot \"zero_slot_change_source_test_id\" is active for PID 268\n if (e.code === PG_OBJECT_IN_USE) {\n // The freeing up of the replication slot is not transactional;\n // sometimes it takes time for Postgres to consider the slot\n // inactive.\n lc.warn?.(`attempt ${i + 1}: ${String(e)}`, e);\n await sleep(Math.min(10 * 2 ** i, 1000));\n continue;\n }\n // error: This slot has been invalidated because it exceeded the maximum reserved size.\n // (This is a different manifestation of a slot being invalidated when\n // the wal exceeds the max_slot_wal_keep_size)\n if (e.code === PG_OBJECT_NOT_IN_PREREQUISITE_STATE) {\n lc.error?.(`error starting replication stream`, e);\n throw new AutoResetSignal(`unable to start replication stream`, {\n cause: e,\n });\n }\n }\n throw e;\n }\n }\n throw new Error(\n `exceeded max attempts (${maxAttempts}) to start the Postgres stream`,\n );\n}\n\n/**\n * Starts a \"dummy\" replication session for the specified `slot` which:\n * * does not consume any records\n * * keeps the session alive by sending status messages at the starting LSN\n *\n * This essentially reserves the replication slot for this process by marking\n * it as \"active\" in pg_replication_slots, allowing replication slot cleanup\n * logic to distinguish a new-but-initializing replication slot from one that\n * has been abandoned / orphaned.\n *\n * When initialization (e.g. initial-sync) has finished, the server should take\n * over the replication slot using pg_terminate_backend, which will close the\n * session.\n *\n * Note that caller should not close the supplied `session`, as that will\n * also terminate the dummy replication session.\n */\nexport async function keepSlotActiveUntilTakenOver(\n lc: LogContext,\n session: postgres.Sql,\n slot: string,\n dummyPublication: string,\n lsn: bigint,\n) {\n const [readable, writeable] = await startReplicationStream(\n lc,\n session,\n slot,\n [dummyPublication],\n lsn,\n 1,\n );\n lc.info?.(`keeping ${slot} active until taken over ...`);\n // Discard the streamed messages. Otherwise the paused socket backs up the\n // wal_sender (e.g. with keepalives and logical messages) for the duration\n // of the initial sync / restore, and a wal_sender blocked on writing to the\n // client does not exit (and release the slot) when it is terminated by the\n // takeover; it instead blocks on sending the termination error to the\n // client, until signaled again.\n readable.resume();\n // Send periodic status messages upstream to indicate that the session is\n // alive, but keep the confirmed_flush_lsn at the initial lsn.\n const keepalive = setInterval(() => writeable.write(makeAck(lsn)), 10_000);\n\n readable.on('close', async () => {\n lc.info?.(`released initial reservation of ${slot}`);\n clearInterval(keepalive);\n try {\n await session.end();\n lc.info?.(`ended session`);\n } catch (e) {\n lc.warn?.(`error ending session`, e);\n }\n });\n readable.once('error', e => {\n lc.info?.(`closing replication session to ${session.options.host}`, e);\n readable.destroy();\n });\n return readable;\n}\n\nfunction parseStreamMessage(\n lc: LogContext,\n buffer: Buffer,\n parser: PgoutputParser,\n): StreamMessage | null {\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-START-REPLICATION\n if (buffer[0] !== 0x77 && buffer[0] !== 0x6b) {\n lc.warn?.('Unknown message', buffer[0]);\n return null;\n }\n const lsn = buffer.readBigUInt64BE(1);\n if (buffer[0] === 0x77) {\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-XLOGDATA\n // (Byte 25 is where the WAL data begins)\n return [lsn, parser.parse(buffer.subarray(25))];\n }\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-PRIMARY-KEEPALIVE-MESSAGE\n // (Byte 17: shouldRespond)\n const shouldRespond = buffer.readInt8(17) !== 0;\n return [lsn, {tag: 'keepalive', shouldRespond}];\n}\n\n// https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-STANDBY-STATUS-UPDATE\nfunction makeAck(lsn: bigint): Buffer {\n const microNow = BigInt(Date.now() - Date.UTC(2000, 0, 1)) * BigInt(1000);\n\n const x = Buffer.alloc(34);\n x[0] = 'r'.charCodeAt(0);\n x.writeBigInt64BE(lsn, 1);\n x.writeBigInt64BE(lsn, 9);\n x.writeBigInt64BE(lsn, 17);\n x.writeBigInt64BE(microNow, 25);\n return x;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"stream.js","names":["#lc","#start","#restartLSN","#confirmedFlushLSN","#startTime","#done","#first","#worker","#closedPromise","#sigtermTimer","#stopped"],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/logical-replication/stream.ts"],"sourcesContent":["import {Readable} from 'node:stream';\nimport {\n PG_ADMIN_SHUTDOWN,\n PG_OBJECT_IN_USE,\n PG_OBJECT_NOT_IN_PREREQUISITE_STATE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {defu} from 'defu';\nimport postgres, {type Options, type PostgresType} from 'postgres';\nimport {sleep} from '../../../../../../shared/src/sleep.ts';\nimport {getTypeParsers} from '../../../../db/pg-type-parser.ts';\nimport {getOrCreateHistogram} from '../../../../observability/metrics.ts';\nimport type {SlotKeeperConfig} from '../../../../server/slot-keeper.ts';\nimport {SLOT_KEEPER_URL} from '../../../../server/worker-urls.ts';\nimport {type PostgresDB} from '../../../../types/pg.ts';\nimport {childWorker, type Worker} from '../../../../types/processes.ts';\nimport {id, lit} from '../../../../types/sql.ts';\nimport {pipe, type Sink, type Source} from '../../../../types/streams.ts';\nimport {Subscription} from '../../../../types/subscription.ts';\nimport {AutoResetSignal} from '../../../change-streamer/schema/tables.ts';\nimport {fromBigInt, toBigInt, type LSN} from '../lsn.ts';\nimport {PgoutputParser} from './pgoutput-parser.ts';\nimport type {Message} from './pgoutput.types.ts';\n\nexport const DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE = 5;\n\nexport type StreamMessage = [\n lsn: bigint,\n Message | {tag: 'keepalive'; shouldRespond: boolean},\n];\n\n// Expose the `queued` variable of the Subscription to allow\n// the change-source to determine if there are more messages\n// immediately available.\ntype SourceWithPendingQueue<T> = Source<T> & {\n queued: number;\n};\n\nexport function createReplicationSessionFor(\n db: PostgresDB,\n applicationName: string,\n) {\n return postgres(\n defu(\n {\n max: 1,\n ['fetch_types']: false, // Necessary for the streaming protocol\n ['idle_timeout']: null,\n ['max_lifetime']: null as unknown as number,\n connection: {\n ['application_name']: applicationName,\n replication: 'database', // https://www.postgresql.org/docs/current/protocol-replication.html\n },\n },\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 db.options as unknown as Options<Record<string, PostgresType>>,\n ),\n );\n}\n\nexport async function subscribe(\n lc: LogContext,\n db: PostgresDB,\n slot: string,\n publications: string[],\n lsn: bigint,\n retriesIfReplicationSlotActive = DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE,\n applicationName = 'zero-replicator',\n inboundTimeoutOverrideMs?: number | undefined,\n): Promise<{\n messages: SourceWithPendingQueue<StreamMessage>;\n acks: Sink<bigint>;\n}> {\n const session = createReplicationSessionFor(db, applicationName);\n\n // Postgres will send keepalives before timing out a wal_sender. It is possible that\n // these keepalives are not received if there is back-pressure in the replication\n // stream. To keep the connection alive, explicitly send keepalives if none have been\n // sent within the last 75% of the wal_sender_timeout.\n //\n // https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT\n const [{walSenderTimeoutMs}] = await session<\n {walSenderTimeoutMs: number}[]\n >`SELECT EXTRACT(EPOCH FROM (setting || unit)::interval) * 1000 \n AS \"walSenderTimeoutMs\" FROM pg_settings\n WHERE name = 'wal_sender_timeout'`.simple();\n const {\n enabled: livenessEnabled,\n manualKeepaliveTimeout,\n inboundTimeoutMs,\n timerIntervalMs,\n } = computeLivenessTimings(walSenderTimeoutMs, inboundTimeoutOverrideMs);\n lc.info?.(\n livenessEnabled\n ? `wal_sender_timeout: ${walSenderTimeoutMs}ms. ` +\n `Ensuring manual keepalives at least every ${manualKeepaliveTimeout}ms. ` +\n `Inbound timeout: ${inboundTimeoutMs}ms` +\n (inboundTimeoutMs === walSenderTimeoutMs * 2\n ? ''\n : ' (configured override)')\n : `wal_sender_timeout is disabled (0); skipping manual keepalives and ` +\n `inbound liveness detection.`,\n );\n\n const decode = await InitialDecodeTracker.start(lc, session, slot, lsn);\n const [readable, writable] = await startReplicationStream(\n lc,\n session,\n slot,\n publications,\n lsn,\n retriesIfReplicationSlotActive + 1,\n );\n\n let lastAckTime = Date.now();\n function sendAck(lsn: bigint) {\n writable.write(makeAck(lsn));\n lastAckTime = Date.now();\n }\n\n // Tear down the readable to force a reconnect if the inbound half of the\n // connection goes silent (our outbound acks keep flowing into the void,\n // neither side errors, and the change-source would otherwise sit there\n // forever). See computeLivenessTimings for the derivation of these\n // thresholds — and why the whole timer is disabled when wal_sender_timeout\n // is 0.\n let lastReceivedTime = Date.now();\n\n const livenessTimer = livenessEnabled\n ? setInterval(() => {\n const now = Date.now();\n if (now - lastAckTime > manualKeepaliveTimeout) {\n sendAck(0n);\n lc.debug?.(`sent manual keepalive`);\n }\n const sinceLastReceived = now - lastReceivedTime;\n const {resetWindow, timedOut} = evaluateInboundLiveness({\n sinceLastReceived,\n inboundTimeoutMs,\n queued: messages.queued,\n });\n if (resetWindow) {\n // Downstream back-pressure is preventing us from consuming (and thus\n // receiving) upstream messages. Keep the inbound window fresh so that\n // when the backlog finally drains we grant a full inboundTimeoutMs\n // before concluding the connection is dead — otherwise a long stall\n // would leave lastReceivedTime stale and fire this watchdog on the\n // first post-drain tick, tearing down a healthy connection.\n lastReceivedTime = now;\n } else if (timedOut && !readable.destroyed) {\n lc.warn?.(\n `no message received from ${db.options.host} in ${sinceLastReceived}ms ` +\n `(> inbound timeout ${inboundTimeoutMs}ms). Destroying ` +\n `replication stream to force reconnect.`,\n );\n readable.destroy(\n new Error(\n `replication stream inbound timeout after ${sinceLastReceived}ms`,\n ),\n );\n }\n }, timerIntervalMs)\n : undefined;\n\n let destroyed = false;\n const typeParsers = await getTypeParsers(db, {returnJsonAsString: true});\n const parser = new PgoutputParser(typeParsers);\n const messages = Subscription.create<StreamMessage>({\n cleanup: () => {\n destroyed = true;\n readable.destroyed || readable.destroy();\n clearInterval(livenessTimer);\n return session.end();\n },\n });\n\n readable.once(\n 'close',\n () =>\n // Only log a warning if the stream was not manually closed.\n destroyed || lc.warn?.(`replication stream closed by ${db.options.host}`),\n );\n readable.once(\n 'error',\n e =>\n // Don't log the shutdown signal. This is the expected way for upstream\n // to close the connection (and will be logged downstream).\n (e instanceof postgres.PostgresError && e.code === PG_ADMIN_SHUTDOWN) ||\n lc.warn?.(`error from ${db.options.host}`, e),\n );\n\n pipe({\n source: readable,\n sink: messages,\n parse: buffer => {\n lastReceivedTime = Date.now();\n const msg = parseStreamMessage(lc, buffer, parser);\n if (msg) {\n decode.observe(msg, lastReceivedTime);\n }\n return msg;\n },\n // Allow a small buffer of messages to be queued in the subscription so\n // that the change-source loop can check the queue to determine if more\n // messages are immediately available.\n bufferMessages: 5,\n });\n\n return {\n messages,\n acks: {push: sendAck},\n };\n}\n\nconst INITIAL_DECODE_HISTOGRAM_BOUNDARIES_S = [\n 0.1, 0.5, 1, 2, 5, 10, 30, 60, 120, 300, 600, 1200, 2400, 3600, 7200,\n];\n\nfunction initialDecodeHistogram() {\n return getOrCreateHistogram('replication', 'stream_initial_decode', {\n description:\n 'Time from START_REPLICATION until upstream has decoded the WAL ' +\n \"between the slot's restart_lsn and the start of the stream\",\n unit: 's',\n bucketBoundaries: INITIAL_DECODE_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\n/**\n * Measures how long upstream takes to decode the WAL between the slot's\n * `restart_lsn` and the start of the stream, during which the stream\n * delivers nothing but keepalives. This span can be large, e.g. for a slot\n * that was created (or last acked) long before the requested start LSN.\n *\n * The walsender streams from `max(lsn, confirmed_flush_lsn)`, but must\n * decode from `restart_lsn` to rebuild its transaction state. The decode is\n * considered complete upon the first message at or past the start:\n * * any XLogData message, since only transactions committed after the\n * start are sent, or\n * * a keepalive at or past the start. Keepalives report the decoding\n * position, except for the first one of the stream, which reports the\n * slot's `confirmed_flush_lsn` and is thus ignored if it is at or past the\n * start.\n *\n * Note that on an idle upstream, a walsender whose start is already\n * confirmed may not send a keepalive until other WAL is written or the\n * periodic keepalive (`wal_sender_timeout / 2`) is due, so the measured\n * time is an upper bound when completed by a keepalive.\n */\nexport class InitialDecodeTracker {\n static async start(\n lc: LogContext,\n session: postgres.Sql,\n slot: string,\n lsn: bigint,\n ): Promise<InitialDecodeTracker> {\n let restartLSN: bigint | undefined;\n let confirmedFlushLSN: bigint | undefined;\n try {\n // Replication sessions only support the simple query protocol, so the\n // slot name is inlined as a literal.\n const [row] = await session\n .unsafe<{restartLSN: LSN | null; confirmedFlushLSN: LSN | null}[]>(\n `SELECT restart_lsn AS \"restartLSN\",\n confirmed_flush_lsn AS \"confirmedFlushLSN\"\n FROM pg_replication_slots\n WHERE slot_name = ${lit(slot)}`,\n )\n .simple();\n restartLSN = row?.restartLSN ? toBigInt(row.restartLSN) : undefined;\n confirmedFlushLSN = row?.confirmedFlushLSN\n ? toBigInt(row.confirmedFlushLSN)\n : undefined;\n } catch (e) {\n lc.warn?.(`unable to read position of replication slot ${slot}`, e);\n }\n return new InitialDecodeTracker(\n lc,\n lsn,\n restartLSN,\n confirmedFlushLSN,\n Date.now(),\n );\n }\n\n readonly #lc: LogContext;\n readonly #start: bigint;\n readonly #restartLSN: bigint | undefined;\n readonly #confirmedFlushLSN: bigint | undefined;\n readonly #startTime: number;\n #first = true;\n #done = false;\n\n constructor(\n lc: LogContext,\n lsn: bigint,\n restartLSN: bigint | undefined,\n confirmedFlushLSN: bigint | undefined,\n startTime: number,\n ) {\n // The walsender silently starts at confirmed_flush_lsn if the\n // requested lsn is behind it.\n this.#start =\n confirmedFlushLSN !== undefined && confirmedFlushLSN > lsn\n ? confirmedFlushLSN\n : lsn;\n this.#lc = lc.withContext('decodeStart', fromBigInt(this.#start));\n this.#restartLSN = restartLSN;\n this.#confirmedFlushLSN = confirmedFlushLSN;\n this.#startTime = startTime;\n\n if (restartLSN !== undefined) {\n this.#lc.info?.(\n `decoding ${this.#start - restartLSN} bytes of WAL from ` +\n `restart_lsn ${fromBigInt(restartLSN)} to the start of the stream`,\n );\n }\n }\n\n /**\n * Observes a received message, returning the elapsed decode time (ms) if\n * this message completed the initial decode.\n */\n observe([lsn, msg]: StreamMessage, now: number): number | undefined {\n if (this.#done) {\n return undefined;\n }\n const first = this.#first;\n this.#first = false;\n if (msg.tag === 'keepalive') {\n if (\n lsn < this.#start ||\n (first && lsn === this.#confirmedFlushLSN) // reports confirmed_flush\n ) {\n return undefined;\n }\n }\n this.#done = true;\n const elapsed = now - this.#startTime;\n initialDecodeHistogram().recordMs(elapsed);\n this.#lc.info?.(\n `upstream decoded the WAL to the start of the stream in ` +\n `${elapsed} ms (${msg.tag} at ${fromBigInt(lsn)}` +\n (this.#restartLSN === undefined\n ? ')'\n : `, ${this.#start - this.#restartLSN} bytes from restart_lsn)`),\n {elapsed},\n );\n return elapsed;\n }\n}\n\n/**\n * Derives the timings for the replication liveness timer from the server's\n * `wal_sender_timeout` (in milliseconds).\n *\n * Postgres treats `wal_sender_timeout = 0` as \"disabled\": it will neither\n * terminate an idle wal sender nor emit the periodic keepalives it otherwise\n * sends at roughly `wal_sender_timeout / 2`. Every timing below is derived from\n * that interval, so a disabled timeout disables the liveness machinery entirely\n * (`enabled: false`). Otherwise the derived thresholds would all collapse to 0,\n * inverting the intended meaning: the inbound watchdog would fire on the very\n * first tick and destroy the stream, while the timer itself would busy-spin at\n * a 0ms interval — together producing a continuous reconnect storm.\n *\n * An `inboundTimeoutOverrideMs` (when positive) replaces the default\n * `2x wal_sender_timeout` inbound threshold. The two timeouts protect\n * different parties with asymmetric false-positive costs: `wal_sender_timeout`\n * is the server reaping dead clients (a false positive there costs a cheap\n * reconnect), while the inbound watchdog tears down the stream and replays the\n * in-flight transaction — expensive, and self-defeating when the wal sender is\n * legitimately silent for long stretches (e.g. decoding through WAL from\n * unpublished tables, or assembling a large transaction that pgoutput only\n * sends at commit). Operators who don't own the server setting (e.g. managed\n * or operator-provisioned Postgres with an aggressive cluster-wide\n * `wal_sender_timeout`) can widen the client-side fuse independently.\n *\n * The override has no effect when `wal_sender_timeout` is disabled (0): the\n * server then sends no periodic keepalives, so inbound silence is normal even\n * on an idle healthy connection and any silence-based watchdog would\n * false-positive.\n *\n * https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT\n */\nexport function computeLivenessTimings(\n walSenderTimeoutMs: number,\n inboundTimeoutOverrideMs?: number | undefined,\n): {\n enabled: boolean;\n manualKeepaliveTimeout: number;\n inboundTimeoutMs: number;\n timerIntervalMs: number;\n} {\n // Guard with `!(x > 0)` so that 0, negative, and NaN values all disable the\n // timer rather than producing degenerate (zero / NaN) thresholds.\n if (!(walSenderTimeoutMs > 0)) {\n return {\n enabled: false,\n manualKeepaliveTimeout: 0,\n inboundTimeoutMs: 0,\n timerIntervalMs: 0,\n };\n }\n // Send a manual keepalive if nothing has been sent in the last 75% of the\n // wal_sender_timeout, so Postgres never times us out under back-pressure.\n const manualKeepaliveTimeout = Math.floor(walSenderTimeoutMs * 0.75);\n return {\n enabled: true,\n manualKeepaliveTimeout,\n // Force a reconnect after 2x wal_sender_timeout — i.e. two consecutive\n // missed keepalives — without any inbound message, to avoid spurious\n // reconnects from a single delayed keepalive (PG scheduling jitter under\n // load, our own event-loop stalls, etc.). A 0 / negative / NaN override\n // falls back to the default rather than degenerating the threshold.\n inboundTimeoutMs:\n inboundTimeoutOverrideMs !== undefined && inboundTimeoutOverrideMs > 0\n ? inboundTimeoutOverrideMs\n : walSenderTimeoutMs * 2,\n // Poll at 1/5th of the manual keepalive interval.\n timerIntervalMs: manualKeepaliveTimeout / 5,\n };\n}\n\n/**\n * Pure decision for the inbound-liveness watchdog, given the time since the\n * last inbound message, the timeout threshold, and the number of messages\n * currently queued downstream (see {@link SourceWithPendingQueue.queued}).\n *\n * A non-empty downstream queue means we have received data we haven't yet\n * consumed: the reason we aren't receiving *more* is our own back-pressure, not\n * upstream silence. In that case we `resetWindow` (treat the backlog as\n * liveness evidence) rather than counting the stall against the connection.\n * This both suppresses false timeouts during back-pressure and — crucially —\n * keeps `lastReceivedTime` fresh so the drain transition (queue emptying after\n * a long stall) doesn't fire the watchdog against a stale timestamp on the very\n * next tick.\n *\n * Only when the queue is empty (consumption has caught up, so reading — and\n * thus keepalives — should be flowing again) does an over-threshold gap\n * indicate a genuinely dead inbound half.\n */\nexport function evaluateInboundLiveness({\n sinceLastReceived,\n inboundTimeoutMs,\n queued,\n}: {\n sinceLastReceived: number;\n inboundTimeoutMs: number;\n queued: number;\n}): {resetWindow: boolean; timedOut: boolean} {\n if (queued > 0) {\n return {resetWindow: true, timedOut: false};\n }\n return {resetWindow: false, timedOut: sinceLastReceived > inboundTimeoutMs};\n}\n\n/**\n * Formats publication names for the START_REPLICATION command.\n * The replication protocol expects format: publication_names 'pub1,pub2'\n * Each name is escaped to handle any quotes that may have passed validation.\n */\nfunction formatPublicationNames(publications: string[]): string {\n // lit() returns 'escaped_name' with surrounding quotes\n // We strip the quotes since the outer quotes are in the template\n return publications.map(p => lit(p).slice(1, -1)).join(',');\n}\n\nexport async function startReplicationStream(\n lc: LogContext,\n session: postgres.Sql,\n slot: string,\n publications: string[],\n lsn: bigint,\n maxAttempts: number = DEFAULT_RETRIES_IF_REPLICATION_SLOT_ACTIVE + 1,\n) {\n for (let i = 0; i < maxAttempts; i++) {\n try {\n const stream = session\n .unsafe(\n `START_REPLICATION SLOT ${id(slot)} LOGICAL ${fromBigInt(lsn)} (\n proto_version '1',\n publication_names '${formatPublicationNames(publications)}',\n messages 'true'\n )`,\n )\n .execute();\n return await Promise.all([stream.readable(), stream.writable()]);\n } catch (e) {\n if (e instanceof postgres.PostgresError) {\n // error: replication slot \"zero_slot_change_source_test_id\" is active for PID 268\n if (e.code === PG_OBJECT_IN_USE) {\n // The freeing up of the replication slot is not transactional;\n // sometimes it takes time for Postgres to consider the slot\n // inactive.\n lc.warn?.(`attempt ${i + 1}: ${String(e)}`, e);\n await sleep(Math.min(10 * 2 ** i, 1000));\n continue;\n }\n // error: This slot has been invalidated because it exceeded the maximum reserved size.\n // (This is a different manifestation of a slot being invalidated when\n // the wal exceeds the max_slot_wal_keep_size)\n if (e.code === PG_OBJECT_NOT_IN_PREREQUISITE_STATE) {\n lc.error?.(`error starting replication stream`, e);\n throw new AutoResetSignal(`unable to start replication stream`, {\n cause: e,\n });\n }\n }\n throw e;\n }\n }\n throw new Error(\n `exceeded max attempts (${maxAttempts}) to start the Postgres stream`,\n );\n}\n\n/**\n * Starts a \"dummy\" replication session for the specified `slot` which:\n * * does not consume any records\n * * keeps the session alive by sending status messages at the starting LSN\n *\n * This essentially reserves the replication slot for this process by marking\n * it as \"active\" in pg_replication_slots, allowing replication slot cleanup\n * logic to distinguish a new-but-initializing replication slot from one that\n * has been abandoned / orphaned.\n *\n * When initialization (e.g. initial-sync) has finished, the server should take\n * over the replication slot using pg_terminate_backend, which will close the\n * session.\n *\n * Note that caller should not close the supplied `session`, as that will\n * also terminate the dummy replication session.\n */\nexport class SlotReservationHandle extends Readable {\n readonly #worker: Worker;\n readonly #closedPromise: Promise<void>;\n #stopped = false;\n #sigtermTimer?: NodeJS.Timeout | undefined;\n\n constructor(worker: Worker) {\n super({read() {}});\n this.#worker = worker;\n this.#closedPromise = new Promise(resolve => {\n worker.once('close', () => {\n clearTimeout(this.#sigtermTimer);\n resolve();\n });\n });\n // Suppress unhandled error events if destroyed with an error\n this.on('error', () => {});\n worker.on('close', () => {\n if (!this.destroyed) {\n this.destroy();\n }\n });\n worker.on('error', err => {\n if (!this.destroyed) {\n this.destroy(err instanceof Error ? err : new Error(String(err)));\n }\n });\n }\n\n override _destroy(\n error: Error | null,\n callback: (error?: Error | null) => void,\n ): void {\n if (!this.#stopped) {\n this.#stopped = true;\n try {\n this.#worker.send(['stop', {}]);\n } catch {\n // ignore\n }\n this.#sigtermTimer = setTimeout(() => {\n try {\n this.#worker.kill('SIGTERM');\n } catch {}\n }, 5_000);\n this.#sigtermTimer.unref();\n }\n callback(error);\n }\n\n async end(timeoutMs = 5_000): Promise<void> {\n this.destroy();\n const closed = await Promise.race([\n this.#closedPromise.then(() => true),\n sleep(timeoutMs).then(() => false),\n ]);\n if (!closed) {\n try {\n this.#worker.kill('SIGKILL');\n } catch {}\n // Await worker closure after forced kill so that the replication session\n // is completely terminated before any subsequent slot drop attempt.\n await Promise.race([this.#closedPromise, sleep(2_000)]);\n }\n }\n}\n\nexport async function endSession(session?: Readable): Promise<void> {\n if (!session) {\n return;\n }\n if ('end' in session && typeof session.end === 'function') {\n await (session as {end: () => Promise<void>}).end();\n } else {\n session.destroy();\n }\n}\n\nexport type PgConnectionConfig = {\n host?: string | string[] | undefined;\n port?: number | number[] | undefined;\n path?: string | undefined;\n database?: string | undefined;\n user?: string | undefined;\n pass?: string | null | undefined;\n ssl?:\n | boolean\n | string\n | {rejectUnauthorized?: boolean | undefined}\n | undefined;\n};\n\nexport function extractConnectionConfig(\n session: postgres.Sql,\n): PgConnectionConfig {\n const opts = session.options;\n return {\n host: opts.host,\n port: opts.port,\n path: (opts as {path?: string | undefined}).path,\n database: opts.database,\n user: opts.user,\n pass: opts.pass,\n ssl:\n typeof opts.ssl === 'object' && opts.ssl !== null\n ? {\n rejectUnauthorized: (\n opts.ssl as {rejectUnauthorized?: boolean | undefined}\n ).rejectUnauthorized,\n }\n : (opts.ssl as boolean | string | undefined),\n };\n}\n\n/**\n * Starts a \"dummy\" replication session for the specified `slot` which:\n * * does not consume any records\n * * keeps the session alive by sending status messages at the starting LSN\n *\n * Runs in a separate process with its own event loop and connection reconnect\n * loop to prevent slot drop races during heavy initial sync table copy or\n * SQLite index building.\n */\nexport async function keepSlotActiveUntilTakenOver(\n lc: LogContext,\n config: SlotKeeperConfig,\n): Promise<Readable> {\n const {slot, lsn} = config;\n lc.info?.(`spawning slot-keeper process for ${slot} at lsn ${lsn} ...`);\n const worker = childWorker(SLOT_KEEPER_URL, process.env);\n worker.onMessageType('init', () => {\n worker.send(['start', config]);\n });\n\n // Wait for the slot keeper to establish replication before returning\n await new Promise<void>((resolve, reject) => {\n const onReady = () => {\n cleanup();\n resolve();\n };\n const onError = (err: unknown) => {\n cleanup();\n reject(err);\n };\n const onClose = (code: number) => {\n cleanup();\n reject(\n new Error(\n `slot-keeper for ${slot} exited with code ${code} before becoming ready`,\n ),\n );\n };\n function cleanup() {\n worker.off('error', onError);\n worker.off('close', onClose);\n }\n worker.onceMessageType('ready', onReady);\n worker.once('error', onError);\n worker.once('close', onClose);\n });\n\n lc.info?.(`slot-keeper active for ${slot}`);\n return new SlotReservationHandle(worker);\n}\n\nfunction parseStreamMessage(\n lc: LogContext,\n buffer: Buffer,\n parser: PgoutputParser,\n): StreamMessage | null {\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-START-REPLICATION\n if (buffer[0] !== 0x77 && buffer[0] !== 0x6b) {\n lc.warn?.('Unknown message', buffer[0]);\n return null;\n }\n const lsn = buffer.readBigUInt64BE(1);\n if (buffer[0] === 0x77) {\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-XLOGDATA\n // (Byte 25 is where the WAL data begins)\n return [lsn, parser.parse(buffer.subarray(25))];\n }\n // https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-PRIMARY-KEEPALIVE-MESSAGE\n // (Byte 17: shouldRespond)\n const shouldRespond = buffer.readInt8(17) !== 0;\n return [lsn, {tag: 'keepalive', shouldRespond}];\n}\n\n// https://www.postgresql.org/docs/current/protocol-replication.html#PROTOCOL-REPLICATION-STANDBY-STATUS-UPDATE\nexport function makeAck(lsn: bigint): Buffer {\n const microNow = BigInt(Date.now() - Date.UTC(2000, 0, 1)) * BigInt(1000);\n\n const x = Buffer.alloc(34);\n x[0] = 'r'.charCodeAt(0);\n x.writeBigInt64BE(lsn, 1);\n x.writeBigInt64BE(lsn, 9);\n x.writeBigInt64BE(lsn, 17);\n x.writeBigInt64BE(microNow, 25);\n return x;\n}\n"],"mappings":"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"}
@@ -101,7 +101,7 @@ export declare function createReplicaAndSlot<T>(lc: LogContext, sql: PostgresDB,
101
101
  * replicas whose slots were dropped outside of zero-cache's control may share
102
102
  * the slot name.
103
103
  */
104
- export declare function claimSlotForResumption(lc: LogContext, sql: PostgresDB, shard: ShardID, sessionName: string, { id: replicaID, slot }: Pick<ReplicaState, 'id' | 'slot'>): Promise<ReservedSlot | null>;
104
+ export declare function claimSlotForResumption(lc: LogContext, sql: PostgresDB, shard: ShardID, _sessionName: string, { id: replicaID, slot }: Pick<ReplicaState, 'id' | 'slot'>): Promise<ReservedSlot | null>;
105
105
  export declare function dropInactiveSlotsAndReplicas(lc: LogContext, sql: PostgresDB, shard: ShardID, slots: string[]): Promise<void>;
106
106
  export declare function slotPoolSuffix(n: number): string;
107
107
  //# sourceMappingURL=replication-slots.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"replication-slots.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-source/pg/replication-slots.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,aAAa,CAAC;AAK1C,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AAKrC,OAAO,EAAkB,KAAK,UAAU,EAAC,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAiB,KAAK,OAAO,EAAC,MAAM,0BAA0B,CAAC;AAQtE,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,mBAAmB,CAAC;AAG3B,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACrC,IAAI,EAAE,eAAe,CAAC;IACtB,gBAAgB,EAAE,CAAC,CAAC;IACpB,cAAc,EAAE,QAAQ,CAAC;IACzB,OAAO,EAAE,YAAY,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,QAAQ,CAAC;IACtB,OAAO,EAAE,YAAY,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,MAAM,CAAC;IAGjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAInB,SAAS,CAAC,EAAE,OAAO,CAAC;IAGpB,WAAW,CAAC,EAAE,MAAM,CAAC;IAIrB,aAAa,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CACxC,CAAC;AAUF,eAAO,MAAM,kCAAkC,QAAS,CAAC;AAUzD,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,QAAQ,CAAC,GAAG,EACrB,EACE,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,WAAoC,EACpC,aAAa,GACd,EAAE,cAAc,GAChB,OAAO,CAAC,eAAe,CAAC,CAiD1B;AAKD;;;;;;;;;;;GAWG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,UAAU,EACf,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,iBAmCjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,UAAU,EACf,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,EACjB,aAAa,EAAE,aAAa,EAC5B,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,EACjD,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAoHnC;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,OAAO,EACd,WAAW,EAAE,MAAM,EACnB,EAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAC,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,MAAM,CAAC,GACvD,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAsD9B;AAED,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,EAAE,iBA+BhB;AAmFD,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,UAUvC"}
1
+ {"version":3,"file":"replication-slots.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-source/pg/replication-slots.ts"],"names":[],"mappings":"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"}
@@ -7,8 +7,8 @@ import "./schema/replica-stage-enum.js";
7
7
  import { createReplica, getReplicaState, getRestoreCandidates, metadataPublicationName, replicationSlotExpression, replicationSlotPrefix } from "./schema/shard.js";
8
8
  import { AutoResetSignal } from "../../change-streamer/schema/tables.js";
9
9
  import "../../../types/pg-versions.js";
10
+ import { createReplicationSessionFor, endSession, extractConnectionConfig, keepSlotActiveUntilTakenOver } from "./logical-replication/stream.js";
10
11
  import { orTimeout } from "../../../types/timeout.js";
11
- import { createReplicationSessionFor, keepSlotActiveUntilTakenOver } from "./logical-replication/stream.js";
12
12
  import { PG_CONFIGURATION_LIMIT_EXCEEDED, PG_INSUFFICIENT_PRIVILEGE } from "@drdgvhbh/postgres-error-codes";
13
13
  //#region ../zero-cache/src/services/change-source/pg/replication-slots.ts
14
14
  var CREATE_REPLICATION_SLOT_TIMEOUT_MS = 6e4;
@@ -129,6 +129,7 @@ async function createReplicaAndSlot(lc, sql, sessionName, shard, epoch, replicaI
129
129
  for (let first = true;; first = false) {
130
130
  await dropUnclaimedSlots(lc, sql, shard);
131
131
  let slotName;
132
+ let initialSession;
132
133
  try {
133
134
  return await runTx(sql, async (tx) => {
134
135
  await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;
@@ -158,13 +159,20 @@ async function createReplicaAndSlot(lc, sql, sessionName, shard, epoch, replicaI
158
159
  diagnosticsDB: sql
159
160
  });
160
161
  const capturedSnapshot = await captureSnapshot(slot.snapshot_name);
161
- const initialSession = await keepSlotActiveUntilTakenOver(lc, replicationSession, slot.slot_name, metadataPublicationName(shard.appID, shard.shardNum), toBigInt(slot.consistent_point));
162
+ await replicationSession.end();
163
+ initialSession = await keepSlotActiveUntilTakenOver(lc, {
164
+ db: extractConnectionConfig(sql),
165
+ slot: slot.slot_name,
166
+ dummyPublication: metadataPublicationName(shard.appID, shard.shardNum),
167
+ lsn: String(toBigInt(slot.consistent_point))
168
+ });
162
169
  await createReplica(tx, shard, replicaID, slot.slot_name, epoch, stage === 0 ? toStateVersionString(slot.consistent_point) : "", backupOptions, stage);
170
+ const replica = must(await getReplicaState(tx, shard, replicaID), `replica ${replicaID} was not created`);
163
171
  return {
164
172
  slot,
165
173
  capturedSnapshot,
166
174
  initialSession,
167
- replica: must(await getReplicaState(tx, shard, replicaID), `replica ${replicaID} was not created`)
175
+ replica
168
176
  };
169
177
  });
170
178
  } catch (e) {
@@ -180,9 +188,14 @@ async function createReplicaAndSlot(lc, sql, sessionName, shard, epoch, replicaI
180
188
  WHERE replicas.slot = slots.slot_name AND NOT slots.active`;
181
189
  continue;
182
190
  }
191
+ if (initialSession) try {
192
+ await endSession(initialSession);
193
+ } catch {}
183
194
  if (slotName) {
184
195
  lc.warn?.(`deleting slot ${slotName} due to error`, e);
185
- await replicationSession.end();
196
+ try {
197
+ await replicationSession.end();
198
+ } catch {}
186
199
  await dropUnclaimedSlots(lc, sql, shard);
187
200
  }
188
201
  throw e;
@@ -198,10 +211,10 @@ async function createReplicaAndSlot(lc, sql, sessionName, shard, epoch, replicaI
198
211
  * replicas whose slots were dropped outside of zero-cache's control may share
199
212
  * the slot name.
200
213
  */
201
- async function claimSlotForResumption(lc, sql, shard, sessionName, { id: replicaID, slot }) {
202
- const replicationSession = createReplicationSessionFor(sql, sessionName);
214
+ async function claimSlotForResumption(lc, sql, shard, _sessionName, { id: replicaID, slot }) {
203
215
  const lockName = replicationSlotManagementLock(shard);
204
216
  const replicasTable = `${upstreamSchema(shard)}.replicas`;
217
+ let reservation;
205
218
  try {
206
219
  const reserved = await runTx(sql, async (tx) => {
207
220
  await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;
@@ -223,17 +236,26 @@ async function claimSlotForResumption(lc, sql, shard, sessionName, { id: replica
223
236
  await tx`
224
237
  UPDATE ${tx(replicasTable)} SET "stage" = ${2} WHERE id = ${replicaID};
225
238
  `;
239
+ const replica = must(await getReplicaState(tx, shard, replicaID), `replica ${replicaID} disappeared`);
240
+ reservation = await keepSlotActiveUntilTakenOver(lc, {
241
+ db: extractConnectionConfig(sql),
242
+ slot,
243
+ dummyPublication: metadataPublicationName(shard.appID, shard.shardNum),
244
+ lsn: String(toBigInt(lsn))
245
+ });
226
246
  return {
227
247
  slot,
228
- replica: must(await getReplicaState(tx, shard, replicaID), `replica ${replicaID} disappeared`),
229
- reservation: await keepSlotActiveUntilTakenOver(lc, replicationSession, slot, metadataPublicationName(shard.appID, shard.shardNum), toBigInt(lsn))
248
+ replica,
249
+ reservation
230
250
  };
231
251
  });
232
252
  lc.info?.(`successfully claimed slot ${slot}`);
233
253
  return reserved;
234
254
  } catch (e) {
255
+ if (reservation) try {
256
+ await endSession(reservation);
257
+ } catch {}
235
258
  lc.error?.(`unable to claim replication slot ${slot}`, e);
236
- await replicationSession.end();
237
259
  return null;
238
260
  }
239
261
  }
@@ -1 +1 @@
1
- {"version":3,"file":"replication-slots.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-source/pg/replication-slots.ts"],"sourcesContent":["import type {Readable} from 'node:stream';\nimport {\n PG_CONFIGURATION_LIMIT_EXCEEDED,\n PG_INSUFFICIENT_PRIVILEGE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport type postgres from 'postgres';\nimport type {JSONObject} from '../../../../../shared/src/bigint-json.ts';\nimport {must} from '../../../../../shared/src/must.ts';\nimport {runTx} from '../../../db/run-transaction.ts';\nimport {PG_17} from '../../../types/pg-versions.ts';\nimport {isPostgresError, type PostgresDB} from '../../../types/pg.ts';\nimport {upstreamSchema, type ShardID} from '../../../types/shards.ts';\nimport {orTimeout} from '../../../types/timeout.ts';\nimport {AutoResetSignal} from '../../change-streamer/schema/tables.ts';\nimport {\n createReplicationSessionFor,\n keepSlotActiveUntilTakenOver,\n} from './logical-replication/stream.ts';\nimport {toBigInt, toStateVersionString} from './lsn.ts';\nimport {\n InitialSync,\n Restore,\n type ReplicaStage,\n} from './schema/replica-stage-enum.ts';\nimport {\n createReplica,\n getReplicaState,\n getRestoreCandidates,\n metadataPublicationName,\n replicationSlotExpression,\n replicationSlotPrefix,\n type BackupOptions,\n type ReplicaState,\n} from './schema/shard.ts';\n\n// Record returned by `CREATE_REPLICATION_SLOT`\nexport type ReplicationSlot = {\n slot_name: string;\n consistent_point: string;\n snapshot_name: string;\n output_plugin: string;\n};\n\nexport type ReplicationSlotResult<T> = {\n slot: ReplicationSlot;\n capturedSnapshot: T;\n initialSession: Readable;\n replica: ReplicaState;\n};\n\nexport type ReservedSlot = {\n slot: string;\n reservation: Readable;\n replica: ReplicaState;\n};\n\nexport type CreateSlotSpec = {\n slotName: string;\n\n // Note: ignored if pgVersion < PG_17.\n failover?: boolean;\n\n // Create a temporary slot (i.e. not persisted, and automatically\n // cleaned up when the replication session ends).\n temporary?: boolean;\n\n // For overriding in tests.\n lockTimeout?: number;\n\n // A (non-replication) connection to the same upstream instance, used to\n // log what slot creation was waiting on if it times out.\n diagnosticsDB?: PostgresDB | undefined;\n};\n\n// When creating a replication slot, Postgres waits for open transactions\n// to complete before reserving a consistent_point (LSN) in the WAL and creating\n// a matching transaction snapshot. As such, it can technically take an arbitrary\n// amount of time (e.g. DDL operations, table-wide operations, etc.).\n//\n// However, to detect pathological situations, bound the amount of time that\n// the server waits for replication slot creation, so that a continual failure to\n// create a replication slot is surfaced by errors / alerts.\nexport const CREATE_REPLICATION_SLOT_TIMEOUT_MS = 60_000;\n\n// The lock_timeout is set 1s before the client-side orTimeout so that\n// Postgres reliably aborts first and tears down the walsender cleanly.\n// The client-side timeout remains as a fallback for network-level failures.\nconst SERVER_LOCK_TIMEOUT_MS = CREATE_REPLICATION_SLOT_TIMEOUT_MS - 1_000;\n\n// Note: The replication connection does not support the extended query protocol,\n// so all commands must be sent using sql.unsafe(). This is technically safe\n// because all placeholder values are under our control (i.e. \"slotName\").\nexport async function createReplicationSlot(\n lc: LogContext,\n session: postgres.Sql,\n {\n slotName,\n failover,\n temporary,\n lockTimeout = SERVER_LOCK_TIMEOUT_MS,\n diagnosticsDB,\n }: CreateSlotSpec,\n): Promise<ReplicationSlot> {\n // CREATE_REPLICATION_SLOT can hang indefinitely waiting for long-running\n // transactions to finish: internally it calls SnapBuildWaitSnapshot →\n // XactLockTableWait → LockAcquire on each running XID. statement_timeout\n // does NOT apply to replication commands, but lock_timeout does (it governs\n // the heavyweight lock wait inside LockAcquire). Setting it here causes\n // Postgres to raise ERRCODE_LOCK_NOT_AVAILABLE and cleanly tear down the\n // walsender, rather than relying solely on the client-side orTimeout\n // which can leave an orphaned backend.\n //\n // An orphaned walsender is actively harmful: by this point the replication\n // slot has already been created and is pinning WAL retention and catalog_xmin.\n // Worse, the slot is marked `active` (the walsender PID is still alive), so\n // the existing cleanup code (which drops inactive slots on retry) can't\n // reclaim it. Without lock_timeout the orphan persists until TCP keepalive\n // fires (~2h default) or the blocking transaction finishes.\n await session.unsafe(`SET lock_timeout = ${lockTimeout}`);\n\n const {pgVersion, pid: walsenderPID} = (\n await session.unsafe<{pgVersion: number; pid: number}[]>(`\n SELECT current_setting('server_version_num') as \"pgVersion\",\n pg_backend_pid() as \"pid\";\n `)\n )[0];\n\n const maybeTemporary = temporary ? 'TEMPORARY' : '';\n const options = failover && pgVersion >= PG_17 ? '(FAILOVER)' : '';\n const createSlot = session.unsafe<ReplicationSlot[]>(/*sql*/ `\n CREATE_REPLICATION_SLOT \"${slotName}\" ${maybeTemporary} LOGICAL pgoutput ${options}`);\n\n const raced = await orTimeout(createSlot, CREATE_REPLICATION_SLOT_TIMEOUT_MS);\n if (raced === 'timed-out') {\n if (diagnosticsDB) {\n await logSlotCreationBlockers(lc, diagnosticsDB, walsenderPID, slotName);\n }\n // Create slot can block indefinitely waiting for old transactions. End\n // this connection in the background and fail fast so the process restarts.\n void session\n .end()\n .catch(e =>\n lc.warn?.(`Error closing timed out replication slot session`, e),\n );\n throw new Error(\n `Timed out after ${CREATE_REPLICATION_SLOT_TIMEOUT_MS} ms creating replication slot ${slotName}.`,\n );\n }\n const [slot] = raced;\n lc.info?.(`Created replication slot ${slotName}`, slot);\n return slot;\n}\n\nconst SLOT_DIAGNOSTICS_TIMEOUT_MS = 5_000;\nconst SLOT_DIAGNOSTICS_MAX_TRANSACTIONS = 20;\n\n/**\n * Best-effort logging of what a timed out CREATE_REPLICATION_SLOT was waiting\n * on: the walsender's wait event and blocking pids, and the oldest\n * transactions holding an xid. Slot creation waits for every such transaction\n * on the instance (in all databases), each with its own lock_timeout, so a\n * chain of overlapping transactions can exceed the client-side timeout\n * without the lock_timeout ever firing.\n *\n * Query text is deliberately omitted since it can contain row values.\n *\n * Exported for testing.\n */\nexport async function logSlotCreationBlockers(\n lc: LogContext,\n sql: PostgresDB,\n walsenderPID: number,\n slotName: string,\n) {\n try {\n const result = await orTimeout(\n Promise.all([\n sql<JSONObject[]> /*sql*/ `\n SELECT pid, wait_event_type as \"waitEventType\", wait_event as \"waitEvent\",\n pg_blocking_pids(pid) as \"blockingPIDs\"\n FROM pg_stat_activity WHERE pid = ${walsenderPID}`,\n sql<JSONObject[]> /*sql*/ `\n SELECT pid, datname, usename, application_name as \"applicationName\",\n backend_type as \"backendType\", state,\n wait_event_type as \"waitEventType\", wait_event as \"waitEvent\",\n backend_xid::text as xid,\n xact_start::text as \"xactStart\",\n extract(epoch from now() - xact_start)::float8 as \"xactAgeSec\"\n FROM pg_stat_activity\n WHERE backend_xid IS NOT NULL AND pid <> ${walsenderPID}\n ORDER BY xact_start NULLS LAST\n LIMIT ${SLOT_DIAGNOSTICS_MAX_TRANSACTIONS}`,\n ]),\n SLOT_DIAGNOSTICS_TIMEOUT_MS,\n );\n if (result === 'timed-out') {\n lc.warn?.(`Timed out collecting diagnostics for slot ${slotName}`);\n return;\n }\n const [[walsender], transactions] = result;\n lc.warn?.(`Replication slot ${slotName} creation blocked`, {\n walsender: walsender ?? null,\n transactions,\n });\n } catch (e) {\n lc.warn?.(`Unable to collect diagnostics for slot ${slotName}`, e);\n }\n}\n\n/**\n * Replica and slot creation involves two sessions for proper coordination\n * with other replica management logic:\n *\n * * A normal transaction is started and acquires an advisory lock for\n * replica slot management. This is the same lock that cleanup logic\n * acquires before cleaning up replication slots.\n * * With the lock held, a new replication slot is created in a\n * replication session. The API of CREATE_REPLICATION_SLOT is such\n * that it cannot be done in a transaction, and cannot be followed by\n * any writes, or else its snapshot (which is needed for initial sync)\n * would be invalidated.\n * * Once the slot is created, the slot and replica information are recorded\n * in the `replicas` table before releasing the lock.\n *\n * This locking ensures that:\n * 1. multiple replication managers attempting to create a replication slot\n * will not use the same name for the replication slot (which is selected\n * from a pool of reused names).\n * 2. Running replication managers (which use an earlier replica of a lower\n * rank) will not delete the new slot during their cleanup logic, since\n * the slot will belong to a replica of a higher rank.\n *\n * When the replication slot is created, `captureSnapshot` callback is first\n * run while the snapshot at the consistent point is available. The callback\n * must capture the snapshot in postgres (e.g. `SET TRANSACTION SNAPSHOT`)\n * in order to use it. Once the callback completes, a placeholder replication\n * session (i.e. that does not consume any messages) is started, in order to\n * reserve the slot by mark it \"active\", effectively distinguishing the\n * initializing session from a failed or abandoned session.\n *\n * Note that starting the replication session releases the initial snapshot,\n * which is why the callback must capture it synchronously, before the\n * replication session is started.\n *\n * The placeholder replication session is closed when the\n * PostgresChangeSource takes over the slot to process the stream in earnest.\n * It is also returned for cleanup in the case of failures (and for control\n * in unit tests).\n *\n * For replica's created in the `Restore` stage, the `generation` is\n * initialized to an empty string instead of the slot's consistent point,\n * since the slot is not being used for initial sync. It is the\n * responsibiliy of the caller to call {@link initRestoreReplica} when\n * the source replica has been selected for restoring, which will carry\n * over the `generation` and other relevant values from the source.\n */\nexport async function createReplicaAndSlot<T>(\n lc: LogContext,\n sql: PostgresDB,\n sessionName: string,\n shard: ShardID,\n epoch: number,\n replicaID: string,\n failover: boolean,\n backupOptions: BackupOptions,\n captureSnapshot: (snapshot: string) => Promise<T>,\n stage: ReplicaStage,\n): Promise<ReplicationSlotResult<T>> {\n // Note: The replicationSession is closed by keepSlotActiveUntilTakenOver,\n // and only closed in this function if an error occurrs.\n const replicationSession = createReplicationSessionFor(sql, sessionName);\n const lockName = replicationSlotManagementLock(shard);\n const slotPoolPrefix = replicationSlotPrefix(shard);\n\n for (let first = true; ; first = false) {\n await dropUnclaimedSlots(lc, sql, shard);\n\n let slotName: string | undefined;\n try {\n return await runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n if (stage === InitialSync) {\n // With the lock acquired, ensure that only one initial sync is\n // active at a time. getRestoreCandidates() orders its results\n // with stage = InitialSync (and active = true) first.\n const others = await getRestoreCandidates(tx, shard, epoch);\n lc.info?.(`current replicas at epoch ${epoch}`, {replicas: others});\n if (others.length) {\n const [{id, stage, active}] = others;\n if (stage === InitialSync && active) {\n throw new AutoResetSignal(\n `another replica (${id}) is performing initial sync`,\n );\n }\n }\n }\n\n // Pick an available slotName from the slotPoolPrefix pool.\n const names = await tx<{name: string}[]> /*sql*/ `\n SELECT slot_name as name FROM pg_replication_slots\n WHERE slot_name LIKE ${slotPoolPrefix + '%'};\n `.values();\n const inUse = new Set(names.flat());\n for (let next = 0; ; next++) {\n const candidateName = `${slotPoolPrefix}${slotPoolSuffix(next)}`;\n if (!inUse.has(candidateName)) {\n slotName = candidateName;\n break;\n }\n }\n\n const slot = await createReplicationSlot(lc, replicationSession, {\n slotName,\n failover,\n diagnosticsDB: sql,\n });\n const capturedSnapshot = await captureSnapshot(slot.snapshot_name);\n const initialSession = await keepSlotActiveUntilTakenOver(\n lc,\n replicationSession,\n slot.slot_name,\n metadataPublicationName(shard.appID, shard.shardNum),\n toBigInt(slot.consistent_point),\n );\n\n await createReplica(\n tx,\n shard,\n replicaID,\n slot.slot_name,\n epoch,\n stage === InitialSync\n ? toStateVersionString(slot.consistent_point)\n : '', // initialized with initRestoreReplica\n backupOptions,\n stage,\n );\n\n const replica = must(\n await getReplicaState(tx, shard, replicaID),\n `replica ${replicaID} was not created`,\n );\n\n return {slot, capturedSnapshot, initialSession, replica};\n });\n } catch (e) {\n if (first && isPostgresError(e, PG_INSUFFICIENT_PRIVILEGE)) {\n // Some Postgres variants (e.g. Google Cloud SQL) require that\n // the user have the REPLICATION role in order to create a slot.\n // Note that this must be done by the upstreamDB connection, and\n // does not work in the replicationSession itself.\n await sql`ALTER ROLE current_user WITH REPLICATION`;\n lc.info?.(`Added the REPLICATION role to database user`);\n continue;\n }\n // Note: This is currently manually tested since max_replication_slots\n // is a PG startup parameter that other tests depend on.\n // TODO: Figure out a way to unit test this (with the full PG setup).\n if (first && isPostgresError(e, PG_CONFIGURATION_LIMIT_EXCEEDED)) {\n lc.warn?.(\n `Reached max replication slots. Attempting to clean up unused slots`,\n e,\n );\n // Drop any inactive replicas from failed initial syncs (e.g. inactive slots).\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n await sql`\n DELETE FROM ${sql(replicasTable)} USING pg_replication_slots slots\n WHERE replicas.slot = slots.slot_name AND NOT slots.active`;\n continue; // then let dropUnclaimedSlots() perform its cleanup\n }\n // Otherwise, clean up any created slot if something went wrong. The\n // slot may not exist (e.g. if its creation failed), and its name may\n // have been reused since the lock was released, so leave it to\n // dropUnclaimedSlots(), which only drops slots without a replica.\n if (slotName) {\n lc.warn?.(`deleting slot ${slotName} due to error`, e);\n await replicationSession.end();\n await dropUnclaimedSlots(lc, sql, shard);\n }\n throw e;\n }\n }\n}\n\n/**\n * Claims the inactive slot of the given replica for use by this task, opening\n * a replication session to mark it as active. The process should follow up by\n * taking over the slot via the PostgresChangeSource.\n *\n * The replica is identified by its ID (and not just its slot) because rows of\n * replicas whose slots were dropped outside of zero-cache's control may share\n * the slot name.\n */\nexport async function claimSlotForResumption(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n sessionName: string,\n {id: replicaID, slot}: Pick<ReplicaState, 'id' | 'slot'>,\n): Promise<ReservedSlot | null> {\n const replicationSession = createReplicationSessionFor(sql, sessionName);\n const lockName = replicationSlotManagementLock(shard);\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n\n try {\n const reserved = await runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n const replicas = await tx<{active: boolean; lsn: string}[]> /*sql*/ `\n SELECT active, confirmed_flush_lsn as lsn\n FROM pg_replication_slots\n JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name = ${slot} AND replica.id = ${replicaID};\n `;\n if (replicas.length === 0) {\n lc.warn?.(`no replica ${replicaID} found for slot ${slot}`);\n return null;\n }\n const [{active, lsn}] = replicas;\n if (active) {\n lc.warn?.(\n `replica ${replicaID} for slot ${slot} is active and cannot be claimed`,\n );\n return null;\n }\n // Mark the replica as Restoring.\n await tx`\n UPDATE ${tx(replicasTable)} SET \"stage\" = ${Restore} WHERE id = ${replicaID};\n `;\n const replica = must(\n await getReplicaState(tx, shard, replicaID),\n `replica ${replicaID} disappeared`,\n );\n\n return {\n slot,\n replica,\n reservation: await keepSlotActiveUntilTakenOver(\n lc,\n replicationSession,\n slot,\n metadataPublicationName(shard.appID, shard.shardNum),\n toBigInt(lsn),\n ),\n };\n });\n lc.info?.(`successfully claimed slot ${slot}`);\n return reserved;\n } catch (e) {\n lc.error?.(`unable to claim replication slot ${slot}`, e);\n await replicationSession.end();\n return null;\n }\n}\n\nexport function dropInactiveSlotsAndReplicas(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n slots: string[],\n) {\n const lockName = replicationSlotManagementLock(shard);\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n\n return runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n const dropped = await tx<{slot: string; replicaID: string | null}[]>\n /*sql*/ `\n SELECT slot_name as slot, replica.id as \"replicaID\", pg_drop_replication_slot(slot_name) \n FROM pg_replication_slots\n LEFT JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name IN ${tx(slots)} AND NOT active;\n `;\n if (dropped.length) {\n lc.info?.(`dropped ${dropped.length} inactive replication slot(s)`, {\n dropped,\n });\n }\n\n const replicas = dropped\n .map(({replicaID}) => replicaID)\n .filter(id => id !== null);\n if (replicas.length) {\n // Delete replicas associated with the inactive (and now dropped) slots.\n await tx\n /*sql*/ `DELETE FROM ${tx(replicasTable)} WHERE id IN ${tx(replicas)}`;\n lc.info?.(`deleted ${replicas.length} old replica(s)`, {replicas});\n }\n });\n}\n\nfunction dropUnclaimedSlots(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n): Promise<{dropped: number; active: number; draining: number}> {\n // The slot / replica cleanup happens within a transaction while holding\n // the replication slot management lock for this shard, to ensure that no\n // slot that belongs to a newer replica is dropped.\n const lockName = replicationSlotManagementLock(shard);\n const slotExpression = replicationSlotExpression(shard);\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n\n return runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n const dropped = await tx /*sql*/ `\n SELECT slot_name as slot, pg_drop_replication_slot(slot_name) \n FROM pg_replication_slots\n LEFT JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name LIKE ${slotExpression} \n AND NOT active\n AND replica.id IS NULL;\n `;\n if (dropped.length) {\n lc.info?.(`dropped inactive replication slots`, {dropped});\n }\n\n // Conversely, delete replicas whose slots no longer exist, e.g. because\n // they were dropped outside of zero-cache's control (such as in an\n // upstream failover). Slot names are reused, so such rows would otherwise\n // be associated with a new slot of the same name. This is done (and\n // committed) before the slot is created, both so that the rows are never\n // associated with it, and because CREATE_REPLICATION_SLOT waits for\n // transactions that have written (i.e. been assigned an xid).\n const orphaned = await tx<{id: string}[]> /*sql*/ `\n DELETE FROM ${tx(replicasTable)} replica\n WHERE NOT EXISTS (\n SELECT 1 FROM pg_replication_slots WHERE slot_name = replica.slot\n )\n RETURNING id;\n `;\n if (orphaned.length) {\n lc.warn?.(`deleted replicas whose slots no longer exist`, {\n replicas: orphaned.map(({id}) => id),\n });\n }\n\n const remaining = await tx<\n {slot: string; pid: number | null; id: string | null}[]\n > /*sql*/ `\n SELECT slot_name as slot, active_pid as pid, replica.id as id\n FROM pg_replication_slots\n LEFT JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name LIKE ${slotExpression};\n `;\n if (remaining.length) {\n lc.info?.(`remaining replication slots`, {remaining});\n }\n\n let active = 0;\n let draining = 0;\n for (const {id} of remaining) {\n if (id === null) {\n draining++;\n } else {\n active++;\n }\n }\n\n return {\n dropped: dropped.length,\n active,\n draining,\n };\n });\n}\n\nconst ALPHABET = 'abcdefghijklmnopqrstuvwxyz';\n\n// Alphabetic notation is used as the slot pool suffix to distinguish\n// it from the (numeric) shard num that's also encoded in the slot name.\nexport function slotPoolSuffix(n: number) {\n n++; // Adjust for 0-based indexing\n\n let suffix = '';\n while (n > 0) {\n n--;\n suffix = ALPHABET[n % 26] + suffix;\n n = Math.floor(n / 26);\n }\n return suffix;\n}\n\nfunction replicationSlotManagementLock(shard: ShardID) {\n return `replication-slot-management:${shard.appID}_${shard.shardNum}`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"replication-slots.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-source/pg/replication-slots.ts"],"sourcesContent":["import type {Readable} from 'node:stream';\nimport {\n PG_CONFIGURATION_LIMIT_EXCEEDED,\n PG_INSUFFICIENT_PRIVILEGE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport type postgres from 'postgres';\nimport type {JSONObject} from '../../../../../shared/src/bigint-json.ts';\nimport {must} from '../../../../../shared/src/must.ts';\nimport {runTx} from '../../../db/run-transaction.ts';\nimport {PG_17} from '../../../types/pg-versions.ts';\nimport {isPostgresError, type PostgresDB} from '../../../types/pg.ts';\nimport {upstreamSchema, type ShardID} from '../../../types/shards.ts';\nimport {orTimeout} from '../../../types/timeout.ts';\nimport {AutoResetSignal} from '../../change-streamer/schema/tables.ts';\nimport {\n createReplicationSessionFor,\n endSession,\n extractConnectionConfig,\n keepSlotActiveUntilTakenOver,\n} from './logical-replication/stream.ts';\nimport {toBigInt, toStateVersionString} from './lsn.ts';\nimport {\n InitialSync,\n Restore,\n type ReplicaStage,\n} from './schema/replica-stage-enum.ts';\nimport {\n createReplica,\n getReplicaState,\n getRestoreCandidates,\n metadataPublicationName,\n replicationSlotExpression,\n replicationSlotPrefix,\n type BackupOptions,\n type ReplicaState,\n} from './schema/shard.ts';\n\n// Record returned by `CREATE_REPLICATION_SLOT`\nexport type ReplicationSlot = {\n slot_name: string;\n consistent_point: string;\n snapshot_name: string;\n output_plugin: string;\n};\n\nexport type ReplicationSlotResult<T> = {\n slot: ReplicationSlot;\n capturedSnapshot: T;\n initialSession: Readable;\n replica: ReplicaState;\n};\n\nexport type ReservedSlot = {\n slot: string;\n reservation: Readable;\n replica: ReplicaState;\n};\n\nexport type CreateSlotSpec = {\n slotName: string;\n\n // Note: ignored if pgVersion < PG_17.\n failover?: boolean;\n\n // Create a temporary slot (i.e. not persisted, and automatically\n // cleaned up when the replication session ends).\n temporary?: boolean;\n\n // For overriding in tests.\n lockTimeout?: number;\n\n // A (non-replication) connection to the same upstream instance, used to\n // log what slot creation was waiting on if it times out.\n diagnosticsDB?: PostgresDB | undefined;\n};\n\n// When creating a replication slot, Postgres waits for open transactions\n// to complete before reserving a consistent_point (LSN) in the WAL and creating\n// a matching transaction snapshot. As such, it can technically take an arbitrary\n// amount of time (e.g. DDL operations, table-wide operations, etc.).\n//\n// However, to detect pathological situations, bound the amount of time that\n// the server waits for replication slot creation, so that a continual failure to\n// create a replication slot is surfaced by errors / alerts.\nexport const CREATE_REPLICATION_SLOT_TIMEOUT_MS = 60_000;\n\n// The lock_timeout is set 1s before the client-side orTimeout so that\n// Postgres reliably aborts first and tears down the walsender cleanly.\n// The client-side timeout remains as a fallback for network-level failures.\nconst SERVER_LOCK_TIMEOUT_MS = CREATE_REPLICATION_SLOT_TIMEOUT_MS - 1_000;\n\n// Note: The replication connection does not support the extended query protocol,\n// so all commands must be sent using sql.unsafe(). This is technically safe\n// because all placeholder values are under our control (i.e. \"slotName\").\nexport async function createReplicationSlot(\n lc: LogContext,\n session: postgres.Sql,\n {\n slotName,\n failover,\n temporary,\n lockTimeout = SERVER_LOCK_TIMEOUT_MS,\n diagnosticsDB,\n }: CreateSlotSpec,\n): Promise<ReplicationSlot> {\n // CREATE_REPLICATION_SLOT can hang indefinitely waiting for long-running\n // transactions to finish: internally it calls SnapBuildWaitSnapshot →\n // XactLockTableWait → LockAcquire on each running XID. statement_timeout\n // does NOT apply to replication commands, but lock_timeout does (it governs\n // the heavyweight lock wait inside LockAcquire). Setting it here causes\n // Postgres to raise ERRCODE_LOCK_NOT_AVAILABLE and cleanly tear down the\n // walsender, rather than relying solely on the client-side orTimeout\n // which can leave an orphaned backend.\n //\n // An orphaned walsender is actively harmful: by this point the replication\n // slot has already been created and is pinning WAL retention and catalog_xmin.\n // Worse, the slot is marked `active` (the walsender PID is still alive), so\n // the existing cleanup code (which drops inactive slots on retry) can't\n // reclaim it. Without lock_timeout the orphan persists until TCP keepalive\n // fires (~2h default) or the blocking transaction finishes.\n await session.unsafe(`SET lock_timeout = ${lockTimeout}`);\n\n const {pgVersion, pid: walsenderPID} = (\n await session.unsafe<{pgVersion: number; pid: number}[]>(`\n SELECT current_setting('server_version_num') as \"pgVersion\",\n pg_backend_pid() as \"pid\";\n `)\n )[0];\n\n const maybeTemporary = temporary ? 'TEMPORARY' : '';\n const options = failover && pgVersion >= PG_17 ? '(FAILOVER)' : '';\n const createSlot = session.unsafe<ReplicationSlot[]>(/*sql*/ `\n CREATE_REPLICATION_SLOT \"${slotName}\" ${maybeTemporary} LOGICAL pgoutput ${options}`);\n\n const raced = await orTimeout(createSlot, CREATE_REPLICATION_SLOT_TIMEOUT_MS);\n if (raced === 'timed-out') {\n if (diagnosticsDB) {\n await logSlotCreationBlockers(lc, diagnosticsDB, walsenderPID, slotName);\n }\n // Create slot can block indefinitely waiting for old transactions. End\n // this connection in the background and fail fast so the process restarts.\n void session\n .end()\n .catch(e =>\n lc.warn?.(`Error closing timed out replication slot session`, e),\n );\n throw new Error(\n `Timed out after ${CREATE_REPLICATION_SLOT_TIMEOUT_MS} ms creating replication slot ${slotName}.`,\n );\n }\n const [slot] = raced;\n lc.info?.(`Created replication slot ${slotName}`, slot);\n return slot;\n}\n\nconst SLOT_DIAGNOSTICS_TIMEOUT_MS = 5_000;\nconst SLOT_DIAGNOSTICS_MAX_TRANSACTIONS = 20;\n\n/**\n * Best-effort logging of what a timed out CREATE_REPLICATION_SLOT was waiting\n * on: the walsender's wait event and blocking pids, and the oldest\n * transactions holding an xid. Slot creation waits for every such transaction\n * on the instance (in all databases), each with its own lock_timeout, so a\n * chain of overlapping transactions can exceed the client-side timeout\n * without the lock_timeout ever firing.\n *\n * Query text is deliberately omitted since it can contain row values.\n *\n * Exported for testing.\n */\nexport async function logSlotCreationBlockers(\n lc: LogContext,\n sql: PostgresDB,\n walsenderPID: number,\n slotName: string,\n) {\n try {\n const result = await orTimeout(\n Promise.all([\n sql<JSONObject[]> /*sql*/ `\n SELECT pid, wait_event_type as \"waitEventType\", wait_event as \"waitEvent\",\n pg_blocking_pids(pid) as \"blockingPIDs\"\n FROM pg_stat_activity WHERE pid = ${walsenderPID}`,\n sql<JSONObject[]> /*sql*/ `\n SELECT pid, datname, usename, application_name as \"applicationName\",\n backend_type as \"backendType\", state,\n wait_event_type as \"waitEventType\", wait_event as \"waitEvent\",\n backend_xid::text as xid,\n xact_start::text as \"xactStart\",\n extract(epoch from now() - xact_start)::float8 as \"xactAgeSec\"\n FROM pg_stat_activity\n WHERE backend_xid IS NOT NULL AND pid <> ${walsenderPID}\n ORDER BY xact_start NULLS LAST\n LIMIT ${SLOT_DIAGNOSTICS_MAX_TRANSACTIONS}`,\n ]),\n SLOT_DIAGNOSTICS_TIMEOUT_MS,\n );\n if (result === 'timed-out') {\n lc.warn?.(`Timed out collecting diagnostics for slot ${slotName}`);\n return;\n }\n const [[walsender], transactions] = result;\n lc.warn?.(`Replication slot ${slotName} creation blocked`, {\n walsender: walsender ?? null,\n transactions,\n });\n } catch (e) {\n lc.warn?.(`Unable to collect diagnostics for slot ${slotName}`, e);\n }\n}\n\n/**\n * Replica and slot creation involves two sessions for proper coordination\n * with other replica management logic:\n *\n * * A normal transaction is started and acquires an advisory lock for\n * replica slot management. This is the same lock that cleanup logic\n * acquires before cleaning up replication slots.\n * * With the lock held, a new replication slot is created in a\n * replication session. The API of CREATE_REPLICATION_SLOT is such\n * that it cannot be done in a transaction, and cannot be followed by\n * any writes, or else its snapshot (which is needed for initial sync)\n * would be invalidated.\n * * Once the slot is created, the slot and replica information are recorded\n * in the `replicas` table before releasing the lock.\n *\n * This locking ensures that:\n * 1. multiple replication managers attempting to create a replication slot\n * will not use the same name for the replication slot (which is selected\n * from a pool of reused names).\n * 2. Running replication managers (which use an earlier replica of a lower\n * rank) will not delete the new slot during their cleanup logic, since\n * the slot will belong to a replica of a higher rank.\n *\n * When the replication slot is created, `captureSnapshot` callback is first\n * run while the snapshot at the consistent point is available. The callback\n * must capture the snapshot in postgres (e.g. `SET TRANSACTION SNAPSHOT`)\n * in order to use it. Once the callback completes, a placeholder replication\n * session (i.e. that does not consume any messages) is started, in order to\n * reserve the slot by mark it \"active\", effectively distinguishing the\n * initializing session from a failed or abandoned session.\n *\n * Note that starting the replication session releases the initial snapshot,\n * which is why the callback must capture it synchronously, before the\n * replication session is started.\n *\n * The placeholder replication session is closed when the\n * PostgresChangeSource takes over the slot to process the stream in earnest.\n * It is also returned for cleanup in the case of failures (and for control\n * in unit tests).\n *\n * For replica's created in the `Restore` stage, the `generation` is\n * initialized to an empty string instead of the slot's consistent point,\n * since the slot is not being used for initial sync. It is the\n * responsibiliy of the caller to call {@link initRestoreReplica} when\n * the source replica has been selected for restoring, which will carry\n * over the `generation` and other relevant values from the source.\n */\nexport async function createReplicaAndSlot<T>(\n lc: LogContext,\n sql: PostgresDB,\n sessionName: string,\n shard: ShardID,\n epoch: number,\n replicaID: string,\n failover: boolean,\n backupOptions: BackupOptions,\n captureSnapshot: (snapshot: string) => Promise<T>,\n stage: ReplicaStage,\n): Promise<ReplicationSlotResult<T>> {\n // Note: The replicationSession is used to create the replication slot\n // and closed immediately afterwards, or on error.\n const replicationSession = createReplicationSessionFor(sql, sessionName);\n const lockName = replicationSlotManagementLock(shard);\n const slotPoolPrefix = replicationSlotPrefix(shard);\n\n for (let first = true; ; first = false) {\n await dropUnclaimedSlots(lc, sql, shard);\n\n let slotName: string | undefined;\n let initialSession: Readable | undefined;\n try {\n return await runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n if (stage === InitialSync) {\n // With the lock acquired, ensure that only one initial sync is\n // active at a time. getRestoreCandidates() orders its results\n // with stage = InitialSync (and active = true) first.\n const others = await getRestoreCandidates(tx, shard, epoch);\n lc.info?.(`current replicas at epoch ${epoch}`, {replicas: others});\n if (others.length) {\n const [{id, stage, active}] = others;\n if (stage === InitialSync && active) {\n throw new AutoResetSignal(\n `another replica (${id}) is performing initial sync`,\n );\n }\n }\n }\n\n // Pick an available slotName from the slotPoolPrefix pool.\n const names = await tx<{name: string}[]> /*sql*/ `\n SELECT slot_name as name FROM pg_replication_slots\n WHERE slot_name LIKE ${slotPoolPrefix + '%'};\n `.values();\n const inUse = new Set(names.flat());\n for (let next = 0; ; next++) {\n const candidateName = `${slotPoolPrefix}${slotPoolSuffix(next)}`;\n if (!inUse.has(candidateName)) {\n slotName = candidateName;\n break;\n }\n }\n\n const slot = await createReplicationSlot(lc, replicationSession, {\n slotName,\n failover,\n diagnosticsDB: sql,\n });\n const capturedSnapshot = await captureSnapshot(slot.snapshot_name);\n await replicationSession.end();\n initialSession = await keepSlotActiveUntilTakenOver(lc, {\n db: extractConnectionConfig(sql),\n slot: slot.slot_name,\n dummyPublication: metadataPublicationName(\n shard.appID,\n shard.shardNum,\n ),\n lsn: String(toBigInt(slot.consistent_point)),\n });\n\n await createReplica(\n tx,\n shard,\n replicaID,\n slot.slot_name,\n epoch,\n stage === InitialSync\n ? toStateVersionString(slot.consistent_point)\n : '', // initialized with initRestoreReplica\n backupOptions,\n stage,\n );\n\n const replica = must(\n await getReplicaState(tx, shard, replicaID),\n `replica ${replicaID} was not created`,\n );\n\n return {slot, capturedSnapshot, initialSession, replica};\n });\n } catch (e) {\n if (first && isPostgresError(e, PG_INSUFFICIENT_PRIVILEGE)) {\n // Some Postgres variants (e.g. Google Cloud SQL) require that\n // the user have the REPLICATION role in order to create a slot.\n // Note that this must be done by the upstreamDB connection, and\n // does not work in the replicationSession itself.\n await sql`ALTER ROLE current_user WITH REPLICATION`;\n lc.info?.(`Added the REPLICATION role to database user`);\n continue;\n }\n // Note: This is currently manually tested since max_replication_slots\n // is a PG startup parameter that other tests depend on.\n // TODO: Figure out a way to unit test this (with the full PG setup).\n if (first && isPostgresError(e, PG_CONFIGURATION_LIMIT_EXCEEDED)) {\n lc.warn?.(\n `Reached max replication slots. Attempting to clean up unused slots`,\n e,\n );\n // Drop any inactive replicas from failed initial syncs (e.g. inactive slots).\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n await sql`\n DELETE FROM ${sql(replicasTable)} USING pg_replication_slots slots\n WHERE replicas.slot = slots.slot_name AND NOT slots.active`;\n continue; // then let dropUnclaimedSlots() perform its cleanup\n }\n if (initialSession) {\n try {\n await endSession(initialSession);\n } catch {}\n }\n // Otherwise, clean up any created slot if something went wrong. The\n // slot may not exist (e.g. if its creation failed), and its name may\n // have been reused since the lock was released, so leave it to\n // dropUnclaimedSlots(), which only drops slots without a replica.\n if (slotName) {\n lc.warn?.(`deleting slot ${slotName} due to error`, e);\n try {\n await replicationSession.end();\n } catch {}\n await dropUnclaimedSlots(lc, sql, shard);\n }\n throw e;\n }\n }\n}\n\n/**\n * Claims the inactive slot of the given replica for use by this task, opening\n * a replication session to mark it as active. The process should follow up by\n * taking over the slot via the PostgresChangeSource.\n *\n * The replica is identified by its ID (and not just its slot) because rows of\n * replicas whose slots were dropped outside of zero-cache's control may share\n * the slot name.\n */\nexport async function claimSlotForResumption(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n _sessionName: string,\n {id: replicaID, slot}: Pick<ReplicaState, 'id' | 'slot'>,\n): Promise<ReservedSlot | null> {\n const lockName = replicationSlotManagementLock(shard);\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n\n let reservation: Readable | undefined;\n try {\n const reserved = await runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n const replicas = await tx<{active: boolean; lsn: string}[]> /*sql*/ `\n SELECT active, confirmed_flush_lsn as lsn\n FROM pg_replication_slots\n JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name = ${slot} AND replica.id = ${replicaID};\n `;\n if (replicas.length === 0) {\n lc.warn?.(`no replica ${replicaID} found for slot ${slot}`);\n return null;\n }\n const [{active, lsn}] = replicas;\n if (active) {\n lc.warn?.(\n `replica ${replicaID} for slot ${slot} is active and cannot be claimed`,\n );\n return null;\n }\n // Mark the replica as Restoring.\n await tx`\n UPDATE ${tx(replicasTable)} SET \"stage\" = ${Restore} WHERE id = ${replicaID};\n `;\n const replica = must(\n await getReplicaState(tx, shard, replicaID),\n `replica ${replicaID} disappeared`,\n );\n\n reservation = await keepSlotActiveUntilTakenOver(lc, {\n db: extractConnectionConfig(sql),\n slot,\n dummyPublication: metadataPublicationName(shard.appID, shard.shardNum),\n lsn: String(toBigInt(lsn)),\n });\n return {\n slot,\n replica,\n reservation,\n };\n });\n lc.info?.(`successfully claimed slot ${slot}`);\n return reserved;\n } catch (e) {\n if (reservation) {\n try {\n await endSession(reservation);\n } catch {}\n }\n lc.error?.(`unable to claim replication slot ${slot}`, e);\n return null;\n }\n}\n\nexport function dropInactiveSlotsAndReplicas(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n slots: string[],\n) {\n const lockName = replicationSlotManagementLock(shard);\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n\n return runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n const dropped = await tx<{slot: string; replicaID: string | null}[]>\n /*sql*/ `\n SELECT slot_name as slot, replica.id as \"replicaID\", pg_drop_replication_slot(slot_name) \n FROM pg_replication_slots\n LEFT JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name IN ${tx(slots)} AND NOT active;\n `;\n if (dropped.length) {\n lc.info?.(`dropped ${dropped.length} inactive replication slot(s)`, {\n dropped,\n });\n }\n\n const replicas = dropped\n .map(({replicaID}) => replicaID)\n .filter(id => id !== null);\n if (replicas.length) {\n // Delete replicas associated with the inactive (and now dropped) slots.\n await tx\n /*sql*/ `DELETE FROM ${tx(replicasTable)} WHERE id IN ${tx(replicas)}`;\n lc.info?.(`deleted ${replicas.length} old replica(s)`, {replicas});\n }\n });\n}\n\nfunction dropUnclaimedSlots(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n): Promise<{dropped: number; active: number; draining: number}> {\n // The slot / replica cleanup happens within a transaction while holding\n // the replication slot management lock for this shard, to ensure that no\n // slot that belongs to a newer replica is dropped.\n const lockName = replicationSlotManagementLock(shard);\n const slotExpression = replicationSlotExpression(shard);\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n\n return runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n const dropped = await tx /*sql*/ `\n SELECT slot_name as slot, pg_drop_replication_slot(slot_name) \n FROM pg_replication_slots\n LEFT JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name LIKE ${slotExpression} \n AND NOT active\n AND replica.id IS NULL;\n `;\n if (dropped.length) {\n lc.info?.(`dropped inactive replication slots`, {dropped});\n }\n\n // Conversely, delete replicas whose slots no longer exist, e.g. because\n // they were dropped outside of zero-cache's control (such as in an\n // upstream failover). Slot names are reused, so such rows would otherwise\n // be associated with a new slot of the same name. This is done (and\n // committed) before the slot is created, both so that the rows are never\n // associated with it, and because CREATE_REPLICATION_SLOT waits for\n // transactions that have written (i.e. been assigned an xid).\n const orphaned = await tx<{id: string}[]> /*sql*/ `\n DELETE FROM ${tx(replicasTable)} replica\n WHERE NOT EXISTS (\n SELECT 1 FROM pg_replication_slots WHERE slot_name = replica.slot\n )\n RETURNING id;\n `;\n if (orphaned.length) {\n lc.warn?.(`deleted replicas whose slots no longer exist`, {\n replicas: orphaned.map(({id}) => id),\n });\n }\n\n const remaining = await tx<\n {slot: string; pid: number | null; id: string | null}[]\n > /*sql*/ `\n SELECT slot_name as slot, active_pid as pid, replica.id as id\n FROM pg_replication_slots\n LEFT JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name LIKE ${slotExpression};\n `;\n if (remaining.length) {\n lc.info?.(`remaining replication slots`, {remaining});\n }\n\n let active = 0;\n let draining = 0;\n for (const {id} of remaining) {\n if (id === null) {\n draining++;\n } else {\n active++;\n }\n }\n\n return {\n dropped: dropped.length,\n active,\n draining,\n };\n });\n}\n\nconst ALPHABET = 'abcdefghijklmnopqrstuvwxyz';\n\n// Alphabetic notation is used as the slot pool suffix to distinguish\n// it from the (numeric) shard num that's also encoded in the slot name.\nexport function slotPoolSuffix(n: number) {\n n++; // Adjust for 0-based indexing\n\n let suffix = '';\n while (n > 0) {\n n--;\n suffix = ALPHABET[n % 26] + suffix;\n n = Math.floor(n / 26);\n }\n return suffix;\n}\n\nfunction replicationSlotManagementLock(shard: ShardID) {\n return `replication-slot-management:${shard.appID}_${shard.shardNum}`;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAQjD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAiB,KAAK,WAAW,EAAC,MAAM,6BAA6B,CAAC;AAa7E;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,WAAW,EAClB,2BAA2B,UAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAiBf;AAyQD,eAAO,MAAM,sBAAsB,QASwB,CAAC"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAQjD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAiB,KAAK,WAAW,EAAC,MAAM,6BAA6B,CAAC;AAa7E;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,WAAW,EAClB,2BAA2B,UAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAiBf;AA8SD,eAAO,MAAM,sBAAsB,QASwB,CAAC"}
@@ -161,7 +161,21 @@ function getIncrementalMigrations(shard, installPartialIndexTriggers) {
161
161
  `;
162
162
  }
163
163
  },
164
- ...installPartialIndexTriggers ? { 29: { migrateSchema: async (lc, sql) => {
164
+ 30: {
165
+ migrateSchema: async (_, sql) => {
166
+ await sql`
167
+ ALTER TABLE ${sql(upstreamSchema(shard))}.replicas
168
+ ADD COLUMN IF NOT EXISTS "pgChangeLog" BOOL DEFAULT false;
169
+ `;
170
+ },
171
+ migrateData: async (_, sql) => {
172
+ await sql`
173
+ UPDATE ${sql(upstreamSchema(shard))}.replicas SET "pgChangeLog" = true
174
+ WHERE "subscriberContext" IS NOT NULL;
175
+ `;
176
+ }
177
+ },
178
+ ...installPartialIndexTriggers ? { 31: { migrateSchema: async (lc, sql) => {
165
179
  const [{ publications }] = await sql`
166
180
  SELECT publications FROM ${sql(shardConfigTable)}`;
167
181
  await setupTriggers(lc, sql, {
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/init.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {\n runSchemaMigrations,\n type IncrementalMigrationMap,\n type Migration,\n} from '../../../../db/migration.ts';\nimport type {PostgresDB} from '../../../../types/pg.ts';\nimport {upstreamSchema, type ShardConfig} from '../../../../types/shards.ts';\nimport {id} from '../../../../types/sql.ts';\nimport {AutoResetSignal} from '../../../change-streamer/schema/tables.ts';\nimport {publishedSchema} from './published.ts';\nimport * as ReplicaStage from './replica-stage-enum.ts';\nimport {\n getMutationsTableDefinition,\n legacyReplicationSlot,\n metadataPublicationName,\n setupTablesAndReplication,\n setupTriggers,\n} from './shard.ts';\n\n/**\n * Ensures that a shard is set up for initial sync.\n */\nexport async function ensureShardSchema(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardConfig,\n installPartialIndexTriggers = true,\n): Promise<void> {\n const initialSetup: Migration = {\n migrateSchema: (lc, tx) =>\n setupTablesAndReplication(lc, tx, shard, installPartialIndexTriggers),\n minSafeVersion: 1,\n };\n await runSchemaMigrations(\n lc,\n `upstream-shard-${shard.appID}`,\n upstreamSchema(shard),\n db,\n initialSetup,\n // The incremental migration of any existing replicas will be replaced by\n // the incoming replica being synced, so the replicaVersion here is\n // unnecessary.\n getIncrementalMigrations(shard, installPartialIndexTriggers),\n );\n}\n\nfunction getIncrementalMigrations(\n shard: ShardConfig,\n installPartialIndexTriggers: boolean,\n): IncrementalMigrationMap {\n const shardConfigTable = `${upstreamSchema(shard)}.shardConfig`;\n\n return {\n 6: {\n migrateSchema: () => {\n throw new AutoResetSignal('resetting to upgrade shard schema');\n },\n },\n\n // v7: Updates the DDL event trigger protocol to v2, and adds support for\n // ALTER SCHEMA x RENAME TO y\n // (migration logic subsumbed by v23)\n\n // v8: Adds support for non-disruptive resyncs, which tracks multiple\n // replicas with different slot names.\n 8: {\n migrateSchema: async (lc, sql) => {\n const legacyShardConfigSchema = v.object({\n replicaVersion: v.string().nullable(),\n initialSchema: publishedSchema.nullable(),\n });\n const result = await sql`\n SELECT \"replicaVersion\", \"initialSchema\" FROM ${sql(shardConfigTable)}`;\n assert(\n result.length === 1,\n () => `Expected exactly one shardConfig row, got ${result.length}`,\n );\n const {replicaVersion, initialSchema} = v.parse(\n result[0],\n legacyShardConfigSchema,\n 'passthrough',\n );\n\n await Promise.all([\n sql`\n CREATE TABLE ${sql(upstreamSchema(shard))}.replicas (\n \"slot\" TEXT PRIMARY KEY,\n \"version\" TEXT NOT NULL,\n \"initialSchema\" JSON NOT NULL\n );\n `,\n sql`\n INSERT INTO ${sql(upstreamSchema(shard))}.replicas ${sql({\n slot: legacyReplicationSlot(shard),\n version: replicaVersion,\n initialSchema,\n })}\n `,\n sql`\n ALTER TABLE ${sql(shardConfigTable)} DROP \"replicaVersion\", DROP \"initialSchema\"\n `,\n ]);\n lc.info?.(`Upgraded schema to support non-disruptive resyncs`);\n },\n },\n\n // v9: Fixes field ordering of compound indexes. This incremental migration\n // only fixes indexes resulting from new schema changes. A full resync is\n // required to fix existing indexes.\n //\n // The migration has been subsumed by the identical logic for migrating\n // to v12 (i.e. a trigger upgrade).\n\n // Adds the `mutations` table used to track mutation results.\n 10: {\n migrateSchema: async (lc, sql) => {\n await sql.unsafe(/*sql*/ `\n ${getMutationsTableDefinition(upstreamSchema(shard))}\n ALTER PUBLICATION ${id(metadataPublicationName(shard.appID, shard.shardNum))} ADD TABLE ${id(upstreamSchema(shard))}.\"mutations\";\n `);\n lc.info?.('Upgraded schema with new mutations table');\n },\n },\n\n // v11: Formerly dropped the schemaVersions table, but restored in the v13\n // migration for rollback safety.\n\n // v12: Upgrade DDL trigger to query schemaOID, needed information for auto-backfill.\n // (subsumed by v14)\n\n // Recreates the legacy schemaVersions table that was prematurely dropped\n // in the (former) v11 migration. It needs to remain present for at least one\n // release in order to be rollback safe.\n //\n // TODO: Drop the table once a release that no longer reads the table has\n // been rolled out.\n 13: {\n migrateSchema: async (_, sql) => {\n await sql`\n CREATE TABLE IF NOT EXISTS ${sql(upstreamSchema(shard))}.\"schemaVersions\" (\n \"minSupportedVersion\" INT4,\n \"maxSupportedVersion\" INT4,\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );`;\n await sql`\n INSERT INTO ${sql(upstreamSchema(shard))}.\"schemaVersions\" \n (\"lock\", \"minSupportedVersion\", \"maxSupportedVersion\")\n VALUES (true, 1, 1)\n ON CONFLICT DO NOTHING;\n `;\n },\n },\n\n // v14: Upgrade DDL trigger to log more info to PG logs.\n // (subsumed by v16)\n\n // Add initialSyncContext column to replicas table.\n 15: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas\n ADD COLUMN \"initialSyncContext\" JSON,\n ADD COLUMN \"subscriberContext\" JSON\n `;\n },\n },\n\n // v16: Upgrade DDL trigger to fire on all ALTER TABLE statements\n // to catch the *removal* of a table from the published set.\n // v17 (1.0.0): Upgrade DDL triggers to support the COMMENT ON PUBLICATION hook for\n // working around the lack of event trigger support for PUBLICATION\n // changes in supabase.\n // This also adds forwards-compatible support for hierarchical logical\n // message prefixes and unknown ddl event types.\n // v18: Pure refactoring of event trigger code.\n // v19 (1.4.0): Correctly handle concurrently issued DDL statements.\n // v20 (1.4.0): Handle nested DDL triggers\n // v21 (1.5.0): Handle cross-transaction DDL operations (i.e. concurrent\n // index operations), and support manual invocation of update_schemas().\n\n 22: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ALTER \"initialSchema\" DROP NOT NULL;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n DROP CONSTRAINT replicas_pkey;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN id TEXT PRIMARY KEY DEFAULT replace(gen_random_uuid()::text, '-', '');\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN rank BIGSERIAL;\n `;\n },\n },\n\n // v23 (1.6.0): Event triggers: Handle the case where current_query()\n // returns NULL. Add more logging to debug unexpected messages\n 23: {\n migrateSchema: async (lc, sql) => {\n const [{publications}] = await sql<{publications: string[]}[]>`\n SELECT publications FROM ${sql(shardConfigTable)}`;\n await setupTriggers(lc, sql, {...shard, publications});\n lc.info?.(`Upgraded DDL event triggers`);\n },\n },\n\n // v24 (1.10.0):\n // - adds the \"backupPath\" column for per-replica backups.\n // - adds the \"generation\" column to replace \"version\".\n 24: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN \"backupPath\" TEXT;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN \"generation\" TEXT;\n `;\n },\n\n migrateData: async (_, sql) => {\n // Backfill the \"generation\" column to eventually replace \"version\"\n await sql`\n UPDATE ${sql(upstreamSchema(shard))}.replicas SET \"generation\" = \"version\";\n `;\n },\n },\n\n // v25 (1.10.0):\n // - adds the \"backupV5\" column to signal the current semantics\n // of the replication slot's confirmed_flush_lsn with respect to what\n // has been backed up.\n 25: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN \"backupV5\" BOOL DEFAULT false;\n `;\n },\n },\n\n // v27 (1.11.0):\n // - make `generation` NOT NULL and stop reading `version`\n 27: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ALTER \"generation\" SET NOT NULL;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ALTER \"version\" DROP NOT NULL;\n `;\n },\n },\n\n 28: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ADD \"epoch\" INT4 DEFAULT 0;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ADD \"stage\" INT4 DEFAULT 0; -- ReplicaStage.InitialSync\n `;\n\n const metapub = metadataPublicationName(shard.appID, shard.shardNum);\n await sql`\n ALTER PUBLICATION ${sql(metapub)} ADD TABLE ${sql(upstreamSchema(shard))}.replicas (id);\n `;\n // Make sure supabase gets the ALTER PUBLICATION change\n await sql`\n SELECT ${sql(upstreamSchema(shard))}.update_schemas();\n `;\n },\n\n migrateData: async (_, sql) => {\n await sql`\n UPDATE ${sql(upstreamSchema(shard))}.replicas SET \"stage\" = ${ReplicaStage.Replicate}\n WHERE \"subscriberContext\" IS NOT NULL;\n `;\n },\n },\n\n // Note: While this is conditional, it always has to be bumped to the last version.\n ...(installPartialIndexTriggers\n ? {\n // v28: Upgrade the DDL event triggers to include partial indexes in\n // schema snapshots. Note that setupTriggers() also refreshes the\n // stored \"publishedSchema\" so that the change in format does not\n // manifest as a spurious schema change.\n 29: {\n migrateSchema: async (lc, sql) => {\n const [{publications}] = await sql<{publications: string[]}[]>`\n SELECT publications FROM ${sql(shardConfigTable)}`;\n await setupTriggers(lc, sql, {...shard, publications});\n lc.info?.(`Upgraded DDL event triggers`);\n },\n },\n }\n : {}),\n };\n}\n\n// Referenced in tests.\nexport const CURRENT_SCHEMA_VERSION = Object.keys(\n getIncrementalMigrations(\n {\n appID: 'unused',\n shardNum: 0,\n publications: ['foo'],\n },\n true,\n ),\n).reduce((prev, curr) => Math.max(prev, parseInt(curr)), 0);\n"],"mappings":"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"}
1
+ {"version":3,"file":"init.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/init.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {\n runSchemaMigrations,\n type IncrementalMigrationMap,\n type Migration,\n} from '../../../../db/migration.ts';\nimport type {PostgresDB} from '../../../../types/pg.ts';\nimport {upstreamSchema, type ShardConfig} from '../../../../types/shards.ts';\nimport {id} from '../../../../types/sql.ts';\nimport {AutoResetSignal} from '../../../change-streamer/schema/tables.ts';\nimport {publishedSchema} from './published.ts';\nimport * as ReplicaStage from './replica-stage-enum.ts';\nimport {\n getMutationsTableDefinition,\n legacyReplicationSlot,\n metadataPublicationName,\n setupTablesAndReplication,\n setupTriggers,\n} from './shard.ts';\n\n/**\n * Ensures that a shard is set up for initial sync.\n */\nexport async function ensureShardSchema(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardConfig,\n installPartialIndexTriggers = true,\n): Promise<void> {\n const initialSetup: Migration = {\n migrateSchema: (lc, tx) =>\n setupTablesAndReplication(lc, tx, shard, installPartialIndexTriggers),\n minSafeVersion: 1,\n };\n await runSchemaMigrations(\n lc,\n `upstream-shard-${shard.appID}`,\n upstreamSchema(shard),\n db,\n initialSetup,\n // The incremental migration of any existing replicas will be replaced by\n // the incoming replica being synced, so the replicaVersion here is\n // unnecessary.\n getIncrementalMigrations(shard, installPartialIndexTriggers),\n );\n}\n\nfunction getIncrementalMigrations(\n shard: ShardConfig,\n installPartialIndexTriggers: boolean,\n): IncrementalMigrationMap {\n const shardConfigTable = `${upstreamSchema(shard)}.shardConfig`;\n\n return {\n 6: {\n migrateSchema: () => {\n throw new AutoResetSignal('resetting to upgrade shard schema');\n },\n },\n\n // v7: Updates the DDL event trigger protocol to v2, and adds support for\n // ALTER SCHEMA x RENAME TO y\n // (migration logic subsumbed by v23)\n\n // v8: Adds support for non-disruptive resyncs, which tracks multiple\n // replicas with different slot names.\n 8: {\n migrateSchema: async (lc, sql) => {\n const legacyShardConfigSchema = v.object({\n replicaVersion: v.string().nullable(),\n initialSchema: publishedSchema.nullable(),\n });\n const result = await sql`\n SELECT \"replicaVersion\", \"initialSchema\" FROM ${sql(shardConfigTable)}`;\n assert(\n result.length === 1,\n () => `Expected exactly one shardConfig row, got ${result.length}`,\n );\n const {replicaVersion, initialSchema} = v.parse(\n result[0],\n legacyShardConfigSchema,\n 'passthrough',\n );\n\n await Promise.all([\n sql`\n CREATE TABLE ${sql(upstreamSchema(shard))}.replicas (\n \"slot\" TEXT PRIMARY KEY,\n \"version\" TEXT NOT NULL,\n \"initialSchema\" JSON NOT NULL\n );\n `,\n sql`\n INSERT INTO ${sql(upstreamSchema(shard))}.replicas ${sql({\n slot: legacyReplicationSlot(shard),\n version: replicaVersion,\n initialSchema,\n })}\n `,\n sql`\n ALTER TABLE ${sql(shardConfigTable)} DROP \"replicaVersion\", DROP \"initialSchema\"\n `,\n ]);\n lc.info?.(`Upgraded schema to support non-disruptive resyncs`);\n },\n },\n\n // v9: Fixes field ordering of compound indexes. This incremental migration\n // only fixes indexes resulting from new schema changes. A full resync is\n // required to fix existing indexes.\n //\n // The migration has been subsumed by the identical logic for migrating\n // to v12 (i.e. a trigger upgrade).\n\n // Adds the `mutations` table used to track mutation results.\n 10: {\n migrateSchema: async (lc, sql) => {\n await sql.unsafe(/*sql*/ `\n ${getMutationsTableDefinition(upstreamSchema(shard))}\n ALTER PUBLICATION ${id(metadataPublicationName(shard.appID, shard.shardNum))} ADD TABLE ${id(upstreamSchema(shard))}.\"mutations\";\n `);\n lc.info?.('Upgraded schema with new mutations table');\n },\n },\n\n // v11: Formerly dropped the schemaVersions table, but restored in the v13\n // migration for rollback safety.\n\n // v12: Upgrade DDL trigger to query schemaOID, needed information for auto-backfill.\n // (subsumed by v14)\n\n // Recreates the legacy schemaVersions table that was prematurely dropped\n // in the (former) v11 migration. It needs to remain present for at least one\n // release in order to be rollback safe.\n //\n // TODO: Drop the table once a release that no longer reads the table has\n // been rolled out.\n 13: {\n migrateSchema: async (_, sql) => {\n await sql`\n CREATE TABLE IF NOT EXISTS ${sql(upstreamSchema(shard))}.\"schemaVersions\" (\n \"minSupportedVersion\" INT4,\n \"maxSupportedVersion\" INT4,\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );`;\n await sql`\n INSERT INTO ${sql(upstreamSchema(shard))}.\"schemaVersions\" \n (\"lock\", \"minSupportedVersion\", \"maxSupportedVersion\")\n VALUES (true, 1, 1)\n ON CONFLICT DO NOTHING;\n `;\n },\n },\n\n // v14: Upgrade DDL trigger to log more info to PG logs.\n // (subsumed by v16)\n\n // Add initialSyncContext column to replicas table.\n 15: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas\n ADD COLUMN \"initialSyncContext\" JSON,\n ADD COLUMN \"subscriberContext\" JSON\n `;\n },\n },\n\n // v16: Upgrade DDL trigger to fire on all ALTER TABLE statements\n // to catch the *removal* of a table from the published set.\n // v17 (1.0.0): Upgrade DDL triggers to support the COMMENT ON PUBLICATION hook for\n // working around the lack of event trigger support for PUBLICATION\n // changes in supabase.\n // This also adds forwards-compatible support for hierarchical logical\n // message prefixes and unknown ddl event types.\n // v18: Pure refactoring of event trigger code.\n // v19 (1.4.0): Correctly handle concurrently issued DDL statements.\n // v20 (1.4.0): Handle nested DDL triggers\n // v21 (1.5.0): Handle cross-transaction DDL operations (i.e. concurrent\n // index operations), and support manual invocation of update_schemas().\n\n 22: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ALTER \"initialSchema\" DROP NOT NULL;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n DROP CONSTRAINT replicas_pkey;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN id TEXT PRIMARY KEY DEFAULT replace(gen_random_uuid()::text, '-', '');\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN rank BIGSERIAL;\n `;\n },\n },\n\n // v23 (1.6.0): Event triggers: Handle the case where current_query()\n // returns NULL. Add more logging to debug unexpected messages\n 23: {\n migrateSchema: async (lc, sql) => {\n const [{publications}] = await sql<{publications: string[]}[]>`\n SELECT publications FROM ${sql(shardConfigTable)}`;\n await setupTriggers(lc, sql, {...shard, publications});\n lc.info?.(`Upgraded DDL event triggers`);\n },\n },\n\n // v24 (1.10.0):\n // - adds the \"backupPath\" column for per-replica backups.\n // - adds the \"generation\" column to replace \"version\".\n 24: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN \"backupPath\" TEXT;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN \"generation\" TEXT;\n `;\n },\n\n migrateData: async (_, sql) => {\n // Backfill the \"generation\" column to eventually replace \"version\"\n await sql`\n UPDATE ${sql(upstreamSchema(shard))}.replicas SET \"generation\" = \"version\";\n `;\n },\n },\n\n // v25 (1.10.0):\n // - adds the \"backupV5\" column to signal the current semantics\n // of the replication slot's confirmed_flush_lsn with respect to what\n // has been backed up.\n 25: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN \"backupV5\" BOOL DEFAULT false;\n `;\n },\n },\n\n // v27 (1.11.0):\n // - make `generation` NOT NULL and stop reading `version`\n 27: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ALTER \"generation\" SET NOT NULL;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ALTER \"version\" DROP NOT NULL;\n `;\n },\n },\n\n 28: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ADD \"epoch\" INT4 DEFAULT 0;\n `;\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas ADD \"stage\" INT4 DEFAULT 0; -- ReplicaStage.InitialSync\n `;\n\n const metapub = metadataPublicationName(shard.appID, shard.shardNum);\n await sql`\n ALTER PUBLICATION ${sql(metapub)} ADD TABLE ${sql(upstreamSchema(shard))}.replicas (id);\n `;\n // Make sure supabase gets the ALTER PUBLICATION change\n await sql`\n SELECT ${sql(upstreamSchema(shard))}.update_schemas();\n `;\n },\n\n migrateData: async (_, sql) => {\n await sql`\n UPDATE ${sql(upstreamSchema(shard))}.replicas SET \"stage\" = ${ReplicaStage.Replicate}\n WHERE \"subscriberContext\" IS NOT NULL;\n `;\n },\n },\n\n // v30: Adds the \"pgChangeLog\" column, which records whether the\n // replication-manager that took over a replica's slot acks it only after\n // storing transactions in the PG change log (RMv1.5), in which case a\n // slot position past the change log's head reflects acked keepalives\n // rather than missing transactions.\n //\n // The column defaults to false (i.e. unknown), so that a slot is assumed\n // to be possibly ahead of the change log until it is taken over by a\n // replication-manager that sets it. Existing replicas that have been taken\n // over are backfilled as true (see migrateData). Note that a replication-manager that\n // predates this column does not reset it when taking over a slot, so\n // once the column is set, RMv2 (in which acks are not gated on the PG\n // change log) must run at least this version.\n 30: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas \n ADD COLUMN IF NOT EXISTS \"pgChangeLog\" BOOL DEFAULT false;\n `;\n },\n\n // Existing replicas whose slots have been taken over (i.e. that have a\n // subscriberContext) were acked by RMv1 or RMv1.5, which store\n // transactions in the PG change log before acking them. This assumes\n // that no RMv2 is running when the migration is run. Replicas that have\n // not been taken over (e.g. fork destinations) remain unknown, as their\n // slots have not been acked.\n migrateData: async (_, sql) => {\n await sql`\n UPDATE ${sql(upstreamSchema(shard))}.replicas SET \"pgChangeLog\" = true\n WHERE \"subscriberContext\" IS NOT NULL;\n `;\n },\n },\n\n // Note: While this is conditional, it always has to be bumped to the last\n // version. (It was v29, and is re-run for shards at v29 when it is bumped,\n // which is harmless as setupTriggers() is idempotent.)\n ...(installPartialIndexTriggers\n ? {\n // v31: Upgrade the DDL event triggers to include partial indexes in\n // schema snapshots. Note that setupTriggers() also refreshes the\n // stored \"publishedSchema\" so that the change in format does not\n // manifest as a spurious schema change.\n 31: {\n migrateSchema: async (lc, sql) => {\n const [{publications}] = await sql<{publications: string[]}[]>`\n SELECT publications FROM ${sql(shardConfigTable)}`;\n await setupTriggers(lc, sql, {...shard, publications});\n lc.info?.(`Upgraded DDL event triggers`);\n },\n },\n }\n : {}),\n };\n}\n\n// Referenced in tests.\nexport const CURRENT_SCHEMA_VERSION = Object.keys(\n getIncrementalMigrations(\n {\n appID: 'unused',\n shardNum: 0,\n publications: ['foo'],\n },\n true,\n ),\n).reduce((prev, curr) => Math.max(prev, parseInt(curr)), 0);\n"],"mappings":"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"}
@@ -42,7 +42,7 @@ export type InternalShardConfig = v.Infer<typeof internalShardConfigSchema>;
42
42
  declare const replicaSchema: v.ObjectType<Omit<{
43
43
  publications: v.ArrayType<v.Type<string>>;
44
44
  ddlDetection: v.Type<boolean>;
45
- }, "backupPath" | "backupV5" | "epoch" | "generation" | "id" | "initialSchema" | "initialSyncContext" | "rank" | "slot" | "stage" | "subscriberContext"> & Omit<{
45
+ }, "backupPath" | "backupV5" | "epoch" | "generation" | "id" | "initialSchema" | "initialSyncContext" | "pgChangeLog" | "rank" | "slot" | "stage" | "subscriberContext"> & Omit<{
46
46
  id: v.Type<string>;
47
47
  rank: v.Type<bigint>;
48
48
  slot: v.Type<string>;
@@ -51,6 +51,14 @@ declare const replicaSchema: v.ObjectType<Omit<{
51
51
  stage: v.Type<0 | 1 | 2>;
52
52
  backupPath: v.UnionType<[v.Type<null>, v.Type<string>]>;
53
53
  backupV5: v.Type<boolean>;
54
+ /**
55
+ * Whether the replication-manager that took over the replica's slot acks it
56
+ * only after storing transactions in the PG change log (i.e. RMv1.5). The
57
+ * slot's position past the change log's head then reflects acked
58
+ * keepalives, rather than transactions missing from the change log.
59
+ * False if unknown.
60
+ */
61
+ pgChangeLog: v.Type<boolean>;
54
62
  }, "initialSchema" | "initialSyncContext" | "subscriberContext"> & {
55
63
  initialSchema: v.Type<{
56
64
  indexes: {
@@ -131,6 +139,14 @@ declare const replicaStateSchema: v.ObjectType<Omit<{
131
139
  stage: v.Type<0 | 1 | 2>;
132
140
  backupPath: v.UnionType<[v.Type<null>, v.Type<string>]>;
133
141
  backupV5: v.Type<boolean>;
142
+ /**
143
+ * Whether the replication-manager that took over the replica's slot acks it
144
+ * only after storing transactions in the PG change log (i.e. RMv1.5). The
145
+ * slot's position past the change log's head then reflects acked
146
+ * keepalives, rather than transactions missing from the change log.
147
+ * False if unknown.
148
+ */
149
+ pgChangeLog: v.Type<boolean>;
134
150
  }, "active" | "confirmedFlushLsn"> & {
135
151
  active: v.Type<boolean>;
136
152
  confirmedFlushLsn: v.Type<string>;
@@ -139,6 +155,8 @@ export type ReplicaState = v.Infer<typeof replicaStateSchema>;
139
155
  export type BackupOptions = {
140
156
  backupPath: string | null;
141
157
  backupV5: boolean;
158
+ /** See `pgChangeLog` in {@link ReplicaState}. */
159
+ pgChangeLog: boolean;
142
160
  };
143
161
  /**
144
162
  * Creates a new replica to mark it as the owner of a specified `slot`.
@@ -149,7 +167,7 @@ export type BackupOptions = {
149
167
  * Once initial sync is complete, {@link initInitialSyncReplica} should be called to
150
168
  * make the replica usable for incremental sync.
151
169
  */
152
- export declare function createReplica(sql: PostgresDB, shard: ShardID, id: string, slot: string, epoch: number, replicaVersion: string, { backupPath, backupV5 }: BackupOptions, stage: ReplicaStage): Promise<void>;
170
+ export declare function createReplica(sql: PostgresDB, shard: ShardID, id: string, slot: string, epoch: number, replicaVersion: string, { backupPath, backupV5, pgChangeLog }: BackupOptions, stage: ReplicaStage): Promise<void>;
153
171
  export declare function initInitialSyncReplica(sql: PostgresDB, shard: ShardID, id: string, { tables, indexes }: PublishedSchema, initialSyncContext: JSONObject): Promise<void>;
154
172
  export declare function initRestoreReplica(sql: PostgresDB, shard: ShardID, { sourceID, destID }: {
155
173
  sourceID: string;