@rocicorp/zero 1.10.0-canary.7 → 1.10.0-canary.9
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/shared/src/promise-race.d.ts +14 -0
- package/out/shared/src/promise-race.d.ts.map +1 -1
- package/out/shared/src/promise-race.js +32 -1
- package/out/shared/src/promise-race.js.map +1 -1
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +5 -5
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/db/migration-lite.d.ts.map +1 -1
- package/out/zero-cache/src/db/migration-lite.js +5 -3
- package/out/zero-cache/src/db/migration-lite.js.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.d.ts +0 -13
- package/out/zero-cache/src/db/transaction-pool.d.ts.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.js +3 -69
- package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-schema.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-schema.js +8 -2
- package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
- 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 +3 -2
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/forwarder.js +1 -1
- package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js +11 -5
- package/out/zero-cache/src/services/change-streamer/litestream3-prometheus-poller.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.d.ts +34 -0
- package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js +152 -42
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/types/pg.d.ts.map +1 -1
- package/out/zero-cache/src/types/pg.js.map +1 -1
- package/out/zero-cache/src/types/streams.d.ts +4 -9
- package/out/zero-cache/src/types/streams.d.ts.map +1 -1
- package/out/zero-cache/src/types/streams.js.map +1 -1
- package/out/zero-cache/src/types/subscription.d.ts +2 -11
- package/out/zero-cache/src/types/subscription.d.ts.map +1 -1
- package/out/zero-cache/src/types/subscription.js +4 -25
- package/out/zero-cache/src/types/subscription.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":"litestream3-prometheus-poller.js","names":["#lc","#replicaFile","#backupURL","#metricsEndpoint","#state","#stream","#watermarks","#verifyBackupState","#purgesBlocked","#checkMetricsTimer","#runStartTime","#confirmRestorableBackup","#lastVerifiedUploadTime","#verifyBackupStateTimed","#checkWatermarks","#publishVerifiedWatermarks","#checkInitialBackupDeadline","#fetchWatermarks","#lastWatermark","#latestBackupTime","#maxBackupBefore","#confirmedDurable","#backupStaleSince","#shutDownOnWedgedBackup","#initialBackupGraceMs","#shutDownOnMissingInitialBackup"],"sources":["../../../../../../zero-cache/src/services/change-streamer/litestream3-prometheus-poller.ts"],"sourcesContent":["import {statSync} from 'node:fs';\nimport type {LogContext} from '@rocicorp/logger';\nimport parsePrometheusTextFormat from 'parse-prometheus-text-format';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {\n litestreamBackupVerificationDuration,\n litestreamMonitorMetricAttrs,\n} from '../litestream/metrics.ts';\nimport {RunningState, UnrecoverableError} from '../running-state.ts';\nimport type {BackedUpWatermark} from './backup-monitor.ts';\n\nexport const CHECK_INTERVAL_MS = 10_000;\n\n/**\n * Allowance for clock skew between the machine reporting litestream metrics\n * and the timestamps reported by the backup destination (e.g. S3).\n */\nexport const BACKUP_VERIFICATION_SLACK_MS = 60_000;\n\n/**\n * How long the actual backup state may remain *continuously* behind the\n * backup progress claimed by litestream metrics before the backup is\n * considered genuinely wedged and the process is shut down.\n *\n * A wedged backup is dangerous: litestream believes it is making durable\n * progress when it is not, which can silently corrupt the backup (the WAL\n * format only makes *some* such gaps detectable). Once that is confirmed to\n * be the persistent state, continuing to run is worse than crashing, so the\n * process exits loudly (non-zero, with a logged error) and the wedged backup\n * destination becomes the priority to investigate.\n *\n * The change-log is conservatively *not* purged for the entire time the\n * backup is stale, so the only cost of a generous grace period is unbounded\n * change-log growth. We therefore err well on the side of slack: the backup\n * must stay stuck across many {@link CHECK_INTERVAL_MS} checks before we tear\n * the server down, so that a transient hiccup (a slow or briefly unreachable\n * destination, or litestream restarting) does not trigger a shutdown. The\n * staleness clock is reset the moment a purge is confirmed against a real\n * upload.\n */\nexport const WEDGED_SHUTDOWN_GRACE_MS = 15 * 60_000; // 15 minutes\n\nconst GiB = 1024 ** 3;\n\n/**\n * How long the BackupMonitor waits for the *first* restorable backup to appear\n * before concluding that the backup pipeline is broken and shutting the process\n * down (see {@link BackupMonitor.#shutDownOnMissingInitialBackup}).\n *\n * On a fresh stack the first backup is not restorable until the initial\n * Postgres->replica sync completes and litestream finishes uploading the\n * initial snapshot — a multipart upload that is not listable in the destination\n * until it is committed. The producer cannot observe that sub-snapshot progress,\n * so this is necessarily a generous timeout. It is scaled by replica size to\n * mirror the platform's storage-scaled startup-probe allowance (~1 hour per\n * 50GB) so that it never fires before the platform would restart a view-syncer\n * that is waiting on the backup.\n */\nexport const INITIAL_BACKUP_GRACE_MS_PER_UNIT = 60 * 60_000; // 1 hour ...\nconst INITIAL_BACKUP_GRACE_UNIT_BYTES = 50 * GiB; // ... per 50 GiB\n\n/**\n * How often the BackupMonitor re-checks whether the first restorable backup has\n * appeared while a view-syncer holds a snapshot reservation open during a cold\n * start.\n */\nexport const RESTORABLE_BACKUP_POLL_INTERVAL_MS = 10_000;\n\n/**\n * Returns the time of the most recent object actually uploaded to the\n * backup replica destination (e.g. as determined by listing the snapshots\n * and WAL segments in S3). Rejects if the backup state cannot be determined.\n *\n * See `getLastBackupTime()` in `../litestream/commands.ts` for the\n * production implementation.\n */\nexport type BackupStateVerifier = () => Promise<Date>;\n\n/**\n * The Litestream3PrometheusPoller polls the litestream \"/metrics\" endpoint to\n * track the watermark (label) value of the `litestream_replica_progress` gauge\n * and emits verified watermarks to a stream of {@link BackedUpWatermark}s.\n *\n * See: https://github.com/rocicorp/litestream/pull/3\n *\n * Because the watermarks reported by litestream metrics reflect what\n * litestream *believes* has been backed up (which has been observed to\n * diverge from reality when uploads silently fail), the cleanup watermark is\n * only published after verifying it against the actual backup state in the\n * replica destination via a {@link BackupStateVerifier}. If the\n * actual backup state stays behind the claimed progress for longer than\n * {@link WEDGED_SHUTDOWN_GRACE_MS}, the backup is treated as wedged and the\n * process is shut down rather than risk corrupting the backup.\n */\nexport class Litestream3PrometheusPoller {\n readonly #lc: LogContext;\n readonly #replicaFile: string;\n readonly #backupURL: string;\n readonly #metricsEndpoint: string;\n readonly #state = new RunningState('litestream3-prometheus-poller');\n\n readonly #stream: Subscription<BackedUpWatermark>;\n readonly #watermarks = new Map<string, Date>();\n\n readonly #verifyBackupState: BackupStateVerifier;\n readonly #purgesBlocked = getOrCreateCounter('replica', 'purge_blocked', {\n description:\n 'Number of change-log purges blocked because the actual backup state ' +\n '(as listed from the replica destination) could not be verified, or ' +\n 'is older than the backup progress claimed by litestream metrics. ' +\n 'A steadily increasing value indicates a wedged or failing backup. ' +\n 'The \"backup-wedged\" reason is emitted once just before the process ' +\n 'shuts itself down due to a persistently stale backup.',\n });\n\n #lastWatermark: string = '';\n #latestBackupTime: Date | null = null;\n // The time of the most recent object actually verified in the backup\n // destination, or `null` if none has ever been verified. Only assigned from a\n // successful verifyBackupState() and never cleared, so a non-null value also\n // means a restorable backup has been confirmed to exist (which is what gates\n // the snapshot `status` signal and satisfies the initial-backup deadline).\n #lastVerifiedUploadTime: Date | null = null;\n // Epoch ms at which the actual backup state was first observed to be\n // continuously behind the watermark litestream claims to have backed up,\n // or `null` while the backup is keeping up. Reset whenever a purge is\n // confirmed against a real upload.\n #backupStaleSince: number | null = null;\n // Epoch ms at which run() started, used to bound how long to wait for the\n // first restorable backup to appear. `null` until run() is called.\n #runStartTime: number | null = null;\n #checkMetricsTimer: NodeJS.Timeout | undefined;\n\n constructor(\n lc: LogContext,\n replicaFile: string,\n backupURL: string,\n metricsEndpoint: string,\n verifyBackupState: BackupStateVerifier,\n ) {\n this.#lc = lc.withContext('component', 'litestream3-prometheus-poller');\n this.#replicaFile = replicaFile;\n this.#backupURL = backupURL;\n this.#metricsEndpoint = metricsEndpoint;\n this.#verifyBackupState = verifyBackupState;\n this.#stream = Subscription.create<BackedUpWatermark>({\n cleanup: () => {\n clearInterval(this.#checkMetricsTimer);\n this.#state.stop(this.#lc);\n },\n });\n }\n\n /**\n * Starts polling for watermarks and pushes new ones to returned stream.\n * Must only be called once.\n */\n start(): Source<BackedUpWatermark> {\n assert(this.#runStartTime === null, `Already called start()`);\n this.#runStartTime = Date.now();\n this.#lc.info?.(`monitoring backups at ${this.#metricsEndpoint}`);\n this.#checkMetricsTimer = setInterval(\n this.checkVerifyAndPublishWatermarks,\n CHECK_INTERVAL_MS,\n );\n return this.#stream;\n }\n\n /**\n * Verifies whether a restorable backup actually exists in the destination\n * (as opposed to what litestream metrics merely claim). On success the\n * verified upload time is cached — seeding the cleanup-verification fast path\n * ({@link #confirmedDurable}) — and the initial-backup deadline is satisfied.\n * Returns false if no backup is listable yet (e.g. the initial snapshot is\n * still uploading).\n */\n async #confirmRestorableBackup(): Promise<boolean> {\n try {\n this.#lastVerifiedUploadTime = await this.#verifyBackupStateTimed();\n return true;\n } catch (e) {\n this.#lc.info?.(`backup not yet restorable at ${this.#backupURL}`, e);\n return false;\n }\n }\n\n // Exported for testing\n readonly checkVerifyAndPublishWatermarks = async () => {\n try {\n await this.#checkWatermarks();\n } catch (e) {\n this.#lc.warn?.(`unable to fetch metrics at ${this.#metricsEndpoint}`, e);\n }\n try {\n await this.#publishVerifiedWatermarks();\n } catch (e) {\n this.#lc.warn?.(`error scheduling cleanup`, e);\n }\n try {\n await this.#checkInitialBackupDeadline();\n } catch (e) {\n this.#lc.warn?.(`error checking initial backup deadline`, e);\n }\n };\n\n async *#fetchWatermarks(): AsyncGenerator<{\n watermark: string;\n time: Date;\n name?: string | undefined;\n }> {\n const metricsEndpoint = this.#metricsEndpoint;\n const signal = this.#state.signal;\n let resp;\n try {\n resp = await fetch(metricsEndpoint, {signal});\n } catch (e) {\n if (signal.aborted) {\n // not an error.\n return;\n }\n // Treat exceptions from fetch (e.g. network errors) as non-fatal, and simply\n // log them and skip the watermark check until the next interval.\n this.#lc.warn?.(`unable to fetch metrics at ${this.#metricsEndpoint}`, e);\n return;\n }\n if (!resp.ok) {\n this.#lc.warn?.(\n `unable to fetch metrics at ${this.#metricsEndpoint}: ${await resp.text()}`,\n );\n return;\n }\n\n const families = parsePrometheusTextFormat(await resp.text());\n for (const family of families) {\n if (\n family.type === 'GAUGE' &&\n family.name === 'litestream_replica_progress'\n ) {\n for (const metric of family.metrics) {\n const watermark = metric.labels?.watermark;\n const name = metric.labels?.name;\n const time = new Date(parseFloat(metric.value) * 1000);\n\n if (watermark) {\n yield {watermark, time, name};\n }\n }\n }\n }\n }\n\n async #checkWatermarks() {\n for await (const {watermark, name, time} of this.#fetchWatermarks()) {\n if (watermark > this.#lastWatermark && !this.#watermarks.has(watermark)) {\n this.#lc.info?.(\n `replicated watermark=${watermark} to ${name}` +\n ` at ${time.toISOString()}.`,\n );\n this.#watermarks.set(watermark, time);\n this.#latestBackupTime = time;\n }\n }\n return this.#latestBackupTime;\n }\n\n async #publishVerifiedWatermarks() {\n const maxBackup = this.#maxBackupBefore(Date.now());\n if (!maxBackup) {\n return;\n }\n const {watermark: maxWatermark, backupTime: claimedTime} = maxBackup;\n // Purge guard: the watermarks (and their backup times) come from\n // litestream metrics, which are exported when litestream *believes*\n // an upload succeeded, and have been observed to advance even when\n // nothing was actually written to the backup destination. Purging the\n // change-log based on a falsely advancing watermark permanently breaks\n // the ability to restore + catch up. Before advancing the cleanup\n // watermark, verify it against the actual backup state: a claimed\n // backup time is only trusted if an object was actually uploaded to\n // the replica destination at (or after) that time, modulo clock skew.\n if (!this.#confirmedDurable(claimedTime)) {\n try {\n this.#lastVerifiedUploadTime = await this.#verifyBackupStateTimed();\n } catch (e) {\n this.#purgesBlocked.add(1, {reason: 'verification-failed'});\n // Skipping the purge is safe: the change-log just grows.\n this.#lc.warn?.(\n `unable to verify backup state. skipping change-log cleanup ` +\n `up to watermark ${maxWatermark} ` +\n `(claimed backup time ${claimedTime.toISOString()})`,\n e,\n );\n return;\n }\n }\n // Watermarks whose backup time isn't yet confirmed durable remain in the\n // map and are re-evaluated at the next check.\n const lastUpload = must(this.#lastVerifiedUploadTime);\n const verifiedBackup = this.#maxBackupBefore(\n lastUpload.getTime() + BACKUP_VERIFICATION_SLACK_MS,\n );\n if (!verifiedBackup) {\n this.#purgesBlocked.add(1, {reason: 'backup-stale'});\n const now = Date.now();\n if (this.#backupStaleSince === null) {\n this.#backupStaleSince = now;\n }\n const staleForMs = now - this.#backupStaleSince;\n this.#lc.warn?.(\n `blocked change-log cleanup up to watermark ${maxWatermark}: ` +\n `litestream claims it was backed up at ` +\n `${claimedTime.toISOString()}, but the last object actually ` +\n `uploaded to ${this.#backupURL} was at ` +\n `${lastUpload.toISOString()}. ` +\n `The backup has been stale for ${staleForMs} ms.`,\n );\n if (staleForMs >= WEDGED_SHUTDOWN_GRACE_MS) {\n this.#shutDownOnWedgedBackup(staleForMs, claimedTime, lastUpload);\n }\n return;\n }\n // The cleanup watermark advanced past a real upload, so the backup is\n // keeping up: clear the staleness clock.\n this.#backupStaleSince = null;\n const {watermark: verifiedWatermark, backupTime} = verifiedBackup;\n for (const watermark of this.#watermarks.keys()) {\n if (watermark <= verifiedWatermark) {\n this.#watermarks.delete(watermark);\n }\n }\n this.#stream.push({\n watermark: verifiedWatermark,\n backupTimeMs: backupTime.getTime(),\n });\n this.#lastWatermark = verifiedWatermark;\n }\n\n async #verifyBackupStateTimed(): Promise<Date> {\n const start = performance.now();\n let result: 'success' | 'error' = 'error';\n try {\n const ret = await this.#verifyBackupState();\n result = 'success';\n return ret;\n } finally {\n litestreamBackupVerificationDuration().recordMs(\n performance.now() - start,\n {\n ...litestreamMonitorMetricAttrs(\n this.#backupURL,\n 'legacy',\n 'replication_manager',\n ),\n result,\n },\n );\n }\n }\n\n /**\n * Called when the backup has been continuously stale for longer than\n * {@link WEDGED_SHUTDOWN_GRACE_MS}. Continuing to run risks corrupting the\n * backup, so the process is shut down by rejecting the {@link run()}\n * promise. The exit is non-zero and logged at ERROR for alerting; on\n * restart the monitor re-verifies, so a still-wedged backup keeps the\n * process down until the destination is fixed.\n */\n #shutDownOnWedgedBackup(\n staleForMs: number,\n claimedTime: Date,\n lastUpload: Date,\n ) {\n this.#purgesBlocked.add(1, {reason: 'backup-wedged'});\n const err = new UnrecoverableError(\n `backup at ${this.#backupURL} is wedged: litestream claims a backup ` +\n `at ${claimedTime.toISOString()}, but the last object actually ` +\n `uploaded was at ${lastUpload.toISOString()}, and the backup has ` +\n `not advanced for ${staleForMs} ms (grace period ` +\n `${WEDGED_SHUTDOWN_GRACE_MS} ms). Shutting down to avoid corrupting ` +\n `the backup; investigate the backup destination.`,\n );\n this.#lc.error?.(err.message);\n this.#stream.fail(err);\n }\n\n /**\n * On a fresh stack, fails loudly if no restorable backup has appeared within\n * a generous, replica-size-scaled grace period (see\n * {@link INITIAL_BACKUP_GRACE_MS_PER_UNIT}). This moves the \"give up\" decision\n * onto the replication-manager — the producer responsible for creating the\n * backup — instead of leaving view-syncers to wait (and the platform to keep\n * restarting them) while the real fault is here. Does nothing once a\n * restorable backup has been confirmed.\n */\n async #checkInitialBackupDeadline() {\n if (this.#lastVerifiedUploadTime !== null || this.#runStartTime === null) {\n return;\n }\n const elapsed = Date.now() - this.#runStartTime;\n if (elapsed < this.#initialBackupGraceMs()) {\n return;\n }\n // The deadline has elapsed without a confirmed backup. Do one definitive\n // check against the destination before giving up, in case a backup landed\n // but was never exercised by a reservation or a cleanup verification.\n if (await this.#confirmRestorableBackup()) {\n return;\n }\n this.#shutDownOnMissingInitialBackup(elapsed);\n }\n\n /**\n * The grace period for the first restorable backup, scaled by replica size to\n * mirror the platform's storage-scaled startup-probe allowance. Falls back to\n * the minimum (one unit) if the replica file size cannot be determined.\n */\n #initialBackupGraceMs(): number {\n let bytes = 0;\n try {\n bytes = statSync(this.#replicaFile).size;\n } catch {\n // Replica file not present/readable yet; fall back to the minimum grace.\n }\n const units = Math.max(\n 1,\n Math.ceil(bytes / INITIAL_BACKUP_GRACE_UNIT_BYTES),\n );\n return units * INITIAL_BACKUP_GRACE_MS_PER_UNIT;\n }\n\n /**\n * Called when no restorable backup has appeared within\n * {@link #initialBackupGraceMs}. Continuing to run is pointless — no\n * view-syncer can restore — and the fault is the backup pipeline, so the\n * process exits non-zero with a logged error for alerting, mirroring\n * {@link #shutDownOnWedgedBackup}.\n */\n #shutDownOnMissingInitialBackup(elapsedMs: number) {\n const err = new UnrecoverableError(\n `no restorable backup has appeared at ${this.#backupURL} within ` +\n `${elapsedMs} ms of startup (grace period ` +\n `${this.#initialBackupGraceMs()} ms). The initial backup pipeline ` +\n `appears to be broken; shutting down so that the replication-manager ` +\n `is flagged as the cause rather than the view-syncers waiting on it. ` +\n `Investigate litestream replication to ${this.#backupURL}.`,\n );\n this.#lc.error?.(err.message);\n this.#stream.fail(err);\n }\n\n /**\n * Returns the newest watermark whose backup time is at or before `cutoff`\n * (epoch ms), or `''` if there is none.\n */\n #maxBackupBefore(\n cutoff: number,\n ): {watermark: string; backupTime: Date} | null {\n let max = '';\n let maxBackupTime: Date | undefined;\n for (const [watermark, backupTime] of this.#watermarks) {\n if (backupTime.getTime() <= cutoff && watermark > max) {\n max = watermark;\n maxBackupTime = backupTime;\n }\n }\n if (!maxBackupTime) {\n return null;\n }\n return {watermark: max, backupTime: maxBackupTime};\n }\n\n /**\n * Returns `true` if the actual backup state, as last verified against the\n * backup destination, confirms that data claimed to be backed up at\n * `claimedTime` is durable (i.e. an object was actually uploaded at or\n * after `claimedTime`, allowing {@link BACKUP_VERIFICATION_SLACK_MS} of\n * clock skew).\n */\n #confirmedDurable(claimedTime: Date): boolean {\n const lastUpload = this.#lastVerifiedUploadTime;\n return (\n lastUpload !== null &&\n claimedTime.getTime() <=\n lastUpload.getTime() + BACKUP_VERIFICATION_SLACK_MS\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"litestream3-prometheus-poller.js","names":["#lc","#replicaFile","#backupURL","#metricsEndpoint","#state","#stream","#watermarks","#verifyBackupState","#purgesBlocked","#checkMetricsTimer","#runStartTime","#confirmRestorableBackup","#lastVerifiedUploadTime","#verifyBackupStateTimed","#checkWatermarks","#publishVerifiedWatermarks","#checkInitialBackupDeadline","#fetchWatermarks","#lastWatermark","#latestBackupTime","#maxBackupBefore","#confirmedDurable","#backupStaleSince","#shutDownOnWedgedBackup","#initialBackupGraceMs","#shutDownOnMissingInitialBackup"],"sources":["../../../../../../zero-cache/src/services/change-streamer/litestream3-prometheus-poller.ts"],"sourcesContent":["import {statSync} from 'node:fs';\nimport type {LogContext} from '@rocicorp/logger';\nimport parsePrometheusTextFormat from 'parse-prometheus-text-format';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport {must} from '../../../../shared/src/must.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {versionFromLexi} from '../../types/lexi-version.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {\n litestreamBackupVerificationDuration,\n litestreamMonitorMetricAttrs,\n} from '../litestream/metrics.ts';\nimport {RunningState, UnrecoverableError} from '../running-state.ts';\nimport type {BackedUpWatermark} from './backup-monitor.ts';\n\nexport const CHECK_INTERVAL_MS = 10_000;\n\n/**\n * Allowance for clock skew between the machine reporting litestream metrics\n * and the timestamps reported by the backup destination (e.g. S3).\n */\nexport const BACKUP_VERIFICATION_SLACK_MS = 60_000;\n\n/**\n * How long the actual backup state may remain *continuously* behind the\n * backup progress claimed by litestream metrics before the backup is\n * considered genuinely wedged and the process is shut down.\n *\n * A wedged backup is dangerous: litestream believes it is making durable\n * progress when it is not, which can silently corrupt the backup (the WAL\n * format only makes *some* such gaps detectable). Once that is confirmed to\n * be the persistent state, continuing to run is worse than crashing, so the\n * process exits loudly (non-zero, with a logged error) and the wedged backup\n * destination becomes the priority to investigate.\n *\n * The change-log is conservatively *not* purged for the entire time the\n * backup is stale, so the only cost of a generous grace period is unbounded\n * change-log growth. We therefore err well on the side of slack: the backup\n * must stay stuck across many {@link CHECK_INTERVAL_MS} checks before we tear\n * the server down, so that a transient hiccup (a slow or briefly unreachable\n * destination, or litestream restarting) does not trigger a shutdown. The\n * staleness clock is reset the moment a purge is confirmed against a real\n * upload.\n */\nexport const WEDGED_SHUTDOWN_GRACE_MS = 15 * 60_000; // 15 minutes\n\nconst GiB = 1024 ** 3;\n\n/**\n * How long the BackupMonitor waits for the *first* restorable backup to appear\n * before concluding that the backup pipeline is broken and shutting the process\n * down (see {@link BackupMonitor.#shutDownOnMissingInitialBackup}).\n *\n * On a fresh stack the first backup is not restorable until the initial\n * Postgres->replica sync completes and litestream finishes uploading the\n * initial snapshot — a multipart upload that is not listable in the destination\n * until it is committed. The producer cannot observe that sub-snapshot progress,\n * so this is necessarily a generous timeout. It is scaled by replica size to\n * mirror the platform's storage-scaled startup-probe allowance (~1 hour per\n * 50GB) so that it never fires before the platform would restart a view-syncer\n * that is waiting on the backup.\n */\nexport const INITIAL_BACKUP_GRACE_MS_PER_UNIT = 60 * 60_000; // 1 hour ...\nconst INITIAL_BACKUP_GRACE_UNIT_BYTES = 50 * GiB; // ... per 50 GiB\n\n/**\n * How often the BackupMonitor re-checks whether the first restorable backup has\n * appeared while a view-syncer holds a snapshot reservation open during a cold\n * start.\n */\nexport const RESTORABLE_BACKUP_POLL_INTERVAL_MS = 10_000;\n\n/**\n * Returns the time of the most recent object actually uploaded to the\n * backup replica destination (e.g. as determined by listing the snapshots\n * and WAL segments in S3). Rejects if the backup state cannot be determined.\n *\n * See `getLastBackupTime()` in `../litestream/commands.ts` for the\n * production implementation.\n */\nexport type BackupStateVerifier = () => Promise<Date>;\n\n/**\n * The Litestream3PrometheusPoller polls the litestream \"/metrics\" endpoint to\n * track the watermark (label) value of the `litestream_replica_progress` gauge\n * and emits verified watermarks to a stream of {@link BackedUpWatermark}s.\n *\n * See: https://github.com/rocicorp/litestream/pull/3\n *\n * Because the watermarks reported by litestream metrics reflect what\n * litestream *believes* has been backed up (which has been observed to\n * diverge from reality when uploads silently fail), the cleanup watermark is\n * only published after verifying it against the actual backup state in the\n * replica destination via a {@link BackupStateVerifier}. If the\n * actual backup state stays behind the claimed progress for longer than\n * {@link WEDGED_SHUTDOWN_GRACE_MS}, the backup is treated as wedged and the\n * process is shut down rather than risk corrupting the backup.\n */\nexport class Litestream3PrometheusPoller {\n readonly #lc: LogContext;\n readonly #replicaFile: string;\n readonly #backupURL: string;\n readonly #metricsEndpoint: string;\n readonly #state = new RunningState('litestream3-prometheus-poller');\n\n readonly #stream: Subscription<BackedUpWatermark>;\n readonly #watermarks = new Map<string, Date>();\n\n readonly #verifyBackupState: BackupStateVerifier;\n readonly #purgesBlocked = getOrCreateCounter('replica', 'purge_blocked', {\n description:\n 'Number of change-log purges blocked because the actual backup state ' +\n '(as listed from the replica destination) could not be verified, or ' +\n 'is older than the backup progress claimed by litestream metrics. ' +\n 'A steadily increasing value indicates a wedged or failing backup. ' +\n 'The \"backup-wedged\" reason is emitted once just before the process ' +\n 'shuts itself down due to a persistently stale backup.',\n });\n\n #lastWatermark: string = '';\n #latestBackupTime: Date | null = null;\n // The time of the most recent object actually verified in the backup\n // destination, or `null` if none has ever been verified. Only assigned from a\n // successful verifyBackupState() and never cleared, so a non-null value also\n // means a restorable backup has been confirmed to exist (which is what gates\n // the snapshot `status` signal and satisfies the initial-backup deadline).\n #lastVerifiedUploadTime: Date | null = null;\n // Epoch ms at which the actual backup state was first observed to be\n // continuously behind the watermark litestream claims to have backed up,\n // or `null` while the backup is keeping up. Reset whenever a purge is\n // confirmed against a real upload.\n #backupStaleSince: number | null = null;\n // Epoch ms at which run() started, used to bound how long to wait for the\n // first restorable backup to appear. `null` until run() is called.\n #runStartTime: number | null = null;\n #checkMetricsTimer: NodeJS.Timeout | undefined;\n\n constructor(\n lc: LogContext,\n replicaFile: string,\n backupURL: string,\n metricsEndpoint: string,\n verifyBackupState: BackupStateVerifier,\n ) {\n this.#lc = lc.withContext('component', 'litestream3-prometheus-poller');\n this.#replicaFile = replicaFile;\n this.#backupURL = backupURL;\n this.#metricsEndpoint = metricsEndpoint;\n this.#verifyBackupState = verifyBackupState;\n this.#stream = Subscription.create<BackedUpWatermark>({\n cleanup: () => {\n clearInterval(this.#checkMetricsTimer);\n this.#state.stop(this.#lc);\n },\n });\n }\n\n /**\n * Starts polling for watermarks and pushes new ones to returned stream.\n * Must only be called once.\n */\n start(): Source<BackedUpWatermark> {\n assert(this.#runStartTime === null, `Already called start()`);\n this.#runStartTime = Date.now();\n this.#lc.info?.(`monitoring backups at ${this.#metricsEndpoint}`);\n this.#checkMetricsTimer = setInterval(\n this.checkVerifyAndPublishWatermarks,\n CHECK_INTERVAL_MS,\n );\n return this.#stream;\n }\n\n /**\n * Verifies whether a restorable backup actually exists in the destination\n * (as opposed to what litestream metrics merely claim). On success the\n * verified upload time is cached — seeding the cleanup-verification fast path\n * ({@link #confirmedDurable}) — and the initial-backup deadline is satisfied.\n * Returns false if no backup is listable yet (e.g. the initial snapshot is\n * still uploading).\n */\n async #confirmRestorableBackup(): Promise<boolean> {\n try {\n this.#lastVerifiedUploadTime = await this.#verifyBackupStateTimed();\n return true;\n } catch (e) {\n this.#lc.info?.(`backup not yet restorable at ${this.#backupURL}`, e);\n return false;\n }\n }\n\n // Exported for testing\n readonly checkVerifyAndPublishWatermarks = async () => {\n try {\n await this.#checkWatermarks();\n } catch (e) {\n this.#lc.warn?.(`unable to fetch metrics at ${this.#metricsEndpoint}`, e);\n }\n try {\n await this.#publishVerifiedWatermarks();\n } catch (e) {\n this.#lc.warn?.(`error scheduling cleanup`, e);\n }\n try {\n await this.#checkInitialBackupDeadline();\n } catch (e) {\n this.#lc.warn?.(`error checking initial backup deadline`, e);\n }\n };\n\n async *#fetchWatermarks(): AsyncGenerator<{\n watermark: string;\n time: Date;\n name?: string | undefined;\n }> {\n const metricsEndpoint = this.#metricsEndpoint;\n const signal = this.#state.signal;\n let resp;\n try {\n resp = await fetch(metricsEndpoint, {signal});\n } catch (e) {\n if (signal.aborted) {\n // not an error.\n return;\n }\n // Treat exceptions from fetch (e.g. network errors) as non-fatal, and simply\n // log them and skip the watermark check until the next interval.\n this.#lc.warn?.(`unable to fetch metrics at ${this.#metricsEndpoint}`, e);\n return;\n }\n if (!resp.ok) {\n this.#lc.warn?.(\n `unable to fetch metrics at ${this.#metricsEndpoint}: ${await resp.text()}`,\n );\n return;\n }\n\n const families = parsePrometheusTextFormat(await resp.text());\n for (const family of families) {\n if (\n family.type === 'GAUGE' &&\n family.name === 'litestream_replica_progress'\n ) {\n for (const metric of family.metrics) {\n const watermark = metric.labels?.watermark;\n const name = metric.labels?.name;\n const time = new Date(parseFloat(metric.value) * 1000);\n if (watermark) {\n try {\n versionFromLexi(watermark); // Sanity check: Ensure that it is valid.\n yield {watermark, time, name};\n } catch (e) {\n this.#lc.warn?.(\n `ignoring invalid watermark ${watermark}`,\n {metric},\n e,\n );\n }\n }\n }\n }\n }\n }\n\n async #checkWatermarks() {\n for await (const {watermark, name, time} of this.#fetchWatermarks()) {\n if (watermark > this.#lastWatermark && !this.#watermarks.has(watermark)) {\n this.#lc.info?.(\n `replicated watermark=${watermark} to ${name}` +\n ` at ${time.toISOString()}.`,\n );\n this.#watermarks.set(watermark, time);\n this.#latestBackupTime = time;\n }\n }\n return this.#latestBackupTime;\n }\n\n async #publishVerifiedWatermarks() {\n const maxBackup = this.#maxBackupBefore(Date.now());\n if (!maxBackup) {\n return;\n }\n const {watermark: maxWatermark, backupTime: claimedTime} = maxBackup;\n // Purge guard: the watermarks (and their backup times) come from\n // litestream metrics, which are exported when litestream *believes*\n // an upload succeeded, and have been observed to advance even when\n // nothing was actually written to the backup destination. Purging the\n // change-log based on a falsely advancing watermark permanently breaks\n // the ability to restore + catch up. Before advancing the cleanup\n // watermark, verify it against the actual backup state: a claimed\n // backup time is only trusted if an object was actually uploaded to\n // the replica destination at (or after) that time, modulo clock skew.\n if (!this.#confirmedDurable(claimedTime)) {\n try {\n this.#lastVerifiedUploadTime = await this.#verifyBackupStateTimed();\n } catch (e) {\n this.#purgesBlocked.add(1, {reason: 'verification-failed'});\n // Skipping the purge is safe: the change-log just grows.\n this.#lc.warn?.(\n `unable to verify backup state. skipping change-log cleanup ` +\n `up to watermark ${maxWatermark} ` +\n `(claimed backup time ${claimedTime.toISOString()})`,\n e,\n );\n return;\n }\n }\n // Watermarks whose backup time isn't yet confirmed durable remain in the\n // map and are re-evaluated at the next check.\n const lastUpload = must(this.#lastVerifiedUploadTime);\n const verifiedBackup = this.#maxBackupBefore(\n lastUpload.getTime() + BACKUP_VERIFICATION_SLACK_MS,\n );\n if (!verifiedBackup) {\n this.#purgesBlocked.add(1, {reason: 'backup-stale'});\n const now = Date.now();\n if (this.#backupStaleSince === null) {\n this.#backupStaleSince = now;\n }\n const staleForMs = now - this.#backupStaleSince;\n this.#lc.warn?.(\n `blocked change-log cleanup up to watermark ${maxWatermark}: ` +\n `litestream claims it was backed up at ` +\n `${claimedTime.toISOString()}, but the last object actually ` +\n `uploaded to ${this.#backupURL} was at ` +\n `${lastUpload.toISOString()}. ` +\n `The backup has been stale for ${staleForMs} ms.`,\n );\n if (staleForMs >= WEDGED_SHUTDOWN_GRACE_MS) {\n this.#shutDownOnWedgedBackup(staleForMs, claimedTime, lastUpload);\n }\n return;\n }\n // The cleanup watermark advanced past a real upload, so the backup is\n // keeping up: clear the staleness clock.\n this.#backupStaleSince = null;\n const {watermark: verifiedWatermark, backupTime} = verifiedBackup;\n for (const watermark of this.#watermarks.keys()) {\n if (watermark <= verifiedWatermark) {\n this.#watermarks.delete(watermark);\n }\n }\n this.#stream.push({\n watermark: verifiedWatermark,\n backupTimeMs: backupTime.getTime(),\n });\n this.#lastWatermark = verifiedWatermark;\n }\n\n async #verifyBackupStateTimed(): Promise<Date> {\n const start = performance.now();\n let result: 'success' | 'error' = 'error';\n try {\n const ret = await this.#verifyBackupState();\n result = 'success';\n return ret;\n } finally {\n litestreamBackupVerificationDuration().recordMs(\n performance.now() - start,\n {\n ...litestreamMonitorMetricAttrs(\n this.#backupURL,\n 'legacy',\n 'replication_manager',\n ),\n result,\n },\n );\n }\n }\n\n /**\n * Called when the backup has been continuously stale for longer than\n * {@link WEDGED_SHUTDOWN_GRACE_MS}. Continuing to run risks corrupting the\n * backup, so the process is shut down by rejecting the {@link run()}\n * promise. The exit is non-zero and logged at ERROR for alerting; on\n * restart the monitor re-verifies, so a still-wedged backup keeps the\n * process down until the destination is fixed.\n */\n #shutDownOnWedgedBackup(\n staleForMs: number,\n claimedTime: Date,\n lastUpload: Date,\n ) {\n this.#purgesBlocked.add(1, {reason: 'backup-wedged'});\n const err = new UnrecoverableError(\n `backup at ${this.#backupURL} is wedged: litestream claims a backup ` +\n `at ${claimedTime.toISOString()}, but the last object actually ` +\n `uploaded was at ${lastUpload.toISOString()}, and the backup has ` +\n `not advanced for ${staleForMs} ms (grace period ` +\n `${WEDGED_SHUTDOWN_GRACE_MS} ms). Shutting down to avoid corrupting ` +\n `the backup; investigate the backup destination.`,\n );\n this.#lc.error?.(err.message);\n this.#stream.fail(err);\n }\n\n /**\n * On a fresh stack, fails loudly if no restorable backup has appeared within\n * a generous, replica-size-scaled grace period (see\n * {@link INITIAL_BACKUP_GRACE_MS_PER_UNIT}). This moves the \"give up\" decision\n * onto the replication-manager — the producer responsible for creating the\n * backup — instead of leaving view-syncers to wait (and the platform to keep\n * restarting them) while the real fault is here. Does nothing once a\n * restorable backup has been confirmed.\n */\n async #checkInitialBackupDeadline() {\n if (this.#lastVerifiedUploadTime !== null || this.#runStartTime === null) {\n return;\n }\n const elapsed = Date.now() - this.#runStartTime;\n if (elapsed < this.#initialBackupGraceMs()) {\n return;\n }\n // The deadline has elapsed without a confirmed backup. Do one definitive\n // check against the destination before giving up, in case a backup landed\n // but was never exercised by a reservation or a cleanup verification.\n if (await this.#confirmRestorableBackup()) {\n return;\n }\n this.#shutDownOnMissingInitialBackup(elapsed);\n }\n\n /**\n * The grace period for the first restorable backup, scaled by replica size to\n * mirror the platform's storage-scaled startup-probe allowance. Falls back to\n * the minimum (one unit) if the replica file size cannot be determined.\n */\n #initialBackupGraceMs(): number {\n let bytes = 0;\n try {\n bytes = statSync(this.#replicaFile).size;\n } catch {\n // Replica file not present/readable yet; fall back to the minimum grace.\n }\n const units = Math.max(\n 1,\n Math.ceil(bytes / INITIAL_BACKUP_GRACE_UNIT_BYTES),\n );\n return units * INITIAL_BACKUP_GRACE_MS_PER_UNIT;\n }\n\n /**\n * Called when no restorable backup has appeared within\n * {@link #initialBackupGraceMs}. Continuing to run is pointless — no\n * view-syncer can restore — and the fault is the backup pipeline, so the\n * process exits non-zero with a logged error for alerting, mirroring\n * {@link #shutDownOnWedgedBackup}.\n */\n #shutDownOnMissingInitialBackup(elapsedMs: number) {\n const err = new UnrecoverableError(\n `no restorable backup has appeared at ${this.#backupURL} within ` +\n `${elapsedMs} ms of startup (grace period ` +\n `${this.#initialBackupGraceMs()} ms). The initial backup pipeline ` +\n `appears to be broken; shutting down so that the replication-manager ` +\n `is flagged as the cause rather than the view-syncers waiting on it. ` +\n `Investigate litestream replication to ${this.#backupURL}.`,\n );\n this.#lc.error?.(err.message);\n this.#stream.fail(err);\n }\n\n /**\n * Returns the newest watermark whose backup time is at or before `cutoff`\n * (epoch ms), or `''` if there is none.\n */\n #maxBackupBefore(\n cutoff: number,\n ): {watermark: string; backupTime: Date} | null {\n let max = '';\n let maxBackupTime: Date | undefined;\n for (const [watermark, backupTime] of this.#watermarks) {\n if (backupTime.getTime() <= cutoff && watermark > max) {\n max = watermark;\n maxBackupTime = backupTime;\n }\n }\n if (!maxBackupTime) {\n return null;\n }\n return {watermark: max, backupTime: maxBackupTime};\n }\n\n /**\n * Returns `true` if the actual backup state, as last verified against the\n * backup destination, confirms that data claimed to be backed up at\n * `claimedTime` is durable (i.e. an object was actually uploaded at or\n * after `claimedTime`, allowing {@link BACKUP_VERIFICATION_SLACK_MS} of\n * clock skew).\n */\n #confirmedDurable(claimedTime: Date): boolean {\n const lastUpload = this.#lastVerifiedUploadTime;\n return (\n lastUpload !== null &&\n claimedTime.getTime() <=\n lastUpload.getTime() + BACKUP_VERIFICATION_SLACK_MS\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LogContext } from '@rocicorp/logger';
|
|
2
|
+
import { type JSONObject } from '../../../../shared/src/bigint-json.ts';
|
|
2
3
|
import { TransactionPool } from '../../db/transaction-pool.ts';
|
|
3
4
|
import { type PostgresDB } from '../../types/pg.ts';
|
|
4
5
|
import { type ShardID } from '../../types/shards.ts';
|
|
@@ -133,4 +134,37 @@ export declare class PurgeLocker {
|
|
|
133
134
|
constructor(lc: LogContext, shard: ShardID, db: PostgresDB);
|
|
134
135
|
acquire(): Promise<PurgeLock | null>;
|
|
135
136
|
}
|
|
137
|
+
type TaskKey = JSONObject;
|
|
138
|
+
type TaskDoneFn = () => void;
|
|
139
|
+
/**
|
|
140
|
+
* Periodic monitor for detecting the pathological condition in which a db
|
|
141
|
+
* query hangs forever because of a connection problem, such as a half-closed
|
|
142
|
+
* socket.
|
|
143
|
+
*
|
|
144
|
+
* Use this for db calls that recur or accumulate progress within a single
|
|
145
|
+
* logical operation on the main storer loop (e.g. the queue-processing loop,
|
|
146
|
+
* or a multi-batch catchup read), where "no progress for N ms" is the
|
|
147
|
+
* meaningful signal. For a one-off db call -- including ones made before
|
|
148
|
+
* {@link Storer.run} starts this monitor's polling -- use a plain timeout
|
|
149
|
+
* (see `Storer.#withTimeout`) instead.
|
|
150
|
+
*
|
|
151
|
+
* The failure handler should stop the server. The ProgressMonitor will
|
|
152
|
+
* assume this and stop itself after calling it.
|
|
153
|
+
*
|
|
154
|
+
* Internal to the Storer, but exported for testing.
|
|
155
|
+
*/
|
|
156
|
+
export declare class ProgressMonitor {
|
|
157
|
+
#private;
|
|
158
|
+
constructor(lc: LogContext, failureThresholdMs: number, onFailure: (err: Error) => void);
|
|
159
|
+
start(): void;
|
|
160
|
+
checkTaskProgress(now: Date): void;
|
|
161
|
+
/**
|
|
162
|
+
* Starts a task to be tracked.
|
|
163
|
+
*
|
|
164
|
+
* @return a callback to invoke when the task is done.
|
|
165
|
+
*/
|
|
166
|
+
trackTask(task: TaskKey, now?: Date): TaskDoneFn;
|
|
167
|
+
stop(): void;
|
|
168
|
+
}
|
|
169
|
+
export {};
|
|
136
170
|
//# sourceMappingURL=storer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/storer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"storer.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/storer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAKjD,OAAO,EAEL,KAAK,UAAU,EAChB,MAAM,uCAAuC,CAAC;AAM/C,OAAO,EAAC,eAAe,EAAC,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAC,KAAK,UAAU,EAA2B,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAY,KAAK,OAAO,EAAC,MAAM,uBAAuB,CAAC;AAE9D,OAAO,EAKL,KAAK,eAAe,EAMrB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,MAAM,EACZ,MAAM,iDAAiD,CAAC;AACzD,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,6CAA6C,CAAC;AACvF,OAAO,EAIL,KAAK,SAAS,EAEf,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,eAAe,CAAC;AAC3C,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAQ/B,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAsChD,MAAM,MAAM,aAAa,GAAG;IAC1B,+BAA+B,EAAE,MAAM,CAAC;IACxC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC,CAAC;AAaF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,MAAO,YAAW,OAAO;;IACpC,QAAQ,CAAC,EAAE,YAAY;gBAqBrB,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,iBAAiB,EAAE,MAAM,EACzB,EAAE,EAAE,UAAU,EACd,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,qBAAqB,KAAK,IAAI,EACxD,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,EAC7B,EACE,+BAA+B,EAC/B,kBAAkB,EAClB,kBAAkB,EAClB,cAAuB,GACxB,EAAE,aAAa;IA+DZ,eAAe,CAAC,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI;IA8BlD;;;;;;;;;;;;;;;;OAgBG;IACG,sCAAsC,IAAI,OAAO,CAAC;QACtD,aAAa,EAAE,MAAM,CAAC;QACtB,gBAAgB,EAAE,eAAe,EAAE,CAAC;QACpC,OAAO,EAAE,SAAS,CAAC;KACpB,CAAC;IAwDI,yBAAyB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IASzD;;;;OAIG;IACG,gBAAgB,IAAI,OAAO,CAAC;QAChC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC;IAWF;;;OAGG;IACG,oBAAoB,CACxB,cAAc,EAAE,MAAM,EACtB,gBAAgB,EAAE,MAAM,EACxB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,EAAE,CAAC;IAWpB;;;;;;OAMG;IACH,gBAAgB,CACd,cAAc,EAAE,MAAM,EACtB,gBAAgB,EAAE,MAAM,EACxB,SAAS,SAAO,GACf,aAAa,CAAC,cAAc,EAAE,CAAC;IAkBlC,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAyCtD;;OAEG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB;IAkB/C,KAAK;IAIL,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc;IAMpD,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS;IA2EzC;;;OAGG;IACG,GAAG;IA8fT;;;OAGG;IACG,YAAY;IAQlB;;;;OAIG;IACG,IAAI;CAeX;AAED,qBAAa,SAAS;;IAGpB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAG5B,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,eAAe,EACnB,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM;IAUb,OAAO;CAWd;AAED,qBAAa,WAAW;;gBAKV,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU;IAWpD,OAAO;CA4Bd;AAKD,KAAK,OAAO,GAAG,UAAU,CAAC;AAE1B,KAAK,UAAU,GAAG,MAAM,IAAI,CAAC;AAE7B;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,eAAe;;gBASxB,EAAE,EAAE,UAAU,EACd,kBAAkB,EAAE,MAAM,EAC1B,SAAS,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI;IAOjC,KAAK;IASL,iBAAiB,CAAC,GAAG,EAAE,IAAI;IAkB3B;;;;OAIG;IACH,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAa,GAAG,UAAU;IAOtD,IAAI;CAIL"}
|
|
@@ -2,6 +2,7 @@ import { assert, unreachable } from "../../../../shared/src/asserts.js";
|
|
|
2
2
|
import { promiseVoid } from "../../../../shared/src/resolved-promises.js";
|
|
3
3
|
import { AbortError } from "../../../../shared/src/abort-error.js";
|
|
4
4
|
import { parse, valita_exports } from "../../../../shared/src/valita.js";
|
|
5
|
+
import { BigIntJSON } from "../../../../shared/src/bigint-json.js";
|
|
5
6
|
import { cdcSchema } from "../../types/shards.js";
|
|
6
7
|
import { isDataChange, isSchemaChange } from "../change-source/protocol/current/data.js";
|
|
7
8
|
import "../change-source/protocol/current/downstream.js";
|
|
@@ -67,6 +68,7 @@ var Storer = class {
|
|
|
67
68
|
#statementTimeoutMs;
|
|
68
69
|
#changeLogBatchSize;
|
|
69
70
|
#drainTimeoutMs;
|
|
71
|
+
#progressMonitor;
|
|
70
72
|
#approximateQueuedBytes = 0;
|
|
71
73
|
#running = false;
|
|
72
74
|
constructor(lc, shard, taskID, discoveryAddress, discoveryProtocol, db, replicaVersion, onCommitted, onFatal, { backPressureLimitHeapProportion, statementTimeoutMs, changeLogBatchSize, drainTimeoutMs = 3e4 }) {
|
|
@@ -82,6 +84,7 @@ var Storer = class {
|
|
|
82
84
|
this.#statementTimeoutMs = statementTimeoutMs;
|
|
83
85
|
this.#changeLogBatchSize = Math.max(1, changeLogBatchSize);
|
|
84
86
|
this.#drainTimeoutMs = drainTimeoutMs;
|
|
87
|
+
this.#progressMonitor = new ProgressMonitor(lc, statementTimeoutMs, onFatal);
|
|
85
88
|
const heapStats = getHeapStatistics();
|
|
86
89
|
this.#backPressureThresholdBytes = (heapStats.heap_size_limit - heapStats.used_heap_size) * backPressureLimitHeapProportion;
|
|
87
90
|
this.#lc.info?.(`Using up to ${(this.#backPressureThresholdBytes / 1024 ** 2).toFixed(2)} MB of --max-old-space-size (~${(heapStats.heap_size_limit / 1024 ** 2).toFixed(2)} MB) to absorb upstream spikes`, { heapStats });
|
|
@@ -89,6 +92,28 @@ var Storer = class {
|
|
|
89
92
|
#cdc(table) {
|
|
90
93
|
return this.#db(`${cdcSchema(this.#shard)}.${table}`);
|
|
91
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Bounds a one-off db call (i.e. not part of the main storer loop or the
|
|
97
|
+
* background catchup read, which are tracked by the ProgressMonitor
|
|
98
|
+
* instead) with a plain timeout. This covers calls like
|
|
99
|
+
* {@link assumeOwnership} and {@link getStartStreamInitializationParameters}
|
|
100
|
+
* that are made by the caller *before* {@link run()} -- and thus before the
|
|
101
|
+
* ProgressMonitor's polling starts -- as well as ones made well after,
|
|
102
|
+
* where a continuously-polling watchdog would be overkill for a single
|
|
103
|
+
* bounded db round trip.
|
|
104
|
+
*
|
|
105
|
+
* Does not cancel `promise` on timeout (there is no cancellation mechanism
|
|
106
|
+
* for a postgres.js query); it just stops waiting for it, and swallows a
|
|
107
|
+
* later rejection so it doesn't surface as an unhandled rejection.
|
|
108
|
+
*/
|
|
109
|
+
async #withTimeout(name, promise) {
|
|
110
|
+
const result = await orTimeout(promise, this.#statementTimeoutMs);
|
|
111
|
+
if (result === "timed-out") {
|
|
112
|
+
promise.catch(() => {});
|
|
113
|
+
throw new AbortError(`${name} did not complete within ${this.#statementTimeoutMs}ms`);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
92
117
|
async assumeOwnership(purgeLock) {
|
|
93
118
|
const db = this.#db;
|
|
94
119
|
const owner = this.#taskID;
|
|
@@ -97,10 +122,10 @@ var Storer = class {
|
|
|
97
122
|
const addressWithProtocol = ownerProtocol === "ws" ? ownerAddress : `${ownerProtocol}://${ownerAddress}`;
|
|
98
123
|
this.#lc.info?.(`assuming ownership at ${addressWithProtocol}`);
|
|
99
124
|
const start = performance.now();
|
|
100
|
-
await db`UPDATE ${this.#cdc("replicationState")} SET ${db({
|
|
125
|
+
await this.#withTimeout("assume-ownership", db`UPDATE ${this.#cdc("replicationState")} SET ${db({
|
|
101
126
|
owner,
|
|
102
127
|
ownerAddress: addressWithProtocol
|
|
103
|
-
})}
|
|
128
|
+
})}`);
|
|
104
129
|
const elapsed = (performance.now() - start).toFixed(2);
|
|
105
130
|
this.#lc.info?.(`assumed ownership at ${addressWithProtocol} (${elapsed} ms)`);
|
|
106
131
|
if (purgeLock) purgeLock.release();
|
|
@@ -123,7 +148,7 @@ var Storer = class {
|
|
|
123
148
|
* snapshot that the SQLite change log continues folding onto.
|
|
124
149
|
*/
|
|
125
150
|
async getStartStreamInitializationParameters() {
|
|
126
|
-
const [[{ lastWatermark }], result, tableMetadata, backfilling] = await runTx(this.#db, (sql) => [
|
|
151
|
+
const [[{ lastWatermark }], result, tableMetadata, backfilling] = await this.#withTimeout("get-stream-params", runTx(this.#db, (sql) => [
|
|
127
152
|
sql`
|
|
128
153
|
SELECT "lastWatermark" FROM ${this.#cdc("replicationState")}`,
|
|
129
154
|
sql`
|
|
@@ -146,7 +171,7 @@ var Storer = class {
|
|
|
146
171
|
sql`
|
|
147
172
|
SELECT "schema", "table", "column", "backfill" FROM ${this.#cdc("backfilling")}
|
|
148
173
|
ORDER BY "schema" COLLATE "C", "table" COLLATE "C", "column" COLLATE "C"`
|
|
149
|
-
], { mode: READONLY });
|
|
174
|
+
], { mode: READONLY }));
|
|
150
175
|
return {
|
|
151
176
|
lastWatermark,
|
|
152
177
|
backfillRequests: parse(result, backfillRequestsSchema),
|
|
@@ -157,8 +182,8 @@ var Storer = class {
|
|
|
157
182
|
};
|
|
158
183
|
}
|
|
159
184
|
async getMinWatermarkForCatchup() {
|
|
160
|
-
const [{ minWatermark }] = await this.#db`
|
|
161
|
-
SELECT min(watermark) as "minWatermark" FROM ${this.#cdc("changeLog")}
|
|
185
|
+
const [{ minWatermark }] = await this.#withTimeout("get-min-watermark", this.#db`
|
|
186
|
+
SELECT min(watermark) as "minWatermark" FROM ${this.#cdc("changeLog")}`);
|
|
162
187
|
return minWatermark;
|
|
163
188
|
}
|
|
164
189
|
/**
|
|
@@ -167,10 +192,10 @@ var Storer = class {
|
|
|
167
192
|
* `lastWatermark` is the durable head.
|
|
168
193
|
*/
|
|
169
194
|
async getCatchupBounds() {
|
|
170
|
-
const [bounds] = await this.#db`
|
|
195
|
+
const [bounds] = await this.#withTimeout("get-catchup-bounds", this.#db`
|
|
171
196
|
SELECT
|
|
172
197
|
(SELECT min(watermark) FROM ${this.#cdc("changeLog")}) as "minWatermark",
|
|
173
|
-
(SELECT "lastWatermark" FROM ${this.#cdc("replicationState")}) as "lastWatermark"
|
|
198
|
+
(SELECT "lastWatermark" FROM ${this.#cdc("replicationState")}) as "lastWatermark"`);
|
|
174
199
|
return bounds;
|
|
175
200
|
}
|
|
176
201
|
/**
|
|
@@ -254,30 +279,22 @@ var Storer = class {
|
|
|
254
279
|
}]);
|
|
255
280
|
}
|
|
256
281
|
#readyForMore = null;
|
|
257
|
-
#backpressureTimeout;
|
|
258
282
|
readyForMore() {
|
|
259
283
|
if (!this.#running) return;
|
|
260
284
|
if (this.#readyForMore === null && this.#approximateQueuedBytes > this.#backPressureThresholdBytes) {
|
|
261
285
|
this.#lc.warn?.(`applying back pressure with ${this.#queue.size()} queued changes (~${(this.#approximateQueuedBytes / 1024 ** 2).toFixed(2)} MB)\n\nTo inspect changeLog backlog in your change DB:\n SELECT\n (change->'relation'->>'schema') || '.' || (change->'relation'->>'name') AS table_name,\n change->>'tag' AS operation,\n COUNT(*) AS count\n FROM "<app_id>/cdc"."changeLog"\n GROUP BY 1, 2\n ORDER BY 3 DESC\n LIMIT 20;`);
|
|
262
286
|
this.#readyForMore = resolver();
|
|
263
|
-
this.#resetBackpressureTimeout(true);
|
|
264
287
|
}
|
|
265
288
|
return this.#readyForMore?.promise;
|
|
266
289
|
}
|
|
267
|
-
#resetBackpressureTimeout(reschedule) {
|
|
268
|
-
clearTimeout(this.#backpressureTimeout);
|
|
269
|
-
this.#backpressureTimeout = reschedule ? setTimeout(() => {
|
|
270
|
-
this.#readyForMore?.reject(new AbortError(`No statement processed within the last ${this.#statementTimeoutMs}ms. Considering the change-log to be wedged.`));
|
|
271
|
-
this.#readyForMore = null;
|
|
272
|
-
}, this.#statementTimeoutMs) : void 0;
|
|
273
|
-
}
|
|
274
290
|
#maybeReleaseBackPressure() {
|
|
275
|
-
if (this.#readyForMore !== null)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
291
|
+
if (this.#readyForMore !== null) {
|
|
292
|
+
if (this.#approximateQueuedBytes < this.#backPressureThresholdBytes * .8) {
|
|
293
|
+
this.#lc.info?.(`releasing back pressure with ${this.#queue.size()} queued changes (~${(this.#approximateQueuedBytes / 1024 ** 2).toFixed(2)} MB)`);
|
|
294
|
+
this.#readyForMore.resolve();
|
|
295
|
+
this.#readyForMore = null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
281
298
|
}
|
|
282
299
|
/**
|
|
283
300
|
* Flushes any buffered {@link PendingTransaction.batch} rows to the changeLog.
|
|
@@ -316,6 +333,7 @@ var Storer = class {
|
|
|
316
333
|
this.#running = true;
|
|
317
334
|
this.#stopped = stopped;
|
|
318
335
|
this.#lc.info?.("starting storer");
|
|
336
|
+
this.#progressMonitor.start();
|
|
319
337
|
let err;
|
|
320
338
|
try {
|
|
321
339
|
await this.#processQueue();
|
|
@@ -327,7 +345,6 @@ var Storer = class {
|
|
|
327
345
|
this.#readyForMore.resolve();
|
|
328
346
|
this.#readyForMore = null;
|
|
329
347
|
}
|
|
330
|
-
this.#resetBackpressureTimeout(false);
|
|
331
348
|
this.#cancelQueueEntries(this.#queue.drain().filter((entry) => entry !== void 0), err);
|
|
332
349
|
this.#running = false;
|
|
333
350
|
signalStopped();
|
|
@@ -353,9 +370,20 @@ var Storer = class {
|
|
|
353
370
|
async #processQueue() {
|
|
354
371
|
let tx = null;
|
|
355
372
|
let msg;
|
|
373
|
+
let lastTaskDone;
|
|
374
|
+
const nextMessage = async () => {
|
|
375
|
+
lastTaskDone?.();
|
|
376
|
+
lastTaskDone = void 0;
|
|
377
|
+
const entry = await this.#queue.dequeue();
|
|
378
|
+
if (entry !== "stop") lastTaskDone = this.#progressMonitor.trackTask({
|
|
379
|
+
task: "queue-entry",
|
|
380
|
+
type: entry[0]
|
|
381
|
+
});
|
|
382
|
+
return entry;
|
|
383
|
+
};
|
|
356
384
|
const catchupQueue = [];
|
|
357
385
|
try {
|
|
358
|
-
while ((msg = await
|
|
386
|
+
while ((msg = await nextMessage()) !== "stop") {
|
|
359
387
|
const [msgType] = msg;
|
|
360
388
|
switch (msgType) {
|
|
361
389
|
case "ready": {
|
|
@@ -385,10 +413,7 @@ var Storer = class {
|
|
|
385
413
|
const { promise, resolve, reject } = resolver();
|
|
386
414
|
promise.catch(() => {});
|
|
387
415
|
tx = {
|
|
388
|
-
pool: new TransactionPool(this.#lc.withContext("watermark", watermark), {
|
|
389
|
-
mode: READ_COMMITTED,
|
|
390
|
-
statementResponseTimeout: this.#statementTimeoutMs
|
|
391
|
-
}),
|
|
416
|
+
pool: new TransactionPool(this.#lc.withContext("watermark", watermark), { mode: READ_COMMITTED }),
|
|
392
417
|
preCommitWatermark: watermark,
|
|
393
418
|
pos: 0,
|
|
394
419
|
startingReplicationState: promise,
|
|
@@ -460,6 +485,10 @@ var Storer = class {
|
|
|
460
485
|
const reader = new TransactionPool(lc, { mode: READONLY });
|
|
461
486
|
reader.run(this.#db);
|
|
462
487
|
let lastWatermark;
|
|
488
|
+
const catchupSnapshotted = this.#progressMonitor.trackTask({
|
|
489
|
+
task: "capture-catchup-snapshot",
|
|
490
|
+
subscribers: subs.map(({ subscriber: s }) => s.id)
|
|
491
|
+
});
|
|
463
492
|
try {
|
|
464
493
|
[{lastWatermark}] = await reader.processReadTask((sql) => sql`
|
|
465
494
|
SELECT "lastWatermark" FROM ${this.#cdc("replicationState")}
|
|
@@ -467,10 +496,18 @@ var Storer = class {
|
|
|
467
496
|
} catch (e) {
|
|
468
497
|
subs.map(({ subscriber }) => subscriber.fail(e));
|
|
469
498
|
throw e;
|
|
499
|
+
} finally {
|
|
500
|
+
catchupSnapshotted();
|
|
470
501
|
}
|
|
471
502
|
Promise.all(subs.map((sub) => this.#catchup(lc.withContext("subscriber", sub.subscriber.id), sub, lastWatermark, reader))).finally(() => reader.setDone());
|
|
472
503
|
}
|
|
473
504
|
async #catchup(lc, { subscriber: sub, mode }, lastWatermark, reader) {
|
|
505
|
+
let entriesReceived = 0;
|
|
506
|
+
let catchupProgressed = this.#progressMonitor.trackTask({
|
|
507
|
+
task: "catchup",
|
|
508
|
+
subscriber: sub.id,
|
|
509
|
+
entriesReceived
|
|
510
|
+
});
|
|
474
511
|
try {
|
|
475
512
|
lc.info?.(`starting catchup`);
|
|
476
513
|
await reader.processReadTask(async (tx) => {
|
|
@@ -484,21 +521,32 @@ var Storer = class {
|
|
|
484
521
|
WHERE watermark >= ${sub.watermark}
|
|
485
522
|
AND watermark <= ${lastWatermark}
|
|
486
523
|
ORDER BY watermark, pos`.cursor(2e3)) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
lastBatchConsumed
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
524
|
+
catchupProgressed();
|
|
525
|
+
entriesReceived += entries.length;
|
|
526
|
+
try {
|
|
527
|
+
const start = performance.now();
|
|
528
|
+
await lastBatchConsumed;
|
|
529
|
+
const elapsed = performance.now() - start;
|
|
530
|
+
if (lastBatchConsumed) lc[elapsed > 100 ? "info" : "debug"]?.(`waited ${elapsed.toFixed(3)} ms for ${sub.id} to consume last batch of catchup entries`);
|
|
531
|
+
for (const entry of entries) if (entry.watermark === sub.watermark) watermarkFound = true;
|
|
532
|
+
else if (watermarkFound) {
|
|
533
|
+
lastBatchConsumed = sub.catchup(reconstructWatermarkedChange(entry));
|
|
534
|
+
count++;
|
|
535
|
+
} else if (mode === "backup") throw new AutoResetSignal(`backup replica at watermark ${sub.watermark} is behind change db: ${entry.watermark})`);
|
|
536
|
+
else {
|
|
537
|
+
lc.warn?.(`rejecting subscriber at watermark ${sub.watermark} (earliest watermark: ${entry.watermark})`);
|
|
538
|
+
sub.close(2, `earliest supported watermark is ${entry.watermark} (requested ${sub.watermark})`);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
} finally {
|
|
542
|
+
catchupProgressed = this.#progressMonitor.trackTask({
|
|
543
|
+
task: "catchup",
|
|
544
|
+
subscriber: sub.id,
|
|
545
|
+
entriesReceived
|
|
546
|
+
});
|
|
500
547
|
}
|
|
501
548
|
}
|
|
549
|
+
catchupProgressed();
|
|
502
550
|
if (watermarkFound) {
|
|
503
551
|
await lastBatchConsumed;
|
|
504
552
|
lc.info?.(`caught up ${sub.id} with ${count} changes (${Date.now() - start} ms)`);
|
|
@@ -512,6 +560,8 @@ var Storer = class {
|
|
|
512
560
|
this.#onFatal(err);
|
|
513
561
|
}
|
|
514
562
|
sub.fail(err);
|
|
563
|
+
} finally {
|
|
564
|
+
catchupProgressed();
|
|
515
565
|
}
|
|
516
566
|
}
|
|
517
567
|
/**
|
|
@@ -609,6 +659,7 @@ var Storer = class {
|
|
|
609
659
|
*/
|
|
610
660
|
async stop() {
|
|
611
661
|
if (this.#running) {
|
|
662
|
+
this.#progressMonitor.stop();
|
|
612
663
|
this.#lc.info?.(`draining ${this.#queue.size()} changeLog entries`);
|
|
613
664
|
this.abort();
|
|
614
665
|
this.#queue.enqueue("stop");
|
|
@@ -669,6 +720,65 @@ var PurgeLocker = class {
|
|
|
669
720
|
return new PurgeLock(this.#lc, tx, replicaVersion, watermark);
|
|
670
721
|
}
|
|
671
722
|
};
|
|
723
|
+
/**
|
|
724
|
+
* Periodic monitor for detecting the pathological condition in which a db
|
|
725
|
+
* query hangs forever because of a connection problem, such as a half-closed
|
|
726
|
+
* socket.
|
|
727
|
+
*
|
|
728
|
+
* Use this for db calls that recur or accumulate progress within a single
|
|
729
|
+
* logical operation on the main storer loop (e.g. the queue-processing loop,
|
|
730
|
+
* or a multi-batch catchup read), where "no progress for N ms" is the
|
|
731
|
+
* meaningful signal. For a one-off db call -- including ones made before
|
|
732
|
+
* {@link Storer.run} starts this monitor's polling -- use a plain timeout
|
|
733
|
+
* (see `Storer.#withTimeout`) instead.
|
|
734
|
+
*
|
|
735
|
+
* The failure handler should stop the server. The ProgressMonitor will
|
|
736
|
+
* assume this and stop itself after calling it.
|
|
737
|
+
*
|
|
738
|
+
* Internal to the Storer, but exported for testing.
|
|
739
|
+
*/
|
|
740
|
+
var ProgressMonitor = class {
|
|
741
|
+
#lc;
|
|
742
|
+
#failureThresholdMs;
|
|
743
|
+
#onFailure;
|
|
744
|
+
#taskUpdates = /* @__PURE__ */ new Map();
|
|
745
|
+
#timer;
|
|
746
|
+
constructor(lc, failureThresholdMs, onFailure) {
|
|
747
|
+
this.#lc = lc.withContext("component", "storer-progress-monitor");
|
|
748
|
+
this.#failureThresholdMs = failureThresholdMs;
|
|
749
|
+
this.#onFailure = onFailure;
|
|
750
|
+
}
|
|
751
|
+
start() {
|
|
752
|
+
clearInterval(this.#timer);
|
|
753
|
+
this.#timer = setInterval(() => this.checkTaskProgress(/* @__PURE__ */ new Date()), this.#failureThresholdMs).unref();
|
|
754
|
+
}
|
|
755
|
+
checkTaskProgress(now) {
|
|
756
|
+
for (const [task, lastUpdate] of this.#taskUpdates.entries()) if (now.getTime() - lastUpdate.getTime() >= this.#failureThresholdMs) {
|
|
757
|
+
this.#lc.error?.(`Last task update was more than ${this.#failureThresholdMs}ms ago`, {
|
|
758
|
+
task,
|
|
759
|
+
lastUpdate
|
|
760
|
+
});
|
|
761
|
+
this.#onFailure(/* @__PURE__ */ new Error(`Task failed to progress for over ${this.#failureThresholdMs}ms: ${BigIntJSON.stringify(task)}`));
|
|
762
|
+
this.stop();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Starts a task to be tracked.
|
|
768
|
+
*
|
|
769
|
+
* @return a callback to invoke when the task is done.
|
|
770
|
+
*/
|
|
771
|
+
trackTask(task, now = /* @__PURE__ */ new Date()) {
|
|
772
|
+
this.#taskUpdates.set(task, now);
|
|
773
|
+
return () => {
|
|
774
|
+
this.#taskUpdates.delete(task);
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
stop() {
|
|
778
|
+
clearInterval(this.#timer);
|
|
779
|
+
this.#taskUpdates.clear();
|
|
780
|
+
}
|
|
781
|
+
};
|
|
672
782
|
//#endregion
|
|
673
783
|
export { PurgeLocker, Storer };
|
|
674
784
|
|