@rocicorp/zero 1.12.0-canary.8 → 1.12.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 (516) hide show
  1. package/out/replicache/src/btree/node.d.ts +0 -2
  2. package/out/replicache/src/btree/node.d.ts.map +1 -1
  3. package/out/replicache/src/btree/node.js.map +1 -1
  4. package/out/replicache/src/connection-loop.d.ts.map +1 -1
  5. package/out/replicache/src/connection-loop.js +1 -1
  6. package/out/replicache/src/connection-loop.js.map +1 -1
  7. package/out/replicache/src/db/commit.d.ts +0 -1
  8. package/out/replicache/src/db/commit.d.ts.map +1 -1
  9. package/out/replicache/src/db/commit.js +1 -1
  10. package/out/replicache/src/db/commit.js.map +1 -1
  11. package/out/replicache/src/db/write.d.ts +0 -2
  12. package/out/replicache/src/db/write.d.ts.map +1 -1
  13. package/out/replicache/src/db/write.js +1 -1
  14. package/out/replicache/src/db/write.js.map +1 -1
  15. package/out/replicache/src/deleted-clients.d.ts +0 -2
  16. package/out/replicache/src/deleted-clients.d.ts.map +1 -1
  17. package/out/replicache/src/deleted-clients.js +1 -1
  18. package/out/replicache/src/deleted-clients.js.map +1 -1
  19. package/out/replicache/src/frozen-json.d.ts +0 -1
  20. package/out/replicache/src/frozen-json.d.ts.map +1 -1
  21. package/out/replicache/src/frozen-json.js.map +1 -1
  22. package/out/replicache/src/index-defs.d.ts +0 -2
  23. package/out/replicache/src/index-defs.d.ts.map +1 -1
  24. package/out/replicache/src/index-defs.js +1 -1
  25. package/out/replicache/src/index-defs.js.map +1 -1
  26. package/out/replicache/src/kv/expo-sqlite/store.d.ts +0 -1
  27. package/out/replicache/src/kv/expo-sqlite/store.d.ts.map +1 -1
  28. package/out/replicache/src/kv/expo-sqlite/store.js +1 -1
  29. package/out/replicache/src/kv/expo-sqlite/store.js.map +1 -1
  30. package/out/replicache/src/kv/idb-store.js +1 -1
  31. package/out/replicache/src/kv/idb-store.js.map +1 -1
  32. package/out/replicache/src/kv/throw-if-closed.d.ts +0 -3
  33. package/out/replicache/src/kv/throw-if-closed.d.ts.map +1 -1
  34. package/out/replicache/src/kv/throw-if-closed.js.map +1 -1
  35. package/out/replicache/src/persist/client-group-gc.d.ts +0 -7
  36. package/out/replicache/src/persist/client-group-gc.d.ts.map +1 -1
  37. package/out/replicache/src/persist/client-group-gc.js +1 -1
  38. package/out/replicache/src/persist/client-group-gc.js.map +1 -1
  39. package/out/replicache/src/process-scheduler.js +1 -1
  40. package/out/replicache/src/process-scheduler.js.map +1 -1
  41. package/out/replicache/src/replicache-impl.d.ts +2 -7
  42. package/out/replicache/src/replicache-impl.d.ts.map +1 -1
  43. package/out/replicache/src/replicache-impl.js +1 -1
  44. package/out/replicache/src/replicache-impl.js.map +1 -1
  45. package/out/replicache/src/sync/patch.d.ts +0 -5
  46. package/out/replicache/src/sync/patch.d.ts.map +1 -1
  47. package/out/replicache/src/sync/patch.js +1 -1
  48. package/out/replicache/src/sync/patch.js.map +1 -1
  49. package/out/replicache/src/sync/pull.d.ts +0 -1
  50. package/out/replicache/src/sync/pull.d.ts.map +1 -1
  51. package/out/replicache/src/sync/pull.js.map +1 -1
  52. package/out/replicache/src/sync/push.d.ts +2 -1
  53. package/out/replicache/src/sync/push.d.ts.map +1 -1
  54. package/out/replicache/src/sync/push.js.map +1 -1
  55. package/out/replicache/src/types.d.ts +2 -1
  56. package/out/replicache/src/types.d.ts.map +1 -1
  57. package/out/shared/src/asserts.d.ts +0 -5
  58. package/out/shared/src/asserts.d.ts.map +1 -1
  59. package/out/shared/src/asserts.js +1 -1
  60. package/out/shared/src/asserts.js.map +1 -1
  61. package/out/shared/src/browser-env.d.ts +0 -17
  62. package/out/shared/src/browser-env.d.ts.map +1 -1
  63. package/out/shared/src/browser-env.js.map +1 -1
  64. package/out/shared/src/deep-clone.d.ts +0 -1
  65. package/out/shared/src/deep-clone.d.ts.map +1 -1
  66. package/out/shared/src/deep-clone.js +1 -1
  67. package/out/shared/src/deep-clone.js.map +1 -1
  68. package/out/shared/src/document-visible.js +1 -1
  69. package/out/shared/src/document-visible.js.map +1 -1
  70. package/out/shared/src/expand.d.ts +0 -3
  71. package/out/shared/src/expand.d.ts.map +1 -1
  72. package/out/shared/src/immutable.d.ts +1 -1
  73. package/out/shared/src/immutable.d.ts.map +1 -1
  74. package/out/shared/src/iterables.d.ts +0 -1
  75. package/out/shared/src/iterables.d.ts.map +1 -1
  76. package/out/shared/src/iterables.js.map +1 -1
  77. package/out/shared/src/logging-test-utils.js +1 -1
  78. package/out/shared/src/logging-test-utils.js.map +1 -1
  79. package/out/shared/src/options-types.d.ts +1 -1
  80. package/out/shared/src/options-types.d.ts.map +1 -1
  81. package/out/shared/src/options.d.ts +2 -3
  82. package/out/shared/src/options.d.ts.map +1 -1
  83. package/out/shared/src/options.js +1 -1
  84. package/out/shared/src/options.js.map +1 -1
  85. package/out/shared/src/promise-race.js +1 -1
  86. package/out/shared/src/promise-race.js.map +1 -1
  87. package/out/shared/src/queue.js +1 -1
  88. package/out/shared/src/queue.js.map +1 -1
  89. package/out/shared/src/resolved-promises.d.ts +0 -4
  90. package/out/shared/src/resolved-promises.d.ts.map +1 -1
  91. package/out/shared/src/resolved-promises.js +0 -1
  92. package/out/shared/src/resolved-promises.js.map +1 -1
  93. package/out/shared/src/resolver.d.ts +9 -0
  94. package/out/shared/src/resolver.d.ts.map +1 -0
  95. package/out/shared/src/resolver.js +6 -0
  96. package/out/shared/src/resolver.js.map +1 -0
  97. package/out/shared/src/sleep.js +1 -1
  98. package/out/shared/src/sleep.js.map +1 -1
  99. package/out/shared/src/tdigest.d.ts +0 -1
  100. package/out/shared/src/tdigest.d.ts.map +1 -1
  101. package/out/shared/src/tdigest.js.map +1 -1
  102. package/out/z2s/src/sql.d.ts +1 -5
  103. package/out/z2s/src/sql.d.ts.map +1 -1
  104. package/out/z2s/src/sql.js.map +1 -1
  105. package/out/zero/package.js +2 -3
  106. package/out/zero/package.js.map +1 -1
  107. package/out/zero/src/zero-cache-dev.js +1 -1
  108. package/out/zero/src/zero-cache-dev.js.map +1 -1
  109. package/out/zero-cache/src/config/zero-config.d.ts +14 -11
  110. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  111. package/out/zero-cache/src/config/zero-config.js +23 -0
  112. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  113. package/out/zero-cache/src/db/migration-lite.js.map +1 -1
  114. package/out/zero-cache/src/db/migration.js +1 -1
  115. package/out/zero-cache/src/db/migration.js.map +1 -1
  116. package/out/zero-cache/src/db/pg-copy-binary.js +1 -1
  117. package/out/zero-cache/src/db/pg-copy-binary.js.map +1 -1
  118. package/out/zero-cache/src/db/pg-type-parser.js.map +1 -1
  119. package/out/zero-cache/src/db/specs.d.ts +3 -4
  120. package/out/zero-cache/src/db/specs.d.ts.map +1 -1
  121. package/out/zero-cache/src/db/specs.js +5 -7
  122. package/out/zero-cache/src/db/specs.js.map +1 -1
  123. package/out/zero-cache/src/db/transaction-pool.js +2 -2
  124. package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
  125. package/out/zero-cache/src/observability/events.js +1 -1
  126. package/out/zero-cache/src/observability/events.js.map +1 -1
  127. package/out/zero-cache/src/server/change-streamer.js +1 -1
  128. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  129. package/out/zero-cache/src/server/main.js +1 -1
  130. package/out/zero-cache/src/server/main.js.map +1 -1
  131. package/out/zero-cache/src/server/runner/run-worker.js +1 -1
  132. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  133. package/out/zero-cache/src/services/change-source/common/backfill-manager.js +1 -1
  134. package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
  135. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js +1 -1
  136. package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
  137. package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -1
  138. package/out/zero-cache/src/services/change-source/custom/change-source.js +1 -1
  139. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  140. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +1 -1
  141. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
  142. package/out/zero-cache/src/services/change-source/pg/change-source.js +1 -1
  143. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  144. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +1 -1
  145. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  146. package/out/zero-cache/src/services/change-source/pg/lsn.d.ts +0 -2
  147. package/out/zero-cache/src/services/change-source/pg/lsn.d.ts.map +1 -1
  148. package/out/zero-cache/src/services/change-source/pg/lsn.js.map +1 -1
  149. package/out/zero-cache/src/services/change-source/pg/replication-slots.js +1 -1
  150. package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
  151. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +0 -12
  152. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
  153. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +5 -3
  154. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  155. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.d.ts +1 -1
  156. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.d.ts.map +1 -1
  157. package/out/zero-cache/src/services/change-source/pg/schema/index-predicate.js.map +1 -1
  158. package/out/zero-cache/src/services/change-source/pg/schema/validation.d.ts +0 -1
  159. package/out/zero-cache/src/services/change-source/pg/schema/validation.d.ts.map +1 -1
  160. package/out/zero-cache/src/services/change-source/pg/schema/validation.js.map +1 -1
  161. package/out/zero-cache/src/services/change-streamer/backup-monitor.js +1 -1
  162. package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -1
  163. package/out/zero-cache/src/services/change-streamer/broadcast.js +1 -1
  164. package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
  165. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +1 -1
  166. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  167. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +1 -1
  168. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  169. package/out/zero-cache/src/services/change-streamer/change-streamer.js +1 -1
  170. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  171. package/out/zero-cache/src/services/change-streamer/drain-tracker.js +1 -1
  172. package/out/zero-cache/src/services/change-streamer/drain-tracker.js.map +1 -1
  173. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js +2 -2
  174. package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js.map +1 -1
  175. package/out/zero-cache/src/services/change-streamer/restore-admission.js +2 -2
  176. package/out/zero-cache/src/services/change-streamer/restore-admission.js.map +1 -1
  177. package/out/zero-cache/src/services/change-streamer/schema/tables.js +1 -1
  178. package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
  179. package/out/zero-cache/src/services/change-streamer/snapshot-message.js +2 -3
  180. package/out/zero-cache/src/services/change-streamer/snapshot-message.js.map +1 -1
  181. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js +2 -2
  182. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-catchup.js.map +1 -1
  183. package/out/zero-cache/src/services/change-streamer/sqlite-change-log-read-router.js.map +1 -1
  184. package/out/zero-cache/src/services/change-streamer/storer.js +1 -1
  185. package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
  186. package/out/zero-cache/src/services/change-streamer/subscribe.js +1 -1
  187. package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
  188. package/out/zero-cache/src/services/change-streamer/subscriber.js +1 -1
  189. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  190. package/out/zero-cache/src/services/life-cycle.js +1 -1
  191. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  192. package/out/zero-cache/src/services/litestream/commands.js +1 -1
  193. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  194. package/out/zero-cache/src/services/litestream/metrics.js.map +1 -1
  195. package/out/zero-cache/src/services/mutagen/mutagen.js +2 -2
  196. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  197. package/out/zero-cache/src/services/plannerz.d.ts +4 -12
  198. package/out/zero-cache/src/services/plannerz.d.ts.map +1 -1
  199. package/out/zero-cache/src/services/plannerz.js +1 -3
  200. package/out/zero-cache/src/services/plannerz.js.map +1 -1
  201. package/out/zero-cache/src/services/replicator/change-log-db.js +1 -1
  202. package/out/zero-cache/src/services/replicator/change-log-db.js.map +1 -1
  203. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js +1 -1
  204. package/out/zero-cache/src/services/replicator/change-log-stream-writer.js.map +1 -1
  205. package/out/zero-cache/src/services/replicator/drain-handler.js +1 -1
  206. package/out/zero-cache/src/services/replicator/drain-handler.js.map +1 -1
  207. package/out/zero-cache/src/services/replicator/reporter/recorder.js +1 -1
  208. package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
  209. package/out/zero-cache/src/services/replicator/reporter/report-schema.d.ts +2 -11
  210. package/out/zero-cache/src/services/replicator/reporter/report-schema.d.ts.map +1 -1
  211. package/out/zero-cache/src/services/replicator/reporter/report-schema.js +1 -1
  212. package/out/zero-cache/src/services/replicator/reporter/report-schema.js.map +1 -1
  213. package/out/zero-cache/src/services/replicator/schema/change-log.d.ts +0 -10
  214. package/out/zero-cache/src/services/replicator/schema/change-log.d.ts.map +1 -1
  215. package/out/zero-cache/src/services/replicator/schema/change-log.js.map +1 -1
  216. package/out/zero-cache/src/services/replicator/schema/column-metadata.d.ts +0 -7
  217. package/out/zero-cache/src/services/replicator/schema/column-metadata.d.ts.map +1 -1
  218. package/out/zero-cache/src/services/replicator/schema/column-metadata.js +1 -1
  219. package/out/zero-cache/src/services/replicator/schema/column-metadata.js.map +1 -1
  220. package/out/zero-cache/src/services/replicator/write-worker-client.js +1 -1
  221. package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
  222. package/out/zero-cache/src/services/running-state.js +1 -1
  223. package/out/zero-cache/src/services/running-state.js.map +1 -1
  224. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  225. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  226. package/out/zero-cache/src/services/view-syncer/cvr-store.js +1 -1
  227. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  228. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  229. package/out/zero-cache/src/services/view-syncer/drain-coordinator.js +1 -1
  230. package/out/zero-cache/src/services/view-syncer/drain-coordinator.js.map +1 -1
  231. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  232. package/out/zero-cache/src/services/view-syncer/query-stats.js.map +1 -1
  233. package/out/zero-cache/src/services/view-syncer/row-record-cache.js +1 -1
  234. package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
  235. package/out/zero-cache/src/services/view-syncer/schema/cvr.js +1 -1
  236. package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
  237. package/out/zero-cache/src/services/view-syncer/schema/types.js +3 -17
  238. package/out/zero-cache/src/services/view-syncer/schema/types.js.map +1 -1
  239. package/out/zero-cache/src/services/view-syncer/snapshotter.js +1 -1
  240. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  241. package/out/zero-cache/src/services/view-syncer/view-syncer.js +10 -10
  242. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  243. package/out/zero-cache/src/types/lite.d.ts +0 -1
  244. package/out/zero-cache/src/types/lite.d.ts.map +1 -1
  245. package/out/zero-cache/src/types/lite.js +2 -2
  246. package/out/zero-cache/src/types/lite.js.map +1 -1
  247. package/out/zero-cache/src/types/pg-types.d.ts +0 -40
  248. package/out/zero-cache/src/types/pg-types.d.ts.map +1 -1
  249. package/out/zero-cache/src/types/pg-types.js.map +1 -1
  250. package/out/zero-cache/src/types/poke-chunk.js +1 -1
  251. package/out/zero-cache/src/types/poke-chunk.js.map +1 -1
  252. package/out/zero-cache/src/types/row-key.d.ts +0 -5
  253. package/out/zero-cache/src/types/row-key.d.ts.map +1 -1
  254. package/out/zero-cache/src/types/row-key.js.map +1 -1
  255. package/out/zero-cache/src/types/streams.js +1 -1
  256. package/out/zero-cache/src/types/streams.js.map +1 -1
  257. package/out/zero-cache/src/types/subscription.js +1 -1
  258. package/out/zero-cache/src/types/subscription.js.map +1 -1
  259. package/out/zero-cache/src/types/websocket-handoff.d.ts +0 -6
  260. package/out/zero-cache/src/types/websocket-handoff.d.ts.map +1 -1
  261. package/out/zero-cache/src/types/websocket-handoff.js +8 -2
  262. package/out/zero-cache/src/types/websocket-handoff.js.map +1 -1
  263. package/out/zero-cache/src/types/ws.d.ts +2 -1
  264. package/out/zero-cache/src/types/ws.d.ts.map +1 -1
  265. package/out/zero-cache/src/types/ws.js +1 -1
  266. package/out/zero-cache/src/types/ws.js.map +1 -1
  267. package/out/zero-cache/src/workers/connection.js +4 -1
  268. package/out/zero-cache/src/workers/connection.js.map +1 -1
  269. package/out/zero-cache/src/workers/mutator.js +1 -1
  270. package/out/zero-cache/src/workers/mutator.js.map +1 -1
  271. package/out/zero-cache/src/workers/syncer.js +36 -2
  272. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  273. package/out/zero-client/src/client/active-clients-manager.d.ts.map +1 -1
  274. package/out/zero-client/src/client/active-clients-manager.js +1 -1
  275. package/out/zero-client/src/client/active-clients-manager.js.map +1 -1
  276. package/out/zero-client/src/client/connection-manager.js +1 -1
  277. package/out/zero-client/src/client/connection-manager.js.map +1 -1
  278. package/out/zero-client/src/client/crud.d.ts +2 -8
  279. package/out/zero-client/src/client/crud.d.ts.map +1 -1
  280. package/out/zero-client/src/client/crud.js +1 -1
  281. package/out/zero-client/src/client/crud.js.map +1 -1
  282. package/out/zero-client/src/client/custom.d.ts +0 -2
  283. package/out/zero-client/src/client/custom.d.ts.map +1 -1
  284. package/out/zero-client/src/client/custom.js +1 -1
  285. package/out/zero-client/src/client/custom.js.map +1 -1
  286. package/out/zero-client/src/client/enable-analytics.d.ts +0 -1
  287. package/out/zero-client/src/client/enable-analytics.d.ts.map +1 -1
  288. package/out/zero-client/src/client/enable-analytics.js +2 -1
  289. package/out/zero-client/src/client/enable-analytics.js.map +1 -1
  290. package/out/zero-client/src/client/error.d.ts +4 -5
  291. package/out/zero-client/src/client/error.d.ts.map +1 -1
  292. package/out/zero-client/src/client/error.js.map +1 -1
  293. package/out/zero-client/src/client/keys.d.ts +0 -1
  294. package/out/zero-client/src/client/keys.d.ts.map +1 -1
  295. package/out/zero-client/src/client/keys.js +4 -2
  296. package/out/zero-client/src/client/keys.js.map +1 -1
  297. package/out/zero-client/src/client/mutation-tracker.d.ts.map +1 -1
  298. package/out/zero-client/src/client/mutation-tracker.js +1 -1
  299. package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
  300. package/out/zero-client/src/client/options.d.ts +0 -4
  301. package/out/zero-client/src/client/options.d.ts.map +1 -1
  302. package/out/zero-client/src/client/options.js.map +1 -1
  303. package/out/zero-client/src/client/replicache-types.d.ts +3 -2
  304. package/out/zero-client/src/client/replicache-types.d.ts.map +1 -1
  305. package/out/zero-client/src/client/version.js +1 -1
  306. package/out/zero-client/src/client/zero-poke-handler.d.ts +1 -2
  307. package/out/zero-client/src/client/zero-poke-handler.d.ts.map +1 -1
  308. package/out/zero-client/src/client/zero-poke-handler.js +1 -1
  309. package/out/zero-client/src/client/zero-poke-handler.js.map +1 -1
  310. package/out/zero-client/src/client/zero.d.ts +1 -5
  311. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  312. package/out/zero-client/src/client/zero.js +1 -1
  313. package/out/zero-client/src/client/zero.js.map +1 -1
  314. package/out/zero-client/src/types/client-state.d.ts +0 -6
  315. package/out/zero-client/src/types/client-state.d.ts.map +1 -1
  316. package/out/zero-permissions/src/permissions.d.ts +1 -12
  317. package/out/zero-permissions/src/permissions.d.ts.map +1 -1
  318. package/out/zero-permissions/src/permissions.js +1 -2
  319. package/out/zero-permissions/src/permissions.js.map +1 -1
  320. package/out/zero-protocol/src/ast.d.ts +0 -4
  321. package/out/zero-protocol/src/ast.d.ts.map +1 -1
  322. package/out/zero-protocol/src/ast.js +1 -2
  323. package/out/zero-protocol/src/ast.js.map +1 -1
  324. package/out/zero-protocol/src/change-desired-queries.d.ts +0 -20
  325. package/out/zero-protocol/src/change-desired-queries.d.ts.map +1 -1
  326. package/out/zero-protocol/src/change-desired-queries.js.map +1 -1
  327. package/out/zero-protocol/src/client-schema.d.ts +2 -5
  328. package/out/zero-protocol/src/client-schema.d.ts.map +1 -1
  329. package/out/zero-protocol/src/client-schema.js +1 -1
  330. package/out/zero-protocol/src/client-schema.js.map +1 -1
  331. package/out/zero-protocol/src/close-connection.d.ts +0 -8
  332. package/out/zero-protocol/src/close-connection.d.ts.map +1 -1
  333. package/out/zero-protocol/src/close-connection.js +1 -1
  334. package/out/zero-protocol/src/close-connection.js.map +1 -1
  335. package/out/zero-protocol/src/connect.d.ts +0 -12
  336. package/out/zero-protocol/src/connect.d.ts.map +1 -1
  337. package/out/zero-protocol/src/connect.js +1 -1
  338. package/out/zero-protocol/src/connect.js.map +1 -1
  339. package/out/zero-protocol/src/custom-queries.d.ts +3 -17
  340. package/out/zero-protocol/src/custom-queries.d.ts.map +1 -1
  341. package/out/zero-protocol/src/custom-queries.js +1 -1
  342. package/out/zero-protocol/src/custom-queries.js.map +1 -1
  343. package/out/zero-protocol/src/data.d.ts +0 -1
  344. package/out/zero-protocol/src/data.d.ts.map +1 -1
  345. package/out/zero-protocol/src/data.js +1 -1
  346. package/out/zero-protocol/src/data.js.map +1 -1
  347. package/out/zero-protocol/src/down.d.ts +27 -27
  348. package/out/zero-protocol/src/inspect-down.d.ts +82 -84
  349. package/out/zero-protocol/src/inspect-down.d.ts.map +1 -1
  350. package/out/zero-protocol/src/inspect-down.js +1 -1
  351. package/out/zero-protocol/src/inspect-down.js.map +1 -1
  352. package/out/zero-protocol/src/inspect-up.d.ts +0 -28
  353. package/out/zero-protocol/src/inspect-up.d.ts.map +1 -1
  354. package/out/zero-protocol/src/inspect-up.js +1 -1
  355. package/out/zero-protocol/src/inspect-up.js.map +1 -1
  356. package/out/zero-protocol/src/mutate-server.d.ts +0 -21
  357. package/out/zero-protocol/src/mutate-server.d.ts.map +1 -1
  358. package/out/zero-protocol/src/mutate-server.js +1 -1
  359. package/out/zero-protocol/src/mutate-server.js.map +1 -1
  360. package/out/zero-protocol/src/mutation-type-enum.d.ts +0 -1
  361. package/out/zero-protocol/src/mutation-type-enum.d.ts.map +1 -1
  362. package/out/zero-protocol/src/mutation-type-enum.js.map +1 -1
  363. package/out/zero-protocol/src/mutation.d.ts +2 -4
  364. package/out/zero-protocol/src/mutation.d.ts.map +1 -1
  365. package/out/zero-protocol/src/mutation.js +1 -1
  366. package/out/zero-protocol/src/mutation.js.map +1 -1
  367. package/out/zero-protocol/src/mutations-patch.d.ts +0 -33
  368. package/out/zero-protocol/src/mutations-patch.d.ts.map +1 -1
  369. package/out/zero-protocol/src/mutations-patch.js +1 -1
  370. package/out/zero-protocol/src/mutations-patch.js.map +1 -1
  371. package/out/zero-protocol/src/ping.d.ts +0 -2
  372. package/out/zero-protocol/src/ping.d.ts.map +1 -1
  373. package/out/zero-protocol/src/ping.js +1 -1
  374. package/out/zero-protocol/src/ping.js.map +1 -1
  375. package/out/zero-protocol/src/poke.d.ts +3 -2
  376. package/out/zero-protocol/src/poke.d.ts.map +1 -1
  377. package/out/zero-protocol/src/poke.js +1 -1
  378. package/out/zero-protocol/src/poke.js.map +1 -1
  379. package/out/zero-protocol/src/pong.d.ts +0 -2
  380. package/out/zero-protocol/src/pong.d.ts.map +1 -1
  381. package/out/zero-protocol/src/pong.js +1 -1
  382. package/out/zero-protocol/src/pong.js.map +1 -1
  383. package/out/zero-protocol/src/pull.d.ts +2 -7
  384. package/out/zero-protocol/src/pull.d.ts.map +1 -1
  385. package/out/zero-protocol/src/pull.js +1 -1
  386. package/out/zero-protocol/src/pull.js.map +1 -1
  387. package/out/zero-protocol/src/push.d.ts +1 -3
  388. package/out/zero-protocol/src/push.d.ts.map +1 -1
  389. package/out/zero-protocol/src/push.js +1 -1
  390. package/out/zero-protocol/src/push.js.map +1 -1
  391. package/out/zero-protocol/src/queries-patch.d.ts +0 -16
  392. package/out/zero-protocol/src/queries-patch.d.ts.map +1 -1
  393. package/out/zero-protocol/src/queries-patch.js +1 -1
  394. package/out/zero-protocol/src/queries-patch.js.map +1 -1
  395. package/out/zero-protocol/src/query-server.d.ts +2 -41
  396. package/out/zero-protocol/src/query-server.d.ts.map +1 -1
  397. package/out/zero-protocol/src/query-server.js +1 -1
  398. package/out/zero-protocol/src/query-server.js.map +1 -1
  399. package/out/zero-protocol/src/up.d.ts +42 -42
  400. package/out/zero-protocol/src/version.d.ts +0 -1
  401. package/out/zero-protocol/src/version.d.ts.map +1 -1
  402. package/out/zero-protocol/src/version.js.map +1 -1
  403. package/out/zero-react/src/use-query.js +1 -1
  404. package/out/zero-react/src/use-query.js.map +1 -1
  405. package/out/zero-schema/src/builder/relationship-builder.d.ts +0 -2
  406. package/out/zero-schema/src/builder/relationship-builder.d.ts.map +1 -1
  407. package/out/zero-schema/src/builder/relationship-builder.js.map +1 -1
  408. package/out/zero-schema/src/builder/table-builder.d.ts +1 -1
  409. package/out/zero-schema/src/builder/table-builder.d.ts.map +1 -1
  410. package/out/zero-schema/src/builder/table-builder.js +1 -1
  411. package/out/zero-schema/src/builder/table-builder.js.map +1 -1
  412. package/out/zero-schema/src/schema-config.d.ts +0 -1
  413. package/out/zero-schema/src/schema-config.d.ts.map +1 -1
  414. package/out/zero-schema/src/schema-config.js +1 -1
  415. package/out/zero-schema/src/schema-config.js.map +1 -1
  416. package/out/zero-schema/src/table-schema.d.ts +3 -11
  417. package/out/zero-schema/src/table-schema.d.ts.map +1 -1
  418. package/out/zero-server/src/queries/process-queries.d.ts +2 -4
  419. package/out/zero-server/src/queries/process-queries.d.ts.map +1 -1
  420. package/out/zero-server/src/queries/process-queries.js.map +1 -1
  421. package/out/zero-solid/src/solid-view.d.ts +0 -2
  422. package/out/zero-solid/src/solid-view.d.ts.map +1 -1
  423. package/out/zero-solid/src/solid-view.js +1 -1
  424. package/out/zero-solid/src/solid-view.js.map +1 -1
  425. package/out/zero-types/src/name-mapper.d.ts +2 -1
  426. package/out/zero-types/src/name-mapper.d.ts.map +1 -1
  427. package/out/zero-types/src/name-mapper.js.map +1 -1
  428. package/out/zero-types/src/schema-value.d.ts +3 -2
  429. package/out/zero-types/src/schema-value.d.ts.map +1 -1
  430. package/out/zero-types/src/schema.d.ts +1 -1
  431. package/out/zero-types/src/schema.d.ts.map +1 -1
  432. package/out/zql/src/builder/builder.d.ts +1 -11
  433. package/out/zql/src/builder/builder.d.ts.map +1 -1
  434. package/out/zql/src/builder/builder.js +1 -1
  435. package/out/zql/src/builder/builder.js.map +1 -1
  436. package/out/zql/src/builder/filter.d.ts +0 -1
  437. package/out/zql/src/builder/filter.d.ts.map +1 -1
  438. package/out/zql/src/builder/filter.js.map +1 -1
  439. package/out/zql/src/error.d.ts +0 -5
  440. package/out/zql/src/error.d.ts.map +1 -1
  441. package/out/zql/src/error.js.map +1 -1
  442. package/out/zql/src/ivm/constraint.d.ts +0 -7
  443. package/out/zql/src/ivm/constraint.d.ts.map +1 -1
  444. package/out/zql/src/ivm/constraint.js +1 -1
  445. package/out/zql/src/ivm/constraint.js.map +1 -1
  446. package/out/zql/src/ivm/data.d.ts +0 -1
  447. package/out/zql/src/ivm/data.d.ts.map +1 -1
  448. package/out/zql/src/ivm/data.js.map +1 -1
  449. package/out/zql/src/ivm/memory-source.d.ts +1 -2
  450. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  451. package/out/zql/src/ivm/memory-source.js +1 -1
  452. package/out/zql/src/ivm/memory-source.js.map +1 -1
  453. package/out/zql/src/ivm/stream.d.ts +0 -1
  454. package/out/zql/src/ivm/stream.d.ts.map +1 -1
  455. package/out/zql/src/ivm/stream.js.map +1 -1
  456. package/out/zql/src/ivm/union-fan-in.d.ts +0 -1
  457. package/out/zql/src/ivm/union-fan-in.d.ts.map +1 -1
  458. package/out/zql/src/ivm/union-fan-in.js +1 -1
  459. package/out/zql/src/ivm/union-fan-in.js.map +1 -1
  460. package/out/zql/src/ivm/view-apply-change.d.ts +5 -35
  461. package/out/zql/src/ivm/view-apply-change.d.ts.map +1 -1
  462. package/out/zql/src/ivm/view-apply-change.js +1 -1
  463. package/out/zql/src/ivm/view-apply-change.js.map +1 -1
  464. package/out/zql/src/mutate/crud.d.ts +1 -3
  465. package/out/zql/src/mutate/crud.d.ts.map +1 -1
  466. package/out/zql/src/mutate/crud.js +1 -1
  467. package/out/zql/src/mutate/crud.js.map +1 -1
  468. package/out/zql/src/mutate/mutator-registry.d.ts +1 -1
  469. package/out/zql/src/mutate/mutator-registry.d.ts.map +1 -1
  470. package/out/zql/src/mutate/mutator-registry.js.map +1 -1
  471. package/out/zql/src/mutate/mutator.d.ts +1 -1
  472. package/out/zql/src/mutate/mutator.d.ts.map +1 -1
  473. package/out/zql/src/mutate/mutator.js.map +1 -1
  474. package/out/zql/src/planner/planner-debug.d.ts +10 -9
  475. package/out/zql/src/planner/planner-debug.d.ts.map +1 -1
  476. package/out/zql/src/planner/planner-debug.js.map +1 -1
  477. package/out/zql/src/planner/planner-node.d.ts +0 -1
  478. package/out/zql/src/planner/planner-node.d.ts.map +1 -1
  479. package/out/zql/src/planner/planner-node.js.map +1 -1
  480. package/out/zql/src/planner/planner-warnings.d.ts +3 -3
  481. package/out/zql/src/planner/planner-warnings.d.ts.map +1 -1
  482. package/out/zql/src/planner/planner-warnings.js.map +1 -1
  483. package/out/zql/src/query/expression.d.ts +0 -22
  484. package/out/zql/src/query/expression.d.ts.map +1 -1
  485. package/out/zql/src/query/expression.js +1 -1
  486. package/out/zql/src/query/expression.js.map +1 -1
  487. package/out/zql/src/query/query-delegate-base.d.ts +0 -7
  488. package/out/zql/src/query/query-delegate-base.d.ts.map +1 -1
  489. package/out/zql/src/query/query-delegate-base.js +2 -2
  490. package/out/zql/src/query/query-delegate-base.js.map +1 -1
  491. package/out/zql/src/query/query-delegate.d.ts +1 -4
  492. package/out/zql/src/query/query-delegate.d.ts.map +1 -1
  493. package/out/zql/src/query/query-internals.d.ts +0 -3
  494. package/out/zql/src/query/query-internals.d.ts.map +1 -1
  495. package/out/zql/src/query/query-internals.js.map +1 -1
  496. package/out/zql/src/query/query-registry.d.ts +5 -5
  497. package/out/zql/src/query/query-registry.d.ts.map +1 -1
  498. package/out/zql/src/query/query-registry.js.map +1 -1
  499. package/out/zql/src/query/query.d.ts +2 -13
  500. package/out/zql/src/query/query.d.ts.map +1 -1
  501. package/out/zql/src/query/runnable-query-impl.d.ts +1 -19
  502. package/out/zql/src/query/runnable-query-impl.d.ts.map +1 -1
  503. package/out/zql/src/query/runnable-query-impl.js +1 -1
  504. package/out/zql/src/query/runnable-query-impl.js.map +1 -1
  505. package/out/zql/src/query/ttl.d.ts +2 -3
  506. package/out/zql/src/query/ttl.d.ts.map +1 -1
  507. package/out/zql/src/query/ttl.js.map +1 -1
  508. package/out/zqlite/src/query-builder.d.ts +0 -2
  509. package/out/zqlite/src/query-builder.d.ts.map +1 -1
  510. package/out/zqlite/src/query-builder.js +1 -1
  511. package/out/zqlite/src/query-builder.js.map +1 -1
  512. package/out/zqlite/src/table-source.d.ts +0 -1
  513. package/out/zqlite/src/table-source.d.ts.map +1 -1
  514. package/out/zqlite/src/table-source.js +1 -1
  515. package/out/zqlite/src/table-source.js.map +1 -1
  516. package/package.json +2 -3
@@ -1 +1 @@
1
- {"version":3,"file":"subscriber.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/subscriber.ts"],"sourcesContent":["import {resolver, type Resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport type {Enum} from '../../../../shared/src/enum.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {RingBuffer} from '../../../../shared/src/ring-buffer.ts';\nimport {max} from '../../types/lexi-version.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport type {BackfillState} from './backfill-state.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport type {\n ChangeTag,\n Downstream,\n Status,\n WatermarkedChange,\n} from './change-streamer.ts';\nimport type * as ErrorType from './error-type-enum.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\nconst DEFAULT_BACKLOG_HIGH_WATER_BYTES = 16 * 1024 * 1024;\nconst DEFAULT_BACKLOG_LOW_WATER_RATIO = 0.8;\n\nexport type SubscriberOptions = {\n backlogHighWaterBytes?: number | undefined;\n backlogLowWaterRatio?: number | undefined;\n wsBatched?: boolean | undefined;\n\n /**\n * Called whenever the subscriber's acked watermark advances, i.e. when it\n * has confirmed that a commit was durably applied to its replica. The ACK\n * therefore lags the subscriber's replica rather than leading it.\n */\n onAck?: ((watermark: string) => void) | undefined;\n\n /**\n * The subscriber's pending backfills (protocol v8+), which are tracked\n * over the changes sent to the subscriber.\n */\n backfills?: BackfillState | undefined;\n\n /**\n * Called once the subscriber has been sent all changes up to the head of\n * the stream, i.e. its catchup and backlog have been flushed. From this\n * point on, the subscriber's tracked {@link backfills} reflect the same\n * position in the stream as that of the change-streamer.\n */\n onAligned?: ((subscriber: Subscriber) => void) | undefined;\n\n /**\n * Called when a `backfill` or `backfill-completed` message is ignored for\n * some of the subscriber's pending backfill columns, after the subscriber\n * is aligned.\n */\n onBackfillIgnored?: ((columns: string[]) => void) | undefined;\n\n /**\n * Resolves, once it is known whether lag reporting is enabled upstream, to\n * the means of requesting a lag report, or to `undefined` if it is disabled.\n *\n * Subscribers use lag reports to determine that they are caught up (e.g. to\n * advertise readiness to serve traffic). Once the subscriber is aligned, a\n * report is therefore requested, so that it is received without waiting out\n * the report interval. If lag reporting is disabled, a\n * {@link LAG_REPORTING_DISABLED} status is sent as a stand-in instead.\n */\n lagReports?: Promise<LagReports | undefined> | undefined;\n\n /**\n * Whether the subscriber switches over to another replication-manager\n * when this one drains (see `DrainTracker`).\n */\n drainable?: boolean | undefined;\n};\n\n/**\n * A status message queued in the backlog, so that it is sent downstream in\n * the order in which it was received relative to (backlogged) changes.\n */\ntype QueuedStatus = {readonly status: string};\n\ntype BacklogEntry = WatermarkedChange | QueuedStatus;\n\nfunction isQueuedStatus(entry: BacklogEntry): entry is QueuedStatus {\n return !Array.isArray(entry);\n}\n\nfunction entryBytes(entry: BacklogEntry): number {\n return isQueuedStatus(entry) ? entry.status.length : entry[2].length;\n}\n\nexport type BacklogFullWait = {\n readonly promise: Promise<void>;\n cancel(): void;\n};\n\nexport type SubscriberStats = {\n processRate: number;\n pending: number;\n backlog: number;\n backlogBytes: number;\n totalBufferedBytes: number;\n missedLastTimeout: boolean;\n};\n\n/**\n * Encapsulates a subscriber to changes. All subscribers start in a\n * \"catchup\" phase in which changes are buffered in a backlog while the\n * storer is queried to send any changes that were committed since the\n * subscriber's watermark. Once the catchup is complete, calls to\n * {@link send()} result in immediately sending the change.\n */\nexport class Subscriber {\n readonly #protocolVersion: number;\n readonly id: string;\n readonly mode: ReplicatorMode;\n readonly drainable: boolean;\n readonly #downstream: Subscription<string | PreSerializedBatch>;\n readonly #wsBatched: boolean;\n #watermark: string;\n #acked: string;\n #backlog: RingBuffer<BacklogEntry> | null;\n // While catchup is running, live changes are buffered here instead of being\n // pushed downstream. RingBuffer lets drainBacklog consume that backlog without\n // shifting an array, which matters when a subscriber is far behind.\n #backlogBytes = 0;\n #backlogInFlightBytes = 0;\n #backlogDrain: Promise<void> | null = null;\n readonly #backlogBackpressure: ByteBackpressureGate;\n readonly #backlogFullWaiters = new Set<Resolver<void>>();\n readonly #onAck: ((watermark: string) => void) | undefined;\n readonly #backfills: BackfillState | undefined;\n readonly #onAligned: ((subscriber: Subscriber) => void) | undefined;\n readonly #onBackfillIgnored: ((columns: string[]) => void) | undefined;\n readonly #lagReports: Promise<LagReports | undefined> | undefined;\n #aligned = false;\n // The watermark to be acked for the subscriber to be caught up, set when\n // it is aligned.\n #caughtUpWatermark: string | undefined;\n readonly #caughtUp = resolver<void>();\n\n constructor(\n protocolVersion: number,\n id: string,\n mode: ReplicatorMode,\n watermark: string,\n downstream: Subscription<string | PreSerializedBatch>,\n options: SubscriberOptions = {},\n ) {\n this.#protocolVersion = protocolVersion;\n this.id = id;\n this.mode = mode;\n this.drainable = options.drainable ?? false;\n this.#downstream = downstream;\n this.#wsBatched = options.wsBatched ?? false;\n this.#watermark = watermark;\n this.#acked = watermark;\n this.#backlog = new RingBuffer();\n this.#backlogBackpressure = new ByteBackpressureGate(\n options.backlogHighWaterBytes ?? DEFAULT_BACKLOG_HIGH_WATER_BYTES,\n options.backlogLowWaterRatio ?? DEFAULT_BACKLOG_LOW_WATER_RATIO,\n );\n this.#onAck = options.onAck;\n this.#backfills = options.backfills;\n this.#onAligned = options.onAligned;\n this.#onBackfillIgnored = options.onBackfillIgnored;\n this.#lagReports = options.lagReports;\n }\n\n /**\n * The subscriber's tracked pending backfills, or `undefined` if the\n * subscriber does not report them (i.e. protocol < v8).\n */\n get backfills(): BackfillState | undefined {\n return this.#backfills;\n }\n\n /**\n * Whether the subscriber has been sent all changes up to the head of the\n * stream (see {@link SubscriberOptions.onAligned}).\n */\n get aligned(): boolean {\n return this.#aligned;\n }\n\n /**\n * Resolves once the subscriber has caught up, i.e. once it has acked\n * (durably applied) all changes up to the head of the stream at the time\n * it was {@link aligned}. Never resolves if the subscriber is closed first.\n */\n get caughtUp(): Promise<void> {\n return this.#caughtUp.promise;\n }\n\n #trackBackfills(change: WatermarkedChange) {\n if (this.#backfills) {\n const ignored = this.#backfills.applySerialized(change);\n if (ignored.length && this.#aligned) {\n this.#onBackfillIgnored?.(ignored);\n }\n }\n }\n\n get watermark() {\n return this.#watermark;\n }\n\n get acked() {\n return this.#acked;\n }\n\n /**\n * Whether the backlog of live changes buffered during catchup has reached the\n * point at which {@link send()} stops resolving. Past it the subscriber is no\n * longer free: it holds up every subsequent flush, and with no other\n * subscriber to form a majority it stalls replication outright.\n */\n get backlogFull() {\n return (\n this.#bufferedBacklogBytes >= this.#backlogBackpressure.highWaterBytes\n );\n }\n\n /**\n * @returns whether the subscriber is currently sending backlogged messages,\n * vs caught up and sending the \"head\" of the replication stream.\n */\n isBacklogged() {\n return this.#backlog !== null || this.#bufferedBacklogBytes > 0;\n }\n\n /**\n * Resolves the first time {@link backlogFull} becomes true, so that a caller\n * holding the subscriber in catchup can give up at the moment the subscriber\n * starts costing replication rather than on a timer. Call cancel() when the\n * wait is no longer needed so the subscriber does not retain it.\n */\n whenBacklogFull(): BacklogFullWait {\n if (this.backlogFull) {\n return {promise: promiseVoid, cancel() {}};\n }\n const r = resolver<void>();\n this.#backlogFullWaiters.add(r);\n return {\n promise: r.promise,\n cancel: () => {\n this.#backlogFullWaiters.delete(r);\n },\n };\n }\n\n send(change: WatermarkedChange): Promise<void> {\n const [watermark] = change;\n if (watermark > this.#watermark) {\n if (this.#backlog) {\n // During catchup, buffer live changes behind the durable catchup stream.\n // The returned promise applies backpressure if the buffered bytes cross\n // the high water mark.\n this.#pushBacklog(change);\n return this.#maybeWaitForBacklogSpace();\n }\n return this.#sendChange(change);\n }\n return promiseVoid;\n }\n\n sendBatch(\n changes: readonly WatermarkedChange[],\n preSerialized?: PreSerializedBatch | undefined,\n ): Promise<void> {\n if (changes.length === 0) {\n return promiseVoid;\n }\n\n if (\n this.#wsBatched &&\n preSerialized &&\n !this.#backlog &&\n this.#initialized &&\n changes[0][0] > this.#watermark &&\n (this.#protocolVersion >= 5 ||\n changes.every(c => this.supportsMessage(c[1])))\n ) {\n let commitWatermark: string | undefined;\n for (let i = changes.length - 1; i >= 0; i--) {\n if (changes[i][1] === 'commit') {\n commitWatermark = changes[i][0];\n break;\n }\n }\n if (commitWatermark) {\n this.#watermark = commitWatermark;\n }\n for (const change of changes) {\n this.#trackBackfills(change);\n }\n return this.#sendPreSerializedDownstream(preSerialized, commitWatermark);\n }\n\n const promises: Promise<void>[] = [];\n for (const change of changes) {\n const p = this.send(change);\n if (p !== promiseVoid) {\n promises.push(p);\n }\n }\n if (promises.length === 0) {\n return promiseVoid;\n }\n if (promises.length === 1) {\n return promises[0];\n }\n return Promise.all(promises).then(() => {});\n }\n\n #initialized = false;\n\n /**\n * Called once the subscriber's watermark has been validated in the initial\n * catchup process.\n */\n #initialize() {\n if (!this.#initialized) {\n this.#initialized = true;\n // The initial status precedes the catchup, so it bypasses the backlog.\n void this.#sendDownstream(['status', {tag: 'status'}]);\n }\n }\n\n /**\n * Sends a status message (e.g. a lag report) downstream. While the\n * subscriber is catching up, the status is queued in the backlog behind\n * the changes that preceded it in the replication stream, so that the\n * subscriber processes it in stream order. This is necessary for lag\n * reports to measure the lag of the subscriber, including its catchup.\n *\n * Status messages received before the subscriber is initialized are\n * dropped.\n */\n sendStatus(status: Status) {\n if (!this.#initialized) {\n return;\n }\n const downstream: Downstream = ['status', status];\n if (this.#backlog) {\n this.#pushBacklog({status: BigIntJSON.stringify(downstream)});\n return;\n }\n void this.#sendDownstream(downstream);\n }\n\n /**\n * Sends a control status (e.g. `draining` or `restore`) downstream\n * immediately, regardless of whether the subscriber is initialized or\n * catching up. Unlike {@link sendStatus}, such statuses are not tied to a\n * position in the replication stream.\n */\n sendControlStatus(status: Status) {\n void this.#sendDownstream(['status', status]);\n }\n\n /** catchup() is called on ChangeEntries loaded from the store. */\n async catchup(change: WatermarkedChange) {\n this.#initialize();\n await this.#sendChange(change);\n }\n\n /**\n * Marks the Subscribe as \"caught up\" and flushes any backlog of\n * entries that were received during the catchup.\n */\n setCaughtUp(): Promise<void> {\n this.#initialize();\n if (!this.#backlog) {\n return this.#backlogDrain ?? promiseVoid;\n }\n if (!this.#backlogDrain) {\n // Keep #backlog non-null while queued entries are being handed to\n // downstream. That preserves ordering for sends that race with\n // setCaughtUp(): they append to the same backlog instead of bypassing\n // older buffered changes.\n this.#backlogDrain = this.#drainBacklog();\n void this.#backlogDrain.catch(e => this.fail(e));\n }\n return this.#backlogDrain;\n }\n\n async #sendChange(change: WatermarkedChange) {\n const [watermark, tag, json] = change;\n if (watermark <= this.watermark) {\n return;\n }\n if (!this.supportsMessage(tag)) {\n return;\n }\n if (tag === 'commit') {\n this.#watermark = watermark;\n }\n this.#trackBackfills(change);\n const result = await this.#sendStringifiedDownstream(json);\n if (tag === 'commit' && result === 'consumed') {\n this.#advanceAck(watermark);\n }\n }\n\n #advanceAck(watermark: string) {\n // Sends can complete out of order (e.g. the bounded window in\n // #drainBacklog), so the ack only advances monotonically, and listeners\n // are only notified when it does.\n const acked = max(this.#acked, watermark);\n if (acked !== this.#acked) {\n this.#acked = acked;\n this.#onAck?.(acked);\n this.#checkCaughtUp();\n }\n }\n\n #checkCaughtUp() {\n if (\n this.#caughtUpWatermark !== undefined &&\n this.#acked >= this.#caughtUpWatermark\n ) {\n this.#caughtUpWatermark = undefined;\n this.#caughtUp.resolve();\n }\n }\n\n #sendDownstream(downstream: Downstream) {\n return this.#sendStringifiedDownstream(BigIntJSON.stringify(downstream));\n }\n\n async #sendStringifiedDownstream(json: string) {\n const size = json.length;\n this.#pending++;\n this.#pendingBytes += size;\n const {result} = this.#downstream.push(json);\n try {\n return await result;\n } finally {\n this.#pending--;\n this.#pendingBytes -= size;\n this.#processed++;\n }\n }\n\n async #sendPreSerializedDownstream(\n batch: PreSerializedBatch,\n commitWatermark?: string | undefined,\n ): Promise<void> {\n const size = batch.byteLength;\n this.#pending += batch.changes.length;\n this.#pendingBytes += size;\n const {result} = this.#downstream.push(batch);\n try {\n const outcome = await result;\n if (commitWatermark && outcome === 'consumed') {\n this.#advanceAck(commitWatermark);\n }\n } finally {\n this.#pending -= batch.changes.length;\n this.#pendingBytes -= size;\n this.#processed += batch.changes.length;\n }\n }\n\n // `pending` and `processed` stats are tracked by periodically sampling\n // the running totals (by the progress tracker in the Forwarder).\n // This information was originally collected for use in flow control\n // decisions. The final flow control algorithm ended up being simpler\n // than expected and does not actually use this information. However, the\n // stats are still tracked and logged during flow control decisions for\n // debugging, forensics, and potential improvements to the algorithm.\n\n #pending = 0;\n #pendingBytes = 0;\n #processed = 0;\n #samples: {processed: number; timestamp: number}[] = [\n {processed: 0, timestamp: performance.now()},\n ];\n\n /**\n * The number of downstream messages that have yet to be acked.\n */\n get numPending() {\n return this.#pending + this.#backlogCount;\n }\n\n /**\n * The total number of downstream messages that the subscriber has\n * processed (i.e. acked).\n */\n get numProcessed() {\n return this.#processed;\n }\n\n /**\n * Records a new history entry for the number of messages processed,\n * keeping the number of samples bounded to `maxSamples`.\n */\n sampleProcessRate(now: number, maxSamples = 10): this {\n while (this.#samples.length >= maxSamples) {\n this.#samples.shift();\n }\n this.#samples.push({processed: this.#processed, timestamp: now});\n return this;\n }\n\n getStats(): SubscriberStats {\n const pending = this.numPending;\n if (this.#samples.length < 2) {\n return {\n processRate: 0,\n pending,\n backlog: this.#backlogCount,\n backlogBytes: this.#bufferedBacklogBytes,\n totalBufferedBytes: this.#totalBufferedBytes,\n missedLastTimeout: this.#missedLastTimeout,\n };\n }\n const from = this.#samples[0];\n const to = must(this.#samples.at(-1));\n const processed = to.processed - from.processed;\n const seconds = (to.timestamp - from.timestamp) / 1000;\n const processRate = seconds === 0 ? 0 : processed / seconds;\n return {\n processRate,\n pending,\n backlog: this.#backlogCount,\n backlogBytes: this.#bufferedBacklogBytes,\n totalBufferedBytes: this.#totalBufferedBytes,\n missedLastTimeout: this.#missedLastTimeout,\n };\n }\n\n #missedLastTimeout = false;\n #laggingSinceMs: number | undefined;\n\n trackResponseResult(result: 'on-time' | 'timed-out') {\n this.#missedLastTimeout = result === 'timed-out';\n if (result === 'on-time') {\n this.#laggingSinceMs = undefined;\n }\n }\n\n /**\n * Reports the change rate of a slow subscriber (i.e. that missed the last\n * timeout) compared to the change rate of the (slowest) subscriber that\n * responded on time.\n *\n * * `lagging` indicates that this subscriber is slower\n * * `catching-up` indicates that it is faster\n *\n * Returns the total duration in which subscriber has been continuously\n * reported as `lagging`.\n */\n reportChangeRate(now: number, status: 'lagging' | 'catching-up') {\n assert(\n this.#missedLastTimeout,\n `reportChangeRate should only be called for slow subscribers`,\n );\n if (status === 'catching-up') {\n this.#laggingSinceMs = undefined;\n return 0;\n }\n this.#laggingSinceMs ??= now;\n return now - this.#laggingSinceMs;\n }\n\n supportsMessage(tag: ChangeTag) {\n switch (tag) {\n case 'update-table-metadata':\n // update-table-row-key is only understood by subscribers >= protocol v5\n return this.#protocolVersion >= 5;\n }\n return true;\n }\n\n /**\n * Ends the subscription without sending a downstream `['error', ...]`.\n *\n * This is deliberate, and not the same as reporting the failure to the\n * subscriber: `IncrementalSyncer` treats any `['error', ...]` as terminal and\n * shuts down to restore a fresh replica from litestream, whereas a clean end\n * backs off and re-subscribes. The failures routed here -- storer catchup\n * errors, backlog drain errors, backup-monitor errors -- are transient, so a\n * reconnect is the proportionate response and a fleet-wide restore is not.\n *\n * Callers are responsible for logging `err`; it is not carried downstream.\n */\n fail(_err?: unknown) {\n this.close();\n }\n\n close(\n error?: ErrorType,\n message?: string,\n details?: {replicaVersion?: string | undefined},\n ) {\n // Closing the subscriber must also release producers that are blocked on\n // backlog capacity; there is no future drain that could wake them.\n this.#backlog = null;\n this.#backlogBytes = 0;\n this.#backlogBackpressure.releaseAll();\n // The backlog can no longer grow, so nothing would ever resolve these.\n // Waiters re-check backlogFull, which is now false, and see the close.\n this.#resolveBacklogFullWaiters();\n\n if (error !== undefined) {\n // Wait for the ACK of the error message before closing the connection.\n void this.#sendDownstream([\n 'error',\n {type: error, message, ...details},\n ]).finally(() => this.#downstream.cancel());\n } else {\n this.#downstream.cancel();\n }\n }\n\n get #backlogCount() {\n return this.#backlog?.size ?? 0;\n }\n\n get #bufferedBacklogBytes() {\n // Include entries already handed to downstream but not yet consumed. Without\n // this, setCaughtUp() could move bytes out of #backlog faster than the\n // downstream Subscription can process them and release producers too early.\n return this.#backlogBytes + this.#backlogInFlightBytes;\n }\n\n get #totalBufferedBytes() {\n return this.#bufferedBacklogBytes + this.#pendingBytes;\n }\n\n #pushBacklog(entry: BacklogEntry) {\n assert(this.#backlog, 'cannot push to backlog after catchup completed');\n this.#backlog.push(entry);\n this.#backlogBytes += entryBytes(entry);\n if (this.backlogFull) {\n this.#resolveBacklogFullWaiters();\n }\n }\n\n #resolveBacklogFullWaiters() {\n const waiters = [...this.#backlogFullWaiters];\n this.#backlogFullWaiters.clear();\n for (const waiter of waiters) {\n waiter.resolve();\n }\n }\n\n #maybeWaitForBacklogSpace(): Promise<void> {\n return this.#backlogBackpressure.waitForSpace(this.#bufferedBacklogBytes);\n }\n\n async #drainBacklog() {\n const inFlight: {promise: Promise<void>; bytes: number}[] = [];\n let inFlightBytes = 0;\n\n try {\n for (;;) {\n const entry = this.#backlog?.shift();\n if (!entry) {\n const closed = this.#backlog === null;\n this.#backlog = null;\n this.#backlogBytes = 0;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n if (!closed && !this.#aligned) {\n // All changes up to the head have been handed to #sendChange()\n // (and thus tracked), and subsequent changes are sent directly.\n this.#aligned = true;\n this.#onAligned?.(this);\n this.#caughtUpWatermark = this.#watermark;\n this.#checkCaughtUp();\n void this.#lagReports?.then(lagReports => {\n if (lagReports) {\n lagReports.request();\n } else {\n this.sendStatus(LAG_REPORTING_DISABLED);\n }\n });\n }\n break;\n }\n\n const bytes = entryBytes(entry);\n this.#backlogBytes -= bytes;\n this.#backlogInFlightBytes += bytes;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n\n // Send backlog entries in order, but keep only a bounded byte window in\n // flight. This avoids replacing one unbounded buffer with another inside\n // the downstream Subscription during catchup completion.\n const send = isQueuedStatus(entry)\n ? this.#sendStringifiedDownstream(entry.status).then(() => {})\n : this.#sendChange(entry);\n const promise = send.finally(() => {\n this.#backlogInFlightBytes -= bytes;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n });\n inFlight.push({promise, bytes});\n inFlightBytes += bytes;\n\n while (inFlightBytes >= this.#backlogBackpressure.highWaterBytes) {\n const next = must(inFlight.shift());\n await next.promise;\n inFlightBytes -= next.bytes;\n }\n }\n\n for (const {promise} of inFlight) {\n await promise;\n }\n } finally {\n this.#backlogDrain = null;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n }\n }\n}\n\nclass ByteBackpressureGate {\n readonly highWaterBytes: number;\n readonly #lowWaterBytes: number;\n readonly #waiters: Resolver<void>[] = [];\n\n constructor(highWaterBytes: number, lowWaterRatio: number) {\n this.highWaterBytes = Math.max(1, highWaterBytes);\n this.#lowWaterBytes =\n this.highWaterBytes * Math.min(1, Math.max(0, lowWaterRatio));\n }\n\n waitForSpace(bufferedBytes: number): Promise<void> {\n if (bufferedBytes < this.highWaterBytes) {\n return promiseVoid;\n }\n\n // One waiter represents one send() call that has already appended its\n // change. The producer is released when the backlog falls back below the low\n // water mark or the subscriber closes.\n const r = resolver<void>();\n this.#waiters.push(r);\n return r.promise;\n }\n\n releaseIfUnderLowWater(bufferedBytes: number) {\n if (this.#waiters.length === 0 || bufferedBytes > this.#lowWaterBytes) {\n return;\n }\n\n // Use a low water mark so waiting producers are released in batches instead\n // of waking one at a time around the high water boundary.\n this.releaseAll();\n }\n\n releaseAll() {\n const waiters = this.#waiters.splice(0);\n for (const waiter of waiters) {\n waiter.resolve();\n }\n }\n}\n\n/** Requests lag reports from upstream (see {@link SubscriberOptions.lagReports}). */\nexport type LagReports = {\n /**\n * Requests that a lag report be sent now rather than at the next report\n * interval. Requests are coalesced upstream, e.g. with a report in flight.\n */\n request(): void;\n};\n\n/**\n * Sent to an aligned subscriber when lag reporting is disabled upstream,\n * signaling that the subscriber is caught up. This is a lag report with a\n * `nextSendTimeMs` of 0 and no `lastTimings`, so that no lag is measured\n * (see `isLagReportingDisabledSignal()` in the replicator's recorder).\n */\nexport const LAG_REPORTING_DISABLED: Status = {\n tag: 'status',\n lagReport: {nextSendTimeMs: 0},\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"subscriber.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/subscriber.ts"],"sourcesContent":["import {assert} from '../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport type {Enum} from '../../../../shared/src/enum.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {resolver, type Resolver} from '../../../../shared/src/resolver.ts';\nimport {RingBuffer} from '../../../../shared/src/ring-buffer.ts';\nimport {max} from '../../types/lexi-version.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport type {BackfillState} from './backfill-state.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport type {\n ChangeTag,\n Downstream,\n Status,\n WatermarkedChange,\n} from './change-streamer.ts';\nimport type * as ErrorType from './error-type-enum.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\nconst DEFAULT_BACKLOG_HIGH_WATER_BYTES = 16 * 1024 * 1024;\nconst DEFAULT_BACKLOG_LOW_WATER_RATIO = 0.8;\n\nexport type SubscriberOptions = {\n backlogHighWaterBytes?: number | undefined;\n backlogLowWaterRatio?: number | undefined;\n wsBatched?: boolean | undefined;\n\n /**\n * Called whenever the subscriber's acked watermark advances, i.e. when it\n * has confirmed that a commit was durably applied to its replica. The ACK\n * therefore lags the subscriber's replica rather than leading it.\n */\n onAck?: ((watermark: string) => void) | undefined;\n\n /**\n * The subscriber's pending backfills (protocol v8+), which are tracked\n * over the changes sent to the subscriber.\n */\n backfills?: BackfillState | undefined;\n\n /**\n * Called once the subscriber has been sent all changes up to the head of\n * the stream, i.e. its catchup and backlog have been flushed. From this\n * point on, the subscriber's tracked {@link backfills} reflect the same\n * position in the stream as that of the change-streamer.\n */\n onAligned?: ((subscriber: Subscriber) => void) | undefined;\n\n /**\n * Called when a `backfill` or `backfill-completed` message is ignored for\n * some of the subscriber's pending backfill columns, after the subscriber\n * is aligned.\n */\n onBackfillIgnored?: ((columns: string[]) => void) | undefined;\n\n /**\n * Resolves, once it is known whether lag reporting is enabled upstream, to\n * the means of requesting a lag report, or to `undefined` if it is disabled.\n *\n * Subscribers use lag reports to determine that they are caught up (e.g. to\n * advertise readiness to serve traffic). Once the subscriber is aligned, a\n * report is therefore requested, so that it is received without waiting out\n * the report interval. If lag reporting is disabled, a\n * {@link LAG_REPORTING_DISABLED} status is sent as a stand-in instead.\n */\n lagReports?: Promise<LagReports | undefined> | undefined;\n\n /**\n * Whether the subscriber switches over to another replication-manager\n * when this one drains (see `DrainTracker`).\n */\n drainable?: boolean | undefined;\n};\n\n/**\n * A status message queued in the backlog, so that it is sent downstream in\n * the order in which it was received relative to (backlogged) changes.\n */\ntype QueuedStatus = {readonly status: string};\n\ntype BacklogEntry = WatermarkedChange | QueuedStatus;\n\nfunction isQueuedStatus(entry: BacklogEntry): entry is QueuedStatus {\n return !Array.isArray(entry);\n}\n\nfunction entryBytes(entry: BacklogEntry): number {\n return isQueuedStatus(entry) ? entry.status.length : entry[2].length;\n}\n\nexport type BacklogFullWait = {\n readonly promise: Promise<void>;\n cancel(): void;\n};\n\nexport type SubscriberStats = {\n processRate: number;\n pending: number;\n backlog: number;\n backlogBytes: number;\n totalBufferedBytes: number;\n missedLastTimeout: boolean;\n};\n\n/**\n * Encapsulates a subscriber to changes. All subscribers start in a\n * \"catchup\" phase in which changes are buffered in a backlog while the\n * storer is queried to send any changes that were committed since the\n * subscriber's watermark. Once the catchup is complete, calls to\n * {@link send()} result in immediately sending the change.\n */\nexport class Subscriber {\n readonly #protocolVersion: number;\n readonly id: string;\n readonly mode: ReplicatorMode;\n readonly drainable: boolean;\n readonly #downstream: Subscription<string | PreSerializedBatch>;\n readonly #wsBatched: boolean;\n #watermark: string;\n #acked: string;\n #backlog: RingBuffer<BacklogEntry> | null;\n // While catchup is running, live changes are buffered here instead of being\n // pushed downstream. RingBuffer lets drainBacklog consume that backlog without\n // shifting an array, which matters when a subscriber is far behind.\n #backlogBytes = 0;\n #backlogInFlightBytes = 0;\n #backlogDrain: Promise<void> | null = null;\n readonly #backlogBackpressure: ByteBackpressureGate;\n readonly #backlogFullWaiters = new Set<Resolver<void>>();\n readonly #onAck: ((watermark: string) => void) | undefined;\n readonly #backfills: BackfillState | undefined;\n readonly #onAligned: ((subscriber: Subscriber) => void) | undefined;\n readonly #onBackfillIgnored: ((columns: string[]) => void) | undefined;\n readonly #lagReports: Promise<LagReports | undefined> | undefined;\n #aligned = false;\n // The watermark to be acked for the subscriber to be caught up, set when\n // it is aligned.\n #caughtUpWatermark: string | undefined;\n readonly #caughtUp = resolver<void>();\n\n constructor(\n protocolVersion: number,\n id: string,\n mode: ReplicatorMode,\n watermark: string,\n downstream: Subscription<string | PreSerializedBatch>,\n options: SubscriberOptions = {},\n ) {\n this.#protocolVersion = protocolVersion;\n this.id = id;\n this.mode = mode;\n this.drainable = options.drainable ?? false;\n this.#downstream = downstream;\n this.#wsBatched = options.wsBatched ?? false;\n this.#watermark = watermark;\n this.#acked = watermark;\n this.#backlog = new RingBuffer();\n this.#backlogBackpressure = new ByteBackpressureGate(\n options.backlogHighWaterBytes ?? DEFAULT_BACKLOG_HIGH_WATER_BYTES,\n options.backlogLowWaterRatio ?? DEFAULT_BACKLOG_LOW_WATER_RATIO,\n );\n this.#onAck = options.onAck;\n this.#backfills = options.backfills;\n this.#onAligned = options.onAligned;\n this.#onBackfillIgnored = options.onBackfillIgnored;\n this.#lagReports = options.lagReports;\n }\n\n /**\n * The subscriber's tracked pending backfills, or `undefined` if the\n * subscriber does not report them (i.e. protocol < v8).\n */\n get backfills(): BackfillState | undefined {\n return this.#backfills;\n }\n\n /**\n * Whether the subscriber has been sent all changes up to the head of the\n * stream (see {@link SubscriberOptions.onAligned}).\n */\n get aligned(): boolean {\n return this.#aligned;\n }\n\n /**\n * Resolves once the subscriber has caught up, i.e. once it has acked\n * (durably applied) all changes up to the head of the stream at the time\n * it was {@link aligned}. Never resolves if the subscriber is closed first.\n */\n get caughtUp(): Promise<void> {\n return this.#caughtUp.promise;\n }\n\n #trackBackfills(change: WatermarkedChange) {\n if (this.#backfills) {\n const ignored = this.#backfills.applySerialized(change);\n if (ignored.length && this.#aligned) {\n this.#onBackfillIgnored?.(ignored);\n }\n }\n }\n\n get watermark() {\n return this.#watermark;\n }\n\n get acked() {\n return this.#acked;\n }\n\n /**\n * Whether the backlog of live changes buffered during catchup has reached the\n * point at which {@link send()} stops resolving. Past it the subscriber is no\n * longer free: it holds up every subsequent flush, and with no other\n * subscriber to form a majority it stalls replication outright.\n */\n get backlogFull() {\n return (\n this.#bufferedBacklogBytes >= this.#backlogBackpressure.highWaterBytes\n );\n }\n\n /**\n * @returns whether the subscriber is currently sending backlogged messages,\n * vs caught up and sending the \"head\" of the replication stream.\n */\n isBacklogged() {\n return this.#backlog !== null || this.#bufferedBacklogBytes > 0;\n }\n\n /**\n * Resolves the first time {@link backlogFull} becomes true, so that a caller\n * holding the subscriber in catchup can give up at the moment the subscriber\n * starts costing replication rather than on a timer. Call cancel() when the\n * wait is no longer needed so the subscriber does not retain it.\n */\n whenBacklogFull(): BacklogFullWait {\n if (this.backlogFull) {\n return {promise: promiseVoid, cancel() {}};\n }\n const r = resolver<void>();\n this.#backlogFullWaiters.add(r);\n return {\n promise: r.promise,\n cancel: () => {\n this.#backlogFullWaiters.delete(r);\n },\n };\n }\n\n send(change: WatermarkedChange): Promise<void> {\n const [watermark] = change;\n if (watermark > this.#watermark) {\n if (this.#backlog) {\n // During catchup, buffer live changes behind the durable catchup stream.\n // The returned promise applies backpressure if the buffered bytes cross\n // the high water mark.\n this.#pushBacklog(change);\n return this.#maybeWaitForBacklogSpace();\n }\n return this.#sendChange(change);\n }\n return promiseVoid;\n }\n\n sendBatch(\n changes: readonly WatermarkedChange[],\n preSerialized?: PreSerializedBatch | undefined,\n ): Promise<void> {\n if (changes.length === 0) {\n return promiseVoid;\n }\n\n if (\n this.#wsBatched &&\n preSerialized &&\n !this.#backlog &&\n this.#initialized &&\n changes[0][0] > this.#watermark &&\n (this.#protocolVersion >= 5 ||\n changes.every(c => this.supportsMessage(c[1])))\n ) {\n let commitWatermark: string | undefined;\n for (let i = changes.length - 1; i >= 0; i--) {\n if (changes[i][1] === 'commit') {\n commitWatermark = changes[i][0];\n break;\n }\n }\n if (commitWatermark) {\n this.#watermark = commitWatermark;\n }\n for (const change of changes) {\n this.#trackBackfills(change);\n }\n return this.#sendPreSerializedDownstream(preSerialized, commitWatermark);\n }\n\n const promises: Promise<void>[] = [];\n for (const change of changes) {\n const p = this.send(change);\n if (p !== promiseVoid) {\n promises.push(p);\n }\n }\n if (promises.length === 0) {\n return promiseVoid;\n }\n if (promises.length === 1) {\n return promises[0];\n }\n return Promise.all(promises).then(() => {});\n }\n\n #initialized = false;\n\n /**\n * Called once the subscriber's watermark has been validated in the initial\n * catchup process.\n */\n #initialize() {\n if (!this.#initialized) {\n this.#initialized = true;\n // The initial status precedes the catchup, so it bypasses the backlog.\n void this.#sendDownstream(['status', {tag: 'status'}]);\n }\n }\n\n /**\n * Sends a status message (e.g. a lag report) downstream. While the\n * subscriber is catching up, the status is queued in the backlog behind\n * the changes that preceded it in the replication stream, so that the\n * subscriber processes it in stream order. This is necessary for lag\n * reports to measure the lag of the subscriber, including its catchup.\n *\n * Status messages received before the subscriber is initialized are\n * dropped.\n */\n sendStatus(status: Status) {\n if (!this.#initialized) {\n return;\n }\n const downstream: Downstream = ['status', status];\n if (this.#backlog) {\n this.#pushBacklog({status: BigIntJSON.stringify(downstream)});\n return;\n }\n void this.#sendDownstream(downstream);\n }\n\n /**\n * Sends a control status (e.g. `draining` or `restore`) downstream\n * immediately, regardless of whether the subscriber is initialized or\n * catching up. Unlike {@link sendStatus}, such statuses are not tied to a\n * position in the replication stream.\n */\n sendControlStatus(status: Status) {\n void this.#sendDownstream(['status', status]);\n }\n\n /** catchup() is called on ChangeEntries loaded from the store. */\n async catchup(change: WatermarkedChange) {\n this.#initialize();\n await this.#sendChange(change);\n }\n\n /**\n * Marks the Subscribe as \"caught up\" and flushes any backlog of\n * entries that were received during the catchup.\n */\n setCaughtUp(): Promise<void> {\n this.#initialize();\n if (!this.#backlog) {\n return this.#backlogDrain ?? promiseVoid;\n }\n if (!this.#backlogDrain) {\n // Keep #backlog non-null while queued entries are being handed to\n // downstream. That preserves ordering for sends that race with\n // setCaughtUp(): they append to the same backlog instead of bypassing\n // older buffered changes.\n this.#backlogDrain = this.#drainBacklog();\n void this.#backlogDrain.catch(e => this.fail(e));\n }\n return this.#backlogDrain;\n }\n\n async #sendChange(change: WatermarkedChange) {\n const [watermark, tag, json] = change;\n if (watermark <= this.watermark) {\n return;\n }\n if (!this.supportsMessage(tag)) {\n return;\n }\n if (tag === 'commit') {\n this.#watermark = watermark;\n }\n this.#trackBackfills(change);\n const result = await this.#sendStringifiedDownstream(json);\n if (tag === 'commit' && result === 'consumed') {\n this.#advanceAck(watermark);\n }\n }\n\n #advanceAck(watermark: string) {\n // Sends can complete out of order (e.g. the bounded window in\n // #drainBacklog), so the ack only advances monotonically, and listeners\n // are only notified when it does.\n const acked = max(this.#acked, watermark);\n if (acked !== this.#acked) {\n this.#acked = acked;\n this.#onAck?.(acked);\n this.#checkCaughtUp();\n }\n }\n\n #checkCaughtUp() {\n if (\n this.#caughtUpWatermark !== undefined &&\n this.#acked >= this.#caughtUpWatermark\n ) {\n this.#caughtUpWatermark = undefined;\n this.#caughtUp.resolve();\n }\n }\n\n #sendDownstream(downstream: Downstream) {\n return this.#sendStringifiedDownstream(BigIntJSON.stringify(downstream));\n }\n\n async #sendStringifiedDownstream(json: string) {\n const size = json.length;\n this.#pending++;\n this.#pendingBytes += size;\n const {result} = this.#downstream.push(json);\n try {\n return await result;\n } finally {\n this.#pending--;\n this.#pendingBytes -= size;\n this.#processed++;\n }\n }\n\n async #sendPreSerializedDownstream(\n batch: PreSerializedBatch,\n commitWatermark?: string | undefined,\n ): Promise<void> {\n const size = batch.byteLength;\n this.#pending += batch.changes.length;\n this.#pendingBytes += size;\n const {result} = this.#downstream.push(batch);\n try {\n const outcome = await result;\n if (commitWatermark && outcome === 'consumed') {\n this.#advanceAck(commitWatermark);\n }\n } finally {\n this.#pending -= batch.changes.length;\n this.#pendingBytes -= size;\n this.#processed += batch.changes.length;\n }\n }\n\n // `pending` and `processed` stats are tracked by periodically sampling\n // the running totals (by the progress tracker in the Forwarder).\n // This information was originally collected for use in flow control\n // decisions. The final flow control algorithm ended up being simpler\n // than expected and does not actually use this information. However, the\n // stats are still tracked and logged during flow control decisions for\n // debugging, forensics, and potential improvements to the algorithm.\n\n #pending = 0;\n #pendingBytes = 0;\n #processed = 0;\n #samples: {processed: number; timestamp: number}[] = [\n {processed: 0, timestamp: performance.now()},\n ];\n\n /**\n * The number of downstream messages that have yet to be acked.\n */\n get numPending() {\n return this.#pending + this.#backlogCount;\n }\n\n /**\n * The total number of downstream messages that the subscriber has\n * processed (i.e. acked).\n */\n get numProcessed() {\n return this.#processed;\n }\n\n /**\n * Records a new history entry for the number of messages processed,\n * keeping the number of samples bounded to `maxSamples`.\n */\n sampleProcessRate(now: number, maxSamples = 10): this {\n while (this.#samples.length >= maxSamples) {\n this.#samples.shift();\n }\n this.#samples.push({processed: this.#processed, timestamp: now});\n return this;\n }\n\n getStats(): SubscriberStats {\n const pending = this.numPending;\n if (this.#samples.length < 2) {\n return {\n processRate: 0,\n pending,\n backlog: this.#backlogCount,\n backlogBytes: this.#bufferedBacklogBytes,\n totalBufferedBytes: this.#totalBufferedBytes,\n missedLastTimeout: this.#missedLastTimeout,\n };\n }\n const from = this.#samples[0];\n const to = must(this.#samples.at(-1));\n const processed = to.processed - from.processed;\n const seconds = (to.timestamp - from.timestamp) / 1000;\n const processRate = seconds === 0 ? 0 : processed / seconds;\n return {\n processRate,\n pending,\n backlog: this.#backlogCount,\n backlogBytes: this.#bufferedBacklogBytes,\n totalBufferedBytes: this.#totalBufferedBytes,\n missedLastTimeout: this.#missedLastTimeout,\n };\n }\n\n #missedLastTimeout = false;\n #laggingSinceMs: number | undefined;\n\n trackResponseResult(result: 'on-time' | 'timed-out') {\n this.#missedLastTimeout = result === 'timed-out';\n if (result === 'on-time') {\n this.#laggingSinceMs = undefined;\n }\n }\n\n /**\n * Reports the change rate of a slow subscriber (i.e. that missed the last\n * timeout) compared to the change rate of the (slowest) subscriber that\n * responded on time.\n *\n * * `lagging` indicates that this subscriber is slower\n * * `catching-up` indicates that it is faster\n *\n * Returns the total duration in which subscriber has been continuously\n * reported as `lagging`.\n */\n reportChangeRate(now: number, status: 'lagging' | 'catching-up') {\n assert(\n this.#missedLastTimeout,\n `reportChangeRate should only be called for slow subscribers`,\n );\n if (status === 'catching-up') {\n this.#laggingSinceMs = undefined;\n return 0;\n }\n this.#laggingSinceMs ??= now;\n return now - this.#laggingSinceMs;\n }\n\n supportsMessage(tag: ChangeTag) {\n switch (tag) {\n case 'update-table-metadata':\n // update-table-row-key is only understood by subscribers >= protocol v5\n return this.#protocolVersion >= 5;\n }\n return true;\n }\n\n /**\n * Ends the subscription without sending a downstream `['error', ...]`.\n *\n * This is deliberate, and not the same as reporting the failure to the\n * subscriber: `IncrementalSyncer` treats any `['error', ...]` as terminal and\n * shuts down to restore a fresh replica from litestream, whereas a clean end\n * backs off and re-subscribes. The failures routed here -- storer catchup\n * errors, backlog drain errors, backup-monitor errors -- are transient, so a\n * reconnect is the proportionate response and a fleet-wide restore is not.\n *\n * Callers are responsible for logging `err`; it is not carried downstream.\n */\n fail(_err?: unknown) {\n this.close();\n }\n\n close(\n error?: ErrorType,\n message?: string,\n details?: {replicaVersion?: string | undefined},\n ) {\n // Closing the subscriber must also release producers that are blocked on\n // backlog capacity; there is no future drain that could wake them.\n this.#backlog = null;\n this.#backlogBytes = 0;\n this.#backlogBackpressure.releaseAll();\n // The backlog can no longer grow, so nothing would ever resolve these.\n // Waiters re-check backlogFull, which is now false, and see the close.\n this.#resolveBacklogFullWaiters();\n\n if (error !== undefined) {\n // Wait for the ACK of the error message before closing the connection.\n void this.#sendDownstream([\n 'error',\n {type: error, message, ...details},\n ]).finally(() => this.#downstream.cancel());\n } else {\n this.#downstream.cancel();\n }\n }\n\n get #backlogCount() {\n return this.#backlog?.size ?? 0;\n }\n\n get #bufferedBacklogBytes() {\n // Include entries already handed to downstream but not yet consumed. Without\n // this, setCaughtUp() could move bytes out of #backlog faster than the\n // downstream Subscription can process them and release producers too early.\n return this.#backlogBytes + this.#backlogInFlightBytes;\n }\n\n get #totalBufferedBytes() {\n return this.#bufferedBacklogBytes + this.#pendingBytes;\n }\n\n #pushBacklog(entry: BacklogEntry) {\n assert(this.#backlog, 'cannot push to backlog after catchup completed');\n this.#backlog.push(entry);\n this.#backlogBytes += entryBytes(entry);\n if (this.backlogFull) {\n this.#resolveBacklogFullWaiters();\n }\n }\n\n #resolveBacklogFullWaiters() {\n const waiters = [...this.#backlogFullWaiters];\n this.#backlogFullWaiters.clear();\n for (const waiter of waiters) {\n waiter.resolve();\n }\n }\n\n #maybeWaitForBacklogSpace(): Promise<void> {\n return this.#backlogBackpressure.waitForSpace(this.#bufferedBacklogBytes);\n }\n\n async #drainBacklog() {\n const inFlight: {promise: Promise<void>; bytes: number}[] = [];\n let inFlightBytes = 0;\n\n try {\n for (;;) {\n const entry = this.#backlog?.shift();\n if (!entry) {\n const closed = this.#backlog === null;\n this.#backlog = null;\n this.#backlogBytes = 0;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n if (!closed && !this.#aligned) {\n // All changes up to the head have been handed to #sendChange()\n // (and thus tracked), and subsequent changes are sent directly.\n this.#aligned = true;\n this.#onAligned?.(this);\n this.#caughtUpWatermark = this.#watermark;\n this.#checkCaughtUp();\n void this.#lagReports?.then(lagReports => {\n if (lagReports) {\n lagReports.request();\n } else {\n this.sendStatus(LAG_REPORTING_DISABLED);\n }\n });\n }\n break;\n }\n\n const bytes = entryBytes(entry);\n this.#backlogBytes -= bytes;\n this.#backlogInFlightBytes += bytes;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n\n // Send backlog entries in order, but keep only a bounded byte window in\n // flight. This avoids replacing one unbounded buffer with another inside\n // the downstream Subscription during catchup completion.\n const send = isQueuedStatus(entry)\n ? this.#sendStringifiedDownstream(entry.status).then(() => {})\n : this.#sendChange(entry);\n const promise = send.finally(() => {\n this.#backlogInFlightBytes -= bytes;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n });\n inFlight.push({promise, bytes});\n inFlightBytes += bytes;\n\n while (inFlightBytes >= this.#backlogBackpressure.highWaterBytes) {\n const next = must(inFlight.shift());\n await next.promise;\n inFlightBytes -= next.bytes;\n }\n }\n\n for (const {promise} of inFlight) {\n await promise;\n }\n } finally {\n this.#backlogDrain = null;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n }\n }\n}\n\nclass ByteBackpressureGate {\n readonly highWaterBytes: number;\n readonly #lowWaterBytes: number;\n readonly #waiters: Resolver<void>[] = [];\n\n constructor(highWaterBytes: number, lowWaterRatio: number) {\n this.highWaterBytes = Math.max(1, highWaterBytes);\n this.#lowWaterBytes =\n this.highWaterBytes * Math.min(1, Math.max(0, lowWaterRatio));\n }\n\n waitForSpace(bufferedBytes: number): Promise<void> {\n if (bufferedBytes < this.highWaterBytes) {\n return promiseVoid;\n }\n\n // One waiter represents one send() call that has already appended its\n // change. The producer is released when the backlog falls back below the low\n // water mark or the subscriber closes.\n const r = resolver<void>();\n this.#waiters.push(r);\n return r.promise;\n }\n\n releaseIfUnderLowWater(bufferedBytes: number) {\n if (this.#waiters.length === 0 || bufferedBytes > this.#lowWaterBytes) {\n return;\n }\n\n // Use a low water mark so waiting producers are released in batches instead\n // of waking one at a time around the high water boundary.\n this.releaseAll();\n }\n\n releaseAll() {\n const waiters = this.#waiters.splice(0);\n for (const waiter of waiters) {\n waiter.resolve();\n }\n }\n}\n\n/** Requests lag reports from upstream (see {@link SubscriberOptions.lagReports}). */\nexport type LagReports = {\n /**\n * Requests that a lag report be sent now rather than at the next report\n * interval. Requests are coalesced upstream, e.g. with a report in flight.\n */\n request(): void;\n};\n\n/**\n * Sent to an aligned subscriber when lag reporting is disabled upstream,\n * signaling that the subscriber is caught up. This is a lag report with a\n * `nextSendTimeMs` of 0 and no `lastTimings`, so that no lag is measured\n * (see `isLagReportingDisabledSignal()` in the replicator's recorder).\n */\nexport const LAG_REPORTING_DISABLED: Status = {\n tag: 'status',\n lagReport: {nextSendTimeMs: 0},\n};\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { resolver } from "../../../shared/src/resolver.js";
1
2
  import { AbortError } from "../../../shared/src/abort-error.js";
2
3
  import { singleProcessMode } from "../types/processes.js";
3
4
  import { ConfigurationError } from "../types/configuration-error.js";
@@ -5,7 +6,6 @@ import { logLastChanceSQLiteCorruptionDiagnostics } from "../db/sqlite-corruptio
5
6
  import { LONG_DURATION_HISTOGRAM_BOUNDARIES_S, getOrCreateHistogram } from "../observability/metrics.js";
6
7
  import { orTimeout } from "../types/timeout.js";
7
8
  import { RunningState } from "./running-state.js";
8
- import { resolver } from "@rocicorp/resolver";
9
9
  import { pid } from "node:process";
10
10
  //#region ../zero-cache/src/services/life-cycle.ts
11
11
  var GRACEFUL_SHUTDOWN = ["SIGTERM", "SIGINT"];
@@ -1 +1 @@
1
- {"version":3,"file":"life-cycle.js","names":[],"sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"sourcesContent":["import type {IncomingHttpHeaders} from 'node:http';\nimport {pid} from 'node:process';\nimport type {EventEmitter} from 'stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {AbortError} from '../../../shared/src/abort-error.ts';\nimport {logLastChanceSQLiteCorruptionDiagnostics} from '../db/sqlite-corruption.ts';\nimport {\n getOrCreateHistogram,\n LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n} from '../observability/metrics.ts';\nimport {ConfigurationError} from '../types/configuration-error.ts';\nimport {\n singleProcessMode,\n type Subprocess,\n type Worker,\n} from '../types/processes.ts';\nimport {orTimeout} from '../types/timeout.ts';\nimport {RunningState} from './running-state.ts';\nimport type {SingletonService} from './service.ts';\n\n/**\n * * `user-facing` workers serve external requests and are the first to\n * receive a `SIGTERM` or `SIGINT` signal for graceful shutdown.\n *\n * * `supporting` workers support `user-facing` workers and are sent\n * the `SIGTERM` signal only after all `user-facing` workers have\n * exited.\n *\n * For other kill signals, such as `SIGQUIT` and `SIGABRT`, all workers\n * are stopped without draining. `SIGQUIT` is used to represent an\n * intentional shutdown (for which draining is not beneficial), whereas\n * `SIGABRT` is used for unexpected process exits.\n *\n * A clean exit (i.e. code 0) of a worker of either type that is not the\n * result of a drain is treated as an intentional shutdown of the server.\n */\nexport type WorkerType = 'user-facing' | 'supporting';\n\nexport const GRACEFUL_SHUTDOWN = ['SIGTERM', 'SIGINT'] as const;\nexport const FORCEFUL_SHUTDOWN = ['SIGQUIT', 'SIGABRT'] as const;\n\ntype GracefulShutdownSignal = (typeof GRACEFUL_SHUTDOWN)[number];\n\nfunction workerStartupMetricName(name: string) {\n if (name === 'zero-cache') {\n return undefined;\n }\n if (name.startsWith('change-streamer')) {\n return 'change_streamer';\n }\n if (name.startsWith('replicator')) {\n if (name.includes('(backup)')) {\n return 'backup_replicator';\n }\n if (name.includes('(serving-copy)')) {\n return 'serving_copy_replicator';\n }\n if (name.includes('(serving)')) {\n return 'serving_replicator';\n }\n return 'other';\n }\n if (name.startsWith('syncer')) {\n return 'syncer';\n }\n if (name.startsWith('reaper')) {\n return 'reaper';\n }\n if (name.startsWith('shadow-syncer')) {\n return 'shadow_syncer';\n }\n if (name.startsWith('mutator')) {\n return 'mutator';\n }\n return 'other';\n}\n\nfunction startupDuration() {\n return getOrCreateHistogram('server', 'startup_duration', {\n description: 'Duration from starting zero-cache to its ready signal.',\n unit: 's',\n bucketBoundaries: LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\nexport function recordStartupDurationMs(durationMs: number) {\n startupDuration().recordMs(durationMs, {component: 'dispatcher'});\n}\n\nfunction workerStartupDuration() {\n return getOrCreateHistogram('server', 'worker_startup_duration', {\n description: 'Duration from starting a worker to its ready signal.',\n unit: 's',\n bucketBoundaries: LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\n// An internal error code used to indicate that a message has already been\n// logged at level ERRROR. When a process exits with this error code, the\n// parent process logs the exit at level WARN instead of ERROR.\nexport const UNHANDLED_EXCEPTION_ERROR_CODE = 13;\n\n// An internal error code used to indicate that the server should exit\n// without draining (e.g. due to a worker exiting cleanly outside of a drain,\n// such as for an auto reset), but the exit is otherwise intentional.\nexport const INTENTIONAL_SHUTDOWN_ERROR_CODE = 14;\n\n/**\n * Handles readiness, termination signals, and coordination of graceful\n * shutdown.\n */\nexport class ProcessManager {\n readonly #lc: LogContext;\n readonly #userFacing = new Set<Subprocess>();\n readonly #all = new Set<Subprocess>();\n readonly #exitImpl: (code: number) => never;\n readonly #start = Date.now();\n readonly #ready: Promise<void>[] = [];\n\n #runningState = new RunningState('process-manager');\n #drainStart = 0;\n\n constructor(lc: LogContext, proc: EventEmitter) {\n this.#lc = lc.withContext('component', 'process-manager');\n\n // Propagate `SIGTERM` and `SIGINT` to all user-facing workers,\n // initiating a graceful shutdown. The parent process will\n // exit once all user-facing workers have exited ...\n for (const signal of GRACEFUL_SHUTDOWN) {\n proc.on(signal, () => this.#startDrain(signal));\n }\n\n // ... which will result in sending `SIGTERM` to the remaining workers.\n proc.on('exit', code =>\n this.#kill(\n this.#all,\n code === 0\n ? 'SIGTERM' // graceful, drained shutdown\n : code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'SIGQUIT' // intentional abort without drain\n : 'SIGABRT', // unintentional shutdown, alertable error\n ),\n );\n\n // For other (catchable) kill signals, exit with a non-zero error code\n // to send a `SIGQUIT` (intentional shutdown) or `SIGABRT` (unexpected\n // shutdown) to all workers. For these signals, workers are stopped\n // immediately without draining, since there is no merit to slowly draining\n // when supporting workers have stopped.\n //\n // The logic for handling these signals is in `runUntilKilled()`.\n for (const signal of FORCEFUL_SHUTDOWN) {\n proc.on(signal, () =>\n this.#exit(signal === 'SIGQUIT' ? INTENTIONAL_SHUTDOWN_ERROR_CODE : -1),\n );\n }\n\n this.#exitImpl = (code: number) => {\n if (singleProcessMode()) {\n return proc.emit('exit', code) as never; // For unit / integration tests.\n }\n process.exit(code);\n };\n }\n\n async shutdown() {\n if (this.#drainStart === 0) {\n this.#startDrain(GRACEFUL_SHUTDOWN[0]);\n }\n await this.#runningState.stopped();\n }\n\n #exit(code: number) {\n this.#lc.info?.('exiting with code', code);\n this.#runningState.stop(this.#lc);\n void this.#lc.flush().finally(() => this.#exitImpl(code));\n }\n\n #startDrain(signal: GracefulShutdownSignal) {\n if (this.#all.size === 0) {\n // Shutdown if a signal is received before any subprocesses are added.\n this.#lc.info?.(`exiting on ${signal}`);\n this.#exit(0);\n }\n this.#lc.info?.(`initiating drain (${signal})`);\n this.#drainStart = Date.now();\n if (this.#userFacing.size) {\n this.#kill(this.#userFacing, signal);\n } else {\n this.#kill(this.#all, signal);\n }\n }\n\n addSubprocess(proc: Subprocess, type: WorkerType, name: string) {\n if (type === 'user-facing') {\n this.#userFacing.add(proc);\n }\n this.#all.add(proc);\n\n let isOpen = true;\n proc.on('close', (code, signal) => {\n isOpen = false;\n this.#onExit(code, signal, null, type, name, proc);\n });\n\n // As per https://nodejs.org/api/child_process.html#event-error\n // 'error' events can happen when sending a message to a child process\n // fails. This is not really an error when the server is shutting down,\n // so log any post-close errors at 'warn'.\n proc.on('error', err =>\n this.#lc[!isOpen || this.#drainStart > 0 ? 'warn' : 'error']?.(\n `error from ${name} ${proc.pid}`,\n err,\n ),\n );\n }\n\n readonly #initializing = new Map<number, string>();\n #nextID = 0;\n\n addWorker(worker: Worker, type: WorkerType, name: string): Worker {\n this.addSubprocess(worker, type, name);\n\n const start = performance.now();\n const id = ++this.#nextID;\n this.#initializing.set(id, name);\n const {promise, resolve} = resolver();\n this.#ready.push(promise);\n\n worker.onceMessageType('ready', () => {\n const elapsed = performance.now() - start;\n const workerMetricName = workerStartupMetricName(name);\n if (workerMetricName) {\n workerStartupDuration().recordMs(elapsed, {\n worker: workerMetricName,\n type,\n });\n }\n this.#lc.debug?.(`${name} ready (${Date.now() - this.#start} ms)`);\n this.#initializing.delete(id);\n resolve();\n });\n\n return worker;\n }\n\n initializing(): string[] {\n return [...this.#initializing.values()];\n }\n\n async allWorkersReady() {\n await Promise.all(this.#ready);\n }\n\n logErrorAndExit(err: unknown, name: string) {\n // only accessible by the main (i.e. user-facing) process.\n this.#onExit(-1, null, err, 'user-facing', name, undefined);\n }\n\n #onExit(\n code: number,\n sig: NodeJS.Signals | null,\n err: unknown,\n type: WorkerType,\n name: string,\n worker: Subprocess | undefined,\n ) {\n // Remove the worker from maps to avoid attempting to send more signals to it.\n if (worker) {\n this.#userFacing.delete(worker);\n this.#all.delete(worker);\n }\n\n const pid = worker?.pid ?? process.pid;\n\n // A clean exit outside of a drain is an intentional shutdown, e.g. the\n // replication-manager shutting down for protocol-specific instructions\n // (like auto reset), or a configuration error. In this case, a special\n // error code is used to signal that the server should be shut down\n // without draining, but it is otherwise not considered an\n // unexpected/alertable error. The same applies to a supporting worker\n // that exits while user-facing workers are still draining.\n if (\n code === 0 &&\n (this.#drainStart === 0 ||\n (type === 'supporting' && this.#userFacing.size > 0))\n ) {\n code = INTENTIONAL_SHUTDOWN_ERROR_CODE;\n }\n\n if (type === 'supporting') {\n const log =\n code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'info'\n : 'warn';\n this.#lc[log]?.(`${name} (${pid}) exited with code (${code})`, err ?? '');\n return this.#exit(code);\n }\n\n const log =\n code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'info'\n : this.#drainStart > 0 || code === UNHANDLED_EXCEPTION_ERROR_CODE\n ? 'warn'\n : 'error';\n this.#lc[log]?.(\n sig\n ? `${name} (${pid}) killed with (${sig})`\n : `${name} (${pid}) exited with code (${code})`,\n err ?? '',\n );\n\n // user-facing workers exited or finished draining.\n if (this.#userFacing.size === 0) {\n this.#lc.info?.(\n this.#drainStart\n ? `all user-facing workers drained (${\n Date.now() - this.#drainStart\n } ms)`\n : `all user-facing workers exited`,\n );\n return this.#exit(this.#drainStart ? 0 : code);\n }\n\n if (this.#drainStart === 0) {\n // If a user-facing worker exits without receiving a drain signal,\n // shutdown the server.\n return this.#exit(code);\n }\n\n return undefined;\n }\n\n #kill(workers: Iterable<Subprocess>, signal: NodeJS.Signals) {\n for (const worker of workers) {\n try {\n worker.kill(signal);\n } catch (e) {\n this.#lc.error?.(e);\n }\n }\n }\n}\n\n/**\n * Runs the supporting subprocesses of a worker (e.g. the litestream process\n * of the backup-replicator) as a {@link SingletonService}.\n *\n * Unlike a {@link ProcessManager}, it does not forward `SIGTERM` or `SIGINT`\n * to its subprocesses. Instead, {@link runUntilKilled} stops it along with the\n * worker's other services, i.e. after they have drained, so that the\n * subprocesses keep supporting them until then. Forceful signals (`SIGQUIT`\n * and `SIGABRT`), on the other hand, are forwarded immediately once the\n * service is running, and the subprocesses are killed if the worker exits\n * before they are stopped.\n *\n * `run()` settles when a subprocess exits before {@link stop()} is called,\n * which stops the worker: a clean exit resolves (and the parent in turn treats\n * the worker's clean exit outside of a drain as an intentional shutdown),\n * while any other exit rejects.\n */\nexport class SupportingProcesses implements SingletonService {\n readonly id = 'supporting-processes';\n readonly #lc: LogContext;\n readonly #parent: EventEmitter;\n readonly #procs = new Map<Subprocess, string>();\n readonly #done = resolver<void>();\n #signaled: NodeJS.Signals | undefined;\n\n constructor(lc: LogContext, parent: EventEmitter) {\n this.#lc = lc.withContext('component', 'supporting-processes');\n this.#parent = parent;\n // run() may never be called if the worker fails before running services.\n this.#done.promise.catch(() => {});\n\n parent.on('exit', code =>\n this.#kill(\n code === 0\n ? 'SIGTERM'\n : code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'SIGQUIT'\n : 'SIGABRT',\n ),\n );\n }\n\n add<P extends Subprocess>(proc: P, name: string): P {\n this.#procs.set(proc, name);\n proc.on('close', (code: number | null, sig: NodeJS.Signals | null) =>\n this.#onClose(proc, name, code, sig),\n );\n // As with the ProcessManager, 'error' events after a signal (e.g. failing\n // to send a message to an exiting process) are logged at 'warn'.\n proc.on('error', err =>\n this.#lc[this.#signaled ? 'warn' : 'error']?.(\n `error from ${name} ${proc.pid}`,\n err,\n ),\n );\n return proc;\n }\n\n run(): Promise<void> {\n // Signal listeners are only installed when running, i.e. alongside those\n // of runUntilKilled(). Installing them any earlier would consume signals\n // received during worker initialization, which would otherwise terminate\n // the worker (or be handled by the initialization logic).\n for (const signal of FORCEFUL_SHUTDOWN) {\n this.#parent.on(signal, () => this.#kill(signal));\n }\n return this.#done.promise;\n }\n\n async stop(): Promise<void> {\n if (!this.#signaled) {\n this.#kill('SIGTERM');\n }\n if (this.#procs.size === 0) {\n this.#done.resolve();\n }\n await this.#done.promise.catch(() => {});\n }\n\n #onClose(\n proc: Subprocess,\n name: string,\n code: number | null,\n sig: NodeJS.Signals | null,\n ) {\n this.#procs.delete(proc);\n const exit = sig\n ? `${name} (${proc.pid}) killed with (${sig})`\n : `${name} (${proc.pid}) exited with code (${code})`;\n\n if (this.#signaled) {\n this.#lc.info?.(exit);\n if (this.#procs.size === 0) {\n this.#done.resolve();\n }\n } else if (code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE) {\n this.#lc.info?.(exit);\n this.#done.resolve();\n } else {\n this.#done.reject(new Error(exit));\n }\n }\n\n #kill(signal: NodeJS.Signals) {\n this.#signaled = signal;\n for (const proc of this.#procs.keys()) {\n try {\n proc.kill(signal);\n } catch (e) {\n this.#lc.error?.(e);\n }\n }\n }\n}\n\nexport type SelfDrainOptions = {\n /**\n * The time after a self drain is requested at which the server sends itself\n * a `SIGTERM`, unless one was received sooner (e.g. from the container\n * runner).\n */\n delayMs: number;\n\n /**\n * Called when a self drain is requested. This should fail health checks\n * (both readiness and liveness) so that the container runner stops routing\n * to the server and initiates its normal shutdown process.\n */\n onRequested: () => void;\n};\n\n/**\n * Coordinates a drain that is initiated by the server itself (e.g. by a\n * view-syncer that has been granted permission to restore a new replica),\n * as opposed to one initiated by the container runner with a `SIGTERM`.\n *\n * Health checks start failing immediately, upon which an orchestrator such\n * as Kubernetes stops routing to the server and shuts it down as usual\n * (i.e. running any pre-stop hooks before sending `SIGTERM`). As a backstop,\n * and for environments without liveness checks, the server sends itself a\n * `SIGTERM` after the configured delay.\n */\nexport class SelfDrain {\n readonly #lc: LogContext;\n readonly #proc: EventEmitter;\n readonly #delayMs: number;\n readonly #onRequested: () => void;\n #requested = false;\n #signaled = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n\n constructor(lc: LogContext, proc: EventEmitter, opts: SelfDrainOptions) {\n this.#lc = lc.withContext('component', 'self-drain');\n this.#proc = proc;\n this.#delayMs = opts.delayMs;\n this.#onRequested = opts.onRequested;\n\n for (const signal of GRACEFUL_SHUTDOWN) {\n proc.once(signal, () => {\n this.#signaled = true;\n clearTimeout(this.#timer);\n });\n }\n }\n\n request() {\n if (this.#requested || this.#signaled) {\n return; // Already draining.\n }\n this.#requested = true;\n this.#lc.info?.(\n `self drain requested. sending SIGTERM in ${this.#delayMs} ms`,\n );\n this.#onRequested();\n this.#timer = setTimeout(() => {\n if (!this.#signaled) {\n this.#lc.info?.(`initiating self drain`);\n this.#proc.emit('SIGTERM', 'SIGTERM');\n }\n }, this.#delayMs);\n }\n}\n\n/**\n * The maximum time that {@link runUntilKilled} waits for its services to stop\n * (i.e. for their `run()` to settle) after stopping them.\n */\nexport const STOP_TIMEOUT_MS = 60_000;\n\n/**\n * Runs the specified services, stopping them on `SIGTERM` or `SIGINT` with\n * an optional {@link SingletonService.drain drain()}, or stopping them\n * without draining for `SIGQUIT`, or when the parent process disconnects.\n * Repeated graceful signals are ignored. If a service stops (or throws) on its\n * own, the remaining services are stopped without draining.\n *\n * @returns a Promise that settles once all services have stopped (or\n * {@link STOP_TIMEOUT_MS} has elapsed), resolving or rejecting\n * with the outcome of the first service to stop.\n */\nexport async function runUntilKilled(\n lc: LogContext,\n parent: EventEmitter,\n ...services: SingletonService[]\n): Promise<void> {\n if (services.length === 0) {\n return;\n }\n const stopped = new Set<SingletonService>();\n const runs = services.map(svc => svc.run().finally(() => stopped.add(svc)));\n\n const allStopped = resolver<void>();\n let stopping: Promise<void> | undefined;\n const stopAll = (reason: string) =>\n (stopping ??= stopServices(lc, services, runs, stopped, reason).then(\n allStopped.resolve,\n ));\n\n const GRACEFUL_SIGNALS = GRACEFUL_SHUTDOWN as readonly NodeJS.Signals[];\n let draining = false;\n const onSignal = async (signal: NodeJS.Signals) => {\n if (GRACEFUL_SIGNALS.includes(signal)) {\n if (draining || stopping) {\n // e.g. A SIGINT from both the terminal and a wrapping process.\n lc.info?.(`already shutting down. ignoring ${signal}`);\n return;\n }\n draining = true;\n // Drain all services before stopping any of them. Stopping a service\n // resolves its run() and thus ends runUntilKilled(), so a service\n // without a drain() must not be stopped while others are draining.\n const results = await Promise.allSettled(\n services.map(async svc => {\n if (svc.drain) {\n lc.info?.(`draining ${svc.constructor.name} ${svc.id} (${signal})`);\n await svc.drain();\n }\n }),\n );\n for (const result of results) {\n if (result.status === 'rejected') {\n lc.warn?.(`error while draining (${signal})`, result.reason);\n }\n }\n }\n // A forceful signal stops the services without waiting for (or after)\n // a drain.\n await stopAll(signal);\n };\n // The listeners remain installed for the lifetime of the process, as a\n // signal without a listener would terminate the process (e.g. a repeated\n // SIGINT during a drain).\n for (const signal of [...GRACEFUL_SHUTDOWN, ...FORCEFUL_SHUTDOWN]) {\n parent.on(signal, () => void onSignal(signal));\n }\n // A forked worker whose parent has exited (e.g. was killed by a signal,\n // in which case it cannot signal its workers) stops without draining, so\n // that it (and in turn its own subprocesses) is not orphaned.\n parent.once('disconnect', () => {\n lc.warn?.(`parent process disconnected. stopping without draining`);\n void stopAll('disconnect');\n });\n\n try {\n // Run all services and resolve when any of them stops, or when stopping\n // them (after a signal) has timed out.\n const svc = await Promise.race([\n ...runs.map((run, i) => run.then(() => services[i])),\n allStopped.promise,\n ]);\n if (svc) {\n lc.info?.(`${svc.constructor.name} (${svc.id}) stopped`);\n }\n } catch (e) {\n lc.error?.(`exiting on error`, e);\n throw e;\n } finally {\n await stopAll('stopped');\n }\n}\n\nasync function stopServices(\n lc: LogContext,\n services: SingletonService[],\n runs: Promise<void>[],\n stopped: Set<SingletonService>,\n reason: string,\n) {\n for (const svc of services) {\n if (!stopped.has(svc)) {\n lc.info?.(`stopping ${svc.constructor.name} ${svc.id} (${reason})`);\n void svc\n .stop()\n .catch(e =>\n lc.warn?.(`error stopping ${svc.constructor.name} ${svc.id}`, e),\n );\n }\n }\n if (\n (await orTimeout(Promise.allSettled(runs), STOP_TIMEOUT_MS)) === 'timed-out'\n ) {\n const running = services.filter(svc => !stopped.has(svc));\n lc.warn?.(\n `services did not stop within ${STOP_TIMEOUT_MS} ms: ` +\n running.map(svc => `${svc.constructor.name} ${svc.id}`).join(', '),\n );\n }\n}\n\nexport async function exitAfter(\n logContext: LogContext | (() => LogContext),\n run: () => Promise<void>,\n) {\n const getLogContext =\n typeof logContext === 'function' ? logContext : () => logContext;\n try {\n await run();\n const lc = getLogContext();\n lc.info?.(`pid ${pid} exiting normally`);\n process.exit(0);\n } catch (e) {\n const lc = getLogContext();\n if (e instanceof ConfigurationError) {\n lc.error?.(`exiting with configuration error: ${String(e)}`, e);\n process.exit(0);\n }\n if (e instanceof AbortError) {\n // AbortErrors signal an intentional shutdown or restart (e.g. SIGTERM\n // during a rollout, AutoResetSignal) and should not raise alerts. The\n // non-zero exit code is kept so that restart semantics are unchanged.\n lc.warn?.(`exiting after abort: ${String(e)}`, e);\n } else {\n lc.error?.(`exiting with error: ${String(e)}`, e);\n }\n try {\n logLastChanceSQLiteCorruptionDiagnostics(lc, e);\n } catch (diagnosticError) {\n lc.error?.('SQLite corruption last-chance diagnostic failed', {\n error: diagnosticError,\n });\n }\n try {\n await lc.flush();\n } finally {\n process.exit(-1);\n }\n }\n}\n\n/**\n * The HeartbeatMonitor monitors the cadence heartbeats (e.g. \"/keepalive\"\n * health checks made to HttpServices) that signal that the server\n * should continue processing requests. When a configurable `stopInterval`\n * elapses without receiving these heartbeats, the monitor initiates a\n * graceful shutdown of the server. This works with common load balancing\n * frameworks such as AWS Elastic Load Balancing.\n *\n * The HeartbeatMonitor is **opt-in** in that it only kicks in after it\n * starts receiving keepalives.\n */\nexport class HeartbeatMonitor {\n readonly #stopInterval: number;\n\n #lc: LogContext;\n #checkIntervalTimer: NodeJS.Timeout | undefined;\n #checkImmediateTimer: NodeJS.Immediate | undefined;\n #lastHeartbeat = 0;\n\n constructor(lc: LogContext, stopInterval: number) {\n this.#lc = lc;\n this.#stopInterval = stopInterval;\n }\n\n onHeartbeat(reqHeaders: IncomingHttpHeaders) {\n this.#lastHeartbeat = Date.now();\n if (this.#checkIntervalTimer === undefined) {\n this.#lc.info?.(\n `starting heartbeat monitor at ${\n this.#stopInterval / 1000\n } second interval`,\n reqHeaders,\n );\n // e.g. check every 5 seconds to see if it's been over 20 seconds\n // since the last heartbeat.\n this.#checkIntervalTimer = setInterval(\n this.#checkStopInterval,\n this.#stopInterval / 4,\n );\n }\n }\n\n #checkStopInterval = () => {\n // In the Node.js event loop, timers like setInterval and setTimeout\n // run *before* I/O events coming from network sockets or file reads/writes.\n // When this process gets starved of CPU resources for long periods of time,\n // for example when other processes are monopolizing all available cores,\n // pathological behavior can emerge:\n // - keepalive network request comes in, but is queued in Node internals waiting\n // for time on the event loop\n // - CPU is starved/monopolized by other processes for longer than the time\n // configured via this.#stopInterval\n // - When CPU becomes available and the event loop wakes up, this stop interval\n // check is run *before* the keepalive request is processed. The value of\n // this.#lastHeartbeat is now very stale, and erroneously triggers a shutdown\n // even though keepalive requests were about to be processed and update\n // this.#lastHeartbeat. Downtime ensues.\n //\n // To avoid this, we push the check out to a phase of the event loop *after*\n // I/O events are processed, using setImmediate():\n // https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout\n //\n // This ensures we see a value for this.#lastHeartbeat that reflects\n // any keepalive requests that came in during the current event loop turn.\n this.#checkImmediateTimer = setImmediate(() => {\n this.#checkImmediateTimer = undefined;\n const timeSinceLastHeartbeat = Date.now() - this.#lastHeartbeat;\n if (timeSinceLastHeartbeat >= this.#stopInterval) {\n this.#lc.info?.(\n `last heartbeat received ${\n timeSinceLastHeartbeat / 1000\n } seconds ago. draining.`,\n );\n process.kill(process.pid, GRACEFUL_SHUTDOWN[0]);\n }\n });\n };\n\n stop() {\n clearTimeout(this.#checkIntervalTimer);\n if (this.#checkImmediateTimer) {\n clearImmediate(this.#checkImmediateTimer);\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAuCA,IAAa,oBAAoB,CAAC,WAAW,QAAQ;AACrD,IAAa,oBAAoB,CAAC,WAAW,SAAS;AAItD,SAAS,wBAAwB,MAAc;CAC7C,IAAI,SAAS,cACX;CAEF,IAAI,KAAK,WAAW,iBAAiB,GACnC,OAAO;CAET,IAAI,KAAK,WAAW,YAAY,GAAG;EACjC,IAAI,KAAK,SAAS,UAAU,GAC1B,OAAO;EAET,IAAI,KAAK,SAAS,gBAAgB,GAChC,OAAO;EAET,IAAI,KAAK,SAAS,WAAW,GAC3B,OAAO;EAET,OAAO;CACT;CACA,IAAI,KAAK,WAAW,QAAQ,GAC1B,OAAO;CAET,IAAI,KAAK,WAAW,QAAQ,GAC1B,OAAO;CAET,IAAI,KAAK,WAAW,eAAe,GACjC,OAAO;CAET,IAAI,KAAK,WAAW,SAAS,GAC3B,OAAO;CAET,OAAO;AACT;AAEA,SAAS,kBAAkB;CACzB,OAAO,qBAAqB,UAAU,oBAAoB;EACxD,aAAa;EACb,MAAM;EACN,kBAAkB;CACpB,CAAC;AACH;AAEA,SAAgB,wBAAwB,YAAoB;CAC1D,gBAAgB,CAAC,CAAC,SAAS,YAAY,EAAC,WAAW,aAAY,CAAC;AAClE;AAEA,SAAS,wBAAwB;CAC/B,OAAO,qBAAqB,UAAU,2BAA2B;EAC/D,aAAa;EACb,MAAM;EACN,kBAAkB;CACpB,CAAC;AACH;;;;;AAgBA,IAAa,iBAAb,MAA4B;CAC1B;CACA,8BAAuB,IAAI,IAAgB;CAC3C,uBAAgB,IAAI,IAAgB;CACpC;CACA,SAAkB,KAAK,IAAI;CAC3B,SAAmC,CAAC;CAEpC,gBAAgB,IAAI,aAAa,iBAAiB;CAClD,cAAc;CAEd,YAAY,IAAgB,MAAoB;EAC9C,KAAK,MAAM,GAAG,YAAY,aAAa,iBAAiB;EAKxD,KAAK,MAAM,UAAU,mBACnB,KAAK,GAAG,cAAc,KAAK,YAAY,MAAM,CAAC;EAIhD,KAAK,GAAG,SAAQ,SACd,KAAK,MACH,KAAK,MACL,SAAS,IACL,YACA,SAAA,KACE,YACA,SACR,CACF;EASA,KAAK,MAAM,UAAU,mBACnB,KAAK,GAAG,cACN,KAAK,MAAM,WAAW,YAAA,KAA8C,EAAE,CACxE;EAGF,KAAK,aAAa,SAAiB;GACjC,IAAI,kBAAkB,GACpB,OAAO,KAAK,KAAK,QAAQ,IAAI;GAE/B,QAAQ,KAAK,IAAI;EACnB;CACF;CAEA,MAAM,WAAW;EACf,IAAI,KAAK,gBAAgB,GACvB,KAAK,YAAY,kBAAkB,EAAE;EAEvC,MAAM,KAAK,cAAc,QAAQ;CACnC;CAEA,MAAM,MAAc;EAClB,KAAK,IAAI,OAAO,qBAAqB,IAAI;EACzC,KAAK,cAAc,KAAK,KAAK,GAAG;EAChC,KAAU,IAAI,MAAM,CAAC,CAAC,cAAc,KAAK,UAAU,IAAI,CAAC;CAC1D;CAEA,YAAY,QAAgC;EAC1C,IAAI,KAAK,KAAK,SAAS,GAAG;GAExB,KAAK,IAAI,OAAO,cAAc,QAAQ;GACtC,KAAK,MAAM,CAAC;EACd;EACA,KAAK,IAAI,OAAO,qBAAqB,OAAO,EAAE;EAC9C,KAAK,cAAc,KAAK,IAAI;EAC5B,IAAI,KAAK,YAAY,MACnB,KAAK,MAAM,KAAK,aAAa,MAAM;OAEnC,KAAK,MAAM,KAAK,MAAM,MAAM;CAEhC;CAEA,cAAc,MAAkB,MAAkB,MAAc;EAC9D,IAAI,SAAS,eACX,KAAK,YAAY,IAAI,IAAI;EAE3B,KAAK,KAAK,IAAI,IAAI;EAElB,IAAI,SAAS;EACb,KAAK,GAAG,UAAU,MAAM,WAAW;GACjC,SAAS;GACT,KAAK,QAAQ,MAAM,QAAQ,MAAM,MAAM,MAAM,IAAI;EACnD,CAAC;EAMD,KAAK,GAAG,UAAS,QACf,KAAK,IAAI,CAAC,UAAU,KAAK,cAAc,IAAI,SAAS,QAAQ,GAC1D,cAAc,KAAK,GAAG,KAAK,OAC3B,GACF,CACF;CACF;CAEA,gCAAyB,IAAI,IAAoB;CACjD,UAAU;CAEV,UAAU,QAAgB,MAAkB,MAAsB;EAChE,KAAK,cAAc,QAAQ,MAAM,IAAI;EAErC,MAAM,QAAQ,YAAY,IAAI;EAC9B,MAAM,KAAK,EAAE,KAAK;EAClB,KAAK,cAAc,IAAI,IAAI,IAAI;EAC/B,MAAM,EAAC,SAAS,YAAW,SAAS;EACpC,KAAK,OAAO,KAAK,OAAO;EAExB,OAAO,gBAAgB,eAAe;GACpC,MAAM,UAAU,YAAY,IAAI,IAAI;GACpC,MAAM,mBAAmB,wBAAwB,IAAI;GACrD,IAAI,kBACF,sBAAsB,CAAC,CAAC,SAAS,SAAS;IACxC,QAAQ;IACR;GACF,CAAC;GAEH,KAAK,IAAI,QAAQ,GAAG,KAAK,UAAU,KAAK,IAAI,IAAI,KAAK,OAAO,KAAK;GACjE,KAAK,cAAc,OAAO,EAAE;GAC5B,QAAQ;EACV,CAAC;EAED,OAAO;CACT;CAEA,eAAyB;EACvB,OAAO,CAAC,GAAG,KAAK,cAAc,OAAO,CAAC;CACxC;CAEA,MAAM,kBAAkB;EACtB,MAAM,QAAQ,IAAI,KAAK,MAAM;CAC/B;CAEA,gBAAgB,KAAc,MAAc;EAE1C,KAAK,QAAQ,IAAI,MAAM,KAAK,eAAe,MAAM,KAAA,CAAS;CAC5D;CAEA,QACE,MACA,KACA,KACA,MACA,MACA,QACA;EAEA,IAAI,QAAQ;GACV,KAAK,YAAY,OAAO,MAAM;GAC9B,KAAK,KAAK,OAAO,MAAM;EACzB;EAEA,MAAM,MAAM,QAAQ,OAAO,QAAQ;EASnC,IACE,SAAS,MACR,KAAK,gBAAgB,KACnB,SAAS,gBAAgB,KAAK,YAAY,OAAO,IAEpD,OAAA;EAGF,IAAI,SAAS,cAAc;GACzB,MAAM,MACJ,SAAS,KAAK,SAAA,KACV,SACA;GACN,KAAK,IAAI,IAAI,GAAG,GAAG,KAAK,IAAI,IAAI,sBAAsB,KAAK,IAAI,OAAO,EAAE;GACxE,OAAO,KAAK,MAAM,IAAI;EACxB;EAEA,MAAM,MACJ,SAAS,KAAK,SAAA,KACV,SACA,KAAK,cAAc,KAAK,SAAA,KACtB,SACA;EACR,KAAK,IAAI,IAAI,GACX,MACI,GAAG,KAAK,IAAI,IAAI,iBAAiB,IAAI,KACrC,GAAG,KAAK,IAAI,IAAI,sBAAsB,KAAK,IAC/C,OAAO,EACT;EAGA,IAAI,KAAK,YAAY,SAAS,GAAG;GAC/B,KAAK,IAAI,OACP,KAAK,cACD,oCACE,KAAK,IAAI,IAAI,KAAK,YACnB,QACD,gCACN;GACA,OAAO,KAAK,MAAM,KAAK,cAAc,IAAI,IAAI;EAC/C;EAEA,IAAI,KAAK,gBAAgB,GAGvB,OAAO,KAAK,MAAM,IAAI;CAI1B;CAEA,MAAM,SAA+B,QAAwB;EAC3D,KAAK,MAAM,UAAU,SACnB,IAAI;GACF,OAAO,KAAK,MAAM;EACpB,SAAS,GAAG;GACV,KAAK,IAAI,QAAQ,CAAC;EACpB;CAEJ;AACF;;;;;;;;;;;;;;;;;;AAmBA,IAAa,sBAAb,MAA6D;CAC3D,KAAc;CACd;CACA;CACA,yBAAkB,IAAI,IAAwB;CAC9C,QAAiB,SAAe;CAChC;CAEA,YAAY,IAAgB,QAAsB;EAChD,KAAK,MAAM,GAAG,YAAY,aAAa,sBAAsB;EAC7D,KAAK,UAAU;EAEf,KAAK,MAAM,QAAQ,YAAY,CAAC,CAAC;EAEjC,OAAO,GAAG,SAAQ,SAChB,KAAK,MACH,SAAS,IACL,YACA,SAAA,KACE,YACA,SACR,CACF;CACF;CAEA,IAA0B,MAAS,MAAiB;EAClD,KAAK,OAAO,IAAI,MAAM,IAAI;EAC1B,KAAK,GAAG,UAAU,MAAqB,QACrC,KAAK,SAAS,MAAM,MAAM,MAAM,GAAG,CACrC;EAGA,KAAK,GAAG,UAAS,QACf,KAAK,IAAI,KAAK,YAAY,SAAS,QAAQ,GACzC,cAAc,KAAK,GAAG,KAAK,OAC3B,GACF,CACF;EACA,OAAO;CACT;CAEA,MAAqB;EAKnB,KAAK,MAAM,UAAU,mBACnB,KAAK,QAAQ,GAAG,cAAc,KAAK,MAAM,MAAM,CAAC;EAElD,OAAO,KAAK,MAAM;CACpB;CAEA,MAAM,OAAsB;EAC1B,IAAI,CAAC,KAAK,WACR,KAAK,MAAM,SAAS;EAEtB,IAAI,KAAK,OAAO,SAAS,GACvB,KAAK,MAAM,QAAQ;EAErB,MAAM,KAAK,MAAM,QAAQ,YAAY,CAAC,CAAC;CACzC;CAEA,SACE,MACA,MACA,MACA,KACA;EACA,KAAK,OAAO,OAAO,IAAI;EACvB,MAAM,OAAO,MACT,GAAG,KAAK,IAAI,KAAK,IAAI,iBAAiB,IAAI,KAC1C,GAAG,KAAK,IAAI,KAAK,IAAI,sBAAsB,KAAK;EAEpD,IAAI,KAAK,WAAW;GAClB,KAAK,IAAI,OAAO,IAAI;GACpB,IAAI,KAAK,OAAO,SAAS,GACvB,KAAK,MAAM,QAAQ;EAEvB,OAAO,IAAI,SAAS,KAAK,SAAA,IAA0C;GACjE,KAAK,IAAI,OAAO,IAAI;GACpB,KAAK,MAAM,QAAQ;EACrB,OACE,KAAK,MAAM,OAAO,IAAI,MAAM,IAAI,CAAC;CAErC;CAEA,MAAM,QAAwB;EAC5B,KAAK,YAAY;EACjB,KAAK,MAAM,QAAQ,KAAK,OAAO,KAAK,GAClC,IAAI;GACF,KAAK,KAAK,MAAM;EAClB,SAAS,GAAG;GACV,KAAK,IAAI,QAAQ,CAAC;EACpB;CAEJ;AACF;;;;;;;;;;;;AA6BA,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CACA;CACA,aAAa;CACb,YAAY;CACZ;CAEA,YAAY,IAAgB,MAAoB,MAAwB;EACtE,KAAK,MAAM,GAAG,YAAY,aAAa,YAAY;EACnD,KAAK,QAAQ;EACb,KAAK,WAAW,KAAK;EACrB,KAAK,eAAe,KAAK;EAEzB,KAAK,MAAM,UAAU,mBACnB,KAAK,KAAK,cAAc;GACtB,KAAK,YAAY;GACjB,aAAa,KAAK,MAAM;EAC1B,CAAC;CAEL;CAEA,UAAU;EACR,IAAI,KAAK,cAAc,KAAK,WAC1B;EAEF,KAAK,aAAa;EAClB,KAAK,IAAI,OACP,4CAA4C,KAAK,SAAS,IAC5D;EACA,KAAK,aAAa;EAClB,KAAK,SAAS,iBAAiB;GAC7B,IAAI,CAAC,KAAK,WAAW;IACnB,KAAK,IAAI,OAAO,uBAAuB;IACvC,KAAK,MAAM,KAAK,WAAW,SAAS;GACtC;EACF,GAAG,KAAK,QAAQ;CAClB;AACF;;;;;AAMA,IAAa,kBAAkB;;;;;;;;;;;;AAa/B,eAAsB,eACpB,IACA,QACA,GAAG,UACY;CACf,IAAI,SAAS,WAAW,GACtB;CAEF,MAAM,0BAAU,IAAI,IAAsB;CAC1C,MAAM,OAAO,SAAS,KAAI,QAAO,IAAI,IAAI,CAAC,CAAC,cAAc,QAAQ,IAAI,GAAG,CAAC,CAAC;CAE1E,MAAM,aAAa,SAAe;CAClC,IAAI;CACJ,MAAM,WAAW,WACd,aAAa,aAAa,IAAI,UAAU,MAAM,SAAS,MAAM,CAAC,CAAC,KAC9D,WAAW,OACb;CAEF,MAAM,mBAAmB;CACzB,IAAI,WAAW;CACf,MAAM,WAAW,OAAO,WAA2B;EACjD,IAAI,iBAAiB,SAAS,MAAM,GAAG;GACrC,IAAI,YAAY,UAAU;IAExB,GAAG,OAAO,mCAAmC,QAAQ;IACrD;GACF;GACA,WAAW;GAIX,MAAM,UAAU,MAAM,QAAQ,WAC5B,SAAS,IAAI,OAAM,QAAO;IACxB,IAAI,IAAI,OAAO;KACb,GAAG,OAAO,YAAY,IAAI,YAAY,KAAK,GAAG,IAAI,GAAG,IAAI,OAAO,EAAE;KAClE,MAAM,IAAI,MAAM;IAClB;GACF,CAAC,CACH;GACA,KAAK,MAAM,UAAU,SACnB,IAAI,OAAO,WAAW,YACpB,GAAG,OAAO,yBAAyB,OAAO,IAAI,OAAO,MAAM;EAGjE;EAGA,MAAM,QAAQ,MAAM;CACtB;CAIA,KAAK,MAAM,UAAU,CAAC,GAAG,mBAAmB,GAAG,iBAAiB,GAC9D,OAAO,GAAG,cAAc,KAAK,SAAS,MAAM,CAAC;CAK/C,OAAO,KAAK,oBAAoB;EAC9B,GAAG,OAAO,wDAAwD;EAClE,QAAa,YAAY;CAC3B,CAAC;CAED,IAAI;EAGF,MAAM,MAAM,MAAM,QAAQ,KAAK,CAC7B,GAAG,KAAK,KAAK,KAAK,MAAM,IAAI,WAAW,SAAS,EAAE,CAAC,GACnD,WAAW,OACb,CAAC;EACD,IAAI,KACF,GAAG,OAAO,GAAG,IAAI,YAAY,KAAK,IAAI,IAAI,GAAG,UAAU;CAE3D,SAAS,GAAG;EACV,GAAG,QAAQ,oBAAoB,CAAC;EAChC,MAAM;CACR,UAAU;EACR,MAAM,QAAQ,SAAS;CACzB;AACF;AAEA,eAAe,aACb,IACA,UACA,MACA,SACA,QACA;CACA,KAAK,MAAM,OAAO,UAChB,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG;EACrB,GAAG,OAAO,YAAY,IAAI,YAAY,KAAK,GAAG,IAAI,GAAG,IAAI,OAAO,EAAE;EAClE,IACG,KAAK,CAAC,CACN,OAAM,MACL,GAAG,OAAO,kBAAkB,IAAI,YAAY,KAAK,GAAG,IAAI,MAAM,CAAC,CACjE;CACJ;CAEF,IACG,MAAM,UAAU,QAAQ,WAAW,IAAI,GAAA,GAAkB,MAAO,aACjE;EACA,MAAM,UAAU,SAAS,QAAO,QAAO,CAAC,QAAQ,IAAI,GAAG,CAAC;EACxD,GAAG,OACD,gCAAgC,gBAAgB,SAC9C,QAAQ,KAAI,QAAO,GAAG,IAAI,YAAY,KAAK,GAAG,IAAI,IAAI,CAAC,CAAC,KAAK,IAAI,CACrE;CACF;AACF;AAEA,eAAsB,UACpB,YACA,KACA;CACA,MAAM,gBACJ,OAAO,eAAe,aAAa,mBAAmB;CACxD,IAAI;EACF,MAAM,IAAI;EAEV,cAAA,CAAA,CAAG,OAAO,OAAO,IAAI,kBAAkB;EACvC,QAAQ,KAAK,CAAC;CAChB,SAAS,GAAG;EACV,MAAM,KAAK,cAAc;EACzB,IAAI,aAAa,oBAAoB;GACnC,GAAG,QAAQ,qCAAqC,OAAO,CAAC,KAAK,CAAC;GAC9D,QAAQ,KAAK,CAAC;EAChB;EACA,IAAI,aAAa,YAIf,GAAG,OAAO,wBAAwB,OAAO,CAAC,KAAK,CAAC;OAEhD,GAAG,QAAQ,uBAAuB,OAAO,CAAC,KAAK,CAAC;EAElD,IAAI;GACF,yCAAyC,IAAI,CAAC;EAChD,SAAS,iBAAiB;GACxB,GAAG,QAAQ,mDAAmD,EAC5D,OAAO,gBACT,CAAC;EACH;EACA,IAAI;GACF,MAAM,GAAG,MAAM;EACjB,UAAU;GACR,QAAQ,KAAK,EAAE;EACjB;CACF;AACF;;;;;;;;;;;;AAaA,IAAa,mBAAb,MAA8B;CAC5B;CAEA;CACA;CACA;CACA,iBAAiB;CAEjB,YAAY,IAAgB,cAAsB;EAChD,KAAK,MAAM;EACX,KAAK,gBAAgB;CACvB;CAEA,YAAY,YAAiC;EAC3C,KAAK,iBAAiB,KAAK,IAAI;EAC/B,IAAI,KAAK,wBAAwB,KAAA,GAAW;GAC1C,KAAK,IAAI,OACP,iCACE,KAAK,gBAAgB,IACtB,mBACD,UACF;GAGA,KAAK,sBAAsB,YACzB,KAAK,oBACL,KAAK,gBAAgB,CACvB;EACF;CACF;CAEA,2BAA2B;EAsBzB,KAAK,uBAAuB,mBAAmB;GAC7C,KAAK,uBAAuB,KAAA;GAC5B,MAAM,yBAAyB,KAAK,IAAI,IAAI,KAAK;GACjD,IAAI,0BAA0B,KAAK,eAAe;IAChD,KAAK,IAAI,OACP,2BACE,yBAAyB,IAC1B,wBACH;IACA,QAAQ,KAAK,QAAQ,KAAK,kBAAkB,EAAE;GAChD;EACF,CAAC;CACH;CAEA,OAAO;EACL,aAAa,KAAK,mBAAmB;EACrC,IAAI,KAAK,sBACP,eAAe,KAAK,oBAAoB;CAE5C;AACF"}
1
+ {"version":3,"file":"life-cycle.js","names":[],"sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"sourcesContent":["import type {IncomingHttpHeaders} from 'node:http';\nimport {pid} from 'node:process';\nimport type {EventEmitter} from 'stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {AbortError} from '../../../shared/src/abort-error.ts';\nimport {resolver} from '../../../shared/src/resolver.ts';\nimport {logLastChanceSQLiteCorruptionDiagnostics} from '../db/sqlite-corruption.ts';\nimport {\n getOrCreateHistogram,\n LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n} from '../observability/metrics.ts';\nimport {ConfigurationError} from '../types/configuration-error.ts';\nimport {\n singleProcessMode,\n type Subprocess,\n type Worker,\n} from '../types/processes.ts';\nimport {orTimeout} from '../types/timeout.ts';\nimport {RunningState} from './running-state.ts';\nimport type {SingletonService} from './service.ts';\n\n/**\n * * `user-facing` workers serve external requests and are the first to\n * receive a `SIGTERM` or `SIGINT` signal for graceful shutdown.\n *\n * * `supporting` workers support `user-facing` workers and are sent\n * the `SIGTERM` signal only after all `user-facing` workers have\n * exited.\n *\n * For other kill signals, such as `SIGQUIT` and `SIGABRT`, all workers\n * are stopped without draining. `SIGQUIT` is used to represent an\n * intentional shutdown (for which draining is not beneficial), whereas\n * `SIGABRT` is used for unexpected process exits.\n *\n * A clean exit (i.e. code 0) of a worker of either type that is not the\n * result of a drain is treated as an intentional shutdown of the server.\n */\nexport type WorkerType = 'user-facing' | 'supporting';\n\nexport const GRACEFUL_SHUTDOWN = ['SIGTERM', 'SIGINT'] as const;\nexport const FORCEFUL_SHUTDOWN = ['SIGQUIT', 'SIGABRT'] as const;\n\ntype GracefulShutdownSignal = (typeof GRACEFUL_SHUTDOWN)[number];\n\nfunction workerStartupMetricName(name: string) {\n if (name === 'zero-cache') {\n return undefined;\n }\n if (name.startsWith('change-streamer')) {\n return 'change_streamer';\n }\n if (name.startsWith('replicator')) {\n if (name.includes('(backup)')) {\n return 'backup_replicator';\n }\n if (name.includes('(serving-copy)')) {\n return 'serving_copy_replicator';\n }\n if (name.includes('(serving)')) {\n return 'serving_replicator';\n }\n return 'other';\n }\n if (name.startsWith('syncer')) {\n return 'syncer';\n }\n if (name.startsWith('reaper')) {\n return 'reaper';\n }\n if (name.startsWith('shadow-syncer')) {\n return 'shadow_syncer';\n }\n if (name.startsWith('mutator')) {\n return 'mutator';\n }\n return 'other';\n}\n\nfunction startupDuration() {\n return getOrCreateHistogram('server', 'startup_duration', {\n description: 'Duration from starting zero-cache to its ready signal.',\n unit: 's',\n bucketBoundaries: LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\nexport function recordStartupDurationMs(durationMs: number) {\n startupDuration().recordMs(durationMs, {component: 'dispatcher'});\n}\n\nfunction workerStartupDuration() {\n return getOrCreateHistogram('server', 'worker_startup_duration', {\n description: 'Duration from starting a worker to its ready signal.',\n unit: 's',\n bucketBoundaries: LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n\n// An internal error code used to indicate that a message has already been\n// logged at level ERRROR. When a process exits with this error code, the\n// parent process logs the exit at level WARN instead of ERROR.\nexport const UNHANDLED_EXCEPTION_ERROR_CODE = 13;\n\n// An internal error code used to indicate that the server should exit\n// without draining (e.g. due to a worker exiting cleanly outside of a drain,\n// such as for an auto reset), but the exit is otherwise intentional.\nexport const INTENTIONAL_SHUTDOWN_ERROR_CODE = 14;\n\n/**\n * Handles readiness, termination signals, and coordination of graceful\n * shutdown.\n */\nexport class ProcessManager {\n readonly #lc: LogContext;\n readonly #userFacing = new Set<Subprocess>();\n readonly #all = new Set<Subprocess>();\n readonly #exitImpl: (code: number) => never;\n readonly #start = Date.now();\n readonly #ready: Promise<void>[] = [];\n\n #runningState = new RunningState('process-manager');\n #drainStart = 0;\n\n constructor(lc: LogContext, proc: EventEmitter) {\n this.#lc = lc.withContext('component', 'process-manager');\n\n // Propagate `SIGTERM` and `SIGINT` to all user-facing workers,\n // initiating a graceful shutdown. The parent process will\n // exit once all user-facing workers have exited ...\n for (const signal of GRACEFUL_SHUTDOWN) {\n proc.on(signal, () => this.#startDrain(signal));\n }\n\n // ... which will result in sending `SIGTERM` to the remaining workers.\n proc.on('exit', code =>\n this.#kill(\n this.#all,\n code === 0\n ? 'SIGTERM' // graceful, drained shutdown\n : code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'SIGQUIT' // intentional abort without drain\n : 'SIGABRT', // unintentional shutdown, alertable error\n ),\n );\n\n // For other (catchable) kill signals, exit with a non-zero error code\n // to send a `SIGQUIT` (intentional shutdown) or `SIGABRT` (unexpected\n // shutdown) to all workers. For these signals, workers are stopped\n // immediately without draining, since there is no merit to slowly draining\n // when supporting workers have stopped.\n //\n // The logic for handling these signals is in `runUntilKilled()`.\n for (const signal of FORCEFUL_SHUTDOWN) {\n proc.on(signal, () =>\n this.#exit(signal === 'SIGQUIT' ? INTENTIONAL_SHUTDOWN_ERROR_CODE : -1),\n );\n }\n\n this.#exitImpl = (code: number) => {\n if (singleProcessMode()) {\n return proc.emit('exit', code) as never; // For unit / integration tests.\n }\n process.exit(code);\n };\n }\n\n async shutdown() {\n if (this.#drainStart === 0) {\n this.#startDrain(GRACEFUL_SHUTDOWN[0]);\n }\n await this.#runningState.stopped();\n }\n\n #exit(code: number) {\n this.#lc.info?.('exiting with code', code);\n this.#runningState.stop(this.#lc);\n void this.#lc.flush().finally(() => this.#exitImpl(code));\n }\n\n #startDrain(signal: GracefulShutdownSignal) {\n if (this.#all.size === 0) {\n // Shutdown if a signal is received before any subprocesses are added.\n this.#lc.info?.(`exiting on ${signal}`);\n this.#exit(0);\n }\n this.#lc.info?.(`initiating drain (${signal})`);\n this.#drainStart = Date.now();\n if (this.#userFacing.size) {\n this.#kill(this.#userFacing, signal);\n } else {\n this.#kill(this.#all, signal);\n }\n }\n\n addSubprocess(proc: Subprocess, type: WorkerType, name: string) {\n if (type === 'user-facing') {\n this.#userFacing.add(proc);\n }\n this.#all.add(proc);\n\n let isOpen = true;\n proc.on('close', (code, signal) => {\n isOpen = false;\n this.#onExit(code, signal, null, type, name, proc);\n });\n\n // As per https://nodejs.org/api/child_process.html#event-error\n // 'error' events can happen when sending a message to a child process\n // fails. This is not really an error when the server is shutting down,\n // so log any post-close errors at 'warn'.\n proc.on('error', err =>\n this.#lc[!isOpen || this.#drainStart > 0 ? 'warn' : 'error']?.(\n `error from ${name} ${proc.pid}`,\n err,\n ),\n );\n }\n\n readonly #initializing = new Map<number, string>();\n #nextID = 0;\n\n addWorker(worker: Worker, type: WorkerType, name: string): Worker {\n this.addSubprocess(worker, type, name);\n\n const start = performance.now();\n const id = ++this.#nextID;\n this.#initializing.set(id, name);\n const {promise, resolve} = resolver();\n this.#ready.push(promise);\n\n worker.onceMessageType('ready', () => {\n const elapsed = performance.now() - start;\n const workerMetricName = workerStartupMetricName(name);\n if (workerMetricName) {\n workerStartupDuration().recordMs(elapsed, {\n worker: workerMetricName,\n type,\n });\n }\n this.#lc.debug?.(`${name} ready (${Date.now() - this.#start} ms)`);\n this.#initializing.delete(id);\n resolve();\n });\n\n return worker;\n }\n\n initializing(): string[] {\n return [...this.#initializing.values()];\n }\n\n async allWorkersReady() {\n await Promise.all(this.#ready);\n }\n\n logErrorAndExit(err: unknown, name: string) {\n // only accessible by the main (i.e. user-facing) process.\n this.#onExit(-1, null, err, 'user-facing', name, undefined);\n }\n\n #onExit(\n code: number,\n sig: NodeJS.Signals | null,\n err: unknown,\n type: WorkerType,\n name: string,\n worker: Subprocess | undefined,\n ) {\n // Remove the worker from maps to avoid attempting to send more signals to it.\n if (worker) {\n this.#userFacing.delete(worker);\n this.#all.delete(worker);\n }\n\n const pid = worker?.pid ?? process.pid;\n\n // A clean exit outside of a drain is an intentional shutdown, e.g. the\n // replication-manager shutting down for protocol-specific instructions\n // (like auto reset), or a configuration error. In this case, a special\n // error code is used to signal that the server should be shut down\n // without draining, but it is otherwise not considered an\n // unexpected/alertable error. The same applies to a supporting worker\n // that exits while user-facing workers are still draining.\n if (\n code === 0 &&\n (this.#drainStart === 0 ||\n (type === 'supporting' && this.#userFacing.size > 0))\n ) {\n code = INTENTIONAL_SHUTDOWN_ERROR_CODE;\n }\n\n if (type === 'supporting') {\n const log =\n code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'info'\n : 'warn';\n this.#lc[log]?.(`${name} (${pid}) exited with code (${code})`, err ?? '');\n return this.#exit(code);\n }\n\n const log =\n code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'info'\n : this.#drainStart > 0 || code === UNHANDLED_EXCEPTION_ERROR_CODE\n ? 'warn'\n : 'error';\n this.#lc[log]?.(\n sig\n ? `${name} (${pid}) killed with (${sig})`\n : `${name} (${pid}) exited with code (${code})`,\n err ?? '',\n );\n\n // user-facing workers exited or finished draining.\n if (this.#userFacing.size === 0) {\n this.#lc.info?.(\n this.#drainStart\n ? `all user-facing workers drained (${\n Date.now() - this.#drainStart\n } ms)`\n : `all user-facing workers exited`,\n );\n return this.#exit(this.#drainStart ? 0 : code);\n }\n\n if (this.#drainStart === 0) {\n // If a user-facing worker exits without receiving a drain signal,\n // shutdown the server.\n return this.#exit(code);\n }\n\n return undefined;\n }\n\n #kill(workers: Iterable<Subprocess>, signal: NodeJS.Signals) {\n for (const worker of workers) {\n try {\n worker.kill(signal);\n } catch (e) {\n this.#lc.error?.(e);\n }\n }\n }\n}\n\n/**\n * Runs the supporting subprocesses of a worker (e.g. the litestream process\n * of the backup-replicator) as a {@link SingletonService}.\n *\n * Unlike a {@link ProcessManager}, it does not forward `SIGTERM` or `SIGINT`\n * to its subprocesses. Instead, {@link runUntilKilled} stops it along with the\n * worker's other services, i.e. after they have drained, so that the\n * subprocesses keep supporting them until then. Forceful signals (`SIGQUIT`\n * and `SIGABRT`), on the other hand, are forwarded immediately once the\n * service is running, and the subprocesses are killed if the worker exits\n * before they are stopped.\n *\n * `run()` settles when a subprocess exits before {@link stop()} is called,\n * which stops the worker: a clean exit resolves (and the parent in turn treats\n * the worker's clean exit outside of a drain as an intentional shutdown),\n * while any other exit rejects.\n */\nexport class SupportingProcesses implements SingletonService {\n readonly id = 'supporting-processes';\n readonly #lc: LogContext;\n readonly #parent: EventEmitter;\n readonly #procs = new Map<Subprocess, string>();\n readonly #done = resolver<void>();\n #signaled: NodeJS.Signals | undefined;\n\n constructor(lc: LogContext, parent: EventEmitter) {\n this.#lc = lc.withContext('component', 'supporting-processes');\n this.#parent = parent;\n // run() may never be called if the worker fails before running services.\n this.#done.promise.catch(() => {});\n\n parent.on('exit', code =>\n this.#kill(\n code === 0\n ? 'SIGTERM'\n : code === INTENTIONAL_SHUTDOWN_ERROR_CODE\n ? 'SIGQUIT'\n : 'SIGABRT',\n ),\n );\n }\n\n add<P extends Subprocess>(proc: P, name: string): P {\n this.#procs.set(proc, name);\n proc.on('close', (code: number | null, sig: NodeJS.Signals | null) =>\n this.#onClose(proc, name, code, sig),\n );\n // As with the ProcessManager, 'error' events after a signal (e.g. failing\n // to send a message to an exiting process) are logged at 'warn'.\n proc.on('error', err =>\n this.#lc[this.#signaled ? 'warn' : 'error']?.(\n `error from ${name} ${proc.pid}`,\n err,\n ),\n );\n return proc;\n }\n\n run(): Promise<void> {\n // Signal listeners are only installed when running, i.e. alongside those\n // of runUntilKilled(). Installing them any earlier would consume signals\n // received during worker initialization, which would otherwise terminate\n // the worker (or be handled by the initialization logic).\n for (const signal of FORCEFUL_SHUTDOWN) {\n this.#parent.on(signal, () => this.#kill(signal));\n }\n return this.#done.promise;\n }\n\n async stop(): Promise<void> {\n if (!this.#signaled) {\n this.#kill('SIGTERM');\n }\n if (this.#procs.size === 0) {\n this.#done.resolve();\n }\n await this.#done.promise.catch(() => {});\n }\n\n #onClose(\n proc: Subprocess,\n name: string,\n code: number | null,\n sig: NodeJS.Signals | null,\n ) {\n this.#procs.delete(proc);\n const exit = sig\n ? `${name} (${proc.pid}) killed with (${sig})`\n : `${name} (${proc.pid}) exited with code (${code})`;\n\n if (this.#signaled) {\n this.#lc.info?.(exit);\n if (this.#procs.size === 0) {\n this.#done.resolve();\n }\n } else if (code === 0 || code === INTENTIONAL_SHUTDOWN_ERROR_CODE) {\n this.#lc.info?.(exit);\n this.#done.resolve();\n } else {\n this.#done.reject(new Error(exit));\n }\n }\n\n #kill(signal: NodeJS.Signals) {\n this.#signaled = signal;\n for (const proc of this.#procs.keys()) {\n try {\n proc.kill(signal);\n } catch (e) {\n this.#lc.error?.(e);\n }\n }\n }\n}\n\nexport type SelfDrainOptions = {\n /**\n * The time after a self drain is requested at which the server sends itself\n * a `SIGTERM`, unless one was received sooner (e.g. from the container\n * runner).\n */\n delayMs: number;\n\n /**\n * Called when a self drain is requested. This should fail health checks\n * (both readiness and liveness) so that the container runner stops routing\n * to the server and initiates its normal shutdown process.\n */\n onRequested: () => void;\n};\n\n/**\n * Coordinates a drain that is initiated by the server itself (e.g. by a\n * view-syncer that has been granted permission to restore a new replica),\n * as opposed to one initiated by the container runner with a `SIGTERM`.\n *\n * Health checks start failing immediately, upon which an orchestrator such\n * as Kubernetes stops routing to the server and shuts it down as usual\n * (i.e. running any pre-stop hooks before sending `SIGTERM`). As a backstop,\n * and for environments without liveness checks, the server sends itself a\n * `SIGTERM` after the configured delay.\n */\nexport class SelfDrain {\n readonly #lc: LogContext;\n readonly #proc: EventEmitter;\n readonly #delayMs: number;\n readonly #onRequested: () => void;\n #requested = false;\n #signaled = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n\n constructor(lc: LogContext, proc: EventEmitter, opts: SelfDrainOptions) {\n this.#lc = lc.withContext('component', 'self-drain');\n this.#proc = proc;\n this.#delayMs = opts.delayMs;\n this.#onRequested = opts.onRequested;\n\n for (const signal of GRACEFUL_SHUTDOWN) {\n proc.once(signal, () => {\n this.#signaled = true;\n clearTimeout(this.#timer);\n });\n }\n }\n\n request() {\n if (this.#requested || this.#signaled) {\n return; // Already draining.\n }\n this.#requested = true;\n this.#lc.info?.(\n `self drain requested. sending SIGTERM in ${this.#delayMs} ms`,\n );\n this.#onRequested();\n this.#timer = setTimeout(() => {\n if (!this.#signaled) {\n this.#lc.info?.(`initiating self drain`);\n this.#proc.emit('SIGTERM', 'SIGTERM');\n }\n }, this.#delayMs);\n }\n}\n\n/**\n * The maximum time that {@link runUntilKilled} waits for its services to stop\n * (i.e. for their `run()` to settle) after stopping them.\n */\nexport const STOP_TIMEOUT_MS = 60_000;\n\n/**\n * Runs the specified services, stopping them on `SIGTERM` or `SIGINT` with\n * an optional {@link SingletonService.drain drain()}, or stopping them\n * without draining for `SIGQUIT`, or when the parent process disconnects.\n * Repeated graceful signals are ignored. If a service stops (or throws) on its\n * own, the remaining services are stopped without draining.\n *\n * @returns a Promise that settles once all services have stopped (or\n * {@link STOP_TIMEOUT_MS} has elapsed), resolving or rejecting\n * with the outcome of the first service to stop.\n */\nexport async function runUntilKilled(\n lc: LogContext,\n parent: EventEmitter,\n ...services: SingletonService[]\n): Promise<void> {\n if (services.length === 0) {\n return;\n }\n const stopped = new Set<SingletonService>();\n const runs = services.map(svc => svc.run().finally(() => stopped.add(svc)));\n\n const allStopped = resolver<void>();\n let stopping: Promise<void> | undefined;\n const stopAll = (reason: string) =>\n (stopping ??= stopServices(lc, services, runs, stopped, reason).then(\n allStopped.resolve,\n ));\n\n const GRACEFUL_SIGNALS = GRACEFUL_SHUTDOWN as readonly NodeJS.Signals[];\n let draining = false;\n const onSignal = async (signal: NodeJS.Signals) => {\n if (GRACEFUL_SIGNALS.includes(signal)) {\n if (draining || stopping) {\n // e.g. A SIGINT from both the terminal and a wrapping process.\n lc.info?.(`already shutting down. ignoring ${signal}`);\n return;\n }\n draining = true;\n // Drain all services before stopping any of them. Stopping a service\n // resolves its run() and thus ends runUntilKilled(), so a service\n // without a drain() must not be stopped while others are draining.\n const results = await Promise.allSettled(\n services.map(async svc => {\n if (svc.drain) {\n lc.info?.(`draining ${svc.constructor.name} ${svc.id} (${signal})`);\n await svc.drain();\n }\n }),\n );\n for (const result of results) {\n if (result.status === 'rejected') {\n lc.warn?.(`error while draining (${signal})`, result.reason);\n }\n }\n }\n // A forceful signal stops the services without waiting for (or after)\n // a drain.\n await stopAll(signal);\n };\n // The listeners remain installed for the lifetime of the process, as a\n // signal without a listener would terminate the process (e.g. a repeated\n // SIGINT during a drain).\n for (const signal of [...GRACEFUL_SHUTDOWN, ...FORCEFUL_SHUTDOWN]) {\n parent.on(signal, () => void onSignal(signal));\n }\n // A forked worker whose parent has exited (e.g. was killed by a signal,\n // in which case it cannot signal its workers) stops without draining, so\n // that it (and in turn its own subprocesses) is not orphaned.\n parent.once('disconnect', () => {\n lc.warn?.(`parent process disconnected. stopping without draining`);\n void stopAll('disconnect');\n });\n\n try {\n // Run all services and resolve when any of them stops, or when stopping\n // them (after a signal) has timed out.\n const svc = await Promise.race([\n ...runs.map((run, i) => run.then(() => services[i])),\n allStopped.promise,\n ]);\n if (svc) {\n lc.info?.(`${svc.constructor.name} (${svc.id}) stopped`);\n }\n } catch (e) {\n lc.error?.(`exiting on error`, e);\n throw e;\n } finally {\n await stopAll('stopped');\n }\n}\n\nasync function stopServices(\n lc: LogContext,\n services: SingletonService[],\n runs: Promise<void>[],\n stopped: Set<SingletonService>,\n reason: string,\n) {\n for (const svc of services) {\n if (!stopped.has(svc)) {\n lc.info?.(`stopping ${svc.constructor.name} ${svc.id} (${reason})`);\n void svc\n .stop()\n .catch(e =>\n lc.warn?.(`error stopping ${svc.constructor.name} ${svc.id}`, e),\n );\n }\n }\n if (\n (await orTimeout(Promise.allSettled(runs), STOP_TIMEOUT_MS)) === 'timed-out'\n ) {\n const running = services.filter(svc => !stopped.has(svc));\n lc.warn?.(\n `services did not stop within ${STOP_TIMEOUT_MS} ms: ` +\n running.map(svc => `${svc.constructor.name} ${svc.id}`).join(', '),\n );\n }\n}\n\nexport async function exitAfter(\n logContext: LogContext | (() => LogContext),\n run: () => Promise<void>,\n) {\n const getLogContext =\n typeof logContext === 'function' ? logContext : () => logContext;\n try {\n await run();\n const lc = getLogContext();\n lc.info?.(`pid ${pid} exiting normally`);\n process.exit(0);\n } catch (e) {\n const lc = getLogContext();\n if (e instanceof ConfigurationError) {\n lc.error?.(`exiting with configuration error: ${String(e)}`, e);\n process.exit(0);\n }\n if (e instanceof AbortError) {\n // AbortErrors signal an intentional shutdown or restart (e.g. SIGTERM\n // during a rollout, AutoResetSignal) and should not raise alerts. The\n // non-zero exit code is kept so that restart semantics are unchanged.\n lc.warn?.(`exiting after abort: ${String(e)}`, e);\n } else {\n lc.error?.(`exiting with error: ${String(e)}`, e);\n }\n try {\n logLastChanceSQLiteCorruptionDiagnostics(lc, e);\n } catch (diagnosticError) {\n lc.error?.('SQLite corruption last-chance diagnostic failed', {\n error: diagnosticError,\n });\n }\n try {\n await lc.flush();\n } finally {\n process.exit(-1);\n }\n }\n}\n\n/**\n * The HeartbeatMonitor monitors the cadence heartbeats (e.g. \"/keepalive\"\n * health checks made to HttpServices) that signal that the server\n * should continue processing requests. When a configurable `stopInterval`\n * elapses without receiving these heartbeats, the monitor initiates a\n * graceful shutdown of the server. This works with common load balancing\n * frameworks such as AWS Elastic Load Balancing.\n *\n * The HeartbeatMonitor is **opt-in** in that it only kicks in after it\n * starts receiving keepalives.\n */\nexport class HeartbeatMonitor {\n readonly #stopInterval: number;\n\n #lc: LogContext;\n #checkIntervalTimer: NodeJS.Timeout | undefined;\n #checkImmediateTimer: NodeJS.Immediate | undefined;\n #lastHeartbeat = 0;\n\n constructor(lc: LogContext, stopInterval: number) {\n this.#lc = lc;\n this.#stopInterval = stopInterval;\n }\n\n onHeartbeat(reqHeaders: IncomingHttpHeaders) {\n this.#lastHeartbeat = Date.now();\n if (this.#checkIntervalTimer === undefined) {\n this.#lc.info?.(\n `starting heartbeat monitor at ${\n this.#stopInterval / 1000\n } second interval`,\n reqHeaders,\n );\n // e.g. check every 5 seconds to see if it's been over 20 seconds\n // since the last heartbeat.\n this.#checkIntervalTimer = setInterval(\n this.#checkStopInterval,\n this.#stopInterval / 4,\n );\n }\n }\n\n #checkStopInterval = () => {\n // In the Node.js event loop, timers like setInterval and setTimeout\n // run *before* I/O events coming from network sockets or file reads/writes.\n // When this process gets starved of CPU resources for long periods of time,\n // for example when other processes are monopolizing all available cores,\n // pathological behavior can emerge:\n // - keepalive network request comes in, but is queued in Node internals waiting\n // for time on the event loop\n // - CPU is starved/monopolized by other processes for longer than the time\n // configured via this.#stopInterval\n // - When CPU becomes available and the event loop wakes up, this stop interval\n // check is run *before* the keepalive request is processed. The value of\n // this.#lastHeartbeat is now very stale, and erroneously triggers a shutdown\n // even though keepalive requests were about to be processed and update\n // this.#lastHeartbeat. Downtime ensues.\n //\n // To avoid this, we push the check out to a phase of the event loop *after*\n // I/O events are processed, using setImmediate():\n // https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout\n //\n // This ensures we see a value for this.#lastHeartbeat that reflects\n // any keepalive requests that came in during the current event loop turn.\n this.#checkImmediateTimer = setImmediate(() => {\n this.#checkImmediateTimer = undefined;\n const timeSinceLastHeartbeat = Date.now() - this.#lastHeartbeat;\n if (timeSinceLastHeartbeat >= this.#stopInterval) {\n this.#lc.info?.(\n `last heartbeat received ${\n timeSinceLastHeartbeat / 1000\n } seconds ago. draining.`,\n );\n process.kill(process.pid, GRACEFUL_SHUTDOWN[0]);\n }\n });\n };\n\n stop() {\n clearTimeout(this.#checkIntervalTimer);\n if (this.#checkImmediateTimer) {\n clearImmediate(this.#checkImmediateTimer);\n }\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { resolver } from "../../../../shared/src/resolver.js";
1
2
  import { parse, valita_exports } from "../../../../shared/src/valita.js";
2
3
  import { must } from "../../../../shared/src/must.js";
3
4
  import "../../config/normalize.js";
@@ -9,7 +10,6 @@ import { deleteLiteDB } from "../../db/delete-lite-db.js";
9
10
  import { deleteChangeLogDB } from "../replicator/change-log-db.js";
10
11
  import { litestreamSocketPath } from "./litestream-controller.js";
11
12
  import { litestreamBackupListDuration, litestreamBackupMetricAttrs, litestreamBackupProcessDuration, litestreamBackupProcessMetricAttrs, litestreamBackupProcessRuns, litestreamRestoreAttempts, litestreamRestoreMetricAttrs, litestreamRestoreProcessDuration, litestreamRestoreValidationDuration, litestreamRestoredDbBytes } from "./metrics.js";
12
- import { resolver } from "@rocicorp/resolver";
13
13
  import { spawn } from "node:child_process";
14
14
  import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
15
15
  import { basename, dirname, join } from "node:path";
@@ -1 +1 @@
1
- {"version":3,"file":"commands.js","names":["v","v.parse"],"sources":["../../../../../../zero-cache/src/services/litestream/commands.ts"],"sourcesContent":["import type {ChildProcess} from 'node:child_process';\nimport {spawn} from 'node:child_process';\nimport {existsSync, readdirSync, rmSync, statSync} from 'node:fs';\nimport {basename, dirname, join} from 'node:path';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {must} from '../../../../shared/src/must.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {type LitestreamConfig} from '../../config/normalize.ts';\nimport {deleteLiteDB} from '../../db/delete-lite-db.ts';\nimport {\n isSQLiteCorruption,\n logSQLiteCorruptionDiagnostics,\n} from '../../db/sqlite-corruption.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport {deleteChangeLogDB} from '../replicator/change-log-db.ts';\nimport {getSubscriptionState} from '../replicator/schema/replication-state.ts';\nimport {litestreamSocketPath} from './litestream-controller.ts';\nimport {\n litestreamBackupListDuration,\n litestreamBackupMetricAttrs,\n litestreamBackupProcessDuration,\n litestreamBackupProcessMetricAttrs,\n litestreamBackupProcessRuns,\n litestreamRestoreAttempts,\n litestreamRestoredDbBytes,\n litestreamRestoreMetricAttrs,\n litestreamRestoreProcessDuration,\n litestreamRestoreValidationDuration,\n type LitestreamRole,\n} from './metrics.ts';\n\nexport type ReplicaConstraints = {\n replicaVersion: string;\n minWatermark: string;\n};\n\nexport type RestoreResult =\n | 'success'\n | 'no_backup'\n | 'invalid_replica'\n | 'error';\n\nexport type RestoreAttempt = {\n restored: boolean;\n backupURL: string | undefined;\n result: RestoreResult;\n /**\n * Set when the restore forked the backup (see {@link ForkBackup}), which\n * then continues from the restored replica's WAL. The WAL is kept when the\n * last connection closes (see `Database.persistWal()`).\n */\n forked?: boolean | undefined;\n};\n\n/**\n * Prepares the restored replica to continue from a forked backup\n * instead of starting a new lineage with a full snapshot.\n */\nexport type ForkBackup = {\n /** The restore also forks its backup into the (new, empty) one at this URL. */\n url: string;\n};\n\nconst MAX_LOGGED_RESTORE_DIRECTORY_ENTRIES = 100;\nconst LEGACY_RESTORE_WAL_INDEX = /^[0-9a-f]{8}$/;\n\nexport class BackupNotFoundException extends Error {\n static readonly name = 'BackupNotFoundException';\n\n constructor(backupURL: string | undefined) {\n super(`backup not found at ${backupURL}`);\n }\n}\n\n/**\n * The effective backup URL: the `backupURLOverride` if present (i.e. a\n * lineage-specific location computed by the replication-manager), or\n * otherwise the configured `backupURL`.\n */\nexport function backupDestinationURL(\n config: Pick<LitestreamConfig, 'backupURL' | 'backupURLOverride'>,\n): string | undefined {\n return config.backupURLOverride ?? config.backupURL;\n}\n\nfunction getLitestream(\n mode: 'restore' | 'replicate',\n config: LitestreamConfig,\n replicaFile: string,\n logLevelOverride?: LogLevel,\n): {\n litestream: string;\n env: NodeJS.ProcessEnv;\n} {\n const {\n executable,\n executableV5,\n restoreUsingV5,\n backupUsingV5,\n logLevel,\n configPath,\n configPathV5,\n endpoint,\n region,\n port,\n checkpointThresholdMB,\n // Note: This assumes the default page size. If page size configuration\n // is added, this computation must be adjusted accordingly.\n minCheckpointPageCount = checkpointThresholdMB * 256, // 1 MiB / 4 KiB page size\n maxCheckpointPageCount = minCheckpointPageCount * 10,\n forceCheckpointThresholdMB,\n incrementalBackupIntervalMinutes,\n incrementalBackupIntervalSeconds,\n snapshotBackupIntervalHours,\n multipartConcurrency,\n multipartSize,\n } = config;\n\n const v5 =\n (mode === 'restore' && restoreUsingV5) ||\n (mode === 'replicate' && backupUsingV5);\n const litestream =\n (v5 ? executableV5 : executable) ??\n must(executable, `Missing --litestream-executable`);\n const litestreamConfig = v5 ? configPathV5 : configPath;\n // Disable truncate-page-n if forced checkpoints are enabled,\n // and otherwise use litestream's default.\n const truncatePageN = forceCheckpointThresholdMB ? -1 : 121359;\n\n return {\n litestream,\n env: {\n ...process.env,\n ['ZERO_REPLICA_FILE']: replicaFile,\n ['ZERO_LITESTREAM_BACKUP_URL']: must(backupDestinationURL(config)),\n ['ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT']: String(\n minCheckpointPageCount,\n ),\n ['ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT']: String(\n maxCheckpointPageCount,\n ),\n ['ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES']: String(\n incrementalBackupIntervalMinutes, // v3 only\n ),\n ['ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS']: String(\n incrementalBackupIntervalSeconds, // v5 only\n ),\n // v5 only\n ['ZERO_LITESTREAM_MONITOR_INTERVAL_SECONDS']: String(\n // zero-cache explicitly requests syncs at the incremental backup\n // interval (see LitestreamSyncRequester), and litestream's\n // own monitor becomes a backstop that keeps backups and checkpoints\n // progressing if zero-cache stops requesting them.\n 2 * incrementalBackupIntervalSeconds,\n ),\n ['ZERO_LITESTREAM_TRUNCATE_PAGE_N']: String(truncatePageN),\n ['ZERO_LITESTREAM_LOG_LEVEL']: logLevelOverride ?? logLevel,\n ['ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS']: String(\n snapshotBackupIntervalHours,\n ),\n ['ZERO_LITESTREAM_SNAPSHOT_RETENTION_INTERVAL_HOURS']: String(\n snapshotBackupIntervalHours + 6, // delete old snapshots after 6 hours\n ),\n ['ZERO_LITESTREAM_MULTIPART_CONCURRENCY']: String(multipartConcurrency),\n ['ZERO_LITESTREAM_MULTIPART_SIZE']: String(multipartSize),\n ['ZERO_LOG_FORMAT']: 'json',\n ['ZERO_LITESTREAM_SOCKET_PATH']: litestreamSocketPath(replicaFile),\n ['LITESTREAM_CONFIG']: litestreamConfig,\n ['LITESTREAM_PORT']: String(port),\n ...(endpoint ? {['ZERO_LITESTREAM_ENDPOINT']: endpoint} : {}),\n ...(region ? {['ZERO_LITESTREAM_REGION']: region} : {}),\n ['LITESTREAM_COMPACTION_SERIALIZE']: 'false',\n },\n };\n}\n\nexport async function tryRestore(\n lc: LogContext,\n config: LitestreamConfig,\n replicaFile: string,\n replicaConstraints: ReplicaConstraints | undefined,\n role: LitestreamRole,\n signal?: AbortSignal,\n forkBackup?: ForkBackup | undefined,\n): Promise<RestoreAttempt> {\n const {reusedExisting, ...attempt} = await restoreOnce(\n lc,\n config,\n replicaFile,\n replicaConstraints,\n role,\n signal,\n forkBackup,\n );\n if (attempt.result === 'invalid_replica' && reusedExisting) {\n // `-if-db-not-exists` made the restore a no-op, and the existing local\n // replica (e.g. one left behind by a failed initial sync of a different\n // generation) was invalid. It has since been deleted, so restore from the\n // backup.\n const {reusedExisting: _, ...retry} = await restoreOnce(\n lc,\n config,\n replicaFile,\n replicaConstraints,\n role,\n signal,\n forkBackup,\n );\n return retry;\n }\n return attempt;\n}\n\nasync function restoreOnce(\n lc: LogContext,\n config: LitestreamConfig,\n replicaFile: string,\n replicaConstraints: ReplicaConstraints | undefined,\n role: LitestreamRole,\n signal: AbortSignal | undefined,\n forkBackup: ForkBackup | undefined,\n): Promise<RestoreAttempt & {reusedExisting: boolean}> {\n const {backupUsingV5} = config;\n const backupURL = backupDestinationURL(config);\n if (forkBackup && !backupUsingV5) {\n forkBackup = undefined; // Ignore forkBackup if v5 is disabled.\n }\n const attrs = litestreamRestoreMetricAttrs(config, role, backupURL);\n let result: RestoreResult = 'error';\n try {\n logRestoreDirectoryContents(lc, replicaFile, 'before-temp-cleanup');\n deleteRestoreTempFiles(replicaFile);\n const reusedExisting = existsSync(replicaFile);\n if (!reusedExisting) {\n // The local litestream state (and anything recorded in it) describes a\n // replica that no longer exists. Discard it before the restore, which\n // (if it seeds a backup) writes the state that belongs to the new one.\n deleteLitestreamMetaDir(replicaFile);\n }\n const {litestream, env} = getLitestream(\n 'restore',\n config,\n replicaFile,\n 'debug', // Include all output from `litestream restore`, as it's minimal.\n );\n const {\n restoreParallelism: parallelism,\n multipartConcurrency,\n multipartSize,\n } = config;\n lc.info?.(`starting litestream restore`, {\n restoreParallelism: parallelism,\n multipartConcurrency,\n multipartSize,\n });\n // Pipe (rather than inherit) litestream's stdout/stderr so that its own\n // `\"level\":\"ERROR\"` output on a failed restore does not go straight to the\n // pod's stdout — where a log-scraper alert would page on it — before our\n // code has decided how to handle the failure. The captured output is\n // included in the thrown error so the caller can log the final outcome.\n // See INC-961.\n const proc = spawn(\n litestream,\n [\n 'restore',\n '-if-db-not-exists',\n '-if-replica-exists',\n '-parallelism',\n String(parallelism),\n ...(forkBackup ? ['-json', '-fork-to-url', forkBackup.url] : []),\n replicaFile,\n ],\n {env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, signal},\n );\n let stdout = '';\n let stderr = '';\n proc.stdout.setEncoding('utf-8');\n proc.stderr.setEncoding('utf-8');\n // Output AND capture the stdout and stderr, the former for observability\n // and the latter for error handling.\n proc.stdout.on('data', chunk => {\n process.stdout.write(chunk);\n stdout += chunk;\n });\n proc.stderr.on('data', chunk => {\n process.stderr.write(chunk);\n stderr += chunk;\n });\n const {promise, resolve, reject} = resolver();\n proc.on('error', reject);\n proc.on('close', (code, signal) => {\n if (signal) {\n reject(`litestream killed with ${signal}`);\n } else if (code !== 0) {\n reject(`litestream exited with code ${code}`);\n } else {\n resolve();\n }\n });\n const processStart = performance.now();\n try {\n await promise;\n litestreamRestoreProcessDuration().recordMs(\n performance.now() - processStart,\n {...attrs, result: 'success'},\n );\n } catch (e) {\n litestreamRestoreProcessDuration().recordMs(\n performance.now() - processStart,\n {...attrs, result: 'error'},\n );\n const output = [stdout, stderr]\n .map(value => value.trim())\n .filter(Boolean)\n .join('\\n');\n throw new Error(output ? `${String(e)}\\n${output}` : String(e), {\n cause: e,\n });\n }\n if (!existsSync(replicaFile)) {\n result = 'no_backup';\n return {restored: false, backupURL, result, reusedExisting};\n }\n const validationStart = performance.now();\n const db = openValidReplica(lc, replicaFile, replicaConstraints);\n litestreamRestoreValidationDuration().recordMs(\n performance.now() - validationStart,\n {...attrs, result: db ? 'success' : 'invalid_replica'},\n );\n if (!db) {\n result = 'invalid_replica';\n lc.info?.(`Deleting local replica and retrying restore`);\n deleteLiteDB(replicaFile);\n deleteChangeLogDB(replicaFile);\n // The litestream state (e.g. the anchor of a seeded backup) describes\n // the deleted replica.\n deleteLitestreamMetaDir(replicaFile);\n return {restored: false, backupURL, result, reusedExisting};\n }\n try {\n result = 'success';\n if (!reusedExisting) {\n // This restore materialized the replica file, so any change log beside it\n // was written against a replica that is no longer there. Reconciliation\n // is the safety net — a restored replica keeps its replicaVersion, so the\n // rule falls through to truncate-or-reseed on head mismatch — but the log\n // is a cache and deleting it here is the explicit statement that nothing\n // in it survives the restore.\n //\n // A restore that reuses an existing replica (`-if-db-not-exists` made it\n // a no-op) deliberately keeps the log: that is the process-restart path,\n // where the log is still the one written beside this exact replica and\n // discarding it would cost every reconnecting subscriber a `too-old`.\n deleteChangeLogDB(replicaFile);\n litestreamRestoredDbBytes().add(statSync(replicaFile).size, {\n ...attrs,\n result: 'success',\n });\n }\n const forked =\n forkBackup !== undefined &&\n !reusedExisting &&\n restoreForked(lc, stdout);\n return {restored: true, backupURL, result, reusedExisting, forked};\n } finally {\n db.close();\n }\n } finally {\n try {\n deleteRestoreTempFiles(replicaFile);\n } catch (e) {\n lc.warn?.('Unable to clean up Litestream restore temporary files', {\n replicaFile,\n error: String(e),\n });\n }\n logRestoreDirectoryContents(lc, replicaFile, 'after-temp-cleanup');\n litestreamRestoreAttempts().add(1, {...attrs, result});\n }\n}\n\n// From litestream/cmd/litestream/restore.go\nconst restoreForkOutputSchema = v.object({\n fork: v\n .object({\n forked: v.boolean(),\n txid: v.string().optional(),\n })\n .optional(),\n});\n\ntype RestoreForkOutput = v.Infer<typeof restoreForkOutputSchema>;\n\n/**\n * Whether the `-json` output of `litestream restore -fork-to-url` reports that\n * the backup was forked. A restore plan that cannot fork the backup is\n * restored without forking.\n */\nfunction restoreForked(lc: LogContext, stdout: string): boolean {\n const start = stdout.indexOf('{');\n const end = stdout.lastIndexOf('}');\n if (start < 0 || end < start) {\n lc.warn?.('litestream restore did not report a fork result');\n return false;\n }\n let output: RestoreForkOutput;\n try {\n output = v.parse(\n JSON.parse(stdout.slice(start, end + 1)),\n restoreForkOutputSchema,\n 'passthrough',\n );\n } catch (e) {\n lc.warn?.('unable to parse the litestream restore result', e);\n return false;\n }\n const forked = output.fork?.forked === true;\n lc.info?.(\n forked\n ? `forked the backup at txid ${output.fork?.txid}`\n : 'restored without forking the backup',\n );\n return forked;\n}\n\n/**\n * Deletes litestream's local state for `replicaFile` (its default meta\n * directory, `.<file>-litestream`).\n */\nexport function deleteLitestreamMetaDir(replicaFile: string) {\n rmSync(join(dirname(replicaFile), `.${basename(replicaFile)}-litestream`), {\n recursive: true,\n force: true,\n });\n}\n\nfunction deleteRestoreTempFiles(replicaFile: string) {\n const temporaryReplicaFile = `${replicaFile}.tmp`;\n deleteLiteDB(temporaryReplicaFile);\n\n // rocicorp/litestream zero@v0.0.9 downloads WAL indexes in parallel to\n // `<output>.tmp-<8 lowercase hex digits>-wal` before applying them.\n const directory = dirname(temporaryReplicaFile);\n if (!existsSync(directory)) {\n return;\n }\n const prefix = `${basename(temporaryReplicaFile)}-`;\n for (const name of readdirSync(directory)) {\n if (!name.startsWith(prefix) || !name.endsWith('-wal')) {\n continue;\n }\n const index = name.slice(prefix.length, -'-wal'.length);\n if (LEGACY_RESTORE_WAL_INDEX.test(index)) {\n rmSync(join(directory, name), {force: true});\n }\n }\n}\n\nfunction logRestoreDirectoryContents(\n lc: LogContext,\n replicaFile: string,\n phase: 'before-temp-cleanup' | 'after-temp-cleanup',\n) {\n const directory = dirname(replicaFile);\n try {\n const names = readdirSync(directory).sort();\n const entries = names\n .slice(0, MAX_LOGGED_RESTORE_DIRECTORY_ENTRIES)\n .map(name => ({name, sizeBytes: statSync(join(directory, name)).size}));\n lc.info?.('Litestream restore directory contents', {\n phase,\n directory,\n replicaFile,\n entryCount: names.length,\n omittedEntryCount: Math.max(\n 0,\n names.length - MAX_LOGGED_RESTORE_DIRECTORY_ENTRIES,\n ),\n entries,\n });\n } catch (e) {\n lc.warn?.('Unable to inspect Litestream restore directory', {\n phase,\n directory,\n replicaFile,\n error: String(e),\n });\n }\n}\n\n/**\n * Returns the replica version and watermark of the local replica file, or\n * `undefined` if it does not exist or cannot be read.\n */\nexport function readLocalReplica(\n lc: LogContext,\n replica: string,\n): {replicaVersion: string; watermark: string} | undefined {\n if (!existsSync(replica)) {\n return undefined;\n }\n let db: Database | undefined;\n try {\n db = new Database(lc, replica, {readonly: true});\n const {replicaVersion, watermark} = getSubscriptionState(\n new StatementRunner(db),\n );\n return {replicaVersion, watermark};\n } catch (e) {\n lc.warn?.(`unable to read the subscription state of ${replica}`, e);\n return undefined;\n } finally {\n db?.close();\n }\n}\n\n/**\n * Opens and validates the restored replica. Returns the open connection if\n * the replica is valid, which the caller must close; otherwise closes it and\n * returns `undefined`.\n */\nfunction openValidReplica(\n lc: LogContext,\n replica: string,\n constraints: ReplicaConstraints | undefined,\n): Database | undefined {\n let db: Database | undefined;\n try {\n // Note: Open the database and read the subscription state as a\n // sanity / corruption check, even if there are no constraints.\n db = new Database(lc, replica);\n const {replicaVersion, watermark} = getSubscriptionState(\n new StatementRunner(db),\n );\n if (constraints) {\n if (replicaVersion !== constraints.replicaVersion) {\n lc.warn?.(\n `Local replica version ${replicaVersion} does not match expected replicaVersion ${constraints.replicaVersion}`,\n constraints,\n );\n return undefined;\n }\n if (watermark < constraints.minWatermark) {\n lc.warn?.(\n `Local replica watermark ${watermark} is earlier than minWatermark ${constraints.minWatermark}`,\n );\n return undefined;\n }\n lc.info?.(\n `Local replica at version ${replicaVersion} and watermark ${watermark} is compatible`,\n constraints,\n );\n }\n const valid = db;\n db = undefined; // owned by the caller\n return valid;\n } catch (e) {\n if (isSQLiteCorruption(e)) {\n logSQLiteCorruptionDiagnostics(lc, 'restored replica', replica, e);\n }\n lc.error?.('Error while validating restored replica', e);\n return undefined;\n } finally {\n db?.close();\n }\n}\n\nexport function startReplicaBackupProcess(\n lc: LogContext,\n config: LitestreamConfig,\n replicaFile: string,\n): ChildProcess {\n const {litestream, env} = getLitestream('replicate', config, replicaFile);\n const attrs = litestreamBackupProcessMetricAttrs(config);\n lc.info?.(`starting litestream backup to ${backupDestinationURL(config)}`);\n const start = performance.now();\n const proc = spawn(litestream, ['replicate'], {\n env,\n stdio: 'inherit',\n windowsHide: true,\n });\n let recorded = false;\n const record = (result: 'success' | 'error' | 'stopped') => {\n if (recorded) {\n return;\n }\n recorded = true;\n const labels = {...attrs, result};\n litestreamBackupProcessRuns().add(1, labels);\n litestreamBackupProcessDuration().recordMs(\n performance.now() - start,\n labels,\n );\n };\n proc.on('error', e => {\n lc.warn?.(`litestream backup process error`, e);\n record('error');\n });\n proc.on('close', (code, signal) => {\n if (signal) {\n lc.info?.(`litestream backup process stopped`, {signal});\n record('stopped');\n } else if (code === 0) {\n record('success');\n } else {\n lc.warn?.(`litestream backup process exited with code ${code}`);\n record('error');\n }\n });\n return proc;\n}\n\n// Listing the backup state requires a few S3 LIST requests, which should\n// normally complete well within this timeout.\nconst LIST_BACKUP_TIMEOUT_MS = 30_000;\n\nconst wsRe = /\\s+/;\n\n/**\n * Returns the time of the most recent object (snapshot or WAL segment)\n * actually uploaded to the backup replica destination, as listed by the\n * bundled litestream CLI (`litestream snapshots` / `litestream wal`).\n *\n * This queries the replica destination (e.g. S3) directly, and thus serves\n * as a source of truth for backup durability. This is in contrast to the\n * `litestream_replica_progress` metric, which is exported when litestream\n * *believes* an upload has succeeded, and has been observed to advance even\n * when nothing is actually written to the destination.\n *\n * Rejects if the backup state cannot be determined (spawn error, non-zero\n * exit, timeout, or empty/unparseable listing).\n */\nexport async function getLastBackupTime(\n lc: LogContext,\n config: LitestreamConfig,\n replicaFile: string,\n): Promise<Date> {\n const [snapshots, wal] = await Promise.all([\n listBackupCreatedTimes(lc, config, replicaFile, 'snapshots'),\n listBackupCreatedTimes(lc, config, replicaFile, 'wal'),\n ]);\n const times = [...snapshots, ...wal];\n if (times.length === 0) {\n // Note: the litestream CLI exits with code 0 and logs listing errors\n // (e.g. S3 failures) to stderr, so an empty listing cannot be\n // distinguished from a failed one. Since a valid backup always contains\n // at least one snapshot, an empty listing is treated as a failure.\n throw new Error(\n `no snapshots or WAL segments listed at ${backupDestinationURL(config)}`,\n );\n }\n return new Date(Math.max(...times.map(time => time.getTime())));\n}\n\n/**\n * Runs `litestream <snapshots|wal> <replica-file>` with the same config /\n * environment used by the `litestream replicate` process (so that the\n * backupURL, endpoint, region, and credentials are identical), and parses\n * the `created` column (RFC3339) of the tab-formatted output, e.g.:\n *\n * ```\n * replica generation index size created\n * s3 1862f44967b3863f 0 4546445 2026-06-10T01:11:32Z\n * ```\n */\nasync function listBackupCreatedTimes(\n lc: LogContext,\n config: LitestreamConfig,\n replicaFile: string,\n command: 'snapshots' | 'wal',\n): Promise<Date[]> {\n const start = performance.now();\n let result: 'success' | 'empty' | 'timeout' | 'error' = 'error';\n const {litestream, env} = getLitestream('replicate', config, replicaFile);\n const proc = spawn(litestream, [command, replicaFile], {\n env,\n stdio: ['ignore', 'pipe', 'inherit'],\n windowsHide: true,\n });\n const {promise, resolve, reject} = resolver<string>();\n let stdout = '';\n proc.stdout.setEncoding('utf-8');\n proc.stdout.on('data', chunk => (stdout += chunk));\n proc.on('error', reject);\n proc.on('close', (code, signal) => {\n if (signal) {\n reject(new Error(`litestream ${command} killed with ${signal}`));\n } else if (code !== 0) {\n reject(new Error(`litestream ${command} exited with code ${code}`));\n } else {\n resolve(stdout);\n }\n });\n const timeout = setTimeout(() => {\n result = 'timeout';\n reject(new Error(`timed out listing backup state (litestream ${command})`));\n proc.kill('SIGKILL');\n }, LIST_BACKUP_TIMEOUT_MS);\n\n try {\n const output = await promise;\n const times = parseBackupCreatedTimes(lc, command, output);\n result = times.length ? 'success' : 'empty';\n return times;\n } finally {\n clearTimeout(timeout);\n litestreamBackupListDuration().recordMs(performance.now() - start, {\n ...litestreamBackupMetricAttrs(config),\n command,\n result,\n });\n }\n}\n\n/**\n * Parses the `created` column (the last, RFC3339-formatted column) from the\n * tab-formatted output of `litestream snapshots` / `litestream wal`. The\n * header row and any unparseable lines are skipped.\n *\n * Exported for testing.\n */\nexport function parseBackupCreatedTimes(\n lc: LogContext,\n command: 'snapshots' | 'wal',\n output: string,\n): Date[] {\n const times: Date[] = [];\n for (const line of output.split('\\n')) {\n const cols = line.trim().split(wsRe);\n const created = cols.at(-1);\n if (\n cols.length < 2 ||\n created === undefined ||\n created === 'created' /* header row */\n ) {\n continue;\n }\n const time = new Date(created);\n if (Number.isNaN(time.getTime())) {\n lc.warn?.(`unexpected line in litestream ${command} output: ${line}`);\n continue;\n }\n times.push(time);\n }\n return times;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"commands.js","names":["v","v.parse"],"sources":["../../../../../../zero-cache/src/services/litestream/commands.ts"],"sourcesContent":["import type {ChildProcess} from 'node:child_process';\nimport {spawn} from 'node:child_process';\nimport {existsSync, readdirSync, rmSync, statSync} from 'node:fs';\nimport {basename, dirname, join} from 'node:path';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport {must} from '../../../../shared/src/must.ts';\nimport {resolver} from '../../../../shared/src/resolver.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {type LitestreamConfig} from '../../config/normalize.ts';\nimport {deleteLiteDB} from '../../db/delete-lite-db.ts';\nimport {\n isSQLiteCorruption,\n logSQLiteCorruptionDiagnostics,\n} from '../../db/sqlite-corruption.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport {deleteChangeLogDB} from '../replicator/change-log-db.ts';\nimport {getSubscriptionState} from '../replicator/schema/replication-state.ts';\nimport {litestreamSocketPath} from './litestream-controller.ts';\nimport {\n litestreamBackupListDuration,\n litestreamBackupMetricAttrs,\n litestreamBackupProcessDuration,\n litestreamBackupProcessMetricAttrs,\n litestreamBackupProcessRuns,\n litestreamRestoreAttempts,\n litestreamRestoredDbBytes,\n litestreamRestoreMetricAttrs,\n litestreamRestoreProcessDuration,\n litestreamRestoreValidationDuration,\n type LitestreamRole,\n} from './metrics.ts';\n\nexport type ReplicaConstraints = {\n replicaVersion: string;\n minWatermark: string;\n};\n\nexport type RestoreResult =\n | 'success'\n | 'no_backup'\n | 'invalid_replica'\n | 'error';\n\nexport type RestoreAttempt = {\n restored: boolean;\n backupURL: string | undefined;\n result: RestoreResult;\n /**\n * Set when the restore forked the backup (see {@link ForkBackup}), which\n * then continues from the restored replica's WAL. The WAL is kept when the\n * last connection closes (see `Database.persistWal()`).\n */\n forked?: boolean | undefined;\n};\n\n/**\n * Prepares the restored replica to continue from a forked backup\n * instead of starting a new lineage with a full snapshot.\n */\nexport type ForkBackup = {\n /** The restore also forks its backup into the (new, empty) one at this URL. */\n url: string;\n};\n\nconst MAX_LOGGED_RESTORE_DIRECTORY_ENTRIES = 100;\nconst LEGACY_RESTORE_WAL_INDEX = /^[0-9a-f]{8}$/;\n\n/**\n * The effective backup URL: the `backupURLOverride` if present (i.e. a\n * lineage-specific location computed by the replication-manager), or\n * otherwise the configured `backupURL`.\n */\nexport function backupDestinationURL(\n config: Pick<LitestreamConfig, 'backupURL' | 'backupURLOverride'>,\n): string | undefined {\n return config.backupURLOverride ?? config.backupURL;\n}\n\nfunction getLitestream(\n mode: 'restore' | 'replicate',\n config: LitestreamConfig,\n replicaFile: string,\n logLevelOverride?: LogLevel,\n): {\n litestream: string;\n env: NodeJS.ProcessEnv;\n} {\n const {\n executable,\n executableV5,\n restoreUsingV5,\n backupUsingV5,\n logLevel,\n configPath,\n configPathV5,\n endpoint,\n region,\n port,\n checkpointThresholdMB,\n // Note: This assumes the default page size. If page size configuration\n // is added, this computation must be adjusted accordingly.\n minCheckpointPageCount = checkpointThresholdMB * 256, // 1 MiB / 4 KiB page size\n maxCheckpointPageCount = minCheckpointPageCount * 10,\n forceCheckpointThresholdMB,\n incrementalBackupIntervalMinutes,\n incrementalBackupIntervalSeconds,\n snapshotBackupIntervalHours,\n multipartConcurrency,\n multipartSize,\n } = config;\n\n const v5 =\n (mode === 'restore' && restoreUsingV5) ||\n (mode === 'replicate' && backupUsingV5);\n const litestream =\n (v5 ? executableV5 : executable) ??\n must(executable, `Missing --litestream-executable`);\n const litestreamConfig = v5 ? configPathV5 : configPath;\n // Disable truncate-page-n if forced checkpoints are enabled,\n // and otherwise use litestream's default.\n const truncatePageN = forceCheckpointThresholdMB ? -1 : 121359;\n\n return {\n litestream,\n env: {\n ...process.env,\n ['ZERO_REPLICA_FILE']: replicaFile,\n ['ZERO_LITESTREAM_BACKUP_URL']: must(backupDestinationURL(config)),\n ['ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT']: String(\n minCheckpointPageCount,\n ),\n ['ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT']: String(\n maxCheckpointPageCount,\n ),\n ['ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES']: String(\n incrementalBackupIntervalMinutes, // v3 only\n ),\n ['ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_SECONDS']: String(\n incrementalBackupIntervalSeconds, // v5 only\n ),\n // v5 only\n ['ZERO_LITESTREAM_MONITOR_INTERVAL_SECONDS']: String(\n // zero-cache explicitly requests syncs at the incremental backup\n // interval (see LitestreamSyncRequester), and litestream's\n // own monitor becomes a backstop that keeps backups and checkpoints\n // progressing if zero-cache stops requesting them.\n 2 * incrementalBackupIntervalSeconds,\n ),\n ['ZERO_LITESTREAM_TRUNCATE_PAGE_N']: String(truncatePageN),\n ['ZERO_LITESTREAM_LOG_LEVEL']: logLevelOverride ?? logLevel,\n ['ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_HOURS']: String(\n snapshotBackupIntervalHours,\n ),\n ['ZERO_LITESTREAM_SNAPSHOT_RETENTION_INTERVAL_HOURS']: String(\n snapshotBackupIntervalHours + 6, // delete old snapshots after 6 hours\n ),\n ['ZERO_LITESTREAM_MULTIPART_CONCURRENCY']: String(multipartConcurrency),\n ['ZERO_LITESTREAM_MULTIPART_SIZE']: String(multipartSize),\n ['ZERO_LOG_FORMAT']: 'json',\n ['ZERO_LITESTREAM_SOCKET_PATH']: litestreamSocketPath(replicaFile),\n ['LITESTREAM_CONFIG']: litestreamConfig,\n ['LITESTREAM_PORT']: String(port),\n ...(endpoint ? {['ZERO_LITESTREAM_ENDPOINT']: endpoint} : {}),\n ...(region ? {['ZERO_LITESTREAM_REGION']: region} : {}),\n ['LITESTREAM_COMPACTION_SERIALIZE']: 'false',\n },\n };\n}\n\nexport async function tryRestore(\n lc: LogContext,\n config: LitestreamConfig,\n replicaFile: string,\n replicaConstraints: ReplicaConstraints | undefined,\n role: LitestreamRole,\n signal?: AbortSignal,\n forkBackup?: ForkBackup | undefined,\n): Promise<RestoreAttempt> {\n const {reusedExisting, ...attempt} = await restoreOnce(\n lc,\n config,\n replicaFile,\n replicaConstraints,\n role,\n signal,\n forkBackup,\n );\n if (attempt.result === 'invalid_replica' && reusedExisting) {\n // `-if-db-not-exists` made the restore a no-op, and the existing local\n // replica (e.g. one left behind by a failed initial sync of a different\n // generation) was invalid. It has since been deleted, so restore from the\n // backup.\n const {reusedExisting: _, ...retry} = await restoreOnce(\n lc,\n config,\n replicaFile,\n replicaConstraints,\n role,\n signal,\n forkBackup,\n );\n return retry;\n }\n return attempt;\n}\n\nasync function restoreOnce(\n lc: LogContext,\n config: LitestreamConfig,\n replicaFile: string,\n replicaConstraints: ReplicaConstraints | undefined,\n role: LitestreamRole,\n signal: AbortSignal | undefined,\n forkBackup: ForkBackup | undefined,\n): Promise<RestoreAttempt & {reusedExisting: boolean}> {\n const {backupUsingV5} = config;\n const backupURL = backupDestinationURL(config);\n if (forkBackup && !backupUsingV5) {\n forkBackup = undefined; // Ignore forkBackup if v5 is disabled.\n }\n const attrs = litestreamRestoreMetricAttrs(config, role, backupURL);\n let result: RestoreResult = 'error';\n try {\n logRestoreDirectoryContents(lc, replicaFile, 'before-temp-cleanup');\n deleteRestoreTempFiles(replicaFile);\n const reusedExisting = existsSync(replicaFile);\n if (!reusedExisting) {\n // The local litestream state (and anything recorded in it) describes a\n // replica that no longer exists. Discard it before the restore, which\n // (if it seeds a backup) writes the state that belongs to the new one.\n deleteLitestreamMetaDir(replicaFile);\n }\n const {litestream, env} = getLitestream(\n 'restore',\n config,\n replicaFile,\n 'debug', // Include all output from `litestream restore`, as it's minimal.\n );\n const {\n restoreParallelism: parallelism,\n multipartConcurrency,\n multipartSize,\n } = config;\n lc.info?.(`starting litestream restore`, {\n restoreParallelism: parallelism,\n multipartConcurrency,\n multipartSize,\n });\n // Pipe (rather than inherit) litestream's stdout/stderr so that its own\n // `\"level\":\"ERROR\"` output on a failed restore does not go straight to the\n // pod's stdout — where a log-scraper alert would page on it — before our\n // code has decided how to handle the failure. The captured output is\n // included in the thrown error so the caller can log the final outcome.\n // See INC-961.\n const proc = spawn(\n litestream,\n [\n 'restore',\n '-if-db-not-exists',\n '-if-replica-exists',\n '-parallelism',\n String(parallelism),\n ...(forkBackup ? ['-json', '-fork-to-url', forkBackup.url] : []),\n replicaFile,\n ],\n {env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, signal},\n );\n let stdout = '';\n let stderr = '';\n proc.stdout.setEncoding('utf-8');\n proc.stderr.setEncoding('utf-8');\n // Output AND capture the stdout and stderr, the former for observability\n // and the latter for error handling.\n proc.stdout.on('data', chunk => {\n process.stdout.write(chunk);\n stdout += chunk;\n });\n proc.stderr.on('data', chunk => {\n process.stderr.write(chunk);\n stderr += chunk;\n });\n const {promise, resolve, reject} = resolver();\n proc.on('error', reject);\n proc.on('close', (code, signal) => {\n if (signal) {\n reject(`litestream killed with ${signal}`);\n } else if (code !== 0) {\n reject(`litestream exited with code ${code}`);\n } else {\n resolve();\n }\n });\n const processStart = performance.now();\n try {\n await promise;\n litestreamRestoreProcessDuration().recordMs(\n performance.now() - processStart,\n {...attrs, result: 'success'},\n );\n } catch (e) {\n litestreamRestoreProcessDuration().recordMs(\n performance.now() - processStart,\n {...attrs, result: 'error'},\n );\n const output = [stdout, stderr]\n .map(value => value.trim())\n .filter(Boolean)\n .join('\\n');\n throw new Error(output ? `${String(e)}\\n${output}` : String(e), {\n cause: e,\n });\n }\n if (!existsSync(replicaFile)) {\n result = 'no_backup';\n return {restored: false, backupURL, result, reusedExisting};\n }\n const validationStart = performance.now();\n const db = openValidReplica(lc, replicaFile, replicaConstraints);\n litestreamRestoreValidationDuration().recordMs(\n performance.now() - validationStart,\n {...attrs, result: db ? 'success' : 'invalid_replica'},\n );\n if (!db) {\n result = 'invalid_replica';\n lc.info?.(`Deleting local replica and retrying restore`);\n deleteLiteDB(replicaFile);\n deleteChangeLogDB(replicaFile);\n // The litestream state (e.g. the anchor of a seeded backup) describes\n // the deleted replica.\n deleteLitestreamMetaDir(replicaFile);\n return {restored: false, backupURL, result, reusedExisting};\n }\n try {\n result = 'success';\n if (!reusedExisting) {\n // This restore materialized the replica file, so any change log beside it\n // was written against a replica that is no longer there. Reconciliation\n // is the safety net — a restored replica keeps its replicaVersion, so the\n // rule falls through to truncate-or-reseed on head mismatch — but the log\n // is a cache and deleting it here is the explicit statement that nothing\n // in it survives the restore.\n //\n // A restore that reuses an existing replica (`-if-db-not-exists` made it\n // a no-op) deliberately keeps the log: that is the process-restart path,\n // where the log is still the one written beside this exact replica and\n // discarding it would cost every reconnecting subscriber a `too-old`.\n deleteChangeLogDB(replicaFile);\n litestreamRestoredDbBytes().add(statSync(replicaFile).size, {\n ...attrs,\n result: 'success',\n });\n }\n const forked =\n forkBackup !== undefined &&\n !reusedExisting &&\n restoreForked(lc, stdout);\n return {restored: true, backupURL, result, reusedExisting, forked};\n } finally {\n db.close();\n }\n } finally {\n try {\n deleteRestoreTempFiles(replicaFile);\n } catch (e) {\n lc.warn?.('Unable to clean up Litestream restore temporary files', {\n replicaFile,\n error: String(e),\n });\n }\n logRestoreDirectoryContents(lc, replicaFile, 'after-temp-cleanup');\n litestreamRestoreAttempts().add(1, {...attrs, result});\n }\n}\n\n// From litestream/cmd/litestream/restore.go\nconst restoreForkOutputSchema = v.object({\n fork: v\n .object({\n forked: v.boolean(),\n txid: v.string().optional(),\n })\n .optional(),\n});\n\ntype RestoreForkOutput = v.Infer<typeof restoreForkOutputSchema>;\n\n/**\n * Whether the `-json` output of `litestream restore -fork-to-url` reports that\n * the backup was forked. A restore plan that cannot fork the backup is\n * restored without forking.\n */\nfunction restoreForked(lc: LogContext, stdout: string): boolean {\n const start = stdout.indexOf('{');\n const end = stdout.lastIndexOf('}');\n if (start < 0 || end < start) {\n lc.warn?.('litestream restore did not report a fork result');\n return false;\n }\n let output: RestoreForkOutput;\n try {\n output = v.parse(\n JSON.parse(stdout.slice(start, end + 1)),\n restoreForkOutputSchema,\n 'passthrough',\n );\n } catch (e) {\n lc.warn?.('unable to parse the litestream restore result', e);\n return false;\n }\n const forked = output.fork?.forked === true;\n lc.info?.(\n forked\n ? `forked the backup at txid ${output.fork?.txid}`\n : 'restored without forking the backup',\n );\n return forked;\n}\n\n/**\n * Deletes litestream's local state for `replicaFile` (its default meta\n * directory, `.<file>-litestream`).\n */\nexport function deleteLitestreamMetaDir(replicaFile: string) {\n rmSync(join(dirname(replicaFile), `.${basename(replicaFile)}-litestream`), {\n recursive: true,\n force: true,\n });\n}\n\nfunction deleteRestoreTempFiles(replicaFile: string) {\n const temporaryReplicaFile = `${replicaFile}.tmp`;\n deleteLiteDB(temporaryReplicaFile);\n\n // rocicorp/litestream zero@v0.0.9 downloads WAL indexes in parallel to\n // `<output>.tmp-<8 lowercase hex digits>-wal` before applying them.\n const directory = dirname(temporaryReplicaFile);\n if (!existsSync(directory)) {\n return;\n }\n const prefix = `${basename(temporaryReplicaFile)}-`;\n for (const name of readdirSync(directory)) {\n if (!name.startsWith(prefix) || !name.endsWith('-wal')) {\n continue;\n }\n const index = name.slice(prefix.length, -'-wal'.length);\n if (LEGACY_RESTORE_WAL_INDEX.test(index)) {\n rmSync(join(directory, name), {force: true});\n }\n }\n}\n\nfunction logRestoreDirectoryContents(\n lc: LogContext,\n replicaFile: string,\n phase: 'before-temp-cleanup' | 'after-temp-cleanup',\n) {\n const directory = dirname(replicaFile);\n try {\n const names = readdirSync(directory).sort();\n const entries = names\n .slice(0, MAX_LOGGED_RESTORE_DIRECTORY_ENTRIES)\n .map(name => ({name, sizeBytes: statSync(join(directory, name)).size}));\n lc.info?.('Litestream restore directory contents', {\n phase,\n directory,\n replicaFile,\n entryCount: names.length,\n omittedEntryCount: Math.max(\n 0,\n names.length - MAX_LOGGED_RESTORE_DIRECTORY_ENTRIES,\n ),\n entries,\n });\n } catch (e) {\n lc.warn?.('Unable to inspect Litestream restore directory', {\n phase,\n directory,\n replicaFile,\n error: String(e),\n });\n }\n}\n\n/**\n * Returns the replica version and watermark of the local replica file, or\n * `undefined` if it does not exist or cannot be read.\n */\nexport function readLocalReplica(\n lc: LogContext,\n replica: string,\n): {replicaVersion: string; watermark: string} | undefined {\n if (!existsSync(replica)) {\n return undefined;\n }\n let db: Database | undefined;\n try {\n db = new Database(lc, replica, {readonly: true});\n const {replicaVersion, watermark} = getSubscriptionState(\n new StatementRunner(db),\n );\n return {replicaVersion, watermark};\n } catch (e) {\n lc.warn?.(`unable to read the subscription state of ${replica}`, e);\n return undefined;\n } finally {\n db?.close();\n }\n}\n\n/**\n * Opens and validates the restored replica. Returns the open connection if\n * the replica is valid, which the caller must close; otherwise closes it and\n * returns `undefined`.\n */\nfunction openValidReplica(\n lc: LogContext,\n replica: string,\n constraints: ReplicaConstraints | undefined,\n): Database | undefined {\n let db: Database | undefined;\n try {\n // Note: Open the database and read the subscription state as a\n // sanity / corruption check, even if there are no constraints.\n db = new Database(lc, replica);\n const {replicaVersion, watermark} = getSubscriptionState(\n new StatementRunner(db),\n );\n if (constraints) {\n if (replicaVersion !== constraints.replicaVersion) {\n lc.warn?.(\n `Local replica version ${replicaVersion} does not match expected replicaVersion ${constraints.replicaVersion}`,\n constraints,\n );\n return undefined;\n }\n if (watermark < constraints.minWatermark) {\n lc.warn?.(\n `Local replica watermark ${watermark} is earlier than minWatermark ${constraints.minWatermark}`,\n );\n return undefined;\n }\n lc.info?.(\n `Local replica at version ${replicaVersion} and watermark ${watermark} is compatible`,\n constraints,\n );\n }\n const valid = db;\n db = undefined; // owned by the caller\n return valid;\n } catch (e) {\n if (isSQLiteCorruption(e)) {\n logSQLiteCorruptionDiagnostics(lc, 'restored replica', replica, e);\n }\n lc.error?.('Error while validating restored replica', e);\n return undefined;\n } finally {\n db?.close();\n }\n}\n\nexport function startReplicaBackupProcess(\n lc: LogContext,\n config: LitestreamConfig,\n replicaFile: string,\n): ChildProcess {\n const {litestream, env} = getLitestream('replicate', config, replicaFile);\n const attrs = litestreamBackupProcessMetricAttrs(config);\n lc.info?.(`starting litestream backup to ${backupDestinationURL(config)}`);\n const start = performance.now();\n const proc = spawn(litestream, ['replicate'], {\n env,\n stdio: 'inherit',\n windowsHide: true,\n });\n let recorded = false;\n const record = (result: 'success' | 'error' | 'stopped') => {\n if (recorded) {\n return;\n }\n recorded = true;\n const labels = {...attrs, result};\n litestreamBackupProcessRuns().add(1, labels);\n litestreamBackupProcessDuration().recordMs(\n performance.now() - start,\n labels,\n );\n };\n proc.on('error', e => {\n lc.warn?.(`litestream backup process error`, e);\n record('error');\n });\n proc.on('close', (code, signal) => {\n if (signal) {\n lc.info?.(`litestream backup process stopped`, {signal});\n record('stopped');\n } else if (code === 0) {\n record('success');\n } else {\n lc.warn?.(`litestream backup process exited with code ${code}`);\n record('error');\n }\n });\n return proc;\n}\n\n// Listing the backup state requires a few S3 LIST requests, which should\n// normally complete well within this timeout.\nconst LIST_BACKUP_TIMEOUT_MS = 30_000;\n\nconst wsRe = /\\s+/;\n\n/**\n * Returns the time of the most recent object (snapshot or WAL segment)\n * actually uploaded to the backup replica destination, as listed by the\n * bundled litestream CLI (`litestream snapshots` / `litestream wal`).\n *\n * This queries the replica destination (e.g. S3) directly, and thus serves\n * as a source of truth for backup durability. This is in contrast to the\n * `litestream_replica_progress` metric, which is exported when litestream\n * *believes* an upload has succeeded, and has been observed to advance even\n * when nothing is actually written to the destination.\n *\n * Rejects if the backup state cannot be determined (spawn error, non-zero\n * exit, timeout, or empty/unparseable listing).\n */\nexport async function getLastBackupTime(\n lc: LogContext,\n config: LitestreamConfig,\n replicaFile: string,\n): Promise<Date> {\n const [snapshots, wal] = await Promise.all([\n listBackupCreatedTimes(lc, config, replicaFile, 'snapshots'),\n listBackupCreatedTimes(lc, config, replicaFile, 'wal'),\n ]);\n const times = [...snapshots, ...wal];\n if (times.length === 0) {\n // Note: the litestream CLI exits with code 0 and logs listing errors\n // (e.g. S3 failures) to stderr, so an empty listing cannot be\n // distinguished from a failed one. Since a valid backup always contains\n // at least one snapshot, an empty listing is treated as a failure.\n throw new Error(\n `no snapshots or WAL segments listed at ${backupDestinationURL(config)}`,\n );\n }\n return new Date(Math.max(...times.map(time => time.getTime())));\n}\n\n/**\n * Runs `litestream <snapshots|wal> <replica-file>` with the same config /\n * environment used by the `litestream replicate` process (so that the\n * backupURL, endpoint, region, and credentials are identical), and parses\n * the `created` column (RFC3339) of the tab-formatted output, e.g.:\n *\n * ```\n * replica generation index size created\n * s3 1862f44967b3863f 0 4546445 2026-06-10T01:11:32Z\n * ```\n */\nasync function listBackupCreatedTimes(\n lc: LogContext,\n config: LitestreamConfig,\n replicaFile: string,\n command: 'snapshots' | 'wal',\n): Promise<Date[]> {\n const start = performance.now();\n let result: 'success' | 'empty' | 'timeout' | 'error' = 'error';\n const {litestream, env} = getLitestream('replicate', config, replicaFile);\n const proc = spawn(litestream, [command, replicaFile], {\n env,\n stdio: ['ignore', 'pipe', 'inherit'],\n windowsHide: true,\n });\n const {promise, resolve, reject} = resolver<string>();\n let stdout = '';\n proc.stdout.setEncoding('utf-8');\n proc.stdout.on('data', chunk => (stdout += chunk));\n proc.on('error', reject);\n proc.on('close', (code, signal) => {\n if (signal) {\n reject(new Error(`litestream ${command} killed with ${signal}`));\n } else if (code !== 0) {\n reject(new Error(`litestream ${command} exited with code ${code}`));\n } else {\n resolve(stdout);\n }\n });\n const timeout = setTimeout(() => {\n result = 'timeout';\n reject(new Error(`timed out listing backup state (litestream ${command})`));\n proc.kill('SIGKILL');\n }, LIST_BACKUP_TIMEOUT_MS);\n\n try {\n const output = await promise;\n const times = parseBackupCreatedTimes(lc, command, output);\n result = times.length ? 'success' : 'empty';\n return times;\n } finally {\n clearTimeout(timeout);\n litestreamBackupListDuration().recordMs(performance.now() - start, {\n ...litestreamBackupMetricAttrs(config),\n command,\n result,\n });\n }\n}\n\n/**\n * Parses the `created` column (the last, RFC3339-formatted column) from the\n * tab-formatted output of `litestream snapshots` / `litestream wal`. The\n * header row and any unparseable lines are skipped.\n *\n * Exported for testing.\n */\nexport function parseBackupCreatedTimes(\n lc: LogContext,\n command: 'snapshots' | 'wal',\n output: string,\n): Date[] {\n const times: Date[] = [];\n for (const line of output.split('\\n')) {\n const cols = line.trim().split(wsRe);\n const created = cols.at(-1);\n if (\n cols.length < 2 ||\n created === undefined ||\n created === 'created' /* header row */\n ) {\n continue;\n }\n const time = new Date(created);\n if (Number.isNaN(time.getTime())) {\n lc.warn?.(`unexpected line in litestream ${command} output: ${line}`);\n continue;\n }\n times.push(time);\n }\n return times;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","names":[],"sources":["../../../../../../zero-cache/src/services/litestream/metrics.ts"],"sourcesContent":["import type {LitestreamConfig} from '../../config/normalize.ts';\nimport {\n getOrCreateCounter,\n getOrCreateHistogram,\n LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n} from '../../observability/metrics.ts';\n\nexport type LitestreamRole = 'replication_manager' | 'view_syncer';\nexport type LitestreamVersion = 'legacy' | 'v5';\n\nexport type LitestreamMetricAttrs = {\n role: LitestreamRole;\n backup_scheme: string;\n litestream: LitestreamVersion;\n};\n\ntype LitestreamMultipartMetricAttrs = {\n multipart_concurrency: number;\n multipart_size_mib: number;\n};\n\nexport function litestreamRestoreMetricAttrs(\n config: LitestreamConfig,\n role: LitestreamRole,\n backupURL = config.backupURL,\n): LitestreamMetricAttrs & LitestreamMultipartMetricAttrs {\n const {executable, executableV5, restoreUsingV5} = config;\n const selectedExecutable =\n (restoreUsingV5 ? executableV5 : executable) ?? executable;\n return {\n role,\n backup_scheme: litestreamBackupScheme(backupURL),\n litestream:\n executableV5 !== undefined && selectedExecutable === executableV5\n ? 'v5'\n : 'legacy',\n ...litestreamMultipartMetricAttrs(config),\n };\n}\n\nexport function litestreamBackupMetricAttrs(\n config: LitestreamConfig,\n): LitestreamMetricAttrs {\n return {\n role: 'replication_manager',\n backup_scheme: litestreamBackupScheme(config.backupURL),\n litestream:\n config.executableV5 !== undefined &&\n config.executable === config.executableV5\n ? 'v5'\n : 'legacy',\n };\n}\n\nexport function litestreamBackupProcessMetricAttrs(\n config: LitestreamConfig,\n): LitestreamMetricAttrs & LitestreamMultipartMetricAttrs {\n return {\n ...litestreamBackupMetricAttrs(config),\n ...litestreamMultipartMetricAttrs(config),\n };\n}\n\nexport function litestreamMonitorMetricAttrs(\n backupURL: string,\n litestream: LitestreamVersion,\n role: LitestreamRole,\n): LitestreamMetricAttrs {\n return {\n role,\n backup_scheme: litestreamBackupScheme(backupURL),\n litestream,\n };\n}\n\nfunction litestreamBackupScheme(backupURL: string | undefined): string {\n if (!backupURL) {\n return 'unknown';\n }\n try {\n const protocol = new URL(backupURL).protocol;\n return protocol.endsWith(':') ? protocol.slice(0, -1) : protocol;\n } catch {\n return 'unknown';\n }\n}\n\nfunction litestreamMultipartMetricAttrs(\n config: LitestreamConfig,\n): LitestreamMultipartMetricAttrs {\n return {\n multipart_concurrency: config.multipartConcurrency,\n multipart_size_mib: Math.round(config.multipartSize / 1024 / 1024),\n };\n}\n\nexport function litestreamRestoreRuns() {\n return getOrCreateCounter(\n 'replica',\n 'litestream.restore.runs',\n 'Litestream restore runs, labeled by result.',\n );\n}\n\nexport function litestreamRestoreAttempts() {\n return getOrCreateCounter(\n 'replica',\n 'litestream.restore.attempts',\n 'Litestream restore subprocess attempts, labeled by result.',\n );\n}\n\nexport function litestreamRestoredDbBytes() {\n return getOrCreateCounter('replica', 'litestream.restore.db_bytes', {\n description:\n 'SQLite database bytes restored by successful litestream restores.',\n unit: 'bytes',\n });\n}\n\nexport function litestreamBackupProcessRuns() {\n return getOrCreateCounter(\n 'replica',\n 'litestream.backup.process_runs',\n 'Litestream backup process exits, labeled by result.',\n );\n}\n\nexport function litestreamRestoreDuration() {\n return litestreamDurationHistogram(\n 'litestream.restore.duration',\n 'Wall-clock duration of a litestream restore run, labeled by result.',\n );\n}\n\nexport function litestreamRestoreWaitDuration() {\n return litestreamDurationHistogram(\n 'litestream.restore.wait_duration',\n 'Time spent waiting for the replication-manager snapshot status before restoring.',\n );\n}\n\nexport function litestreamRestoreProcessDuration() {\n return litestreamDurationHistogram(\n 'litestream.restore.process_duration',\n 'Wall-clock duration of the litestream restore subprocess.',\n );\n}\n\nexport function litestreamRestoreValidationDuration() {\n return litestreamDurationHistogram(\n 'litestream.restore.validation_duration',\n 'Time spent validating a restored replica database.',\n );\n}\n\nexport function litestreamBackupProcessDuration() {\n return litestreamDurationHistogram(\n 'litestream.backup.process_duration',\n 'Runtime duration of the litestream backup subprocess before it exits.',\n );\n}\n\nexport function litestreamBackupListDuration() {\n return litestreamDurationHistogram(\n 'litestream.backup.list_duration',\n 'Duration of litestream backup destination listing commands.',\n );\n}\n\nexport function litestreamBackupVerificationDuration() {\n return litestreamDurationHistogram(\n 'litestream.backup.verification_duration',\n 'Duration of verifying the actual backup state in the backup destination.',\n );\n}\n\nexport function litestreamSnapshotReservationDuration() {\n return litestreamDurationHistogram(\n 'litestream.snapshot.reservation_duration',\n 'Duration of a snapshot reservation while a view-syncer restores and subscribes.',\n );\n}\n\nexport function litestreamSnapshotReservationConfirmDuration() {\n return litestreamDurationHistogram(\n 'litestream.snapshot.reservation_confirm_duration',\n 'Time from opening a snapshot reservation to confirming its change-log ' +\n 'bounds, by the source those bounds came from.',\n );\n}\n\nfunction litestreamDurationHistogram(name: string, description: string) {\n return getOrCreateHistogram('replica', name, {\n description,\n unit: 's',\n bucketBoundaries: LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n"],"mappings":";;AAqBA,SAAgB,6BACd,QACA,MACA,YAAY,OAAO,WACqC;CACxD,MAAM,EAAC,YAAY,cAAc,mBAAkB;CACnD,MAAM,sBACH,iBAAiB,eAAe,eAAe;CAClD,OAAO;EACL;EACA,eAAe,uBAAuB,SAAS;EAC/C,YACE,iBAAiB,KAAA,KAAa,uBAAuB,eACjD,OACA;EACN,GAAG,+BAA+B,MAAM;CAC1C;AACF;AAEA,SAAgB,4BACd,QACuB;CACvB,OAAO;EACL,MAAM;EACN,eAAe,uBAAuB,OAAO,SAAS;EACtD,YACE,OAAO,iBAAiB,KAAA,KACxB,OAAO,eAAe,OAAO,eACzB,OACA;CACR;AACF;AAEA,SAAgB,mCACd,QACwD;CACxD,OAAO;EACL,GAAG,4BAA4B,MAAM;EACrC,GAAG,+BAA+B,MAAM;CAC1C;AACF;AAEA,SAAgB,6BACd,WACA,YACA,MACuB;CACvB,OAAO;EACL;EACA,eAAe,uBAAuB,SAAS;EAC/C;CACF;AACF;AAEA,SAAS,uBAAuB,WAAuC;CACrE,IAAI,CAAC,WACH,OAAO;CAET,IAAI;EACF,MAAM,WAAW,IAAI,IAAI,SAAS,CAAC,CAAC;EACpC,OAAO,SAAS,SAAS,GAAG,IAAI,SAAS,MAAM,GAAG,EAAE,IAAI;CAC1D,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,+BACP,QACgC;CAChC,OAAO;EACL,uBAAuB,OAAO;EAC9B,oBAAoB,KAAK,MAAM,OAAO,gBAAgB,OAAO,IAAI;CACnE;AACF;AAEA,SAAgB,wBAAwB;CACtC,OAAO,mBACL,WACA,2BACA,6CACF;AACF;AAEA,SAAgB,4BAA4B;CAC1C,OAAO,mBACL,WACA,+BACA,4DACF;AACF;AAEA,SAAgB,4BAA4B;CAC1C,OAAO,mBAAmB,WAAW,+BAA+B;EAClE,aACE;EACF,MAAM;CACR,CAAC;AACH;AAEA,SAAgB,8BAA8B;CAC5C,OAAO,mBACL,WACA,kCACA,qDACF;AACF;AAEA,SAAgB,4BAA4B;CAC1C,OAAO,4BACL,+BACA,qEACF;AACF;AASA,SAAgB,mCAAmC;CACjD,OAAO,4BACL,uCACA,2DACF;AACF;AAEA,SAAgB,sCAAsC;CACpD,OAAO,4BACL,0CACA,oDACF;AACF;AAEA,SAAgB,kCAAkC;CAChD,OAAO,4BACL,sCACA,uEACF;AACF;AAEA,SAAgB,+BAA+B;CAC7C,OAAO,4BACL,mCACA,6DACF;AACF;AAEA,SAAgB,uCAAuC;CACrD,OAAO,4BACL,2CACA,0EACF;AACF;AAEA,SAAgB,wCAAwC;CACtD,OAAO,4BACL,4CACA,iFACF;AACF;AAEA,SAAgB,+CAA+C;CAC7D,OAAO,4BACL,oDACA,qHAEF;AACF;AAEA,SAAS,4BAA4B,MAAc,aAAqB;CACtE,OAAO,qBAAqB,WAAW,MAAM;EAC3C;EACA,MAAM;EACN,kBAAkB;CACpB,CAAC;AACH"}
1
+ {"version":3,"file":"metrics.js","names":[],"sources":["../../../../../../zero-cache/src/services/litestream/metrics.ts"],"sourcesContent":["import type {LitestreamConfig} from '../../config/normalize.ts';\nimport {\n getOrCreateCounter,\n getOrCreateHistogram,\n LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n} from '../../observability/metrics.ts';\n\nexport type LitestreamRole = 'replication_manager' | 'view_syncer';\nexport type LitestreamVersion = 'legacy' | 'v5';\n\nexport type LitestreamMetricAttrs = {\n role: LitestreamRole;\n backup_scheme: string;\n litestream: LitestreamVersion;\n};\n\ntype LitestreamMultipartMetricAttrs = {\n multipart_concurrency: number;\n multipart_size_mib: number;\n};\n\nexport function litestreamRestoreMetricAttrs(\n config: LitestreamConfig,\n role: LitestreamRole,\n backupURL = config.backupURL,\n): LitestreamMetricAttrs & LitestreamMultipartMetricAttrs {\n const {executable, executableV5, restoreUsingV5} = config;\n const selectedExecutable =\n (restoreUsingV5 ? executableV5 : executable) ?? executable;\n return {\n role,\n backup_scheme: litestreamBackupScheme(backupURL),\n litestream:\n executableV5 !== undefined && selectedExecutable === executableV5\n ? 'v5'\n : 'legacy',\n ...litestreamMultipartMetricAttrs(config),\n };\n}\n\nexport function litestreamBackupMetricAttrs(\n config: LitestreamConfig,\n): LitestreamMetricAttrs {\n return {\n role: 'replication_manager',\n backup_scheme: litestreamBackupScheme(config.backupURL),\n litestream:\n config.executableV5 !== undefined &&\n config.executable === config.executableV5\n ? 'v5'\n : 'legacy',\n };\n}\n\nexport function litestreamBackupProcessMetricAttrs(\n config: LitestreamConfig,\n): LitestreamMetricAttrs & LitestreamMultipartMetricAttrs {\n return {\n ...litestreamBackupMetricAttrs(config),\n ...litestreamMultipartMetricAttrs(config),\n };\n}\n\nexport function litestreamMonitorMetricAttrs(\n backupURL: string,\n litestream: LitestreamVersion,\n role: LitestreamRole,\n): LitestreamMetricAttrs {\n return {\n role,\n backup_scheme: litestreamBackupScheme(backupURL),\n litestream,\n };\n}\n\nfunction litestreamBackupScheme(backupURL: string | undefined): string {\n if (!backupURL) {\n return 'unknown';\n }\n try {\n const protocol = new URL(backupURL).protocol;\n return protocol.endsWith(':') ? protocol.slice(0, -1) : protocol;\n } catch {\n return 'unknown';\n }\n}\n\nfunction litestreamMultipartMetricAttrs(\n config: LitestreamConfig,\n): LitestreamMultipartMetricAttrs {\n return {\n multipart_concurrency: config.multipartConcurrency,\n multipart_size_mib: Math.round(config.multipartSize / 1024 / 1024),\n };\n}\n\nexport function litestreamRestoreRuns() {\n return getOrCreateCounter(\n 'replica',\n 'litestream.restore.runs',\n 'Litestream restore runs, labeled by result.',\n );\n}\n\nexport function litestreamRestoreAttempts() {\n return getOrCreateCounter(\n 'replica',\n 'litestream.restore.attempts',\n 'Litestream restore subprocess attempts, labeled by result.',\n );\n}\n\nexport function litestreamRestoredDbBytes() {\n return getOrCreateCounter('replica', 'litestream.restore.db_bytes', {\n description:\n 'SQLite database bytes restored by successful litestream restores.',\n unit: 'bytes',\n });\n}\n\nexport function litestreamBackupProcessRuns() {\n return getOrCreateCounter(\n 'replica',\n 'litestream.backup.process_runs',\n 'Litestream backup process exits, labeled by result.',\n );\n}\n\nexport function litestreamRestoreDuration() {\n return litestreamDurationHistogram(\n 'litestream.restore.duration',\n 'Wall-clock duration of a litestream restore run, labeled by result.',\n );\n}\n\nexport function litestreamRestoreProcessDuration() {\n return litestreamDurationHistogram(\n 'litestream.restore.process_duration',\n 'Wall-clock duration of the litestream restore subprocess.',\n );\n}\n\nexport function litestreamRestoreValidationDuration() {\n return litestreamDurationHistogram(\n 'litestream.restore.validation_duration',\n 'Time spent validating a restored replica database.',\n );\n}\n\nexport function litestreamBackupProcessDuration() {\n return litestreamDurationHistogram(\n 'litestream.backup.process_duration',\n 'Runtime duration of the litestream backup subprocess before it exits.',\n );\n}\n\nexport function litestreamBackupListDuration() {\n return litestreamDurationHistogram(\n 'litestream.backup.list_duration',\n 'Duration of litestream backup destination listing commands.',\n );\n}\n\nexport function litestreamBackupVerificationDuration() {\n return litestreamDurationHistogram(\n 'litestream.backup.verification_duration',\n 'Duration of verifying the actual backup state in the backup destination.',\n );\n}\n\nexport function litestreamSnapshotReservationDuration() {\n return litestreamDurationHistogram(\n 'litestream.snapshot.reservation_duration',\n 'Duration of a snapshot reservation while a view-syncer restores and subscribes.',\n );\n}\n\nexport function litestreamSnapshotReservationConfirmDuration() {\n return litestreamDurationHistogram(\n 'litestream.snapshot.reservation_confirm_duration',\n 'Time from opening a snapshot reservation to confirming its change-log ' +\n 'bounds, by the source those bounds came from.',\n );\n}\n\nfunction litestreamDurationHistogram(name: string, description: string) {\n return getOrCreateHistogram('replica', name, {\n description,\n unit: 's',\n bucketBoundaries: LONG_DURATION_HISTOGRAM_BOUNDARIES_S,\n });\n}\n"],"mappings":";;AAqBA,SAAgB,6BACd,QACA,MACA,YAAY,OAAO,WACqC;CACxD,MAAM,EAAC,YAAY,cAAc,mBAAkB;CACnD,MAAM,sBACH,iBAAiB,eAAe,eAAe;CAClD,OAAO;EACL;EACA,eAAe,uBAAuB,SAAS;EAC/C,YACE,iBAAiB,KAAA,KAAa,uBAAuB,eACjD,OACA;EACN,GAAG,+BAA+B,MAAM;CAC1C;AACF;AAEA,SAAgB,4BACd,QACuB;CACvB,OAAO;EACL,MAAM;EACN,eAAe,uBAAuB,OAAO,SAAS;EACtD,YACE,OAAO,iBAAiB,KAAA,KACxB,OAAO,eAAe,OAAO,eACzB,OACA;CACR;AACF;AAEA,SAAgB,mCACd,QACwD;CACxD,OAAO;EACL,GAAG,4BAA4B,MAAM;EACrC,GAAG,+BAA+B,MAAM;CAC1C;AACF;AAEA,SAAgB,6BACd,WACA,YACA,MACuB;CACvB,OAAO;EACL;EACA,eAAe,uBAAuB,SAAS;EAC/C;CACF;AACF;AAEA,SAAS,uBAAuB,WAAuC;CACrE,IAAI,CAAC,WACH,OAAO;CAET,IAAI;EACF,MAAM,WAAW,IAAI,IAAI,SAAS,CAAC,CAAC;EACpC,OAAO,SAAS,SAAS,GAAG,IAAI,SAAS,MAAM,GAAG,EAAE,IAAI;CAC1D,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,+BACP,QACgC;CAChC,OAAO;EACL,uBAAuB,OAAO;EAC9B,oBAAoB,KAAK,MAAM,OAAO,gBAAgB,OAAO,IAAI;CACnE;AACF;AAEA,SAAgB,wBAAwB;CACtC,OAAO,mBACL,WACA,2BACA,6CACF;AACF;AAEA,SAAgB,4BAA4B;CAC1C,OAAO,mBACL,WACA,+BACA,4DACF;AACF;AAEA,SAAgB,4BAA4B;CAC1C,OAAO,mBAAmB,WAAW,+BAA+B;EAClE,aACE;EACF,MAAM;CACR,CAAC;AACH;AAEA,SAAgB,8BAA8B;CAC5C,OAAO,mBACL,WACA,kCACA,qDACF;AACF;AAEA,SAAgB,4BAA4B;CAC1C,OAAO,4BACL,+BACA,qEACF;AACF;AAEA,SAAgB,mCAAmC;CACjD,OAAO,4BACL,uCACA,2DACF;AACF;AAEA,SAAgB,sCAAsC;CACpD,OAAO,4BACL,0CACA,oDACF;AACF;AAEA,SAAgB,kCAAkC;CAChD,OAAO,4BACL,sCACA,uEACF;AACF;AAEA,SAAgB,+BAA+B;CAC7C,OAAO,4BACL,mCACA,6DACF;AACF;AAEA,SAAgB,uCAAuC;CACrD,OAAO,4BACL,2CACA,0EACF;AACF;AAEA,SAAgB,wCAAwC;CACtD,OAAO,4BACL,4CACA,iFACF;AACF;AAEA,SAAgB,+CAA+C;CAC7D,OAAO,4BACL,oDACA,qHAEF;AACF;AAEA,SAAS,4BAA4B,MAAc,aAAqB;CACtE,OAAO,qBAAqB,WAAW,MAAM;EAC3C;EACA,MAAM;EACN,kBAAkB;CACpB,CAAC;AACH"}