@rocicorp/zero 1.12.0-canary.10 → 1.12.0-canary.12

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 (231) hide show
  1. package/out/analyze-query/src/zql-chain.js +9 -1
  2. package/out/analyze-query/src/zql-chain.js.map +1 -1
  3. package/out/ast-to-zql/src/ast-to-zql.d.ts.map +1 -1
  4. package/out/ast-to-zql/src/ast-to-zql.js +61 -20
  5. package/out/ast-to-zql/src/ast-to-zql.js.map +1 -1
  6. package/out/z2s/src/compiler.d.ts +1 -1
  7. package/out/z2s/src/compiler.d.ts.map +1 -1
  8. package/out/z2s/src/compiler.js +339 -2
  9. package/out/z2s/src/compiler.js.map +1 -1
  10. package/out/z2s/src/sql.d.ts +8 -0
  11. package/out/z2s/src/sql.d.ts.map +1 -1
  12. package/out/z2s/src/sql.js +58 -11
  13. package/out/z2s/src/sql.js.map +1 -1
  14. package/out/zero/package.js +1 -1
  15. package/out/zero/package.js.map +1 -1
  16. package/out/zero/src/bindings.js +4 -4
  17. package/out/zero-cache/src/auth/write-authorizer.js +1 -1
  18. package/out/zero-cache/src/config/zero-config.d.ts +4 -0
  19. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  20. package/out/zero-cache/src/config/zero-config.js +13 -0
  21. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  22. package/out/zero-cache/src/db/create.js +1 -1
  23. package/out/zero-cache/src/scripts/decommission.js +1 -1
  24. package/out/zero-cache/src/server/replicator.js +4 -2
  25. package/out/zero-cache/src/server/replicator.js.map +1 -1
  26. package/out/zero-cache/src/server/runner/run-worker.d.ts.map +1 -1
  27. package/out/zero-cache/src/server/runner/run-worker.js +8 -5
  28. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  29. package/out/zero-cache/src/server/serving-checkpointer.js +41 -0
  30. package/out/zero-cache/src/server/serving-checkpointer.js.map +1 -0
  31. package/out/zero-cache/src/server/worker-urls.d.ts +1 -0
  32. package/out/zero-cache/src/server/worker-urls.d.ts.map +1 -1
  33. package/out/zero-cache/src/server/worker-urls.js +2 -1
  34. package/out/zero-cache/src/server/worker-urls.js.map +1 -1
  35. package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +1 -1
  36. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +1 -1
  37. package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +1 -1
  38. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +1 -1
  39. package/out/zero-cache/src/services/change-source/pg/schema/init.js +1 -1
  40. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +1 -1
  41. package/out/zero-cache/src/services/http-service.d.ts +6 -0
  42. package/out/zero-cache/src/services/http-service.d.ts.map +1 -1
  43. package/out/zero-cache/src/services/http-service.js +13 -1
  44. package/out/zero-cache/src/services/http-service.js.map +1 -1
  45. package/out/zero-cache/src/services/life-cycle.d.ts +19 -0
  46. package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
  47. package/out/zero-cache/src/services/life-cycle.js +53 -1
  48. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  49. package/out/zero-cache/src/services/replicator/change-processor.js +1 -1
  50. package/out/zero-cache/src/services/replicator/serving-checkpointer.js +85 -0
  51. package/out/zero-cache/src/services/replicator/serving-checkpointer.js.map +1 -0
  52. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +101 -13
  53. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  54. package/out/zero-cache/src/services/view-syncer/query-covering.js +2 -2
  55. package/out/zero-cache/src/services/view-syncer/query-covering.js.map +1 -1
  56. package/out/zero-cache/src/services/view-syncer/snapshotter.js +1 -1
  57. package/out/zero-cache/src/services/view-syncer/view-syncer.js +1 -1
  58. package/out/zero-cache/src/types/processes.js +4 -4
  59. package/out/zero-cache/src/types/processes.js.map +1 -1
  60. package/out/zero-cache/src/types/row-key.js +8 -1
  61. package/out/zero-cache/src/types/row-key.js.map +1 -1
  62. package/out/zero-cache/src/workers/connection.js +2 -2
  63. package/out/zero-cache/src/workers/replicator.js +9 -4
  64. package/out/zero-cache/src/workers/replicator.js.map +1 -1
  65. package/out/zero-client/src/client/bindings.d.ts +1 -1
  66. package/out/zero-client/src/client/bindings.d.ts.map +1 -1
  67. package/out/zero-client/src/client/bindings.js +4 -4
  68. package/out/zero-client/src/client/context.d.ts +7 -1
  69. package/out/zero-client/src/client/context.d.ts.map +1 -1
  70. package/out/zero-client/src/client/context.js +7 -0
  71. package/out/zero-client/src/client/context.js.map +1 -1
  72. package/out/zero-client/src/client/crud-impl.d.ts.map +1 -1
  73. package/out/zero-client/src/client/crud-impl.js +171 -2
  74. package/out/zero-client/src/client/crud-impl.js.map +1 -1
  75. package/out/zero-client/src/client/custom.d.ts.map +1 -1
  76. package/out/zero-client/src/client/custom.js +42 -3
  77. package/out/zero-client/src/client/custom.js.map +1 -1
  78. package/out/zero-client/src/client/ivm-branch.d.ts +62 -2
  79. package/out/zero-client/src/client/ivm-branch.d.ts.map +1 -1
  80. package/out/zero-client/src/client/ivm-branch.js +152 -31
  81. package/out/zero-client/src/client/ivm-branch.js.map +1 -1
  82. package/out/zero-client/src/client/keys.d.ts +33 -0
  83. package/out/zero-client/src/client/keys.d.ts.map +1 -1
  84. package/out/zero-client/src/client/keys.js +43 -4
  85. package/out/zero-client/src/client/keys.js.map +1 -1
  86. package/out/zero-client/src/client/version.js +1 -1
  87. package/out/zero-client/src/client/zero-poke-handler.d.ts.map +1 -1
  88. package/out/zero-client/src/client/zero-poke-handler.js +29 -1
  89. package/out/zero-client/src/client/zero-poke-handler.js.map +1 -1
  90. package/out/zero-client/src/client/zero-rep.d.ts +1 -1
  91. package/out/zero-client/src/client/zero-rep.d.ts.map +1 -1
  92. package/out/zero-client/src/client/zero-rep.js +2 -15
  93. package/out/zero-client/src/client/zero-rep.js.map +1 -1
  94. package/out/zero-client/src/client/zero.js +1 -1
  95. package/out/zero-client/src/mod.d.ts +3 -3
  96. package/out/zero-client/src/mod.d.ts.map +1 -1
  97. package/out/zero-protocol/src/aggregate-table.d.ts +15 -0
  98. package/out/zero-protocol/src/aggregate-table.d.ts.map +1 -0
  99. package/out/zero-protocol/src/aggregate-table.js +5 -0
  100. package/out/zero-protocol/src/aggregate-table.js.map +1 -0
  101. package/out/zero-protocol/src/ast.d.ts +141 -5
  102. package/out/zero-protocol/src/ast.d.ts.map +1 -1
  103. package/out/zero-protocol/src/ast.js +244 -18
  104. package/out/zero-protocol/src/ast.js.map +1 -1
  105. package/out/zero-protocol/src/down.d.ts +29 -29
  106. package/out/zero-protocol/src/inspect-down.d.ts +81 -81
  107. package/out/zero-protocol/src/protocol-version.d.ts +1 -1
  108. package/out/zero-protocol/src/protocol-version.d.ts.map +1 -1
  109. package/out/zero-protocol/src/protocol-version.js.map +1 -1
  110. package/out/zero-protocol/src/query-hash-visitor.d.ts.map +1 -1
  111. package/out/zero-protocol/src/query-hash-visitor.js +38 -0
  112. package/out/zero-protocol/src/query-hash-visitor.js.map +1 -1
  113. package/out/zero-react/src/bindings.js +4 -4
  114. package/out/zero-react/src/use-query.d.ts +3 -1
  115. package/out/zero-react/src/use-query.d.ts.map +1 -1
  116. package/out/zero-react/src/use-query.js +4 -2
  117. package/out/zero-react/src/use-query.js.map +1 -1
  118. package/out/zero-schema/src/builder/schema-builder.d.ts.map +1 -1
  119. package/out/zero-schema/src/builder/schema-builder.js +2 -0
  120. package/out/zero-schema/src/builder/schema-builder.js.map +1 -1
  121. package/out/zero-schema/src/name-mapper.d.ts.map +1 -1
  122. package/out/zero-schema/src/name-mapper.js +7 -4
  123. package/out/zero-schema/src/name-mapper.js.map +1 -1
  124. package/out/zero-server/src/custom.js +1 -1
  125. package/out/zero-server/src/schema.d.ts.map +1 -1
  126. package/out/zero-server/src/schema.js +13 -2
  127. package/out/zero-server/src/schema.js.map +1 -1
  128. package/out/zero-solid/src/bindings.js +4 -4
  129. package/out/zero-solid/src/solid-view.d.ts.map +1 -1
  130. package/out/zero-solid/src/solid-view.js +3 -3
  131. package/out/zero-solid/src/solid-view.js.map +1 -1
  132. package/out/zero-types/src/format.d.ts +13 -0
  133. package/out/zero-types/src/format.d.ts.map +1 -1
  134. package/out/zero-types/src/format.js.map +1 -1
  135. package/out/zero-types/src/name-mapper.d.ts +14 -0
  136. package/out/zero-types/src/name-mapper.d.ts.map +1 -1
  137. package/out/zero-types/src/name-mapper.js +12 -0
  138. package/out/zero-types/src/name-mapper.js.map +1 -1
  139. package/out/zero-types/src/schema-value.d.ts +2 -0
  140. package/out/zero-types/src/schema-value.d.ts.map +1 -1
  141. package/out/zero-types/src/schema-value.js +15 -0
  142. package/out/zero-types/src/schema-value.js.map +1 -0
  143. package/out/zero-types/src/server-schema.d.ts +18 -0
  144. package/out/zero-types/src/server-schema.d.ts.map +1 -1
  145. package/out/zql/src/builder/builder.d.ts +126 -1
  146. package/out/zql/src/builder/builder.d.ts.map +1 -1
  147. package/out/zql/src/builder/builder.js +281 -11
  148. package/out/zql/src/builder/builder.js.map +1 -1
  149. package/out/zql/src/builder/filter.d.ts +1 -1
  150. package/out/zql/src/builder/filter.d.ts.map +1 -1
  151. package/out/zql/src/builder/filter.js +41 -3
  152. package/out/zql/src/builder/filter.js.map +1 -1
  153. package/out/zql/src/ivm/aggregate.d.ts +188 -0
  154. package/out/zql/src/ivm/aggregate.d.ts.map +1 -0
  155. package/out/zql/src/ivm/aggregate.js +470 -0
  156. package/out/zql/src/ivm/aggregate.js.map +1 -0
  157. package/out/zql/src/ivm/array-view.d.ts +1 -1
  158. package/out/zql/src/ivm/array-view.d.ts.map +1 -1
  159. package/out/zql/src/ivm/array-view.js +3 -3
  160. package/out/zql/src/ivm/array-view.js.map +1 -1
  161. package/out/zql/src/ivm/cap.js +2 -2
  162. package/out/zql/src/ivm/constraint.d.ts.map +1 -1
  163. package/out/zql/src/ivm/constraint.js +2 -1
  164. package/out/zql/src/ivm/constraint.js.map +1 -1
  165. package/out/zql/src/ivm/deferred-input.js +1 -1
  166. package/out/zql/src/ivm/fan-in.js +1 -1
  167. package/out/zql/src/ivm/flipped-join.d.ts.map +1 -1
  168. package/out/zql/src/ivm/flipped-join.js +3 -2
  169. package/out/zql/src/ivm/flipped-join.js.map +1 -1
  170. package/out/zql/src/ivm/group-by.d.ts +98 -0
  171. package/out/zql/src/ivm/group-by.d.ts.map +1 -0
  172. package/out/zql/src/ivm/group-by.js +384 -0
  173. package/out/zql/src/ivm/group-by.js.map +1 -0
  174. package/out/zql/src/ivm/join-index.d.ts.map +1 -1
  175. package/out/zql/src/ivm/join-index.js +2 -1
  176. package/out/zql/src/ivm/join-index.js.map +1 -1
  177. package/out/zql/src/ivm/join-utils.d.ts.map +1 -1
  178. package/out/zql/src/ivm/join-utils.js +2 -1
  179. package/out/zql/src/ivm/join-utils.js.map +1 -1
  180. package/out/zql/src/ivm/join.js +1 -1
  181. package/out/zql/src/ivm/lift-field.d.ts +54 -0
  182. package/out/zql/src/ivm/lift-field.d.ts.map +1 -0
  183. package/out/zql/src/ivm/lift-field.js +168 -0
  184. package/out/zql/src/ivm/lift-field.js.map +1 -0
  185. package/out/zql/src/ivm/maybe-split-and-push-edit-change.js +1 -1
  186. package/out/zql/src/ivm/memory-source.d.ts +2 -0
  187. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  188. package/out/zql/src/ivm/memory-source.js +9 -4
  189. package/out/zql/src/ivm/memory-source.js.map +1 -1
  190. package/out/zql/src/ivm/push-accumulated.js +1 -1
  191. package/out/zql/src/ivm/schema.d.ts +1 -0
  192. package/out/zql/src/ivm/schema.d.ts.map +1 -1
  193. package/out/zql/src/ivm/source.d.ts +21 -0
  194. package/out/zql/src/ivm/source.d.ts.map +1 -1
  195. package/out/zql/src/ivm/source.js.map +1 -1
  196. package/out/zql/src/ivm/take.js +1 -1
  197. package/out/zql/src/ivm/union-fan-in.js +1 -1
  198. package/out/zql/src/ivm/view-apply-change.d.ts +5 -0
  199. package/out/zql/src/ivm/view-apply-change.d.ts.map +1 -1
  200. package/out/zql/src/ivm/view-apply-change.js +23 -3
  201. package/out/zql/src/ivm/view-apply-change.js.map +1 -1
  202. package/out/zql/src/planner/planner-debug.d.ts +1 -1
  203. package/out/zql/src/planner/planner-debug.d.ts.map +1 -1
  204. package/out/zql/src/planner/planner-debug.js +4 -0
  205. package/out/zql/src/planner/planner-debug.js.map +1 -1
  206. package/out/zql/src/query/expression.d.ts +50 -3
  207. package/out/zql/src/query/expression.d.ts.map +1 -1
  208. package/out/zql/src/query/expression.js +62 -6
  209. package/out/zql/src/query/expression.js.map +1 -1
  210. package/out/zql/src/query/measure-push-operator.js +1 -1
  211. package/out/zql/src/query/query-delegate-base.d.ts +8 -0
  212. package/out/zql/src/query/query-delegate-base.d.ts.map +1 -1
  213. package/out/zql/src/query/query-delegate-base.js +13 -5
  214. package/out/zql/src/query/query-delegate-base.js.map +1 -1
  215. package/out/zql/src/query/query-impl.d.ts +17 -3
  216. package/out/zql/src/query/query-impl.d.ts.map +1 -1
  217. package/out/zql/src/query/query-impl.js +239 -7
  218. package/out/zql/src/query/query-impl.js.map +1 -1
  219. package/out/zql/src/query/query.d.ts +319 -7
  220. package/out/zql/src/query/query.d.ts.map +1 -1
  221. package/out/zqlite/src/pending-delta.d.ts.map +1 -1
  222. package/out/zqlite/src/pending-delta.js +4 -3
  223. package/out/zqlite/src/pending-delta.js.map +1 -1
  224. package/out/zqlite/src/query-builder.d.ts +12 -1
  225. package/out/zqlite/src/query-builder.d.ts.map +1 -1
  226. package/out/zqlite/src/query-builder.js +114 -13
  227. package/out/zqlite/src/query-builder.js.map +1 -1
  228. package/out/zqlite/src/table-source.d.ts.map +1 -1
  229. package/out/zqlite/src/table-source.js +37 -3
  230. package/out/zqlite/src/table-source.js.map +1 -1
  231. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"replicator.js","names":["v.parse"],"sources":["../../../../../zero-cache/src/server/replicator.ts"],"sourcesContent":["import {stat} from 'node:fs/promises';\nimport {pid} from 'node:process';\nimport type {ObservableCallback} from '@opentelemetry/api';\nimport {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport type {\n LitestreamConfig,\n NormalizedZeroConfig,\n} from '../config/normalize.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {registerSQLiteCorruptionDiagnosticTarget} from '../db/sqlite-corruption.ts';\nimport {initEventSink} from '../observability/events.ts';\nimport {getOrCreateGauge} from '../observability/metrics.ts';\nimport {\n ChangeStreamerHttpClient,\n type ReservationFollowup,\n type SnapshotReserver,\n} from '../services/change-streamer/change-streamer-http.ts';\nimport {reserveAndGetSnapshotStatus} from '../services/change-streamer/snapshot.ts';\nimport {\n exitAfter,\n runUntilKilled,\n SupportingProcesses,\n} from '../services/life-cycle.ts';\nimport {\n readLocalReplica,\n startReplicaBackupProcess,\n tryRestore,\n type RestoreAttempt,\n type RestoreResult,\n} from '../services/litestream/commands.ts';\nimport {\n litestreamRestoreDuration,\n litestreamRestoreMetricAttrs,\n litestreamRestoreRuns,\n} from '../services/litestream/metrics.ts';\nimport {RestoreProgressReporter} from '../services/litestream/restore-progress.ts';\nimport {ReplicationStatusPublisher} from '../services/replicator/replication-status.ts';\nimport {\n ReplicatorService,\n type ReplicatorMode,\n} from '../services/replicator/replicator.ts';\nimport {sqliteFileBytes} from '../services/replicator/sqlite-change-log-observability.ts';\nimport {\n ThreadWriteWorkerClient,\n type ForceCheckpointConfig,\n} from '../services/replicator/write-worker-client.ts';\nimport {\n parentWorker,\n singleProcessMode,\n type DrainRequestedMessage,\n type Worker,\n} from '../types/processes.ts';\nimport {installProfileHandler} from '../types/profiler.ts';\nimport {getShardConfig} from '../types/shards.ts';\nimport {\n deleteStaleChangeLog,\n getPragmaConfig,\n replicaFileModeSchema,\n setUpMessageHandlers,\n setupReplica,\n type WalMode,\n} from '../workers/replicator.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\n\n// Default LogContext, overridden in runWorker\nlet lc = new LogContext('info', {}, consoleLogSink);\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...args: string[]\n): Promise<void> {\n assert(args.length > 0, `replicator mode not specified`);\n const fileMode = v.parse(args[0], replicaFileModeSchema);\n\n const config = getNormalizedZeroConfig({env, argv: args.slice(1)});\n const mode: ReplicatorMode = fileMode === 'backup' ? 'backup' : 'serving';\n const runningLocalChangeStreamer =\n config.changeStreamer.mode === 'dedicated' && !config.changeStreamer.uri;\n const workerName = `${mode}-replicator`;\n installProfileHandler(parent, workerName);\n startOtelAuto(createLogContext(config, workerName, 0, false), workerName, 0);\n lc = createLogContext(config, workerName);\n const unregisterInitialCorruptionDiagnosticTarget =\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: `${workerName} replica`,\n dbPath: config.replica.file,\n },\n config.sqliteCorruptionChecks,\n );\n initEventSink(lc, config);\n\n const shard = getShardConfig(config);\n const {\n taskID,\n change,\n changeStreamer: {\n port,\n uri: changeStreamerURI = runningLocalChangeStreamer\n ? `http://localhost:${port}/`\n : undefined,\n },\n replicationLag: {initialReadinessThresholdMs, maxReadinessDelayMs},\n } = config;\n const changeStreamer = new ChangeStreamerHttpClient(\n lc,\n shard,\n change.db,\n changeStreamerURI,\n );\n\n // Remote view-syncers restore from the replication-manager, which supplies\n // the backup URL. A local change-streamer owns the canonical replica and\n // must not infer backup configuration from bundled executable paths.\n //\n // The reservation's connection (`initialConnection`) is threaded through to\n // IncrementalSyncer's first subscribe(), so the replication-manager that\n // reserved the change log for this restore is the one that serves the\n // subscription (single-connection handoff).\n let initialConnection: ReservationFollowup | undefined;\n if (\n fileMode === 'serving' &&\n !runningLocalChangeStreamer &&\n (config.litestream.executable || config.litestream.executableV5)\n ) {\n initialConnection = await restoreReplica(lc, config, changeStreamer);\n }\n\n const {\n file: dbPath,\n walMode,\n pageSize,\n } = await setupReplica(lc, fileMode, config.replica);\n unregisterInitialCorruptionDiagnosticTarget();\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: `${workerName} replica`,\n dbPath,\n },\n config.sqliteCorruptionChecks,\n );\n\n // A file left behind by a run with the writer enabled would otherwise hold\n // local disk indefinitely. See `replicatorDeletesStaleChangeLog` for why\n // this is keyed on the config flag and nothing else.\n deleteStaleChangeLog(config.changeStreamer.sqliteChangeLogMode, dbPath);\n\n setupMetrics(lc, dbPath, walMode, pageSize);\n\n // Create the write worker for async SQLite writes. Enable write-path\n // checkpoint backpressure only for the backup replicator on litestream v5,\n // which owns the replica litestream backs up.\n const pragmas = getPragmaConfig(fileMode);\n const checkpoint: ForceCheckpointConfig | null =\n mode === 'backup' &&\n config.litestream.backupURL &&\n config.litestream.backupUsingV5 &&\n config.litestream.forceCheckpointThresholdMB\n ? {\n checkpointThresholdPages:\n (config.litestream.forceCheckpointThresholdMB * 1024 ** 2) /\n pageSize,\n maxWalPages: !config.litestream.maxWalSizeMB\n ? undefined\n : (config.litestream.maxWalSizeMB * 1024 ** 2) / pageSize,\n }\n : null;\n\n const workerClient = new ThreadWriteWorkerClient();\n await workerClient.init(dbPath, mode, pragmas, config.log, checkpoint);\n\n // The backup-replicator manages the litestream backup process. It is\n // started after the write worker has prepared the db file in order to\n // avoid contending on the sqlite lock.\n const processes = new SupportingProcesses(lc, parent);\n if (mode === 'backup') {\n processes.add(\n startReplicaBackupProcess(lc, config.litestream, dbPath),\n 'litestream',\n );\n }\n\n const replicator = new ReplicatorService(\n lc,\n taskID,\n `${workerName}-${pid}`,\n mode,\n changeStreamer,\n workerClient,\n runningLocalChangeStreamer\n ? // publish ReplicationStatusEvents from backup-replicator only\n ReplicationStatusPublisher.forReplicaFile(dbPath)\n : null,\n {\n readinessThresholdMs: initialReadinessThresholdMs,\n maxReadinessDelayMs,\n },\n initialConnection,\n // View-syncers (i.e. with a remote change-streamer) switch over to\n // another replication-manager when theirs drains.\n runningLocalChangeStreamer\n ? undefined\n : {\n onRestoreGranted: () =>\n parent.send<DrainRequestedMessage>(['drainRequested', {}]),\n replicaFile: dbPath,\n },\n );\n\n setUpMessageHandlers(lc, replicator, parent);\n\n void replicator.ready().then(() => {\n parent.send(['ready', {ready: true}]);\n });\n\n return runUntilKilled(lc, parent, replicator, processes);\n}\n\nexport function setupMetrics(\n lc: LogContext,\n file: string,\n walMode: WalMode,\n pageSize: number,\n) {\n let db: Database | undefined;\n const getDb = () => {\n if (!db) {\n try {\n db = new Database(lc, file, {readonly: true});\n } catch (e) {\n lc.warn?.(`unable to open ${file} for wal metrics`, e);\n }\n }\n return db;\n };\n\n getOrCreateGauge('replica', 'db_size', {\n description:\n `The size of the replica's main db file, ` +\n `which does not include the wal file(s)`,\n unit: 'bytes',\n }).addCallback(observeFileSize(lc, file));\n\n getOrCreateGauge('replica', 'wal_size', {\n description: `The size of the replica's wal file`,\n unit: 'bytes',\n }).addCallback(observeFileSize(lc, `${file}-wal`));\n\n if (walMode === 'wal2') {\n getOrCreateGauge('replica', 'wal2_size', {\n description: `The size of the replica's wal2 file`,\n unit: 'bytes',\n }).addCallback(observeFileSize(lc, `${file}-wal2`));\n }\n\n getOrCreateGauge('replica', 'wal_active_bytes', {\n description:\n `The actual size of active WAL frames currently in use ` +\n `(excluding pre-allocated or reclaimed space).`,\n unit: 'bytes',\n }).addCallback(observeWalActiveBytes(lc, getDb, pageSize));\n\n getOrCreateGauge('replica', 'wal_uncheckpointed_bytes', {\n description:\n `The size of un-checkpointed WAL frames that have not yet ` +\n `been backfilled into the main database.`,\n unit: 'bytes',\n }).addCallback(observeWalUncheckpointedBytes(lc, getDb, pageSize));\n\n // A whole-database total rather than a per-file one. Its counterpart,\n // `sqlite_change_log.file_bytes`, is emitted by the change-streamer, which is\n // the process that writes the log.\n getOrCreateGauge('replica', 'file_bytes', {\n description:\n `The replica's total on-disk footprint: its main db file plus its ` +\n `wal sidecars.`,\n unit: 'By',\n }).addCallback(observeSQLiteFileBytes(lc, file));\n}\n\nfunction observeFileSize(lc: LogContext, file: string): ObservableCallback {\n return async o => {\n try {\n const stats = await stat(file);\n o.observe(stats.size);\n } catch (e) {\n lc.warn?.(`unable to stat ${file} for size metrics`, e);\n }\n };\n}\n\nfunction observeWalActiveBytes(\n lc: LogContext,\n getDb: () => Database | undefined,\n pageSize: number,\n): ObservableCallback {\n return o => {\n try {\n const db = getDb();\n if (!db) {\n return;\n }\n const [{log}] = db.pragma<{log: number}>('wal_checkpoint(NOOP)');\n if (typeof log === 'number') {\n o.observe(Math.max(0, log) * pageSize);\n }\n } catch (e) {\n lc.warn?.('unable to inspect wal_checkpoint for active bytes metric', e);\n }\n };\n}\n\nfunction observeWalUncheckpointedBytes(\n lc: LogContext,\n getDb: () => Database | undefined,\n pageSize: number,\n): ObservableCallback {\n return o => {\n try {\n const db = getDb();\n if (!db) {\n return;\n }\n const [{log, checkpointed}] = db.pragma<{\n log: number;\n checkpointed: number;\n }>('wal_checkpoint(NOOP)');\n if (typeof log === 'number' && typeof checkpointed === 'number') {\n o.observe(Math.max(0, log - checkpointed) * pageSize);\n }\n } catch (e) {\n lc.warn?.(\n 'unable to inspect wal_checkpoint for uncheckpointed bytes metric',\n e,\n );\n }\n };\n}\n\nfunction observeSQLiteFileBytes(\n lc: LogContext,\n file: string,\n): ObservableCallback {\n return async o => o.observe(await sqliteFileBytes(lc, file));\n}\n\nconst RETRY_INTERVAL_MS = 3000;\n\n// The number of times a view-syncer retries a snapshot reservation from a\n// replication-manager with an older replica version than its local replica\n// (e.g. a not-yet-terminated replication-manager during a resync rollout),\n// before accepting the older version (e.g. if the upstream was legitimately\n// replaced by one with a lower replica version).\nconst MAX_OLDER_REPLICA_VERSION_RETRIES = 20;\n\n// View-syncers wait indefinitely for the replication-manager to publish a\n// restorable backup. On a fresh stack the first backup is not durable until\n// the initial sync completes and litestream uploads the initial snapshot,\n// which can take many minutes for a large replica. The platform's startup\n// probe budget (which scales with replica size) is the backstop, so\n// restoreReplica must not impose its own shorter cap and self-terminate\n// while the backup is still being produced.\n//\n// Returns the `followup` of the reservation whose backup was actually\n// restored, still open, so the caller can hand it to IncrementalSyncer's\n// first subscribe() via `followup.subscribe()`: the same replication-manager\n// that reserved the change log is the one that serves the subscription.\n/** Exported for testing. */\nexport async function restoreReplica(\n lc: LogContext,\n config: NormalizedZeroConfig,\n changeStreamer: SnapshotReserver,\n): Promise<ReservationFollowup> {\n const start = performance.now();\n let backupURL: string | undefined;\n let result: RestoreResult | undefined;\n const progress = new RestoreProgressReporter(lc, config.replica.file);\n let olderReplicaVersionRetries = 0;\n // Describing the local replica (if any) allows the replication-manager to\n // account for replicas that are being replaced (see RestoreAdmission). It\n // is read once, before the first attempt, as a failed attempt may have\n // already deleted it; it continues to describe the replica being replaced\n // (and the version not to downgrade from) on every retry.\n const localReplica = readLocalReplica(lc, config.replica.file);\n const localReplicaVersion = localReplica?.replicaVersion;\n try {\n for (;;) {\n const {reserved, followup} = await reserveAndGetSnapshotStatus(\n lc,\n config.taskID,\n changeStreamer,\n localReplica,\n );\n if (\n localReplicaVersion !== undefined &&\n reserved.replicaVersion < localReplicaVersion &&\n olderReplicaVersionRetries++ < MAX_OLDER_REPLICA_VERSION_RETRIES\n ) {\n // Don't replace a newer local replica with an older one, e.g. from\n // a replication-manager that has yet to be replaced by a resync.\n followup.cancel();\n lc.info?.(\n `replication-manager is at an older replica version ` +\n `(${reserved.replicaVersion}) than the local replica ` +\n `(${localReplicaVersion}). retrying in ${RETRY_INTERVAL_MS / 1000} seconds`,\n );\n await sleep(RETRY_INTERVAL_MS);\n continue;\n }\n // The backupURL comes from the replication-manager's snapshot response.\n ({backupURL} = reserved);\n const litestream: LitestreamConfig = {...config.litestream, backupURL};\n progress.start(reserved.replicaSize);\n let attempt: RestoreAttempt;\n try {\n attempt = await tryRestore(\n lc,\n litestream,\n config.replica.file,\n reserved,\n 'view_syncer',\n followup.signal, // abort the restore if the reservation disconnects\n );\n } catch (e) {\n if (followup.signal.aborted) {\n lc.info?.(\n `reservation disconnected during restore. retrying`,\n String(e),\n );\n continue;\n }\n followup.cancel();\n throw e;\n } finally {\n progress.stop();\n }\n if (attempt.restored) {\n result = attempt.result;\n progress.done();\n return followup; // connection to be converted to a subscription\n }\n // This attempt's reservation is stale; a fresh one is made on retry.\n followup.cancel();\n lc.info?.(\n `replica not found. retrying in ${RETRY_INTERVAL_MS / 1000} seconds`,\n );\n await sleep(RETRY_INTERVAL_MS);\n }\n } finally {\n progress.stop();\n const attrs = litestreamRestoreMetricAttrs(\n config.litestream,\n 'view_syncer',\n backupURL,\n );\n const labels = {...attrs, result: result ?? 'error'};\n litestreamRestoreRuns().add(1, labels);\n litestreamRestoreDuration().recordMs(performance.now() - start, labels);\n }\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(\n () => lc,\n () => runWorker(must(parentWorker), process.env, ...process.argv.slice(2)),\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"replicator.js","names":["v.parse"],"sources":["../../../../../zero-cache/src/server/replicator.ts"],"sourcesContent":["import {stat} from 'node:fs/promises';\nimport {pid} from 'node:process';\nimport type {ObservableCallback} from '@opentelemetry/api';\nimport {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport type {\n LitestreamConfig,\n NormalizedZeroConfig,\n} from '../config/normalize.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {registerSQLiteCorruptionDiagnosticTarget} from '../db/sqlite-corruption.ts';\nimport {initEventSink} from '../observability/events.ts';\nimport {getOrCreateGauge} from '../observability/metrics.ts';\nimport {\n ChangeStreamerHttpClient,\n type ReservationFollowup,\n type SnapshotReserver,\n} from '../services/change-streamer/change-streamer-http.ts';\nimport {reserveAndGetSnapshotStatus} from '../services/change-streamer/snapshot.ts';\nimport {\n exitAfter,\n runUntilKilled,\n SupportingProcesses,\n} from '../services/life-cycle.ts';\nimport {\n readLocalReplica,\n startReplicaBackupProcess,\n tryRestore,\n type RestoreAttempt,\n type RestoreResult,\n} from '../services/litestream/commands.ts';\nimport {\n litestreamRestoreDuration,\n litestreamRestoreMetricAttrs,\n litestreamRestoreRuns,\n} from '../services/litestream/metrics.ts';\nimport {RestoreProgressReporter} from '../services/litestream/restore-progress.ts';\nimport {ReplicationStatusPublisher} from '../services/replicator/replication-status.ts';\nimport {\n ReplicatorService,\n type ReplicatorMode,\n} from '../services/replicator/replicator.ts';\nimport {sqliteFileBytes} from '../services/replicator/sqlite-change-log-observability.ts';\nimport {\n ThreadWriteWorkerClient,\n type ForceCheckpointConfig,\n} from '../services/replicator/write-worker-client.ts';\nimport {\n childWorker,\n parentWorker,\n singleProcessMode,\n type DrainRequestedMessage,\n type Worker,\n} from '../types/processes.ts';\nimport {installProfileHandler} from '../types/profiler.ts';\nimport {getShardConfig} from '../types/shards.ts';\nimport {\n deleteStaleChangeLog,\n getPragmaConfig,\n replicaFileModeSchema,\n setUpMessageHandlers,\n setupReplica,\n type WalMode,\n} from '../workers/replicator.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\nimport {SERVING_CHECKPOINTER_URL} from './worker-urls.ts';\n\n// Default LogContext, overridden in runWorker\nlet lc = new LogContext('info', {}, consoleLogSink);\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...args: string[]\n): Promise<void> {\n assert(args.length > 0, `replicator mode not specified`);\n const fileMode = v.parse(args[0], replicaFileModeSchema);\n\n const config = getNormalizedZeroConfig({env, argv: args.slice(1)});\n const mode: ReplicatorMode = fileMode === 'backup' ? 'backup' : 'serving';\n const runningLocalChangeStreamer =\n config.changeStreamer.mode === 'dedicated' && !config.changeStreamer.uri;\n const workerName = `${mode}-replicator`;\n installProfileHandler(parent, workerName);\n startOtelAuto(createLogContext(config, workerName, 0, false), workerName, 0);\n lc = createLogContext(config, workerName);\n const unregisterInitialCorruptionDiagnosticTarget =\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: `${workerName} replica`,\n dbPath: config.replica.file,\n },\n config.sqliteCorruptionChecks,\n );\n initEventSink(lc, config);\n\n const shard = getShardConfig(config);\n const {\n taskID,\n change,\n changeStreamer: {\n port,\n uri: changeStreamerURI = runningLocalChangeStreamer\n ? `http://localhost:${port}/`\n : undefined,\n },\n replicationLag: {initialReadinessThresholdMs, maxReadinessDelayMs},\n } = config;\n const changeStreamer = new ChangeStreamerHttpClient(\n lc,\n shard,\n change.db,\n changeStreamerURI,\n );\n\n // Remote view-syncers restore from the replication-manager, which supplies\n // the backup URL. A local change-streamer owns the canonical replica and\n // must not infer backup configuration from bundled executable paths.\n //\n // The reservation's connection (`initialConnection`) is threaded through to\n // IncrementalSyncer's first subscribe(), so the replication-manager that\n // reserved the change log for this restore is the one that serves the\n // subscription (single-connection handoff).\n let initialConnection: ReservationFollowup | undefined;\n if (\n fileMode === 'serving' &&\n !runningLocalChangeStreamer &&\n (config.litestream.executable || config.litestream.executableV5)\n ) {\n initialConnection = await restoreReplica(lc, config, changeStreamer);\n }\n\n const {\n file: dbPath,\n walMode,\n pageSize,\n } = await setupReplica(lc, fileMode, config.replica);\n unregisterInitialCorruptionDiagnosticTarget();\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: `${workerName} replica`,\n dbPath,\n },\n config.sqliteCorruptionChecks,\n );\n\n // A file left behind by a run with the writer enabled would otherwise hold\n // local disk indefinitely. See `replicatorDeletesStaleChangeLog` for why\n // this is keyed on the config flag and nothing else.\n deleteStaleChangeLog(config.changeStreamer.sqliteChangeLogMode, dbPath);\n\n setupMetrics(lc, dbPath, walMode, pageSize);\n\n // Create the write worker for async SQLite writes. Enable write-path\n // checkpoint backpressure only for the backup replicator on litestream v5,\n // which owns the replica litestream backs up.\n const pragmas = getPragmaConfig();\n const checkpoint: ForceCheckpointConfig | null =\n mode === 'backup' &&\n config.litestream.backupURL &&\n config.litestream.backupUsingV5 &&\n config.litestream.forceCheckpointThresholdMB\n ? {\n checkpointThresholdPages:\n (config.litestream.forceCheckpointThresholdMB * 1024 ** 2) /\n pageSize,\n maxWalPages: !config.litestream.maxWalSizeMB\n ? undefined\n : (config.litestream.maxWalSizeMB * 1024 ** 2) / pageSize,\n }\n : null;\n\n const workerClient = new ThreadWriteWorkerClient();\n await workerClient.init(dbPath, mode, pragmas, config.log, checkpoint);\n\n // The backup-replicator manages the litestream backup process, which\n // checkpoints the backup replica. A serving replicator instead manages a\n // checkpointer for its replica. Either is started after the write worker\n // has prepared the db file in order to avoid contending on the sqlite lock.\n const processes = new SupportingProcesses(lc, parent);\n if (mode === 'backup') {\n processes.add(\n startReplicaBackupProcess(lc, config.litestream, dbPath),\n 'litestream',\n );\n } else {\n processes.add(\n childWorker(SERVING_CHECKPOINTER_URL, env, dbPath),\n 'serving-checkpointer',\n );\n }\n\n const replicator = new ReplicatorService(\n lc,\n taskID,\n `${workerName}-${pid}`,\n mode,\n changeStreamer,\n workerClient,\n runningLocalChangeStreamer\n ? // publish ReplicationStatusEvents from backup-replicator only\n ReplicationStatusPublisher.forReplicaFile(dbPath)\n : null,\n {\n readinessThresholdMs: initialReadinessThresholdMs,\n maxReadinessDelayMs,\n },\n initialConnection,\n // View-syncers (i.e. with a remote change-streamer) switch over to\n // another replication-manager when theirs drains.\n runningLocalChangeStreamer\n ? undefined\n : {\n onRestoreGranted: () =>\n parent.send<DrainRequestedMessage>(['drainRequested', {}]),\n replicaFile: dbPath,\n },\n );\n\n setUpMessageHandlers(lc, replicator, parent);\n\n void replicator.ready().then(() => {\n parent.send(['ready', {ready: true}]);\n });\n\n return runUntilKilled(lc, parent, replicator, processes);\n}\n\nexport function setupMetrics(\n lc: LogContext,\n file: string,\n walMode: WalMode,\n pageSize: number,\n) {\n let db: Database | undefined;\n const getDb = () => {\n if (!db) {\n try {\n db = new Database(lc, file, {readonly: true});\n } catch (e) {\n lc.warn?.(`unable to open ${file} for wal metrics`, e);\n }\n }\n return db;\n };\n\n getOrCreateGauge('replica', 'db_size', {\n description:\n `The size of the replica's main db file, ` +\n `which does not include the wal file(s)`,\n unit: 'bytes',\n }).addCallback(observeFileSize(lc, file));\n\n getOrCreateGauge('replica', 'wal_size', {\n description: `The size of the replica's wal file`,\n unit: 'bytes',\n }).addCallback(observeFileSize(lc, `${file}-wal`));\n\n if (walMode === 'wal2') {\n getOrCreateGauge('replica', 'wal2_size', {\n description: `The size of the replica's wal2 file`,\n unit: 'bytes',\n }).addCallback(observeFileSize(lc, `${file}-wal2`));\n }\n\n getOrCreateGauge('replica', 'wal_active_bytes', {\n description:\n `The actual size of active WAL frames currently in use ` +\n `(excluding pre-allocated or reclaimed space).`,\n unit: 'bytes',\n }).addCallback(observeWalActiveBytes(lc, getDb, pageSize));\n\n getOrCreateGauge('replica', 'wal_uncheckpointed_bytes', {\n description:\n `The size of un-checkpointed WAL frames that have not yet ` +\n `been backfilled into the main database.`,\n unit: 'bytes',\n }).addCallback(observeWalUncheckpointedBytes(lc, getDb, pageSize));\n\n // A whole-database total rather than a per-file one. Its counterpart,\n // `sqlite_change_log.file_bytes`, is emitted by the change-streamer, which is\n // the process that writes the log.\n getOrCreateGauge('replica', 'file_bytes', {\n description:\n `The replica's total on-disk footprint: its main db file plus its ` +\n `wal sidecars.`,\n unit: 'By',\n }).addCallback(observeSQLiteFileBytes(lc, file));\n}\n\nfunction observeFileSize(lc: LogContext, file: string): ObservableCallback {\n return async o => {\n try {\n const stats = await stat(file);\n o.observe(stats.size);\n } catch (e) {\n lc.warn?.(`unable to stat ${file} for size metrics`, e);\n }\n };\n}\n\nfunction observeWalActiveBytes(\n lc: LogContext,\n getDb: () => Database | undefined,\n pageSize: number,\n): ObservableCallback {\n return o => {\n try {\n const db = getDb();\n if (!db) {\n return;\n }\n const [{log}] = db.pragma<{log: number}>('wal_checkpoint(NOOP)');\n if (typeof log === 'number') {\n o.observe(Math.max(0, log) * pageSize);\n }\n } catch (e) {\n lc.warn?.('unable to inspect wal_checkpoint for active bytes metric', e);\n }\n };\n}\n\nfunction observeWalUncheckpointedBytes(\n lc: LogContext,\n getDb: () => Database | undefined,\n pageSize: number,\n): ObservableCallback {\n return o => {\n try {\n const db = getDb();\n if (!db) {\n return;\n }\n const [{log, checkpointed}] = db.pragma<{\n log: number;\n checkpointed: number;\n }>('wal_checkpoint(NOOP)');\n if (typeof log === 'number' && typeof checkpointed === 'number') {\n o.observe(Math.max(0, log - checkpointed) * pageSize);\n }\n } catch (e) {\n lc.warn?.(\n 'unable to inspect wal_checkpoint for uncheckpointed bytes metric',\n e,\n );\n }\n };\n}\n\nfunction observeSQLiteFileBytes(\n lc: LogContext,\n file: string,\n): ObservableCallback {\n return async o => o.observe(await sqliteFileBytes(lc, file));\n}\n\nconst RETRY_INTERVAL_MS = 3000;\n\n// The number of times a view-syncer retries a snapshot reservation from a\n// replication-manager with an older replica version than its local replica\n// (e.g. a not-yet-terminated replication-manager during a resync rollout),\n// before accepting the older version (e.g. if the upstream was legitimately\n// replaced by one with a lower replica version).\nconst MAX_OLDER_REPLICA_VERSION_RETRIES = 20;\n\n// View-syncers wait indefinitely for the replication-manager to publish a\n// restorable backup. On a fresh stack the first backup is not durable until\n// the initial sync completes and litestream uploads the initial snapshot,\n// which can take many minutes for a large replica. The platform's startup\n// probe budget (which scales with replica size) is the backstop, so\n// restoreReplica must not impose its own shorter cap and self-terminate\n// while the backup is still being produced.\n//\n// Returns the `followup` of the reservation whose backup was actually\n// restored, still open, so the caller can hand it to IncrementalSyncer's\n// first subscribe() via `followup.subscribe()`: the same replication-manager\n// that reserved the change log is the one that serves the subscription.\n/** Exported for testing. */\nexport async function restoreReplica(\n lc: LogContext,\n config: NormalizedZeroConfig,\n changeStreamer: SnapshotReserver,\n): Promise<ReservationFollowup> {\n const start = performance.now();\n let backupURL: string | undefined;\n let result: RestoreResult | undefined;\n const progress = new RestoreProgressReporter(lc, config.replica.file);\n let olderReplicaVersionRetries = 0;\n // Describing the local replica (if any) allows the replication-manager to\n // account for replicas that are being replaced (see RestoreAdmission). It\n // is read once, before the first attempt, as a failed attempt may have\n // already deleted it; it continues to describe the replica being replaced\n // (and the version not to downgrade from) on every retry.\n const localReplica = readLocalReplica(lc, config.replica.file);\n const localReplicaVersion = localReplica?.replicaVersion;\n try {\n for (;;) {\n const {reserved, followup} = await reserveAndGetSnapshotStatus(\n lc,\n config.taskID,\n changeStreamer,\n localReplica,\n );\n if (\n localReplicaVersion !== undefined &&\n reserved.replicaVersion < localReplicaVersion &&\n olderReplicaVersionRetries++ < MAX_OLDER_REPLICA_VERSION_RETRIES\n ) {\n // Don't replace a newer local replica with an older one, e.g. from\n // a replication-manager that has yet to be replaced by a resync.\n followup.cancel();\n lc.info?.(\n `replication-manager is at an older replica version ` +\n `(${reserved.replicaVersion}) than the local replica ` +\n `(${localReplicaVersion}). retrying in ${RETRY_INTERVAL_MS / 1000} seconds`,\n );\n await sleep(RETRY_INTERVAL_MS);\n continue;\n }\n // The backupURL comes from the replication-manager's snapshot response.\n ({backupURL} = reserved);\n const litestream: LitestreamConfig = {...config.litestream, backupURL};\n progress.start(reserved.replicaSize);\n let attempt: RestoreAttempt;\n try {\n attempt = await tryRestore(\n lc,\n litestream,\n config.replica.file,\n reserved,\n 'view_syncer',\n followup.signal, // abort the restore if the reservation disconnects\n );\n } catch (e) {\n if (followup.signal.aborted) {\n lc.info?.(\n `reservation disconnected during restore. retrying`,\n String(e),\n );\n continue;\n }\n followup.cancel();\n throw e;\n } finally {\n progress.stop();\n }\n if (attempt.restored) {\n result = attempt.result;\n progress.done();\n return followup; // connection to be converted to a subscription\n }\n // This attempt's reservation is stale; a fresh one is made on retry.\n followup.cancel();\n lc.info?.(\n `replica not found. retrying in ${RETRY_INTERVAL_MS / 1000} seconds`,\n );\n await sleep(RETRY_INTERVAL_MS);\n }\n } finally {\n progress.stop();\n const attrs = litestreamRestoreMetricAttrs(\n config.litestream,\n 'view_syncer',\n backupURL,\n );\n const labels = {...attrs, result: result ?? 'error'};\n litestreamRestoreRuns().add(1, labels);\n litestreamRestoreDuration().recordMs(performance.now() - start, labels);\n }\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(\n () => lc,\n () => runWorker(must(parentWorker), process.env, ...process.argv.slice(2)),\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"run-worker.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/server/runner/run-worker.ts"],"names":[],"mappings":"AAAA,OAAO,kCAAkC,CAAC;AAa1C,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,0BAA0B,CAAC;AAsBlC;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CAuEf"}
1
+ {"version":3,"file":"run-worker.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/server/runner/run-worker.ts"],"names":[],"mappings":"AAAA,OAAO,kCAAkC,CAAC;AAe1C,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,0BAA0B,CAAC;AAsBlC;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CAuFf"}
@@ -5,7 +5,7 @@ import { colorConsole } from "../../../../shared/src/logging.js";
5
5
  import { childWorker } from "../../types/processes.js";
6
6
  import { normalizeZeroConfig } from "../../config/normalize.js";
7
7
  import { getServerVersion, getZeroConfig } from "../../config/zero-config.js";
8
- import { ProcessManager, SelfDrain, runUntilKilled } from "../../services/life-cycle.js";
8
+ import { PRE_DRAIN, PreDrain, ProcessManager, SelfDrain, runUntilKilled } from "../../services/life-cycle.js";
9
9
  import { createLogContext } from "../logging.js";
10
10
  import { MAIN_URL } from "../worker-urls.js";
11
11
  import { getTaskID } from "./runtime.js";
@@ -33,20 +33,23 @@ async function runWorker(parent, env) {
33
33
  const lc = createLogContext(cfg, "runner");
34
34
  const defaultTaskID = await getTaskID(lc);
35
35
  const config = normalizeZeroConfig(lc, cfg, env, defaultTaskID);
36
- const processes = new ProcessManager(lc, parent ?? process);
37
- const { port, keepaliveTimeoutMs, lazyStartup, selfDrainDelayMs } = config;
36
+ const { port, keepaliveTimeoutMs, lazyStartup, selfDrainDelayMs, preDrainDelayMs } = config;
38
37
  const serverVersion = getServerVersion(config);
39
38
  lc.info?.(`starting server${!serverVersion ? "" : `@${serverVersion}`} `, {
40
- protocolVersion: 53,
39
+ protocolVersion: 57,
41
40
  taskID: config.taskID,
42
41
  app: config.app,
43
42
  shard: config.shard,
44
43
  port: config.port
45
44
  });
45
+ const preDrain = preDrainDelayMs > 0 ? new PreDrain(lc, parent ?? process, preDrainDelayMs) : void 0;
46
+ const signals = preDrain ?? parent ?? process;
47
+ const processes = new ProcessManager(lc, signals);
46
48
  const dispatcher = new ZeroDispatcher(config, lc, {
47
49
  port,
48
50
  keepaliveTimeoutMs
49
51
  }, startZeroCache, () => printStartupMessage(env), () => processes.shutdown());
52
+ preDrain?.on(PRE_DRAIN, () => dispatcher.failReadiness());
50
53
  const selfDrain = new SelfDrain(lc, parent ?? process, {
51
54
  delayMs: selfDrainDelayMs,
52
55
  onRequested: () => dispatcher.failHealthChecks()
@@ -70,7 +73,7 @@ async function runWorker(parent, env) {
70
73
  await processes.allWorkersReady();
71
74
  parent?.send(["ready", { ready: true }]);
72
75
  try {
73
- await runUntilKilled(lc, parent ?? process, dispatcher);
76
+ await runUntilKilled(lc, signals, dispatcher);
74
77
  } catch (err) {
75
78
  processes.logErrorAndExit(err, "main");
76
79
  } finally {
@@ -1 +1 @@
1
- {"version":3,"file":"run-worker.js","names":[],"sources":["../../../../../../zero-cache/src/server/runner/run-worker.ts"],"sourcesContent":["import '../../../../shared/src/dotenv.ts';\n\nimport {styleText} from 'node:util';\nimport {colorConsole} from '../../../../shared/src/logging.ts';\nimport {resolver, type Resolver} from '../../../../shared/src/resolver.ts';\nimport {PROTOCOL_VERSION} from '../../../../zero-protocol/src/protocol-version.ts';\nimport {normalizeZeroConfig} from '../../config/normalize.ts';\nimport {getServerVersion, getZeroConfig} from '../../config/zero-config.ts';\nimport {\n ProcessManager,\n runUntilKilled,\n SelfDrain,\n} from '../../services/life-cycle.ts';\nimport {\n childWorker,\n type DrainRequestedMessage,\n type Worker,\n} from '../../types/processes.ts';\nimport {createLogContext} from '../logging.ts';\nimport {MAIN_URL} from '../worker-urls.ts';\nimport {getTaskID} from './runtime.ts';\nimport {ZeroDispatcher} from './zero-dispatcher.ts';\n\nconst startupMessageEnv = 'ZERO_ENABLE_STARTUP_MESSAGE';\n\nfunction printStartupMessage(env: NodeJS.ProcessEnv) {\n if (env[startupMessageEnv] !== '1') {\n return;\n }\n\n colorConsole.log(\n `\\nBTW, ${styleText(['bold', 'cyan'], 'Cloud Zero')} ` +\n 'is now available - professional Zero hosting from the team that built it.\\n' +\n `Get started now: ${styleText(['blue', 'underline'], 'https://zero.rocicorp.dev/cloud')}\\n\\n` +\n styleText('dim', `Disable this message with ${startupMessageEnv}=0`) +\n '\\n',\n );\n}\n\n/**\n * Top-level `runner` entry point to the zero-cache. This layer is responsible for:\n * * runtime-based config normalization\n * * lazy startup\n * * serving /statsz\n * * auto-reset restarts (TODO)\n */\nexport async function runWorker(\n parent: Worker | null,\n env: NodeJS.ProcessEnv,\n): Promise<void> {\n // Note: Deprecation warnings are only emitted at this top-level parse;\n // they are suppressed when parsed in subprocesses.\n const cfg = getZeroConfig({env, emitDeprecationWarnings: true});\n const lc = createLogContext(cfg, 'runner');\n\n const defaultTaskID = await getTaskID(lc);\n const config = normalizeZeroConfig(lc, cfg, env, defaultTaskID);\n const processes = new ProcessManager(lc, parent ?? process);\n\n const {port, keepaliveTimeoutMs, lazyStartup, selfDrainDelayMs} = config;\n const serverVersion = getServerVersion(config);\n lc.info?.(`starting server${!serverVersion ? '' : `@${serverVersion}`} `, {\n protocolVersion: PROTOCOL_VERSION,\n taskID: config.taskID,\n app: config.app,\n shard: config.shard,\n port: config.port,\n });\n\n const dispatcher = new ZeroDispatcher(\n config,\n lc,\n {port, keepaliveTimeoutMs},\n startZeroCache,\n () => printStartupMessage(env),\n // Keep the listener open until the zero-cache has drained and exited.\n () => processes.shutdown(),\n );\n const selfDrain = new SelfDrain(lc, parent ?? process, {\n delayMs: selfDrainDelayMs,\n onRequested: () => dispatcher.failHealthChecks(),\n });\n\n let zeroCache: Resolver<Worker> | undefined;\n function startZeroCache(): Promise<Worker> {\n if (zeroCache === undefined) {\n const startMs = performance.now();\n lc.info?.('starting zero-cache');\n\n const r = (zeroCache = resolver<Worker>());\n const w = childWorker(MAIN_URL, env)\n .once('message', () => {\n r.resolve(w);\n lc.info?.(`zero-cache ready (${performance.now() - startMs} ms)`);\n })\n .once('error', r.reject);\n w.onMessageType<DrainRequestedMessage>('drainRequested', () =>\n selfDrain.request(),\n );\n\n processes.addWorker(w, 'user-facing', 'zero-cache');\n }\n return zeroCache.promise;\n }\n\n // Eagerly start the zero-cache if it was not configured with --lazy-startup.\n if (!lazyStartup) {\n void startZeroCache();\n }\n\n await processes.allWorkersReady();\n parent?.send(['ready', {ready: true}]);\n\n try {\n await runUntilKilled(lc, parent ?? process, dispatcher);\n } catch (err) {\n processes.logErrorAndExit(err, 'main');\n } finally {\n await processes.shutdown();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAuBA,IAAM,oBAAoB;AAE1B,SAAS,oBAAoB,KAAwB;CACnD,IAAI,IAAI,uBAAuB,KAC7B;CAGF,aAAa,IACX,UAAU,UAAU,CAAC,QAAQ,MAAM,GAAG,YAAY,EAAE;mBAE9B,UAAU,CAAC,QAAQ,WAAW,GAAG,iCAAiC,EAAE,QACxF,UAAU,OAAO,6BAA6B,kBAAkB,GAAG,IACnE,IACJ;AACF;;;;;;;;AASA,eAAsB,UACpB,QACA,KACe;CAGf,MAAM,MAAM,cAAc;EAAC;EAAK,yBAAyB;CAAI,CAAC;CAC9D,MAAM,KAAK,iBAAiB,KAAK,QAAQ;CAEzC,MAAM,gBAAgB,MAAM,UAAU,EAAE;CACxC,MAAM,SAAS,oBAAoB,IAAI,KAAK,KAAK,aAAa;CAC9D,MAAM,YAAY,IAAI,eAAe,IAAI,UAAU,OAAO;CAE1D,MAAM,EAAC,MAAM,oBAAoB,aAAa,qBAAoB;CAClE,MAAM,gBAAgB,iBAAiB,MAAM;CAC7C,GAAG,OAAO,kBAAkB,CAAC,gBAAgB,KAAK,IAAI,gBAAgB,IAAI;EACxE,iBAAA;EACA,QAAQ,OAAO;EACf,KAAK,OAAO;EACZ,OAAO,OAAO;EACd,MAAM,OAAO;CACf,CAAC;CAED,MAAM,aAAa,IAAI,eACrB,QACA,IACA;EAAC;EAAM;CAAkB,GACzB,sBACM,oBAAoB,GAAG,SAEvB,UAAU,SAAS,CAC3B;CACA,MAAM,YAAY,IAAI,UAAU,IAAI,UAAU,SAAS;EACrD,SAAS;EACT,mBAAmB,WAAW,iBAAiB;CACjD,CAAC;CAED,IAAI;CACJ,SAAS,iBAAkC;EACzC,IAAI,cAAc,KAAA,GAAW;GAC3B,MAAM,UAAU,YAAY,IAAI;GAChC,GAAG,OAAO,qBAAqB;GAE/B,MAAM,IAAK,YAAY,SAAiB;GACxC,MAAM,IAAI,YAAY,UAAU,GAAG,CAAC,CACjC,KAAK,iBAAiB;IACrB,EAAE,QAAQ,CAAC;IACX,GAAG,OAAO,qBAAqB,YAAY,IAAI,IAAI,QAAQ,KAAK;GAClE,CAAC,CAAC,CACD,KAAK,SAAS,EAAE,MAAM;GACzB,EAAE,cAAqC,wBACrC,UAAU,QAAQ,CACpB;GAEA,UAAU,UAAU,GAAG,eAAe,YAAY;EACpD;EACA,OAAO,UAAU;CACnB;CAGA,IAAI,CAAC,aACH,eAAoB;CAGtB,MAAM,UAAU,gBAAgB;CAChC,QAAQ,KAAK,CAAC,SAAS,EAAC,OAAO,KAAI,CAAC,CAAC;CAErC,IAAI;EACF,MAAM,eAAe,IAAI,UAAU,SAAS,UAAU;CACxD,SAAS,KAAK;EACZ,UAAU,gBAAgB,KAAK,MAAM;CACvC,UAAU;EACR,MAAM,UAAU,SAAS;CAC3B;AACF"}
1
+ {"version":3,"file":"run-worker.js","names":[],"sources":["../../../../../../zero-cache/src/server/runner/run-worker.ts"],"sourcesContent":["import '../../../../shared/src/dotenv.ts';\n\nimport {styleText} from 'node:util';\nimport {colorConsole} from '../../../../shared/src/logging.ts';\nimport {resolver, type Resolver} from '../../../../shared/src/resolver.ts';\nimport {PROTOCOL_VERSION} from '../../../../zero-protocol/src/protocol-version.ts';\nimport {normalizeZeroConfig} from '../../config/normalize.ts';\nimport {getServerVersion, getZeroConfig} from '../../config/zero-config.ts';\nimport {\n PRE_DRAIN,\n PreDrain,\n ProcessManager,\n runUntilKilled,\n SelfDrain,\n} from '../../services/life-cycle.ts';\nimport {\n childWorker,\n type DrainRequestedMessage,\n type Worker,\n} from '../../types/processes.ts';\nimport {createLogContext} from '../logging.ts';\nimport {MAIN_URL} from '../worker-urls.ts';\nimport {getTaskID} from './runtime.ts';\nimport {ZeroDispatcher} from './zero-dispatcher.ts';\n\nconst startupMessageEnv = 'ZERO_ENABLE_STARTUP_MESSAGE';\n\nfunction printStartupMessage(env: NodeJS.ProcessEnv) {\n if (env[startupMessageEnv] !== '1') {\n return;\n }\n\n colorConsole.log(\n `\\nBTW, ${styleText(['bold', 'cyan'], 'Cloud Zero')} ` +\n 'is now available - professional Zero hosting from the team that built it.\\n' +\n `Get started now: ${styleText(['blue', 'underline'], 'https://zero.rocicorp.dev/cloud')}\\n\\n` +\n styleText('dim', `Disable this message with ${startupMessageEnv}=0`) +\n '\\n',\n );\n}\n\n/**\n * Top-level `runner` entry point to the zero-cache. This layer is responsible for:\n * * runtime-based config normalization\n * * lazy startup\n * * serving /statsz\n * * auto-reset restarts (TODO)\n */\nexport async function runWorker(\n parent: Worker | null,\n env: NodeJS.ProcessEnv,\n): Promise<void> {\n // Note: Deprecation warnings are only emitted at this top-level parse;\n // they are suppressed when parsed in subprocesses.\n const cfg = getZeroConfig({env, emitDeprecationWarnings: true});\n const lc = createLogContext(cfg, 'runner');\n\n const defaultTaskID = await getTaskID(lc);\n const config = normalizeZeroConfig(lc, cfg, env, defaultTaskID);\n\n const {\n port,\n keepaliveTimeoutMs,\n lazyStartup,\n selfDrainDelayMs,\n preDrainDelayMs,\n } = config;\n const serverVersion = getServerVersion(config);\n lc.info?.(`starting server${!serverVersion ? '' : `@${serverVersion}`} `, {\n protocolVersion: PROTOCOL_VERSION,\n taskID: config.taskID,\n app: config.app,\n shard: config.shard,\n port: config.port,\n });\n\n // With a pre-drain delay, graceful shutdown signals reach the\n // ProcessManager and the dispatcher's drain only after the delay.\n const preDrain =\n preDrainDelayMs > 0\n ? new PreDrain(lc, parent ?? process, preDrainDelayMs)\n : undefined;\n const signals = preDrain ?? parent ?? process;\n const processes = new ProcessManager(lc, signals);\n\n const dispatcher = new ZeroDispatcher(\n config,\n lc,\n {port, keepaliveTimeoutMs},\n startZeroCache,\n () => printStartupMessage(env),\n // Keep the listener open until the zero-cache has drained and exited.\n () => processes.shutdown(),\n );\n // Fail readiness checks during the delay, while continuing to serve.\n preDrain?.on(PRE_DRAIN, () => dispatcher.failReadiness());\n const selfDrain = new SelfDrain(lc, parent ?? process, {\n delayMs: selfDrainDelayMs,\n onRequested: () => dispatcher.failHealthChecks(),\n });\n\n let zeroCache: Resolver<Worker> | undefined;\n function startZeroCache(): Promise<Worker> {\n if (zeroCache === undefined) {\n const startMs = performance.now();\n lc.info?.('starting zero-cache');\n\n const r = (zeroCache = resolver<Worker>());\n const w = childWorker(MAIN_URL, env)\n .once('message', () => {\n r.resolve(w);\n lc.info?.(`zero-cache ready (${performance.now() - startMs} ms)`);\n })\n .once('error', r.reject);\n w.onMessageType<DrainRequestedMessage>('drainRequested', () =>\n selfDrain.request(),\n );\n\n processes.addWorker(w, 'user-facing', 'zero-cache');\n }\n return zeroCache.promise;\n }\n\n // Eagerly start the zero-cache if it was not configured with --lazy-startup.\n if (!lazyStartup) {\n void startZeroCache();\n }\n\n await processes.allWorkersReady();\n parent?.send(['ready', {ready: true}]);\n\n try {\n await runUntilKilled(lc, signals, dispatcher);\n } catch (err) {\n processes.logErrorAndExit(err, 'main');\n } finally {\n await processes.shutdown();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAyBA,IAAM,oBAAoB;AAE1B,SAAS,oBAAoB,KAAwB;CACnD,IAAI,IAAI,uBAAuB,KAC7B;CAGF,aAAa,IACX,UAAU,UAAU,CAAC,QAAQ,MAAM,GAAG,YAAY,EAAE;mBAE9B,UAAU,CAAC,QAAQ,WAAW,GAAG,iCAAiC,EAAE,QACxF,UAAU,OAAO,6BAA6B,kBAAkB,GAAG,IACnE,IACJ;AACF;;;;;;;;AASA,eAAsB,UACpB,QACA,KACe;CAGf,MAAM,MAAM,cAAc;EAAC;EAAK,yBAAyB;CAAI,CAAC;CAC9D,MAAM,KAAK,iBAAiB,KAAK,QAAQ;CAEzC,MAAM,gBAAgB,MAAM,UAAU,EAAE;CACxC,MAAM,SAAS,oBAAoB,IAAI,KAAK,KAAK,aAAa;CAE9D,MAAM,EACJ,MACA,oBACA,aACA,kBACA,oBACE;CACJ,MAAM,gBAAgB,iBAAiB,MAAM;CAC7C,GAAG,OAAO,kBAAkB,CAAC,gBAAgB,KAAK,IAAI,gBAAgB,IAAI;EACxE,iBAAA;EACA,QAAQ,OAAO;EACf,KAAK,OAAO;EACZ,OAAO,OAAO;EACd,MAAM,OAAO;CACf,CAAC;CAID,MAAM,WACJ,kBAAkB,IACd,IAAI,SAAS,IAAI,UAAU,SAAS,eAAe,IACnD,KAAA;CACN,MAAM,UAAU,YAAY,UAAU;CACtC,MAAM,YAAY,IAAI,eAAe,IAAI,OAAO;CAEhD,MAAM,aAAa,IAAI,eACrB,QACA,IACA;EAAC;EAAM;CAAkB,GACzB,sBACM,oBAAoB,GAAG,SAEvB,UAAU,SAAS,CAC3B;CAEA,UAAU,GAAG,iBAAiB,WAAW,cAAc,CAAC;CACxD,MAAM,YAAY,IAAI,UAAU,IAAI,UAAU,SAAS;EACrD,SAAS;EACT,mBAAmB,WAAW,iBAAiB;CACjD,CAAC;CAED,IAAI;CACJ,SAAS,iBAAkC;EACzC,IAAI,cAAc,KAAA,GAAW;GAC3B,MAAM,UAAU,YAAY,IAAI;GAChC,GAAG,OAAO,qBAAqB;GAE/B,MAAM,IAAK,YAAY,SAAiB;GACxC,MAAM,IAAI,YAAY,UAAU,GAAG,CAAC,CACjC,KAAK,iBAAiB;IACrB,EAAE,QAAQ,CAAC;IACX,GAAG,OAAO,qBAAqB,YAAY,IAAI,IAAI,QAAQ,KAAK;GAClE,CAAC,CAAC,CACD,KAAK,SAAS,EAAE,MAAM;GACzB,EAAE,cAAqC,wBACrC,UAAU,QAAQ,CACpB;GAEA,UAAU,UAAU,GAAG,eAAe,YAAY;EACpD;EACA,OAAO,UAAU;CACnB;CAGA,IAAI,CAAC,aACH,eAAoB;CAGtB,MAAM,UAAU,gBAAgB;CAChC,QAAQ,KAAK,CAAC,SAAS,EAAC,OAAO,KAAI,CAAC,CAAC;CAErC,IAAI;EACF,MAAM,eAAe,IAAI,SAAS,UAAU;CAC9C,SAAS,KAAK;EACZ,UAAU,gBAAgB,KAAK,MAAM;CACvC,UAAU;EACR,MAAM,UAAU,SAAS;CAC3B;AACF"}
@@ -0,0 +1,41 @@
1
+ import { assert } from "../../../shared/src/asserts.js";
2
+ import { parentWorker, singleProcessMode } from "../types/processes.js";
3
+ import { getLogConfig } from "../config/zero-config.js";
4
+ import { exitAfter, runUntilKilled } from "../services/life-cycle.js";
5
+ import { createLogContext } from "./logging.js";
6
+ import { startOtelAuto } from "./otel-start.js";
7
+ import { ServingCheckpointer } from "../services/replicator/serving-checkpointer.js";
8
+ import { LogContext, consoleLogSink } from "@rocicorp/logger";
9
+ import { resolve } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ //#region ../zero-cache/src/server/serving-checkpointer.ts
12
+ var lc = new LogContext("info", {}, consoleLogSink);
13
+ /**
14
+ * Checkpoints a serving replica on behalf of its replicator, which starts
15
+ * this worker as one of its supporting processes once the replica is set up,
16
+ * passing the path of its replica file as the first argument.
17
+ *
18
+ * Only the logging config is parsed from the remaining arguments, which
19
+ * ignores everything else. In particular, `childWorker()` forwards the
20
+ * replicator's own arguments, which start with its replica file mode and
21
+ * would fail a full config parse.
22
+ */
23
+ function runWorker(parent, env, ...args) {
24
+ const [dbPath, ...argv] = args;
25
+ assert(dbPath, `replica file not specified`);
26
+ const logConfig = getLogConfig({
27
+ env,
28
+ argv
29
+ });
30
+ const workerName = "serving-checkpointer";
31
+ startOtelAuto(createLogContext(logConfig, workerName, 0, false), workerName, 0);
32
+ lc = createLogContext(logConfig, workerName);
33
+ return runUntilKilled(lc, parent, new ServingCheckpointer(lc, dbPath));
34
+ }
35
+ var isMain = typeof process.argv[1] === "string" && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
36
+ var parent = parentWorker;
37
+ if (isMain && parent !== null && !singleProcessMode()) exitAfter(() => lc, () => runWorker(parent, process.env, ...process.argv.slice(2)));
38
+ //#endregion
39
+ export { runWorker as default };
40
+
41
+ //# sourceMappingURL=serving-checkpointer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serving-checkpointer.js","names":[],"sources":["../../../../../zero-cache/src/server/serving-checkpointer.ts"],"sourcesContent":["import {resolve} from 'node:path';\nimport {fileURLToPath} from 'node:url';\nimport {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {getLogConfig} from '../config/zero-config.ts';\nimport {exitAfter, runUntilKilled} from '../services/life-cycle.ts';\nimport {ServingCheckpointer} from '../services/replicator/serving-checkpointer.ts';\nimport {\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\n\n// Default LogContext, overridden in runWorker\nlet lc = new LogContext('info', {}, consoleLogSink);\n\n/**\n * Checkpoints a serving replica on behalf of its replicator, which starts\n * this worker as one of its supporting processes once the replica is set up,\n * passing the path of its replica file as the first argument.\n *\n * Only the logging config is parsed from the remaining arguments, which\n * ignores everything else. In particular, `childWorker()` forwards the\n * replicator's own arguments, which start with its replica file mode and\n * would fail a full config parse.\n */\nexport default function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...args: string[]\n): Promise<void> {\n const [dbPath, ...argv] = args;\n assert(dbPath, `replica file not specified`);\n const logConfig = getLogConfig({env, argv});\n const workerName = 'serving-checkpointer';\n startOtelAuto(\n createLogContext(logConfig, workerName, 0, false),\n workerName,\n 0,\n );\n lc = createLogContext(logConfig, workerName);\n\n return runUntilKilled(lc, parent, new ServingCheckpointer(lc, dbPath));\n}\n\n// fork()\nconst isMain =\n typeof process.argv[1] === 'string' &&\n fileURLToPath(import.meta.url) === resolve(process.argv[1]);\n\nconst parent = parentWorker;\nif (isMain && parent !== null && !singleProcessMode()) {\n void exitAfter(\n () => lc,\n () => runWorker(parent, process.env, ...process.argv.slice(2)),\n );\n}\n"],"mappings":";;;;;;;;;;;AAgBA,IAAI,KAAK,IAAI,WAAW,QAAQ,CAAC,GAAG,cAAc;;;;;;;;;;;AAYlD,SAAwB,UACtB,QACA,KACA,GAAG,MACY;CACf,MAAM,CAAC,QAAQ,GAAG,QAAQ;CAC1B,OAAO,QAAQ,4BAA4B;CAC3C,MAAM,YAAY,aAAa;EAAC;EAAK;CAAI,CAAC;CAC1C,MAAM,aAAa;CACnB,cACE,iBAAiB,WAAW,YAAY,GAAG,KAAK,GAChD,YACA,CACF;CACA,KAAK,iBAAiB,WAAW,UAAU;CAE3C,OAAO,eAAe,IAAI,QAAQ,IAAI,oBAAoB,IAAI,MAAM,CAAC;AACvE;AAGA,IAAM,SACJ,OAAO,QAAQ,KAAK,OAAO,YAC3B,cAAc,YAAY,GAAG,MAAM,QAAQ,QAAQ,KAAK,EAAE;AAE5D,IAAM,SAAS;AACf,IAAI,UAAU,WAAW,QAAQ,CAAC,kBAAkB,GAClD,gBACQ,UACA,UAAU,QAAQ,QAAQ,KAAK,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC,CAC/D"}
@@ -3,6 +3,7 @@ export declare const MAIN_URL: URL;
3
3
  export declare const MUTATOR_URL: URL;
4
4
  export declare const REAPER_URL: URL;
5
5
  export declare const REPLICATOR_URL: URL;
6
+ export declare const SERVING_CHECKPOINTER_URL: URL;
6
7
  export declare const SHADOW_SYNCER_URL: URL;
7
8
  export declare const SLOT_KEEPER_URL: URL;
8
9
  export declare const SYNCER_URL: URL;
@@ -1 +1 @@
1
- {"version":3,"file":"worker-urls.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/worker-urls.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,mBAAmB,KAAkC,CAAC;AACnE,eAAO,MAAM,QAAQ,KAAuB,CAAC;AAC7C,eAAO,MAAM,WAAW,KAA0B,CAAC;AACnD,eAAO,MAAM,UAAU,KAAyB,CAAC;AACjD,eAAO,MAAM,cAAc,KAA6B,CAAC;AACzD,eAAO,MAAM,iBAAiB,KAAgC,CAAC;AAC/D,eAAO,MAAM,eAAe,KAA8B,CAAC;AAC3D,eAAO,MAAM,UAAU,KAAyB,CAAC;AACjD,eAAO,MAAM,gBAAgB,KAA+B,CAAC"}
1
+ {"version":3,"file":"worker-urls.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/worker-urls.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,mBAAmB,KAAkC,CAAC;AACnE,eAAO,MAAM,QAAQ,KAAuB,CAAC;AAC7C,eAAO,MAAM,WAAW,KAA0B,CAAC;AACnD,eAAO,MAAM,UAAU,KAAyB,CAAC;AACjD,eAAO,MAAM,cAAc,KAA6B,CAAC;AACzD,eAAO,MAAM,wBAAwB,KAAuC,CAAC;AAC7E,eAAO,MAAM,iBAAiB,KAAgC,CAAC;AAC/D,eAAO,MAAM,eAAe,KAA8B,CAAC;AAC3D,eAAO,MAAM,UAAU,KAAyB,CAAC;AACjD,eAAO,MAAM,gBAAgB,KAA+B,CAAC"}
@@ -10,11 +10,12 @@ var MAIN_URL = resolve("./main.ts");
10
10
  resolve("./mutator.ts");
11
11
  var REAPER_URL = resolve("./reaper.ts");
12
12
  var REPLICATOR_URL = resolve("./replicator.ts");
13
+ var SERVING_CHECKPOINTER_URL = resolve("./serving-checkpointer.ts");
13
14
  var SHADOW_SYNCER_URL = resolve("./shadow-syncer.ts");
14
15
  var SLOT_KEEPER_URL = resolve("./slot-keeper.ts");
15
16
  var SYNCER_URL = resolve("./syncer.ts");
16
17
  var WRITE_WORKER_URL = resolve("./write-worker.ts");
17
18
  //#endregion
18
- export { CHANGE_STREAMER_URL, MAIN_URL, REAPER_URL, REPLICATOR_URL, SHADOW_SYNCER_URL, SLOT_KEEPER_URL, SYNCER_URL, WRITE_WORKER_URL };
19
+ export { CHANGE_STREAMER_URL, MAIN_URL, REAPER_URL, REPLICATOR_URL, SERVING_CHECKPOINTER_URL, SHADOW_SYNCER_URL, SLOT_KEEPER_URL, SYNCER_URL, WRITE_WORKER_URL };
19
20
 
20
21
  //# sourceMappingURL=worker-urls.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"worker-urls.js","names":[],"sources":["../../../../../zero-cache/src/server/worker-urls.ts"],"sourcesContent":["// This module provides URLs for worker files.\n\nconst tsRe = /\\.ts$/;\n\nfunction resolve(path: string): URL {\n const {url} = import.meta;\n if (url.endsWith('.js')) {\n // When compiled, change .ts to .js\n path = path.replace(tsRe, '.js');\n }\n return new URL(path, url);\n}\n\n// These URLs are part of the build process. See ../../zero/tool/build.ts\n// All these urls must be relative to this file and be located in the same directory.\n\nexport const CHANGE_STREAMER_URL = resolve('./change-streamer.ts');\nexport const MAIN_URL = resolve('./main.ts');\nexport const MUTATOR_URL = resolve('./mutator.ts');\nexport const REAPER_URL = resolve('./reaper.ts');\nexport const REPLICATOR_URL = resolve('./replicator.ts');\nexport const SHADOW_SYNCER_URL = resolve('./shadow-syncer.ts');\nexport const SLOT_KEEPER_URL = resolve('./slot-keeper.ts');\nexport const SYNCER_URL = resolve('./syncer.ts');\nexport const WRITE_WORKER_URL = resolve('./write-worker.ts');\n"],"mappings":";AAEA,IAAM,OAAO;AAEb,SAAS,QAAQ,MAAmB;CAClC,MAAM,EAAC,QAAO;CACd,IAAI,IAAI,SAAS,KAAK,GAEpB,OAAO,KAAK,QAAQ,MAAM,KAAK;CAEjC,OAAO,IAAI,IAAI,MAAM,GAAG;AAC1B;AAKA,IAAa,sBAAsB,QAAQ,sBAAsB;AACjE,IAAa,WAAW,QAAQ,WAAW;AAChB,QAAQ,cAAc;AACjD,IAAa,aAAa,QAAQ,aAAa;AAC/C,IAAa,iBAAiB,QAAQ,iBAAiB;AACvD,IAAa,oBAAoB,QAAQ,oBAAoB;AAC7D,IAAa,kBAAkB,QAAQ,kBAAkB;AACzD,IAAa,aAAa,QAAQ,aAAa;AAC/C,IAAa,mBAAmB,QAAQ,mBAAmB"}
1
+ {"version":3,"file":"worker-urls.js","names":[],"sources":["../../../../../zero-cache/src/server/worker-urls.ts"],"sourcesContent":["// This module provides URLs for worker files.\n\nconst tsRe = /\\.ts$/;\n\nfunction resolve(path: string): URL {\n const {url} = import.meta;\n if (url.endsWith('.js')) {\n // When compiled, change .ts to .js\n path = path.replace(tsRe, '.js');\n }\n return new URL(path, url);\n}\n\n// These URLs are part of the build process. See ../../zero/tool/build.ts\n// All these urls must be relative to this file and be located in the same directory.\n\nexport const CHANGE_STREAMER_URL = resolve('./change-streamer.ts');\nexport const MAIN_URL = resolve('./main.ts');\nexport const MUTATOR_URL = resolve('./mutator.ts');\nexport const REAPER_URL = resolve('./reaper.ts');\nexport const REPLICATOR_URL = resolve('./replicator.ts');\nexport const SERVING_CHECKPOINTER_URL = resolve('./serving-checkpointer.ts');\nexport const SHADOW_SYNCER_URL = resolve('./shadow-syncer.ts');\nexport const SLOT_KEEPER_URL = resolve('./slot-keeper.ts');\nexport const SYNCER_URL = resolve('./syncer.ts');\nexport const WRITE_WORKER_URL = resolve('./write-worker.ts');\n"],"mappings":";AAEA,IAAM,OAAO;AAEb,SAAS,QAAQ,MAAmB;CAClC,MAAM,EAAC,QAAO;CACd,IAAI,IAAI,SAAS,KAAK,GAEpB,OAAO,KAAK,QAAQ,MAAM,KAAK;CAEjC,OAAO,IAAI,IAAI,MAAM,GAAG;AAC1B;AAKA,IAAa,sBAAsB,QAAQ,sBAAsB;AACjE,IAAa,WAAW,QAAQ,WAAW;AAChB,QAAQ,cAAc;AACjD,IAAa,aAAa,QAAQ,aAAa;AAC/C,IAAa,iBAAiB,QAAQ,iBAAiB;AACvD,IAAa,2BAA2B,QAAQ,2BAA2B;AAC3E,IAAa,oBAAoB,QAAQ,oBAAoB;AAC7D,IAAa,kBAAkB,QAAQ,kBAAkB;AACzD,IAAa,aAAa,QAAQ,aAAa;AAC/C,IAAa,mBAAmB,QAAQ,mBAAmB"}
@@ -3,11 +3,11 @@ import { parse } from "../../../../../shared/src/valita.js";
3
3
  import { must } from "../../../../../shared/src/must.js";
4
4
  import { randomCharacters } from "../../../../../shared/src/random-values.js";
5
5
  import { equals } from "../../../../../shared/src/set-utils.js";
6
+ import { id } from "../../../types/sql.js";
6
7
  import { connectPgClient, pgClient } from "../../../types/pg.js";
7
8
  import { toStateVersionString } from "./lsn.js";
8
9
  import { READONLY } from "../../../db/mode-enum.js";
9
10
  import { getPublicationInfo } from "./schema/published.js";
10
- import { id } from "../../../types/sql.js";
11
11
  import { Queue } from "../../../../../shared/src/queue.js";
12
12
  import { resumePoint } from "../protocol/backfill-progress.js";
13
13
  import { SchemaIncompatibilityError } from "../common/backfill-manager.js";
@@ -2,6 +2,7 @@ import { assert } from "../../../../../shared/src/asserts.js";
2
2
  import { must } from "../../../../../shared/src/must.js";
3
3
  import { equals } from "../../../../../shared/src/set-utils.js";
4
4
  import { ALLOWED_APP_ID_CHARACTERS } from "../../../types/shards.js";
5
+ import { id } from "../../../types/sql.js";
5
6
  import { Database } from "../../../../../zqlite/src/db.js";
6
7
  import { connectPgClient } from "../../../types/pg.js";
7
8
  import { getOrCreateCounter, getOrCreateHistogram } from "../../../observability/metrics.js";
@@ -16,7 +17,6 @@ import { toStateVersionString } from "./lsn.js";
16
17
  import { READONLY } from "../../../db/mode-enum.js";
17
18
  import { runTx } from "../../../db/run-transaction.js";
18
19
  import { getPublicationInfo, warnForSkippedIndexes } from "./schema/published.js";
19
- import { id } from "../../../types/sql.js";
20
20
  import "./schema/replica-stage-enum.js";
21
21
  import { dropShard, getInternalShardConfig, getReplicaState, initInitialSyncReplica, validatePublications } from "./schema/shard.js";
22
22
  import { createLiteIndexStatement, createLiteTableStatement } from "../../../db/create.js";
@@ -1,10 +1,10 @@
1
1
  import { sleep } from "../../../../../../shared/src/sleep.js";
2
2
  import { childWorker } from "../../../../types/processes.js";
3
+ import { id, lit } from "../../../../types/sql.js";
3
4
  import "../../../../types/pg.js";
4
5
  import { getOrCreateHistogram } from "../../../../observability/metrics.js";
5
6
  import { SLOT_KEEPER_URL } from "../../../../server/worker-urls.js";
6
7
  import { fromBigInt, toBigInt } from "../lsn.js";
7
- import { id, lit } from "../../../../types/sql.js";
8
8
  import { Subscription } from "../../../../types/subscription.js";
9
9
  import { pipe } from "../../../../types/streams.js";
10
10
  import { AutoResetSignal } from "../../../change-streamer/schema/tables.js";
@@ -1,8 +1,8 @@
1
1
  import { assert } from "../../../../../../shared/src/asserts.js";
2
2
  import { literalUnion, valita_exports } from "../../../../../../shared/src/valita.js";
3
3
  import { upstreamSchema } from "../../../../types/shards.js";
4
- import { publishedSchema, publishedSchemaQuery } from "./published.js";
5
4
  import { id } from "../../../../types/sql.js";
5
+ import { publishedSchema, publishedSchemaQuery } from "./published.js";
6
6
  import { literal } from "pg-format";
7
7
  //#region ../zero-cache/src/services/change-source/pg/schema/ddl.ts
8
8
  var PROTOCOL_VERSION = 1;
@@ -1,8 +1,8 @@
1
1
  import { assert } from "../../../../../../shared/src/asserts.js";
2
2
  import { parse, valita_exports } from "../../../../../../shared/src/valita.js";
3
3
  import { upstreamSchema } from "../../../../types/shards.js";
4
- import { publishedSchema } from "./published.js";
5
4
  import { id } from "../../../../types/sql.js";
5
+ import { publishedSchema } from "./published.js";
6
6
  import "./replica-stage-enum.js";
7
7
  import { getMutationsTableDefinition, legacyReplicationSlot, metadataPublicationName, setupTablesAndReplication, setupTriggers } from "./shard.js";
8
8
  import { AutoResetSignal } from "../../../change-streamer/schema/tables.js";
@@ -2,8 +2,8 @@ import { assert } from "../../../../../../shared/src/asserts.js";
2
2
  import { parse, valita_exports } from "../../../../../../shared/src/valita.js";
3
3
  import { jsonObjectSchema, stringify } from "../../../../../../shared/src/bigint-json.js";
4
4
  import { appSchema, check, upstreamSchema } from "../../../../types/shards.js";
5
- import { getPublicationInfo, publishedSchema } from "./published.js";
6
5
  import { id } from "../../../../types/sql.js";
6
+ import { getPublicationInfo, publishedSchema } from "./published.js";
7
7
  import { createEventFunctionStatements, createEventTriggerStatements } from "./ddl.js";
8
8
  import { replicaStageSchema } from "./replica-stage-enum.js";
9
9
  import { validate } from "./validation.js";
@@ -28,6 +28,12 @@ export declare class HttpService implements Service {
28
28
  * is draining (see {@link SelfDrain}).
29
29
  */
30
30
  failHealthChecks(): void;
31
+ /**
32
+ * Responds to subsequent readiness checks ("/") with a 503, while liveness
33
+ * checks ("/keepalive") continue to succeed, e.g. to signal the load
34
+ * balancer to stop routing to a server that is about to drain.
35
+ */
36
+ failReadiness(): void;
31
37
  /**
32
38
  * Stops accepting new connections (e.g. when draining), while existing
33
39
  * connections, including websockets, continue to be served until
@@ -1 +1 @@
1
- {"version":3,"file":"http-service.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/http-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAgB,EAAC,KAAK,eAAe,EAAC,MAAM,SAAS,CAAC;AAGtD,OAAO,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAE1C,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IAGvC,aAAa,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;CAC9C,CAAC;AAEF;;;;GAIG;AACH,qBAAa,WAAY,YAAW,OAAO;;IACzC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IAGnC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAOxC,YACE,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAczD;IAGD,SAAS,CAAC,QAAQ,SAAK;IACvB,SAAS,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAEjC;IAGK,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAwB7B;IAEK,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAGzB;IAED;;;;OAIG;IACH,gBAAgB,SAKf;IAED;;;;OAIG;IACH,aAAa,SAKZ;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1B;CACF"}
1
+ {"version":3,"file":"http-service.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/http-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAgB,EAAC,KAAK,eAAe,EAAC,MAAM,SAAS,CAAC;AAGtD,OAAO,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAE1C,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC;IAGvC,aAAa,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;CAC9C,CAAC;AAEF;;;;GAIG;AACH,qBAAa,WAAY,YAAW,OAAO;;IACzC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IAGnC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAQxC,YACE,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAczD;IAGD,SAAS,CAAC,QAAQ,SAAK;IACvB,SAAS,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAEjC;IAGK,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAwB7B;IAEK,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAGzB;IAED;;;;OAIG;IACH,gBAAgB,SAKf;IAED;;;;OAIG;IACH,aAAa,SAKZ;IAED;;;;OAIG;IACH,aAAa,SAKZ;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1B;CACF"}
@@ -17,6 +17,7 @@ var HttpService = class {
17
17
  #heartbeatMonitor;
18
18
  #init;
19
19
  #ready = false;
20
+ #failReadiness = false;
20
21
  #failHealthChecks = false;
21
22
  constructor(id, lc, opts, init) {
22
23
  const { port, keepaliveTimeoutMs, readinessGate = promiseVoid } = opts;
@@ -35,7 +36,7 @@ var HttpService = class {
35
36
  }
36
37
  async start() {
37
38
  this.#fastify.get("/", (_req, res) => {
38
- if (this.#failHealthChecks) res.code(503).send("Draining");
39
+ if (this.#failReadiness || this.#failHealthChecks) res.code(503).send("Draining");
39
40
  else if (this.#ready) res.send("OK");
40
41
  });
41
42
  this.#fastify.get("/keepalive", ({ headers }, res) => {
@@ -68,6 +69,17 @@ var HttpService = class {
68
69
  }
69
70
  }
70
71
  /**
72
+ * Responds to subsequent readiness checks ("/") with a 503, while liveness
73
+ * checks ("/keepalive") continue to succeed, e.g. to signal the load
74
+ * balancer to stop routing to a server that is about to drain.
75
+ */
76
+ failReadiness() {
77
+ if (!this.#failReadiness) {
78
+ this._lc.info?.(`${this.id}: failing readiness checks before draining`);
79
+ this.#failReadiness = true;
80
+ }
81
+ }
82
+ /**
71
83
  * Stops accepting new connections (e.g. when draining), while existing
72
84
  * connections, including websockets, continue to be served until
73
85
  * {@link stop} is called.
@@ -1 +1 @@
1
- {"version":3,"file":"http-service.js","names":[],"sources":["../../../../../zero-cache/src/services/http-service.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport Fastify, {type FastifyInstance} from 'fastify';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {HeartbeatMonitor} from './life-cycle.ts';\nimport {RunningState} from './running-state.ts';\nimport type {Service} from './service.ts';\n\nexport type Options = {\n port: number;\n keepaliveTimeoutMs: number | undefined;\n\n // Wait for the readinessGate to resolve before responding to health checks.\n readinessGate?: Promise<unknown> | undefined;\n};\n\n/**\n * Common functionality for all HttpServices. These include:\n * * Responding to health checks at \"/\"\n * * Tracking optional heartbeats at \"/keepalive\" and draining when they stop.\n */\nexport class HttpService implements Service {\n readonly id: string;\n protected readonly _lc: LogContext;\n readonly #fastify: FastifyInstance;\n readonly #port: number;\n protected readonly _state: RunningState;\n readonly #heartbeatMonitor: HeartbeatMonitor | undefined;\n readonly #init: (fastify: FastifyInstance) => void | Promise<void>;\n\n #ready = false;\n #failHealthChecks = false;\n\n constructor(\n id: string,\n lc: LogContext,\n opts: Options,\n init: (fastify: FastifyInstance) => void | Promise<void>,\n ) {\n const {port, keepaliveTimeoutMs, readinessGate = promiseVoid} = opts;\n this.id = id;\n this._lc = lc.withContext('component', this.id);\n this.#fastify = Fastify();\n this.#port = port;\n this.#init = init;\n this._state = new RunningState(id);\n this.#heartbeatMonitor = keepaliveTimeoutMs\n ? new HeartbeatMonitor(this._lc, keepaliveTimeoutMs)\n : undefined;\n\n void readinessGate.then(() => (this.#ready = true));\n }\n\n // Life-cycle hooks for subclass implementations\n protected _onStart() {}\n protected _onStop(): Promise<void> {\n return promiseVoid;\n }\n // start() is used in unit tests.\n // run() is the lifecycle method called by the ServiceRunner.\n async start(): Promise<string> {\n this.#fastify.get('/', (_req, res) => {\n if (this.#failHealthChecks) {\n res.code(503).send('Draining');\n } else if (this.#ready) {\n res.send('OK');\n }\n });\n this.#fastify.get('/keepalive', ({headers}, res) => {\n this.#heartbeatMonitor?.onHeartbeat(headers);\n if (this.#failHealthChecks) {\n res.code(503).send('Draining');\n } else if (this.#ready) {\n res.send('OK');\n }\n });\n await this.#init(this.#fastify);\n const address = await this.#fastify.listen({\n host: '::',\n port: this.#port,\n });\n this._lc.info?.(`${this.id} listening at ${address}`);\n this._onStart();\n return address;\n }\n\n async run(): Promise<void> {\n await this.start();\n await this._state.stopped();\n }\n\n /**\n * Responds to subsequent health checks (both \"/\" and \"/keepalive\") with a\n * 503, e.g. to signal the load balancer to stop routing to a server that\n * is draining (see {@link SelfDrain}).\n */\n failHealthChecks() {\n if (!this.#failHealthChecks) {\n this._lc.info?.(`${this.id}: failing health checks while draining`);\n this.#failHealthChecks = true;\n }\n }\n\n /**\n * Stops accepting new connections (e.g. when draining), while existing\n * connections, including websockets, continue to be served until\n * {@link stop} is called.\n */\n stopAccepting() {\n if (this.#fastify.server.listening) {\n this._lc.info?.(`${this.id}: no longer accepting new connections`);\n this.#fastify.server.close();\n }\n }\n\n async stop(): Promise<void> {\n this._lc.info?.(`${this.id}: no longer accepting connections`);\n this.#heartbeatMonitor?.stop();\n this._state.stop(this._lc);\n await this.#fastify.close();\n await this._onStop();\n }\n}\n"],"mappings":";;;;;;;;;;AAoBA,IAAa,cAAb,MAA4C;CAC1C;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,SAAS;CACT,oBAAoB;CAEpB,YACE,IACA,IACA,MACA,MACA;EACA,MAAM,EAAC,MAAM,oBAAoB,gBAAgB,gBAAe;EAChE,KAAK,KAAK;EACV,KAAK,MAAM,GAAG,YAAY,aAAa,KAAK,EAAE;EAC9C,KAAK,WAAW,QAAQ;EACxB,KAAK,QAAQ;EACb,KAAK,QAAQ;EACb,KAAK,SAAS,IAAI,aAAa,EAAE;EACjC,KAAK,oBAAoB,qBACrB,IAAI,iBAAiB,KAAK,KAAK,kBAAkB,IACjD,KAAA;EAEJ,cAAmB,WAAY,KAAK,SAAS,IAAK;CACpD;CAGA,WAAqB,CAAC;CACtB,UAAmC;EACjC,OAAO;CACT;CAGA,MAAM,QAAyB;EAC7B,KAAK,SAAS,IAAI,MAAM,MAAM,QAAQ;GACpC,IAAI,KAAK,mBACP,IAAI,KAAK,GAAG,CAAC,CAAC,KAAK,UAAU;QACxB,IAAI,KAAK,QACd,IAAI,KAAK,IAAI;EAEjB,CAAC;EACD,KAAK,SAAS,IAAI,eAAe,EAAC,WAAU,QAAQ;GAClD,KAAK,mBAAmB,YAAY,OAAO;GAC3C,IAAI,KAAK,mBACP,IAAI,KAAK,GAAG,CAAC,CAAC,KAAK,UAAU;QACxB,IAAI,KAAK,QACd,IAAI,KAAK,IAAI;EAEjB,CAAC;EACD,MAAM,KAAK,MAAM,KAAK,QAAQ;EAC9B,MAAM,UAAU,MAAM,KAAK,SAAS,OAAO;GACzC,MAAM;GACN,MAAM,KAAK;EACb,CAAC;EACD,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,gBAAgB,SAAS;EACpD,KAAK,SAAS;EACd,OAAO;CACT;CAEA,MAAM,MAAqB;EACzB,MAAM,KAAK,MAAM;EACjB,MAAM,KAAK,OAAO,QAAQ;CAC5B;;;;;;CAOA,mBAAmB;EACjB,IAAI,CAAC,KAAK,mBAAmB;GAC3B,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,uCAAuC;GAClE,KAAK,oBAAoB;EAC3B;CACF;;;;;;CAOA,gBAAgB;EACd,IAAI,KAAK,SAAS,OAAO,WAAW;GAClC,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,sCAAsC;GACjE,KAAK,SAAS,OAAO,MAAM;EAC7B;CACF;CAEA,MAAM,OAAsB;EAC1B,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,kCAAkC;EAC7D,KAAK,mBAAmB,KAAK;EAC7B,KAAK,OAAO,KAAK,KAAK,GAAG;EACzB,MAAM,KAAK,SAAS,MAAM;EAC1B,MAAM,KAAK,QAAQ;CACrB;AACF"}
1
+ {"version":3,"file":"http-service.js","names":[],"sources":["../../../../../zero-cache/src/services/http-service.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport Fastify, {type FastifyInstance} from 'fastify';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {HeartbeatMonitor} from './life-cycle.ts';\nimport {RunningState} from './running-state.ts';\nimport type {Service} from './service.ts';\n\nexport type Options = {\n port: number;\n keepaliveTimeoutMs: number | undefined;\n\n // Wait for the readinessGate to resolve before responding to health checks.\n readinessGate?: Promise<unknown> | undefined;\n};\n\n/**\n * Common functionality for all HttpServices. These include:\n * * Responding to health checks at \"/\"\n * * Tracking optional heartbeats at \"/keepalive\" and draining when they stop.\n */\nexport class HttpService implements Service {\n readonly id: string;\n protected readonly _lc: LogContext;\n readonly #fastify: FastifyInstance;\n readonly #port: number;\n protected readonly _state: RunningState;\n readonly #heartbeatMonitor: HeartbeatMonitor | undefined;\n readonly #init: (fastify: FastifyInstance) => void | Promise<void>;\n\n #ready = false;\n #failReadiness = false;\n #failHealthChecks = false;\n\n constructor(\n id: string,\n lc: LogContext,\n opts: Options,\n init: (fastify: FastifyInstance) => void | Promise<void>,\n ) {\n const {port, keepaliveTimeoutMs, readinessGate = promiseVoid} = opts;\n this.id = id;\n this._lc = lc.withContext('component', this.id);\n this.#fastify = Fastify();\n this.#port = port;\n this.#init = init;\n this._state = new RunningState(id);\n this.#heartbeatMonitor = keepaliveTimeoutMs\n ? new HeartbeatMonitor(this._lc, keepaliveTimeoutMs)\n : undefined;\n\n void readinessGate.then(() => (this.#ready = true));\n }\n\n // Life-cycle hooks for subclass implementations\n protected _onStart() {}\n protected _onStop(): Promise<void> {\n return promiseVoid;\n }\n // start() is used in unit tests.\n // run() is the lifecycle method called by the ServiceRunner.\n async start(): Promise<string> {\n this.#fastify.get('/', (_req, res) => {\n if (this.#failReadiness || this.#failHealthChecks) {\n res.code(503).send('Draining');\n } else if (this.#ready) {\n res.send('OK');\n }\n });\n this.#fastify.get('/keepalive', ({headers}, res) => {\n this.#heartbeatMonitor?.onHeartbeat(headers);\n if (this.#failHealthChecks) {\n res.code(503).send('Draining');\n } else if (this.#ready) {\n res.send('OK');\n }\n });\n await this.#init(this.#fastify);\n const address = await this.#fastify.listen({\n host: '::',\n port: this.#port,\n });\n this._lc.info?.(`${this.id} listening at ${address}`);\n this._onStart();\n return address;\n }\n\n async run(): Promise<void> {\n await this.start();\n await this._state.stopped();\n }\n\n /**\n * Responds to subsequent health checks (both \"/\" and \"/keepalive\") with a\n * 503, e.g. to signal the load balancer to stop routing to a server that\n * is draining (see {@link SelfDrain}).\n */\n failHealthChecks() {\n if (!this.#failHealthChecks) {\n this._lc.info?.(`${this.id}: failing health checks while draining`);\n this.#failHealthChecks = true;\n }\n }\n\n /**\n * Responds to subsequent readiness checks (\"/\") with a 503, while liveness\n * checks (\"/keepalive\") continue to succeed, e.g. to signal the load\n * balancer to stop routing to a server that is about to drain.\n */\n failReadiness() {\n if (!this.#failReadiness) {\n this._lc.info?.(`${this.id}: failing readiness checks before draining`);\n this.#failReadiness = true;\n }\n }\n\n /**\n * Stops accepting new connections (e.g. when draining), while existing\n * connections, including websockets, continue to be served until\n * {@link stop} is called.\n */\n stopAccepting() {\n if (this.#fastify.server.listening) {\n this._lc.info?.(`${this.id}: no longer accepting new connections`);\n this.#fastify.server.close();\n }\n }\n\n async stop(): Promise<void> {\n this._lc.info?.(`${this.id}: no longer accepting connections`);\n this.#heartbeatMonitor?.stop();\n this._state.stop(this._lc);\n await this.#fastify.close();\n await this._onStop();\n }\n}\n"],"mappings":";;;;;;;;;;AAoBA,IAAa,cAAb,MAA4C;CAC1C;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,SAAS;CACT,iBAAiB;CACjB,oBAAoB;CAEpB,YACE,IACA,IACA,MACA,MACA;EACA,MAAM,EAAC,MAAM,oBAAoB,gBAAgB,gBAAe;EAChE,KAAK,KAAK;EACV,KAAK,MAAM,GAAG,YAAY,aAAa,KAAK,EAAE;EAC9C,KAAK,WAAW,QAAQ;EACxB,KAAK,QAAQ;EACb,KAAK,QAAQ;EACb,KAAK,SAAS,IAAI,aAAa,EAAE;EACjC,KAAK,oBAAoB,qBACrB,IAAI,iBAAiB,KAAK,KAAK,kBAAkB,IACjD,KAAA;EAEJ,cAAmB,WAAY,KAAK,SAAS,IAAK;CACpD;CAGA,WAAqB,CAAC;CACtB,UAAmC;EACjC,OAAO;CACT;CAGA,MAAM,QAAyB;EAC7B,KAAK,SAAS,IAAI,MAAM,MAAM,QAAQ;GACpC,IAAI,KAAK,kBAAkB,KAAK,mBAC9B,IAAI,KAAK,GAAG,CAAC,CAAC,KAAK,UAAU;QACxB,IAAI,KAAK,QACd,IAAI,KAAK,IAAI;EAEjB,CAAC;EACD,KAAK,SAAS,IAAI,eAAe,EAAC,WAAU,QAAQ;GAClD,KAAK,mBAAmB,YAAY,OAAO;GAC3C,IAAI,KAAK,mBACP,IAAI,KAAK,GAAG,CAAC,CAAC,KAAK,UAAU;QACxB,IAAI,KAAK,QACd,IAAI,KAAK,IAAI;EAEjB,CAAC;EACD,MAAM,KAAK,MAAM,KAAK,QAAQ;EAC9B,MAAM,UAAU,MAAM,KAAK,SAAS,OAAO;GACzC,MAAM;GACN,MAAM,KAAK;EACb,CAAC;EACD,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,gBAAgB,SAAS;EACpD,KAAK,SAAS;EACd,OAAO;CACT;CAEA,MAAM,MAAqB;EACzB,MAAM,KAAK,MAAM;EACjB,MAAM,KAAK,OAAO,QAAQ;CAC5B;;;;;;CAOA,mBAAmB;EACjB,IAAI,CAAC,KAAK,mBAAmB;GAC3B,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,uCAAuC;GAClE,KAAK,oBAAoB;EAC3B;CACF;;;;;;CAOA,gBAAgB;EACd,IAAI,CAAC,KAAK,gBAAgB;GACxB,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,2CAA2C;GACtE,KAAK,iBAAiB;EACxB;CACF;;;;;;CAOA,gBAAgB;EACd,IAAI,KAAK,SAAS,OAAO,WAAW;GAClC,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,sCAAsC;GACjE,KAAK,SAAS,OAAO,MAAM;EAC7B;CACF;CAEA,MAAM,OAAsB;EAC1B,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,kCAAkC;EAC7D,KAAK,mBAAmB,KAAK;EAC7B,KAAK,OAAO,KAAK,KAAK,GAAG;EACzB,MAAM,KAAK,SAAS,MAAM;EAC1B,MAAM,KAAK,QAAQ;CACrB;AACF"}
@@ -1,3 +1,4 @@
1
+ import { EventEmitter as NodeEventEmitter } from 'node:events';
1
2
  import type { IncomingHttpHeaders } from 'node:http';
2
3
  import type { EventEmitter } from 'stream';
3
4
  import type { LogContext } from '@rocicorp/logger';
@@ -94,6 +95,24 @@ export declare class SelfDrain {
94
95
  constructor(lc: LogContext, proc: EventEmitter, opts: SelfDrainOptions);
95
96
  request(): void;
96
97
  }
98
+ /**
99
+ * The event that a {@link PreDrain} emits immediately upon the first graceful
100
+ * shutdown signal, with that signal as its argument, before the delay.
101
+ */
102
+ export declare const PRE_DRAIN = "preDrain";
103
+ /**
104
+ * Delays graceful shutdown signals (`SIGTERM`, `SIGINT`), e.g. so that load
105
+ * balancers can observe a failed readiness check before the server begins
106
+ * to drain. The first graceful signal is announced immediately with a
107
+ * {@link PRE_DRAIN} event, and is re-emitted to listeners of the signal, such
108
+ * as those of the {@link ProcessManager} and {@link runUntilKilled}, once the
109
+ * delay has elapsed. Repeated graceful signals are dropped. All other events,
110
+ * including forceful signals, are forwarded immediately.
111
+ */
112
+ export declare class PreDrain extends NodeEventEmitter {
113
+ #private;
114
+ constructor(lc: LogContext, proc: EventEmitter, delayMs: number);
115
+ }
97
116
  /**
98
117
  * The maximum time that {@link runUntilKilled} waits for its services to stop
99
118
  * (i.e. for their `run()` to settle) after stopping them.
@@ -1 +1 @@
1
- {"version":3,"file":"life-cycle.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,WAAW,CAAC;AAEnD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,QAAQ,CAAC;AACzC,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AASjD,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,YAAY,CAAC;AAEtD,eAAO,MAAM,iBAAiB,YAAI,SAAS,EAAE,QAAQ,CAAU,CAAC;AAChE,eAAO,MAAM,iBAAiB,YAAI,SAAS,EAAE,SAAS,CAAU,CAAC;AA8CjE,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,QAEzD;AAaD,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAKjD,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAElD;;;GAGG;AACH,qBAAa,cAAc;;IAWzB,YAAY,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAyC7C;IAEK,QAAQ,kBAKb;IAuBD,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAsB7D;IAKD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAwBhE;IAED,YAAY,IAAI,MAAM,EAAE,CAEvB;IAEK,eAAe,kBAEpB;IAED,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,QAGzC;CAqFF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,mBAAoB,YAAW,gBAAgB;;IAC1D,QAAQ,CAAC,EAAE,0BAA0B;IAOrC,YAAY,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAe/C;IAED,GAAG,CAAC,CAAC,SAAS,UAAU,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAclD;IAED,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CASnB;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ1B;CAoCF;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,qBAAa,SAAS;;IASpB,YAAY,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,gBAAgB,EAYrE;IAED,OAAO,SAeN;CACF;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,QAAS,CAAC;AAEtC;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,YAAY,EACpB,GAAG,QAAQ,EAAE,gBAAgB,EAAE,GAC9B,OAAO,CAAC,IAAI,CAAC,CA2Ef;AA8BD,wBAAsB,SAAS,CAC7B,UAAU,EAAE,UAAU,GAAG,CAAC,MAAM,UAAU,CAAC,EAC3C,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,iBAoCzB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,gBAAgB;;IAQ3B,YAAY,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAG/C;IAED,WAAW,CAAC,UAAU,EAAE,mBAAmB,QAgB1C;IAsCD,IAAI,SAKH;CACF"}
1
+ {"version":3,"file":"life-cycle.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/life-cycle.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@ import { LONG_DURATION_HISTOGRAM_BOUNDARIES_S, getOrCreateHistogram } from "../o
7
7
  import { orTimeout } from "../types/timeout.js";
8
8
  import { RunningState } from "./running-state.js";
9
9
  import { pid } from "node:process";
10
+ import { EventEmitter } from "node:events";
10
11
  //#region ../zero-cache/src/services/life-cycle.ts
11
12
  var GRACEFUL_SHUTDOWN = ["SIGTERM", "SIGINT"];
12
13
  var FORCEFUL_SHUTDOWN = ["SIGQUIT", "SIGABRT"];
@@ -261,6 +262,57 @@ var SelfDrain = class {
261
262
  }
262
263
  };
263
264
  /**
265
+ * The event that a {@link PreDrain} emits immediately upon the first graceful
266
+ * shutdown signal, with that signal as its argument, before the delay.
267
+ */
268
+ var PRE_DRAIN = "preDrain";
269
+ /**
270
+ * Delays graceful shutdown signals (`SIGTERM`, `SIGINT`), e.g. so that load
271
+ * balancers can observe a failed readiness check before the server begins
272
+ * to drain. The first graceful signal is announced immediately with a
273
+ * {@link PRE_DRAIN} event, and is re-emitted to listeners of the signal, such
274
+ * as those of the {@link ProcessManager} and {@link runUntilKilled}, once the
275
+ * delay has elapsed. Repeated graceful signals are dropped. All other events,
276
+ * including forceful signals, are forwarded immediately.
277
+ */
278
+ var PreDrain = class extends EventEmitter {
279
+ #lc;
280
+ #proc;
281
+ #delayMs;
282
+ #forwarded = /* @__PURE__ */ new Set(["newListener", PRE_DRAIN]);
283
+ #signaled = false;
284
+ #timer;
285
+ constructor(lc, proc, delayMs) {
286
+ super();
287
+ this.#lc = lc.withContext("component", "pre-drain");
288
+ this.#proc = proc;
289
+ this.#delayMs = delayMs;
290
+ for (const signal of GRACEFUL_SHUTDOWN) {
291
+ this.#forwarded.add(signal);
292
+ proc.on(signal, () => this.#onGracefulSignal(signal));
293
+ }
294
+ this.on("newListener", (event) => this.#forward(event));
295
+ }
296
+ #forward(event) {
297
+ if (this.#forwarded.has(event)) return;
298
+ this.#forwarded.add(event);
299
+ this.#proc.on(event, (...args) => {
300
+ if (event === "disconnect" || FORCEFUL_SHUTDOWN.includes(event)) clearTimeout(this.#timer);
301
+ this.emit(event, ...args);
302
+ });
303
+ }
304
+ #onGracefulSignal(signal) {
305
+ if (this.#signaled) {
306
+ this.#lc.info?.(`already shutting down. ignoring ${signal}`);
307
+ return;
308
+ }
309
+ this.#signaled = true;
310
+ this.#lc.info?.(`received ${signal}. draining in ${this.#delayMs} ms`);
311
+ this.emit(PRE_DRAIN, signal);
312
+ this.#timer = setTimeout(() => this.emit(signal, signal), this.#delayMs);
313
+ }
314
+ };
315
+ /**
264
316
  * The maximum time that {@link runUntilKilled} waits for its services to stop
265
317
  * (i.e. for their `run()` to settle) after stopping them.
266
318
  */
@@ -397,6 +449,6 @@ var HeartbeatMonitor = class {
397
449
  }
398
450
  };
399
451
  //#endregion
400
- export { FORCEFUL_SHUTDOWN, GRACEFUL_SHUTDOWN, HeartbeatMonitor, ProcessManager, STOP_TIMEOUT_MS, SelfDrain, SupportingProcesses, exitAfter, recordStartupDurationMs, runUntilKilled };
452
+ export { FORCEFUL_SHUTDOWN, GRACEFUL_SHUTDOWN, HeartbeatMonitor, PRE_DRAIN, PreDrain, ProcessManager, STOP_TIMEOUT_MS, SelfDrain, SupportingProcesses, exitAfter, recordStartupDurationMs, runUntilKilled };
401
453
 
402
454
  //# sourceMappingURL=life-cycle.js.map