@rocicorp/zero 1.11.0 → 1.12.0-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/analyze-query/src/analyze-cli.js +2 -2
- package/out/analyze-query/src/analyze-cli.js.map +1 -1
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +9 -6
- package/out/zero-cache/src/config/zero-config.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 +14 -5
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +7 -5
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +11 -7
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +37 -30
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.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 +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replication-status.d.ts +18 -0
- package/out/zero-cache/src/services/replicator/replication-status.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/replication-status.js +54 -4
- package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
- package/out/zero-cache/src/services/statz.d.ts.map +1 -1
- package/out/zero-cache/src/services/statz.js +1 -1
- package/out/zero-cache/src/services/statz.js.map +1 -1
- package/out/zero-cache/src/workers/connection.js +3 -3
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +2 -2
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"incremental-sync.js","names":["#lc","#taskID","#id","#changeStreamer","#worker","#mode","#statusPublisher","#notifier","#reporter","#drainHandler","#state","#replicationEvents","#initialConnection","#committedContext","#subscriberContext","#handleResult","#olderReplicaVersionRejections"],"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 {Database} from '../../../../zqlite/src/db.ts';\nimport {mapPostgresToLiteIndex} from '../../db/pg-to-lite.ts';\nimport {StatementRunner} from '../../db/statements.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 type {ReservationFollowup} from '../change-streamer/change-streamer-http.ts';\nimport {\n errorTypeToReadableName,\n PROTOCOL_VERSION,\n type ChangeStreamer,\n type SubscriberContext,\n} from '../change-streamer/change-streamer.ts';\nimport type * as ErrorType from '../change-streamer/error-type-enum.ts';\nimport {WrongReplicaVersion} from '../change-streamer/error-type-enum.ts';\nimport type {SubscribeDownstream} from '../change-streamer/subscribe.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {CommitResult} from './change-processor.ts';\nimport {\n DrainHandler,\n type Downstream,\n type SwitchoverOptions,\n} from './drain-handler.ts';\nimport {Notifier} from './notifier.ts';\nimport {\n IndexingProgress,\n type ReplicationStatusPublisher,\n} from './replication-status.ts';\nimport type {ReplicaState, ReplicatorMode} from './replicator.ts';\nimport {\n ReplicationReportRecorder,\n type ReplicationLagReadinessOptions,\n} from './reporter/recorder.ts';\nimport type {ReplicationReport} from './reporter/report-schema.ts';\nimport {readReplicaBackfills} from './schema/backfilling.ts';\nimport {getSubscriptionState} from './schema/replication-state.ts';\nimport type {WriteWorkerClient} from './write-worker-client.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\nconst MAX_WORKER_BATCH_MESSAGES = 256;\nconst MAX_WORKER_BATCH_SIZE = 1024 * 1024;\n\n// The number of times to reconnect after being rejected by a\n// replication-manager with an older replica version (e.g. one that has yet\n// to be replaced by a resync), before accepting the rejection and restarting\n// (e.g. if the upstream was legitimately replaced by one with a lower replica\n// version).\nconst MAX_OLDER_REPLICA_VERSION_REJECTIONS = 20;\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 // Consumed (and cleared) by the first subscribe() in run()'s loop; every\n // subsequent (re)connection goes through #changeStreamer.subscribe().\n #initialConnection: ReservationFollowup | undefined;\n\n // Only set for serving replicas that switch over between\n // replication-managers.\n readonly #drainHandler: DrainHandler | undefined;\n #olderReplicaVersionRejections = 0;\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 replicationLagReadinessOptions: ReplicationLagReadinessOptions = {},\n initialConnection?: ReservationFollowup,\n switchover?: SwitchoverOptions,\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(\n lc,\n replicationLagReadinessOptions,\n );\n this.#initialConnection = initialConnection;\n // Only serving replicas switch over between replication-managers. In\n // particular, the in-RM backup-replicator never does.\n this.#drainHandler =\n mode === 'serving' && switchover\n ? new DrainHandler(lc, changeStreamer, this.#state, switchover, () =>\n this.#committedContext(switchover.replicaFile),\n )\n : undefined;\n }\n\n ready(): Promise<void> {\n return this.#reporter.ready();\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, backfills} =\n await this.#worker.getSubscriptionState();\n\n let downstream: Downstream | undefined;\n let unregister = () => {};\n let err: unknown | undefined;\n\n try {\n const ctx = this.#subscriberContext(\n replicaVersion,\n watermark,\n backfills,\n );\n // A successful switchover has already established the subscription\n // on a new connection.\n const switched = this.#drainHandler?.getNextConnection();\n // The first connection reuses the still-open reservation connection\n // (if any) from the backup restore, so the replication-manager that\n // reserved the change log is the one that serves the subscription.\n // Every reconnect after that opens a fresh connection.\n const initialConnection = this.#initialConnection;\n this.#initialConnection = undefined;\n downstream =\n switched ??\n (initialConnection\n ? await initialConnection.subscribe(ctx)\n : await this.#changeStreamer.subscribe(ctx));\n unregister = this.#state.cancelOnStop(downstream);\n this.#statusPublisher?.publish(\n lc,\n 'Replicating',\n `Replicating from ${watermark}`,\n );\n\n let backfill:\n | {status: DownloadStatus; table: string; columns: string[]}\n | undefined;\n let writeBatch: ChangeStreamData[] = [];\n let writeBatchSize = 0;\n let inTransaction = false;\n // Indexes created in the current run of consecutive index creations.\n let indexing: IndexingProgress | undefined;\n\n const publishBackfillStatus = (table: string) =>\n this.#statusPublisher?.publish(\n lc,\n 'Replicating',\n `Backfilling ${table} table`,\n 3000,\n () =>\n backfill\n ? {\n downloadStatus: [\n {\n ...backfill.status,\n table: backfill.table,\n columns: backfill.columns,\n },\n ],\n }\n : {},\n );\n\n const flushWrites = async () => {\n if (writeBatch.length === 0) {\n return;\n }\n const batch = writeBatch;\n writeBatch = [];\n writeBatchSize = 0;\n\n const result = await this.#worker.processMessages(batch);\n this.#handleResult(lc, result);\n if (result?.completedBackfill) {\n backfill = undefined;\n }\n };\n\n // Whether the current transaction is being skipped.\n let skipping = false;\n\n // A subscription established by a switchover starts at the\n // watermark at which it was requested, and may thus include\n // transactions that were since committed from the previous\n // subscription.\n const shouldSkip = (message: SubscribeDownstream) => {\n const type = message[0];\n if (type === 'begin' && message[2].commitWatermark <= watermark) {\n lc.debug?.(\n `skipping committed transaction ${message[2].commitWatermark}`,\n );\n skipping = true;\n }\n if (\n skipping &&\n (type === 'begin' ||\n type === 'data' ||\n type === 'commit' ||\n type === 'rollback')\n ) {\n skipping = type !== 'commit' && type !== 'rollback';\n return true;\n }\n return false;\n };\n\n for await (const {data: message, size} of downstream) {\n if (shouldSkip(message)) {\n continue;\n }\n // Only reset backoff after receiving an actual message.\n this.#state.resetBackoff();\n\n this.#replicationEvents.add(1);\n switch (message[0]) {\n case 'reserved':\n lc.warn?.(`unexpected snapshot reservation message received`, {\n message: message[0],\n });\n break;\n case 'status': {\n const {lagReport, draining} = message[1];\n this.#olderReplicaVersionRejections = 0;\n if (draining) {\n this.#drainHandler?.onChangeStreamerDrain(downstream);\n }\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 const {\n type,\n message: msg,\n replicaVersion: rmVersion,\n } = message[1];\n if (\n type === WrongReplicaVersion &&\n rmVersion !== undefined &&\n rmVersion < replicaVersion &&\n this.#olderReplicaVersionRejections++ <\n MAX_OLDER_REPLICA_VERSION_REJECTIONS\n ) {\n // The replication-manager is behind this replica (e.g. it\n // has yet to be replaced by a resync). Rather than restoring\n // its older replica, reconnect (with backoff), which may\n // reach a different replication-manager.\n lc.warn?.(\n `replication-manager is at an older replica version ` +\n `(${rmVersion}) than ${replicaVersion}. reconnecting`,\n );\n break;\n }\n // Signal from the replication-manager that the view-syncer must\n // shut down and restore a new backup from litestream.\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, relation} = msg;\n const first = !backfill;\n backfill = {\n status, // Update the current status\n table: relation.name,\n columns: [...relation.rowKey.columns, ...msg.columns],\n };\n if (first) {\n // Start publishing the status every 3 seconds.\n publishBackfillStatus(relation.name);\n }\n }\n\n if (msg.tag === 'create-index' && this.#statusPublisher) {\n // Creating an index on an existing table can take a long\n // time. Flush preceding changes so that the index creation\n // is processed (and timed) on its own.\n await flushWrites();\n const index = mapPostgresToLiteIndex(msg.spec);\n indexing ??= new IndexingProgress();\n indexing.start(index);\n this.#statusPublisher.publish(\n lc,\n 'Replicating',\n `Creating index ${index.name} on ${index.tableName}`,\n 3000,\n indexing.state,\n );\n // Exclude the time spent publishing the status.\n indexing.restartTimer();\n writeBatch.push(message as ChangeStreamData);\n writeBatchSize += size;\n await flushWrites();\n const elapsed = indexing.finish();\n lc.info?.(\n `Created index ${index.name} (${elapsed.toFixed(3)} ms)`,\n );\n this.#statusPublisher.publish(\n lc,\n 'Replicating',\n `Created index ${index.name} on ${index.tableName}`,\n 0,\n indexing.state,\n );\n if (backfill) {\n // Resume reporting the progress of the ongoing backfill.\n publishBackfillStatus(backfill.table);\n }\n break;\n }\n indexing = undefined;\n\n const type = message[0];\n const invalidTransactionSequence =\n type === 'begin' ? inTransaction : !inTransaction;\n if (type === 'begin') {\n inTransaction = true;\n } else if (type === 'commit' || type === 'rollback') {\n inTransaction = false;\n }\n\n writeBatch.push(message as ChangeStreamData);\n writeBatchSize += size;\n if (\n // Waiting here ensures that consuming the commit, which ACKs\n // it upstream, only happens after the SQLite commit finishes.\n type === 'commit' ||\n type === 'rollback' ||\n // Promptly surface malformed transaction sequences instead\n // of leaving a partial batch waiting for another message.\n invalidTransactionSequence ||\n writeBatch.length >= MAX_WORKER_BATCH_MESSAGES ||\n writeBatchSize >= MAX_WORKER_BATCH_SIZE\n ) {\n await flushWrites();\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 if (err === undefined && this.#drainHandler?.hasNextConnection) {\n continue; // Switched over to a new subscription; no backoff.\n }\n this.#drainHandler?.getNextConnection()?.cancel();\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 #subscriberContext(\n replicaVersion: string,\n watermark: string,\n backfills: SubscriberContext['backfills'],\n ): SubscriberContext {\n return {\n protocolVersion: PROTOCOL_VERSION,\n taskID: this.#taskID,\n id: this.#id,\n mode: this.#mode,\n watermark,\n replicaVersion,\n backfills,\n ...(this.#drainHandler ? {drainable: true} : {}),\n };\n }\n\n /**\n * Returns the subscriber context as of the last committed transaction,\n * read from the replica file (i.e. not from the write worker, whose\n * connection may be in the middle of a transaction).\n */\n #committedContext(replicaFile: string): SubscriberContext {\n const db = new Database(this.#lc, replicaFile, {readonly: true});\n try {\n const {replicaVersion, watermark, backfills} = db.transaction(() => ({\n ...getSubscriptionState(new StatementRunner(db)),\n backfills: readReplicaBackfills(db),\n }));\n return this.#subscriberContext(replicaVersion, watermark, backfills);\n } finally {\n db.close();\n }\n }\n\n stop(lc: LogContext, err?: unknown) {\n this.#state.stop(lc, err);\n // Abort any polling loop that the write worker may be in.\n this.#worker.abort();\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","#drainHandler","#state","#replicationEvents","#initialConnection","#committedContext","#subscriberContext","#handleResult","#olderReplicaVersionRejections"],"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 {Database} from '../../../../zqlite/src/db.ts';\nimport {mapPostgresToLiteIndex} from '../../db/pg-to-lite.ts';\nimport {StatementRunner} from '../../db/statements.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 type {ReservationFollowup} from '../change-streamer/change-streamer-http.ts';\nimport {\n errorTypeToReadableName,\n PROTOCOL_VERSION,\n type ChangeStreamer,\n type SubscriberContext,\n} from '../change-streamer/change-streamer.ts';\nimport type * as ErrorType from '../change-streamer/error-type-enum.ts';\nimport {WrongReplicaVersion} from '../change-streamer/error-type-enum.ts';\nimport type {SubscribeDownstream} from '../change-streamer/subscribe.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {CommitResult} from './change-processor.ts';\nimport {\n DrainHandler,\n type Downstream,\n type SwitchoverOptions,\n} from './drain-handler.ts';\nimport {Notifier} from './notifier.ts';\nimport {\n IndexingProgress,\n type ReplicationStatusPublisher,\n} from './replication-status.ts';\nimport type {ReplicaState, ReplicatorMode} from './replicator.ts';\nimport {\n ReplicationReportRecorder,\n type ReplicationLagReadinessOptions,\n} from './reporter/recorder.ts';\nimport type {ReplicationReport} from './reporter/report-schema.ts';\nimport {readReplicaBackfills} from './schema/backfilling.ts';\nimport {getSubscriptionState} from './schema/replication-state.ts';\nimport type {WriteWorkerClient} from './write-worker-client.ts';\n\ntype ErrorType = Enum<typeof ErrorType>;\n\nconst MAX_WORKER_BATCH_MESSAGES = 256;\nconst MAX_WORKER_BATCH_SIZE = 1024 * 1024;\n\n// The number of times to reconnect after being rejected by a\n// replication-manager with an older replica version (e.g. one that has yet\n// to be replaced by a resync), before accepting the rejection and restarting\n// (e.g. if the upstream was legitimately replaced by one with a lower replica\n// version).\nconst MAX_OLDER_REPLICA_VERSION_REJECTIONS = 20;\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 // Consumed (and cleared) by the first subscribe() in run()'s loop; every\n // subsequent (re)connection goes through #changeStreamer.subscribe().\n #initialConnection: ReservationFollowup | undefined;\n\n // Only set for serving replicas that switch over between\n // replication-managers.\n readonly #drainHandler: DrainHandler | undefined;\n #olderReplicaVersionRejections = 0;\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 replicationLagReadinessOptions: ReplicationLagReadinessOptions = {},\n initialConnection?: ReservationFollowup,\n switchover?: SwitchoverOptions,\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(\n lc,\n replicationLagReadinessOptions,\n );\n this.#initialConnection = initialConnection;\n // Only serving replicas switch over between replication-managers. In\n // particular, the in-RM backup-replicator never does.\n this.#drainHandler =\n mode === 'serving' && switchover\n ? new DrainHandler(lc, changeStreamer, this.#state, switchover, () =>\n this.#committedContext(switchover.replicaFile),\n )\n : undefined;\n }\n\n ready(): Promise<void> {\n return this.#reporter.ready();\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, backfills} =\n await this.#worker.getSubscriptionState();\n\n let downstream: Downstream | undefined;\n let unregister = () => {};\n let err: unknown | undefined;\n\n try {\n const ctx = this.#subscriberContext(\n replicaVersion,\n watermark,\n backfills,\n );\n // A successful switchover has already established the subscription\n // on a new connection.\n const switched = this.#drainHandler?.getNextConnection();\n // The first connection reuses the still-open reservation connection\n // (if any) from the backup restore, so the replication-manager that\n // reserved the change log is the one that serves the subscription.\n // Every reconnect after that opens a fresh connection.\n const initialConnection = this.#initialConnection;\n this.#initialConnection = undefined;\n downstream =\n switched ??\n (initialConnection\n ? await initialConnection.subscribe(ctx)\n : await this.#changeStreamer.subscribe(ctx));\n unregister = this.#state.cancelOnStop(downstream);\n this.#statusPublisher\n ?.publish(lc, 'Replicating', `Replicating from ${watermark}`)\n .watchGrowth(lc);\n\n let backfill:\n | {status: DownloadStatus; table: string; columns: string[]}\n | undefined;\n let writeBatch: ChangeStreamData[] = [];\n let writeBatchSize = 0;\n let inTransaction = false;\n // Indexes created in the current run of consecutive index creations.\n let indexing: IndexingProgress | undefined;\n\n const publishBackfillStatus = (table: string) =>\n this.#statusPublisher?.publish(\n lc,\n 'Replicating',\n `Backfilling ${table} table`,\n 3000,\n () =>\n backfill\n ? {\n downloadStatus: [\n {\n ...backfill.status,\n table: backfill.table,\n columns: backfill.columns,\n },\n ],\n }\n : {},\n );\n\n const flushWrites = async () => {\n if (writeBatch.length === 0) {\n return;\n }\n const batch = writeBatch;\n writeBatch = [];\n writeBatchSize = 0;\n\n const result = await this.#worker.processMessages(batch);\n this.#handleResult(lc, result);\n if (result?.completedBackfill) {\n backfill = undefined;\n }\n };\n\n // Whether the current transaction is being skipped.\n let skipping = false;\n\n // A subscription established by a switchover starts at the\n // watermark at which it was requested, and may thus include\n // transactions that were since committed from the previous\n // subscription.\n const shouldSkip = (message: SubscribeDownstream) => {\n const type = message[0];\n if (type === 'begin' && message[2].commitWatermark <= watermark) {\n lc.debug?.(\n `skipping committed transaction ${message[2].commitWatermark}`,\n );\n skipping = true;\n }\n if (\n skipping &&\n (type === 'begin' ||\n type === 'data' ||\n type === 'commit' ||\n type === 'rollback')\n ) {\n skipping = type !== 'commit' && type !== 'rollback';\n return true;\n }\n return false;\n };\n\n for await (const {data: message, size} of downstream) {\n if (shouldSkip(message)) {\n continue;\n }\n // Only reset backoff after receiving an actual message.\n this.#state.resetBackoff();\n\n this.#replicationEvents.add(1);\n switch (message[0]) {\n case 'reserved':\n lc.warn?.(`unexpected snapshot reservation message received`, {\n message: message[0],\n });\n break;\n case 'status': {\n const {lagReport, draining} = message[1];\n this.#olderReplicaVersionRejections = 0;\n if (draining) {\n this.#drainHandler?.onChangeStreamerDrain(downstream);\n }\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 const {\n type,\n message: msg,\n replicaVersion: rmVersion,\n } = message[1];\n if (\n type === WrongReplicaVersion &&\n rmVersion !== undefined &&\n rmVersion < replicaVersion &&\n this.#olderReplicaVersionRejections++ <\n MAX_OLDER_REPLICA_VERSION_REJECTIONS\n ) {\n // The replication-manager is behind this replica (e.g. it\n // has yet to be replaced by a resync). Rather than restoring\n // its older replica, reconnect (with backoff), which may\n // reach a different replication-manager.\n lc.warn?.(\n `replication-manager is at an older replica version ` +\n `(${rmVersion}) than ${replicaVersion}. reconnecting`,\n );\n break;\n }\n // Signal from the replication-manager that the view-syncer must\n // shut down and restore a new backup from litestream.\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, relation} = msg;\n const first = !backfill;\n backfill = {\n status, // Update the current status\n table: relation.name,\n columns: [...relation.rowKey.columns, ...msg.columns],\n };\n if (first) {\n // Start publishing the status every 3 seconds.\n publishBackfillStatus(relation.name);\n }\n }\n\n if (msg.tag === 'create-index' && this.#statusPublisher) {\n // Creating an index on an existing table can take a long\n // time. Flush preceding changes so that the index creation\n // is processed (and timed) on its own.\n await flushWrites();\n const index = mapPostgresToLiteIndex(msg.spec);\n indexing ??= new IndexingProgress();\n indexing.start(index);\n this.#statusPublisher.publish(\n lc,\n 'Replicating',\n `Creating index ${index.name} on ${index.tableName}`,\n 3000,\n indexing.state,\n );\n // Exclude the time spent publishing the status.\n indexing.restartTimer();\n writeBatch.push(message as ChangeStreamData);\n writeBatchSize += size;\n await flushWrites();\n const elapsed = indexing.finish();\n lc.info?.(\n `Created index ${index.name} (${elapsed.toFixed(3)} ms)`,\n );\n this.#statusPublisher.publish(\n lc,\n 'Replicating',\n `Created index ${index.name} on ${index.tableName}`,\n 0,\n indexing.state,\n );\n if (backfill) {\n // Resume reporting the progress of the ongoing backfill.\n publishBackfillStatus(backfill.table);\n }\n break;\n }\n indexing = undefined;\n\n const type = message[0];\n const invalidTransactionSequence =\n type === 'begin' ? inTransaction : !inTransaction;\n if (type === 'begin') {\n inTransaction = true;\n } else if (type === 'commit' || type === 'rollback') {\n inTransaction = false;\n }\n\n writeBatch.push(message as ChangeStreamData);\n writeBatchSize += size;\n if (\n // Waiting here ensures that consuming the commit, which ACKs\n // it upstream, only happens after the SQLite commit finishes.\n type === 'commit' ||\n type === 'rollback' ||\n // Promptly surface malformed transaction sequences instead\n // of leaving a partial batch waiting for another message.\n invalidTransactionSequence ||\n writeBatch.length >= MAX_WORKER_BATCH_MESSAGES ||\n writeBatchSize >= MAX_WORKER_BATCH_SIZE\n ) {\n await flushWrites();\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 if (err === undefined && this.#drainHandler?.hasNextConnection) {\n continue; // Switched over to a new subscription; no backoff.\n }\n this.#drainHandler?.getNextConnection()?.cancel();\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 #subscriberContext(\n replicaVersion: string,\n watermark: string,\n backfills: SubscriberContext['backfills'],\n ): SubscriberContext {\n return {\n protocolVersion: PROTOCOL_VERSION,\n taskID: this.#taskID,\n id: this.#id,\n mode: this.#mode,\n watermark,\n replicaVersion,\n backfills,\n ...(this.#drainHandler ? {drainable: true} : {}),\n };\n }\n\n /**\n * Returns the subscriber context as of the last committed transaction,\n * read from the replica file (i.e. not from the write worker, whose\n * connection may be in the middle of a transaction).\n */\n #committedContext(replicaFile: string): SubscriberContext {\n const db = new Database(this.#lc, replicaFile, {readonly: true});\n try {\n const {replicaVersion, watermark, backfills} = db.transaction(() => ({\n ...getSubscriptionState(new StatementRunner(db)),\n backfills: readReplicaBackfills(db),\n }));\n return this.#subscriberContext(replicaVersion, watermark, backfills);\n } finally {\n db.close();\n }\n }\n\n stop(lc: LogContext, err?: unknown) {\n this.#state.stop(lc, err);\n // Abort any polling loop that the write worker may be in.\n this.#worker.abort();\n }\n}\n"],"mappings":"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"}
|
|
@@ -4,6 +4,14 @@ import type { IndexingStatus, ReplicationStage, ReplicationState, ReplicationSta
|
|
|
4
4
|
import { Database } from '../../../../zqlite/src/db.ts';
|
|
5
5
|
import type { LiteIndexSpec } from '../../db/specs.ts';
|
|
6
6
|
import { publishCriticalEvent } from '../../observability/events.ts';
|
|
7
|
+
export type GrowthOptions = {
|
|
8
|
+
/** How often to check the replica size. */
|
|
9
|
+
pollMs?: number;
|
|
10
|
+
/** Republish when the replica grew by at least this many bytes ... */
|
|
11
|
+
minDeltaBytes?: number;
|
|
12
|
+
/** ... and by at least this fraction of the last published size. */
|
|
13
|
+
minDeltaRatio?: number;
|
|
14
|
+
};
|
|
7
15
|
export declare class ReplicationStatusPublisher {
|
|
8
16
|
#private;
|
|
9
17
|
static forTesting(): ReplicationStatusPublisher;
|
|
@@ -18,7 +26,17 @@ export declare class ReplicationStatusPublisher {
|
|
|
18
26
|
* event from being sent until the operation completes.
|
|
19
27
|
*/
|
|
20
28
|
publishAndFlush(lc: LogContext, stage: ReplicationStage, description?: string, interval?: number, extraState?: () => Partial<ReplicationState>, maxWaitMs?: number): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Republishes the last published status whenever the replica has grown by
|
|
31
|
+
* the given delta since the last published `replicaSize`, until
|
|
32
|
+
* {@link stop}. Consumers (e.g. Cloud Zero, which sizes the disk requests of
|
|
33
|
+
* replicas from it) thereby see growth from any source, not only from
|
|
34
|
+
* backfills and initial syncs. The status interval of {@link publish} keeps
|
|
35
|
+
* running alongside.
|
|
36
|
+
*/
|
|
37
|
+
watchGrowth(lc: LogContext, options?: GrowthOptions): this;
|
|
21
38
|
publishAndThrowError(lc: LogContext, stage: ReplicationStage, e: unknown): Promise<never>;
|
|
39
|
+
/** Stops the status interval and the growth watch. */
|
|
22
40
|
stop(): this;
|
|
23
41
|
}
|
|
24
42
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replication-status.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/replicator/replication-status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,qCAAqC,CAAC;AACpE,OAAO,KAAK,EACV,cAAc,EAGd,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,MAAM,EACP,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAC,QAAQ,EAAC,MAAM,8BAA8B,CAAC;AAEtD,OAAO,KAAK,EAAC,aAAa,EAAgB,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAEL,oBAAoB,EACrB,MAAM,+BAA+B,CAAC;AAKvC,qBAAa,0BAA0B;;
|
|
1
|
+
{"version":3,"file":"replication-status.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/replicator/replication-status.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,10 +4,17 @@ import { makeErrorDetails, publishCriticalEvent } from "../../observability/even
|
|
|
4
4
|
import { createSilentLogContext } from "../../../../shared/src/logging-test-utils.js";
|
|
5
5
|
//#region ../zero-cache/src/services/replicator/replication-status.ts
|
|
6
6
|
var byKeys = (a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
|
|
7
|
+
var DEFAULT_GROWTH_OPTIONS = {
|
|
8
|
+
pollMs: 3e4,
|
|
9
|
+
minDeltaBytes: 1024 ** 3,
|
|
10
|
+
minDeltaRatio: .05
|
|
11
|
+
};
|
|
7
12
|
var ReplicationStatusPublisher = class ReplicationStatusPublisher {
|
|
8
13
|
#dbRunner;
|
|
9
14
|
#publishFn;
|
|
10
15
|
#timer;
|
|
16
|
+
#growthTimer;
|
|
17
|
+
#last;
|
|
11
18
|
static forTesting() {
|
|
12
19
|
return ReplicationStatusPublisher.forReplicaFile(":memory:");
|
|
13
20
|
}
|
|
@@ -49,16 +56,57 @@ var ReplicationStatusPublisher = class ReplicationStatusPublisher {
|
|
|
49
56
|
clearTimeout(timer);
|
|
50
57
|
}
|
|
51
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Republishes the last published status whenever the replica has grown by
|
|
61
|
+
* the given delta since the last published `replicaSize`, until
|
|
62
|
+
* {@link stop}. Consumers (e.g. Cloud Zero, which sizes the disk requests of
|
|
63
|
+
* replicas from it) thereby see growth from any source, not only from
|
|
64
|
+
* backfills and initial syncs. The status interval of {@link publish} keeps
|
|
65
|
+
* running alongside.
|
|
66
|
+
*/
|
|
67
|
+
watchGrowth(lc, options = {}) {
|
|
68
|
+
const { pollMs, minDeltaBytes, minDeltaRatio } = {
|
|
69
|
+
...DEFAULT_GROWTH_OPTIONS,
|
|
70
|
+
...options
|
|
71
|
+
};
|
|
72
|
+
clearInterval(this.#growthTimer);
|
|
73
|
+
this.#growthTimer = setInterval(() => {
|
|
74
|
+
const last = this.#last;
|
|
75
|
+
if (!last) return;
|
|
76
|
+
let size;
|
|
77
|
+
try {
|
|
78
|
+
size = this.#dbRunner(lc, getReplicaSize);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
lc.warn?.(`Unable to read the replica size`, e);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const published = last.replicaSize ?? 0;
|
|
84
|
+
if (size - published >= Math.max(minDeltaBytes, published * minDeltaRatio)) {
|
|
85
|
+
lc.info?.(`Replica grew from ${published} to ${size} bytes. Republishing status.`);
|
|
86
|
+
this.#send(lc, last.stage, last.description, last.extraState);
|
|
87
|
+
}
|
|
88
|
+
}, pollMs);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
52
91
|
#publish(lc, stage, description, interval, extraState, now = /* @__PURE__ */ new Date()) {
|
|
53
|
-
this
|
|
92
|
+
clearInterval(this.#timer);
|
|
93
|
+
const published = this.#send(lc, stage, description, extraState, now);
|
|
94
|
+
if (interval) this.#timer = setInterval(() => this.publish(lc, stage, description, interval, extraState), interval);
|
|
95
|
+
return published;
|
|
96
|
+
}
|
|
97
|
+
#send(lc, stage, description, extraState, now = /* @__PURE__ */ new Date()) {
|
|
54
98
|
const event = this.#dbRunner(lc, (db) => replicationStatusEvent(lc, db, stage, "OK", description, now));
|
|
55
99
|
if (event.state) event.state = {
|
|
56
100
|
...event.state,
|
|
57
101
|
...extraState?.()
|
|
58
102
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
103
|
+
this.#last = {
|
|
104
|
+
stage,
|
|
105
|
+
description,
|
|
106
|
+
extraState,
|
|
107
|
+
replicaSize: event.state?.replicaSize
|
|
108
|
+
};
|
|
109
|
+
return this.#publishFn(lc, event);
|
|
62
110
|
}
|
|
63
111
|
async publishAndThrowError(lc, stage, e) {
|
|
64
112
|
this.stop();
|
|
@@ -66,8 +114,10 @@ var ReplicationStatusPublisher = class ReplicationStatusPublisher {
|
|
|
66
114
|
await this.#publishFn(lc, event);
|
|
67
115
|
throw e;
|
|
68
116
|
}
|
|
117
|
+
/** Stops the status interval and the growth watch. */
|
|
69
118
|
stop() {
|
|
70
119
|
clearInterval(this.#timer);
|
|
120
|
+
clearInterval(this.#growthTimer);
|
|
71
121
|
return this;
|
|
72
122
|
}
|
|
73
123
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replication-status.js","names":["#dbRunner","#publishFn","#publish","#timer","#totalIndexes","#now","#count","#current","#completedMs"],"sources":["../../../../../../zero-cache/src/services/replicator/replication-status.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {createSilentLogContext} from '../../../../shared/src/logging-test-utils.ts';\nimport type {JSONObject} from '../../../../zero-events/src/json.ts';\nimport type {\n IndexingStatus,\n ReplicatedIndex,\n ReplicatedTable,\n ReplicationStage,\n ReplicationState,\n ReplicationStatusEvent,\n Status,\n} from '../../../../zero-events/src/status.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {computeZqlSpecs, listIndexes} from '../../db/lite-tables.ts';\nimport type {LiteIndexSpec, LiteTableSpec} from '../../db/specs.ts';\nimport {\n makeErrorDetails,\n publishCriticalEvent,\n} from '../../observability/events.ts';\n\nconst byKeys = (a: [string, unknown], b: [string, unknown]) =>\n a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;\n\nexport class ReplicationStatusPublisher {\n readonly #dbRunner: <T>(lc: LogContext, fn: (db: Database) => T) => T;\n readonly #publishFn: typeof publishCriticalEvent;\n #timer: NodeJS.Timeout | undefined;\n\n static forTesting() {\n return ReplicationStatusPublisher.forReplicaFile(':memory:');\n }\n\n static forRunningTransaction(tx: Database, publishFn = publishCriticalEvent) {\n return new ReplicationStatusPublisher((_lc, fn) => fn(tx), publishFn);\n }\n\n static forReplicaFile(file: string, publishFn = publishCriticalEvent) {\n return new ReplicationStatusPublisher((lc, fn) => {\n const db = new Database(lc, file, {readonly: true});\n try {\n return fn(db);\n } finally {\n db.close();\n }\n }, publishFn);\n }\n\n constructor(\n dbRunner: <T>(lc: LogContext, fn: (db: Database) => T) => T,\n publishFn = publishCriticalEvent,\n ) {\n this.#dbRunner = dbRunner;\n this.#publishFn = publishFn;\n }\n\n publish(\n lc: LogContext,\n stage: ReplicationStage,\n description?: string,\n interval = 0,\n extraState?: () => Partial<ReplicationState>,\n now = new Date(),\n ): this {\n void this.#publish(lc, stage, description, interval, extraState, now);\n return this;\n }\n\n /**\n * Like {@link publish}, but waits (for at most `maxWaitMs`) for the event\n * to be sent. Use this before a long synchronous operation (e.g. creating\n * an index) that blocks the event loop and would otherwise prevent the\n * event from being sent until the operation completes.\n */\n async publishAndFlush(\n lc: LogContext,\n stage: ReplicationStage,\n description?: string,\n interval = 0,\n extraState?: () => Partial<ReplicationState>,\n maxWaitMs = 1000,\n ): Promise<void> {\n const published = this.#publish(\n lc,\n stage,\n description,\n interval,\n extraState,\n );\n let timer: NodeJS.Timeout | undefined;\n try {\n await Promise.race([\n published,\n new Promise<void>(resolve => {\n timer = setTimeout(resolve, maxWaitMs);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n }\n\n #publish(\n lc: LogContext,\n stage: ReplicationStage,\n description: string | undefined,\n interval: number,\n extraState: (() => Partial<ReplicationState>) | undefined,\n now = new Date(),\n ): Promise<void> {\n this.stop();\n\n const event = this.#dbRunner(lc, db =>\n replicationStatusEvent(lc, db, stage, 'OK', description, now),\n );\n if (event.state) {\n event.state = {\n ...event.state,\n ...extraState?.(),\n };\n }\n const published = this.#publishFn(lc, event);\n\n if (interval) {\n this.#timer = setInterval(\n () => this.publish(lc, stage, description, interval, extraState),\n interval,\n );\n }\n return published;\n }\n\n async publishAndThrowError(\n lc: LogContext,\n stage: ReplicationStage,\n e: unknown,\n ): Promise<never> {\n this.stop();\n const event = this.#dbRunner(lc, db =>\n replicationStatusError(lc, stage, e, db),\n );\n await this.#publishFn(lc, event);\n throw e;\n }\n\n stop(): this {\n clearInterval(this.#timer);\n return this;\n }\n}\n\n/**\n * Tracks the progress of a sequence of index creations for reporting in\n * the `indexingStatus` field of {@link ReplicationState}.\n */\nexport class IndexingProgress {\n readonly #totalIndexes: number | undefined;\n readonly #now: () => number;\n #count = 0;\n #completedMs = 0;\n #current:\n | {index: LiteIndexSpec; start: number; elapsedMs: number | undefined}\n | undefined;\n\n /**\n * @param totalIndexes The total number of indexes to be created, if known.\n */\n constructor(totalIndexes?: number, now = () => performance.now()) {\n this.#totalIndexes = totalIndexes;\n this.#now = now;\n }\n\n /** Marks the start of creating the `index`. */\n start(index: LiteIndexSpec) {\n this.#count++;\n this.#current = {index, start: this.#now(), elapsedMs: undefined};\n }\n\n /**\n * Restarts the timer of the current index, e.g. to exclude time spent\n * reporting its start from its creation time.\n */\n restartTimer() {\n if (this.#current) {\n this.#current.start = this.#now();\n }\n }\n\n /**\n * Marks the completion of the index passed to the last call to\n * {@link start}, returning the milliseconds it took to create.\n */\n finish(): number {\n const current = this.#current;\n if (!current) {\n return 0;\n }\n if (current.elapsedMs === undefined) {\n current.elapsedMs = this.#now() - current.start;\n this.#completedMs += current.elapsedMs;\n }\n return current.elapsedMs;\n }\n\n /** The `extraState` for {@link ReplicationStatusPublisher.publish}. */\n readonly state = (): Partial<ReplicationState> => {\n const status = this.status();\n return status ? {indexingStatus: status} : {};\n };\n\n status(): IndexingStatus | undefined {\n const current = this.#current;\n if (!current) {\n return undefined;\n }\n const {index, start, elapsedMs} = current;\n const done = elapsedMs !== undefined;\n return {\n name: index.name,\n table: index.tableName,\n columns: Object.keys(index.columns),\n unique: index.unique,\n index: this.#count,\n totalIndexes: this.#totalIndexes,\n elapsedMs: Math.round(done ? elapsedMs : this.#now() - start),\n completedMs: Math.round(\n done ? this.#completedMs - elapsedMs : this.#completedMs,\n ),\n done,\n };\n }\n}\n\nexport async function publishReplicationError(\n lc: LogContext,\n stage: ReplicationStage,\n description: string,\n errorDetails?: JSONObject,\n now = new Date(),\n) {\n const event: ReplicationStatusEvent = {\n type: 'zero/events/status/replication/v1',\n component: 'replication',\n status: 'ERROR',\n stage,\n description,\n errorDetails,\n time: now.toISOString(),\n };\n await publishCriticalEvent(lc, event);\n}\n\nexport function replicationStatusError(\n lc: LogContext,\n stage: ReplicationStage,\n e: unknown,\n db?: Database,\n now = new Date(),\n) {\n const event = replicationStatusEvent(lc, db, stage, 'ERROR', String(e), now);\n event.errorDetails = makeErrorDetails(e);\n return event;\n}\n\n// Exported for testing.\nexport function replicationStatusEvent(\n lc: LogContext,\n db: Database | undefined,\n stage: ReplicationStage,\n status: Status,\n description?: string,\n now = new Date(),\n): ReplicationStatusEvent {\n const start = performance.now();\n try {\n return {\n type: 'zero/events/status/replication/v1',\n component: 'replication',\n status,\n stage,\n description,\n time: now.toISOString(),\n state: {\n tables: db ? getReplicatedTables(db) : [],\n indexes: db ? getReplicatedIndexes(db) : [],\n replicaSize: db ? getReplicaSize(db) : undefined,\n },\n };\n } catch (e) {\n lc.warn?.(`Unable to create full ReplicationStatusEvent`, e);\n return {\n type: 'zero/events/status/replication/v1',\n component: 'replication',\n status,\n stage,\n description,\n time: now.toISOString(),\n state: {\n tables: [],\n indexes: [],\n replicaSize: 0,\n },\n };\n } finally {\n const elapsed = (performance.now() - start).toFixed(3);\n lc.debug?.(`computed schema for replication event (${elapsed} ms)`);\n }\n}\n\nfunction getReplicatedTables(db: Database): ReplicatedTable[] {\n const fullTables = new Map<string, LiteTableSpec>();\n const clientSchema = computeZqlSpecs(\n createSilentLogContext(), // avoid logging warnings about indexes\n db,\n {includeBackfillingColumns: false},\n new Map(),\n fullTables,\n );\n\n // oxlint-disable-next-line e18e/prefer-array-to-sorted\n return [...fullTables.entries()].sort(byKeys).map(([table, spec]) => ({\n table,\n columns: Object.entries(spec.columns)\n .sort(byKeys)\n .map(([column, spec]) => ({\n column,\n upstreamType: spec.dataType.split('|')[0],\n clientType: clientSchema.get(table)?.zqlSpec[column]?.type ?? null,\n })),\n }));\n}\n\nfunction getReplicatedIndexes(db: Database): ReplicatedIndex[] {\n return listIndexes(db).map(({name, tableName: table, columns, unique}) => ({\n name,\n table,\n unique,\n columns: Object.entries(columns).map(([column, dir]) => ({column, dir})),\n }));\n}\n\nfunction getReplicaSize(db: Database) {\n const [{page_count: pageCount}] = db.pragma<{page_count: number}>(\n 'page_count',\n );\n const [{page_size: pageSize}] = db.pragma<{page_size: number}>('page_size');\n return pageCount * pageSize;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"replication-status.js","names":["#dbRunner","#publishFn","#publish","#growthTimer","#last","#send","#timer","#totalIndexes","#now","#count","#current","#completedMs"],"sources":["../../../../../../zero-cache/src/services/replicator/replication-status.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {createSilentLogContext} from '../../../../shared/src/logging-test-utils.ts';\nimport type {JSONObject} from '../../../../zero-events/src/json.ts';\nimport type {\n IndexingStatus,\n ReplicatedIndex,\n ReplicatedTable,\n ReplicationStage,\n ReplicationState,\n ReplicationStatusEvent,\n Status,\n} from '../../../../zero-events/src/status.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {computeZqlSpecs, listIndexes} from '../../db/lite-tables.ts';\nimport type {LiteIndexSpec, LiteTableSpec} from '../../db/specs.ts';\nimport {\n makeErrorDetails,\n publishCriticalEvent,\n} from '../../observability/events.ts';\n\nconst byKeys = (a: [string, unknown], b: [string, unknown]) =>\n a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;\n\nexport type GrowthOptions = {\n /** How often to check the replica size. */\n pollMs?: number;\n /** Republish when the replica grew by at least this many bytes ... */\n minDeltaBytes?: number;\n /** ... and by at least this fraction of the last published size. */\n minDeltaRatio?: number;\n};\n\nconst DEFAULT_GROWTH_OPTIONS: Required<GrowthOptions> = {\n pollMs: 30_000,\n minDeltaBytes: 1024 ** 3,\n minDeltaRatio: 0.05,\n};\n\ntype PublishedStatus = {\n stage: ReplicationStage;\n description: string | undefined;\n extraState: (() => Partial<ReplicationState>) | undefined;\n replicaSize: number | undefined;\n};\n\nexport class ReplicationStatusPublisher {\n readonly #dbRunner: <T>(lc: LogContext, fn: (db: Database) => T) => T;\n readonly #publishFn: typeof publishCriticalEvent;\n #timer: NodeJS.Timeout | undefined;\n #growthTimer: NodeJS.Timeout | undefined;\n #last: PublishedStatus | undefined;\n\n static forTesting() {\n return ReplicationStatusPublisher.forReplicaFile(':memory:');\n }\n\n static forRunningTransaction(tx: Database, publishFn = publishCriticalEvent) {\n return new ReplicationStatusPublisher((_lc, fn) => fn(tx), publishFn);\n }\n\n static forReplicaFile(file: string, publishFn = publishCriticalEvent) {\n return new ReplicationStatusPublisher((lc, fn) => {\n const db = new Database(lc, file, {readonly: true});\n try {\n return fn(db);\n } finally {\n db.close();\n }\n }, publishFn);\n }\n\n constructor(\n dbRunner: <T>(lc: LogContext, fn: (db: Database) => T) => T,\n publishFn = publishCriticalEvent,\n ) {\n this.#dbRunner = dbRunner;\n this.#publishFn = publishFn;\n }\n\n publish(\n lc: LogContext,\n stage: ReplicationStage,\n description?: string,\n interval = 0,\n extraState?: () => Partial<ReplicationState>,\n now = new Date(),\n ): this {\n void this.#publish(lc, stage, description, interval, extraState, now);\n return this;\n }\n\n /**\n * Like {@link publish}, but waits (for at most `maxWaitMs`) for the event\n * to be sent. Use this before a long synchronous operation (e.g. creating\n * an index) that blocks the event loop and would otherwise prevent the\n * event from being sent until the operation completes.\n */\n async publishAndFlush(\n lc: LogContext,\n stage: ReplicationStage,\n description?: string,\n interval = 0,\n extraState?: () => Partial<ReplicationState>,\n maxWaitMs = 1000,\n ): Promise<void> {\n const published = this.#publish(\n lc,\n stage,\n description,\n interval,\n extraState,\n );\n let timer: NodeJS.Timeout | undefined;\n try {\n await Promise.race([\n published,\n new Promise<void>(resolve => {\n timer = setTimeout(resolve, maxWaitMs);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n }\n\n /**\n * Republishes the last published status whenever the replica has grown by\n * the given delta since the last published `replicaSize`, until\n * {@link stop}. Consumers (e.g. Cloud Zero, which sizes the disk requests of\n * replicas from it) thereby see growth from any source, not only from\n * backfills and initial syncs. The status interval of {@link publish} keeps\n * running alongside.\n */\n watchGrowth(lc: LogContext, options: GrowthOptions = {}): this {\n const {pollMs, minDeltaBytes, minDeltaRatio} = {\n ...DEFAULT_GROWTH_OPTIONS,\n ...options,\n };\n clearInterval(this.#growthTimer);\n this.#growthTimer = setInterval(() => {\n const last = this.#last;\n if (!last) {\n return;\n }\n let size: number;\n try {\n size = this.#dbRunner(lc, getReplicaSize);\n } catch (e) {\n lc.warn?.(`Unable to read the replica size`, e);\n return;\n }\n const published = last.replicaSize ?? 0;\n if (\n size - published >=\n Math.max(minDeltaBytes, published * minDeltaRatio)\n ) {\n lc.info?.(\n `Replica grew from ${published} to ${size} bytes. Republishing status.`,\n );\n void this.#send(lc, last.stage, last.description, last.extraState);\n }\n }, pollMs);\n return this;\n }\n\n #publish(\n lc: LogContext,\n stage: ReplicationStage,\n description: string | undefined,\n interval: number,\n extraState: (() => Partial<ReplicationState>) | undefined,\n now = new Date(),\n ): Promise<void> {\n clearInterval(this.#timer);\n\n const published = this.#send(lc, stage, description, extraState, now);\n\n if (interval) {\n this.#timer = setInterval(\n () => this.publish(lc, stage, description, interval, extraState),\n interval,\n );\n }\n return published;\n }\n\n #send(\n lc: LogContext,\n stage: ReplicationStage,\n description: string | undefined,\n extraState: (() => Partial<ReplicationState>) | undefined,\n now = new Date(),\n ): Promise<void> {\n const event = this.#dbRunner(lc, db =>\n replicationStatusEvent(lc, db, stage, 'OK', description, now),\n );\n if (event.state) {\n event.state = {\n ...event.state,\n ...extraState?.(),\n };\n }\n this.#last = {\n stage,\n description,\n extraState,\n replicaSize: event.state?.replicaSize,\n };\n return this.#publishFn(lc, event);\n }\n\n async publishAndThrowError(\n lc: LogContext,\n stage: ReplicationStage,\n e: unknown,\n ): Promise<never> {\n this.stop();\n const event = this.#dbRunner(lc, db =>\n replicationStatusError(lc, stage, e, db),\n );\n await this.#publishFn(lc, event);\n throw e;\n }\n\n /** Stops the status interval and the growth watch. */\n stop(): this {\n clearInterval(this.#timer);\n clearInterval(this.#growthTimer);\n return this;\n }\n}\n\n/**\n * Tracks the progress of a sequence of index creations for reporting in\n * the `indexingStatus` field of {@link ReplicationState}.\n */\nexport class IndexingProgress {\n readonly #totalIndexes: number | undefined;\n readonly #now: () => number;\n #count = 0;\n #completedMs = 0;\n #current:\n | {index: LiteIndexSpec; start: number; elapsedMs: number | undefined}\n | undefined;\n\n /**\n * @param totalIndexes The total number of indexes to be created, if known.\n */\n constructor(totalIndexes?: number, now = () => performance.now()) {\n this.#totalIndexes = totalIndexes;\n this.#now = now;\n }\n\n /** Marks the start of creating the `index`. */\n start(index: LiteIndexSpec) {\n this.#count++;\n this.#current = {index, start: this.#now(), elapsedMs: undefined};\n }\n\n /**\n * Restarts the timer of the current index, e.g. to exclude time spent\n * reporting its start from its creation time.\n */\n restartTimer() {\n if (this.#current) {\n this.#current.start = this.#now();\n }\n }\n\n /**\n * Marks the completion of the index passed to the last call to\n * {@link start}, returning the milliseconds it took to create.\n */\n finish(): number {\n const current = this.#current;\n if (!current) {\n return 0;\n }\n if (current.elapsedMs === undefined) {\n current.elapsedMs = this.#now() - current.start;\n this.#completedMs += current.elapsedMs;\n }\n return current.elapsedMs;\n }\n\n /** The `extraState` for {@link ReplicationStatusPublisher.publish}. */\n readonly state = (): Partial<ReplicationState> => {\n const status = this.status();\n return status ? {indexingStatus: status} : {};\n };\n\n status(): IndexingStatus | undefined {\n const current = this.#current;\n if (!current) {\n return undefined;\n }\n const {index, start, elapsedMs} = current;\n const done = elapsedMs !== undefined;\n return {\n name: index.name,\n table: index.tableName,\n columns: Object.keys(index.columns),\n unique: index.unique,\n index: this.#count,\n totalIndexes: this.#totalIndexes,\n elapsedMs: Math.round(done ? elapsedMs : this.#now() - start),\n completedMs: Math.round(\n done ? this.#completedMs - elapsedMs : this.#completedMs,\n ),\n done,\n };\n }\n}\n\nexport async function publishReplicationError(\n lc: LogContext,\n stage: ReplicationStage,\n description: string,\n errorDetails?: JSONObject,\n now = new Date(),\n) {\n const event: ReplicationStatusEvent = {\n type: 'zero/events/status/replication/v1',\n component: 'replication',\n status: 'ERROR',\n stage,\n description,\n errorDetails,\n time: now.toISOString(),\n };\n await publishCriticalEvent(lc, event);\n}\n\nexport function replicationStatusError(\n lc: LogContext,\n stage: ReplicationStage,\n e: unknown,\n db?: Database,\n now = new Date(),\n) {\n const event = replicationStatusEvent(lc, db, stage, 'ERROR', String(e), now);\n event.errorDetails = makeErrorDetails(e);\n return event;\n}\n\n// Exported for testing.\nexport function replicationStatusEvent(\n lc: LogContext,\n db: Database | undefined,\n stage: ReplicationStage,\n status: Status,\n description?: string,\n now = new Date(),\n): ReplicationStatusEvent {\n const start = performance.now();\n try {\n return {\n type: 'zero/events/status/replication/v1',\n component: 'replication',\n status,\n stage,\n description,\n time: now.toISOString(),\n state: {\n tables: db ? getReplicatedTables(db) : [],\n indexes: db ? getReplicatedIndexes(db) : [],\n replicaSize: db ? getReplicaSize(db) : undefined,\n },\n };\n } catch (e) {\n lc.warn?.(`Unable to create full ReplicationStatusEvent`, e);\n return {\n type: 'zero/events/status/replication/v1',\n component: 'replication',\n status,\n stage,\n description,\n time: now.toISOString(),\n state: {\n tables: [],\n indexes: [],\n replicaSize: 0,\n },\n };\n } finally {\n const elapsed = (performance.now() - start).toFixed(3);\n lc.debug?.(`computed schema for replication event (${elapsed} ms)`);\n }\n}\n\nfunction getReplicatedTables(db: Database): ReplicatedTable[] {\n const fullTables = new Map<string, LiteTableSpec>();\n const clientSchema = computeZqlSpecs(\n createSilentLogContext(), // avoid logging warnings about indexes\n db,\n {includeBackfillingColumns: false},\n new Map(),\n fullTables,\n );\n\n // oxlint-disable-next-line e18e/prefer-array-to-sorted\n return [...fullTables.entries()].sort(byKeys).map(([table, spec]) => ({\n table,\n columns: Object.entries(spec.columns)\n .sort(byKeys)\n .map(([column, spec]) => ({\n column,\n upstreamType: spec.dataType.split('|')[0],\n clientType: clientSchema.get(table)?.zqlSpec[column]?.type ?? null,\n })),\n }));\n}\n\nfunction getReplicatedIndexes(db: Database): ReplicatedIndex[] {\n return listIndexes(db).map(({name, tableName: table, columns, unique}) => ({\n name,\n table,\n unique,\n columns: Object.entries(columns).map(([column, dir]) => ({column, dir})),\n }));\n}\n\nfunction getReplicaSize(db: Database) {\n const [{page_count: pageCount}] = db.pragma<{page_count: number}>(\n 'page_count',\n );\n const [{page_size: pageSize}] = db.pragma<{page_size: number}>('page_size');\n return pageCount * pageSize;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statz.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/statz.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,YAAY,EAAE,cAAc,EAAC,MAAM,SAAS,CAAC;AAG1D,OAAO,KAAK,EAAC,oBAAoB,IAAI,UAAU,EAAC,MAAM,wBAAwB,CAAC;AAiT/E;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"statz.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/services/statz.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,YAAY,EAAE,cAAc,EAAC,MAAM,SAAS,CAAC;AAG1D,OAAO,KAAK,EAAC,oBAAoB,IAAI,UAAU,EAAC,MAAM,wBAAwB,CAAC;AAiT/E;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,YAAY,iBA0DlB"}
|
|
@@ -228,7 +228,7 @@ async function handleStatzRequest(lc, config, req, res) {
|
|
|
228
228
|
const statsFns = {
|
|
229
229
|
upstream: () => upstreamStats(lc, config),
|
|
230
230
|
cvr: () => cvrStats(lc, config),
|
|
231
|
-
changeLog: () => changeLogStats(lc, config),
|
|
231
|
+
...config.changeStreamer.pgChangeLogEnabled ? { changeLog: () => changeLogStats(lc, config) } : {},
|
|
232
232
|
replica: () => replicaStats(lc, config),
|
|
233
233
|
replication: () => replicationStats(lc, config),
|
|
234
234
|
os: () => osStats()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statz.js","names":[],"sources":["../../../../../zero-cache/src/services/statz.ts"],"sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport type {LogContext} from '@rocicorp/logger';\nimport auth from 'basic-auth';\nimport type {FastifyReply, FastifyRequest} from 'fastify';\nimport {BigIntJSON} from '../../../shared/src/bigint-json.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport type {NormalizedZeroConfig as ZeroConfig} from '../config/normalize.ts';\nimport {getOperatorAccess} from '../config/zero-config.ts';\nimport {StatementRunner} from '../db/statements.ts';\nimport {pgClient} from '../types/pg.ts';\nimport {getShardID, upstreamSchema} from '../types/shards.ts';\nimport {fromStateVersionString} from './change-source/pg/lsn.ts';\nimport {getReplicationState} from './replicator/schema/replication-state.ts';\n\nasync function upstreamStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config));\n const sql = pgClient(lc, config.upstream.db, 'statz-upstream');\n try {\n return await getPgStats([\n [\n 'numReplicas',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"replicas\"`,\n ],\n [\n 'numClientsWithMutations',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"clients\"`,\n ],\n [\n 'numMutationsProcessed',\n sql`SELECT SUM(\"lastMutationID\") as \"c\" FROM ${sql(schema)}.\"clients\"`,\n ],\n ]);\n } finally {\n await sql.end();\n }\n}\n\nasync function cvrStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config)) + '/cvr';\n const sql = pgClient(lc, config.cvr.db, 'statz-cvr');\n\n function numQueriesPerClientGroup(\n active: boolean,\n ): ReturnType<ReturnType<typeof pgClient>> {\n const filter = active\n ? sql`WHERE \"inactivatedAt\" IS NULL AND deleted = false`\n : sql`WHERE \"inactivatedAt\" IS NOT NULL AND (\"inactivatedAt\" + \"ttl\") > NOW()`;\n return sql`WITH\n group_counts AS (\n SELECT\n \"clientGroupID\",\n COUNT(*) AS num_queries\n FROM ${sql(schema)}.\"desires\"\n ${filter}\n GROUP BY \"clientGroupID\"\n ),\n -- Count distinct clientIDs per clientGroupID\n client_per_group_counts AS (\n SELECT\n \"clientGroupID\",\n COUNT(DISTINCT \"clientID\") AS num_clients\n FROM ${sql(schema)}.\"desires\"\n ${filter}\n GROUP BY \"clientGroupID\"\n )\n -- Combine all the information\n SELECT\n g.\"clientGroupID\",\n cpg.num_clients,\n g.num_queries\n FROM group_counts g\n JOIN client_per_group_counts cpg ON g.\"clientGroupID\" = cpg.\"clientGroupID\"\n ORDER BY g.num_queries DESC;`;\n }\n\n try {\n return await getPgStats([\n [\n 'totalNumQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\"`,\n ],\n [\n 'numUniqueQueryHashes',\n sql`SELECT COUNT(DISTINCT \"queryHash\") as \"c\" FROM ${sql(\n schema,\n )}.\"desires\"`,\n ],\n [\n 'numActiveQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"inactivatedAt\" IS NULL AND \"deleted\" = false`,\n ],\n [\n 'numInactiveQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"inactivatedAt\" IS NOT NULL AND (\"inactivatedAt\" + \"ttl\") > NOW()`,\n ],\n [\n 'numDeletedQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"deleted\" = true`,\n ],\n [\n 'freshQueriesPercentiles',\n sql`WITH client_group_counts AS (\n -- Count inactive desires per clientGroupID\n SELECT\n \"clientGroupID\",\n COUNT(*) AS fresh_count\n FROM ${sql(schema)}.\"desires\"\n WHERE\n (\"inactivatedAt\" IS NOT NULL\n AND (\"inactivatedAt\" + \"ttl\") > NOW()) OR (\"inactivatedAt\" IS NULL\n AND deleted = false)\n GROUP BY \"clientGroupID\"\n )\n\n SELECT\n percentile_cont(0.50) WITHIN GROUP (ORDER BY fresh_count) AS \"p50\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY fresh_count) AS \"p75\",\n percentile_cont(0.90) WITHIN GROUP (ORDER BY fresh_count) AS \"p90\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY fresh_count) AS \"p95\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY fresh_count) AS \"p99\",\n MIN(fresh_count) AS \"min\",\n MAX(fresh_count) AS \"max\",\n AVG(fresh_count) AS \"avg\"\n FROM client_group_counts;`,\n ],\n [\n 'rowsPerClientGroupPercentiles',\n sql`WITH client_group_counts AS (\n -- Count inactive desires per clientGroupID\n SELECT\n \"clientGroupID\",\n COUNT(*) AS row_count\n FROM ${sql(schema)}.\"rows\"\n GROUP BY \"clientGroupID\"\n )\n SELECT\n percentile_cont(0.50) WITHIN GROUP (ORDER BY row_count) AS \"p50\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY row_count) AS \"p75\",\n percentile_cont(0.90) WITHIN GROUP (ORDER BY row_count) AS \"p90\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY row_count) AS \"p95\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY row_count) AS \"p99\",\n MIN(row_count) AS \"min\",\n MAX(row_count) AS \"max\",\n AVG(row_count) AS \"avg\"\n FROM client_group_counts;`,\n ],\n [\n // check for AST blowup due to DNF conversion.\n 'astSizes',\n sql`SELECT\n percentile_cont(0.25) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"25th_percentile\",\n percentile_cont(0.5) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"50th_percentile\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"75th_percentile\",\n percentile_cont(0.9) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"90th_percentile\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"95th_percentile\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"99th_percentile\",\n MIN(length(\"clientAST\"::text)) AS \"minimum_length\",\n MAX(length(\"clientAST\"::text)) AS \"maximum_length\",\n AVG(length(\"clientAST\"::text))::integer AS \"average_length\",\n COUNT(*) AS \"total_records\"\n FROM ${sql(schema)}.\"queries\";`,\n ],\n [\n // output the hash of the largest AST\n 'biggestAstHash',\n sql`SELECT \"queryHash\", length(\"clientAST\"::text) AS \"ast_length\"\n FROM ${sql(schema)}.\"queries\"\n ORDER BY length(\"clientAST\"::text) DESC\n LIMIT 1;`,\n ],\n [\n 'totalActiveQueriesPerClientAndClientGroup',\n numQueriesPerClientGroup(true),\n ],\n [\n 'totalInactiveQueriesPerClientAndClientGroup',\n numQueriesPerClientGroup(false),\n ],\n [\n 'totalRowsPerClientGroup',\n sql`SELECT \"clientGroupID\", COUNT(*) as \"c\" FROM ${sql(\n schema,\n )}.\"rows\" GROUP BY \"clientGroupID\" ORDER BY \"c\" DESC`,\n ],\n [\n 'numRowsPerQuery',\n sql`SELECT\n k.key AS \"queryHash\",\n COUNT(*) AS row_count\n FROM ${sql(schema)}.\"rows\" r,\n LATERAL jsonb_each(r.\"refCounts\") k\n GROUP BY k.key\n ORDER BY row_count DESC;`,\n ],\n ] satisfies [\n name: string,\n query: ReturnType<ReturnType<typeof pgClient>>,\n ][]);\n } finally {\n await sql.end();\n }\n}\n\nasync function changeLogStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config)) + '/cdc';\n const sql = pgClient(lc, config.change.db, 'statz-change');\n\n try {\n return await getPgStats([\n [\n 'changeLogSize',\n sql`SELECT COUNT(*) as \"change_log_size\" FROM ${sql(schema)}.\"changeLog\"`,\n ],\n ]);\n } finally {\n await sql.end();\n }\n}\n\n// The replica is opened read-only: /statz is available to the operator\n// password, and must not change the replica. Among other things, a writable\n// connection would run `PRAGMA optimize` when closed.\nfunction replicaStats(lc: LogContext, config: ZeroConfig) {\n const db = new Database(lc, config.replica.file, {readonly: true});\n try {\n return Object.fromEntries([\n ['wal file sizes', walFileSizes(config.replica.file)],\n ['page count', pick(first(db.pragma('PAGE_COUNT')))],\n ['page size', pick(first(db.pragma('PAGE_SIZE')))],\n ['journal mode', pick(first(db.pragma('JOURNAL_MODE')))],\n ['synchronous', pick(first(db.pragma('SYNCHRONOUS')))],\n ['cache size', pick(first(db.pragma('CACHE_SIZE')))],\n ['auto vacuum', pick(first(db.pragma('AUTO_VACUUM')))],\n ['freelist count', pick(first(db.pragma('FREELIST_COUNT')))],\n ['wal autocheckpoint', pick(first(db.pragma('WAL_AUTOCHECKPOINT')))],\n ['db file stats', fs.statSync(config.replica.file)],\n ] as const);\n } finally {\n db.close();\n }\n}\n\nfunction replicationStats(lc: LogContext, config: ZeroConfig) {\n const db = new Database(lc, config.replica.file, {readonly: true});\n try {\n return getReplicationStats(db);\n } finally {\n db.close();\n }\n}\n\n/**\n * The sizes of the replica's WAL files, by suffix, read from the filesystem.\n * `PRAGMA wal_checkpoint` would report the WAL's frame counts, but it runs a\n * checkpoint to do so, and fails on a read-only connection in WAL mode.\n */\nfunction walFileSizes(file: string): Record<string, number> {\n const sizes: Record<string, number> = {};\n // `-wal` is used in WAL mode, and both files in WAL2 mode.\n for (const suffix of ['-wal', '-wal2']) {\n const stats = fs.statSync(file + suffix, {throwIfNoEntry: false});\n if (stats) {\n sizes[suffix] = stats.size;\n }\n }\n return sizes;\n}\n\nfunction getReplicationStats(db: Database) {\n const {stateVersion} = getReplicationState(new StatementRunner(db));\n const lsn = fromStateVersionString(stateVersion);\n return {lsn};\n}\n\nfunction osStats() {\n return Object.fromEntries([\n ['load avg', os.loadavg()],\n ['uptime', os.uptime()],\n ['total mem', os.totalmem()],\n ['free mem', os.freemem()],\n ['cpus', os.cpus().length],\n ['available parallelism', os.availableParallelism()],\n ['platform', os.platform()],\n ['arch', os.arch()],\n ['release', os.release()],\n ] as const);\n}\n\nasync function getPgStats(\n pendingQueries: [\n name: string,\n query: ReturnType<ReturnType<typeof pgClient>>,\n ][],\n) {\n const results = await Promise.all(\n pendingQueries.map(async ([name, query]) => [name, await query] as const),\n );\n return Object.fromEntries(results);\n}\n\ntype StatsObject = Record<string, unknown>;\n\nfunction printStats(group: string, statsObject: StatsObject): string {\n const lines: string[] = ['\\n' + header(group)];\n for (const [name, result] of Object.entries(statsObject)) {\n lines.push('\\n' + name + ': ' + BigIntJSON.stringify(result, null, 2));\n }\n lines.push('\\n');\n return lines.join('');\n}\n\n/**\n * Serves counts and sizes. This is available to the operator password as well\n * as the admin password (see `--operator-password`), so it must never return\n * application data: no row values, query ASTs or query arguments.\n *\n * HTTP query parameters:\n * * `group`: restricts the groups for which stats are computed\n * * `format=json`: returns the stats as a JSON object\n * * `pretty`: formats the JSON object with indentation\n */\nexport async function handleStatzRequest(\n lc: LogContext,\n config: ZeroConfig,\n req: FastifyRequest,\n res: FastifyReply,\n) {\n const credentials = auth(req);\n if (getOperatorAccess(lc, config, credentials?.pass) === undefined) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Statz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n const statsFns: Record<string, () => Promise<StatsObject> | StatsObject> = {\n upstream: () => upstreamStats(lc, config),\n cvr: () => cvrStats(lc, config),\n changeLog: () => changeLogStats(lc, config),\n replica: () => replicaStats(lc, config),\n replication: () => replicationStats(lc, config),\n os: () => osStats(),\n };\n\n async function computeStats(group: string): Promise<[string, StatsObject]> {\n try {\n return [group, await statsFns[group]()];\n } catch (e) {\n lc.error?.(`error computing ${group} stats`, e);\n return [group, {error: String(e)}];\n }\n }\n\n const query = req.query as Record<string, unknown>;\n const groups =\n typeof query.group === 'string'\n ? query.group.split(',')\n : Array.isArray(query.group)\n ? query.group\n : undefined;\n\n const stats = await Promise.all(\n groups\n ? groups.filter(g => g in statsFns).map(computeStats)\n : Object.keys(statsFns).map(computeStats),\n );\n\n if (query.format === 'json') {\n const indent = query.pretty !== undefined ? 2 : undefined;\n await res\n .header('Content-Type', 'application/json')\n .send(BigIntJSON.stringify(Object.fromEntries(stats), null, indent));\n return;\n } else {\n const body = stats\n .map(([group, statsObject]) => printStats(group, statsObject))\n .join('');\n await res.header('Content-Type', 'text/plain; charset=utf-8').send(body);\n }\n}\n\nfunction first(x: object[]): object {\n return x[0];\n}\n\nfunction pick(x: object): unknown {\n return Object.values(x)[0];\n}\n\nfunction header(name: string): string {\n return `=== ${name} ===\\n`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"statz.js","names":[],"sources":["../../../../../zero-cache/src/services/statz.ts"],"sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport type {LogContext} from '@rocicorp/logger';\nimport auth from 'basic-auth';\nimport type {FastifyReply, FastifyRequest} from 'fastify';\nimport {BigIntJSON} from '../../../shared/src/bigint-json.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport type {NormalizedZeroConfig as ZeroConfig} from '../config/normalize.ts';\nimport {getOperatorAccess} from '../config/zero-config.ts';\nimport {StatementRunner} from '../db/statements.ts';\nimport {pgClient} from '../types/pg.ts';\nimport {getShardID, upstreamSchema} from '../types/shards.ts';\nimport {fromStateVersionString} from './change-source/pg/lsn.ts';\nimport {getReplicationState} from './replicator/schema/replication-state.ts';\n\nasync function upstreamStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config));\n const sql = pgClient(lc, config.upstream.db, 'statz-upstream');\n try {\n return await getPgStats([\n [\n 'numReplicas',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"replicas\"`,\n ],\n [\n 'numClientsWithMutations',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"clients\"`,\n ],\n [\n 'numMutationsProcessed',\n sql`SELECT SUM(\"lastMutationID\") as \"c\" FROM ${sql(schema)}.\"clients\"`,\n ],\n ]);\n } finally {\n await sql.end();\n }\n}\n\nasync function cvrStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config)) + '/cvr';\n const sql = pgClient(lc, config.cvr.db, 'statz-cvr');\n\n function numQueriesPerClientGroup(\n active: boolean,\n ): ReturnType<ReturnType<typeof pgClient>> {\n const filter = active\n ? sql`WHERE \"inactivatedAt\" IS NULL AND deleted = false`\n : sql`WHERE \"inactivatedAt\" IS NOT NULL AND (\"inactivatedAt\" + \"ttl\") > NOW()`;\n return sql`WITH\n group_counts AS (\n SELECT\n \"clientGroupID\",\n COUNT(*) AS num_queries\n FROM ${sql(schema)}.\"desires\"\n ${filter}\n GROUP BY \"clientGroupID\"\n ),\n -- Count distinct clientIDs per clientGroupID\n client_per_group_counts AS (\n SELECT\n \"clientGroupID\",\n COUNT(DISTINCT \"clientID\") AS num_clients\n FROM ${sql(schema)}.\"desires\"\n ${filter}\n GROUP BY \"clientGroupID\"\n )\n -- Combine all the information\n SELECT\n g.\"clientGroupID\",\n cpg.num_clients,\n g.num_queries\n FROM group_counts g\n JOIN client_per_group_counts cpg ON g.\"clientGroupID\" = cpg.\"clientGroupID\"\n ORDER BY g.num_queries DESC;`;\n }\n\n try {\n return await getPgStats([\n [\n 'totalNumQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\"`,\n ],\n [\n 'numUniqueQueryHashes',\n sql`SELECT COUNT(DISTINCT \"queryHash\") as \"c\" FROM ${sql(\n schema,\n )}.\"desires\"`,\n ],\n [\n 'numActiveQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"inactivatedAt\" IS NULL AND \"deleted\" = false`,\n ],\n [\n 'numInactiveQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"inactivatedAt\" IS NOT NULL AND (\"inactivatedAt\" + \"ttl\") > NOW()`,\n ],\n [\n 'numDeletedQueries',\n sql`SELECT COUNT(*) as \"c\" FROM ${sql(schema)}.\"desires\" WHERE \"deleted\" = true`,\n ],\n [\n 'freshQueriesPercentiles',\n sql`WITH client_group_counts AS (\n -- Count inactive desires per clientGroupID\n SELECT\n \"clientGroupID\",\n COUNT(*) AS fresh_count\n FROM ${sql(schema)}.\"desires\"\n WHERE\n (\"inactivatedAt\" IS NOT NULL\n AND (\"inactivatedAt\" + \"ttl\") > NOW()) OR (\"inactivatedAt\" IS NULL\n AND deleted = false)\n GROUP BY \"clientGroupID\"\n )\n\n SELECT\n percentile_cont(0.50) WITHIN GROUP (ORDER BY fresh_count) AS \"p50\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY fresh_count) AS \"p75\",\n percentile_cont(0.90) WITHIN GROUP (ORDER BY fresh_count) AS \"p90\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY fresh_count) AS \"p95\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY fresh_count) AS \"p99\",\n MIN(fresh_count) AS \"min\",\n MAX(fresh_count) AS \"max\",\n AVG(fresh_count) AS \"avg\"\n FROM client_group_counts;`,\n ],\n [\n 'rowsPerClientGroupPercentiles',\n sql`WITH client_group_counts AS (\n -- Count inactive desires per clientGroupID\n SELECT\n \"clientGroupID\",\n COUNT(*) AS row_count\n FROM ${sql(schema)}.\"rows\"\n GROUP BY \"clientGroupID\"\n )\n SELECT\n percentile_cont(0.50) WITHIN GROUP (ORDER BY row_count) AS \"p50\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY row_count) AS \"p75\",\n percentile_cont(0.90) WITHIN GROUP (ORDER BY row_count) AS \"p90\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY row_count) AS \"p95\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY row_count) AS \"p99\",\n MIN(row_count) AS \"min\",\n MAX(row_count) AS \"max\",\n AVG(row_count) AS \"avg\"\n FROM client_group_counts;`,\n ],\n [\n // check for AST blowup due to DNF conversion.\n 'astSizes',\n sql`SELECT\n percentile_cont(0.25) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"25th_percentile\",\n percentile_cont(0.5) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"50th_percentile\",\n percentile_cont(0.75) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"75th_percentile\",\n percentile_cont(0.9) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"90th_percentile\",\n percentile_cont(0.95) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"95th_percentile\",\n percentile_cont(0.99) WITHIN GROUP (ORDER BY length(\"clientAST\"::text)) AS \"99th_percentile\",\n MIN(length(\"clientAST\"::text)) AS \"minimum_length\",\n MAX(length(\"clientAST\"::text)) AS \"maximum_length\",\n AVG(length(\"clientAST\"::text))::integer AS \"average_length\",\n COUNT(*) AS \"total_records\"\n FROM ${sql(schema)}.\"queries\";`,\n ],\n [\n // output the hash of the largest AST\n 'biggestAstHash',\n sql`SELECT \"queryHash\", length(\"clientAST\"::text) AS \"ast_length\"\n FROM ${sql(schema)}.\"queries\"\n ORDER BY length(\"clientAST\"::text) DESC\n LIMIT 1;`,\n ],\n [\n 'totalActiveQueriesPerClientAndClientGroup',\n numQueriesPerClientGroup(true),\n ],\n [\n 'totalInactiveQueriesPerClientAndClientGroup',\n numQueriesPerClientGroup(false),\n ],\n [\n 'totalRowsPerClientGroup',\n sql`SELECT \"clientGroupID\", COUNT(*) as \"c\" FROM ${sql(\n schema,\n )}.\"rows\" GROUP BY \"clientGroupID\" ORDER BY \"c\" DESC`,\n ],\n [\n 'numRowsPerQuery',\n sql`SELECT\n k.key AS \"queryHash\",\n COUNT(*) AS row_count\n FROM ${sql(schema)}.\"rows\" r,\n LATERAL jsonb_each(r.\"refCounts\") k\n GROUP BY k.key\n ORDER BY row_count DESC;`,\n ],\n ] satisfies [\n name: string,\n query: ReturnType<ReturnType<typeof pgClient>>,\n ][]);\n } finally {\n await sql.end();\n }\n}\n\nasync function changeLogStats(lc: LogContext, config: ZeroConfig) {\n const schema = upstreamSchema(getShardID(config)) + '/cdc';\n const sql = pgClient(lc, config.change.db, 'statz-change');\n\n try {\n return await getPgStats([\n [\n 'changeLogSize',\n sql`SELECT COUNT(*) as \"change_log_size\" FROM ${sql(schema)}.\"changeLog\"`,\n ],\n ]);\n } finally {\n await sql.end();\n }\n}\n\n// The replica is opened read-only: /statz is available to the operator\n// password, and must not change the replica. Among other things, a writable\n// connection would run `PRAGMA optimize` when closed.\nfunction replicaStats(lc: LogContext, config: ZeroConfig) {\n const db = new Database(lc, config.replica.file, {readonly: true});\n try {\n return Object.fromEntries([\n ['wal file sizes', walFileSizes(config.replica.file)],\n ['page count', pick(first(db.pragma('PAGE_COUNT')))],\n ['page size', pick(first(db.pragma('PAGE_SIZE')))],\n ['journal mode', pick(first(db.pragma('JOURNAL_MODE')))],\n ['synchronous', pick(first(db.pragma('SYNCHRONOUS')))],\n ['cache size', pick(first(db.pragma('CACHE_SIZE')))],\n ['auto vacuum', pick(first(db.pragma('AUTO_VACUUM')))],\n ['freelist count', pick(first(db.pragma('FREELIST_COUNT')))],\n ['wal autocheckpoint', pick(first(db.pragma('WAL_AUTOCHECKPOINT')))],\n ['db file stats', fs.statSync(config.replica.file)],\n ] as const);\n } finally {\n db.close();\n }\n}\n\nfunction replicationStats(lc: LogContext, config: ZeroConfig) {\n const db = new Database(lc, config.replica.file, {readonly: true});\n try {\n return getReplicationStats(db);\n } finally {\n db.close();\n }\n}\n\n/**\n * The sizes of the replica's WAL files, by suffix, read from the filesystem.\n * `PRAGMA wal_checkpoint` would report the WAL's frame counts, but it runs a\n * checkpoint to do so, and fails on a read-only connection in WAL mode.\n */\nfunction walFileSizes(file: string): Record<string, number> {\n const sizes: Record<string, number> = {};\n // `-wal` is used in WAL mode, and both files in WAL2 mode.\n for (const suffix of ['-wal', '-wal2']) {\n const stats = fs.statSync(file + suffix, {throwIfNoEntry: false});\n if (stats) {\n sizes[suffix] = stats.size;\n }\n }\n return sizes;\n}\n\nfunction getReplicationStats(db: Database) {\n const {stateVersion} = getReplicationState(new StatementRunner(db));\n const lsn = fromStateVersionString(stateVersion);\n return {lsn};\n}\n\nfunction osStats() {\n return Object.fromEntries([\n ['load avg', os.loadavg()],\n ['uptime', os.uptime()],\n ['total mem', os.totalmem()],\n ['free mem', os.freemem()],\n ['cpus', os.cpus().length],\n ['available parallelism', os.availableParallelism()],\n ['platform', os.platform()],\n ['arch', os.arch()],\n ['release', os.release()],\n ] as const);\n}\n\nasync function getPgStats(\n pendingQueries: [\n name: string,\n query: ReturnType<ReturnType<typeof pgClient>>,\n ][],\n) {\n const results = await Promise.all(\n pendingQueries.map(async ([name, query]) => [name, await query] as const),\n );\n return Object.fromEntries(results);\n}\n\ntype StatsObject = Record<string, unknown>;\n\nfunction printStats(group: string, statsObject: StatsObject): string {\n const lines: string[] = ['\\n' + header(group)];\n for (const [name, result] of Object.entries(statsObject)) {\n lines.push('\\n' + name + ': ' + BigIntJSON.stringify(result, null, 2));\n }\n lines.push('\\n');\n return lines.join('');\n}\n\n/**\n * Serves counts and sizes. This is available to the operator password as well\n * as the admin password (see `--operator-password`), so it must never return\n * application data: no row values, query ASTs or query arguments.\n *\n * HTTP query parameters:\n * * `group`: restricts the groups for which stats are computed\n * * `format=json`: returns the stats as a JSON object\n * * `pretty`: formats the JSON object with indentation\n */\nexport async function handleStatzRequest(\n lc: LogContext,\n config: ZeroConfig,\n req: FastifyRequest,\n res: FastifyReply,\n) {\n const credentials = auth(req);\n if (getOperatorAccess(lc, config, credentials?.pass) === undefined) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Statz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n const statsFns: Record<string, () => Promise<StatsObject> | StatsObject> = {\n upstream: () => upstreamStats(lc, config),\n cvr: () => cvrStats(lc, config),\n // The PG change log (and thus the change DB) is not used when disabled.\n ...(config.changeStreamer.pgChangeLogEnabled\n ? {changeLog: () => changeLogStats(lc, config)}\n : {}),\n replica: () => replicaStats(lc, config),\n replication: () => replicationStats(lc, config),\n os: () => osStats(),\n };\n\n async function computeStats(group: string): Promise<[string, StatsObject]> {\n try {\n return [group, await statsFns[group]()];\n } catch (e) {\n lc.error?.(`error computing ${group} stats`, e);\n return [group, {error: String(e)}];\n }\n }\n\n const query = req.query as Record<string, unknown>;\n const groups =\n typeof query.group === 'string'\n ? query.group.split(',')\n : Array.isArray(query.group)\n ? query.group\n : undefined;\n\n const stats = await Promise.all(\n groups\n ? groups.filter(g => g in statsFns).map(computeStats)\n : Object.keys(statsFns).map(computeStats),\n );\n\n if (query.format === 'json') {\n const indent = query.pretty !== undefined ? 2 : undefined;\n await res\n .header('Content-Type', 'application/json')\n .send(BigIntJSON.stringify(Object.fromEntries(stats), null, indent));\n return;\n } else {\n const body = stats\n .map(([group, statsObject]) => printStats(group, statsObject))\n .join('');\n await res.header('Content-Type', 'text/plain; charset=utf-8').send(body);\n }\n}\n\nfunction first(x: object[]): object {\n return x[0];\n}\n\nfunction pick(x: object): unknown {\n return Object.values(x)[0];\n}\n\nfunction header(name: string): string {\n return `=== ${name} ===\\n`;\n}\n"],"mappings":"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"}
|
|
@@ -9,7 +9,7 @@ import { getOrCreateCounter } from "../observability/metrics.js";
|
|
|
9
9
|
import { ProtocolErrorWithLevel, getLogLevel, wrapWithProtocolError } from "../types/error-with-level.js";
|
|
10
10
|
import { upstreamSchemaWithUnparsedAnalyzeQuery } from "../../../zero-protocol/src/up.js";
|
|
11
11
|
import { PokeChunkEncoder } from "../types/poke-chunk.js";
|
|
12
|
-
import WebSocket
|
|
12
|
+
import WebSocket, { createWebSocketStream } from "ws";
|
|
13
13
|
import { Readable, Writable, pipeline } from "node:stream";
|
|
14
14
|
//#region ../zero-cache/src/workers/connection.ts
|
|
15
15
|
function hasOwn(value, property) {
|
|
@@ -224,7 +224,7 @@ var Connection = class {
|
|
|
224
224
|
}
|
|
225
225
|
};
|
|
226
226
|
function send(lc, ws, data, callback, serialized) {
|
|
227
|
-
if (ws.readyState === WebSocket
|
|
227
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
228
228
|
serialized ??= JSON.stringify(data);
|
|
229
229
|
if (callback === "ignore-backpressure") {
|
|
230
230
|
ws.send(serialized);
|
|
@@ -310,7 +310,7 @@ function sendAsync(lc, ws, data) {
|
|
|
310
310
|
}
|
|
311
311
|
function sendBinary(lc, ws, chunk) {
|
|
312
312
|
return new Promise((resolve, reject) => {
|
|
313
|
-
if (ws.readyState !== WebSocket
|
|
313
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
314
314
|
const error = webSocketClosedError();
|
|
315
315
|
lc.debug?.(`Dropping outbound binary message on ws (state: ${ws.readyState})`);
|
|
316
316
|
reject(error);
|