@rocicorp/zero 1.11.0-canary.7 → 1.11.0-canary.9

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 (258) hide show
  1. package/out/replicache/src/dag/gc.d.ts.map +1 -1
  2. package/out/replicache/src/dag/gc.js +2 -1
  3. package/out/replicache/src/dag/gc.js.map +1 -1
  4. package/out/replicache/src/dag/invalid-ref-count-error.d.ts +12 -0
  5. package/out/replicache/src/dag/invalid-ref-count-error.d.ts.map +1 -0
  6. package/out/replicache/src/dag/invalid-ref-count-error.js +19 -0
  7. package/out/replicache/src/dag/invalid-ref-count-error.js.map +1 -0
  8. package/out/replicache/src/dag/store-impl.d.ts +12 -2
  9. package/out/replicache/src/dag/store-impl.d.ts.map +1 -1
  10. package/out/replicache/src/dag/store-impl.js +22 -8
  11. package/out/replicache/src/dag/store-impl.js.map +1 -1
  12. package/out/replicache/src/db/rebase.d.ts +14 -2
  13. package/out/replicache/src/db/rebase.d.ts.map +1 -1
  14. package/out/replicache/src/db/rebase.js +27 -5
  15. package/out/replicache/src/db/rebase.js.map +1 -1
  16. package/out/replicache/src/persist/collect-idb-databases.d.ts +10 -5
  17. package/out/replicache/src/persist/collect-idb-databases.d.ts.map +1 -1
  18. package/out/replicache/src/persist/collect-idb-databases.js +16 -9
  19. package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
  20. package/out/replicache/src/persist/persist.d.ts.map +1 -1
  21. package/out/replicache/src/persist/persist.js +12 -5
  22. package/out/replicache/src/persist/persist.js.map +1 -1
  23. package/out/replicache/src/persist/refresh.js +6 -2
  24. package/out/replicache/src/persist/refresh.js.map +1 -1
  25. package/out/replicache/src/replicache-impl.d.ts +7 -3
  26. package/out/replicache/src/replicache-impl.d.ts.map +1 -1
  27. package/out/replicache/src/replicache-impl.js +98 -11
  28. package/out/replicache/src/replicache-impl.js.map +1 -1
  29. package/out/replicache/src/replicache-options.d.ts +9 -0
  30. package/out/replicache/src/replicache-options.d.ts.map +1 -1
  31. package/out/shared/src/iterables.d.ts +2 -0
  32. package/out/shared/src/iterables.d.ts.map +1 -1
  33. package/out/shared/src/iterables.js +18 -1
  34. package/out/shared/src/iterables.js.map +1 -1
  35. package/out/shared/src/ring-buffer.d.ts +8 -0
  36. package/out/shared/src/ring-buffer.d.ts.map +1 -1
  37. package/out/shared/src/ring-buffer.js +22 -1
  38. package/out/shared/src/ring-buffer.js.map +1 -1
  39. package/out/zero/package.js +2 -3
  40. package/out/zero/package.js.map +1 -1
  41. package/out/zero-cache/src/config/zero-config.js +2 -2
  42. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  43. package/out/zero-cache/src/server/change-streamer.js +1 -1
  44. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  45. package/out/zero-cache/src/server/inspector-delegate.d.ts +18 -2
  46. package/out/zero-cache/src/server/inspector-delegate.d.ts.map +1 -1
  47. package/out/zero-cache/src/server/inspector-delegate.js +20 -7
  48. package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
  49. package/out/zero-cache/src/services/change-source/pg/backfill-resume.d.ts +156 -0
  50. package/out/zero-cache/src/services/change-source/pg/backfill-resume.d.ts.map +1 -0
  51. package/out/zero-cache/src/services/change-source/pg/backfill-resume.js +15 -0
  52. package/out/zero-cache/src/services/change-source/pg/backfill-resume.js.map +1 -0
  53. package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts +15 -0
  54. package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts.map +1 -0
  55. package/out/zero-cache/src/services/change-source/pg/change-source-init.js +99 -0
  56. package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -0
  57. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +14 -16
  58. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  59. package/out/zero-cache/src/services/change-source/pg/change-source.js +33 -126
  60. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  61. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +16 -1
  62. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  63. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +8 -4
  64. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  65. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.d.ts +16 -0
  66. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.d.ts.map +1 -0
  67. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js +113 -0
  68. package/out/zero-cache/src/services/change-source/pg/replication-slot-cleanup-monitor.js.map +1 -0
  69. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts +1 -15
  70. package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts.map +1 -1
  71. package/out/zero-cache/src/services/change-source/pg/replication-slots.js +18 -21
  72. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
  73. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +8 -9
  74. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  75. package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts +17 -1
  76. package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts.map +1 -1
  77. package/out/zero-cache/src/services/change-streamer/upstream-acker.js +19 -3
  78. package/out/zero-cache/src/services/change-streamer/upstream-acker.js.map +1 -1
  79. package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.d.ts.map +1 -1
  80. package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.js +1 -0
  81. package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.js.map +1 -1
  82. package/out/zero-cache/src/services/replicator/change-processor.js +1 -0
  83. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  84. package/out/zero-cache/src/services/view-syncer/drain-coordinator.d.ts +14 -1
  85. package/out/zero-cache/src/services/view-syncer/drain-coordinator.d.ts.map +1 -1
  86. package/out/zero-cache/src/services/view-syncer/drain-coordinator.js +30 -4
  87. package/out/zero-cache/src/services/view-syncer/drain-coordinator.js.map +1 -1
  88. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts +1 -1
  89. package/out/zero-cache/src/services/view-syncer/inspect-handler.d.ts.map +1 -1
  90. package/out/zero-cache/src/services/view-syncer/inspect-handler.js +2 -2
  91. package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
  92. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  93. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +6 -10
  94. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  95. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  96. package/out/zero-cache/src/services/view-syncer/view-syncer.js +14 -4
  97. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  98. package/out/zero-cache/src/types/streams.d.ts.map +1 -1
  99. package/out/zero-cache/src/types/streams.js +1 -1
  100. package/out/zero-cache/src/types/streams.js.map +1 -1
  101. package/out/zero-client/src/client/connection-manager.d.ts +20 -0
  102. package/out/zero-client/src/client/connection-manager.d.ts.map +1 -1
  103. package/out/zero-client/src/client/connection-manager.js +32 -14
  104. package/out/zero-client/src/client/connection-manager.js.map +1 -1
  105. package/out/zero-client/src/client/connection-status-enum.d.ts +2 -0
  106. package/out/zero-client/src/client/connection-status-enum.d.ts.map +1 -1
  107. package/out/zero-client/src/client/connection-status-enum.js +3 -1
  108. package/out/zero-client/src/client/connection-status-enum.js.map +1 -1
  109. package/out/zero-client/src/client/connection.d.ts +6 -0
  110. package/out/zero-client/src/client/connection.d.ts.map +1 -1
  111. package/out/zero-client/src/client/connection.js +2 -1
  112. package/out/zero-client/src/client/connection.js.map +1 -1
  113. package/out/zero-client/src/client/context.d.ts +8 -11
  114. package/out/zero-client/src/client/context.d.ts.map +1 -1
  115. package/out/zero-client/src/client/context.js +11 -15
  116. package/out/zero-client/src/client/context.js.map +1 -1
  117. package/out/zero-client/src/client/custom.js +2 -2
  118. package/out/zero-client/src/client/custom.js.map +1 -1
  119. package/out/zero-client/src/client/error.js +1 -1
  120. package/out/zero-client/src/client/mutator-proxy.d.ts.map +1 -1
  121. package/out/zero-client/src/client/mutator-proxy.js +2 -1
  122. package/out/zero-client/src/client/mutator-proxy.js.map +1 -1
  123. package/out/zero-client/src/client/options.d.ts +3 -0
  124. package/out/zero-client/src/client/options.d.ts.map +1 -1
  125. package/out/zero-client/src/client/options.js.map +1 -1
  126. package/out/zero-client/src/client/query-manager.d.ts.map +1 -1
  127. package/out/zero-client/src/client/query-manager.js +42 -6
  128. package/out/zero-client/src/client/query-manager.js.map +1 -1
  129. package/out/zero-client/src/client/version.js +1 -1
  130. package/out/zero-client/src/client/zero-rep.d.ts +1 -0
  131. package/out/zero-client/src/client/zero-rep.d.ts.map +1 -1
  132. package/out/zero-client/src/client/zero-rep.js +7 -5
  133. package/out/zero-client/src/client/zero-rep.js.map +1 -1
  134. package/out/zero-client/src/client/zero.d.ts +9 -3
  135. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  136. package/out/zero-client/src/client/zero.js +48 -11
  137. package/out/zero-client/src/client/zero.js.map +1 -1
  138. package/out/zero-client/src/types/query-result.d.ts +2 -0
  139. package/out/zero-client/src/types/query-result.d.ts.map +1 -1
  140. package/out/zero-react/src/use-query.d.ts +5 -4
  141. package/out/zero-react/src/use-query.d.ts.map +1 -1
  142. package/out/zero-react/src/use-query.js +12 -1
  143. package/out/zero-react/src/use-query.js.map +1 -1
  144. package/out/zero-server/src/adapters/drizzle.d.ts +2 -1
  145. package/out/zero-server/src/adapters/drizzle.d.ts.map +1 -1
  146. package/out/zero-server/src/adapters/drizzle.js +12 -7
  147. package/out/zero-server/src/adapters/drizzle.js.map +1 -1
  148. package/out/zero-server/src/adapters/kysely.d.ts +2 -1
  149. package/out/zero-server/src/adapters/kysely.d.ts.map +1 -1
  150. package/out/zero-server/src/adapters/kysely.js +8 -2
  151. package/out/zero-server/src/adapters/kysely.js.map +1 -1
  152. package/out/zero-server/src/adapters/pg.d.ts +1 -0
  153. package/out/zero-server/src/adapters/pg.d.ts.map +1 -1
  154. package/out/zero-server/src/adapters/pg.js +8 -2
  155. package/out/zero-server/src/adapters/pg.js.map +1 -1
  156. package/out/zero-server/src/adapters/postgresjs.d.ts +1 -0
  157. package/out/zero-server/src/adapters/postgresjs.d.ts.map +1 -1
  158. package/out/zero-server/src/adapters/postgresjs.js +7 -1
  159. package/out/zero-server/src/adapters/postgresjs.js.map +1 -1
  160. package/out/zero-server/src/adapters/prisma.d.ts +3 -2
  161. package/out/zero-server/src/adapters/prisma.d.ts.map +1 -1
  162. package/out/zero-server/src/adapters/prisma.js +8 -2
  163. package/out/zero-server/src/adapters/prisma.js.map +1 -1
  164. package/out/zero-server/src/custom.d.ts +5 -1
  165. package/out/zero-server/src/custom.d.ts.map +1 -1
  166. package/out/zero-server/src/custom.js +6 -3
  167. package/out/zero-server/src/custom.js.map +1 -1
  168. package/out/zero-server/src/mod.d.ts +1 -1
  169. package/out/zero-server/src/mod.d.ts.map +1 -1
  170. package/out/zero-server/src/pg-query-executor.d.ts +2 -2
  171. package/out/zero-server/src/pg-query-executor.d.ts.map +1 -1
  172. package/out/zero-server/src/pg-query-executor.js +2 -2
  173. package/out/zero-server/src/pg-query-executor.js.map +1 -1
  174. package/out/zero-server/src/schema.d.ts +2 -2
  175. package/out/zero-server/src/schema.d.ts.map +1 -1
  176. package/out/zero-server/src/schema.js +2 -2
  177. package/out/zero-server/src/schema.js.map +1 -1
  178. package/out/zero-server/src/zql-database.d.ts +21 -1
  179. package/out/zero-server/src/zql-database.d.ts.map +1 -1
  180. package/out/zero-server/src/zql-database.js +33 -2
  181. package/out/zero-server/src/zql-database.js.map +1 -1
  182. package/out/zero-solid/src/solid-view.d.ts +10 -0
  183. package/out/zero-solid/src/solid-view.d.ts.map +1 -1
  184. package/out/zero-solid/src/solid-view.js +27 -0
  185. package/out/zero-solid/src/solid-view.js.map +1 -1
  186. package/out/zql/src/ivm/array-view.d.ts +10 -0
  187. package/out/zql/src/ivm/array-view.d.ts.map +1 -1
  188. package/out/zql/src/ivm/array-view.js +20 -8
  189. package/out/zql/src/ivm/array-view.js.map +1 -1
  190. package/out/zql/src/ivm/cap.d.ts.map +1 -1
  191. package/out/zql/src/ivm/cap.js +5 -3
  192. package/out/zql/src/ivm/cap.js.map +1 -1
  193. package/out/zql/src/ivm/deferred-input.d.ts.map +1 -1
  194. package/out/zql/src/ivm/deferred-input.js +3 -2
  195. package/out/zql/src/ivm/deferred-input.js.map +1 -1
  196. package/out/zql/src/ivm/exists.d.ts +1 -1
  197. package/out/zql/src/ivm/exists.d.ts.map +1 -1
  198. package/out/zql/src/ivm/exists.js.map +1 -1
  199. package/out/zql/src/ivm/fan-in.d.ts +2 -2
  200. package/out/zql/src/ivm/fan-in.d.ts.map +1 -1
  201. package/out/zql/src/ivm/fan-in.js +6 -5
  202. package/out/zql/src/ivm/fan-in.js.map +1 -1
  203. package/out/zql/src/ivm/fan-out.d.ts +2 -2
  204. package/out/zql/src/ivm/fan-out.d.ts.map +1 -1
  205. package/out/zql/src/ivm/fan-out.js.map +1 -1
  206. package/out/zql/src/ivm/filter-operators.d.ts +6 -6
  207. package/out/zql/src/ivm/filter-operators.d.ts.map +1 -1
  208. package/out/zql/src/ivm/filter-operators.js +10 -8
  209. package/out/zql/src/ivm/filter-operators.js.map +1 -1
  210. package/out/zql/src/ivm/filter.d.ts +2 -2
  211. package/out/zql/src/ivm/filter.d.ts.map +1 -1
  212. package/out/zql/src/ivm/filter.js +7 -4
  213. package/out/zql/src/ivm/filter.js.map +1 -1
  214. package/out/zql/src/ivm/flipped-join.js +2 -2
  215. package/out/zql/src/ivm/flipped-join.js.map +1 -1
  216. package/out/zql/src/ivm/maybe-split-and-push-edit-change.d.ts +1 -1
  217. package/out/zql/src/ivm/maybe-split-and-push-edit-change.d.ts.map +1 -1
  218. package/out/zql/src/ivm/maybe-split-and-push-edit-change.js +6 -4
  219. package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
  220. package/out/zql/src/ivm/memory-source.d.ts +2 -2
  221. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  222. package/out/zql/src/ivm/memory-source.js +9 -9
  223. package/out/zql/src/ivm/memory-source.js.map +1 -1
  224. package/out/zql/src/ivm/take.d.ts.map +1 -1
  225. package/out/zql/src/ivm/take.js +3 -4
  226. package/out/zql/src/ivm/take.js.map +1 -1
  227. package/out/zql/src/ivm/union-fan-in.d.ts.map +1 -1
  228. package/out/zql/src/ivm/union-fan-in.js +9 -7
  229. package/out/zql/src/ivm/union-fan-in.js.map +1 -1
  230. package/out/zql/src/ivm/view-apply-change.d.ts.map +1 -1
  231. package/out/zql/src/ivm/view-apply-change.js +3 -3
  232. package/out/zql/src/ivm/view-apply-change.js.map +1 -1
  233. package/out/zql/src/mutate/custom.d.ts +24 -1
  234. package/out/zql/src/mutate/custom.d.ts.map +1 -1
  235. package/out/zql/src/mutate/custom.js.map +1 -1
  236. package/out/zql/src/mutate/mutator-registry.d.ts.map +1 -1
  237. package/out/zql/src/mutate/mutator-registry.js +2 -2
  238. package/out/zql/src/mutate/mutator-registry.js.map +1 -1
  239. package/out/zql/src/query/query-delegate-base.d.ts +2 -0
  240. package/out/zql/src/query/query-delegate-base.d.ts.map +1 -1
  241. package/out/zql/src/query/query-delegate-base.js +55 -28
  242. package/out/zql/src/query/query-delegate-base.js.map +1 -1
  243. package/out/zql/src/query/query-delegate.d.ts +11 -1
  244. package/out/zql/src/query/query-delegate.d.ts.map +1 -1
  245. package/out/zql/src/query/query-impl.d.ts +1 -0
  246. package/out/zql/src/query/query-impl.d.ts.map +1 -1
  247. package/out/zql/src/query/query-impl.js.map +1 -1
  248. package/out/zql/src/query/query.d.ts +7 -1
  249. package/out/zql/src/query/query.d.ts.map +1 -1
  250. package/out/zql/src/query/runnable-query-impl.d.ts +1 -0
  251. package/out/zql/src/query/runnable-query-impl.d.ts.map +1 -1
  252. package/out/zql/src/query/runnable-query-impl.js.map +1 -1
  253. package/out/zql/src/query/typed-view.d.ts +1 -1
  254. package/out/zql/src/query/typed-view.d.ts.map +1 -1
  255. package/out/zqlite/src/table-source.d.ts.map +1 -1
  256. package/out/zqlite/src/table-source.js +7 -5
  257. package/out/zqlite/src/table-source.js.map +1 -1
  258. package/package.json +2 -3
@@ -1 +1 @@
1
- {"version":3,"file":"replication-slots.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-source/pg/replication-slots.ts"],"sourcesContent":["import type {Readable} from 'node:stream';\nimport {\n PG_CONFIGURATION_LIMIT_EXCEEDED,\n PG_INSUFFICIENT_PRIVILEGE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport type postgres from 'postgres';\nimport {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 {\n createReplicationSessionFor,\n keepSlotActiveUntilTakenOver,\n} from './logical-replication/stream.ts';\nimport {toBigInt, toStateVersionString} from './lsn.ts';\nimport {\n createReplica,\n metadataPublicationName,\n replicationSlotExpression,\n replicationSlotPrefix,\n type BackupOptions,\n} from './schema/shard.ts';\n\n// Record returned by `CREATE_REPLICATION_SLOT`\nexport type ReplicationSlot = {\n slot_name: string;\n consistent_point: string;\n snapshot_name: string;\n output_plugin: string;\n};\n\nexport type ReplicationSlotResult<T> = {\n slot: ReplicationSlot;\n capturedSnapshot: T;\n initialSession: Readable;\n};\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 // Create a temporary slot (i.e. not persisted, and automatically\n // cleaned up when the replication session ends).\n temporary?: boolean;\n\n // For overriding in tests.\n lockTimeout?: number;\n};\n\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 {\n slotName,\n failover,\n temporary,\n lockTimeout = SERVER_LOCK_TIMEOUT_MS,\n }: CreateSlotSpec,\n): Promise<ReplicationSlot> {\n // CREATE_REPLICATION_SLOT can hang indefinitely waiting for long-running\n // transactions to finish: internally it calls SnapBuildWaitSnapshot →\n // XactLockTableWait → LockAcquire on each running XID. statement_timeout\n // does NOT apply to replication commands, but lock_timeout does (it governs\n // the heavyweight lock wait inside LockAcquire). Setting it here causes\n // Postgres to raise ERRCODE_LOCK_NOT_AVAILABLE and cleanly tear down the\n // walsender, rather than relying solely on the client-side orTimeout\n // which can leave an orphaned backend.\n //\n // An orphaned walsender is actively harmful: by this point the replication\n // slot has already been created and is pinning WAL retention and catalog_xmin.\n // Worse, the slot is marked `active` (the walsender PID is still alive), so\n // the existing cleanup code (which drops inactive slots on retry) can't\n // reclaim it. Without lock_timeout the orphan persists until TCP keepalive\n // fires (~2h default) or the blocking transaction finishes.\n await session.unsafe(`SET lock_timeout = ${lockTimeout}`);\n\n const maybeTemporary = temporary ? 'TEMPORARY' : '';\n const options = failover ? '(FAILOVER)' : '';\n const createSlot = session.unsafe<ReplicationSlot[]>(/*sql*/ `\n CREATE_REPLICATION_SLOT \"${slotName}\" ${maybeTemporary} LOGICAL pgoutput ${options}`);\n\n const raced = await orTimeout(createSlot, CREATE_REPLICATION_SLOT_TIMEOUT_MS);\n if (raced === 'timed-out') {\n // 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 *\n * When the replication slot is created, `captureSnapshot` callback is first\n * run while the snapshot at the consistent point is available. The callback\n * must capture the snapshot in postgres (e.g. `SET TRANSACTION SNAPSHOT`)\n * in order to use it. Once the callback completes, a placeholder replication\n * session (i.e. that does not consume any messages) is started, in order to\n * reserve the slot by mark it \"active\", effectively distinguishing the\n * initializing session from a failed or abandoned session.\n *\n * Note that starting the replication session releases the initial snapshot,\n * which is why the callback must capture it synchronously, before the\n * replication session is started.\n *\n * The placeholder replication session is closed when the\n * PostgresChangeSource takes over the slot to process the stream in earnest.\n * It is also returned for cleanup in the case of failures (and for control\n * in unit tests).\n */\nexport async function createReplicaAndSlot<T>(\n lc: LogContext,\n sql: PostgresDB,\n sessionName: string,\n shard: ShardID,\n replicaID: string,\n failover: boolean,\n backupOptions: BackupOptions,\n captureSnapshot: (snapshot: string) => Promise<T>,\n): Promise<ReplicationSlotResult<T>> {\n // Note: The replicationSession is closed by keepSlotActiveUntilTakenOver,\n // and only closed in this function if an error occurrs.\n const replicationSession = createReplicationSessionFor(sql, sessionName);\n const lockName = replicationSlotManagementLock(shard);\n const slotPoolPrefix = replicationSlotPrefix(shard);\n\n for (let first = true; ; first = false) {\n await dropUnclaimedSlots(lc, sql, shard);\n\n let slotName: string | undefined;\n try {\n return await runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n // Pick an available slotName from the slotPoolPrefix pool.\n const names = await tx<{name: string}[]> /*sql*/ `\n SELECT slot_name as name FROM pg_replication_slots\n WHERE slot_name LIKE ${slotPoolPrefix + '%'};\n `.values();\n const inUse = new Set(names.flat());\n for (let next = 0; ; next++) {\n const candidateName = `${slotPoolPrefix}${slotPoolSuffix(next)}`;\n if (!inUse.has(candidateName)) {\n slotName = candidateName;\n break;\n }\n }\n\n const slot = await createReplicationSlot(lc, replicationSession, {\n slotName,\n failover,\n });\n const capturedSnapshot = await captureSnapshot(slot.snapshot_name);\n const initialSession = await keepSlotActiveUntilTakenOver(\n lc,\n replicationSession,\n slot.slot_name,\n metadataPublicationName(shard.appID, shard.shardNum),\n toBigInt(slot.consistent_point),\n );\n\n await createReplica(\n tx,\n shard,\n replicaID,\n slot.slot_name,\n toStateVersionString(slot.consistent_point),\n backupOptions,\n );\n\n return {slot, capturedSnapshot, initialSession};\n });\n } catch (e) {\n if (first && isPostgresError(e, PG_INSUFFICIENT_PRIVILEGE)) {\n // Some Postgres variants (e.g. Google Cloud SQL) require that\n // the user have the REPLICATION role in order to create a slot.\n // Note that this must be done by the upstreamDB connection, and\n // does not work in the replicationSession itself.\n await sql`ALTER ROLE current_user WITH REPLICATION`;\n lc.info?.(`Added the REPLICATION role to database user`);\n continue;\n }\n // Note: This is currently manually tested since max_replication_slots\n // is a PG startup parameter that other tests depend on.\n // TODO: Figure out a way to unit test this (with the full PG setup).\n if (first && isPostgresError(e, PG_CONFIGURATION_LIMIT_EXCEEDED)) {\n lc.warn?.(\n `Reached max replication slots. Attempting to clean up unused slots`,\n e,\n );\n // Drop any inactive replicas from failed initial syncs (e.g. inactive slots).\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n await sql`\n DELETE FROM ${sql(replicasTable)} USING pg_replication_slots slots\n WHERE replicas.slot = slots.slot_name AND NOT slots.active`;\n continue; // then let dropUnclaimedSlots() perform its cleanup\n }\n // Otherwise, clean up any created slot if something went wrong.\n if (slotName) {\n lc.warn?.(`deleting slot ${slotName} due to error`, e);\n await replicationSession.end();\n await sql`SELECT pg_drop_replication_slot(${slotName})`;\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":";;;;;;;;;AA4DA,IAAM,qCAAqC;AAK3C,IAAM,yBAAyB,qCAAqC;AAKpE,eAAsB,sBACpB,IACA,SACA,EACE,UACA,UACA,WACA,cAAc,0BAEU;CAgB1B,MAAM,QAAQ,OAAO,sBAAsB,aAAa;CAExD,MAAM,iBAAiB,YAAY,cAAc;CACjD,MAAM,UAAU,WAAW,eAAe;CAI1C,MAAM,QAAQ,MAAM,UAHD,QAAQ,OAAkC;+BAChC,SAAS,IAAI,eAAe,oBAAoB,SAE/C,GAAY,kCAAkC;CAC5E,IAAI,UAAU,aAAa;EAGzB,QACG,IAAI,EACJ,OAAM,MACL,GAAG,OAAO,oDAAoD,CAAC,CACjE;EACF,MAAM,IAAI,MACR,mBAAmB,mCAAmC,gCAAgC,SAAS,qCAEjG;CACF;CACA,MAAM,CAAC,QAAQ;CACf,GAAG,OAAO,4BAA4B,YAAY,IAAI;CACtD,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,eAAsB,qBACpB,IACA,KACA,aACA,OACA,WACA,UACA,eACA,iBACmC;CAGnC,MAAM,qBAAqB,4BAA4B,KAAK,WAAW;CACvE,MAAM,WAAW,8BAA8B,KAAK;CACpD,MAAM,iBAAiB,sBAAsB,KAAK;CAElD,KAAK,IAAI,QAAQ,OAAQ,QAAQ,OAAO;EACtC,MAAM,mBAAmB,IAAI,KAAK,KAAK;EAEvC,IAAI;EACJ,IAAI;GACF,OAAO,MAAM,MAAM,KAAK,OAAM,OAAM;IAClC,MAAM,EAAE,yCAAyC,SAAS;IAG1D,MAAM,QAAQ,MAAM,EAA6B;;mCAEtB,iBAAiB,IAAI;UAC9C,OAAO;IACT,MAAM,QAAQ,IAAI,IAAI,MAAM,KAAK,CAAC;IAClC,KAAK,IAAI,OAAO,IAAK,QAAQ;KAC3B,MAAM,gBAAgB,GAAG,iBAAiB,eAAe,IAAI;KAC7D,IAAI,CAAC,MAAM,IAAI,aAAa,GAAG;MAC7B,WAAW;MACX;KACF;IACF;IAEA,MAAM,OAAO,MAAM,sBAAsB,IAAI,oBAAoB;KAC/D;KACA;IACF,CAAC;IACD,MAAM,mBAAmB,MAAM,gBAAgB,KAAK,aAAa;IACjE,MAAM,iBAAiB,MAAM,6BAC3B,IACA,oBACA,KAAK,WACL,wBAAwB,MAAM,OAAO,MAAM,QAAQ,GACnD,SAAS,KAAK,gBAAgB,CAChC;IAEA,MAAM,cACJ,IACA,OACA,WACA,KAAK,WACL,qBAAqB,KAAK,gBAAgB,GAC1C,aACF;IAEA,OAAO;KAAC;KAAM;KAAkB;IAAc;GAChD,CAAC;EACH,SAAS,GAAG;GACV,IAAI,SAAS,gBAAgB,GAAG,yBAAyB,GAAG;IAK1D,MAAM,GAAG;IACT,GAAG,OAAO,6CAA6C;IACvD;GACF;GAIA,IAAI,SAAS,gBAAgB,GAAG,+BAA+B,GAAG;IAChE,GAAG,OACD,sEACA,CACF;IAGA,MAAM,GAAG;wBACO,IAAI,GAFK,eAAe,KAAK,EAAE,UAEd,EAAE;;IAEnC;GACF;GAEA,IAAI,UAAU;IACZ,GAAG,OAAO,iBAAiB,SAAS,gBAAgB,CAAC;IACrD,MAAM,mBAAmB,IAAI;IAC7B,MAAM,GAAG,mCAAmC,SAAS;GACvD;GACA,MAAM;EACR;CACF;AACF;;;;;;;;;;;AAYA,eAAsB,wBACpB,IACA,KACA,OACA,YAC8D;CAC9D,MAAM,gBAAgB,GAAG,eAAe,KAAK,EAAE;CAC/C,MAAM,cAAc,MAAM,GAAG;;YAEnB,IAAI,aAAa,EAAE,gBAAgB,WAAW;;CAExD,IAAI,YAAY,QAAQ;EACtB,GAAG,OAAO,YAAY,YAAY,OAAO,kBAAkB,EAAC,YAAW,CAAC;EACxE,MAAM,GAAG,eAAe,IAAI,aAAa,EAAE,gBAAgB;CAC7D;CAEA,OAAO,mBAAmB,IAAI,KAAK,KAAK;AAC1C;AAEA,SAAS,mBACP,IACA,KACA,OAC8D;CAI9D,MAAM,WAAW,8BAA8B,KAAK;CACpD,MAAM,iBAAiB,0BAA0B,KAAK;CACtD,MAAM,gBAAgB,GAAG,eAAe,KAAK,EAAE;CAE/C,OAAO,MAAM,KAAK,OAAM,OAAM;EAC5B,MAAM,EAAE,yCAAyC,SAAS;EAE1D,MAAM,UAAU,MAAM,EAAW;;;oBAGjB,GAAG,aAAa,EAAE;+BACP,eAAe;;;;EAI1C,IAAI,QAAQ,QACV,GAAG,OAAO,sCAAsC,EAAC,QAAO,CAAC;EAG3D,MAAM,YAAY,MAAM,EAEd;;;oBAGM,GAAG,aAAa,EAAE;+BACP,eAAe;;EAE1C,IAAI,UAAU,QACZ,GAAG,OAAO,+BAA+B,EAAC,UAAS,CAAC;EAGtD,IAAI,SAAS;EACb,IAAI,WAAW;EACf,KAAK,MAAM,EAAC,QAAO,WACjB,IAAI,OAAO,MACT;OAEA;EAIJ,OAAO;GACL,SAAS,QAAQ;GACjB;GACA;EACF;CACF,CAAC;AACH;AAEA,IAAM,WAAW;AAIjB,SAAgB,eAAe,GAAW;CACxC;CAEA,IAAI,SAAS;CACb,OAAO,IAAI,GAAG;EACZ;EACA,SAAS,SAAS,IAAI,MAAM;EAC5B,IAAI,KAAK,MAAM,IAAI,EAAE;CACvB;CACA,OAAO;AACT;AAEA,SAAS,8BAA8B,OAAgB;CACrD,OAAO,+BAA+B,MAAM,MAAM,GAAG,MAAM;AAC7D"}
1
+ {"version":3,"file":"replication-slots.js","names":[],"sources":["../../../../../../../zero-cache/src/services/change-source/pg/replication-slots.ts"],"sourcesContent":["import type {Readable} from 'node:stream';\nimport {\n PG_CONFIGURATION_LIMIT_EXCEEDED,\n PG_INSUFFICIENT_PRIVILEGE,\n} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport type postgres from 'postgres';\nimport {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 {\n createReplicationSessionFor,\n keepSlotActiveUntilTakenOver,\n} from './logical-replication/stream.ts';\nimport {toBigInt, toStateVersionString} from './lsn.ts';\nimport {\n createReplica,\n metadataPublicationName,\n replicationSlotExpression,\n replicationSlotPrefix,\n type BackupOptions,\n} from './schema/shard.ts';\n\n// Record returned by `CREATE_REPLICATION_SLOT`\nexport type ReplicationSlot = {\n slot_name: string;\n consistent_point: string;\n snapshot_name: string;\n output_plugin: string;\n};\n\nexport type ReplicationSlotResult<T> = {\n slot: ReplicationSlot;\n capturedSnapshot: T;\n initialSession: Readable;\n};\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 // Create a temporary slot (i.e. not persisted, and automatically\n // cleaned up when the replication session ends).\n temporary?: boolean;\n\n // For overriding in tests.\n lockTimeout?: number;\n};\n\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 {\n slotName,\n failover,\n temporary,\n lockTimeout = SERVER_LOCK_TIMEOUT_MS,\n }: CreateSlotSpec,\n): Promise<ReplicationSlot> {\n // CREATE_REPLICATION_SLOT can hang indefinitely waiting for long-running\n // transactions to finish: internally it calls SnapBuildWaitSnapshot →\n // XactLockTableWait → LockAcquire on each running XID. statement_timeout\n // does NOT apply to replication commands, but lock_timeout does (it governs\n // the heavyweight lock wait inside LockAcquire). Setting it here causes\n // Postgres to raise ERRCODE_LOCK_NOT_AVAILABLE and cleanly tear down the\n // walsender, rather than relying solely on the client-side orTimeout\n // which can leave an orphaned backend.\n //\n // An orphaned walsender is actively harmful: by this point the replication\n // slot has already been created and is pinning WAL retention and catalog_xmin.\n // Worse, the slot is marked `active` (the walsender PID is still alive), so\n // the existing cleanup code (which drops inactive slots on retry) can't\n // reclaim it. Without lock_timeout the orphan persists until TCP keepalive\n // fires (~2h default) or the blocking transaction finishes.\n await session.unsafe(`SET lock_timeout = ${lockTimeout}`);\n\n const maybeTemporary = temporary ? 'TEMPORARY' : '';\n const options = failover ? '(FAILOVER)' : '';\n const createSlot = session.unsafe<ReplicationSlot[]>(/*sql*/ `\n CREATE_REPLICATION_SLOT \"${slotName}\" ${maybeTemporary} LOGICAL pgoutput ${options}`);\n\n const raced = await orTimeout(createSlot, CREATE_REPLICATION_SLOT_TIMEOUT_MS);\n if (raced === 'timed-out') {\n // 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 *\n * When the replication slot is created, `captureSnapshot` callback is first\n * run while the snapshot at the consistent point is available. The callback\n * must capture the snapshot in postgres (e.g. `SET TRANSACTION SNAPSHOT`)\n * in order to use it. Once the callback completes, a placeholder replication\n * session (i.e. that does not consume any messages) is started, in order to\n * reserve the slot by mark it \"active\", effectively distinguishing the\n * initializing session from a failed or abandoned session.\n *\n * Note that starting the replication session releases the initial snapshot,\n * which is why the callback must capture it synchronously, before the\n * replication session is started.\n *\n * The placeholder replication session is closed when the\n * PostgresChangeSource takes over the slot to process the stream in earnest.\n * It is also returned for cleanup in the case of failures (and for control\n * in unit tests).\n */\nexport async function createReplicaAndSlot<T>(\n lc: LogContext,\n sql: PostgresDB,\n sessionName: string,\n shard: ShardID,\n replicaID: string,\n failover: boolean,\n backupOptions: BackupOptions,\n captureSnapshot: (snapshot: string) => Promise<T>,\n): Promise<ReplicationSlotResult<T>> {\n // Note: The replicationSession is closed by keepSlotActiveUntilTakenOver,\n // and only closed in this function if an error occurrs.\n const replicationSession = createReplicationSessionFor(sql, sessionName);\n const lockName = replicationSlotManagementLock(shard);\n const slotPoolPrefix = replicationSlotPrefix(shard);\n\n for (let first = true; ; first = false) {\n await dropUnclaimedSlots(lc, sql, shard);\n\n let slotName: string | undefined;\n try {\n return await runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n // Pick an available slotName from the slotPoolPrefix pool.\n const names = await tx<{name: string}[]> /*sql*/ `\n SELECT slot_name as name FROM pg_replication_slots\n WHERE slot_name LIKE ${slotPoolPrefix + '%'};\n `.values();\n const inUse = new Set(names.flat());\n for (let next = 0; ; next++) {\n const candidateName = `${slotPoolPrefix}${slotPoolSuffix(next)}`;\n if (!inUse.has(candidateName)) {\n slotName = candidateName;\n break;\n }\n }\n\n const slot = await createReplicationSlot(lc, replicationSession, {\n slotName,\n failover,\n });\n const capturedSnapshot = await captureSnapshot(slot.snapshot_name);\n const initialSession = await keepSlotActiveUntilTakenOver(\n lc,\n replicationSession,\n slot.slot_name,\n metadataPublicationName(shard.appID, shard.shardNum),\n toBigInt(slot.consistent_point),\n );\n\n await createReplica(\n tx,\n shard,\n replicaID,\n slot.slot_name,\n toStateVersionString(slot.consistent_point),\n backupOptions,\n );\n\n return {slot, capturedSnapshot, initialSession};\n });\n } catch (e) {\n if (first && isPostgresError(e, PG_INSUFFICIENT_PRIVILEGE)) {\n // Some Postgres variants (e.g. Google Cloud SQL) require that\n // the user have the REPLICATION role in order to create a slot.\n // Note that this must be done by the upstreamDB connection, and\n // does not work in the replicationSession itself.\n await sql`ALTER ROLE current_user WITH REPLICATION`;\n lc.info?.(`Added the REPLICATION role to database user`);\n continue;\n }\n // Note: This is currently manually tested since max_replication_slots\n // is a PG startup parameter that other tests depend on.\n // TODO: Figure out a way to unit test this (with the full PG setup).\n if (first && isPostgresError(e, PG_CONFIGURATION_LIMIT_EXCEEDED)) {\n lc.warn?.(\n `Reached max replication slots. Attempting to clean up unused slots`,\n e,\n );\n // Drop any inactive replicas from failed initial syncs (e.g. inactive slots).\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n await sql`\n DELETE FROM ${sql(replicasTable)} USING pg_replication_slots slots\n WHERE replicas.slot = slots.slot_name AND NOT slots.active`;\n continue; // then let dropUnclaimedSlots() perform its cleanup\n }\n // Otherwise, clean up any created slot if something went wrong.\n if (slotName) {\n lc.warn?.(`deleting slot ${slotName} due to error`, e);\n await replicationSession.end();\n await sql`SELECT pg_drop_replication_slot(${slotName})`;\n }\n throw e;\n }\n }\n}\n\nexport function dropInactiveSlotsAndReplicas(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n slots: string[],\n) {\n const lockName = replicationSlotManagementLock(shard);\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n\n return runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n const dropped = await tx<{slot: string; replicaID: string | null}[]>\n /*sql*/ `\n SELECT slot_name as slot, replica.id as \"replicaID\", pg_drop_replication_slot(slot_name) \n FROM pg_replication_slots\n LEFT JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name IN ${tx(slots)} AND NOT active;\n `;\n if (dropped.length) {\n lc.info?.(`dropped ${dropped.length} inactive replication slot(s)`, {\n dropped,\n });\n }\n\n const replicas = dropped\n .map(({replicaID}) => replicaID)\n .filter(id => id !== null);\n if (replicas.length) {\n // Delete replicas associated with the inactive (and now dropped) slots.\n await tx\n /*sql*/ `DELETE FROM ${tx(replicasTable)} WHERE id IN ${tx(replicas)}`;\n lc.info?.(`deleted ${replicas.length} old replica(s)`, {replicas});\n }\n });\n}\n\nfunction dropUnclaimedSlots(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n): Promise<{dropped: number; active: number; draining: number}> {\n // The slot / replica cleanup happens within a transaction while holding\n // the replication slot management lock for this shard, to ensure that no\n // slot that belongs to a newer replica is dropped.\n const lockName = replicationSlotManagementLock(shard);\n const slotExpression = replicationSlotExpression(shard);\n const replicasTable = `${upstreamSchema(shard)}.replicas`;\n\n return runTx(sql, async tx => {\n await tx`SELECT pg_advisory_xact_lock(hashtext(${lockName}))`;\n\n const dropped = await tx /*sql*/ `\n SELECT slot_name as slot, pg_drop_replication_slot(slot_name) \n FROM pg_replication_slots\n LEFT JOIN ${tx(replicasTable)} replica on slot_name = slot\n WHERE slot_name LIKE ${slotExpression} \n AND NOT active\n AND replica.id IS NULL;\n `;\n if (dropped.length) {\n lc.info?.(`dropped inactive replication slots`, {dropped});\n }\n\n 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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"}
@@ -334,7 +334,7 @@ var ChangeStreamerImpl = class {
334
334
  this.#stream = stream;
335
335
  if (this.#state.resetBackoff() > REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS) this.#replicationStatusPublisher.publish(this.#lc, "Replicating", `Replicating from ${lastWatermark}`);
336
336
  watermark = null;
337
- this.#acker.reset(stream.acks);
337
+ this.#acker.reset(stream.acks, this.#pgChangeLogEnabled ? "" : lastWatermark);
338
338
  for await (const change of stream.changes) {
339
339
  this.#acker.trackDownstream(change);
340
340
  const [type, msg] = change;
@@ -371,16 +371,15 @@ var ChangeStreamerImpl = class {
371
371
  json
372
372
  ];
373
373
  unflushedBytes += json.length;
374
- if (unflushedBytes < flushBytesThreshold) {
375
- this.#forwarder.forward(entry);
376
- this.#recordForwardedTransactionBoundary(type, entry[0]);
377
- } else {
378
- const forwarded = this.#forwarder.forwardWithFlowControl(entry);
379
- this.#recordForwardedTransactionBoundary(type, entry[0]);
380
- await promiseOrAbort(forwarded, stream.changes.signal, this.#state.signal);
374
+ let flowControl;
375
+ if (unflushedBytes < flushBytesThreshold) this.#forwarder.forward(entry);
376
+ else flowControl = this.#forwarder.forwardWithFlowControl(entry);
377
+ this.#recordForwardedTransactionBoundary(type, entry[0]);
378
+ if (type === "commit" || type === "rollback") watermark = null;
379
+ if (flowControl) {
380
+ await promiseOrAbort(flowControl, stream.changes.signal, this.#state.signal);
381
381
  unflushedBytes = 0;
382
382
  }
383
- if (type === "commit" || type === "rollback") watermark = null;
384
383
  const readyForMore = this.#pgChangeLogEnabled ? this.#storer.readyForMore() : void 0;
385
384
  if (readyForMore) await promiseOrAbort(readyForMore, stream.changes.signal, this.#state.signal);
386
385
  }
@@ -1 +1 @@
1
- {"version":3,"file":"change-streamer-service.js","names":["#lc","#shard","#changeDBProvider","#replicaVersion","#source","#storer","#pgChangeLogEnabled","#forwarder","#reservations","#replicationStatusPublisher","#sqliteCatchupOptions","#changeLogWriter","#purgeScheduler","#comparator","#acker","#initializer","#readRouter","#autoReset","#state","#serving","#txCounter","#changeCounter","#transactionForwardDuration","#catchupRoutes","#reservationDemotions","#reservationConfirmDelays","#sqliteCatchup","#closeSQLiteCatchup","#purgeLock","#latestStatus","#latestLagReportCommitTimeMs","#lastForwardedCommitWatermark","#stream","#handleControlMessage","#recordForwardedTransactionBoundary","#selectSQLiteCatchup","#captureRequiredHead","#recordCatchupRoute","#confirmReservations","#backupWatermark","#requestSQLitePurge","#maybeSchedulePGPurge","#maybeScheduleSQLitePurge","#getChangeLogState","#pgPurgeScheduled","#pgPurgeRunning","#pgPurgedWatermark","#purgePGChangeLog","#sqlitePurgeScheduled","#sqlitePurgeRunning","#sqlitePurgeContinuation","#purgeSQLiteChangeLog","#getCleanupFloor","#loggedBehindWatermark","#currentTransactionCompletion","#transactionForwardStartedAt","#recordTransactionForwardDuration","#fallbackOrReject","#declineSQLiteCatchup","#openSQLiteCatchup","#changeLogUnavailableSince"],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-service.ts"],"sourcesContent":["import {getDefaultHighWaterMark} from 'node:stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {defu} from 'defu';\nimport postgres, {type Options, type PostgresType} from 'postgres';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseOrAbort} from '../../../../shared/src/promise-race.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {publishCriticalEvent} from '../../observability/events.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {min} from '../../types/lexi-version.ts';\nimport type {PostgresDB} from '../../types/pg.ts';\nimport type {ShardID} from '../../types/shards.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {\n ChangeSource,\n ChangeStream,\n} from '../change-source/change-source.ts';\nimport {\n type ChangeStreamControl,\n type ChangeStreamData,\n type Rollback,\n} from '../change-source/protocol/current/downstream.ts';\nimport type {LitestreamVersion} from '../litestream/metrics.ts';\nimport {\n publishReplicationError,\n replicationStatusError,\n type ReplicationStatusPublisher,\n} from '../replicator/replication-status.ts';\nimport type {SubscriptionState} from '../replicator/schema/replication-state.ts';\nimport {\n DEFAULT_MAX_RETRY_DELAY_MS,\n RunningState,\n UnrecoverableError,\n} from '../running-state.ts';\nimport {serializeChangeStreamDataWithChange} from './change-log-codec.ts';\nimport {\n ChangeLogInitializer,\n replicaInitializationSource,\n} from './change-log-initializer.ts';\nimport {\n type ChangeStreamerService,\n type Status,\n type SubscriberContext,\n type WatermarkedChange,\n} from './change-streamer.ts';\nimport * as ErrorType from './error-type-enum.ts';\nimport {Forwarder} from './forwarder.ts';\nimport {\n AutoResetSignal,\n ensureReplicationConfig,\n markResetRequired,\n} from './schema/tables.ts';\nimport {SnapshotReservations} from './snapshot-reservations.ts';\nimport type {SnapshotMessage} from './snapshot.ts';\nimport {\n SQLiteChangeLogCatchup,\n type SQLiteChangeLogCleanupGuard,\n} from './sqlite-change-log-catchup.ts';\nimport {\n SQLiteChangeLogComparator,\n type SQLiteChangeLogCompareOptions,\n} from './sqlite-change-log-comparator.ts';\nimport {\n SQLiteChangeLogPurgeScheduler,\n type PurgeContinuation,\n type SQLiteChangeLogPurgeSchedulerOptions,\n} from './sqlite-change-log-purge-scheduler.ts';\nimport {\n inspectSQLiteChangeLog,\n SQLiteChangeLogReadRouter,\n type ChangeLogReadRoute,\n type SQLiteChangeLogCoverage,\n} from './sqlite-change-log-read-router.ts';\nimport {SQLiteChangeLogReader} from './sqlite-change-log-reader.ts';\nimport {\n SQLiteChangeLogWriter,\n type SQLiteChangeLogWriterOptions,\n} from './sqlite-change-log-writer.ts';\nimport type {PostgresDBProvider} from './storer.ts';\nimport {\n Storer,\n type PurgeLock,\n type TuningOptions as StorerOptions,\n} from './storer.ts';\nimport {Subscriber} from './subscriber.ts';\nimport {UpstreamAcker} from './upstream-acker.ts';\n\nexport type BackupConfig = {\n backupURL: string;\n litestreamVersion: LitestreamVersion;\n};\n\nexport type SQLiteCatchupOptions = {\n /** The change-log database, i.e. `changeLogFileName(replicaFile)`. */\n changeLogFile: string;\n readBatchRows: number;\n barrierTimeoutMs: number;\n /**\n * Backstop for the change-log writer's ACK, which is what normally releases\n * the barrier. Defaults to the catchup coordinator's interval.\n */\n barrierPollIntervalMs?: number | undefined;\n /**\n * Optional policy hook used by focused tests. Production canary selection is\n * supplied by `sqliteChangeLogServe` and is stable by shard plus task ID.\n * Backup subscribers are rejected before either selector is invoked.\n */\n shouldUse?: ((ctx: SubscriberContext) => boolean) | undefined;\n /**\n * Overrides the purge scheduler's guard, for tests. When absent, the\n * service supplies the scheduler's real writer-serialized guard, or the\n * catchup's no-op default when purging is not configured.\n */\n cleanupGuard?: SQLiteChangeLogCleanupGuard | undefined;\n /**\n * How long the change log may be unavailable before declining to serve from\n * it is reported as a warning rather than a debug line. Defaults to\n * {@link DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS}.\n */\n notReadyWarnThresholdMs?: number | undefined;\n};\n\nexport type SQLiteChangeLogServeOptions = {\n /** Stable percentage of eligible serving tasks routed to SQLite. */\n readPercent: number;\n /**\n * Stable percentage of eligible serving tasks routed to a log that has not\n * yet aged through {@link retentionMs}. Zero keeps a reseeded log on PG for\n * that whole window; above zero it serves that share of tasks, and a\n * reservation it cannot cover is demoted rather than held pending.\n * Defaults to zero.\n */\n coldReadPercent?: number | undefined;\n /** The warm-up window a reseeded log ages through. */\n retentionMs: number;\n /** Injectable for deterministic breaker tests. */\n failureCooldownMs?: number | undefined;\n /** Injectable for deterministic warm-up and breaker tests. */\n now?: (() => number) | undefined;\n /** Injectable readiness inspection for service-level tests. */\n inspect?: (() => SQLiteChangeLogCoverage | undefined) | undefined;\n};\n\nexport type TuningOptions = StorerOptions & {\n /**\n * Keeps the legacy Postgres change log on the initialization, persistence,\n * catchup, purge, and ACK paths. Disable only after SQLite and the replica\n * backup are authoritative for all of those responsibilities.\n */\n pgChangeLogEnabled: boolean;\n flowControlConsensusTimeoutProportion: number;\n flowControlSlowSubscriberGracePeriodMs?: number | undefined;\n sqliteCatchup?: SQLiteCatchupOptions | undefined;\n /**\n * Supplied when `sqliteChangeLogMode != off`, i.e. this is the gate on the\n * change-log writer. Absent, nothing writes the log.\n */\n sqliteChangeLogWriter?:\n | Omit<\n SQLiteChangeLogWriterOptions,\n 'onCommit' | 'onDisabled' | 'onRebuilt'\n >\n | undefined;\n /**\n * Also supplied when `sqliteChangeLogMode != off`, and deliberately not\n * gated on the read path: in `write` mode no reader ever opens, and this is\n * the configuration the purge scheduler actually ships in. The scheduler\n * runs on the writer's own connection, so with the writer absent (mode\n * `off`) it is never constructed, and a leftover file on disk is left to\n * the replicator's cleanup.\n */\n sqliteChangeLogPurge?: SQLiteChangeLogPurgeSchedulerOptions | undefined;\n /**\n * Supplied in `compare` mode and later modes.\n * `replicaFile` enables the initialization comparison.\n * The other fields configure sampled catchup comparisons.\n * Both checks are advisory, and Postgres remains authoritative.\n * The comparator also requires the writer and catchup options.\n */\n sqliteChangeLogCompare?:\n | (SQLiteChangeLogCompareOptions & {replicaFile: string})\n | undefined;\n /** Supplied only in `serve` mode. A zero percentage keeps every read on PG. */\n sqliteChangeLogServe?: SQLiteChangeLogServeOptions | undefined;\n};\n\n/**\n * Performs initialization and schema migrations to initialize a ChangeStreamerImpl.\n */\nexport async function initializeStreamer(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n changeDB: PostgresDB,\n changeSource: ChangeSource,\n replicationStatusPublisher: ReplicationStatusPublisher,\n subscriptionState: SubscriptionState,\n backupConfig: BackupConfig | null,\n purgeLock: PurgeLock | null,\n autoReset: boolean,\n opts: TuningOptions,\n setTimeoutFn = setTimeout,\n): Promise<ChangeStreamerService> {\n await ensureReplicationConfig(\n lc,\n changeDB,\n subscriptionState,\n shard,\n autoReset,\n purgeLock ?? undefined,\n setTimeoutFn,\n opts.pgChangeLogEnabled,\n );\n\n // Dynamically creates connection pools that the implementation uses to\n // isolate concurrent, long-running transactions. This works around a bug in\n // the postgres.js client where connections get swapped in certain conditions.\n //\n // https://github.com/porsager/postgres/issues/1204\n const changeDBProvider: PostgresDBProvider = (\n applicationName: string,\n max: number,\n ) =>\n postgres(\n defu(\n {max, connection: {['application_name']: applicationName}},\n // ParsedOptions are technically compatible with Options, but happen\n // to not be typed that way. The postgres.js author does an equivalent\n // merge of ParsedOptions and Options here:\n // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/subscribe.js#L13\n changeDB.options as unknown as Options<Record<string, PostgresType>>,\n ),\n ) as PostgresDB;\n\n const {replicaVersion} = subscriptionState;\n return new ChangeStreamerImpl(\n lc,\n shard,\n taskID,\n discoveryAddress,\n discoveryProtocol,\n changeDBProvider,\n replicaVersion,\n changeSource,\n replicationStatusPublisher,\n backupConfig,\n purgeLock,\n autoReset,\n opts,\n setTimeoutFn,\n );\n}\n\nconst REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS = 5000;\n\n// How long the change log may be unavailable before declining to serve from it\n// stops looking like normal startup ordering. Subscriptions can arrive before\n// the stream loop's first reconcile has created and seeded the log, so early\n// declines are expected. Still declining a minute later means the log is not\n// being written at all -- `sqliteChangeLogMode=off`, a writer that has failed\n// soft, or a path mismatch -- which is worth surfacing.\nconst DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS = 60_000;\n\n/**\n * Upstream-agnostic dispatch of messages in a {@link ChangeStreamMessage} to a\n * {@link Forwarder} and {@link Storer} to execute the forward-store-ack\n * procedure described in {@link ChangeStreamer}.\n *\n * ### Subscriber Catchup\n *\n * Connecting clients first need to be \"caught up\" to the current watermark\n * (from stored change log entries) before new entries are forwarded to\n * them. This is non-trivial because the replication stream may be in the\n * middle of a pending streamed Transaction for which some entries have\n * already been forwarded but are not yet committed to the store.\n *\n *\n * ```\n * ------------------------------- - - - - - - - - - - - - - - - - - - -\n * | Historic changes in storage | Pending (streamed) tx | Next tx\n * ------------------------------- - - - - - - - - - - - - - - - - - - -\n * Replication stream\n * > > > > > > > > >\n * ^ ---> required catchup ---> ^\n * Subscriber watermark Subscription begins\n * ```\n *\n * Preemptively buffering the changes of every pending transaction\n * would be wasteful and consume too much memory for large transactions.\n *\n * Instead, the streamer synchronously dispatches changes and subscriptions\n * to the {@link Forwarder} and the {@link Storer} such that the two\n * components are aligned as to where in the stream the subscription started.\n * The two components then coordinate catchup and handoff via the\n * {@link Subscriber} object with the following algorithm:\n *\n * * If the streamer is in the middle of a pending Transaction, the\n * Subscriber is \"queued\" on both the Forwarder and the Storer. In this\n * state, new changes are *not* forwarded to the Subscriber, and catchup\n * is not yet executed.\n * * Once the commit message for the pending Transaction is processed\n * by the Storer, it begins catchup on the Subscriber (with a READONLY\n * snapshot so that it does not block subsequent storage operations).\n * This catchup is thus guaranteed to load the change log entries of\n * that last Transaction.\n * * When the Forwarder processes that same commit message, it moves the\n * Subscriber from the \"queued\" to the \"active\" set of clients such that\n * the Subscriber begins receiving new changes, starting from the next\n * Transaction.\n * * The Subscriber does not forward those changes, however, if its catchup\n * is not complete. Until then, it buffers the changes in memory.\n * * Once catchup is complete, the buffered changes are immediately sent\n * and the Subscriber henceforth forwards changes as they are received.\n *\n * In the (common) case where the streamer is not in the middle of a pending\n * transaction when a subscription begins, the Storer begins catchup\n * immediately and the Forwarder directly adds the Subscriber to its active\n * set. However, the Subscriber still buffers any forwarded messages until\n * its catchup is complete.\n *\n * ### Watermarks and ordering\n *\n * The ChangeStreamerService depends on its {@link ChangeSource} to send\n * changes in contiguous [`begin`, `data` ..., `data`, `commit`] sequences\n * in commit order. This follows Postgres's Logical Replication Protocol\n * Message Flow:\n *\n * https://www.postgresql.org/docs/16/protocol-logical-replication.html#PROTOCOL-LOGICAL-MESSAGES-FLOW\n *\n * > The logical replication protocol sends individual transactions one by one.\n * > This means that all messages between a pair of Begin and Commit messages belong to the same transaction.\n *\n * In order to correctly replay (new) and filter (old) messages to subscribers\n * at different points in the replication stream, these changes must be assigned\n * watermarks such that they preserve the order in which they were received\n * from the ChangeSource.\n *\n * A previous implementation incorrectly derived these watermarks from the Postgres\n * Log Sequence Numbers (LSN) of each message. However, LSNs from concurrent,\n * non-conflicting transactions can overlap, which can result in a `begin` message\n * with an earlier LSN arriving after a `commit` message. For example, the\n * changes for these transactions:\n *\n * ```\n * LSN: 1 2 3 4 5 6 7 8 9 10\n * tx1: begin data data data commit\n * tx2: begin data data data commit\n * ```\n *\n * will arrive as:\n *\n * ```\n * begin1, data2, data4, data6, commit8, begin3, data5, data7, data9, commit10\n * ```\n *\n * Thus, LSN of non-commit messages are not suitable for tracking the sorting\n * order of the replication stream.\n *\n * Instead, the ChangeStreamer uses the following algorithm for deterministic\n * catchup and filtering of changes:\n *\n * * A `commit` message is assigned to a watermark corresponding to its LSN.\n * These are guaranteed to be in commit order by definition.\n *\n * * `begin` and `data` messages are assigned to the watermark of the\n * preceding `commit` (the previous transaction, or the replication\n * slot's starting LSN) plus 1. This guarantees that they will be sorted\n * after the previously commit transaction even if their LSNs came before it.\n * This is referred to as the `preCommitWatermark`.\n *\n * * In the ChangeLog DB, messages have a secondary sort column `pos`, which is\n * the position of the message within its transaction, with the `begin` message\n * starting at `0`. This guarantees that `begin` and `data` messages will be\n * fetched in the original ChangeSource order during catchup.\n *\n * `begin` and `data` messages share the same watermark, but this is sufficient for\n * Subscriber filtering because subscribers only know about the `commit` watermarks\n * exposed in the `Downstream` `Commit` message. The Subscriber object thus compares\n * the internal watermarks of the incoming messages against the commit watermark of\n * the caller, updating the watermark at every `Commit` message that is forwarded.\n *\n * ### Cleanup\n *\n * As mentioned in the {@link ChangeStreamer} documentation: \"the ChangeStreamer\n * uses a combination of [the \"initial\", i.e. backup-derived watermark and] ACK\n * responses from connected subscribers to determine the watermark up\n * to which it is safe to purge old change log entries.\"\n *\n * More concretely:\n *\n * * The `initial`, backup-derived watermark is the earliest to which cleanup\n * should ever happen.\n *\n * * However, it is possible for the replica backup to be *ahead* of a connected\n * subscriber; and if a network error causes that subscriber to retry from its\n * last watermark, the change streamer must support it.\n *\n * Thus, before cleaning up to an `initial` backup-derived watermark, the change\n * streamer first confirms that all connected subscribers have also passed\n * that watermark.\n */\nclass ChangeStreamerImpl implements ChangeStreamerService {\n readonly id: string;\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #changeDBProvider: PostgresDBProvider;\n readonly #replicaVersion: string;\n readonly #source: ChangeSource;\n readonly #storer: Storer;\n readonly #pgChangeLogEnabled: boolean;\n readonly #forwarder: Forwarder;\n readonly #reservations: SnapshotReservations | undefined;\n readonly #replicationStatusPublisher: ReplicationStatusPublisher;\n readonly #sqliteCatchupOptions: SQLiteCatchupOptions | undefined;\n readonly #changeLogWriter: SQLiteChangeLogWriter | undefined;\n readonly #purgeScheduler: SQLiteChangeLogPurgeScheduler | undefined;\n readonly #comparator: SQLiteChangeLogComparator | undefined;\n readonly #acker: UpstreamAcker;\n readonly #initializer: ChangeLogInitializer;\n readonly #readRouter: SQLiteChangeLogReadRouter | undefined;\n\n readonly #autoReset: boolean;\n readonly #state: RunningState;\n\n // Starting the (Postgres) ChangeStream results in killing the previous\n // Postgres subscriber, potentially creating a gap in which the old\n // change-streamer has shut down and the new change-streamer has not yet\n // been recognized as \"healthy\" (and thus does not get any requests).\n //\n // To minimize this gap, delay starting the ChangeStream until the first\n // request from a `serving` replicator, indicating that higher level\n // load-balancing / routing logic has begun routing requests to this task.\n readonly #serving = resolver();\n\n readonly #txCounter = getOrCreateCounter(\n 'replication',\n 'transactions',\n 'Count of replicated transactions',\n );\n readonly #changeCounter = getOrCreateCounter(\n 'replication',\n 'changes',\n 'Count of replicated changes (DML or DDL statements)',\n );\n // The number the SQLite change-log commit lands in. It is labeled by whether\n // the log is being written so that the cost of putting a commit on the\n // forward path is attributable rather than inferred.\n readonly #transactionForwardDuration = getOrCreateLatencyHistogram(\n 'replication',\n 'transaction_forward_duration',\n \"Time from receiving a transaction's `begin` to forwarding its `commit`, \" +\n 'i.e. the change-streamer half of forward-to-subscriber latency.',\n );\n readonly #catchupRoutes = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.catchup_routes',\n 'Catchup subscriptions by selected source and low-cardinality reason.',\n );\n readonly #reservationDemotions = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.reservation_demotions',\n 'Snapshot reservations demoted from SQLite to PG because the change ' +\n 'log could not cover the backup being restored. With PG retired ' +\n 'these followers have no fallback, so this is the rate at which one ' +\n 'would instead have to wait for a later backup.',\n );\n readonly #reservationConfirmDelays = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.reservation_confirm_delays',\n 'Snapshot reservations whose confirmation was deferred because the ' +\n \"selected source's change-log minimum was later than the backup \" +\n 'watermark. Counted once per reservation, by that source.',\n );\n\n #latestStatus: Status;\n #latestLagReportCommitTimeMs = 0;\n #backupWatermark: string | undefined;\n #pgPurgedWatermark: string = '';\n /**\n * The floor last logged as held back by a laggard. The level-triggered\n * retry re-evaluates every {@link CLEANUP_DELAY_MS}; logging only when the\n * blocking floor moves keeps a stuck subscriber from emitting the same\n * line indefinitely.\n */\n #loggedBehindWatermark: string | undefined;\n #sqlitePurgeContinuation: PurgeContinuation | undefined;\n #purgeLock: PurgeLock | null;\n // PG and SQLite intentionally own separate level-triggered loops. Neither\n // waits for, advances, or retries the other.\n #pgPurgeScheduled = false;\n #pgPurgeRunning = false;\n #sqlitePurgeScheduled = false;\n #sqlitePurgeRunning = false;\n #stream: ChangeStream | undefined;\n #sqliteCatchup: SQLiteChangeLogCatchup | undefined;\n #changeLogUnavailableSince: number | undefined;\n #lastForwardedCommitWatermark: string | undefined;\n #transactionForwardStartedAt: number | undefined;\n #currentTransactionCompletion:\n | Resolver<ForwardedTransactionCompletion>\n | undefined;\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n changeDBProvider: PostgresDBProvider,\n replicaVersion: string,\n source: ChangeSource,\n replicationStatusPublisher: ReplicationStatusPublisher,\n backupConfig: BackupConfig | null,\n initialPurgeLock: PurgeLock | null,\n autoReset: boolean,\n opts: TuningOptions,\n setTimeoutFn = setTimeout,\n ) {\n this.id = `change-streamer`;\n this.#lc = lc.withContext('component', 'change-streamer');\n this.#shard = shard;\n this.#changeDBProvider = changeDBProvider;\n this.#replicaVersion = replicaVersion;\n this.#source = source;\n this.#pgChangeLogEnabled = opts.pgChangeLogEnabled;\n if (!this.#pgChangeLogEnabled) {\n assert(\n opts.sqliteChangeLogWriter &&\n opts.sqliteChangeLogPurge &&\n opts.sqliteCatchup &&\n opts.sqliteChangeLogServe?.readPercent === 100 &&\n opts.sqliteChangeLogServe.coldReadPercent === 100 &&\n backupConfig?.litestreamVersion === 'v5',\n 'disabling the PG change log requires SQLite serving at 100 percent and a v5 backup',\n );\n assert(\n opts.sqliteChangeLogCompare === undefined,\n 'SQLite change-log comparison requires the PG change log',\n );\n }\n this.#storer = new Storer(\n lc,\n shard,\n taskID,\n discoveryAddress,\n discoveryProtocol,\n changeDBProvider,\n replicaVersion,\n consumed => this.#acker.trackPgChangeLog(consumed[2].watermark),\n err => this.stop(err),\n opts,\n );\n this.#forwarder = new Forwarder(lc, {\n flowControlConsensusTimeoutProportion:\n opts.flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodMs:\n opts.flowControlSlowSubscriberGracePeriodMs,\n });\n const serveOptions = opts.sqliteChangeLogServe;\n const writerOptions = opts.sqliteChangeLogWriter;\n const catchupOptions = opts.sqliteCatchup;\n this.#readRouter =\n serveOptions && writerOptions && catchupOptions\n ? new SQLiteChangeLogReadRouter({\n shard,\n readPercent: serveOptions.readPercent,\n coldReadPercent: serveOptions.coldReadPercent,\n retentionMs: serveOptions.retentionMs,\n failureCooldownMs: serveOptions.failureCooldownMs,\n now: serveOptions.now,\n inspect:\n serveOptions.inspect ??\n (() =>\n inspectSQLiteChangeLog(\n lc,\n catchupOptions.changeLogFile,\n writerOptions.identity,\n )),\n })\n : undefined;\n this.#reservations = backupConfig\n ? new SnapshotReservations(lc, backupConfig, taskID => {\n this.#readRouter?.release(taskID);\n this.#purgeScheduler?.resume(taskID);\n })\n : undefined;\n this.#replicationStatusPublisher = replicationStatusPublisher;\n this.#changeLogWriter = opts.sqliteChangeLogWriter\n ? new SQLiteChangeLogWriter(lc, {\n ...opts.sqliteChangeLogWriter,\n onCommit: watermark => {\n this.#sqliteCatchup?.onChangeLogCommit(watermark);\n // The commit closed the log's transaction, i.e. opened a purge\n // window (§3.3).\n this.#purgeScheduler?.onWriterIdle();\n },\n // Fail-soft deletes the file, and the reader is cached here, so it\n // has to go with it: otherwise it serves an unlinked inode while\n // every new open sees nothing.\n onDisabled: () => {\n // The writer stays disabled and the file stays absent for the life\n // of this process, so unlike a transient read/barrier failure this\n // breaker never expires.\n this.#readRouter?.trip(true);\n this.#closeSQLiteCatchup();\n this.#comparator?.stop();\n },\n onRebuilt: () => {\n this.#closeSQLiteCatchup();\n // Invalidate cycles that can still read the replaced file.\n this.#comparator?.invalidate();\n },\n })\n : undefined;\n // The purge scheduler runs on the writer's own connection, which is also\n // its gate: no writer (mode `off`) means no scheduler, and a writer that\n // has not created the file yet -- or failed soft and deleted it -- makes\n // cycles skip rather than fail, so a late-appearing file is picked up\n // without a restart.\n this.#purgeScheduler =\n opts.sqliteChangeLogPurge && this.#changeLogWriter\n ? new SQLiteChangeLogPurgeScheduler(\n lc,\n () => this.#changeLogWriter?.connection,\n () => this.#forwarder.getAcks(),\n opts.sqliteChangeLogPurge,\n )\n : undefined;\n this.#acker = new UpstreamAcker({\n trackPgChangeLog: this.#pgChangeLogEnabled,\n trackBackup: backupConfig?.litestreamVersion === 'v5',\n });\n const replicaFileForInitialization =\n opts.sqliteChangeLogCompare?.replicaFile ??\n (!this.#pgChangeLogEnabled\n ? opts.sqliteChangeLogWriter?.replicaFile\n : undefined);\n const replicaSource = replicaFileForInitialization\n ? replicaInitializationSource(lc, replicaFileForInitialization)\n : undefined;\n this.#initializer = new ChangeLogInitializer(\n lc,\n {\n initFromPgChangeLog: this.#pgChangeLogEnabled,\n initFromReplica: replicaSource !== undefined,\n },\n {\n pgChangeLog: () =>\n this.#storer.getStartStreamInitializationParameters(),\n // Only reached when `initFromReplica` is set, i.e. when the option\n // that supplies the file is present.\n replica: () => must(replicaSource)(),\n // Use the Postgres resume point when it is present. Otherwise, let the\n // SQLite change log supply its own resume point.\n reconcileChangeLog: (resumeFrom, seed) =>\n resumeFrom\n ? this.#changeLogWriter?.reconcile(resumeFrom)\n : this.#changeLogWriter?.reconcileFromLog(seed),\n changeLog: () => this.#changeLogWriter?.connection,\n },\n );\n this.#sqliteCatchupOptions = opts.sqliteCatchup\n ? {\n ...opts.sqliteCatchup,\n // The real cleanup guard, in place of the catchup's no-op default:\n // registration and purge batches now serialize on one mutex.\n cleanupGuard:\n opts.sqliteCatchup.cleanupGuard ??\n this.#purgeScheduler?.cleanupGuard,\n }\n : undefined;\n // Compare mode requires the writer and catchup configuration.\n this.#comparator =\n this.#pgChangeLogEnabled &&\n opts.sqliteChangeLogCompare &&\n opts.sqliteChangeLogWriter &&\n opts.sqliteCatchup\n ? new SQLiteChangeLogComparator(\n lc,\n shard,\n opts.sqliteCatchup.changeLogFile,\n opts.sqliteChangeLogWriter.identity,\n this.#storer,\n {setTimeoutFn, ...opts.sqliteChangeLogCompare},\n )\n : undefined;\n this.#purgeLock = initialPurgeLock;\n this.#autoReset = autoReset;\n this.#state = new RunningState(this.id, undefined, setTimeoutFn);\n this.#latestStatus = {tag: 'status'};\n }\n\n async run() {\n this.#lc.info?.('starting change stream');\n\n this.#forwarder.startProgressMonitor();\n\n const lagReportInit = await this.#source.startLagReporter();\n if (lagReportInit) {\n this.#latestStatus.lagReport = {\n nextSendTimeMs: lagReportInit.nextSendTimeMs,\n };\n // Record the commit time of the initiated lag report (i.e. \"head\")\n // for the purpose of skipping over any lag reports that are re-streamed\n // by the change-source in the case of a change-streamer starting from\n // an older watermark.\n this.#latestLagReportCommitTimeMs = lagReportInit.firstCommitTimeMs;\n }\n\n // Once this change-streamer acquires \"ownership\" of the change DB,\n // it is safe to start the storer.\n await this.#storer.assumeOwnership(this.#purgeLock);\n this.#purgeLock = null;\n\n // The threshold in (estimated number of) bytes to send() on subscriber\n // websockets before `await`-ing the I/O buffers to be ready for more.\n const flushBytesThreshold = getDefaultHighWaterMark(false);\n\n while (this.#state.shouldRun()) {\n let err: unknown;\n let watermark: string | null = null;\n let unflushedBytes = 0;\n try {\n // Initialization reconciles the change log for every stream\n // connection. It completes before `startStream`, so no change can\n // arrive during reconciliation.\n const {lastWatermark, backfillRequests} =\n await this.#initializer.initialize();\n // SQLite catchup must not be eligible until this has been initialized\n // from the selected durable head. Commits observed only since process\n // startup are insufficient after a change-streamer restart.\n this.#lastForwardedCommitWatermark = lastWatermark;\n const stream = await this.#source.startStream(\n lastWatermark,\n backfillRequests,\n );\n if (this.#pgChangeLogEnabled) {\n this.#storer.run().catch(e => stream.changes.cancel(e));\n }\n\n this.#stream = stream;\n if (\n this.#state.resetBackoff() >\n REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS\n ) {\n // After recovering from a backoff for which a replication status\n // error was published, publish an OK status\n this.#replicationStatusPublisher.publish(\n this.#lc,\n 'Replicating',\n `Replicating from ${lastWatermark}`,\n );\n }\n watermark = null;\n\n this.#acker.reset(stream.acks);\n for await (const change of stream.changes) {\n this.#acker.trackDownstream(change);\n\n const [type, msg] = change;\n switch (type) {\n case 'status':\n if (\n msg.lagReport &&\n msg.lagReport.lastTimings.commitTimeMs >=\n this.#latestLagReportCommitTimeMs\n ) {\n // Lag reports are not stored in the cdc change log, but rather\n // only forwarded on \"live\" connections. When a new subscriber\n // is catching up, it is initialized with the #latestStatus\n // from which it can measure lag while catching up.\n this.#latestStatus.lagReport = msg.lagReport;\n this.#latestLagReportCommitTimeMs =\n msg.lagReport.lastTimings.commitTimeMs;\n this.#forwarder.sendStatus(this.#latestStatus);\n }\n continue;\n case 'control':\n await this.#handleControlMessage(msg);\n continue; // control messages are not stored/forwarded\n case 'begin':\n watermark = change[2].commitWatermark;\n break;\n case 'commit':\n if (watermark !== change[2].watermark) {\n throw new UnrecoverableError(\n `commit watermark ${change[2].watermark} does not match 'begin' watermark ${watermark}`,\n );\n }\n this.#txCounter.add(1);\n break;\n default:\n if (type === 'data') {\n this.#changeCounter.add(1);\n }\n if (watermark === null) {\n throw new UnrecoverableError(\n `${type} change (${msg.tag}) received before 'begin' message`,\n );\n }\n break;\n }\n\n const serialized = serializeChangeStreamDataWithChange(change);\n const {json} = serialized;\n if (this.#pgChangeLogEnabled) {\n this.#storer.store(watermark, change, serialized);\n }\n // The SQLite change log commits at transaction boundaries, and its\n // commit for this transaction lands here -- before the forward of the\n // `commit` message, and before #recordForwardedTransactionBoundary\n // advances what #captureRequiredHead reads. No `await` separates it\n // from serialization or the optional PG enqueue above. This is the\n // assertable form of invariant 1: a synchronous SQLite commit in the\n // same loop iteration always precedes anything that can advance the\n // watermark this stream would resume from. Never throws; a write\n // failure disables the writer rather than stopping replication.\n this.#changeLogWriter?.write(change, json, serialized.change);\n const entry: WatermarkedChange = [watermark, change[1].tag, json];\n unflushedBytes += json.length;\n if (unflushedBytes < flushBytesThreshold) {\n // pipeline changes until flushBytesThreshold\n this.#forwarder.forward(entry);\n this.#recordForwardedTransactionBoundary(type, entry[0]);\n } else {\n // Wait for messages to clear socket buffers to ensure that they\n // make their way to subscribers. Without this `await`, the\n // messages end up being buffered in this process, which:\n // (1) results in memory pressure and increased GC activity\n // (2) prevents subscribers from processing the messages as they\n // arrive, instead getting them in a large batch after being\n // idle while they were queued (causing further delays).\n const forwarded = this.#forwarder.forwardWithFlowControl(entry);\n // forwardWithFlowControl synchronously sends the entry and updates\n // the Forwarder's transaction state before returning its flow-\n // control promise. Record the boundary before awaiting that promise\n // so registrations during the wait observe the forwarded state.\n this.#recordForwardedTransactionBoundary(type, entry[0]);\n await promiseOrAbort(\n forwarded,\n stream.changes.signal,\n this.#state.signal,\n );\n unflushedBytes = 0;\n }\n\n if (type === 'commit' || type === 'rollback') {\n watermark = null;\n }\n\n // Allow the PG storer to exert back pressure when it is enabled.\n const readyForMore = this.#pgChangeLogEnabled\n ? this.#storer.readyForMore()\n : undefined;\n if (readyForMore) {\n await promiseOrAbort(\n readyForMore,\n stream.changes.signal,\n this.#state.signal,\n );\n }\n }\n } catch (e) {\n err = e;\n } finally {\n this.#stream?.changes.cancel();\n this.#stream = undefined;\n }\n\n // When the change stream is interrupted, abort any pending transaction.\n if (watermark) {\n this.#lc.warn?.(`aborting interrupted transaction ${watermark}`);\n if (this.#pgChangeLogEnabled) {\n this.#storer.abort();\n }\n // Rolling back the log leaves no rows for the interrupted transaction,\n // so the next connection's reconciliation sees a head at or below its\n // resume watermark rather than a partial transaction.\n this.#changeLogWriter?.abort();\n // A rollback ends the log's open transaction without a commit\n // notification; wake any purge batch waiting for that window.\n this.#purgeScheduler?.onWriterIdle();\n this.#forwarder.forward([watermark, 'rollback', ROLLBACK_JSON]);\n this.#recordForwardedTransactionBoundary('rollback', watermark);\n }\n\n // Backoff and drain any pending entries in the storer before reconnecting.\n await Promise.all([\n this.#storer.stop(),\n this.#state.backoff(this.#lc, err),\n this.#state.retryDelay > REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS\n ? publishCriticalEvent(\n this.#lc,\n replicationStatusError(this.#lc, 'Replicating', err),\n )\n : promiseVoid,\n ]);\n }\n\n this.#forwarder.stopProgressMonitor();\n this.#lc.info?.('ChangeStreamer stopped');\n }\n\n async #handleControlMessage(msg: ChangeStreamControl[1]) {\n this.#lc.info?.('received control message', msg);\n const {tag} = msg;\n\n switch (tag) {\n case 'reset-required':\n await markResetRequired(\n this.#changeDBProvider('change-streamer-reset', 1),\n this.#shard,\n );\n await publishReplicationError(\n this.#lc,\n 'Replicating',\n msg.message ?? 'Resync required',\n msg.errorDetails,\n );\n if (this.#autoReset) {\n this.#lc.warn?.('shutting down for auto-reset');\n await this.stop(new AutoResetSignal());\n }\n break;\n default:\n unreachable(tag);\n }\n }\n\n async subscribe(ctx: SubscriberContext): Promise<Source<string>> {\n const {protocolVersion, id, mode, replicaVersion, watermark} = ctx;\n if (mode === 'serving') {\n this.#serving.resolve();\n }\n let cleanupSubscriber = () => {};\n const downstream = Subscription.create<string>({\n cleanup: () => cleanupSubscriber(),\n });\n // No subscriber's ACK advances the SQLite change log's head any more: the\n // writer runs in this process, so the barrier is notified from the commit\n // itself (see #changeLogWriter's onCommit).\n const subscriber = new Subscriber(\n protocolVersion,\n id,\n mode,\n watermark,\n downstream,\n () => this.#latestStatus,\n {},\n );\n const lc = this.#lc.withContext('subscriber', subscriber.id);\n const removeFromForwarder = () => {\n lc.info?.(`removing subscriber ${subscriber.id}`);\n this.#forwarder.remove(subscriber);\n };\n cleanupSubscriber = removeFromForwarder;\n if (replicaVersion !== this.#replicaVersion) {\n lc.warn?.(`rejecting subscriber at replica version ${replicaVersion}`);\n subscriber.close(\n ErrorType.WrongReplicaVersion,\n `current replica version is ${\n this.#replicaVersion\n } (requested ${replicaVersion})`,\n );\n } else {\n lc.info?.(`adding subscriber ${subscriber.id}`);\n\n const catchupFromPG = () => {\n assert(\n this.#pgChangeLogEnabled,\n 'cannot catch up from a disabled PG change log',\n );\n // Keep the existing PG registration/catchup lockstep unchanged when\n // SQLite was not selected before Forwarder.add().\n cleanupSubscriber = removeFromForwarder;\n this.#forwarder.add(subscriber);\n this.#storer.catchup(subscriber, mode);\n };\n const sqliteDecision = this.#selectSQLiteCatchup(lc, ctx);\n if (!sqliteDecision) {\n catchupFromPG();\n } else if (sqliteDecision.kind === 'rejected') {\n lc.warn?.(\n `${\n sqliteDecision.terminal ? 'rejecting' : 'ending'\n } subscription for ${ctx.id}: ${sqliteDecision.message}`,\n );\n if (sqliteDecision.terminal) {\n subscriber.close(ErrorType.WatermarkTooOld, sqliteDecision.message);\n } else {\n subscriber.close();\n }\n } else {\n const {catchup, reason, coverage, logWarm} = sqliteDecision;\n cleanupSubscriber = () => catchup.remove(subscriber);\n const registration = await catchup.catchup(\n subscriber,\n () => this.#captureRequiredHead(),\n {logWarm},\n );\n switch (registration.kind) {\n case 'registered':\n lc.debug?.(\n `serving ${ctx.id} from SQLite catchup`,\n ...(coverage ? [{sqliteChangeLogCoverage: coverage}] : []),\n );\n this.#recordCatchupRoute('sqlite', reason);\n break;\n case 'uncovered':\n if (this.#pgChangeLogEnabled) {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: subscriber watermark ` +\n `${ctx.watermark} is below the SQLite change-log minimum ` +\n registration.minWatermark,\n );\n this.#recordCatchupRoute('pg', 'watermark-uncovered');\n catchupFromPG();\n } else {\n const message =\n `earliest supported watermark is ` +\n `${registration.minWatermark} (requested ${ctx.watermark})`;\n lc.warn?.(\n `rejecting subscriber at watermark ${ctx.watermark}: ` +\n `the SQLite change log starts at ` +\n `${registration.minWatermark} and the PG change log is disabled`,\n );\n this.#recordCatchupRoute('none', 'watermark-uncovered');\n subscriber.close(ErrorType.WatermarkTooOld, message);\n }\n break;\n case 'declined':\n // Registration failed before the subscriber was committed to\n // SQLite. Use PG when enabled; otherwise end this subscription so\n // it retries. The coordinator has already tripped the breaker,\n // which keeps the retry off SQLite for the cooldown; it is\n // deliberately not closed here, since closing it would abort the\n // catchups of every other subscriber it is serving.\n if (this.#pgChangeLogEnabled) {\n lc.error?.(\n `serving ${ctx.id} from PG catchup: SQLite catchup ` +\n `registration failed`,\n registration.error,\n );\n this.#recordCatchupRoute('pg', 'registration-failed');\n catchupFromPG();\n } else {\n lc.error?.(\n `ending subscription for ${ctx.id} to retry SQLite catchup: ` +\n `registration failed and the PG change log is disabled`,\n registration.error,\n );\n this.#recordCatchupRoute('none', 'registration-failed');\n subscriber.fail(registration.error);\n }\n break;\n case 'handled':\n // The coordinator closed or failed the subscriber itself, so\n // there is nothing left to route. Still counted, so that the\n // route counter sums to the number of subscriptions.\n this.#recordCatchupRoute('sqlite', 'registration-handled');\n break;\n default:\n unreachable(registration);\n }\n }\n }\n // Any snapshot reservation held by this task can be closed now that\n // it is subscribed to the change stream.\n this.#reservations?.close(ctx.taskID);\n return downstream;\n }\n\n async startSnapshotReservation(\n taskID: string,\n ): Promise<Source<SnapshotMessage>> {\n if (!this.#reservations) {\n throw new Error('backups are not configured');\n }\n const downstream = this.#reservations.open(taskID);\n\n try {\n // Wait for an in-flight SQLite purge batch before reading and\n // advertising the reservation's bounds.\n await (this.#purgeScheduler?.pause(taskID) ?? promiseVoid);\n // A concurrent retry for this task may have superseded and cancelled\n // this reservation while its purge pause was settling. Only the current\n // owner may replace the task's source pin.\n if (!this.#reservations.isCurrent(taskID, downstream)) {\n return downstream;\n }\n // Pin after the purge pause has settled, so the SQLite minimum captured\n // by the router cannot move before it is advertised. #confirmReservations\n // skips an unpinned task while this await is in flight.\n this.#readRouter?.pin(taskID);\n // If a backup has been confirmed, immediately confirm the reservation.\n await this.#confirmReservations();\n return downstream;\n } catch (e) {\n // Cancel the reservation this call opened, not whatever currently\n // holds the task's slot: an overlapping retry for the same taskID may\n // have already replaced it, and a by-taskID close would tear down the\n // replacement and release its purge pause while it is advertising\n // snapshot bounds. cancel() routes through the subscription's cleanup,\n // which closes the reservation only if this instance still owns it.\n downstream.cancel();\n throw e;\n }\n }\n\n trackBackupWatermark(watermark: string) {\n this.#backupWatermark = watermark;\n this.#acker.trackBackup(watermark);\n // The durable backup floor is an independent input to each change-log\n // implementation. SQLite receives it even when the PG log has already\n // reached this watermark.\n this.#requestSQLitePurge('deferred');\n this.#maybeSchedulePGPurge();\n this.#maybeScheduleSQLitePurge();\n\n // Confirm any waiting reservations now that a backup has been confirmed.\n // Note that this is asynchronous and best effort; if it fails, the watermark\n // is still \"tracked\" and the confirmation will be retried on the next backup.\n void this.#confirmReservations().catch(e =>\n this.#lc.warn?.(`error confirming snapshot reservation`, e),\n );\n }\n\n async #confirmReservations() {\n const backupWatermark = this.#backupWatermark;\n const reservations = this.#reservations;\n if (\n backupWatermark === undefined ||\n !reservations?.confirmationsRequired()\n ) {\n return;\n }\n\n // Resolve PG bounds, when enabled, before touching any pin. Everything\n // below runs to completion without awaiting, which keeps a reservation's\n // advertised bounds and its pin in agreement: a concurrent /snapshot\n // retry for the same task replaces the reservation and re-pins it, and\n // confirming that replacement with the previous pin's bounds is exactly\n // the mismatch pinning exists to prevent.\n const pgState = this.#pgChangeLogEnabled\n ? await this.#getChangeLogState()\n : undefined;\n for (const taskID of reservations.unconfirmedTaskIDs()) {\n let route = this.#readRouter?.peek(taskID);\n // startSnapshotReservation pins only after an in-flight purge has\n // completed. A task with no pin yet is confirmed by that path instead.\n if (this.#readRouter && route === undefined) {\n continue;\n }\n\n // A transiently unavailable or broken SQLite log normally pins the\n // reservation to PG. With PG disabled, re-evaluate that choice on each\n // backup notification and keep the reservation pending until SQLite can\n // supply bounds.\n if (!this.#pgChangeLogEnabled && route?.source === 'pg') {\n this.#readRouter?.release(taskID);\n route = this.#readRouter?.pin(taskID);\n if (route?.source === 'pg') {\n if (reservations.noteConfirmationDelayed(taskID)) {\n this.#reservationConfirmDelays.add(1);\n }\n this.#lc.warn?.(\n `delaying snapshot reservation for ${taskID}: SQLite change log ` +\n `is unavailable and the PG change log is disabled`,\n );\n continue;\n }\n }\n\n if (route?.source === 'sqlite') {\n const coverage = must(\n route.coverage,\n 'a pinned SQLite route must carry its covered range',\n );\n if (\n this.#pgChangeLogEnabled &&\n coverage.minWatermark > backupWatermark\n ) {\n // A log seeded after this backup, most often. Holding the\n // reservation until a backup reaches the log's minimum would stall\n // a follower that PG can serve now, so move it -- pin included.\n this.#lc.info?.(\n `demoting ${taskID} to PG catchup: SQLite change-log minimum ` +\n `${coverage.minWatermark} is later than backupWatermark ` +\n backupWatermark,\n );\n this.#reservationDemotions.add(1);\n route = must(this.#readRouter).demote(taskID);\n }\n }\n\n const source = route?.source ?? 'pg';\n let minWatermark: string;\n if (route?.source === 'sqlite') {\n // Demotion above already moved every SQLite route the backup is\n // outside of, so this one covers it and confirms below.\n minWatermark = must(\n route.coverage,\n 'a pinned SQLite route must carry its covered range',\n ).minWatermark;\n } else {\n minWatermark = must(\n pgState,\n 'a PG reservation route requires the PG change log',\n ).minWatermark;\n }\n\n if (minWatermark <= backupWatermark) {\n reservations.confirmFor(\n taskID,\n this.#replicaVersion,\n backupWatermark,\n source,\n );\n } else {\n // PG cannot catch a restored replica up from this backup yet. Keep\n // the reservation pending until a later backup moves the durable\n // watermark into its covered range.\n if (reservations.noteConfirmationDelayed(taskID)) {\n this.#reservationConfirmDelays.add(1);\n }\n this.#lc.error?.(\n `${source} change-log minWatermark ${minWatermark} is later than ` +\n `backupWatermark ${backupWatermark}. Delaying confirmation of ` +\n `snapshot reservation until next backup.`,\n );\n }\n }\n }\n\n #maybeSchedulePGPurge(): void {\n const backupWatermark = this.#backupWatermark;\n if (\n !this.#pgChangeLogEnabled ||\n this.#pgPurgeScheduled ||\n this.#pgPurgeRunning ||\n backupWatermark === undefined ||\n this.#pgPurgedWatermark >= backupWatermark\n ) {\n return;\n }\n this.#pgPurgeScheduled = true;\n this.#state.setTimeout(() => {\n this.#pgPurgeScheduled = false;\n this.#pgPurgeRunning = true;\n return this.#purgePGChangeLog().finally(() => {\n this.#pgPurgeRunning = false;\n this.#maybeSchedulePGPurge();\n });\n }, CLEANUP_DELAY_MS);\n }\n\n #maybeScheduleSQLitePurge(): void {\n if (\n this.#sqlitePurgeScheduled ||\n this.#sqlitePurgeRunning ||\n this.#backupWatermark === undefined ||\n this.#sqlitePurgeContinuation === undefined\n ) {\n return;\n }\n const delay =\n this.#sqlitePurgeContinuation === 'immediate' ? 0 : CLEANUP_DELAY_MS;\n this.#sqlitePurgeScheduled = true;\n this.#state.setTimeout(() => {\n this.#sqlitePurgeScheduled = false;\n this.#sqlitePurgeRunning = true;\n return this.#purgeSQLiteChangeLog().finally(() => {\n this.#sqlitePurgeRunning = false;\n this.#maybeScheduleSQLitePurge();\n });\n }, delay);\n }\n\n async #getChangeLogState(): Promise<{\n replicaVersion: string;\n minWatermark: string;\n }> {\n const minWatermark = await this.#storer.getMinWatermarkForCatchup();\n if (!minWatermark) {\n this.#lc.warn?.(\n `Unexpected empty changeLog. Resync if \"Local replica watermark\" errors arise`,\n );\n }\n return {\n replicaVersion: this.#replicaVersion,\n minWatermark: minWatermark ?? this.#replicaVersion,\n };\n }\n\n #getCleanupFloor(): {\n backupWatermark: string;\n purgeWatermark: string;\n current: string[];\n } {\n const backupWatermark = this.#backupWatermark;\n assert(\n backupWatermark !== undefined,\n 'cleanup cannot run without a backup watermark',\n );\n const current = [\n ...this.#forwarder.getAcks(),\n ...(this.#reservations?.getReservedWatermarks() ?? []),\n ];\n // The cleanup delay above is the grace period for disconnected\n // subscribers to reconnect and expose their ACKs. Once it expires, an\n // empty set places no additional constraint on the confirmed backup\n // watermark and must not pin either change log indefinitely.\n return {\n backupWatermark,\n purgeWatermark: min(backupWatermark, ...current),\n current,\n };\n }\n\n async #purgePGChangeLog(): Promise<void> {\n try {\n const {backupWatermark, purgeWatermark, current} =\n this.#getCleanupFloor();\n if (purgeWatermark < backupWatermark) {\n if (this.#loggedBehindWatermark !== purgeWatermark) {\n this.#loggedBehindWatermark = purgeWatermark;\n this.#lc.info?.(\n `At least one client is behind backup ${backupWatermark}`,\n {watermarks: current},\n );\n }\n } else {\n this.#loggedBehindWatermark = undefined;\n }\n if (purgeWatermark <= this.#pgPurgedWatermark) {\n return;\n }\n this.#lc.info?.(`Purging PG changes before ${purgeWatermark} ...`);\n const start = performance.now();\n const deleted = await this.#storer.purgeRecordsBefore(purgeWatermark);\n const elapsed = (performance.now() - start).toFixed(2);\n this.#lc.info?.(\n `Purged ${deleted} PG changes before ${purgeWatermark} (${elapsed} ms)`,\n );\n this.#pgPurgedWatermark = purgeWatermark;\n } catch (e) {\n this.#lc.warn?.(`error purging the PG change log`, e);\n }\n }\n\n async #purgeSQLiteChangeLog(): Promise<void> {\n const scheduler = this.#purgeScheduler;\n if (!scheduler) {\n return;\n }\n try {\n const {backupWatermark, purgeWatermark} = this.#getCleanupFloor();\n // Consume the request before starting. A backup notification that\n // arrives during this pass records another request, which is merged with\n // the continuation returned by this pass rather than being overwritten.\n this.#sqlitePurgeContinuation = undefined;\n if (purgeWatermark < backupWatermark) {\n // Live constraint changes are not edge-triggered. Keep evaluating the\n // floor until it reaches the durable backup watermark, independently\n // of whether the PG implementation still exists.\n this.#requestSQLitePurge('deferred');\n }\n const result = await scheduler.purge(purgeWatermark);\n this.#requestSQLitePurge(result.continuation);\n } catch (e) {\n // purge() is fail-soft, but retain the coordinator's retry if an\n // unexpected caller-boundary failure escapes it.\n this.#requestSQLitePurge('deferred');\n this.#lc.warn?.(`error purging the SQLite change log`, e);\n }\n }\n\n #requestSQLitePurge(continuation: PurgeContinuation | undefined): void {\n if (!this.#purgeScheduler || continuation === undefined) {\n return;\n }\n if (\n continuation === 'immediate' ||\n this.#sqlitePurgeContinuation === undefined\n ) {\n this.#sqlitePurgeContinuation = continuation;\n }\n }\n\n async stop(err?: unknown) {\n this.#state.stop(this.#lc, err);\n this.#stream?.changes.cancel();\n this.#purgeScheduler?.stop();\n this.#comparator?.stop();\n this.#sqliteCatchup?.close();\n this.#changeLogWriter?.close();\n await Promise.allSettled([this.#storer.stop(), this.#source.stop()]);\n }\n\n #recordForwardedTransactionBoundary(\n type: ChangeStreamData[0],\n watermark: string,\n ) {\n switch (type) {\n case 'begin':\n assert(\n this.#currentTransactionCompletion === undefined,\n 'forwarded begin while a transaction is already in progress',\n );\n this.#currentTransactionCompletion =\n resolver<ForwardedTransactionCompletion>();\n this.#transactionForwardStartedAt = performance.now();\n break;\n case 'commit': {\n const completion = this.#currentTransactionCompletion;\n assert(completion, 'forwarded commit without a pending transaction');\n this.#recordTransactionForwardDuration();\n this.#lastForwardedCommitWatermark = watermark;\n this.#currentTransactionCompletion = undefined;\n completion.resolve({kind: 'committed', watermark});\n break;\n }\n case 'rollback': {\n const completion = this.#currentTransactionCompletion;\n assert(completion, 'forwarded rollback without a pending transaction');\n const committed = this.#lastForwardedCommitWatermark;\n assert(committed, 'last forwarded commit watermark is not initialized');\n this.#transactionForwardStartedAt = undefined;\n this.#currentTransactionCompletion = undefined;\n completion.resolve({kind: 'rolled-back', watermark: committed});\n break;\n }\n }\n }\n\n #recordTransactionForwardDuration() {\n const startedAt = this.#transactionForwardStartedAt;\n this.#transactionForwardStartedAt = undefined;\n if (startedAt !== undefined) {\n this.#transactionForwardDuration.recordMs(performance.now() - startedAt, {\n sqlite_change_log: this.#changeLogWriter?.enabled ? 'on' : 'off',\n });\n }\n }\n\n /**\n * Closes and forgets the cached catchup coordinator, so that a later\n * subscription re-opens the log from scratch rather than reading a handle\n * whose file is gone.\n */\n #closeSQLiteCatchup() {\n this.#sqliteCatchup?.close();\n this.#sqliteCatchup = undefined;\n }\n\n #captureRequiredHead(): string | Promise<string> {\n const committed = this.#lastForwardedCommitWatermark;\n assert(committed, 'last forwarded commit watermark is not initialized');\n return (\n this.#currentTransactionCompletion?.promise.then(\n completion => completion.watermark,\n ) ?? committed\n );\n }\n\n #selectSQLiteCatchup(\n lc: LogContext,\n ctx: SubscriberContext,\n ): SQLiteCatchupDecision | undefined {\n const opts = this.#sqliteCatchupOptions;\n if (this.#lastForwardedCommitWatermark === undefined || !opts) {\n return this.#fallbackOrReject(\n 'not-ready',\n 'SQLite catchup is not ready and the PG change log is disabled',\n );\n }\n // While the PG change log is available, backup subscribers retain its\n // existing recovery policy and cannot be moved by a canary selector.\n // Once PG is disabled, the backup replicator must use SQLite catchup: it\n // advances the canonical replica that Litestream backs up.\n if (ctx.mode === 'backup' && this.#pgChangeLogEnabled) {\n lc.info?.(`not serving backup subscriber ${ctx.id} from SQLite catchup`);\n return this.#fallbackOrReject(\n 'ineligible-mode',\n 'backup subscribers retain the PG change-log recovery policy',\n );\n }\n\n let route: ChangeLogReadRoute | undefined;\n if (this.#readRouter) {\n route = this.#readRouter.consume(ctx.taskID);\n if (route.source === 'pg') {\n if (route.reason === 'cold-log') {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: SQLite change log is still warming`,\n {sqliteChangeLogCoverage: route.coverage},\n );\n } else if (route.reason === 'log-unavailable') {\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `${opts.changeLogFile} is unavailable or incompatible`,\n );\n } else {\n lc.debug?.(\n `serving ${ctx.id} from PG catchup: SQLite route ${route.reason}`,\n ...(route.coverage\n ? [{sqliteChangeLogCoverage: route.coverage}]\n : []),\n );\n }\n return this.#fallbackOrReject(\n route.reason,\n `SQLite catchup route ${route.reason} is unavailable and the PG ` +\n `change log is disabled`,\n );\n }\n const coverage = route.coverage;\n assert(coverage, 'a SQLite route must carry its covered range');\n if (ctx.watermark < coverage.minWatermark) {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: subscriber watermark ` +\n `${ctx.watermark} is below the SQLite change-log minimum ` +\n coverage.minWatermark,\n {sqliteChangeLogCoverage: coverage},\n );\n return this.#fallbackOrReject(\n 'watermark-uncovered',\n `earliest supported watermark is ${coverage.minWatermark} ` +\n `(requested ${ctx.watermark})`,\n true,\n );\n }\n }\n\n // `shouldUse` remains as a test hook and an optional extra policy gate.\n // Production selection comes from #readRouter.\n if (!this.#readRouter && !opts.shouldUse?.(ctx)) {\n return this.#fallbackOrReject(\n 'selector',\n 'SQLite catchup was not selected and the PG change log is disabled',\n );\n }\n if (this.#readRouter && opts.shouldUse && !opts.shouldUse(ctx)) {\n return this.#fallbackOrReject(\n 'selector',\n 'SQLite catchup was not selected and the PG change log is disabled',\n );\n }\n\n const catchup =\n this.#sqliteCatchup ?? this.#openSQLiteCatchup(lc, opts, ctx);\n if (catchup) {\n return {\n kind: 'selected',\n catchup,\n reason: route?.reason ?? 'selector',\n coverage: route?.coverage,\n // Legacy focused-test selection has no warm classification.\n logWarm:\n route === undefined ? undefined : route.reason !== 'selected-cold',\n };\n } else {\n return this.#fallbackOrReject(\n 'log-unavailable',\n 'SQLite change log is unavailable and the PG change log is disabled',\n );\n }\n }\n\n #fallbackOrReject(\n reason: string,\n message: string,\n terminal = false,\n ): SQLiteCatchupRejection | undefined {\n this.#recordCatchupRoute(this.#pgChangeLogEnabled ? 'pg' : 'none', reason);\n return this.#pgChangeLogEnabled\n ? undefined\n : {kind: 'rejected', message, terminal};\n }\n\n #recordCatchupRoute(source: 'pg' | 'sqlite' | 'none', reason: string): void {\n this.#catchupRoutes.add(1, {source, reason});\n }\n\n /**\n * Opens the change log and, if it can serve, wraps it in the coordinator that\n * subsequent subscriptions reuse.\n *\n * The log may not exist yet -- the writer creates it at the stream loop's\n * first reconcile, and deletes it when it fails soft -- or may exist without\n * content, or may be unreadable. None of those can be allowed to fail a\n * subscription:\n * this is the last point at which PG catchup can be selected when it is\n * enabled -- past `Forwarder.add()` the subscriber is committed to SQLite --\n * so each declines here instead. With PG disabled, the caller ends the\n * subscription so that it retries. Neither the failure nor the reader is\n * retained, so a later subscription retries from scratch.\n */\n #openSQLiteCatchup(\n lc: LogContext,\n opts: SQLiteCatchupOptions,\n ctx: SubscriberContext,\n ): SQLiteChangeLogCatchup | undefined {\n let reader: SQLiteChangeLogReader | undefined;\n try {\n reader = new SQLiteChangeLogReader(this.#lc, opts.changeLogFile);\n // `plan()` doubles as the readiness check: it reports `not-ready` when\n // the writer has not created or seeded the stream table. The barrier\n // cannot wait its way out of that, since a log with no head can never\n // reach the required one.\n if (reader.plan(ctx.watermark).kind === 'not-ready') {\n reader.close();\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `${opts.changeLogFile} has no changes to serve yet`,\n );\n return undefined;\n }\n } catch (e) {\n // An absent file is the common case, since a readonly handle cannot\n // create one. A corrupt or truncated file lands here too, and is equally\n // not a reason to fail the subscription.\n reader?.close();\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `cannot read ${opts.changeLogFile}`,\n e,\n );\n return undefined;\n }\n this.#changeLogUnavailableSince = undefined;\n // Readiness is checked once, on the way to the cached coordinator: a log\n // with content keeps it. Purging preserves the latest transaction as a\n // catchup boundary, and `reconcileChangeLog` reseeds inside a single\n // transaction, so a reader never observes an emptied log.\n this.#sqliteCatchup = new SQLiteChangeLogCatchup(\n this.#lc,\n this.#forwarder,\n reader,\n {\n batchSize: opts.readBatchRows,\n barrierTimeoutMs: opts.barrierTimeoutMs,\n barrierPollIntervalMs: opts.barrierPollIntervalMs,\n cleanupGuard: opts.cleanupGuard,\n onFailure: failure => {\n this.#lc.warn?.(\n `temporarily disabling SQLite catchup after ${failure}`,\n );\n this.#readRouter?.trip();\n },\n },\n );\n return this.#sqliteCatchup;\n }\n\n /**\n * Reports falling back to PG catchup, tracking how long the log has been\n * unavailable so that a subscription that merely arrived before the writer's\n * first reconcile does not look like an incident.\n */\n #declineSQLiteCatchup(\n lc: LogContext,\n ctx: SubscriberContext,\n opts: SQLiteCatchupOptions,\n reason: string,\n error?: unknown,\n ): void {\n const now = Date.now();\n this.#changeLogUnavailableSince ??= now;\n const unavailableMs = now - this.#changeLogUnavailableSince;\n const threshold =\n opts.notReadyWarnThresholdMs ??\n DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS;\n lc[unavailableMs >= threshold ? 'warn' : 'debug']?.(\n (this.#pgChangeLogEnabled\n ? `serving ${ctx.id} from PG catchup: `\n : `cannot serve ${ctx.id} from SQLite catchup: `) +\n `${reason} ` +\n `(unavailable for ${unavailableMs} ms)`,\n ...(error === undefined ? [] : [error]),\n );\n }\n}\n\ntype ForwardedTransactionCompletion =\n | {kind: 'committed'; watermark: string}\n | {kind: 'rolled-back'; watermark: string};\n\ntype SQLiteCatchupSelection = {\n readonly kind: 'selected';\n readonly catchup: SQLiteChangeLogCatchup;\n readonly reason: string;\n readonly coverage: SQLiteChangeLogCoverage | undefined;\n /**\n * Whether the router classified the log as warm for this subscriber, or\n * undefined when selection did not run through the router (focused tests).\n */\n readonly logWarm: boolean | undefined;\n};\n\ntype SQLiteCatchupRejection = {\n readonly kind: 'rejected';\n readonly message: string;\n /** A terminal rejection forces the view-syncer to restore a fresh replica. */\n readonly terminal: boolean;\n};\n\ntype SQLiteCatchupDecision = SQLiteCatchupSelection | SQLiteCatchupRejection;\n\n// The delay between receiving an initial, backup-based watermark\n// and performing a check of whether to purge records before it.\n// This delay should be long enough to handle situations like the following:\n//\n// 1. `litestream restore` downloads a backup for the `replication-manager`\n// 2. `replication-manager` starts up and runs this `change-streamer`\n// 3. `zero-cache`s that are running on a different replica connect to this\n// `change-streamer` after exponential backoff retries.\n//\n// It is possible for a `zero-cache`[3] to be behind the backup restored [1].\n// This cleanup delay (30 seconds) is thus set to be a value comfortably\n// longer than the max delay for exponential backoff (10 seconds) in\n// `services/running-state.ts`. This allows the `zero-cache` [3] to reconnect\n// so that the `change-streamer` can track its progress and know when it has\n// surpassed the initial watermark of the backup [1].\nconst CLEANUP_DELAY_MS = DEFAULT_MAX_RETRY_DELAY_MS * 3;\n\nconst ROLLBACK_JSON = JSON.stringify([\n 'rollback',\n {tag: 'rollback'},\n] satisfies Rollback);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmMA,eAAsB,mBACpB,IACA,OACA,QACA,kBACA,mBACA,UACA,cACA,4BACA,mBACA,cACA,WACA,WACA,MACA,eAAe,YACiB;CAChC,MAAM,wBACJ,IACA,UACA,mBACA,OACA,WACA,aAAa,KAAA,GACb,cACA,KAAK,kBACP;CAOA,MAAM,oBACJ,iBACA,QAEA,SACE,KACE;EAAC;EAAK,YAAY,GAAE,qBAAqB,gBAAe;CAAC,GAKzD,SAAS,OACX,CACF;CAEF,MAAM,EAAC,mBAAkB;CACzB,OAAO,IAAI,mBACT,IACA,OACA,QACA,kBACA,mBACA,kBACA,gBACA,cACA,4BACA,cACA,WACA,WACA,MACA,YACF;AACF;AAEA,IAAM,8CAA8C;AAQpD,IAAM,mDAAmD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4IzD,IAAM,qBAAN,MAA0D;CACxD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CAUA,WAAoB,SAAS;CAE7B,aAAsB,mBACpB,eACA,gBACA,kCACF;CACA,iBAA0B,mBACxB,eACA,WACA,qDACF;CAIA,8BAAuC,4BACrC,eACA,gCACA,yIAEF;CACA,iBAA0B,mBACxB,eACA,oCACA,sEACF;CACA,wBAAiC,mBAC/B,eACA,2CACA,qPAIF;CACA,4BAAqC,mBACnC,eACA,gDACA,2LAGF;CAEA;CACA,+BAA+B;CAC/B;CACA,qBAA6B;;;;;;;CAO7B;CACA;CACA;CAGA,oBAAoB;CACpB,kBAAkB;CAClB,wBAAwB;CACxB,sBAAsB;CACtB;CACA;CACA;CACA;CACA;CACA;CAIA,YACE,IACA,OACA,QACA,kBACA,mBACA,kBACA,gBACA,QACA,4BACA,cACA,kBACA,WACA,MACA,eAAe,YACf;EACA,KAAK,KAAK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"}
1
+ {"version":3,"file":"change-streamer-service.js","names":["#lc","#shard","#changeDBProvider","#replicaVersion","#source","#storer","#pgChangeLogEnabled","#forwarder","#reservations","#replicationStatusPublisher","#sqliteCatchupOptions","#changeLogWriter","#purgeScheduler","#comparator","#acker","#initializer","#readRouter","#autoReset","#state","#serving","#txCounter","#changeCounter","#transactionForwardDuration","#catchupRoutes","#reservationDemotions","#reservationConfirmDelays","#sqliteCatchup","#closeSQLiteCatchup","#purgeLock","#latestStatus","#latestLagReportCommitTimeMs","#lastForwardedCommitWatermark","#stream","#handleControlMessage","#recordForwardedTransactionBoundary","#selectSQLiteCatchup","#captureRequiredHead","#recordCatchupRoute","#confirmReservations","#backupWatermark","#requestSQLitePurge","#maybeSchedulePGPurge","#maybeScheduleSQLitePurge","#getChangeLogState","#pgPurgeScheduled","#pgPurgeRunning","#pgPurgedWatermark","#purgePGChangeLog","#sqlitePurgeScheduled","#sqlitePurgeRunning","#sqlitePurgeContinuation","#purgeSQLiteChangeLog","#getCleanupFloor","#loggedBehindWatermark","#currentTransactionCompletion","#transactionForwardStartedAt","#recordTransactionForwardDuration","#fallbackOrReject","#declineSQLiteCatchup","#openSQLiteCatchup","#changeLogUnavailableSince"],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-service.ts"],"sourcesContent":["import {getDefaultHighWaterMark} from 'node:stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {defu} from 'defu';\nimport postgres, {type Options, type PostgresType} from 'postgres';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseOrAbort} from '../../../../shared/src/promise-race.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {publishCriticalEvent} from '../../observability/events.ts';\nimport {\n getOrCreateCounter,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {min} from '../../types/lexi-version.ts';\nimport type {PostgresDB} from '../../types/pg.ts';\nimport type {ShardID} from '../../types/shards.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport type {\n ChangeSource,\n ChangeStream,\n} from '../change-source/change-source.ts';\nimport {\n type ChangeStreamControl,\n type ChangeStreamData,\n type Rollback,\n} from '../change-source/protocol/current/downstream.ts';\nimport type {LitestreamVersion} from '../litestream/metrics.ts';\nimport {\n publishReplicationError,\n replicationStatusError,\n type ReplicationStatusPublisher,\n} from '../replicator/replication-status.ts';\nimport type {SubscriptionState} from '../replicator/schema/replication-state.ts';\nimport {\n DEFAULT_MAX_RETRY_DELAY_MS,\n RunningState,\n UnrecoverableError,\n} from '../running-state.ts';\nimport {serializeChangeStreamDataWithChange} from './change-log-codec.ts';\nimport {\n ChangeLogInitializer,\n replicaInitializationSource,\n} from './change-log-initializer.ts';\nimport {\n type ChangeStreamerService,\n type Status,\n type SubscriberContext,\n type WatermarkedChange,\n} from './change-streamer.ts';\nimport * as ErrorType from './error-type-enum.ts';\nimport {Forwarder} from './forwarder.ts';\nimport {\n AutoResetSignal,\n ensureReplicationConfig,\n markResetRequired,\n} from './schema/tables.ts';\nimport {SnapshotReservations} from './snapshot-reservations.ts';\nimport type {SnapshotMessage} from './snapshot.ts';\nimport {\n SQLiteChangeLogCatchup,\n type SQLiteChangeLogCleanupGuard,\n} from './sqlite-change-log-catchup.ts';\nimport {\n SQLiteChangeLogComparator,\n type SQLiteChangeLogCompareOptions,\n} from './sqlite-change-log-comparator.ts';\nimport {\n SQLiteChangeLogPurgeScheduler,\n type PurgeContinuation,\n type SQLiteChangeLogPurgeSchedulerOptions,\n} from './sqlite-change-log-purge-scheduler.ts';\nimport {\n inspectSQLiteChangeLog,\n SQLiteChangeLogReadRouter,\n type ChangeLogReadRoute,\n type SQLiteChangeLogCoverage,\n} from './sqlite-change-log-read-router.ts';\nimport {SQLiteChangeLogReader} from './sqlite-change-log-reader.ts';\nimport {\n SQLiteChangeLogWriter,\n type SQLiteChangeLogWriterOptions,\n} from './sqlite-change-log-writer.ts';\nimport type {PostgresDBProvider} from './storer.ts';\nimport {\n Storer,\n type PurgeLock,\n type TuningOptions as StorerOptions,\n} from './storer.ts';\nimport {Subscriber} from './subscriber.ts';\nimport {UpstreamAcker} from './upstream-acker.ts';\n\nexport type BackupConfig = {\n backupURL: string;\n litestreamVersion: LitestreamVersion;\n};\n\nexport type SQLiteCatchupOptions = {\n /** The change-log database, i.e. `changeLogFileName(replicaFile)`. */\n changeLogFile: string;\n readBatchRows: number;\n barrierTimeoutMs: number;\n /**\n * Backstop for the change-log writer's ACK, which is what normally releases\n * the barrier. Defaults to the catchup coordinator's interval.\n */\n barrierPollIntervalMs?: number | undefined;\n /**\n * Optional policy hook used by focused tests. Production canary selection is\n * supplied by `sqliteChangeLogServe` and is stable by shard plus task ID.\n * Backup subscribers are rejected before either selector is invoked.\n */\n shouldUse?: ((ctx: SubscriberContext) => boolean) | undefined;\n /**\n * Overrides the purge scheduler's guard, for tests. When absent, the\n * service supplies the scheduler's real writer-serialized guard, or the\n * catchup's no-op default when purging is not configured.\n */\n cleanupGuard?: SQLiteChangeLogCleanupGuard | undefined;\n /**\n * How long the change log may be unavailable before declining to serve from\n * it is reported as a warning rather than a debug line. Defaults to\n * {@link DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS}.\n */\n notReadyWarnThresholdMs?: number | undefined;\n};\n\nexport type SQLiteChangeLogServeOptions = {\n /** Stable percentage of eligible serving tasks routed to SQLite. */\n readPercent: number;\n /**\n * Stable percentage of eligible serving tasks routed to a log that has not\n * yet aged through {@link retentionMs}. Zero keeps a reseeded log on PG for\n * that whole window; above zero it serves that share of tasks, and a\n * reservation it cannot cover is demoted rather than held pending.\n * Defaults to zero.\n */\n coldReadPercent?: number | undefined;\n /** The warm-up window a reseeded log ages through. */\n retentionMs: number;\n /** Injectable for deterministic breaker tests. */\n failureCooldownMs?: number | undefined;\n /** Injectable for deterministic warm-up and breaker tests. */\n now?: (() => number) | undefined;\n /** Injectable readiness inspection for service-level tests. */\n inspect?: (() => SQLiteChangeLogCoverage | undefined) | undefined;\n};\n\nexport type TuningOptions = StorerOptions & {\n /**\n * Keeps the legacy Postgres change log on the initialization, persistence,\n * catchup, purge, and ACK paths. Disable only after SQLite and the replica\n * backup are authoritative for all of those responsibilities.\n */\n pgChangeLogEnabled: boolean;\n flowControlConsensusTimeoutProportion: number;\n flowControlSlowSubscriberGracePeriodMs?: number | undefined;\n sqliteCatchup?: SQLiteCatchupOptions | undefined;\n /**\n * Supplied when `sqliteChangeLogMode != off`, i.e. this is the gate on the\n * change-log writer. Absent, nothing writes the log.\n */\n sqliteChangeLogWriter?:\n | Omit<\n SQLiteChangeLogWriterOptions,\n 'onCommit' | 'onDisabled' | 'onRebuilt'\n >\n | undefined;\n /**\n * Also supplied when `sqliteChangeLogMode != off`, and deliberately not\n * gated on the read path: in `write` mode no reader ever opens, and this is\n * the configuration the purge scheduler actually ships in. The scheduler\n * runs on the writer's own connection, so with the writer absent (mode\n * `off`) it is never constructed, and a leftover file on disk is left to\n * the replicator's cleanup.\n */\n sqliteChangeLogPurge?: SQLiteChangeLogPurgeSchedulerOptions | undefined;\n /**\n * Supplied in `compare` mode and later modes.\n * `replicaFile` enables the initialization comparison.\n * The other fields configure sampled catchup comparisons.\n * Both checks are advisory, and Postgres remains authoritative.\n * The comparator also requires the writer and catchup options.\n */\n sqliteChangeLogCompare?:\n | (SQLiteChangeLogCompareOptions & {replicaFile: string})\n | undefined;\n /** Supplied only in `serve` mode. A zero percentage keeps every read on PG. */\n sqliteChangeLogServe?: SQLiteChangeLogServeOptions | undefined;\n};\n\n/**\n * Performs initialization and schema migrations to initialize a ChangeStreamerImpl.\n */\nexport async function initializeStreamer(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n changeDB: PostgresDB,\n changeSource: ChangeSource,\n replicationStatusPublisher: ReplicationStatusPublisher,\n subscriptionState: SubscriptionState,\n backupConfig: BackupConfig | null,\n purgeLock: PurgeLock | null,\n autoReset: boolean,\n opts: TuningOptions,\n setTimeoutFn = setTimeout,\n): Promise<ChangeStreamerService> {\n await ensureReplicationConfig(\n lc,\n changeDB,\n subscriptionState,\n shard,\n autoReset,\n purgeLock ?? undefined,\n setTimeoutFn,\n opts.pgChangeLogEnabled,\n );\n\n // Dynamically creates connection pools that the implementation uses to\n // isolate concurrent, long-running transactions. This works around a bug in\n // the postgres.js client where connections get swapped in certain conditions.\n //\n // https://github.com/porsager/postgres/issues/1204\n const changeDBProvider: PostgresDBProvider = (\n applicationName: string,\n max: number,\n ) =>\n postgres(\n defu(\n {max, connection: {['application_name']: applicationName}},\n // ParsedOptions are technically compatible with Options, but happen\n // to not be typed that way. The postgres.js author does an equivalent\n // merge of ParsedOptions and Options here:\n // https://github.com/porsager/postgres/blob/089214e85c23c90cf142d47fb30bd03f42874984/src/subscribe.js#L13\n changeDB.options as unknown as Options<Record<string, PostgresType>>,\n ),\n ) as PostgresDB;\n\n const {replicaVersion} = subscriptionState;\n return new ChangeStreamerImpl(\n lc,\n shard,\n taskID,\n discoveryAddress,\n discoveryProtocol,\n changeDBProvider,\n replicaVersion,\n changeSource,\n replicationStatusPublisher,\n backupConfig,\n purgeLock,\n autoReset,\n opts,\n setTimeoutFn,\n );\n}\n\nconst REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS = 5000;\n\n// How long the change log may be unavailable before declining to serve from it\n// stops looking like normal startup ordering. Subscriptions can arrive before\n// the stream loop's first reconcile has created and seeded the log, so early\n// declines are expected. Still declining a minute later means the log is not\n// being written at all -- `sqliteChangeLogMode=off`, a writer that has failed\n// soft, or a path mismatch -- which is worth surfacing.\nconst DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS = 60_000;\n\n/**\n * Upstream-agnostic dispatch of messages in a {@link ChangeStreamMessage} to a\n * {@link Forwarder} and {@link Storer} to execute the forward-store-ack\n * procedure described in {@link ChangeStreamer}.\n *\n * ### Subscriber Catchup\n *\n * Connecting clients first need to be \"caught up\" to the current watermark\n * (from stored change log entries) before new entries are forwarded to\n * them. This is non-trivial because the replication stream may be in the\n * middle of a pending streamed Transaction for which some entries have\n * already been forwarded but are not yet committed to the store.\n *\n *\n * ```\n * ------------------------------- - - - - - - - - - - - - - - - - - - -\n * | Historic changes in storage | Pending (streamed) tx | Next tx\n * ------------------------------- - - - - - - - - - - - - - - - - - - -\n * Replication stream\n * > > > > > > > > >\n * ^ ---> required catchup ---> ^\n * Subscriber watermark Subscription begins\n * ```\n *\n * Preemptively buffering the changes of every pending transaction\n * would be wasteful and consume too much memory for large transactions.\n *\n * Instead, the streamer synchronously dispatches changes and subscriptions\n * to the {@link Forwarder} and the {@link Storer} such that the two\n * components are aligned as to where in the stream the subscription started.\n * The two components then coordinate catchup and handoff via the\n * {@link Subscriber} object with the following algorithm:\n *\n * * If the streamer is in the middle of a pending Transaction, the\n * Subscriber is \"queued\" on both the Forwarder and the Storer. In this\n * state, new changes are *not* forwarded to the Subscriber, and catchup\n * is not yet executed.\n * * Once the commit message for the pending Transaction is processed\n * by the Storer, it begins catchup on the Subscriber (with a READONLY\n * snapshot so that it does not block subsequent storage operations).\n * This catchup is thus guaranteed to load the change log entries of\n * that last Transaction.\n * * When the Forwarder processes that same commit message, it moves the\n * Subscriber from the \"queued\" to the \"active\" set of clients such that\n * the Subscriber begins receiving new changes, starting from the next\n * Transaction.\n * * The Subscriber does not forward those changes, however, if its catchup\n * is not complete. Until then, it buffers the changes in memory.\n * * Once catchup is complete, the buffered changes are immediately sent\n * and the Subscriber henceforth forwards changes as they are received.\n *\n * In the (common) case where the streamer is not in the middle of a pending\n * transaction when a subscription begins, the Storer begins catchup\n * immediately and the Forwarder directly adds the Subscriber to its active\n * set. However, the Subscriber still buffers any forwarded messages until\n * its catchup is complete.\n *\n * ### Watermarks and ordering\n *\n * The ChangeStreamerService depends on its {@link ChangeSource} to send\n * changes in contiguous [`begin`, `data` ..., `data`, `commit`] sequences\n * in commit order. This follows Postgres's Logical Replication Protocol\n * Message Flow:\n *\n * https://www.postgresql.org/docs/16/protocol-logical-replication.html#PROTOCOL-LOGICAL-MESSAGES-FLOW\n *\n * > The logical replication protocol sends individual transactions one by one.\n * > This means that all messages between a pair of Begin and Commit messages belong to the same transaction.\n *\n * In order to correctly replay (new) and filter (old) messages to subscribers\n * at different points in the replication stream, these changes must be assigned\n * watermarks such that they preserve the order in which they were received\n * from the ChangeSource.\n *\n * A previous implementation incorrectly derived these watermarks from the Postgres\n * Log Sequence Numbers (LSN) of each message. However, LSNs from concurrent,\n * non-conflicting transactions can overlap, which can result in a `begin` message\n * with an earlier LSN arriving after a `commit` message. For example, the\n * changes for these transactions:\n *\n * ```\n * LSN: 1 2 3 4 5 6 7 8 9 10\n * tx1: begin data data data commit\n * tx2: begin data data data commit\n * ```\n *\n * will arrive as:\n *\n * ```\n * begin1, data2, data4, data6, commit8, begin3, data5, data7, data9, commit10\n * ```\n *\n * Thus, LSN of non-commit messages are not suitable for tracking the sorting\n * order of the replication stream.\n *\n * Instead, the ChangeStreamer uses the following algorithm for deterministic\n * catchup and filtering of changes:\n *\n * * A `commit` message is assigned to a watermark corresponding to its LSN.\n * These are guaranteed to be in commit order by definition.\n *\n * * `begin` and `data` messages are assigned to the watermark of the\n * preceding `commit` (the previous transaction, or the replication\n * slot's starting LSN) plus 1. This guarantees that they will be sorted\n * after the previously commit transaction even if their LSNs came before it.\n * This is referred to as the `preCommitWatermark`.\n *\n * * In the ChangeLog DB, messages have a secondary sort column `pos`, which is\n * the position of the message within its transaction, with the `begin` message\n * starting at `0`. This guarantees that `begin` and `data` messages will be\n * fetched in the original ChangeSource order during catchup.\n *\n * `begin` and `data` messages share the same watermark, but this is sufficient for\n * Subscriber filtering because subscribers only know about the `commit` watermarks\n * exposed in the `Downstream` `Commit` message. The Subscriber object thus compares\n * the internal watermarks of the incoming messages against the commit watermark of\n * the caller, updating the watermark at every `Commit` message that is forwarded.\n *\n * ### Cleanup\n *\n * As mentioned in the {@link ChangeStreamer} documentation: \"the ChangeStreamer\n * uses a combination of [the \"initial\", i.e. backup-derived watermark and] ACK\n * responses from connected subscribers to determine the watermark up\n * to which it is safe to purge old change log entries.\"\n *\n * More concretely:\n *\n * * The `initial`, backup-derived watermark is the earliest to which cleanup\n * should ever happen.\n *\n * * However, it is possible for the replica backup to be *ahead* of a connected\n * subscriber; and if a network error causes that subscriber to retry from its\n * last watermark, the change streamer must support it.\n *\n * Thus, before cleaning up to an `initial` backup-derived watermark, the change\n * streamer first confirms that all connected subscribers have also passed\n * that watermark.\n */\nclass ChangeStreamerImpl implements ChangeStreamerService {\n readonly id: string;\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #changeDBProvider: PostgresDBProvider;\n readonly #replicaVersion: string;\n readonly #source: ChangeSource;\n readonly #storer: Storer;\n readonly #pgChangeLogEnabled: boolean;\n readonly #forwarder: Forwarder;\n readonly #reservations: SnapshotReservations | undefined;\n readonly #replicationStatusPublisher: ReplicationStatusPublisher;\n readonly #sqliteCatchupOptions: SQLiteCatchupOptions | undefined;\n readonly #changeLogWriter: SQLiteChangeLogWriter | undefined;\n readonly #purgeScheduler: SQLiteChangeLogPurgeScheduler | undefined;\n readonly #comparator: SQLiteChangeLogComparator | undefined;\n readonly #acker: UpstreamAcker;\n readonly #initializer: ChangeLogInitializer;\n readonly #readRouter: SQLiteChangeLogReadRouter | undefined;\n\n readonly #autoReset: boolean;\n readonly #state: RunningState;\n\n // Starting the (Postgres) ChangeStream results in killing the previous\n // Postgres subscriber, potentially creating a gap in which the old\n // change-streamer has shut down and the new change-streamer has not yet\n // been recognized as \"healthy\" (and thus does not get any requests).\n //\n // To minimize this gap, delay starting the ChangeStream until the first\n // request from a `serving` replicator, indicating that higher level\n // load-balancing / routing logic has begun routing requests to this task.\n readonly #serving = resolver();\n\n readonly #txCounter = getOrCreateCounter(\n 'replication',\n 'transactions',\n 'Count of replicated transactions',\n );\n readonly #changeCounter = getOrCreateCounter(\n 'replication',\n 'changes',\n 'Count of replicated changes (DML or DDL statements)',\n );\n // The number the SQLite change-log commit lands in. It is labeled by whether\n // the log is being written so that the cost of putting a commit on the\n // forward path is attributable rather than inferred.\n readonly #transactionForwardDuration = getOrCreateLatencyHistogram(\n 'replication',\n 'transaction_forward_duration',\n \"Time from receiving a transaction's `begin` to forwarding its `commit`, \" +\n 'i.e. the change-streamer half of forward-to-subscriber latency.',\n );\n readonly #catchupRoutes = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.catchup_routes',\n 'Catchup subscriptions by selected source and low-cardinality reason.',\n );\n readonly #reservationDemotions = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.reservation_demotions',\n 'Snapshot reservations demoted from SQLite to PG because the change ' +\n 'log could not cover the backup being restored. With PG retired ' +\n 'these followers have no fallback, so this is the rate at which one ' +\n 'would instead have to wait for a later backup.',\n );\n readonly #reservationConfirmDelays = getOrCreateCounter(\n 'replication',\n 'sqlite_change_log.reservation_confirm_delays',\n 'Snapshot reservations whose confirmation was deferred because the ' +\n \"selected source's change-log minimum was later than the backup \" +\n 'watermark. Counted once per reservation, by that source.',\n );\n\n #latestStatus: Status;\n #latestLagReportCommitTimeMs = 0;\n #backupWatermark: string | undefined;\n #pgPurgedWatermark: string = '';\n /**\n * The floor last logged as held back by a laggard. The level-triggered\n * retry re-evaluates every {@link CLEANUP_DELAY_MS}; logging only when the\n * blocking floor moves keeps a stuck subscriber from emitting the same\n * line indefinitely.\n */\n #loggedBehindWatermark: string | undefined;\n #sqlitePurgeContinuation: PurgeContinuation | undefined;\n #purgeLock: PurgeLock | null;\n // PG and SQLite intentionally own separate level-triggered loops. Neither\n // waits for, advances, or retries the other.\n #pgPurgeScheduled = false;\n #pgPurgeRunning = false;\n #sqlitePurgeScheduled = false;\n #sqlitePurgeRunning = false;\n #stream: ChangeStream | undefined;\n #sqliteCatchup: SQLiteChangeLogCatchup | undefined;\n #changeLogUnavailableSince: number | undefined;\n #lastForwardedCommitWatermark: string | undefined;\n #transactionForwardStartedAt: number | undefined;\n #currentTransactionCompletion:\n | Resolver<ForwardedTransactionCompletion>\n | undefined;\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n changeDBProvider: PostgresDBProvider,\n replicaVersion: string,\n source: ChangeSource,\n replicationStatusPublisher: ReplicationStatusPublisher,\n backupConfig: BackupConfig | null,\n initialPurgeLock: PurgeLock | null,\n autoReset: boolean,\n opts: TuningOptions,\n setTimeoutFn = setTimeout,\n ) {\n this.id = `change-streamer`;\n this.#lc = lc.withContext('component', 'change-streamer');\n this.#shard = shard;\n this.#changeDBProvider = changeDBProvider;\n this.#replicaVersion = replicaVersion;\n this.#source = source;\n this.#pgChangeLogEnabled = opts.pgChangeLogEnabled;\n if (!this.#pgChangeLogEnabled) {\n assert(\n opts.sqliteChangeLogWriter &&\n opts.sqliteChangeLogPurge &&\n opts.sqliteCatchup &&\n opts.sqliteChangeLogServe?.readPercent === 100 &&\n opts.sqliteChangeLogServe.coldReadPercent === 100 &&\n backupConfig?.litestreamVersion === 'v5',\n 'disabling the PG change log requires SQLite serving at 100 percent and a v5 backup',\n );\n assert(\n opts.sqliteChangeLogCompare === undefined,\n 'SQLite change-log comparison requires the PG change log',\n );\n }\n this.#storer = new Storer(\n lc,\n shard,\n taskID,\n discoveryAddress,\n discoveryProtocol,\n changeDBProvider,\n replicaVersion,\n consumed => this.#acker.trackPgChangeLog(consumed[2].watermark),\n err => this.stop(err),\n opts,\n );\n this.#forwarder = new Forwarder(lc, {\n flowControlConsensusTimeoutProportion:\n opts.flowControlConsensusTimeoutProportion,\n flowControlSlowSubscriberGracePeriodMs:\n opts.flowControlSlowSubscriberGracePeriodMs,\n });\n const serveOptions = opts.sqliteChangeLogServe;\n const writerOptions = opts.sqliteChangeLogWriter;\n const catchupOptions = opts.sqliteCatchup;\n this.#readRouter =\n serveOptions && writerOptions && catchupOptions\n ? new SQLiteChangeLogReadRouter({\n shard,\n readPercent: serveOptions.readPercent,\n coldReadPercent: serveOptions.coldReadPercent,\n retentionMs: serveOptions.retentionMs,\n failureCooldownMs: serveOptions.failureCooldownMs,\n now: serveOptions.now,\n inspect:\n serveOptions.inspect ??\n (() =>\n inspectSQLiteChangeLog(\n lc,\n catchupOptions.changeLogFile,\n writerOptions.identity,\n )),\n })\n : undefined;\n this.#reservations = backupConfig\n ? new SnapshotReservations(lc, backupConfig, taskID => {\n this.#readRouter?.release(taskID);\n this.#purgeScheduler?.resume(taskID);\n })\n : undefined;\n this.#replicationStatusPublisher = replicationStatusPublisher;\n this.#changeLogWriter = opts.sqliteChangeLogWriter\n ? new SQLiteChangeLogWriter(lc, {\n ...opts.sqliteChangeLogWriter,\n onCommit: watermark => {\n this.#sqliteCatchup?.onChangeLogCommit(watermark);\n // The commit closed the log's transaction, i.e. opened a purge\n // window (§3.3).\n this.#purgeScheduler?.onWriterIdle();\n },\n // Fail-soft deletes the file, and the reader is cached here, so it\n // has to go with it: otherwise it serves an unlinked inode while\n // every new open sees nothing.\n onDisabled: () => {\n // The writer stays disabled and the file stays absent for the life\n // of this process, so unlike a transient read/barrier failure this\n // breaker never expires.\n this.#readRouter?.trip(true);\n this.#closeSQLiteCatchup();\n this.#comparator?.stop();\n },\n onRebuilt: () => {\n this.#closeSQLiteCatchup();\n // Invalidate cycles that can still read the replaced file.\n this.#comparator?.invalidate();\n },\n })\n : undefined;\n // The purge scheduler runs on the writer's own connection, which is also\n // its gate: no writer (mode `off`) means no scheduler, and a writer that\n // has not created the file yet -- or failed soft and deleted it -- makes\n // cycles skip rather than fail, so a late-appearing file is picked up\n // without a restart.\n this.#purgeScheduler =\n opts.sqliteChangeLogPurge && this.#changeLogWriter\n ? new SQLiteChangeLogPurgeScheduler(\n lc,\n () => this.#changeLogWriter?.connection,\n () => this.#forwarder.getAcks(),\n opts.sqliteChangeLogPurge,\n )\n : undefined;\n this.#acker = new UpstreamAcker({\n trackPgChangeLog: this.#pgChangeLogEnabled,\n trackBackup: backupConfig?.litestreamVersion === 'v5',\n });\n const replicaFileForInitialization =\n opts.sqliteChangeLogCompare?.replicaFile ??\n (!this.#pgChangeLogEnabled\n ? opts.sqliteChangeLogWriter?.replicaFile\n : undefined);\n const replicaSource = replicaFileForInitialization\n ? replicaInitializationSource(lc, replicaFileForInitialization)\n : undefined;\n this.#initializer = new ChangeLogInitializer(\n lc,\n {\n initFromPgChangeLog: this.#pgChangeLogEnabled,\n initFromReplica: replicaSource !== undefined,\n },\n {\n pgChangeLog: () =>\n this.#storer.getStartStreamInitializationParameters(),\n // Only reached when `initFromReplica` is set, i.e. when the option\n // that supplies the file is present.\n replica: () => must(replicaSource)(),\n // Use the Postgres resume point when it is present. Otherwise, let the\n // SQLite change log supply its own resume point.\n reconcileChangeLog: (resumeFrom, seed) =>\n resumeFrom\n ? this.#changeLogWriter?.reconcile(resumeFrom)\n : this.#changeLogWriter?.reconcileFromLog(seed),\n changeLog: () => this.#changeLogWriter?.connection,\n },\n );\n this.#sqliteCatchupOptions = opts.sqliteCatchup\n ? {\n ...opts.sqliteCatchup,\n // The real cleanup guard, in place of the catchup's no-op default:\n // registration and purge batches now serialize on one mutex.\n cleanupGuard:\n opts.sqliteCatchup.cleanupGuard ??\n this.#purgeScheduler?.cleanupGuard,\n }\n : undefined;\n // Compare mode requires the writer and catchup configuration.\n this.#comparator =\n this.#pgChangeLogEnabled &&\n opts.sqliteChangeLogCompare &&\n opts.sqliteChangeLogWriter &&\n opts.sqliteCatchup\n ? new SQLiteChangeLogComparator(\n lc,\n shard,\n opts.sqliteCatchup.changeLogFile,\n opts.sqliteChangeLogWriter.identity,\n this.#storer,\n {setTimeoutFn, ...opts.sqliteChangeLogCompare},\n )\n : undefined;\n this.#purgeLock = initialPurgeLock;\n this.#autoReset = autoReset;\n this.#state = new RunningState(this.id, undefined, setTimeoutFn);\n this.#latestStatus = {tag: 'status'};\n }\n\n async run() {\n this.#lc.info?.('starting change stream');\n\n this.#forwarder.startProgressMonitor();\n\n const lagReportInit = await this.#source.startLagReporter();\n if (lagReportInit) {\n this.#latestStatus.lagReport = {\n nextSendTimeMs: lagReportInit.nextSendTimeMs,\n };\n // Record the commit time of the initiated lag report (i.e. \"head\")\n // for the purpose of skipping over any lag reports that are re-streamed\n // by the change-source in the case of a change-streamer starting from\n // an older watermark.\n this.#latestLagReportCommitTimeMs = lagReportInit.firstCommitTimeMs;\n }\n\n // Once this change-streamer acquires \"ownership\" of the change DB,\n // it is safe to start the storer.\n await this.#storer.assumeOwnership(this.#purgeLock);\n this.#purgeLock = null;\n\n // The threshold in (estimated number of) bytes to send() on subscriber\n // websockets before `await`-ing the I/O buffers to be ready for more.\n const flushBytesThreshold = getDefaultHighWaterMark(false);\n\n while (this.#state.shouldRun()) {\n let err: unknown;\n let watermark: string | null = null;\n let unflushedBytes = 0;\n try {\n // Initialization reconciles the change log for every stream\n // connection. It completes before `startStream`, so no change can\n // arrive during reconciliation.\n const {lastWatermark, backfillRequests} =\n await this.#initializer.initialize();\n // SQLite catchup must not be eligible until this has been initialized\n // from the selected durable head. Commits observed only since process\n // startup are insufficient after a change-streamer restart.\n this.#lastForwardedCommitWatermark = lastWatermark;\n const stream = await this.#source.startStream(\n lastWatermark,\n backfillRequests,\n );\n if (this.#pgChangeLogEnabled) {\n this.#storer.run().catch(e => stream.changes.cancel(e));\n }\n\n this.#stream = stream;\n if (\n this.#state.resetBackoff() >\n REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS\n ) {\n // After recovering from a backoff for which a replication status\n // error was published, publish an OK status\n this.#replicationStatusPublisher.publish(\n this.#lc,\n 'Replicating',\n `Replicating from ${lastWatermark}`,\n );\n }\n watermark = null;\n\n // With the PG change log enabled, the stream resumes from what it has\n // persisted, so nothing before the stream is outstanding. Otherwise it\n // resumes from the SQLite change log's head, which the backup can\n // trail.\n this.#acker.reset(\n stream.acks,\n this.#pgChangeLogEnabled ? '' : lastWatermark,\n );\n\n for await (const change of stream.changes) {\n this.#acker.trackDownstream(change);\n\n const [type, msg] = change;\n switch (type) {\n case 'status':\n if (\n msg.lagReport &&\n msg.lagReport.lastTimings.commitTimeMs >=\n this.#latestLagReportCommitTimeMs\n ) {\n // Lag reports are not stored in the cdc change log, but rather\n // only forwarded on \"live\" connections. When a new subscriber\n // is catching up, it is initialized with the #latestStatus\n // from which it can measure lag while catching up.\n this.#latestStatus.lagReport = msg.lagReport;\n this.#latestLagReportCommitTimeMs =\n msg.lagReport.lastTimings.commitTimeMs;\n this.#forwarder.sendStatus(this.#latestStatus);\n }\n continue;\n case 'control':\n await this.#handleControlMessage(msg);\n continue; // control messages are not stored/forwarded\n case 'begin':\n watermark = change[2].commitWatermark;\n break;\n case 'commit':\n if (watermark !== change[2].watermark) {\n throw new UnrecoverableError(\n `commit watermark ${change[2].watermark} does not match 'begin' watermark ${watermark}`,\n );\n }\n this.#txCounter.add(1);\n break;\n default:\n if (type === 'data') {\n this.#changeCounter.add(1);\n }\n if (watermark === null) {\n throw new UnrecoverableError(\n `${type} change (${msg.tag}) received before 'begin' message`,\n );\n }\n break;\n }\n\n const serialized = serializeChangeStreamDataWithChange(change);\n const {json} = serialized;\n if (this.#pgChangeLogEnabled) {\n this.#storer.store(watermark, change, serialized);\n }\n // The SQLite change log commits at transaction boundaries, and its\n // commit for this transaction lands here -- before the forward of the\n // `commit` message, and before #recordForwardedTransactionBoundary\n // advances what #captureRequiredHead reads. No `await` separates it\n // from serialization or the optional PG enqueue above. This is the\n // assertable form of invariant 1: a synchronous SQLite commit in the\n // same loop iteration always precedes anything that can advance the\n // watermark this stream would resume from. Never throws; a write\n // failure disables the writer rather than stopping replication.\n this.#changeLogWriter?.write(change, json, serialized.change);\n const entry: WatermarkedChange = [watermark, change[1].tag, json];\n unflushedBytes += json.length;\n let flowControl: Promise<void> | undefined;\n if (unflushedBytes < flushBytesThreshold) {\n // pipeline changes until flushBytesThreshold\n this.#forwarder.forward(entry);\n } else {\n // Wait for messages to clear socket buffers to ensure that they\n // make their way to subscribers. Without this `await`, the\n // messages end up being buffered in this process, which:\n // (1) results in memory pressure and increased GC activity\n // (2) prevents subscribers from processing the messages as they\n // arrive, instead getting them in a large batch after being\n // idle while they were queued (causing further delays).\n flowControl = this.#forwarder.forwardWithFlowControl(entry);\n }\n // Both forwards send the entry and update the Forwarder's transaction\n // state synchronously. Record the boundary, and end the transaction at\n // its commit or rollback, before awaiting flow control: registrations\n // during the wait then observe the forwarded state, and a stream that\n // is interrupted during the wait does not roll back a transaction\n // that was already forwarded whole.\n this.#recordForwardedTransactionBoundary(type, entry[0]);\n if (type === 'commit' || type === 'rollback') {\n watermark = null;\n }\n if (flowControl) {\n await promiseOrAbort(\n flowControl,\n stream.changes.signal,\n this.#state.signal,\n );\n unflushedBytes = 0;\n }\n\n // Allow the PG storer to exert back pressure when it is enabled.\n const readyForMore = this.#pgChangeLogEnabled\n ? this.#storer.readyForMore()\n : undefined;\n if (readyForMore) {\n await promiseOrAbort(\n readyForMore,\n stream.changes.signal,\n this.#state.signal,\n );\n }\n }\n } catch (e) {\n err = e;\n } finally {\n this.#stream?.changes.cancel();\n this.#stream = undefined;\n }\n\n // When the change stream is interrupted, abort any pending transaction.\n if (watermark) {\n this.#lc.warn?.(`aborting interrupted transaction ${watermark}`);\n if (this.#pgChangeLogEnabled) {\n this.#storer.abort();\n }\n // Rolling back the log leaves no rows for the interrupted transaction,\n // so the next connection's reconciliation sees a head at or below its\n // resume watermark rather than a partial transaction.\n this.#changeLogWriter?.abort();\n // A rollback ends the log's open transaction without a commit\n // notification; wake any purge batch waiting for that window.\n this.#purgeScheduler?.onWriterIdle();\n this.#forwarder.forward([watermark, 'rollback', ROLLBACK_JSON]);\n this.#recordForwardedTransactionBoundary('rollback', watermark);\n }\n\n // Backoff and drain any pending entries in the storer before reconnecting.\n await Promise.all([\n this.#storer.stop(),\n this.#state.backoff(this.#lc, err),\n this.#state.retryDelay > REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS\n ? publishCriticalEvent(\n this.#lc,\n replicationStatusError(this.#lc, 'Replicating', err),\n )\n : promiseVoid,\n ]);\n }\n\n this.#forwarder.stopProgressMonitor();\n this.#lc.info?.('ChangeStreamer stopped');\n }\n\n async #handleControlMessage(msg: ChangeStreamControl[1]) {\n this.#lc.info?.('received control message', msg);\n const {tag} = msg;\n\n switch (tag) {\n case 'reset-required':\n await markResetRequired(\n this.#changeDBProvider('change-streamer-reset', 1),\n this.#shard,\n );\n await publishReplicationError(\n this.#lc,\n 'Replicating',\n msg.message ?? 'Resync required',\n msg.errorDetails,\n );\n if (this.#autoReset) {\n this.#lc.warn?.('shutting down for auto-reset');\n await this.stop(new AutoResetSignal());\n }\n break;\n default:\n unreachable(tag);\n }\n }\n\n async subscribe(ctx: SubscriberContext): Promise<Source<string>> {\n const {protocolVersion, id, mode, replicaVersion, watermark} = ctx;\n if (mode === 'serving') {\n this.#serving.resolve();\n }\n let cleanupSubscriber = () => {};\n const downstream = Subscription.create<string>({\n cleanup: () => cleanupSubscriber(),\n });\n // No subscriber's ACK advances the SQLite change log's head any more: the\n // writer runs in this process, so the barrier is notified from the commit\n // itself (see #changeLogWriter's onCommit).\n const subscriber = new Subscriber(\n protocolVersion,\n id,\n mode,\n watermark,\n downstream,\n () => this.#latestStatus,\n {},\n );\n const lc = this.#lc.withContext('subscriber', subscriber.id);\n const removeFromForwarder = () => {\n lc.info?.(`removing subscriber ${subscriber.id}`);\n this.#forwarder.remove(subscriber);\n };\n cleanupSubscriber = removeFromForwarder;\n if (replicaVersion !== this.#replicaVersion) {\n lc.warn?.(`rejecting subscriber at replica version ${replicaVersion}`);\n subscriber.close(\n ErrorType.WrongReplicaVersion,\n `current replica version is ${\n this.#replicaVersion\n } (requested ${replicaVersion})`,\n );\n } else {\n lc.info?.(`adding subscriber ${subscriber.id}`);\n\n const catchupFromPG = () => {\n assert(\n this.#pgChangeLogEnabled,\n 'cannot catch up from a disabled PG change log',\n );\n // Keep the existing PG registration/catchup lockstep unchanged when\n // SQLite was not selected before Forwarder.add().\n cleanupSubscriber = removeFromForwarder;\n this.#forwarder.add(subscriber);\n this.#storer.catchup(subscriber, mode);\n };\n const sqliteDecision = this.#selectSQLiteCatchup(lc, ctx);\n if (!sqliteDecision) {\n catchupFromPG();\n } else if (sqliteDecision.kind === 'rejected') {\n lc.warn?.(\n `${\n sqliteDecision.terminal ? 'rejecting' : 'ending'\n } subscription for ${ctx.id}: ${sqliteDecision.message}`,\n );\n if (sqliteDecision.terminal) {\n subscriber.close(ErrorType.WatermarkTooOld, sqliteDecision.message);\n } else {\n subscriber.close();\n }\n } else {\n const {catchup, reason, coverage, logWarm} = sqliteDecision;\n cleanupSubscriber = () => catchup.remove(subscriber);\n const registration = await catchup.catchup(\n subscriber,\n () => this.#captureRequiredHead(),\n {logWarm},\n );\n switch (registration.kind) {\n case 'registered':\n lc.debug?.(\n `serving ${ctx.id} from SQLite catchup`,\n ...(coverage ? [{sqliteChangeLogCoverage: coverage}] : []),\n );\n this.#recordCatchupRoute('sqlite', reason);\n break;\n case 'uncovered':\n if (this.#pgChangeLogEnabled) {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: subscriber watermark ` +\n `${ctx.watermark} is below the SQLite change-log minimum ` +\n registration.minWatermark,\n );\n this.#recordCatchupRoute('pg', 'watermark-uncovered');\n catchupFromPG();\n } else {\n const message =\n `earliest supported watermark is ` +\n `${registration.minWatermark} (requested ${ctx.watermark})`;\n lc.warn?.(\n `rejecting subscriber at watermark ${ctx.watermark}: ` +\n `the SQLite change log starts at ` +\n `${registration.minWatermark} and the PG change log is disabled`,\n );\n this.#recordCatchupRoute('none', 'watermark-uncovered');\n subscriber.close(ErrorType.WatermarkTooOld, message);\n }\n break;\n case 'declined':\n // Registration failed before the subscriber was committed to\n // SQLite. Use PG when enabled; otherwise end this subscription so\n // it retries. The coordinator has already tripped the breaker,\n // which keeps the retry off SQLite for the cooldown; it is\n // deliberately not closed here, since closing it would abort the\n // catchups of every other subscriber it is serving.\n if (this.#pgChangeLogEnabled) {\n lc.error?.(\n `serving ${ctx.id} from PG catchup: SQLite catchup ` +\n `registration failed`,\n registration.error,\n );\n this.#recordCatchupRoute('pg', 'registration-failed');\n catchupFromPG();\n } else {\n lc.error?.(\n `ending subscription for ${ctx.id} to retry SQLite catchup: ` +\n `registration failed and the PG change log is disabled`,\n registration.error,\n );\n this.#recordCatchupRoute('none', 'registration-failed');\n subscriber.fail(registration.error);\n }\n break;\n case 'handled':\n // The coordinator closed or failed the subscriber itself, so\n // there is nothing left to route. Still counted, so that the\n // route counter sums to the number of subscriptions.\n this.#recordCatchupRoute('sqlite', 'registration-handled');\n break;\n default:\n unreachable(registration);\n }\n }\n }\n // Any snapshot reservation held by this task can be closed now that\n // it is subscribed to the change stream.\n this.#reservations?.close(ctx.taskID);\n return downstream;\n }\n\n async startSnapshotReservation(\n taskID: string,\n ): Promise<Source<SnapshotMessage>> {\n if (!this.#reservations) {\n throw new Error('backups are not configured');\n }\n const downstream = this.#reservations.open(taskID);\n\n try {\n // Wait for an in-flight SQLite purge batch before reading and\n // advertising the reservation's bounds.\n await (this.#purgeScheduler?.pause(taskID) ?? promiseVoid);\n // A concurrent retry for this task may have superseded and cancelled\n // this reservation while its purge pause was settling. Only the current\n // owner may replace the task's source pin.\n if (!this.#reservations.isCurrent(taskID, downstream)) {\n return downstream;\n }\n // Pin after the purge pause has settled, so the SQLite minimum captured\n // by the router cannot move before it is advertised. #confirmReservations\n // skips an unpinned task while this await is in flight.\n this.#readRouter?.pin(taskID);\n // If a backup has been confirmed, immediately confirm the reservation.\n await this.#confirmReservations();\n return downstream;\n } catch (e) {\n // Cancel the reservation this call opened, not whatever currently\n // holds the task's slot: an overlapping retry for the same taskID may\n // have already replaced it, and a by-taskID close would tear down the\n // replacement and release its purge pause while it is advertising\n // snapshot bounds. cancel() routes through the subscription's cleanup,\n // which closes the reservation only if this instance still owns it.\n downstream.cancel();\n throw e;\n }\n }\n\n trackBackupWatermark(watermark: string) {\n this.#backupWatermark = watermark;\n this.#acker.trackBackup(watermark);\n // The durable backup floor is an independent input to each change-log\n // implementation. SQLite receives it even when the PG log has already\n // reached this watermark.\n this.#requestSQLitePurge('deferred');\n this.#maybeSchedulePGPurge();\n this.#maybeScheduleSQLitePurge();\n\n // Confirm any waiting reservations now that a backup has been confirmed.\n // Note that this is asynchronous and best effort; if it fails, the watermark\n // is still \"tracked\" and the confirmation will be retried on the next backup.\n void this.#confirmReservations().catch(e =>\n this.#lc.warn?.(`error confirming snapshot reservation`, e),\n );\n }\n\n async #confirmReservations() {\n const backupWatermark = this.#backupWatermark;\n const reservations = this.#reservations;\n if (\n backupWatermark === undefined ||\n !reservations?.confirmationsRequired()\n ) {\n return;\n }\n\n // Resolve PG bounds, when enabled, before touching any pin. Everything\n // below runs to completion without awaiting, which keeps a reservation's\n // advertised bounds and its pin in agreement: a concurrent /snapshot\n // retry for the same task replaces the reservation and re-pins it, and\n // confirming that replacement with the previous pin's bounds is exactly\n // the mismatch pinning exists to prevent.\n const pgState = this.#pgChangeLogEnabled\n ? await this.#getChangeLogState()\n : undefined;\n for (const taskID of reservations.unconfirmedTaskIDs()) {\n let route = this.#readRouter?.peek(taskID);\n // startSnapshotReservation pins only after an in-flight purge has\n // completed. A task with no pin yet is confirmed by that path instead.\n if (this.#readRouter && route === undefined) {\n continue;\n }\n\n // A transiently unavailable or broken SQLite log normally pins the\n // reservation to PG. With PG disabled, re-evaluate that choice on each\n // backup notification and keep the reservation pending until SQLite can\n // supply bounds.\n if (!this.#pgChangeLogEnabled && route?.source === 'pg') {\n this.#readRouter?.release(taskID);\n route = this.#readRouter?.pin(taskID);\n if (route?.source === 'pg') {\n if (reservations.noteConfirmationDelayed(taskID)) {\n this.#reservationConfirmDelays.add(1);\n }\n this.#lc.warn?.(\n `delaying snapshot reservation for ${taskID}: SQLite change log ` +\n `is unavailable and the PG change log is disabled`,\n );\n continue;\n }\n }\n\n if (route?.source === 'sqlite') {\n const coverage = must(\n route.coverage,\n 'a pinned SQLite route must carry its covered range',\n );\n if (\n this.#pgChangeLogEnabled &&\n coverage.minWatermark > backupWatermark\n ) {\n // A log seeded after this backup, most often. Holding the\n // reservation until a backup reaches the log's minimum would stall\n // a follower that PG can serve now, so move it -- pin included.\n this.#lc.info?.(\n `demoting ${taskID} to PG catchup: SQLite change-log minimum ` +\n `${coverage.minWatermark} is later than backupWatermark ` +\n backupWatermark,\n );\n this.#reservationDemotions.add(1);\n route = must(this.#readRouter).demote(taskID);\n }\n }\n\n const source = route?.source ?? 'pg';\n let minWatermark: string;\n if (route?.source === 'sqlite') {\n // Demotion above already moved every SQLite route the backup is\n // outside of, so this one covers it and confirms below.\n minWatermark = must(\n route.coverage,\n 'a pinned SQLite route must carry its covered range',\n ).minWatermark;\n } else {\n minWatermark = must(\n pgState,\n 'a PG reservation route requires the PG change log',\n ).minWatermark;\n }\n\n if (minWatermark <= backupWatermark) {\n reservations.confirmFor(\n taskID,\n this.#replicaVersion,\n backupWatermark,\n source,\n );\n } else {\n // PG cannot catch a restored replica up from this backup yet. Keep\n // the reservation pending until a later backup moves the durable\n // watermark into its covered range.\n if (reservations.noteConfirmationDelayed(taskID)) {\n this.#reservationConfirmDelays.add(1);\n }\n this.#lc.error?.(\n `${source} change-log minWatermark ${minWatermark} is later than ` +\n `backupWatermark ${backupWatermark}. Delaying confirmation of ` +\n `snapshot reservation until next backup.`,\n );\n }\n }\n }\n\n #maybeSchedulePGPurge(): void {\n const backupWatermark = this.#backupWatermark;\n if (\n !this.#pgChangeLogEnabled ||\n this.#pgPurgeScheduled ||\n this.#pgPurgeRunning ||\n backupWatermark === undefined ||\n this.#pgPurgedWatermark >= backupWatermark\n ) {\n return;\n }\n this.#pgPurgeScheduled = true;\n this.#state.setTimeout(() => {\n this.#pgPurgeScheduled = false;\n this.#pgPurgeRunning = true;\n return this.#purgePGChangeLog().finally(() => {\n this.#pgPurgeRunning = false;\n this.#maybeSchedulePGPurge();\n });\n }, CLEANUP_DELAY_MS);\n }\n\n #maybeScheduleSQLitePurge(): void {\n if (\n this.#sqlitePurgeScheduled ||\n this.#sqlitePurgeRunning ||\n this.#backupWatermark === undefined ||\n this.#sqlitePurgeContinuation === undefined\n ) {\n return;\n }\n const delay =\n this.#sqlitePurgeContinuation === 'immediate' ? 0 : CLEANUP_DELAY_MS;\n this.#sqlitePurgeScheduled = true;\n this.#state.setTimeout(() => {\n this.#sqlitePurgeScheduled = false;\n this.#sqlitePurgeRunning = true;\n return this.#purgeSQLiteChangeLog().finally(() => {\n this.#sqlitePurgeRunning = false;\n this.#maybeScheduleSQLitePurge();\n });\n }, delay);\n }\n\n async #getChangeLogState(): Promise<{\n replicaVersion: string;\n minWatermark: string;\n }> {\n const minWatermark = await this.#storer.getMinWatermarkForCatchup();\n if (!minWatermark) {\n this.#lc.warn?.(\n `Unexpected empty changeLog. Resync if \"Local replica watermark\" errors arise`,\n );\n }\n return {\n replicaVersion: this.#replicaVersion,\n minWatermark: minWatermark ?? this.#replicaVersion,\n };\n }\n\n #getCleanupFloor(): {\n backupWatermark: string;\n purgeWatermark: string;\n current: string[];\n } {\n const backupWatermark = this.#backupWatermark;\n assert(\n backupWatermark !== undefined,\n 'cleanup cannot run without a backup watermark',\n );\n const current = [\n ...this.#forwarder.getAcks(),\n ...(this.#reservations?.getReservedWatermarks() ?? []),\n ];\n // The cleanup delay above is the grace period for disconnected\n // subscribers to reconnect and expose their ACKs. Once it expires, an\n // empty set places no additional constraint on the confirmed backup\n // watermark and must not pin either change log indefinitely.\n return {\n backupWatermark,\n purgeWatermark: min(backupWatermark, ...current),\n current,\n };\n }\n\n async #purgePGChangeLog(): Promise<void> {\n try {\n const {backupWatermark, purgeWatermark, current} =\n this.#getCleanupFloor();\n if (purgeWatermark < backupWatermark) {\n if (this.#loggedBehindWatermark !== purgeWatermark) {\n this.#loggedBehindWatermark = purgeWatermark;\n this.#lc.info?.(\n `At least one client is behind backup ${backupWatermark}`,\n {watermarks: current},\n );\n }\n } else {\n this.#loggedBehindWatermark = undefined;\n }\n if (purgeWatermark <= this.#pgPurgedWatermark) {\n return;\n }\n this.#lc.info?.(`Purging PG changes before ${purgeWatermark} ...`);\n const start = performance.now();\n const deleted = await this.#storer.purgeRecordsBefore(purgeWatermark);\n const elapsed = (performance.now() - start).toFixed(2);\n this.#lc.info?.(\n `Purged ${deleted} PG changes before ${purgeWatermark} (${elapsed} ms)`,\n );\n this.#pgPurgedWatermark = purgeWatermark;\n } catch (e) {\n this.#lc.warn?.(`error purging the PG change log`, e);\n }\n }\n\n async #purgeSQLiteChangeLog(): Promise<void> {\n const scheduler = this.#purgeScheduler;\n if (!scheduler) {\n return;\n }\n try {\n const {backupWatermark, purgeWatermark} = this.#getCleanupFloor();\n // Consume the request before starting. A backup notification that\n // arrives during this pass records another request, which is merged with\n // the continuation returned by this pass rather than being overwritten.\n this.#sqlitePurgeContinuation = undefined;\n if (purgeWatermark < backupWatermark) {\n // Live constraint changes are not edge-triggered. Keep evaluating the\n // floor until it reaches the durable backup watermark, independently\n // of whether the PG implementation still exists.\n this.#requestSQLitePurge('deferred');\n }\n const result = await scheduler.purge(purgeWatermark);\n this.#requestSQLitePurge(result.continuation);\n } catch (e) {\n // purge() is fail-soft, but retain the coordinator's retry if an\n // unexpected caller-boundary failure escapes it.\n this.#requestSQLitePurge('deferred');\n this.#lc.warn?.(`error purging the SQLite change log`, e);\n }\n }\n\n #requestSQLitePurge(continuation: PurgeContinuation | undefined): void {\n if (!this.#purgeScheduler || continuation === undefined) {\n return;\n }\n if (\n continuation === 'immediate' ||\n this.#sqlitePurgeContinuation === undefined\n ) {\n this.#sqlitePurgeContinuation = continuation;\n }\n }\n\n async stop(err?: unknown) {\n this.#state.stop(this.#lc, err);\n this.#stream?.changes.cancel();\n this.#purgeScheduler?.stop();\n this.#comparator?.stop();\n this.#sqliteCatchup?.close();\n this.#changeLogWriter?.close();\n await Promise.allSettled([this.#storer.stop(), this.#source.stop()]);\n }\n\n #recordForwardedTransactionBoundary(\n type: ChangeStreamData[0],\n watermark: string,\n ) {\n switch (type) {\n case 'begin':\n assert(\n this.#currentTransactionCompletion === undefined,\n 'forwarded begin while a transaction is already in progress',\n );\n this.#currentTransactionCompletion =\n resolver<ForwardedTransactionCompletion>();\n this.#transactionForwardStartedAt = performance.now();\n break;\n case 'commit': {\n const completion = this.#currentTransactionCompletion;\n assert(completion, 'forwarded commit without a pending transaction');\n this.#recordTransactionForwardDuration();\n this.#lastForwardedCommitWatermark = watermark;\n this.#currentTransactionCompletion = undefined;\n completion.resolve({kind: 'committed', watermark});\n break;\n }\n case 'rollback': {\n const completion = this.#currentTransactionCompletion;\n assert(completion, 'forwarded rollback without a pending transaction');\n const committed = this.#lastForwardedCommitWatermark;\n assert(committed, 'last forwarded commit watermark is not initialized');\n this.#transactionForwardStartedAt = undefined;\n this.#currentTransactionCompletion = undefined;\n completion.resolve({kind: 'rolled-back', watermark: committed});\n break;\n }\n }\n }\n\n #recordTransactionForwardDuration() {\n const startedAt = this.#transactionForwardStartedAt;\n this.#transactionForwardStartedAt = undefined;\n if (startedAt !== undefined) {\n this.#transactionForwardDuration.recordMs(performance.now() - startedAt, {\n sqlite_change_log: this.#changeLogWriter?.enabled ? 'on' : 'off',\n });\n }\n }\n\n /**\n * Closes and forgets the cached catchup coordinator, so that a later\n * subscription re-opens the log from scratch rather than reading a handle\n * whose file is gone.\n */\n #closeSQLiteCatchup() {\n this.#sqliteCatchup?.close();\n this.#sqliteCatchup = undefined;\n }\n\n #captureRequiredHead(): string | Promise<string> {\n const committed = this.#lastForwardedCommitWatermark;\n assert(committed, 'last forwarded commit watermark is not initialized');\n return (\n this.#currentTransactionCompletion?.promise.then(\n completion => completion.watermark,\n ) ?? committed\n );\n }\n\n #selectSQLiteCatchup(\n lc: LogContext,\n ctx: SubscriberContext,\n ): SQLiteCatchupDecision | undefined {\n const opts = this.#sqliteCatchupOptions;\n if (this.#lastForwardedCommitWatermark === undefined || !opts) {\n return this.#fallbackOrReject(\n 'not-ready',\n 'SQLite catchup is not ready and the PG change log is disabled',\n );\n }\n // While the PG change log is available, backup subscribers retain its\n // existing recovery policy and cannot be moved by a canary selector.\n // Once PG is disabled, the backup replicator must use SQLite catchup: it\n // advances the canonical replica that Litestream backs up.\n if (ctx.mode === 'backup' && this.#pgChangeLogEnabled) {\n lc.info?.(`not serving backup subscriber ${ctx.id} from SQLite catchup`);\n return this.#fallbackOrReject(\n 'ineligible-mode',\n 'backup subscribers retain the PG change-log recovery policy',\n );\n }\n\n let route: ChangeLogReadRoute | undefined;\n if (this.#readRouter) {\n route = this.#readRouter.consume(ctx.taskID);\n if (route.source === 'pg') {\n if (route.reason === 'cold-log') {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: SQLite change log is still warming`,\n {sqliteChangeLogCoverage: route.coverage},\n );\n } else if (route.reason === 'log-unavailable') {\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `${opts.changeLogFile} is unavailable or incompatible`,\n );\n } else {\n lc.debug?.(\n `serving ${ctx.id} from PG catchup: SQLite route ${route.reason}`,\n ...(route.coverage\n ? [{sqliteChangeLogCoverage: route.coverage}]\n : []),\n );\n }\n return this.#fallbackOrReject(\n route.reason,\n `SQLite catchup route ${route.reason} is unavailable and the PG ` +\n `change log is disabled`,\n );\n }\n const coverage = route.coverage;\n assert(coverage, 'a SQLite route must carry its covered range');\n if (ctx.watermark < coverage.minWatermark) {\n lc.info?.(\n `serving ${ctx.id} from PG catchup: subscriber watermark ` +\n `${ctx.watermark} is below the SQLite change-log minimum ` +\n coverage.minWatermark,\n {sqliteChangeLogCoverage: coverage},\n );\n return this.#fallbackOrReject(\n 'watermark-uncovered',\n `earliest supported watermark is ${coverage.minWatermark} ` +\n `(requested ${ctx.watermark})`,\n true,\n );\n }\n }\n\n // `shouldUse` remains as a test hook and an optional extra policy gate.\n // Production selection comes from #readRouter.\n if (!this.#readRouter && !opts.shouldUse?.(ctx)) {\n return this.#fallbackOrReject(\n 'selector',\n 'SQLite catchup was not selected and the PG change log is disabled',\n );\n }\n if (this.#readRouter && opts.shouldUse && !opts.shouldUse(ctx)) {\n return this.#fallbackOrReject(\n 'selector',\n 'SQLite catchup was not selected and the PG change log is disabled',\n );\n }\n\n const catchup =\n this.#sqliteCatchup ?? this.#openSQLiteCatchup(lc, opts, ctx);\n if (catchup) {\n return {\n kind: 'selected',\n catchup,\n reason: route?.reason ?? 'selector',\n coverage: route?.coverage,\n // Legacy focused-test selection has no warm classification.\n logWarm:\n route === undefined ? undefined : route.reason !== 'selected-cold',\n };\n } else {\n return this.#fallbackOrReject(\n 'log-unavailable',\n 'SQLite change log is unavailable and the PG change log is disabled',\n );\n }\n }\n\n #fallbackOrReject(\n reason: string,\n message: string,\n terminal = false,\n ): SQLiteCatchupRejection | undefined {\n this.#recordCatchupRoute(this.#pgChangeLogEnabled ? 'pg' : 'none', reason);\n return this.#pgChangeLogEnabled\n ? undefined\n : {kind: 'rejected', message, terminal};\n }\n\n #recordCatchupRoute(source: 'pg' | 'sqlite' | 'none', reason: string): void {\n this.#catchupRoutes.add(1, {source, reason});\n }\n\n /**\n * Opens the change log and, if it can serve, wraps it in the coordinator that\n * subsequent subscriptions reuse.\n *\n * The log may not exist yet -- the writer creates it at the stream loop's\n * first reconcile, and deletes it when it fails soft -- or may exist without\n * content, or may be unreadable. None of those can be allowed to fail a\n * subscription:\n * this is the last point at which PG catchup can be selected when it is\n * enabled -- past `Forwarder.add()` the subscriber is committed to SQLite --\n * so each declines here instead. With PG disabled, the caller ends the\n * subscription so that it retries. Neither the failure nor the reader is\n * retained, so a later subscription retries from scratch.\n */\n #openSQLiteCatchup(\n lc: LogContext,\n opts: SQLiteCatchupOptions,\n ctx: SubscriberContext,\n ): SQLiteChangeLogCatchup | undefined {\n let reader: SQLiteChangeLogReader | undefined;\n try {\n reader = new SQLiteChangeLogReader(this.#lc, opts.changeLogFile);\n // `plan()` doubles as the readiness check: it reports `not-ready` when\n // the writer has not created or seeded the stream table. The barrier\n // cannot wait its way out of that, since a log with no head can never\n // reach the required one.\n if (reader.plan(ctx.watermark).kind === 'not-ready') {\n reader.close();\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `${opts.changeLogFile} has no changes to serve yet`,\n );\n return undefined;\n }\n } catch (e) {\n // An absent file is the common case, since a readonly handle cannot\n // create one. A corrupt or truncated file lands here too, and is equally\n // not a reason to fail the subscription.\n reader?.close();\n this.#declineSQLiteCatchup(\n lc,\n ctx,\n opts,\n `cannot read ${opts.changeLogFile}`,\n e,\n );\n return undefined;\n }\n this.#changeLogUnavailableSince = undefined;\n // Readiness is checked once, on the way to the cached coordinator: a log\n // with content keeps it. Purging preserves the latest transaction as a\n // catchup boundary, and `reconcileChangeLog` reseeds inside a single\n // transaction, so a reader never observes an emptied log.\n this.#sqliteCatchup = new SQLiteChangeLogCatchup(\n this.#lc,\n this.#forwarder,\n reader,\n {\n batchSize: opts.readBatchRows,\n barrierTimeoutMs: opts.barrierTimeoutMs,\n barrierPollIntervalMs: opts.barrierPollIntervalMs,\n cleanupGuard: opts.cleanupGuard,\n onFailure: failure => {\n this.#lc.warn?.(\n `temporarily disabling SQLite catchup after ${failure}`,\n );\n this.#readRouter?.trip();\n },\n },\n );\n return this.#sqliteCatchup;\n }\n\n /**\n * Reports falling back to PG catchup, tracking how long the log has been\n * unavailable so that a subscription that merely arrived before the writer's\n * first reconcile does not look like an incident.\n */\n #declineSQLiteCatchup(\n lc: LogContext,\n ctx: SubscriberContext,\n opts: SQLiteCatchupOptions,\n reason: string,\n error?: unknown,\n ): void {\n const now = Date.now();\n this.#changeLogUnavailableSince ??= now;\n const unavailableMs = now - this.#changeLogUnavailableSince;\n const threshold =\n opts.notReadyWarnThresholdMs ??\n DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS;\n lc[unavailableMs >= threshold ? 'warn' : 'debug']?.(\n (this.#pgChangeLogEnabled\n ? `serving ${ctx.id} from PG catchup: `\n : `cannot serve ${ctx.id} from SQLite catchup: `) +\n `${reason} ` +\n `(unavailable for ${unavailableMs} ms)`,\n ...(error === undefined ? [] : [error]),\n );\n }\n}\n\ntype ForwardedTransactionCompletion =\n | {kind: 'committed'; watermark: string}\n | {kind: 'rolled-back'; watermark: string};\n\ntype SQLiteCatchupSelection = {\n readonly kind: 'selected';\n readonly catchup: SQLiteChangeLogCatchup;\n readonly reason: string;\n readonly coverage: SQLiteChangeLogCoverage | undefined;\n /**\n * Whether the router classified the log as warm for this subscriber, or\n * undefined when selection did not run through the router (focused tests).\n */\n readonly logWarm: boolean | undefined;\n};\n\ntype SQLiteCatchupRejection = {\n readonly kind: 'rejected';\n readonly message: string;\n /** A terminal rejection forces the view-syncer to restore a fresh replica. */\n readonly terminal: boolean;\n};\n\ntype SQLiteCatchupDecision = SQLiteCatchupSelection | SQLiteCatchupRejection;\n\n// The delay between receiving an initial, backup-based watermark\n// and performing a check of whether to purge records before it.\n// This delay should be long enough to handle situations like the following:\n//\n// 1. `litestream restore` downloads a backup for the `replication-manager`\n// 2. `replication-manager` starts up and runs this `change-streamer`\n// 3. `zero-cache`s that are running on a different replica connect to this\n// `change-streamer` after exponential backoff retries.\n//\n// It is possible for a `zero-cache`[3] to be behind the backup restored [1].\n// This cleanup delay (30 seconds) is thus set to be a value comfortably\n// longer than the max delay for exponential backoff (10 seconds) in\n// `services/running-state.ts`. This allows the `zero-cache` [3] to reconnect\n// so that the `change-streamer` can track its progress and know when it has\n// surpassed the initial watermark of the backup [1].\nconst CLEANUP_DELAY_MS = DEFAULT_MAX_RETRY_DELAY_MS * 3;\n\nconst ROLLBACK_JSON = JSON.stringify([\n 'rollback',\n {tag: 'rollback'},\n] satisfies Rollback);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmMA,eAAsB,mBACpB,IACA,OACA,QACA,kBACA,mBACA,UACA,cACA,4BACA,mBACA,cACA,WACA,WACA,MACA,eAAe,YACiB;CAChC,MAAM,wBACJ,IACA,UACA,mBACA,OACA,WACA,aAAa,KAAA,GACb,cACA,KAAK,kBACP;CAOA,MAAM,oBACJ,iBACA,QAEA,SACE,KACE;EAAC;EAAK,YAAY,GAAE,qBAAqB,gBAAe;CAAC,GAKzD,SAAS,OACX,CACF;CAEF,MAAM,EAAC,mBAAkB;CACzB,OAAO,IAAI,mBACT,IACA,OACA,QACA,kBACA,mBACA,kBACA,gBACA,cACA,4BACA,cACA,WACA,WACA,MACA,YACF;AACF;AAEA,IAAM,8CAA8C;AAQpD,IAAM,mDAAmD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4IzD,IAAM,qBAAN,MAA0D;CACxD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CAUA,WAAoB,SAAS;CAE7B,aAAsB,mBACpB,eACA,gBACA,kCACF;CACA,iBAA0B,mBACxB,eACA,WACA,qDACF;CAIA,8BAAuC,4BACrC,eACA,gCACA,yIAEF;CACA,iBAA0B,mBACxB,eACA,oCACA,sEACF;CACA,wBAAiC,mBAC/B,eACA,2CACA,qPAIF;CACA,4BAAqC,mBACnC,eACA,gDACA,2LAGF;CAEA;CACA,+BAA+B;CAC/B;CACA,qBAA6B;;;;;;;CAO7B;CACA;CACA;CAGA,oBAAoB;CACpB,kBAAkB;CAClB,wBAAwB;CACxB,sBAAsB;CACtB;CACA;CACA;CACA;CACA;CACA;CAIA,YACE,IACA,OACA,QACA,kBACA,mBACA,kBACA,gBACA,QACA,4BACA,cACA,kBACA,WACA,MACA,eAAe,YACf;EACA,KAAK,KAAK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"}
@@ -25,7 +25,23 @@ type Opts = {
25
25
  export declare class UpstreamAcker {
26
26
  #private;
27
27
  constructor({ trackPgChangeLog, trackBackup }: Opts);
28
- reset(upstream: Sink<ChangeSourceUpstream>): void;
28
+ /**
29
+ * Starts tracking a new upstream connection, which resumes the change stream
30
+ * after `resumeWatermark`.
31
+ *
32
+ * Everything at or before `resumeWatermark` was committed on an earlier
33
+ * connection, so it counts as committed on this one: a status watermark past
34
+ * it waits for the tracked stores to reach it, as it would for a commit on
35
+ * this connection. Otherwise the first keepalive of a stream that resumes
36
+ * ahead of the stores -- a SQLite change log ahead of its backup, which is
37
+ * the normal state -- would move the replication slot past transactions that
38
+ * no store has persisted, and a task restored from the backup would then
39
+ * resume below the slot, which the upstream moves forward without a word.
40
+ *
41
+ * Pass `''` when the stream resumes from what a tracked store has itself
42
+ * persisted, which leaves nothing before it outstanding.
43
+ */
44
+ reset(upstream: Sink<ChangeSourceUpstream>, resumeWatermark: string): void;
29
45
  trackDownstream(downstream: ChangeStreamMessage): void;
30
46
  trackPgChangeLog(committedWatermark: string): void;
31
47
  trackBackup(backedUpWatermark: string): void;
@@ -1 +1 @@
1
- {"version":3,"file":"upstream-acker.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/upstream-acker.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AACjD,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,sCAAsC,CAAC;AAE9C,KAAK,IAAI,GAAG;IACV,gBAAgB,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,aAAa;;IAYxB,YAAY,EAAC,gBAAgB,EAAE,WAAW,EAAC,EAAE,IAAI,EAOhD;IAED,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,oBAAoB,CAAC,QAKzC;IAED,eAAe,CAAC,UAAU,EAAE,mBAAmB,QAc9C;IAED,gBAAgB,CAAC,kBAAkB,EAAE,MAAM,QAO1C;IAED,WAAW,CAAC,iBAAiB,EAAE,MAAM,QAIpC;CA2BF"}
1
+ {"version":3,"file":"upstream-acker.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/upstream-acker.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AACjD,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,sCAAsC,CAAC;AAE9C,KAAK,IAAI,GAAG;IACV,gBAAgB,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,aAAa;;IAYxB,YAAY,EAAC,gBAAgB,EAAE,WAAW,EAAC,EAAE,IAAI,EAOhD;IAED;;;;;;;;;;;;;;;OAeG;IACH,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,oBAAoB,CAAC,EAAE,eAAe,EAAE,MAAM,QAKlE;IAED,eAAe,CAAC,UAAU,EAAE,mBAAmB,QAe9C;IAED,gBAAgB,CAAC,kBAAkB,EAAE,MAAM,QAO1C;IAED,WAAW,CAAC,iBAAiB,EAAE,MAAM,QAIpC;CA2BF"}