@rocicorp/zero 1.9.1-canary.1 → 1.9.1-canary.2
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/out/analyze-query/src/analyze-cli.js +2 -2
- package/out/analyze-query/src/analyze-cli.js.map +1 -1
- package/out/shared/src/promise-race.d.ts +14 -0
- package/out/shared/src/promise-race.d.ts.map +1 -1
- package/out/shared/src/promise-race.js +32 -1
- package/out/shared/src/promise-race.js.map +1 -1
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +5 -1
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +48 -12
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.d.ts +0 -13
- package/out/zero-cache/src/db/transaction-pool.d.ts.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.js +3 -69
- package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +3 -2
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/server/replicator.js +1 -1
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.d.ts +67 -77
- package/out/zero-cache/src/services/change-streamer/broadcast.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.js +84 -92
- package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +3 -3
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +2 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +7 -4
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/error-type-enum.d.ts +8 -0
- package/out/zero-cache/src/services/change-streamer/error-type-enum.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/forwarder.d.ts +13 -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 +42 -12
- package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.d.ts +34 -0
- package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js +149 -40
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.d.ts +22 -6
- package/out/zero-cache/src/services/change-streamer/subscriber.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.js +39 -2
- package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.d.ts +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.js +6 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.d.ts +1 -1
- package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.js +2 -2
- package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
- package/out/zero-cache/src/types/pg.d.ts.map +1 -1
- package/out/zero-cache/src/types/pg.js.map +1 -1
- package/out/zero-cache/src/types/streams.d.ts +4 -9
- package/out/zero-cache/src/types/streams.d.ts.map +1 -1
- package/out/zero-cache/src/types/streams.js.map +1 -1
- package/out/zero-cache/src/types/subscription.d.ts +2 -11
- package/out/zero-cache/src/types/subscription.d.ts.map +1 -1
- package/out/zero-cache/src/types/subscription.js +4 -25
- package/out/zero-cache/src/types/subscription.js.map +1 -1
- package/out/zero-cache/src/workers/connection.js +2 -2
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/package.json +1 -1
|
@@ -3,12 +3,24 @@ import type { WatermarkedChange } from './change-streamer-service.ts';
|
|
|
3
3
|
import type { Status } from './change-streamer.ts';
|
|
4
4
|
import type { Subscriber } from './subscriber.ts';
|
|
5
5
|
export type ProgressMonitorOptions = {
|
|
6
|
-
|
|
6
|
+
flowControlConsensusTimeoutProportion: number;
|
|
7
|
+
flowControlSlowSubscriberGracePeriodMs?: number | undefined;
|
|
8
|
+
setTimeoutFn?: typeof setTimeout | undefined;
|
|
9
|
+
clearTimeoutFn?: typeof clearTimeout | undefined;
|
|
7
10
|
};
|
|
8
11
|
export declare class Forwarder {
|
|
9
12
|
#private;
|
|
10
13
|
constructor(lc: LogContext, opts?: ProgressMonitorOptions);
|
|
11
14
|
startProgressMonitor(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Runs one progress-monitor tick: samples each active subscriber's change
|
|
17
|
+
* rate, logs slow broadcasts, and disconnects subscribers that have been
|
|
18
|
+
* continuously lagging for longer than the configured grace period.
|
|
19
|
+
*
|
|
20
|
+
* Invoked on an interval by {@link startProgressMonitor()}; exposed (rather
|
|
21
|
+
* than private) so tests can drive a single tick with an explicit `now`.
|
|
22
|
+
*/
|
|
23
|
+
checkSubscriberProgress(now: number): void;
|
|
12
24
|
stopProgressMonitor(): void;
|
|
13
25
|
/**
|
|
14
26
|
* `add()` is called in lock step with `Storer.catchup()` so that the
|
|
@@ -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;AAQjD,OAAO,KAAK,EAAY,iBAAiB,EAAC,MAAM,8BAA8B,CAAC;AAC/E,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,sBAAsB,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;AAEjD,OAAO,KAAK,EAAC,UAAU,EAAkB,MAAM,iBAAiB,CAAC;AAEjE,MAAM,MAAM,sBAAsB,GAAG;IACnC,qCAAqC,EAAE,MAAM,CAAC;IAC9C,sCAAsC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE5D,YAAY,CAAC,EAAE,OAAO,UAAU,GAAG,SAAS,CAAC;IAC7C,cAAc,CAAC,EAAE,OAAO,YAAY,GAAG,SAAS,CAAC;CAClD,CAAC;AAEF,qBAAa,SAAS;;gBAyBlB,EAAE,EAAE,UAAU,EACd,IAAI,GAAE,sBAEL;IA+DH,oBAAoB;IAQpB;;;;;;;OAOG;IACH,uBAAuB,CAAC,GAAG,EAAE,MAAM;IAyCnC,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;IAsErD,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC;CAQvB"}
|
|
@@ -4,17 +4,27 @@ import { Broadcast } from "./broadcast.js";
|
|
|
4
4
|
//#region ../zero-cache/src/services/change-streamer/forwarder.ts
|
|
5
5
|
var Forwarder = class {
|
|
6
6
|
#lc;
|
|
7
|
-
#
|
|
7
|
+
#setTimeout;
|
|
8
|
+
#clearTimeout;
|
|
8
9
|
#active = /* @__PURE__ */ new Set();
|
|
9
10
|
#queued = /* @__PURE__ */ new Set();
|
|
11
|
+
#earlyReleaseOptions;
|
|
12
|
+
#flowControlSlowSubscriberGracePeriodMs;
|
|
10
13
|
#flowControlWaits = getOrCreateCounter("replication", "flow_control.waits", "Flow-control checkpoints completed, labeled by release mode.");
|
|
11
14
|
#flowControlWaitTime = getOrCreateLatencyHistogram("replication", "flow_control.wait_duration", "Time replication waits at flow-control checkpoints.");
|
|
12
15
|
#inTransaction = false;
|
|
13
16
|
#currentBroadcast;
|
|
14
17
|
#progressMonitor;
|
|
15
|
-
constructor(lc, opts = {
|
|
16
|
-
this.#lc = lc.withContext("component", "progress-monitor");
|
|
17
|
-
this.#
|
|
18
|
+
constructor(lc, opts = { flowControlConsensusTimeoutProportion: 2 }) {
|
|
19
|
+
this.#lc = lc.withContext("component", "subscriber-progress-monitor");
|
|
20
|
+
this.#setTimeout = opts.setTimeoutFn ?? setTimeout;
|
|
21
|
+
this.#clearTimeout = opts.clearTimeoutFn ?? clearTimeout;
|
|
22
|
+
this.#earlyReleaseOptions = opts.flowControlConsensusTimeoutProportion < 0 ? void 0 : {
|
|
23
|
+
consensusTimeoutProportion: opts.flowControlConsensusTimeoutProportion,
|
|
24
|
+
setTimeoutFn: this.#setTimeout,
|
|
25
|
+
clearTimeoutFn: this.#clearTimeout
|
|
26
|
+
};
|
|
27
|
+
this.#flowControlSlowSubscriberGracePeriodMs = opts.flowControlSlowSubscriberGracePeriodMs;
|
|
18
28
|
getOrCreateGauge("replication", "flow_control.active_subscribers", "Active change-stream subscribers receiving live changes.").addCallback((result) => result.observe(this.#active.size));
|
|
19
29
|
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
30
|
getOrCreateGauge("replication", "flow_control.pending_messages", "Downstream change-stream messages not yet acked by subscribers.").addCallback((result) => result.observe(this.#subscriberStats().pendingMessages));
|
|
@@ -30,14 +40,34 @@ var Forwarder = class {
|
|
|
30
40
|
}
|
|
31
41
|
startProgressMonitor() {
|
|
32
42
|
clearInterval(this.#progressMonitor);
|
|
33
|
-
this.#progressMonitor = setInterval(this
|
|
43
|
+
this.#progressMonitor = setInterval(() => this.checkSubscriberProgress(performance.now()), 1e3);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Runs one progress-monitor tick: samples each active subscriber's change
|
|
47
|
+
* rate, logs slow broadcasts, and disconnects subscribers that have been
|
|
48
|
+
* continuously lagging for longer than the configured grace period.
|
|
49
|
+
*
|
|
50
|
+
* Invoked on an interval by {@link startProgressMonitor()}; exposed (rather
|
|
51
|
+
* than private) so tests can drive a single tick with an explicit `now`.
|
|
52
|
+
*/
|
|
53
|
+
checkSubscriberProgress(now) {
|
|
54
|
+
let timedOut;
|
|
55
|
+
let slowestOnTime;
|
|
56
|
+
for (const sub of this.#active) {
|
|
57
|
+
const stats = sub.sampleProcessRate(now).getStats();
|
|
58
|
+
if (stats.missedLastTimeout) (timedOut ??= /* @__PURE__ */ new Map()).set(sub, stats);
|
|
59
|
+
else if (stats.processRate <= (slowestOnTime?.processRate ?? Infinity)) slowestOnTime = stats;
|
|
60
|
+
}
|
|
61
|
+
this.#currentBroadcast?.logSlowRequests(now);
|
|
62
|
+
if (this.#flowControlSlowSubscriberGracePeriodMs && timedOut?.size && slowestOnTime) for (const [sub, stats] of timedOut.entries()) {
|
|
63
|
+
const rate = stats.processRate < slowestOnTime.processRate ? "lagging" : "catching-up";
|
|
64
|
+
const laggingDuration = sub.reportChangeRate(now, rate);
|
|
65
|
+
if (laggingDuration >= this.#flowControlSlowSubscriberGracePeriodMs) {
|
|
66
|
+
this.#lc.warn?.(`subscriber ${sub.id} has lagged for ${laggingDuration}ms. disconnecting`, { stats });
|
|
67
|
+
sub.close(3, `lagging for over ${this.#flowControlSlowSubscriberGracePeriodMs}ms (${laggingDuration}ms)`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
34
70
|
}
|
|
35
|
-
#trackProgress = () => {
|
|
36
|
-
const now = performance.now();
|
|
37
|
-
for (const sub of this.#active) sub.sampleProcessRate(now);
|
|
38
|
-
const { flowControlConsensusPaddingSeconds } = this.#progressMonitorOptions;
|
|
39
|
-
if (flowControlConsensusPaddingSeconds >= 0) this.#currentBroadcast?.checkProgress(this.#lc, flowControlConsensusPaddingSeconds * 1e3, now);
|
|
40
|
-
};
|
|
41
71
|
stopProgressMonitor() {
|
|
42
72
|
clearInterval(this.#progressMonitor);
|
|
43
73
|
}
|
|
@@ -77,7 +107,7 @@ var Forwarder = class {
|
|
|
77
107
|
*/
|
|
78
108
|
async forwardWithFlowControl(entry) {
|
|
79
109
|
const start = performance.now();
|
|
80
|
-
const broadcast = new Broadcast(this.#active.values(), entry);
|
|
110
|
+
const broadcast = new Broadcast(this.#lc, this.#active.values(), entry, this.#earlyReleaseOptions);
|
|
81
111
|
this.#updateActiveSubscribers(entry[1]);
|
|
82
112
|
this.#currentBroadcast = broadcast;
|
|
83
113
|
try {
|
|
@@ -1 +1 @@
|
|
|
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
|
+
{"version":3,"file":"forwarder.js","names":["#lc","#setTimeout","#clearTimeout","#active","#queued","#earlyReleaseOptions","#flowControlSlowSubscriberGracePeriodMs","#flowControlWaits","#flowControlWaitTime","#subscriberStats","#progressMonitor","#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, type EarlyReleaseOptions} from './broadcast.ts';\nimport type {ChangeTag, WatermarkedChange} from './change-streamer-service.ts';\nimport type {Status} from './change-streamer.ts';\nimport {StreamTooFarBehind} from './error-type-enum.ts';\nimport type {Subscriber, SubscriberStats} from './subscriber.ts';\n\nexport type ProgressMonitorOptions = {\n flowControlConsensusTimeoutProportion: number;\n flowControlSlowSubscriberGracePeriodMs?: number | undefined;\n // Injectable timers, for deterministic testing.\n setTimeoutFn?: typeof setTimeout | undefined;\n clearTimeoutFn?: typeof clearTimeout | undefined;\n};\n\nexport class Forwarder {\n readonly #lc: LogContext;\n readonly #setTimeout: typeof setTimeout;\n readonly #clearTimeout: typeof clearTimeout;\n readonly #active = new Set<Subscriber>();\n readonly #queued = new Set<Subscriber>();\n readonly #earlyReleaseOptions: EarlyReleaseOptions | undefined;\n readonly #flowControlSlowSubscriberGracePeriodMs: number | undefined;\n\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 = {\n flowControlConsensusTimeoutProportion: 2.0,\n },\n ) {\n this.#lc = lc.withContext('component', 'subscriber-progress-monitor');\n this.#setTimeout = opts.setTimeoutFn ?? setTimeout;\n this.#clearTimeout = opts.clearTimeoutFn ?? clearTimeout;\n\n this.#earlyReleaseOptions =\n opts.flowControlConsensusTimeoutProportion < 0\n ? undefined\n : {\n consensusTimeoutProportion:\n opts.flowControlConsensusTimeoutProportion,\n setTimeoutFn: this.#setTimeout,\n clearTimeoutFn: this.#clearTimeout,\n };\n this.#flowControlSlowSubscriberGracePeriodMs =\n opts.flowControlSlowSubscriberGracePeriodMs;\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(\n () => this.checkSubscriberProgress(performance.now()),\n 1000,\n );\n }\n\n /**\n * Runs one progress-monitor tick: samples each active subscriber's change\n * rate, logs slow broadcasts, and disconnects subscribers that have been\n * continuously lagging for longer than the configured grace period.\n *\n * Invoked on an interval by {@link startProgressMonitor()}; exposed (rather\n * than private) so tests can drive a single tick with an explicit `now`.\n */\n checkSubscriberProgress(now: number) {\n let timedOut: Map<Subscriber, SubscriberStats> | undefined;\n let slowestOnTime: SubscriberStats | undefined;\n\n for (const sub of this.#active) {\n const stats = sub.sampleProcessRate(now).getStats();\n if (stats.missedLastTimeout) {\n (timedOut ??= new Map()).set(sub, stats);\n } else if (\n stats.processRate <= (slowestOnTime?.processRate ?? Infinity)\n ) {\n slowestOnTime = stats;\n }\n }\n this.#currentBroadcast?.logSlowRequests(now);\n\n if (\n this.#flowControlSlowSubscriberGracePeriodMs &&\n timedOut?.size &&\n slowestOnTime\n ) {\n for (const [sub, stats] of timedOut.entries()) {\n const rate =\n stats.processRate < slowestOnTime.processRate\n ? 'lagging'\n : 'catching-up';\n const laggingDuration = sub.reportChangeRate(now, rate);\n if (laggingDuration >= this.#flowControlSlowSubscriberGracePeriodMs) {\n this.#lc.warn?.(\n `subscriber ${sub.id} has lagged for ${laggingDuration}ms. disconnecting`,\n {stats},\n );\n sub.close(\n StreamTooFarBehind,\n `lagging for over ${this.#flowControlSlowSubscriberGracePeriodMs}ms (${laggingDuration}ms)`,\n );\n }\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(\n this.#lc,\n this.#active.values(),\n entry,\n this.#earlyReleaseOptions,\n );\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,4 +1,5 @@
|
|
|
1
1
|
import type { LogContext } from '@rocicorp/logger';
|
|
2
|
+
import { type JSONObject } from '../../../../shared/src/bigint-json.ts';
|
|
2
3
|
import { TransactionPool } from '../../db/transaction-pool.ts';
|
|
3
4
|
import { type PostgresDB } from '../../types/pg.ts';
|
|
4
5
|
import { type ShardID } from '../../types/shards.ts';
|
|
@@ -100,4 +101,37 @@ export declare class PurgeLocker {
|
|
|
100
101
|
constructor(lc: LogContext, shard: ShardID, db: PostgresDB);
|
|
101
102
|
acquire(): Promise<PurgeLock | null>;
|
|
102
103
|
}
|
|
104
|
+
type TaskKey = JSONObject;
|
|
105
|
+
type TaskDoneFn = () => void;
|
|
106
|
+
/**
|
|
107
|
+
* Periodic monitor for detecting the pathological condition in which a db
|
|
108
|
+
* query hangs forever because of a connection problem, such as a half-closed
|
|
109
|
+
* socket.
|
|
110
|
+
*
|
|
111
|
+
* Use this for db calls that recur or accumulate progress within a single
|
|
112
|
+
* logical operation on the main storer loop (e.g. the queue-processing loop,
|
|
113
|
+
* or a multi-batch catchup read), where "no progress for N ms" is the
|
|
114
|
+
* meaningful signal. For a one-off db call -- including ones made before
|
|
115
|
+
* {@link Storer.run} starts this monitor's polling -- use a plain timeout
|
|
116
|
+
* (see `Storer.#withTimeout`) instead.
|
|
117
|
+
*
|
|
118
|
+
* The failure handler should stop the server. The ProgressMonitor will
|
|
119
|
+
* assume this and stop itself after calling it.
|
|
120
|
+
*
|
|
121
|
+
* Internal to the Storer, but exported for testing.
|
|
122
|
+
*/
|
|
123
|
+
export declare class ProgressMonitor {
|
|
124
|
+
#private;
|
|
125
|
+
constructor(lc: LogContext, failureThresholdMs: number, onFailure: (err: Error) => void);
|
|
126
|
+
start(): void;
|
|
127
|
+
checkTaskProgress(now: Date): void;
|
|
128
|
+
/**
|
|
129
|
+
* Starts a task to be tracked.
|
|
130
|
+
*
|
|
131
|
+
* @return a callback to invoke when the task is done.
|
|
132
|
+
*/
|
|
133
|
+
trackTask(task: TaskKey, now?: Date): TaskDoneFn;
|
|
134
|
+
stop(): void;
|
|
135
|
+
}
|
|
136
|
+
export {};
|
|
103
137
|
//# sourceMappingURL=storer.d.ts.map
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"storer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/storer.ts"],"names":[],"mappings":"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"}
|
|
@@ -66,6 +66,7 @@ var Storer = class {
|
|
|
66
66
|
#statementTimeoutMs;
|
|
67
67
|
#changeLogBatchSize;
|
|
68
68
|
#drainTimeoutMs;
|
|
69
|
+
#progressMonitor;
|
|
69
70
|
#approximateQueuedBytes = 0;
|
|
70
71
|
#running = false;
|
|
71
72
|
constructor(lc, shard, taskID, discoveryAddress, discoveryProtocol, db, replicaVersion, onConsumed, onFatal, { backPressureLimitHeapProportion, statementTimeoutMs, changeLogBatchSize, drainTimeoutMs = 3e4 }) {
|
|
@@ -81,6 +82,7 @@ var Storer = class {
|
|
|
81
82
|
this.#statementTimeoutMs = statementTimeoutMs;
|
|
82
83
|
this.#changeLogBatchSize = Math.max(1, changeLogBatchSize);
|
|
83
84
|
this.#drainTimeoutMs = drainTimeoutMs;
|
|
85
|
+
this.#progressMonitor = new ProgressMonitor(lc, statementTimeoutMs, onFatal);
|
|
84
86
|
const heapStats = getHeapStatistics();
|
|
85
87
|
this.#backPressureThresholdBytes = (heapStats.heap_size_limit - heapStats.used_heap_size) * backPressureLimitHeapProportion;
|
|
86
88
|
this.#lc.info?.(`Using up to ${(this.#backPressureThresholdBytes / 1024 ** 2).toFixed(2)} MB of --max-old-space-size (~${(heapStats.heap_size_limit / 1024 ** 2).toFixed(2)} MB) to absorb upstream spikes`, { heapStats });
|
|
@@ -88,6 +90,28 @@ var Storer = class {
|
|
|
88
90
|
#cdc(table) {
|
|
89
91
|
return this.#db(`${cdcSchema(this.#shard)}.${table}`);
|
|
90
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Bounds a one-off db call (i.e. not part of the main storer loop or the
|
|
95
|
+
* background catchup read, which are tracked by the ProgressMonitor
|
|
96
|
+
* instead) with a plain timeout. This covers calls like
|
|
97
|
+
* {@link assumeOwnership} and {@link getStartStreamInitializationParameters}
|
|
98
|
+
* that are made by the caller *before* {@link run()} -- and thus before the
|
|
99
|
+
* ProgressMonitor's polling starts -- as well as ones made well after,
|
|
100
|
+
* where a continuously-polling watchdog would be overkill for a single
|
|
101
|
+
* bounded db round trip.
|
|
102
|
+
*
|
|
103
|
+
* Does not cancel `promise` on timeout (there is no cancellation mechanism
|
|
104
|
+
* for a postgres.js query); it just stops waiting for it, and swallows a
|
|
105
|
+
* later rejection so it doesn't surface as an unhandled rejection.
|
|
106
|
+
*/
|
|
107
|
+
async #withTimeout(name, promise) {
|
|
108
|
+
const result = await orTimeout(promise, this.#statementTimeoutMs);
|
|
109
|
+
if (result === "timed-out") {
|
|
110
|
+
promise.catch(() => {});
|
|
111
|
+
throw new AbortError(`${name} did not complete within ${this.#statementTimeoutMs}ms`);
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
91
115
|
async assumeOwnership(purgeLock) {
|
|
92
116
|
const db = this.#db;
|
|
93
117
|
const owner = this.#taskID;
|
|
@@ -96,16 +120,16 @@ var Storer = class {
|
|
|
96
120
|
const addressWithProtocol = ownerProtocol === "ws" ? ownerAddress : `${ownerProtocol}://${ownerAddress}`;
|
|
97
121
|
this.#lc.info?.(`assuming ownership at ${addressWithProtocol}`);
|
|
98
122
|
const start = performance.now();
|
|
99
|
-
await db`UPDATE ${this.#cdc("replicationState")} SET ${db({
|
|
123
|
+
await this.#withTimeout("assume-ownership", db`UPDATE ${this.#cdc("replicationState")} SET ${db({
|
|
100
124
|
owner,
|
|
101
125
|
ownerAddress: addressWithProtocol
|
|
102
|
-
})}
|
|
126
|
+
})}`);
|
|
103
127
|
const elapsed = (performance.now() - start).toFixed(2);
|
|
104
128
|
this.#lc.info?.(`assumed ownership at ${addressWithProtocol} (${elapsed} ms)`);
|
|
105
129
|
if (purgeLock) purgeLock.release();
|
|
106
130
|
}
|
|
107
131
|
async getStartStreamInitializationParameters() {
|
|
108
|
-
const [[{ lastWatermark }], result] = await runTx(this.#db, (sql) => [sql`
|
|
132
|
+
const [[{ lastWatermark }], result] = await this.#withTimeout("get-stream-params", runTx(this.#db, (sql) => [sql`
|
|
109
133
|
SELECT "lastWatermark" FROM ${this.#cdc("replicationState")}`, sql`
|
|
110
134
|
SELECT
|
|
111
135
|
json_build_object(
|
|
@@ -119,15 +143,15 @@ var Storer = class {
|
|
|
119
143
|
LEFT JOIN ${this.#cdc("tableMetadata")} as t
|
|
120
144
|
ON (b."schema" = t."schema" AND b."table" = t."table")
|
|
121
145
|
GROUP BY b."schema", b."table", t."metadata"
|
|
122
|
-
`], { mode: READONLY });
|
|
146
|
+
`], { mode: READONLY }));
|
|
123
147
|
return {
|
|
124
148
|
lastWatermark,
|
|
125
149
|
backfillRequests: parse(result, backfillRequestsSchema)
|
|
126
150
|
};
|
|
127
151
|
}
|
|
128
152
|
async getMinWatermarkForCatchup() {
|
|
129
|
-
const [{ minWatermark }] = await this.#db`
|
|
130
|
-
SELECT min(watermark) as "minWatermark" FROM ${this.#cdc("changeLog")}
|
|
153
|
+
const [{ minWatermark }] = await this.#withTimeout("get-min-watermark", this.#db`
|
|
154
|
+
SELECT min(watermark) as "minWatermark" FROM ${this.#cdc("changeLog")}`);
|
|
131
155
|
return minWatermark;
|
|
132
156
|
}
|
|
133
157
|
purgeRecordsBefore(watermark) {
|
|
@@ -186,30 +210,22 @@ var Storer = class {
|
|
|
186
210
|
}]);
|
|
187
211
|
}
|
|
188
212
|
#readyForMore = null;
|
|
189
|
-
#backpressureTimeout;
|
|
190
213
|
readyForMore() {
|
|
191
214
|
if (!this.#running) return;
|
|
192
215
|
if (this.#readyForMore === null && this.#approximateQueuedBytes > this.#backPressureThresholdBytes) {
|
|
193
216
|
this.#lc.warn?.(`applying back pressure with ${this.#queue.size()} queued changes (~${(this.#approximateQueuedBytes / 1024 ** 2).toFixed(2)} MB)\n\nTo inspect changeLog backlog in your change DB:\n SELECT\n (change->'relation'->>'schema') || '.' || (change->'relation'->>'name') AS table_name,\n change->>'tag' AS operation,\n COUNT(*) AS count\n FROM "<app_id>/cdc"."changeLog"\n GROUP BY 1, 2\n ORDER BY 3 DESC\n LIMIT 20;`);
|
|
194
217
|
this.#readyForMore = resolver();
|
|
195
|
-
this.#resetBackpressureTimeout(true);
|
|
196
218
|
}
|
|
197
219
|
return this.#readyForMore?.promise;
|
|
198
220
|
}
|
|
199
|
-
#resetBackpressureTimeout(reschedule) {
|
|
200
|
-
clearTimeout(this.#backpressureTimeout);
|
|
201
|
-
this.#backpressureTimeout = reschedule ? setTimeout(() => {
|
|
202
|
-
this.#readyForMore?.reject(new AbortError(`No statement processed within the last ${this.#statementTimeoutMs}ms. Considering the change-log to be wedged.`));
|
|
203
|
-
this.#readyForMore = null;
|
|
204
|
-
}, this.#statementTimeoutMs) : void 0;
|
|
205
|
-
}
|
|
206
221
|
#maybeReleaseBackPressure() {
|
|
207
|
-
if (this.#readyForMore !== null)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
222
|
+
if (this.#readyForMore !== null) {
|
|
223
|
+
if (this.#approximateQueuedBytes < this.#backPressureThresholdBytes * .8) {
|
|
224
|
+
this.#lc.info?.(`releasing back pressure with ${this.#queue.size()} queued changes (~${(this.#approximateQueuedBytes / 1024 ** 2).toFixed(2)} MB)`);
|
|
225
|
+
this.#readyForMore.resolve();
|
|
226
|
+
this.#readyForMore = null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
213
229
|
}
|
|
214
230
|
/**
|
|
215
231
|
* Flushes any buffered {@link PendingTransaction.batch} rows to the changeLog.
|
|
@@ -248,6 +264,7 @@ var Storer = class {
|
|
|
248
264
|
this.#running = true;
|
|
249
265
|
this.#stopped = stopped;
|
|
250
266
|
this.#lc.info?.("starting storer");
|
|
267
|
+
this.#progressMonitor.start();
|
|
251
268
|
let err;
|
|
252
269
|
try {
|
|
253
270
|
await this.#processQueue();
|
|
@@ -259,7 +276,6 @@ var Storer = class {
|
|
|
259
276
|
this.#readyForMore.resolve();
|
|
260
277
|
this.#readyForMore = null;
|
|
261
278
|
}
|
|
262
|
-
this.#resetBackpressureTimeout(false);
|
|
263
279
|
this.#cancelQueueEntries(this.#queue.drain().filter((entry) => entry !== void 0), err);
|
|
264
280
|
this.#running = false;
|
|
265
281
|
signalStopped();
|
|
@@ -285,9 +301,20 @@ var Storer = class {
|
|
|
285
301
|
async #processQueue() {
|
|
286
302
|
let tx = null;
|
|
287
303
|
let msg;
|
|
304
|
+
let lastTaskDone;
|
|
305
|
+
const nextMessage = async () => {
|
|
306
|
+
lastTaskDone?.();
|
|
307
|
+
lastTaskDone = void 0;
|
|
308
|
+
const entry = await this.#queue.dequeue();
|
|
309
|
+
if (entry !== "stop") lastTaskDone = this.#progressMonitor.trackTask({
|
|
310
|
+
task: "queue-entry",
|
|
311
|
+
type: entry[0]
|
|
312
|
+
});
|
|
313
|
+
return entry;
|
|
314
|
+
};
|
|
288
315
|
const catchupQueue = [];
|
|
289
316
|
try {
|
|
290
|
-
while ((msg = await
|
|
317
|
+
while ((msg = await nextMessage()) !== "stop") {
|
|
291
318
|
const [msgType] = msg;
|
|
292
319
|
switch (msgType) {
|
|
293
320
|
case "ready": {
|
|
@@ -320,10 +347,7 @@ var Storer = class {
|
|
|
320
347
|
const { promise, resolve, reject } = resolver();
|
|
321
348
|
promise.catch(() => {});
|
|
322
349
|
tx = {
|
|
323
|
-
pool: new TransactionPool(this.#lc.withContext("watermark", watermark), {
|
|
324
|
-
mode: READ_COMMITTED,
|
|
325
|
-
statementResponseTimeout: this.#statementTimeoutMs
|
|
326
|
-
}),
|
|
350
|
+
pool: new TransactionPool(this.#lc.withContext("watermark", watermark), { mode: READ_COMMITTED }),
|
|
327
351
|
preCommitWatermark: watermark,
|
|
328
352
|
pos: 0,
|
|
329
353
|
startingReplicationState: promise,
|
|
@@ -394,6 +418,10 @@ var Storer = class {
|
|
|
394
418
|
const reader = new TransactionPool(this.#lc.withContext("pool", "catchup"), { mode: READONLY });
|
|
395
419
|
reader.run(this.#db);
|
|
396
420
|
let lastWatermark;
|
|
421
|
+
const catchupSnapshotted = this.#progressMonitor.trackTask({
|
|
422
|
+
task: "capture-catchup-snapshot",
|
|
423
|
+
subscribers: subs.map(({ subscriber: s }) => s.id)
|
|
424
|
+
});
|
|
397
425
|
try {
|
|
398
426
|
[{lastWatermark}] = await reader.processReadTask((sql) => sql`
|
|
399
427
|
SELECT "lastWatermark" FROM ${this.#cdc("replicationState")}
|
|
@@ -401,10 +429,18 @@ var Storer = class {
|
|
|
401
429
|
} catch (e) {
|
|
402
430
|
subs.map(({ subscriber }) => subscriber.fail(e));
|
|
403
431
|
throw e;
|
|
432
|
+
} finally {
|
|
433
|
+
catchupSnapshotted();
|
|
404
434
|
}
|
|
405
435
|
Promise.all(subs.map((sub) => this.#catchup(sub, lastWatermark, reader))).finally(() => reader.setDone());
|
|
406
436
|
}
|
|
407
437
|
async #catchup({ subscriber: sub, mode }, lastWatermark, reader) {
|
|
438
|
+
let entriesReceived = 0;
|
|
439
|
+
let catchupProgressed = this.#progressMonitor.trackTask({
|
|
440
|
+
task: "catchup",
|
|
441
|
+
subscriber: sub.id,
|
|
442
|
+
entriesReceived
|
|
443
|
+
});
|
|
408
444
|
try {
|
|
409
445
|
await reader.processReadTask(async (tx) => {
|
|
410
446
|
const start = Date.now();
|
|
@@ -416,21 +452,32 @@ var Storer = class {
|
|
|
416
452
|
WHERE watermark >= ${sub.watermark}
|
|
417
453
|
AND watermark <= ${lastWatermark}
|
|
418
454
|
ORDER BY watermark, pos`.cursor(2e3)) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
lastBatchConsumed
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
455
|
+
catchupProgressed();
|
|
456
|
+
entriesReceived += entries.length;
|
|
457
|
+
try {
|
|
458
|
+
const start = performance.now();
|
|
459
|
+
await lastBatchConsumed;
|
|
460
|
+
const elapsed = performance.now() - start;
|
|
461
|
+
if (lastBatchConsumed) this.#lc[elapsed > 100 ? "info" : "debug"]?.(`waited ${elapsed.toFixed(3)} ms for ${sub.id} to consume last batch of catchup entries`);
|
|
462
|
+
for (const entry of entries) if (entry.watermark === sub.watermark) watermarkFound = true;
|
|
463
|
+
else if (watermarkFound) {
|
|
464
|
+
lastBatchConsumed = sub.catchup(toDownstream(entry));
|
|
465
|
+
count++;
|
|
466
|
+
} else if (mode === "backup") throw new AutoResetSignal(`backup replica at watermark ${sub.watermark} is behind change db: ${entry.watermark})`);
|
|
467
|
+
else {
|
|
468
|
+
this.#lc.warn?.(`rejecting subscriber at watermark ${sub.watermark} (earliest watermark: ${entry.watermark})`);
|
|
469
|
+
sub.close(2, `earliest supported watermark is ${entry.watermark} (requested ${sub.watermark})`);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
} finally {
|
|
473
|
+
catchupProgressed = this.#progressMonitor.trackTask({
|
|
474
|
+
task: "catchup",
|
|
475
|
+
subscriber: sub.id,
|
|
476
|
+
entriesReceived
|
|
477
|
+
});
|
|
432
478
|
}
|
|
433
479
|
}
|
|
480
|
+
catchupProgressed();
|
|
434
481
|
if (watermarkFound) {
|
|
435
482
|
await lastBatchConsumed;
|
|
436
483
|
this.#lc.info?.(`caught up ${sub.id} with ${count} changes (${Date.now() - start} ms)`);
|
|
@@ -444,6 +491,8 @@ var Storer = class {
|
|
|
444
491
|
this.#onFatal(err);
|
|
445
492
|
}
|
|
446
493
|
sub.fail(err);
|
|
494
|
+
} finally {
|
|
495
|
+
catchupProgressed();
|
|
447
496
|
}
|
|
448
497
|
}
|
|
449
498
|
/**
|
|
@@ -554,6 +603,7 @@ var Storer = class {
|
|
|
554
603
|
*/
|
|
555
604
|
async stop() {
|
|
556
605
|
if (this.#running) {
|
|
606
|
+
this.#progressMonitor.stop();
|
|
557
607
|
this.#lc.info?.(`draining ${this.#queue.size()} changeLog entries`);
|
|
558
608
|
this.#queue.enqueue("stop");
|
|
559
609
|
}
|
|
@@ -653,6 +703,65 @@ var PurgeLocker = class {
|
|
|
653
703
|
return new PurgeLock(this.#lc, tx, replicaVersion, watermark);
|
|
654
704
|
}
|
|
655
705
|
};
|
|
706
|
+
/**
|
|
707
|
+
* Periodic monitor for detecting the pathological condition in which a db
|
|
708
|
+
* query hangs forever because of a connection problem, such as a half-closed
|
|
709
|
+
* socket.
|
|
710
|
+
*
|
|
711
|
+
* Use this for db calls that recur or accumulate progress within a single
|
|
712
|
+
* logical operation on the main storer loop (e.g. the queue-processing loop,
|
|
713
|
+
* or a multi-batch catchup read), where "no progress for N ms" is the
|
|
714
|
+
* meaningful signal. For a one-off db call -- including ones made before
|
|
715
|
+
* {@link Storer.run} starts this monitor's polling -- use a plain timeout
|
|
716
|
+
* (see `Storer.#withTimeout`) instead.
|
|
717
|
+
*
|
|
718
|
+
* The failure handler should stop the server. The ProgressMonitor will
|
|
719
|
+
* assume this and stop itself after calling it.
|
|
720
|
+
*
|
|
721
|
+
* Internal to the Storer, but exported for testing.
|
|
722
|
+
*/
|
|
723
|
+
var ProgressMonitor = class {
|
|
724
|
+
#lc;
|
|
725
|
+
#failureThresholdMs;
|
|
726
|
+
#onFailure;
|
|
727
|
+
#taskUpdates = /* @__PURE__ */ new Map();
|
|
728
|
+
#timer;
|
|
729
|
+
constructor(lc, failureThresholdMs, onFailure) {
|
|
730
|
+
this.#lc = lc.withContext("component", "storer-progress-monitor");
|
|
731
|
+
this.#failureThresholdMs = failureThresholdMs;
|
|
732
|
+
this.#onFailure = onFailure;
|
|
733
|
+
}
|
|
734
|
+
start() {
|
|
735
|
+
clearInterval(this.#timer);
|
|
736
|
+
this.#timer = setInterval(() => this.checkTaskProgress(/* @__PURE__ */ new Date()), this.#failureThresholdMs).unref();
|
|
737
|
+
}
|
|
738
|
+
checkTaskProgress(now) {
|
|
739
|
+
for (const [task, lastUpdate] of this.#taskUpdates.entries()) if (now.getTime() - lastUpdate.getTime() >= this.#failureThresholdMs) {
|
|
740
|
+
this.#lc.error?.(`Last task update was more than ${this.#failureThresholdMs}ms ago`, {
|
|
741
|
+
task,
|
|
742
|
+
lastUpdate
|
|
743
|
+
});
|
|
744
|
+
this.#onFailure(/* @__PURE__ */ new Error(`Task failed to progress for over ${this.#failureThresholdMs}ms: ${BigIntJSON.stringify(task)}`));
|
|
745
|
+
this.stop();
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Starts a task to be tracked.
|
|
751
|
+
*
|
|
752
|
+
* @return a callback to invoke when the task is done.
|
|
753
|
+
*/
|
|
754
|
+
trackTask(task, now = /* @__PURE__ */ new Date()) {
|
|
755
|
+
this.#taskUpdates.set(task, now);
|
|
756
|
+
return () => {
|
|
757
|
+
this.#taskUpdates.delete(task);
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
stop() {
|
|
761
|
+
clearInterval(this.#timer);
|
|
762
|
+
this.#taskUpdates.clear();
|
|
763
|
+
}
|
|
764
|
+
};
|
|
656
765
|
//#endregion
|
|
657
766
|
export { PurgeLocker, Storer };
|
|
658
767
|
|