@rocicorp/zero 1.9.0-canary.1 → 1.9.0-canary.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/out/zero/package.js +1 -1
  2. package/out/zero/package.js.map +1 -1
  3. package/out/zero-cache/src/config/zero-config.d.ts +5 -0
  4. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  5. package/out/zero-cache/src/config/zero-config.js +5 -0
  6. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  7. package/out/zero-cache/src/db/sqlite-corruption.d.ts +3 -2
  8. package/out/zero-cache/src/db/sqlite-corruption.d.ts.map +1 -1
  9. package/out/zero-cache/src/db/sqlite-corruption.js +11 -6
  10. package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -1
  11. package/out/zero-cache/src/observability/metrics.d.ts +1 -1
  12. package/out/zero-cache/src/observability/metrics.d.ts.map +1 -1
  13. package/out/zero-cache/src/observability/metrics.js +5 -1
  14. package/out/zero-cache/src/observability/metrics.js.map +1 -1
  15. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  16. package/out/zero-cache/src/server/change-streamer.js +2 -2
  17. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  18. package/out/zero-cache/src/server/main.d.ts.map +1 -1
  19. package/out/zero-cache/src/server/main.js +1 -1
  20. package/out/zero-cache/src/server/main.js.map +1 -1
  21. package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
  22. package/out/zero-cache/src/server/replicator.js +2 -2
  23. package/out/zero-cache/src/server/replicator.js.map +1 -1
  24. package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
  25. package/out/zero-cache/src/server/syncer.js +1 -1
  26. package/out/zero-cache/src/server/syncer.js.map +1 -1
  27. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  28. package/out/zero-cache/src/services/change-source/pg/change-source.js +4 -1
  29. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  30. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts +1 -0
  31. package/out/zero-cache/src/services/change-source/protocol/current/data.d.ts.map +1 -1
  32. package/out/zero-cache/src/services/change-source/protocol/current/data.js +4 -1
  33. package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
  34. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts +3 -0
  35. package/out/zero-cache/src/services/change-source/protocol/current/downstream.d.ts.map +1 -1
  36. package/out/zero-cache/src/services/change-source/protocol/current/status.d.ts +1 -0
  37. package/out/zero-cache/src/services/change-source/protocol/current/status.d.ts.map +1 -1
  38. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts +1 -0
  39. package/out/zero-cache/src/services/change-source/protocol/current/upstream.d.ts.map +1 -1
  40. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +1 -0
  41. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  42. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  43. package/out/zero-cache/src/services/life-cycle.js +1 -1
  44. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  45. package/out/zero-cache/src/services/replicator/change-processor.d.ts +6 -0
  46. package/out/zero-cache/src/services/replicator/change-processor.d.ts.map +1 -1
  47. package/out/zero-cache/src/services/replicator/change-processor.js +25 -7
  48. package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
  49. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  50. package/out/zero-cache/src/services/replicator/incremental-sync.js +8 -2
  51. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  52. package/out/zero-cache/src/services/replicator/notifier.d.ts +0 -22
  53. package/out/zero-cache/src/services/replicator/notifier.d.ts.map +1 -1
  54. package/out/zero-cache/src/services/replicator/notifier.js +7 -1
  55. package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
  56. package/out/zero-cache/src/services/replicator/replicator.d.ts +16 -1
  57. package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
  58. package/out/zero-cache/src/services/replicator/replicator.js +7 -1
  59. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  60. package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts +27 -0
  61. package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts.map +1 -1
  62. package/out/zero-cache/src/services/replicator/reporter/recorder.js +32 -0
  63. package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
  64. package/out/zero-cache/src/services/replicator/reporter/report-schema.d.ts +1 -1
  65. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.d.ts +44 -0
  66. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.d.ts.map +1 -0
  67. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js +59 -0
  68. package/out/zero-cache/src/services/view-syncer/e2e-serving-lag.js.map +1 -0
  69. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  70. package/out/zero-cache/src/services/view-syncer/view-syncer.js +18 -1
  71. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  72. package/out/zero-client/src/client/version.js +1 -1
  73. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"incremental-sync.js","names":["#lc","#taskID","#id","#changeStreamer","#worker","#mode","#statusPublisher","#notifier","#reporter","#state","#replicationEvents","#handleResult"],"sources":["../../../../../../zero-cache/src/services/replicator/incremental-sync.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {AbortError} from '../../../../shared/src/abort-error.ts';\nimport type {Enum} from '../../../../shared/src/enum.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport type {Source} from '../../types/streams.ts';\nimport type {DownloadStatus} from '../change-source/protocol/current.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport {\n errorTypeToReadableName,\n PROTOCOL_VERSION,\n type ChangeStreamer,\n type Downstream,\n} from '../change-streamer/change-streamer.ts';\nimport type * as ErrorType from '../change-streamer/error-type-enum.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {CommitResult} from './change-processor.ts';\nimport {Notifier} from './notifier.ts';\nimport type {ReplicationStatusPublisher} from './replication-status.ts';\nimport type {ReplicaState, ReplicatorMode} from './replicator.ts';\nimport {ReplicationReportRecorder} from './reporter/recorder.ts';\nimport type {ReplicationReport} from './reporter/report-schema.ts';\nimport type {WriteWorkerClient} from './write-worker-client.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\n/**\n * The {@link IncrementalSyncer} manages a logical replication stream from upstream,\n * handling application lifecycle events (start, stop) and retrying the\n * connection with exponential backoff. The actual handling of the logical\n * replication messages is done by the {@link ChangeProcessor}, which runs\n * in a worker thread via the {@link WriteWorkerClient}.\n */\nexport class IncrementalSyncer {\n readonly #lc: LogContext;\n readonly #taskID: string;\n readonly #id: string;\n readonly #changeStreamer: ChangeStreamer;\n readonly #worker: WriteWorkerClient;\n readonly #mode: ReplicatorMode;\n readonly #statusPublisher: ReplicationStatusPublisher | null;\n readonly #notifier: Notifier;\n readonly #reporter: ReplicationReportRecorder;\n\n readonly #state = new RunningState('IncrementalSyncer');\n\n readonly #replicationEvents = getOrCreateCounter(\n 'replication',\n 'events',\n 'Number of replication events processed',\n );\n\n constructor(\n lc: LogContext,\n taskID: string,\n id: string,\n changeStreamer: ChangeStreamer,\n worker: WriteWorkerClient,\n mode: ReplicatorMode,\n statusPublisher: ReplicationStatusPublisher | null,\n ) {\n this.#lc = lc;\n this.#taskID = taskID;\n this.#id = id;\n this.#changeStreamer = changeStreamer;\n this.#worker = worker;\n this.#mode = mode;\n this.#statusPublisher = statusPublisher;\n this.#notifier = new Notifier();\n this.#reporter = new ReplicationReportRecorder(lc);\n }\n\n async run() {\n const lc = this.#lc;\n this.#worker.onError(err => this.#state.stop(lc, err));\n lc.info?.(`Starting IncrementalSyncer`);\n const {watermark: initialWatermark} =\n await this.#worker.getSubscriptionState();\n\n // Notify any waiting subscribers that the replica is ready to be read.\n // This initial notification intentionally omits replicaReadyTimeMs because\n // it represents already-current state, not newly-unserved work.\n void this.#notifier.notifySubscribers({\n state: 'version-ready',\n watermark: initialWatermark,\n });\n\n while (this.#state.shouldRun()) {\n const {replicaVersion, watermark} =\n await this.#worker.getSubscriptionState();\n\n let downstream: Source<Downstream> | undefined;\n let unregister = () => {};\n let err: unknown | undefined;\n\n try {\n downstream = await this.#changeStreamer.subscribe({\n protocolVersion: PROTOCOL_VERSION,\n taskID: this.#taskID,\n id: this.#id,\n mode: this.#mode,\n watermark,\n replicaVersion,\n initial: watermark === initialWatermark,\n });\n this.#state.resetBackoff();\n unregister = this.#state.cancelOnStop(downstream);\n this.#statusPublisher?.publish(\n lc,\n 'Replicating',\n `Replicating from ${watermark}`,\n );\n\n let backfillStatus: DownloadStatus | undefined;\n\n for await (const message of downstream) {\n this.#replicationEvents.add(1);\n switch (message[0]) {\n case 'status': {\n const {lagReport} = message[1];\n if (lagReport) {\n const report: ReplicationReport = {\n nextSendTimeMs: lagReport.nextSendTimeMs,\n };\n if (lagReport.lastTimings) {\n report.lastTimings = {\n ...lagReport.lastTimings,\n replicateTimeMs: Date.now(),\n };\n }\n this.#reporter.record(report);\n }\n break;\n }\n case 'error': {\n // Signal from the replication-manager that the view-syncer must\n // shut down and restore a new backup from litestream.\n const {type, message: msg} = message[1];\n this.stop(\n lc,\n // Note: The AbortError indicates a clean / intentional shutdown.\n new AbortError(\n `${errorTypeToReadableName(type as ErrorType)}: ${msg}`,\n ),\n );\n break;\n }\n default: {\n const msg = message[1];\n if (msg.tag === 'backfill' && msg.status) {\n const {status} = msg;\n if (!backfillStatus) {\n // Start publishing the status every 3 seconds.\n backfillStatus = status;\n this.#statusPublisher?.publish(\n lc,\n 'Replicating',\n `Backfilling ${msg.relation.name} table`,\n 3000,\n () =>\n backfillStatus\n ? {\n downloadStatus: [\n {\n ...backfillStatus,\n table: msg.relation.name,\n columns: [\n ...msg.relation.rowKey.columns,\n ...msg.columns,\n ],\n },\n ],\n }\n : {},\n );\n }\n backfillStatus = status; // Update the current status\n }\n\n const result = await this.#worker.processMessage(\n message as ChangeStreamData,\n );\n\n this.#handleResult(lc, result);\n if (result?.completedBackfill) {\n backfillStatus = undefined;\n }\n break;\n }\n }\n }\n this.#worker.abort();\n } catch (e) {\n err = e;\n this.#worker.abort();\n } finally {\n downstream?.cancel();\n unregister();\n this.#statusPublisher?.stop();\n }\n await this.#state.backoff(lc, err);\n }\n lc.info?.('IncrementalSyncer stopped');\n }\n\n #handleResult(lc: LogContext, result: CommitResult | null) {\n if (!result) {\n return;\n }\n if (result.completedBackfill) {\n // Publish the final status\n const status = result.completedBackfill;\n this.#statusPublisher?.publish(\n lc,\n 'Replicating',\n `Backfilled ${status.table} table`,\n 0,\n () => ({downloadStatus: [status]}),\n );\n } else if (result.schemaUpdated) {\n this.#statusPublisher?.publish(lc, 'Replicating', 'Schema updated');\n }\n if (result.watermark && result.changeLogUpdated) {\n void this.#notifier.notifySubscribers({\n state: 'version-ready',\n watermark: result.watermark,\n replicaReadyTimeMs: Date.now(),\n });\n }\n }\n\n subscribe(): Source<ReplicaState> {\n return this.#notifier.subscribe();\n }\n\n stop(lc: LogContext, err?: unknown) {\n this.#state.stop(lc, err);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"incremental-sync.js","names":["#lc","#taskID","#id","#changeStreamer","#worker","#mode","#statusPublisher","#notifier","#reporter","#state","#replicationEvents","#handleResult"],"sources":["../../../../../../zero-cache/src/services/replicator/incremental-sync.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {AbortError} from '../../../../shared/src/abort-error.ts';\nimport type {Enum} from '../../../../shared/src/enum.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport type {Source} from '../../types/streams.ts';\nimport type {DownloadStatus} from '../change-source/protocol/current.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport {\n errorTypeToReadableName,\n PROTOCOL_VERSION,\n type ChangeStreamer,\n type Downstream,\n} from '../change-streamer/change-streamer.ts';\nimport type * as ErrorType from '../change-streamer/error-type-enum.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {CommitResult} from './change-processor.ts';\nimport {Notifier} from './notifier.ts';\nimport type {ReplicationStatusPublisher} from './replication-status.ts';\nimport type {ReplicaState, ReplicatorMode} from './replicator.ts';\nimport {ReplicationReportRecorder} from './reporter/recorder.ts';\nimport type {ReplicationReport} from './reporter/report-schema.ts';\nimport type {WriteWorkerClient} from './write-worker-client.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\n/**\n * The {@link IncrementalSyncer} manages a logical replication stream from upstream,\n * handling application lifecycle events (start, stop) and retrying the\n * connection with exponential backoff. The actual handling of the logical\n * replication messages is done by the {@link ChangeProcessor}, which runs\n * in a worker thread via the {@link WriteWorkerClient}.\n */\nexport class IncrementalSyncer {\n readonly #lc: LogContext;\n readonly #taskID: string;\n readonly #id: string;\n readonly #changeStreamer: ChangeStreamer;\n readonly #worker: WriteWorkerClient;\n readonly #mode: ReplicatorMode;\n readonly #statusPublisher: ReplicationStatusPublisher | null;\n readonly #notifier: Notifier;\n readonly #reporter: ReplicationReportRecorder;\n\n readonly #state = new RunningState('IncrementalSyncer');\n\n readonly #replicationEvents = getOrCreateCounter(\n 'replication',\n 'events',\n 'Number of replication events processed',\n );\n\n constructor(\n lc: LogContext,\n taskID: string,\n id: string,\n changeStreamer: ChangeStreamer,\n worker: WriteWorkerClient,\n mode: ReplicatorMode,\n statusPublisher: ReplicationStatusPublisher | null,\n ) {\n this.#lc = lc;\n this.#taskID = taskID;\n this.#id = id;\n this.#changeStreamer = changeStreamer;\n this.#worker = worker;\n this.#mode = mode;\n this.#statusPublisher = statusPublisher;\n this.#notifier = new Notifier();\n this.#reporter = new ReplicationReportRecorder(lc);\n }\n\n async run() {\n const lc = this.#lc;\n let workerError: Error | undefined;\n this.#worker.onError(err => {\n workerError ??= err;\n this.#state.stop(lc, err);\n });\n lc.info?.(`Starting IncrementalSyncer`);\n const {watermark: initialWatermark} =\n await this.#worker.getSubscriptionState();\n\n // Notify any waiting subscribers that the replica is ready to be read.\n // This initial notification intentionally omits replicaReadyTimeMs because\n // it represents already-current state, not newly-unserved work.\n void this.#notifier.notifySubscribers({\n state: 'version-ready',\n watermark: initialWatermark,\n });\n\n while (this.#state.shouldRun()) {\n const {replicaVersion, watermark} =\n await this.#worker.getSubscriptionState();\n\n let downstream: Source<Downstream> | undefined;\n let unregister = () => {};\n let err: unknown | undefined;\n\n try {\n downstream = await this.#changeStreamer.subscribe({\n protocolVersion: PROTOCOL_VERSION,\n taskID: this.#taskID,\n id: this.#id,\n mode: this.#mode,\n watermark,\n replicaVersion,\n initial: watermark === initialWatermark,\n });\n this.#state.resetBackoff();\n unregister = this.#state.cancelOnStop(downstream);\n this.#statusPublisher?.publish(\n lc,\n 'Replicating',\n `Replicating from ${watermark}`,\n );\n\n let backfillStatus: DownloadStatus | undefined;\n\n for await (const message of downstream) {\n this.#replicationEvents.add(1);\n switch (message[0]) {\n case 'status': {\n const {lagReport} = message[1];\n if (lagReport) {\n const report: ReplicationReport = {\n nextSendTimeMs: lagReport.nextSendTimeMs,\n };\n if (lagReport.lastTimings) {\n report.lastTimings = {\n ...lagReport.lastTimings,\n replicateTimeMs: Date.now(),\n };\n }\n this.#reporter.record(report);\n }\n break;\n }\n case 'error': {\n // Signal from the replication-manager that the view-syncer must\n // shut down and restore a new backup from litestream.\n const {type, message: msg} = message[1];\n this.stop(\n lc,\n // Note: The AbortError indicates a clean / intentional shutdown.\n new AbortError(\n `${errorTypeToReadableName(type as ErrorType)}: ${msg}`,\n ),\n );\n break;\n }\n default: {\n const msg = message[1];\n if (msg.tag === 'backfill' && msg.status) {\n const {status} = msg;\n if (!backfillStatus) {\n // Start publishing the status every 3 seconds.\n backfillStatus = status;\n this.#statusPublisher?.publish(\n lc,\n 'Replicating',\n `Backfilling ${msg.relation.name} table`,\n 3000,\n () =>\n backfillStatus\n ? {\n downloadStatus: [\n {\n ...backfillStatus,\n table: msg.relation.name,\n columns: [\n ...msg.relation.rowKey.columns,\n ...msg.columns,\n ],\n },\n ],\n }\n : {},\n );\n }\n backfillStatus = status; // Update the current status\n }\n\n const result = await this.#worker.processMessage(\n message as ChangeStreamData,\n );\n\n this.#handleResult(lc, result);\n if (result?.completedBackfill) {\n backfillStatus = undefined;\n }\n break;\n }\n }\n }\n this.#worker.abort();\n } catch (e) {\n err = e;\n this.#worker.abort();\n } finally {\n downstream?.cancel();\n unregister();\n this.#statusPublisher?.stop();\n }\n await this.#state.backoff(lc, err);\n }\n lc.info?.('IncrementalSyncer stopped');\n if (workerError) {\n throw workerError;\n }\n }\n\n #handleResult(lc: LogContext, result: CommitResult | null) {\n if (!result) {\n return;\n }\n if (result.completedBackfill) {\n // Publish the final status\n const status = result.completedBackfill;\n this.#statusPublisher?.publish(\n lc,\n 'Replicating',\n `Backfilled ${status.table} table`,\n 0,\n () => ({downloadStatus: [status]}),\n );\n } else if (result.schemaUpdated) {\n this.#statusPublisher?.publish(lc, 'Replicating', 'Schema updated');\n }\n if (result.watermark && result.changeLogUpdated) {\n void this.#notifier.notifySubscribers({\n state: 'version-ready',\n watermark: result.watermark,\n replicaReadyTimeMs: Date.now(),\n upstreamCommitTimeMs: result.upstreamCommitTimeMs,\n });\n }\n }\n\n subscribe(): Source<ReplicaState> {\n return this.#notifier.subscribe();\n }\n\n stop(lc: LogContext, err?: unknown) {\n this.#state.stop(lc, err);\n }\n}\n"],"mappings":"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"}
@@ -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;AAExE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,QAAS,YAAW,oBAAoB;;IAInD,IAAI,WAAW,IAAI,YAAY,GAAG,SAAS,CAE1C;IAmBD,SAAS,IAAI,YAAY,CAAC,YAAY,CAAC;IAWvC,iBAAiB,CACf,KAAK,GAAE,YAAuC,GAC7C,OAAO,CAAC,MAAM,CAAC,EAAE;CAOrB"}
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: curr.replicaReadyTimeMs === void 0 ? prev.replicaReadyTimeMs : prev.replicaReadyTimeMs === void 0 ? curr.replicaReadyTimeMs : Math.min(curr.replicaReadyTimeMs, prev.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:\n curr.replicaReadyTimeMs === undefined\n ? prev.replicaReadyTimeMs\n : prev.replicaReadyTimeMs === undefined\n ? curr.replicaReadyTimeMs\n : Math.min(curr.replicaReadyTimeMs, prev.replicaReadyTimeMs),\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,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;GACrD,WAAW,MAAM,UAAU;IACzB,GAAG;IACH,oBACE,KAAK,uBAAuB,KAAA,IACxB,KAAK,qBACL,KAAK,uBAAuB,KAAA,IAC1B,KAAK,qBACL,KAAK,IAAI,KAAK,oBAAoB,KAAK,kBAAkB;GACnE;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"}
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 { ReplicationStatusPublisher } from './replication-status.ts';
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,EAAC,0BAA0B,EAAC,MAAM,yBAAyB,CAAC;AACxE,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;IAIjD,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;gBAOlB,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;IAmBpD,MAAM;;;IAIN,GAAG;IAKH,SAAS,IAAI,MAAM,CAAC,YAAY,CAAC;IAI3B,IAAI;CAQX"}
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 {ReplicationStatusPublisher} 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 // 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 #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\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();\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":";;AAgEA,IAAa,oBAAb,MAA8D;CAC5D;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,KAAKE,UAAU;EAEf,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,KAAKG,cAAc,KAAKF,mBAAmB,IAAI;EAC/C,OAAO,KAAKE;CACd;CAEA,YAAkC;EAChC,OAAO,KAAKF,mBAAmB,UAAU;CAC3C;CAEA,MAAM,OAAO;EACX,KAAKA,mBAAmB,KAAK,KAAKD,GAAG;EAIrC,MAAM,KAAKG;EACX,MAAM,KAAKD,QAAQ,KAAK;CAC1B;AACF"}
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;IAoDhC,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;CACH"}
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;EACxC;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;AACF"}
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
  }>;
@@ -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":"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"}
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 { state } of this.#stateChanges) {
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
  /**