@rocicorp/zero 1.9.0-canary.1 → 1.9.0-canary.10
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/zero/package.js +2 -2
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +9 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +26 -0
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/db/delete-lite-db.js +2 -1
- package/out/zero-cache/src/db/delete-lite-db.js.map +1 -1
- package/out/zero-cache/src/db/sqlite-corruption.d.ts +3 -2
- package/out/zero-cache/src/db/sqlite-corruption.d.ts.map +1 -1
- package/out/zero-cache/src/db/sqlite-corruption.js +13 -8
- package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -1
- package/out/zero-cache/src/observability/metrics.d.ts +1 -1
- package/out/zero-cache/src/observability/metrics.d.ts.map +1 -1
- package/out/zero-cache/src/observability/metrics.js +5 -1
- package/out/zero-cache/src/observability/metrics.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 +4 -4
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/main.d.ts.map +1 -1
- package/out/zero-cache/src/server/main.js +1 -1
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/server/replicator.js +2 -2
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/server/syncer.js +1 -1
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +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 +10 -5
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts +45 -2
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +58 -7
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts +1 -0
- package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/data.js +4 -1
- package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts +3 -0
- package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/status.d.ts +1 -0
- package/out/zero-cache/src/services/change-source/protocol/current/status.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts +1 -0
- package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +4 -4
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +1 -0
- package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.d.ts +7 -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 +24 -5
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +1 -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 +68 -5
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-reader.js +2 -2
- package/out/zero-cache/src/services/replicator/change-processor.d.ts +6 -0
- package/out/zero-cache/src/services/replicator/change-processor.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.js +25 -7
- package/out/zero-cache/src/services/replicator/change-processor.js.map +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 +8 -2
- package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
- package/out/zero-cache/src/services/replicator/notifier.d.ts +0 -22
- package/out/zero-cache/src/services/replicator/notifier.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/notifier.js +7 -1
- package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.d.ts +16 -1
- package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.js +7 -1
- package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts +27 -0
- package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.js +32 -0
- package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/report-schema.d.ts +1 -1
- package/out/zero-cache/src/services/replicator/write-worker.js +14 -6
- package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.d.ts +44 -0
- package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js +59 -0
- package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js.map +1 -0
- 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 +18 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/streams.d.ts +11 -0
- 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 +11 -0
- package/out/zero-cache/src/types/subscription.d.ts.map +1 -1
- package/out/zero-cache/src/types/subscription.js +27 -0
- package/out/zero-cache/src/types/subscription.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +31 -16
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zqlite/src/db.js +2 -2
- package/out/zqlite/src/sqlite-cost-model.js +5 -5
- package/out/zqlite/src/table-source.js +3 -3
- package/package.json +2 -2
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
import { type Result, Subscription } from '../../types/subscription.ts';
|
|
2
2
|
import type { ReplicaState, ReplicaStateNotifier } from './replicator.ts';
|
|
3
|
-
/**
|
|
4
|
-
* Handles the semantics of {@link ReplicatorVersionNotifier.subscribe()}
|
|
5
|
-
* notifications, namely:
|
|
6
|
-
*
|
|
7
|
-
* * New subscribers are notified immediately with the latest received
|
|
8
|
-
* ReplicaState.
|
|
9
|
-
*
|
|
10
|
-
* * Non-latest notifications are discarded if the subscriber is too
|
|
11
|
-
* busy to consume them.
|
|
12
|
-
*
|
|
13
|
-
* By encapsulating the state for the first behavior (essentially, whether
|
|
14
|
-
* the first notification has been sent by the Replicator), Notifier objects
|
|
15
|
-
* can be chained to simplify fanout from Replicator to View Syncers.
|
|
16
|
-
*
|
|
17
|
-
* In particular, each Syncer Thread can manage a single Subscription to
|
|
18
|
-
* the Replicator across a MessageChannel, which it uses for its own Notifier
|
|
19
|
-
* instance to manage subscriptions from View Syncers within its thread. This
|
|
20
|
-
* way a Replicator only deals with sending notifications to a bounded set
|
|
21
|
-
* of MessageChannel-based subscribers (Syncer Threads), while the dynamic
|
|
22
|
-
* subscribe and unsubscribe traffic from View Syncers remains within each
|
|
23
|
-
* Syncer Thread.
|
|
24
|
-
*/
|
|
25
3
|
export declare class Notifier implements ReplicaStateNotifier {
|
|
26
4
|
#private;
|
|
27
5
|
get latestState(): ReplicaState | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifier.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/replicator/notifier.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,MAAM,EACX,YAAY,EACb,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAC,YAAY,EAAE,oBAAoB,EAAC,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"notifier.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/replicator/notifier.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,MAAM,EACX,YAAY,EACb,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAC,YAAY,EAAE,oBAAoB,EAAC,MAAM,iBAAiB,CAAC;AAqCxE,qBAAa,QAAS,YAAW,oBAAoB;;IAInD,IAAI,WAAW,IAAI,YAAY,GAAG,SAAS,CAE1C;IAyBD,SAAS,IAAI,YAAY,CAAC,YAAY,CAAC;IAWvC,iBAAiB,CACf,KAAK,GAAE,YAAuC,GAC7C,OAAO,CAAC,MAAM,CAAC,EAAE;CAOrB"}
|
|
@@ -23,6 +23,11 @@ import { EventEmitter } from "eventemitter3";
|
|
|
23
23
|
* subscribe and unsubscribe traffic from View Syncers remains within each
|
|
24
24
|
* Syncer Thread.
|
|
25
25
|
*/
|
|
26
|
+
function oldest(curr, prev) {
|
|
27
|
+
if (curr === void 0) return prev;
|
|
28
|
+
if (prev === void 0) return curr;
|
|
29
|
+
return Math.min(curr, prev);
|
|
30
|
+
}
|
|
26
31
|
var Notifier = class {
|
|
27
32
|
#eventEmitter = new EventEmitter();
|
|
28
33
|
#lastStateReceived;
|
|
@@ -34,7 +39,8 @@ var Notifier = class {
|
|
|
34
39
|
const subscription = Subscription.create({
|
|
35
40
|
coalesce: (curr, prev) => ({
|
|
36
41
|
...curr,
|
|
37
|
-
replicaReadyTimeMs:
|
|
42
|
+
replicaReadyTimeMs: oldest(curr.replicaReadyTimeMs, prev.replicaReadyTimeMs),
|
|
43
|
+
upstreamCommitTimeMs: oldest(curr.upstreamCommitTimeMs, prev.upstreamCommitTimeMs)
|
|
38
44
|
}),
|
|
39
45
|
cleanup: () => this.#eventEmitter.off("version", notify)
|
|
40
46
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifier.js","names":["#eventEmitter","#lastStateReceived","#newSubscription"],"sources":["../../../../../../zero-cache/src/services/replicator/notifier.ts"],"sourcesContent":["import {EventEmitter} from 'eventemitter3';\nimport {\n type PendingResult,\n type Result,\n Subscription,\n} from '../../types/subscription.ts';\nimport type {ReplicaState, ReplicaStateNotifier} from './replicator.ts';\n\n/**\n * Handles the semantics of {@link ReplicatorVersionNotifier.subscribe()}\n * notifications, namely:\n *\n * * New subscribers are notified immediately with the latest received\n * ReplicaState.\n *\n * * Non-latest notifications are discarded if the subscriber is too\n * busy to consume them.\n *\n * By encapsulating the state for the first behavior (essentially, whether\n * the first notification has been sent by the Replicator), Notifier objects\n * can be chained to simplify fanout from Replicator to View Syncers.\n *\n * In particular, each Syncer Thread can manage a single Subscription to\n * the Replicator across a MessageChannel, which it uses for its own Notifier\n * instance to manage subscriptions from View Syncers within its thread. This\n * way a Replicator only deals with sending notifications to a bounded set\n * of MessageChannel-based subscribers (Syncer Threads), while the dynamic\n * subscribe and unsubscribe traffic from View Syncers remains within each\n * Syncer Thread.\n */\nexport class Notifier implements ReplicaStateNotifier {\n readonly #eventEmitter = new EventEmitter();\n #lastStateReceived: ReplicaState | undefined;\n\n get latestState(): ReplicaState | undefined {\n return this.#lastStateReceived;\n }\n\n #newSubscription() {\n const notify = (state: ReplicaState) => subscription.push(state);\n const subscription = Subscription.create<ReplicaState>({\n coalesce: (curr, prev) => ({\n ...curr,\n replicaReadyTimeMs
|
|
1
|
+
{"version":3,"file":"notifier.js","names":["#eventEmitter","#lastStateReceived","#newSubscription"],"sources":["../../../../../../zero-cache/src/services/replicator/notifier.ts"],"sourcesContent":["import {EventEmitter} from 'eventemitter3';\nimport {\n type PendingResult,\n type Result,\n Subscription,\n} from '../../types/subscription.ts';\nimport type {ReplicaState, ReplicaStateNotifier} from './replicator.ts';\n\n/**\n * Handles the semantics of {@link ReplicatorVersionNotifier.subscribe()}\n * notifications, namely:\n *\n * * New subscribers are notified immediately with the latest received\n * ReplicaState.\n *\n * * Non-latest notifications are discarded if the subscriber is too\n * busy to consume them.\n *\n * By encapsulating the state for the first behavior (essentially, whether\n * the first notification has been sent by the Replicator), Notifier objects\n * can be chained to simplify fanout from Replicator to View Syncers.\n *\n * In particular, each Syncer Thread can manage a single Subscription to\n * the Replicator across a MessageChannel, which it uses for its own Notifier\n * instance to manage subscriptions from View Syncers within its thread. This\n * way a Replicator only deals with sending notifications to a bounded set\n * of MessageChannel-based subscribers (Syncer Threads), while the dynamic\n * subscribe and unsubscribe traffic from View Syncers remains within each\n * Syncer Thread.\n */\nfunction oldest(\n curr: number | undefined,\n prev: number | undefined,\n): number | undefined {\n if (curr === undefined) {\n return prev;\n }\n if (prev === undefined) {\n return curr;\n }\n return Math.min(curr, prev);\n}\n\nexport class Notifier implements ReplicaStateNotifier {\n readonly #eventEmitter = new EventEmitter();\n #lastStateReceived: ReplicaState | undefined;\n\n get latestState(): ReplicaState | undefined {\n return this.#lastStateReceived;\n }\n\n #newSubscription() {\n const notify = (state: ReplicaState) => subscription.push(state);\n const subscription = Subscription.create<ReplicaState>({\n // A coalesced notification stands in for every notification it\n // subsumed, so both timestamps keep the *oldest* value: they measure how\n // long work has been outstanding, and the subsumed watermarks are still\n // unserved.\n coalesce: (curr, prev) => ({\n ...curr,\n replicaReadyTimeMs: oldest(\n curr.replicaReadyTimeMs,\n prev.replicaReadyTimeMs,\n ),\n upstreamCommitTimeMs: oldest(\n curr.upstreamCommitTimeMs,\n prev.upstreamCommitTimeMs,\n ),\n }),\n cleanup: () => this.#eventEmitter.off('version', notify),\n });\n return {notify, subscription};\n }\n\n subscribe(): Subscription<ReplicaState> {\n const {notify, subscription} = this.#newSubscription();\n this.#eventEmitter.on('version', notify);\n if (this.#lastStateReceived) {\n // Per Replicator.subscribe() semantics, the current state of the\n // replica, if known, is immediately sent on subscribe.\n notify(this.#lastStateReceived);\n }\n return subscription;\n }\n\n notifySubscribers(\n state: ReplicaState = {state: 'version-ready'},\n ): Promise<Result>[] {\n this.#lastStateReceived = state;\n return this.#eventEmitter\n .listeners('version')\n .map(notify => notify(state) as unknown as PendingResult)\n .map(pending => pending.result);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAS,OACP,MACA,MACoB;CACpB,IAAI,SAAS,KAAA,GACX,OAAO;CAET,IAAI,SAAS,KAAA,GACX,OAAO;CAET,OAAO,KAAK,IAAI,MAAM,IAAI;AAC5B;AAEA,IAAa,WAAb,MAAsD;CACpD,gBAAyB,IAAI,aAAa;CAC1C;CAEA,IAAI,cAAwC;EAC1C,OAAO,KAAKC;CACd;CAEA,mBAAmB;EACjB,MAAM,UAAU,UAAwB,aAAa,KAAK,KAAK;EAC/D,MAAM,eAAe,aAAa,OAAqB;GAKrD,WAAW,MAAM,UAAU;IACzB,GAAG;IACH,oBAAoB,OAClB,KAAK,oBACL,KAAK,kBACP;IACA,sBAAsB,OACpB,KAAK,sBACL,KAAK,oBACP;GACF;GACA,eAAe,KAAKD,cAAc,IAAI,WAAW,MAAM;EACzD,CAAC;EACD,OAAO;GAAC;GAAQ;EAAY;CAC9B;CAEA,YAAwC;EACtC,MAAM,EAAC,QAAQ,iBAAgB,KAAKE,iBAAiB;EACrD,KAAKF,cAAc,GAAG,WAAW,MAAM;EACvC,IAAI,KAAKC,oBAGP,OAAO,KAAKA,kBAAkB;EAEhC,OAAO;CACT;CAEA,kBACE,QAAsB,EAAC,OAAO,gBAAe,GAC1B;EACnB,KAAKA,qBAAqB;EAC1B,OAAO,KAAKD,cACT,UAAU,SAAS,EACnB,KAAI,WAAU,OAAO,KAAK,CAA6B,EACvD,KAAI,YAAW,QAAQ,MAAM;CAClC;AACF"}
|
|
@@ -3,7 +3,7 @@ import type { ReadonlyJSONObject } from '../../../../shared/src/json.ts';
|
|
|
3
3
|
import type { Source } from '../../types/streams.ts';
|
|
4
4
|
import type { ChangeStreamer } from '../change-streamer/change-streamer.ts';
|
|
5
5
|
import type { Service } from '../service.ts';
|
|
6
|
-
import type
|
|
6
|
+
import { type ReplicationStatusPublisher } from './replication-status.ts';
|
|
7
7
|
import type { WriteWorkerClient } from './write-worker-client.ts';
|
|
8
8
|
/** See {@link ReplicaStateNotifier.subscribe()}. */
|
|
9
9
|
export type ReplicaState = {
|
|
@@ -18,6 +18,21 @@ export type ReplicaState = {
|
|
|
18
18
|
* Millisecond epoch when `watermark` became ready to read from the replica.
|
|
19
19
|
*/
|
|
20
20
|
readonly replicaReadyTimeMs?: number | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Millisecond epoch at which the transaction behind `watermark` committed
|
|
23
|
+
* *upstream*, as reported by the ChangeSource. Unlike `replicaReadyTimeMs`,
|
|
24
|
+
* which is stamped locally when the change lands in the replica, this is the
|
|
25
|
+
* origin of the whole pipeline, so `now - upstreamCommitTimeMs` at the point
|
|
26
|
+
* the change is poked to clients is the end-to-end serving lag.
|
|
27
|
+
*
|
|
28
|
+
* Note that this crosses a clock domain: it comes from the upstream
|
|
29
|
+
* database, whereas it is subtracted from the local clock in the ViewSyncer.
|
|
30
|
+
* The existing `zero.replication.total_lag` metric spans the same boundary.
|
|
31
|
+
*
|
|
32
|
+
* Undefined when the ChangeSource does not report a commit time, and for the
|
|
33
|
+
* initial "current state is ready" notification.
|
|
34
|
+
*/
|
|
35
|
+
readonly upstreamCommitTimeMs?: number | undefined;
|
|
21
36
|
readonly testSeqNum?: number | undefined;
|
|
22
37
|
};
|
|
23
38
|
export interface ReplicaStateNotifier {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replicator.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/replicator/replicator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,gCAAgC,CAAC;AACvE,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,wBAAwB,CAAC;AACnD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,eAAe,CAAC;AAE3C,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"replicator.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/replicator/replicator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,gCAAgC,CAAC;AACvE,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,wBAAwB,CAAC;AACnD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,eAAe,CAAC;AAE3C,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,0BAA0B,CAAC;AAEhE,oDAAoD;AACpD,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC;;OAEG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEjD;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAInD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,SAAS,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD;;;;OAIG;IACH,MAAM,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACvC;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAC;AAElD,qBAAa,iBAAkB,YAAW,UAAU,EAAE,OAAO;;IAC3D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;gBAQlB,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,cAAc,EACpB,cAAc,EAAE,cAAc,EAC9B,MAAM,EAAE,iBAAiB,EACzB,eAAe,EAAE,0BAA0B,GAAG,IAAI;IAoBpD,MAAM;;;IAIN,GAAG;IAcH,SAAS,IAAI,MAAM,CAAC,YAAY,CAAC;IAI3B,IAAI;CAQX"}
|
|
@@ -1,22 +1,28 @@
|
|
|
1
|
+
import { publishReplicationError } from "./replication-status.js";
|
|
1
2
|
import { IncrementalSyncer } from "./incremental-sync.js";
|
|
2
3
|
//#region ../zero-cache/src/services/replicator/replicator.ts
|
|
3
4
|
var ReplicatorService = class {
|
|
4
5
|
id;
|
|
5
6
|
#lc;
|
|
6
7
|
#incrementalSyncer;
|
|
8
|
+
#shouldPublishErrors;
|
|
7
9
|
#worker;
|
|
8
10
|
#runPromise;
|
|
9
11
|
constructor(lc, taskID, id, mode, changeStreamer, worker, statusPublisher) {
|
|
10
12
|
this.id = id;
|
|
11
13
|
this.#lc = lc.withContext("component", "replicator").withContext("serviceID", this.id);
|
|
12
14
|
this.#worker = worker;
|
|
15
|
+
this.#shouldPublishErrors = statusPublisher !== null;
|
|
13
16
|
this.#incrementalSyncer = new IncrementalSyncer(this.#lc, taskID, `${taskID}/${id}`, changeStreamer, worker, mode, statusPublisher);
|
|
14
17
|
}
|
|
15
18
|
status() {
|
|
16
19
|
return Promise.resolve({ status: "ok" });
|
|
17
20
|
}
|
|
18
21
|
run() {
|
|
19
|
-
this.#runPromise = this.#incrementalSyncer.run()
|
|
22
|
+
this.#runPromise = this.#incrementalSyncer.run().catch(async (error) => {
|
|
23
|
+
if (this.#shouldPublishErrors) await publishReplicationError(this.#lc, "Replicating", "Replication stopped because the replica writer failed");
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
20
26
|
return this.#runPromise;
|
|
21
27
|
}
|
|
22
28
|
subscribe() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replicator.js","names":["#lc","#incrementalSyncer","#worker","#runPromise"],"sources":["../../../../../../zero-cache/src/services/replicator/replicator.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {ReadonlyJSONObject} from '../../../../shared/src/json.ts';\nimport type {Source} from '../../types/streams.ts';\nimport type {ChangeStreamer} from '../change-streamer/change-streamer.ts';\nimport type {Service} from '../service.ts';\nimport {IncrementalSyncer} from './incremental-sync.ts';\nimport type
|
|
1
|
+
{"version":3,"file":"replicator.js","names":["#lc","#incrementalSyncer","#shouldPublishErrors","#worker","#runPromise"],"sources":["../../../../../../zero-cache/src/services/replicator/replicator.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {ReadonlyJSONObject} from '../../../../shared/src/json.ts';\nimport type {Source} from '../../types/streams.ts';\nimport type {ChangeStreamer} from '../change-streamer/change-streamer.ts';\nimport type {Service} from '../service.ts';\nimport {IncrementalSyncer} from './incremental-sync.ts';\nimport {\n publishReplicationError,\n type ReplicationStatusPublisher,\n} from './replication-status.ts';\nimport type {WriteWorkerClient} from './write-worker-client.ts';\n\n/** See {@link ReplicaStateNotifier.subscribe()}. */\nexport type ReplicaState = {\n readonly state: 'version-ready';\n /**\n * The replica watermark that became ready. Omitted for the initial\n * \"current state is ready\" notification, which should not count as newly\n * unserved work for a freshly-started ViewSyncer.\n */\n readonly watermark?: string | undefined;\n /**\n * Millisecond epoch when `watermark` became ready to read from the replica.\n */\n readonly replicaReadyTimeMs?: number | undefined;\n\n /**\n * Millisecond epoch at which the transaction behind `watermark` committed\n * *upstream*, as reported by the ChangeSource. Unlike `replicaReadyTimeMs`,\n * which is stamped locally when the change lands in the replica, this is the\n * origin of the whole pipeline, so `now - upstreamCommitTimeMs` at the point\n * the change is poked to clients is the end-to-end serving lag.\n *\n * Note that this crosses a clock domain: it comes from the upstream\n * database, whereas it is subtracted from the local clock in the ViewSyncer.\n * The existing `zero.replication.total_lag` metric spans the same boundary.\n *\n * Undefined when the ChangeSource does not report a commit time, and for the\n * initial \"current state is ready\" notification.\n */\n readonly upstreamCommitTimeMs?: number | undefined;\n\n // Used in tests to verify behavior when additional information\n // is ferried in the future. Not set in production.\n readonly testSeqNum?: number | undefined;\n};\n\nexport interface ReplicaStateNotifier {\n /**\n * Creates a cancelable subscription of changes in the replica state.\n *\n * A `version-ready` message indicates that the replica is ready to be\n * read, and henceforth that a _new_ version is ready, i.e. whenever a\n * change is committed to the replica. The `version-ready` message itself\n * otherwise contains no other information; the subscriber queries the\n * replica for the current data.\n *\n * A `maintenance` state indicates that the replica should not be read from.\n * If a subscriber is holding any transaction locks, it should release them\n * until the next `version-ready` signal.\n *\n * Upon subscription, the current state of the replica is sent immediately\n * if known. If multiple notifications occur before the subscriber\n * can consume them, all but the last notification are discarded by the\n * Subscription object (i.e. not buffered). Thus, a subscriber only\n * ever consumes the current (i.e. known) state of the replica. This avoids\n * a buildup of \"work\" if a subscriber is too busy to consume all\n * notifications.\n */\n subscribe(): Source<ReplicaState>;\n}\n\nexport interface Replicator extends ReplicaStateNotifier {\n /**\n * Returns an opaque message for human-readable consumption. This is\n * purely for ensuring that the Replicator has started at least once to\n * bootstrap a new replica.\n */\n status(): Promise<ReadonlyJSONObject>;\n}\n\nexport type ReplicatorMode = 'backup' | 'serving';\n\nexport class ReplicatorService implements Replicator, Service {\n readonly id: string;\n readonly #lc: LogContext;\n readonly #incrementalSyncer: IncrementalSyncer;\n readonly #shouldPublishErrors: boolean;\n readonly #worker: WriteWorkerClient;\n #runPromise: Promise<void> | undefined;\n\n constructor(\n lc: LogContext,\n taskID: string,\n id: string,\n mode: ReplicatorMode,\n changeStreamer: ChangeStreamer,\n worker: WriteWorkerClient,\n statusPublisher: ReplicationStatusPublisher | null,\n ) {\n this.id = id;\n this.#lc = lc\n .withContext('component', 'replicator')\n .withContext('serviceID', this.id);\n this.#worker = worker;\n this.#shouldPublishErrors = statusPublisher !== null;\n\n this.#incrementalSyncer = new IncrementalSyncer(\n this.#lc,\n taskID,\n `${taskID}/${id}`,\n changeStreamer,\n worker,\n mode,\n statusPublisher,\n );\n }\n\n status() {\n return Promise.resolve({status: 'ok'});\n }\n\n run() {\n this.#runPromise = this.#incrementalSyncer.run().catch(async error => {\n if (this.#shouldPublishErrors) {\n await publishReplicationError(\n this.#lc,\n 'Replicating',\n 'Replication stopped because the replica writer failed',\n );\n }\n throw error;\n });\n return this.#runPromise;\n }\n\n subscribe(): Source<ReplicaState> {\n return this.#incrementalSyncer.subscribe();\n }\n\n async stop() {\n this.#incrementalSyncer.stop(this.#lc);\n // Wait for the syncer's run loop to finish so that any in-flight\n // worker.processMessage() call completes and clears #pending\n // before we send the 'stop' message to the worker.\n await this.#runPromise;\n await this.#worker.stop();\n }\n}\n"],"mappings":";;;AAmFA,IAAa,oBAAb,MAA8D;CAC5D;CACA;CACA;CACA;CACA;CACA;CAEA,YACE,IACA,QACA,IACA,MACA,gBACA,QACA,iBACA;EACA,KAAK,KAAK;EACV,KAAKA,MAAM,GACR,YAAY,aAAa,YAAY,EACrC,YAAY,aAAa,KAAK,EAAE;EACnC,KAAKG,UAAU;EACf,KAAKD,uBAAuB,oBAAoB;EAEhD,KAAKD,qBAAqB,IAAI,kBAC5B,KAAKD,KACL,QACA,GAAG,OAAO,GAAG,MACb,gBACA,QACA,MACA,eACF;CACF;CAEA,SAAS;EACP,OAAO,QAAQ,QAAQ,EAAC,QAAQ,KAAI,CAAC;CACvC;CAEA,MAAM;EACJ,KAAKI,cAAc,KAAKH,mBAAmB,IAAI,EAAE,MAAM,OAAM,UAAS;GACpE,IAAI,KAAKC,sBACP,MAAM,wBACJ,KAAKF,KACL,eACA,uDACF;GAEF,MAAM;EACR,CAAC;EACD,OAAO,KAAKI;CACd;CAEA,YAAkC;EAChC,OAAO,KAAKH,mBAAmB,UAAU;CAC3C;CAEA,MAAM,OAAO;EACX,KAAKA,mBAAmB,KAAK,KAAKD,GAAG;EAIrC,MAAM,KAAKI;EACX,MAAM,KAAKD,QAAQ,KAAK;CAC1B;AACF"}
|
|
@@ -9,5 +9,32 @@ export declare class ReplicationReportRecorder {
|
|
|
9
9
|
readonly reportReplicaLag: (o: ObservableResult) => void;
|
|
10
10
|
readonly reportTotalLag: (o: ObservableResult) => void;
|
|
11
11
|
readonly reportLastTotalLag: (o: ObservableResult) => void;
|
|
12
|
+
readonly reportUpstreamClockSkew: (o: ObservableResult) => void;
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Estimates how far the upstream database's clock is from ours, in
|
|
16
|
+
* milliseconds, positive meaning upstream is ahead.
|
|
17
|
+
*
|
|
18
|
+
* `sendTimeMs` and `receiveTimeMs` are both stamped in the
|
|
19
|
+
* replication-manager, either side of `commitTimeMs`, which upstream stamps.
|
|
20
|
+
* Assuming the upstream commit fell midway through that round trip, the
|
|
21
|
+
* difference between where it actually landed and that midpoint is the clock
|
|
22
|
+
* offset -- the same round-trip estimate NTP uses.
|
|
23
|
+
*
|
|
24
|
+
* The estimate is biased by any asymmetry between the two legs: the outbound
|
|
25
|
+
* leg is a SQL round trip, while the return leg additionally waits on a WAL
|
|
26
|
+
* flush and the walsender (which the pre-PG-17 path notes can add 50~100ms on
|
|
27
|
+
* an idle database). So treat this as a detector of gross skew -- the kind
|
|
28
|
+
* that invalidates sync.e2e_serving_lag -- and not as a sub-100ms correction.
|
|
29
|
+
*
|
|
30
|
+
* The rigorous bound, if a tighter reading is ever needed: the commit
|
|
31
|
+
* genuinely happened between send and receive, so a `commitTimeMs` outside
|
|
32
|
+
* that window proves skew of at least the amount by which it falls outside,
|
|
33
|
+
* independent of any latency assumption.
|
|
34
|
+
*/
|
|
35
|
+
export declare function estimateUpstreamClockSkewMs({ sendTimeMs, commitTimeMs, receiveTimeMs, }: {
|
|
36
|
+
sendTimeMs: number;
|
|
37
|
+
commitTimeMs: number;
|
|
38
|
+
receiveTimeMs: number;
|
|
39
|
+
}): number;
|
|
13
40
|
//# sourceMappingURL=recorder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/replicator/reporter/recorder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AAM1D,qBAAa,yBAAyB;;gBAIxB,EAAE,EAAE,UAAU;IAI1B,MAAM,CAAC,MAAM,EAAE,iBAAiB;
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../../../../../../zero-cache/src/services/replicator/reporter/recorder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AAM1D,qBAAa,yBAAyB;;gBAIxB,EAAE,EAAE,UAAU;IAI1B,MAAM,CAAC,MAAM,EAAE,iBAAiB;IAkEhC,QAAQ,CAAC,iBAAiB,GAAI,GAAG,gBAAgB,UAK/C;IAEF,QAAQ,CAAC,gBAAgB,GAAI,GAAG,gBAAgB,UAK9C;IAEF,QAAQ,CAAC,cAAc,GAAI,GAAG,gBAAgB,UAK5C;IAEF,QAAQ,CAAC,kBAAkB,GAAI,GAAG,gBAAgB,UAKhD;IAEF,QAAQ,CAAC,uBAAuB,GAAI,GAAG,gBAAgB,UAKrD;CACH;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,2BAA2B,CAAC,EAC1C,UAAU,EACV,YAAY,EACZ,aAAa,GACd,EAAE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,MAAM,CAET"}
|
|
@@ -34,6 +34,10 @@ var ReplicationReportRecorder = class {
|
|
|
34
34
|
description: "Latency from sending the most recently received upstream replication report to its reaching the replica. This is an alias of replication.total_lag retained for dashboards that explicitly want the non-extrapolated value.",
|
|
35
35
|
unit: "millisecond"
|
|
36
36
|
}).addCallback(this.reportLastTotalLag);
|
|
37
|
+
getOrCreateGauge("replication", "upstream_clock_skew", {
|
|
38
|
+
description: "Estimated offset of the upstream database clock relative to zero-cache's. Positive means upstream is running ahead. Derived from the lag report round-trip, whose send and receive times are stamped on our clock either side of the upstream commit timestamp. This matters because sync.e2e_serving_lag subtracts an upstream commit timestamp from a local clock, so a skewed upstream clock biases it directly -- and an upstream clock running ahead biases it *low*, which looks like healthy serving rather than a broken measurement.",
|
|
39
|
+
unit: "millisecond"
|
|
40
|
+
}).addCallback(this.reportUpstreamClockSkew);
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
reportUpstreamLag = (o) => {
|
|
@@ -52,7 +56,35 @@ var ReplicationReportRecorder = class {
|
|
|
52
56
|
const last = this.#last?.lastTimings;
|
|
53
57
|
if (last) o.observe(last.replicateTimeMs - last.sendTimeMs);
|
|
54
58
|
};
|
|
59
|
+
reportUpstreamClockSkew = (o) => {
|
|
60
|
+
const last = this.#last?.lastTimings;
|
|
61
|
+
if (last) o.observe(estimateUpstreamClockSkewMs(last));
|
|
62
|
+
};
|
|
55
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* Estimates how far the upstream database's clock is from ours, in
|
|
66
|
+
* milliseconds, positive meaning upstream is ahead.
|
|
67
|
+
*
|
|
68
|
+
* `sendTimeMs` and `receiveTimeMs` are both stamped in the
|
|
69
|
+
* replication-manager, either side of `commitTimeMs`, which upstream stamps.
|
|
70
|
+
* Assuming the upstream commit fell midway through that round trip, the
|
|
71
|
+
* difference between where it actually landed and that midpoint is the clock
|
|
72
|
+
* offset -- the same round-trip estimate NTP uses.
|
|
73
|
+
*
|
|
74
|
+
* The estimate is biased by any asymmetry between the two legs: the outbound
|
|
75
|
+
* leg is a SQL round trip, while the return leg additionally waits on a WAL
|
|
76
|
+
* flush and the walsender (which the pre-PG-17 path notes can add 50~100ms on
|
|
77
|
+
* an idle database). So treat this as a detector of gross skew -- the kind
|
|
78
|
+
* that invalidates sync.e2e_serving_lag -- and not as a sub-100ms correction.
|
|
79
|
+
*
|
|
80
|
+
* The rigorous bound, if a tighter reading is ever needed: the commit
|
|
81
|
+
* genuinely happened between send and receive, so a `commitTimeMs` outside
|
|
82
|
+
* that window proves skew of at least the amount by which it falls outside,
|
|
83
|
+
* independent of any latency assumption.
|
|
84
|
+
*/
|
|
85
|
+
function estimateUpstreamClockSkewMs({ sendTimeMs, commitTimeMs, receiveTimeMs }) {
|
|
86
|
+
return commitTimeMs - (sendTimeMs + receiveTimeMs) / 2;
|
|
87
|
+
}
|
|
56
88
|
//#endregion
|
|
57
89
|
export { ReplicationReportRecorder };
|
|
58
90
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recorder.js","names":["#lc","#last"],"sources":["../../../../../../../zero-cache/src/services/replicator/reporter/recorder.ts"],"sourcesContent":["import type {ObservableResult} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {getOrCreateGauge} from '../../../observability/metrics.ts';\nimport type {ReplicationReport} from './report-schema.ts';\n\n// Hook for sanity checking lag reports in development.\nconst LOG_ALL_REPLICATION_REPORTS_AT_DEBUG =\n process.env.ZERO_LOG_ALL_REPLICATION_REPORTS_AT_DEBUG === '1';\n\nexport class ReplicationReportRecorder {\n readonly #lc: LogContext;\n #last: ReplicationReport | null = null;\n\n constructor(lc: LogContext) {\n this.#lc = lc;\n }\n\n record(report: ReplicationReport) {\n const first = this.#last === null;\n this.#last = report;\n\n const {lastTimings} = report;\n if (lastTimings) {\n const total = lastTimings.replicateTimeMs - lastTimings.sendTimeMs;\n if (total > 10_000) {\n this.#lc.warn?.(`high replication lag: ${total} ms`, report);\n } else if (total > 1_000) {\n this.#lc.info?.(`replication lag: ${total} ms`, report);\n }\n if (LOG_ALL_REPLICATION_REPORTS_AT_DEBUG) {\n this.#lc.debug?.(`replication lag ${total} ms`, report);\n }\n }\n\n if (first) {\n getOrCreateGauge('replication', 'upstream_lag', {\n description:\n 'Latency from sending an upstream replication report ' +\n 'to receiving it in the replication stream',\n unit: 'millisecond',\n }).addCallback(this.reportUpstreamLag);\n\n getOrCreateGauge('replication', 'replica_lag', {\n description:\n 'Latency from receiving an upstream replication report ' +\n 'to its reaching the replica',\n unit: 'millisecond',\n }).addCallback(this.reportReplicaLag);\n\n getOrCreateGauge('replication', 'total_lag', {\n description:\n 'Latency from sending an upstream replication report to its ' +\n 'reaching the replica. This reflects the actual measured ' +\n 'round-trip of the most recently received report and does not ' +\n 'grow when reports stop arriving.',\n unit: 'millisecond',\n }).addCallback(this.reportTotalLag);\n\n getOrCreateGauge('replication', 'last_total_lag', {\n description:\n 'Latency from sending the most recently received upstream ' +\n 'replication report to its reaching the replica. This is an alias ' +\n 'of replication.total_lag retained for dashboards that explicitly ' +\n 'want the non-extrapolated value.',\n unit: 'millisecond',\n }).addCallback(this.reportLastTotalLag);\n }\n }\n\n readonly reportUpstreamLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.receiveTimeMs - last.sendTimeMs);\n }\n };\n\n readonly reportReplicaLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.replicateTimeMs - last.receiveTimeMs);\n }\n };\n\n readonly reportTotalLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.replicateTimeMs - last.sendTimeMs);\n }\n };\n\n readonly reportLastTotalLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.replicateTimeMs - last.sendTimeMs);\n }\n };\n}\n"],"mappings":";;AAMA,IAAM,uCACJ,QAAQ,IAAI,8CAA8C;AAE5D,IAAa,4BAAb,MAAuC;CACrC;CACA,QAAkC;CAElC,YAAY,IAAgB;EAC1B,KAAKA,MAAM;CACb;CAEA,OAAO,QAA2B;EAChC,MAAM,QAAQ,KAAKC,UAAU;EAC7B,KAAKA,QAAQ;EAEb,MAAM,EAAC,gBAAe;EACtB,IAAI,aAAa;GACf,MAAM,QAAQ,YAAY,kBAAkB,YAAY;GACxD,IAAI,QAAQ,KACV,KAAKD,IAAI,OAAO,yBAAyB,MAAM,MAAM,MAAM;QACtD,IAAI,QAAQ,KACjB,KAAKA,IAAI,OAAO,oBAAoB,MAAM,MAAM,MAAM;GAExD,IAAI,sCACF,KAAKA,IAAI,QAAQ,mBAAmB,MAAM,MAAM,MAAM;EAE1D;EAEA,IAAI,OAAO;GACT,iBAAiB,eAAe,gBAAgB;IAC9C,aACE;IAEF,MAAM;GACR,CAAC,EAAE,YAAY,KAAK,iBAAiB;GAErC,iBAAiB,eAAe,eAAe;IAC7C,aACE;IAEF,MAAM;GACR,CAAC,EAAE,YAAY,KAAK,gBAAgB;GAEpC,iBAAiB,eAAe,aAAa;IAC3C,aACE;IAIF,MAAM;GACR,CAAC,EAAE,YAAY,KAAK,cAAc;GAElC,iBAAiB,eAAe,kBAAkB;IAChD,aACE;IAIF,MAAM;GACR,CAAC,EAAE,YAAY,KAAK,kBAAkB;
|
|
1
|
+
{"version":3,"file":"recorder.js","names":["#lc","#last"],"sources":["../../../../../../../zero-cache/src/services/replicator/reporter/recorder.ts"],"sourcesContent":["import type {ObservableResult} from '@opentelemetry/api';\nimport type {LogContext} from '@rocicorp/logger';\nimport {getOrCreateGauge} from '../../../observability/metrics.ts';\nimport type {ReplicationReport} from './report-schema.ts';\n\n// Hook for sanity checking lag reports in development.\nconst LOG_ALL_REPLICATION_REPORTS_AT_DEBUG =\n process.env.ZERO_LOG_ALL_REPLICATION_REPORTS_AT_DEBUG === '1';\n\nexport class ReplicationReportRecorder {\n readonly #lc: LogContext;\n #last: ReplicationReport | null = null;\n\n constructor(lc: LogContext) {\n this.#lc = lc;\n }\n\n record(report: ReplicationReport) {\n const first = this.#last === null;\n this.#last = report;\n\n const {lastTimings} = report;\n if (lastTimings) {\n const total = lastTimings.replicateTimeMs - lastTimings.sendTimeMs;\n if (total > 10_000) {\n this.#lc.warn?.(`high replication lag: ${total} ms`, report);\n } else if (total > 1_000) {\n this.#lc.info?.(`replication lag: ${total} ms`, report);\n }\n if (LOG_ALL_REPLICATION_REPORTS_AT_DEBUG) {\n this.#lc.debug?.(`replication lag ${total} ms`, report);\n }\n }\n\n if (first) {\n getOrCreateGauge('replication', 'upstream_lag', {\n description:\n 'Latency from sending an upstream replication report ' +\n 'to receiving it in the replication stream',\n unit: 'millisecond',\n }).addCallback(this.reportUpstreamLag);\n\n getOrCreateGauge('replication', 'replica_lag', {\n description:\n 'Latency from receiving an upstream replication report ' +\n 'to its reaching the replica',\n unit: 'millisecond',\n }).addCallback(this.reportReplicaLag);\n\n getOrCreateGauge('replication', 'total_lag', {\n description:\n 'Latency from sending an upstream replication report to its ' +\n 'reaching the replica. This reflects the actual measured ' +\n 'round-trip of the most recently received report and does not ' +\n 'grow when reports stop arriving.',\n unit: 'millisecond',\n }).addCallback(this.reportTotalLag);\n\n getOrCreateGauge('replication', 'last_total_lag', {\n description:\n 'Latency from sending the most recently received upstream ' +\n 'replication report to its reaching the replica. This is an alias ' +\n 'of replication.total_lag retained for dashboards that explicitly ' +\n 'want the non-extrapolated value.',\n unit: 'millisecond',\n }).addCallback(this.reportLastTotalLag);\n\n getOrCreateGauge('replication', 'upstream_clock_skew', {\n description:\n 'Estimated offset of the upstream database clock relative to ' +\n \"zero-cache's. Positive means upstream is running ahead. \" +\n 'Derived from the lag report round-trip, whose send and receive ' +\n 'times are stamped on our clock either side of the upstream ' +\n 'commit timestamp. This matters because ' +\n 'sync.e2e_serving_lag subtracts an upstream commit timestamp from ' +\n 'a local clock, so a skewed upstream clock biases it directly -- ' +\n 'and an upstream clock running ahead biases it *low*, which looks ' +\n 'like healthy serving rather than a broken measurement.',\n unit: 'millisecond',\n }).addCallback(this.reportUpstreamClockSkew);\n }\n }\n\n readonly reportUpstreamLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.receiveTimeMs - last.sendTimeMs);\n }\n };\n\n readonly reportReplicaLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.replicateTimeMs - last.receiveTimeMs);\n }\n };\n\n readonly reportTotalLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.replicateTimeMs - last.sendTimeMs);\n }\n };\n\n readonly reportLastTotalLag = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(last.replicateTimeMs - last.sendTimeMs);\n }\n };\n\n readonly reportUpstreamClockSkew = (o: ObservableResult) => {\n const last = this.#last?.lastTimings;\n if (last) {\n o.observe(estimateUpstreamClockSkewMs(last));\n }\n };\n}\n\n/**\n * Estimates how far the upstream database's clock is from ours, in\n * milliseconds, positive meaning upstream is ahead.\n *\n * `sendTimeMs` and `receiveTimeMs` are both stamped in the\n * replication-manager, either side of `commitTimeMs`, which upstream stamps.\n * Assuming the upstream commit fell midway through that round trip, the\n * difference between where it actually landed and that midpoint is the clock\n * offset -- the same round-trip estimate NTP uses.\n *\n * The estimate is biased by any asymmetry between the two legs: the outbound\n * leg is a SQL round trip, while the return leg additionally waits on a WAL\n * flush and the walsender (which the pre-PG-17 path notes can add 50~100ms on\n * an idle database). So treat this as a detector of gross skew -- the kind\n * that invalidates sync.e2e_serving_lag -- and not as a sub-100ms correction.\n *\n * The rigorous bound, if a tighter reading is ever needed: the commit\n * genuinely happened between send and receive, so a `commitTimeMs` outside\n * that window proves skew of at least the amount by which it falls outside,\n * independent of any latency assumption.\n */\nexport function estimateUpstreamClockSkewMs({\n sendTimeMs,\n commitTimeMs,\n receiveTimeMs,\n}: {\n sendTimeMs: number;\n commitTimeMs: number;\n receiveTimeMs: number;\n}): number {\n return commitTimeMs - (sendTimeMs + receiveTimeMs) / 2;\n}\n"],"mappings":";;AAMA,IAAM,uCACJ,QAAQ,IAAI,8CAA8C;AAE5D,IAAa,4BAAb,MAAuC;CACrC;CACA,QAAkC;CAElC,YAAY,IAAgB;EAC1B,KAAKA,MAAM;CACb;CAEA,OAAO,QAA2B;EAChC,MAAM,QAAQ,KAAKC,UAAU;EAC7B,KAAKA,QAAQ;EAEb,MAAM,EAAC,gBAAe;EACtB,IAAI,aAAa;GACf,MAAM,QAAQ,YAAY,kBAAkB,YAAY;GACxD,IAAI,QAAQ,KACV,KAAKD,IAAI,OAAO,yBAAyB,MAAM,MAAM,MAAM;QACtD,IAAI,QAAQ,KACjB,KAAKA,IAAI,OAAO,oBAAoB,MAAM,MAAM,MAAM;GAExD,IAAI,sCACF,KAAKA,IAAI,QAAQ,mBAAmB,MAAM,MAAM,MAAM;EAE1D;EAEA,IAAI,OAAO;GACT,iBAAiB,eAAe,gBAAgB;IAC9C,aACE;IAEF,MAAM;GACR,CAAC,EAAE,YAAY,KAAK,iBAAiB;GAErC,iBAAiB,eAAe,eAAe;IAC7C,aACE;IAEF,MAAM;GACR,CAAC,EAAE,YAAY,KAAK,gBAAgB;GAEpC,iBAAiB,eAAe,aAAa;IAC3C,aACE;IAIF,MAAM;GACR,CAAC,EAAE,YAAY,KAAK,cAAc;GAElC,iBAAiB,eAAe,kBAAkB;IAChD,aACE;IAIF,MAAM;GACR,CAAC,EAAE,YAAY,KAAK,kBAAkB;GAEtC,iBAAiB,eAAe,uBAAuB;IACrD,aACE;IASF,MAAM;GACR,CAAC,EAAE,YAAY,KAAK,uBAAuB;EAC7C;CACF;CAEA,qBAA8B,MAAwB;EACpD,MAAM,OAAO,KAAKC,OAAO;EACzB,IAAI,MACF,EAAE,QAAQ,KAAK,gBAAgB,KAAK,UAAU;CAElD;CAEA,oBAA6B,MAAwB;EACnD,MAAM,OAAO,KAAKA,OAAO;EACzB,IAAI,MACF,EAAE,QAAQ,KAAK,kBAAkB,KAAK,aAAa;CAEvD;CAEA,kBAA2B,MAAwB;EACjD,MAAM,OAAO,KAAKA,OAAO;EACzB,IAAI,MACF,EAAE,QAAQ,KAAK,kBAAkB,KAAK,UAAU;CAEpD;CAEA,sBAA+B,MAAwB;EACrD,MAAM,OAAO,KAAKA,OAAO;EACzB,IAAI,MACF,EAAE,QAAQ,KAAK,kBAAkB,KAAK,UAAU;CAEpD;CAEA,2BAAoC,MAAwB;EAC1D,MAAM,OAAO,KAAKA,OAAO;EACzB,IAAI,MACF,EAAE,QAAQ,4BAA4B,IAAI,CAAC;CAE/C;AACF;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,4BAA4B,EAC1C,YACA,cACA,iBAKS;CACT,OAAO,gBAAgB,aAAa,iBAAiB;AACvD"}
|
|
@@ -21,8 +21,8 @@ export declare const replicationTimingsSchema: v.ObjectType<Omit<{
|
|
|
21
21
|
}, undefined>;
|
|
22
22
|
export declare const replicationReportSchema: v.ObjectType<{
|
|
23
23
|
lastTimings: v.Optional<{
|
|
24
|
-
sendTimeMs: number;
|
|
25
24
|
commitTimeMs: number;
|
|
25
|
+
sendTimeMs: number;
|
|
26
26
|
receiveTimeMs: number;
|
|
27
27
|
replicateTimeMs: number;
|
|
28
28
|
}>;
|
|
@@ -4,6 +4,7 @@ import { getSubscriptionState } from "./schema/replication-state.js";
|
|
|
4
4
|
import { StatementRunner } from "../../db/statements.js";
|
|
5
5
|
import { isSQLiteCorruption, logSQLiteCorruptionDiagnostics, registerSQLiteCorruptionDiagnosticTarget } from "../../db/sqlite-corruption.js";
|
|
6
6
|
import { createLogContext } from "../../server/logging.js";
|
|
7
|
+
import { deleteLiteDB } from "../../db/delete-lite-db.js";
|
|
7
8
|
import { ChangeProcessor } from "./change-processor.js";
|
|
8
9
|
import { applyPragmas, serializeError } from "./write-worker-client.js";
|
|
9
10
|
import { parentPort } from "node:worker_threads";
|
|
@@ -18,12 +19,19 @@ function createAPI() {
|
|
|
18
19
|
let lc;
|
|
19
20
|
let replicaDbPath;
|
|
20
21
|
let unregisterCorruptionDiagnosticTarget;
|
|
21
|
-
function
|
|
22
|
-
if (lc
|
|
22
|
+
function handleCorruptedDb(err) {
|
|
23
|
+
if (!lc || !replicaDbPath || !isSQLiteCorruption(err)) return;
|
|
24
|
+
logSQLiteCorruptionDiagnostics(lc, "write-worker", replicaDbPath, err);
|
|
25
|
+
try {
|
|
26
|
+
lc.warn?.(`deleting corrupted db at ${replicaDbPath}`);
|
|
27
|
+
deleteLiteDB(replicaDbPath);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
lc.warn?.(`error deleting corrupted db at ${replicaDbPath}`, e);
|
|
30
|
+
}
|
|
23
31
|
}
|
|
24
32
|
function createProcessor() {
|
|
25
33
|
processor = new ChangeProcessor(must(runner), must(mode), (_lc, err) => {
|
|
26
|
-
|
|
34
|
+
handleCorruptedDb(err);
|
|
27
35
|
port.postMessage({ writeError: serializeError(err) });
|
|
28
36
|
});
|
|
29
37
|
}
|
|
@@ -43,7 +51,7 @@ function createAPI() {
|
|
|
43
51
|
mode = cpMode;
|
|
44
52
|
createProcessor();
|
|
45
53
|
} catch (e) {
|
|
46
|
-
|
|
54
|
+
handleCorruptedDb(e);
|
|
47
55
|
throw e;
|
|
48
56
|
}
|
|
49
57
|
},
|
|
@@ -51,7 +59,7 @@ function createAPI() {
|
|
|
51
59
|
try {
|
|
52
60
|
return getSubscriptionState(must(runner));
|
|
53
61
|
} catch (e) {
|
|
54
|
-
|
|
62
|
+
handleCorruptedDb(e);
|
|
55
63
|
throw e;
|
|
56
64
|
}
|
|
57
65
|
},
|
|
@@ -59,7 +67,7 @@ function createAPI() {
|
|
|
59
67
|
try {
|
|
60
68
|
return must(processor).processMessage(must(lc), downstream);
|
|
61
69
|
} catch (e) {
|
|
62
|
-
|
|
70
|
+
handleCorruptedDb(e);
|
|
63
71
|
throw e;
|
|
64
72
|
}
|
|
65
73
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write-worker.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/write-worker.ts"],"sourcesContent":["import {parentPort} from 'node:worker_threads';\nimport type {LogContext} from '@rocicorp/logger';\nimport type {LogConfig} from '../../../../shared/src/logging.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {\n isSQLiteCorruption,\n logSQLiteCorruptionDiagnostics,\n registerSQLiteCorruptionDiagnosticTarget,\n} from '../../db/sqlite-corruption.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport {createLogContext} from '../../server/logging.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport {ChangeProcessor, type ChangeProcessorMode} from './change-processor.ts';\nimport {getSubscriptionState} from './schema/replication-state.ts';\nimport {\n applyPragmas,\n serializeError,\n type ArgsMap,\n type Method,\n type PragmaConfig,\n type Request,\n type Response,\n type ResultMap,\n type WriteError,\n} from './write-worker-client.ts';\n\nif (!parentPort) {\n throw new Error('write-worker must be run as a worker thread');\n}\n\nconst port = parentPort;\n\ntype API = {[M in Method]: (...args: ArgsMap[M]) => ResultMap[M]};\n\nfunction createAPI(): API {\n let db: Database | undefined;\n let runner: StatementRunner | undefined;\n let processor: ChangeProcessor | undefined;\n let mode: ChangeProcessorMode | undefined;\n let lc: LogContext | undefined;\n let replicaDbPath: string | undefined;\n let unregisterCorruptionDiagnosticTarget: (() => void) | undefined;\n\n function
|
|
1
|
+
{"version":3,"file":"write-worker.js","names":[],"sources":["../../../../../../zero-cache/src/services/replicator/write-worker.ts"],"sourcesContent":["import {parentPort} from 'node:worker_threads';\nimport type {LogContext} from '@rocicorp/logger';\nimport type {LogConfig} from '../../../../shared/src/logging.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {deleteLiteDB} from '../../db/delete-lite-db.ts';\nimport {\n isSQLiteCorruption,\n logSQLiteCorruptionDiagnostics,\n registerSQLiteCorruptionDiagnosticTarget,\n} from '../../db/sqlite-corruption.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport {createLogContext} from '../../server/logging.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport {ChangeProcessor, type ChangeProcessorMode} from './change-processor.ts';\nimport {getSubscriptionState} from './schema/replication-state.ts';\nimport {\n applyPragmas,\n serializeError,\n type ArgsMap,\n type Method,\n type PragmaConfig,\n type Request,\n type Response,\n type ResultMap,\n type WriteError,\n} from './write-worker-client.ts';\n\nif (!parentPort) {\n throw new Error('write-worker must be run as a worker thread');\n}\n\nconst port = parentPort;\n\ntype API = {[M in Method]: (...args: ArgsMap[M]) => ResultMap[M]};\n\nfunction createAPI(): API {\n let db: Database | undefined;\n let runner: StatementRunner | undefined;\n let processor: ChangeProcessor | undefined;\n let mode: ChangeProcessorMode | undefined;\n let lc: LogContext | undefined;\n let replicaDbPath: string | undefined;\n let unregisterCorruptionDiagnosticTarget: (() => void) | undefined;\n\n function handleCorruptedDb(err: unknown) {\n if (!lc || !replicaDbPath || !isSQLiteCorruption(err)) {\n return;\n }\n logSQLiteCorruptionDiagnostics(lc, 'write-worker', replicaDbPath, err);\n try {\n lc.warn?.(`deleting corrupted db at ${replicaDbPath}`);\n deleteLiteDB(replicaDbPath);\n } catch (e) {\n lc.warn?.(`error deleting corrupted db at ${replicaDbPath}`, e);\n }\n }\n\n function createProcessor() {\n processor = new ChangeProcessor(must(runner), must(mode), (_lc, err) => {\n handleCorruptedDb(err);\n port.postMessage({\n writeError: serializeError(err),\n } satisfies WriteError);\n });\n }\n\n return {\n init(\n dbPath: string,\n cpMode: ChangeProcessorMode,\n pragmas: PragmaConfig,\n logConfig: LogConfig,\n ) {\n replicaDbPath = dbPath;\n lc = createLogContext({log: logConfig}, 'write-worker');\n unregisterCorruptionDiagnosticTarget?.();\n unregisterCorruptionDiagnosticTarget =\n registerSQLiteCorruptionDiagnosticTarget({\n debugName: 'write-worker',\n dbPath,\n });\n try {\n db = new Database(lc, dbPath);\n applyPragmas(db, pragmas);\n runner = new StatementRunner(db);\n mode = cpMode;\n createProcessor();\n } catch (e) {\n handleCorruptedDb(e);\n throw e;\n }\n },\n\n getSubscriptionState() {\n try {\n return getSubscriptionState(must(runner));\n } catch (e) {\n handleCorruptedDb(e);\n throw e;\n }\n },\n\n processMessage(downstream: ChangeStreamData) {\n try {\n return must(processor).processMessage(must(lc), downstream);\n } catch (e) {\n handleCorruptedDb(e);\n throw e;\n }\n },\n\n abort() {\n must(processor).abort(must(lc));\n createProcessor();\n },\n\n stop() {\n db?.close();\n db = undefined;\n runner = undefined;\n processor = undefined;\n replicaDbPath = undefined;\n unregisterCorruptionDiagnosticTarget?.();\n unregisterCorruptionDiagnosticTarget = undefined;\n },\n };\n}\n\nconst api = createAPI();\n\nport.on('message', (msg: Request) => {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TS can't narrow msg.method + msg.args together\n const result = (api[msg.method] as (...args: any[]) => unknown)(\n ...msg.args,\n );\n // abort is fire-and-forget — no pending slot on the client side.\n if (msg.method !== 'abort') {\n port.postMessage({method: msg.method, result} as Response);\n }\n } catch (e) {\n if (msg.method !== 'abort') {\n port.postMessage({\n method: msg.method,\n error: serializeError(e),\n } as Response);\n }\n }\n});\n"],"mappings":";;;;;;;;;;;AA4BA,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,6CAA6C;AAG/D,IAAM,OAAO;AAIb,SAAS,YAAiB;CACxB,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,SAAS,kBAAkB,KAAc;EACvC,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC,mBAAmB,GAAG,GAClD;EAEF,+BAA+B,IAAI,gBAAgB,eAAe,GAAG;EACrE,IAAI;GACF,GAAG,OAAO,4BAA4B,eAAe;GACrD,aAAa,aAAa;EAC5B,SAAS,GAAG;GACV,GAAG,OAAO,kCAAkC,iBAAiB,CAAC;EAChE;CACF;CAEA,SAAS,kBAAkB;EACzB,YAAY,IAAI,gBAAgB,KAAK,MAAM,GAAG,KAAK,IAAI,IAAI,KAAK,QAAQ;GACtE,kBAAkB,GAAG;GACrB,KAAK,YAAY,EACf,YAAY,eAAe,GAAG,EAChC,CAAsB;EACxB,CAAC;CACH;CAEA,OAAO;EACL,KACE,QACA,QACA,SACA,WACA;GACA,gBAAgB;GAChB,KAAK,iBAAiB,EAAC,KAAK,UAAS,GAAG,cAAc;GACtD,uCAAuC;GACvC,uCACE,yCAAyC;IACvC,WAAW;IACX;GACF,CAAC;GACH,IAAI;IACF,KAAK,IAAI,SAAS,IAAI,MAAM;IAC5B,aAAa,IAAI,OAAO;IACxB,SAAS,IAAI,gBAAgB,EAAE;IAC/B,OAAO;IACP,gBAAgB;GAClB,SAAS,GAAG;IACV,kBAAkB,CAAC;IACnB,MAAM;GACR;EACF;EAEA,uBAAuB;GACrB,IAAI;IACF,OAAO,qBAAqB,KAAK,MAAM,CAAC;GAC1C,SAAS,GAAG;IACV,kBAAkB,CAAC;IACnB,MAAM;GACR;EACF;EAEA,eAAe,YAA8B;GAC3C,IAAI;IACF,OAAO,KAAK,SAAS,EAAE,eAAe,KAAK,EAAE,GAAG,UAAU;GAC5D,SAAS,GAAG;IACV,kBAAkB,CAAC;IACnB,MAAM;GACR;EACF;EAEA,QAAQ;GACN,KAAK,SAAS,EAAE,MAAM,KAAK,EAAE,CAAC;GAC9B,gBAAgB;EAClB;EAEA,OAAO;GACL,IAAI,MAAM;GACV,KAAK,KAAA;GACL,SAAS,KAAA;GACT,YAAY,KAAA;GACZ,gBAAgB,KAAA;GAChB,uCAAuC;GACvC,uCAAuC,KAAA;EACzC;CACF;AACF;AAEA,IAAM,MAAM,UAAU;AAEtB,KAAK,GAAG,YAAY,QAAiB;CACnC,IAAI;EAEF,MAAM,SAAU,IAAI,IAAI,QACtB,GAAG,IAAI,IACT;EAEA,IAAI,IAAI,WAAW,SACjB,KAAK,YAAY;GAAC,QAAQ,IAAI;GAAQ;EAAM,CAAa;CAE7D,SAAS,GAAG;EACV,IAAI,IAAI,WAAW,SACjB,KAAK,YAAY;GACf,QAAQ,IAAI;GACZ,OAAO,eAAe,CAAC;EACzB,CAAa;CAEjB;AACF,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ReplicaState } from '../replicator/replicator.ts';
|
|
2
|
+
export type PendingUpstreamCommit = {
|
|
3
|
+
readonly watermark: string;
|
|
4
|
+
readonly commitTimeMs: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Pairs the upstream commit timestamps carried by `version-ready`
|
|
8
|
+
* notifications with the moment the corresponding change is poked to clients,
|
|
9
|
+
* yielding the end-to-end serving lag.
|
|
10
|
+
*
|
|
11
|
+
* This measures completion, not backlog: an observation is produced only when
|
|
12
|
+
* work actually reaches clients, so the resulting histogram is a latency
|
|
13
|
+
* distribution rather than a periodic snapshot of how far behind things are.
|
|
14
|
+
* A ViewSyncer that is stuck contributes nothing until it recovers, instead of
|
|
15
|
+
* re-reporting its age on every sample tick.
|
|
16
|
+
*/
|
|
17
|
+
export declare class E2EServingLagTracker {
|
|
18
|
+
#private;
|
|
19
|
+
get pending(): PendingUpstreamCommit | null;
|
|
20
|
+
/**
|
|
21
|
+
* Records the upstream commit behind a `version-ready` notification.
|
|
22
|
+
*
|
|
23
|
+
* Notifications coalesce when the ViewSyncer is busy, so one state may stand
|
|
24
|
+
* in for several commits. The *oldest* commit time is kept, since it bounds
|
|
25
|
+
* the lag of everything the notification subsumed, while the watermark
|
|
26
|
+
* advances to the newest — that is the one that must be served for all of
|
|
27
|
+
* the subsumed commits to have been delivered.
|
|
28
|
+
*/
|
|
29
|
+
onVersionReady({ watermark, upstreamCommitTimeMs }: ReplicaState): void;
|
|
30
|
+
/**
|
|
31
|
+
* Called once a version has been poked to clients.
|
|
32
|
+
*
|
|
33
|
+
* @return the observation to record, or `null` if the served version does
|
|
34
|
+
* not yet cover an outstanding upstream commit.
|
|
35
|
+
*/
|
|
36
|
+
onVersionServed(servedVersion: string, nowMs: number): Observation | null;
|
|
37
|
+
}
|
|
38
|
+
export type Observation = {
|
|
39
|
+
/** End-to-end lag in milliseconds, clamped to be non-negative. */
|
|
40
|
+
readonly lagMs: number;
|
|
41
|
+
/** Whether the clamp was applied, i.e. the raw measurement was negative. */
|
|
42
|
+
readonly clamped: boolean;
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=e2e-serving-lag.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"e2e-serving-lag.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/e2e-serving-lag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,6BAA6B,CAAC;AAE9D,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,qBAAa,oBAAoB;;IAG/B,IAAI,OAAO,IAAI,qBAAqB,GAAG,IAAI,CAE1C;IAED;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAC,SAAS,EAAE,oBAAoB,EAAC,EAAE,YAAY,GAAG,IAAI;IAcrE;;;;;OAKG;IACH,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;CAoB1E;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region ../zero-cache/src/services/view-syncer/e2e-serving-lag.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pairs the upstream commit timestamps carried by `version-ready`
|
|
4
|
+
* notifications with the moment the corresponding change is poked to clients,
|
|
5
|
+
* yielding the end-to-end serving lag.
|
|
6
|
+
*
|
|
7
|
+
* This measures completion, not backlog: an observation is produced only when
|
|
8
|
+
* work actually reaches clients, so the resulting histogram is a latency
|
|
9
|
+
* distribution rather than a periodic snapshot of how far behind things are.
|
|
10
|
+
* A ViewSyncer that is stuck contributes nothing until it recovers, instead of
|
|
11
|
+
* re-reporting its age on every sample tick.
|
|
12
|
+
*/
|
|
13
|
+
var E2EServingLagTracker = class {
|
|
14
|
+
#pending = null;
|
|
15
|
+
get pending() {
|
|
16
|
+
return this.#pending;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Records the upstream commit behind a `version-ready` notification.
|
|
20
|
+
*
|
|
21
|
+
* Notifications coalesce when the ViewSyncer is busy, so one state may stand
|
|
22
|
+
* in for several commits. The *oldest* commit time is kept, since it bounds
|
|
23
|
+
* the lag of everything the notification subsumed, while the watermark
|
|
24
|
+
* advances to the newest — that is the one that must be served for all of
|
|
25
|
+
* the subsumed commits to have been delivered.
|
|
26
|
+
*/
|
|
27
|
+
onVersionReady({ watermark, upstreamCommitTimeMs }) {
|
|
28
|
+
if (watermark === void 0 || upstreamCommitTimeMs === void 0) return;
|
|
29
|
+
const pending = this.#pending;
|
|
30
|
+
this.#pending = {
|
|
31
|
+
watermark,
|
|
32
|
+
commitTimeMs: pending === null ? upstreamCommitTimeMs : Math.min(pending.commitTimeMs, upstreamCommitTimeMs)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Called once a version has been poked to clients.
|
|
37
|
+
*
|
|
38
|
+
* @return the observation to record, or `null` if the served version does
|
|
39
|
+
* not yet cover an outstanding upstream commit.
|
|
40
|
+
*/
|
|
41
|
+
onVersionServed(servedVersion, nowMs) {
|
|
42
|
+
const pending = this.#pending;
|
|
43
|
+
if (pending === null || servedVersion < pending.watermark) return null;
|
|
44
|
+
this.#pending = null;
|
|
45
|
+
const lagMs = nowMs - pending.commitTimeMs;
|
|
46
|
+
if (lagMs >= 0) return {
|
|
47
|
+
lagMs,
|
|
48
|
+
clamped: false
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
lagMs: 0,
|
|
52
|
+
clamped: true
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { E2EServingLagTracker };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=e2e-serving-lag.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"e2e-serving-lag.js","names":["#pending"],"sources":["../../../../../../zero-cache/src/services/view-syncer/e2e-serving-lag.ts"],"sourcesContent":["import type {ReplicaState} from '../replicator/replicator.ts';\n\nexport type PendingUpstreamCommit = {\n readonly watermark: string;\n readonly commitTimeMs: number;\n};\n\n/**\n * Pairs the upstream commit timestamps carried by `version-ready`\n * notifications with the moment the corresponding change is poked to clients,\n * yielding the end-to-end serving lag.\n *\n * This measures completion, not backlog: an observation is produced only when\n * work actually reaches clients, so the resulting histogram is a latency\n * distribution rather than a periodic snapshot of how far behind things are.\n * A ViewSyncer that is stuck contributes nothing until it recovers, instead of\n * re-reporting its age on every sample tick.\n */\nexport class E2EServingLagTracker {\n #pending: PendingUpstreamCommit | null = null;\n\n get pending(): PendingUpstreamCommit | null {\n return this.#pending;\n }\n\n /**\n * Records the upstream commit behind a `version-ready` notification.\n *\n * Notifications coalesce when the ViewSyncer is busy, so one state may stand\n * in for several commits. The *oldest* commit time is kept, since it bounds\n * the lag of everything the notification subsumed, while the watermark\n * advances to the newest — that is the one that must be served for all of\n * the subsumed commits to have been delivered.\n */\n onVersionReady({watermark, upstreamCommitTimeMs}: ReplicaState): void {\n if (watermark === undefined || upstreamCommitTimeMs === undefined) {\n return;\n }\n const pending = this.#pending;\n this.#pending = {\n watermark,\n commitTimeMs:\n pending === null\n ? upstreamCommitTimeMs\n : Math.min(pending.commitTimeMs, upstreamCommitTimeMs),\n };\n }\n\n /**\n * Called once a version has been poked to clients.\n *\n * @return the observation to record, or `null` if the served version does\n * not yet cover an outstanding upstream commit.\n */\n onVersionServed(servedVersion: string, nowMs: number): Observation | null {\n const pending = this.#pending;\n if (pending === null || servedVersion < pending.watermark) {\n return null;\n }\n this.#pending = null;\n\n const lagMs = nowMs - pending.commitTimeMs;\n if (lagMs >= 0) {\n return {lagMs, clamped: false};\n }\n // The commit time is on the upstream database's clock while `nowMs` is\n // local, so a negative duration means upstream's clock is running ahead of\n // ours by more than the entire pipeline latency. Clamp, because a negative\n // value would corrupt the histogram's sum -- but report the clamp, since\n // it is proof of gross clock skew, and skew in this direction biases the\n // metric *low*, which reads as healthy serving rather than as a broken\n // measurement. See replication.upstream_clock_skew for the magnitude.\n return {lagMs: 0, clamped: true};\n }\n}\n\nexport type Observation = {\n /** End-to-end lag in milliseconds, clamped to be non-negative. */\n readonly lagMs: number;\n /** Whether the clamp was applied, i.e. the raw measurement was negative. */\n readonly clamped: boolean;\n};\n"],"mappings":";;;;;;;;;;;;AAkBA,IAAa,uBAAb,MAAkC;CAChC,WAAyC;CAEzC,IAAI,UAAwC;EAC1C,OAAO,KAAKA;CACd;;;;;;;;;;CAWA,eAAe,EAAC,WAAW,wBAA2C;EACpE,IAAI,cAAc,KAAA,KAAa,yBAAyB,KAAA,GACtD;EAEF,MAAM,UAAU,KAAKA;EACrB,KAAKA,WAAW;GACd;GACA,cACE,YAAY,OACR,uBACA,KAAK,IAAI,QAAQ,cAAc,oBAAoB;EAC3D;CACF;;;;;;;CAQA,gBAAgB,eAAuB,OAAmC;EACxE,MAAM,UAAU,KAAKA;EACrB,IAAI,YAAY,QAAQ,gBAAgB,QAAQ,WAC9C,OAAO;EAET,KAAKA,WAAW;EAEhB,MAAM,QAAQ,QAAQ,QAAQ;EAC9B,IAAI,SAAS,GACX,OAAO;GAAC;GAAO,SAAS;EAAK;EAS/B,OAAO;GAAC,OAAO;GAAG,SAAS;EAAI;CACjC;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"view-syncer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/view-syncer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAcjD,OAAO,KAAK,EAAC,2BAA2B,EAAC,MAAM,yDAAyD,CAAC;AACzG,OAAO,KAAK,EAEV,qBAAqB,EACtB,MAAM,0CAA0C,CAAC;AAElD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,iDAAiD,CAAC;AAC1F,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,uCAAuC,CAAC;AAQtE,OAAO,KAAK,EAEV,gBAAgB,EACjB,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,8CAA8C,CAAC;AAGpF,OAAO,EAAkB,KAAK,IAAI,EAAC,MAAM,oBAAoB,CAAC;AAK9D,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EACV,sBAAsB,EAEvB,MAAM,yCAAyC,CAAC;AAOjD,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,oCAAoC,CAAC;AAM1E,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,mBAAmB,CAAC;AAElD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,uBAAuB,CAAC;AACnD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAC,YAAY,EAAC,MAAM,6BAA6B,CAAC;AACzD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,6BAA6B,CAAC;AAE9D,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,eAAe,CAAC;AAQxD,OAAO,KAAK,EAEV,wBAAwB,EACxB,kBAAkB,EAEnB,MAAM,iCAAiC,CAAC;AAUzC,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"view-syncer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/view-syncer.ts"],"names":[],"mappings":"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"}
|
|
@@ -25,6 +25,7 @@ import { ClientHandler, startPoke } from "./client-handler.js";
|
|
|
25
25
|
import { tracer } from "./tracer.js";
|
|
26
26
|
import { CVRStore, ClientNotFoundError } from "./cvr-store.js";
|
|
27
27
|
import { CVRConfigDrivenUpdater, CVRQueryDrivenUpdater, nextEvictionTime } from "./cvr.js";
|
|
28
|
+
import { E2EServingLagTracker } from "./e2e-serving-lag.js";
|
|
28
29
|
import { handleInspect } from "./inspect-handler.js";
|
|
29
30
|
import { QueryCoveringIndex } from "./query-covering.js";
|
|
30
31
|
import { resolver } from "@rocicorp/resolver";
|
|
@@ -90,6 +91,7 @@ var ViewSyncerService = class {
|
|
|
90
91
|
#cvr;
|
|
91
92
|
#pipelinesSynced = false;
|
|
92
93
|
#servedVersion = null;
|
|
94
|
+
#e2eServingLagTracker = new E2EServingLagTracker();
|
|
93
95
|
#expiredQueriesTimer = 0;
|
|
94
96
|
#authMaintenanceTimer = 0;
|
|
95
97
|
#setTimeout;
|
|
@@ -102,6 +104,14 @@ var ViewSyncerService = class {
|
|
|
102
104
|
description: "Time from ViewSyncer query sync requiring hydration to output for a client group. Includes query transformation, query materialization, CVR flush, catchup, and pokeEnd.",
|
|
103
105
|
unit: "s"
|
|
104
106
|
});
|
|
107
|
+
#e2eServingLag = getOrCreateNativeHistogram("sync", "e2e_serving_lag", {
|
|
108
|
+
description: "End-to-end lag from upstream commit to ViewSyncer output. Spans the whole pipeline: the upstream transaction commit, replication to the replica, IVM advancement, CVR flush, and pokeEnd. Recorded once per served version, not sampled, so each observation is the completion latency of real replicated work.",
|
|
109
|
+
unit: "s"
|
|
110
|
+
});
|
|
111
|
+
#e2eServingLagClamps = getOrCreateCounter("sync", "e2e_serving_lag_clamps", {
|
|
112
|
+
description: "Observations of sync.e2e_serving_lag that came out negative and were clamped to zero. Non-zero means the upstream database clock is running ahead of this pod by more than the entire pipeline latency, so sync.e2e_serving_lag is biased low and reads healthier than reality. See replication.upstream_clock_skew for the magnitude.",
|
|
113
|
+
unit: "{observation}"
|
|
114
|
+
});
|
|
105
115
|
#transactionAdvanceTime = getOrCreateLatencyHistogram("sync", "advance-time", "Time to advance all queries for a given client group after applying a new transaction to the replica.");
|
|
106
116
|
#queryTransformations = getOrCreateCounter("sync", "query.transformations", "Number of query transformations performed");
|
|
107
117
|
#queryTransformationTime = getOrCreateLatencyHistogram("sync", "query.transformation-time", "Time to transform custom queries via API server.");
|
|
@@ -186,12 +196,14 @@ var ViewSyncerService = class {
|
|
|
186
196
|
this.#lc.debug?.(`draining view-syncer ${this.id} before running`);
|
|
187
197
|
this.stop();
|
|
188
198
|
}
|
|
189
|
-
for await (const
|
|
199
|
+
for await (const replicaState of this.#stateChanges) {
|
|
200
|
+
const { state } = replicaState;
|
|
190
201
|
if (this.#drainCoordinator.shouldDrain()) {
|
|
191
202
|
this.#lc.debug?.(`draining view-syncer ${this.id} (elective)`);
|
|
192
203
|
break;
|
|
193
204
|
}
|
|
194
205
|
assert(state === "version-ready", "state should be version-ready");
|
|
206
|
+
this.#e2eServingLagTracker.onVersionReady(replicaState);
|
|
195
207
|
await this.#runInLockWithCVR(async (lc, cvr) => {
|
|
196
208
|
const clientSchema = must(cvr.clientSchema, "cvr.clientSchema missing after initialization");
|
|
197
209
|
if (!this.#pipelines.initialized()) this.#pipelines.init(clientSchema);
|
|
@@ -258,6 +270,11 @@ var ViewSyncerService = class {
|
|
|
258
270
|
}
|
|
259
271
|
#markVersionServed(version) {
|
|
260
272
|
this.#servedVersion = version.stateVersion;
|
|
273
|
+
const observation = this.#e2eServingLagTracker.onVersionServed(version.stateVersion, Date.now());
|
|
274
|
+
if (observation !== null) {
|
|
275
|
+
this.#e2eServingLag.recordMs(observation.lagMs);
|
|
276
|
+
if (observation.clamped) this.#e2eServingLagClamps.add(1);
|
|
277
|
+
}
|
|
261
278
|
}
|
|
262
279
|
#keepAliveUntil = 0;
|
|
263
280
|
/**
|