@rocicorp/zero 1.5.0-canary.4 → 1.6.0-canary.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/out/replicache/src/btree/node.d.ts +3 -0
  2. package/out/replicache/src/btree/node.d.ts.map +1 -1
  3. package/out/replicache/src/btree/node.js +114 -1
  4. package/out/replicache/src/btree/node.js.map +1 -1
  5. package/out/replicache/src/btree/write.d.ts +7 -0
  6. package/out/replicache/src/btree/write.d.ts.map +1 -1
  7. package/out/replicache/src/btree/write.js +50 -0
  8. package/out/replicache/src/btree/write.js.map +1 -1
  9. package/out/replicache/src/db/write.d.ts +8 -0
  10. package/out/replicache/src/db/write.d.ts.map +1 -1
  11. package/out/replicache/src/db/write.js +15 -0
  12. package/out/replicache/src/db/write.js.map +1 -1
  13. package/out/replicache/src/kv/sqlite-store.d.ts +2 -5
  14. package/out/replicache/src/kv/sqlite-store.d.ts.map +1 -1
  15. package/out/replicache/src/kv/sqlite-store.js +21 -24
  16. package/out/replicache/src/kv/sqlite-store.js.map +1 -1
  17. package/out/replicache/src/replicache-impl.d.ts.map +1 -1
  18. package/out/replicache/src/replicache-impl.js.map +1 -1
  19. package/out/replicache/src/sync/patch.d.ts +15 -0
  20. package/out/replicache/src/sync/patch.d.ts.map +1 -1
  21. package/out/replicache/src/sync/patch.js +85 -26
  22. package/out/replicache/src/sync/patch.js.map +1 -1
  23. package/out/shared/src/testing.d.ts +3 -0
  24. package/out/shared/src/testing.d.ts.map +1 -0
  25. package/out/zero/package.js +5 -6
  26. package/out/zero/package.js.map +1 -1
  27. package/out/zero-cache/src/auth/write-authorizer.js +1 -1
  28. package/out/zero-cache/src/config/zero-config.d.ts +4 -0
  29. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  30. package/out/zero-cache/src/config/zero-config.js +8 -0
  31. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  32. package/out/zero-cache/src/server/inspector-delegate.d.ts +3 -2
  33. package/out/zero-cache/src/server/inspector-delegate.d.ts.map +1 -1
  34. package/out/zero-cache/src/server/inspector-delegate.js +19 -9
  35. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  36. package/out/zero-cache/src/server/runner/run-worker.js +1 -1
  37. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +7 -6
  38. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  39. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  40. package/out/zero-cache/src/services/change-source/pg/change-source.js +62 -70
  41. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  42. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +0 -8
  43. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  44. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +30 -53
  45. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  46. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts +57 -0
  47. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts.map +1 -0
  48. package/out/zero-cache/src/services/change-source/pg/replication-slots.js +162 -0
  49. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -0
  50. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +1 -1
  51. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  52. package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
  53. package/out/zero-cache/src/services/change-source/pg/schema/init.js +19 -1
  54. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  55. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +17 -3
  56. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
  57. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +43 -16
  58. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  59. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +2 -3
  60. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  61. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +2 -2
  62. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  63. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +10 -1
  64. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
  65. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +13 -3
  66. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  67. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +6 -11
  68. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  69. package/out/zero-cache/src/services/change-streamer/change-streamer.js +0 -1
  70. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  71. package/out/zero-cache/src/services/change-streamer/forwarder.d.ts.map +1 -1
  72. package/out/zero-cache/src/services/change-streamer/forwarder.js +2 -2
  73. package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
  74. package/out/zero-cache/src/services/change-streamer/storer.d.ts +12 -5
  75. package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
  76. package/out/zero-cache/src/services/change-streamer/storer.js +43 -21
  77. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  78. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +4 -5
  79. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
  80. package/out/zero-cache/src/services/change-streamer/subscriber.js +18 -16
  81. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  82. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  83. package/out/zero-cache/src/services/litestream/commands.js +3 -2
  84. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  85. package/out/zero-cache/src/services/litestream/config.yml +1 -0
  86. package/out/zero-cache/src/services/mutagen/pusher.d.ts +2 -2
  87. package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js +7 -0
  88. package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
  89. package/out/zero-cache/src/services/view-syncer/cvr-store.js +2 -2
  90. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  91. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +1 -1
  92. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  93. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  94. package/out/zero-cache/src/services/view-syncer/view-syncer.js +5 -6
  95. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  96. package/out/zero-cache/src/types/streams.d.ts +4 -0
  97. package/out/zero-cache/src/types/streams.d.ts.map +1 -1
  98. package/out/zero-cache/src/types/streams.js +13 -10
  99. package/out/zero-cache/src/types/streams.js.map +1 -1
  100. package/out/zero-cache/src/workers/connection.js +3 -3
  101. package/out/zero-cache/src/workers/connection.js.map +1 -1
  102. package/out/zero-client/src/client/inspector/inspector.d.ts.map +1 -1
  103. package/out/zero-client/src/client/inspector/inspector.js +15 -2
  104. package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
  105. package/out/zero-client/src/client/inspector/lazy-inspector.d.ts +9 -3
  106. package/out/zero-client/src/client/inspector/lazy-inspector.d.ts.map +1 -1
  107. package/out/zero-client/src/client/inspector/lazy-inspector.js +27 -6
  108. package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
  109. package/out/zero-client/src/client/inspector/query.d.ts.map +1 -1
  110. package/out/zero-client/src/client/inspector/query.js +3 -3
  111. package/out/zero-client/src/client/inspector/query.js.map +1 -1
  112. package/out/zero-client/src/client/ivm-branch.d.ts.map +1 -1
  113. package/out/zero-client/src/client/ivm-branch.js +16 -2
  114. package/out/zero-client/src/client/ivm-branch.js.map +1 -1
  115. package/out/zero-client/src/client/options.d.ts +12 -4
  116. package/out/zero-client/src/client/options.d.ts.map +1 -1
  117. package/out/zero-client/src/client/options.js.map +1 -1
  118. package/out/zero-client/src/client/query-manager.d.ts +8 -1
  119. package/out/zero-client/src/client/query-manager.d.ts.map +1 -1
  120. package/out/zero-client/src/client/query-manager.js +28 -3
  121. package/out/zero-client/src/client/query-manager.js.map +1 -1
  122. package/out/zero-client/src/client/version.js +1 -1
  123. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  124. package/out/zero-client/src/client/zero.js +12 -11
  125. package/out/zero-client/src/client/zero.js.map +1 -1
  126. package/out/zero-protocol/src/down.d.ts +1 -1
  127. package/out/zero-protocol/src/inspect-down.d.ts +15 -4
  128. package/out/zero-protocol/src/inspect-down.d.ts.map +1 -1
  129. package/out/zero-protocol/src/inspect-down.js +11 -1
  130. package/out/zero-protocol/src/inspect-down.js.map +1 -1
  131. package/out/zero-protocol/src/protocol-version.d.ts +1 -1
  132. package/out/zero-protocol/src/protocol-version.d.ts.map +1 -1
  133. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  134. package/out/zero-react/src/use-query.d.ts.map +1 -1
  135. package/out/zero-react/src/use-query.js.map +1 -1
  136. package/out/zero-react/src/zero-provider.d.ts +6 -0
  137. package/out/zero-react/src/zero-provider.d.ts.map +1 -1
  138. package/out/zero-react/src/zero-provider.js +21 -1
  139. package/out/zero-react/src/zero-provider.js.map +1 -1
  140. package/out/zero-solid/src/use-zero.d.ts +6 -0
  141. package/out/zero-solid/src/use-zero.d.ts.map +1 -1
  142. package/out/zero-solid/src/use-zero.js +24 -4
  143. package/out/zero-solid/src/use-zero.js.map +1 -1
  144. package/out/zql/src/builder/builder.js +2 -2
  145. package/out/zql/src/ivm/constraint.d.ts.map +1 -1
  146. package/out/zql/src/ivm/constraint.js.map +1 -1
  147. package/out/zql/src/ivm/flipped-join.d.ts +9 -0
  148. package/out/zql/src/ivm/flipped-join.d.ts.map +1 -1
  149. package/out/zql/src/ivm/flipped-join.js +56 -69
  150. package/out/zql/src/ivm/flipped-join.js.map +1 -1
  151. package/out/zql/src/ivm/memory-source.d.ts +24 -3
  152. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  153. package/out/zql/src/ivm/memory-source.js +162 -7
  154. package/out/zql/src/ivm/memory-source.js.map +1 -1
  155. package/out/zql/src/ivm/operator.d.ts +26 -0
  156. package/out/zql/src/ivm/operator.d.ts.map +1 -1
  157. package/out/zql/src/ivm/operator.js.map +1 -1
  158. package/out/zqlite/src/query-builder.d.ts +14 -2
  159. package/out/zqlite/src/query-builder.d.ts.map +1 -1
  160. package/out/zqlite/src/query-builder.js +32 -1
  161. package/out/zqlite/src/query-builder.js.map +1 -1
  162. package/out/zqlite/src/table-source.d.ts.map +1 -1
  163. package/out/zqlite/src/table-source.js +4 -4
  164. package/out/zqlite/src/table-source.js.map +1 -1
  165. package/package.json +5 -6
@@ -1 +1 @@
1
- {"version":3,"file":"backfill-stream.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-source/pg/backfill-stream.ts"],"sourcesContent":["import {\n PG_UNDEFINED_COLUMN,\n PG_UNDEFINED_TABLE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport postgres from 'postgres';\nimport {assert} from '../../../../../shared/src/asserts.ts';\nimport {equals} from '../../../../../shared/src/set-utils.ts';\nimport * as v from '../../../../../shared/src/valita.ts';\nimport {READONLY} 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 {getTypeParsers} from '../../../db/pg-type-parser.ts';\nimport type {PublishedTableSpec} from '../../../db/specs.ts';\nimport {importSnapshot, TransactionPool} from '../../../db/transaction-pool.ts';\nimport {pgClient, type PostgresDB} from '../../../types/pg.ts';\nimport {SchemaIncompatibilityError} from '../common/backfill-manager.ts';\nimport type {\n BackfillCompleted,\n BackfillRequest,\n DownloadStatus,\n JSONValue,\n MessageBackfill,\n} from '../protocol/current.ts';\nimport {\n columnMetadataSchema,\n tableMetadataSchema,\n} from './backfill-metadata.ts';\nimport {\n createReplicationSlot,\n makeBinarySelectExprs,\n makeDownloadStatements,\n type DownloadStatements,\n} from './initial-sync.ts';\nimport {toStateVersionString} from './lsn.ts';\nimport {getPublicationInfo} from './schema/published.ts';\nimport type {Replica} from './schema/shard.ts';\n\ntype BackfillParams = Omit<BackfillCompleted, 'tag'>;\n\ntype StreamOptions = {\n /**\n * The number of bytes at which to flush a batch of rows in a\n * backfill message. Defaults to Node's getDefaultHighWatermark().\n */\n flushThresholdBytes?: number | undefined;\n\n /**\n * Use text-format COPY instead of binary COPY.\n * Binary is faster and handles all types (unknown types are cast to\n * `::text` in the SELECT). This flag exists as an escape hatch to\n * revert to the old code path if needed.\n */\n textCopy?: boolean | undefined;\n};\n\n// The size of chunks that Postgres sends on COPY stream.\n// This happens to match NodeJS's getDefaultHighWatermark()\n// (for Node v20+).\nconst POSTGRES_COPY_CHUNK_SIZE = 64 * 1024;\n\n// Matches the exact clauses emitted by makeDownloadStatements; quoted\n// identifiers like \"limit\" won't match because they lack the surrounding\n// whitespace.\nconst SAMPLE_OR_LIMIT_RE = /\\sTABLESAMPLE\\s+BERNOULLI\\b|\\sLIMIT\\s+\\d/i;\n\n/**\n * Streams a series of `backfill` messages (ending with `backfill-complete`)\n * at a set watermark (i.e. LSN). The data is retrieved via a COPY stream\n * made at a transaction snapshot corresponding to specific LSN, obtained by\n * creating a short-lived replication slot.\n */\nexport async function* streamBackfill(\n lc: LogContext,\n upstreamURI: string,\n {slot, publications}: Pick<Replica, 'slot' | 'publications'>,\n bf: BackfillRequest,\n opts: StreamOptions = {},\n): AsyncGenerator<MessageBackfill | BackfillCompleted> {\n lc = lc\n .withContext('component', 'backfill')\n .withContext('table', bf.table.name);\n\n const {flushThresholdBytes = POSTGRES_COPY_CHUNK_SIZE, textCopy = false} =\n opts;\n const db = pgClient(lc, upstreamURI, 'backfill-stream', {\n ['max_lifetime']: 120 * 60, // set a long (2h) limit for COPY streaming\n });\n let tx: TransactionPool | undefined;\n let watermark: string;\n try {\n ({tx, watermark} = await createSnapshotTransaction(\n lc,\n upstreamURI,\n db,\n slot,\n ));\n const {tableSpec, backfill} = await validateSchema(\n tx,\n publications,\n bf,\n watermark,\n );\n\n // Note: validateSchema ensures that the rowKey and columns are disjoint\n const {relation, columns} = backfill;\n const cols = [...relation.rowKey.columns, ...columns];\n const stmts = makeDownloadStatements(tableSpec, cols);\n\n if (textCopy) {\n const types = await getTypeParsers(db, {returnJsonAsString: true});\n yield* stream(\n lc,\n tx,\n backfill,\n stmts,\n `COPY (${stmts.select}) TO STDOUT`,\n new TsvParser(),\n cols.map(col => {\n const parser = types.getTypeParser(tableSpec.columns[col].typeOID);\n return (text: string) => parser(text) as JSONValue;\n }),\n flushThresholdBytes,\n );\n } else {\n const binaryStmts = makeDownloadStatements(\n tableSpec,\n cols,\n undefined,\n undefined,\n makeBinarySelectExprs(tableSpec, cols),\n );\n\n yield* stream(\n lc,\n tx,\n backfill,\n stmts,\n `COPY (${binaryStmts.select}) TO STDOUT WITH (FORMAT binary)`,\n new BinaryCopyParser(),\n cols.map(col => {\n const spec = tableSpec.columns[col];\n const decoder = hasBinaryDecoder(spec)\n ? makeBinaryDecoder(spec)\n : textCastDecoder;\n return (buf: Buffer) => decoder(buf) as unknown as JSONValue;\n }),\n flushThresholdBytes,\n );\n }\n } catch (e) {\n // Although we make the best effort to validate the schema at the\n // transaction snapshot, certain forms of `ALTER TABLE` are not\n // MVCC safe and not \"frozen\" in the snapshot:\n //\n // https://www.postgresql.org/docs/current/mvcc-caveats.html\n //\n // Handle these errors as schema incompatibility errors rather than\n // unknown runtime errors.\n if (\n e instanceof postgres.PostgresError &&\n (e.code === PG_UNDEFINED_TABLE || e.code === PG_UNDEFINED_COLUMN)\n ) {\n throw new SchemaIncompatibilityError(bf, String(e), {cause: e});\n }\n throw e;\n } finally {\n tx?.setDone();\n // Workaround postgres.js hanging at the end of some COPY commands:\n // https://github.com/porsager/postgres/issues/499\n void db.end().catch(e => lc.warn?.(`error closing backfill connection`, e));\n }\n}\n\nasync function* stream<T>(\n lc: LogContext,\n tx: TransactionPool,\n backfill: BackfillParams,\n {\n getTotalRows,\n getTotalBytes,\n }: Pick<DownloadStatements, 'getTotalRows' | 'getTotalBytes'>,\n copyCommand: string,\n parser: {parse(chunk: Buffer): Iterable<T | null>},\n decoders: ((field: T) => JSONValue)[],\n flushThresholdBytes: number,\n): AsyncGenerator<MessageBackfill | BackfillCompleted> {\n // Backfill must read every row: TABLESAMPLE / LIMIT are reserved for shadow\n // sync and must never appear in a backfill COPY.\n assert(\n !SAMPLE_OR_LIMIT_RE.test(copyCommand),\n `backfill COPY must not sample or limit: ${copyCommand}`,\n );\n const start = performance.now();\n const [rows, bytes] = await tx.processReadTask(sql =>\n Promise.all([\n sql.unsafe<{totalRows: bigint}[]>(getTotalRows),\n sql.unsafe<{totalBytes: bigint}[]>(getTotalBytes),\n ]),\n );\n const status: DownloadStatus = {\n rows: 0,\n totalRows: Number(rows[0].totalRows),\n totalBytes: Number(bytes[0].totalBytes),\n };\n\n let elapsed = (performance.now() - start).toFixed(3);\n lc.info?.(\n `Computed total rows and bytes for: ${copyCommand} (${elapsed} ms)`,\n {\n status,\n },\n );\n const copyStream = await tx.processReadTask(sql =>\n sql.unsafe(copyCommand).readable(),\n );\n\n let totalBytes = 0;\n let totalMsgs = 0;\n let rowValues: JSONValue[][] = [];\n let bufferedBytes = 0;\n\n const logFlushed = () => {\n lc.debug?.(\n `Flushed ${rowValues.length} rows, ${bufferedBytes} bytes ` +\n `(total: rows=${status.rows}, msgs=${totalMsgs}, bytes=${totalBytes})`,\n );\n };\n\n // Tracks the row being parsed.\n let row: JSONValue[] = Array.from({length: decoders.length});\n let col = 0;\n\n for await (const data of copyStream) {\n const chunk = data as Buffer;\n for (const field of parser.parse(chunk)) {\n row[col] = field === null ? null : decoders[col](field);\n\n if (++col === decoders.length) {\n rowValues.push(row);\n status.rows++;\n row = Array.from({length: decoders.length});\n col = 0;\n }\n }\n bufferedBytes += chunk.byteLength;\n totalBytes += chunk.byteLength;\n\n if (bufferedBytes >= flushThresholdBytes) {\n yield {tag: 'backfill', ...backfill, rowValues, status};\n totalMsgs++;\n logFlushed();\n rowValues = [];\n bufferedBytes = 0;\n }\n }\n\n // Flush the last batch of rows.\n if (rowValues.length > 0) {\n yield {tag: 'backfill', ...backfill, rowValues, status};\n totalMsgs++;\n logFlushed();\n }\n\n yield {tag: 'backfill-completed', ...backfill, status};\n elapsed = (performance.now() - start).toFixed(3);\n lc.info?.(\n `Finished streaming ${status.rows} rows, ${totalMsgs} msgs, ${totalBytes} bytes ` +\n `(${elapsed} ms)`,\n );\n}\n\n/**\n * Creates (and drops) a replication slot in order to obtain a snapshot\n * that corresponds with a specific LSN. Sets the snapshot on the\n * TransactionPool and returns the watermark corresponding to the LSN.\n *\n * (Note that PG's other LSN-related functions are not scoped to a\n * transaction; this is the only way to get set a transaction at a specific\n * LSN.)\n */\nasync function createSnapshotTransaction(\n lc: LogContext,\n upstreamURI: string,\n db: PostgresDB,\n slotNamePrefix: string,\n) {\n const replicationSession = pgClient(\n lc,\n upstreamURI,\n 'backfill-replication-session',\n {\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 tempSlot = `${slotNamePrefix}_bf_${Date.now()}`;\n try {\n const {snapshot_name: snapshot, consistent_point: lsn} =\n await createReplicationSlot(lc, replicationSession, tempSlot);\n\n const {init, imported} = importSnapshot(snapshot);\n const tx = new TransactionPool(lc, {mode: READONLY, init}).run(db);\n await imported;\n await replicationSession.unsafe(`DROP_REPLICATION_SLOT \"${tempSlot}\"`);\n\n const watermark = toStateVersionString(lsn);\n lc.info?.(`Opened snapshot transaction at LSN ${lsn} (${watermark})`);\n return {tx, watermark};\n } catch (e) {\n // In the event of a failure, clean up the replication slot if created.\n await replicationSession.unsafe(\n /*sql*/\n `SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots\n WHERE slot_name = '${tempSlot}'`,\n );\n lc.error?.(`Failed to create backfill snapshot`, e);\n throw e;\n } finally {\n await replicationSession.end();\n }\n}\n\nfunction validateSchema(\n tx: TransactionPool,\n publications: string[],\n bf: BackfillRequest,\n watermark: string,\n): Promise<{\n tableSpec: PublishedTableSpec;\n backfill: BackfillParams;\n}> {\n return tx.processReadTask(async sql => {\n const {tables} = await getPublicationInfo(sql, publications);\n const spec = tables.find(\n spec => spec.schema === bf.table.schema && spec.name === bf.table.name,\n );\n if (!spec) {\n throw new SchemaIncompatibilityError(\n bf,\n `Table has been renamed or dropped`,\n );\n }\n const tableMeta = v.parse(bf.table.metadata, tableMetadataSchema);\n if (spec.schemaOID !== tableMeta.schemaOID) {\n throw new SchemaIncompatibilityError(\n bf,\n `Schema no longer corresponds to the original schema`,\n );\n }\n if (spec.oid !== tableMeta.relationOID) {\n throw new SchemaIncompatibilityError(\n bf,\n `Table no longer corresponds to the original table`,\n );\n }\n if (\n !equals(\n new Set(Object.keys(tableMeta.rowKey)),\n new Set(spec.replicaIdentityColumns),\n )\n ) {\n throw new SchemaIncompatibilityError(\n bf,\n 'Row key (e.g. PRIMARY KEY or INDEX) has changed',\n );\n }\n const allCols = [\n ...Object.entries(tableMeta.rowKey),\n ...Object.entries(bf.columns),\n ];\n for (const [col, val] of allCols) {\n const colSpec = spec.columns[col];\n if (!colSpec) {\n throw new SchemaIncompatibilityError(\n bf,\n `Column ${col} has been renamed or dropped`,\n );\n }\n const colMeta = v.parse(val, columnMetadataSchema);\n if (colMeta.attNum !== colSpec.pos) {\n throw new SchemaIncompatibilityError(\n bf,\n `Column ${col} no longer corresponds to the original column`,\n );\n }\n }\n const backfill: BackfillParams = {\n relation: {\n schema: bf.table.schema,\n name: bf.table.name,\n rowKey: {columns: Object.keys(tableMeta.rowKey)},\n },\n columns: Object.keys(bf.columns).filter(\n col => !(col in tableMeta.rowKey),\n ),\n watermark,\n };\n return {tableSpec: spec, backfill};\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"backfill-stream.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-source/pg/backfill-stream.ts"],"sourcesContent":["import {\n PG_UNDEFINED_COLUMN,\n PG_UNDEFINED_TABLE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport postgres from 'postgres';\nimport {assert} from '../../../../../shared/src/asserts.ts';\nimport {equals} from '../../../../../shared/src/set-utils.ts';\nimport * as v from '../../../../../shared/src/valita.ts';\nimport {READONLY} 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 {getTypeParsers} from '../../../db/pg-type-parser.ts';\nimport type {PublishedTableSpec} from '../../../db/specs.ts';\nimport {importSnapshot, TransactionPool} from '../../../db/transaction-pool.ts';\nimport {pgClient, type PostgresDB} from '../../../types/pg.ts';\nimport {SchemaIncompatibilityError} from '../common/backfill-manager.ts';\nimport type {\n BackfillCompleted,\n BackfillRequest,\n DownloadStatus,\n JSONValue,\n MessageBackfill,\n} from '../protocol/current.ts';\nimport {\n columnMetadataSchema,\n tableMetadataSchema,\n} from './backfill-metadata.ts';\nimport {\n makeBinarySelectExprs,\n makeDownloadStatements,\n type DownloadStatements,\n} from './initial-sync.ts';\nimport {toStateVersionString} from './lsn.ts';\nimport {createReplicationSlot} from './replication-slots.ts';\nimport {getPublicationInfo} from './schema/published.ts';\nimport type {Replica} from './schema/shard.ts';\n\ntype BackfillParams = Omit<BackfillCompleted, 'tag'>;\n\ntype StreamOptions = {\n /**\n * The number of bytes at which to flush a batch of rows in a\n * backfill message. Defaults to Node's getDefaultHighWatermark().\n */\n flushThresholdBytes?: number | undefined;\n\n /**\n * Use text-format COPY instead of binary COPY.\n * Binary is faster and handles all types (unknown types are cast to\n * `::text` in the SELECT). This flag exists as an escape hatch to\n * revert to the old code path if needed.\n */\n textCopy?: boolean | undefined;\n};\n\n// The size of chunks that Postgres sends on COPY stream.\n// This happens to match NodeJS's getDefaultHighWatermark()\n// (for Node v20+).\nconst POSTGRES_COPY_CHUNK_SIZE = 64 * 1024;\n\n// Matches the exact clauses emitted by makeDownloadStatements; quoted\n// identifiers like \"limit\" won't match because they lack the surrounding\n// whitespace.\nconst SAMPLE_OR_LIMIT_RE = /\\sTABLESAMPLE\\s+BERNOULLI\\b|\\sLIMIT\\s+\\d/i;\n\n/**\n * Streams a series of `backfill` messages (ending with `backfill-complete`)\n * at a set watermark (i.e. LSN). The data is retrieved via a COPY stream\n * made at a transaction snapshot corresponding to specific LSN, obtained by\n * creating a short-lived replication slot.\n */\nexport async function* streamBackfill(\n lc: LogContext,\n upstreamURI: string,\n {slot, publications}: Pick<Replica, 'slot' | 'publications'>,\n bf: BackfillRequest,\n opts: StreamOptions = {},\n): AsyncGenerator<MessageBackfill | BackfillCompleted> {\n lc = lc\n .withContext('component', 'backfill')\n .withContext('table', bf.table.name);\n\n const {flushThresholdBytes = POSTGRES_COPY_CHUNK_SIZE, textCopy = false} =\n opts;\n const db = pgClient(lc, upstreamURI, 'backfill-stream', {\n ['max_lifetime']: 120 * 60, // set a long (2h) limit for COPY streaming\n });\n let tx: TransactionPool | undefined;\n let watermark: string;\n try {\n ({tx, watermark} = await createSnapshotTransaction(\n lc,\n upstreamURI,\n db,\n slot,\n ));\n const {tableSpec, backfill} = await validateSchema(\n tx,\n publications,\n bf,\n watermark,\n );\n\n // Note: validateSchema ensures that the rowKey and columns are disjoint\n const {relation, columns} = backfill;\n const cols = [...relation.rowKey.columns, ...columns];\n const stmts = makeDownloadStatements(tableSpec, cols);\n\n if (textCopy) {\n const types = await getTypeParsers(db, {returnJsonAsString: true});\n yield* stream(\n lc,\n tx,\n backfill,\n stmts,\n `COPY (${stmts.select}) TO STDOUT`,\n new TsvParser(),\n cols.map(col => {\n const parser = types.getTypeParser(tableSpec.columns[col].typeOID);\n return (text: string) => parser(text) as JSONValue;\n }),\n flushThresholdBytes,\n );\n } else {\n const binaryStmts = makeDownloadStatements(\n tableSpec,\n cols,\n undefined,\n undefined,\n makeBinarySelectExprs(tableSpec, cols),\n );\n\n yield* stream(\n lc,\n tx,\n backfill,\n stmts,\n `COPY (${binaryStmts.select}) TO STDOUT WITH (FORMAT binary)`,\n new BinaryCopyParser(),\n cols.map(col => {\n const spec = tableSpec.columns[col];\n const decoder = hasBinaryDecoder(spec)\n ? makeBinaryDecoder(spec)\n : textCastDecoder;\n return (buf: Buffer) => decoder(buf) as unknown as JSONValue;\n }),\n flushThresholdBytes,\n );\n }\n } catch (e) {\n // Although we make the best effort to validate the schema at the\n // transaction snapshot, certain forms of `ALTER TABLE` are not\n // MVCC safe and not \"frozen\" in the snapshot:\n //\n // https://www.postgresql.org/docs/current/mvcc-caveats.html\n //\n // Handle these errors as schema incompatibility errors rather than\n // unknown runtime errors.\n if (\n e instanceof postgres.PostgresError &&\n (e.code === PG_UNDEFINED_TABLE || e.code === PG_UNDEFINED_COLUMN)\n ) {\n throw new SchemaIncompatibilityError(bf, String(e), {cause: e});\n }\n throw e;\n } finally {\n tx?.setDone();\n // Workaround postgres.js hanging at the end of some COPY commands:\n // https://github.com/porsager/postgres/issues/499\n void db.end().catch(e => lc.warn?.(`error closing backfill connection`, e));\n }\n}\n\nasync function* stream<T>(\n lc: LogContext,\n tx: TransactionPool,\n backfill: BackfillParams,\n {\n getTotalRows,\n getTotalBytes,\n }: Pick<DownloadStatements, 'getTotalRows' | 'getTotalBytes'>,\n copyCommand: string,\n parser: {parse(chunk: Buffer): Iterable<T | null>},\n decoders: ((field: T) => JSONValue)[],\n flushThresholdBytes: number,\n): AsyncGenerator<MessageBackfill | BackfillCompleted> {\n // Backfill must read every row: TABLESAMPLE / LIMIT are reserved for shadow\n // sync and must never appear in a backfill COPY.\n assert(\n !SAMPLE_OR_LIMIT_RE.test(copyCommand),\n `backfill COPY must not sample or limit: ${copyCommand}`,\n );\n const start = performance.now();\n const [rows, bytes] = await tx.processReadTask(sql =>\n Promise.all([\n sql.unsafe<{totalRows: bigint}[]>(getTotalRows),\n sql.unsafe<{totalBytes: bigint}[]>(getTotalBytes),\n ]),\n );\n const status: DownloadStatus = {\n rows: 0,\n totalRows: Number(rows[0].totalRows),\n totalBytes: Number(bytes[0].totalBytes),\n };\n\n let elapsed = (performance.now() - start).toFixed(3);\n lc.info?.(\n `Computed total rows and bytes for: ${copyCommand} (${elapsed} ms)`,\n {\n status,\n },\n );\n const copyStream = await tx.processReadTask(sql =>\n sql.unsafe(copyCommand).readable(),\n );\n\n let totalBytes = 0;\n let totalMsgs = 0;\n let rowValues: JSONValue[][] = [];\n let bufferedBytes = 0;\n\n const logFlushed = () => {\n lc.debug?.(\n `Flushed ${rowValues.length} rows, ${bufferedBytes} bytes ` +\n `(total: rows=${status.rows}, msgs=${totalMsgs}, bytes=${totalBytes})`,\n );\n };\n\n // Tracks the row being parsed.\n let row: JSONValue[] = Array.from({length: decoders.length});\n let col = 0;\n\n for await (const data of copyStream) {\n const chunk = data as Buffer;\n for (const field of parser.parse(chunk)) {\n row[col] = field === null ? null : decoders[col](field);\n\n if (++col === decoders.length) {\n rowValues.push(row);\n status.rows++;\n row = Array.from({length: decoders.length});\n col = 0;\n }\n }\n bufferedBytes += chunk.byteLength;\n totalBytes += chunk.byteLength;\n\n if (bufferedBytes >= flushThresholdBytes) {\n yield {tag: 'backfill', ...backfill, rowValues, status};\n totalMsgs++;\n logFlushed();\n rowValues = [];\n bufferedBytes = 0;\n }\n }\n\n // Flush the last batch of rows.\n if (rowValues.length > 0) {\n yield {tag: 'backfill', ...backfill, rowValues, status};\n totalMsgs++;\n logFlushed();\n }\n\n yield {tag: 'backfill-completed', ...backfill, status};\n elapsed = (performance.now() - start).toFixed(3);\n lc.info?.(\n `Finished streaming ${status.rows} rows, ${totalMsgs} msgs, ${totalBytes} bytes ` +\n `(${elapsed} ms)`,\n );\n}\n\n/**\n * Creates (and drops) a replication slot in order to obtain a snapshot\n * that corresponds with a specific LSN. Sets the snapshot on the\n * TransactionPool and returns the watermark corresponding to the LSN.\n *\n * (Note that PG's other LSN-related functions are not scoped to a\n * transaction; this is the only way to get set a transaction at a specific\n * LSN.)\n */\nasync function createSnapshotTransaction(\n lc: LogContext,\n upstreamURI: string,\n db: PostgresDB,\n slotNamePrefix: string,\n) {\n const replicationSession = pgClient(\n lc,\n upstreamURI,\n 'backfill-replication-session',\n {\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 slotName = `${slotNamePrefix}_bf_${Date.now()}`;\n try {\n const {snapshot_name: snapshot, consistent_point: lsn} =\n await createReplicationSlot(lc, replicationSession, {slotName});\n\n const {init, imported} = importSnapshot(snapshot);\n const tx = new TransactionPool(lc, {mode: READONLY, init}).run(db);\n await imported;\n await replicationSession.unsafe(`DROP_REPLICATION_SLOT \"${slotName}\"`);\n\n const watermark = toStateVersionString(lsn);\n lc.info?.(`Opened snapshot transaction at LSN ${lsn} (${watermark})`);\n return {tx, watermark};\n } catch (e) {\n // In the event of a failure, clean up the replication slot if created.\n await replicationSession.unsafe(\n /*sql*/\n `SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots\n WHERE slot_name = '${slotName}'`,\n );\n lc.warn?.(`Failed to create backfill snapshot`, e);\n throw e;\n } finally {\n await replicationSession.end();\n }\n}\n\nfunction validateSchema(\n tx: TransactionPool,\n publications: string[],\n bf: BackfillRequest,\n watermark: string,\n): Promise<{\n tableSpec: PublishedTableSpec;\n backfill: BackfillParams;\n}> {\n return tx.processReadTask(async sql => {\n const {tables} = await getPublicationInfo(sql, publications);\n const spec = tables.find(\n spec => spec.schema === bf.table.schema && spec.name === bf.table.name,\n );\n if (!spec) {\n throw new SchemaIncompatibilityError(\n bf,\n `Table has been renamed or dropped`,\n );\n }\n const tableMeta = v.parse(bf.table.metadata, tableMetadataSchema);\n if (spec.schemaOID !== tableMeta.schemaOID) {\n throw new SchemaIncompatibilityError(\n bf,\n `Schema no longer corresponds to the original schema`,\n );\n }\n if (spec.oid !== tableMeta.relationOID) {\n throw new SchemaIncompatibilityError(\n bf,\n `Table no longer corresponds to the original table`,\n );\n }\n if (\n !equals(\n new Set(Object.keys(tableMeta.rowKey)),\n new Set(spec.replicaIdentityColumns),\n )\n ) {\n throw new SchemaIncompatibilityError(\n bf,\n 'Row key (e.g. PRIMARY KEY or INDEX) has changed',\n );\n }\n const allCols = [\n ...Object.entries(tableMeta.rowKey),\n ...Object.entries(bf.columns),\n ];\n for (const [col, val] of allCols) {\n const colSpec = spec.columns[col];\n if (!colSpec) {\n throw new SchemaIncompatibilityError(\n bf,\n `Column ${col} has been renamed or dropped`,\n );\n }\n const colMeta = v.parse(val, columnMetadataSchema);\n if (colMeta.attNum !== colSpec.pos) {\n throw new SchemaIncompatibilityError(\n bf,\n `Column ${col} no longer corresponds to the original column`,\n );\n }\n }\n const backfill: BackfillParams = {\n relation: {\n schema: bf.table.schema,\n name: bf.table.name,\n rowKey: {columns: Object.keys(tableMeta.rowKey)},\n },\n columns: Object.keys(bf.columns).filter(\n col => !(col in tableMeta.rowKey),\n ),\n watermark,\n };\n return {tableSpec: spec, backfill};\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"change-source.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-source/pg/change-source.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAejD,OAAO,KAAK,CAAC,MAAM,qCAAqC,CAAC;AAOzD,OAAO,KAAK,EAGV,kBAAkB,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAC,KAAK,WAAW,EAAC,MAAM,gCAAgC,CAAC;AAGhE,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,0BAA0B,CAAC;AAKlC,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,2BAA2B,CAAC;AAEpD,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAC,YAAY,EAAe,MAAM,qBAAqB,CAAC;AAEpE,OAAO,EAEL,KAAK,QAAQ,EACd,MAAM,wCAAwC,CAAC;AAchD,OAAO,KAAK,EAEV,mBAAmB,EAEpB,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAGV,eAAe,IAAI,gBAAgB,EACpC,MAAM,yCAAyC,CAAC;AAuBjD;;;;GAIG;AACH,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,UAAU,EACd,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,kBAAkB,EAC/B,OAAO,EAAE,aAAa,EACtB,mBAAmB,SAAI,GACtB,OAAO,CAAC;IAAC,iBAAiB,EAAE,iBAAiB,CAAC;IAAC,YAAY,EAAE,YAAY,CAAA;CAAC,CAAC,CAuC7E;AA4cD,qBAAa,KAAM,YAAW,QAAQ;;gBAIxB,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;IAI9B,QAAQ,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI;IAgC3C,GAAG,CAAC,SAAS,EAAE,WAAW;CAoB3B;AAED,QAAA,MAAM,eAAe;;;;aAInB,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAqzBxD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,kBAAkB,EAAE,CAAC,EAAE,gBAAgB,WAwB3E"}
1
+ {"version":3,"file":"change-source.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/change-source/pg/change-source.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAcjD,OAAO,KAAK,CAAC,MAAM,qCAAqC,CAAC;AAMzD,OAAO,KAAK,EAGV,kBAAkB,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAC,KAAK,WAAW,EAAC,MAAM,gCAAgC,CAAC;AAGhE,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,0BAA0B,CAAC;AAKlC,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,2BAA2B,CAAC;AAEpD,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAC,YAAY,EAAe,MAAM,qBAAqB,CAAC;AAEpE,OAAO,EAEL,KAAK,QAAQ,EACd,MAAM,wCAAwC,CAAC;AAchD,OAAO,KAAK,EAEV,mBAAmB,EAEpB,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAGV,eAAe,IAAI,gBAAgB,EACpC,MAAM,yCAAyC,CAAC;AAyBjD;;;;GAIG;AACH,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,UAAU,EACd,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,kBAAkB,EAC/B,OAAO,EAAE,aAAa,EACtB,mBAAmB,SAAI,GACtB,OAAO,CAAC;IAAC,iBAAiB,EAAE,iBAAiB,CAAC;IAAC,YAAY,EAAE,YAAY,CAAA;CAAC,CAAC,CAuC7E;AA+ZD,qBAAa,KAAM,YAAW,QAAQ;;gBAIxB,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC;IAI9B,QAAQ,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI;IAgC3C,GAAG,CAAC,SAAS,EAAE,WAAW;CAoB3B;AAED,QAAA,MAAM,eAAe;;;;aAInB,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAmzBxD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,kBAAkB,EAAE,CAAC,EAAE,gBAAgB,WAwB3E"}
@@ -1,7 +1,6 @@
1
1
  import { assert } from "../../../../../shared/src/asserts.js";
2
2
  import { deepEqual } from "../../../../../shared/src/json.js";
3
3
  import { AbortError } from "../../../../../shared/src/abort-error.js";
4
- import { sleep } from "../../../../../shared/src/sleep.js";
5
4
  import { parse, valita_exports } from "../../../../../shared/src/valita.js";
6
5
  import { must } from "../../../../../shared/src/must.js";
7
6
  import { mapValues } from "../../../../../shared/src/objects.js";
@@ -15,24 +14,25 @@ import { StatementRunner } from "../../../db/statements.js";
15
14
  import { isPostgresError, pgClient } from "../../../types/pg.js";
16
15
  import { majorVersionFromString, majorVersionToString } from "../../../types/state-version.js";
17
16
  import { fromBigInt, toBigInt, toStateVersionString } from "./lsn.js";
18
- import { runTx } from "../../../db/run-transaction.js";
19
17
  import { getPublicationInfo } from "./schema/published.js";
20
18
  import { replicationEventSchema } from "./schema/ddl.js";
21
19
  import { validate } from "./schema/validation.js";
22
- import { dropShard, getInternalShardConfig, getReplicaAtVersion, internalPublicationPrefix, legacyReplicationSlot, replicaIdentitiesForTablesWithoutPrimaryKeys, replicationSlotExpression } from "./schema/shard.js";
20
+ import { dropShard, getInternalShardConfig, getReplicaAtVersion, internalPublicationPrefix, replicaIdentitiesForTablesWithoutPrimaryKeys, replicationSlotPrefix } from "./schema/shard.js";
23
21
  import { AutoResetSignal } from "../../change-streamer/schema/tables.js";
24
22
  import { initReplica } from "../common/replica-schema.js";
25
23
  import "../../../types/pg-versions.js";
26
24
  import { BackfillManager } from "../common/backfill-manager.js";
27
25
  import { ChangeStreamMultiplexer } from "../common/change-stream-multiplexer.js";
26
+ import { dropOldReplicasAndSlots } from "./replication-slots.js";
28
27
  import { updateShardSchema } from "./schema/init.js";
29
28
  import { initialSync } from "./initial-sync.js";
30
29
  import { streamBackfill } from "./backfill-stream.js";
31
30
  import { subscribe } from "./logical-replication/stream.js";
32
31
  import { nanoid } from "nanoid";
33
32
  import postgres from "postgres";
34
- import { PG_ADMIN_SHUTDOWN, PG_INSUFFICIENT_PRIVILEGE, PG_OBJECT_IN_USE } from "@drdgvhbh/postgres-error-codes";
33
+ import { PG_ADMIN_SHUTDOWN, PG_INSUFFICIENT_PRIVILEGE } from "@drdgvhbh/postgres-error-codes";
35
34
  //#region ../zero-cache/src/services/change-source/pg/change-source.ts
35
+ var REPLICA_SLOT_CLEANUP_INTERVAL_MS = 3e4;
36
36
  /**
37
37
  * Initializes a Postgres change source, including the initial sync of the
38
38
  * replica, before streaming changes from the corresponding logical replication
@@ -107,6 +107,7 @@ var PostgresChangeSource = class {
107
107
  }
108
108
  async stop() {
109
109
  this.#lagReporter?.stop();
110
+ clearTimeout(this.#cleanupTimer);
110
111
  await this.#db.end();
111
112
  }
112
113
  async startLagReporter() {
@@ -121,9 +122,9 @@ var PostgresChangeSource = class {
121
122
  return null;
122
123
  }
123
124
  async startStream(clientWatermark, backfillRequests = []) {
124
- const { slot } = this.#replica;
125
- await this.#stopExistingReplicationSlotSubscribers(slot);
125
+ await this.#stopExistingReplicationSlotSubscriber();
126
126
  const config = await getInternalShardConfig(this.#db, this.#shard);
127
+ const { slot } = this.#replica;
127
128
  this.#lc.info?.(`starting replication stream@${slot}`);
128
129
  return this.#startStream(slot, clientWatermark, config, backfillRequests);
129
130
  }
@@ -214,73 +215,55 @@ var PostgresChangeSource = class {
214
215
  }
215
216
  }
216
217
  /**
217
- * Stops replication slots associated with this shard, and returns
218
- * a `cleanup` task that drops any slot other than the specified
219
- * `slotToKeep`.
220
- *
221
- * Note that replication slots created after `slotToKeep` (as indicated by
222
- * the timestamp suffix) are preserved, as those are newly syncing replicas
223
- * that will soon take over the slot.
218
+ * Stops replication slots associated with this shard, and asynchronously
219
+ * runs a cleanup task that drops older replicas and slots.
224
220
  */
225
- async #stopExistingReplicationSlotSubscribers(slotToKeep) {
226
- const slotExpression = replicationSlotExpression(this.#shard);
227
- const legacySlotName = legacyReplicationSlot(this.#shard);
228
- const result = await runTx(this.#db, async (sql) => {
229
- const result = await sql`
230
- SELECT slot_name as slot, pg_terminate_backend(active_pid) as terminated, active_pid as pid
231
- FROM pg_replication_slots
232
- WHERE (slot_name LIKE ${slotExpression} OR slot_name = ${legacySlotName})
233
- AND slot_name <= ${slotToKeep}`;
234
- this.#lc.info?.(`terminated replication slots: ${JSON.stringify(result)}`);
235
- const replicasTable = `${upstreamSchema(this.#shard)}.replicas`;
236
- const replicasBefore = await sql`
237
- SELECT slot, version, "initialSyncContext", "subscriberContext"
238
- FROM ${sql(replicasTable)} ORDER BY slot`;
239
- if (result.length === 0) {
240
- const shardSlots = await sql`
221
+ async #stopExistingReplicationSlotSubscriber() {
222
+ const sql = this.#db;
223
+ const { id: replicaID, slot } = this.#replica;
224
+ const replicasTable = `${upstreamSchema(this.#shard)}.replicas`;
225
+ const result = await sql`
226
+ SELECT pg_terminate_backend(active_pid) as terminated, active_pid as pid
227
+ FROM pg_replication_slots
228
+ WHERE slot_name = ${slot}
229
+ `;
230
+ if (result.length === 0) {
231
+ const slotExpression = replicationSlotPrefix(this.#shard);
232
+ const replicas = await sql`
233
+ SELECT id, rank, slot, version, "initialSyncContext", "subscriberContext"
234
+ FROM ${sql(replicasTable)} ORDER BY rank DESC`;
235
+ const slots = await sql`
241
236
  SELECT slot_name as slot, active, active_pid as pid
242
237
  FROM pg_replication_slots
243
- WHERE slot_name LIKE ${slotExpression} OR slot_name = ${legacySlotName}
238
+ WHERE slot_name LIKE ${slotExpression}
244
239
  ORDER BY slot_name`;
245
- this.#lc.warn?.(`slot ${slotToKeep} not found while cleaning subscribers`, {
246
- slots: shardSlots,
247
- replicas: replicasBefore
248
- });
249
- throw new AbortError(`replication slot ${slotToKeep} is missing. A different replication-manager should now be running on a new replication slot.`);
240
+ this.#lc.warn?.(`slot ${slot} not found while cleaning subscribers`, {
241
+ slots,
242
+ replicas
243
+ });
244
+ throw new AbortError(`replication slot ${slot} is missing. A different replication-manager should now be running on a new replication slot.`);
245
+ }
246
+ this.#lc.info?.(`terminated replication slots: ${JSON.stringify(result)}`);
247
+ await sql`
248
+ UPDATE ${sql(replicasTable)}
249
+ SET "subscriberContext" = ${this.#context}
250
+ WHERE id = ${replicaID}`;
251
+ this.#cleanUpOlderReplicasAndSlots();
252
+ }
253
+ #cleanupTimer;
254
+ async #cleanUpOlderReplicasAndSlots() {
255
+ clearTimeout(this.#cleanupTimer);
256
+ try {
257
+ const result = await dropOldReplicasAndSlots(this.#lc, this.#db, this.#shard, this.#replica.rank);
258
+ if (result.draining === 0) {
259
+ this.#lc.info?.(`finished cleaning up replicas and slots`, { result });
260
+ return;
250
261
  }
251
- this.#lc.info?.(`replicas before cleanup (slotToKeep=${slotToKeep}): ${JSON.stringify(replicasBefore)}`);
252
- await sql`
253
- DELETE FROM ${sql(replicasTable)} WHERE slot < ${slotToKeep}`;
254
- await sql`
255
- UPDATE ${sql(replicasTable)}
256
- SET "subscriberContext" = ${this.#context}
257
- WHERE slot = ${slotToKeep}`;
258
- const replicasAfter = await sql`
259
- SELECT slot, version FROM ${sql(replicasTable)} ORDER BY slot`;
260
- this.#lc.info?.(`replicas after cleanup (slotToKeep=${slotToKeep}): ${JSON.stringify(replicasAfter)}`);
261
- return result;
262
- });
263
- const pids = result.filter(({ pid }) => pid !== null).map(({ pid }) => pid);
264
- if (pids.length) this.#lc.info?.(`signaled subscriber ${pids} to shut down`);
265
- const otherSlots = result.filter(({ slot }) => slot !== slotToKeep).map(({ slot }) => slot);
266
- if (otherSlots.length) this.#dropReplicationSlots(otherSlots).catch((e) => this.#lc.warn?.(`error dropping replication slots`, e));
267
- }
268
- async #dropReplicationSlots(slots) {
269
- this.#lc.info?.(`dropping other replication slot(s) ${slots}`);
270
- const sql = this.#db;
271
- for (let i = 0; i < 5; i++) try {
272
- await sql`
273
- SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots
274
- WHERE slot_name IN ${sql(slots)}
275
- `;
276
- this.#lc.info?.(`successfully dropped ${slots}`);
277
- return;
262
+ this.#lc.info?.(`old slots still draining`, { result });
278
263
  } catch (e) {
279
- if (e instanceof postgres.PostgresError && e.code === PG_OBJECT_IN_USE) this.#lc.debug?.(`attempt ${i + 1}: ${String(e)}`, e);
280
- else this.#lc.warn?.(`error dropping ${slots}`, e);
281
- await sleep(1e3);
264
+ this.#lc.warn?.(`error dropping replication slots`, e);
282
265
  }
283
- this.#lc.warn?.(`maximum attempts exceeded dropping ${slots}`);
266
+ this.#cleanupTimer = setTimeout(() => this.#cleanUpOlderReplicasAndSlots(), REPLICA_SLOT_CLEANUP_INTERVAL_MS);
284
267
  }
285
268
  };
286
269
  var Acker = class {
@@ -438,7 +421,7 @@ var LagReporter = class LagReporter {
438
421
  }
439
422
  processLagReportMessage(msg) {
440
423
  assert(msg.prefix === this.messagePrefix, `unexpected message prefix: ${msg.prefix}`);
441
- const report = parseLogicalMessageContent(msg, lagReportSchema);
424
+ const report = parseLogicalMessageContent(this.#lc, msg, lagReportSchema);
442
425
  return this.#processLagReport(report, toStateVersionString(msg.messageLsn ?? "0/0"));
443
426
  }
444
427
  #processLagReport(report, watermark) {
@@ -580,7 +563,7 @@ var ChangeMaker = class {
580
563
  }
581
564
  #lastReplicationEvent;
582
565
  #handleDdlMessage(lc, lsn, msg) {
583
- const event = parseLogicalMessageContent(msg, replicationEventSchema);
566
+ const event = parseLogicalMessageContent(lc, msg, replicationEventSchema);
584
567
  lc = lc.withContext("lsn", fromBigInt(lsn)).withContext("tag", event.event.tag).withContext("query", event.context.query);
585
568
  clearTimeout(this.#replicaIdentityTimer);
586
569
  const { type } = event;
@@ -927,9 +910,18 @@ var ShutdownSignal = class extends AbortError {
927
910
  super("shutdown signal received (e.g. another zero-cache taking over the replication stream)", { cause });
928
911
  }
929
912
  };
930
- function parseLogicalMessageContent({ content }, schema) {
913
+ function parseLogicalMessageContent(lc, msg, schema) {
914
+ const { content } = msg;
931
915
  const str = content instanceof Buffer ? content.toString("utf-8") : new TextDecoder().decode(content);
932
- return parse(JSON.parse(str), schema, "passthrough");
916
+ try {
917
+ return parse(JSON.parse(str), schema, "passthrough");
918
+ } catch (e) {
919
+ lc.error?.(`unable to parse logical message content: ${String(e)}`, { message: {
920
+ ...msg,
921
+ content: str
922
+ } });
923
+ throw e;
924
+ }
933
925
  }
934
926
  //#endregion
935
927
  export { initializePostgresChangeSource };