@rocicorp/zero 1.11.0-canary.36 → 1.11.0-canary.37

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 (84) hide show
  1. package/out/zero/package.js +1 -1
  2. package/out/zero/package.js.map +1 -1
  3. package/out/zero-cache/src/config/zero-config.d.ts +8 -0
  4. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  5. package/out/zero-cache/src/config/zero-config.js +31 -0
  6. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  7. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  8. package/out/zero-cache/src/server/change-streamer.js +2 -1
  9. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  10. package/out/zero-cache/src/server/main.d.ts.map +1 -1
  11. package/out/zero-cache/src/server/main.js +5 -1
  12. package/out/zero-cache/src/server/main.js.map +1 -1
  13. package/out/zero-cache/src/server/replicator.d.ts +4 -0
  14. package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
  15. package/out/zero-cache/src/server/replicator.js +18 -4
  16. package/out/zero-cache/src/server/replicator.js.map +1 -1
  17. package/out/zero-cache/src/server/runner/run-worker.d.ts.map +1 -1
  18. package/out/zero-cache/src/server/runner/run-worker.js +12 -6
  19. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  20. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +9 -3
  21. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  22. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +19 -6
  23. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  24. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +8 -0
  25. package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
  26. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +131 -62
  27. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  28. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +124 -2
  29. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  30. package/out/zero-cache/src/services/change-streamer/change-streamer.js +33 -2
  31. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  32. package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts +24 -0
  33. package/out/zero-cache/src/services/change-streamer/drain-tracker.d.ts.map +1 -0
  34. package/out/zero-cache/src/services/change-streamer/drain-tracker.js +67 -0
  35. package/out/zero-cache/src/services/change-streamer/drain-tracker.js.map +1 -0
  36. package/out/zero-cache/src/services/change-streamer/restore-admission.d.ts +214 -0
  37. package/out/zero-cache/src/services/change-streamer/restore-admission.d.ts.map +1 -0
  38. package/out/zero-cache/src/services/change-streamer/restore-admission.js +294 -0
  39. package/out/zero-cache/src/services/change-streamer/restore-admission.js.map +1 -0
  40. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts +13 -1
  41. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts.map +1 -1
  42. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js +7 -6
  43. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -1
  44. package/out/zero-cache/src/services/change-streamer/snapshot.d.ts +2 -2
  45. package/out/zero-cache/src/services/change-streamer/snapshot.d.ts.map +1 -1
  46. package/out/zero-cache/src/services/change-streamer/snapshot.js +2 -2
  47. package/out/zero-cache/src/services/change-streamer/snapshot.js.map +1 -1
  48. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts +39 -0
  49. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts.map +1 -1
  50. package/out/zero-cache/src/services/change-streamer/subscribe.js +19 -2
  51. package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -1
  52. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +22 -1
  53. package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
  54. package/out/zero-cache/src/services/change-streamer/subscriber.js +41 -15
  55. package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
  56. package/out/zero-cache/src/services/http-service.d.ts +6 -0
  57. package/out/zero-cache/src/services/http-service.d.ts.map +1 -1
  58. package/out/zero-cache/src/services/http-service.js +14 -2
  59. package/out/zero-cache/src/services/http-service.js.map +1 -1
  60. package/out/zero-cache/src/services/life-cycle.d.ts +33 -0
  61. package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
  62. package/out/zero-cache/src/services/life-cycle.js +56 -10
  63. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  64. package/out/zero-cache/src/services/litestream/commands.d.ts +8 -0
  65. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  66. package/out/zero-cache/src/services/litestream/commands.js +22 -1
  67. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  68. package/out/zero-cache/src/services/replicator/drain-handler.d.ts +74 -0
  69. package/out/zero-cache/src/services/replicator/drain-handler.d.ts.map +1 -0
  70. package/out/zero-cache/src/services/replicator/drain-handler.js +185 -0
  71. package/out/zero-cache/src/services/replicator/drain-handler.js.map +1 -0
  72. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts +2 -1
  73. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  74. package/out/zero-cache/src/services/replicator/incremental-sync.js +66 -13
  75. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  76. package/out/zero-cache/src/services/replicator/replicator.d.ts +2 -1
  77. package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
  78. package/out/zero-cache/src/services/replicator/replicator.js +2 -2
  79. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  80. package/out/zero-cache/src/types/processes.d.ts +8 -0
  81. package/out/zero-cache/src/types/processes.d.ts.map +1 -1
  82. package/out/zero-cache/src/types/processes.js.map +1 -1
  83. package/out/zero-client/src/client/version.js +1 -1
  84. package/package.json +1 -1
@@ -31,7 +31,20 @@ import { statusSchema } from "./snapshot-message.js";
31
31
  * passthrough parsing.
32
32
  */
33
33
  var snapshotStatusSchema = statusSchema.extend({ tag: valita_exports.literal("snapshot") });
34
- var reserveSnapshotMessageSchema = valita_exports.tuple([valita_exports.literal("reserve-snapshot"), valita_exports.object({ taskID: valita_exports.string() })]);
34
+ /**
35
+ * The state of the replica on the subscriber's disk, if any. This allows the
36
+ * replication-manager to distinguish a subscriber that is replacing its
37
+ * replica (i.e. one that was serving, see `RestoreAdmission`) from one that
38
+ * is restoring onto an empty disk.
39
+ */
40
+ var localReplicaSchema = valita_exports.object({
41
+ replicaVersion: valita_exports.string(),
42
+ watermark: valita_exports.string()
43
+ });
44
+ var reserveSnapshotMessageSchema = valita_exports.tuple([valita_exports.literal("reserve-snapshot"), valita_exports.object({
45
+ taskID: valita_exports.string(),
46
+ localReplica: localReplicaSchema.optional()
47
+ })]);
35
48
  /**
36
49
  * The subscription-initialization context, sent in the `start-subscription`
37
50
  * message. Equivalent to the previous URL-encoded `SubscriberContext`, minus
@@ -58,7 +71,11 @@ var subscribeContextSchema = valita_exports.object({
58
71
  * `watermark`. This should be considered non-optional as of protocol v8;
59
72
  * it is only absent from v7 subscribers.
60
73
  */
61
- backfills: valita_exports.array(backfillRequestSchema).optional()
74
+ backfills: valita_exports.array(backfillRequestSchema).optional(),
75
+ /** See `SubscriberContext.drainable`. */
76
+ drainable: valita_exports.boolean().optional(),
77
+ /** See `SubscriberContext.switchover`. */
78
+ switchover: valita_exports.boolean().optional()
62
79
  });
63
80
  var startSubscriptionMessageSchema = valita_exports.tuple([valita_exports.literal("start-subscription"), subscribeContextSchema]);
64
81
  var subscribeUpstreamSchema = valita_exports.union(reserveSnapshotMessageSchema, startSubscriptionMessageSchema);
@@ -1 +1 @@
1
- {"version":3,"file":"subscribe.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/subscribe.ts"],"sourcesContent":["/**\n * The `subscribe` API (protocol v7+) merges the previous `/snapshot` and\n * `/changes` connections into a single bidirectional WebSocket, so that the\n * replication-manager that reserves change-log entries for a soon-to-be\n * subscriber is the same replication-manager that serves the subscription.\n *\n * The connection is driven by an upstream (view-syncer → replication-manager)\n * application message that selects the flow:\n *\n * - **Fresh startup:** the view-syncer sends `['reserve-snapshot', {taskID}]`.\n * The change-streamer opens a snapshot reservation (pinning the change log)\n * and replies downstream with a single `['reserved', …]` message telling the\n * view-syncer where to restore the backup from and the watermark from which\n * catchup is possible. Once the backup is restored, the view-syncer sends\n * `['start-subscription', ctx]`, which transitions the same connection into\n * the subscription phase (change stream downstream). The reservation is\n * released only after the subscriber is registered, so there is never a gap\n * in which the change-streamer does not know the client's watermark.\n *\n * - **Reconnect (replica already valid):** the view-syncer sends\n * `['start-subscription', ctx]` immediately, with no reservation.\n *\n * The reservation status uses a distinct `'reserved'` tag (rather than the\n * change stream's `'status'`) so the downstream union is unambiguous under\n * passthrough parsing.\n */\n\nimport * as v from '../../../../shared/src/valita.ts';\nimport {backfillRequestSchema} from '../change-source/protocol/current.ts';\nimport {downstreamSchema} from './change-streamer.ts';\nimport {statusSchema} from './snapshot-message.ts';\n\nconst snapshotStatusSchema = statusSchema.extend({\n tag: v.literal('snapshot'), // Replace tag: 'status' with tag: 'snapshot'\n});\n\nexport type SnapshotStatus = v.Infer<typeof snapshotStatusSchema>;\n\n// ---------------------------------------------------------------------------\n// Upstream (view-syncer → replication-manager) — application control messages.\n// ---------------------------------------------------------------------------\n\nexport const reserveSnapshotMessageSchema = v.tuple([\n v.literal('reserve-snapshot'),\n v.object({taskID: v.string()}),\n]);\n\n/**\n * The subscription-initialization context, sent in the `start-subscription`\n * message. Equivalent to the previous URL-encoded `SubscriberContext`, minus\n * the fields that are now implicit on the merged connection:\n * - `protocolVersion` comes from the request path.\n * - `wsBatched` is always on (the transport batches internally).\n *\n * Parsed in `passthrough` mode so a forthcoming, orthogonal change can add\n * arbitrary table/column/backfill state without a protocol bump.\n */\nexport const subscribeContextSchema = v.object({\n /** Task ID, links the subscription to a preceding snapshot reservation. */\n taskID: v.string(),\n /** Subscriber id (debugging only). */\n id: v.string(),\n /** 'serving' (user-facing) or 'backup' (in-RM replicator). */\n mode: v.union(v.literal('serving'), v.literal('backup')),\n /** The replica version the subscriber restored / is running. */\n replicaVersion: v.string(),\n /** The watermark up to which the subscriber is up to date. */\n watermark: v.string(),\n /**\n * The subscriber's pending backfills (and their progress) as of its\n * `watermark`. This should be considered non-optional as of protocol v8;\n * it is only absent from v7 subscribers.\n */\n backfills: v.array(backfillRequestSchema).optional(),\n});\n\nexport type SubscribeContext = v.Infer<typeof subscribeContextSchema>;\n\nexport const startSubscriptionMessageSchema = v.tuple([\n v.literal('start-subscription'),\n subscribeContextSchema,\n]);\n\nexport const subscribeUpstreamSchema = v.union(\n reserveSnapshotMessageSchema,\n startSubscriptionMessageSchema,\n);\n\nexport type SubscribeUpstream = v.Infer<typeof subscribeUpstreamSchema>;\n\n// ---------------------------------------------------------------------------\n// Downstream (replication-manager → view-syncer).\n// ---------------------------------------------------------------------------\n\n/**\n * The reservation confirmation. Carries the same payload as the legacy\n * `/snapshot` status (`backupURL`, `replicaVersion`, `minWatermark`,\n * `replicaSize?`) but under a distinct `'reserved'` tag so it does not collide\n * with the change stream's `['status', …]` message.\n */\nexport const reservedMessageSchema = v.tuple([\n v.literal('reserved'),\n snapshotStatusSchema,\n]);\n\nexport type ReservedMessage = v.Infer<typeof reservedMessageSchema>;\n\nexport const subscribeDownstreamSchema = v.union(\n reservedMessageSchema,\n downstreamSchema,\n);\n\nexport type SubscribeDownstream = v.Infer<typeof subscribeDownstreamSchema>;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAM,uBAAuB,aAAa,OAAO,EAC/C,KAAK,eAAE,QAAQ,UAAU,EAC3B,CAAC;AAQD,IAAa,+BAA+B,eAAE,MAAM,CAClD,eAAE,QAAQ,kBAAkB,GAC5B,eAAE,OAAO,EAAC,QAAQ,eAAE,OAAO,EAAC,CAAC,CAC/B,CAAC;;;;;;;;;;;AAYD,IAAa,yBAAyB,eAAE,OAAO;;CAE7C,QAAQ,eAAE,OAAO;;CAEjB,IAAI,eAAE,OAAO;;CAEb,MAAM,eAAE,MAAM,eAAE,QAAQ,SAAS,GAAG,eAAE,QAAQ,QAAQ,CAAC;;CAEvD,gBAAgB,eAAE,OAAO;;CAEzB,WAAW,eAAE,OAAO;;;;;;CAMpB,WAAW,eAAE,MAAM,qBAAqB,EAAE,SAAS;AACrD,CAAC;AAID,IAAa,iCAAiC,eAAE,MAAM,CACpD,eAAE,QAAQ,oBAAoB,GAC9B,sBACF,CAAC;AAED,IAAa,0BAA0B,eAAE,MACvC,8BACA,8BACF;;;;;;;AAcA,IAAa,wBAAwB,eAAE,MAAM,CAC3C,eAAE,QAAQ,UAAU,GACpB,oBACF,CAAC;AAID,IAAa,4BAA4B,eAAE,MACzC,uBACA,gBACF"}
1
+ {"version":3,"file":"subscribe.js","names":[],"sources":["../../../../../../zero-cache/src/services/change-streamer/subscribe.ts"],"sourcesContent":["/**\n * The `subscribe` API (protocol v7+) merges the previous `/snapshot` and\n * `/changes` connections into a single bidirectional WebSocket, so that the\n * replication-manager that reserves change-log entries for a soon-to-be\n * subscriber is the same replication-manager that serves the subscription.\n *\n * The connection is driven by an upstream (view-syncer → replication-manager)\n * application message that selects the flow:\n *\n * - **Fresh startup:** the view-syncer sends `['reserve-snapshot', {taskID}]`.\n * The change-streamer opens a snapshot reservation (pinning the change log)\n * and replies downstream with a single `['reserved', …]` message telling the\n * view-syncer where to restore the backup from and the watermark from which\n * catchup is possible. Once the backup is restored, the view-syncer sends\n * `['start-subscription', ctx]`, which transitions the same connection into\n * the subscription phase (change stream downstream). The reservation is\n * released only after the subscriber is registered, so there is never a gap\n * in which the change-streamer does not know the client's watermark.\n *\n * - **Reconnect (replica already valid):** the view-syncer sends\n * `['start-subscription', ctx]` immediately, with no reservation.\n *\n * The reservation status uses a distinct `'reserved'` tag (rather than the\n * change stream's `'status'`) so the downstream union is unambiguous under\n * passthrough parsing.\n */\n\nimport * as v from '../../../../shared/src/valita.ts';\nimport {backfillRequestSchema} from '../change-source/protocol/current.ts';\nimport {downstreamSchema} from './change-streamer.ts';\nimport {statusSchema} from './snapshot-message.ts';\n\nconst snapshotStatusSchema = statusSchema.extend({\n tag: v.literal('snapshot'), // Replace tag: 'status' with tag: 'snapshot'\n});\n\nexport type SnapshotStatus = v.Infer<typeof snapshotStatusSchema>;\n\n// ---------------------------------------------------------------------------\n// Upstream (view-syncer → replication-manager) — application control messages.\n// ---------------------------------------------------------------------------\n\n/**\n * The state of the replica on the subscriber's disk, if any. This allows the\n * replication-manager to distinguish a subscriber that is replacing its\n * replica (i.e. one that was serving, see `RestoreAdmission`) from one that\n * is restoring onto an empty disk.\n */\nexport const localReplicaSchema = v.object({\n replicaVersion: v.string(),\n watermark: v.string(),\n});\n\nexport type LocalReplica = v.Infer<typeof localReplicaSchema>;\n\nexport const reserveSnapshotMessageSchema = v.tuple([\n v.literal('reserve-snapshot'),\n v.object({\n taskID: v.string(),\n localReplica: localReplicaSchema.optional(),\n }),\n]);\n\n/**\n * The subscription-initialization context, sent in the `start-subscription`\n * message. Equivalent to the previous URL-encoded `SubscriberContext`, minus\n * the fields that are now implicit on the merged connection:\n * - `protocolVersion` comes from the request path.\n * - `wsBatched` is always on (the transport batches internally).\n *\n * Parsed in `passthrough` mode so a forthcoming, orthogonal change can add\n * arbitrary table/column/backfill state without a protocol bump.\n */\nexport const subscribeContextSchema = v.object({\n /** Task ID, links the subscription to a preceding snapshot reservation. */\n taskID: v.string(),\n /** Subscriber id (debugging only). */\n id: v.string(),\n /** 'serving' (user-facing) or 'backup' (in-RM replicator). */\n mode: v.union(v.literal('serving'), v.literal('backup')),\n /** The replica version the subscriber restored / is running. */\n replicaVersion: v.string(),\n /** The watermark up to which the subscriber is up to date. */\n watermark: v.string(),\n /**\n * The subscriber's pending backfills (and their progress) as of its\n * `watermark`. This should be considered non-optional as of protocol v8;\n * it is only absent from v7 subscribers.\n */\n backfills: v.array(backfillRequestSchema).optional(),\n /** See `SubscriberContext.drainable`. */\n drainable: v.boolean().optional(),\n /** See `SubscriberContext.switchover`. */\n switchover: v.boolean().optional(),\n});\n\nexport type SubscribeContext = v.Infer<typeof subscribeContextSchema>;\n\nexport const startSubscriptionMessageSchema = v.tuple([\n v.literal('start-subscription'),\n subscribeContextSchema,\n]);\n\nexport const subscribeUpstreamSchema = v.union(\n reserveSnapshotMessageSchema,\n startSubscriptionMessageSchema,\n);\n\nexport type SubscribeUpstream = v.Infer<typeof subscribeUpstreamSchema>;\n\n// ---------------------------------------------------------------------------\n// Downstream (replication-manager → view-syncer).\n// ---------------------------------------------------------------------------\n\n/**\n * The reservation confirmation. Carries the same payload as the legacy\n * `/snapshot` status (`backupURL`, `replicaVersion`, `minWatermark`,\n * `replicaSize?`) but under a distinct `'reserved'` tag so it does not collide\n * with the change stream's `['status', …]` message.\n */\nexport const reservedMessageSchema = v.tuple([\n v.literal('reserved'),\n snapshotStatusSchema,\n]);\n\nexport type ReservedMessage = v.Infer<typeof reservedMessageSchema>;\n\nexport const subscribeDownstreamSchema = v.union(\n reservedMessageSchema,\n downstreamSchema,\n);\n\nexport type SubscribeDownstream = v.Infer<typeof subscribeDownstreamSchema>;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAM,uBAAuB,aAAa,OAAO,EAC/C,KAAK,eAAE,QAAQ,UAAU,EAC3B,CAAC;;;;;;;AAcD,IAAa,qBAAqB,eAAE,OAAO;CACzC,gBAAgB,eAAE,OAAO;CACzB,WAAW,eAAE,OAAO;AACtB,CAAC;AAID,IAAa,+BAA+B,eAAE,MAAM,CAClD,eAAE,QAAQ,kBAAkB,GAC5B,eAAE,OAAO;CACP,QAAQ,eAAE,OAAO;CACjB,cAAc,mBAAmB,SAAS;AAC5C,CAAC,CACH,CAAC;;;;;;;;;;;AAYD,IAAa,yBAAyB,eAAE,OAAO;;CAE7C,QAAQ,eAAE,OAAO;;CAEjB,IAAI,eAAE,OAAO;;CAEb,MAAM,eAAE,MAAM,eAAE,QAAQ,SAAS,GAAG,eAAE,QAAQ,QAAQ,CAAC;;CAEvD,gBAAgB,eAAE,OAAO;;CAEzB,WAAW,eAAE,OAAO;;;;;;CAMpB,WAAW,eAAE,MAAM,qBAAqB,EAAE,SAAS;;CAEnD,WAAW,eAAE,QAAQ,EAAE,SAAS;;CAEhC,YAAY,eAAE,QAAQ,EAAE,SAAS;AACnC,CAAC;AAID,IAAa,iCAAiC,eAAE,MAAM,CACpD,eAAE,QAAQ,oBAAoB,GAC9B,sBACF,CAAC;AAED,IAAa,0BAA0B,eAAE,MACvC,8BACA,8BACF;;;;;;;AAcA,IAAa,wBAAwB,eAAE,MAAM,CAC3C,eAAE,QAAQ,UAAU,GACpB,oBACF,CAAC;AAID,IAAa,4BAA4B,eAAE,MACzC,uBACA,gBACF"}
@@ -42,6 +42,11 @@ export type SubscriberOptions = {
42
42
  * readiness to serve traffic).
43
43
  */
44
44
  lagReportingDisabled?: Promise<boolean> | undefined;
45
+ /**
46
+ * Whether the subscriber switches over to another replication-manager
47
+ * when this one drains (see `DrainTracker`).
48
+ */
49
+ drainable?: boolean | undefined;
45
50
  };
46
51
  export type BacklogFullWait = {
47
52
  readonly promise: Promise<void>;
@@ -66,6 +71,7 @@ export declare class Subscriber {
66
71
  #private;
67
72
  readonly id: string;
68
73
  readonly mode: ReplicatorMode;
74
+ readonly drainable: boolean;
69
75
  constructor(protocolVersion: number, id: string, mode: ReplicatorMode, watermark: string, downstream: Subscription<string | PreSerializedBatch>, options?: SubscriberOptions);
70
76
  /**
71
77
  * The subscriber's tracked pending backfills, or `undefined` if the
@@ -77,6 +83,12 @@ export declare class Subscriber {
77
83
  * stream (see {@link SubscriberOptions.onAligned}).
78
84
  */
79
85
  get aligned(): boolean;
86
+ /**
87
+ * Resolves once the subscriber has caught up, i.e. once it has acked
88
+ * (durably applied) all changes up to the head of the stream at the time
89
+ * it was {@link aligned}. Never resolves if the subscriber is closed first.
90
+ */
91
+ get caughtUp(): Promise<void>;
80
92
  get watermark(): string;
81
93
  get acked(): string;
82
94
  /**
@@ -111,6 +123,13 @@ export declare class Subscriber {
111
123
  * dropped.
112
124
  */
113
125
  sendStatus(status: Status): void;
126
+ /**
127
+ * Sends a control status (e.g. `draining` or `restore`) downstream
128
+ * immediately, regardless of whether the subscriber is initialized or
129
+ * catching up. Unlike {@link sendStatus}, such statuses are not tied to a
130
+ * position in the replication stream.
131
+ */
132
+ sendControlStatus(status: Status): void;
114
133
  /** catchup() is called on ChangeEntries loaded from the store. */
115
134
  catchup(change: WatermarkedChange): Promise<void>;
116
135
  /**
@@ -160,7 +179,9 @@ export declare class Subscriber {
160
179
  * Callers are responsible for logging `err`; it is not carried downstream.
161
180
  */
162
181
  fail(_err?: unknown): void;
163
- close(error?: ErrorType, message?: string): void;
182
+ close(error?: ErrorType, message?: string, details?: {
183
+ replicaVersion?: string | undefined;
184
+ }): void;
164
185
  }
165
186
  /**
166
187
  * Sent to an aligned subscriber when lag reporting is disabled upstream,
@@ -1 +1 @@
1
- {"version":3,"file":"subscriber.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/subscriber.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"subscriber.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/subscriber.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,gCAAgC,CAAC;AAKzD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EACV,SAAS,EAET,MAAM,EACN,iBAAiB,EAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,KAAK,SAAS,MAAM,sBAAsB,CAAC;AAEvD,KAAK,SAAS,GAAG,IAAI,CAAC,OAAO,SAAS,CAAC,CAAC;AAKxC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,qBAAqB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAEhC;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IAElD;;;OAGG;IACH,SAAS,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAEtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IAE3D;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IAE9D;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;IAEpD;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACjC,CAAC;AAkBF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF;;;;;;GAMG;AACH,qBAAa,UAAU;;IAErB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAyB5B,YACE,eAAe,EAAE,MAAM,EACvB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,cAAc,EACpB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,YAAY,CAAC,MAAM,GAAG,kBAAkB,CAAC,EACrD,OAAO,GAAE,iBAAsB,EAoBhC;IAED;;;OAGG;IACH,IAAI,SAAS,IAAI,aAAa,GAAG,SAAS,CAEzC;IAED;;;OAGG;IACH,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;OAIG;IACH,IAAI,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAE5B;IAWD,IAAI,SAAS,WAEZ;IAED,IAAI,KAAK,WAER;IAED;;;;;OAKG;IACH,IAAI,WAAW,YAId;IAED;;;OAGG;IACH,YAAY,YAEX;IAED;;;;;OAKG;IACH,eAAe,IAAI,eAAe,CAYjC;IAED,IAAI,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAa7C;IAED,SAAS,CACP,OAAO,EAAE,SAAS,iBAAiB,EAAE,EACrC,aAAa,CAAC,EAAE,kBAAkB,GAAG,SAAS,GAC7C,OAAO,CAAC,IAAI,CAAC,CA4Cf;IAgBD;;;;;;;;;OASG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,QAUxB;IAED;;;;;OAKG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,QAE/B;IAED,kEAAkE;IAC5D,OAAO,CAAC,MAAM,EAAE,iBAAiB,iBAGtC;IAED;;;OAGG;IACH,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAc3B;IA+FD;;OAEG;IACH,IAAI,UAAU,WAEb;IAED;;;OAGG;IACH,IAAI,YAAY,WAEf;IAED;;;OAGG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,SAAK,GAAG,IAAI,CAMpD;IAED,QAAQ,IAAI,eAAe,CAyB1B;IAKD,mBAAmB,CAAC,MAAM,EAAE,SAAS,GAAG,WAAW,QAKlD;IAED;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,aAAa,UAW9D;IAED,eAAe,CAAC,GAAG,EAAE,SAAS,WAO7B;IAED;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,QAElB;IAED,KAAK,CACH,KAAK,CAAC,EAAE,SAAS,EACjB,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAC,QAoBhD;CA2GF;AA4CD;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAGpC,CAAC"}
@@ -25,6 +25,7 @@ var Subscriber = class {
25
25
  #protocolVersion;
26
26
  id;
27
27
  mode;
28
+ drainable;
28
29
  #downstream;
29
30
  #wsBatched;
30
31
  #watermark;
@@ -41,10 +42,13 @@ var Subscriber = class {
41
42
  #onBackfillIgnored;
42
43
  #lagReportingDisabled;
43
44
  #aligned = false;
45
+ #caughtUpWatermark;
46
+ #caughtUp = resolver();
44
47
  constructor(protocolVersion, id, mode, watermark, downstream, options = {}) {
45
48
  this.#protocolVersion = protocolVersion;
46
49
  this.id = id;
47
50
  this.mode = mode;
51
+ this.drainable = options.drainable ?? false;
48
52
  this.#downstream = downstream;
49
53
  this.#wsBatched = options.wsBatched ?? false;
50
54
  this.#watermark = watermark;
@@ -71,6 +75,14 @@ var Subscriber = class {
71
75
  get aligned() {
72
76
  return this.#aligned;
73
77
  }
78
+ /**
79
+ * Resolves once the subscriber has caught up, i.e. once it has acked
80
+ * (durably applied) all changes up to the head of the stream at the time
81
+ * it was {@link aligned}. Never resolves if the subscriber is closed first.
82
+ */
83
+ get caughtUp() {
84
+ return this.#caughtUp.promise;
85
+ }
74
86
  #trackBackfills(change) {
75
87
  if (this.#backfills) {
76
88
  const ignored = this.#backfills.applySerialized(change);
@@ -181,6 +193,15 @@ var Subscriber = class {
181
193
  }
182
194
  this.#sendDownstream(downstream);
183
195
  }
196
+ /**
197
+ * Sends a control status (e.g. `draining` or `restore`) downstream
198
+ * immediately, regardless of whether the subscriber is initialized or
199
+ * catching up. Unlike {@link sendStatus}, such statuses are not tied to a
200
+ * position in the replication stream.
201
+ */
202
+ sendControlStatus(status) {
203
+ this.#sendDownstream(["status", status]);
204
+ }
184
205
  /** catchup() is called on ChangeEntries loaded from the store. */
185
206
  async catchup(change) {
186
207
  this.#initialize();
@@ -206,12 +227,20 @@ var Subscriber = class {
206
227
  if (tag === "commit") this.#watermark = watermark;
207
228
  this.#trackBackfills(change);
208
229
  const result = await this.#sendStringifiedDownstream(json);
209
- if (tag === "commit" && result === "consumed") {
210
- const acked = max(this.#acked, watermark);
211
- if (acked !== this.#acked) {
212
- this.#acked = acked;
213
- this.#onAck?.(acked);
214
- }
230
+ if (tag === "commit" && result === "consumed") this.#advanceAck(watermark);
231
+ }
232
+ #advanceAck(watermark) {
233
+ const acked = max(this.#acked, watermark);
234
+ if (acked !== this.#acked) {
235
+ this.#acked = acked;
236
+ this.#onAck?.(acked);
237
+ this.#checkCaughtUp();
238
+ }
239
+ }
240
+ #checkCaughtUp() {
241
+ if (this.#caughtUpWatermark !== void 0 && this.#acked >= this.#caughtUpWatermark) {
242
+ this.#caughtUpWatermark = void 0;
243
+ this.#caughtUp.resolve();
215
244
  }
216
245
  }
217
246
  #sendDownstream(downstream) {
@@ -237,13 +266,7 @@ var Subscriber = class {
237
266
  const { result } = this.#downstream.push(batch);
238
267
  try {
239
268
  const outcome = await result;
240
- if (commitWatermark && outcome === "consumed") {
241
- const acked = max(this.#acked, commitWatermark);
242
- if (acked !== this.#acked) {
243
- this.#acked = acked;
244
- this.#onAck?.(acked);
245
- }
246
- }
269
+ if (commitWatermark && outcome === "consumed") this.#advanceAck(commitWatermark);
247
270
  } finally {
248
271
  this.#pending -= batch.changes.length;
249
272
  this.#pendingBytes -= size;
@@ -352,14 +375,15 @@ var Subscriber = class {
352
375
  fail(_err) {
353
376
  this.close();
354
377
  }
355
- close(error, message) {
378
+ close(error, message, details) {
356
379
  this.#backlog = null;
357
380
  this.#backlogBytes = 0;
358
381
  this.#backlogBackpressure.releaseAll();
359
382
  this.#resolveBacklogFullWaiters();
360
383
  if (error !== void 0) this.#sendDownstream(["error", {
361
384
  type: error,
362
- message
385
+ message,
386
+ ...details
363
387
  }]).finally(() => this.#downstream.cancel());
364
388
  else this.#downstream.cancel();
365
389
  }
@@ -400,6 +424,8 @@ var Subscriber = class {
400
424
  if (!closed && !this.#aligned) {
401
425
  this.#aligned = true;
402
426
  this.#onAligned?.(this);
427
+ this.#caughtUpWatermark = this.#watermark;
428
+ this.#checkCaughtUp();
403
429
  this.#lagReportingDisabled?.then((disabled) => {
404
430
  if (disabled) this.sendStatus(LAG_REPORTING_DISABLED);
405
431
  });
@@ -1 +1 @@
1
- {"version":3,"file":"subscriber.js","names":["#protocolVersion","#downstream","#wsBatched","#backlogBackpressure","#backlogFullWaiters","#onAck","#backfills","#onAligned","#onBackfillIgnored","#lagReportingDisabled","#watermark","#acked","#backlog","#aligned","#bufferedBacklogBytes","#pushBacklog","#maybeWaitForBacklogSpace","#sendChange","#initialized","#trackBackfills","#sendPreSerializedDownstream","#sendDownstream","#initialize","#backlogDrain","#drainBacklog","#sendStringifiedDownstream","#pending","#pendingBytes","#processed","#backlogCount","#samples","#totalBufferedBytes","#missedLastTimeout","#laggingSinceMs","#backlogBytes","#resolveBacklogFullWaiters","#backlogInFlightBytes","#lowWaterBytes","#waiters"],"sources":["../../../../../../zero-cache/src/services/change-streamer/subscriber.ts"],"sourcesContent":["import {resolver, type Resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport type {Enum} from '../../../../shared/src/enum.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {RingBuffer} from '../../../../shared/src/ring-buffer.ts';\nimport {max} from '../../types/lexi-version.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport type {BackfillState} from './backfill-state.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport type {\n ChangeTag,\n Downstream,\n Status,\n WatermarkedChange,\n} from './change-streamer.ts';\nimport type * as ErrorType from './error-type-enum.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\nconst DEFAULT_BACKLOG_HIGH_WATER_BYTES = 16 * 1024 * 1024;\nconst DEFAULT_BACKLOG_LOW_WATER_RATIO = 0.8;\n\nexport type SubscriberOptions = {\n backlogHighWaterBytes?: number | undefined;\n backlogLowWaterRatio?: number | undefined;\n wsBatched?: boolean | undefined;\n\n /**\n * Called whenever the subscriber's acked watermark advances, i.e. when it\n * has confirmed that a commit was durably applied to its replica. The ACK\n * therefore lags the subscriber's replica rather than leading it.\n */\n onAck?: ((watermark: string) => void) | undefined;\n\n /**\n * The subscriber's pending backfills (protocol v8+), which are tracked\n * over the changes sent to the subscriber.\n */\n backfills?: BackfillState | undefined;\n\n /**\n * Called once the subscriber has been sent all changes up to the head of\n * the stream, i.e. its catchup and backlog have been flushed. From this\n * point on, the subscriber's tracked {@link backfills} reflect the same\n * position in the stream as that of the change-streamer.\n */\n onAligned?: ((subscriber: Subscriber) => void) | undefined;\n\n /**\n * Called when a `backfill` or `backfill-completed` message is ignored for\n * some of the subscriber's pending backfill columns, after the subscriber\n * is aligned.\n */\n onBackfillIgnored?: ((columns: string[]) => void) | undefined;\n\n /**\n * Resolves to whether lag reporting is disabled upstream. If so, a\n * {@link LAG_REPORTING_DISABLED} status is sent once the subscriber is\n * aligned, as a stand-in for the lag reports that a subscriber would\n * otherwise use to determine that it is caught up (e.g. to advertise\n * readiness to serve traffic).\n */\n lagReportingDisabled?: Promise<boolean> | undefined;\n};\n\n/**\n * A status message queued in the backlog, so that it is sent downstream in\n * the order in which it was received relative to (backlogged) changes.\n */\ntype QueuedStatus = {readonly status: string};\n\ntype BacklogEntry = WatermarkedChange | QueuedStatus;\n\nfunction isQueuedStatus(entry: BacklogEntry): entry is QueuedStatus {\n return !Array.isArray(entry);\n}\n\nfunction entryBytes(entry: BacklogEntry): number {\n return isQueuedStatus(entry) ? entry.status.length : entry[2].length;\n}\n\nexport type BacklogFullWait = {\n readonly promise: Promise<void>;\n cancel(): void;\n};\n\nexport type SubscriberStats = {\n processRate: number;\n pending: number;\n backlog: number;\n backlogBytes: number;\n totalBufferedBytes: number;\n missedLastTimeout: boolean;\n};\n\n/**\n * Encapsulates a subscriber to changes. All subscribers start in a\n * \"catchup\" phase in which changes are buffered in a backlog while the\n * storer is queried to send any changes that were committed since the\n * subscriber's watermark. Once the catchup is complete, calls to\n * {@link send()} result in immediately sending the change.\n */\nexport class Subscriber {\n readonly #protocolVersion: number;\n readonly id: string;\n readonly mode: ReplicatorMode;\n readonly #downstream: Subscription<string | PreSerializedBatch>;\n readonly #wsBatched: boolean;\n #watermark: string;\n #acked: string;\n #backlog: RingBuffer<BacklogEntry> | null;\n // While catchup is running, live changes are buffered here instead of being\n // pushed downstream. RingBuffer lets drainBacklog consume that backlog without\n // shifting an array, which matters when a subscriber is far behind.\n #backlogBytes = 0;\n #backlogInFlightBytes = 0;\n #backlogDrain: Promise<void> | null = null;\n readonly #backlogBackpressure: ByteBackpressureGate;\n readonly #backlogFullWaiters = new Set<Resolver<void>>();\n readonly #onAck: ((watermark: string) => void) | undefined;\n readonly #backfills: BackfillState | undefined;\n readonly #onAligned: ((subscriber: Subscriber) => void) | undefined;\n readonly #onBackfillIgnored: ((columns: string[]) => void) | undefined;\n readonly #lagReportingDisabled: Promise<boolean> | undefined;\n #aligned = false;\n\n constructor(\n protocolVersion: number,\n id: string,\n mode: ReplicatorMode,\n watermark: string,\n downstream: Subscription<string | PreSerializedBatch>,\n options: SubscriberOptions = {},\n ) {\n this.#protocolVersion = protocolVersion;\n this.id = id;\n this.mode = mode;\n this.#downstream = downstream;\n this.#wsBatched = options.wsBatched ?? false;\n this.#watermark = watermark;\n this.#acked = watermark;\n this.#backlog = new RingBuffer();\n this.#backlogBackpressure = new ByteBackpressureGate(\n options.backlogHighWaterBytes ?? DEFAULT_BACKLOG_HIGH_WATER_BYTES,\n options.backlogLowWaterRatio ?? DEFAULT_BACKLOG_LOW_WATER_RATIO,\n );\n this.#onAck = options.onAck;\n this.#backfills = options.backfills;\n this.#onAligned = options.onAligned;\n this.#onBackfillIgnored = options.onBackfillIgnored;\n this.#lagReportingDisabled = options.lagReportingDisabled;\n }\n\n /**\n * The subscriber's tracked pending backfills, or `undefined` if the\n * subscriber does not report them (i.e. protocol < v8).\n */\n get backfills(): BackfillState | undefined {\n return this.#backfills;\n }\n\n /**\n * Whether the subscriber has been sent all changes up to the head of the\n * stream (see {@link SubscriberOptions.onAligned}).\n */\n get aligned(): boolean {\n return this.#aligned;\n }\n\n #trackBackfills(change: WatermarkedChange) {\n if (this.#backfills) {\n const ignored = this.#backfills.applySerialized(change);\n if (ignored.length && this.#aligned) {\n this.#onBackfillIgnored?.(ignored);\n }\n }\n }\n\n get watermark() {\n return this.#watermark;\n }\n\n get acked() {\n return this.#acked;\n }\n\n /**\n * Whether the backlog of live changes buffered during catchup has reached the\n * point at which {@link send()} stops resolving. Past it the subscriber is no\n * longer free: it holds up every subsequent flush, and with no other\n * subscriber to form a majority it stalls replication outright.\n */\n get backlogFull() {\n return (\n this.#bufferedBacklogBytes >= this.#backlogBackpressure.highWaterBytes\n );\n }\n\n /**\n * @returns whether the subscriber is currently sending backlogged messages,\n * vs caught up and sending the \"head\" of the replication stream.\n */\n isBacklogged() {\n return this.#backlog !== null || this.#bufferedBacklogBytes > 0;\n }\n\n /**\n * Resolves the first time {@link backlogFull} becomes true, so that a caller\n * holding the subscriber in catchup can give up at the moment the subscriber\n * starts costing replication rather than on a timer. Call cancel() when the\n * wait is no longer needed so the subscriber does not retain it.\n */\n whenBacklogFull(): BacklogFullWait {\n if (this.backlogFull) {\n return {promise: promiseVoid, cancel() {}};\n }\n const r = resolver<void>();\n this.#backlogFullWaiters.add(r);\n return {\n promise: r.promise,\n cancel: () => {\n this.#backlogFullWaiters.delete(r);\n },\n };\n }\n\n send(change: WatermarkedChange): Promise<void> {\n const [watermark] = change;\n if (watermark > this.#watermark) {\n if (this.#backlog) {\n // During catchup, buffer live changes behind the durable catchup stream.\n // The returned promise applies backpressure if the buffered bytes cross\n // the high water mark.\n this.#pushBacklog(change);\n return this.#maybeWaitForBacklogSpace();\n }\n return this.#sendChange(change);\n }\n return promiseVoid;\n }\n\n sendBatch(\n changes: readonly WatermarkedChange[],\n preSerialized?: PreSerializedBatch | undefined,\n ): Promise<void> {\n if (changes.length === 0) {\n return promiseVoid;\n }\n\n if (\n this.#wsBatched &&\n preSerialized &&\n !this.#backlog &&\n this.#initialized &&\n changes[0][0] > this.#watermark &&\n (this.#protocolVersion >= 5 ||\n changes.every(c => this.supportsMessage(c[1])))\n ) {\n let commitWatermark: string | undefined;\n for (let i = changes.length - 1; i >= 0; i--) {\n if (changes[i][1] === 'commit') {\n commitWatermark = changes[i][0];\n break;\n }\n }\n if (commitWatermark) {\n this.#watermark = commitWatermark;\n }\n for (const change of changes) {\n this.#trackBackfills(change);\n }\n return this.#sendPreSerializedDownstream(preSerialized, commitWatermark);\n }\n\n const promises: Promise<void>[] = [];\n for (const change of changes) {\n const p = this.send(change);\n if (p !== promiseVoid) {\n promises.push(p);\n }\n }\n if (promises.length === 0) {\n return promiseVoid;\n }\n if (promises.length === 1) {\n return promises[0];\n }\n return Promise.all(promises).then(() => {});\n }\n\n #initialized = false;\n\n /**\n * Called once the subscriber's watermark has been validated in the initial\n * catchup process.\n */\n #initialize() {\n if (!this.#initialized) {\n this.#initialized = true;\n // The initial status precedes the catchup, so it bypasses the backlog.\n void this.#sendDownstream(['status', {tag: 'status'}]);\n }\n }\n\n /**\n * Sends a status message (e.g. a lag report) downstream. While the\n * subscriber is catching up, the status is queued in the backlog behind\n * the changes that preceded it in the replication stream, so that the\n * subscriber processes it in stream order. This is necessary for lag\n * reports to measure the lag of the subscriber, including its catchup.\n *\n * Status messages received before the subscriber is initialized are\n * dropped.\n */\n sendStatus(status: Status) {\n if (!this.#initialized) {\n return;\n }\n const downstream: Downstream = ['status', status];\n if (this.#backlog) {\n this.#pushBacklog({status: BigIntJSON.stringify(downstream)});\n return;\n }\n void this.#sendDownstream(downstream);\n }\n\n /** catchup() is called on ChangeEntries loaded from the store. */\n async catchup(change: WatermarkedChange) {\n this.#initialize();\n await this.#sendChange(change);\n }\n\n /**\n * Marks the Subscribe as \"caught up\" and flushes any backlog of\n * entries that were received during the catchup.\n */\n setCaughtUp(): Promise<void> {\n this.#initialize();\n if (!this.#backlog) {\n return this.#backlogDrain ?? promiseVoid;\n }\n if (!this.#backlogDrain) {\n // Keep #backlog non-null while queued entries are being handed to\n // downstream. That preserves ordering for sends that race with\n // setCaughtUp(): they append to the same backlog instead of bypassing\n // older buffered changes.\n this.#backlogDrain = this.#drainBacklog();\n void this.#backlogDrain.catch(e => this.fail(e));\n }\n return this.#backlogDrain;\n }\n\n async #sendChange(change: WatermarkedChange) {\n const [watermark, tag, json] = change;\n if (watermark <= this.watermark) {\n return;\n }\n if (!this.supportsMessage(tag)) {\n return;\n }\n if (tag === 'commit') {\n this.#watermark = watermark;\n }\n this.#trackBackfills(change);\n const result = await this.#sendStringifiedDownstream(json);\n if (tag === 'commit' && result === 'consumed') {\n // Sends can complete out of order (e.g. the bounded window in\n // #drainBacklog), so the ack only advances monotonically, and listeners\n // are only notified when it does.\n const acked = max(this.#acked, watermark);\n if (acked !== this.#acked) {\n this.#acked = acked;\n this.#onAck?.(acked);\n }\n }\n }\n\n #sendDownstream(downstream: Downstream) {\n return this.#sendStringifiedDownstream(BigIntJSON.stringify(downstream));\n }\n\n async #sendStringifiedDownstream(json: string) {\n const size = json.length;\n this.#pending++;\n this.#pendingBytes += size;\n const {result} = this.#downstream.push(json);\n try {\n return await result;\n } finally {\n this.#pending--;\n this.#pendingBytes -= size;\n this.#processed++;\n }\n }\n\n async #sendPreSerializedDownstream(\n batch: PreSerializedBatch,\n commitWatermark?: string | undefined,\n ): Promise<void> {\n const size = batch.byteLength;\n this.#pending += batch.changes.length;\n this.#pendingBytes += size;\n const {result} = this.#downstream.push(batch);\n try {\n const outcome = await result;\n if (commitWatermark && outcome === 'consumed') {\n const acked = max(this.#acked, commitWatermark);\n if (acked !== this.#acked) {\n this.#acked = acked;\n this.#onAck?.(acked);\n }\n }\n } finally {\n this.#pending -= batch.changes.length;\n this.#pendingBytes -= size;\n this.#processed += batch.changes.length;\n }\n }\n\n // `pending` and `processed` stats are tracked by periodically sampling\n // the running totals (by the progress tracker in the Forwarder).\n // This information was originally collected for use in flow control\n // decisions. The final flow control algorithm ended up being simpler\n // than expected and does not actually use this information. However, the\n // stats are still tracked and logged during flow control decisions for\n // debugging, forensics, and potential improvements to the algorithm.\n\n #pending = 0;\n #pendingBytes = 0;\n #processed = 0;\n #samples: {processed: number; timestamp: number}[] = [\n {processed: 0, timestamp: performance.now()},\n ];\n\n /**\n * The number of downstream messages that have yet to be acked.\n */\n get numPending() {\n return this.#pending + this.#backlogCount;\n }\n\n /**\n * The total number of downstream messages that the subscriber has\n * processed (i.e. acked).\n */\n get numProcessed() {\n return this.#processed;\n }\n\n /**\n * Records a new history entry for the number of messages processed,\n * keeping the number of samples bounded to `maxSamples`.\n */\n sampleProcessRate(now: number, maxSamples = 10): this {\n while (this.#samples.length >= maxSamples) {\n this.#samples.shift();\n }\n this.#samples.push({processed: this.#processed, timestamp: now});\n return this;\n }\n\n getStats(): SubscriberStats {\n const pending = this.numPending;\n if (this.#samples.length < 2) {\n return {\n processRate: 0,\n pending,\n backlog: this.#backlogCount,\n backlogBytes: this.#bufferedBacklogBytes,\n totalBufferedBytes: this.#totalBufferedBytes,\n missedLastTimeout: this.#missedLastTimeout,\n };\n }\n const from = this.#samples[0];\n const to = must(this.#samples.at(-1));\n const processed = to.processed - from.processed;\n const seconds = (to.timestamp - from.timestamp) / 1000;\n const processRate = seconds === 0 ? 0 : processed / seconds;\n return {\n processRate,\n pending,\n backlog: this.#backlogCount,\n backlogBytes: this.#bufferedBacklogBytes,\n totalBufferedBytes: this.#totalBufferedBytes,\n missedLastTimeout: this.#missedLastTimeout,\n };\n }\n\n #missedLastTimeout = false;\n #laggingSinceMs: number | undefined;\n\n trackResponseResult(result: 'on-time' | 'timed-out') {\n this.#missedLastTimeout = result === 'timed-out';\n if (result === 'on-time') {\n this.#laggingSinceMs = undefined;\n }\n }\n\n /**\n * Reports the change rate of a slow subscriber (i.e. that missed the last\n * timeout) compared to the change rate of the (slowest) subscriber that\n * responded on time.\n *\n * * `lagging` indicates that this subscriber is slower\n * * `catching-up` indicates that it is faster\n *\n * Returns the total duration in which subscriber has been continuously\n * reported as `lagging`.\n */\n reportChangeRate(now: number, status: 'lagging' | 'catching-up') {\n assert(\n this.#missedLastTimeout,\n `reportChangeRate should only be called for slow subscribers`,\n );\n if (status === 'catching-up') {\n this.#laggingSinceMs = undefined;\n return 0;\n }\n this.#laggingSinceMs ??= now;\n return now - this.#laggingSinceMs;\n }\n\n supportsMessage(tag: ChangeTag) {\n switch (tag) {\n case 'update-table-metadata':\n // update-table-row-key is only understood by subscribers >= protocol v5\n return this.#protocolVersion >= 5;\n }\n return true;\n }\n\n /**\n * Ends the subscription without sending a downstream `['error', ...]`.\n *\n * This is deliberate, and not the same as reporting the failure to the\n * subscriber: `IncrementalSyncer` treats any `['error', ...]` as terminal and\n * shuts down to restore a fresh replica from litestream, whereas a clean end\n * backs off and re-subscribes. The failures routed here -- storer catchup\n * errors, backlog drain errors, backup-monitor errors -- are transient, so a\n * reconnect is the proportionate response and a fleet-wide restore is not.\n *\n * Callers are responsible for logging `err`; it is not carried downstream.\n */\n fail(_err?: unknown) {\n this.close();\n }\n\n close(error?: ErrorType, message?: string) {\n // Closing the subscriber must also release producers that are blocked on\n // backlog capacity; there is no future drain that could wake them.\n this.#backlog = null;\n this.#backlogBytes = 0;\n this.#backlogBackpressure.releaseAll();\n // The backlog can no longer grow, so nothing would ever resolve these.\n // Waiters re-check backlogFull, which is now false, and see the close.\n this.#resolveBacklogFullWaiters();\n\n if (error !== undefined) {\n // Wait for the ACK of the error message before closing the connection.\n void this.#sendDownstream(['error', {type: error, message}]).finally(() =>\n this.#downstream.cancel(),\n );\n } else {\n this.#downstream.cancel();\n }\n }\n\n get #backlogCount() {\n return this.#backlog?.size ?? 0;\n }\n\n get #bufferedBacklogBytes() {\n // Include entries already handed to downstream but not yet consumed. Without\n // this, setCaughtUp() could move bytes out of #backlog faster than the\n // downstream Subscription can process them and release producers too early.\n return this.#backlogBytes + this.#backlogInFlightBytes;\n }\n\n get #totalBufferedBytes() {\n return this.#bufferedBacklogBytes + this.#pendingBytes;\n }\n\n #pushBacklog(entry: BacklogEntry) {\n assert(this.#backlog, 'cannot push to backlog after catchup completed');\n this.#backlog.push(entry);\n this.#backlogBytes += entryBytes(entry);\n if (this.backlogFull) {\n this.#resolveBacklogFullWaiters();\n }\n }\n\n #resolveBacklogFullWaiters() {\n const waiters = [...this.#backlogFullWaiters];\n this.#backlogFullWaiters.clear();\n for (const waiter of waiters) {\n waiter.resolve();\n }\n }\n\n #maybeWaitForBacklogSpace(): Promise<void> {\n return this.#backlogBackpressure.waitForSpace(this.#bufferedBacklogBytes);\n }\n\n async #drainBacklog() {\n const inFlight: {promise: Promise<void>; bytes: number}[] = [];\n let inFlightBytes = 0;\n\n try {\n for (;;) {\n const entry = this.#backlog?.shift();\n if (!entry) {\n const closed = this.#backlog === null;\n this.#backlog = null;\n this.#backlogBytes = 0;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n if (!closed && !this.#aligned) {\n // All changes up to the head have been handed to #sendChange()\n // (and thus tracked), and subsequent changes are sent directly.\n this.#aligned = true;\n this.#onAligned?.(this);\n void this.#lagReportingDisabled?.then(disabled => {\n if (disabled) {\n this.sendStatus(LAG_REPORTING_DISABLED);\n }\n });\n }\n break;\n }\n\n const bytes = entryBytes(entry);\n this.#backlogBytes -= bytes;\n this.#backlogInFlightBytes += bytes;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n\n // Send backlog entries in order, but keep only a bounded byte window in\n // flight. This avoids replacing one unbounded buffer with another inside\n // the downstream Subscription during catchup completion.\n const send = isQueuedStatus(entry)\n ? this.#sendStringifiedDownstream(entry.status).then(() => {})\n : this.#sendChange(entry);\n const promise = send.finally(() => {\n this.#backlogInFlightBytes -= bytes;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n });\n inFlight.push({promise, bytes});\n inFlightBytes += bytes;\n\n while (inFlightBytes >= this.#backlogBackpressure.highWaterBytes) {\n const next = must(inFlight.shift());\n await next.promise;\n inFlightBytes -= next.bytes;\n }\n }\n\n for (const {promise} of inFlight) {\n await promise;\n }\n } finally {\n this.#backlogDrain = null;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n }\n }\n}\n\nclass ByteBackpressureGate {\n readonly highWaterBytes: number;\n readonly #lowWaterBytes: number;\n readonly #waiters: Resolver<void>[] = [];\n\n constructor(highWaterBytes: number, lowWaterRatio: number) {\n this.highWaterBytes = Math.max(1, highWaterBytes);\n this.#lowWaterBytes =\n this.highWaterBytes * Math.min(1, Math.max(0, lowWaterRatio));\n }\n\n waitForSpace(bufferedBytes: number): Promise<void> {\n if (bufferedBytes < this.highWaterBytes) {\n return promiseVoid;\n }\n\n // One waiter represents one send() call that has already appended its\n // change. The producer is released when the backlog falls back below the low\n // water mark or the subscriber closes.\n const r = resolver<void>();\n this.#waiters.push(r);\n return r.promise;\n }\n\n releaseIfUnderLowWater(bufferedBytes: number) {\n if (this.#waiters.length === 0 || bufferedBytes > this.#lowWaterBytes) {\n return;\n }\n\n // Use a low water mark so waiting producers are released in batches instead\n // of waking one at a time around the high water boundary.\n this.releaseAll();\n }\n\n releaseAll() {\n const waiters = this.#waiters.splice(0);\n for (const waiter of waiters) {\n waiter.resolve();\n }\n }\n}\n\n/**\n * Sent to an aligned subscriber when lag reporting is disabled upstream,\n * signaling that the subscriber is caught up. This is a lag report with a\n * `nextSendTimeMs` of 0 and no `lastTimings`, so that no lag is measured\n * (see `isLagReportingDisabledSignal()` in the replicator's recorder).\n */\nexport const LAG_REPORTING_DISABLED: Status = {\n tag: 'status',\n lagReport: {nextSendTimeMs: 0},\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"subscriber.js","names":["#protocolVersion","#downstream","#wsBatched","#backlogBackpressure","#backlogFullWaiters","#onAck","#backfills","#onAligned","#onBackfillIgnored","#lagReportingDisabled","#caughtUp","#watermark","#acked","#backlog","#aligned","#bufferedBacklogBytes","#pushBacklog","#maybeWaitForBacklogSpace","#sendChange","#initialized","#trackBackfills","#sendPreSerializedDownstream","#sendDownstream","#initialize","#backlogDrain","#drainBacklog","#sendStringifiedDownstream","#advanceAck","#checkCaughtUp","#caughtUpWatermark","#pending","#pendingBytes","#processed","#backlogCount","#samples","#totalBufferedBytes","#missedLastTimeout","#laggingSinceMs","#backlogBytes","#resolveBacklogFullWaiters","#backlogInFlightBytes","#lowWaterBytes","#waiters"],"sources":["../../../../../../zero-cache/src/services/change-streamer/subscriber.ts"],"sourcesContent":["import {resolver, type Resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport type {Enum} from '../../../../shared/src/enum.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {RingBuffer} from '../../../../shared/src/ring-buffer.ts';\nimport {max} from '../../types/lexi-version.ts';\nimport type {Subscription} from '../../types/subscription.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport type {BackfillState} from './backfill-state.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport type {\n ChangeTag,\n Downstream,\n Status,\n WatermarkedChange,\n} from './change-streamer.ts';\nimport type * as ErrorType from './error-type-enum.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\nconst DEFAULT_BACKLOG_HIGH_WATER_BYTES = 16 * 1024 * 1024;\nconst DEFAULT_BACKLOG_LOW_WATER_RATIO = 0.8;\n\nexport type SubscriberOptions = {\n backlogHighWaterBytes?: number | undefined;\n backlogLowWaterRatio?: number | undefined;\n wsBatched?: boolean | undefined;\n\n /**\n * Called whenever the subscriber's acked watermark advances, i.e. when it\n * has confirmed that a commit was durably applied to its replica. The ACK\n * therefore lags the subscriber's replica rather than leading it.\n */\n onAck?: ((watermark: string) => void) | undefined;\n\n /**\n * The subscriber's pending backfills (protocol v8+), which are tracked\n * over the changes sent to the subscriber.\n */\n backfills?: BackfillState | undefined;\n\n /**\n * Called once the subscriber has been sent all changes up to the head of\n * the stream, i.e. its catchup and backlog have been flushed. From this\n * point on, the subscriber's tracked {@link backfills} reflect the same\n * position in the stream as that of the change-streamer.\n */\n onAligned?: ((subscriber: Subscriber) => void) | undefined;\n\n /**\n * Called when a `backfill` or `backfill-completed` message is ignored for\n * some of the subscriber's pending backfill columns, after the subscriber\n * is aligned.\n */\n onBackfillIgnored?: ((columns: string[]) => void) | undefined;\n\n /**\n * Resolves to whether lag reporting is disabled upstream. If so, a\n * {@link LAG_REPORTING_DISABLED} status is sent once the subscriber is\n * aligned, as a stand-in for the lag reports that a subscriber would\n * otherwise use to determine that it is caught up (e.g. to advertise\n * readiness to serve traffic).\n */\n lagReportingDisabled?: Promise<boolean> | undefined;\n\n /**\n * Whether the subscriber switches over to another replication-manager\n * when this one drains (see `DrainTracker`).\n */\n drainable?: boolean | undefined;\n};\n\n/**\n * A status message queued in the backlog, so that it is sent downstream in\n * the order in which it was received relative to (backlogged) changes.\n */\ntype QueuedStatus = {readonly status: string};\n\ntype BacklogEntry = WatermarkedChange | QueuedStatus;\n\nfunction isQueuedStatus(entry: BacklogEntry): entry is QueuedStatus {\n return !Array.isArray(entry);\n}\n\nfunction entryBytes(entry: BacklogEntry): number {\n return isQueuedStatus(entry) ? entry.status.length : entry[2].length;\n}\n\nexport type BacklogFullWait = {\n readonly promise: Promise<void>;\n cancel(): void;\n};\n\nexport type SubscriberStats = {\n processRate: number;\n pending: number;\n backlog: number;\n backlogBytes: number;\n totalBufferedBytes: number;\n missedLastTimeout: boolean;\n};\n\n/**\n * Encapsulates a subscriber to changes. All subscribers start in a\n * \"catchup\" phase in which changes are buffered in a backlog while the\n * storer is queried to send any changes that were committed since the\n * subscriber's watermark. Once the catchup is complete, calls to\n * {@link send()} result in immediately sending the change.\n */\nexport class Subscriber {\n readonly #protocolVersion: number;\n readonly id: string;\n readonly mode: ReplicatorMode;\n readonly drainable: boolean;\n readonly #downstream: Subscription<string | PreSerializedBatch>;\n readonly #wsBatched: boolean;\n #watermark: string;\n #acked: string;\n #backlog: RingBuffer<BacklogEntry> | null;\n // While catchup is running, live changes are buffered here instead of being\n // pushed downstream. RingBuffer lets drainBacklog consume that backlog without\n // shifting an array, which matters when a subscriber is far behind.\n #backlogBytes = 0;\n #backlogInFlightBytes = 0;\n #backlogDrain: Promise<void> | null = null;\n readonly #backlogBackpressure: ByteBackpressureGate;\n readonly #backlogFullWaiters = new Set<Resolver<void>>();\n readonly #onAck: ((watermark: string) => void) | undefined;\n readonly #backfills: BackfillState | undefined;\n readonly #onAligned: ((subscriber: Subscriber) => void) | undefined;\n readonly #onBackfillIgnored: ((columns: string[]) => void) | undefined;\n readonly #lagReportingDisabled: Promise<boolean> | undefined;\n #aligned = false;\n // The watermark to be acked for the subscriber to be caught up, set when\n // it is aligned.\n #caughtUpWatermark: string | undefined;\n readonly #caughtUp = resolver<void>();\n\n constructor(\n protocolVersion: number,\n id: string,\n mode: ReplicatorMode,\n watermark: string,\n downstream: Subscription<string | PreSerializedBatch>,\n options: SubscriberOptions = {},\n ) {\n this.#protocolVersion = protocolVersion;\n this.id = id;\n this.mode = mode;\n this.drainable = options.drainable ?? false;\n this.#downstream = downstream;\n this.#wsBatched = options.wsBatched ?? false;\n this.#watermark = watermark;\n this.#acked = watermark;\n this.#backlog = new RingBuffer();\n this.#backlogBackpressure = new ByteBackpressureGate(\n options.backlogHighWaterBytes ?? DEFAULT_BACKLOG_HIGH_WATER_BYTES,\n options.backlogLowWaterRatio ?? DEFAULT_BACKLOG_LOW_WATER_RATIO,\n );\n this.#onAck = options.onAck;\n this.#backfills = options.backfills;\n this.#onAligned = options.onAligned;\n this.#onBackfillIgnored = options.onBackfillIgnored;\n this.#lagReportingDisabled = options.lagReportingDisabled;\n }\n\n /**\n * The subscriber's tracked pending backfills, or `undefined` if the\n * subscriber does not report them (i.e. protocol < v8).\n */\n get backfills(): BackfillState | undefined {\n return this.#backfills;\n }\n\n /**\n * Whether the subscriber has been sent all changes up to the head of the\n * stream (see {@link SubscriberOptions.onAligned}).\n */\n get aligned(): boolean {\n return this.#aligned;\n }\n\n /**\n * Resolves once the subscriber has caught up, i.e. once it has acked\n * (durably applied) all changes up to the head of the stream at the time\n * it was {@link aligned}. Never resolves if the subscriber is closed first.\n */\n get caughtUp(): Promise<void> {\n return this.#caughtUp.promise;\n }\n\n #trackBackfills(change: WatermarkedChange) {\n if (this.#backfills) {\n const ignored = this.#backfills.applySerialized(change);\n if (ignored.length && this.#aligned) {\n this.#onBackfillIgnored?.(ignored);\n }\n }\n }\n\n get watermark() {\n return this.#watermark;\n }\n\n get acked() {\n return this.#acked;\n }\n\n /**\n * Whether the backlog of live changes buffered during catchup has reached the\n * point at which {@link send()} stops resolving. Past it the subscriber is no\n * longer free: it holds up every subsequent flush, and with no other\n * subscriber to form a majority it stalls replication outright.\n */\n get backlogFull() {\n return (\n this.#bufferedBacklogBytes >= this.#backlogBackpressure.highWaterBytes\n );\n }\n\n /**\n * @returns whether the subscriber is currently sending backlogged messages,\n * vs caught up and sending the \"head\" of the replication stream.\n */\n isBacklogged() {\n return this.#backlog !== null || this.#bufferedBacklogBytes > 0;\n }\n\n /**\n * Resolves the first time {@link backlogFull} becomes true, so that a caller\n * holding the subscriber in catchup can give up at the moment the subscriber\n * starts costing replication rather than on a timer. Call cancel() when the\n * wait is no longer needed so the subscriber does not retain it.\n */\n whenBacklogFull(): BacklogFullWait {\n if (this.backlogFull) {\n return {promise: promiseVoid, cancel() {}};\n }\n const r = resolver<void>();\n this.#backlogFullWaiters.add(r);\n return {\n promise: r.promise,\n cancel: () => {\n this.#backlogFullWaiters.delete(r);\n },\n };\n }\n\n send(change: WatermarkedChange): Promise<void> {\n const [watermark] = change;\n if (watermark > this.#watermark) {\n if (this.#backlog) {\n // During catchup, buffer live changes behind the durable catchup stream.\n // The returned promise applies backpressure if the buffered bytes cross\n // the high water mark.\n this.#pushBacklog(change);\n return this.#maybeWaitForBacklogSpace();\n }\n return this.#sendChange(change);\n }\n return promiseVoid;\n }\n\n sendBatch(\n changes: readonly WatermarkedChange[],\n preSerialized?: PreSerializedBatch | undefined,\n ): Promise<void> {\n if (changes.length === 0) {\n return promiseVoid;\n }\n\n if (\n this.#wsBatched &&\n preSerialized &&\n !this.#backlog &&\n this.#initialized &&\n changes[0][0] > this.#watermark &&\n (this.#protocolVersion >= 5 ||\n changes.every(c => this.supportsMessage(c[1])))\n ) {\n let commitWatermark: string | undefined;\n for (let i = changes.length - 1; i >= 0; i--) {\n if (changes[i][1] === 'commit') {\n commitWatermark = changes[i][0];\n break;\n }\n }\n if (commitWatermark) {\n this.#watermark = commitWatermark;\n }\n for (const change of changes) {\n this.#trackBackfills(change);\n }\n return this.#sendPreSerializedDownstream(preSerialized, commitWatermark);\n }\n\n const promises: Promise<void>[] = [];\n for (const change of changes) {\n const p = this.send(change);\n if (p !== promiseVoid) {\n promises.push(p);\n }\n }\n if (promises.length === 0) {\n return promiseVoid;\n }\n if (promises.length === 1) {\n return promises[0];\n }\n return Promise.all(promises).then(() => {});\n }\n\n #initialized = false;\n\n /**\n * Called once the subscriber's watermark has been validated in the initial\n * catchup process.\n */\n #initialize() {\n if (!this.#initialized) {\n this.#initialized = true;\n // The initial status precedes the catchup, so it bypasses the backlog.\n void this.#sendDownstream(['status', {tag: 'status'}]);\n }\n }\n\n /**\n * Sends a status message (e.g. a lag report) downstream. While the\n * subscriber is catching up, the status is queued in the backlog behind\n * the changes that preceded it in the replication stream, so that the\n * subscriber processes it in stream order. This is necessary for lag\n * reports to measure the lag of the subscriber, including its catchup.\n *\n * Status messages received before the subscriber is initialized are\n * dropped.\n */\n sendStatus(status: Status) {\n if (!this.#initialized) {\n return;\n }\n const downstream: Downstream = ['status', status];\n if (this.#backlog) {\n this.#pushBacklog({status: BigIntJSON.stringify(downstream)});\n return;\n }\n void this.#sendDownstream(downstream);\n }\n\n /**\n * Sends a control status (e.g. `draining` or `restore`) downstream\n * immediately, regardless of whether the subscriber is initialized or\n * catching up. Unlike {@link sendStatus}, such statuses are not tied to a\n * position in the replication stream.\n */\n sendControlStatus(status: Status) {\n void this.#sendDownstream(['status', status]);\n }\n\n /** catchup() is called on ChangeEntries loaded from the store. */\n async catchup(change: WatermarkedChange) {\n this.#initialize();\n await this.#sendChange(change);\n }\n\n /**\n * Marks the Subscribe as \"caught up\" and flushes any backlog of\n * entries that were received during the catchup.\n */\n setCaughtUp(): Promise<void> {\n this.#initialize();\n if (!this.#backlog) {\n return this.#backlogDrain ?? promiseVoid;\n }\n if (!this.#backlogDrain) {\n // Keep #backlog non-null while queued entries are being handed to\n // downstream. That preserves ordering for sends that race with\n // setCaughtUp(): they append to the same backlog instead of bypassing\n // older buffered changes.\n this.#backlogDrain = this.#drainBacklog();\n void this.#backlogDrain.catch(e => this.fail(e));\n }\n return this.#backlogDrain;\n }\n\n async #sendChange(change: WatermarkedChange) {\n const [watermark, tag, json] = change;\n if (watermark <= this.watermark) {\n return;\n }\n if (!this.supportsMessage(tag)) {\n return;\n }\n if (tag === 'commit') {\n this.#watermark = watermark;\n }\n this.#trackBackfills(change);\n const result = await this.#sendStringifiedDownstream(json);\n if (tag === 'commit' && result === 'consumed') {\n this.#advanceAck(watermark);\n }\n }\n\n #advanceAck(watermark: string) {\n // Sends can complete out of order (e.g. the bounded window in\n // #drainBacklog), so the ack only advances monotonically, and listeners\n // are only notified when it does.\n const acked = max(this.#acked, watermark);\n if (acked !== this.#acked) {\n this.#acked = acked;\n this.#onAck?.(acked);\n this.#checkCaughtUp();\n }\n }\n\n #checkCaughtUp() {\n if (\n this.#caughtUpWatermark !== undefined &&\n this.#acked >= this.#caughtUpWatermark\n ) {\n this.#caughtUpWatermark = undefined;\n this.#caughtUp.resolve();\n }\n }\n\n #sendDownstream(downstream: Downstream) {\n return this.#sendStringifiedDownstream(BigIntJSON.stringify(downstream));\n }\n\n async #sendStringifiedDownstream(json: string) {\n const size = json.length;\n this.#pending++;\n this.#pendingBytes += size;\n const {result} = this.#downstream.push(json);\n try {\n return await result;\n } finally {\n this.#pending--;\n this.#pendingBytes -= size;\n this.#processed++;\n }\n }\n\n async #sendPreSerializedDownstream(\n batch: PreSerializedBatch,\n commitWatermark?: string | undefined,\n ): Promise<void> {\n const size = batch.byteLength;\n this.#pending += batch.changes.length;\n this.#pendingBytes += size;\n const {result} = this.#downstream.push(batch);\n try {\n const outcome = await result;\n if (commitWatermark && outcome === 'consumed') {\n this.#advanceAck(commitWatermark);\n }\n } finally {\n this.#pending -= batch.changes.length;\n this.#pendingBytes -= size;\n this.#processed += batch.changes.length;\n }\n }\n\n // `pending` and `processed` stats are tracked by periodically sampling\n // the running totals (by the progress tracker in the Forwarder).\n // This information was originally collected for use in flow control\n // decisions. The final flow control algorithm ended up being simpler\n // than expected and does not actually use this information. However, the\n // stats are still tracked and logged during flow control decisions for\n // debugging, forensics, and potential improvements to the algorithm.\n\n #pending = 0;\n #pendingBytes = 0;\n #processed = 0;\n #samples: {processed: number; timestamp: number}[] = [\n {processed: 0, timestamp: performance.now()},\n ];\n\n /**\n * The number of downstream messages that have yet to be acked.\n */\n get numPending() {\n return this.#pending + this.#backlogCount;\n }\n\n /**\n * The total number of downstream messages that the subscriber has\n * processed (i.e. acked).\n */\n get numProcessed() {\n return this.#processed;\n }\n\n /**\n * Records a new history entry for the number of messages processed,\n * keeping the number of samples bounded to `maxSamples`.\n */\n sampleProcessRate(now: number, maxSamples = 10): this {\n while (this.#samples.length >= maxSamples) {\n this.#samples.shift();\n }\n this.#samples.push({processed: this.#processed, timestamp: now});\n return this;\n }\n\n getStats(): SubscriberStats {\n const pending = this.numPending;\n if (this.#samples.length < 2) {\n return {\n processRate: 0,\n pending,\n backlog: this.#backlogCount,\n backlogBytes: this.#bufferedBacklogBytes,\n totalBufferedBytes: this.#totalBufferedBytes,\n missedLastTimeout: this.#missedLastTimeout,\n };\n }\n const from = this.#samples[0];\n const to = must(this.#samples.at(-1));\n const processed = to.processed - from.processed;\n const seconds = (to.timestamp - from.timestamp) / 1000;\n const processRate = seconds === 0 ? 0 : processed / seconds;\n return {\n processRate,\n pending,\n backlog: this.#backlogCount,\n backlogBytes: this.#bufferedBacklogBytes,\n totalBufferedBytes: this.#totalBufferedBytes,\n missedLastTimeout: this.#missedLastTimeout,\n };\n }\n\n #missedLastTimeout = false;\n #laggingSinceMs: number | undefined;\n\n trackResponseResult(result: 'on-time' | 'timed-out') {\n this.#missedLastTimeout = result === 'timed-out';\n if (result === 'on-time') {\n this.#laggingSinceMs = undefined;\n }\n }\n\n /**\n * Reports the change rate of a slow subscriber (i.e. that missed the last\n * timeout) compared to the change rate of the (slowest) subscriber that\n * responded on time.\n *\n * * `lagging` indicates that this subscriber is slower\n * * `catching-up` indicates that it is faster\n *\n * Returns the total duration in which subscriber has been continuously\n * reported as `lagging`.\n */\n reportChangeRate(now: number, status: 'lagging' | 'catching-up') {\n assert(\n this.#missedLastTimeout,\n `reportChangeRate should only be called for slow subscribers`,\n );\n if (status === 'catching-up') {\n this.#laggingSinceMs = undefined;\n return 0;\n }\n this.#laggingSinceMs ??= now;\n return now - this.#laggingSinceMs;\n }\n\n supportsMessage(tag: ChangeTag) {\n switch (tag) {\n case 'update-table-metadata':\n // update-table-row-key is only understood by subscribers >= protocol v5\n return this.#protocolVersion >= 5;\n }\n return true;\n }\n\n /**\n * Ends the subscription without sending a downstream `['error', ...]`.\n *\n * This is deliberate, and not the same as reporting the failure to the\n * subscriber: `IncrementalSyncer` treats any `['error', ...]` as terminal and\n * shuts down to restore a fresh replica from litestream, whereas a clean end\n * backs off and re-subscribes. The failures routed here -- storer catchup\n * errors, backlog drain errors, backup-monitor errors -- are transient, so a\n * reconnect is the proportionate response and a fleet-wide restore is not.\n *\n * Callers are responsible for logging `err`; it is not carried downstream.\n */\n fail(_err?: unknown) {\n this.close();\n }\n\n close(\n error?: ErrorType,\n message?: string,\n details?: {replicaVersion?: string | undefined},\n ) {\n // Closing the subscriber must also release producers that are blocked on\n // backlog capacity; there is no future drain that could wake them.\n this.#backlog = null;\n this.#backlogBytes = 0;\n this.#backlogBackpressure.releaseAll();\n // The backlog can no longer grow, so nothing would ever resolve these.\n // Waiters re-check backlogFull, which is now false, and see the close.\n this.#resolveBacklogFullWaiters();\n\n if (error !== undefined) {\n // Wait for the ACK of the error message before closing the connection.\n void this.#sendDownstream([\n 'error',\n {type: error, message, ...details},\n ]).finally(() => this.#downstream.cancel());\n } else {\n this.#downstream.cancel();\n }\n }\n\n get #backlogCount() {\n return this.#backlog?.size ?? 0;\n }\n\n get #bufferedBacklogBytes() {\n // Include entries already handed to downstream but not yet consumed. Without\n // this, setCaughtUp() could move bytes out of #backlog faster than the\n // downstream Subscription can process them and release producers too early.\n return this.#backlogBytes + this.#backlogInFlightBytes;\n }\n\n get #totalBufferedBytes() {\n return this.#bufferedBacklogBytes + this.#pendingBytes;\n }\n\n #pushBacklog(entry: BacklogEntry) {\n assert(this.#backlog, 'cannot push to backlog after catchup completed');\n this.#backlog.push(entry);\n this.#backlogBytes += entryBytes(entry);\n if (this.backlogFull) {\n this.#resolveBacklogFullWaiters();\n }\n }\n\n #resolveBacklogFullWaiters() {\n const waiters = [...this.#backlogFullWaiters];\n this.#backlogFullWaiters.clear();\n for (const waiter of waiters) {\n waiter.resolve();\n }\n }\n\n #maybeWaitForBacklogSpace(): Promise<void> {\n return this.#backlogBackpressure.waitForSpace(this.#bufferedBacklogBytes);\n }\n\n async #drainBacklog() {\n const inFlight: {promise: Promise<void>; bytes: number}[] = [];\n let inFlightBytes = 0;\n\n try {\n for (;;) {\n const entry = this.#backlog?.shift();\n if (!entry) {\n const closed = this.#backlog === null;\n this.#backlog = null;\n this.#backlogBytes = 0;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n if (!closed && !this.#aligned) {\n // All changes up to the head have been handed to #sendChange()\n // (and thus tracked), and subsequent changes are sent directly.\n this.#aligned = true;\n this.#onAligned?.(this);\n this.#caughtUpWatermark = this.#watermark;\n this.#checkCaughtUp();\n void this.#lagReportingDisabled?.then(disabled => {\n if (disabled) {\n this.sendStatus(LAG_REPORTING_DISABLED);\n }\n });\n }\n break;\n }\n\n const bytes = entryBytes(entry);\n this.#backlogBytes -= bytes;\n this.#backlogInFlightBytes += bytes;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n\n // Send backlog entries in order, but keep only a bounded byte window in\n // flight. This avoids replacing one unbounded buffer with another inside\n // the downstream Subscription during catchup completion.\n const send = isQueuedStatus(entry)\n ? this.#sendStringifiedDownstream(entry.status).then(() => {})\n : this.#sendChange(entry);\n const promise = send.finally(() => {\n this.#backlogInFlightBytes -= bytes;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n });\n inFlight.push({promise, bytes});\n inFlightBytes += bytes;\n\n while (inFlightBytes >= this.#backlogBackpressure.highWaterBytes) {\n const next = must(inFlight.shift());\n await next.promise;\n inFlightBytes -= next.bytes;\n }\n }\n\n for (const {promise} of inFlight) {\n await promise;\n }\n } finally {\n this.#backlogDrain = null;\n this.#backlogBackpressure.releaseIfUnderLowWater(\n this.#bufferedBacklogBytes,\n );\n }\n }\n}\n\nclass ByteBackpressureGate {\n readonly highWaterBytes: number;\n readonly #lowWaterBytes: number;\n readonly #waiters: Resolver<void>[] = [];\n\n constructor(highWaterBytes: number, lowWaterRatio: number) {\n this.highWaterBytes = Math.max(1, highWaterBytes);\n this.#lowWaterBytes =\n this.highWaterBytes * Math.min(1, Math.max(0, lowWaterRatio));\n }\n\n waitForSpace(bufferedBytes: number): Promise<void> {\n if (bufferedBytes < this.highWaterBytes) {\n return promiseVoid;\n }\n\n // One waiter represents one send() call that has already appended its\n // change. The producer is released when the backlog falls back below the low\n // water mark or the subscriber closes.\n const r = resolver<void>();\n this.#waiters.push(r);\n return r.promise;\n }\n\n releaseIfUnderLowWater(bufferedBytes: number) {\n if (this.#waiters.length === 0 || bufferedBytes > this.#lowWaterBytes) {\n return;\n }\n\n // Use a low water mark so waiting producers are released in batches instead\n // of waking one at a time around the high water boundary.\n this.releaseAll();\n }\n\n releaseAll() {\n const waiters = this.#waiters.splice(0);\n for (const waiter of waiters) {\n waiter.resolve();\n }\n }\n}\n\n/**\n * Sent to an aligned subscriber when lag reporting is disabled upstream,\n * signaling that the subscriber is caught up. This is a lag report with a\n * `nextSendTimeMs` of 0 and no `lastTimings`, so that no lag is measured\n * (see `isLagReportingDisabledSignal()` in the replicator's recorder).\n */\nexport const LAG_REPORTING_DISABLED: Status = {\n tag: 'status',\n lagReport: {nextSendTimeMs: 0},\n};\n"],"mappings":"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"}
@@ -22,6 +22,12 @@ export declare class HttpService implements Service {
22
22
  protected _onStop(): Promise<void>;
23
23
  start(): Promise<string>;
24
24
  run(): Promise<void>;
25
+ /**
26
+ * Responds to subsequent health checks (both "/" and "/keepalive") with a
27
+ * 503, e.g. to signal the container runner to stop routing to, and shut
28
+ * down, a server that is draining itself (see {@link SelfDrain}).
29
+ */
30
+ failHealthChecks(): void;
25
31
  stop(): Promise<void>;
26
32
  }
27
33
  //# sourceMappingURL=http-service.d.ts.map
@@ -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;IAMxC,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,CAoB7B;IAEK,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAGzB;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;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,SAGf;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
+ #failHealthChecks = false;
20
21
  constructor(id, lc, opts, init) {
21
22
  const { port, keepaliveTimeoutMs, readinessGate = promiseVoid } = opts;
22
23
  this.id = id;
@@ -34,11 +35,13 @@ var HttpService = class {
34
35
  }
35
36
  async start() {
36
37
  this.#fastify.get("/", (_req, res) => {
37
- if (this.#ready) res.send("OK");
38
+ if (this.#failHealthChecks) res.code(503).send("Draining");
39
+ else if (this.#ready) res.send("OK");
38
40
  });
39
41
  this.#fastify.get("/keepalive", ({ headers }, res) => {
40
42
  this.#heartbeatMonitor?.onHeartbeat(headers);
41
- if (this.#ready) res.send("OK");
43
+ if (this.#failHealthChecks) res.code(503).send("Draining");
44
+ else if (this.#ready) res.send("OK");
42
45
  });
43
46
  await this.#init(this.#fastify);
44
47
  const address = await this.#fastify.listen({
@@ -53,6 +56,15 @@ var HttpService = class {
53
56
  await this.start();
54
57
  await this._state.stopped();
55
58
  }
59
+ /**
60
+ * Responds to subsequent health checks (both "/" and "/keepalive") with a
61
+ * 503, e.g. to signal the container runner to stop routing to, and shut
62
+ * down, a server that is draining itself (see {@link SelfDrain}).
63
+ */
64
+ failHealthChecks() {
65
+ this._lc.info?.(`${this.id}: failing health checks to self drain`);
66
+ this.#failHealthChecks = true;
67
+ }
56
68
  async stop() {
57
69
  this._lc.info?.(`${this.id}: no longer accepting connections`);
58
70
  this.#heartbeatMonitor?.stop();
@@ -1 +1 @@
1
- {"version":3,"file":"http-service.js","names":["#fastify","#port","#heartbeatMonitor","#init","#ready"],"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\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.#ready) {\n res.send('OK');\n }\n });\n this.#fastify.get('/keepalive', ({headers}, res) => {\n this.#heartbeatMonitor?.onHeartbeat(headers);\n 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 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;CAET,YACE,IACA,IACA,MACA,MACA;EACA,MAAM,EAAC,MAAM,oBAAoB,gBAAgB,gBAAe;EAChE,KAAK,KAAK;EACV,KAAK,MAAM,GAAG,YAAY,aAAa,KAAK,EAAE;EAC9C,KAAKA,WAAW,QAAQ;EACxB,KAAKC,QAAQ;EACb,KAAKE,QAAQ;EACb,KAAK,SAAS,IAAI,aAAa,EAAE;EACjC,KAAKD,oBAAoB,qBACrB,IAAI,iBAAiB,KAAK,KAAK,kBAAkB,IACjD,KAAA;EAEJ,cAAmB,WAAY,KAAKE,SAAS,IAAK;CACpD;CAGA,WAAqB,CAAC;CACtB,UAAmC;EACjC,OAAO;CACT;CAGA,MAAM,QAAyB;EAC7B,KAAKJ,SAAS,IAAI,MAAM,MAAM,QAAQ;GACpC,IAAI,KAAKI,QACP,IAAI,KAAK,IAAI;EAEjB,CAAC;EACD,KAAKJ,SAAS,IAAI,eAAe,EAAC,WAAU,QAAQ;GAClD,KAAKE,mBAAmB,YAAY,OAAO;GAC3C,IAAI,KAAKE,QACP,IAAI,KAAK,IAAI;EAEjB,CAAC;EACD,MAAM,KAAKD,MAAM,KAAKH,QAAQ;EAC9B,MAAM,UAAU,MAAM,KAAKA,SAAS,OAAO;GACzC,MAAM;GACN,MAAM,KAAKC;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;CAEA,MAAM,OAAsB;EAC1B,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,kCAAkC;EAC7D,KAAKC,mBAAmB,KAAK;EAC7B,KAAK,OAAO,KAAK,KAAK,GAAG;EACzB,MAAM,KAAKF,SAAS,MAAM;EAC1B,MAAM,KAAK,QAAQ;CACrB;AACF"}
1
+ {"version":3,"file":"http-service.js","names":["#fastify","#port","#heartbeatMonitor","#init","#ready","#failHealthChecks"],"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 container runner to stop routing to, and shut\n * down, a server that is draining itself (see {@link SelfDrain}).\n */\n failHealthChecks() {\n this._lc.info?.(`${this.id}: failing health checks to self drain`);\n this.#failHealthChecks = true;\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,KAAKA,WAAW,QAAQ;EACxB,KAAKC,QAAQ;EACb,KAAKE,QAAQ;EACb,KAAK,SAAS,IAAI,aAAa,EAAE;EACjC,KAAKD,oBAAoB,qBACrB,IAAI,iBAAiB,KAAK,KAAK,kBAAkB,IACjD,KAAA;EAEJ,cAAmB,WAAY,KAAKE,SAAS,IAAK;CACpD;CAGA,WAAqB,CAAC;CACtB,UAAmC;EACjC,OAAO;CACT;CAGA,MAAM,QAAyB;EAC7B,KAAKJ,SAAS,IAAI,MAAM,MAAM,QAAQ;GACpC,IAAI,KAAKK,mBACP,IAAI,KAAK,GAAG,EAAE,KAAK,UAAU;QACxB,IAAI,KAAKD,QACd,IAAI,KAAK,IAAI;EAEjB,CAAC;EACD,KAAKJ,SAAS,IAAI,eAAe,EAAC,WAAU,QAAQ;GAClD,KAAKE,mBAAmB,YAAY,OAAO;GAC3C,IAAI,KAAKG,mBACP,IAAI,KAAK,GAAG,EAAE,KAAK,UAAU;QACxB,IAAI,KAAKD,QACd,IAAI,KAAK,IAAI;EAEjB,CAAC;EACD,MAAM,KAAKD,MAAM,KAAKH,QAAQ;EAC9B,MAAM,UAAU,MAAM,KAAKA,SAAS,OAAO;GACzC,MAAM;GACN,MAAM,KAAKC;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,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,sCAAsC;EACjE,KAAKI,oBAAoB;CAC3B;CAEA,MAAM,OAAsB;EAC1B,KAAK,IAAI,OAAO,GAAG,KAAK,GAAG,kCAAkC;EAC7D,KAAKH,mBAAmB,KAAK;EAC7B,KAAK,OAAO,KAAK,KAAK,GAAG;EACzB,MAAM,KAAKF,SAAS,MAAM;EAC1B,MAAM,KAAK,QAAQ;CACrB;AACF"}
@@ -15,6 +15,9 @@ import type { SingletonService } from './service.ts';
15
15
  * are stopped without draining. `SIGQUIT` is used to represent an
16
16
  * intentional shutdown (for which draining is not beneficial), whereas
17
17
  * `SIGABRT` is used for unexpected process exits.
18
+ *
19
+ * A clean exit (i.e. code 0) of a worker of either type that is not the
20
+ * result of a drain is treated as an intentional shutdown of the server.
18
21
  */
19
22
  export type WorkerType = 'user-facing' | 'supporting';
20
23
  export declare const GRACEFUL_SHUTDOWN: readonly ['SIGTERM', 'SIGINT'];
@@ -36,6 +39,36 @@ export declare class ProcessManager {
36
39
  allWorkersReady(): Promise<void>;
37
40
  logErrorAndExit(err: unknown, name: string): void;
38
41
  }
42
+ export type SelfDrainOptions = {
43
+ /**
44
+ * The time after a self drain is requested at which the server sends itself
45
+ * a `SIGTERM`, unless one was received sooner (e.g. from the container
46
+ * runner).
47
+ */
48
+ delayMs: number;
49
+ /**
50
+ * Called when a self drain is requested. This should fail health checks
51
+ * (both readiness and liveness) so that the container runner stops routing
52
+ * to the server and initiates its normal shutdown process.
53
+ */
54
+ onRequested: () => void;
55
+ };
56
+ /**
57
+ * Coordinates a drain that is initiated by the server itself (e.g. by a
58
+ * view-syncer that has been granted permission to restore a new replica),
59
+ * as opposed to one initiated by the container runner with a `SIGTERM`.
60
+ *
61
+ * Health checks start failing immediately, upon which an orchestrator such
62
+ * as Kubernetes stops routing to the server and shuts it down as usual
63
+ * (i.e. running any pre-stop hooks before sending `SIGTERM`). As a backstop,
64
+ * and for environments without liveness checks, the server sends itself a
65
+ * `SIGTERM` after the configured delay.
66
+ */
67
+ export declare class SelfDrain {
68
+ #private;
69
+ constructor(lc: LogContext, proc: EventEmitter, opts: SelfDrainOptions);
70
+ request(): void;
71
+ }
39
72
  /**
40
73
  * Runs the specified services, stopping them on `SIGTERM` or `SIGINT` with
41
74
  * an optional {@link SingletonService.drain drain()}, or 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;AAE/B,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;;;;GAYG;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;CA8EF;AAED;;;;;;GAMG;AAEH,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,YAAY,EACpB,GAAG,QAAQ,EAAE,gBAAgB,EAAE,GAC9B,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,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":"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;AAE/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,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;;;;;;GAMG;AAEH,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,YAAY,EACpB,GAAG,QAAQ,EAAE,gBAAgB,EAAE,GAC9B,OAAO,CAAC,IAAI,CAAC,CA6Cf;AAED,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"}