@rocicorp/zero 1.12.0-canary.12 → 1.12.0-canary.13
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/replicache/src/btree/diff.d.ts.map +1 -1
- package/out/replicache/src/btree/diff.js +1 -2
- package/out/replicache/src/btree/diff.js.map +1 -1
- package/out/replicache/src/btree/read.d.ts +31 -1
- package/out/replicache/src/btree/read.d.ts.map +1 -1
- package/out/replicache/src/btree/read.js +74 -19
- package/out/replicache/src/btree/read.js.map +1 -1
- package/out/replicache/src/btree/splice.d.ts.map +1 -1
- package/out/replicache/src/btree/splice.js +2 -1
- package/out/replicache/src/btree/splice.js.map +1 -1
- package/out/replicache/src/btree/write.d.ts.map +1 -1
- package/out/replicache/src/btree/write.js +2 -1
- package/out/replicache/src/btree/write.js.map +1 -1
- package/out/replicache/src/persist/collect-idb-databases.d.ts +1 -0
- package/out/replicache/src/persist/collect-idb-databases.d.ts.map +1 -1
- package/out/replicache/src/persist/collect-idb-databases.js +13 -5
- package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
- package/out/replicache/src/scan-iterator.js +1 -1
- package/out/replicache/src/sync/request-id.js +2 -2
- package/out/replicache/src/sync/request-id.js.map +1 -1
- package/out/shared/src/bigint-json.d.ts.map +1 -1
- package/out/shared/src/bigint-json.js +1 -0
- package/out/shared/src/bigint-json.js.map +1 -1
- package/out/shared/src/btree-set.d.ts +21 -0
- package/out/shared/src/btree-set.d.ts.map +1 -1
- package/out/shared/src/btree-set.js +54 -0
- package/out/shared/src/btree-set.js.map +1 -1
- package/out/shared/src/headers.js +20 -0
- package/out/shared/src/headers.js.map +1 -0
- package/out/shared/src/natural-runs.d.ts +191 -0
- package/out/shared/src/natural-runs.d.ts.map +1 -0
- package/out/shared/src/natural-runs.js +337 -0
- package/out/shared/src/natural-runs.js.map +1 -0
- package/out/shared/src/random-uint64.d.ts.map +1 -1
- package/out/shared/src/random-uint64.js +2 -2
- package/out/shared/src/random-uint64.js.map +1 -1
- package/out/shared/src/random-values.d.ts +19 -1
- package/out/shared/src/random-values.d.ts.map +1 -1
- package/out/shared/src/random-values.js +38 -4
- package/out/shared/src/random-values.js.map +1 -1
- package/out/shared/src/tuple-map.d.ts +22 -0
- package/out/shared/src/tuple-map.d.ts.map +1 -0
- package/out/shared/src/tuple-map.js +48 -0
- package/out/shared/src/tuple-map.js.map +1 -0
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
- package/out/zero-cache/src/auth/auth.js +5 -0
- package/out/zero-cache/src/auth/auth.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +21 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +47 -9
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.js +2 -2
- package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
- package/out/zero-cache/src/db/pg-to-lite.d.ts.map +1 -1
- package/out/zero-cache/src/db/pg-to-lite.js +3 -2
- package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
- package/out/zero-cache/src/db/pg-type-parser.js +55 -2
- package/out/zero-cache/src/db/pg-type-parser.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +5 -2
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/inspector-delegate.js +6 -29
- package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
- package/out/zero-cache/src/server/main.js +5 -0
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/replicator.js +3 -2
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/syncer.js +5 -4
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/server/worker-dispatcher.d.ts.map +1 -1
- package/out/zero-cache/src/server/worker-dispatcher.js +2 -1
- package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
- package/out/zero-cache/src/services/admin-auth.d.ts +22 -0
- package/out/zero-cache/src/services/admin-auth.d.ts.map +1 -0
- package/out/zero-cache/src/services/admin-auth.js +42 -0
- package/out/zero-cache/src/services/admin-auth.js.map +1 -0
- package/out/zero-cache/src/services/auth-attempt-counter.d.ts +33 -0
- package/out/zero-cache/src/services/auth-attempt-counter.d.ts.map +1 -0
- package/out/zero-cache/src/services/auth-attempt-counter.js +61 -0
- package/out/zero-cache/src/services/auth-attempt-counter.js.map +1 -0
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +110 -13
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +13 -7
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +38 -5
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js +10 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +4 -0
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js +10 -3
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +23 -2
- 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.js +2 -2
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.js +10 -3
- package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
- package/out/zero-cache/src/services/heapz.d.ts.map +1 -1
- package/out/zero-cache/src/services/heapz.js +4 -5
- package/out/zero-cache/src/services/heapz.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/error.d.ts +5 -0
- package/out/zero-cache/src/services/mutagen/error.d.ts.map +1 -1
- package/out/zero-cache/src/services/mutagen/error.js +7 -1
- package/out/zero-cache/src/services/mutagen/error.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
- package/out/zero-cache/src/services/plannerz.d.ts.map +1 -1
- package/out/zero-cache/src/services/plannerz.js +3 -7
- package/out/zero-cache/src/services/plannerz.js.map +1 -1
- package/out/zero-cache/src/services/profz.d.ts.map +1 -1
- package/out/zero-cache/src/services/profz.js +4 -13
- package/out/zero-cache/src/services/profz.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.js +29 -2
- package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.js +3 -3
- package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
- package/out/zero-cache/src/services/statz.d.ts.map +1 -1
- package/out/zero-cache/src/services/statz.js +2 -7
- package/out/zero-cache/src/services/statz.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js +22 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +40 -2
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js +54 -17
- package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +11 -0
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js +107 -0
- package/out/zero-cache/src/services/view-syncer/inspect-auth-limiter.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js +11 -6
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +2 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js +16 -14
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/cvr.js +4 -1
- package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/init.js +3 -0
- package/out/zero-cache/src/services/view-syncer/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +89 -51
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/client-group-log-id.d.ts +9 -0
- package/out/zero-cache/src/types/client-group-log-id.d.ts.map +1 -0
- package/out/zero-cache/src/types/client-group-log-id.js +16 -0
- package/out/zero-cache/src/types/client-group-log-id.js.map +1 -0
- package/out/zero-cache/src/types/lite.d.ts.map +1 -1
- package/out/zero-cache/src/types/lite.js +1 -0
- package/out/zero-cache/src/types/lite.js.map +1 -1
- package/out/zero-cache/src/types/pg.d.ts +13 -2
- package/out/zero-cache/src/types/pg.d.ts.map +1 -1
- package/out/zero-cache/src/types/pg.js +2 -2
- package/out/zero-cache/src/types/pg.js.map +1 -1
- package/out/zero-cache/src/types/poke-chunk.d.ts +25 -4
- package/out/zero-cache/src/types/poke-chunk.d.ts.map +1 -1
- package/out/zero-cache/src/types/poke-chunk.js +46 -5
- package/out/zero-cache/src/types/poke-chunk.js.map +1 -1
- package/out/zero-cache/src/workers/connection.d.ts.map +1 -1
- package/out/zero-cache/src/workers/connection.js +10 -3
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/rehome.d.ts +10 -0
- package/out/zero-cache/src/workers/rehome.d.ts.map +1 -1
- package/out/zero-cache/src/workers/rehome.js +4 -2
- package/out/zero-cache/src/workers/rehome.js.map +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js +3 -2
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +15 -15
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/inspector/inspector.d.ts +4 -0
- package/out/zero-client/src/client/inspector/inspector.d.ts.map +1 -1
- package/out/zero-client/src/client/inspector/inspector.js +4 -0
- package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
- package/out/zero-client/src/client/inspector/lazy-inspector.d.ts +10 -2
- package/out/zero-client/src/client/inspector/lazy-inspector.d.ts.map +1 -1
- package/out/zero-client/src/client/inspector/lazy-inspector.js +38 -31
- package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
- package/out/zero-client/src/client/inspector/query.d.ts +2 -2
- package/out/zero-client/src/client/inspector/query.d.ts.map +1 -1
- package/out/zero-client/src/client/inspector/query.js +4 -4
- package/out/zero-client/src/client/inspector/query.js.map +1 -1
- package/out/zero-client/src/client/ivm-branch.d.ts +3 -0
- package/out/zero-client/src/client/ivm-branch.d.ts.map +1 -1
- package/out/zero-client/src/client/ivm-branch.js +10 -5
- package/out/zero-client/src/client/ivm-branch.js.map +1 -1
- package/out/zero-client/src/client/reconnect-backoff.d.ts +31 -0
- package/out/zero-client/src/client/reconnect-backoff.d.ts.map +1 -0
- package/out/zero-client/src/client/reconnect-backoff.js +44 -0
- package/out/zero-client/src/client/reconnect-backoff.js.map +1 -0
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +7 -0
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-client/src/util/nanoid.js +2 -2
- package/out/zero-client/src/util/nanoid.js.map +1 -1
- package/out/zero-protocol/src/up.d.ts +42 -42
- package/out/zero-server/src/process-mutations.d.ts +10 -0
- package/out/zero-server/src/process-mutations.d.ts.map +1 -1
- package/out/zero-server/src/process-mutations.js +39 -6
- package/out/zero-server/src/process-mutations.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +89 -6
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/planner/planner-connection.d.ts.map +1 -1
- package/out/zql/src/planner/planner-connection.js +37 -3
- package/out/zql/src/planner/planner-connection.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-processor.js","names":["bufferConstants"],"sources":["../../../../../../zero-cache/src/services/replicator/change-processor.ts"],"sourcesContent":["import {constants as bufferConstants} from 'node:buffer';\nimport type {LogContext} from '@rocicorp/logger';\nimport {SqliteError} from '@rocicorp/zero-sqlite3';\nimport {AbortError} from '../../../../shared/src/abort-error.ts';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport {stringify} from '../../../../shared/src/bigint-json.ts';\nimport {deepEqual} from '../../../../shared/src/json.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {mapEntries} from '../../../../shared/src/objects.ts';\nimport type {DownloadStatus} from '../../../../zero-events/src/status.ts';\nimport {\n createLiteIndexStatement,\n createLiteTableStatement,\n liteColumnDef,\n} from '../../db/create.ts';\nimport {\n computeZqlSpecs,\n listTables,\n type LiteTableSpecWithReplicationStatus,\n} from '../../db/lite-tables.ts';\nimport {\n isArrayColumn,\n isEnumColumn,\n mapPostgresToLite,\n mapPostgresToLiteColumn,\n mapPostgresToLiteIndex,\n} from '../../db/pg-to-lite.ts';\nimport type {ColumnSpec} from '../../db/specs.ts';\nimport type {StatementRunner} from '../../db/statements.ts';\nimport type {LexiVersion} from '../../types/lexi-version.ts';\nimport {\n JSON_PARSED,\n liteRow,\n type JSONFormat,\n type LiteRow,\n type LiteRowKey,\n type LiteValueType,\n} from '../../types/lite.ts';\nimport {liteTableName} from '../../types/names.ts';\nimport {id} from '../../types/sql.ts';\nimport {\n acceptBackfill,\n type AcceptResult,\n} from '../change-source/protocol/backfill-progress.ts';\nimport type {\n BackfillCompleted,\n BackfillProgressMark,\n Change,\n ColumnAdd,\n ColumnDrop,\n ColumnUpdate,\n Identifier,\n IndexCreate,\n IndexDrop,\n MessageBackfill,\n MessageCommit,\n MessageDelete,\n MessageInsert,\n MessageRelation,\n MessageTruncate,\n MessageUpdate,\n TableCreate,\n TableDrop,\n TableRename,\n TableUpdateMetadata,\n} from '../change-source/protocol/current/data.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport type {ReplicatorMode} from './replicator.ts';\nimport {BackfillingTracker} from './schema/backfilling.ts';\nimport {ChangeLog, DEL_OP, SET_OP} from './schema/change-log.ts';\nimport {ColumnMetadataStore} from './schema/column-metadata.ts';\nimport {\n ZERO_VERSION_COLUMN_NAME,\n updateReplicationWatermark,\n} from './schema/replication-state.ts';\nimport {TableMetadataTracker} from './schema/table-metadata.ts';\n\nexport type ChangeProcessorMode = ReplicatorMode | 'initial-sync';\n\nconst BIND_VALUE_TOO_BIG_ERROR =\n 'The bound string, buffer, or bigint is too big';\nconst MAX_SQLITE_BIND_BYTES = Math.min(\n bufferConstants.MAX_LENGTH,\n bufferConstants.MAX_STRING_LENGTH,\n);\n\nexport type CommitResult = {\n watermark: string;\n completedBackfill: DownloadStatus | undefined;\n schemaUpdated: boolean;\n changeLogUpdated: boolean;\n /**\n * Millisecond epoch at which the transaction committed upstream, if the\n * ChangeSource reported one. Propagated to ViewSyncers as the origin\n * timestamp of the end-to-end serving lag measurement.\n */\n upstreamCommitTimeMs?: number | undefined;\n};\n\n/**\n * The ChangeProcessor partitions the stream of messages into transactions\n * by creating a {@link TransactionProcessor} when a transaction begins, and dispatching\n * messages to it until the commit is received.\n *\n * From https://www.postgresql.org/docs/current/protocol-logical-replication.html#PROTOCOL-LOGICAL-MESSAGES-FLOW :\n *\n * \"The logical replication protocol sends individual transactions one by one.\n * This means that all messages between a pair of Begin and Commit messages\n * belong to the same transaction.\"\n */\nexport class ChangeProcessor {\n readonly #db: StatementRunner;\n readonly #changeLog: ChangeLog;\n readonly #tableMetadata: TableMetadataTracker;\n readonly #backfilling: BackfillingTracker;\n readonly #mode: ChangeProcessorMode;\n readonly #failService: (lc: LogContext, err: unknown) => void;\n\n // The TransactionProcessor lazily loads table specs into this Map,\n // and reloads them after a schema change. It is cached here to avoid\n // reading them from the DB on every transaction.\n readonly #tableSpecs = new Map<string, LiteTableSpecWithReplicationStatus>();\n\n #currentTx: TransactionProcessor | null = null;\n\n #failure: Error | undefined;\n\n constructor(\n db: StatementRunner,\n mode: ChangeProcessorMode,\n failService: (lc: LogContext, err: unknown) => void,\n ) {\n this.#db = db;\n this.#changeLog = new ChangeLog(db.db);\n this.#tableMetadata = new TableMetadataTracker(db.db);\n this.#backfilling = new BackfillingTracker(db.db);\n this.#mode = mode;\n this.#failService = failService;\n }\n\n #fail(lc: LogContext, err: unknown) {\n if (!this.#failure) {\n let failureError = err;\n const rollbackErrors = [attempt(() => this.#currentTx?.abort(lc))].filter(\n e => e !== undefined,\n );\n\n if (rollbackErrors.length) {\n const combinedError = new Error(\n `Message processing failed and rollback also failed: operation error = ${String(err)}; rollback error = ${rollbackErrors.map(String).join('; ')}`,\n );\n combinedError.cause = err;\n failureError = combinedError;\n }\n\n this.#failure = ensureError(failureError);\n\n if (!(this.#failure instanceof AbortError)) {\n // Propagate the failure up to the service.\n lc.error?.('Message Processing failed:', this.#failure);\n this.#failService(lc, this.#failure);\n }\n }\n }\n\n abort(lc: LogContext) {\n this.#fail(lc, new AbortError());\n }\n\n /** @return If a transaction was committed. */\n processMessage(\n lc: LogContext,\n downstream: ChangeStreamData,\n ): CommitResult | null {\n const [type, message] = downstream;\n if (this.#failure) {\n lc.debug?.(`Dropping ${message.tag}`);\n return null;\n }\n try {\n const watermark =\n type === 'begin'\n ? downstream[2].commitWatermark\n : type === 'commit'\n ? downstream[2].watermark\n : undefined;\n return this.#processMessage(lc, message, watermark);\n } catch (e) {\n this.#fail(lc, e);\n }\n return null;\n }\n\n #beginTransaction(\n lc: LogContext,\n commitVersion: string,\n jsonFormat: JSONFormat,\n ): TransactionProcessor {\n const start = Date.now();\n\n // litestream can technically hold the lock for an arbitrary amount of time\n // when checkpointing a large commit. Crashing on the busy-timeout in this\n // scenario will either produce a corrupt backup or otherwise prevent\n // replication from proceeding.\n //\n // Instead, retry the lock acquisition indefinitely. If this masks\n // an unknown deadlock situation, manual intervention will be necessary.\n for (let i = 0; ; i++) {\n try {\n return new TransactionProcessor(\n lc,\n this.#db,\n this.#mode,\n this.#changeLog,\n this.#tableMetadata,\n this.#backfilling,\n this.#tableSpecs,\n commitVersion,\n jsonFormat,\n );\n } catch (e) {\n if (e instanceof SqliteError && e.code === 'SQLITE_BUSY') {\n lc.warn?.(\n `SQLITE_BUSY for ${Date.now() - start} ms (attempt ${i + 1}). ` +\n `This is only expected if litestream is performing a large ` +\n `checkpoint.`,\n e,\n );\n continue;\n }\n throw e;\n }\n }\n }\n\n /** @return If a transaction was committed. */\n #processMessage(\n lc: LogContext,\n msg: Change,\n watermark: string | undefined,\n ): CommitResult | null {\n if (msg.tag === 'begin') {\n if (this.#currentTx) {\n throw new Error(`Already in a transaction ${stringify(msg)}`);\n }\n this.#currentTx = this.#beginTransaction(\n lc,\n must(watermark),\n msg.json ?? JSON_PARSED,\n );\n return null;\n }\n\n // For non-begin messages, there should be a #currentTx set.\n const tx = this.#currentTx;\n if (!tx) {\n throw new Error(\n `Received message outside of transaction: ${stringify(msg)}`,\n );\n }\n\n if (msg.tag === 'commit') {\n assert(watermark, 'watermark is required for commit messages');\n const result = tx.processCommit(msg, watermark);\n // Clear only after a successful commit so #fail can roll back a commit\n // path that throws before SQLite has committed.\n this.#currentTx = null;\n return result;\n }\n\n if (msg.tag === 'rollback') {\n tx.abort(lc);\n this.#currentTx = null;\n return null;\n }\n\n switch (msg.tag) {\n case 'insert':\n tx.processInsert(msg);\n break;\n case 'update':\n tx.processUpdate(msg);\n break;\n case 'delete':\n tx.processDelete(msg);\n break;\n case 'truncate':\n tx.processTruncate(msg);\n break;\n case 'create-table':\n tx.processCreateTable(msg);\n break;\n case 'rename-table':\n tx.processRenameTable(msg);\n break;\n case 'update-table-metadata':\n tx.processTableMetadata(msg);\n break;\n case 'add-column':\n tx.processAddColumn(msg);\n break;\n case 'update-column':\n tx.processUpdateColumn(msg);\n break;\n case 'drop-column':\n tx.processDropColumn(msg);\n break;\n case 'drop-table':\n tx.processDropTable(msg);\n break;\n case 'create-index':\n tx.processCreateIndex(msg);\n break;\n case 'drop-index':\n tx.processDropIndex(msg);\n break;\n case 'backfill':\n tx.processBackfill(msg);\n break;\n case 'backfill-completed':\n tx.processBackfillCompleted(msg);\n break;\n default:\n unreachable(msg);\n }\n\n return null;\n }\n}\n\n/**\n * The {@link TransactionProcessor} handles the sequence of messages from\n * upstream, from `BEGIN` to `COMMIT` and executes the corresponding mutations\n * on the {@link postgres.TransactionSql} on the replica.\n *\n * When applying row contents to the replica, the `_0_version` column is added / updated,\n * and a corresponding entry in the `ChangeLog` is added. The version value is derived\n * from the watermark of the preceding transaction (stored as the `nextStateVersion` in the\n * `ReplicationState` table).\n *\n * Side note: For non-streaming Postgres transactions, the commitEndLsn (and thus\n * commit watermark) is available in the `begin` message, so it could theoretically\n * be used for the row version of changes within the transaction. However, the\n * commitEndLsn is not available in the streaming (in-progress) transaction\n * protocol, and may not be available for CDC streams of other upstream types.\n * Therefore, the zero replication protocol is designed to not require the commit\n * watermark when a transaction begins.\n *\n * Also of interest is the fact that all INSERT Messages are logically applied as\n * UPSERTs. See {@link processInsert} for the underlying motivation.\n */\nclass TransactionProcessor {\n readonly #lc: LogContext;\n readonly #startMs: number;\n readonly #db: StatementRunner;\n readonly #mode: ChangeProcessorMode;\n readonly #version: LexiVersion;\n readonly #changeLog: ChangeLog;\n readonly #tableMetadata: TableMetadataTracker;\n readonly #backfilling: BackfillingTracker;\n readonly #tableSpecs: Map<string, LiteTableSpecWithReplicationStatus>;\n readonly #jsonFormat: JSONFormat;\n readonly #columnMetadata: ColumnMetadataStore;\n\n #pos = 0;\n #schemaChanged = false;\n #numChangeLogEntries = 0;\n\n constructor(\n lc: LogContext,\n db: StatementRunner,\n mode: ChangeProcessorMode,\n changeLog: ChangeLog,\n tableMetadata: TableMetadataTracker,\n backfilling: BackfillingTracker,\n tableSpecs: Map<string, LiteTableSpecWithReplicationStatus>,\n commitVersion: LexiVersion,\n jsonFormat: JSONFormat,\n ) {\n this.#startMs = Date.now();\n this.#mode = mode;\n this.#jsonFormat = jsonFormat;\n\n switch (mode) {\n case 'serving':\n // This is the only transaction that commits to the serving replica.\n // Snapshotters use BEGIN CONCURRENT for private changes that are\n // always rolled back, while BEGIN IMMEDIATE lets this writer spill\n // dirty pages during large transactions.\n db.beginImmediate();\n break;\n case 'backup':\n // For the backup-replicator (i.e. replication-manager), there are no View Syncers\n // and thus BEGIN CONCURRENT is not necessary. In fact, BEGIN CONCURRENT can cause\n // deadlocks with forced wal-checkpoints (which `litestream replicate` performs),\n // so it is important to use vanilla transactions in this configuration.\n db.beginImmediate();\n break;\n case 'initial-sync':\n // When the ChangeProcessor is used for initial-sync, the calling code\n // handles the transaction boundaries.\n break;\n default:\n unreachable();\n }\n this.#db = db;\n this.#version = commitVersion;\n this.#lc = lc.withContext('version', commitVersion);\n this.#changeLog = changeLog;\n this.#tableMetadata = tableMetadata;\n this.#backfilling = backfilling;\n this.#tableSpecs = tableSpecs;\n // The column_metadata table is guaranteed to exist since the\n // replica-schema.ts migration to v8.\n this.#columnMetadata = must(ColumnMetadataStore.getInstance(db.db));\n\n if (this.#tableSpecs.size === 0) {\n this.#reloadTableSpecs();\n }\n }\n\n #reloadTableSpecs() {\n this.#tableSpecs.clear();\n // zqlSpecs include the primary key derived from unique indexes\n const zqlSpecs = computeZqlSpecs(this.#lc, this.#db.db, {\n includeBackfillingColumns: true,\n });\n for (let spec of listTables(this.#db.db)) {\n if (!spec.primaryKey) {\n spec = {\n ...spec,\n primaryKey: [\n ...(zqlSpecs.get(spec.name)?.tableSpec.primaryKey ?? []),\n ],\n };\n }\n this.#tableSpecs.set(spec.name, spec);\n }\n }\n\n #tableSpec(name: string) {\n return must(this.#tableSpecs.get(name), `Unknown table ${name}`);\n }\n\n #getKey(\n {row, numCols}: {row: LiteRow; numCols: number},\n {relation}: {relation: MessageRelation},\n ): LiteRowKey {\n const keyColumns =\n relation.rowKey.type !== 'full'\n ? relation.rowKey.columns // already a suitable key\n : this.#tableSpec(liteTableName(relation)).primaryKey;\n if (!keyColumns?.length) {\n throw new Error(\n `Cannot replicate table \"${relation.name}\" without a PRIMARY KEY or UNIQUE INDEX`,\n );\n }\n // For the common case (replica identity default), the row is already the\n // key for deletes and updates, in which case a new object can be avoided.\n if (numCols === keyColumns.length) {\n return row;\n }\n const key: Record<string, LiteValueType> = {};\n for (const col of keyColumns) {\n key[col] = row[col];\n }\n return key;\n }\n\n processInsert(insert: MessageInsert) {\n const table = liteTableName(insert.relation);\n const tableSpec = this.#tableSpec(table);\n const newRow = liteRow(insert.new, tableSpec, this.#jsonFormat);\n\n this.#upsert(table, {\n ...newRow.row,\n [ZERO_VERSION_COLUMN_NAME]: this.#version,\n });\n\n if (insert.relation.rowKey.columns.length === 0) {\n // INSERTs can be replicated for rows without a PRIMARY KEY or a\n // UNIQUE INDEX. These are written to the replica but not recorded\n // in the changeLog, because these rows cannot participate in IVM.\n //\n // (Once the table schema has been corrected to include a key, the\n // associated schema change will reset pipelines and data can be\n // loaded via hydration.)\n return;\n }\n const key = this.#getKey(newRow, insert);\n this.#logSetOp(table, key, getBackfilledColumns(newRow.row, tableSpec));\n }\n\n #upsert(table: string, row: LiteRow) {\n const columns = Object.keys(row).map(c => id(c));\n this.#db.run(\n `\n INSERT OR REPLACE INTO ${id(table)} (${columns.join(',')})\n VALUES (${Array.from({length: columns.length}).fill('?').join(',')})\n `,\n Object.values(row),\n );\n }\n\n // Updates by default are applied as UPDATE commands to support partial\n // row specifications from the change source. In particular, this is needed\n // to handle updates for which unchanged TOASTed values are not sent:\n //\n // https://www.postgresql.org/docs/current/protocol-logicalrep-message-formats.html#PROTOCOL-LOGICALREP-MESSAGE-FORMATS-TUPLEDATA\n //\n // However, in certain cases an UPDATE may be received for a row that\n // was not initially synced, such as when, an existing table is added\n // to the app's publication.\n //\n // In order to facilitate \"resumptive\" replication, the logic falls back to\n // an INSERT if the update did not change any rows.\n processUpdate(update: MessageUpdate) {\n const table = liteTableName(update.relation);\n const tableSpec = this.#tableSpec(table);\n const newRow = liteRow(update.new, tableSpec, this.#jsonFormat);\n const row = {...newRow.row, [ZERO_VERSION_COLUMN_NAME]: this.#version};\n\n // update.key is set with the old values if the key has changed.\n const oldKey = update.key\n ? this.#getKey(\n liteRow(update.key, this.#tableSpec(table), this.#jsonFormat),\n update,\n )\n : null;\n const newKey = this.#getKey(newRow, update);\n\n if (oldKey) {\n this.#logDeleteOp(table, oldKey, tableSpec.backfilling);\n }\n this.#logSetOp(table, newKey, getBackfilledColumns(newRow.row, tableSpec));\n\n const currKey = oldKey ?? newKey;\n const conds = Object.keys(currKey).map(col => `${id(col)}=?`);\n const setExprs = Object.keys(row).map(col => `${id(col)}=?`);\n\n try {\n const {changes} = this.#db.run(\n `\n UPDATE ${id(table)}\n SET ${setExprs.join(',')}\n WHERE ${conds.join(' AND ')}\n `,\n [...Object.values(row), ...Object.values(currKey)],\n );\n\n // If the UPDATE did not affect any rows, perform an UPSERT of the\n // new row for resumptive replication.\n if (changes === 0) {\n this.#upsert(table, row);\n }\n } catch (e) {\n if (\n !(e instanceof RangeError) ||\n e.message !== BIND_VALUE_TOO_BIG_ERROR\n ) {\n throw e;\n }\n\n const binding = [...Object.entries(row), ...Object.entries(currKey)]\n .map(\n ([column, value]) =>\n [column, oversizedValueDescription(value)] as const,\n )\n .find(([, description]) => description);\n const relationOid =\n 'relationOid' in update.relation &&\n typeof update.relation.relationOid === 'number'\n ? ` relationOid=${update.relation.relationOid}`\n : '';\n const error = new Error(\n `Oversized SQLite update binding: tx=${this.#version}${relationOid} table=${update.relation.schema}.${update.relation.name} column=${binding?.[0] ?? 'unknown'}${binding?.[1] ? ` ${binding[1]}` : ''}`,\n {cause: e},\n );\n error.name = 'OversizedUpdateBindingError';\n throw error;\n }\n }\n\n processDelete(del: MessageDelete) {\n const table = liteTableName(del.relation);\n const tableSpec = this.#tableSpec(table);\n const rowKey = this.#getKey(\n liteRow(del.key, tableSpec, this.#jsonFormat),\n del,\n );\n\n this.#delete(table, rowKey);\n this.#logDeleteOp(table, rowKey, tableSpec.backfilling);\n }\n\n #delete(table: string, rowKey: LiteRowKey) {\n const conds = Object.keys(rowKey).map(col => `${id(col)}=?`);\n this.#db.run(\n `DELETE FROM ${id(table)} WHERE ${conds.join(' AND ')}`,\n Object.values(rowKey),\n );\n }\n\n processTruncate(truncate: MessageTruncate) {\n for (const relation of truncate.relations) {\n const table = liteTableName(relation);\n // Update replica data.\n this.#db.run(`DELETE FROM ${id(table)}`);\n\n // Update change log.\n this.#logTruncateOp(table);\n }\n }\n\n processCreateTable(create: TableCreate) {\n if (create.metadata) {\n this.#tableMetadata.setUpstreamMetadata(create.spec, create.metadata);\n }\n this.#backfilling.apply(create);\n const table = mapPostgresToLite(create.spec);\n this.#db.db.exec(createLiteTableStatement(table));\n\n // Write to metadata table\n for (const [colName, colSpec] of Object.entries(create.spec.columns)) {\n this.#columnMetadata.insert(\n table.name,\n colName,\n colSpec,\n create.backfill?.[colName],\n );\n }\n\n this.#logResetOp(table.name);\n this.#lc.info?.(create.tag, table.name);\n }\n\n processTableMetadata(msg: TableUpdateMetadata) {\n this.#tableMetadata.setUpstreamMetadata(msg.table, msg.new);\n // Inert today: the fold's only op for this tag is `upsert-metadata`, which\n // the line above is the replica's interpreter of. Called anyway so that\n // every site that hands a schema change to one cookie tracker hands it to\n // both -- an exception here would be a schema change the shared fold has an\n // opinion about and this store never sees, which is the drift the fold\n // exists to make impossible.\n this.#backfilling.apply(msg);\n }\n\n processRenameTable(rename: TableRename) {\n this.#tableMetadata.rename(rename.old, rename.new);\n this.#backfilling.apply(rename);\n\n const oldName = liteTableName(rename.old);\n const newName = liteTableName(rename.new);\n this.#db.db.exec(`ALTER TABLE ${id(oldName)} RENAME TO ${id(newName)}`);\n\n // Rename in metadata table\n this.#columnMetadata.renameTable(oldName, newName);\n\n this.#bumpVersions(rename.new);\n this.#logResetOp(oldName);\n this.#lc.info?.(rename.tag, oldName, newName);\n }\n\n processAddColumn(msg: ColumnAdd) {\n if (msg.tableMetadata) {\n this.#tableMetadata.setUpstreamMetadata(msg.table, msg.tableMetadata);\n }\n this.#backfilling.apply(msg);\n const table = liteTableName(msg.table);\n const {name} = msg.column;\n const spec = mapPostgresToLiteColumn(table, msg.column);\n this.#db.db.exec(\n `ALTER TABLE ${id(table)} ADD ${id(name)} ${liteColumnDef(spec)}`,\n );\n\n // Write to metadata table\n this.#columnMetadata.insert(table, name, msg.column.spec, msg.backfill);\n\n if (msg.backfill) {\n this.#reloadTableSpecs();\n } else {\n // Make the new column visible immediately if it's not being backfilled.\n // Otherwise, the version bump will happen with the backfill is complete.\n this.#bumpVersions(msg.table);\n }\n this.#lc.info?.(msg.tag, table, msg.column);\n }\n\n processUpdateColumn(msg: ColumnUpdate) {\n // A no-op unless this is a rename, which is the only part of a column\n // update that moves the backfill cookie.\n this.#backfilling.apply(msg);\n\n const table = liteTableName(msg.table);\n let oldName = msg.old.name;\n const newName = msg.new.name;\n\n const storageTypesDiffer = differentStorageTypes(\n msg.old.spec,\n msg.new.spec,\n );\n\n // If neither the column name nor the SQLite data type changes, only the\n // upstream metadata needs to be updated. This includes changes such as a\n // varchar character limit and nullability, which SQLite does not enforce\n // but a freshly built replica still records.\n if (oldName === newName && !storageTypesDiffer) {\n this.#columnMetadata.update(\n table,\n msg.old.name,\n msg.new.name,\n msg.new.spec,\n );\n if (Boolean(msg.old.spec.notNull) !== Boolean(msg.new.spec.notNull)) {\n this.#bumpVersions(msg.table);\n }\n this.#lc.info?.(msg.tag, 'updated metadata only', msg.old, msg.new);\n return;\n }\n // Storage type changes require a new column so that SQLite uses the new\n // representation for existing and future values.\n if (storageTypesDiffer) {\n // update-column can ignore defaults because it does not change the values\n // in existing rows.\n //\n // https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-DESC-SET-DROP-DEFAULT\n //\n // \"The new default value will only apply in subsequent INSERT or UPDATE\n // commands; it does not cause rows already in the table to change.\"\n //\n // This allows support for _changing_ column defaults to any expression,\n // since it does not affect what the replica needs to do.\n const newLiteSpec = mapPostgresToLiteColumn(\n table,\n msg.new,\n 'ignore-default',\n );\n if (oldName !== newName) {\n // Rename the column in place first so that SQLite rewrites every\n // reference to it in the table's index definitions, including the\n // WHERE clauses of partial indexes.\n this.#db.db.exec(\n `ALTER TABLE ${id(table)} RENAME ${id(oldName)} TO ${id(newName)}`,\n );\n oldName = newName;\n }\n const tableSpec = must(\n listTables(this.#db.db, false, false).find(\n tableSpec => tableSpec.name === table,\n ),\n );\n // Recreating the table drops its indexes, so capture their DDL to\n // replay afterwards. Auto-indexes (PRIMARY KEY) have no DDL and are\n // recreated by the CREATE TABLE statement itself.\n const indexDDL = this.#db.db\n .prepare(\n `SELECT sql FROM sqlite_master\n WHERE type = 'index' AND tbl_name = ? AND sql IS NOT NULL\n ORDER BY rowid`,\n )\n .all<{sql: string}>(table)\n .map(({sql}) => sql);\n const tmpTable = `tmp.${table}`;\n const columns = Object.keys(tableSpec.columns);\n const stmts = [\n createLiteTableStatement({\n ...tableSpec,\n name: tmpTable,\n columns: mapEntries(tableSpec.columns, (column, spec) => [\n column,\n column === newName ? {...newLiteSpec, pos: spec.pos} : spec,\n ]),\n }),\n `INSERT INTO ${id(tmpTable)} (${columns.map(id).join(',')})\n SELECT ${columns.map(id).join(',')} FROM ${id(table)};`,\n `DROP TABLE ${id(table)};`,\n `ALTER TABLE ${id(tmpTable)} RENAME TO ${id(table)};`,\n ...indexDDL.map(sql => `${sql};`),\n ];\n this.#db.db.exec(stmts.join(''));\n }\n if (oldName !== newName) {\n this.#db.db.exec(\n `ALTER TABLE ${id(table)} RENAME ${id(oldName)} TO ${id(newName)}`,\n );\n }\n\n // Update metadata table\n this.#columnMetadata.update(\n table,\n msg.old.name,\n msg.new.name,\n msg.new.spec,\n );\n\n this.#bumpVersions(msg.table);\n this.#lc.info?.(msg.tag, table, msg.new);\n }\n\n processDropColumn(msg: ColumnDrop) {\n const table = liteTableName(msg.table);\n const {column} = msg;\n this.#db.db.exec(`ALTER TABLE ${id(table)} DROP ${id(column)}`);\n\n // Delete from metadata table\n this.#columnMetadata.deleteColumn(table, column);\n this.#backfilling.apply(msg);\n\n this.#bumpVersions(msg.table);\n this.#lc.info?.(msg.tag, table, column);\n }\n\n processDropTable(drop: TableDrop) {\n this.#tableMetadata.drop(drop.id);\n this.#backfilling.apply(drop);\n\n const name = liteTableName(drop.id);\n this.#db.db.exec(`DROP TABLE IF EXISTS ${id(name)}`);\n\n // Delete from metadata table\n this.#columnMetadata.deleteTable(name);\n\n this.#logResetOp(name);\n this.#lc.info?.(drop.tag, name);\n }\n\n processCreateIndex(create: IndexCreate) {\n const index = mapPostgresToLiteIndex(create.spec);\n this.#db.db.exec(createLiteIndexStatement(index));\n\n // indexes affect tables visibility (e.g. sync-ability is gated on\n // having a unique index), so reset pipelines to refresh table schemas.\n this.#logResetOp(index.tableName);\n this.#lc.info?.(create.tag, index.name);\n }\n\n processDropIndex(drop: IndexDrop) {\n const name = liteTableName(drop.id);\n const indexExisted = this.#db.db\n .prepare(\n `SELECT tbl_name as tableName FROM sqlite_master WHERE type = 'index' AND name = ?`,\n )\n .get(name) as {tableName: string} | undefined;\n\n this.#db.db.exec(`DROP INDEX IF EXISTS ${id(name)}`);\n\n if (indexExisted) {\n // indexes affect tables visibility (e.g. sync-ability is gated on\n // having a unique index), so reset pipelines to refresh table schemas.\n this.#logResetOp(indexExisted.tableName);\n }\n this.#lc.info?.(drop.tag, name);\n }\n\n #bumpVersions(table: Identifier) {\n this.#tableMetadata.setMinRowVersion(table, this.#version);\n this.#logResetOp(liteTableName(table));\n }\n\n /**\n * @param backfilledColumns `backfilling` columns for which values were set\n */\n #logSetOp(\n table: string,\n key: LiteRowKey,\n backfilledColumns: string[] | undefined,\n ) {\n // The \"serving\" replicator always writes to the change-log (for IVM).\n // The \"backup\" replicator only needs to write to the change log\n // when writing columns that are being backfilled.\n if (this.#mode === 'serving' || backfilledColumns !== undefined) {\n this.#changeLog.logSetOp(\n this.#version,\n this.#pos++,\n table,\n key,\n backfilledColumns,\n );\n this.#numChangeLogEntries++;\n }\n }\n\n #logDeleteOp(table: string, key: LiteRowKey, backfilling?: string[]) {\n // The \"serving\" replicator always writes to the change-log (for IVM).\n // The \"backup\" replicator only needs to write to the change log\n // when writing columns that are being backfilled.\n if (this.#mode === 'serving' || backfilling?.length) {\n this.#changeLog.logDeleteOp(this.#version, this.#pos++, table, key);\n this.#numChangeLogEntries++;\n }\n }\n\n #logTruncateOp(table: string) {\n if (this.#mode === 'serving') {\n this.#changeLog.logTruncateOp(this.#version, table);\n this.#numChangeLogEntries++;\n }\n }\n\n #logResetOp(table: string) {\n this.#schemaChanged = true;\n if (this.#mode === 'serving') {\n this.#changeLog.logResetOp(this.#version, table);\n this.#numChangeLogEntries++;\n }\n this.#reloadTableSpecs();\n }\n\n processBackfill({\n relation,\n watermark,\n columns,\n rowValues,\n progressMarks,\n }: MessageBackfill) {\n const tableName = liteTableName(relation);\n const tableSpec = must(this.#tableSpecs.get(tableName));\n const rowKeyCols = relation.rowKey.columns;\n const cols = [...rowKeyCols, ...columns];\n\n // Only columns that are still being backfilled on _this_ replica are\n // written, and of those, only the columns for which this message is\n // contiguous with the data received so far (whose progress is advanced).\n //\n // * Once a column has been published (backfill completed), its values\n // strictly follow the replication timeline, so a subsequent backfill must\n // never overwrite them. This guards against a redundant backfill, e.g.\n // when the view-syncer (re)connects to a replication-manager that is\n // (re)running a backfill this replica already completed.\n // * Data for non-contiguous (ignored) columns is necessarily re-delivered\n // (by a resumed or from-scratch run) before the column can be published,\n // so writing it would be wasted I/O.\n const {accepted, ignored} = this.#trackBackfillProgress(\n relation,\n cols,\n own => acceptBackfill(own, progressMarks),\n );\n if (accepted.length === 0) {\n this.#lc.debug?.(\n ignored.length\n ? `skipping backfill of ${rowValues.length} rows into ` +\n `${tableName}: not contiguous with the progress of columns ` +\n `[${ignored.join(',')}]`\n : `skipping redundant backfill of ${rowValues.length} rows into ` +\n `${tableName} (all columns already published)`,\n progressMarks,\n );\n return;\n }\n if (ignored.length) {\n this.#lc.debug?.(\n `backfill of ${tableName} is not contiguous with the progress ` +\n `of columns [${ignored.join(',')}]`,\n progressMarks,\n );\n }\n const acceptedSet = new Set(accepted);\n\n // Common parts of the INSERT sql statement.\n const insertColsStr = [...cols, ZERO_VERSION_COLUMN_NAME].map(id).join(',');\n const qMarks = Array.from({length: cols.length + 1})\n .fill('?')\n .join(',');\n const rowKeyColsStr = rowKeyCols.map(id).join(',');\n\n let backfilled = 0;\n let skipped = 0;\n for (const v of rowValues) {\n const row = liteRow(\n Object.fromEntries(cols.map((c, i) => [c, v[i]])),\n tableSpec,\n this.#jsonFormat,\n );\n const rowKey = this.#getKey(row, {relation});\n const rowOp = this.#changeLog.getLatestRowOp(tableName, rowKey);\n if (rowOp?.op === DEL_OP && rowOp.stateVersion > watermark) {\n skipped++;\n continue; // the row was deleted after the backfill snapshot\n }\n // Restrict the DO UPDATE columns to those still being backfilled. Never\n // update an already backfilled (i.e. published) column.\n const updates = (\n rowOp?.op === SET_OP\n ? cols.filter(\n c => (rowOp.backfillingColumnVersions[c] ?? '') <= watermark,\n )\n : cols\n ).filter(c => acceptedSet.has(c));\n if (updates.length === 0) {\n // row already has newer values for all accepted backfilling columns.\n skipped++;\n continue;\n }\n const updateStmts = updates.map(col => `${id(col)}=excluded.${id(col)}`);\n this.#db.run(\n /*sql*/ `\n INSERT INTO ${id(tableName)} (${insertColsStr}) VALUES (${qMarks})\n ON CONFLICT (${rowKeyColsStr})\n DO UPDATE SET ${updateStmts.join(',')};\n `,\n ...Object.values(row.row),\n watermark, // the _0_version for new rows (i.e. table backfill)\n );\n backfilled++;\n }\n\n this.#lc.debug?.(\n `backfilled ${backfilled} rows (skipped ${skipped}) into ${tableName}`,\n );\n }\n\n #completedBackfill: DownloadStatus | undefined;\n\n /**\n * Applies `accept` to the tracked progress of each of the `cols` that are\n * still backfilling on this replica, updating the progress of the accepted\n * ones. Columns that are not backfilling (e.g. already published) are\n * neither accepted nor ignored.\n */\n #trackBackfillProgress(\n relation: Identifier,\n cols: readonly string[],\n accept: (progress: BackfillProgressMark | undefined) => AcceptResult,\n ): {accepted: string[]; ignored: string[]} {\n const tracked = this.#backfilling.getProgress(relation);\n const accepted: string[] = [];\n const ignored: string[] = [];\n for (const col of cols) {\n if (!tracked.has(col)) {\n continue; // not backfilling\n }\n const progress = tracked.get(col);\n const result = accept(progress);\n if (!result.accept) {\n ignored.push(col);\n continue;\n }\n accepted.push(col);\n if (!deepEqual(result.progress, progress)) {\n this.#backfilling.setProgress(relation, col, result.progress);\n }\n }\n return {accepted, ignored};\n }\n\n processBackfillCompleted(msg: BackfillCompleted) {\n const {relation, columns, status, progressMarks} = msg;\n const tableName = liteTableName(relation);\n const rowKeyCols = relation.rowKey.columns;\n\n // Only complete the columns whose backfill progress is contiguous with\n // the completed backfill. The others remain backfilling (and will be\n // re-requested by the change-streamer).\n const {accepted, ignored} = this.#trackBackfillProgress(\n relation,\n [...rowKeyCols, ...columns],\n own =>\n acceptBackfill(\n own,\n progressMarks ? {previous: progressMarks.previous} : undefined,\n ),\n );\n if (ignored.length) {\n this.#lc.info?.(\n `ignoring backfill-completed of ${tableName} for columns ` +\n `[${ignored.join(',')}], which have not received all of the data`,\n progressMarks,\n );\n }\n\n // If none of the columns are still backfilling on this replica, they were\n // already published (e.g. a redundant backfill from a replication-manager\n // re-running a backfill this replica already completed). Bumping versions\n // again would spuriously reset pipelines and re-report completion, so treat\n // the completion as a no-op.\n if (accepted.length === 0) {\n if (ignored.length === 0) {\n this.#lc.debug?.(\n `skipping redundant backfill-completed for ${tableName} ` +\n `(all columns already published)`,\n );\n }\n return;\n }\n const ignoredSet = new Set(ignored);\n const cols = [\n ...rowKeyCols.filter(c => !ignoredSet.has(c)),\n ...columns.filter(c => !ignoredSet.has(c)),\n ];\n\n const columnMetadata = must(ColumnMetadataStore.getInstance(this.#db.db));\n for (const col of cols) {\n columnMetadata.clearBackfilling(tableName, col);\n }\n this.#backfilling.complete(relation, cols);\n // Given that new columns are being exposed for every row in the table, bump the\n // row version for all rows.\n this.#bumpVersions(relation);\n if (status) {\n this.#completedBackfill = {table: tableName, columns: cols, ...status};\n }\n this.#lc.info?.(`finished backfilling ${tableName}`);\n\n // Note that there is no need to clear the backfillingColumnVersions values\n // in the changeLog. It could theoretically be done for clarity but:\n // (1) it could be non-trivial in terms of latency introduced and\n // (2) the data must be preserved if _other_ columns are in the process\n // of being backfilled\n //\n // Thus, for speed and simplicity, the values are left as is. (Note that\n // subsequent replicated changes to those rows will clear the values if\n // no backfills are in progress).\n }\n\n processCommit(commit: MessageCommit, watermark: string): CommitResult {\n if (watermark !== this.#version) {\n throw new Error(\n `'commit' version ${watermark} does not match 'begin' version ${\n this.#version\n }: ${stringify(commit)}`,\n );\n }\n updateReplicationWatermark(this.#db, watermark);\n\n if (this.#schemaChanged) {\n const start = Date.now();\n this.#db.db.pragma('optimize');\n this.#lc.info?.(\n `PRAGMA optimized after schema change (${Date.now() - start} ms)`,\n );\n }\n\n if (this.#mode !== 'initial-sync') {\n this.#db.commit();\n }\n\n const elapsedMs = Date.now() - this.#startMs;\n this.#lc.debug?.(`Committed tx@${this.#version} (${elapsedMs} ms)`);\n\n return {\n watermark,\n completedBackfill: this.#completedBackfill,\n schemaUpdated: this.#schemaChanged,\n changeLogUpdated: this.#numChangeLogEntries > 0,\n upstreamCommitTimeMs: commit.commitTimeMs,\n };\n }\n\n abort(lc: LogContext) {\n lc.info?.(`aborting transaction ${this.#version}`);\n this.#db.rollback();\n // A schema change in the transaction reloaded the table specs, which now\n // describe the schema that the rollback undid. Clearing them makes the\n // next transaction reload them from the database.\n this.#tableSpecs.clear();\n }\n}\n\nfunction differentStorageTypes(\n oldSpec: ColumnSpec,\n newSpec: ColumnSpec,\n): boolean {\n return (\n oldSpec.dataType !== newSpec.dataType ||\n isEnumColumn(oldSpec) !== isEnumColumn(newSpec) ||\n isArrayColumn(oldSpec) !== isArrayColumn(newSpec)\n );\n}\n\nfunction getBackfilledColumns(\n row: LiteRow,\n {backfilling}: LiteTableSpecWithReplicationStatus,\n): string[] | undefined {\n if (!backfilling?.length) {\n return undefined; // common case\n }\n return backfilling.filter(col => col in row);\n}\n\nfunction oversizedValueDescription(value: LiteValueType): string | undefined {\n if (typeof value === 'bigint') {\n return BigInt.asIntN(64, value) !== value\n ? 'valueType=bigint fitsInt64=false'\n : undefined;\n }\n\n const sizeBytes =\n typeof value === 'string'\n ? Buffer.byteLength(value)\n : value instanceof Uint8Array\n ? value.byteLength\n : undefined;\n if (sizeBytes !== undefined && sizeBytes > MAX_SQLITE_BIND_BYTES) {\n const valueType = typeof value === 'string' ? 'string' : 'buffer';\n return `valueType=${valueType} sizeBytes=${sizeBytes} limitBytes=${MAX_SQLITE_BIND_BYTES}`;\n }\n\n return undefined;\n}\n\n/** Runs `fn`, returning its error rather than throwing it. */\nfunction attempt(fn: () => void): unknown {\n try {\n fn();\n return undefined;\n } catch (e) {\n return e ?? new Error('rollback failed');\n }\n}\n\nfunction ensureError(err: unknown): Error {\n if (err instanceof Error) {\n return err;\n }\n const error = new Error();\n error.cause = err;\n return error;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"change-processor.js","names":["bufferConstants"],"sources":["../../../../../../zero-cache/src/services/replicator/change-processor.ts"],"sourcesContent":["import {constants as bufferConstants} from 'node:buffer';\nimport type {LogContext} from '@rocicorp/logger';\nimport {SqliteError} from '@rocicorp/zero-sqlite3';\nimport {AbortError} from '../../../../shared/src/abort-error.ts';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport {stringify} from '../../../../shared/src/bigint-json.ts';\nimport {deepEqual} from '../../../../shared/src/json.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {mapEntries} from '../../../../shared/src/objects.ts';\nimport type {DownloadStatus} from '../../../../zero-events/src/status.ts';\nimport {\n createLiteIndexStatement,\n createLiteTableStatement,\n liteColumnDef,\n} from '../../db/create.ts';\nimport {\n computeZqlSpecs,\n listTables,\n type LiteTableSpecWithReplicationStatus,\n} from '../../db/lite-tables.ts';\nimport {\n isArrayColumn,\n isEnumColumn,\n mapPostgresToLite,\n mapPostgresToLiteColumn,\n mapPostgresToLiteIndex,\n} from '../../db/pg-to-lite.ts';\nimport type {ColumnSpec} from '../../db/specs.ts';\nimport type {StatementRunner} from '../../db/statements.ts';\nimport type {LexiVersion} from '../../types/lexi-version.ts';\nimport {\n JSON_PARSED,\n liteRow,\n type JSONFormat,\n type LiteRow,\n type LiteRowKey,\n type LiteValueType,\n} from '../../types/lite.ts';\nimport {liteTableName} from '../../types/names.ts';\nimport {id} from '../../types/sql.ts';\nimport {\n acceptBackfill,\n type AcceptResult,\n} from '../change-source/protocol/backfill-progress.ts';\nimport type {\n BackfillCompleted,\n BackfillProgressMark,\n Change,\n ColumnAdd,\n ColumnDrop,\n ColumnUpdate,\n Identifier,\n IndexCreate,\n IndexDrop,\n MessageBackfill,\n MessageCommit,\n MessageDelete,\n MessageInsert,\n MessageRelation,\n MessageTruncate,\n MessageUpdate,\n TableCreate,\n TableDrop,\n TableRename,\n TableUpdateMetadata,\n} from '../change-source/protocol/current/data.ts';\nimport type {ChangeStreamData} from '../change-source/protocol/current/downstream.ts';\nimport type {ReplicatorMode} from './replicator.ts';\nimport {BackfillingTracker} from './schema/backfilling.ts';\nimport {ChangeLog, DEL_OP, SET_OP} from './schema/change-log.ts';\nimport {ColumnMetadataStore} from './schema/column-metadata.ts';\nimport {\n ZERO_VERSION_COLUMN_NAME,\n updateReplicationWatermark,\n} from './schema/replication-state.ts';\nimport {TableMetadataTracker} from './schema/table-metadata.ts';\n\nexport type ChangeProcessorMode = ReplicatorMode | 'initial-sync';\n\nconst MAX_COMMIT_ATTEMPTS = 5;\n\nconst BIND_VALUE_TOO_BIG_ERROR =\n 'The bound string, buffer, or bigint is too big';\nconst MAX_SQLITE_BIND_BYTES = Math.min(\n bufferConstants.MAX_LENGTH,\n bufferConstants.MAX_STRING_LENGTH,\n);\n\nexport type CommitResult = {\n watermark: string;\n completedBackfill: DownloadStatus | undefined;\n schemaUpdated: boolean;\n changeLogUpdated: boolean;\n /**\n * Millisecond epoch at which the transaction committed upstream, if the\n * ChangeSource reported one. Propagated to ViewSyncers as the origin\n * timestamp of the end-to-end serving lag measurement.\n */\n upstreamCommitTimeMs?: number | undefined;\n};\n\n/**\n * The ChangeProcessor partitions the stream of messages into transactions\n * by creating a {@link TransactionProcessor} when a transaction begins, and dispatching\n * messages to it until the commit is received.\n *\n * From https://www.postgresql.org/docs/current/protocol-logical-replication.html#PROTOCOL-LOGICAL-MESSAGES-FLOW :\n *\n * \"The logical replication protocol sends individual transactions one by one.\n * This means that all messages between a pair of Begin and Commit messages\n * belong to the same transaction.\"\n */\nexport class ChangeProcessor {\n readonly #db: StatementRunner;\n readonly #changeLog: ChangeLog;\n readonly #tableMetadata: TableMetadataTracker;\n readonly #backfilling: BackfillingTracker;\n readonly #mode: ChangeProcessorMode;\n readonly #failService: (lc: LogContext, err: unknown) => void;\n\n // The TransactionProcessor lazily loads table specs into this Map,\n // and reloads them after a schema change. It is cached here to avoid\n // reading them from the DB on every transaction.\n readonly #tableSpecs = new Map<string, LiteTableSpecWithReplicationStatus>();\n\n #currentTx: TransactionProcessor | null = null;\n\n #failure: Error | undefined;\n\n constructor(\n db: StatementRunner,\n mode: ChangeProcessorMode,\n failService: (lc: LogContext, err: unknown) => void,\n ) {\n this.#db = db;\n this.#changeLog = new ChangeLog(db.db);\n this.#tableMetadata = new TableMetadataTracker(db.db);\n this.#backfilling = new BackfillingTracker(db.db);\n this.#mode = mode;\n this.#failService = failService;\n }\n\n #fail(lc: LogContext, err: unknown) {\n if (!this.#failure) {\n let failureError = err;\n const rollbackErrors = [attempt(() => this.#currentTx?.abort(lc))].filter(\n e => e !== undefined,\n );\n\n if (rollbackErrors.length) {\n const combinedError = new Error(\n `Message processing failed and rollback also failed: operation error = ${String(err)}; rollback error = ${rollbackErrors.map(String).join('; ')}`,\n );\n combinedError.cause = err;\n failureError = combinedError;\n }\n\n this.#failure = ensureError(failureError);\n\n if (!(this.#failure instanceof AbortError)) {\n // Propagate the failure up to the service.\n lc.error?.('Message Processing failed:', this.#failure);\n this.#failService(lc, this.#failure);\n }\n }\n }\n\n abort(lc: LogContext) {\n this.#fail(lc, new AbortError());\n }\n\n /** @return If a transaction was committed. */\n processMessage(\n lc: LogContext,\n downstream: ChangeStreamData,\n ): CommitResult | null {\n const [type, message] = downstream;\n if (this.#failure) {\n lc.debug?.(`Dropping ${message.tag}`);\n return null;\n }\n try {\n const watermark =\n type === 'begin'\n ? downstream[2].commitWatermark\n : type === 'commit'\n ? downstream[2].watermark\n : undefined;\n return this.#processMessage(lc, message, watermark);\n } catch (e) {\n this.#fail(lc, e);\n }\n return null;\n }\n\n #beginTransaction(\n lc: LogContext,\n commitVersion: string,\n jsonFormat: JSONFormat,\n ): TransactionProcessor {\n const start = Date.now();\n\n // litestream can technically hold the lock for an arbitrary amount of time\n // when checkpointing a large commit. Crashing on the busy-timeout in this\n // scenario will either produce a corrupt backup or otherwise prevent\n // replication from proceeding.\n //\n // Instead, retry the lock acquisition indefinitely. If this masks\n // an unknown deadlock situation, manual intervention will be necessary.\n for (let i = 0; ; i++) {\n try {\n return new TransactionProcessor(\n lc,\n this.#db,\n this.#mode,\n this.#changeLog,\n this.#tableMetadata,\n this.#backfilling,\n this.#tableSpecs,\n commitVersion,\n jsonFormat,\n );\n } catch (e) {\n if (e instanceof SqliteError && e.code === 'SQLITE_BUSY') {\n lc.warn?.(\n `SQLITE_BUSY for ${Date.now() - start} ms (attempt ${i + 1}). ` +\n `This is only expected if litestream is performing a large ` +\n `checkpoint.`,\n e,\n );\n continue;\n }\n throw e;\n }\n }\n }\n\n /** @return If a transaction was committed. */\n #processMessage(\n lc: LogContext,\n msg: Change,\n watermark: string | undefined,\n ): CommitResult | null {\n if (msg.tag === 'begin') {\n if (this.#currentTx) {\n throw new Error(`Already in a transaction ${stringify(msg)}`);\n }\n this.#currentTx = this.#beginTransaction(\n lc,\n must(watermark),\n msg.json ?? JSON_PARSED,\n );\n return null;\n }\n\n // For non-begin messages, there should be a #currentTx set.\n const tx = this.#currentTx;\n if (!tx) {\n throw new Error(\n `Received message outside of transaction: ${stringify(msg)}`,\n );\n }\n\n if (msg.tag === 'commit') {\n assert(watermark, 'watermark is required for commit messages');\n const result = tx.processCommit(msg, watermark);\n // Clear only after a successful commit so #fail can roll back a commit\n // path that throws before SQLite has committed.\n this.#currentTx = null;\n return result;\n }\n\n if (msg.tag === 'rollback') {\n tx.abort(lc);\n this.#currentTx = null;\n return null;\n }\n\n switch (msg.tag) {\n case 'insert':\n tx.processInsert(msg);\n break;\n case 'update':\n tx.processUpdate(msg);\n break;\n case 'delete':\n tx.processDelete(msg);\n break;\n case 'truncate':\n tx.processTruncate(msg);\n break;\n case 'create-table':\n tx.processCreateTable(msg);\n break;\n case 'rename-table':\n tx.processRenameTable(msg);\n break;\n case 'update-table-metadata':\n tx.processTableMetadata(msg);\n break;\n case 'add-column':\n tx.processAddColumn(msg);\n break;\n case 'update-column':\n tx.processUpdateColumn(msg);\n break;\n case 'drop-column':\n tx.processDropColumn(msg);\n break;\n case 'drop-table':\n tx.processDropTable(msg);\n break;\n case 'create-index':\n tx.processCreateIndex(msg);\n break;\n case 'drop-index':\n tx.processDropIndex(msg);\n break;\n case 'backfill':\n tx.processBackfill(msg);\n break;\n case 'backfill-completed':\n tx.processBackfillCompleted(msg);\n break;\n default:\n unreachable(msg);\n }\n\n return null;\n }\n}\n\n/**\n * The {@link TransactionProcessor} handles the sequence of messages from\n * upstream, from `BEGIN` to `COMMIT` and executes the corresponding mutations\n * on the {@link postgres.TransactionSql} on the replica.\n *\n * When applying row contents to the replica, the `_0_version` column is added / updated,\n * and a corresponding entry in the `ChangeLog` is added. The version value is derived\n * from the watermark of the preceding transaction (stored as the `nextStateVersion` in the\n * `ReplicationState` table).\n *\n * Side note: For non-streaming Postgres transactions, the commitEndLsn (and thus\n * commit watermark) is available in the `begin` message, so it could theoretically\n * be used for the row version of changes within the transaction. However, the\n * commitEndLsn is not available in the streaming (in-progress) transaction\n * protocol, and may not be available for CDC streams of other upstream types.\n * Therefore, the zero replication protocol is designed to not require the commit\n * watermark when a transaction begins.\n *\n * Also of interest is the fact that all INSERT Messages are logically applied as\n * UPSERTs. See {@link processInsert} for the underlying motivation.\n */\nclass TransactionProcessor {\n readonly #lc: LogContext;\n readonly #startMs: number;\n readonly #db: StatementRunner;\n readonly #mode: ChangeProcessorMode;\n readonly #version: LexiVersion;\n readonly #changeLog: ChangeLog;\n readonly #tableMetadata: TableMetadataTracker;\n readonly #backfilling: BackfillingTracker;\n readonly #tableSpecs: Map<string, LiteTableSpecWithReplicationStatus>;\n readonly #jsonFormat: JSONFormat;\n readonly #columnMetadata: ColumnMetadataStore;\n\n #pos = 0;\n #schemaChanged = false;\n #numChangeLogEntries = 0;\n\n constructor(\n lc: LogContext,\n db: StatementRunner,\n mode: ChangeProcessorMode,\n changeLog: ChangeLog,\n tableMetadata: TableMetadataTracker,\n backfilling: BackfillingTracker,\n tableSpecs: Map<string, LiteTableSpecWithReplicationStatus>,\n commitVersion: LexiVersion,\n jsonFormat: JSONFormat,\n ) {\n this.#startMs = Date.now();\n this.#mode = mode;\n this.#jsonFormat = jsonFormat;\n\n switch (mode) {\n case 'serving':\n // This is the only transaction that commits to the serving replica.\n // Snapshotters use BEGIN CONCURRENT for private changes that are\n // always rolled back, while BEGIN IMMEDIATE lets this writer spill\n // dirty pages during large transactions.\n db.beginImmediate();\n break;\n case 'backup':\n // For the backup-replicator (i.e. replication-manager), there are no View Syncers\n // and thus BEGIN CONCURRENT is not necessary. In fact, BEGIN CONCURRENT can cause\n // deadlocks with forced wal-checkpoints (which `litestream replicate` performs),\n // so it is important to use vanilla transactions in this configuration.\n db.beginImmediate();\n break;\n case 'initial-sync':\n // When the ChangeProcessor is used for initial-sync, the calling code\n // handles the transaction boundaries.\n break;\n default:\n unreachable();\n }\n this.#db = db;\n this.#version = commitVersion;\n this.#lc = lc.withContext('version', commitVersion);\n this.#changeLog = changeLog;\n this.#tableMetadata = tableMetadata;\n this.#backfilling = backfilling;\n this.#tableSpecs = tableSpecs;\n // The column_metadata table is guaranteed to exist since the\n // replica-schema.ts migration to v8.\n this.#columnMetadata = must(ColumnMetadataStore.getInstance(db.db));\n\n if (this.#tableSpecs.size === 0) {\n this.#reloadTableSpecs();\n }\n }\n\n #reloadTableSpecs() {\n this.#tableSpecs.clear();\n // zqlSpecs include the primary key derived from unique indexes\n const zqlSpecs = computeZqlSpecs(this.#lc, this.#db.db, {\n includeBackfillingColumns: true,\n });\n for (let spec of listTables(this.#db.db)) {\n if (!spec.primaryKey) {\n spec = {\n ...spec,\n primaryKey: [\n ...(zqlSpecs.get(spec.name)?.tableSpec.primaryKey ?? []),\n ],\n };\n }\n this.#tableSpecs.set(spec.name, spec);\n }\n }\n\n #tableSpec(name: string) {\n return must(this.#tableSpecs.get(name), `Unknown table ${name}`);\n }\n\n #getKey(\n {row, numCols}: {row: LiteRow; numCols: number},\n {relation}: {relation: MessageRelation},\n ): LiteRowKey {\n const keyColumns =\n relation.rowKey.type !== 'full'\n ? relation.rowKey.columns // already a suitable key\n : this.#tableSpec(liteTableName(relation)).primaryKey;\n if (!keyColumns?.length) {\n throw new Error(\n `Cannot replicate table \"${relation.name}\" without a PRIMARY KEY or UNIQUE INDEX`,\n );\n }\n // For the common case (replica identity default), the row is already the\n // key for deletes and updates, in which case a new object can be avoided.\n if (numCols === keyColumns.length) {\n return row;\n }\n const key: Record<string, LiteValueType> = {};\n for (const col of keyColumns) {\n key[col] = row[col];\n }\n return key;\n }\n\n processInsert(insert: MessageInsert) {\n const table = liteTableName(insert.relation);\n const tableSpec = this.#tableSpec(table);\n const newRow = liteRow(insert.new, tableSpec, this.#jsonFormat);\n\n this.#upsert(table, {\n ...newRow.row,\n [ZERO_VERSION_COLUMN_NAME]: this.#version,\n });\n\n if (insert.relation.rowKey.columns.length === 0) {\n // INSERTs can be replicated for rows without a PRIMARY KEY or a\n // UNIQUE INDEX. These are written to the replica but not recorded\n // in the changeLog, because these rows cannot participate in IVM.\n //\n // (Once the table schema has been corrected to include a key, the\n // associated schema change will reset pipelines and data can be\n // loaded via hydration.)\n return;\n }\n const key = this.#getKey(newRow, insert);\n this.#logSetOp(table, key, getBackfilledColumns(newRow.row, tableSpec));\n }\n\n #upsert(table: string, row: LiteRow) {\n const columns = Object.keys(row).map(c => id(c));\n this.#db.run(\n `\n INSERT OR REPLACE INTO ${id(table)} (${columns.join(',')})\n VALUES (${Array.from({length: columns.length}).fill('?').join(',')})\n `,\n Object.values(row),\n );\n }\n\n // Updates by default are applied as UPDATE commands to support partial\n // row specifications from the change source. In particular, this is needed\n // to handle updates for which unchanged TOASTed values are not sent:\n //\n // https://www.postgresql.org/docs/current/protocol-logicalrep-message-formats.html#PROTOCOL-LOGICALREP-MESSAGE-FORMATS-TUPLEDATA\n //\n // However, in certain cases an UPDATE may be received for a row that\n // was not initially synced, such as when, an existing table is added\n // to the app's publication.\n //\n // In order to facilitate \"resumptive\" replication, the logic falls back to\n // an INSERT if the update did not change any rows.\n processUpdate(update: MessageUpdate) {\n const table = liteTableName(update.relation);\n const tableSpec = this.#tableSpec(table);\n const newRow = liteRow(update.new, tableSpec, this.#jsonFormat);\n const row = {...newRow.row, [ZERO_VERSION_COLUMN_NAME]: this.#version};\n\n // update.key is set with the old values if the key has changed.\n const oldKey = update.key\n ? this.#getKey(\n liteRow(update.key, this.#tableSpec(table), this.#jsonFormat),\n update,\n )\n : null;\n const newKey = this.#getKey(newRow, update);\n\n if (oldKey) {\n this.#logDeleteOp(table, oldKey, tableSpec.backfilling);\n }\n this.#logSetOp(table, newKey, getBackfilledColumns(newRow.row, tableSpec));\n\n const currKey = oldKey ?? newKey;\n const conds = Object.keys(currKey).map(col => `${id(col)}=?`);\n const setExprs = Object.keys(row).map(col => `${id(col)}=?`);\n\n try {\n const {changes} = this.#db.run(\n `\n UPDATE ${id(table)}\n SET ${setExprs.join(',')}\n WHERE ${conds.join(' AND ')}\n `,\n [...Object.values(row), ...Object.values(currKey)],\n );\n\n // If the UPDATE did not affect any rows, perform an UPSERT of the\n // new row for resumptive replication.\n if (changes === 0) {\n this.#upsert(table, row);\n }\n } catch (e) {\n if (\n !(e instanceof RangeError) ||\n e.message !== BIND_VALUE_TOO_BIG_ERROR\n ) {\n throw e;\n }\n\n const binding = [...Object.entries(row), ...Object.entries(currKey)]\n .map(\n ([column, value]) =>\n [column, oversizedValueDescription(value)] as const,\n )\n .find(([, description]) => description);\n const relationOid =\n 'relationOid' in update.relation &&\n typeof update.relation.relationOid === 'number'\n ? ` relationOid=${update.relation.relationOid}`\n : '';\n const error = new Error(\n `Oversized SQLite update binding: tx=${this.#version}${relationOid} table=${update.relation.schema}.${update.relation.name} column=${binding?.[0] ?? 'unknown'}${binding?.[1] ? ` ${binding[1]}` : ''}`,\n {cause: e},\n );\n error.name = 'OversizedUpdateBindingError';\n throw error;\n }\n }\n\n processDelete(del: MessageDelete) {\n const table = liteTableName(del.relation);\n const tableSpec = this.#tableSpec(table);\n const rowKey = this.#getKey(\n liteRow(del.key, tableSpec, this.#jsonFormat),\n del,\n );\n\n this.#delete(table, rowKey);\n this.#logDeleteOp(table, rowKey, tableSpec.backfilling);\n }\n\n #delete(table: string, rowKey: LiteRowKey) {\n const conds = Object.keys(rowKey).map(col => `${id(col)}=?`);\n this.#db.run(\n `DELETE FROM ${id(table)} WHERE ${conds.join(' AND ')}`,\n Object.values(rowKey),\n );\n }\n\n processTruncate(truncate: MessageTruncate) {\n for (const relation of truncate.relations) {\n const table = liteTableName(relation);\n // Update replica data.\n this.#db.run(`DELETE FROM ${id(table)}`);\n\n // Update change log.\n this.#logTruncateOp(table);\n }\n }\n\n processCreateTable(create: TableCreate) {\n if (create.metadata) {\n this.#tableMetadata.setUpstreamMetadata(create.spec, create.metadata);\n }\n this.#backfilling.apply(create);\n const table = mapPostgresToLite(create.spec);\n this.#db.db.exec(createLiteTableStatement(table));\n\n // Write to metadata table\n for (const [colName, colSpec] of Object.entries(create.spec.columns)) {\n this.#columnMetadata.insert(\n table.name,\n colName,\n colSpec,\n create.backfill?.[colName],\n );\n }\n\n this.#logResetOp(table.name);\n this.#lc.info?.(create.tag, table.name);\n }\n\n processTableMetadata(msg: TableUpdateMetadata) {\n this.#tableMetadata.setUpstreamMetadata(msg.table, msg.new);\n // Inert today: the fold's only op for this tag is `upsert-metadata`, which\n // the line above is the replica's interpreter of. Called anyway so that\n // every site that hands a schema change to one cookie tracker hands it to\n // both -- an exception here would be a schema change the shared fold has an\n // opinion about and this store never sees, which is the drift the fold\n // exists to make impossible.\n this.#backfilling.apply(msg);\n }\n\n processRenameTable(rename: TableRename) {\n this.#tableMetadata.rename(rename.old, rename.new);\n this.#backfilling.apply(rename);\n\n const oldName = liteTableName(rename.old);\n const newName = liteTableName(rename.new);\n this.#db.db.exec(`ALTER TABLE ${id(oldName)} RENAME TO ${id(newName)}`);\n\n // Rename in metadata table\n this.#columnMetadata.renameTable(oldName, newName);\n\n this.#bumpVersions(rename.new);\n this.#logResetOp(oldName);\n this.#lc.info?.(rename.tag, oldName, newName);\n }\n\n processAddColumn(msg: ColumnAdd) {\n if (msg.tableMetadata) {\n this.#tableMetadata.setUpstreamMetadata(msg.table, msg.tableMetadata);\n }\n this.#backfilling.apply(msg);\n const table = liteTableName(msg.table);\n const {name} = msg.column;\n const spec = mapPostgresToLiteColumn(table, msg.column);\n this.#db.db.exec(\n `ALTER TABLE ${id(table)} ADD ${id(name)} ${liteColumnDef(spec)}`,\n );\n\n // Write to metadata table\n this.#columnMetadata.insert(table, name, msg.column.spec, msg.backfill);\n\n if (msg.backfill) {\n this.#reloadTableSpecs();\n } else {\n // Make the new column visible immediately if it's not being backfilled.\n // Otherwise, the version bump will happen with the backfill is complete.\n this.#bumpVersions(msg.table);\n }\n this.#lc.info?.(msg.tag, table, msg.column);\n }\n\n processUpdateColumn(msg: ColumnUpdate) {\n // A no-op unless this is a rename, which is the only part of a column\n // update that moves the backfill cookie.\n this.#backfilling.apply(msg);\n\n const table = liteTableName(msg.table);\n let oldName = msg.old.name;\n const newName = msg.new.name;\n\n const storageTypesDiffer = differentStorageTypes(\n msg.old.spec,\n msg.new.spec,\n );\n\n // If neither the column name nor the SQLite data type changes, only the\n // upstream metadata needs to be updated. This includes changes such as a\n // varchar character limit and nullability, which SQLite does not enforce\n // but a freshly built replica still records.\n if (oldName === newName && !storageTypesDiffer) {\n this.#columnMetadata.update(\n table,\n msg.old.name,\n msg.new.name,\n msg.new.spec,\n );\n if (Boolean(msg.old.spec.notNull) !== Boolean(msg.new.spec.notNull)) {\n this.#bumpVersions(msg.table);\n }\n this.#lc.info?.(msg.tag, 'updated metadata only', msg.old, msg.new);\n return;\n }\n // Storage type changes require a new column so that SQLite uses the new\n // representation for existing and future values.\n if (storageTypesDiffer) {\n // update-column can ignore defaults because it does not change the values\n // in existing rows.\n //\n // https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-DESC-SET-DROP-DEFAULT\n //\n // \"The new default value will only apply in subsequent INSERT or UPDATE\n // commands; it does not cause rows already in the table to change.\"\n //\n // This allows support for _changing_ column defaults to any expression,\n // since it does not affect what the replica needs to do.\n const newLiteSpec = mapPostgresToLiteColumn(\n table,\n msg.new,\n 'ignore-default',\n );\n if (oldName !== newName) {\n // Rename the column in place first so that SQLite rewrites every\n // reference to it in the table's index definitions, including the\n // WHERE clauses of partial indexes.\n this.#db.db.exec(\n `ALTER TABLE ${id(table)} RENAME ${id(oldName)} TO ${id(newName)}`,\n );\n oldName = newName;\n }\n const tableSpec = must(\n listTables(this.#db.db, false, false).find(\n tableSpec => tableSpec.name === table,\n ),\n );\n // Recreating the table drops its indexes, so capture their DDL to\n // replay afterwards. Auto-indexes (PRIMARY KEY) have no DDL and are\n // recreated by the CREATE TABLE statement itself.\n const indexDDL = this.#db.db\n .prepare(\n `SELECT sql FROM sqlite_master\n WHERE type = 'index' AND tbl_name = ? AND sql IS NOT NULL\n ORDER BY rowid`,\n )\n .all<{sql: string}>(table)\n .map(({sql}) => sql);\n const tmpTable = `tmp.${table}`;\n const columns = Object.keys(tableSpec.columns);\n const stmts = [\n createLiteTableStatement({\n ...tableSpec,\n name: tmpTable,\n columns: mapEntries(tableSpec.columns, (column, spec) => [\n column,\n column === newName ? {...newLiteSpec, pos: spec.pos} : spec,\n ]),\n }),\n `INSERT INTO ${id(tmpTable)} (${columns.map(id).join(',')})\n SELECT ${columns.map(id).join(',')} FROM ${id(table)};`,\n `DROP TABLE ${id(table)};`,\n `ALTER TABLE ${id(tmpTable)} RENAME TO ${id(table)};`,\n ...indexDDL.map(sql => `${sql};`),\n ];\n this.#db.db.exec(stmts.join(''));\n }\n if (oldName !== newName) {\n this.#db.db.exec(\n `ALTER TABLE ${id(table)} RENAME ${id(oldName)} TO ${id(newName)}`,\n );\n }\n\n // Update metadata table\n this.#columnMetadata.update(\n table,\n msg.old.name,\n msg.new.name,\n msg.new.spec,\n );\n\n this.#bumpVersions(msg.table);\n this.#lc.info?.(msg.tag, table, msg.new);\n }\n\n processDropColumn(msg: ColumnDrop) {\n const table = liteTableName(msg.table);\n const {column} = msg;\n this.#db.db.exec(`ALTER TABLE ${id(table)} DROP ${id(column)}`);\n\n // Delete from metadata table\n this.#columnMetadata.deleteColumn(table, column);\n this.#backfilling.apply(msg);\n\n this.#bumpVersions(msg.table);\n this.#lc.info?.(msg.tag, table, column);\n }\n\n processDropTable(drop: TableDrop) {\n this.#tableMetadata.drop(drop.id);\n this.#backfilling.apply(drop);\n\n const name = liteTableName(drop.id);\n this.#db.db.exec(`DROP TABLE IF EXISTS ${id(name)}`);\n\n // Delete from metadata table\n this.#columnMetadata.deleteTable(name);\n\n this.#logResetOp(name);\n this.#lc.info?.(drop.tag, name);\n }\n\n processCreateIndex(create: IndexCreate) {\n const index = mapPostgresToLiteIndex(create.spec);\n this.#db.db.exec(createLiteIndexStatement(index));\n\n // indexes affect tables visibility (e.g. sync-ability is gated on\n // having a unique index), so reset pipelines to refresh table schemas.\n this.#logResetOp(index.tableName);\n this.#lc.info?.(create.tag, index.name);\n }\n\n processDropIndex(drop: IndexDrop) {\n const name = liteTableName(drop.id);\n const indexExisted = this.#db.db\n .prepare(\n `SELECT tbl_name as tableName FROM sqlite_master WHERE type = 'index' AND name = ?`,\n )\n .get(name) as {tableName: string} | undefined;\n\n this.#db.db.exec(`DROP INDEX IF EXISTS ${id(name)}`);\n\n if (indexExisted) {\n // indexes affect tables visibility (e.g. sync-ability is gated on\n // having a unique index), so reset pipelines to refresh table schemas.\n this.#logResetOp(indexExisted.tableName);\n }\n this.#lc.info?.(drop.tag, name);\n }\n\n #bumpVersions(table: Identifier) {\n this.#tableMetadata.setMinRowVersion(table, this.#version);\n this.#logResetOp(liteTableName(table));\n }\n\n /**\n * @param backfilledColumns `backfilling` columns for which values were set\n */\n #logSetOp(\n table: string,\n key: LiteRowKey,\n backfilledColumns: string[] | undefined,\n ) {\n // The \"serving\" replicator always writes to the change-log (for IVM).\n // The \"backup\" replicator only needs to write to the change log\n // when writing columns that are being backfilled.\n if (this.#mode === 'serving' || backfilledColumns !== undefined) {\n this.#changeLog.logSetOp(\n this.#version,\n this.#pos++,\n table,\n key,\n backfilledColumns,\n );\n this.#numChangeLogEntries++;\n }\n }\n\n #logDeleteOp(table: string, key: LiteRowKey, backfilling?: string[]) {\n // The \"serving\" replicator always writes to the change-log (for IVM).\n // The \"backup\" replicator only needs to write to the change log\n // when writing columns that are being backfilled.\n if (this.#mode === 'serving' || backfilling?.length) {\n this.#changeLog.logDeleteOp(this.#version, this.#pos++, table, key);\n this.#numChangeLogEntries++;\n }\n }\n\n #logTruncateOp(table: string) {\n if (this.#mode === 'serving') {\n this.#changeLog.logTruncateOp(this.#version, table);\n this.#numChangeLogEntries++;\n }\n }\n\n #logResetOp(table: string) {\n this.#schemaChanged = true;\n if (this.#mode === 'serving') {\n this.#changeLog.logResetOp(this.#version, table);\n this.#numChangeLogEntries++;\n }\n this.#reloadTableSpecs();\n }\n\n processBackfill({\n relation,\n watermark,\n columns,\n rowValues,\n progressMarks,\n }: MessageBackfill) {\n const tableName = liteTableName(relation);\n const tableSpec = must(this.#tableSpecs.get(tableName));\n const rowKeyCols = relation.rowKey.columns;\n const cols = [...rowKeyCols, ...columns];\n\n // Only columns that are still being backfilled on _this_ replica are\n // written, and of those, only the columns for which this message is\n // contiguous with the data received so far (whose progress is advanced).\n //\n // * Once a column has been published (backfill completed), its values\n // strictly follow the replication timeline, so a subsequent backfill must\n // never overwrite them. This guards against a redundant backfill, e.g.\n // when the view-syncer (re)connects to a replication-manager that is\n // (re)running a backfill this replica already completed.\n // * Data for non-contiguous (ignored) columns is necessarily re-delivered\n // (by a resumed or from-scratch run) before the column can be published,\n // so writing it would be wasted I/O.\n const {accepted, ignored} = this.#trackBackfillProgress(\n relation,\n cols,\n own => acceptBackfill(own, progressMarks),\n );\n if (accepted.length === 0) {\n this.#lc.debug?.(\n ignored.length\n ? `skipping backfill of ${rowValues.length} rows into ` +\n `${tableName}: not contiguous with the progress of columns ` +\n `[${ignored.join(',')}]`\n : `skipping redundant backfill of ${rowValues.length} rows into ` +\n `${tableName} (all columns already published)`,\n progressMarks,\n );\n return;\n }\n if (ignored.length) {\n this.#lc.debug?.(\n `backfill of ${tableName} is not contiguous with the progress ` +\n `of columns [${ignored.join(',')}]`,\n progressMarks,\n );\n }\n const acceptedSet = new Set(accepted);\n\n // Common parts of the INSERT sql statement.\n const insertColsStr = [...cols, ZERO_VERSION_COLUMN_NAME].map(id).join(',');\n const qMarks = Array.from({length: cols.length + 1})\n .fill('?')\n .join(',');\n const rowKeyColsStr = rowKeyCols.map(id).join(',');\n\n let backfilled = 0;\n let skipped = 0;\n for (const v of rowValues) {\n const row = liteRow(\n Object.fromEntries(cols.map((c, i) => [c, v[i]])),\n tableSpec,\n this.#jsonFormat,\n );\n const rowKey = this.#getKey(row, {relation});\n const rowOp = this.#changeLog.getLatestRowOp(tableName, rowKey);\n if (rowOp?.op === DEL_OP && rowOp.stateVersion > watermark) {\n skipped++;\n continue; // the row was deleted after the backfill snapshot\n }\n // Restrict the DO UPDATE columns to those still being backfilled. Never\n // update an already backfilled (i.e. published) column.\n const updates = (\n rowOp?.op === SET_OP\n ? cols.filter(\n c => (rowOp.backfillingColumnVersions[c] ?? '') <= watermark,\n )\n : cols\n ).filter(c => acceptedSet.has(c));\n if (updates.length === 0) {\n // row already has newer values for all accepted backfilling columns.\n skipped++;\n continue;\n }\n const updateStmts = updates.map(col => `${id(col)}=excluded.${id(col)}`);\n this.#db.run(\n /*sql*/ `\n INSERT INTO ${id(tableName)} (${insertColsStr}) VALUES (${qMarks})\n ON CONFLICT (${rowKeyColsStr})\n DO UPDATE SET ${updateStmts.join(',')};\n `,\n ...Object.values(row.row),\n watermark, // the _0_version for new rows (i.e. table backfill)\n );\n backfilled++;\n }\n\n this.#lc.debug?.(\n `backfilled ${backfilled} rows (skipped ${skipped}) into ${tableName}`,\n );\n }\n\n #completedBackfill: DownloadStatus | undefined;\n\n /**\n * Applies `accept` to the tracked progress of each of the `cols` that are\n * still backfilling on this replica, updating the progress of the accepted\n * ones. Columns that are not backfilling (e.g. already published) are\n * neither accepted nor ignored.\n */\n #trackBackfillProgress(\n relation: Identifier,\n cols: readonly string[],\n accept: (progress: BackfillProgressMark | undefined) => AcceptResult,\n ): {accepted: string[]; ignored: string[]} {\n const tracked = this.#backfilling.getProgress(relation);\n const accepted: string[] = [];\n const ignored: string[] = [];\n for (const col of cols) {\n if (!tracked.has(col)) {\n continue; // not backfilling\n }\n const progress = tracked.get(col);\n const result = accept(progress);\n if (!result.accept) {\n ignored.push(col);\n continue;\n }\n accepted.push(col);\n if (!deepEqual(result.progress, progress)) {\n this.#backfilling.setProgress(relation, col, result.progress);\n }\n }\n return {accepted, ignored};\n }\n\n processBackfillCompleted(msg: BackfillCompleted) {\n const {relation, columns, status, progressMarks} = msg;\n const tableName = liteTableName(relation);\n const rowKeyCols = relation.rowKey.columns;\n\n // Only complete the columns whose backfill progress is contiguous with\n // the completed backfill. The others remain backfilling (and will be\n // re-requested by the change-streamer).\n const {accepted, ignored} = this.#trackBackfillProgress(\n relation,\n [...rowKeyCols, ...columns],\n own =>\n acceptBackfill(\n own,\n progressMarks ? {previous: progressMarks.previous} : undefined,\n ),\n );\n if (ignored.length) {\n this.#lc.info?.(\n `ignoring backfill-completed of ${tableName} for columns ` +\n `[${ignored.join(',')}], which have not received all of the data`,\n progressMarks,\n );\n }\n\n // If none of the columns are still backfilling on this replica, they were\n // already published (e.g. a redundant backfill from a replication-manager\n // re-running a backfill this replica already completed). Bumping versions\n // again would spuriously reset pipelines and re-report completion, so treat\n // the completion as a no-op.\n if (accepted.length === 0) {\n if (ignored.length === 0) {\n this.#lc.debug?.(\n `skipping redundant backfill-completed for ${tableName} ` +\n `(all columns already published)`,\n );\n }\n return;\n }\n const ignoredSet = new Set(ignored);\n const cols = [\n ...rowKeyCols.filter(c => !ignoredSet.has(c)),\n ...columns.filter(c => !ignoredSet.has(c)),\n ];\n\n const columnMetadata = must(ColumnMetadataStore.getInstance(this.#db.db));\n for (const col of cols) {\n columnMetadata.clearBackfilling(tableName, col);\n }\n this.#backfilling.complete(relation, cols);\n // Given that new columns are being exposed for every row in the table, bump the\n // row version for all rows.\n this.#bumpVersions(relation);\n if (status) {\n this.#completedBackfill = {table: tableName, columns: cols, ...status};\n }\n this.#lc.info?.(`finished backfilling ${tableName}`);\n\n // Note that there is no need to clear the backfillingColumnVersions values\n // in the changeLog. It could theoretically be done for clarity but:\n // (1) it could be non-trivial in terms of latency introduced and\n // (2) the data must be preserved if _other_ columns are in the process\n // of being backfilled\n //\n // Thus, for speed and simplicity, the values are left as is. (Note that\n // subsequent replicated changes to those rows will clear the values if\n // no backfills are in progress).\n }\n\n processCommit(commit: MessageCommit, watermark: string): CommitResult {\n if (watermark !== this.#version) {\n throw new Error(\n `'commit' version ${watermark} does not match 'begin' version ${\n this.#version\n }: ${stringify(commit)}`,\n );\n }\n updateReplicationWatermark(this.#db, watermark);\n\n if (this.#schemaChanged) {\n const start = Date.now();\n this.#db.db.pragma('optimize');\n this.#lc.info?.(\n `PRAGMA optimized after schema change (${Date.now() - start} ms)`,\n );\n }\n\n if (this.#mode !== 'initial-sync') {\n this.#commit();\n }\n\n const elapsedMs = Date.now() - this.#startMs;\n this.#lc.debug?.(`Committed tx@${this.#version} (${elapsedMs} ms)`);\n\n return {\n watermark,\n completedBackfill: this.#completedBackfill,\n schemaUpdated: this.#schemaChanged,\n changeLogUpdated: this.#numChangeLogEntries > 0,\n upstreamCommitTimeMs: commit.commitTimeMs,\n };\n }\n\n /**\n * A wal2 `COMMIT` can fail with SQLITE_BUSY, without consulting the\n * busy_timeout, if it switches wal files just as the `ServingCheckpointer`\n * briefly locks the file it is switching to: the writer releases its locks\n * on that file before taking the read lock that it needs on it. The\n * transaction stays open and the switch has already happened, so the next\n * attempt does not contend for that lock.\n *\n * No backoff is needed between attempts: the retry does not switch files\n * again, so it never asks for the contended lock, whether or not the\n * checkpointer still holds it. A second failure has some other cause.\n *\n * This is a workaround for a bug in SQLite's wal2 branch (in\n * `walRestartLog()`), reported at\n * https://sqlite.org/forum/forumpost/63ba0208a66aa7139119b56edb43a416b160ca1996a23de2b0da24ca48fb9b3a.\n * The retry runs without the read lock the failed attempt meant to take.\n * Remove it once zero-sqlite3 includes a fix.\n */\n #commit() {\n for (let attempt = 1; ; attempt++) {\n try {\n this.#db.commit();\n return;\n } catch (e) {\n if (\n attempt === MAX_COMMIT_ATTEMPTS ||\n !(e instanceof SqliteError && e.code === 'SQLITE_BUSY')\n ) {\n throw e;\n }\n this.#lc.warn?.(`SQLITE_BUSY on COMMIT (attempt ${attempt})`, e);\n }\n }\n }\n\n abort(lc: LogContext) {\n lc.info?.(`aborting transaction ${this.#version}`);\n this.#db.rollback();\n // A schema change in the transaction reloaded the table specs, which now\n // describe the schema that the rollback undid. Clearing them makes the\n // next transaction reload them from the database.\n this.#tableSpecs.clear();\n }\n}\n\nfunction differentStorageTypes(\n oldSpec: ColumnSpec,\n newSpec: ColumnSpec,\n): boolean {\n return (\n oldSpec.dataType !== newSpec.dataType ||\n isEnumColumn(oldSpec) !== isEnumColumn(newSpec) ||\n isArrayColumn(oldSpec) !== isArrayColumn(newSpec)\n );\n}\n\nfunction getBackfilledColumns(\n row: LiteRow,\n {backfilling}: LiteTableSpecWithReplicationStatus,\n): string[] | undefined {\n if (!backfilling?.length) {\n return undefined; // common case\n }\n return backfilling.filter(col => col in row);\n}\n\nfunction oversizedValueDescription(value: LiteValueType): string | undefined {\n const sizeBytes =\n typeof value === 'string'\n ? Buffer.byteLength(value)\n : value instanceof Uint8Array\n ? value.byteLength\n : undefined;\n if (sizeBytes !== undefined && sizeBytes > MAX_SQLITE_BIND_BYTES) {\n const valueType = typeof value === 'string' ? 'string' : 'buffer';\n return `valueType=${valueType} sizeBytes=${sizeBytes} limitBytes=${MAX_SQLITE_BIND_BYTES}`;\n }\n\n return undefined;\n}\n\n/** Runs `fn`, returning its error rather than throwing it. */\nfunction attempt(fn: () => void): unknown {\n try {\n fn();\n return undefined;\n } catch (e) {\n return e ?? new Error('rollback failed');\n }\n}\n\nfunction ensureError(err: unknown): Error {\n if (err instanceof Error) {\n return err;\n }\n const error = new Error();\n error.cause = err;\n return error;\n}\n"],"mappings":"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"}
|
|
@@ -134,9 +134,9 @@ var IncrementalSyncer = class {
|
|
|
134
134
|
break;
|
|
135
135
|
}
|
|
136
136
|
case "error": {
|
|
137
|
-
const { type, message: msg, replicaVersion: rmVersion } = message[1];
|
|
138
|
-
if (type === 1 && rmVersion !== void 0 && rmVersion < replicaVersion && this.#olderReplicaVersionRejections++ < MAX_OLDER_REPLICA_VERSION_REJECTIONS) {
|
|
139
|
-
lc.warn?.(`replication-manager is at an older replica version
|
|
137
|
+
const { type, message: msg, olderReplica, replicaVersion: rmVersion } = message[1];
|
|
138
|
+
if (type === 1 && (olderReplica ?? (rmVersion !== void 0 && rmVersion < replicaVersion)) && this.#olderReplicaVersionRejections++ < MAX_OLDER_REPLICA_VERSION_REJECTIONS) {
|
|
139
|
+
lc.warn?.(`replication-manager is at an older replica version than ${replicaVersion}. reconnecting`);
|
|
140
140
|
break;
|
|
141
141
|
}
|
|
142
142
|
this.stop(lc, new AbortError(`${errorTypeToReadableName(type)}: ${msg}`));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"incremental-sync.js","names":[],"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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"}
|
|
1
|
+
{"version":3,"file":"incremental-sync.js","names":[],"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 olderReplica,\n replicaVersion: rmVersion,\n } = message[1];\n if (\n type === WrongReplicaVersion &&\n // Older replication-managers disclose their version instead.\n (olderReplica ??\n (rmVersion !== undefined && 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 `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":";;;;;;;;;;;;;;AA4CA,IAAM,4BAA4B;AAClC,IAAM,wBAAwB;AAO9B,IAAM,uCAAuC;;;;;;;;AAS7C,IAAa,oBAAb,MAA+B;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CAIA;CACA,iCAAiC;CAEjC,SAAkB,IAAI,aAAa,mBAAmB;CAEtD,qBAA8B,mBAC5B,eACA,UACA,wCACF;CAEA,YACE,IACA,QACA,IACA,gBACA,QACA,MACA,iBACA,iCAAiE,CAAC,GAClE,mBACA,YACA;EACA,KAAK,MAAM;EACX,KAAK,UAAU;EACf,KAAK,MAAM;EACX,KAAK,kBAAkB;EACvB,KAAK,UAAU;EACf,KAAK,QAAQ;EACb,KAAK,mBAAmB;EACxB,KAAK,YAAY,IAAI,SAAS;EAC9B,KAAK,YAAY,IAAI,0BACnB,IACA,8BACF;EACA,KAAK,qBAAqB;EAG1B,KAAK,gBACH,SAAS,aAAa,aAClB,IAAI,aAAa,IAAI,gBAAgB,KAAK,QAAQ,kBAChD,KAAK,kBAAkB,WAAW,WAAW,CAC/C,IACA,KAAA;CACR;CAEA,QAAuB;EACrB,OAAO,KAAK,UAAU,MAAM;CAC9B;CAEA,MAAM,MAAM;EACV,MAAM,KAAK,KAAK;EAChB,IAAI;EACJ,KAAK,QAAQ,SAAQ,QAAO;GAC1B,gBAAgB;GAChB,KAAK,OAAO,KAAK,IAAI,GAAG;EAC1B,CAAC;EACD,GAAG,OAAO,4BAA4B;EACtC,MAAM,EAAC,WAAW,qBAChB,MAAM,KAAK,QAAQ,qBAAqB;EAK1C,KAAU,UAAU,kBAAkB;GACpC,OAAO;GACP,WAAW;EACb,CAAC;EAED,OAAO,KAAK,OAAO,UAAU,GAAG;GAC9B,MAAM,EAAC,gBAAgB,WAAW,cAChC,MAAM,KAAK,QAAQ,qBAAqB;GAE1C,IAAI;GACJ,IAAI,mBAAmB,CAAC;GACxB,IAAI;GAEJ,IAAI;IACF,MAAM,MAAM,KAAK,mBACf,gBACA,WACA,SACF;IAGA,MAAM,WAAW,KAAK,eAAe,kBAAkB;IAKvD,MAAM,oBAAoB,KAAK;IAC/B,KAAK,qBAAqB,KAAA;IAC1B,aACE,aACC,oBACG,MAAM,kBAAkB,UAAU,GAAG,IACrC,MAAM,KAAK,gBAAgB,UAAU,GAAG;IAC9C,aAAa,KAAK,OAAO,aAAa,UAAU;IAChD,KAAK,kBACD,QAAQ,IAAI,eAAe,oBAAoB,WAAW,CAAC,CAC5D,YAAY,EAAE;IAEjB,IAAI;IAGJ,IAAI,aAAiC,CAAC;IACtC,IAAI,iBAAiB;IACrB,IAAI,gBAAgB;IAEpB,IAAI;IAEJ,MAAM,yBAAyB,UAC7B,KAAK,kBAAkB,QACrB,IACA,eACA,eAAe,MAAM,SACrB,WAEE,WACI,EACE,gBAAgB,CACd;KACE,GAAG,SAAS;KACZ,OAAO,SAAS;KAChB,SAAS,SAAS;IACpB,CACF,EACF,IACA,CAAC,CACT;IAEF,MAAM,cAAc,YAAY;KAC9B,IAAI,WAAW,WAAW,GACxB;KAEF,MAAM,QAAQ;KACd,aAAa,CAAC;KACd,iBAAiB;KAEjB,MAAM,SAAS,MAAM,KAAK,QAAQ,gBAAgB,KAAK;KACvD,KAAK,cAAc,IAAI,MAAM;KAC7B,IAAI,QAAQ,mBACV,WAAW,KAAA;IAEf;IAGA,IAAI,WAAW;IAMf,MAAM,cAAc,YAAiC;KACnD,MAAM,OAAO,QAAQ;KACrB,IAAI,SAAS,WAAW,QAAQ,EAAE,CAAC,mBAAmB,WAAW;MAC/D,GAAG,QACD,kCAAkC,QAAQ,EAAE,CAAC,iBAC/C;MACA,WAAW;KACb;KACA,IACE,aACC,SAAS,WACR,SAAS,UACT,SAAS,YACT,SAAS,aACX;MACA,WAAW,SAAS,YAAY,SAAS;MACzC,OAAO;KACT;KACA,OAAO;IACT;IAEA,WAAW,MAAM,EAAC,MAAM,SAAS,UAAS,YAAY;KACpD,IAAI,WAAW,OAAO,GACpB;KAGF,KAAK,OAAO,aAAa;KAEzB,KAAK,mBAAmB,IAAI,CAAC;KAC7B,QAAQ,QAAQ,IAAhB;MACE,KAAK;OACH,GAAG,OAAO,oDAAoD,EAC5D,SAAS,QAAQ,GACnB,CAAC;OACD;MACF,KAAK,UAAU;OACb,MAAM,EAAC,WAAW,aAAY,QAAQ;OACtC,KAAK,iCAAiC;OACtC,IAAI,UACF,KAAK,eAAe,sBAAsB,UAAU;OAEtD,IAAI,WAAW;QACb,MAAM,SAA4B,EAChC,gBAAgB,UAAU,eAC5B;QACA,IAAI,UAAU,aACZ,OAAO,cAAc;SACnB,GAAG,UAAU;SACb,iBAAiB,KAAK,IAAI;QAC5B;QAEF,KAAK,UAAU,OAAO,MAAM;OAC9B;OACA;MACF;MACA,KAAK,SAAS;OACZ,MAAM,EACJ,MACA,SAAS,KACT,cACA,gBAAgB,cACd,QAAQ;OACZ,IACE,SAAA,MAEC,iBACE,cAAc,KAAA,KAAa,YAAY,oBAC1C,KAAK,mCACH,sCACF;QAKA,GAAG,OACD,2DACU,eAAe,eAC3B;QACA;OACF;OAGA,KAAK,KACH,IAEA,IAAI,WACF,GAAG,wBAAwB,IAAiB,EAAE,IAAI,KACpD,CACF;OACA;MACF;MACA,SAAS;OACP,MAAM,MAAM,QAAQ;OACpB,IAAI,IAAI,QAAQ,cAAc,IAAI,QAAQ;QACxC,MAAM,EAAC,QAAQ,aAAY;QAC3B,MAAM,QAAQ,CAAC;QACf,WAAW;SACT;SACA,OAAO,SAAS;SAChB,SAAS,CAAC,GAAG,SAAS,OAAO,SAAS,GAAG,IAAI,OAAO;QACtD;QACA,IAAI,OAEF,sBAAsB,SAAS,IAAI;OAEvC;OAEA,IAAI,IAAI,QAAQ,kBAAkB,KAAK,kBAAkB;QAIvD,MAAM,YAAY;QAClB,MAAM,QAAQ,uBAAuB,IAAI,IAAI;QAC7C,aAAa,IAAI,iBAAiB;QAClC,SAAS,MAAM,KAAK;QACpB,KAAK,iBAAiB,QACpB,IACA,eACA,kBAAkB,MAAM,KAAK,MAAM,MAAM,aACzC,KACA,SAAS,KACX;QAEA,SAAS,aAAa;QACtB,WAAW,KAAK,OAA2B;QAC3C,kBAAkB;QAClB,MAAM,YAAY;QAClB,MAAM,UAAU,SAAS,OAAO;QAChC,GAAG,OACD,iBAAiB,MAAM,KAAK,IAAI,QAAQ,QAAQ,CAAC,EAAE,KACrD;QACA,KAAK,iBAAiB,QACpB,IACA,eACA,iBAAiB,MAAM,KAAK,MAAM,MAAM,aACxC,GACA,SAAS,KACX;QACA,IAAI,UAEF,sBAAsB,SAAS,KAAK;QAEtC;OACF;OACA,WAAW,KAAA;OAEX,MAAM,OAAO,QAAQ;OACrB,MAAM,6BACJ,SAAS,UAAU,gBAAgB,CAAC;OACtC,IAAI,SAAS,SACX,gBAAgB;YACX,IAAI,SAAS,YAAY,SAAS,YACvC,gBAAgB;OAGlB,WAAW,KAAK,OAA2B;OAC3C,kBAAkB;OAClB,IAGE,SAAS,YACT,SAAS,cAGT,8BACA,WAAW,UAAU,6BACrB,kBAAkB,uBAElB,MAAM,YAAY;OAEpB;MACF;KACF;IACF;IACA,KAAK,QAAQ,MAAM;GACrB,SAAS,GAAG;IACV,MAAM;IACN,KAAK,QAAQ,MAAM;GACrB,UAAU;IACR,YAAY,OAAO;IACnB,WAAW;IACX,KAAK,kBAAkB,KAAK;GAC9B;GACA,IAAI,QAAQ,KAAA,KAAa,KAAK,eAAe,mBAC3C;GAEF,KAAK,eAAe,kBAAkB,CAAC,EAAE,OAAO;GAChD,MAAM,KAAK,OAAO,QAAQ,IAAI,GAAG;EACnC;EACA,GAAG,OAAO,2BAA2B;EACrC,IAAI,aACF,MAAM;CAEV;CAEA,cAAc,IAAgB,QAA6B;EACzD,IAAI,CAAC,QACH;EAEF,IAAI,OAAO,mBAAmB;GAE5B,MAAM,SAAS,OAAO;GACtB,KAAK,kBAAkB,QACrB,IACA,eACA,cAAc,OAAO,MAAM,SAC3B,UACO,EAAC,gBAAgB,CAAC,MAAM,EAAC,EAClC;EACF,OAAO,IAAI,OAAO,eAChB,KAAK,kBAAkB,QAAQ,IAAI,eAAe,gBAAgB;EAEpE,IAAI,OAAO,aAAa,OAAO,kBAC7B,KAAU,UAAU,kBAAkB;GACpC,OAAO;GACP,WAAW,OAAO;GAClB,oBAAoB,KAAK,IAAI;GAC7B,sBAAsB,OAAO;EAC/B,CAAC;CAEL;CAEA,YAAkC;EAChC,OAAO,KAAK,UAAU,UAAU;CAClC;CAEA,mBACE,gBACA,WACA,WACmB;EACnB,OAAO;GACL,iBAAA;GACA,QAAQ,KAAK;GACb,IAAI,KAAK;GACT,MAAM,KAAK;GACX;GACA;GACA;GACA,GAAI,KAAK,gBAAgB,EAAC,WAAW,KAAI,IAAI,CAAC;EAChD;CACF;;;;;;CAOA,kBAAkB,aAAwC;EACxD,MAAM,KAAK,IAAI,SAAS,KAAK,KAAK,aAAa,EAAC,UAAU,KAAI,CAAC;EAC/D,IAAI;GACF,MAAM,EAAC,gBAAgB,WAAW,cAAa,GAAG,mBAAmB;IACnE,GAAG,qBAAqB,IAAI,gBAAgB,EAAE,CAAC;IAC/C,WAAW,qBAAqB,EAAE;GACpC,EAAE;GACF,OAAO,KAAK,mBAAmB,gBAAgB,WAAW,SAAS;EACrE,UAAU;GACR,GAAG,MAAM;EACX;CACF;CAEA,KAAK,IAAgB,KAAe;EAClC,KAAK,OAAO,KAAK,IAAI,GAAG;EAExB,KAAK,QAAQ,MAAM;CACrB;AACF"}
|
|
@@ -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;
|
|
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;AACjD,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,iBA8DlB"}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { BigIntJSON } from "../../../shared/src/bigint-json.js";
|
|
2
2
|
import { getShardID, upstreamSchema } from "../types/shards.js";
|
|
3
|
-
import { getOperatorAccess } from "../config/zero-config.js";
|
|
4
3
|
import { Database } from "../../../zqlite/src/db.js";
|
|
5
4
|
import { pgClient } from "../types/pg.js";
|
|
5
|
+
import { authorizeAdminRequest } from "./admin-auth.js";
|
|
6
6
|
import { StatementRunner } from "../db/statements.js";
|
|
7
7
|
import { getReplicationState } from "./replicator/schema/replication-state.js";
|
|
8
8
|
import { fromStateVersionString } from "./change-source/pg/lsn.js";
|
|
9
9
|
import os from "os";
|
|
10
10
|
import fs from "fs";
|
|
11
|
-
import auth from "basic-auth";
|
|
12
11
|
//#region ../zero-cache/src/services/statz.ts
|
|
13
12
|
async function upstreamStats(lc, config) {
|
|
14
13
|
const schema = upstreamSchema(getShardID(config));
|
|
@@ -221,11 +220,7 @@ function printStats(group, statsObject) {
|
|
|
221
220
|
* * `pretty`: formats the JSON object with indentation
|
|
222
221
|
*/
|
|
223
222
|
async function handleStatzRequest(lc, config, req, res) {
|
|
224
|
-
|
|
225
|
-
if (getOperatorAccess(lc, config, credentials?.pass) === void 0) {
|
|
226
|
-
res.code(401).header("WWW-Authenticate", "Basic realm=\"Statz Protected Area\"").send("Unauthorized");
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
223
|
+
if (authorizeAdminRequest(lc, config, req, res, "Statz Protected Area", "operator") === void 0) return;
|
|
229
224
|
const statsFns = {
|
|
230
225
|
upstream: () => upstreamStats(lc, config),
|
|
231
226
|
cvr: () => cvrStats(lc, config),
|
|
@@ -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 // 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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"}
|
|
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 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 {StatementRunner} from '../db/statements.ts';\nimport {pgClient} from '../types/pg.ts';\nimport {getShardID, upstreamSchema} from '../types/shards.ts';\nimport {authorizeAdminRequest} from './admin-auth.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 if (\n authorizeAdminRequest(\n lc,\n config,\n req,\n res,\n 'Statz Protected Area',\n 'operator',\n ) === undefined\n ) {\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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"}
|