@rocicorp/zero 1.8.0 → 1.9.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/shared/src/cache.d.ts +8 -1
- package/out/shared/src/cache.d.ts.map +1 -1
- package/out/shared/src/cache.js +25 -9
- package/out/shared/src/cache.js.map +1 -1
- package/out/shared/src/objects.d.ts +6 -1
- package/out/shared/src/objects.d.ts.map +1 -1
- package/out/shared/src/objects.js +19 -4
- package/out/shared/src/objects.js.map +1 -1
- package/out/zero/package.js +2 -19
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +4 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +21 -6
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.d.ts +6 -0
- package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.js +8 -0
- package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
- package/out/zero-cache/src/db/migration-lite.d.ts.map +1 -1
- package/out/zero-cache/src/db/migration-lite.js +5 -2
- package/out/zero-cache/src/db/migration-lite.js.map +1 -1
- package/out/zero-cache/src/db/migration.d.ts.map +1 -1
- package/out/zero-cache/src/db/migration.js +3 -1
- package/out/zero-cache/src/db/migration.js.map +1 -1
- package/out/zero-cache/src/db/sqlite-corruption.d.ts +11 -0
- package/out/zero-cache/src/db/sqlite-corruption.d.ts.map +1 -0
- package/out/zero-cache/src/db/sqlite-corruption.js +191 -0
- package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -0
- package/out/zero-cache/src/server/anonymous-otel-start.d.ts +1 -1
- package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
- package/out/zero-cache/src/server/backup-watermark-reader.js +1 -1
- package/out/zero-cache/src/server/backup-watermark-reader.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +7 -2
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/logging.d.ts +8 -0
- package/out/zero-cache/src/server/logging.d.ts.map +1 -1
- package/out/zero-cache/src/server/logging.js +27 -5
- package/out/zero-cache/src/server/logging.js.map +1 -1
- package/out/zero-cache/src/server/main.d.ts.map +1 -1
- package/out/zero-cache/src/server/main.js +6 -1
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/mutator.js +1 -1
- package/out/zero-cache/src/server/mutator.js.map +1 -1
- package/out/zero-cache/src/server/otel-log-sink.d.ts +7 -1
- package/out/zero-cache/src/server/otel-log-sink.d.ts.map +1 -1
- package/out/zero-cache/src/server/otel-log-sink.js +7 -2
- package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
- package/out/zero-cache/src/server/reaper.js +1 -1
- package/out/zero-cache/src/server/reaper.js.map +1 -1
- package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/server/replicator.js +11 -1
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/shadow-syncer.js +1 -1
- package/out/zero-cache/src/server/shadow-syncer.js.map +1 -1
- package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/server/syncer.js +6 -1
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-schema.js +8 -5
- package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +36 -2
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +76 -33
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +15 -0
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js +41 -10
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts +21 -0
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +34 -5
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts +2 -2
- package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +61 -87
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +17 -12
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +15 -3
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +20 -3
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-reader.js +2 -2
- package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +4 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.js +28 -15
- package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.js +5 -12
- package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
- package/out/zero-cache/src/services/replicator/write-worker.js +38 -7
- package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +8 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +66 -26
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/query-covering.d.ts +28 -0
- package/out/zero-cache/src/services/view-syncer/query-covering.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/query-covering.js +159 -0
- package/out/zero-cache/src/services/view-syncer/query-covering.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +2 -0
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +71 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/lite.d.ts.map +1 -1
- package/out/zero-cache/src/types/lite.js +4 -1
- package/out/zero-cache/src/types/lite.js.map +1 -1
- package/out/zero-cache/src/types/pg.d.ts +18 -0
- package/out/zero-cache/src/types/pg.d.ts.map +1 -1
- package/out/zero-cache/src/types/pg.js +50 -0
- package/out/zero-cache/src/types/pg.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +1 -0
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/crud.d.ts.map +1 -1
- package/out/zero-client/src/client/crud.js +3 -4
- package/out/zero-client/src/client/crud.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-schema/src/builder/relationship-builder.d.ts.map +1 -1
- package/out/zero-schema/src/builder/relationship-builder.js +3 -2
- package/out/zero-schema/src/builder/relationship-builder.js.map +1 -1
- package/out/zero-schema/src/builder/schema-builder.d.ts.map +1 -1
- package/out/zero-schema/src/builder/schema-builder.js +7 -7
- package/out/zero-schema/src/builder/schema-builder.js.map +1 -1
- package/out/zero-schema/src/builder/table-builder.d.ts.map +1 -1
- package/out/zero-schema/src/builder/table-builder.js +2 -1
- package/out/zero-schema/src/builder/table-builder.js.map +1 -1
- package/out/zero-server/src/custom.js +2 -1
- package/out/zero-server/src/custom.js.map +1 -1
- package/out/zql/src/ivm/view-apply-change.d.ts.map +1 -1
- package/out/zql/src/ivm/view-apply-change.js +2 -1
- package/out/zql/src/ivm/view-apply-change.js.map +1 -1
- package/out/zql/src/mutate/crud.d.ts +10 -8
- package/out/zql/src/mutate/crud.d.ts.map +1 -1
- package/out/zql/src/mutate/crud.js +3 -4
- package/out/zql/src/mutate/crud.js.map +1 -1
- package/out/zqlite/src/db.js +2 -2
- package/out/zqlite/src/internal/statement-cache.d.ts +25 -1
- package/out/zqlite/src/internal/statement-cache.d.ts.map +1 -1
- package/out/zqlite/src/internal/statement-cache.js +48 -18
- package/out/zqlite/src/internal/statement-cache.js.map +1 -1
- package/out/zqlite/src/query-builder.js +3 -3
- package/out/zqlite/src/query-builder.js.map +1 -1
- package/out/zqlite/src/sqlite-cost-model.js +5 -5
- package/out/zqlite/src/table-source.js +3 -3
- package/package.json +2 -33
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initial-sync.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-source/pg/initial-sync.ts"],"sourcesContent":["import {mkdtemp, rm} from 'node:fs/promises';\nimport {platform, tmpdir} from 'node:os';\nimport {join} from 'node:path';\nimport {Writable} from 'node:stream';\nimport {pipeline} from 'node:stream/promises';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../../../shared/src/asserts.ts';\nimport type {JSONObject} from '../../../../../shared/src/bigint-json.ts';\nimport {must} from '../../../../../shared/src/must.ts';\nimport {equals} from '../../../../../shared/src/set-utils.ts';\nimport type {DownloadStatus} from '../../../../../zero-events/src/status.ts';\nimport {Database} from '../../../../../zqlite/src/db.ts';\nimport {\n createLiteIndexStatement,\n createLiteTableStatement,\n} from '../../../db/create.ts';\nimport {listIndexes, listTables} from '../../../db/lite-tables.ts';\nimport * as Mode from '../../../db/mode-enum.ts';\nimport {\n BinaryCopyParser,\n hasBinaryDecoder,\n makeBinaryDecoder,\n textCastDecoder,\n} from '../../../db/pg-copy-binary.ts';\nimport {TsvParser} from '../../../db/pg-copy.ts';\nimport {\n mapPostgresToLite,\n mapPostgresToLiteIndex,\n} from '../../../db/pg-to-lite.ts';\nimport {getTypeParsers} from '../../../db/pg-type-parser.ts';\nimport {runTx} from '../../../db/run-transaction.ts';\nimport type {IndexSpec, PublishedTableSpec} from '../../../db/specs.ts';\nimport {importSnapshot, TransactionPool} from '../../../db/transaction-pool.ts';\nimport {\n getOrCreateCounter,\n getOrCreateHistogram,\n} from '../../../observability/metrics.ts';\nimport {\n JSON_STRINGIFIED,\n liteValue,\n type LiteValueType,\n} from '../../../types/lite.ts';\nimport {liteTableName} from '../../../types/names.ts';\nimport {PG_15, PG_17} from '../../../types/pg-versions.ts';\nimport {\n connectPgClient,\n pgClient,\n type PostgresDB,\n type PostgresTransaction,\n type PostgresValueType,\n} from '../../../types/pg.ts';\nimport {CpuProfiler} from '../../../types/profiler.ts';\nimport type {ShardConfig} from '../../../types/shards.ts';\nimport {ALLOWED_APP_ID_CHARACTERS} from '../../../types/shards.ts';\nimport {id} from '../../../types/sql.ts';\nimport {ReplicationStatusPublisher} from '../../replicator/replication-status.ts';\nimport {ColumnMetadataStore} from '../../replicator/schema/column-metadata.ts';\nimport {initReplicationState} from '../../replicator/schema/replication-state.ts';\nimport {toStateVersionString} from './lsn.ts';\nimport {createReplicaAndSlot} from './replication-slots.ts';\nimport {ensureShardSchema} from './schema/init.ts';\nimport {getPublicationInfo} from './schema/published.ts';\nimport {\n dropShard,\n getInternalShardConfig,\n initReplica,\n validatePublications,\n} from './schema/shard.ts';\n\nexport type InitialSyncOptions = {\n tableCopyWorkers: number;\n profileCopy?: boolean | undefined;\n textCopy?: boolean | undefined;\n replicationSlotFailover?: boolean | undefined;\n /**\n * When set, run initial sync in \"shadow\" mode for verification: skip all\n * upstream mutations (no replication slot, no addReplica, no dropShard, no\n * slot drop on failure), suppress status events, and optionally sample\n * rows from each table via TABLESAMPLE BERNOULLI + LIMIT. The caller is\n * responsible for providing (and discarding) a throwaway SQLite `tx`.\n */\n shadow?:\n | {\n /** 0 < rate <= 1. When 1, no TABLESAMPLE clause is added. */\n sampleRate: number;\n /**\n * LIMIT N cap appended after TABLESAMPLE. Required: shadow sync is\n * for verification only, so every run must commit to a row budget.\n */\n maxRowsPerTable: number;\n }\n | undefined;\n};\n\n/** Server context to store with the initial sync metadata for debugging. */\nexport type ServerContext = JSONObject;\n\nexport async function initialSync(\n lc: LogContext,\n shard: ShardConfig,\n tx: Database,\n upstreamURI: string,\n syncOptions: InitialSyncOptions,\n context: ServerContext,\n) {\n if (!ALLOWED_APP_ID_CHARACTERS.test(shard.appID)) {\n throw new Error(\n 'The App ID may only consist of lower-case letters, numbers, and the underscore character',\n );\n }\n const {\n tableCopyWorkers,\n profileCopy,\n textCopy = false,\n replicationSlotFailover = false,\n shadow,\n } = syncOptions;\n const syncMode: InitialSyncMode = shadow ? 'shadow' : 'initial';\n const copyFormat: CopyFormat = textCopy ? 'text' : 'binary';\n const start = performance.now();\n let sql: PostgresDB | undefined;\n let replicationSession: PostgresDB | undefined;\n const replicaID = Date.now().toString();\n let slotName: string | undefined; // undefined === shadow\n const statusPublisher = ReplicationStatusPublisher.forRunningTransaction(\n tx,\n shadow ? async () => {} : undefined,\n ).publish(lc, 'Initializing');\n let releaseShadowSnapshot: (() => Promise<void>) | undefined;\n try {\n const copyProfiler = profileCopy ? await CpuProfiler.connect() : null;\n sql = await connectPgClient(lc, upstreamURI, 'initial-sync');\n // Replication session is only needed to create a replication slot in the\n // real path. In shadow mode we export a snapshot on a normal connection\n // instead, so no replication session is opened.\n replicationSession = shadow\n ? undefined\n : pgClient(lc, upstreamURI, 'initial-sync-replication-session', {\n ['fetch_types']: false, // Necessary for the streaming protocol\n connection: {replication: 'database'}, // https://www.postgresql.org/docs/current/protocol-replication.html\n });\n\n const pgVersion = await checkUpstreamConfig(sql);\n\n // In shadow mode we assume the shard is already initialized and just\n // read back the existing publications. `ensurePublishedTables` would\n // otherwise run DDL and potentially call `dropShard`, which must never\n // happen during a shadow run.\n const {publications} = shadow\n ? await getInternalShardConfig(sql, shard)\n : await ensurePublishedTables(lc, sql, shard);\n lc.info?.(`Upstream is setup with publications [${publications}]`);\n\n const {database, host} = sql.options;\n lc.info?.(\n shadow\n ? `acquiring exported snapshot on ${database}@${host} (shadow mode)`\n : `opening replication session to ${database}@${host}`,\n );\n\n let snapshot: string;\n let lsn: string;\n\n if (shadow) {\n const acquired = await acquireExportedSnapshotForShadowSync(\n lc,\n upstreamURI,\n );\n snapshot = acquired.snapshot;\n lsn = acquired.lsn;\n releaseShadowSnapshot = acquired.release;\n } else {\n const slot = await createReplicaAndSlot(\n lc,\n sql,\n must(replicationSession),\n shard,\n replicaID,\n replicationSlotFailover && pgVersion >= PG_17,\n );\n snapshot = slot.snapshot_name;\n lsn = slot.consistent_point;\n slotName = slot.slot_name;\n }\n\n const initialVersion = toStateVersionString(lsn);\n\n initReplicationState(tx, publications, initialVersion, context);\n\n // Run up to MAX_WORKERS to copy of tables at the replication slot's snapshot.\n // Retrieve the published schema at the consistent_point.\n const published = await runTx(\n sql,\n async tx => {\n await tx.unsafe(/* sql*/ `SET TRANSACTION SNAPSHOT '${snapshot}'`);\n return getPublicationInfo(tx, publications);\n },\n {mode: Mode.READONLY},\n );\n // Note: If this throws, initial-sync is aborted.\n validatePublications(lc, published);\n\n // Now that tables have been validated, kick off the copiers.\n const {tables, indexes} = published;\n const numTables = tables.length;\n if (platform() === 'win32' && tableCopyWorkers < numTables) {\n lc.warn?.(\n `Increasing the number of copy workers from ${tableCopyWorkers} to ` +\n `${numTables} to work around a Node/Postgres connection bug`,\n );\n }\n const numWorkers =\n platform() === 'win32'\n ? numTables\n : Math.min(tableCopyWorkers, numTables);\n\n const copyPool = await connectPgClient(\n lc,\n upstreamURI,\n 'initial-sync-copy-worker',\n {\n max: numWorkers,\n ['max_lifetime']: 120 * 60, // set a long (2h) limit for COPY streaming\n },\n );\n const copiers = startTableCopyWorkers(\n lc,\n copyPool,\n snapshot,\n numWorkers,\n numTables,\n );\n try {\n createLiteTables(tx, tables, initialVersion);\n const sampleRate = shadow?.sampleRate;\n const maxRowsPerTable = shadow?.maxRowsPerTable;\n const downloads = await Promise.all(\n tables.map(spec =>\n copiers.processReadTask((db, lc) =>\n getInitialDownloadState(lc, db, spec, shadow !== undefined),\n ),\n ),\n );\n statusPublisher.publish(\n lc,\n 'Initializing',\n `Copying ${numTables} upstream tables at version ${initialVersion}`,\n 5000,\n () => ({downloadStatus: downloads.map(({status}) => status)}),\n );\n\n void copyProfiler?.start();\n const copyStart = performance.now();\n const copyResults = await Promise.all(\n downloads.map(table =>\n copiers.processReadTask((db, lc) =>\n copy(\n lc,\n table,\n copyPool,\n db,\n tx,\n textCopy,\n syncMode,\n sampleRate,\n maxRowsPerTable,\n ),\n ),\n ),\n );\n const copyElapsed = performance.now() - copyStart;\n void copyProfiler?.stopAndDispose(lc, 'initial-copy');\n copiers.setDone();\n\n const copySummary = initialSyncCopySummary(copyResults, copyElapsed);\n\n statusPublisher.publish(\n lc,\n 'Indexing',\n `Creating ${indexes.length} indexes`,\n 5000,\n );\n const indexStart = performance.now();\n createLiteIndices(lc, tx, indexes);\n const index = performance.now() - indexStart;\n lc.info?.(`Created indexes (${index.toFixed(3)} ms)`);\n\n if (slotName && replicaID) {\n await initReplica(sql, shard, replicaID, published, context);\n } else {\n assert(shadow, 'expected to be in shadow sync if there is no slotName');\n const rowsByTable = new Map<string, number>();\n for (let i = 0; i < downloads.length; i++) {\n rowsByTable.set(downloads[i].status.table, copyResults[i].rows);\n }\n verifyShadowReplica(lc, tx, published, rowsByTable);\n }\n\n const elapsed = performance.now() - start;\n const copyOtherMs = Math.max(0, elapsed - copySummary.flushMs - index);\n recordInitialSyncRunMetrics(\n {\n durationMs: elapsed,\n rows: copySummary.rows,\n copyBytes: copySummary.copyBytes,\n copyMs: copySummary.copyMs,\n copyOtherMs,\n flushMs: copySummary.flushMs,\n indexMs: index,\n },\n {\n result: 'success',\n syncMode,\n copyFormat,\n },\n );\n lc.info?.(\n `Synced ${copySummary.rows.toLocaleString()} rows of ${numTables} tables in ${publications} up to ${lsn} ` +\n `(flush: ${copySummary.flushMs.toFixed(3)}, index: ${index.toFixed(3)}, total: ${elapsed.toFixed(3)} ms)`,\n {\n syncMode,\n copyFormat,\n publications,\n lsn,\n ...copySummary,\n indexes: indexes.length,\n indexMs: index,\n copyOtherMs,\n totalMs: elapsed,\n },\n );\n } finally {\n // All meaningful errors are handled at the processReadTask() call site.\n void copyPool.end().catch(e => lc.warn?.(`Error closing copyPool`, e));\n }\n } catch (e) {\n recordInitialSyncRunMetrics(\n {durationMs: performance.now() - start},\n {\n result: 'error',\n syncMode,\n copyFormat,\n },\n );\n if (slotName && sql) {\n // If initial-sync did not succeed, make a best effort to drop the\n // orphaned replication slot to avoid running out of slots in\n // pathological cases that result in repeated failures.\n lc.warn?.(`dropping replication slot ${slotName}`, e);\n await sql`\n SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots\n WHERE slot_name = ${slotName};\n `.catch(e => lc.warn?.(`Unable to drop replication slot ${slotName}`, e));\n }\n await statusPublisher.publishAndThrowError(lc, 'Initializing', e);\n } finally {\n statusPublisher.stop();\n if (releaseShadowSnapshot) {\n await releaseShadowSnapshot().catch(e =>\n lc.warn?.(`Error releasing shadow snapshot`, e),\n );\n }\n if (replicationSession) {\n await replicationSession.end();\n }\n if (sql) {\n await sql.end();\n }\n }\n}\n\nexport type ShadowSyncOptions = {\n sampleRate: number;\n maxRowsPerTable: number;\n /**\n * Parent directory for the throwaway SQLite replica. Defaults to the OS\n * tmpdir. Primarily for tests that need to isolate the scratch directory.\n */\n parentDir?: string | undefined;\n};\n\n/**\n * Exercises the initial-sync code path against a sample of rows from every\n * published table, writing into a throwaway SQLite database that is deleted\n * when the run ends. Produces zero upstream mutations: no replication slot,\n * no `addReplica`, no `dropShard`, no status events.\n *\n * Intended to be invoked periodically so that if a customer ever needs a\n * full reset, we have recent confidence that `initialSync` still works.\n * The shard must already be initialized upstream.\n */\nexport async function shadowInitialSync(\n lc: LogContext,\n shard: ShardConfig,\n upstreamURI: string,\n shadow: ShadowSyncOptions,\n context: ServerContext,\n syncOptions?: Pick<InitialSyncOptions, 'textCopy'>,\n): Promise<void> {\n const dir = await mkdtemp(\n join(shadow.parentDir ?? tmpdir(), 'zero-shadow-sync-'),\n );\n const dbPath = join(dir, 'shadow-replica.db');\n const db = new Database(lc, dbPath);\n try {\n await initialSync(\n lc,\n shard,\n db,\n upstreamURI,\n {\n // Shadow sync copies small samples, so one worker is plenty —\n // no reason to burn additional upstream connections.\n tableCopyWorkers: 1,\n textCopy: syncOptions?.textCopy,\n shadow,\n },\n context,\n );\n } finally {\n try {\n db.close();\n } catch (e) {\n lc.warn?.(`Error closing shadow replica db`, e);\n }\n await rm(dir, {recursive: true, force: true}).catch(e =>\n lc.warn?.(`Error cleaning up shadow replica dir ${dir}`, e),\n );\n }\n}\n\nasync function checkUpstreamConfig(sql: PostgresDB) {\n const {walLevel, version} = (\n await sql<{walLevel: string; version: number}[]>`\n SELECT current_setting('wal_level') as \"walLevel\", \n current_setting('server_version_num') as \"version\";\n `\n )[0];\n\n if (walLevel !== 'logical') {\n throw new Error(\n `Postgres must be configured with \"wal_level = logical\" (currently: \"${walLevel})`,\n );\n }\n if (version < PG_15) {\n throw new Error(\n `Must be running Postgres 15 or higher (currently: \"${version}\")`,\n );\n }\n return version;\n}\n\nasync function ensurePublishedTables(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardConfig,\n validate = true,\n): Promise<{publications: string[]}> {\n const {database, host} = sql.options;\n lc.info?.(`Ensuring upstream PUBLICATION on ${database}@${host}`);\n\n await ensureShardSchema(lc, sql, shard);\n const {publications} = await getInternalShardConfig(sql, shard);\n\n if (validate) {\n let valid = false;\n const nonInternalPublications = publications.filter(\n p => !p.startsWith('_'),\n );\n const exists = await sql`\n SELECT pubname FROM pg_publication WHERE pubname IN ${sql(publications)}\n `.values();\n if (exists.length !== publications.length) {\n lc.warn?.(\n `some configured publications [${publications}] are missing: ` +\n `[${exists.flat()}]. resyncing`,\n );\n } else if (\n !equals(new Set(shard.publications), new Set(nonInternalPublications))\n ) {\n lc.warn?.(\n `requested publications [${shard.publications}] differ from previous` +\n `publications [${nonInternalPublications}]. resyncing`,\n );\n } else {\n valid = true;\n }\n if (!valid) {\n await sql.unsafe(dropShard(shard.appID, shard.shardNum));\n return ensurePublishedTables(lc, sql, shard, false);\n }\n }\n return {publications};\n}\n\nfunction startTableCopyWorkers(\n lc: LogContext,\n db: PostgresDB,\n snapshot: string,\n numWorkers: number,\n numTables: number,\n): TransactionPool {\n const {init} = importSnapshot(snapshot);\n const tableCopiers = new TransactionPool(lc, {\n mode: Mode.READONLY,\n init,\n initialWorkers: numWorkers,\n });\n tableCopiers.run(db);\n\n lc.info?.(`Started ${numWorkers} workers to copy ${numTables} tables`);\n\n if (parseInt(process.versions.node) < 22) {\n lc.warn?.(\n `\\n\\n\\n` +\n `Older versions of Node have a bug that results in an unresponsive\\n` +\n `Postgres connection after running certain combinations of COPY commands.\\n` +\n `If initial sync hangs, run zero-cache with Node v22+. This has the additional\\n` +\n `benefit of being consistent with the Node version run in the production container image.` +\n `\\n\\n\\n`,\n );\n }\n return tableCopiers;\n}\n\n/**\n * Shadow-mode alternative to `createReplicationSlot`: opens a dedicated\n * READ ONLY REPEATABLE READ transaction on a normal connection, exports the\n * snapshot and captures the current WAL LSN, then holds the transaction\n * open until `release()` is called. The held transaction keeps the snapshot\n * importable by the table-copy workers for the duration of the COPY phase.\n *\n * Idle-in-transaction timeout is disabled locally so the exporter doesn't\n * get killed while workers are still importing.\n */\nasync function acquireExportedSnapshotForShadowSync(\n lc: LogContext,\n upstreamURI: string,\n): Promise<{\n snapshot: string;\n lsn: string;\n release: () => Promise<void>;\n}> {\n const holder = await connectPgClient(\n lc,\n upstreamURI,\n 'shadow-initial-sync-snapshot',\n {\n max: 1,\n },\n );\n const ready = resolver<{snapshot: string; lsn: string}>();\n const release = resolver<void>();\n const held = holder\n .begin(Mode.READONLY, async tx => {\n await tx`SET LOCAL idle_in_transaction_session_timeout = 0`.execute();\n const [row] = await tx<{snapshot: string; lsn: string}[]>`\n SELECT pg_export_snapshot() AS snapshot,\n pg_current_wal_lsn()::text AS lsn`;\n ready.resolve(row);\n await release.promise;\n })\n .catch(e => ready.reject(e));\n\n let snapshot: string;\n let lsn: string;\n try {\n ({snapshot, lsn} = await ready.promise);\n } catch (e) {\n await holder\n .end()\n .catch(err =>\n lc.warn?.(`Error ending shadow snapshot holder after failure`, err),\n );\n throw e;\n }\n lc.info?.(\n `Exported snapshot ${snapshot} at LSN ${lsn} (shadow initial sync)`,\n );\n return {\n snapshot,\n lsn,\n release: async () => {\n release.resolve();\n try {\n await held;\n } catch (e) {\n lc.warn?.(`snapshot holder transaction ended with error`, e);\n }\n await holder.end();\n },\n };\n}\n\nfunction createLiteTables(\n tx: Database,\n tables: PublishedTableSpec[],\n initialVersion: string,\n) {\n // TODO: Figure out how to reuse the ChangeProcessor here to avoid\n // duplicating the ColumnMetadata logic.\n const columnMetadata = must(ColumnMetadataStore.getInstance(tx));\n for (const t of tables) {\n tx.exec(createLiteTableStatement(mapPostgresToLite(t, initialVersion)));\n const tableName = liteTableName(t);\n for (const [colName, colSpec] of Object.entries(t.columns)) {\n columnMetadata.insert(tableName, colName, colSpec);\n }\n }\n}\n\nfunction createLiteIndices(lc: LogContext, tx: Database, indices: IndexSpec[]) {\n for (const [i, index] of indices.entries()) {\n const stmt = createLiteIndexStatement(mapPostgresToLiteIndex(index));\n lc.info?.(`Creating index ${i + 1}/${indices.length}: ${stmt}`);\n const start = performance.now();\n tx.exec(stmt);\n lc.info?.(\n `Created index ${i + 1}/${indices.length} ` +\n `(${(performance.now() - start).toFixed(3)} ms): ${stmt}`,\n );\n }\n}\n\n/**\n * Runs structural assertions over a just-synced replica and throws if any\n * fail. Only called in shadow mode — a successful return means the replica\n * is schema-complete, row-count consistent, and its column metadata is in\n * sync with its lite schema.\n *\n * Note: this intentionally does NOT verify ZQL-queryability. Tables that\n * `computeZqlSpecs` drops (no PK / no all-NOT-NULL unique index, unsupported\n * column types, etc.) are silently skipped in production too — there's\n * nothing shadow-specific about them, so failing here would diverge from\n * prod over an upstream-schema condition prod accepts.\n *\n * Exported for testing.\n */\nexport function verifyShadowReplica(\n lc: LogContext,\n db: Database,\n published: {tables: PublishedTableSpec[]; indexes: IndexSpec[]},\n rowsByTable: ReadonlyMap<string, number>,\n): void {\n const issues: string[] = [];\n let columnsChecked = 0;\n let rowsChecked = 0;\n\n // 1. Schema completeness: every published table exists in the replica\n // with at least the expected column set.\n const liteTables = listTables(db);\n const liteTableByName = new Map(liteTables.map(t => [t.name, t]));\n for (const pt of published.tables) {\n const name = liteTableName(pt);\n const lite = liteTableByName.get(name);\n if (!lite) {\n issues.push(`missing table in replica: ${name}`);\n continue;\n }\n for (const col of Object.keys(pt.columns)) {\n columnsChecked++;\n if (!(col in lite.columns)) {\n issues.push(`column missing in replica table ${name}: ${col}`);\n }\n }\n }\n\n // Every published index exists in the replica.\n const liteIndexNames = new Set(listIndexes(db).map(i => i.name));\n for (const ix of published.indexes) {\n const mapped = mapPostgresToLiteIndex(ix);\n if (!liteIndexNames.has(mapped.name)) {\n issues.push(\n `missing index in replica: ${mapped.name} on ${mapped.tableName}`,\n );\n }\n }\n\n // 2. Row counts: SQLite COUNT(*) matches the in-memory copy counter.\n for (const [table, expected] of rowsByTable) {\n try {\n const [row] = db\n .prepare(`SELECT COUNT(*) as count FROM \"${table}\"`)\n .all<{count: number}>();\n if (row.count !== expected) {\n issues.push(\n `row count mismatch for table ${table}: ` +\n `copy counter reported ${expected}, replica has ${row.count}`,\n );\n } else {\n rowsChecked += row.count;\n }\n } catch (e) {\n issues.push(`could not count rows in table ${table}: ${String(e)}`);\n }\n }\n\n // 3. Column metadata: every published column has a _zero.column_metadata row.\n const meta = must(ColumnMetadataStore.getInstance(db));\n for (const pt of published.tables) {\n const name = liteTableName(pt);\n const rows = meta.getTable(name);\n for (const col of Object.keys(pt.columns)) {\n if (!rows.has(col)) {\n issues.push(`missing column_metadata row for ${name}.${col}`);\n }\n }\n }\n\n if (issues.length) {\n throw new Error(\n `Shadow replica verification failed (${issues.length} issue(s)):\\n` +\n issues.map(i => ` - ${i}`).join('\\n'),\n );\n }\n\n lc.info?.(\n `Shadow replica verification passed: ` +\n `${published.tables.length} tables, ` +\n `${published.indexes.length} indexes, ` +\n `${columnsChecked} columns, ` +\n `${rowsChecked.toLocaleString()} rows`,\n );\n}\n\n// Verified empirically that batches of 50 seem to be the sweet spot,\n// similar to the report in https://sqlite.org/forum/forumpost/8878a512d3652655\n//\n// Exported for testing.\nexport const INSERT_BATCH_SIZE = 50;\n\nconst MB = 1024 * 1024;\nconst MAX_BUFFERED_ROWS = 10_000;\nconst BUFFERED_SIZE_THRESHOLD = 8 * MB;\n\nexport type DownloadStatements = {\n select: string;\n getTotalRows: string;\n getTotalBytes: string;\n};\n\n/**\n * Produces ` TABLESAMPLE BERNOULLI(n)` when `sampleRate` is < 1, else `''`.\n * Row-level Bernoulli sampling is used (rather than SYSTEM) because it\n * produces a more uniform sample and, unlike SYSTEM, still returns rows\n * for small tables at low rates.\n */\nfunction tableSampleClause(sampleRate: number | undefined): string {\n if (sampleRate === undefined || sampleRate >= 1) {\n return '';\n }\n // Round away float noise (e.g. 0.3 * 100 = 30.000000000000004) while still\n // preserving sub-integer rates like 0.001 (= 0.1%).\n const pct = parseFloat((sampleRate * 100).toFixed(6));\n return /*sql*/ ` TABLESAMPLE BERNOULLI(${pct})`;\n}\n\nfunction limitClause(maxRowsPerTable: number | undefined): string {\n return maxRowsPerTable !== undefined\n ? /*sql*/ ` LIMIT ${maxRowsPerTable}`\n : '';\n}\n\n/**\n * Returns the SELECT column expressions for binary COPY, casting columns\n * without a known binary decoder to `::text`.\n */\nexport function makeBinarySelectExprs(\n table: PublishedTableSpec,\n cols: string[],\n): string[] {\n return cols.map(col => {\n const spec = table.columns[col];\n return hasBinaryDecoder(spec) ? id(col) : `${id(col)}::text`;\n });\n}\n\nexport function makeDownloadStatements(\n table: PublishedTableSpec,\n cols: string[],\n sampleRate?: number | undefined,\n maxRowsPerTable?: number | undefined,\n selectExprs?: string[] | undefined,\n): DownloadStatements {\n const filterConditions = Object.values(table.publications)\n .map(({rowFilter}) => rowFilter)\n .filter(f => !!f); // remove nulls\n const where =\n filterConditions.length === 0\n ? ''\n : /*sql*/ `WHERE ${filterConditions.join(' OR ')}`;\n const sample = tableSampleClause(sampleRate);\n const limit = limitClause(maxRowsPerTable);\n const fromTable = /*sql*/ `FROM ${id(table.schema)}.${id(table.name)}${sample} ${where}`;\n const select = /*sql*/ `SELECT ${(selectExprs ?? cols.map(id)).join(',')} ${fromTable}${limit}`;\n if (limit) {\n // With LIMIT, wrap counts/sums in a subquery so they reflect the\n // capped rowset rather than the full (sampled) table.\n const bytesExpr = cols\n .map(col => `COALESCE(pg_column_size(${id(col)}), 0)`)\n .join(' + ');\n return {\n select,\n getTotalRows: /*sql*/ `SELECT COUNT(*)::bigint AS \"totalRows\" FROM (SELECT 1 AS _ ${fromTable}${limit}) s`,\n getTotalBytes: /*sql*/ `SELECT COALESCE(SUM(b), 0)::bigint AS \"totalBytes\" FROM (SELECT (${bytesExpr}) AS b ${fromTable}${limit}) s`,\n };\n }\n const totalBytes = `(${cols.map(col => `SUM(COALESCE(pg_column_size(${id(col)}), 0))`).join(' + ')})`;\n return {\n select,\n getTotalRows: /*sql*/ `SELECT COUNT(*) AS \"totalRows\" ${fromTable}`,\n getTotalBytes: /*sql*/ `SELECT ${totalBytes} AS \"totalBytes\" ${fromTable}`,\n };\n}\n\ntype DownloadState = {\n spec: PublishedTableSpec;\n status: DownloadStatus;\n};\n\ntype CopyFormat = 'binary' | 'text';\ntype InitialSyncMode = 'initial' | 'shadow';\n\ntype InitialSyncMetricAttrs = {\n syncMode: InitialSyncMode;\n copyFormat: CopyFormat;\n};\n\ntype InitialSyncRunMetricAttrs = {\n syncMode: InitialSyncMode;\n copyFormat: CopyFormat;\n result: 'success' | 'error';\n};\n\ntype CopyResult = {\n schema: string;\n table: string;\n replicaTable: string;\n syncMode: InitialSyncMode;\n copyFormat: CopyFormat;\n columnCount: number;\n rows: number;\n estimatedRows: number;\n estimatedBytes: number | undefined;\n flushMs: number;\n elapsedMs: number;\n copyBytes: number;\n};\n\nconst INITIAL_SYNC_DURATION_HISTOGRAM_BOUNDARIES_S = [\n 1, 2, 5, 10, 30, 60, 120, 300, 600, 1200, 2400, 3600, 7200,\n];\nconst SLOW_COPY_FLUSH_MS = 10_000;\n\n// change-streamer imports this module before startOtelAuto() runs, so create\n// instruments lazily to avoid binding them to OTel's no-op meter provider.\nfunction initialSyncRuns() {\n return getOrCreateCounter(\n 'replication',\n 'initial_sync_runs',\n 'Initial sync runs, labeled by result.',\n );\n}\n\nfunction initialSyncDuration() {\n return initialSyncDurationHistogram(\n 'initial_sync_duration',\n 'Wall-clock duration of an initial sync run, labeled by result.',\n );\n}\n\nfunction initialSyncCopyDuration() {\n return initialSyncDurationHistogram(\n 'initial_sync_copy_duration',\n 'Wall-clock duration of the COPY phase for a successful initial sync run.',\n );\n}\n\nfunction initialSyncCopyOtherDuration() {\n return initialSyncDurationHistogram(\n 'initial_sync_copy_other_duration',\n 'Initial sync total duration excluding SQLite flush and index time for a successful run.',\n );\n}\n\nfunction initialSyncFlushDuration() {\n return initialSyncDurationHistogram(\n 'initial_sync_flush_duration',\n 'Total SQLite flush time for a successful initial sync run.',\n );\n}\n\nfunction initialSyncIndexDuration() {\n return initialSyncDurationHistogram(\n 'initial_sync_index_duration',\n 'SQLite index creation time for a successful initial sync run.',\n );\n}\n\nfunction initialSyncRows() {\n return getOrCreateCounter(\n 'replication',\n 'initial_sync_rows',\n 'Rows copied during successful initial sync runs.',\n );\n}\n\nfunction initialSyncCopyStream() {\n return getOrCreateCounter('replication', 'initial_sync_copy_stream', {\n description:\n 'PostgreSQL COPY stream bytes processed during initial sync, including in-progress and failed runs.',\n unit: 'bytes',\n });\n}\n\nfunction initialSyncCompletedCopyStream() {\n return getOrCreateCounter(\n 'replication',\n 'initial_sync_completed_copy_stream',\n {\n description:\n 'PostgreSQL COPY stream bytes processed during successful initial sync runs.',\n unit: 'bytes',\n },\n );\n}\n\nfunction initialSyncCopyChunks() {\n return getOrCreateCounter(\n 'replication',\n 'initial_sync_copy_chunks',\n 'PostgreSQL COPY stream chunks processed during initial sync.',\n );\n}\n\nfunction initialSyncDurationHistogram(name: string, description: string) {\n return getOrCreateHistogram('replication', name, {\n description,\n unit: 's',\n bucketBoundaries: INITIAL_SYNC_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\nfunction initialSyncMetricAttrs(attrs: InitialSyncMetricAttrs) {\n return {\n sync_mode: attrs.syncMode,\n copy_format: attrs.copyFormat,\n };\n}\n\nfunction initialSyncRunMetricAttrs(attrs: InitialSyncRunMetricAttrs) {\n return {\n ...initialSyncMetricAttrs(attrs),\n result: attrs.result,\n };\n}\n\nfunction recordInitialSyncRunMetrics(\n stats: {\n durationMs: number;\n rows?: number | undefined;\n copyBytes?: number | undefined;\n copyMs?: number | undefined;\n copyOtherMs?: number | undefined;\n flushMs?: number | undefined;\n indexMs?: number | undefined;\n },\n attrs: InitialSyncRunMetricAttrs,\n) {\n const labels = initialSyncRunMetricAttrs(attrs);\n initialSyncRuns().add(1, labels);\n initialSyncDuration().recordMs(stats.durationMs, labels);\n if (attrs.result === 'success') {\n if (stats.copyMs !== undefined) {\n initialSyncCopyDuration().recordMs(stats.copyMs, labels);\n }\n if (stats.copyOtherMs !== undefined) {\n initialSyncCopyOtherDuration().recordMs(stats.copyOtherMs, labels);\n }\n if (stats.flushMs !== undefined) {\n initialSyncFlushDuration().recordMs(stats.flushMs, labels);\n }\n if (stats.indexMs !== undefined) {\n initialSyncIndexDuration().recordMs(stats.indexMs, labels);\n }\n if (stats.rows !== undefined && stats.rows > 0) {\n initialSyncRows().add(stats.rows, labels);\n }\n if (stats.copyBytes !== undefined && stats.copyBytes > 0) {\n initialSyncCompletedCopyStream().add(stats.copyBytes, labels);\n }\n }\n}\n\nfunction initialSyncCopySummary(\n results: readonly CopyResult[],\n copyMs: number,\n) {\n const totals = results.reduce(\n (acc, curr) => {\n acc.rows += curr.rows;\n acc.flushMs += curr.flushMs;\n acc.copyBytes += curr.copyBytes;\n return acc;\n },\n {\n rows: 0,\n flushMs: 0,\n copyBytes: 0,\n },\n );\n return {\n tables: results.length,\n rows: totals.rows,\n copyMs,\n flushMs: totals.flushMs,\n copyBytes: totals.copyBytes,\n };\n}\n\nfunction logSlowCopyFlush(\n lc: LogContext,\n details: {\n schema: string;\n table: string;\n replicaTable: string;\n syncMode: InitialSyncMode;\n copyFormat: CopyFormat;\n elapsedMs: number;\n flushedRows: number;\n flushedBytes: number;\n rows: number;\n copyBytes: number;\n },\n) {\n if (details.elapsedMs < SLOW_COPY_FLUSH_MS || details.flushedRows === 0) {\n return;\n }\n lc.info?.('initial-sync table copy slow flush', details);\n}\n\n// Exported for testing.\nexport async function getInitialDownloadState(\n lc: LogContext,\n sql: PostgresDB,\n spec: PublishedTableSpec,\n skipTotals: boolean,\n): Promise<DownloadState> {\n const start = performance.now();\n const table = liteTableName(spec);\n const columns = Object.keys(spec.columns);\n if (skipTotals) {\n // Shadow sync suppresses status events, so the pg_class\n // estimates would be computed and thrown away.\n return {\n spec,\n status: {table, columns, rows: 0, totalRows: 0, totalBytes: 0},\n };\n }\n // Use pg_class estimates instead of expensive COUNT(*) and\n // SUM(pg_column_size(...)) full table scans. The exact values are only\n // used for progress reporting, so estimates are sufficient.\n const qualifiedName = `${id(spec.schema)}.${id(spec.name)}`;\n const estimateResult = await sql<\n {totalRows: number; totalBytes: number}[]\n >`SELECT GREATEST(reltuples, 0)::float8 AS \"totalRows\",\n pg_table_size(oid)::float8 AS \"totalBytes\"\n FROM pg_class\n WHERE oid = ${qualifiedName}::regclass`;\n\n const {totalRows, totalBytes} = estimateResult[0] ?? {\n totalRows: 0,\n totalBytes: 0,\n };\n\n const state: DownloadState = {\n spec,\n status: {\n table,\n columns,\n rows: 0,\n totalRows,\n totalBytes,\n },\n };\n const elapsed = (performance.now() - start).toFixed(3);\n lc.info?.(`Computed initial download state for ${table} (${elapsed} ms)`, {\n state: state.status,\n });\n return state;\n}\n\nfunction copy(\n lc: LogContext,\n {spec: table, status}: DownloadState,\n dbClient: PostgresDB,\n from: PostgresTransaction,\n to: Database,\n textCopy: boolean,\n syncMode: InitialSyncMode,\n sampleRate?: number | undefined,\n maxRowsPerTable?: number | undefined,\n): Promise<CopyResult> {\n if (textCopy) {\n return copyText(\n lc,\n table,\n status,\n dbClient,\n from,\n to,\n syncMode,\n sampleRate,\n maxRowsPerTable,\n );\n }\n return copyBinary(\n lc,\n table,\n status,\n from,\n to,\n syncMode,\n sampleRate,\n maxRowsPerTable,\n );\n}\n\nasync function copyBinary(\n lc: LogContext,\n table: PublishedTableSpec,\n status: DownloadStatus,\n from: PostgresTransaction,\n to: Database,\n syncMode: InitialSyncMode,\n sampleRate?: number | undefined,\n maxRowsPerTable?: number | undefined,\n): Promise<CopyResult> {\n const start = performance.now();\n const copyFormat: CopyFormat = 'binary';\n const metricLabels = initialSyncMetricAttrs({syncMode, copyFormat});\n const copyStreamMetric = initialSyncCopyStream();\n const copyChunksMetric = initialSyncCopyChunks();\n let flushMs = 0;\n let copyBytes = 0;\n\n const tableName = liteTableName(table);\n const orderedColumns = Object.entries(table.columns);\n\n const columnNames = orderedColumns.map(([c]) => c);\n const columnSpecs = orderedColumns.map(([_name, spec]) => spec);\n const insertColumnList = columnNames.map(c => id(c)).join(',');\n\n const valuesSql =\n columnNames.length > 0 ? `(${'?,'.repeat(columnNames.length - 1)}?)` : '()';\n const insertSql = /*sql*/ `\n INSERT INTO \"${tableName}\" (${insertColumnList}) VALUES ${valuesSql}`;\n const insertStmt = to.prepare(insertSql);\n const insertBatchStmt = to.prepare(\n insertSql + `,${valuesSql}`.repeat(INSERT_BATCH_SIZE - 1),\n );\n\n // Build SELECT with ::text casts for columns without a known binary decoder.\n const select = makeDownloadStatements(\n table,\n columnNames,\n sampleRate,\n maxRowsPerTable,\n makeBinarySelectExprs(table, columnNames),\n ).select;\n\n const decoders = orderedColumns.map(([, spec]) =>\n hasBinaryDecoder(spec) ? makeBinaryDecoder(spec) : textCastDecoder,\n );\n\n const valuesPerRow = columnSpecs.length;\n const valuesPerBatch = valuesPerRow * INSERT_BATCH_SIZE;\n\n const pendingValues: LiteValueType[] = Array.from({\n length: MAX_BUFFERED_ROWS * valuesPerRow,\n });\n let pendingRows = 0;\n let pendingSize = 0;\n\n function flush() {\n const flushStart = performance.now();\n const flushedRows = pendingRows;\n const flushedSize = pendingSize;\n\n let l = 0;\n for (; pendingRows > INSERT_BATCH_SIZE; pendingRows -= INSERT_BATCH_SIZE) {\n insertBatchStmt.run(pendingValues.slice(l, (l += valuesPerBatch)));\n }\n for (; pendingRows > 0; pendingRows--) {\n insertStmt.run(pendingValues.slice(l, (l += valuesPerRow)));\n }\n const flushedValues = flushedRows * valuesPerRow;\n for (let i = 0; i < flushedValues; i++) {\n pendingValues[i] = undefined as unknown as LiteValueType;\n }\n pendingSize = 0;\n status.rows += flushedRows;\n\n const elapsed = performance.now() - flushStart;\n flushMs += elapsed;\n lc.debug?.(\n `flushed ${flushedRows} ${tableName} rows (${flushedSize} bytes) in ${elapsed.toFixed(3)} ms`,\n );\n logSlowCopyFlush(lc, {\n schema: table.schema,\n table: table.name,\n replicaTable: tableName,\n syncMode,\n copyFormat,\n elapsedMs: elapsed,\n flushedRows,\n flushedBytes: flushedSize,\n rows: status.rows,\n copyBytes,\n });\n }\n\n const binaryParser = new BinaryCopyParser();\n let col = 0;\n\n lc.info?.(`Starting binary copy stream of ${tableName}:`, select);\n\n await pipeline(\n await from\n .unsafe(`COPY (${select}) TO STDOUT WITH (FORMAT binary)`)\n .readable(),\n new Writable({\n highWaterMark: BUFFERED_SIZE_THRESHOLD,\n\n write(\n chunk: Buffer,\n _encoding: string,\n callback: (error?: Error) => void,\n ) {\n try {\n copyBytes += chunk.length;\n copyStreamMetric.add(chunk.length, metricLabels);\n copyChunksMetric.add(1, metricLabels);\n for (const fieldBuf of binaryParser.parse(chunk)) {\n const fieldSize = fieldBuf === null ? 4 : fieldBuf.length;\n pendingSize += fieldSize;\n pendingValues[pendingRows * valuesPerRow + col] =\n fieldBuf === null ? null : decoders[col](fieldBuf);\n\n if (++col === decoders.length) {\n col = 0;\n if (\n ++pendingRows >= MAX_BUFFERED_ROWS - valuesPerRow ||\n pendingSize >= BUFFERED_SIZE_THRESHOLD\n ) {\n flush();\n }\n }\n }\n callback();\n } catch (e) {\n callback(e instanceof Error ? e : new Error(String(e)));\n }\n },\n\n final: (callback: (error?: Error) => void) => {\n try {\n flush();\n callback();\n } catch (e) {\n callback(e instanceof Error ? e : new Error(String(e)));\n }\n },\n }),\n );\n\n const elapsed = performance.now() - start;\n const result = {\n schema: table.schema,\n table: table.name,\n replicaTable: tableName,\n syncMode,\n copyFormat,\n columnCount: columnNames.length,\n rows: status.rows,\n estimatedRows: status.totalRows,\n estimatedBytes: status.totalBytes,\n flushMs,\n elapsedMs: elapsed,\n copyBytes,\n } satisfies CopyResult;\n\n lc.info?.(\n `Finished copying ${status.rows} rows into ${tableName} ` +\n `(flush: ${flushMs.toFixed(3)} ms) (total: ${elapsed.toFixed(3)} ms) `,\n result,\n );\n return result;\n}\n\nasync function copyText(\n lc: LogContext,\n table: PublishedTableSpec,\n status: DownloadStatus,\n dbClient: PostgresDB,\n from: PostgresTransaction,\n to: Database,\n syncMode: InitialSyncMode,\n sampleRate?: number | undefined,\n maxRowsPerTable?: number | undefined,\n): Promise<CopyResult> {\n const start = performance.now();\n const copyFormat: CopyFormat = 'text';\n const metricLabels = initialSyncMetricAttrs({syncMode, copyFormat});\n const copyStreamMetric = initialSyncCopyStream();\n const copyChunksMetric = initialSyncCopyChunks();\n let flushMs = 0;\n let copyBytes = 0;\n\n const tableName = liteTableName(table);\n const orderedColumns = Object.entries(table.columns);\n\n const columnNames = orderedColumns.map(([c]) => c);\n const columnSpecs = orderedColumns.map(([_name, spec]) => spec);\n const insertColumnList = columnNames.map(c => id(c)).join(',');\n\n const valuesSql =\n columnNames.length > 0 ? `(${'?,'.repeat(columnNames.length - 1)}?)` : '()';\n const insertSql = /*sql*/ `\n INSERT INTO \"${tableName}\" (${insertColumnList}) VALUES ${valuesSql}`;\n const insertStmt = to.prepare(insertSql);\n const insertBatchStmt = to.prepare(\n insertSql + `,${valuesSql}`.repeat(INSERT_BATCH_SIZE - 1),\n );\n\n const {select} = makeDownloadStatements(\n table,\n columnNames,\n sampleRate,\n maxRowsPerTable,\n );\n const valuesPerRow = columnSpecs.length;\n const valuesPerBatch = valuesPerRow * INSERT_BATCH_SIZE;\n\n const pendingValues: LiteValueType[] = Array.from({\n length: MAX_BUFFERED_ROWS * valuesPerRow,\n });\n let pendingRows = 0;\n let pendingSize = 0;\n\n function flush() {\n const flushStart = performance.now();\n const flushedRows = pendingRows;\n const flushedSize = pendingSize;\n\n let l = 0;\n for (; pendingRows > INSERT_BATCH_SIZE; pendingRows -= INSERT_BATCH_SIZE) {\n insertBatchStmt.run(pendingValues.slice(l, (l += valuesPerBatch)));\n }\n for (; pendingRows > 0; pendingRows--) {\n insertStmt.run(pendingValues.slice(l, (l += valuesPerRow)));\n }\n const flushedValues = flushedRows * valuesPerRow;\n for (let i = 0; i < flushedValues; i++) {\n pendingValues[i] = undefined as unknown as LiteValueType;\n }\n pendingSize = 0;\n status.rows += flushedRows;\n\n const elapsed = performance.now() - flushStart;\n flushMs += elapsed;\n lc.debug?.(\n `flushed ${flushedRows} ${tableName} rows (${flushedSize} bytes) in ${elapsed.toFixed(3)} ms`,\n );\n logSlowCopyFlush(lc, {\n schema: table.schema,\n table: table.name,\n replicaTable: tableName,\n syncMode,\n copyFormat,\n elapsedMs: elapsed,\n flushedRows,\n flushedBytes: flushedSize,\n rows: status.rows,\n copyBytes,\n });\n }\n\n lc.info?.(`Starting text copy stream of ${tableName}:`, select);\n const pgParsers = await getTypeParsers(dbClient, {returnJsonAsString: true});\n const parsers = columnSpecs.map(c => {\n const pgParse = pgParsers.getTypeParser(c.typeOID);\n return (val: string) =>\n liteValue(\n pgParse(val) as PostgresValueType,\n c.dataType,\n JSON_STRINGIFIED,\n );\n });\n\n const tsvParser = new TsvParser();\n let col = 0;\n\n await pipeline(\n await from.unsafe(`COPY (${select}) TO STDOUT`).readable(),\n new Writable({\n highWaterMark: BUFFERED_SIZE_THRESHOLD,\n\n write(\n chunk: Buffer,\n _encoding: string,\n callback: (error?: Error) => void,\n ) {\n try {\n copyBytes += chunk.length;\n copyStreamMetric.add(chunk.length, metricLabels);\n copyChunksMetric.add(1, metricLabels);\n for (const text of tsvParser.parse(chunk)) {\n const fieldSize = text === null ? 4 : text.length;\n pendingSize += fieldSize;\n pendingValues[pendingRows * valuesPerRow + col] =\n text === null ? null : parsers[col](text);\n\n if (++col === parsers.length) {\n col = 0;\n if (\n ++pendingRows >= MAX_BUFFERED_ROWS - valuesPerRow ||\n pendingSize >= BUFFERED_SIZE_THRESHOLD\n ) {\n flush();\n }\n }\n }\n callback();\n } catch (e) {\n callback(e instanceof Error ? e : new Error(String(e)));\n }\n },\n\n final: (callback: (error?: Error) => void) => {\n try {\n flush();\n callback();\n } catch (e) {\n callback(e instanceof Error ? e : new Error(String(e)));\n }\n },\n }),\n );\n\n const elapsed = performance.now() - start;\n const result = {\n schema: table.schema,\n table: table.name,\n replicaTable: tableName,\n syncMode,\n copyFormat,\n columnCount: columnNames.length,\n rows: status.rows,\n estimatedRows: status.totalRows,\n estimatedBytes: status.totalBytes,\n flushMs,\n elapsedMs: elapsed,\n copyBytes,\n } satisfies CopyResult;\n lc.info?.(\n `Finished copying ${status.rows} rows into ${tableName} ` +\n `(flush: ${flushMs.toFixed(3)} ms) (total: ${elapsed.toFixed(3)} ms) `,\n result,\n );\n return result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"initial-sync.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-source/pg/initial-sync.ts"],"sourcesContent":["import {mkdtemp, rm} from 'node:fs/promises';\nimport {platform, tmpdir} from 'node:os';\nimport {join} from 'node:path';\nimport {Writable} from 'node:stream';\nimport {pipeline} from 'node:stream/promises';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../../../shared/src/asserts.ts';\nimport type {JSONObject} from '../../../../../shared/src/bigint-json.ts';\nimport {must} from '../../../../../shared/src/must.ts';\nimport {equals} from '../../../../../shared/src/set-utils.ts';\nimport type {DownloadStatus} from '../../../../../zero-events/src/status.ts';\nimport {Database} from '../../../../../zqlite/src/db.ts';\nimport {\n createLiteIndexStatement,\n createLiteTableStatement,\n} from '../../../db/create.ts';\nimport {listIndexes, listTables} from '../../../db/lite-tables.ts';\nimport * as Mode from '../../../db/mode-enum.ts';\nimport {\n BinaryCopyParser,\n hasBinaryDecoder,\n makeBinaryDecoder,\n textCastDecoder,\n} from '../../../db/pg-copy-binary.ts';\nimport {TsvParser} from '../../../db/pg-copy.ts';\nimport {\n mapPostgresToLite,\n mapPostgresToLiteIndex,\n} from '../../../db/pg-to-lite.ts';\nimport {getTypeParsers} from '../../../db/pg-type-parser.ts';\nimport {runTx} from '../../../db/run-transaction.ts';\nimport type {IndexSpec, PublishedTableSpec} from '../../../db/specs.ts';\nimport {importSnapshot, TransactionPool} from '../../../db/transaction-pool.ts';\nimport {\n getOrCreateCounter,\n getOrCreateHistogram,\n} from '../../../observability/metrics.ts';\nimport {\n JSON_STRINGIFIED,\n liteValue,\n type LiteValueType,\n} from '../../../types/lite.ts';\nimport {liteTableName} from '../../../types/names.ts';\nimport {PG_15, PG_17} from '../../../types/pg-versions.ts';\nimport {\n connectPgClient,\n pgClient,\n type PostgresDB,\n type PostgresTransaction,\n type PostgresValueType,\n} from '../../../types/pg.ts';\nimport {CpuProfiler} from '../../../types/profiler.ts';\nimport type {ShardConfig} from '../../../types/shards.ts';\nimport {ALLOWED_APP_ID_CHARACTERS} from '../../../types/shards.ts';\nimport {id} from '../../../types/sql.ts';\nimport {ReplicationStatusPublisher} from '../../replicator/replication-status.ts';\nimport {ColumnMetadataStore} from '../../replicator/schema/column-metadata.ts';\nimport {initReplicationState} from '../../replicator/schema/replication-state.ts';\nimport {toStateVersionString} from './lsn.ts';\nimport {createReplicaAndSlot} from './replication-slots.ts';\nimport {ensureShardSchema} from './schema/init.ts';\nimport {getPublicationInfo} from './schema/published.ts';\nimport {\n dropShard,\n getInternalShardConfig,\n initReplica,\n validatePublications,\n} from './schema/shard.ts';\n\nexport type InitialSyncOptions = {\n tableCopyWorkers: number;\n profileCopy?: boolean | undefined;\n textCopy?: boolean | undefined;\n replicationSlotFailover?: boolean | undefined;\n /**\n * When set, run initial sync in \"shadow\" mode for verification: skip all\n * upstream mutations (no replication slot, no addReplica, no dropShard, no\n * slot drop on failure), suppress status events, and optionally sample\n * rows from each table via TABLESAMPLE BERNOULLI + LIMIT. The caller is\n * responsible for providing (and discarding) a throwaway SQLite `tx`.\n */\n shadow?:\n | {\n /** 0 < rate <= 1. When 1, no TABLESAMPLE clause is added. */\n sampleRate: number;\n /**\n * LIMIT N cap appended after TABLESAMPLE. Required: shadow sync is\n * for verification only, so every run must commit to a row budget.\n */\n maxRowsPerTable: number;\n }\n | undefined;\n};\n\n/** Server context to store with the initial sync metadata for debugging. */\nexport type ServerContext = JSONObject;\n\nexport async function initialSync(\n lc: LogContext,\n shard: ShardConfig,\n tx: Database,\n upstreamURI: string,\n syncOptions: InitialSyncOptions,\n context: ServerContext,\n) {\n if (!ALLOWED_APP_ID_CHARACTERS.test(shard.appID)) {\n throw new Error(\n 'The App ID may only consist of lower-case letters, numbers, and the underscore character',\n );\n }\n const {\n tableCopyWorkers,\n profileCopy,\n textCopy = false,\n replicationSlotFailover = false,\n shadow,\n } = syncOptions;\n const syncMode: InitialSyncMode = shadow ? 'shadow' : 'initial';\n const copyFormat: CopyFormat = textCopy ? 'text' : 'binary';\n const start = performance.now();\n let sql: PostgresDB | undefined;\n let replicationSession: PostgresDB | undefined;\n const replicaID = Date.now().toString();\n let slotName: string | undefined; // undefined === shadow\n const statusPublisher = ReplicationStatusPublisher.forRunningTransaction(\n tx,\n shadow ? async () => {} : undefined,\n ).publish(lc, 'Initializing');\n let releaseShadowSnapshot: (() => Promise<void>) | undefined;\n try {\n const copyProfiler = profileCopy ? await CpuProfiler.connect() : null;\n sql = await connectPgClient(lc, upstreamURI, 'initial-sync');\n // Replication session is only needed to create a replication slot in the\n // real path. In shadow mode we export a snapshot on a normal connection\n // instead, so no replication session is opened.\n replicationSession = shadow\n ? undefined\n : pgClient(lc, upstreamURI, 'initial-sync-replication-session', {\n ['fetch_types']: false, // Necessary for the streaming protocol\n connection: {replication: 'database'}, // https://www.postgresql.org/docs/current/protocol-replication.html\n });\n\n const pgVersion = await checkUpstreamConfig(sql);\n\n // In shadow mode we assume the shard is already initialized and just\n // read back the existing publications. `ensurePublishedTables` would\n // otherwise run DDL and potentially call `dropShard`, which must never\n // happen during a shadow run.\n const {publications} = shadow\n ? await getInternalShardConfig(sql, shard)\n : await ensurePublishedTables(lc, sql, shard);\n lc.info?.(`Upstream is setup with publications [${publications}]`);\n\n const {database, host} = sql.options;\n lc.info?.(\n shadow\n ? `acquiring exported snapshot on ${database}@${host} (shadow mode)`\n : `opening replication session to ${database}@${host}`,\n );\n\n let snapshot: string;\n let lsn: string;\n\n if (shadow) {\n const acquired = await acquireExportedSnapshotForShadowSync(\n lc,\n upstreamURI,\n );\n snapshot = acquired.snapshot;\n lsn = acquired.lsn;\n releaseShadowSnapshot = acquired.release;\n } else {\n const slot = await createReplicaAndSlot(\n lc,\n sql,\n must(replicationSession),\n shard,\n replicaID,\n replicationSlotFailover && pgVersion >= PG_17,\n );\n snapshot = slot.snapshot_name;\n lsn = slot.consistent_point;\n slotName = slot.slot_name;\n }\n\n const initialVersion = toStateVersionString(lsn);\n\n initReplicationState(tx, publications, initialVersion, context);\n\n // Run up to MAX_WORKERS to copy of tables at the replication slot's snapshot.\n // Retrieve the published schema at the consistent_point.\n const published = await runTx(\n sql,\n async tx => {\n await tx.unsafe(/* sql*/ `SET TRANSACTION SNAPSHOT '${snapshot}'`);\n return getPublicationInfo(tx, publications);\n },\n {mode: Mode.READONLY},\n );\n // Note: If this throws, initial-sync is aborted.\n validatePublications(lc, published);\n\n // Now that tables have been validated, kick off the copiers.\n const {tables, indexes} = published;\n const numTables = tables.length;\n if (platform() === 'win32' && tableCopyWorkers < numTables) {\n lc.warn?.(\n `Increasing the number of copy workers from ${tableCopyWorkers} to ` +\n `${numTables} to work around a Node/Postgres connection bug`,\n );\n }\n const numWorkers =\n platform() === 'win32'\n ? numTables\n : Math.min(tableCopyWorkers, numTables);\n\n const copyPool = await connectPgClient(\n lc,\n upstreamURI,\n 'initial-sync-copy-worker',\n {\n max: numWorkers,\n ['max_lifetime']: 120 * 60, // set a long (2h) limit for COPY streaming\n },\n );\n const copiers = startTableCopyWorkers(\n lc,\n copyPool,\n snapshot,\n numWorkers,\n numTables,\n );\n try {\n createLiteTables(tx, tables, initialVersion);\n const sampleRate = shadow?.sampleRate;\n const maxRowsPerTable = shadow?.maxRowsPerTable;\n const downloads = await Promise.all(\n tables.map(spec =>\n copiers.processReadTask((db, lc) =>\n getInitialDownloadState(lc, db, spec, shadow !== undefined),\n ),\n ),\n );\n statusPublisher.publish(\n lc,\n 'Initializing',\n `Copying ${numTables} upstream tables at version ${initialVersion}`,\n 5000,\n () => ({downloadStatus: downloads.map(({status}) => status)}),\n );\n\n void copyProfiler?.start();\n const copyStart = performance.now();\n const copyResults = await Promise.all(\n downloads.map(table =>\n copiers.processReadTask((db, lc) =>\n copy(\n lc,\n table,\n copyPool,\n db,\n tx,\n textCopy,\n syncMode,\n sampleRate,\n maxRowsPerTable,\n ),\n ),\n ),\n );\n const copyElapsed = performance.now() - copyStart;\n void copyProfiler?.stopAndDispose(lc, 'initial-copy');\n copiers.setDone();\n\n const copySummary = initialSyncCopySummary(copyResults, copyElapsed);\n\n statusPublisher.publish(\n lc,\n 'Indexing',\n `Creating ${indexes.length} indexes`,\n 5000,\n );\n const indexStart = performance.now();\n createLiteIndices(lc, tx, indexes);\n const index = performance.now() - indexStart;\n lc.info?.(`Created indexes (${index.toFixed(3)} ms)`);\n\n if (slotName && replicaID) {\n await initReplica(sql, shard, replicaID, published, context);\n } else {\n assert(shadow, 'expected to be in shadow sync if there is no slotName');\n const rowsByTable = new Map<string, number>();\n for (let i = 0; i < downloads.length; i++) {\n rowsByTable.set(downloads[i].status.table, copyResults[i].rows);\n }\n verifyShadowReplica(lc, tx, published, rowsByTable);\n }\n\n const elapsed = performance.now() - start;\n const copyOtherMs = Math.max(0, elapsed - copySummary.flushMs - index);\n recordInitialSyncRunMetrics(\n {\n durationMs: elapsed,\n rows: copySummary.rows,\n copyBytes: copySummary.copyBytes,\n copyMs: copySummary.copyMs,\n copyOtherMs,\n flushMs: copySummary.flushMs,\n indexMs: index,\n },\n {\n result: 'success',\n syncMode,\n copyFormat,\n },\n );\n lc.info?.(\n `Synced ${copySummary.rows.toLocaleString()} rows of ${numTables} tables in ${publications} up to ${lsn} ` +\n `(flush: ${copySummary.flushMs.toFixed(3)}, index: ${index.toFixed(3)}, total: ${elapsed.toFixed(3)} ms)`,\n {\n syncMode,\n copyFormat,\n publications,\n lsn,\n ...copySummary,\n indexes: indexes.length,\n indexMs: index,\n copyOtherMs,\n totalMs: elapsed,\n },\n );\n } finally {\n // All meaningful errors are handled at the processReadTask() call site.\n void copyPool.end().catch(e => lc.warn?.(`Error closing copyPool`, e));\n }\n } catch (e) {\n recordInitialSyncRunMetrics(\n {durationMs: performance.now() - start},\n {\n result: 'error',\n syncMode,\n copyFormat,\n },\n );\n if (slotName && sql) {\n // If initial-sync did not succeed, make a best effort to drop the\n // orphaned replication slot to avoid running out of slots in\n // pathological cases that result in repeated failures.\n lc.warn?.(`dropping replication slot ${slotName}`, e);\n await sql`\n SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots\n WHERE slot_name = ${slotName};\n `.catch(e => lc.warn?.(`Unable to drop replication slot ${slotName}`, e));\n }\n await statusPublisher.publishAndThrowError(lc, 'Initializing', e);\n } finally {\n statusPublisher.stop();\n if (releaseShadowSnapshot) {\n await releaseShadowSnapshot().catch(e =>\n lc.warn?.(`Error releasing shadow snapshot`, e),\n );\n }\n if (replicationSession) {\n await replicationSession.end();\n }\n if (sql) {\n await sql.end();\n }\n }\n}\n\nexport type ShadowSyncOptions = {\n sampleRate: number;\n maxRowsPerTable: number;\n /**\n * Parent directory for the throwaway SQLite replica. Defaults to the OS\n * tmpdir. Primarily for tests that need to isolate the scratch directory.\n */\n parentDir?: string | undefined;\n};\n\n/**\n * Exercises the initial-sync code path against a sample of rows from every\n * published table, writing into a throwaway SQLite database that is deleted\n * when the run ends. Produces zero upstream mutations: no replication slot,\n * no `addReplica`, no `dropShard`, no status events.\n *\n * Intended to be invoked periodically so that if a customer ever needs a\n * full reset, we have recent confidence that `initialSync` still works.\n * The shard must already be initialized upstream.\n */\nexport async function shadowInitialSync(\n lc: LogContext,\n shard: ShardConfig,\n upstreamURI: string,\n shadow: ShadowSyncOptions,\n context: ServerContext,\n syncOptions?: Pick<InitialSyncOptions, 'textCopy'>,\n): Promise<void> {\n const dir = await mkdtemp(\n join(shadow.parentDir ?? tmpdir(), 'zero-shadow-sync-'),\n );\n const dbPath = join(dir, 'shadow-replica.db');\n const db = new Database(lc, dbPath);\n try {\n await initialSync(\n lc,\n shard,\n db,\n upstreamURI,\n {\n // Shadow sync copies small samples, so one worker is plenty —\n // no reason to burn additional upstream connections.\n tableCopyWorkers: 1,\n textCopy: syncOptions?.textCopy,\n shadow,\n },\n context,\n );\n } finally {\n try {\n db.close();\n } catch (e) {\n lc.warn?.(`Error closing shadow replica db`, e);\n }\n await rm(dir, {recursive: true, force: true}).catch(e =>\n lc.warn?.(`Error cleaning up shadow replica dir ${dir}`, e),\n );\n }\n}\n\nasync function checkUpstreamConfig(sql: PostgresDB) {\n const {walLevel, version} = (\n await sql<{walLevel: string; version: number}[]>`\n SELECT current_setting('wal_level') as \"walLevel\", \n current_setting('server_version_num') as \"version\";\n `\n )[0];\n\n if (walLevel !== 'logical') {\n throw new Error(\n `Postgres must be configured with \"wal_level = logical\" (currently: \"${walLevel})`,\n );\n }\n if (version < PG_15) {\n throw new Error(\n `Must be running Postgres 15 or higher (currently: \"${version}\")`,\n );\n }\n return version;\n}\n\nasync function ensurePublishedTables(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardConfig,\n validate = true,\n): Promise<{publications: string[]}> {\n const {database, host} = sql.options;\n lc.info?.(`Ensuring upstream PUBLICATION on ${database}@${host}`);\n\n await ensureShardSchema(lc, sql, shard);\n const {publications} = await getInternalShardConfig(sql, shard);\n\n if (validate) {\n let valid = false;\n const nonInternalPublications = publications.filter(\n p => !p.startsWith('_'),\n );\n const exists = await sql`\n SELECT pubname FROM pg_publication WHERE pubname IN ${sql(publications)}\n `.values();\n if (exists.length !== publications.length) {\n lc.warn?.(\n `some configured publications [${publications}] are missing: ` +\n `[${exists.flat()}]. resyncing`,\n );\n } else if (\n !equals(new Set(shard.publications), new Set(nonInternalPublications))\n ) {\n lc.warn?.(\n `requested publications [${shard.publications}] differ from previous` +\n `publications [${nonInternalPublications}]. resyncing`,\n );\n } else {\n valid = true;\n }\n if (!valid) {\n await sql.unsafe(dropShard(shard.appID, shard.shardNum));\n return ensurePublishedTables(lc, sql, shard, false);\n }\n }\n return {publications};\n}\n\nfunction startTableCopyWorkers(\n lc: LogContext,\n db: PostgresDB,\n snapshot: string,\n numWorkers: number,\n numTables: number,\n): TransactionPool {\n const {init} = importSnapshot(snapshot);\n const tableCopiers = new TransactionPool(lc, {\n mode: Mode.READONLY,\n init,\n initialWorkers: numWorkers,\n });\n tableCopiers.run(db);\n\n lc.info?.(`Started ${numWorkers} workers to copy ${numTables} tables`);\n\n if (parseInt(process.versions.node) < 22) {\n lc.warn?.(\n `\\n\\n\\n` +\n `Older versions of Node have a bug that results in an unresponsive\\n` +\n `Postgres connection after running certain combinations of COPY commands.\\n` +\n `If initial sync hangs, run zero-cache with Node v22+. This has the additional\\n` +\n `benefit of being consistent with the Node version run in the production container image.` +\n `\\n\\n\\n`,\n );\n }\n return tableCopiers;\n}\n\n/**\n * Shadow-mode alternative to `createReplicationSlot`: opens a dedicated\n * READ ONLY REPEATABLE READ transaction on a normal connection, exports the\n * snapshot and captures the current WAL LSN, then holds the transaction\n * open until `release()` is called. The held transaction keeps the snapshot\n * importable by the table-copy workers for the duration of the COPY phase.\n *\n * Idle-in-transaction timeout is disabled locally so the exporter doesn't\n * get killed while workers are still importing.\n */\nasync function acquireExportedSnapshotForShadowSync(\n lc: LogContext,\n upstreamURI: string,\n): Promise<{\n snapshot: string;\n lsn: string;\n release: () => Promise<void>;\n}> {\n const holder = await connectPgClient(\n lc,\n upstreamURI,\n 'shadow-initial-sync-snapshot',\n {\n max: 1,\n },\n );\n const ready = resolver<{snapshot: string; lsn: string}>();\n const release = resolver<void>();\n const held = holder\n .begin(Mode.READONLY, async tx => {\n await tx`SET LOCAL idle_in_transaction_session_timeout = 0`.execute();\n const [row] = await tx<{snapshot: string; lsn: string}[]>`\n SELECT pg_export_snapshot() AS snapshot,\n pg_current_wal_lsn()::text AS lsn`;\n ready.resolve(row);\n await release.promise;\n })\n .catch(e => ready.reject(e));\n\n let snapshot: string;\n let lsn: string;\n try {\n ({snapshot, lsn} = await ready.promise);\n } catch (e) {\n await holder\n .end()\n .catch(err =>\n lc.warn?.(`Error ending shadow snapshot holder after failure`, err),\n );\n throw e;\n }\n lc.info?.(\n `Exported snapshot ${snapshot} at LSN ${lsn} (shadow initial sync)`,\n );\n return {\n snapshot,\n lsn,\n release: async () => {\n release.resolve();\n try {\n await held;\n } catch (e) {\n lc.warn?.(`snapshot holder transaction ended with error`, e);\n }\n await holder.end();\n },\n };\n}\n\nfunction createLiteTables(\n tx: Database,\n tables: PublishedTableSpec[],\n initialVersion: string,\n) {\n // TODO: Figure out how to reuse the ChangeProcessor here to avoid\n // duplicating the ColumnMetadata logic.\n const columnMetadata = must(ColumnMetadataStore.getInstance(tx));\n for (const t of tables) {\n tx.exec(createLiteTableStatement(mapPostgresToLite(t, initialVersion)));\n const tableName = liteTableName(t);\n for (const [colName, colSpec] of Object.entries(t.columns)) {\n columnMetadata.insert(tableName, colName, colSpec);\n }\n }\n}\n\nfunction createLiteIndices(lc: LogContext, tx: Database, indices: IndexSpec[]) {\n for (const [i, index] of indices.entries()) {\n const stmt = createLiteIndexStatement(mapPostgresToLiteIndex(index));\n lc.info?.(`Creating index ${i + 1}/${indices.length}: ${stmt}`);\n const start = performance.now();\n tx.exec(stmt);\n lc.info?.(\n `Created index ${i + 1}/${indices.length} ` +\n `(${(performance.now() - start).toFixed(3)} ms): ${stmt}`,\n );\n }\n}\n\n/**\n * Runs structural assertions over a just-synced replica and throws if any\n * fail. Only called in shadow mode — a successful return means the replica\n * is schema-complete, row-count consistent, and its column metadata is in\n * sync with its lite schema.\n *\n * Note: this intentionally does NOT verify ZQL-queryability. Tables that\n * `computeZqlSpecs` drops (no PK / no all-NOT-NULL unique index, unsupported\n * column types, etc.) are silently skipped in production too — there's\n * nothing shadow-specific about them, so failing here would diverge from\n * prod over an upstream-schema condition prod accepts.\n *\n * Exported for testing.\n */\nexport function verifyShadowReplica(\n lc: LogContext,\n db: Database,\n published: {tables: PublishedTableSpec[]; indexes: IndexSpec[]},\n rowsByTable: ReadonlyMap<string, number>,\n): void {\n const issues: string[] = [];\n let columnsChecked = 0;\n let rowsChecked = 0;\n\n // 1. Schema completeness: every published table exists in the replica\n // with at least the expected column set.\n const liteTables = listTables(db);\n const liteTableByName = new Map(liteTables.map(t => [t.name, t]));\n for (const pt of published.tables) {\n const name = liteTableName(pt);\n const lite = liteTableByName.get(name);\n if (!lite) {\n issues.push(`missing table in replica: ${name}`);\n continue;\n }\n for (const col of Object.keys(pt.columns)) {\n columnsChecked++;\n if (!(col in lite.columns)) {\n issues.push(`column missing in replica table ${name}: ${col}`);\n }\n }\n }\n\n // Every published index exists in the replica.\n const liteIndexNames = new Set(listIndexes(db).map(i => i.name));\n for (const ix of published.indexes) {\n const mapped = mapPostgresToLiteIndex(ix);\n if (!liteIndexNames.has(mapped.name)) {\n issues.push(\n `missing index in replica: ${mapped.name} on ${mapped.tableName}`,\n );\n }\n }\n\n // 2. Row counts: SQLite COUNT(*) matches the in-memory copy counter.\n for (const [table, expected] of rowsByTable) {\n try {\n const [row] = db\n .prepare(`SELECT COUNT(*) as count FROM \"${table}\"`)\n .all<{count: number}>();\n if (row.count !== expected) {\n issues.push(\n `row count mismatch for table ${table}: ` +\n `copy counter reported ${expected}, replica has ${row.count}`,\n );\n } else {\n rowsChecked += row.count;\n }\n } catch (e) {\n issues.push(`could not count rows in table ${table}: ${String(e)}`);\n }\n }\n\n // 3. Column metadata: every published column has a _zero.column_metadata row.\n const meta = must(ColumnMetadataStore.getInstance(db));\n for (const pt of published.tables) {\n const name = liteTableName(pt);\n const rows = meta.getTable(name);\n for (const col of Object.keys(pt.columns)) {\n if (!rows.has(col)) {\n issues.push(`missing column_metadata row for ${name}.${col}`);\n }\n }\n }\n\n if (issues.length) {\n throw new Error(\n `Shadow replica verification failed (${issues.length} issue(s)):\\n` +\n issues.map(i => ` - ${i}`).join('\\n'),\n );\n }\n\n lc.info?.(\n `Shadow replica verification passed: ` +\n `${published.tables.length} tables, ` +\n `${published.indexes.length} indexes, ` +\n `${columnsChecked} columns, ` +\n `${rowsChecked.toLocaleString()} rows`,\n );\n}\n\n// Verified empirically that batches of 50 seem to be the sweet spot,\n// similar to the report in https://sqlite.org/forum/forumpost/8878a512d3652655\n//\n// Exported for testing.\nexport const INSERT_BATCH_SIZE = 50;\n\nconst MB = 1024 * 1024;\nconst MAX_BUFFERED_ROWS = 10_000;\nconst BUFFERED_SIZE_THRESHOLD = 8 * MB;\n\nexport type DownloadStatements = {\n select: string;\n getTotalRows: string;\n getTotalBytes: string;\n};\n\n/**\n * Produces ` TABLESAMPLE BERNOULLI(n)` when `sampleRate` is < 1, else `''`.\n * Row-level Bernoulli sampling is used (rather than SYSTEM) because it\n * produces a more uniform sample and, unlike SYSTEM, still returns rows\n * for small tables at low rates.\n */\nfunction tableSampleClause(sampleRate: number | undefined): string {\n if (sampleRate === undefined || sampleRate >= 1) {\n return '';\n }\n // Round away float noise (e.g. 0.3 * 100 = 30.000000000000004) while still\n // preserving sub-integer rates like 0.001 (= 0.1%).\n const pct = parseFloat((sampleRate * 100).toFixed(6));\n return /*sql*/ ` TABLESAMPLE BERNOULLI(${pct})`;\n}\n\nfunction limitClause(maxRowsPerTable: number | undefined): string {\n return maxRowsPerTable !== undefined\n ? /*sql*/ ` LIMIT ${maxRowsPerTable}`\n : '';\n}\n\n/**\n * Returns the SELECT column expressions for binary COPY, casting columns\n * without a known binary decoder to `::text`.\n */\nexport function makeBinarySelectExprs(\n table: PublishedTableSpec,\n cols: string[],\n): string[] {\n return cols.map(col => {\n const spec = table.columns[col];\n return hasBinaryDecoder(spec) ? id(col) : `${id(col)}::text`;\n });\n}\n\nexport function makeDownloadStatements(\n table: PublishedTableSpec,\n cols: string[],\n sampleRate?: number | undefined,\n maxRowsPerTable?: number | undefined,\n selectExprs?: string[] | undefined,\n): DownloadStatements {\n const filterConditions = Object.values(table.publications)\n .map(({rowFilter}) => rowFilter)\n .filter(f => !!f); // remove nulls\n const where =\n filterConditions.length === 0\n ? ''\n : /*sql*/ `WHERE ${filterConditions.join(' OR ')}`;\n const sample = tableSampleClause(sampleRate);\n const limit = limitClause(maxRowsPerTable);\n const fromTable = /*sql*/ `FROM ${id(table.schema)}.${id(table.name)}${sample} ${where}`;\n const select = /*sql*/ `SELECT ${(selectExprs ?? cols.map(id)).join(',')} ${fromTable}${limit}`;\n if (limit) {\n // With LIMIT, wrap counts/sums in a subquery so they reflect the\n // capped rowset rather than the full (sampled) table.\n const bytesExpr = cols\n .map(col => `COALESCE(pg_column_size(${id(col)}), 0)`)\n .join(' + ');\n return {\n select,\n getTotalRows: /*sql*/ `SELECT COUNT(*)::bigint AS \"totalRows\" FROM (SELECT 1 AS _ ${fromTable}${limit}) s`,\n getTotalBytes: /*sql*/ `SELECT COALESCE(SUM(b), 0)::bigint AS \"totalBytes\" FROM (SELECT (${bytesExpr}) AS b ${fromTable}${limit}) s`,\n };\n }\n const totalBytes = `(${cols.map(col => `SUM(COALESCE(pg_column_size(${id(col)}), 0))`).join(' + ')})`;\n return {\n select,\n getTotalRows: /*sql*/ `SELECT COUNT(*) AS \"totalRows\" ${fromTable}`,\n getTotalBytes: /*sql*/ `SELECT ${totalBytes} AS \"totalBytes\" ${fromTable}`,\n };\n}\n\ntype DownloadState = {\n spec: PublishedTableSpec;\n status: DownloadStatus;\n};\n\ntype CopyFormat = 'binary' | 'text';\ntype InitialSyncMode = 'initial' | 'shadow';\n\ntype InitialSyncMetricAttrs = {\n syncMode: InitialSyncMode;\n copyFormat: CopyFormat;\n};\n\ntype InitialSyncRunMetricAttrs = {\n syncMode: InitialSyncMode;\n copyFormat: CopyFormat;\n result: 'success' | 'error';\n};\n\ntype CopyResult = {\n schema: string;\n table: string;\n replicaTable: string;\n syncMode: InitialSyncMode;\n copyFormat: CopyFormat;\n columnCount: number;\n rows: number;\n estimatedRows: number;\n estimatedBytes: number | undefined;\n flushMs: number;\n elapsedMs: number;\n copyBytes: number;\n};\n\nconst INITIAL_SYNC_DURATION_HISTOGRAM_BOUNDARIES_S = [\n 1, 2, 5, 10, 30, 60, 120, 300, 600, 1200, 2400, 3600, 7200,\n];\nexport const COPY_METRIC_BATCH_BYTES = 8 * 1024 * 1024;\nconst SLOW_COPY_FLUSH_MS = 10_000;\n\n// change-streamer imports this module before startOtelAuto() runs, so create\n// instruments lazily to avoid binding them to OTel's no-op meter provider.\nfunction initialSyncRuns() {\n return getOrCreateCounter(\n 'replication',\n 'initial_sync_runs',\n 'Initial sync runs, labeled by result.',\n );\n}\n\nfunction initialSyncDuration() {\n return initialSyncDurationHistogram(\n 'initial_sync_duration',\n 'Wall-clock duration of an initial sync run, labeled by result.',\n );\n}\n\nfunction initialSyncCopyDuration() {\n return initialSyncDurationHistogram(\n 'initial_sync_copy_duration',\n 'Wall-clock duration of the COPY phase for a successful initial sync run.',\n );\n}\n\nfunction initialSyncCopyOtherDuration() {\n return initialSyncDurationHistogram(\n 'initial_sync_copy_other_duration',\n 'Initial sync total duration excluding SQLite flush and index time for a successful run.',\n );\n}\n\nfunction initialSyncFlushDuration() {\n return initialSyncDurationHistogram(\n 'initial_sync_flush_duration',\n 'Total SQLite flush time for a successful initial sync run.',\n );\n}\n\nfunction initialSyncIndexDuration() {\n return initialSyncDurationHistogram(\n 'initial_sync_index_duration',\n 'SQLite index creation time for a successful initial sync run.',\n );\n}\n\nfunction initialSyncRows() {\n return getOrCreateCounter(\n 'replication',\n 'initial_sync_rows',\n 'Rows copied during successful initial sync runs.',\n );\n}\n\nfunction initialSyncCopyStream() {\n return getOrCreateCounter('replication', 'initial_sync_copy_stream', {\n description:\n 'PostgreSQL COPY stream bytes processed during initial sync, including in-progress and failed runs.',\n unit: 'bytes',\n });\n}\n\nfunction initialSyncCompletedCopyStream() {\n return getOrCreateCounter(\n 'replication',\n 'initial_sync_completed_copy_stream',\n {\n description:\n 'PostgreSQL COPY stream bytes processed during successful initial sync runs.',\n unit: 'bytes',\n },\n );\n}\n\nfunction initialSyncCopyChunks() {\n return getOrCreateCounter(\n 'replication',\n 'initial_sync_copy_chunks',\n 'PostgreSQL COPY stream chunks processed during initial sync.',\n );\n}\n\nexport function createCopyMetricBatcher(\n record: (bytes: number, chunks: number) => void,\n) {\n let bytes = 0;\n let chunks = 0;\n\n function flush() {\n if (chunks === 0) {\n return;\n }\n record(bytes, chunks);\n bytes = 0;\n chunks = 0;\n }\n\n return {\n add(chunkBytes: number) {\n bytes += chunkBytes;\n chunks++;\n if (bytes >= COPY_METRIC_BATCH_BYTES) {\n flush();\n }\n },\n flush,\n };\n}\n\nexport function initialSyncCopyMetrics(attrs: InitialSyncMetricAttrs) {\n const labels = initialSyncMetricAttrs(attrs);\n const copyStreamMetric = initialSyncCopyStream();\n const copyChunksMetric = initialSyncCopyChunks();\n return createCopyMetricBatcher((bytes, chunks) => {\n copyStreamMetric.add(bytes, labels);\n copyChunksMetric.add(chunks, labels);\n });\n}\n\nfunction initialSyncDurationHistogram(name: string, description: string) {\n return getOrCreateHistogram('replication', name, {\n description,\n unit: 's',\n bucketBoundaries: INITIAL_SYNC_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\nfunction initialSyncMetricAttrs(attrs: InitialSyncMetricAttrs) {\n return {\n sync_mode: attrs.syncMode,\n copy_format: attrs.copyFormat,\n };\n}\n\nfunction initialSyncRunMetricAttrs(attrs: InitialSyncRunMetricAttrs) {\n return {\n ...initialSyncMetricAttrs(attrs),\n result: attrs.result,\n };\n}\n\nfunction recordInitialSyncRunMetrics(\n stats: {\n durationMs: number;\n rows?: number | undefined;\n copyBytes?: number | undefined;\n copyMs?: number | undefined;\n copyOtherMs?: number | undefined;\n flushMs?: number | undefined;\n indexMs?: number | undefined;\n },\n attrs: InitialSyncRunMetricAttrs,\n) {\n const labels = initialSyncRunMetricAttrs(attrs);\n initialSyncRuns().add(1, labels);\n initialSyncDuration().recordMs(stats.durationMs, labels);\n if (attrs.result === 'success') {\n if (stats.copyMs !== undefined) {\n initialSyncCopyDuration().recordMs(stats.copyMs, labels);\n }\n if (stats.copyOtherMs !== undefined) {\n initialSyncCopyOtherDuration().recordMs(stats.copyOtherMs, labels);\n }\n if (stats.flushMs !== undefined) {\n initialSyncFlushDuration().recordMs(stats.flushMs, labels);\n }\n if (stats.indexMs !== undefined) {\n initialSyncIndexDuration().recordMs(stats.indexMs, labels);\n }\n if (stats.rows !== undefined && stats.rows > 0) {\n initialSyncRows().add(stats.rows, labels);\n }\n if (stats.copyBytes !== undefined && stats.copyBytes > 0) {\n initialSyncCompletedCopyStream().add(stats.copyBytes, labels);\n }\n }\n}\n\nfunction initialSyncCopySummary(\n results: readonly CopyResult[],\n copyMs: number,\n) {\n const totals = results.reduce(\n (acc, curr) => {\n acc.rows += curr.rows;\n acc.flushMs += curr.flushMs;\n acc.copyBytes += curr.copyBytes;\n return acc;\n },\n {\n rows: 0,\n flushMs: 0,\n copyBytes: 0,\n },\n );\n return {\n tables: results.length,\n rows: totals.rows,\n copyMs,\n flushMs: totals.flushMs,\n copyBytes: totals.copyBytes,\n };\n}\n\nfunction logSlowCopyFlush(\n lc: LogContext,\n details: {\n schema: string;\n table: string;\n replicaTable: string;\n syncMode: InitialSyncMode;\n copyFormat: CopyFormat;\n elapsedMs: number;\n flushedRows: number;\n flushedBytes: number;\n rows: number;\n copyBytes: number;\n },\n) {\n if (details.elapsedMs < SLOW_COPY_FLUSH_MS || details.flushedRows === 0) {\n return;\n }\n lc.info?.('initial-sync table copy slow flush', details);\n}\n\n// Exported for testing.\nexport async function getInitialDownloadState(\n lc: LogContext,\n sql: PostgresDB,\n spec: PublishedTableSpec,\n skipTotals: boolean,\n): Promise<DownloadState> {\n const start = performance.now();\n const table = liteTableName(spec);\n const columns = Object.keys(spec.columns);\n if (skipTotals) {\n // Shadow sync suppresses status events, so the pg_class\n // estimates would be computed and thrown away.\n return {\n spec,\n status: {table, columns, rows: 0, totalRows: 0, totalBytes: 0},\n };\n }\n // Use pg_class estimates instead of expensive COUNT(*) and\n // SUM(pg_column_size(...)) full table scans. The exact values are only\n // used for progress reporting, so estimates are sufficient.\n const qualifiedName = `${id(spec.schema)}.${id(spec.name)}`;\n const estimateResult = await sql<\n {totalRows: number; totalBytes: number}[]\n >`SELECT GREATEST(reltuples, 0)::float8 AS \"totalRows\",\n pg_table_size(oid)::float8 AS \"totalBytes\"\n FROM pg_class\n WHERE oid = ${qualifiedName}::regclass`;\n\n const {totalRows, totalBytes} = estimateResult[0] ?? {\n totalRows: 0,\n totalBytes: 0,\n };\n\n const state: DownloadState = {\n spec,\n status: {\n table,\n columns,\n rows: 0,\n totalRows,\n totalBytes,\n },\n };\n const elapsed = (performance.now() - start).toFixed(3);\n lc.info?.(`Computed initial download state for ${table} (${elapsed} ms)`, {\n state: state.status,\n });\n return state;\n}\n\nfunction copy(\n lc: LogContext,\n {spec: table, status}: DownloadState,\n dbClient: PostgresDB,\n from: PostgresTransaction,\n to: Database,\n textCopy: boolean,\n syncMode: InitialSyncMode,\n sampleRate?: number | undefined,\n maxRowsPerTable?: number | undefined,\n): Promise<CopyResult> {\n if (textCopy) {\n return copyText(\n lc,\n table,\n status,\n dbClient,\n from,\n to,\n syncMode,\n sampleRate,\n maxRowsPerTable,\n );\n }\n return copyBinary(\n lc,\n table,\n status,\n from,\n to,\n syncMode,\n sampleRate,\n maxRowsPerTable,\n );\n}\n\nasync function copyBinary(\n lc: LogContext,\n table: PublishedTableSpec,\n status: DownloadStatus,\n from: PostgresTransaction,\n to: Database,\n syncMode: InitialSyncMode,\n sampleRate?: number | undefined,\n maxRowsPerTable?: number | undefined,\n): Promise<CopyResult> {\n const start = performance.now();\n const copyFormat: CopyFormat = 'binary';\n const copyMetrics = initialSyncCopyMetrics({syncMode, copyFormat});\n let flushMs = 0;\n let copyBytes = 0;\n\n const tableName = liteTableName(table);\n const orderedColumns = Object.entries(table.columns);\n\n const columnNames = orderedColumns.map(([c]) => c);\n const columnSpecs = orderedColumns.map(([_name, spec]) => spec);\n const insertColumnList = columnNames.map(c => id(c)).join(',');\n\n const valuesSql =\n columnNames.length > 0 ? `(${'?,'.repeat(columnNames.length - 1)}?)` : '()';\n const insertSql = /*sql*/ `\n INSERT INTO \"${tableName}\" (${insertColumnList}) VALUES ${valuesSql}`;\n const insertStmt = to.prepare(insertSql);\n const insertBatchStmt = to.prepare(\n insertSql + `,${valuesSql}`.repeat(INSERT_BATCH_SIZE - 1),\n );\n\n // Build SELECT with ::text casts for columns without a known binary decoder.\n const select = makeDownloadStatements(\n table,\n columnNames,\n sampleRate,\n maxRowsPerTable,\n makeBinarySelectExprs(table, columnNames),\n ).select;\n\n const decoders = orderedColumns.map(([, spec]) =>\n hasBinaryDecoder(spec) ? makeBinaryDecoder(spec) : textCastDecoder,\n );\n\n const valuesPerRow = columnSpecs.length;\n const valuesPerBatch = valuesPerRow * INSERT_BATCH_SIZE;\n\n const pendingValues: LiteValueType[] = Array.from({\n length: MAX_BUFFERED_ROWS * valuesPerRow,\n });\n let pendingRows = 0;\n let pendingSize = 0;\n\n function flush() {\n const flushStart = performance.now();\n const flushedRows = pendingRows;\n const flushedSize = pendingSize;\n\n let l = 0;\n for (; pendingRows > INSERT_BATCH_SIZE; pendingRows -= INSERT_BATCH_SIZE) {\n insertBatchStmt.run(pendingValues.slice(l, (l += valuesPerBatch)));\n }\n for (; pendingRows > 0; pendingRows--) {\n insertStmt.run(pendingValues.slice(l, (l += valuesPerRow)));\n }\n const flushedValues = flushedRows * valuesPerRow;\n for (let i = 0; i < flushedValues; i++) {\n pendingValues[i] = undefined as unknown as LiteValueType;\n }\n pendingSize = 0;\n status.rows += flushedRows;\n\n const elapsed = performance.now() - flushStart;\n flushMs += elapsed;\n lc.debug?.(\n `flushed ${flushedRows} ${tableName} rows (${flushedSize} bytes) in ${elapsed.toFixed(3)} ms`,\n );\n logSlowCopyFlush(lc, {\n schema: table.schema,\n table: table.name,\n replicaTable: tableName,\n syncMode,\n copyFormat,\n elapsedMs: elapsed,\n flushedRows,\n flushedBytes: flushedSize,\n rows: status.rows,\n copyBytes,\n });\n }\n\n const binaryParser = new BinaryCopyParser();\n let col = 0;\n\n lc.info?.(`Starting binary copy stream of ${tableName}:`, select);\n\n await pipeline(\n await from\n .unsafe(`COPY (${select}) TO STDOUT WITH (FORMAT binary)`)\n .readable(),\n new Writable({\n highWaterMark: BUFFERED_SIZE_THRESHOLD,\n\n write(\n chunk: Buffer,\n _encoding: string,\n callback: (error?: Error) => void,\n ) {\n try {\n copyBytes += chunk.length;\n copyMetrics.add(chunk.length);\n for (const fieldBuf of binaryParser.parse(chunk)) {\n const fieldSize = fieldBuf === null ? 4 : fieldBuf.length;\n pendingSize += fieldSize;\n pendingValues[pendingRows * valuesPerRow + col] =\n fieldBuf === null ? null : decoders[col](fieldBuf);\n\n if (++col === decoders.length) {\n col = 0;\n if (\n ++pendingRows >= MAX_BUFFERED_ROWS - valuesPerRow ||\n pendingSize >= BUFFERED_SIZE_THRESHOLD\n ) {\n flush();\n }\n }\n }\n callback();\n } catch (e) {\n callback(e instanceof Error ? e : new Error(String(e)));\n }\n },\n\n final: (callback: (error?: Error) => void) => {\n try {\n copyMetrics.flush();\n flush();\n callback();\n } catch (e) {\n callback(e instanceof Error ? e : new Error(String(e)));\n }\n },\n\n destroy(error, callback) {\n copyMetrics.flush();\n callback(error);\n },\n }),\n );\n\n const elapsed = performance.now() - start;\n const result = {\n schema: table.schema,\n table: table.name,\n replicaTable: tableName,\n syncMode,\n copyFormat,\n columnCount: columnNames.length,\n rows: status.rows,\n estimatedRows: status.totalRows,\n estimatedBytes: status.totalBytes,\n flushMs,\n elapsedMs: elapsed,\n copyBytes,\n } satisfies CopyResult;\n\n lc.info?.(\n `Finished copying ${status.rows} rows into ${tableName} ` +\n `(flush: ${flushMs.toFixed(3)} ms) (total: ${elapsed.toFixed(3)} ms) `,\n result,\n );\n return result;\n}\n\nasync function copyText(\n lc: LogContext,\n table: PublishedTableSpec,\n status: DownloadStatus,\n dbClient: PostgresDB,\n from: PostgresTransaction,\n to: Database,\n syncMode: InitialSyncMode,\n sampleRate?: number | undefined,\n maxRowsPerTable?: number | undefined,\n): Promise<CopyResult> {\n const start = performance.now();\n const copyFormat: CopyFormat = 'text';\n const copyMetrics = initialSyncCopyMetrics({syncMode, copyFormat});\n let flushMs = 0;\n let copyBytes = 0;\n\n const tableName = liteTableName(table);\n const orderedColumns = Object.entries(table.columns);\n\n const columnNames = orderedColumns.map(([c]) => c);\n const columnSpecs = orderedColumns.map(([_name, spec]) => spec);\n const insertColumnList = columnNames.map(c => id(c)).join(',');\n\n const valuesSql =\n columnNames.length > 0 ? `(${'?,'.repeat(columnNames.length - 1)}?)` : '()';\n const insertSql = /*sql*/ `\n INSERT INTO \"${tableName}\" (${insertColumnList}) VALUES ${valuesSql}`;\n const insertStmt = to.prepare(insertSql);\n const insertBatchStmt = to.prepare(\n insertSql + `,${valuesSql}`.repeat(INSERT_BATCH_SIZE - 1),\n );\n\n const {select} = makeDownloadStatements(\n table,\n columnNames,\n sampleRate,\n maxRowsPerTable,\n );\n const valuesPerRow = columnSpecs.length;\n const valuesPerBatch = valuesPerRow * INSERT_BATCH_SIZE;\n\n const pendingValues: LiteValueType[] = Array.from({\n length: MAX_BUFFERED_ROWS * valuesPerRow,\n });\n let pendingRows = 0;\n let pendingSize = 0;\n\n function flush() {\n const flushStart = performance.now();\n const flushedRows = pendingRows;\n const flushedSize = pendingSize;\n\n let l = 0;\n for (; pendingRows > INSERT_BATCH_SIZE; pendingRows -= INSERT_BATCH_SIZE) {\n insertBatchStmt.run(pendingValues.slice(l, (l += valuesPerBatch)));\n }\n for (; pendingRows > 0; pendingRows--) {\n insertStmt.run(pendingValues.slice(l, (l += valuesPerRow)));\n }\n const flushedValues = flushedRows * valuesPerRow;\n for (let i = 0; i < flushedValues; i++) {\n pendingValues[i] = undefined as unknown as LiteValueType;\n }\n pendingSize = 0;\n status.rows += flushedRows;\n\n const elapsed = performance.now() - flushStart;\n flushMs += elapsed;\n lc.debug?.(\n `flushed ${flushedRows} ${tableName} rows (${flushedSize} bytes) in ${elapsed.toFixed(3)} ms`,\n );\n logSlowCopyFlush(lc, {\n schema: table.schema,\n table: table.name,\n replicaTable: tableName,\n syncMode,\n copyFormat,\n elapsedMs: elapsed,\n flushedRows,\n flushedBytes: flushedSize,\n rows: status.rows,\n copyBytes,\n });\n }\n\n lc.info?.(`Starting text copy stream of ${tableName}:`, select);\n const pgParsers = await getTypeParsers(dbClient, {returnJsonAsString: true});\n const parsers = columnSpecs.map(c => {\n const pgParse = pgParsers.getTypeParser(c.typeOID);\n return (val: string) =>\n liteValue(\n pgParse(val) as PostgresValueType,\n c.dataType,\n JSON_STRINGIFIED,\n );\n });\n\n const tsvParser = new TsvParser();\n let col = 0;\n\n await pipeline(\n await from.unsafe(`COPY (${select}) TO STDOUT`).readable(),\n new Writable({\n highWaterMark: BUFFERED_SIZE_THRESHOLD,\n\n write(\n chunk: Buffer,\n _encoding: string,\n callback: (error?: Error) => void,\n ) {\n try {\n copyBytes += chunk.length;\n copyMetrics.add(chunk.length);\n for (const text of tsvParser.parse(chunk)) {\n const fieldSize = text === null ? 4 : text.length;\n pendingSize += fieldSize;\n pendingValues[pendingRows * valuesPerRow + col] =\n text === null ? null : parsers[col](text);\n\n if (++col === parsers.length) {\n col = 0;\n if (\n ++pendingRows >= MAX_BUFFERED_ROWS - valuesPerRow ||\n pendingSize >= BUFFERED_SIZE_THRESHOLD\n ) {\n flush();\n }\n }\n }\n callback();\n } catch (e) {\n callback(e instanceof Error ? e : new Error(String(e)));\n }\n },\n\n final: (callback: (error?: Error) => void) => {\n try {\n copyMetrics.flush();\n flush();\n callback();\n } catch (e) {\n callback(e instanceof Error ? e : new Error(String(e)));\n }\n },\n\n destroy(error, callback) {\n copyMetrics.flush();\n callback(error);\n },\n }),\n );\n\n const elapsed = performance.now() - start;\n const result = {\n schema: table.schema,\n table: table.name,\n replicaTable: tableName,\n syncMode,\n copyFormat,\n columnCount: columnNames.length,\n rows: status.rows,\n estimatedRows: status.totalRows,\n estimatedBytes: status.totalBytes,\n flushMs,\n elapsedMs: elapsed,\n copyBytes,\n } satisfies CopyResult;\n lc.info?.(\n `Finished copying ${status.rows} rows into ${tableName} ` +\n `(flush: ${flushMs.toFixed(3)} ms) (total: ${elapsed.toFixed(3)} ms) `,\n result,\n );\n return result;\n}\n"],"mappings":"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"}
|
|
@@ -16,5 +16,26 @@ export declare function subscribe(lc: LogContext, db: PostgresDB, slot: string,
|
|
|
16
16
|
messages: SourceWithPendingQueue<StreamMessage>;
|
|
17
17
|
acks: Sink<bigint>;
|
|
18
18
|
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Derives the timings for the replication liveness timer from the server's
|
|
21
|
+
* `wal_sender_timeout` (in milliseconds).
|
|
22
|
+
*
|
|
23
|
+
* Postgres treats `wal_sender_timeout = 0` as "disabled": it will neither
|
|
24
|
+
* terminate an idle wal sender nor emit the periodic keepalives it otherwise
|
|
25
|
+
* sends at roughly `wal_sender_timeout / 2`. Every timing below is derived from
|
|
26
|
+
* that interval, so a disabled timeout disables the liveness machinery entirely
|
|
27
|
+
* (`enabled: false`). Otherwise the derived thresholds would all collapse to 0,
|
|
28
|
+
* inverting the intended meaning: the inbound watchdog would fire on the very
|
|
29
|
+
* first tick and destroy the stream, while the timer itself would busy-spin at
|
|
30
|
+
* a 0ms interval — together producing a continuous reconnect storm.
|
|
31
|
+
*
|
|
32
|
+
* https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT
|
|
33
|
+
*/
|
|
34
|
+
export declare function computeLivenessTimings(walSenderTimeoutMs: number): {
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
manualKeepaliveTimeout: number;
|
|
37
|
+
inboundTimeoutMs: number;
|
|
38
|
+
timerIntervalMs: number;
|
|
39
|
+
};
|
|
19
40
|
export {};
|
|
20
41
|
//# sourceMappingURL=stream.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/logical-replication/stream.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAKjD,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAExD,OAAO,EAAO,KAAK,IAAI,EAAE,KAAK,MAAM,EAAC,MAAM,8BAA8B,CAAC;AAK1E,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,qBAAqB,CAAC;AAIjD,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM;IACX,OAAO,GAAG;QAAC,GAAG,EAAE,WAAW,CAAC;QAAC,aAAa,EAAE,OAAO,CAAA;KAAC;CACrD,CAAC;AAKF,KAAK,sBAAsB,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG;IAC3C,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,SAAS,CAC7B,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EAAE,EACtB,GAAG,EAAE,MAAM,EACX,8BAA8B,SAA6C,EAC3E,eAAe,SAAoB,GAClC,OAAO,CAAC;IACT,QAAQ,EAAE,sBAAsB,CAAC,aAAa,CAAC,CAAC;IAChD,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;CACpB,CAAC,
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/logical-replication/stream.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAKjD,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAExD,OAAO,EAAO,KAAK,IAAI,EAAE,KAAK,MAAM,EAAC,MAAM,8BAA8B,CAAC;AAK1E,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,qBAAqB,CAAC;AAIjD,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM;IACX,OAAO,GAAG;QAAC,GAAG,EAAE,WAAW,CAAC;QAAC,aAAa,EAAE,OAAO,CAAA;KAAC;CACrD,CAAC;AAKF,KAAK,sBAAsB,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG;IAC3C,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,SAAS,CAC7B,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EAAE,EACtB,GAAG,EAAE,MAAM,EACX,8BAA8B,SAA6C,EAC3E,eAAe,SAAoB,GAClC,OAAO,CAAC;IACT,QAAQ,EAAE,sBAAsB,CAAC,aAAa,CAAC,CAAC;IAChD,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;CACpB,CAAC,CAwID;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,kBAAkB,EAAE,MAAM,GAAG;IAClE,OAAO,EAAE,OAAO,CAAC;IACjB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;CACzB,CAyBA"}
|
|
@@ -26,17 +26,16 @@ async function subscribe(lc, db, slot, publications, lsn, retriesIfReplicationSl
|
|
|
26
26
|
const [{ walSenderTimeoutMs }] = await session`SELECT EXTRACT(EPOCH FROM (setting || unit)::interval) * 1000
|
|
27
27
|
AS "walSenderTimeoutMs" FROM pg_settings
|
|
28
28
|
WHERE name = 'wal_sender_timeout'`.simple();
|
|
29
|
-
const manualKeepaliveTimeout =
|
|
30
|
-
lc.info?.(`wal_sender_timeout: ${walSenderTimeoutMs}ms. Ensuring manual keepalives at least every ${manualKeepaliveTimeout}ms`);
|
|
29
|
+
const { enabled: livenessEnabled, manualKeepaliveTimeout, inboundTimeoutMs, timerIntervalMs } = computeLivenessTimings(walSenderTimeoutMs);
|
|
30
|
+
lc.info?.(livenessEnabled ? `wal_sender_timeout: ${walSenderTimeoutMs}ms. Ensuring manual keepalives at least every ${manualKeepaliveTimeout}ms` : "wal_sender_timeout is disabled (0); skipping manual keepalives and inbound liveness detection.");
|
|
31
31
|
const [readable, writable] = await startReplicationStream(lc, session, slot, publications, lsn, retriesIfReplicationSlotActive + 1);
|
|
32
32
|
let lastAckTime = Date.now();
|
|
33
33
|
function sendAck(lsn) {
|
|
34
34
|
writable.write(makeAck(lsn));
|
|
35
35
|
lastAckTime = Date.now();
|
|
36
36
|
}
|
|
37
|
-
const inboundTimeoutMs = walSenderTimeoutMs * 2;
|
|
38
37
|
let lastReceivedTime = Date.now();
|
|
39
|
-
const livenessTimer = setInterval(() => {
|
|
38
|
+
const livenessTimer = livenessEnabled ? setInterval(() => {
|
|
40
39
|
const now = Date.now();
|
|
41
40
|
if (now - lastAckTime > manualKeepaliveTimeout) {
|
|
42
41
|
sendAck(0n);
|
|
@@ -47,7 +46,7 @@ async function subscribe(lc, db, slot, publications, lsn, retriesIfReplicationSl
|
|
|
47
46
|
lc.warn?.(`no message received from ${db.options.host} in ${sinceLastReceived}ms (> 2x wal_sender_timeout ${walSenderTimeoutMs}ms). Destroying replication stream to force reconnect.`);
|
|
48
47
|
readable.destroy(/* @__PURE__ */ new Error(`replication stream inbound timeout after ${sinceLastReceived}ms`));
|
|
49
48
|
}
|
|
50
|
-
},
|
|
49
|
+
}, timerIntervalMs) : void 0;
|
|
51
50
|
let destroyed = false;
|
|
52
51
|
const parser = new PgoutputParser(await getTypeParsers(db, { returnJsonAsString: true }));
|
|
53
52
|
const messages = Subscription.create({ cleanup: () => {
|
|
@@ -73,6 +72,36 @@ async function subscribe(lc, db, slot, publications, lsn, retriesIfReplicationSl
|
|
|
73
72
|
};
|
|
74
73
|
}
|
|
75
74
|
/**
|
|
75
|
+
* Derives the timings for the replication liveness timer from the server's
|
|
76
|
+
* `wal_sender_timeout` (in milliseconds).
|
|
77
|
+
*
|
|
78
|
+
* Postgres treats `wal_sender_timeout = 0` as "disabled": it will neither
|
|
79
|
+
* terminate an idle wal sender nor emit the periodic keepalives it otherwise
|
|
80
|
+
* sends at roughly `wal_sender_timeout / 2`. Every timing below is derived from
|
|
81
|
+
* that interval, so a disabled timeout disables the liveness machinery entirely
|
|
82
|
+
* (`enabled: false`). Otherwise the derived thresholds would all collapse to 0,
|
|
83
|
+
* inverting the intended meaning: the inbound watchdog would fire on the very
|
|
84
|
+
* first tick and destroy the stream, while the timer itself would busy-spin at
|
|
85
|
+
* a 0ms interval — together producing a continuous reconnect storm.
|
|
86
|
+
*
|
|
87
|
+
* https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT
|
|
88
|
+
*/
|
|
89
|
+
function computeLivenessTimings(walSenderTimeoutMs) {
|
|
90
|
+
if (!(walSenderTimeoutMs > 0)) return {
|
|
91
|
+
enabled: false,
|
|
92
|
+
manualKeepaliveTimeout: 0,
|
|
93
|
+
inboundTimeoutMs: 0,
|
|
94
|
+
timerIntervalMs: 0
|
|
95
|
+
};
|
|
96
|
+
const manualKeepaliveTimeout = Math.floor(walSenderTimeoutMs * .75);
|
|
97
|
+
return {
|
|
98
|
+
enabled: true,
|
|
99
|
+
manualKeepaliveTimeout,
|
|
100
|
+
inboundTimeoutMs: walSenderTimeoutMs * 2,
|
|
101
|
+
timerIntervalMs: manualKeepaliveTimeout / 5
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
76
105
|
* Formats publication names for the START_REPLICATION command.
|
|
77
106
|
* The replication protocol expects format: publication_names 'pub1,pub2'
|
|
78
107
|
* Each name is escaped to handle any quotes that may have passed validation.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.js","names":[],"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 {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} 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 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): Promise<{\n messages: SourceWithPendingQueue<StreamMessage>;\n acks: Sink<bigint>;\n}> {\n const session = 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 // 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 manualKeepaliveTimeout = Math.floor(walSenderTimeoutMs * 0.75);\n lc.info?.(\n `wal_sender_timeout: ${walSenderTimeoutMs}ms. ` +\n `Ensuring manual keepalives at least every ${manualKeepaliveTimeout}ms`,\n );\n\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 // Postgres promises to send a keepalive at roughly wal_sender_timeout / 2.\n // If we go long enough without receiving anything (data or keepalive), the\n // inbound half of the connection is silently dead — our outbound acks keep\n // flowing into the void, neither side errors, and the change-source sits\n // there forever. Tear down the readable to force a reconnect.\n //\n // The threshold is 2x wal_sender_timeout — i.e. two consecutive keepalives\n // missed — to avoid spurious reconnects from a single delayed keepalive\n // (PG scheduling jitter under load, our own event-loop stalls, etc.).\n const inboundTimeoutMs = walSenderTimeoutMs * 2;\n let lastReceivedTime = Date.now();\n\n const livenessTimer = 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 if (sinceLastReceived > inboundTimeoutMs && !readable.destroyed) {\n lc.warn?.(\n `no message received from ${db.options.host} in ${sinceLastReceived}ms ` +\n `(> 2x wal_sender_timeout ${walSenderTimeoutMs}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 }, manualKeepaliveTimeout / 5);\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 return parseStreamMessage(lc, buffer, parser);\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\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(10);\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\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":[],"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 {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} 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 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): Promise<{\n messages: SourceWithPendingQueue<StreamMessage>;\n acks: Sink<bigint>;\n}> {\n const session = 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 // 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);\n lc.info?.(\n livenessEnabled\n ? `wal_sender_timeout: ${walSenderTimeoutMs}ms. ` +\n `Ensuring manual keepalives at least every ${manualKeepaliveTimeout}ms`\n : `wal_sender_timeout is disabled (0); skipping manual keepalives and ` +\n `inbound liveness detection.`,\n );\n\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 if (sinceLastReceived > inboundTimeoutMs && !readable.destroyed) {\n lc.warn?.(\n `no message received from ${db.options.host} in ${sinceLastReceived}ms ` +\n `(> 2x wal_sender_timeout ${walSenderTimeoutMs}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 return parseStreamMessage(lc, buffer, parser);\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\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 * https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT\n */\nexport function computeLivenessTimings(walSenderTimeoutMs: number): {\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.).\n inboundTimeoutMs: walSenderTimeoutMs * 2,\n // Poll at 1/5th of the manual keepalive interval.\n timerIntervalMs: manualKeepaliveTimeout / 5,\n };\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(10);\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\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,7 +1,7 @@
|
|
|
1
1
|
import type { LogContext } from '@rocicorp/logger';
|
|
2
2
|
import type postgres from 'postgres';
|
|
3
|
-
import { type PostgresDB } from '../../../types/pg';
|
|
4
|
-
import { type ShardID } from '../../../types/shards';
|
|
3
|
+
import { type PostgresDB } from '../../../types/pg.ts';
|
|
4
|
+
import { type ShardID } from '../../../types/shards.ts';
|
|
5
5
|
export type ReplicationSlot = {
|
|
6
6
|
slot_name: string;
|
|
7
7
|
consistent_point: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replication-slots.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-source/pg/replication-slots.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AAErC,OAAO,EAAkB,KAAK,UAAU,EAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"replication-slots.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-source/pg/replication-slots.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AAErC,OAAO,EAAkB,KAAK,UAAU,EAAC,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAiB,KAAK,OAAO,EAAC,MAAM,0BAA0B,CAAC;AAUtE,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,cAAc,GAAG;IAC3B,QAAQ,EAAE,MAAM,CAAC;IAGjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAoBF,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,QAAQ,CAAC,GAAG,EACrB,EAAC,QAAQ,EAAE,QAAQ,EAAE,WAAoC,EAAC,EAAE,cAAc,GACzE,OAAO,CAAC,eAAe,CAAC,CA0C1B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,UAAU,EACf,kBAAkB,EAAE,QAAQ,CAAC,GAAG,EAChC,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,eAAe,CAAC,CAmE1B;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,CAAC,CAY9D;AA+DD,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,UAUvC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replication-slots.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-source/pg/replication-slots.ts"],"sourcesContent":["import {\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 {runTx} from '../../../db/run-transaction';\nimport {isPostgresError, type PostgresDB} from '../../../types/pg';\nimport {upstreamSchema, type ShardID} from '../../../types/shards';\nimport {orTimeout} from '../../../types/timeout';\nimport {toStateVersionString} from './lsn';\nimport {\n createReplica,\n replicationSlotExpression,\n replicationSlotPrefix,\n} from './schema/shard';\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 CreateSlotSpec = {\n slotName: string;\n\n // Note: must be false if pgVersion < PG_17. Caller must verify.\n failover?: boolean;\n\n // For overriding in tests.\n lockTimeout?: number;\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.\nconst CREATE_REPLICATION_SLOT_TIMEOUT_MS = 30_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 {slotName, failover, lockTimeout = SERVER_LOCK_TIMEOUT_MS}: 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 createSlot = failover\n ? session.unsafe<ReplicationSlot[]>(\n /*sql*/ `CREATE_REPLICATION_SLOT \"${slotName}\" LOGICAL pgoutput (FAILOVER)`,\n )\n : session.unsafe<ReplicationSlot[]>(\n /*sql*/ `CREATE_REPLICATION_SLOT \"${slotName}\" LOGICAL pgoutput`,\n );\n const raced = await orTimeout(createSlot, CREATE_REPLICATION_SLOT_TIMEOUT_MS);\n if (raced === 'timed-out') {\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 `Crashing to force a clean restart.`,\n );\n }\n const [slot] = raced;\n lc.info?.(`Created replication slot ${slotName}`, slot);\n return slot;\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 */\nexport async function createReplicaAndSlot(\n lc: LogContext,\n sql: PostgresDB,\n replicationSession: postgres.Sql,\n shard: ShardID,\n replicaID: string,\n failover: boolean,\n): Promise<ReplicationSlot> {\n const lockName = replicationSlotManagementLock(shard);\n const slotPoolPrefix = replicationSlotPrefix(shard);\n for (let first = true; ; first = false) {\n await dropUnclaimedSlots(lc, sql, shard);\n try {\n return await runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n // Pick an available slotName from the slotPoolPrefix pool.\n let slotName: string;\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 });\n\n await createReplica(\n tx,\n shard,\n replicaID,\n slot.slot_name,\n toStateVersionString(slot.consistent_point),\n );\n\n return slot;\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 throw e;\n }\n }\n}\n\n/**\n * Deletes \"old\" replicas (i.e. those with a lower rank than the current)\n * and attempts to drop replication slots that are not associated with any\n * replica.\n *\n * If a slot could not be dropped because there is still an active subscriber,\n * it will be reflected in the `draining` count that is returned. When there\n * are draining slots, the method should be retried until all orphaned slots\n * have been dropped.\n */\nexport async function dropOldReplicasAndSlots(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n beforeRank: bigint,\n): Promise<{dropped: number; active: number; draining: number}> {\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n const oldReplicas = await sql`\n SELECT id, rank::float8, slot, version, \"initialSyncContext\", \"subscriberContext\"\n FROM ${sql(replicasTable)} WHERE rank < ${beforeRank};\n `;\n if (oldReplicas.length) {\n lc.info?.(`Deleting ${oldReplicas.length} old replica(s)`, {oldReplicas});\n await sql`DELETE FROM ${sql(replicasTable)} WHERE rank < ${beforeRank}`;\n }\n\n return dropUnclaimedSlots(lc, sql, shard);\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 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 {\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 {runTx} from '../../../db/run-transaction.ts';\nimport {isPostgresError, type PostgresDB} from '../../../types/pg.ts';\nimport {upstreamSchema, type ShardID} from '../../../types/shards.ts';\nimport {orTimeout} from '../../../types/timeout.ts';\nimport {toStateVersionString} from './lsn.ts';\nimport {\n createReplica,\n replicationSlotExpression,\n replicationSlotPrefix,\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 CreateSlotSpec = {\n slotName: string;\n\n // Note: must be false if pgVersion < PG_17. Caller must verify.\n failover?: boolean;\n\n // For overriding in tests.\n lockTimeout?: number;\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.\nconst CREATE_REPLICATION_SLOT_TIMEOUT_MS = 30_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 {slotName, failover, lockTimeout = SERVER_LOCK_TIMEOUT_MS}: 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 createSlot = failover\n ? session.unsafe<ReplicationSlot[]>(\n /*sql*/ `CREATE_REPLICATION_SLOT \"${slotName}\" LOGICAL pgoutput (FAILOVER)`,\n )\n : session.unsafe<ReplicationSlot[]>(\n /*sql*/ `CREATE_REPLICATION_SLOT \"${slotName}\" LOGICAL pgoutput`,\n );\n const raced = await orTimeout(createSlot, CREATE_REPLICATION_SLOT_TIMEOUT_MS);\n if (raced === 'timed-out') {\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 `Crashing to force a clean restart.`,\n );\n }\n const [slot] = raced;\n lc.info?.(`Created replication slot ${slotName}`, slot);\n return slot;\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 */\nexport async function createReplicaAndSlot(\n lc: LogContext,\n sql: PostgresDB,\n replicationSession: postgres.Sql,\n shard: ShardID,\n replicaID: string,\n failover: boolean,\n): Promise<ReplicationSlot> {\n const lockName = replicationSlotManagementLock(shard);\n const slotPoolPrefix = replicationSlotPrefix(shard);\n for (let first = true; ; first = false) {\n await dropUnclaimedSlots(lc, sql, shard);\n try {\n return await runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n // Pick an available slotName from the slotPoolPrefix pool.\n let slotName: string;\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 });\n\n await createReplica(\n tx,\n shard,\n replicaID,\n slot.slot_name,\n toStateVersionString(slot.consistent_point),\n );\n\n return slot;\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 throw e;\n }\n }\n}\n\n/**\n * Deletes \"old\" replicas (i.e. those with a lower rank than the current)\n * and attempts to drop replication slots that are not associated with any\n * replica.\n *\n * If a slot could not be dropped because there is still an active subscriber,\n * it will be reflected in the `draining` count that is returned. When there\n * are draining slots, the method should be retried until all orphaned slots\n * have been dropped.\n */\nexport async function dropOldReplicasAndSlots(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n beforeRank: bigint,\n): Promise<{dropped: number; active: number; draining: number}> {\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n const oldReplicas = await sql`\n SELECT id, rank::float8, slot, version, \"initialSyncContext\", \"subscriberContext\"\n FROM ${sql(replicasTable)} WHERE rank < ${beforeRank};\n `;\n if (oldReplicas.length) {\n lc.info?.(`Deleting ${oldReplicas.length} old replica(s)`, {oldReplicas});\n await sql`DELETE FROM ${sql(replicasTable)} WHERE rank < ${beforeRank}`;\n }\n\n return dropUnclaimedSlots(lc, sql, shard);\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 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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"}
|