@rocicorp/zero 1.8.0-canary.5 → 1.8.0-canary.7
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.
- package/README.md +6 -23
- package/out/analyze-query/src/bin-analyze.js +1 -1
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero/src/zero.js +1 -1
- package/out/zero-cache/src/custom/fetch.js +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.js +1 -1
- package/out/zero-cache/src/db/migration-lite.d.ts +0 -6
- package/out/zero-cache/src/db/migration-lite.d.ts.map +1 -1
- package/out/zero-cache/src/db/migration-lite.js +2 -22
- package/out/zero-cache/src/db/migration-lite.js.map +1 -1
- package/out/zero-cache/src/observability/metrics.d.ts +3 -0
- package/out/zero-cache/src/observability/metrics.d.ts.map +1 -1
- package/out/zero-cache/src/observability/metrics.js +21 -1
- package/out/zero-cache/src/observability/metrics.js.map +1 -1
- package/out/zero-cache/src/server/anonymous-otel-start.js +1 -1
- package/out/zero-cache/src/server/inspector-delegate.js +1 -1
- package/out/zero-cache/src/server/main.d.ts.map +1 -1
- package/out/zero-cache/src/server/main.js +4 -2
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/otel-start.d.ts.map +1 -1
- package/out/zero-cache/src/server/otel-start.js +7 -0
- package/out/zero-cache/src/server/otel-start.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-schema.js +2 -2
- package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +4 -0
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slot-health.d.ts +15 -0
- package/out/zero-cache/src/services/change-source/pg/replication-slot-health.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js +72 -0
- package/out/zero-cache/src/services/change-source/pg/replication-slot-health.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/broadcast.d.ts +2 -0
- package/out/zero-cache/src/services/change-streamer/broadcast.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.js +11 -4
- package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/forwarder.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/forwarder.js +43 -2
- package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +8 -2
- package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.js +112 -12
- package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts +1 -0
- package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +41 -1
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +0 -2
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/litestream/metrics.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/metrics.js +14 -29
- package/out/zero-cache/src/services/litestream/metrics.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js +1 -1
- package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js +10 -6
- package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js +48 -11
- package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js +12 -0
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +10 -3
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
- package/out/zero-cache/src/workers/connection.js +12 -0
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +72 -22
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/mutation-tracker.js +1 -1
- package/out/zero-client/src/client/query-manager.js +2 -2
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.js +3 -3
- package/out/zero-client/src/mod.d.ts +4 -4
- package/out/zero-client/src/mod.d.ts.map +1 -1
- package/out/zero-client/src/mod.js +1 -1
- package/out/zero-react/src/zero.js +1 -1
- package/out/zero-server/src/custom.js +1 -1
- package/out/zero-server/src/queries/process-queries.js +3 -3
- package/out/zero-solid/src/zero.js +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LogContext } from '@rocicorp/logger';
|
|
2
2
|
import type { WatermarkedChange } from './change-streamer-service.ts';
|
|
3
3
|
import type { Subscriber } from './subscriber.ts';
|
|
4
|
+
export type BroadcastReleaseMode = 'all-subscribers' | 'consensus-timeout';
|
|
4
5
|
/**
|
|
5
6
|
* Initiates and tracks the progress of a change broadcasted to
|
|
6
7
|
* a set of subscribers.
|
|
@@ -26,6 +27,7 @@ export declare class Broadcast {
|
|
|
26
27
|
constructor(subscribers: Iterable<Subscriber>, change: WatermarkedChange);
|
|
27
28
|
get isDone(): boolean;
|
|
28
29
|
get done(): Promise<void>;
|
|
30
|
+
get releaseMode(): BroadcastReleaseMode;
|
|
29
31
|
/**
|
|
30
32
|
* Checks for pathological situations in which flow should be reenabled
|
|
31
33
|
* before all subscribers have acked.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"broadcast.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/broadcast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,8BAA8B,CAAC;AACpE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,qBAAa,SAAS;;IACpB;;;OAGG;IACH,MAAM,CAAC,eAAe,CACpB,WAAW,EAAE,QAAQ,CAAC,UAAU,CAAC,EACjC,MAAM,EAAE,iBAAiB;
|
|
1
|
+
{"version":3,"file":"broadcast.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/broadcast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,8BAA8B,CAAC;AACpE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAEhD,MAAM,MAAM,oBAAoB,GAAG,iBAAiB,GAAG,mBAAmB,CAAC;AAE3E;;;;;;;;;;GAUG;AACH,qBAAa,SAAS;;IACpB;;;OAGG;IACH,MAAM,CAAC,eAAe,CACpB,WAAW,EAAE,QAAQ,CAAC,UAAU,CAAC,EACjC,MAAM,EAAE,iBAAiB;IAmB3B;;;OAGG;gBACS,WAAW,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,iBAAiB;IAsCxE,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,IAAI,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAExB;IAED,IAAI,WAAW,IAAI,oBAAoB,CAEtC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoEG;IACH,aAAa,CACX,EAAE,EAAE,UAAU,EACd,6BAA6B,EAAE,MAAM,EACrC,GAAG,EAAE,MAAM;CAkDd"}
|
|
@@ -23,6 +23,7 @@ var Broadcast = class {
|
|
|
23
23
|
#completed;
|
|
24
24
|
#done = resolver();
|
|
25
25
|
#isDone = false;
|
|
26
|
+
#releaseMode;
|
|
26
27
|
#watermark;
|
|
27
28
|
#majority;
|
|
28
29
|
#start = performance.now();
|
|
@@ -40,7 +41,7 @@ var Broadcast = class {
|
|
|
40
41
|
const changes = sub.numPending + 1;
|
|
41
42
|
sub.send(change).catch(() => {}).finally(() => this.#markCompleted(sub, changes));
|
|
42
43
|
}
|
|
43
|
-
if (this.#pending.size === 0) this.#setDone();
|
|
44
|
+
if (this.#pending.size === 0) this.#setDone("all-subscribers");
|
|
44
45
|
}
|
|
45
46
|
#markCompleted(sub, changes) {
|
|
46
47
|
const elapsed = (this.#latestCompleted = performance.now()) - this.#start;
|
|
@@ -50,10 +51,12 @@ var Broadcast = class {
|
|
|
50
51
|
elapsed
|
|
51
52
|
});
|
|
52
53
|
this.#pending.delete(sub);
|
|
53
|
-
if (this.#pending.size === 0) this.#setDone();
|
|
54
|
+
if (this.#pending.size === 0) this.#setDone("all-subscribers");
|
|
54
55
|
}
|
|
55
|
-
#setDone() {
|
|
56
|
+
#setDone(releaseMode) {
|
|
57
|
+
if (this.#isDone) return;
|
|
56
58
|
this.#isDone = true;
|
|
59
|
+
this.#releaseMode = releaseMode;
|
|
57
60
|
this.#done.resolve();
|
|
58
61
|
}
|
|
59
62
|
get isDone() {
|
|
@@ -62,6 +65,9 @@ var Broadcast = class {
|
|
|
62
65
|
get done() {
|
|
63
66
|
return this.#done.promise;
|
|
64
67
|
}
|
|
68
|
+
get releaseMode() {
|
|
69
|
+
return this.#releaseMode ?? "all-subscribers";
|
|
70
|
+
}
|
|
65
71
|
/**
|
|
66
72
|
* Checks for pathological situations in which flow should be reenabled
|
|
67
73
|
* before all subscribers have acked.
|
|
@@ -132,6 +138,7 @@ var Broadcast = class {
|
|
|
132
138
|
* @returns `true` if the broadcast was already done or was marked done.
|
|
133
139
|
*/
|
|
134
140
|
checkProgress(lc, flowControlConsensusPaddingMs, now) {
|
|
141
|
+
if (this.#isDone) return true;
|
|
135
142
|
if (this.#pending.size === 0) return true;
|
|
136
143
|
const elapsed = now - this.#start;
|
|
137
144
|
if (this.#completed.length < this.#majority) {
|
|
@@ -140,7 +147,7 @@ var Broadcast = class {
|
|
|
140
147
|
}
|
|
141
148
|
if (now - this.#latestCompleted >= flowControlConsensusPaddingMs) {
|
|
142
149
|
this.#logWithState(lc, `continuing with ${this.#pending.size} subscriber(s) still pending`, elapsed);
|
|
143
|
-
this.#setDone();
|
|
150
|
+
this.#setDone("consensus-timeout");
|
|
144
151
|
return true;
|
|
145
152
|
}
|
|
146
153
|
return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"broadcast.js","names":["#pending","#completed","#done","#watermark","#majority","#start","#markCompleted","#setDone","#latestCompleted","#isDone","#logWithState"],"sources":["../../../../../../zero-cache/src/services/change-streamer/broadcast.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport type {WatermarkedChange} from './change-streamer-service.ts';\nimport type {Subscriber} from './subscriber.ts';\n\n/**\n * Initiates and tracks the progress of a change broadcasted to\n * a set of subscribers.\n *\n * Creating a `Broadcast` automatically initiates the send.\n *\n * By default, {@link Broadcast.done} resolves when all subscribers\n * have acked the change. However, {@link Broadcast.checkProgress()}\n * can be called to resolve the broadcast earlier based on the flow\n * control policy.\n */\nexport class Broadcast {\n /**\n * Sends the change to the subscribers without the tracking machinery.\n * This is suitable for fire-and-forget (i.e. pipelined) sends.\n */\n static withoutTracking(\n subscribers: Iterable<Subscriber>,\n change: WatermarkedChange,\n ) {\n for (const sub of subscribers) {\n void sub.send(change);\n }\n }\n\n readonly #pending: Set<Subscriber>;\n readonly #completed: Completed[];\n readonly #done = resolver();\n #isDone = false;\n\n readonly #watermark: string;\n readonly #majority: number;\n\n readonly #start = performance.now();\n #latestCompleted = Number.MAX_VALUE;\n\n /**\n * Broadcasts the `change` to the `subscribers` and tracks their\n * completion.\n */\n constructor(subscribers: Iterable<Subscriber>, change: WatermarkedChange) {\n this.#pending = new Set(subscribers);\n this.#completed = [];\n this.#watermark = change[0];\n this.#majority = Math.floor(this.#pending.size / 2) + 1;\n\n for (const sub of this.#pending) {\n const changes = sub.numPending + 1; // add one for this `change`\n void sub\n .send(change)\n .catch(() => {})\n .finally(() => this.#markCompleted(sub, changes));\n }\n\n // set done if there are no subscribers (mainly for tests)\n if (this.#pending.size === 0) {\n this.#setDone();\n }\n }\n\n #markCompleted(sub: Subscriber, changes: number) {\n const elapsed = (this.#latestCompleted = performance.now()) - this.#start;\n this.#completed.push({sub, changes, elapsed});\n this.#pending.delete(sub);\n if (this.#pending.size === 0) {\n this.#setDone();\n }\n }\n\n #setDone() {\n this.#isDone = true;\n this.#done.resolve();\n }\n\n get isDone(): boolean {\n return this.#isDone;\n }\n\n get done(): Promise<void> {\n return this.#done.promise;\n }\n\n /**\n * Checks for pathological situations in which flow should be reenabled\n * before all subscribers have acked.\n *\n * ### Background\n *\n * The purpose of flow control is to pull upstream replication changes\n * no faster than the rate as they are processed by downstream subscribers\n * in the steady state. In the change-streamer, this is done by occasionally\n * waiting for ACKs from subscribers before continuing; without doing so,\n * I/O buffers fill up and cause the system to spend most of its time in GC.\n *\n * However, the naive algorithm of always waiting for all subscribers (e.g.\n * `Promise.all()`) can behave poorly in scenarios where subscribers\n * are imbalanced:\n * * New subscribers may have a backlog of changes to catch up with.\n * Having all subscribers wait for the new subscriber to catch up results\n * in delaying the entire application.\n * * Broken TCP connections similarly require all subscribers to wait until\n * connection liveness checks kick in and disconnect the subscriber.\n *\n * A simplistic approach is to add a limit to the amount of time waiting for\n * subscribers, i.e. an ack timeout. However, deciding what this timeout\n * should be is non-trivial because of the heterogeneous nature of changes;\n * while most changes operate on single rows and are relatively predictable\n * in terms of running time, some changes are table-wide operations and can\n * legitimately take an arbitrary amount of time. In such scenarios, a\n * timeout that is too short can stop progress on replication altogether.\n *\n * ### Consensus-based Timeout Algorithm\n *\n * To address these shortcomings, a \"consensus-based timeout\" algorithm is\n * used:\n * * Wait for more than half of the subscribers to finish. (In\n * case of a single node, or the case of one replication-manager\n * and one view-syncer, this reduces to waiting for all subscribers.)\n * * Once more than half of the subscribers have finished, proceed after\n * a fixed timeout elapses (e.g. 1 second), even if not all subscribers\n * have finished.\n *\n * In other words, the subscribers themselves are used to determine the\n * timeout of each batch of changes; the majority determines this when\n * they complete, upon which a timeout is logically started.\n *\n * In the common case, the remaining subscribers finish soon afterward and\n * the timeout never elapses. However, in pathological cases where a minority\n * of subscribers have a disproportionate amount of load, some will still\n * be processing (or otherwise unresponsive). These subscribers are given\n * a bounded amount of time to catch up at each flushed batch, up to the\n * timeout interval. This guarantees eventual catchup because the\n * subscribers with a backlog of changes necessarily have a higher\n * processing rate than the subscribers that finished (and are made to wait).\n *\n * ### Not implemented: Broken connection detection\n *\n * If a subscriber has not made progress for a certain interval, the\n * algorithm could theoretically drop it preemptively, supplementing the\n * existing websocket-level liveness checks.\n *\n * However, a more reliable approach would be to change the replicator\n * to use non-blocking writes, and subsequently increase the frequency of\n * connection-level liveness checks. The current synchronous replica writes\n * can delay both ping responsiveness and change progress arbitrarily (e.g.\n * a large index creation); an independently liveness check that is not\n * delayed by synchronous writes on the subscriber would be a more failsafe\n * solution.\n *\n * @returns `true` if the broadcast was already done or was marked done.\n */\n checkProgress(\n lc: LogContext,\n flowControlConsensusPaddingMs: number,\n now: number,\n ) {\n if (this.#pending.size === 0) {\n return true;\n }\n const elapsed = now - this.#start;\n if (this.#completed.length < this.#majority) {\n if (elapsed >= 1000) {\n this.#logWithState(\n lc,\n `waiting for at least ${this.#majority} subscribers to finish`,\n elapsed,\n );\n }\n return false;\n }\n // Note: In the implementation, #latestCompleted is always updated,\n // even after the majority is reached. This is fine and does not affect\n // the important properties of the algorithm.\n if (now - this.#latestCompleted >= flowControlConsensusPaddingMs) {\n this.#logWithState(\n lc,\n `continuing with ${this.#pending.size} subscriber(s) still pending`,\n elapsed,\n );\n this.#setDone();\n return true;\n }\n return false;\n }\n\n #logWithState(lc: LogContext, msg: string, elapsed: number) {\n lc.withContext('watermark', this.#watermark).info?.(\n `${msg} (${elapsed.toFixed(3)} ms)`,\n {\n completed: this.#completed.map(d => ({\n id: d.sub.id,\n processed: d.changes,\n elapsed: d.elapsed,\n })),\n pending: Array.from(this.#pending, sub => ({\n id: sub.id,\n ...sub.getStats(),\n })),\n },\n );\n }\n}\n\n/** Tracks the completed result of a single subscriber. */\ntype Completed = {\n sub: Subscriber;\n /** The number of changes processed. */\n changes: number;\n /** The elapsed milliseconds. */\n elapsed: number;\n};\n"],"mappings":";;;;;;;;;;;;;AAgBA,IAAa,YAAb,MAAuB;;;;;CAKrB,OAAO,gBACL,aACA,QACA;EACA,KAAK,MAAM,OAAO,aAChB,IAAS,KAAK,MAAM;CAExB;CAEA;CACA;CACA,QAAiB,SAAS;CAC1B,UAAU;CAEV;CACA;CAEA,SAAkB,YAAY,IAAI;CAClC,mBAAmB,OAAO;;;;;CAM1B,YAAY,aAAmC,QAA2B;EACxE,KAAKA,WAAW,IAAI,IAAI,WAAW;EACnC,KAAKC,aAAa,CAAC;EACnB,KAAKE,aAAa,OAAO;EACzB,KAAKC,YAAY,KAAK,MAAM,KAAKJ,SAAS,OAAO,CAAC,IAAI;EAEtD,KAAK,MAAM,OAAO,KAAKA,UAAU;GAC/B,MAAM,UAAU,IAAI,aAAa;GACjC,IACG,KAAK,MAAM,EACX,YAAY,CAAC,CAAC,EACd,cAAc,KAAKM,eAAe,KAAK,OAAO,CAAC;EACpD;EAGA,IAAI,KAAKN,SAAS,SAAS,GACzB,KAAKO,SAAS;CAElB;CAEA,eAAe,KAAiB,SAAiB;EAC/C,MAAM,WAAW,KAAKC,mBAAmB,YAAY,IAAI,KAAK,KAAKH;EACnE,KAAKJ,WAAW,KAAK;GAAC;GAAK;GAAS;EAAO,CAAC;EAC5C,KAAKD,SAAS,OAAO,GAAG;EACxB,IAAI,KAAKA,SAAS,SAAS,GACzB,KAAKO,SAAS;CAElB;CAEA,WAAW;EACT,KAAKE,UAAU;EACf,KAAKP,MAAM,QAAQ;CACrB;CAEA,IAAI,SAAkB;EACpB,OAAO,KAAKO;CACd;CAEA,IAAI,OAAsB;EACxB,OAAO,KAAKP,MAAM;CACpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuEA,cACE,IACA,+BACA,KACA;EACA,IAAI,KAAKF,SAAS,SAAS,GACzB,OAAO;EAET,MAAM,UAAU,MAAM,KAAKK;EAC3B,IAAI,KAAKJ,WAAW,SAAS,KAAKG,WAAW;GAC3C,IAAI,WAAW,KACb,KAAKM,cACH,IACA,wBAAwB,KAAKN,UAAU,yBACvC,OACF;GAEF,OAAO;EACT;EAIA,IAAI,MAAM,KAAKI,oBAAoB,+BAA+B;GAChE,KAAKE,cACH,IACA,mBAAmB,KAAKV,SAAS,KAAK,+BACtC,OACF;GACA,KAAKO,SAAS;GACd,OAAO;EACT;EACA,OAAO;CACT;CAEA,cAAc,IAAgB,KAAa,SAAiB;EAC1D,GAAG,YAAY,aAAa,KAAKJ,UAAU,EAAE,OAC3C,GAAG,IAAI,IAAI,QAAQ,QAAQ,CAAC,EAAE,OAC9B;GACE,WAAW,KAAKF,WAAW,KAAI,OAAM;IACnC,IAAI,EAAE,IAAI;IACV,WAAW,EAAE;IACb,SAAS,EAAE;GACb,EAAE;GACF,SAAS,MAAM,KAAK,KAAKD,WAAU,SAAQ;IACzC,IAAI,IAAI;IACR,GAAG,IAAI,SAAS;GAClB,EAAE;EACJ,CACF;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"broadcast.js","names":["#pending","#completed","#done","#watermark","#majority","#start","#markCompleted","#setDone","#latestCompleted","#isDone","#releaseMode","#logWithState"],"sources":["../../../../../../zero-cache/src/services/change-streamer/broadcast.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport type {WatermarkedChange} from './change-streamer-service.ts';\nimport type {Subscriber} from './subscriber.ts';\n\nexport type BroadcastReleaseMode = 'all-subscribers' | 'consensus-timeout';\n\n/**\n * Initiates and tracks the progress of a change broadcasted to\n * a set of subscribers.\n *\n * Creating a `Broadcast` automatically initiates the send.\n *\n * By default, {@link Broadcast.done} resolves when all subscribers\n * have acked the change. However, {@link Broadcast.checkProgress()}\n * can be called to resolve the broadcast earlier based on the flow\n * control policy.\n */\nexport class Broadcast {\n /**\n * Sends the change to the subscribers without the tracking machinery.\n * This is suitable for fire-and-forget (i.e. pipelined) sends.\n */\n static withoutTracking(\n subscribers: Iterable<Subscriber>,\n change: WatermarkedChange,\n ) {\n for (const sub of subscribers) {\n void sub.send(change);\n }\n }\n\n readonly #pending: Set<Subscriber>;\n readonly #completed: Completed[];\n readonly #done = resolver();\n #isDone = false;\n #releaseMode: BroadcastReleaseMode | undefined;\n\n readonly #watermark: string;\n readonly #majority: number;\n\n readonly #start = performance.now();\n #latestCompleted = Number.MAX_VALUE;\n\n /**\n * Broadcasts the `change` to the `subscribers` and tracks their\n * completion.\n */\n constructor(subscribers: Iterable<Subscriber>, change: WatermarkedChange) {\n this.#pending = new Set(subscribers);\n this.#completed = [];\n this.#watermark = change[0];\n this.#majority = Math.floor(this.#pending.size / 2) + 1;\n\n for (const sub of this.#pending) {\n const changes = sub.numPending + 1; // add one for this `change`\n void sub\n .send(change)\n .catch(() => {})\n .finally(() => this.#markCompleted(sub, changes));\n }\n\n // set done if there are no subscribers (mainly for tests)\n if (this.#pending.size === 0) {\n this.#setDone('all-subscribers');\n }\n }\n\n #markCompleted(sub: Subscriber, changes: number) {\n const elapsed = (this.#latestCompleted = performance.now()) - this.#start;\n this.#completed.push({sub, changes, elapsed});\n this.#pending.delete(sub);\n if (this.#pending.size === 0) {\n this.#setDone('all-subscribers');\n }\n }\n\n #setDone(releaseMode: BroadcastReleaseMode) {\n if (this.#isDone) {\n return;\n }\n this.#isDone = true;\n this.#releaseMode = releaseMode;\n this.#done.resolve();\n }\n\n get isDone(): boolean {\n return this.#isDone;\n }\n\n get done(): Promise<void> {\n return this.#done.promise;\n }\n\n get releaseMode(): BroadcastReleaseMode {\n return this.#releaseMode ?? 'all-subscribers';\n }\n\n /**\n * Checks for pathological situations in which flow should be reenabled\n * before all subscribers have acked.\n *\n * ### Background\n *\n * The purpose of flow control is to pull upstream replication changes\n * no faster than the rate as they are processed by downstream subscribers\n * in the steady state. In the change-streamer, this is done by occasionally\n * waiting for ACKs from subscribers before continuing; without doing so,\n * I/O buffers fill up and cause the system to spend most of its time in GC.\n *\n * However, the naive algorithm of always waiting for all subscribers (e.g.\n * `Promise.all()`) can behave poorly in scenarios where subscribers\n * are imbalanced:\n * * New subscribers may have a backlog of changes to catch up with.\n * Having all subscribers wait for the new subscriber to catch up results\n * in delaying the entire application.\n * * Broken TCP connections similarly require all subscribers to wait until\n * connection liveness checks kick in and disconnect the subscriber.\n *\n * A simplistic approach is to add a limit to the amount of time waiting for\n * subscribers, i.e. an ack timeout. However, deciding what this timeout\n * should be is non-trivial because of the heterogeneous nature of changes;\n * while most changes operate on single rows and are relatively predictable\n * in terms of running time, some changes are table-wide operations and can\n * legitimately take an arbitrary amount of time. In such scenarios, a\n * timeout that is too short can stop progress on replication altogether.\n *\n * ### Consensus-based Timeout Algorithm\n *\n * To address these shortcomings, a \"consensus-based timeout\" algorithm is\n * used:\n * * Wait for more than half of the subscribers to finish. (In\n * case of a single node, or the case of one replication-manager\n * and one view-syncer, this reduces to waiting for all subscribers.)\n * * Once more than half of the subscribers have finished, proceed after\n * a fixed timeout elapses (e.g. 1 second), even if not all subscribers\n * have finished.\n *\n * In other words, the subscribers themselves are used to determine the\n * timeout of each batch of changes; the majority determines this when\n * they complete, upon which a timeout is logically started.\n *\n * In the common case, the remaining subscribers finish soon afterward and\n * the timeout never elapses. However, in pathological cases where a minority\n * of subscribers have a disproportionate amount of load, some will still\n * be processing (or otherwise unresponsive). These subscribers are given\n * a bounded amount of time to catch up at each flushed batch, up to the\n * timeout interval. This guarantees eventual catchup because the\n * subscribers with a backlog of changes necessarily have a higher\n * processing rate than the subscribers that finished (and are made to wait).\n *\n * ### Not implemented: Broken connection detection\n *\n * If a subscriber has not made progress for a certain interval, the\n * algorithm could theoretically drop it preemptively, supplementing the\n * existing websocket-level liveness checks.\n *\n * However, a more reliable approach would be to change the replicator\n * to use non-blocking writes, and subsequently increase the frequency of\n * connection-level liveness checks. The current synchronous replica writes\n * can delay both ping responsiveness and change progress arbitrarily (e.g.\n * a large index creation); an independently liveness check that is not\n * delayed by synchronous writes on the subscriber would be a more failsafe\n * solution.\n *\n * @returns `true` if the broadcast was already done or was marked done.\n */\n checkProgress(\n lc: LogContext,\n flowControlConsensusPaddingMs: number,\n now: number,\n ) {\n if (this.#isDone) {\n return true;\n }\n if (this.#pending.size === 0) {\n return true;\n }\n const elapsed = now - this.#start;\n if (this.#completed.length < this.#majority) {\n if (elapsed >= 1000) {\n this.#logWithState(\n lc,\n `waiting for at least ${this.#majority} subscribers to finish`,\n elapsed,\n );\n }\n return false;\n }\n // Note: In the implementation, #latestCompleted is always updated,\n // even after the majority is reached. This is fine and does not affect\n // the important properties of the algorithm.\n if (now - this.#latestCompleted >= flowControlConsensusPaddingMs) {\n this.#logWithState(\n lc,\n `continuing with ${this.#pending.size} subscriber(s) still pending`,\n elapsed,\n );\n this.#setDone('consensus-timeout');\n return true;\n }\n return false;\n }\n\n #logWithState(lc: LogContext, msg: string, elapsed: number) {\n lc.withContext('watermark', this.#watermark).info?.(\n `${msg} (${elapsed.toFixed(3)} ms)`,\n {\n completed: this.#completed.map(d => ({\n id: d.sub.id,\n processed: d.changes,\n elapsed: d.elapsed,\n })),\n pending: Array.from(this.#pending, sub => ({\n id: sub.id,\n ...sub.getStats(),\n })),\n },\n );\n }\n}\n\n/** Tracks the completed result of a single subscriber. */\ntype Completed = {\n sub: Subscriber;\n /** The number of changes processed. */\n changes: number;\n /** The elapsed milliseconds. */\n elapsed: number;\n};\n"],"mappings":";;;;;;;;;;;;;AAkBA,IAAa,YAAb,MAAuB;;;;;CAKrB,OAAO,gBACL,aACA,QACA;EACA,KAAK,MAAM,OAAO,aAChB,IAAS,KAAK,MAAM;CAExB;CAEA;CACA;CACA,QAAiB,SAAS;CAC1B,UAAU;CACV;CAEA;CACA;CAEA,SAAkB,YAAY,IAAI;CAClC,mBAAmB,OAAO;;;;;CAM1B,YAAY,aAAmC,QAA2B;EACxE,KAAKA,WAAW,IAAI,IAAI,WAAW;EACnC,KAAKC,aAAa,CAAC;EACnB,KAAKE,aAAa,OAAO;EACzB,KAAKC,YAAY,KAAK,MAAM,KAAKJ,SAAS,OAAO,CAAC,IAAI;EAEtD,KAAK,MAAM,OAAO,KAAKA,UAAU;GAC/B,MAAM,UAAU,IAAI,aAAa;GACjC,IACG,KAAK,MAAM,EACX,YAAY,CAAC,CAAC,EACd,cAAc,KAAKM,eAAe,KAAK,OAAO,CAAC;EACpD;EAGA,IAAI,KAAKN,SAAS,SAAS,GACzB,KAAKO,SAAS,iBAAiB;CAEnC;CAEA,eAAe,KAAiB,SAAiB;EAC/C,MAAM,WAAW,KAAKC,mBAAmB,YAAY,IAAI,KAAK,KAAKH;EACnE,KAAKJ,WAAW,KAAK;GAAC;GAAK;GAAS;EAAO,CAAC;EAC5C,KAAKD,SAAS,OAAO,GAAG;EACxB,IAAI,KAAKA,SAAS,SAAS,GACzB,KAAKO,SAAS,iBAAiB;CAEnC;CAEA,SAAS,aAAmC;EAC1C,IAAI,KAAKE,SACP;EAEF,KAAKA,UAAU;EACf,KAAKC,eAAe;EACpB,KAAKR,MAAM,QAAQ;CACrB;CAEA,IAAI,SAAkB;EACpB,OAAO,KAAKO;CACd;CAEA,IAAI,OAAsB;EACxB,OAAO,KAAKP,MAAM;CACpB;CAEA,IAAI,cAAoC;EACtC,OAAO,KAAKQ,gBAAgB;CAC9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuEA,cACE,IACA,+BACA,KACA;EACA,IAAI,KAAKD,SACP,OAAO;EAET,IAAI,KAAKT,SAAS,SAAS,GACzB,OAAO;EAET,MAAM,UAAU,MAAM,KAAKK;EAC3B,IAAI,KAAKJ,WAAW,SAAS,KAAKG,WAAW;GAC3C,IAAI,WAAW,KACb,KAAKO,cACH,IACA,wBAAwB,KAAKP,UAAU,yBACvC,OACF;GAEF,OAAO;EACT;EAIA,IAAI,MAAM,KAAKI,oBAAoB,+BAA+B;GAChE,KAAKG,cACH,IACA,mBAAmB,KAAKX,SAAS,KAAK,+BACtC,OACF;GACA,KAAKO,SAAS,mBAAmB;GACjC,OAAO;EACT;EACA,OAAO;CACT;CAEA,cAAc,IAAgB,KAAa,SAAiB;EAC1D,GAAG,YAAY,aAAa,KAAKJ,UAAU,EAAE,OAC3C,GAAG,IAAI,IAAI,QAAQ,QAAQ,CAAC,EAAE,OAC9B;GACE,WAAW,KAAKF,WAAW,KAAI,OAAM;IACnC,IAAI,EAAE,IAAI;IACV,WAAW,EAAE;IACb,SAAS,EAAE;GACb,EAAE;GACF,SAAS,MAAM,KAAK,KAAKD,WAAU,SAAQ;IACzC,IAAI,IAAI;IACR,GAAG,IAAI,SAAS;GAClB,EAAE;EACJ,CACF;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forwarder.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/forwarder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"forwarder.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/forwarder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAQjD,OAAO,KAAK,EAAY,iBAAiB,EAAC,MAAM,8BAA8B,CAAC;AAC/E,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAEhD,MAAM,MAAM,sBAAsB,GAAG;IACnC,kCAAkC,EAAE,MAAM,CAAC;CAC5C,CAAC;AAEF,qBAAa,SAAS;;gBAqBlB,EAAE,EAAE,UAAU,EACd,IAAI,GAAE,sBAAgE;IAkDxE,oBAAoB;IAsBpB,mBAAmB;IAInB;;;;OAIG;IACH,GAAG,CAAC,GAAG,EAAE,UAAU;IAQnB,MAAM,CAAC,GAAG,EAAE,UAAU;IAMtB;;;;;;;;OAQG;IACH,OAAO,CAAC,KAAK,EAAE,iBAAiB;IAKhC,UAAU,CAAC,MAAM,EAAE,MAAM;IAMzB;;;OAGG;IACG,sBAAsB,CAAC,KAAK,EAAE,iBAAiB;IAiErD,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC;CAQvB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { joinIterables, wrapIterable } from "../../../../shared/src/iterables.js";
|
|
2
|
+
import { getOrCreateCounter, getOrCreateGauge, getOrCreateLatencyHistogram } from "../../observability/metrics.js";
|
|
2
3
|
import { Broadcast } from "./broadcast.js";
|
|
3
4
|
//#region ../zero-cache/src/services/change-streamer/forwarder.ts
|
|
4
5
|
var Forwarder = class {
|
|
@@ -6,12 +7,26 @@ var Forwarder = class {
|
|
|
6
7
|
#progressMonitorOptions;
|
|
7
8
|
#active = /* @__PURE__ */ new Set();
|
|
8
9
|
#queued = /* @__PURE__ */ new Set();
|
|
10
|
+
#flowControlWaits = getOrCreateCounter("replication", "flow_control.waits", "Flow-control checkpoints completed, labeled by release mode.");
|
|
11
|
+
#flowControlWaitTime = getOrCreateLatencyHistogram("replication", "flow_control.wait_duration", "Time replication waits at flow-control checkpoints.");
|
|
9
12
|
#inTransaction = false;
|
|
10
13
|
#currentBroadcast;
|
|
11
14
|
#progressMonitor;
|
|
12
15
|
constructor(lc, opts = { flowControlConsensusPaddingSeconds: 1 }) {
|
|
13
16
|
this.#lc = lc.withContext("component", "progress-monitor");
|
|
14
17
|
this.#progressMonitorOptions = opts;
|
|
18
|
+
getOrCreateGauge("replication", "flow_control.active_subscribers", "Active change-stream subscribers receiving live changes.").addCallback((result) => result.observe(this.#active.size));
|
|
19
|
+
getOrCreateGauge("replication", "flow_control.queued_subscribers", "Change-stream subscribers waiting for the current transaction to finish before activation.").addCallback((result) => result.observe(this.#queued.size));
|
|
20
|
+
getOrCreateGauge("replication", "flow_control.pending_messages", "Downstream change-stream messages not yet acked by subscribers.").addCallback((result) => result.observe(this.#subscriberStats().pendingMessages));
|
|
21
|
+
getOrCreateGauge("replication", "flow_control.backlog_messages", "Live change-stream messages buffered while subscribers catch up.").addCallback((result) => result.observe(this.#subscriberStats().backlogMessages));
|
|
22
|
+
getOrCreateGauge("replication", "flow_control.backlog_bytes", {
|
|
23
|
+
description: "Live change-stream bytes buffered while subscribers catch up.",
|
|
24
|
+
unit: "By"
|
|
25
|
+
}).addCallback((result) => result.observe(this.#subscriberStats().backlogBytes));
|
|
26
|
+
getOrCreateGauge("replication", "flow_control.max_backlog_bytes", {
|
|
27
|
+
description: "Maximum live change-stream bytes buffered by a single subscriber.",
|
|
28
|
+
unit: "By"
|
|
29
|
+
}).addCallback((result) => result.observe(this.#subscriberStats().maxBacklogBytes));
|
|
15
30
|
}
|
|
16
31
|
startProgressMonitor() {
|
|
17
32
|
clearInterval(this.#progressMonitor);
|
|
@@ -61,11 +76,37 @@ var Forwarder = class {
|
|
|
61
76
|
* Promise that resolves when replication should continue.
|
|
62
77
|
*/
|
|
63
78
|
async forwardWithFlowControl(entry) {
|
|
79
|
+
const start = performance.now();
|
|
64
80
|
const broadcast = new Broadcast(this.#active.values(), entry);
|
|
65
81
|
this.#updateActiveSubscribers(entry[1]);
|
|
66
82
|
this.#currentBroadcast = broadcast;
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
try {
|
|
84
|
+
await broadcast.done;
|
|
85
|
+
const attributes = { "release.mode": broadcast.releaseMode };
|
|
86
|
+
this.#flowControlWaits.add(1, attributes);
|
|
87
|
+
this.#flowControlWaitTime.recordMs(performance.now() - start, attributes);
|
|
88
|
+
} finally {
|
|
89
|
+
if (this.#currentBroadcast === broadcast) this.#currentBroadcast = void 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
#subscriberStats() {
|
|
93
|
+
let pendingMessages = 0;
|
|
94
|
+
let backlogMessages = 0;
|
|
95
|
+
let backlogBytes = 0;
|
|
96
|
+
let maxBacklogBytes = 0;
|
|
97
|
+
for (const sub of joinIterables(this.#active, this.#queued)) {
|
|
98
|
+
const stats = sub.getStats();
|
|
99
|
+
pendingMessages += stats.pending;
|
|
100
|
+
backlogMessages += stats.backlog;
|
|
101
|
+
backlogBytes += stats.backlogBytes;
|
|
102
|
+
maxBacklogBytes = Math.max(maxBacklogBytes, stats.backlogBytes);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
pendingMessages,
|
|
106
|
+
backlogMessages,
|
|
107
|
+
backlogBytes,
|
|
108
|
+
maxBacklogBytes
|
|
109
|
+
};
|
|
69
110
|
}
|
|
70
111
|
#updateActiveSubscribers(tag) {
|
|
71
112
|
switch (tag) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forwarder.js","names":["#lc","#progressMonitorOptions","#active","#queued","#progressMonitor","#trackProgress","#currentBroadcast","#inTransaction","#updateActiveSubscribers"],"sources":["../../../../../../zero-cache/src/services/change-streamer/forwarder.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {joinIterables, wrapIterable} from '../../../../shared/src/iterables.ts';\nimport {Broadcast} from './broadcast.ts';\nimport type {ChangeTag, WatermarkedChange} from './change-streamer-service.ts';\nimport type {Status} from './change-streamer.ts';\nimport type {Subscriber} from './subscriber.ts';\n\nexport type ProgressMonitorOptions = {\n flowControlConsensusPaddingSeconds: number;\n};\n\nexport class Forwarder {\n readonly #lc: LogContext;\n readonly #progressMonitorOptions: ProgressMonitorOptions;\n readonly #active = new Set<Subscriber>();\n readonly #queued = new Set<Subscriber>();\n #inTransaction = false;\n\n #currentBroadcast: Broadcast | undefined;\n #progressMonitor: NodeJS.Timeout | undefined;\n\n constructor(\n lc: LogContext,\n opts: ProgressMonitorOptions = {flowControlConsensusPaddingSeconds: 1},\n ) {\n this.#lc = lc.withContext('component', 'progress-monitor');\n this.#progressMonitorOptions = opts;\n }\n\n startProgressMonitor() {\n clearInterval(this.#progressMonitor);\n this.#progressMonitor = setInterval(this.#trackProgress, 1000);\n }\n\n readonly #trackProgress = () => {\n const now = performance.now();\n for (const sub of this.#active) {\n sub.sampleProcessRate(now);\n }\n\n const {flowControlConsensusPaddingSeconds} = this.#progressMonitorOptions;\n // A negative number disables early flow control release.\n if (flowControlConsensusPaddingSeconds >= 0) {\n this.#currentBroadcast?.checkProgress(\n this.#lc,\n flowControlConsensusPaddingSeconds * 1000,\n now,\n );\n }\n };\n\n stopProgressMonitor() {\n clearInterval(this.#progressMonitor);\n }\n\n /**\n * `add()` is called in lock step with `Storer.catchup()` so that the\n * two components have an equivalent interpretation of whether a Transaction is\n * currently being streamed.\n */\n add(sub: Subscriber) {\n if (this.#inTransaction) {\n this.#queued.add(sub);\n } else {\n this.#active.add(sub);\n }\n }\n\n remove(sub: Subscriber) {\n this.#active.delete(sub);\n this.#queued.delete(sub);\n sub.close();\n }\n\n /**\n * `forward()` is called in lockstep with `Storer.store()` so that the\n * two components have an equivalent interpretation of whether a Transaction is\n * currently being streamed.\n *\n * This version of forward is fire-and-forget, with no flow control. The\n * change-streamer should call and await {@link forwardWithFlowControl()}\n * occasionally to avoid memory blowup.\n */\n forward(entry: WatermarkedChange) {\n Broadcast.withoutTracking(this.#active.values(), entry);\n this.#updateActiveSubscribers(entry[1]);\n }\n\n sendStatus(status: Status) {\n for (const sub of this.#active.values()) {\n sub.sendStatus(status);\n }\n }\n\n /**\n * The flow-control-aware equivalent of {@link forward()}, returning a\n * Promise that resolves when replication should continue.\n */\n async forwardWithFlowControl(entry: WatermarkedChange) {\n const broadcast = new Broadcast(this.#active.values(), entry);\n this.#updateActiveSubscribers(entry[1]);\n\n // set for progress tracking\n this.#currentBroadcast = broadcast;\n\n await broadcast.done;\n\n // Technically #currentBroadcast may have changed, so only\n // unset if it if is still the same.\n if (this.#currentBroadcast === broadcast) {\n this.#currentBroadcast = undefined;\n }\n }\n\n #updateActiveSubscribers(tag: ChangeTag) {\n switch (tag) {\n case 'begin':\n // While in a Transaction, all added subscribers are \"queued\" so that no\n // messages are forwarded to them. This state corresponds to being queued\n // for catchup in the Storer, which will retrieve historic changes\n // and call catchup() once the current transaction is committed.\n this.#inTransaction = true;\n break;\n case 'commit':\n case 'rollback':\n // Upon commit or rollback, all queued subscribers are transferred to\n // the active set. This means that they can receive messages starting\n // from the next transaction.\n //\n // Note that if catchup is still in progress (in the Storer), these messages\n // will be buffered in the backlog until catchup completes.\n this.#inTransaction = false;\n for (const sub of this.#queued.values()) {\n this.#active.add(sub);\n }\n this.#queued.clear();\n break;\n }\n }\n\n getAcks(): Set<string> {\n return new Set(\n joinIterables(\n wrapIterable(this.#active).map(s => s.acked),\n wrapIterable(this.#queued).map(s => s.acked),\n ),\n );\n }\n}\n"],"mappings":";;;AAWA,IAAa,YAAb,MAAuB;CACrB;CACA;CACA,0BAAmB,IAAI,IAAgB;CACvC,0BAAmB,IAAI,IAAgB;CACvC,iBAAiB;CAEjB;CACA;CAEA,YACE,IACA,OAA+B,EAAC,oCAAoC,EAAC,GACrE;EACA,KAAKA,MAAM,GAAG,YAAY,aAAa,kBAAkB;EACzD,KAAKC,0BAA0B;CACjC;CAEA,uBAAuB;EACrB,cAAc,KAAKG,gBAAgB;EACnC,KAAKA,mBAAmB,YAAY,KAAKC,gBAAgB,GAAI;CAC/D;CAEA,uBAAgC;EAC9B,MAAM,MAAM,YAAY,IAAI;EAC5B,KAAK,MAAM,OAAO,KAAKH,SACrB,IAAI,kBAAkB,GAAG;EAG3B,MAAM,EAAC,uCAAsC,KAAKD;EAElD,IAAI,sCAAsC,GACxC,KAAKK,mBAAmB,cACtB,KAAKN,KACL,qCAAqC,KACrC,GACF;CAEJ;CAEA,sBAAsB;EACpB,cAAc,KAAKI,gBAAgB;CACrC;;;;;;CAOA,IAAI,KAAiB;EACnB,IAAI,KAAKG,gBACP,KAAKJ,QAAQ,IAAI,GAAG;OAEpB,KAAKD,QAAQ,IAAI,GAAG;CAExB;CAEA,OAAO,KAAiB;EACtB,KAAKA,QAAQ,OAAO,GAAG;EACvB,KAAKC,QAAQ,OAAO,GAAG;EACvB,IAAI,MAAM;CACZ;;;;;;;;;;CAWA,QAAQ,OAA0B;EAChC,UAAU,gBAAgB,KAAKD,QAAQ,OAAO,GAAG,KAAK;EACtD,KAAKM,yBAAyB,MAAM,EAAE;CACxC;CAEA,WAAW,QAAgB;EACzB,KAAK,MAAM,OAAO,KAAKN,QAAQ,OAAO,GACpC,IAAI,WAAW,MAAM;CAEzB;;;;;CAMA,MAAM,uBAAuB,OAA0B;EACrD,MAAM,YAAY,IAAI,UAAU,KAAKA,QAAQ,OAAO,GAAG,KAAK;EAC5D,KAAKM,yBAAyB,MAAM,EAAE;EAGtC,KAAKF,oBAAoB;EAEzB,MAAM,UAAU;EAIhB,IAAI,KAAKA,sBAAsB,WAC7B,KAAKA,oBAAoB,KAAA;CAE7B;CAEA,yBAAyB,KAAgB;EACvC,QAAQ,KAAR;GACE,KAAK;IAKH,KAAKC,iBAAiB;IACtB;GACF,KAAK;GACL,KAAK;IAOH,KAAKA,iBAAiB;IACtB,KAAK,MAAM,OAAO,KAAKJ,QAAQ,OAAO,GACpC,KAAKD,QAAQ,IAAI,GAAG;IAEtB,KAAKC,QAAQ,MAAM;IACnB;EACJ;CACF;CAEA,UAAuB;EACrB,OAAO,IAAI,IACT,cACE,aAAa,KAAKD,OAAO,EAAE,KAAI,MAAK,EAAE,KAAK,GAC3C,aAAa,KAAKC,OAAO,EAAE,KAAI,MAAK,EAAE,KAAK,CAC7C,CACF;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"forwarder.js","names":["#lc","#progressMonitorOptions","#active","#queued","#flowControlWaits","#flowControlWaitTime","#subscriberStats","#progressMonitor","#trackProgress","#currentBroadcast","#inTransaction","#updateActiveSubscribers"],"sources":["../../../../../../zero-cache/src/services/change-streamer/forwarder.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {joinIterables, wrapIterable} from '../../../../shared/src/iterables.ts';\nimport {\n getOrCreateCounter,\n getOrCreateGauge,\n getOrCreateLatencyHistogram,\n} from '../../observability/metrics.ts';\nimport {Broadcast} from './broadcast.ts';\nimport type {ChangeTag, WatermarkedChange} from './change-streamer-service.ts';\nimport type {Status} from './change-streamer.ts';\nimport type {Subscriber} from './subscriber.ts';\n\nexport type ProgressMonitorOptions = {\n flowControlConsensusPaddingSeconds: number;\n};\n\nexport class Forwarder {\n readonly #lc: LogContext;\n readonly #progressMonitorOptions: ProgressMonitorOptions;\n readonly #active = new Set<Subscriber>();\n readonly #queued = new Set<Subscriber>();\n readonly #flowControlWaits = getOrCreateCounter(\n 'replication',\n 'flow_control.waits',\n 'Flow-control checkpoints completed, labeled by release mode.',\n );\n readonly #flowControlWaitTime = getOrCreateLatencyHistogram(\n 'replication',\n 'flow_control.wait_duration',\n 'Time replication waits at flow-control checkpoints.',\n );\n #inTransaction = false;\n\n #currentBroadcast: Broadcast | undefined;\n #progressMonitor: NodeJS.Timeout | undefined;\n\n constructor(\n lc: LogContext,\n opts: ProgressMonitorOptions = {flowControlConsensusPaddingSeconds: 1},\n ) {\n this.#lc = lc.withContext('component', 'progress-monitor');\n this.#progressMonitorOptions = opts;\n\n getOrCreateGauge(\n 'replication',\n 'flow_control.active_subscribers',\n 'Active change-stream subscribers receiving live changes.',\n ).addCallback(result => result.observe(this.#active.size));\n\n getOrCreateGauge(\n 'replication',\n 'flow_control.queued_subscribers',\n 'Change-stream subscribers waiting for the current transaction to finish before activation.',\n ).addCallback(result => result.observe(this.#queued.size));\n\n getOrCreateGauge(\n 'replication',\n 'flow_control.pending_messages',\n 'Downstream change-stream messages not yet acked by subscribers.',\n ).addCallback(result =>\n result.observe(this.#subscriberStats().pendingMessages),\n );\n\n getOrCreateGauge(\n 'replication',\n 'flow_control.backlog_messages',\n 'Live change-stream messages buffered while subscribers catch up.',\n ).addCallback(result =>\n result.observe(this.#subscriberStats().backlogMessages),\n );\n\n getOrCreateGauge('replication', 'flow_control.backlog_bytes', {\n description:\n 'Live change-stream bytes buffered while subscribers catch up.',\n unit: 'By',\n }).addCallback(result =>\n result.observe(this.#subscriberStats().backlogBytes),\n );\n\n getOrCreateGauge('replication', 'flow_control.max_backlog_bytes', {\n description:\n 'Maximum live change-stream bytes buffered by a single subscriber.',\n unit: 'By',\n }).addCallback(result =>\n result.observe(this.#subscriberStats().maxBacklogBytes),\n );\n }\n\n startProgressMonitor() {\n clearInterval(this.#progressMonitor);\n this.#progressMonitor = setInterval(this.#trackProgress, 1000);\n }\n\n readonly #trackProgress = () => {\n const now = performance.now();\n for (const sub of this.#active) {\n sub.sampleProcessRate(now);\n }\n\n const {flowControlConsensusPaddingSeconds} = this.#progressMonitorOptions;\n // A negative number disables early flow control release.\n if (flowControlConsensusPaddingSeconds >= 0) {\n this.#currentBroadcast?.checkProgress(\n this.#lc,\n flowControlConsensusPaddingSeconds * 1000,\n now,\n );\n }\n };\n\n stopProgressMonitor() {\n clearInterval(this.#progressMonitor);\n }\n\n /**\n * `add()` is called in lock step with `Storer.catchup()` so that the\n * two components have an equivalent interpretation of whether a Transaction is\n * currently being streamed.\n */\n add(sub: Subscriber) {\n if (this.#inTransaction) {\n this.#queued.add(sub);\n } else {\n this.#active.add(sub);\n }\n }\n\n remove(sub: Subscriber) {\n this.#active.delete(sub);\n this.#queued.delete(sub);\n sub.close();\n }\n\n /**\n * `forward()` is called in lockstep with `Storer.store()` so that the\n * two components have an equivalent interpretation of whether a Transaction is\n * currently being streamed.\n *\n * This version of forward is fire-and-forget, with no flow control. The\n * change-streamer should call and await {@link forwardWithFlowControl()}\n * occasionally to avoid memory blowup.\n */\n forward(entry: WatermarkedChange) {\n Broadcast.withoutTracking(this.#active.values(), entry);\n this.#updateActiveSubscribers(entry[1]);\n }\n\n sendStatus(status: Status) {\n for (const sub of this.#active.values()) {\n sub.sendStatus(status);\n }\n }\n\n /**\n * The flow-control-aware equivalent of {@link forward()}, returning a\n * Promise that resolves when replication should continue.\n */\n async forwardWithFlowControl(entry: WatermarkedChange) {\n const start = performance.now();\n const broadcast = new Broadcast(this.#active.values(), entry);\n this.#updateActiveSubscribers(entry[1]);\n\n // set for progress tracking\n this.#currentBroadcast = broadcast;\n\n try {\n await broadcast.done;\n const attributes = {'release.mode': broadcast.releaseMode};\n this.#flowControlWaits.add(1, attributes);\n this.#flowControlWaitTime.recordMs(performance.now() - start, attributes);\n } finally {\n // Technically #currentBroadcast may have changed, so only\n // unset if it if is still the same.\n if (this.#currentBroadcast === broadcast) {\n this.#currentBroadcast = undefined;\n }\n }\n }\n\n #subscriberStats() {\n let pendingMessages = 0;\n let backlogMessages = 0;\n let backlogBytes = 0;\n let maxBacklogBytes = 0;\n\n for (const sub of joinIterables(this.#active, this.#queued)) {\n const stats = sub.getStats();\n pendingMessages += stats.pending;\n backlogMessages += stats.backlog;\n backlogBytes += stats.backlogBytes;\n maxBacklogBytes = Math.max(maxBacklogBytes, stats.backlogBytes);\n }\n\n return {pendingMessages, backlogMessages, backlogBytes, maxBacklogBytes};\n }\n\n #updateActiveSubscribers(tag: ChangeTag) {\n switch (tag) {\n case 'begin':\n // While in a Transaction, all added subscribers are \"queued\" so that no\n // messages are forwarded to them. This state corresponds to being queued\n // for catchup in the Storer, which will retrieve historic changes\n // and call catchup() once the current transaction is committed.\n this.#inTransaction = true;\n break;\n case 'commit':\n case 'rollback':\n // Upon commit or rollback, all queued subscribers are transferred to\n // the active set. This means that they can receive messages starting\n // from the next transaction.\n //\n // Note that if catchup is still in progress (in the Storer), these messages\n // will be buffered in the backlog until catchup completes.\n this.#inTransaction = false;\n for (const sub of this.#queued.values()) {\n this.#active.add(sub);\n }\n this.#queued.clear();\n break;\n }\n }\n\n getAcks(): Set<string> {\n return new Set(\n joinIterables(\n wrapIterable(this.#active).map(s => s.acked),\n wrapIterable(this.#queued).map(s => s.acked),\n ),\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/storer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAWjD,OAAO,EAAC,eAAe,EAAC,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAC,KAAK,UAAU,EAA2B,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAY,KAAK,OAAO,EAAC,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,MAAM,EAKZ,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,MAAM,EACZ,MAAM,iDAAiD,CAAC;AACzD,OAAO,KAAK,EACV,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,eAAe,CAAC;AAS3C,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAuChD,MAAM,MAAM,aAAa,GAAG;IAC1B,+BAA+B,EAAE,MAAM,CAAC;IACxC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAaF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,MAAO,YAAW,OAAO;;IACpC,QAAQ,CAAC,EAAE,YAAY;gBAmBrB,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,iBAAiB,EAAE,MAAM,EACzB,EAAE,EAAE,UAAU,EACd,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,qBAAqB,KAAK,IAAI,EACvD,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,EAC7B,EACE,+BAA+B,EAC/B,kBAAkB,EAClB,kBAAkB,GACnB,EAAE,aAAa;IAgCZ,eAAe,CAAC,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI;IA2B5C,sCAAsC,IAAI,OAAO,CAAC;QACtD,aAAa,EAAE,MAAM,CAAC;QACtB,gBAAgB,EAAE,eAAe,EAAE,CAAC;KACrC,CAAC;IAkCI,yBAAyB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAOzD,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAyCtD;;OAEG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB;IAkB/C,KAAK;IAIL,MAAM,CAAC,CAAC,EAAE,uBAAuB;IAIjC,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc;IAMpD,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS;IAyEzC;;;OAGG;IACG,GAAG;
|
|
1
|
+
{"version":3,"file":"storer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/storer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAWjD,OAAO,EAAC,eAAe,EAAC,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAC,KAAK,UAAU,EAA2B,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAY,KAAK,OAAO,EAAC,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,MAAM,EAKZ,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,MAAM,EACZ,MAAM,iDAAiD,CAAC;AACzD,OAAO,KAAK,EACV,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,eAAe,CAAC;AAS3C,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAuChD,MAAM,MAAM,aAAa,GAAG;IAC1B,+BAA+B,EAAE,MAAM,CAAC;IACxC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAaF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,MAAO,YAAW,OAAO;;IACpC,QAAQ,CAAC,EAAE,YAAY;gBAmBrB,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,iBAAiB,EAAE,MAAM,EACzB,EAAE,EAAE,UAAU,EACd,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,qBAAqB,KAAK,IAAI,EACvD,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,EAC7B,EACE,+BAA+B,EAC/B,kBAAkB,EAClB,kBAAkB,GACnB,EAAE,aAAa;IAgCZ,eAAe,CAAC,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI;IA2B5C,sCAAsC,IAAI,OAAO,CAAC;QACtD,aAAa,EAAE,MAAM,CAAC;QACtB,gBAAgB,EAAE,eAAe,EAAE,CAAC;KACrC,CAAC;IAkCI,yBAAyB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAOzD,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAyCtD;;OAEG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB;IAkB/C,KAAK;IAIL,MAAM,CAAC,CAAC,EAAE,uBAAuB;IAIjC,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc;IAMpD,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS;IAyEzC;;;OAGG;IACG,GAAG;IA0eT;;;OAGG;IACG,YAAY;IAQlB,IAAI;CAOL;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,iBAAiB,EAAE,MAAM,EACzB,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,SAAS,UAmB/B;AA+BD,qBAAa,SAAS;;IAGpB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAG5B,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,eAAe,EACnB,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM;IAUb,OAAO;CAWd;AAED,qBAAa,WAAW;;gBAKV,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU;IAWpD,OAAO;CA4Bd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storer.js","names":["#lc","#shard","#taskID","#discoveryAddress","#discoveryProtocol","#db","#replicaVersion","#onConsumed","#onFatal","#queue","#backPressureThresholdBytes","#statementTimeoutMs","#changeLogBatchSize","#cdc","#approximateQueuedBytes","#running","#readyForMore","#stopped","#processQueue","#cancelQueueEntries","#startCatchup","#flushChangeLog","#trackBackfillMetadata","#maybeReleaseBackPressure","#catchup","#upsertTableMetadataStmt","#upsertColumnBackfillStmt","#tx","#released"],"sources":["../../../../../../zero-cache/src/services/change-streamer/storer.ts"],"sourcesContent":["import {getHeapStatistics} from 'node:v8';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {type PendingQuery, type Row} from 'postgres';\nimport {AbortError} from '../../../../shared/src/abort-error.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {TransactionPool} from '../../db/transaction-pool.ts';\nimport {type PostgresDB, type PostgresTransaction} from '../../types/pg.ts';\nimport {cdcSchema, type ShardID} from '../../types/shards.ts';\nimport {\n backfillRequestSchema,\n isDataChange,\n isSchemaChange,\n type BackfillID,\n type BackfillRequest,\n type Change,\n type DataChange,\n type Identifier,\n type SchemaChange,\n type TableMetadata,\n} from '../change-source/protocol/current.ts';\nimport {\n type ChangeStreamData,\n type Commit,\n} from '../change-source/protocol/current/downstream.ts';\nimport type {\n DownstreamStatusMessage,\n UpstreamStatusMessage,\n} from '../change-source/protocol/current/status.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport type {Service} from '../service.ts';\nimport type {ChangeTag, WatermarkedChange} from './change-streamer-service.ts';\nimport * as ErrorType from './error-type-enum.ts';\nimport {\n AutoResetSignal,\n markResetRequired,\n type BackfillingColumn,\n type TableMetadataRow,\n} from './schema/tables.ts';\nimport type {Subscriber} from './subscriber.ts';\n\ntype SubscriberAndMode = {\n subscriber: Subscriber;\n mode: ReplicatorMode;\n};\n\ntype QueueEntry =\n | [\n 'change',\n watermark: string,\n json: string,\n orig: Exclude<Change, DataChange> | null, // null for DataChanges\n ]\n | ['ready', callback: () => void]\n | ['subscriber', SubscriberAndMode]\n | DownstreamStatusMessage\n | ['abort']\n | 'stop';\n\ntype PendingTransaction = {\n pool: TransactionPool;\n preCommitWatermark: string;\n pos: number;\n startingReplicationState: Promise<ReplicationOwner>;\n ack: boolean;\n // changeLog rows buffered for the next multi-row INSERT flush.\n batch: ChangeLogRow[];\n // The most recently issued flush (or metadata) process, awaited to bound\n // pipeline depth and to order the commit-time replicationState update.\n lastFlush: Promise<unknown> | undefined;\n};\n\ntype ReplicationOwner = {\n owner: string | null;\n};\n\nconst backfillRequestsSchema = v.array(backfillRequestSchema);\n\nexport type TuningOptions = {\n backPressureLimitHeapProportion: number;\n statementTimeoutMs: number;\n changeLogBatchSize: number;\n};\n\n/**\n * A single `changeLog` row, accumulated in {@link PendingTransaction.batch}\n * and written via `json_to_recordset()` (see `#flushChangeLog()`).\n */\ntype ChangeLogRow = {\n watermark: string;\n precommit: string | null;\n pos: number;\n change: string;\n};\n\n/**\n * Handles the storage of changes and the catchup of subscribers\n * that are behind.\n *\n * In the context of catchup and cleanup, it is the responsibility of the\n * Storer to decide whether a client can be caught up, or whether the\n * changes needed to catch a client up have been purged.\n *\n * **Maintained invariant**: The Change DB is only empty for a\n * completely new replica (i.e. initial-sync with no changes from the\n * replication stream).\n * * In this case, all new subscribers are expected start from the\n * `replicaVersion`, which is the version at which initial sync\n * was performed, and any attempts to catchup from a different\n * point fail.\n *\n * Conversely, if non-initial changes have flowed through the system\n * (i.e. via the replication stream), the ChangeDB must *not* be empty,\n * and the earliest change in the `changeLog` represents the earliest\n * \"commit\" from (after) which a subscriber can be caught up.\n * * Any attempts to catchup from an earlier point must fail with\n * a `WatermarkTooOld` error.\n * * Failure to do so could result in streaming changes to the\n * subscriber such that there is a gap in its replication history.\n *\n * Note: Subscribers (i.e. `incremental-syncer`) consider an \"error\" signal\n * an unrecoverable error and shut down in response. This allows the\n * production system to replace it with a new task and fresh copy of the\n * replica backup.\n */\nexport class Storer implements Service {\n readonly id = 'storer';\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #taskID: string;\n readonly #discoveryAddress: string;\n readonly #discoveryProtocol: string;\n readonly #db: PostgresDB;\n readonly #replicaVersion: string;\n readonly #onConsumed: (c: Commit | UpstreamStatusMessage) => void;\n readonly #onFatal: (err: Error) => void;\n readonly #queue = new Queue<QueueEntry>();\n readonly #backPressureThresholdBytes: number;\n readonly #statementTimeoutMs: number;\n readonly #changeLogBatchSize: number;\n\n #approximateQueuedBytes = 0;\n #running = false;\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n db: PostgresDB,\n replicaVersion: string,\n onConsumed: (c: Commit | UpstreamStatusMessage) => void,\n onFatal: (err: Error) => void,\n {\n backPressureLimitHeapProportion,\n statementTimeoutMs,\n changeLogBatchSize,\n }: TuningOptions,\n ) {\n this.#lc = lc.withContext('component', 'change-log');\n this.#shard = shard;\n this.#taskID = taskID;\n this.#discoveryAddress = discoveryAddress;\n this.#discoveryProtocol = discoveryProtocol;\n this.#db = db;\n this.#replicaVersion = replicaVersion;\n this.#onConsumed = onConsumed;\n this.#onFatal = onFatal;\n this.#statementTimeoutMs = statementTimeoutMs;\n this.#changeLogBatchSize = Math.max(1, changeLogBatchSize);\n\n const heapStats = getHeapStatistics();\n this.#backPressureThresholdBytes =\n (heapStats.heap_size_limit - heapStats.used_heap_size) *\n backPressureLimitHeapProportion;\n\n this.#lc.info?.(\n `Using up to ${(this.#backPressureThresholdBytes / 1024 ** 2).toFixed(2)} MB of ` +\n `--max-old-space-size (~${(heapStats.heap_size_limit / 1024 ** 2).toFixed(2)} MB) ` +\n `to absorb upstream spikes`,\n {heapStats},\n );\n }\n\n // For readability in SQL statements.\n #cdc(table: string) {\n return this.#db(`${cdcSchema(this.#shard)}.${table}`);\n }\n\n async assumeOwnership(purgeLock?: PurgeLock | null) {\n const db = this.#db;\n const owner = this.#taskID;\n const ownerAddress = this.#discoveryAddress;\n const ownerProtocol = this.#discoveryProtocol;\n // we omit `ws://` so that old view syncer versions that are not expecting the protocol continue to not get it\n const addressWithProtocol =\n ownerProtocol === 'ws'\n ? ownerAddress\n : `${ownerProtocol}://${ownerAddress}`;\n this.#lc.info?.(`assuming ownership at ${addressWithProtocol}`);\n const start = performance.now();\n await db`UPDATE ${this.#cdc('replicationState')} SET ${db({owner, ownerAddress: addressWithProtocol})}`;\n const elapsed = (performance.now() - start).toFixed(2);\n this.#lc.info?.(\n `assumed ownership at ${addressWithProtocol} (${elapsed} ms)`,\n );\n\n if (purgeLock) {\n // Once ownership has been assumed, any initial purge-lock preventing the\n // purging of change-log records can be released, as a change-streamer\n // that was attempting to purge records will correspondingly abort on the\n // ownership check.\n void purgeLock.release();\n }\n }\n\n async getStartStreamInitializationParameters(): Promise<{\n lastWatermark: string;\n backfillRequests: BackfillRequest[];\n }> {\n const [[{lastWatermark}], result] = await runTx(\n this.#db,\n sql => [\n sql<{lastWatermark: string}[]>`\n SELECT \"lastWatermark\" FROM ${this.#cdc('replicationState')}`,\n\n // Formats a BackfillRequest using json_object_agg() to construct the\n // `columns` object. It is LEFT JOIN'ed with the `tableMetadata` table\n // to make it optional and possibly `null`.\n sql`\n SELECT \n json_build_object(\n 'schema', b.\"schema\",\n 'name', b.\"table\",\n 'metadata', t.\"metadata\"\n ) as \"table\",\n json_object_agg(b.\"column\", b.\"backfill\") \n as \"columns\"\n FROM ${this.#cdc('backfilling')} as b\n LEFT JOIN ${this.#cdc('tableMetadata')} as t\n ON (b.\"schema\" = t.\"schema\" AND b.\"table\" = t.\"table\")\n GROUP BY b.\"schema\", b.\"table\", t.\"metadata\"\n `,\n ],\n {mode: Mode.READONLY},\n );\n\n return {\n lastWatermark,\n backfillRequests: v.parse(result, backfillRequestsSchema),\n };\n }\n\n async getMinWatermarkForCatchup(): Promise<string | null> {\n const [{minWatermark}] = await this.#db<{minWatermark: string | null}[]>\n /*sql*/ `\n SELECT min(watermark) as \"minWatermark\" FROM ${this.#cdc('changeLog')}`;\n return minWatermark;\n }\n\n purgeRecordsBefore(watermark: string): Promise<number> {\n return runTx(this.#db, async sql => {\n // This NOWAIT pre-check is an optimization to abort the transaction\n // (and release associated resources) early.\n await sql<{watermark: string}[]>`\n SELECT watermark FROM ${this.#cdc('changeLog')}\n ORDER BY watermark, pos LIMIT 1\n FOR UPDATE NOWAIT\n `;\n // If the row is purge-locked by an incoming replication-manager, it\n // will assume ownership of the change-log before releasing the lock.\n // This DELETE blocks until the lock is released, allowing the change\n // in ownership to be reliably detected (and the transaction aborted)\n // in the subsequent check.\n const [{deleted}] = await sql<{deleted: bigint}[]>`\n -- The backup watermark can be ahead of the durable changeLog if the\n -- storer is behind but the backup replica has consumed forwarded\n -- changes. Preserve the latest durable changeLog transaction as the\n -- catchup boundary instead of assuming the backup watermark exists.\n -- The storer inserts each changeLog transaction atomically, so any\n -- durable row for a watermark implies the full transaction is durable.\n WITH keep AS (\n SELECT max(watermark) AS watermark\n FROM ${this.#cdc('changeLog')}\n ), purged AS (\n DELETE FROM ${this.#cdc('changeLog')} WHERE watermark < ${watermark} \n AND watermark < (SELECT watermark FROM keep)\n RETURNING watermark, pos\n ) SELECT COUNT(*) as deleted FROM purged;`;\n\n const [{owner}] = await sql<ReplicationOwner[]>`\n SELECT \"owner\" FROM ${this.#cdc('replicationState')} FOR SHARE`;\n if (owner !== this.#taskID) {\n throw new AbortError(\n `aborting changeLog purge to ${watermark} because ownership has been taken by ${owner}`,\n );\n }\n return Number(deleted);\n });\n }\n\n /**\n * @returns The JSON stringified stream message to be sent downstream.\n */\n store(watermark: string, data: ChangeStreamData) {\n // Eagerly stringify the JSON payload to:\n // - avoid redundant stringification when fanning out to subscribers\n // - efficiently estimate the amount of memory the payload consumes\n const json = BigIntJSON.stringify(data);\n this.#approximateQueuedBytes += json.length;\n\n const change = data[1];\n this.#queue.enqueue([\n 'change',\n watermark,\n json,\n isDataChange(change) ? null : change, // drop DataChanges to save memory\n ]);\n\n return json;\n }\n\n abort() {\n this.#queue.enqueue(['abort']);\n }\n\n status(s: DownstreamStatusMessage) {\n this.#queue.enqueue(s);\n }\n\n catchup(subscriber: Subscriber, mode: ReplicatorMode) {\n this.#queue.enqueue(['subscriber', {subscriber, mode}]);\n }\n\n #readyForMore: Resolver<void> | null = null;\n\n readyForMore(): Promise<void> | undefined {\n if (!this.#running) {\n return undefined;\n }\n if (\n this.#readyForMore === null &&\n this.#approximateQueuedBytes > this.#backPressureThresholdBytes\n ) {\n this.#lc.warn?.(\n `applying back pressure with ${this.#queue.size()} queued changes (~${(this.#approximateQueuedBytes / 1024 ** 2).toFixed(2)} MB)\\n` +\n `\\n` +\n `To inspect changeLog backlog in your change DB:\\n` +\n ` SELECT\\n` +\n ` (change->'relation'->>'schema') || '.' || (change->'relation'->>'name') AS table_name,\\n` +\n ` change->>'tag' AS operation,\\n` +\n ` COUNT(*) AS count\\n` +\n ` FROM \"<app_id>/cdc\".\"changeLog\"\\n` +\n ` GROUP BY 1, 2\\n` +\n ` ORDER BY 3 DESC\\n` +\n ` LIMIT 20;`,\n );\n this.#readyForMore = resolver();\n }\n return this.#readyForMore?.promise;\n }\n\n #maybeReleaseBackPressure() {\n if (\n this.#readyForMore !== null &&\n // Wait for at least 20% of the threshold to free up.\n this.#approximateQueuedBytes < this.#backPressureThresholdBytes * 0.8\n ) {\n this.#lc.info?.(\n `releasing back pressure with ${this.#queue.size()} queued changes (~${(this.#approximateQueuedBytes / 1024 ** 2).toFixed(2)} MB)`,\n );\n this.#readyForMore.resolve();\n this.#readyForMore = null;\n }\n }\n\n /**\n * Flushes any buffered {@link PendingTransaction.batch} rows to the changeLog.\n *\n * Uses `json_to_recordset()` so the batch is a single JSON parameter: the\n * statement text stays constant regardless of batch size, avoiding the\n * unbounded prepared-statement variants (and Postgres memory growth) that a\n * multi-row INSERT would produce. See rocicorp/mono#3511.\n *\n * Returns (and updates) {@link PendingTransaction.lastFlush}; a no-op when the\n * batch is empty.\n */\n #flushChangeLog(tx: PendingTransaction): Promise<unknown> | undefined {\n const {batch} = tx;\n if (batch.length === 0) {\n return tx.lastFlush;\n }\n tx.batch = [];\n tx.lastFlush = tx.pool.process(sql => [\n sql`\n INSERT INTO ${this.#cdc('changeLog')} (\"watermark\", \"pos\", \"change\", \"precommit\")\n SELECT \"watermark\", \"pos\", \"change\"::json, \"precommit\"\n FROM json_to_recordset(${batch}) AS x(\n \"watermark\" TEXT,\n \"pos\" INT8,\n \"change\" TEXT,\n \"precommit\" TEXT\n )`,\n ]);\n return tx.lastFlush;\n }\n\n #stopped = promiseVoid;\n\n /**\n * Runs the storer loop until {@link stop()} is called, or an error is thrown.\n * Once {@link run()} completes, it can be called again.\n */\n async run() {\n assert(!this.#running, `storer is already running`);\n\n const {promise: stopped, resolve: signalStopped} = resolver();\n this.#running = true;\n this.#stopped = stopped;\n\n this.#lc.info?.('starting storer');\n let err: unknown;\n try {\n await this.#processQueue();\n } catch (e) {\n err = e; // used in finally\n throw e;\n } finally {\n // Release any pending backpressure so the upstream can proceed\n if (this.#readyForMore !== null) {\n this.#readyForMore.resolve();\n this.#readyForMore = null;\n }\n this.#cancelQueueEntries(\n this.#queue.drain().filter(entry => entry !== undefined),\n err,\n );\n this.#running = false;\n signalStopped();\n this.#lc.info?.('storer stopped');\n }\n }\n\n #cancelQueueEntries(queue: QueueEntry[], e: unknown) {\n if (queue.length === 0) {\n return;\n }\n this.#lc.info?.(\n `canceling ${queue.length} entries from the changeLog queue`,\n );\n const err = e instanceof Error ? e : new AbortError('server shutting down');\n for (const entry of queue) {\n if (entry === 'stop') {\n continue;\n }\n const type = entry[0];\n switch (type) {\n case 'subscriber': {\n // Disconnect subscribers waiting to be caught up so that they can\n // reconnect and try again.\n const {subscriber} = entry[1];\n this.#lc.info?.(`disconnecting ${subscriber.id}`);\n subscriber.fail(err);\n break;\n }\n }\n }\n }\n\n async #processQueue() {\n let tx: PendingTransaction | null = null;\n let msg: QueueEntry | false;\n\n const catchupQueue: SubscriberAndMode[] = [];\n try {\n while ((msg = await this.#queue.dequeue()) !== 'stop') {\n const [msgType] = msg;\n switch (msgType) {\n case 'ready': {\n const signalReady = msg[1];\n signalReady();\n continue;\n }\n case 'subscriber': {\n const subscriber = msg[1];\n if (tx) {\n catchupQueue.push(subscriber); // Wait for the current tx to complete.\n } else {\n await this.#startCatchup([subscriber]); // Catch up immediately.\n }\n continue;\n }\n case 'status':\n this.#onConsumed(msg);\n continue;\n case 'abort': {\n if (tx) {\n tx.pool.abort();\n await tx.pool.done();\n tx = null;\n }\n continue;\n }\n }\n // msgType === 'change'\n const [_, watermark, json, change] = msg;\n const tag = change?.tag;\n this.#approximateQueuedBytes -= json.length;\n\n if (tag === 'begin') {\n assert(!tx, 'received BEGIN in the middle of a transaction');\n const {promise, resolve, reject} = resolver<ReplicationOwner>();\n void promise.catch(() => {}); // handle rejections before the await\n tx = {\n pool: new TransactionPool(\n this.#lc.withContext('watermark', watermark),\n {\n mode: Mode.READ_COMMITTED,\n statementResponseTimeout: this.#statementTimeoutMs,\n },\n ),\n preCommitWatermark: watermark,\n pos: 0,\n startingReplicationState: promise,\n ack: !change.skipAck,\n batch: [],\n lastFlush: undefined,\n };\n tx.pool.run(this.#db);\n // Acquire a lock on the replicationState row to detect and/or prevent\n // a concurrent ownership change.\n void tx.pool.process(tx => {\n tx<ReplicationOwner[]> /*sql*/ `\n SELECT \"owner\" FROM ${this.#cdc('replicationState')} FOR UPDATE`.then(\n ([result]) => resolve(result),\n reject,\n );\n return [];\n });\n } else {\n assert(tx, () => `received change outside of transaction: ${json}`);\n tx.pos++;\n }\n\n const entry: ChangeLogRow = {\n watermark: tag === 'commit' ? watermark : tx.preCommitWatermark,\n precommit: tag === 'commit' ? tx.preCommitWatermark : null,\n pos: tx.pos,\n // For backwards compatibility, only the change message is stored\n // in the cdc changeLog.\n change: extractChangeSubstring(json, tag),\n };\n\n if (change !== null && isSchemaChange(change)) {\n // Schema changes carry backfill / table-metadata statements that\n // must be applied in stream order relative to the changeLog rows.\n // Flush any buffered rows first, then write this row together with\n // its metadata statements as a single unit (preserving the previous\n // per-change ordering for schema changes).\n await this.#flushChangeLog(tx);\n tx.lastFlush = tx.pool.process(sql => [\n sql`INSERT INTO ${this.#cdc('changeLog')} ${sql(entry)}`,\n ...this.#trackBackfillMetadata(sql, change),\n ]);\n } else {\n // Accumulate plain changeLog rows (begin, data changes, commit) and\n // write them as a single multi-row INSERT. Collapsing the per-change\n // single-row INSERTs into batches is the dominant cost reduction for\n // large transactions, where the previous one-statement-per-change\n // path dominated the upstream replication lag.\n tx.batch.push(entry);\n if (tx.batch.length >= this.#changeLogBatchSize) {\n // Bound pipeline depth (and thus memory) by awaiting the previous\n // flush before issuing the next. This is the batched analog of the\n // previous per-100-statement backpressure await, and likewise\n // guards against memory blowup on very large transactions.\n const prevFlush = tx.lastFlush;\n void this.#flushChangeLog(tx);\n await prevFlush;\n }\n }\n this.#maybeReleaseBackPressure();\n\n if (tag === 'commit') {\n // Flush any remaining buffered changeLog rows (including this commit\n // row) before updating the replication state, so the state update is\n // ordered after all changeLog inserts for this transaction.\n void this.#flushChangeLog(tx);\n\n const {owner} = await tx.startingReplicationState;\n if (owner !== this.#taskID) {\n // Ownership change reflected in the replicationState read in 'begin'.\n tx.pool.fail(\n new AbortError(\n `changeLog ownership has been assumed by ${owner}`,\n ),\n );\n } else {\n // Update the replication state.\n const lastWatermark = watermark;\n void tx.pool.process(tx => [\n tx`\n UPDATE ${this.#cdc('replicationState')} SET ${tx({lastWatermark})}`,\n ]);\n tx.pool.setDone();\n }\n\n await tx.pool.done();\n\n // ACK the LSN to the upstream Postgres.\n if (tx.ack) {\n this.#onConsumed(['commit', change, {watermark}]);\n }\n tx = null;\n\n // Before beginning the next transaction, open a READONLY snapshot to\n // concurrently catchup any queued subscribers.\n await this.#startCatchup(catchupQueue.splice(0));\n } else if (tag === 'rollback') {\n // Aborted transactions are not stored in the changeLog. Abort the current tx\n // and process catchup of subscribers that were waiting for it to end.\n tx.pool.abort();\n await tx.pool.done();\n tx = null;\n\n await this.#startCatchup(catchupQueue.splice(0));\n }\n }\n } catch (e) {\n catchupQueue.forEach(({subscriber}) => subscriber.fail(e));\n throw e;\n }\n }\n\n async #startCatchup(subs: SubscriberAndMode[]) {\n if (subs.length === 0) {\n return;\n }\n\n const reader = new TransactionPool(\n this.#lc.withContext('pool', 'catchup'),\n {mode: Mode.READONLY},\n );\n reader.run(this.#db);\n\n let lastWatermark: string | undefined;\n try {\n // Ensure that the transaction has started (and is thus holding a snapshot\n // of the database) before continuing on to commit more changes. This is\n // done by performing a single read on the db, which determines the\n // snapshot for the REPEATABLE_READ transaction.\n [{lastWatermark}] = await reader.processReadTask(\n sql => sql<{lastWatermark: string}[]>`\n SELECT \"lastWatermark\" FROM ${this.#cdc('replicationState')}\n `,\n );\n } catch (e) {\n subs.map(({subscriber}) => subscriber.fail(e));\n throw e;\n }\n\n // Run the actual catchup queries in the background. Errors are handled in\n // #catchup() by disconnecting the associated subscriber.\n void Promise.all(\n subs.map(sub => this.#catchup(sub, lastWatermark, reader)),\n ).finally(() => reader.setDone());\n }\n\n async #catchup(\n {subscriber: sub, mode}: SubscriberAndMode,\n lastWatermark: string,\n reader: TransactionPool,\n ) {\n try {\n await reader.processReadTask(async tx => {\n const start = Date.now();\n\n // When starting from initial-sync, there won't be a change with a watermark\n // equal to the replica version. This is the empty changeLog scenario.\n let watermarkFound = sub.watermark === this.#replicaVersion;\n let count = 0;\n let lastBatchConsumed: Promise<unknown> | undefined;\n\n for await (const entries of tx<ChangeLogEntry[]> /*sql*/ `\n SELECT watermark, change->'tag' as tag, change::text FROM ${this.#cdc('changeLog')}\n WHERE watermark >= ${sub.watermark}\n AND watermark <= ${lastWatermark}\n ORDER BY watermark, pos`.cursor(2000)) {\n // Wait for the last batch of entries to be consumed by the\n // subscriber before sending down the current batch. This pipelining\n // allows one batch of changes to be received from the change-db\n // while the previous batch of changes are sent to the subscriber,\n // resulting in flow control that caps the number of changes\n // referenced in memory to 2 * batch-size.\n const start = performance.now();\n await lastBatchConsumed;\n const elapsed = performance.now() - start;\n if (lastBatchConsumed) {\n this.#lc[elapsed > 100 ? 'info' : 'debug']?.(\n `waited ${elapsed.toFixed(3)} ms for ${sub.id} to consume last batch of catchup entries`,\n );\n }\n\n for (const entry of entries) {\n if (entry.watermark === sub.watermark) {\n // This should be the first entry.\n // Catchup starts from *after* the watermark.\n watermarkFound = true;\n } else if (watermarkFound) {\n lastBatchConsumed = sub.catchup(toDownstream(entry));\n count++;\n } else if (mode === 'backup') {\n throw new AutoResetSignal(\n `backup replica at watermark ${sub.watermark} is behind change db: ${entry.watermark})`,\n );\n } else {\n this.#lc.warn?.(\n `rejecting subscriber at watermark ${sub.watermark} (earliest watermark: ${entry.watermark})`,\n );\n sub.close(\n ErrorType.WatermarkTooOld,\n `earliest supported watermark is ${entry.watermark} (requested ${sub.watermark})`,\n );\n return;\n }\n }\n }\n if (watermarkFound) {\n await lastBatchConsumed;\n this.#lc.info?.(\n `caught up ${sub.id} with ${count} changes (${\n Date.now() - start\n } ms)`,\n );\n } else {\n // The subscriber is ahead of the latest durable changeLog entry\n // (lastWatermark). This can legitimately happen: changes are\n // forwarded to subscribers (the backup replica and view-syncers)\n // concurrently with — and can outrun — the durable store, so a\n // replica may briefly lead the change DB after the storer falls\n // behind or the change-streamer restarts. No catchup is possible or\n // needed; once the change DB catches back up, forwarding resumes and\n // the subscriber dedups any watermarks it already has. Unlike the\n // AutoResetSignal / WatermarkTooOld cases above, this is not a gap in\n // replication history, so the subscriber is simply marked caught up.\n this.#lc.warn?.(\n `subscriber ${sub.id} at watermark ${sub.watermark} is ahead of ` +\n `the latest durable watermark ${lastWatermark}; waiting for the ` +\n `change DB to catch up`,\n );\n }\n // Flushes the backlog of messages buffered during catchup and\n // allows the subscription to forward subsequent messages immediately.\n sub.setCaughtUp();\n });\n } catch (err) {\n this.#lc.error?.(`error while catching up subscriber ${sub.id}`, err);\n if (err instanceof AutoResetSignal) {\n await markResetRequired(this.#db, this.#shard);\n this.#onFatal(err);\n }\n sub.fail(err);\n }\n }\n\n /**\n * Returns the db statements necessary to track backfill and table metadata\n * presented in the `change`, if any.\n */\n #trackBackfillMetadata(sql: PostgresTransaction, change: SchemaChange) {\n const stmts: PendingQuery<Row[]>[] = [];\n\n switch (change.tag) {\n case 'update-table-metadata': {\n const {table, new: metadata} = change;\n stmts.push(this.#upsertTableMetadataStmt(sql, table, metadata));\n break;\n }\n\n case 'create-table': {\n const {spec, metadata, backfill} = change;\n if (metadata) {\n stmts.push(this.#upsertTableMetadataStmt(sql, spec, metadata));\n }\n if (backfill) {\n Object.entries(backfill).forEach(([col, backfill]) => {\n stmts.push(\n this.#upsertColumnBackfillStmt(sql, spec, col, backfill),\n );\n });\n }\n break;\n }\n\n case 'rename-table': {\n const {old} = change;\n const row = {schema: change.new.schema, table: change.new.name};\n stmts.push(\n sql`UPDATE ${this.#cdc('tableMetadata')} SET ${sql(row)}\n WHERE \"schema\" = ${old.schema} AND \"table\" = ${old.name}`,\n sql`UPDATE ${this.#cdc('backfilling')} SET ${sql(row)}\n WHERE \"schema\" = ${old.schema} AND \"table\" = ${old.name}`,\n );\n break;\n }\n\n case 'drop-table': {\n const {\n id: {schema, name},\n } = change;\n stmts.push(\n sql`DELETE FROM ${this.#cdc('tableMetadata')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${name}`,\n sql`DELETE FROM ${this.#cdc('backfilling')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${name}`,\n );\n break;\n }\n\n case 'add-column': {\n const {table, tableMetadata, column, backfill} = change;\n if (tableMetadata) {\n stmts.push(this.#upsertTableMetadataStmt(sql, table, tableMetadata));\n }\n if (backfill) {\n stmts.push(\n this.#upsertColumnBackfillStmt(sql, table, column.name, backfill),\n );\n }\n break;\n }\n\n case 'update-column': {\n const {\n table: {schema, name: table},\n old: {name: oldName},\n new: {name: newName},\n } = change;\n if (oldName !== newName) {\n stmts.push(\n sql`UPDATE ${this.#cdc('backfilling')} SET \"column\" = ${newName}\n WHERE \"schema\" = ${schema} AND \"table\" = ${table} AND \"column\" = ${oldName}`,\n );\n }\n break;\n }\n\n case 'drop-column': {\n const {\n table: {schema, name},\n column,\n } = change;\n stmts.push(\n sql`DELETE FROM ${this.#cdc('backfilling')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${name} AND \"column\" = ${column}`,\n );\n break;\n }\n\n case 'backfill-completed': {\n const {\n relation: {schema, name: table, rowKey},\n columns,\n } = change;\n const cols = [...rowKey.columns, ...columns];\n stmts.push(\n sql`DELETE FROM ${this.#cdc('backfilling')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${table} AND \"column\" IN ${sql(cols)}`,\n );\n }\n }\n return stmts;\n }\n\n #upsertTableMetadataStmt(\n sql: PostgresTransaction,\n {schema, name: table}: Identifier,\n metadata: TableMetadata,\n ) {\n const row: TableMetadataRow = {schema, table, metadata};\n return sql`\n INSERT INTO ${this.#cdc('tableMetadata')} ${sql(row)}\n ON CONFLICT (\"schema\", \"table\") \n DO UPDATE SET ${sql(row)};\n `;\n }\n\n #upsertColumnBackfillStmt(\n sql: PostgresTransaction,\n {schema, name: table}: Identifier,\n column: string,\n backfill: BackfillID,\n ) {\n const row: BackfillingColumn = {schema, table, column, backfill};\n return sql`\n INSERT INTO ${this.#cdc('backfilling')} ${sql(row)}\n ON CONFLICT (\"schema\", \"table\", \"column\") \n DO UPDATE SET ${sql(row)};\n `;\n }\n\n /**\n * Waits until all currently queued entries have been processed.\n * This is only used in tests.\n */\n async allProcessed() {\n if (this.#running) {\n const {promise, resolve} = resolver();\n this.#queue.enqueue(['ready', resolve]);\n await promise;\n }\n }\n\n stop() {\n if (this.#running) {\n this.#lc.info?.(`draining ${this.#queue.size()} changeLog entries`);\n this.#queue.enqueue('stop');\n }\n return this.#stopped;\n }\n}\n\n/**\n * Extracts the stringified change message from the stringified\n * stream message (e.g. the second tuple element). This optimization\n * facilitates stringifying (and sharing the result of) the stream\n * message exactly once, but storing only the change message substring\n * in the changeLog for backwards compatibility.\n */\nexport function extractChangeSubstring(\n streamMessageJSON: string,\n tag: Change['tag'] | undefined,\n) {\n switch (tag) {\n case 'begin':\n case 'commit':\n // e.g.\n // [\"begin\",<message-json>,{\"commitWatermark\":\"92fj2d0s\"}]\n // [\"commit\",<message-json>,{\"watermark\":\"92fj2d0s\"}]\n return streamMessageJSON.substring(\n streamMessageJSON.indexOf(',') + 1,\n streamMessageJSON.lastIndexOf(','),\n );\n default:\n // [\"data\",<message-json>]\n return streamMessageJSON.substring(\n streamMessageJSON.indexOf(',') + 1,\n streamMessageJSON.lastIndexOf(']'),\n );\n }\n}\n\ntype ChangeLogEntry = {\n watermark: string;\n tag: string;\n change: string;\n};\n\nfunction toDownstream(entry: ChangeLogEntry): WatermarkedChange {\n const {watermark, change} = entry;\n const tag = entry.tag as ChangeTag;\n switch (tag) {\n case 'begin':\n return [\n watermark,\n tag,\n `[\"begin\",${change},{\"commitWatermark\":\"${watermark}\"}]`,\n ];\n case 'commit':\n return [\n watermark,\n tag,\n `[\"commit\",${change},{\"watermark\":\"${watermark}\"}]`,\n ];\n case 'rollback':\n return [watermark, tag, `[\"rollback\",${change}]`];\n default:\n return [watermark, tag, `[\"data\",${change}]`];\n }\n}\n\nexport class PurgeLock {\n readonly #lc: LogContext;\n readonly #tx: TransactionPool;\n readonly replicaVersion: string;\n readonly minWatermark: string;\n\n constructor(\n lc: LogContext,\n tx: TransactionPool,\n replicaVersion: string,\n watermark: string,\n ) {\n this.#lc = lc;\n this.#tx = tx;\n this.replicaVersion = replicaVersion;\n this.minWatermark = watermark;\n }\n\n #released = false;\n\n async release() {\n if (this.#released) {\n return;\n }\n this.#released = true;\n this.#tx.setDone();\n await this.#tx\n .done()\n .catch(e => this.#lc.warn?.(`error from purge-lock release`, e));\n this.#lc.info?.(`released purge lock on ${this.minWatermark}`);\n }\n}\n\nexport class PurgeLocker {\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #db: PostgresDB;\n\n constructor(lc: LogContext, shard: ShardID, db: PostgresDB) {\n this.#lc = lc.withContext('component', 'purge-locker');\n this.#shard = shard;\n this.#db = db;\n }\n\n // For readability in SQL statements.\n #cdc(table: string) {\n return this.#db(`${cdcSchema(this.#shard)}.${table}`);\n }\n\n async acquire() {\n const tx = new TransactionPool(this.#lc, {mode: Mode.READ_COMMITTED}).run(\n this.#db,\n );\n const row = await tx.processReadTask(\n sql => sql<{watermark: string}[]>`\n SELECT watermark FROM ${this.#cdc('changeLog')}\n ORDER BY watermark, pos LIMIT 1\n FOR SHARE \n `,\n );\n if (row.length === 0) {\n this.#lc.info?.(`changeLog is empty. No rows to purge-lock.`);\n tx.setDone();\n await tx.done();\n return null;\n }\n const [{watermark}] = row;\n const [{replicaVersion}] = await tx.processReadTask(\n sql => sql<{replicaVersion: string}[]>`\n SELECT \"replicaVersion\" FROM ${this.#cdc('replicationConfig')}\n `,\n );\n this.#lc.info?.(\n `locked watermark ${watermark} from being purged from replica@${replicaVersion}`,\n );\n return new PurgeLock(this.#lc, tx, replicaVersion, watermark);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"storer.js","names":["#lc","#shard","#taskID","#discoveryAddress","#discoveryProtocol","#db","#replicaVersion","#onConsumed","#onFatal","#queue","#backPressureThresholdBytes","#statementTimeoutMs","#changeLogBatchSize","#cdc","#approximateQueuedBytes","#running","#readyForMore","#stopped","#processQueue","#cancelQueueEntries","#startCatchup","#flushChangeLog","#trackBackfillMetadata","#maybeReleaseBackPressure","#catchup","#upsertTableMetadataStmt","#upsertColumnBackfillStmt","#tx","#released"],"sources":["../../../../../../zero-cache/src/services/change-streamer/storer.ts"],"sourcesContent":["import {getHeapStatistics} from 'node:v8';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver, type Resolver} from '@rocicorp/resolver';\nimport {type PendingQuery, type Row} from 'postgres';\nimport {AbortError} from '../../../../shared/src/abort-error.ts';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport {Queue} from '../../../../shared/src/queue.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {TransactionPool} from '../../db/transaction-pool.ts';\nimport {type PostgresDB, type PostgresTransaction} from '../../types/pg.ts';\nimport {cdcSchema, type ShardID} from '../../types/shards.ts';\nimport {\n backfillRequestSchema,\n isDataChange,\n isSchemaChange,\n type BackfillID,\n type BackfillRequest,\n type Change,\n type DataChange,\n type Identifier,\n type SchemaChange,\n type TableMetadata,\n} from '../change-source/protocol/current.ts';\nimport {\n type ChangeStreamData,\n type Commit,\n} from '../change-source/protocol/current/downstream.ts';\nimport type {\n DownstreamStatusMessage,\n UpstreamStatusMessage,\n} from '../change-source/protocol/current/status.ts';\nimport type {ReplicatorMode} from '../replicator/replicator.ts';\nimport type {Service} from '../service.ts';\nimport type {ChangeTag, WatermarkedChange} from './change-streamer-service.ts';\nimport * as ErrorType from './error-type-enum.ts';\nimport {\n AutoResetSignal,\n markResetRequired,\n type BackfillingColumn,\n type TableMetadataRow,\n} from './schema/tables.ts';\nimport type {Subscriber} from './subscriber.ts';\n\ntype SubscriberAndMode = {\n subscriber: Subscriber;\n mode: ReplicatorMode;\n};\n\ntype QueueEntry =\n | [\n 'change',\n watermark: string,\n json: string,\n orig: Exclude<Change, DataChange> | null, // null for DataChanges\n ]\n | ['ready', callback: () => void]\n | ['subscriber', SubscriberAndMode]\n | DownstreamStatusMessage\n | ['abort']\n | 'stop';\n\ntype PendingTransaction = {\n pool: TransactionPool;\n preCommitWatermark: string;\n pos: number;\n startingReplicationState: Promise<ReplicationOwner>;\n ack: boolean;\n // changeLog rows buffered for the next multi-row INSERT flush.\n batch: ChangeLogRow[];\n // The most recently issued flush (or metadata) process, awaited to bound\n // pipeline depth and to order the commit-time replicationState update.\n lastFlush: Promise<unknown> | undefined;\n};\n\ntype ReplicationOwner = {\n owner: string | null;\n};\n\nconst backfillRequestsSchema = v.array(backfillRequestSchema);\n\nexport type TuningOptions = {\n backPressureLimitHeapProportion: number;\n statementTimeoutMs: number;\n changeLogBatchSize: number;\n};\n\n/**\n * A single `changeLog` row, accumulated in {@link PendingTransaction.batch}\n * and written via `json_to_recordset()` (see `#flushChangeLog()`).\n */\ntype ChangeLogRow = {\n watermark: string;\n precommit: string | null;\n pos: number;\n change: string;\n};\n\n/**\n * Handles the storage of changes and the catchup of subscribers\n * that are behind.\n *\n * In the context of catchup and cleanup, it is the responsibility of the\n * Storer to decide whether a client can be caught up, or whether the\n * changes needed to catch a client up have been purged.\n *\n * **Maintained invariant**: The Change DB is only empty for a\n * completely new replica (i.e. initial-sync with no changes from the\n * replication stream).\n * * In this case, all new subscribers are expected start from the\n * `replicaVersion`, which is the version at which initial sync\n * was performed, and any attempts to catchup from a different\n * point fail.\n *\n * Conversely, if non-initial changes have flowed through the system\n * (i.e. via the replication stream), the ChangeDB must *not* be empty,\n * and the earliest change in the `changeLog` represents the earliest\n * \"commit\" from (after) which a subscriber can be caught up.\n * * Any attempts to catchup from an earlier point must fail with\n * a `WatermarkTooOld` error.\n * * Failure to do so could result in streaming changes to the\n * subscriber such that there is a gap in its replication history.\n *\n * Note: Subscribers (i.e. `incremental-syncer`) consider an \"error\" signal\n * an unrecoverable error and shut down in response. This allows the\n * production system to replace it with a new task and fresh copy of the\n * replica backup.\n */\nexport class Storer implements Service {\n readonly id = 'storer';\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #taskID: string;\n readonly #discoveryAddress: string;\n readonly #discoveryProtocol: string;\n readonly #db: PostgresDB;\n readonly #replicaVersion: string;\n readonly #onConsumed: (c: Commit | UpstreamStatusMessage) => void;\n readonly #onFatal: (err: Error) => void;\n readonly #queue = new Queue<QueueEntry>();\n readonly #backPressureThresholdBytes: number;\n readonly #statementTimeoutMs: number;\n readonly #changeLogBatchSize: number;\n\n #approximateQueuedBytes = 0;\n #running = false;\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n taskID: string,\n discoveryAddress: string,\n discoveryProtocol: string,\n db: PostgresDB,\n replicaVersion: string,\n onConsumed: (c: Commit | UpstreamStatusMessage) => void,\n onFatal: (err: Error) => void,\n {\n backPressureLimitHeapProportion,\n statementTimeoutMs,\n changeLogBatchSize,\n }: TuningOptions,\n ) {\n this.#lc = lc.withContext('component', 'change-log');\n this.#shard = shard;\n this.#taskID = taskID;\n this.#discoveryAddress = discoveryAddress;\n this.#discoveryProtocol = discoveryProtocol;\n this.#db = db;\n this.#replicaVersion = replicaVersion;\n this.#onConsumed = onConsumed;\n this.#onFatal = onFatal;\n this.#statementTimeoutMs = statementTimeoutMs;\n this.#changeLogBatchSize = Math.max(1, changeLogBatchSize);\n\n const heapStats = getHeapStatistics();\n this.#backPressureThresholdBytes =\n (heapStats.heap_size_limit - heapStats.used_heap_size) *\n backPressureLimitHeapProportion;\n\n this.#lc.info?.(\n `Using up to ${(this.#backPressureThresholdBytes / 1024 ** 2).toFixed(2)} MB of ` +\n `--max-old-space-size (~${(heapStats.heap_size_limit / 1024 ** 2).toFixed(2)} MB) ` +\n `to absorb upstream spikes`,\n {heapStats},\n );\n }\n\n // For readability in SQL statements.\n #cdc(table: string) {\n return this.#db(`${cdcSchema(this.#shard)}.${table}`);\n }\n\n async assumeOwnership(purgeLock?: PurgeLock | null) {\n const db = this.#db;\n const owner = this.#taskID;\n const ownerAddress = this.#discoveryAddress;\n const ownerProtocol = this.#discoveryProtocol;\n // we omit `ws://` so that old view syncer versions that are not expecting the protocol continue to not get it\n const addressWithProtocol =\n ownerProtocol === 'ws'\n ? ownerAddress\n : `${ownerProtocol}://${ownerAddress}`;\n this.#lc.info?.(`assuming ownership at ${addressWithProtocol}`);\n const start = performance.now();\n await db`UPDATE ${this.#cdc('replicationState')} SET ${db({owner, ownerAddress: addressWithProtocol})}`;\n const elapsed = (performance.now() - start).toFixed(2);\n this.#lc.info?.(\n `assumed ownership at ${addressWithProtocol} (${elapsed} ms)`,\n );\n\n if (purgeLock) {\n // Once ownership has been assumed, any initial purge-lock preventing the\n // purging of change-log records can be released, as a change-streamer\n // that was attempting to purge records will correspondingly abort on the\n // ownership check.\n void purgeLock.release();\n }\n }\n\n async getStartStreamInitializationParameters(): Promise<{\n lastWatermark: string;\n backfillRequests: BackfillRequest[];\n }> {\n const [[{lastWatermark}], result] = await runTx(\n this.#db,\n sql => [\n sql<{lastWatermark: string}[]>`\n SELECT \"lastWatermark\" FROM ${this.#cdc('replicationState')}`,\n\n // Formats a BackfillRequest using json_object_agg() to construct the\n // `columns` object. It is LEFT JOIN'ed with the `tableMetadata` table\n // to make it optional and possibly `null`.\n sql`\n SELECT \n json_build_object(\n 'schema', b.\"schema\",\n 'name', b.\"table\",\n 'metadata', t.\"metadata\"\n ) as \"table\",\n json_object_agg(b.\"column\", b.\"backfill\") \n as \"columns\"\n FROM ${this.#cdc('backfilling')} as b\n LEFT JOIN ${this.#cdc('tableMetadata')} as t\n ON (b.\"schema\" = t.\"schema\" AND b.\"table\" = t.\"table\")\n GROUP BY b.\"schema\", b.\"table\", t.\"metadata\"\n `,\n ],\n {mode: Mode.READONLY},\n );\n\n return {\n lastWatermark,\n backfillRequests: v.parse(result, backfillRequestsSchema),\n };\n }\n\n async getMinWatermarkForCatchup(): Promise<string | null> {\n const [{minWatermark}] = await this.#db<{minWatermark: string | null}[]>\n /*sql*/ `\n SELECT min(watermark) as \"minWatermark\" FROM ${this.#cdc('changeLog')}`;\n return minWatermark;\n }\n\n purgeRecordsBefore(watermark: string): Promise<number> {\n return runTx(this.#db, async sql => {\n // This NOWAIT pre-check is an optimization to abort the transaction\n // (and release associated resources) early.\n await sql<{watermark: string}[]>`\n SELECT watermark FROM ${this.#cdc('changeLog')}\n ORDER BY watermark, pos LIMIT 1\n FOR UPDATE NOWAIT\n `;\n // If the row is purge-locked by an incoming replication-manager, it\n // will assume ownership of the change-log before releasing the lock.\n // This DELETE blocks until the lock is released, allowing the change\n // in ownership to be reliably detected (and the transaction aborted)\n // in the subsequent check.\n const [{deleted}] = await sql<{deleted: bigint}[]>`\n -- The backup watermark can be ahead of the durable changeLog if the\n -- storer is behind but the backup replica has consumed forwarded\n -- changes. Preserve the latest durable changeLog transaction as the\n -- catchup boundary instead of assuming the backup watermark exists.\n -- The storer inserts each changeLog transaction atomically, so any\n -- durable row for a watermark implies the full transaction is durable.\n WITH keep AS (\n SELECT max(watermark) AS watermark\n FROM ${this.#cdc('changeLog')}\n ), purged AS (\n DELETE FROM ${this.#cdc('changeLog')} WHERE watermark < ${watermark} \n AND watermark < (SELECT watermark FROM keep)\n RETURNING watermark, pos\n ) SELECT COUNT(*) as deleted FROM purged;`;\n\n const [{owner}] = await sql<ReplicationOwner[]>`\n SELECT \"owner\" FROM ${this.#cdc('replicationState')} FOR SHARE`;\n if (owner !== this.#taskID) {\n throw new AbortError(\n `aborting changeLog purge to ${watermark} because ownership has been taken by ${owner}`,\n );\n }\n return Number(deleted);\n });\n }\n\n /**\n * @returns The JSON stringified stream message to be sent downstream.\n */\n store(watermark: string, data: ChangeStreamData) {\n // Eagerly stringify the JSON payload to:\n // - avoid redundant stringification when fanning out to subscribers\n // - efficiently estimate the amount of memory the payload consumes\n const json = BigIntJSON.stringify(data);\n this.#approximateQueuedBytes += json.length;\n\n const change = data[1];\n this.#queue.enqueue([\n 'change',\n watermark,\n json,\n isDataChange(change) ? null : change, // drop DataChanges to save memory\n ]);\n\n return json;\n }\n\n abort() {\n this.#queue.enqueue(['abort']);\n }\n\n status(s: DownstreamStatusMessage) {\n this.#queue.enqueue(s);\n }\n\n catchup(subscriber: Subscriber, mode: ReplicatorMode) {\n this.#queue.enqueue(['subscriber', {subscriber, mode}]);\n }\n\n #readyForMore: Resolver<void> | null = null;\n\n readyForMore(): Promise<void> | undefined {\n if (!this.#running) {\n return undefined;\n }\n if (\n this.#readyForMore === null &&\n this.#approximateQueuedBytes > this.#backPressureThresholdBytes\n ) {\n this.#lc.warn?.(\n `applying back pressure with ${this.#queue.size()} queued changes (~${(this.#approximateQueuedBytes / 1024 ** 2).toFixed(2)} MB)\\n` +\n `\\n` +\n `To inspect changeLog backlog in your change DB:\\n` +\n ` SELECT\\n` +\n ` (change->'relation'->>'schema') || '.' || (change->'relation'->>'name') AS table_name,\\n` +\n ` change->>'tag' AS operation,\\n` +\n ` COUNT(*) AS count\\n` +\n ` FROM \"<app_id>/cdc\".\"changeLog\"\\n` +\n ` GROUP BY 1, 2\\n` +\n ` ORDER BY 3 DESC\\n` +\n ` LIMIT 20;`,\n );\n this.#readyForMore = resolver();\n }\n return this.#readyForMore?.promise;\n }\n\n #maybeReleaseBackPressure() {\n if (\n this.#readyForMore !== null &&\n // Wait for at least 20% of the threshold to free up.\n this.#approximateQueuedBytes < this.#backPressureThresholdBytes * 0.8\n ) {\n this.#lc.info?.(\n `releasing back pressure with ${this.#queue.size()} queued changes (~${(this.#approximateQueuedBytes / 1024 ** 2).toFixed(2)} MB)`,\n );\n this.#readyForMore.resolve();\n this.#readyForMore = null;\n }\n }\n\n /**\n * Flushes any buffered {@link PendingTransaction.batch} rows to the changeLog.\n *\n * Uses `json_to_recordset()` so the batch is a single JSON parameter: the\n * statement text stays constant regardless of batch size, avoiding the\n * unbounded prepared-statement variants (and Postgres memory growth) that a\n * multi-row INSERT would produce. See rocicorp/mono#3511.\n *\n * Returns (and updates) {@link PendingTransaction.lastFlush}; a no-op when the\n * batch is empty.\n */\n #flushChangeLog(tx: PendingTransaction): Promise<unknown> | undefined {\n const {batch} = tx;\n if (batch.length === 0) {\n return tx.lastFlush;\n }\n tx.batch = [];\n tx.lastFlush = tx.pool.process(sql => [\n sql`\n INSERT INTO ${this.#cdc('changeLog')} (\"watermark\", \"pos\", \"change\", \"precommit\")\n SELECT \"watermark\", \"pos\", \"change\"::json, \"precommit\"\n FROM json_to_recordset(${batch}) AS x(\n \"watermark\" TEXT,\n \"pos\" INT8,\n \"change\" TEXT,\n \"precommit\" TEXT\n )`,\n ]);\n return tx.lastFlush;\n }\n\n #stopped = promiseVoid;\n\n /**\n * Runs the storer loop until {@link stop()} is called, or an error is thrown.\n * Once {@link run()} completes, it can be called again.\n */\n async run() {\n assert(!this.#running, `storer is already running`);\n\n const {promise: stopped, resolve: signalStopped} = resolver();\n this.#running = true;\n this.#stopped = stopped;\n\n this.#lc.info?.('starting storer');\n let err: unknown;\n try {\n await this.#processQueue();\n } catch (e) {\n err = e; // used in finally\n throw e;\n } finally {\n // Release any pending backpressure so the upstream can proceed\n if (this.#readyForMore !== null) {\n this.#readyForMore.resolve();\n this.#readyForMore = null;\n }\n this.#cancelQueueEntries(\n this.#queue.drain().filter(entry => entry !== undefined),\n err,\n );\n this.#running = false;\n signalStopped();\n this.#lc.info?.('storer stopped');\n }\n }\n\n #cancelQueueEntries(queue: QueueEntry[], e: unknown) {\n if (queue.length === 0) {\n return;\n }\n this.#lc.info?.(\n `canceling ${queue.length} entries from the changeLog queue`,\n );\n const err = e instanceof Error ? e : new AbortError('server shutting down');\n for (const entry of queue) {\n if (entry === 'stop') {\n continue;\n }\n const type = entry[0];\n switch (type) {\n case 'subscriber': {\n // Disconnect subscribers waiting to be caught up so that they can\n // reconnect and try again.\n const {subscriber} = entry[1];\n this.#lc.info?.(`disconnecting ${subscriber.id}`);\n subscriber.fail(err);\n break;\n }\n }\n }\n }\n\n async #processQueue() {\n let tx: PendingTransaction | null = null;\n let msg: QueueEntry | false;\n\n const catchupQueue: SubscriberAndMode[] = [];\n try {\n while ((msg = await this.#queue.dequeue()) !== 'stop') {\n const [msgType] = msg;\n switch (msgType) {\n case 'ready': {\n const signalReady = msg[1];\n signalReady();\n continue;\n }\n case 'subscriber': {\n const subscriber = msg[1];\n if (tx) {\n catchupQueue.push(subscriber); // Wait for the current tx to complete.\n } else {\n await this.#startCatchup([subscriber]); // Catch up immediately.\n }\n continue;\n }\n case 'status':\n this.#onConsumed(msg);\n continue;\n case 'abort': {\n if (tx) {\n tx.pool.abort();\n await tx.pool.done();\n tx = null;\n }\n continue;\n }\n }\n // msgType === 'change'\n const [_, watermark, json, change] = msg;\n const tag = change?.tag;\n this.#approximateQueuedBytes -= json.length;\n\n if (tag === 'begin') {\n assert(!tx, 'received BEGIN in the middle of a transaction');\n const {promise, resolve, reject} = resolver<ReplicationOwner>();\n void promise.catch(() => {}); // handle rejections before the await\n tx = {\n pool: new TransactionPool(\n this.#lc.withContext('watermark', watermark),\n {\n mode: Mode.READ_COMMITTED,\n statementResponseTimeout: this.#statementTimeoutMs,\n },\n ),\n preCommitWatermark: watermark,\n pos: 0,\n startingReplicationState: promise,\n ack: !change.skipAck,\n batch: [],\n lastFlush: undefined,\n };\n tx.pool.run(this.#db);\n // Acquire a lock on the replicationState row to detect and/or prevent\n // a concurrent ownership change.\n void tx.pool.process(tx => {\n tx<ReplicationOwner[]> /*sql*/ `\n SELECT \"owner\" FROM ${this.#cdc('replicationState')} FOR UPDATE`.then(\n ([result]) => resolve(result),\n reject,\n );\n return [];\n });\n } else {\n assert(tx, () => `received change outside of transaction: ${json}`);\n tx.pos++;\n }\n\n const entry: ChangeLogRow = {\n watermark: tag === 'commit' ? watermark : tx.preCommitWatermark,\n precommit: tag === 'commit' ? tx.preCommitWatermark : null,\n pos: tx.pos,\n // For backwards compatibility, only the change message is stored\n // in the cdc changeLog.\n change: extractChangeSubstring(json, tag),\n };\n\n if (change !== null && isSchemaChange(change)) {\n // Schema changes carry backfill / table-metadata statements that\n // must be applied in stream order relative to the changeLog rows.\n // Flush any buffered rows first, then write this row together with\n // its metadata statements as a single unit (preserving the previous\n // per-change ordering for schema changes).\n await this.#flushChangeLog(tx);\n tx.lastFlush = tx.pool.process(sql => [\n sql`INSERT INTO ${this.#cdc('changeLog')} ${sql(entry)}`,\n ...this.#trackBackfillMetadata(sql, change),\n ]);\n } else {\n // Accumulate plain changeLog rows (begin, data changes, commit) and\n // write them as a single multi-row INSERT. Collapsing the per-change\n // single-row INSERTs into batches is the dominant cost reduction for\n // large transactions, where the previous one-statement-per-change\n // path dominated the upstream replication lag.\n tx.batch.push(entry);\n if (tx.batch.length >= this.#changeLogBatchSize) {\n // Bound pipeline depth (and thus memory) by awaiting the previous\n // flush before issuing the next. This is the batched analog of the\n // previous per-100-statement backpressure await, and likewise\n // guards against memory blowup on very large transactions.\n const prevFlush = tx.lastFlush;\n void this.#flushChangeLog(tx);\n await prevFlush;\n }\n }\n this.#maybeReleaseBackPressure();\n\n if (tag === 'commit') {\n // Flush any remaining buffered changeLog rows (including this commit\n // row) before updating the replication state, so the state update is\n // ordered after all changeLog inserts for this transaction.\n void this.#flushChangeLog(tx);\n\n const {owner} = await tx.startingReplicationState;\n if (owner !== this.#taskID) {\n // Ownership change reflected in the replicationState read in 'begin'.\n tx.pool.fail(\n new AbortError(\n `changeLog ownership has been assumed by ${owner}`,\n ),\n );\n } else {\n // Update the replication state.\n const lastWatermark = watermark;\n void tx.pool.process(tx => [\n tx`\n UPDATE ${this.#cdc('replicationState')} SET ${tx({lastWatermark})}`,\n ]);\n tx.pool.setDone();\n }\n\n await tx.pool.done();\n\n // ACK the LSN to the upstream Postgres.\n if (tx.ack) {\n this.#onConsumed(['commit', change, {watermark}]);\n }\n tx = null;\n\n // Before beginning the next transaction, open a READONLY snapshot to\n // concurrently catchup any queued subscribers.\n await this.#startCatchup(catchupQueue.splice(0));\n } else if (tag === 'rollback') {\n // Aborted transactions are not stored in the changeLog. Abort the current tx\n // and process catchup of subscribers that were waiting for it to end.\n tx.pool.abort();\n await tx.pool.done();\n tx = null;\n\n await this.#startCatchup(catchupQueue.splice(0));\n }\n }\n } catch (e) {\n catchupQueue.forEach(({subscriber}) => subscriber.fail(e));\n throw e;\n }\n }\n\n async #startCatchup(subs: SubscriberAndMode[]) {\n if (subs.length === 0) {\n return;\n }\n\n const reader = new TransactionPool(\n this.#lc.withContext('pool', 'catchup'),\n {mode: Mode.READONLY},\n );\n reader.run(this.#db);\n\n let lastWatermark: string | undefined;\n try {\n // Ensure that the transaction has started (and is thus holding a snapshot\n // of the database) before continuing on to commit more changes. This is\n // done by performing a single read on the db, which determines the\n // snapshot for the REPEATABLE_READ transaction.\n [{lastWatermark}] = await reader.processReadTask(\n sql => sql<{lastWatermark: string}[]>`\n SELECT \"lastWatermark\" FROM ${this.#cdc('replicationState')}\n `,\n );\n } catch (e) {\n subs.map(({subscriber}) => subscriber.fail(e));\n throw e;\n }\n\n // Run the actual catchup queries in the background. Errors are handled in\n // #catchup() by disconnecting the associated subscriber.\n void Promise.all(\n subs.map(sub => this.#catchup(sub, lastWatermark, reader)),\n ).finally(() => reader.setDone());\n }\n\n async #catchup(\n {subscriber: sub, mode}: SubscriberAndMode,\n lastWatermark: string,\n reader: TransactionPool,\n ) {\n try {\n await reader.processReadTask(async tx => {\n const start = Date.now();\n\n // When starting from initial-sync, there won't be a change with a watermark\n // equal to the replica version. This is the empty changeLog scenario.\n let watermarkFound = sub.watermark === this.#replicaVersion;\n let count = 0;\n let lastBatchConsumed: Promise<unknown> | undefined;\n\n for await (const entries of tx<ChangeLogEntry[]> /*sql*/ `\n SELECT watermark, change->'tag' as tag, change::text FROM ${this.#cdc('changeLog')}\n WHERE watermark >= ${sub.watermark}\n AND watermark <= ${lastWatermark}\n ORDER BY watermark, pos`.cursor(2000)) {\n // Wait for the last batch of entries to be consumed by the\n // subscriber before sending down the current batch. This pipelining\n // allows one batch of changes to be received from the change-db\n // while the previous batch of changes are sent to the subscriber,\n // resulting in flow control that caps the number of changes\n // referenced in memory to 2 * batch-size.\n const start = performance.now();\n await lastBatchConsumed;\n const elapsed = performance.now() - start;\n if (lastBatchConsumed) {\n this.#lc[elapsed > 100 ? 'info' : 'debug']?.(\n `waited ${elapsed.toFixed(3)} ms for ${sub.id} to consume last batch of catchup entries`,\n );\n }\n\n for (const entry of entries) {\n if (entry.watermark === sub.watermark) {\n // This should be the first entry.\n // Catchup starts from *after* the watermark.\n watermarkFound = true;\n } else if (watermarkFound) {\n lastBatchConsumed = sub.catchup(toDownstream(entry));\n count++;\n } else if (mode === 'backup') {\n throw new AutoResetSignal(\n `backup replica at watermark ${sub.watermark} is behind change db: ${entry.watermark})`,\n );\n } else {\n this.#lc.warn?.(\n `rejecting subscriber at watermark ${sub.watermark} (earliest watermark: ${entry.watermark})`,\n );\n sub.close(\n ErrorType.WatermarkTooOld,\n `earliest supported watermark is ${entry.watermark} (requested ${sub.watermark})`,\n );\n return;\n }\n }\n }\n if (watermarkFound) {\n await lastBatchConsumed;\n this.#lc.info?.(\n `caught up ${sub.id} with ${count} changes (${\n Date.now() - start\n } ms)`,\n );\n } else {\n // The subscriber is ahead of the latest durable changeLog entry\n // (lastWatermark). This can legitimately happen: changes are\n // forwarded to subscribers (the backup replica and view-syncers)\n // concurrently with — and can outrun — the durable store, so a\n // replica may briefly lead the change DB after the storer falls\n // behind or the change-streamer restarts. No catchup is possible or\n // needed; once the change DB catches back up, forwarding resumes and\n // the subscriber dedups any watermarks it already has. Unlike the\n // AutoResetSignal / WatermarkTooOld cases above, this is not a gap in\n // replication history, so the subscriber is simply marked caught up.\n this.#lc.warn?.(\n `subscriber ${sub.id} at watermark ${sub.watermark} is ahead of ` +\n `the latest durable watermark ${lastWatermark}; waiting for the ` +\n `change DB to catch up`,\n );\n }\n // Start draining messages buffered during catchup. The returned promise\n // is intentionally not awaited here: while the drain is in progress,\n // new sends keep appending to the subscriber backlog and inherit its\n // byte-based backpressure.\n void sub.setCaughtUp();\n });\n } catch (err) {\n this.#lc.error?.(`error while catching up subscriber ${sub.id}`, err);\n if (err instanceof AutoResetSignal) {\n await markResetRequired(this.#db, this.#shard);\n this.#onFatal(err);\n }\n sub.fail(err);\n }\n }\n\n /**\n * Returns the db statements necessary to track backfill and table metadata\n * presented in the `change`, if any.\n */\n #trackBackfillMetadata(sql: PostgresTransaction, change: SchemaChange) {\n const stmts: PendingQuery<Row[]>[] = [];\n\n switch (change.tag) {\n case 'update-table-metadata': {\n const {table, new: metadata} = change;\n stmts.push(this.#upsertTableMetadataStmt(sql, table, metadata));\n break;\n }\n\n case 'create-table': {\n const {spec, metadata, backfill} = change;\n if (metadata) {\n stmts.push(this.#upsertTableMetadataStmt(sql, spec, metadata));\n }\n if (backfill) {\n Object.entries(backfill).forEach(([col, backfill]) => {\n stmts.push(\n this.#upsertColumnBackfillStmt(sql, spec, col, backfill),\n );\n });\n }\n break;\n }\n\n case 'rename-table': {\n const {old} = change;\n const row = {schema: change.new.schema, table: change.new.name};\n stmts.push(\n sql`UPDATE ${this.#cdc('tableMetadata')} SET ${sql(row)}\n WHERE \"schema\" = ${old.schema} AND \"table\" = ${old.name}`,\n sql`UPDATE ${this.#cdc('backfilling')} SET ${sql(row)}\n WHERE \"schema\" = ${old.schema} AND \"table\" = ${old.name}`,\n );\n break;\n }\n\n case 'drop-table': {\n const {\n id: {schema, name},\n } = change;\n stmts.push(\n sql`DELETE FROM ${this.#cdc('tableMetadata')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${name}`,\n sql`DELETE FROM ${this.#cdc('backfilling')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${name}`,\n );\n break;\n }\n\n case 'add-column': {\n const {table, tableMetadata, column, backfill} = change;\n if (tableMetadata) {\n stmts.push(this.#upsertTableMetadataStmt(sql, table, tableMetadata));\n }\n if (backfill) {\n stmts.push(\n this.#upsertColumnBackfillStmt(sql, table, column.name, backfill),\n );\n }\n break;\n }\n\n case 'update-column': {\n const {\n table: {schema, name: table},\n old: {name: oldName},\n new: {name: newName},\n } = change;\n if (oldName !== newName) {\n stmts.push(\n sql`UPDATE ${this.#cdc('backfilling')} SET \"column\" = ${newName}\n WHERE \"schema\" = ${schema} AND \"table\" = ${table} AND \"column\" = ${oldName}`,\n );\n }\n break;\n }\n\n case 'drop-column': {\n const {\n table: {schema, name},\n column,\n } = change;\n stmts.push(\n sql`DELETE FROM ${this.#cdc('backfilling')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${name} AND \"column\" = ${column}`,\n );\n break;\n }\n\n case 'backfill-completed': {\n const {\n relation: {schema, name: table, rowKey},\n columns,\n } = change;\n const cols = [...rowKey.columns, ...columns];\n stmts.push(\n sql`DELETE FROM ${this.#cdc('backfilling')}\n WHERE \"schema\" = ${schema} AND \"table\" = ${table} AND \"column\" IN ${sql(cols)}`,\n );\n }\n }\n return stmts;\n }\n\n #upsertTableMetadataStmt(\n sql: PostgresTransaction,\n {schema, name: table}: Identifier,\n metadata: TableMetadata,\n ) {\n const row: TableMetadataRow = {schema, table, metadata};\n return sql`\n INSERT INTO ${this.#cdc('tableMetadata')} ${sql(row)}\n ON CONFLICT (\"schema\", \"table\") \n DO UPDATE SET ${sql(row)};\n `;\n }\n\n #upsertColumnBackfillStmt(\n sql: PostgresTransaction,\n {schema, name: table}: Identifier,\n column: string,\n backfill: BackfillID,\n ) {\n const row: BackfillingColumn = {schema, table, column, backfill};\n return sql`\n INSERT INTO ${this.#cdc('backfilling')} ${sql(row)}\n ON CONFLICT (\"schema\", \"table\", \"column\") \n DO UPDATE SET ${sql(row)};\n `;\n }\n\n /**\n * Waits until all currently queued entries have been processed.\n * This is only used in tests.\n */\n async allProcessed() {\n if (this.#running) {\n const {promise, resolve} = resolver();\n this.#queue.enqueue(['ready', resolve]);\n await promise;\n }\n }\n\n stop() {\n if (this.#running) {\n this.#lc.info?.(`draining ${this.#queue.size()} changeLog entries`);\n this.#queue.enqueue('stop');\n }\n return this.#stopped;\n }\n}\n\n/**\n * Extracts the stringified change message from the stringified\n * stream message (e.g. the second tuple element). This optimization\n * facilitates stringifying (and sharing the result of) the stream\n * message exactly once, but storing only the change message substring\n * in the changeLog for backwards compatibility.\n */\nexport function extractChangeSubstring(\n streamMessageJSON: string,\n tag: Change['tag'] | undefined,\n) {\n switch (tag) {\n case 'begin':\n case 'commit':\n // e.g.\n // [\"begin\",<message-json>,{\"commitWatermark\":\"92fj2d0s\"}]\n // [\"commit\",<message-json>,{\"watermark\":\"92fj2d0s\"}]\n return streamMessageJSON.substring(\n streamMessageJSON.indexOf(',') + 1,\n streamMessageJSON.lastIndexOf(','),\n );\n default:\n // [\"data\",<message-json>]\n return streamMessageJSON.substring(\n streamMessageJSON.indexOf(',') + 1,\n streamMessageJSON.lastIndexOf(']'),\n );\n }\n}\n\ntype ChangeLogEntry = {\n watermark: string;\n tag: string;\n change: string;\n};\n\nfunction toDownstream(entry: ChangeLogEntry): WatermarkedChange {\n const {watermark, change} = entry;\n const tag = entry.tag as ChangeTag;\n switch (tag) {\n case 'begin':\n return [\n watermark,\n tag,\n `[\"begin\",${change},{\"commitWatermark\":\"${watermark}\"}]`,\n ];\n case 'commit':\n return [\n watermark,\n tag,\n `[\"commit\",${change},{\"watermark\":\"${watermark}\"}]`,\n ];\n case 'rollback':\n return [watermark, tag, `[\"rollback\",${change}]`];\n default:\n return [watermark, tag, `[\"data\",${change}]`];\n }\n}\n\nexport class PurgeLock {\n readonly #lc: LogContext;\n readonly #tx: TransactionPool;\n readonly replicaVersion: string;\n readonly minWatermark: string;\n\n constructor(\n lc: LogContext,\n tx: TransactionPool,\n replicaVersion: string,\n watermark: string,\n ) {\n this.#lc = lc;\n this.#tx = tx;\n this.replicaVersion = replicaVersion;\n this.minWatermark = watermark;\n }\n\n #released = false;\n\n async release() {\n if (this.#released) {\n return;\n }\n this.#released = true;\n this.#tx.setDone();\n await this.#tx\n .done()\n .catch(e => this.#lc.warn?.(`error from purge-lock release`, e));\n this.#lc.info?.(`released purge lock on ${this.minWatermark}`);\n }\n}\n\nexport class PurgeLocker {\n readonly #lc: LogContext;\n readonly #shard: ShardID;\n readonly #db: PostgresDB;\n\n constructor(lc: LogContext, shard: ShardID, db: PostgresDB) {\n this.#lc = lc.withContext('component', 'purge-locker');\n this.#shard = shard;\n this.#db = db;\n }\n\n // For readability in SQL statements.\n #cdc(table: string) {\n return this.#db(`${cdcSchema(this.#shard)}.${table}`);\n }\n\n async acquire() {\n const tx = new TransactionPool(this.#lc, {mode: Mode.READ_COMMITTED}).run(\n this.#db,\n );\n const row = await tx.processReadTask(\n sql => sql<{watermark: string}[]>`\n SELECT watermark FROM ${this.#cdc('changeLog')}\n ORDER BY watermark, pos LIMIT 1\n FOR SHARE \n `,\n );\n if (row.length === 0) {\n this.#lc.info?.(`changeLog is empty. No rows to purge-lock.`);\n tx.setDone();\n await tx.done();\n return null;\n }\n const [{watermark}] = row;\n const [{replicaVersion}] = await tx.processReadTask(\n sql => sql<{replicaVersion: string}[]>`\n SELECT \"replicaVersion\" FROM ${this.#cdc('replicationConfig')}\n `,\n );\n this.#lc.info?.(\n `locked watermark ${watermark} from being purged from replica@${replicaVersion}`,\n );\n return new PurgeLock(this.#lc, tx, replicaVersion, watermark);\n }\n}\n"],"mappings":"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"}
|
|
@@ -4,6 +4,10 @@ import type { ChangeTag, WatermarkedChange } from './change-streamer-service.ts'
|
|
|
4
4
|
import { type Status } from './change-streamer.ts';
|
|
5
5
|
import * as ErrorType from './error-type-enum.ts';
|
|
6
6
|
type ErrorType = Enum<typeof ErrorType>;
|
|
7
|
+
export type SubscriberOptions = {
|
|
8
|
+
backlogHighWaterBytes?: number | undefined;
|
|
9
|
+
backlogLowWaterRatio?: number | undefined;
|
|
10
|
+
};
|
|
7
11
|
/**
|
|
8
12
|
* Encapsulates a subscriber to changes. All subscribers start in a
|
|
9
13
|
* "catchup" phase in which changes are buffered in a backlog while the
|
|
@@ -14,7 +18,7 @@ type ErrorType = Enum<typeof ErrorType>;
|
|
|
14
18
|
export declare class Subscriber {
|
|
15
19
|
#private;
|
|
16
20
|
readonly id: string;
|
|
17
|
-
constructor(protocolVersion: number, id: string, watermark: string, downstream: Subscription<string>, latestStatus: () => Status);
|
|
21
|
+
constructor(protocolVersion: number, id: string, watermark: string, downstream: Subscription<string>, latestStatus: () => Status, options?: SubscriberOptions);
|
|
18
22
|
get watermark(): string;
|
|
19
23
|
get acked(): string;
|
|
20
24
|
send(change: WatermarkedChange): Promise<void>;
|
|
@@ -25,7 +29,7 @@ export declare class Subscriber {
|
|
|
25
29
|
* Marks the Subscribe as "caught up" and flushes any backlog of
|
|
26
30
|
* entries that were received during the catchup.
|
|
27
31
|
*/
|
|
28
|
-
setCaughtUp(): void
|
|
32
|
+
setCaughtUp(): Promise<void>;
|
|
29
33
|
/**
|
|
30
34
|
* The number of downstream messages that have yet to be acked.
|
|
31
35
|
*/
|
|
@@ -43,6 +47,8 @@ export declare class Subscriber {
|
|
|
43
47
|
getStats(): {
|
|
44
48
|
processRate: number;
|
|
45
49
|
pending: number;
|
|
50
|
+
backlog: number;
|
|
51
|
+
backlogBytes: number;
|
|
46
52
|
};
|
|
47
53
|
supportsMessage(tag: ChangeTag): boolean;
|
|
48
54
|
fail(err?: unknown): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscriber.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/subscriber.ts"],"names":[],"mappings":"
|
|
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,SAAS,EAAE,iBAAiB,EAAC,MAAM,8BAA8B,CAAC;AAC/E,OAAO,EAAkB,KAAK,MAAM,EAAC,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAC;AAElD,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;CAC3C,CAAC;AAEF;;;;;;GAMG;AACH,qBAAa,UAAU;;IAErB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;gBAelB,eAAe,EAAE,MAAM,EACvB,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,YAAY,CAAC,MAAM,CAAC,EAChC,YAAY,EAAE,MAAM,MAAM,EAC1B,OAAO,GAAE,iBAAsB;IAejC,IAAI,SAAS,WAEZ;IAED,IAAI,KAAK,WAER;IAED,IAAI,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IA4B9C,UAAU,CAAC,MAAM,EAAE,MAAM;IAMzB,kEAAkE;IAC5D,OAAO,CAAC,MAAM,EAAE,iBAAiB;IAKvC;;;OAGG;IACH,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IA8D5B;;OAEG;IACH,IAAI,UAAU,WAEb;IAED;;;OAGG;IACH,IAAI,YAAY,WAEf;IAED;;;OAGG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,SAAK,GAAG,IAAI;IAQrD,QAAQ,IAAI;QACV,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;KACtB;IAuBD,eAAe,CAAC,GAAG,EAAE,SAAS;IAS9B,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO;IAIlB,KAAK,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,MAAM;CA0F1C"}
|