@rocicorp/zero 1.12.0-canary.0 → 1.12.0-canary.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +0 -4
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +9 -10
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +15 -7
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +0 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js +5 -5
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +1 -1
- 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 +2 -2
- 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.d.ts +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js +8 -8
- 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/published.d.ts +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/published.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/published.js +1 -2
- package/out/zero-cache/src/services/change-source/pg/schema/published.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts +2 -2
- 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 +4 -4
- 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.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +5 -3
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +11 -7
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +37 -30
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/statz.d.ts.map +1 -1
- package/out/zero-cache/src/services/statz.js +1 -1
- package/out/zero-cache/src/services/statz.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-streamer-http.js","names":["#lc","#opts","#changeStreamer","#changes","#reserveSnapshot","#subscribe","#receiveWebsocket","#ensureChangeStreamerStarted","#forwardReservation","#changeStreamerStarted","#shardID","#changeDB","#changeStreamerURI","#resolveChangeStreamer"],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-http.ts"],"sourcesContent":["import type {IncomingMessage} from 'node:http';\nimport websocket from '@fastify/websocket';\nimport type {LogContext} from '@rocicorp/logger';\nimport WebSocket from 'ws';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport type {NormalizedZeroConfig} from '../../config/normalize.ts';\nimport type {IncomingMessageSubset} from '../../types/http.ts';\nimport {pgClient, type PostgresDB} from '../../types/pg.ts';\nimport {type Worker} from '../../types/processes.ts';\nimport {type ShardID} from '../../types/shards.ts';\nimport {\n readHead,\n streamInternal,\n streamInternalStringified,\n streamInternalWithSize,\n type Sized,\n type Source,\n} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {URLParams} from '../../types/url-params.ts';\nimport {installWebSocketReceiver} from '../../types/websocket-handoff.ts';\nimport {closeWithError, PROTOCOL_ERROR} from '../../types/ws.ts';\nimport {HttpService, type Options as HttpOptions} from '../http-service.ts';\nimport {handleProfzRequest} from '../profz.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport {\n PROTOCOL_VERSION,\n type ChangeStreamer,\n type ChangeStreamerService,\n type SubscriberContext,\n} from './change-streamer.ts';\nimport {discoverChangeStreamerAddress} from './schema/tables.ts';\nimport {type SnapshotMessage} from './snapshot-message.ts';\nimport {\n type LocalReplica,\n subscribeDownstreamSchema,\n subscribeUpstreamSchema,\n type ReservedMessage,\n type SnapshotStatus,\n type SubscribeContext,\n type SubscribeDownstream,\n type SubscribeUpstream,\n} from './subscribe.ts';\n\nconst MIN_SUPPORTED_PROTOCOL_VERSION = 4;\n\n// The merged `/subscribe` protocol was introduced at v7.\nconst MIN_SUBSCRIBE_PROTOCOL_VERSION = 7;\n\nconst SNAPSHOT_PATH_PATTERN = '/replication/:version/snapshot';\nconst CHANGES_PATH_PATTERN = '/replication/:version/changes';\nconst SUBSCRIBE_PATH_PATTERN = '/replication/:version/subscribe';\nconst PATH_REGEX =\n /\\/replication\\/v(?<version>\\d+)\\/(changes|snapshot|subscribe)$/;\n\nconst SUBSCRIBE_PATH = `/replication/v${PROTOCOL_VERSION}/subscribe`;\n\n// Max time for a client to establish the /subscribe websocket (TCP connect\n// plus upgrade) before abandoning the attempt.\nconst CONNECT_TIMEOUT_MS = 10_000;\n\ntype Options = HttpOptions & {\n startupDelayMs: number;\n config?:\n | Pick<NormalizedZeroConfig, 'adminPassword' | 'operatorPassword'>\n | undefined;\n getProfileWorker?: (() => Promise<Worker>) | undefined;\n};\n\nexport class ChangeStreamerHttpServer extends HttpService {\n readonly id = 'change-streamer-http-server';\n readonly #lc: LogContext;\n readonly #opts: Options;\n readonly #changeStreamer: ChangeStreamerService;\n\n constructor(\n lc: LogContext,\n opts: Options,\n parent: Worker,\n changeStreamer: ChangeStreamerService,\n ) {\n super('change-streamer-http-server', lc, opts, async fastify => {\n await fastify.register(websocket);\n\n fastify.get(CHANGES_PATH_PATTERN, {websocket: true}, this.#changes);\n fastify.get(\n SNAPSHOT_PATH_PATTERN,\n {websocket: true},\n this.#reserveSnapshot,\n );\n fastify.get(SUBSCRIBE_PATH_PATTERN, {websocket: true}, this.#subscribe);\n\n fastify.get('/profz', (req, res) =>\n handleProfzRequest(\n lc,\n opts.config ?? {\n adminPassword: undefined,\n operatorPassword: undefined,\n },\n req,\n res,\n opts.getProfileWorker,\n undefined,\n 'change-streamer',\n ),\n );\n\n installWebSocketReceiver<'snapshot' | 'changes' | 'subscribe'>(\n lc,\n fastify.websocketServer,\n this.#receiveWebsocket,\n parent,\n );\n });\n\n this.#lc = lc;\n this.#opts = opts;\n this.#changeStreamer = changeStreamer;\n }\n\n // Called when receiving a web socket via the main dispatcher handoff.\n readonly #receiveWebsocket = (\n ws: WebSocket,\n action: 'changes' | 'snapshot' | 'subscribe',\n msg: IncomingMessageSubset,\n ) => {\n switch (action) {\n case 'snapshot':\n return this.#reserveSnapshot(ws, msg);\n case 'changes':\n return this.#changes(ws, msg);\n case 'subscribe':\n return this.#subscribe(ws, msg);\n default:\n closeWithError(\n this._lc,\n ws,\n `invalid action \"${action}\" received in handoff`,\n );\n return;\n }\n };\n\n readonly #reserveSnapshot = async (ws: WebSocket, req: RequestHeaders) => {\n this.#ensureChangeStreamerStarted('incoming snapshot reservation');\n try {\n const url = new URL(\n req.url ?? '',\n req.headers.origin ?? 'http://localhost',\n );\n checkProtocolVersion(url.pathname);\n const taskID = url.searchParams.get('taskID');\n if (!taskID) {\n throw new Error('Missing taskID in snapshot request');\n }\n const downstream =\n await this.#changeStreamer.startSnapshotReservation(taskID);\n // Send-only for now: the reservation carries no inbound application\n // messages (only transport acks, demuxed internally).\n void streamInternal(this._lc, ws, undefined, downstream);\n } catch (err) {\n closeWithError(this._lc, ws, err, PROTOCOL_ERROR);\n }\n };\n\n readonly #changes = async (ws: WebSocket, req: RequestHeaders) => {\n try {\n const ctx = getSubscriberContext(req);\n if (ctx.mode === 'serving') {\n this.#ensureChangeStreamerStarted('incoming subscription');\n }\n\n const downstream = await this.#changeStreamer.subscribe(ctx);\n // Send-only for now: the subscription carries no inbound application\n // messages (only transport acks, demuxed internally).\n void streamInternalStringified(this._lc, ws, undefined, downstream, {\n batched: ctx.wsBatched,\n });\n } catch (err) {\n closeWithError(this._lc, ws, err, PROTOCOL_ERROR);\n }\n };\n\n /**\n * The merged v7 `/subscribe` handler. A single bidirectional connection\n * carries, in sequence: an optional reservation phase (`reserve-snapshot` →\n * `reserved`) and then the subscription phase (`start-subscription` → change\n * stream). This ensures that the subscriber subscribes to the same task from\n * which it restored the backup.\n */\n readonly #subscribe = async (ws: WebSocket, req: RequestHeaders) => {\n let reservation: Source<SnapshotMessage> | undefined;\n const outbound = Subscription.create<string | PreSerializedBatch>();\n\n try {\n const url = new URL(\n req.url ?? '',\n req.headers.origin ?? 'http://localhost',\n );\n const protocolVersion = checkProtocolVersion(url.pathname);\n if (protocolVersion < MIN_SUBSCRIBE_PROTOCOL_VERSION) {\n throw new Error(\n `the /subscribe endpoint requires protocol v${MIN_SUBSCRIBE_PROTOCOL_VERSION} ` +\n `(client is at v${protocolVersion})`,\n );\n }\n\n const instream = await streamInternalStringified(\n this._lc,\n ws,\n subscribeUpstreamSchema,\n outbound,\n {batched: true},\n );\n\n // Keep reading upstream control messages for the life of the connection.\n // Do not manually exit loop, as that would close the socket. The loop\n // exits naturally when the connection closes.\n for await (const msg of instream) {\n if (msg[0] === 'reserve-snapshot') {\n const {taskID, localReplica} = msg[1];\n this.#ensureChangeStreamerStarted('incoming snapshot reservation');\n reservation = await this.#changeStreamer.startSnapshotReservation(\n taskID,\n {localReplica},\n );\n void this.#forwardReservation(reservation, outbound);\n } else if (msg[0] === 'start-subscription') {\n const ctx: SubscriberContext = {\n ...msg[1],\n protocolVersion,\n wsBatched: true,\n };\n if (ctx.mode === 'serving') {\n this.#ensureChangeStreamerStarted('incoming subscription');\n }\n // subscribe() registers the subscriber and then releases any\n // reservation for that task.\n reservation = undefined;\n await this.#changeStreamer.subscribe(ctx, outbound);\n }\n }\n } catch (err) {\n closeWithError(this._lc, ws, err, PROTOCOL_ERROR);\n } finally {\n // Covers a reservation-phase disconnect (instream ended before a\n // subscription started); idempotent with the close handler above.\n reservation?.cancel();\n }\n };\n\n /**\n * Forwards a snapshot reservation's single `['status', ...]` message onto\n * the merged connection's `outbound` sink as a `['reserved', ...]` message.\n * The loop then blocks on the held-open reservation until it is cancelled,\n * by `subscribe()` at the subscription handoff, or by the reservation-phase\n * teardown on disconnect.\n */\n async #forwardReservation(\n reservation: Source<SnapshotMessage>,\n outbound: Subscription<string | PreSerializedBatch>,\n ) {\n try {\n for await (const [, status] of reservation) {\n // Translate the /snapshot protocol's ['status', {tag: 'status', ...}]\n // message to the /subscribe protocol's ['reserved', {tag: 'snapshot', ...}].\n outbound.push(\n BigIntJSON.stringify([\n 'reserved',\n {...status, tag: 'snapshot'},\n ] satisfies ReservedMessage),\n );\n }\n } catch (e) {\n this._lc.warn?.(`error forwarding snapshot reservation`, e);\n outbound.fail(e instanceof Error ? e : new Error(String(e)));\n }\n }\n\n #changeStreamerStarted = false;\n\n #ensureChangeStreamerStarted(reason?: string) {\n if (!this.#changeStreamerStarted && this._state.shouldRun()) {\n this.#lc.info?.(\n `starting ChangeStreamerService ${reason ? `(${reason})` : ''}`,\n );\n void this.#changeStreamer\n .run()\n .catch(e =>\n this.#lc.warn?.(`ChangeStreamerService ended with error`, e),\n )\n .finally(() => this.stop());\n\n this.#changeStreamerStarted = true;\n }\n }\n\n protected override _onStart(): void {\n const {startupDelayMs, readinessGate = promiseVoid} = this.#opts;\n if (startupDelayMs === 0) {\n // In RMv2, there is no need to delay starting the change streamer\n // because RM startup is non-disruptive.\n this.#ensureChangeStreamerStarted();\n } else {\n // In RMv1, starting the change-streamer forcibly shuts down the\n // previous change-streamer, causing view-syncers to reconnect.\n // If this replication-manager has just started, the routing layer may\n // not have registered it with DNS, as that only happens after it\n // confirms health checks. To minimize downtime, the takeover is\n // delayed for the configured startupDelayMs _after_ beginning to\n // advertise readiness.\n void readinessGate.then(() => {\n this.#lc.info?.(\n `waiting ${startupDelayMs}ms before taking over the change log`,\n );\n this._state.setTimeout(\n () =>\n this.#ensureChangeStreamerStarted(\n `startup delay elapsed (${startupDelayMs} ms)`,\n ),\n startupDelayMs,\n );\n });\n }\n }\n\n /**\n * Signals (drainable) subscribers to switch over to another\n * replication-manager, resolving when they have disconnected. Subscribers\n * continue to be served in the meantime, but new connections (e.g.\n * switchover attempts, reconnects, and snapshot reservations) are refused,\n * so that they are routed to another replication-manager.\n */\n async drain(): Promise<void> {\n this.stopAccepting();\n if (this.#changeStreamerStarted && this.#changeStreamer.drain) {\n await this.#changeStreamer.drain();\n }\n }\n\n protected override async _onStop(): Promise<void> {\n if (this.#changeStreamerStarted) {\n await this.#changeStreamer.stop();\n }\n }\n}\n\nexport interface SnapshotReserver {\n reserveSnapshot(\n taskID: string,\n localReplica?: LocalReplica | undefined,\n ): Promise<{\n reserved: SnapshotStatus;\n followup: ReservationFollowup;\n }>;\n}\n\nexport interface SubscriptionStarter {\n subscribe(\n ctx: SubscriberContext,\n ): Promise<Source<Sized<SubscribeDownstream>>>;\n}\n\nexport interface ReservationFollowup extends SubscriptionStarter {\n /**\n * A signal to indicate that the followup is no longer valid (i.e. the\n * connection to the reserving task has been severed).\n */\n readonly signal: AbortSignal;\n\n /**\n * Abandons the reservation without subscribing, closing the connection that\n * was held open to pin the change-log floor. Used when the caller cannot\n * proceed to `subscribe()` (e.g. process shutdown while a restore is in\n * progress) and must not leave the reservation's connection open forever.\n */\n cancel(reason?: Error): void;\n}\n\nexport class ChangeStreamerHttpClient\n implements ChangeStreamer, SnapshotReserver, SubscriptionStarter\n{\n readonly #lc: LogContext;\n readonly #shardID: ShardID;\n readonly #changeDB: PostgresDB;\n readonly #changeStreamerURI: string | undefined;\n\n constructor(\n lc: LogContext,\n shardID: ShardID,\n changeDB: string,\n changeStreamerURI: string | undefined,\n ) {\n this.#lc = lc;\n this.#shardID = shardID;\n // Create a pg client with a single short-lived connection for the purpose\n // of change-streamer discovery (i.e. ChangeDB as DNS).\n this.#changeDB = pgClient(lc, changeDB, 'change-streamer-discovery', {\n max: 1,\n ['idle_timeout']: 15,\n });\n this.#changeStreamerURI = changeStreamerURI;\n }\n\n async #resolveChangeStreamer(path: string) {\n let baseURL = this.#changeStreamerURI;\n if (!baseURL) {\n const address = await discoverChangeStreamerAddress(\n this.#shardID,\n this.#changeDB,\n );\n if (!address) {\n throw new Error(`no change-streamer is running`);\n }\n baseURL = address.includes('://') ? `${address}/` : `ws://${address}/`;\n }\n const uri = new URL(path, baseURL);\n this.#lc.info?.(`connecting to change-streamer@${uri}`);\n return uri;\n }\n\n async reserveSnapshot(\n taskID: string,\n localReplica?: LocalReplica | undefined,\n ): Promise<{\n reserved: SnapshotStatus;\n followup: ReservationFollowup;\n }> {\n const {instream, outbound} = await this.connect(taskID);\n outbound.push([\n 'reserve-snapshot',\n {taskID, ...(localReplica ? {localReplica} : {})},\n ]);\n\n const {head, rest} = readHead(instream);\n const item = await head.next();\n if (!item.value) {\n throw new Error('stream closed without snapshot reservation');\n }\n const {data: msg} = item.value;\n if (msg[0] !== 'reserved') {\n throw new Error(`expected \"reserved\" message but got ${msg[0]}`);\n }\n const reserved: SnapshotStatus = msg[1];\n const followup: ReservationFollowup = {\n subscribe: ctx => {\n outbound.push(['start-subscription', toStartSubscriptionContext(ctx)]);\n return Promise.resolve(rest);\n },\n signal: outbound.signal,\n cancel: reason => outbound.cancel(reason),\n };\n\n return {reserved, followup};\n }\n\n async subscribe(\n ctx: SubscriberContext,\n ): Promise<Source<Sized<SubscribeDownstream>>> {\n const {instream, outbound} = await this.connect(ctx.taskID);\n outbound.push(['start-subscription', toStartSubscriptionContext(ctx)]);\n return instream;\n }\n\n /**\n * Opens a merged v7 `/subscribe` connection (see subscribe.ts). Returns the\n * inbound change/reservation stream and an `outbound` sink into which the\n * caller pushes the `reserve-snapshot` / `start-subscription` control\n * messages that drive the connection's phases. The single connection spans\n * the reservation, backup restore, and subscription, so the reserving\n * replication-manager is the one that serves the subscription.\n */\n // exported for testing\n async connect(taskID: string): Promise<{\n instream: Source<Sized<SubscribeDownstream>>;\n outbound: Subscription<SubscribeUpstream>;\n }> {\n const uri = await this.#resolveChangeStreamer(SUBSCRIBE_PATH);\n\n // taskID is carried in the query for observability/routing; the server\n // reads the authoritative taskID from the control messages themselves.\n const params = new URLSearchParams({taskID});\n // Bound the TCP connect + HTTP upgrade. Without this, a SYN black-holed\n // by a stale Service endpoint (e.g. a just-terminated replication-manager\n // pod) is retransmitted by the kernel for ~2 minutes before ETIMEDOUT.\n // Failing fast lets the caller's retry open a fresh connection that is\n // routed to a live endpoint.\n const ws = new WebSocket(uri + `?${params.toString()}`, {\n handshakeTimeout: CONNECT_TIMEOUT_MS,\n });\n\n const outbound = Subscription.create<SubscribeUpstream>();\n const instream = await streamInternalWithSize(\n this.#lc,\n ws,\n subscribeDownstreamSchema,\n outbound,\n );\n return {instream, outbound};\n }\n}\n\ntype RequestHeaders = Pick<IncomingMessage, 'url' | 'headers'>;\n\nexport function getSubscriberContext(req: RequestHeaders): SubscriberContext {\n const url = new URL(req.url ?? '', req.headers.origin ?? 'http://localhost');\n const protocolVersion = checkProtocolVersion(url.pathname);\n const params = new URLParams(url);\n\n return {\n protocolVersion,\n id: params.get('id', true),\n taskID: params.get('taskID', true),\n mode: params.get('mode', false) === 'backup' ? 'backup' : 'serving',\n replicaVersion: params.get('replicaVersion', true),\n watermark: params.get('watermark', true),\n // Absent for subscribers that predate the parameter, which is the safe\n // default: the barrier falls back to polling rather than waiting on an\n // ACK that would never be attributed to a writer.\n wsBatched: params.getBoolean('wsBatched'),\n };\n}\n\nfunction checkProtocolVersion(pathname: string): number {\n const match = PATH_REGEX.exec(pathname);\n if (!match) {\n throw new Error(`invalid path: ${pathname}`);\n }\n const v = Number(match.groups?.version);\n if (\n Number.isNaN(v) ||\n v > PROTOCOL_VERSION ||\n v < MIN_SUPPORTED_PROTOCOL_VERSION\n ) {\n throw new Error(\n `Cannot service client at protocol v${v}. ` +\n `Supported protocols: [v${MIN_SUPPORTED_PROTOCOL_VERSION} ... v${PROTOCOL_VERSION}]`,\n );\n }\n return v;\n}\n\n// Projects a client-side SubscriberContext onto the start-subscription\n// message payload, dropping the fields that are implicit on the merged\n// connection: protocolVersion (from the request path), wsBatched (always on).\nfunction toStartSubscriptionContext(ctx: SubscriberContext): SubscribeContext {\n const {\n taskID,\n id,\n mode,\n replicaVersion,\n watermark,\n backfills,\n drainable,\n switchover,\n } = ctx;\n return {\n taskID,\n id,\n mode,\n replicaVersion,\n watermark,\n backfills,\n ...(drainable !== undefined ? {drainable} : {}),\n ...(switchover !== undefined ? {switchover} : {}),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"change-streamer-http.js","names":["#lc","#opts","#changeStreamer","#changes","#reserveSnapshot","#subscribe","#receiveWebsocket","#ensureChangeStreamerStarted","#forwardReservation","#changeStreamerStarted","#shardID","#changeDB","#changeStreamerURI","#resolveChangeStreamer"],"sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-http.ts"],"sourcesContent":["import type {IncomingMessage} from 'node:http';\nimport websocket from '@fastify/websocket';\nimport type {LogContext} from '@rocicorp/logger';\nimport WebSocket from 'ws';\nimport {BigIntJSON} from '../../../../shared/src/bigint-json.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport type {NormalizedZeroConfig} from '../../config/normalize.ts';\nimport type {IncomingMessageSubset} from '../../types/http.ts';\nimport {pgClient, type PostgresDB} from '../../types/pg.ts';\nimport {type Worker} from '../../types/processes.ts';\nimport {type ShardID} from '../../types/shards.ts';\nimport {\n readHead,\n streamInternal,\n streamInternalStringified,\n streamInternalWithSize,\n type Sized,\n type Source,\n} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {URLParams} from '../../types/url-params.ts';\nimport {installWebSocketReceiver} from '../../types/websocket-handoff.ts';\nimport {closeWithError, PROTOCOL_ERROR} from '../../types/ws.ts';\nimport {HttpService, type Options as HttpOptions} from '../http-service.ts';\nimport {handleProfzRequest} from '../profz.ts';\nimport type {PreSerializedBatch} from './broadcast.ts';\nimport {\n PROTOCOL_VERSION,\n type ChangeStreamer,\n type ChangeStreamerService,\n type SubscriberContext,\n} from './change-streamer.ts';\nimport {discoverChangeStreamerAddress} from './schema/tables.ts';\nimport {type SnapshotMessage} from './snapshot-message.ts';\nimport {\n type LocalReplica,\n subscribeDownstreamSchema,\n subscribeUpstreamSchema,\n type ReservedMessage,\n type SnapshotStatus,\n type SubscribeContext,\n type SubscribeDownstream,\n type SubscribeUpstream,\n} from './subscribe.ts';\n\nconst MIN_SUPPORTED_PROTOCOL_VERSION = 4;\n\n// The merged `/subscribe` protocol was introduced at v7.\nconst MIN_SUBSCRIBE_PROTOCOL_VERSION = 7;\n\nconst SNAPSHOT_PATH_PATTERN = '/replication/:version/snapshot';\nconst CHANGES_PATH_PATTERN = '/replication/:version/changes';\nconst SUBSCRIBE_PATH_PATTERN = '/replication/:version/subscribe';\nconst PATH_REGEX =\n /\\/replication\\/v(?<version>\\d+)\\/(changes|snapshot|subscribe)$/;\n\nconst SUBSCRIBE_PATH = `/replication/v${PROTOCOL_VERSION}/subscribe`;\n\n// Max time for a client to establish the /subscribe websocket (TCP connect\n// plus upgrade) before abandoning the attempt.\nconst CONNECT_TIMEOUT_MS = 10_000;\n\ntype Options = HttpOptions & {\n startupDelayMs: number;\n config?:\n | Pick<NormalizedZeroConfig, 'adminPassword' | 'operatorPassword'>\n | undefined;\n getProfileWorker?: (() => Promise<Worker>) | undefined;\n};\n\nexport class ChangeStreamerHttpServer extends HttpService {\n readonly id = 'change-streamer-http-server';\n readonly #lc: LogContext;\n readonly #opts: Options;\n readonly #changeStreamer: ChangeStreamerService;\n\n constructor(\n lc: LogContext,\n opts: Options,\n parent: Worker,\n changeStreamer: ChangeStreamerService,\n ) {\n super('change-streamer-http-server', lc, opts, async fastify => {\n await fastify.register(websocket);\n\n fastify.get(CHANGES_PATH_PATTERN, {websocket: true}, this.#changes);\n fastify.get(\n SNAPSHOT_PATH_PATTERN,\n {websocket: true},\n this.#reserveSnapshot,\n );\n fastify.get(SUBSCRIBE_PATH_PATTERN, {websocket: true}, this.#subscribe);\n\n fastify.get('/profz', (req, res) =>\n handleProfzRequest(\n lc,\n opts.config ?? {\n adminPassword: undefined,\n operatorPassword: undefined,\n },\n req,\n res,\n opts.getProfileWorker,\n undefined,\n 'change-streamer',\n ),\n );\n\n installWebSocketReceiver<'snapshot' | 'changes' | 'subscribe'>(\n lc,\n fastify.websocketServer,\n this.#receiveWebsocket,\n parent,\n );\n });\n\n this.#lc = lc;\n this.#opts = opts;\n this.#changeStreamer = changeStreamer;\n }\n\n // Called when receiving a web socket via the main dispatcher handoff.\n readonly #receiveWebsocket = (\n ws: WebSocket,\n action: 'changes' | 'snapshot' | 'subscribe',\n msg: IncomingMessageSubset,\n ) => {\n switch (action) {\n case 'snapshot':\n return this.#reserveSnapshot(ws, msg);\n case 'changes':\n return this.#changes(ws, msg);\n case 'subscribe':\n return this.#subscribe(ws, msg);\n default:\n closeWithError(\n this._lc,\n ws,\n `invalid action \"${action}\" received in handoff`,\n );\n return;\n }\n };\n\n readonly #reserveSnapshot = async (ws: WebSocket, req: RequestHeaders) => {\n this.#ensureChangeStreamerStarted('incoming snapshot reservation');\n try {\n const url = new URL(\n req.url ?? '',\n req.headers.origin ?? 'http://localhost',\n );\n checkProtocolVersion(url.pathname);\n const taskID = url.searchParams.get('taskID');\n if (!taskID) {\n throw new Error('Missing taskID in snapshot request');\n }\n const downstream =\n await this.#changeStreamer.startSnapshotReservation(taskID);\n // Send-only for now: the reservation carries no inbound application\n // messages (only transport acks, demuxed internally).\n void streamInternal(this._lc, ws, undefined, downstream);\n } catch (err) {\n closeWithError(this._lc, ws, err, PROTOCOL_ERROR);\n }\n };\n\n readonly #changes = async (ws: WebSocket, req: RequestHeaders) => {\n try {\n const ctx = getSubscriberContext(req);\n if (ctx.mode === 'serving') {\n this.#ensureChangeStreamerStarted('incoming subscription');\n }\n\n const downstream = await this.#changeStreamer.subscribe(ctx);\n // Send-only for now: the subscription carries no inbound application\n // messages (only transport acks, demuxed internally).\n void streamInternalStringified(this._lc, ws, undefined, downstream, {\n batched: ctx.wsBatched,\n });\n } catch (err) {\n closeWithError(this._lc, ws, err, PROTOCOL_ERROR);\n }\n };\n\n /**\n * The merged v7 `/subscribe` handler. A single bidirectional connection\n * carries, in sequence: an optional reservation phase (`reserve-snapshot` →\n * `reserved`) and then the subscription phase (`start-subscription` → change\n * stream). This ensures that the subscriber subscribes to the same task from\n * which it restored the backup.\n */\n readonly #subscribe = async (ws: WebSocket, req: RequestHeaders) => {\n let reservation: Source<SnapshotMessage> | undefined;\n const outbound = Subscription.create<string | PreSerializedBatch>();\n\n try {\n const url = new URL(\n req.url ?? '',\n req.headers.origin ?? 'http://localhost',\n );\n const protocolVersion = checkProtocolVersion(url.pathname);\n if (protocolVersion < MIN_SUBSCRIBE_PROTOCOL_VERSION) {\n throw new Error(\n `the /subscribe endpoint requires protocol v${MIN_SUBSCRIBE_PROTOCOL_VERSION} ` +\n `(client is at v${protocolVersion})`,\n );\n }\n\n const instream = await streamInternalStringified(\n this._lc,\n ws,\n subscribeUpstreamSchema,\n outbound,\n {batched: true},\n );\n\n // Keep reading upstream control messages for the life of the connection.\n // Do not manually exit loop, as that would close the socket. The loop\n // exits naturally when the connection closes.\n for await (const msg of instream) {\n if (msg[0] === 'reserve-snapshot') {\n const {taskID, localReplica} = msg[1];\n this.#ensureChangeStreamerStarted('incoming snapshot reservation');\n reservation = await this.#changeStreamer.startSnapshotReservation(\n taskID,\n {localReplica},\n );\n void this.#forwardReservation(reservation, outbound);\n } else if (msg[0] === 'start-subscription') {\n const ctx: SubscriberContext = {\n ...msg[1],\n protocolVersion,\n wsBatched: true,\n };\n if (ctx.mode === 'serving') {\n this.#ensureChangeStreamerStarted('incoming subscription');\n }\n // subscribe() registers the subscriber and then releases any\n // reservation for that task.\n reservation = undefined;\n await this.#changeStreamer.subscribe(ctx, outbound);\n }\n }\n } catch (err) {\n closeWithError(this._lc, ws, err, PROTOCOL_ERROR);\n } finally {\n // Covers a reservation-phase disconnect (instream ended before a\n // subscription started); idempotent with the close handler above.\n reservation?.cancel();\n }\n };\n\n /**\n * Forwards a snapshot reservation's single `['status', ...]` message onto\n * the merged connection's `outbound` sink as a `['reserved', ...]` message.\n * The loop then blocks on the held-open reservation until it is cancelled,\n * by `subscribe()` at the subscription handoff, or by the reservation-phase\n * teardown on disconnect.\n */\n async #forwardReservation(\n reservation: Source<SnapshotMessage>,\n outbound: Subscription<string | PreSerializedBatch>,\n ) {\n try {\n for await (const [, status] of reservation) {\n // Translate the /snapshot protocol's ['status', {tag: 'status', ...}]\n // message to the /subscribe protocol's ['reserved', {tag: 'snapshot', ...}].\n outbound.push(\n BigIntJSON.stringify([\n 'reserved',\n {...status, tag: 'snapshot'},\n ] satisfies ReservedMessage),\n );\n }\n } catch (e) {\n this._lc.warn?.(`error forwarding snapshot reservation`, e);\n outbound.fail(e instanceof Error ? e : new Error(String(e)));\n }\n }\n\n #changeStreamerStarted = false;\n\n #ensureChangeStreamerStarted(reason?: string) {\n if (!this.#changeStreamerStarted && this._state.shouldRun()) {\n this.#lc.info?.(\n `starting ChangeStreamerService ${reason ? `(${reason})` : ''}`,\n );\n void this.#changeStreamer\n .run()\n .catch(e =>\n this.#lc.warn?.(`ChangeStreamerService ended with error`, e),\n )\n .finally(() => this.stop());\n\n this.#changeStreamerStarted = true;\n }\n }\n\n protected override _onStart(): void {\n const {startupDelayMs, readinessGate = promiseVoid} = this.#opts;\n if (startupDelayMs === 0) {\n // In RMv2, there is no need to delay starting the change streamer\n // because RM startup is non-disruptive.\n this.#ensureChangeStreamerStarted();\n } else {\n // In RMv1, starting the change-streamer forcibly shuts down the\n // previous change-streamer, causing view-syncers to reconnect.\n // If this replication-manager has just started, the routing layer may\n // not have registered it with DNS, as that only happens after it\n // confirms health checks. To minimize downtime, the takeover is\n // delayed for the configured startupDelayMs _after_ beginning to\n // advertise readiness.\n void readinessGate.then(() => {\n this.#lc.info?.(\n `waiting ${startupDelayMs}ms before taking over the change log`,\n );\n this._state.setTimeout(\n () =>\n this.#ensureChangeStreamerStarted(\n `startup delay elapsed (${startupDelayMs} ms)`,\n ),\n startupDelayMs,\n );\n });\n }\n }\n\n /**\n * Signals (drainable) subscribers to switch over to another\n * replication-manager, resolving when they have disconnected. Subscribers\n * continue to be served in the meantime, but new connections (e.g.\n * switchover attempts, reconnects, and snapshot reservations) are refused,\n * so that they are routed to another replication-manager.\n */\n async drain(): Promise<void> {\n this.stopAccepting();\n if (this.#changeStreamerStarted && this.#changeStreamer.drain) {\n await this.#changeStreamer.drain();\n }\n }\n\n protected override async _onStop(): Promise<void> {\n if (this.#changeStreamerStarted) {\n await this.#changeStreamer.stop();\n }\n }\n}\n\nexport interface SnapshotReserver {\n reserveSnapshot(\n taskID: string,\n localReplica?: LocalReplica | undefined,\n ): Promise<{\n reserved: SnapshotStatus;\n followup: ReservationFollowup;\n }>;\n}\n\nexport interface SubscriptionStarter {\n subscribe(\n ctx: SubscriberContext,\n ): Promise<Source<Sized<SubscribeDownstream>>>;\n}\n\nexport interface ReservationFollowup extends SubscriptionStarter {\n /**\n * A signal to indicate that the followup is no longer valid (i.e. the\n * connection to the reserving task has been severed).\n */\n readonly signal: AbortSignal;\n\n /**\n * Abandons the reservation without subscribing, closing the connection that\n * was held open to pin the change-log floor. Used when the caller cannot\n * proceed to `subscribe()` (e.g. process shutdown while a restore is in\n * progress) and must not leave the reservation's connection open forever.\n */\n cancel(reason?: Error): void;\n}\n\nexport class ChangeStreamerHttpClient\n implements ChangeStreamer, SnapshotReserver, SubscriptionStarter\n{\n readonly #lc: LogContext;\n readonly #shardID: ShardID;\n /** Only used for discovery, i.e. when there is no `changeStreamerURI`. */\n readonly #changeDB: PostgresDB | undefined;\n readonly #changeStreamerURI: string | undefined;\n\n constructor(\n lc: LogContext,\n shardID: ShardID,\n changeDB: string,\n changeStreamerURI: string | undefined,\n ) {\n this.#lc = lc;\n this.#shardID = shardID;\n // Create a pg client with a single short-lived connection for the purpose\n // of change-streamer discovery (i.e. ChangeDB as DNS). This is not\n // needed if the change-streamer URI is specified.\n this.#changeDB = !changeStreamerURI\n ? pgClient(lc, changeDB, 'change-streamer-discovery', {\n max: 1,\n ['idle_timeout']: 15,\n })\n : undefined;\n this.#changeStreamerURI = changeStreamerURI;\n }\n\n async #resolveChangeStreamer(path: string) {\n let baseURL = this.#changeStreamerURI;\n if (!baseURL) {\n const address = await discoverChangeStreamerAddress(\n this.#shardID,\n must(this.#changeDB),\n );\n if (!address) {\n throw new Error(`no change-streamer is running`);\n }\n baseURL = address.includes('://') ? `${address}/` : `ws://${address}/`;\n }\n const uri = new URL(path, baseURL);\n this.#lc.info?.(`connecting to change-streamer@${uri}`);\n return uri;\n }\n\n async reserveSnapshot(\n taskID: string,\n localReplica?: LocalReplica | undefined,\n ): Promise<{\n reserved: SnapshotStatus;\n followup: ReservationFollowup;\n }> {\n const {instream, outbound} = await this.connect(taskID);\n outbound.push([\n 'reserve-snapshot',\n {taskID, ...(localReplica ? {localReplica} : {})},\n ]);\n\n const {head, rest} = readHead(instream);\n const item = await head.next();\n if (!item.value) {\n throw new Error('stream closed without snapshot reservation');\n }\n const {data: msg} = item.value;\n if (msg[0] !== 'reserved') {\n throw new Error(`expected \"reserved\" message but got ${msg[0]}`);\n }\n const reserved: SnapshotStatus = msg[1];\n const followup: ReservationFollowup = {\n subscribe: ctx => {\n outbound.push(['start-subscription', toStartSubscriptionContext(ctx)]);\n return Promise.resolve(rest);\n },\n signal: outbound.signal,\n cancel: reason => outbound.cancel(reason),\n };\n\n return {reserved, followup};\n }\n\n async subscribe(\n ctx: SubscriberContext,\n ): Promise<Source<Sized<SubscribeDownstream>>> {\n const {instream, outbound} = await this.connect(ctx.taskID);\n outbound.push(['start-subscription', toStartSubscriptionContext(ctx)]);\n return instream;\n }\n\n /**\n * Opens a merged v7 `/subscribe` connection (see subscribe.ts). Returns the\n * inbound change/reservation stream and an `outbound` sink into which the\n * caller pushes the `reserve-snapshot` / `start-subscription` control\n * messages that drive the connection's phases. The single connection spans\n * the reservation, backup restore, and subscription, so the reserving\n * replication-manager is the one that serves the subscription.\n */\n // exported for testing\n async connect(taskID: string): Promise<{\n instream: Source<Sized<SubscribeDownstream>>;\n outbound: Subscription<SubscribeUpstream>;\n }> {\n const uri = await this.#resolveChangeStreamer(SUBSCRIBE_PATH);\n\n // taskID is carried in the query for observability/routing; the server\n // reads the authoritative taskID from the control messages themselves.\n const params = new URLSearchParams({taskID});\n // Bound the TCP connect + HTTP upgrade. Without this, a SYN black-holed\n // by a stale Service endpoint (e.g. a just-terminated replication-manager\n // pod) is retransmitted by the kernel for ~2 minutes before ETIMEDOUT.\n // Failing fast lets the caller's retry open a fresh connection that is\n // routed to a live endpoint.\n const ws = new WebSocket(uri + `?${params.toString()}`, {\n handshakeTimeout: CONNECT_TIMEOUT_MS,\n });\n\n const outbound = Subscription.create<SubscribeUpstream>();\n const instream = await streamInternalWithSize(\n this.#lc,\n ws,\n subscribeDownstreamSchema,\n outbound,\n );\n return {instream, outbound};\n }\n}\n\ntype RequestHeaders = Pick<IncomingMessage, 'url' | 'headers'>;\n\nexport function getSubscriberContext(req: RequestHeaders): SubscriberContext {\n const url = new URL(req.url ?? '', req.headers.origin ?? 'http://localhost');\n const protocolVersion = checkProtocolVersion(url.pathname);\n const params = new URLParams(url);\n\n return {\n protocolVersion,\n id: params.get('id', true),\n taskID: params.get('taskID', true),\n mode: params.get('mode', false) === 'backup' ? 'backup' : 'serving',\n replicaVersion: params.get('replicaVersion', true),\n watermark: params.get('watermark', true),\n // Absent for subscribers that predate the parameter, which is the safe\n // default: the barrier falls back to polling rather than waiting on an\n // ACK that would never be attributed to a writer.\n wsBatched: params.getBoolean('wsBatched'),\n };\n}\n\nfunction checkProtocolVersion(pathname: string): number {\n const match = PATH_REGEX.exec(pathname);\n if (!match) {\n throw new Error(`invalid path: ${pathname}`);\n }\n const v = Number(match.groups?.version);\n if (\n Number.isNaN(v) ||\n v > PROTOCOL_VERSION ||\n v < MIN_SUPPORTED_PROTOCOL_VERSION\n ) {\n throw new Error(\n `Cannot service client at protocol v${v}. ` +\n `Supported protocols: [v${MIN_SUPPORTED_PROTOCOL_VERSION} ... v${PROTOCOL_VERSION}]`,\n );\n }\n return v;\n}\n\n// Projects a client-side SubscriberContext onto the start-subscription\n// message payload, dropping the fields that are implicit on the merged\n// connection: protocolVersion (from the request path), wsBatched (always on).\nfunction toStartSubscriptionContext(ctx: SubscriberContext): SubscribeContext {\n const {\n taskID,\n id,\n mode,\n replicaVersion,\n watermark,\n backfills,\n drainable,\n switchover,\n } = ctx;\n return {\n taskID,\n id,\n mode,\n replicaVersion,\n watermark,\n backfills,\n ...(drainable !== undefined ? {drainable} : {}),\n ...(switchover !== undefined ? {switchover} : {}),\n };\n}\n"],"mappings":"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"}
|
|
@@ -71,12 +71,6 @@ export type SQLiteChangeLogServeOptions = {
|
|
|
71
71
|
inspect?: (() => SQLiteChangeLogCoverage | undefined) | undefined;
|
|
72
72
|
};
|
|
73
73
|
export type TuningOptions = StorerOptions & {
|
|
74
|
-
/**
|
|
75
|
-
* Keeps the legacy Postgres change log on the initialization, persistence,
|
|
76
|
-
* catchup, purge, and ACK paths. Disable only after SQLite and the replica
|
|
77
|
-
* backup are authoritative for all of those responsibilities.
|
|
78
|
-
*/
|
|
79
|
-
pgChangeLogEnabled: boolean;
|
|
80
74
|
/**
|
|
81
75
|
* Whether the PG change log's head was behind the position of the
|
|
82
76
|
* replication slot from which replication resumes, in which case the
|
|
@@ -135,5 +129,15 @@ export type TuningOptions = StorerOptions & {
|
|
|
135
129
|
/**
|
|
136
130
|
* Performs initialization and schema migrations to initialize a ChangeStreamerImpl.
|
|
137
131
|
*/
|
|
138
|
-
export declare function initializeStreamer(lc: LogContext, shard: ShardID, taskID: string, discoveryAddress: string, discoveryProtocol: string,
|
|
132
|
+
export declare function initializeStreamer(lc: LogContext, shard: ShardID, taskID: string, discoveryAddress: string, discoveryProtocol: string,
|
|
133
|
+
/**
|
|
134
|
+
* The database of the legacy Postgres change log, or `null` if the PG
|
|
135
|
+
* change log is disabled, in which case no connections are made to it.
|
|
136
|
+
*
|
|
137
|
+
* The PG change log keeps the legacy Postgres change log on the
|
|
138
|
+
* initialization, persistence, catchup, purge, and ACK paths. Disable it
|
|
139
|
+
* only after SQLite and the replica backup are authoritative for all of
|
|
140
|
+
* those responsibilities.
|
|
141
|
+
*/
|
|
142
|
+
changeDB: PostgresDB | null, changeSource: ChangeSource, replicationStatusPublisher: ReplicationStatusPublisher, subscriptionState: SubscriptionState, backupConfig: BackupConfig | null, purgeLock: PurgeLock | null, autoReset: boolean, opts: TuningOptions, setTimeoutFn?: typeof setTimeout): Promise<ChangeStreamerService>;
|
|
139
143
|
//# sourceMappingURL=change-streamer-service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-streamer-service.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAcjD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,uBAAuB,CAAC;AAGnD,OAAO,KAAK,EACV,YAAY,EAEb,MAAM,mCAAmC,CAAC;AAM3C,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAGL,KAAK,0BAA0B,EAChC,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,2CAA2C,CAAC;AAajF,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EAEvB,MAAM,sBAAsB,CAAC;AAa9B,OAAO,EAEL,KAAK,2BAA2B,EACjC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAEL,KAAK,6BAA6B,EACnC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAGL,KAAK,oCAAoC,EAC1C,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAIL,KAAK,uBAAuB,EAC7B,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,aAAa,IAAI,aAAa,EACpC,MAAM,aAAa,CAAC;AAKrB,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,iBAAiB,CAAC;IACrC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,sEAAsE;IACtE,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC;IAC9D;;;;OAIG;IACH,YAAY,CAAC,EAAE,2BAA2B,GAAG,SAAS,CAAC;IACvD;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9C,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,8DAA8D;IAC9D,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;IACjC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,MAAM,uBAAuB,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;CACnE,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG;IAC1C
|
|
1
|
+
{"version":3,"file":"change-streamer-service.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -36,12 +36,11 @@ import { getDefaultHighWaterMark } from "node:stream";
|
|
|
36
36
|
* Performs initialization and schema migrations to initialize a ChangeStreamerImpl.
|
|
37
37
|
*/
|
|
38
38
|
async function initializeStreamer(lc, shard, taskID, discoveryAddress, discoveryProtocol, changeDB, changeSource, replicationStatusPublisher, subscriptionState, backupConfig, purgeLock, autoReset, opts, setTimeoutFn = setTimeout) {
|
|
39
|
-
if (
|
|
40
|
-
|
|
39
|
+
if (changeDB) await checkReplicationConfig(lc, changeDB, subscriptionState, shard, autoReset, opts.pgChangeLogBehindSlot);
|
|
40
|
+
return new ChangeStreamerImpl(lc, shard, taskID, discoveryAddress, discoveryProtocol, changeDB ? (applicationName, max) => postgres(defu({
|
|
41
41
|
max,
|
|
42
42
|
connection: { ["application_name"]: applicationName }
|
|
43
|
-
}, changeDB.options));
|
|
44
|
-
return new ChangeStreamerImpl(lc, shard, taskID, discoveryAddress, discoveryProtocol, changeDBProvider, subscriptionState, changeSource, replicationStatusPublisher, backupConfig, purgeLock, autoReset, opts, setTimeoutFn);
|
|
43
|
+
}, changeDB.options)) : null, subscriptionState, changeSource, replicationStatusPublisher, backupConfig, purgeLock, autoReset, opts, setTimeoutFn);
|
|
45
44
|
}
|
|
46
45
|
var REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS = 5e3;
|
|
47
46
|
var DEFAULT_CHANGE_LOG_UNAVAILABLE_WARN_THRESHOLD_MS = 6e4;
|
|
@@ -190,8 +189,8 @@ var ChangeStreamerImpl = class {
|
|
|
190
189
|
#changeDBProvider;
|
|
191
190
|
#replicaVersion;
|
|
192
191
|
#source;
|
|
192
|
+
/** The PG change log's storer, or `null` when the PG change log is disabled. */
|
|
193
193
|
#storer;
|
|
194
|
-
#pgChangeLogEnabled;
|
|
195
194
|
#pgChangeLogBehindSlot;
|
|
196
195
|
#forwarder;
|
|
197
196
|
#reservations;
|
|
@@ -248,14 +247,13 @@ var ChangeStreamerImpl = class {
|
|
|
248
247
|
const { replicaVersion } = subscriptionState;
|
|
249
248
|
this.#replicaVersion = replicaVersion;
|
|
250
249
|
this.#source = source;
|
|
251
|
-
this.#pgChangeLogEnabled = opts.pgChangeLogEnabled;
|
|
252
250
|
this.#pgChangeLogBehindSlot = opts.pgChangeLogBehindSlot;
|
|
251
|
+
this.#storer = changeDBProvider ? new Storer(lc, shard, taskID, discoveryAddress, discoveryProtocol, changeDBProvider, subscriptionState, (consumed) => this.#acker.trackPgChangeLog(consumed[2].watermark), (err) => this.stop(err), opts) : null;
|
|
253
252
|
if (!this.#pgChangeLogEnabled) {
|
|
254
|
-
assert(opts.sqliteChangeLogWriter && opts.sqliteChangeLogPurge && opts.sqliteCatchup && opts.sqliteChangeLogServe?.readPercent === 100 && opts.sqliteChangeLogServe.coldReadPercent === 100
|
|
253
|
+
assert(opts.sqliteChangeLogWriter && opts.sqliteChangeLogPurge && opts.sqliteCatchup && opts.sqliteChangeLogServe?.readPercent === 100 && opts.sqliteChangeLogServe.coldReadPercent === 100, "disabling the PG change log requires SQLite serving at 100 percent and a v5 backup");
|
|
255
254
|
assert(opts.sqliteChangeLogCompare === void 0, "SQLite change-log comparison requires the PG change log");
|
|
256
255
|
}
|
|
257
256
|
assert(!(this.#pgChangeLogEnabled && opts.pgChangeLogBehindSlot && !backupConfig?.replicaFile), "the PG change log is behind the replication slot, but there is no restored replica to re-initialize it from");
|
|
258
|
-
this.#storer = new Storer(lc, shard, taskID, discoveryAddress, discoveryProtocol, changeDBProvider, subscriptionState, (consumed) => this.#acker.trackPgChangeLog(consumed[2].watermark), (err) => this.stop(err), opts);
|
|
259
257
|
this.#forwarder = new Forwarder(lc, {
|
|
260
258
|
flowControlConsensusTimeoutProportion: opts.flowControlConsensusTimeoutProportion,
|
|
261
259
|
flowControlSlowSubscriberGracePeriodMs: opts.flowControlSlowSubscriberGracePeriodMs
|
|
@@ -305,7 +303,7 @@ var ChangeStreamerImpl = class {
|
|
|
305
303
|
this.#purgeScheduler = opts.sqliteChangeLogPurge && this.#changeLogWriter ? new SQLiteChangeLogPurgeScheduler(lc, () => this.#changeLogWriter?.connection, () => this.#forwarder.getAcks(), opts.sqliteChangeLogPurge) : void 0;
|
|
306
304
|
this.#acker = new UpstreamAcker({
|
|
307
305
|
trackPgChangeLog: this.#pgChangeLogEnabled,
|
|
308
|
-
trackBackup: backupConfig
|
|
306
|
+
trackBackup: backupConfig ? backupConfig.litestreamVersion === "v5" : !this.#pgChangeLogEnabled
|
|
309
307
|
});
|
|
310
308
|
const replicaFileForInitialization = opts.sqliteChangeLogCompare?.replicaFile ?? (!this.#pgChangeLogEnabled ? opts.sqliteChangeLogWriter?.replicaFile : void 0);
|
|
311
309
|
const replicaFileForReinitialization = this.#pgChangeLogEnabled && opts.pgChangeLogBehindSlot ? backupConfig?.replicaFile : void 0;
|
|
@@ -315,17 +313,17 @@ var ChangeStreamerImpl = class {
|
|
|
315
313
|
initFromPgChangeLog: this.#pgChangeLogEnabled,
|
|
316
314
|
initFromReplica: replicaFileForInitialization !== void 0
|
|
317
315
|
}, {
|
|
318
|
-
pgChangeLog: () => this.#storer.getStartStreamInitializationParameters(),
|
|
316
|
+
pgChangeLog: () => must(this.#storer).getStartStreamInitializationParameters(),
|
|
319
317
|
replica: () => must(replicaSource)(),
|
|
320
318
|
reconcileChangeLog: (resumeFrom, seed) => resumeFrom ? this.#changeLogWriter?.reconcile(resumeFrom) : this.#changeLogWriter?.reconcileFromLog(seed),
|
|
321
319
|
changeLog: () => this.#changeLogWriter?.connection,
|
|
322
|
-
reinitializePgChangeLog: replicaFileForReinitialization ? (params) => this.#storer.reinitializeFromReplica(params) : void 0
|
|
320
|
+
reinitializePgChangeLog: replicaFileForReinitialization ? (params) => must(this.#storer).reinitializeFromReplica(params) : void 0
|
|
323
321
|
});
|
|
324
322
|
this.#sqliteCatchupOptions = opts.sqliteCatchup ? {
|
|
325
323
|
...opts.sqliteCatchup,
|
|
326
324
|
cleanupGuard: opts.sqliteCatchup.cleanupGuard ?? this.#purgeScheduler?.cleanupGuard
|
|
327
325
|
} : void 0;
|
|
328
|
-
this.#comparator = this.#
|
|
326
|
+
this.#comparator = this.#storer && opts.sqliteChangeLogCompare && opts.sqliteChangeLogWriter && opts.sqliteCatchup ? new SQLiteChangeLogComparator(lc, shard, opts.sqliteCatchup.changeLogFile, opts.sqliteChangeLogWriter.identity, this.#storer, {
|
|
329
327
|
setTimeoutFn,
|
|
330
328
|
...opts.sqliteChangeLogCompare
|
|
331
329
|
}) : void 0;
|
|
@@ -333,21 +331,29 @@ var ChangeStreamerImpl = class {
|
|
|
333
331
|
this.#autoReset = autoReset;
|
|
334
332
|
this.#state = new RunningState(this.id, void 0, setTimeoutFn);
|
|
335
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Whether the legacy Postgres change log is enabled, which is determined
|
|
336
|
+
* solely by the presence of its {@link Storer} (i.e. of a change DB).
|
|
337
|
+
*/
|
|
338
|
+
get #pgChangeLogEnabled() {
|
|
339
|
+
return this.#storer !== null;
|
|
340
|
+
}
|
|
336
341
|
async run() {
|
|
337
342
|
this.#lc.info?.("starting change stream");
|
|
338
343
|
this.#forwarder.startProgressMonitor();
|
|
339
344
|
const lagReportInit = await this.#source.startLagReporter();
|
|
340
345
|
this.#lagReportingDisabled.resolve(!lagReportInit);
|
|
341
346
|
if (lagReportInit) this.#latestLagReportCommitTimeMs = lagReportInit.firstCommitTimeMs;
|
|
342
|
-
|
|
347
|
+
const storer = this.#storer;
|
|
348
|
+
let takeoverPending = storer !== null;
|
|
343
349
|
const flushBytesThreshold = getDefaultHighWaterMark(false);
|
|
344
350
|
while (this.#state.shouldRun()) {
|
|
345
351
|
let err;
|
|
346
352
|
let watermark = null;
|
|
347
353
|
let unflushedBytes = 0;
|
|
348
354
|
try {
|
|
349
|
-
if (takeoverPending) {
|
|
350
|
-
await
|
|
355
|
+
if (storer && takeoverPending) {
|
|
356
|
+
await storer.assumeOwnershipAtHead({
|
|
351
357
|
purgeLock: this.#purgeLock,
|
|
352
358
|
requestRestart: (reason) => this.#backfills.requestRestart(reason),
|
|
353
359
|
autoReset: this.#autoReset,
|
|
@@ -362,7 +368,7 @@ var ChangeStreamerImpl = class {
|
|
|
362
368
|
const stream = await this.#source.startStream(lastWatermark, backfillRequests);
|
|
363
369
|
this.#stream = stream;
|
|
364
370
|
if (!this.#state.shouldRun()) break;
|
|
365
|
-
|
|
371
|
+
storer?.run().catch((e) => stream.changes.cancel(e));
|
|
366
372
|
if (this.#state.resetBackoff() > REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS) this.#replicationStatusPublisher.publish(this.#lc, "Replicating", `Replicating from ${lastWatermark}`);
|
|
367
373
|
watermark = null;
|
|
368
374
|
this.#acker.reset(stream.acks, lastWatermark);
|
|
@@ -372,7 +378,7 @@ var ChangeStreamerImpl = class {
|
|
|
372
378
|
if (type === "status" && watermark === null && this.#mayRestart()) break;
|
|
373
379
|
switch (type) {
|
|
374
380
|
case "status":
|
|
375
|
-
if (watermark === null)
|
|
381
|
+
if (watermark === null) storer?.status(change[2].watermark);
|
|
376
382
|
if (msg.lagReport && msg.lagReport.lastTimings.commitTimeMs >= this.#latestLagReportCommitTimeMs) {
|
|
377
383
|
this.#latestLagReportCommitTimeMs = msg.lagReport.lastTimings.commitTimeMs;
|
|
378
384
|
this.#forwarder.sendStatus({
|
|
@@ -399,7 +405,7 @@ var ChangeStreamerImpl = class {
|
|
|
399
405
|
this.#backfills.track(change);
|
|
400
406
|
const serialized = serializeChangeStreamDataWithChange(change);
|
|
401
407
|
const { json } = serialized;
|
|
402
|
-
|
|
408
|
+
storer?.store(watermark, change, serialized);
|
|
403
409
|
this.#changeLogWriter?.write(change, json, serialized.change);
|
|
404
410
|
const entry = [
|
|
405
411
|
watermark,
|
|
@@ -416,7 +422,7 @@ var ChangeStreamerImpl = class {
|
|
|
416
422
|
await promiseOrAbort(flowControl, stream.changes.signal, this.#state.signal);
|
|
417
423
|
unflushedBytes = 0;
|
|
418
424
|
}
|
|
419
|
-
const readyForMore =
|
|
425
|
+
const readyForMore = storer?.readyForMore();
|
|
420
426
|
if (readyForMore) await promiseOrAbort(readyForMore, stream.changes.signal, this.#state.signal);
|
|
421
427
|
if (type === "commit" && this.#mayRestart()) break;
|
|
422
428
|
}
|
|
@@ -428,7 +434,7 @@ var ChangeStreamerImpl = class {
|
|
|
428
434
|
}
|
|
429
435
|
if (watermark) {
|
|
430
436
|
this.#lc.warn?.(`aborting interrupted transaction ${watermark}`);
|
|
431
|
-
|
|
437
|
+
storer?.abort();
|
|
432
438
|
this.#changeLogWriter?.abort();
|
|
433
439
|
this.#purgeScheduler?.onWriterIdle();
|
|
434
440
|
this.#forwarder.forward([
|
|
@@ -441,9 +447,9 @@ var ChangeStreamerImpl = class {
|
|
|
441
447
|
}
|
|
442
448
|
if (this.#backfills.restartReason !== null && err === void 0) {
|
|
443
449
|
this.#backfills.recordRestart();
|
|
444
|
-
await
|
|
450
|
+
await storer?.stop();
|
|
445
451
|
} else await Promise.all([
|
|
446
|
-
|
|
452
|
+
storer?.stop(),
|
|
447
453
|
this.#state.backoff(this.#lc, err),
|
|
448
454
|
this.#state.retryDelay > REPLICATION_STATUS_ERROR_DELAY_THRESHOLD_MS ? publishCriticalEvent(this.#lc, replicationStatusError(this.#lc, "Replicating", err)) : promiseVoid
|
|
449
455
|
]);
|
|
@@ -456,7 +462,7 @@ var ChangeStreamerImpl = class {
|
|
|
456
462
|
const { tag } = msg;
|
|
457
463
|
switch (tag) {
|
|
458
464
|
case "reset-required":
|
|
459
|
-
if (this.#
|
|
465
|
+
if (this.#changeDBProvider) await markResetRequired(this.#changeDBProvider("change-streamer-reset", 1), this.#shard);
|
|
460
466
|
await publishReplicationError(this.#lc, "Replicating", msg.message ?? "Resync required", msg.errorDetails);
|
|
461
467
|
if (this.#autoReset) {
|
|
462
468
|
this.#lc.warn?.("shutting down for auto-reset");
|
|
@@ -533,10 +539,10 @@ var ChangeStreamerImpl = class {
|
|
|
533
539
|
this.#forwarder.remove(subscriber);
|
|
534
540
|
};
|
|
535
541
|
const catchupFromPG = () => {
|
|
536
|
-
|
|
542
|
+
const storer = must(this.#storer, "cannot catch up from a disabled PG change log");
|
|
537
543
|
try {
|
|
538
544
|
this.#forwarder.add(subscriber);
|
|
539
|
-
|
|
545
|
+
storer.catchup(subscriber, ctx.mode);
|
|
540
546
|
} catch (e) {
|
|
541
547
|
removeFromForwarder();
|
|
542
548
|
throw e;
|
|
@@ -683,7 +689,7 @@ var ChangeStreamerImpl = class {
|
|
|
683
689
|
}, delay);
|
|
684
690
|
}
|
|
685
691
|
async #getChangeLogState() {
|
|
686
|
-
const minWatermark = await this.#storer.getMinWatermarkForCatchup();
|
|
692
|
+
const minWatermark = await must(this.#storer, "the PG change log is disabled").getMinWatermarkForCatchup();
|
|
687
693
|
if (!minWatermark) this.#lc.warn?.(`Unexpected empty changeLog. Resync if "Local replica watermark" errors arise`);
|
|
688
694
|
return {
|
|
689
695
|
replicaVersion: this.#replicaVersion,
|
|
@@ -701,7 +707,8 @@ var ChangeStreamerImpl = class {
|
|
|
701
707
|
};
|
|
702
708
|
}
|
|
703
709
|
async #purgePGChangeLog() {
|
|
704
|
-
|
|
710
|
+
const storer = this.#storer;
|
|
711
|
+
if (!storer || storer.ownershipPending) return;
|
|
705
712
|
try {
|
|
706
713
|
const { backupWatermark, purgeWatermark, current } = this.#getCleanupFloor();
|
|
707
714
|
if (purgeWatermark < backupWatermark) {
|
|
@@ -713,7 +720,7 @@ var ChangeStreamerImpl = class {
|
|
|
713
720
|
if (purgeWatermark <= this.#pgPurgedWatermark) return;
|
|
714
721
|
this.#lc.info?.(`Purging PG changes before ${purgeWatermark} ...`);
|
|
715
722
|
const start = performance.now();
|
|
716
|
-
const deleted = await
|
|
723
|
+
const deleted = await storer.purgeRecordsBefore(purgeWatermark);
|
|
717
724
|
const elapsed = (performance.now() - start).toFixed(2);
|
|
718
725
|
this.#lc.info?.(`Purged ${deleted} PG changes before ${purgeWatermark} (${elapsed} ms)`);
|
|
719
726
|
this.#pgPurgedWatermark = purgeWatermark;
|
|
@@ -747,7 +754,7 @@ var ChangeStreamerImpl = class {
|
|
|
747
754
|
* anyway (see Storer.assumeOwnershipAtHead()).
|
|
748
755
|
*/
|
|
749
756
|
#mayRestart() {
|
|
750
|
-
return this.#backfills.restartReason !== null && !this.#storer
|
|
757
|
+
return this.#backfills.restartReason !== null && !this.#storer?.ownershipPending;
|
|
751
758
|
}
|
|
752
759
|
async stop(err) {
|
|
753
760
|
this.#state.stop(this.#lc, err);
|
|
@@ -758,7 +765,7 @@ var ChangeStreamerImpl = class {
|
|
|
758
765
|
this.#comparator?.stop();
|
|
759
766
|
this.#sqliteCatchup?.close();
|
|
760
767
|
this.#changeLogWriter?.close();
|
|
761
|
-
await Promise.allSettled([this.#storer
|
|
768
|
+
await Promise.allSettled([this.#storer?.stop(), this.#source.stop()]);
|
|
762
769
|
}
|
|
763
770
|
#recordForwardedTransactionBoundary(type, watermark) {
|
|
764
771
|
switch (type) {
|