@rocicorp/zero 1.8.0-canary.0 → 1.8.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/README.md +1 -1
- package/out/replicache/src/kv/op-sqlite/store.js +3 -3
- package/out/replicache/src/kv/op-sqlite/store.js.map +1 -1
- package/out/replicache/src/kv/op-sqlite/types.d.ts +16 -2
- package/out/replicache/src/kv/op-sqlite/types.d.ts.map +1 -1
- package/out/replicache/src/kv/op-sqlite/types.js +5 -1
- package/out/replicache/src/kv/op-sqlite/types.js.map +1 -1
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/db/migration-lite.d.ts +6 -0
- package/out/zero-cache/src/db/migration-lite.d.ts.map +1 -1
- package/out/zero-cache/src/db/migration-lite.js +22 -2
- package/out/zero-cache/src/db/migration-lite.js.map +1 -1
- package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/server/syncer.js +2 -2
- package/out/zero-cache/src/server/syncer.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 +13 -6
- package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.d.ts +21 -0
- package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.js +115 -6
- package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +10 -7
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/litestream/vfs-watermark-reader.js +2 -2
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +4 -2
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +108 -19
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +3 -3
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/workers/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/workers/replicator.js +29 -26
- package/out/zero-cache/src/workers/replicator.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zqlite/src/db.js +2 -2
- package/out/zqlite/src/query-builder.js +9 -1
- package/out/zqlite/src/query-builder.js.map +1 -1
- package/out/zqlite/src/sqlite-cost-model.js +5 -5
- package/out/zqlite/src/table-source.js +3 -3
- package/package.json +1 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { promiseVoid } from "../../../../shared/src/resolved-promises.js";
|
|
2
|
+
import { sleep } from "../../../../shared/src/sleep.js";
|
|
2
3
|
import { must } from "../../../../shared/src/must.js";
|
|
3
4
|
import { Database } from "../../../../zqlite/src/db.js";
|
|
4
5
|
import { getOrCreateCounter, getOrCreateGauge } from "../../observability/metrics.js";
|
|
5
6
|
import { RunningState, UnrecoverableError } from "../running-state.js";
|
|
6
7
|
import { Subscription } from "../../types/subscription.js";
|
|
7
8
|
import { resolver } from "@rocicorp/resolver";
|
|
9
|
+
import { statSync } from "node:fs";
|
|
8
10
|
import parsePrometheusTextFormat from "parse-prometheus-text-format";
|
|
9
11
|
//#region ../zero-cache/src/services/change-streamer/litestream3-backup-monitor.ts
|
|
10
12
|
var CHECK_INTERVAL_MS = 6e4;
|
|
@@ -36,6 +38,29 @@ var BACKUP_VERIFICATION_SLACK_MS = 6e4;
|
|
|
36
38
|
* upload.
|
|
37
39
|
*/
|
|
38
40
|
var WEDGED_SHUTDOWN_GRACE_MS = 15 * 6e4;
|
|
41
|
+
var GiB = 1024 ** 3;
|
|
42
|
+
/**
|
|
43
|
+
* How long the BackupMonitor waits for the *first* restorable backup to appear
|
|
44
|
+
* before concluding that the backup pipeline is broken and shutting the process
|
|
45
|
+
* down (see {@link BackupMonitor.#shutDownOnMissingInitialBackup}).
|
|
46
|
+
*
|
|
47
|
+
* On a fresh stack the first backup is not restorable until the initial
|
|
48
|
+
* Postgres->replica sync completes and litestream finishes uploading the
|
|
49
|
+
* initial snapshot — a multipart upload that is not listable in the destination
|
|
50
|
+
* until it is committed. The producer cannot observe that sub-snapshot progress,
|
|
51
|
+
* so this is necessarily a generous timeout. It is scaled by replica size to
|
|
52
|
+
* mirror the platform's storage-scaled startup-probe allowance (~1 hour per
|
|
53
|
+
* 50GB) so that it never fires before the platform would restart a view-syncer
|
|
54
|
+
* that is waiting on the backup.
|
|
55
|
+
*/
|
|
56
|
+
var INITIAL_BACKUP_GRACE_MS_PER_UNIT = 60 * 6e4;
|
|
57
|
+
var INITIAL_BACKUP_GRACE_UNIT_BYTES = 50 * GiB;
|
|
58
|
+
/**
|
|
59
|
+
* How often the BackupMonitor re-checks whether the first restorable backup has
|
|
60
|
+
* appeared while a view-syncer holds a snapshot reservation open during a cold
|
|
61
|
+
* start.
|
|
62
|
+
*/
|
|
63
|
+
var RESTORABLE_BACKUP_POLL_INTERVAL_MS = 1e4;
|
|
39
64
|
/**
|
|
40
65
|
* The Litestream3BackupMonitor polls the litestream "/metrics" endpoint to
|
|
41
66
|
* track the watermark (label) value of the `litestream_replica_progress` gauge
|
|
@@ -93,6 +118,7 @@ var Litestream3BackupMonitor = class {
|
|
|
93
118
|
#latestBackupTime = null;
|
|
94
119
|
#lastVerifiedUploadTime = null;
|
|
95
120
|
#backupStaleSince = null;
|
|
121
|
+
#runStartTime = null;
|
|
96
122
|
#cleanupDelayMs;
|
|
97
123
|
#checkMetricsTimer;
|
|
98
124
|
constructor(lc, replicaFile, backupURL, metricsEndpoint, changeStreamer, initialCleanupDelayMs, verifyBackupState) {
|
|
@@ -106,6 +132,7 @@ var Litestream3BackupMonitor = class {
|
|
|
106
132
|
this.#lc.info?.(`backup monitor started ${initialCleanupDelayMs} ms after snapshot restore`);
|
|
107
133
|
}
|
|
108
134
|
run() {
|
|
135
|
+
this.#runStartTime = Date.now();
|
|
109
136
|
this.#lc.info?.(`monitoring backups at ${this.#metricsEndpoint} with ${this.#cleanupDelayMs} ms cleanup delay`);
|
|
110
137
|
this.#checkMetricsTimer = setInterval(this.checkWatermarksAndScheduleCleanup, CHECK_INTERVAL_MS);
|
|
111
138
|
this.#initBackupLagMetric();
|
|
@@ -119,17 +146,53 @@ var Litestream3BackupMonitor = class {
|
|
|
119
146
|
start: /* @__PURE__ */ new Date(),
|
|
120
147
|
sub
|
|
121
148
|
});
|
|
122
|
-
this.#
|
|
123
|
-
|
|
149
|
+
this.#pushStatusWhenRestorable(taskID, sub);
|
|
150
|
+
return sub;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Pushes the `status` snapshot signal once a restorable backup is confirmed
|
|
154
|
+
* to exist in the backup destination. On a warm start this is immediate; on a
|
|
155
|
+
* cold start (a fresh stack whose first backup is still being produced) the
|
|
156
|
+
* reservation is held open — kept alive by websocket liveness pings — and the
|
|
157
|
+
* view-syncer stays unready until the first backup lands, rather than being
|
|
158
|
+
* told to restore a backup that does not yet exist.
|
|
159
|
+
*/
|
|
160
|
+
async #pushStatusWhenRestorable(taskID, sub) {
|
|
161
|
+
try {
|
|
162
|
+
while (this.#lastVerifiedUploadTime === null) {
|
|
163
|
+
if (!sub.active) return;
|
|
164
|
+
if (await this.#confirmRestorableBackup()) break;
|
|
165
|
+
this.#lc.info?.(`no restorable backup at ${this.#backupURL} yet; holding ${taskID}'s reservation open until it lands`);
|
|
166
|
+
await sleep(RESTORABLE_BACKUP_POLL_INTERVAL_MS, this.#state.signal);
|
|
167
|
+
}
|
|
168
|
+
const changeLogState = await this.#changeStreamer.getChangeLogState();
|
|
169
|
+
if (sub.active) sub.push(["status", {
|
|
124
170
|
tag: "status",
|
|
125
171
|
backupURL: this.#backupURL,
|
|
126
172
|
...changeLogState
|
|
127
173
|
}]);
|
|
128
|
-
}
|
|
174
|
+
} catch (e) {
|
|
175
|
+
if (this.#state.signal.aborted) return;
|
|
129
176
|
this.#lc.warn?.(`failing snapshot reservation`, e);
|
|
130
|
-
sub.fail(e);
|
|
131
|
-
}
|
|
132
|
-
|
|
177
|
+
sub.fail(e instanceof Error ? e : new Error(String(e)));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Verifies whether a restorable backup actually exists in the destination
|
|
182
|
+
* (as opposed to what litestream metrics merely claim). On success the
|
|
183
|
+
* verified upload time is cached — seeding the cleanup-verification fast path
|
|
184
|
+
* ({@link #confirmedDurable}) — and the initial-backup deadline is satisfied.
|
|
185
|
+
* Returns false if no backup is listable yet (e.g. the initial snapshot is
|
|
186
|
+
* still uploading).
|
|
187
|
+
*/
|
|
188
|
+
async #confirmRestorableBackup() {
|
|
189
|
+
try {
|
|
190
|
+
this.#lastVerifiedUploadTime = await this.#verifyBackupState();
|
|
191
|
+
return true;
|
|
192
|
+
} catch (e) {
|
|
193
|
+
this.#lc.info?.(`backup not yet restorable at ${this.#backupURL}`, e);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
133
196
|
}
|
|
134
197
|
endReservation(taskID, updateCleanupDelay = true) {
|
|
135
198
|
const res = this.#reservations.get(taskID);
|
|
@@ -157,6 +220,11 @@ var Litestream3BackupMonitor = class {
|
|
|
157
220
|
} catch (e) {
|
|
158
221
|
this.#lc.warn?.(`error scheduling cleanup`, e);
|
|
159
222
|
}
|
|
223
|
+
try {
|
|
224
|
+
await this.#checkInitialBackupDeadline();
|
|
225
|
+
} catch (e) {
|
|
226
|
+
this.#lc.warn?.(`error checking initial backup deadline`, e);
|
|
227
|
+
}
|
|
160
228
|
};
|
|
161
229
|
async *#fetchWatermarks() {
|
|
162
230
|
const metricsEndpoint = this.#metricsEndpoint;
|
|
@@ -241,6 +309,47 @@ var Litestream3BackupMonitor = class {
|
|
|
241
309
|
this.#fatal.reject(err);
|
|
242
310
|
}
|
|
243
311
|
/**
|
|
312
|
+
* On a fresh stack, fails loudly if no restorable backup has appeared within
|
|
313
|
+
* a generous, replica-size-scaled grace period (see
|
|
314
|
+
* {@link INITIAL_BACKUP_GRACE_MS_PER_UNIT}). This moves the "give up" decision
|
|
315
|
+
* onto the replication-manager — the producer responsible for creating the
|
|
316
|
+
* backup — instead of leaving view-syncers to wait (and the platform to keep
|
|
317
|
+
* restarting them) while the real fault is here. Does nothing once a
|
|
318
|
+
* restorable backup has been confirmed.
|
|
319
|
+
*/
|
|
320
|
+
async #checkInitialBackupDeadline() {
|
|
321
|
+
if (this.#lastVerifiedUploadTime !== null || this.#runStartTime === null) return;
|
|
322
|
+
const elapsed = Date.now() - this.#runStartTime;
|
|
323
|
+
if (elapsed < this.#initialBackupGraceMs()) return;
|
|
324
|
+
if (await this.#confirmRestorableBackup()) return;
|
|
325
|
+
this.#shutDownOnMissingInitialBackup(elapsed);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The grace period for the first restorable backup, scaled by replica size to
|
|
329
|
+
* mirror the platform's storage-scaled startup-probe allowance. Falls back to
|
|
330
|
+
* the minimum (one unit) if the replica file size cannot be determined.
|
|
331
|
+
*/
|
|
332
|
+
#initialBackupGraceMs() {
|
|
333
|
+
let bytes = 0;
|
|
334
|
+
try {
|
|
335
|
+
bytes = statSync(this.#replicaFile).size;
|
|
336
|
+
} catch {}
|
|
337
|
+
return Math.max(1, Math.ceil(bytes / INITIAL_BACKUP_GRACE_UNIT_BYTES)) * INITIAL_BACKUP_GRACE_MS_PER_UNIT;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Called when no restorable backup has appeared within
|
|
341
|
+
* {@link #initialBackupGraceMs}. Continuing to run is pointless — no
|
|
342
|
+
* view-syncer can restore — and the fault is the backup pipeline, so the
|
|
343
|
+
* process exits non-zero with a logged error for alerting, mirroring
|
|
344
|
+
* {@link #shutDownOnWedgedBackup}.
|
|
345
|
+
*/
|
|
346
|
+
#shutDownOnMissingInitialBackup(elapsedMs) {
|
|
347
|
+
const err = new UnrecoverableError(`no restorable backup has appeared at ${this.#backupURL} within ${elapsedMs} ms of startup (grace period ${this.#initialBackupGraceMs()} ms). The initial backup pipeline appears to be broken; shutting down so that the replication-manager is flagged as the cause rather than the view-syncers waiting on it. Investigate litestream replication to ${this.#backupURL}.`);
|
|
348
|
+
this.#lc.error?.(err.message);
|
|
349
|
+
clearInterval(this.#checkMetricsTimer);
|
|
350
|
+
this.#fatal.reject(err);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
244
353
|
* Returns the newest watermark whose backup time is at or before `cutoff`
|
|
245
354
|
* (epoch ms), or `''` if there is none.
|
|
246
355
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"litestream3-backup-monitor.js","names":["#lc","#replicaFile","#backupURL","#metricsEndpoint","#changeStreamer","#state","#reservations","#watermarks","#verifyBackupState","#purgesBlocked","#fatal","#cleanupDelayMs","#checkMetricsTimer","#initBackupLagMetric","#checkWatermarks","#scheduleCleanup","#fetchWatermarks","#lastWatermark","#latestBackupTime","#maxWatermarkUpTo","#confirmedDurable","#lastVerifiedUploadTime","#backupStaleSince","#shutDownOnWedgedBackup"],"sources":["../../../../../../zero-cache/src/services/change-streamer/litestream3-backup-monitor.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport parsePrometheusTextFormat from 'parse-prometheus-text-format';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {\n getOrCreateCounter,\n getOrCreateGauge,\n} from '../../observability/metrics.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {RunningState, UnrecoverableError} from '../running-state.ts';\nimport type {BackupMonitor} from './backup-monitor.ts';\nimport type {ChangeStreamerService} from './change-streamer.ts';\nimport type {SnapshotMessage} from './snapshot.ts';\n\nexport const CHECK_INTERVAL_MS = 60_000;\nconst MIN_CLEANUP_DELAY_MS = 30_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\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\ntype Reservation = {\n start: Date;\n sub: Subscription<SnapshotMessage>;\n};\n\n/**\n * The Litestream3BackupMonitor polls the litestream \"/metrics\" endpoint to\n * track the watermark (label) value of the `litestream_replica_progress` gauge\n * and schedules cleanup of change log entries that can be purged as a result.\n *\n * See: https://github.com/rocicorp/litestream/pull/3\n *\n * Note that change log entries cannot simply be purged as soon as they\n * have been applied and backed up by litestream. Consider the case in which\n * litestream backs up new wal segments every minute, but it takes 5 minutes\n * to restore a replica: if a zero-cache starts restoring a replica at\n * minute 0, and new watermarks are replicated at minutes 1, 2, 3, 4, and 5,\n * purging changelog records as soon as those watermarks are replicated would\n * result in the zero-cache not being able to catch up from minute 0 once it\n * has finished restoring the replica.\n *\n * The `/snapshot` reservation protocol is used to prevent premature change\n * log cleanup:\n * - Clients restoring a snapshot initiate a `/snapshot` request and hold that\n * request open while it restores its snapshot, prepares it, and\n * starts its subscription to the change stream. During this time, no\n * cleanups are scheduled.\n * - When the subscription is started, the interval since the beginning of\n * of the reservation is tracked to increase the background cleanup delay\n * interval if needed. The reservation is ended (and request closed), and\n * cleanup scheduling is resumed with the current delay interval.\n *\n * Note that the reservation request is the primary mechanism by which\n * premature change log cleanup is prevented. The cleanup delay interval is\n * a secondary safeguard.\n *\n * Additionally, because the watermarks reported by litestream metrics\n * reflect what litestream *believes* has been backed up (which has been\n * observed to diverge from reality when uploads silently fail), the cleanup\n * watermark is only advanced after verifying it against the actual backup\n * state in the 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 Litestream3BackupMonitor implements BackupMonitor {\n readonly id = 'backup-monitor';\n readonly #lc: LogContext;\n readonly #replicaFile: string;\n readonly #backupURL: string;\n readonly #metricsEndpoint: string;\n readonly #changeStreamer: ChangeStreamerService;\n readonly #state = new RunningState(this.id);\n\n readonly #reservations = new Map<string, Reservation>();\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 // Rejected when the backup is determined to be genuinely wedged, which\n // surfaces as a rejection of the `run()` promise and shuts the process down.\n readonly #fatal = resolver<void>();\n\n #lastWatermark: string = '';\n #latestBackupTime: Date | null = null;\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 #cleanupDelayMs: number;\n #checkMetricsTimer: NodeJS.Timeout | undefined;\n\n constructor(\n lc: LogContext,\n replicaFile: string,\n backupURL: string,\n metricsEndpoint: string,\n changeStreamer: ChangeStreamerService,\n initialCleanupDelayMs: number,\n verifyBackupState: BackupStateVerifier,\n ) {\n this.#lc = lc.withContext('component', this.id);\n this.#replicaFile = replicaFile;\n this.#backupURL = backupURL;\n this.#metricsEndpoint = metricsEndpoint;\n this.#changeStreamer = changeStreamer;\n this.#verifyBackupState = verifyBackupState;\n this.#cleanupDelayMs = Math.max(\n initialCleanupDelayMs,\n MIN_CLEANUP_DELAY_MS, // purely for peace of mind\n );\n\n this.#lc.info?.(\n `backup monitor started ${initialCleanupDelayMs} ms after snapshot restore`,\n );\n }\n\n run(): Promise<void> {\n this.#lc.info?.(\n `monitoring backups at ${this.#metricsEndpoint} with ` +\n `${this.#cleanupDelayMs} ms cleanup delay`,\n );\n this.#checkMetricsTimer = setInterval(\n this.checkWatermarksAndScheduleCleanup,\n CHECK_INTERVAL_MS,\n );\n this.#initBackupLagMetric();\n // Resolves on a normal stop; rejects if the backup is found to be wedged.\n return Promise.race([this.#state.stopped(), this.#fatal.promise]);\n }\n\n startSnapshotReservation(taskID: string): Subscription<SnapshotMessage> {\n this.#lc.info?.(`pausing change-log cleanup while ${taskID} snapshots`);\n // In the case of retries, only track the last reservation.\n this.#reservations.get(taskID)?.sub.cancel();\n\n const sub = Subscription.create<SnapshotMessage>({\n // If the reservation still exists when the connection closes\n // (e.g. subscriber crashed), clean it up without updating the\n // cleanup delay.\n cleanup: () => this.endReservation(taskID, false),\n });\n this.#reservations.set(taskID, {start: new Date(), sub});\n // Note: the Subscription must be returned immediately so that the\n // websocket can begin sending liveness pings.\n void this.#changeStreamer\n .getChangeLogState()\n .then(changeLogState => {\n sub.push([\n 'status',\n {tag: 'status', backupURL: this.#backupURL, ...changeLogState},\n ]);\n })\n .catch(e => {\n this.#lc.warn?.(`failing snapshot reservation`, e);\n sub.fail(e);\n });\n return sub;\n }\n\n endReservation(taskID: string, updateCleanupDelay = true) {\n const res = this.#reservations.get(taskID);\n if (res === undefined) {\n return;\n }\n this.#reservations.delete(taskID);\n const {start, sub} = res;\n sub.cancel(); // closes the connection if still open\n\n if (updateCleanupDelay) {\n const duration = Date.now() - start.getTime();\n this.#lc.info?.(`snapshot initialized by ${taskID} in ${duration} ms`);\n if (duration > this.#cleanupDelayMs) {\n this.#cleanupDelayMs = duration;\n this.#lc.info?.(`increased cleanup delay to ${duration} ms`);\n }\n }\n }\n\n // Exported for testing\n readonly checkWatermarksAndScheduleCleanup = 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.#scheduleCleanup();\n } catch (e) {\n this.#lc.warn?.(`error scheduling cleanup`, 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 #scheduleCleanup() {\n if (this.#reservations.size > 0) {\n this.#lc.info?.(\n `watermark cleanup paused for snapshot(s): ${[...this.#reservations.keys()]}`,\n );\n return;\n }\n const latestCleanupTime = Date.now() - this.#cleanupDelayMs;\n const maxWatermark = this.#maxWatermarkUpTo(latestCleanupTime);\n if (maxWatermark.length === 0) {\n return;\n }\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 const claimedTime = must(this.#watermarks.get(maxWatermark));\n if (!this.#confirmedDurable(claimedTime)) {\n try {\n this.#lastVerifiedUploadTime = await this.#verifyBackupState();\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 verifiedWatermark = this.#maxWatermarkUpTo(\n Math.min(\n latestCleanupTime,\n lastUpload.getTime() + BACKUP_VERIFICATION_SLACK_MS,\n ),\n );\n if (verifiedWatermark.length === 0) {\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 this.#changeStreamer.scheduleCleanup(verifiedWatermark);\n for (const watermark of this.#watermarks.keys()) {\n if (watermark <= verifiedWatermark) {\n this.#watermarks.delete(watermark);\n }\n }\n this.#lastWatermark = verifiedWatermark;\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 clearInterval(this.#checkMetricsTimer);\n this.#fatal.reject(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 #maxWatermarkUpTo(cutoff: number): string {\n let max = '';\n for (const [watermark, backupTime] of this.#watermarks) {\n if (backupTime.getTime() <= cutoff && watermark > max) {\n max = watermark;\n }\n }\n return max;\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 stop(): Promise<void> {\n clearInterval(this.#checkMetricsTimer);\n for (const {sub} of this.#reservations.values()) {\n // Close any pending reservations. This commonly happens when a new\n // replication-manager makes a `/snapshot` reservation on the existing\n // replication-manager, and then shuts it down when it takes over the\n // replication slot.\n sub.cancel();\n }\n this.#state.stop(this.#lc);\n return promiseVoid;\n }\n\n #initBackupLagMetric() {\n getOrCreateGauge('replica', 'backup_lag', {\n description:\n 'Latency from when a change is written to the replica ' +\n 'to when it is backed up to litestream. It is expected to create a saw ' +\n 'pattern from 0 to the configured ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES.',\n unit: 'millisecond',\n }).addCallback(async o => {\n // For legacy litestream, we use the watermark metric (and its associated\n // backup time) exported by litestream metrics to determine the time of\n // of the backed up watermark. This is technically imprecise--it would be\n // more correct to use the committed writeTimeMs--but it is good enough\n // in that it serves the purpose of detecting a non-functioning backup.\n // With litestream v5, this can be made more precise by querying the\n // _zero.replicationState row from the backup directly using an LTX-based\n // database reader.\n const latestBackup = await this.#checkWatermarks();\n if (!latestBackup) {\n this.#lc.warn?.(\n `no backed up watermarks. unable to report replica.backup_lag`,\n );\n return;\n }\n const db = new Database(this.#lc, this.#replicaFile, {readonly: true});\n try {\n const {writeTimeMs} = db\n .prepare(/*sql*/ `SELECT writeTimeMs FROM \"_zero.replicationState\"`)\n .get<{writeTimeMs: number}>();\n const backupLag = Math.max(0, writeTimeMs - latestBackup.getTime());\n o.observe(backupLag);\n } catch (e) {\n this.#lc.warn?.(`error measuring replica.backup_lag metric`, e);\n } finally {\n db.close();\n }\n });\n }\n}\n"],"mappings":";;;;;;;;;AAgBA,IAAa,oBAAoB;AACjC,IAAM,uBAAuB;;;;;AAM7B,IAAa,+BAA+B;;;;;;;;;;;;;;;;;;;;;;AAuB5C,IAAa,2BAA2B,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyD7C,IAAa,2BAAb,MAA+D;CAC7D,KAAc;CACd;CACA;CACA;CACA;CACA;CACA,SAAkB,IAAI,aAAa,KAAK,EAAE;CAE1C,gCAAyB,IAAI,IAAyB;CACtD,8BAAuB,IAAI,IAAkB;CAE7C;CACA,iBAA0B,mBAAmB,WAAW,iBAAiB,EACvE,aACE,uYAMJ,CAAC;CAID,SAAkB,SAAe;CAEjC,iBAAyB;CACzB,oBAAiC;CACjC,0BAAuC;CAKvC,oBAAmC;CACnC;CACA;CAEA,YACE,IACA,aACA,WACA,iBACA,gBACA,uBACA,mBACA;EACA,KAAKA,MAAM,GAAG,YAAY,aAAa,KAAK,EAAE;EAC9C,KAAKC,eAAe;EACpB,KAAKC,aAAa;EAClB,KAAKC,mBAAmB;EACxB,KAAKC,kBAAkB;EACvB,KAAKI,qBAAqB;EAC1B,KAAKG,kBAAkB,KAAK,IAC1B,uBACA,oBACF;EAEA,KAAKX,IAAI,OACP,0BAA0B,sBAAsB,2BAClD;CACF;CAEA,MAAqB;EACnB,KAAKA,IAAI,OACP,yBAAyB,KAAKG,iBAAiB,QAC1C,KAAKQ,gBAAgB,kBAC5B;EACA,KAAKC,qBAAqB,YACxB,KAAK,mCACL,iBACF;EACA,KAAKC,qBAAqB;EAE1B,OAAO,QAAQ,KAAK,CAAC,KAAKR,OAAO,QAAQ,GAAG,KAAKK,OAAO,OAAO,CAAC;CAClE;CAEA,yBAAyB,QAA+C;EACtE,KAAKV,IAAI,OAAO,oCAAoC,OAAO,WAAW;EAEtE,KAAKM,cAAc,IAAI,MAAM,GAAG,IAAI,OAAO;EAE3C,MAAM,MAAM,aAAa,OAAwB,EAI/C,eAAe,KAAK,eAAe,QAAQ,KAAK,EAClD,CAAC;EACD,KAAKA,cAAc,IAAI,QAAQ;GAAC,uBAAO,IAAI,KAAK;GAAG;EAAG,CAAC;EAGvD,KAAUF,gBACP,kBAAkB,EAClB,MAAK,mBAAkB;GACtB,IAAI,KAAK,CACP,UACA;IAAC,KAAK;IAAU,WAAW,KAAKF;IAAY,GAAG;GAAc,CAC/D,CAAC;EACH,CAAC,EACA,OAAM,MAAK;GACV,KAAKF,IAAI,OAAO,gCAAgC,CAAC;GACjD,IAAI,KAAK,CAAC;EACZ,CAAC;EACH,OAAO;CACT;CAEA,eAAe,QAAgB,qBAAqB,MAAM;EACxD,MAAM,MAAM,KAAKM,cAAc,IAAI,MAAM;EACzC,IAAI,QAAQ,KAAA,GACV;EAEF,KAAKA,cAAc,OAAO,MAAM;EAChC,MAAM,EAAC,OAAO,QAAO;EACrB,IAAI,OAAO;EAEX,IAAI,oBAAoB;GACtB,MAAM,WAAW,KAAK,IAAI,IAAI,MAAM,QAAQ;GAC5C,KAAKN,IAAI,OAAO,2BAA2B,OAAO,MAAM,SAAS,IAAI;GACrE,IAAI,WAAW,KAAKW,iBAAiB;IACnC,KAAKA,kBAAkB;IACvB,KAAKX,IAAI,OAAO,8BAA8B,SAAS,IAAI;GAC7D;EACF;CACF;CAGA,oCAA6C,YAAY;EACvD,IAAI;GACF,MAAM,KAAKc,iBAAiB;EAC9B,SAAS,GAAG;GACV,KAAKd,IAAI,OAAO,8BAA8B,KAAKG,oBAAoB,CAAC;EAC1E;EACA,IAAI;GACF,MAAM,KAAKY,iBAAiB;EAC9B,SAAS,GAAG;GACV,KAAKf,IAAI,OAAO,4BAA4B,CAAC;EAC/C;CACF;CAEA,OAAOgB,mBAIJ;EACD,MAAM,kBAAkB,KAAKb;EAC7B,MAAM,SAAS,KAAKE,OAAO;EAC3B,IAAI;EACJ,IAAI;GACF,OAAO,MAAM,MAAM,iBAAiB,EAAC,OAAM,CAAC;EAC9C,SAAS,GAAG;GACV,IAAI,OAAO,SAET;GAIF,KAAKL,IAAI,OAAO,8BAA8B,KAAKG,oBAAoB,CAAC;GACxE;EACF;EACA,IAAI,CAAC,KAAK,IAAI;GACZ,KAAKH,IAAI,OACP,8BAA8B,KAAKG,iBAAiB,IAAI,MAAM,KAAK,KAAK,GAC1E;GACA;EACF;EAEA,MAAM,WAAW,0BAA0B,MAAM,KAAK,KAAK,CAAC;EAC5D,KAAK,MAAM,UAAU,UACnB,IACE,OAAO,SAAS,WAChB,OAAO,SAAS,+BAEhB,KAAK,MAAM,UAAU,OAAO,SAAS;GACnC,MAAM,YAAY,OAAO,QAAQ;GACjC,MAAM,OAAO,OAAO,QAAQ;GAC5B,MAAM,uBAAO,IAAI,KAAK,WAAW,OAAO,KAAK,IAAI,GAAI;GAErD,IAAI,WACF,MAAM;IAAC;IAAW;IAAM;GAAI;EAEhC;CAGN;CAEA,MAAMW,mBAAmB;EACvB,WAAW,MAAM,EAAC,WAAW,MAAM,UAAS,KAAKE,iBAAiB,GAChE,IAAI,YAAY,KAAKC,kBAAkB,CAAC,KAAKV,YAAY,IAAI,SAAS,GAAG;GACvE,KAAKP,IAAI,OACP,wBAAwB,UAAU,MAAM,KAAA,MAC/B,KAAK,YAAY,EAAE,EAC9B;GACA,KAAKO,YAAY,IAAI,WAAW,IAAI;GACpC,KAAKW,oBAAoB;EAC3B;EAEF,OAAO,KAAKA;CACd;CAEA,MAAMH,mBAAmB;EACvB,IAAI,KAAKT,cAAc,OAAO,GAAG;GAC/B,KAAKN,IAAI,OACP,6CAA6C,CAAC,GAAG,KAAKM,cAAc,KAAK,CAAC,GAC5E;GACA;EACF;EACA,MAAM,oBAAoB,KAAK,IAAI,IAAI,KAAKK;EAC5C,MAAM,eAAe,KAAKQ,kBAAkB,iBAAiB;EAC7D,IAAI,aAAa,WAAW,GAC1B;EAWF,MAAM,cAAc,KAAK,KAAKZ,YAAY,IAAI,YAAY,CAAC;EAC3D,IAAI,CAAC,KAAKa,kBAAkB,WAAW,GACrC,IAAI;GACF,KAAKC,0BAA0B,MAAM,KAAKb,mBAAmB;EAC/D,SAAS,GAAG;GACV,KAAKC,eAAe,IAAI,GAAG,EAAC,QAAQ,sBAAqB,CAAC;GAE1D,KAAKT,IAAI,OACP,8EACqB,aAAa,wBACR,YAAY,YAAY,EAAE,IACpD,CACF;GACA;EACF;EAIF,MAAM,aAAa,KAAK,KAAKqB,uBAAuB;EACpD,MAAM,oBAAoB,KAAKF,kBAC7B,KAAK,IACH,mBACA,WAAW,QAAQ,IAAI,4BACzB,CACF;EACA,IAAI,kBAAkB,WAAW,GAAG;GAClC,KAAKV,eAAe,IAAI,GAAG,EAAC,QAAQ,eAAc,CAAC;GACnD,MAAM,MAAM,KAAK,IAAI;GACrB,IAAI,KAAKa,sBAAsB,MAC7B,KAAKA,oBAAoB;GAE3B,MAAM,aAAa,MAAM,KAAKA;GAC9B,KAAKtB,IAAI,OACP,8CAA8C,aAAa,0CAEtD,YAAY,YAAY,EAAE,6CACd,KAAKE,WAAW,UAC5B,WAAW,YAAY,EAAE,kCACK,WAAW,KAChD;GACA,IAAI,cAAA,KACF,KAAKqB,wBAAwB,YAAY,aAAa,UAAU;GAElE;EACF;EAGA,KAAKD,oBAAoB;EACzB,KAAKlB,gBAAgB,gBAAgB,iBAAiB;EACtD,KAAK,MAAM,aAAa,KAAKG,YAAY,KAAK,GAC5C,IAAI,aAAa,mBACf,KAAKA,YAAY,OAAO,SAAS;EAGrC,KAAKU,iBAAiB;CACxB;;;;;;;;;CAUA,wBACE,YACA,aACA,YACA;EACA,KAAKR,eAAe,IAAI,GAAG,EAAC,QAAQ,gBAAe,CAAC;EACpD,MAAM,MAAM,IAAI,mBACd,aAAa,KAAKP,WAAW,4CACrB,YAAY,YAAY,EAAE,iDACb,WAAW,YAAY,EAAE,wCACxB,WAAW,oBAC5B,yBAAyB,wFAEhC;EACA,KAAKF,IAAI,QAAQ,IAAI,OAAO;EAC5B,cAAc,KAAKY,kBAAkB;EACrC,KAAKF,OAAO,OAAO,GAAG;CACxB;;;;;CAMA,kBAAkB,QAAwB;EACxC,IAAI,MAAM;EACV,KAAK,MAAM,CAAC,WAAW,eAAe,KAAKH,aACzC,IAAI,WAAW,QAAQ,KAAK,UAAU,YAAY,KAChD,MAAM;EAGV,OAAO;CACT;;;;;;;;CASA,kBAAkB,aAA4B;EAC5C,MAAM,aAAa,KAAKc;EACxB,OACE,eAAe,QACf,YAAY,QAAQ,KAClB,WAAW,QAAQ,IAAA;CAEzB;CAEA,OAAsB;EACpB,cAAc,KAAKT,kBAAkB;EACrC,KAAK,MAAM,EAAC,SAAQ,KAAKN,cAAc,OAAO,GAK5C,IAAI,OAAO;EAEb,KAAKD,OAAO,KAAK,KAAKL,GAAG;EACzB,OAAO;CACT;CAEA,uBAAuB;EACrB,iBAAiB,WAAW,cAAc;GACxC,aACE;GAGF,MAAM;EACR,CAAC,EAAE,YAAY,OAAM,MAAK;GASxB,MAAM,eAAe,MAAM,KAAKc,iBAAiB;GACjD,IAAI,CAAC,cAAc;IACjB,KAAKd,IAAI,OACP,8DACF;IACA;GACF;GACA,MAAM,KAAK,IAAI,SAAS,KAAKA,KAAK,KAAKC,cAAc,EAAC,UAAU,KAAI,CAAC;GACrE,IAAI;IACF,MAAM,EAAC,gBAAe,GACnB,QAAgB,kDAAkD,EAClE,IAA2B;IAC9B,MAAM,YAAY,KAAK,IAAI,GAAG,cAAc,aAAa,QAAQ,CAAC;IAClE,EAAE,QAAQ,SAAS;GACrB,SAAS,GAAG;IACV,KAAKD,IAAI,OAAO,6CAA6C,CAAC;GAChE,UAAU;IACR,GAAG,MAAM;GACX;EACF,CAAC;CACH;AACF"}
|
|
1
|
+
{"version":3,"file":"litestream3-backup-monitor.js","names":["#lc","#replicaFile","#backupURL","#metricsEndpoint","#changeStreamer","#state","#reservations","#watermarks","#verifyBackupState","#purgesBlocked","#fatal","#cleanupDelayMs","#runStartTime","#checkMetricsTimer","#initBackupLagMetric","#pushStatusWhenRestorable","#lastVerifiedUploadTime","#confirmRestorableBackup","#checkWatermarks","#scheduleCleanup","#checkInitialBackupDeadline","#fetchWatermarks","#lastWatermark","#latestBackupTime","#maxWatermarkUpTo","#confirmedDurable","#backupStaleSince","#shutDownOnWedgedBackup","#initialBackupGraceMs","#shutDownOnMissingInitialBackup"],"sources":["../../../../../../zero-cache/src/services/change-streamer/litestream3-backup-monitor.ts"],"sourcesContent":["import {statSync} from 'node:fs';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport parsePrometheusTextFormat from 'parse-prometheus-text-format';\nimport {must} from '../../../../shared/src/must.ts';\nimport {promiseVoid} from '../../../../shared/src/resolved-promises.ts';\nimport {sleep} from '../../../../shared/src/sleep.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {\n getOrCreateCounter,\n getOrCreateGauge,\n} from '../../observability/metrics.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {RunningState, UnrecoverableError} from '../running-state.ts';\nimport type {BackupMonitor} from './backup-monitor.ts';\nimport type {ChangeStreamerService} from './change-streamer.ts';\nimport type {SnapshotMessage} from './snapshot.ts';\n\nexport const CHECK_INTERVAL_MS = 60_000;\nconst MIN_CLEANUP_DELAY_MS = 30_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\ntype Reservation = {\n start: Date;\n sub: Subscription<SnapshotMessage>;\n};\n\n/**\n * The Litestream3BackupMonitor polls the litestream \"/metrics\" endpoint to\n * track the watermark (label) value of the `litestream_replica_progress` gauge\n * and schedules cleanup of change log entries that can be purged as a result.\n *\n * See: https://github.com/rocicorp/litestream/pull/3\n *\n * Note that change log entries cannot simply be purged as soon as they\n * have been applied and backed up by litestream. Consider the case in which\n * litestream backs up new wal segments every minute, but it takes 5 minutes\n * to restore a replica: if a zero-cache starts restoring a replica at\n * minute 0, and new watermarks are replicated at minutes 1, 2, 3, 4, and 5,\n * purging changelog records as soon as those watermarks are replicated would\n * result in the zero-cache not being able to catch up from minute 0 once it\n * has finished restoring the replica.\n *\n * The `/snapshot` reservation protocol is used to prevent premature change\n * log cleanup:\n * - Clients restoring a snapshot initiate a `/snapshot` request and hold that\n * request open while it restores its snapshot, prepares it, and\n * starts its subscription to the change stream. During this time, no\n * cleanups are scheduled.\n * - When the subscription is started, the interval since the beginning of\n * of the reservation is tracked to increase the background cleanup delay\n * interval if needed. The reservation is ended (and request closed), and\n * cleanup scheduling is resumed with the current delay interval.\n *\n * Note that the reservation request is the primary mechanism by which\n * premature change log cleanup is prevented. The cleanup delay interval is\n * a secondary safeguard.\n *\n * Additionally, because the watermarks reported by litestream metrics\n * reflect what litestream *believes* has been backed up (which has been\n * observed to diverge from reality when uploads silently fail), the cleanup\n * watermark is only advanced after verifying it against the actual backup\n * state in the 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 Litestream3BackupMonitor implements BackupMonitor {\n readonly id = 'backup-monitor';\n readonly #lc: LogContext;\n readonly #replicaFile: string;\n readonly #backupURL: string;\n readonly #metricsEndpoint: string;\n readonly #changeStreamer: ChangeStreamerService;\n readonly #state = new RunningState(this.id);\n\n readonly #reservations = new Map<string, Reservation>();\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 // Rejected when the backup is determined to be genuinely wedged, which\n // surfaces as a rejection of the `run()` promise and shuts the process down.\n readonly #fatal = resolver<void>();\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 #cleanupDelayMs: number;\n #checkMetricsTimer: NodeJS.Timeout | undefined;\n\n constructor(\n lc: LogContext,\n replicaFile: string,\n backupURL: string,\n metricsEndpoint: string,\n changeStreamer: ChangeStreamerService,\n initialCleanupDelayMs: number,\n verifyBackupState: BackupStateVerifier,\n ) {\n this.#lc = lc.withContext('component', this.id);\n this.#replicaFile = replicaFile;\n this.#backupURL = backupURL;\n this.#metricsEndpoint = metricsEndpoint;\n this.#changeStreamer = changeStreamer;\n this.#verifyBackupState = verifyBackupState;\n this.#cleanupDelayMs = Math.max(\n initialCleanupDelayMs,\n MIN_CLEANUP_DELAY_MS, // purely for peace of mind\n );\n\n this.#lc.info?.(\n `backup monitor started ${initialCleanupDelayMs} ms after snapshot restore`,\n );\n }\n\n run(): Promise<void> {\n this.#runStartTime = Date.now();\n this.#lc.info?.(\n `monitoring backups at ${this.#metricsEndpoint} with ` +\n `${this.#cleanupDelayMs} ms cleanup delay`,\n );\n this.#checkMetricsTimer = setInterval(\n this.checkWatermarksAndScheduleCleanup,\n CHECK_INTERVAL_MS,\n );\n this.#initBackupLagMetric();\n // Resolves on a normal stop; rejects if the backup is found to be wedged.\n return Promise.race([this.#state.stopped(), this.#fatal.promise]);\n }\n\n startSnapshotReservation(taskID: string): Subscription<SnapshotMessage> {\n this.#lc.info?.(`pausing change-log cleanup while ${taskID} snapshots`);\n // In the case of retries, only track the last reservation.\n this.#reservations.get(taskID)?.sub.cancel();\n\n const sub = Subscription.create<SnapshotMessage>({\n // If the reservation still exists when the connection closes\n // (e.g. subscriber crashed), clean it up without updating the\n // cleanup delay.\n cleanup: () => this.endReservation(taskID, false),\n });\n this.#reservations.set(taskID, {start: new Date(), sub});\n // Note: the Subscription must be returned immediately so that the\n // websocket can begin sending liveness pings. The `status` signal\n // (which tells the view-syncer to restore) is withheld until a\n // restorable backup actually exists; see #pushStatusWhenRestorable.\n void this.#pushStatusWhenRestorable(taskID, sub);\n return sub;\n }\n\n /**\n * Pushes the `status` snapshot signal once a restorable backup is confirmed\n * to exist in the backup destination. On a warm start this is immediate; on a\n * cold start (a fresh stack whose first backup is still being produced) the\n * reservation is held open — kept alive by websocket liveness pings — and the\n * view-syncer stays unready until the first backup lands, rather than being\n * told to restore a backup that does not yet exist.\n */\n async #pushStatusWhenRestorable(\n taskID: string,\n sub: Subscription<SnapshotMessage>,\n ): Promise<void> {\n try {\n while (this.#lastVerifiedUploadTime === null) {\n if (!sub.active) {\n return; // subscriber disconnected, or a newer reservation took over\n }\n if (await this.#confirmRestorableBackup()) {\n break;\n }\n this.#lc.info?.(\n `no restorable backup at ${this.#backupURL} yet; ` +\n `holding ${taskID}'s reservation open until it lands`,\n );\n await sleep(RESTORABLE_BACKUP_POLL_INTERVAL_MS, this.#state.signal);\n }\n const changeLogState = await this.#changeStreamer.getChangeLogState();\n if (sub.active) {\n sub.push([\n 'status',\n {tag: 'status', backupURL: this.#backupURL, ...changeLogState},\n ]);\n }\n } catch (e) {\n if (this.#state.signal.aborted) {\n return; // shutting down; not an error\n }\n this.#lc.warn?.(`failing snapshot reservation`, e);\n sub.fail(e instanceof Error ? e : new Error(String(e)));\n }\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.#verifyBackupState();\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 endReservation(taskID: string, updateCleanupDelay = true) {\n const res = this.#reservations.get(taskID);\n if (res === undefined) {\n return;\n }\n this.#reservations.delete(taskID);\n const {start, sub} = res;\n sub.cancel(); // closes the connection if still open\n\n if (updateCleanupDelay) {\n const duration = Date.now() - start.getTime();\n this.#lc.info?.(`snapshot initialized by ${taskID} in ${duration} ms`);\n if (duration > this.#cleanupDelayMs) {\n this.#cleanupDelayMs = duration;\n this.#lc.info?.(`increased cleanup delay to ${duration} ms`);\n }\n }\n }\n\n // Exported for testing\n readonly checkWatermarksAndScheduleCleanup = 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.#scheduleCleanup();\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 #scheduleCleanup() {\n if (this.#reservations.size > 0) {\n this.#lc.info?.(\n `watermark cleanup paused for snapshot(s): ${[...this.#reservations.keys()]}`,\n );\n return;\n }\n const latestCleanupTime = Date.now() - this.#cleanupDelayMs;\n const maxWatermark = this.#maxWatermarkUpTo(latestCleanupTime);\n if (maxWatermark.length === 0) {\n return;\n }\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 const claimedTime = must(this.#watermarks.get(maxWatermark));\n if (!this.#confirmedDurable(claimedTime)) {\n try {\n this.#lastVerifiedUploadTime = await this.#verifyBackupState();\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 verifiedWatermark = this.#maxWatermarkUpTo(\n Math.min(\n latestCleanupTime,\n lastUpload.getTime() + BACKUP_VERIFICATION_SLACK_MS,\n ),\n );\n if (verifiedWatermark.length === 0) {\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 this.#changeStreamer.scheduleCleanup(verifiedWatermark);\n for (const watermark of this.#watermarks.keys()) {\n if (watermark <= verifiedWatermark) {\n this.#watermarks.delete(watermark);\n }\n }\n this.#lastWatermark = verifiedWatermark;\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 clearInterval(this.#checkMetricsTimer);\n this.#fatal.reject(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 clearInterval(this.#checkMetricsTimer);\n this.#fatal.reject(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 #maxWatermarkUpTo(cutoff: number): string {\n let max = '';\n for (const [watermark, backupTime] of this.#watermarks) {\n if (backupTime.getTime() <= cutoff && watermark > max) {\n max = watermark;\n }\n }\n return max;\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 stop(): Promise<void> {\n clearInterval(this.#checkMetricsTimer);\n for (const {sub} of this.#reservations.values()) {\n // Close any pending reservations. This commonly happens when a new\n // replication-manager makes a `/snapshot` reservation on the existing\n // replication-manager, and then shuts it down when it takes over the\n // replication slot.\n sub.cancel();\n }\n this.#state.stop(this.#lc);\n return promiseVoid;\n }\n\n #initBackupLagMetric() {\n getOrCreateGauge('replica', 'backup_lag', {\n description:\n 'Latency from when a change is written to the replica ' +\n 'to when it is backed up to litestream. It is expected to create a saw ' +\n 'pattern from 0 to the configured ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES.',\n unit: 'millisecond',\n }).addCallback(async o => {\n // For legacy litestream, we use the watermark metric (and its associated\n // backup time) exported by litestream metrics to determine the time of\n // of the backed up watermark. This is technically imprecise--it would be\n // more correct to use the committed writeTimeMs--but it is good enough\n // in that it serves the purpose of detecting a non-functioning backup.\n // With litestream v5, this can be made more precise by querying the\n // _zero.replicationState row from the backup directly using an LTX-based\n // database reader.\n const latestBackup = await this.#checkWatermarks();\n if (!latestBackup) {\n this.#lc.warn?.(\n `no backed up watermarks. unable to report replica.backup_lag`,\n );\n return;\n }\n const db = new Database(this.#lc, this.#replicaFile, {readonly: true});\n try {\n const {writeTimeMs} = db\n .prepare(/*sql*/ `SELECT writeTimeMs FROM \"_zero.replicationState\"`)\n .get<{writeTimeMs: number}>();\n const backupLag = Math.max(0, writeTimeMs - latestBackup.getTime());\n o.observe(backupLag);\n } catch (e) {\n this.#lc.warn?.(`error measuring replica.backup_lag metric`, e);\n } finally {\n db.close();\n }\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/litestream/commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAGrD,OAAO,KAAK,EAAC,UAAU,EAAW,MAAM,kBAAkB,CAAC;AAM3D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/litestream/commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAGrD,OAAO,KAAK,EAAC,UAAU,EAAW,MAAM,kBAAkB,CAAC;AAM3D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,6BAA6B,CAAC;AAe5D,KAAK,kBAAkB,GAAG;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,MAAM,CAAC,QAAQ,CAAC,IAAI,6BAA6B;gBAErC,SAAS,EAAE,MAAM,GAAG,SAAS;CAG1C;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,UAAU,EAClB,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,iBA4C9C;AAoKD,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,UAAU,GACjB,YAAY,CAQd;AAQD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,IAAI,CAAC,CAgBf;AAqDD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,WAAW,GAAG,KAAK,EAC5B,MAAM,EAAE,MAAM,GACb,IAAI,EAAE,CAoBR"}
|
|
@@ -6,12 +6,12 @@ import { Database } from "../../../../zqlite/src/db.js";
|
|
|
6
6
|
import { getSubscriptionState } from "../replicator/schema/replication-state.js";
|
|
7
7
|
import { StatementRunner } from "../../db/statements.js";
|
|
8
8
|
import { deleteLiteDB } from "../../db/delete-lite-db.js";
|
|
9
|
+
import { assertDatabaseIntegrity } from "../../db/migration-lite.js";
|
|
9
10
|
import { ChangeStreamerHttpClient } from "../change-streamer/change-streamer-http.js";
|
|
10
11
|
import { resolver } from "@rocicorp/resolver";
|
|
11
12
|
import { spawn } from "node:child_process";
|
|
12
13
|
import { existsSync } from "node:fs";
|
|
13
14
|
//#region ../zero-cache/src/services/litestream/commands.ts
|
|
14
|
-
var MAX_RETRIES = 60;
|
|
15
15
|
var RETRY_INTERVAL_MS = 3e3;
|
|
16
16
|
var BackupNotFoundException = class extends Error {
|
|
17
17
|
static name = "BackupNotFoundException";
|
|
@@ -26,12 +26,14 @@ var BackupNotFoundException = class extends Error {
|
|
|
26
26
|
* retrieved from the replication-manager via the snapshot protocol.
|
|
27
27
|
*/
|
|
28
28
|
async function restoreReplica(lc, config, replicaConstraints) {
|
|
29
|
-
|
|
29
|
+
let consecutiveErrors = 0;
|
|
30
|
+
for (;;) {
|
|
30
31
|
try {
|
|
31
32
|
if (await tryRestore(lc, config, replicaConstraints)) return;
|
|
33
|
+
consecutiveErrors = 0;
|
|
32
34
|
} catch (e) {
|
|
33
|
-
if (
|
|
34
|
-
lc.warn?.(`
|
|
35
|
+
if (++consecutiveErrors <= 1) {
|
|
36
|
+
lc.warn?.(`restore attempt failed. retrying once`, e);
|
|
35
37
|
continue;
|
|
36
38
|
}
|
|
37
39
|
throw e;
|
|
@@ -40,7 +42,6 @@ async function restoreReplica(lc, config, replicaConstraints) {
|
|
|
40
42
|
lc.info?.(`replica not found. retrying in ${RETRY_INTERVAL_MS / 1e3} seconds`);
|
|
41
43
|
await sleep(RETRY_INTERVAL_MS);
|
|
42
44
|
}
|
|
43
|
-
throw new Error(`max attempts exceeded restoring replica`);
|
|
44
45
|
}
|
|
45
46
|
function getLitestream(mode, config, logLevelOverride, backupURLOverride) {
|
|
46
47
|
const { executable, executableV5, restoreUsingV5, backupURL, logLevel, configPath, endpoint, region, port = config.port + 2, checkpointThresholdMB, minCheckpointPageCount = checkpointThresholdMB * 250, maxCheckpointPageCount = minCheckpointPageCount * 10, incrementalBackupIntervalMinutes, snapshotBackupIntervalHours, multipartConcurrency, multipartSize } = config.litestream;
|
|
@@ -104,8 +105,10 @@ async function tryRestore(lc, config, replicaConstraints) {
|
|
|
104
105
|
return true;
|
|
105
106
|
}
|
|
106
107
|
function replicaIsValid(lc, replica, constraints) {
|
|
107
|
-
|
|
108
|
+
let db;
|
|
108
109
|
try {
|
|
110
|
+
db = new Database(lc, replica);
|
|
111
|
+
assertDatabaseIntegrity(lc, "restored replica", db);
|
|
109
112
|
const { replicaVersion, watermark } = getSubscriptionState(new StatementRunner(db));
|
|
110
113
|
if (replicaVersion !== constraints.replicaVersion) {
|
|
111
114
|
lc.warn?.(`Local replica version ${replicaVersion} does not match expected replicaVersion ${constraints.replicaVersion}`, constraints);
|
|
@@ -121,7 +124,7 @@ function replicaIsValid(lc, replica, constraints) {
|
|
|
121
124
|
lc.error?.("Error while validating restored replica", e);
|
|
122
125
|
return false;
|
|
123
126
|
} finally {
|
|
124
|
-
db
|
|
127
|
+
db?.close();
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
130
|
function startReplicaBackupProcess(lc, config) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.js","names":[],"sources":["../../../../../../zero-cache/src/services/litestream/commands.ts"],"sourcesContent":["import type {ChildProcess} from 'node:child_process';\nimport {spawn} from 'node:child_process';\nimport {existsSync} from 'node:fs';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {must} from '../../../../shared/src/must.ts';\nimport {sleep} from '../../../../shared/src/sleep.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {assertNormalized} from '../../config/normalize.ts';\nimport type {ZeroConfig} from '../../config/zero-config.ts';\nimport {deleteLiteDB} from '../../db/delete-lite-db.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport {getShardConfig} from '../../types/shards.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {ChangeStreamerHttpClient} from '../change-streamer/change-streamer-http.ts';\nimport type {\n SnapshotMessage,\n SnapshotStatus,\n} from '../change-streamer/snapshot.ts';\nimport {getSubscriptionState} from '../replicator/schema/replication-state.ts';\n\n// Retry for up to 3 minutes (60 times with 3 second delay).\n// Beyond that, let the container runner restart the task.\nconst MAX_RETRIES = 60;\nconst RETRY_INTERVAL_MS = 3000;\n\ntype ReplicaConstraints = {\n replicaVersion: string;\n minWatermark: string;\n};\n\nexport class BackupNotFoundException extends Error {\n static readonly name = 'BackupNotFoundException';\n\n constructor(backupURL: string | undefined) {\n super(`backup not found at ${backupURL}`);\n }\n}\n\n/**\n * @param replicaConstraints The constraints of the restored backup when\n * restoring for the change-streamer (replication-manager). For the\n * view-syncer, this should be unspecified so that the constraints are\n * retrieved from the replication-manager via the snapshot protocol.\n */\nexport async function restoreReplica(\n lc: LogContext,\n config: ZeroConfig,\n replicaConstraints: ReplicaConstraints | null,\n) {\n for (let i = 0; i < MAX_RETRIES; i++) {\n try {\n if (await tryRestore(lc, config, replicaConstraints)) {\n return;\n }\n } catch (e) {\n if (i === 0) {\n // A restore will fail if the `replicate` process creates a new\n // snapshot (and compacts old files) at the same time. Snapshots are\n // infrequent (e.g. once every 12 hours), and the scenario is\n // recoverable with a retry.\n lc.warn?.(`initial restore attempt failed. retrying once`, e);\n continue;\n }\n // If it fails again on the retry, though, bail.\n throw e;\n }\n if (replicaConstraints) {\n // This can happen if the litestream URL is purposefully changed to\n // force a resync.\n throw new BackupNotFoundException(config.litestream.backupURL);\n }\n lc.info?.(\n `replica not found. retrying in ${RETRY_INTERVAL_MS / 1000} seconds`,\n );\n await sleep(RETRY_INTERVAL_MS);\n }\n throw new Error(`max attempts exceeded restoring replica`);\n}\n\nfunction getLitestream(\n mode: 'restore' | 'replicate',\n config: ZeroConfig,\n logLevelOverride?: LogLevel,\n backupURLOverride?: string,\n): {\n litestream: string;\n env: NodeJS.ProcessEnv;\n} {\n const {\n executable,\n executableV5,\n restoreUsingV5,\n backupURL,\n logLevel,\n configPath,\n endpoint,\n region,\n port = config.port + 2,\n checkpointThresholdMB,\n minCheckpointPageCount = checkpointThresholdMB * 250, // SQLite page size is 4KB\n maxCheckpointPageCount = minCheckpointPageCount * 10,\n incrementalBackupIntervalMinutes,\n snapshotBackupIntervalHours,\n multipartConcurrency,\n multipartSize,\n } = config.litestream;\n\n // Set the snapshot interval to something smaller than x hours so that\n // the hourly check triggers on the hour, rather than the hour after.\n const snapshotBackupIntervalMinutes = snapshotBackupIntervalHours * 60 - 5;\n\n const litestream =\n // The v0.5.8+ litestream executable can restore from either the new LTX\n // format or the legacy WAL format, allowing forwards-compatibility /\n // rollback safety with zero-cache versions that backup to LTX.\n (mode === 'restore' && restoreUsingV5 ? executableV5 : executable) ??\n must(executable, `Missing --litestream-executable`);\n return {\n litestream,\n env: {\n ...process.env,\n ['ZERO_REPLICA_FILE']: config.replica.file,\n ['ZERO_LITESTREAM_BACKUP_URL']: must(backupURLOverride ?? backupURL),\n ['ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT']: String(\n minCheckpointPageCount,\n ),\n ['ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT']: String(\n maxCheckpointPageCount,\n ),\n ['ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES']: String(\n incrementalBackupIntervalMinutes,\n ),\n ['ZERO_LITESTREAM_LOG_LEVEL']: logLevelOverride ?? logLevel,\n ['ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_MINUTES']: String(\n snapshotBackupIntervalMinutes,\n ),\n ['ZERO_LITESTREAM_MULTIPART_CONCURRENCY']: String(multipartConcurrency),\n ['ZERO_LITESTREAM_MULTIPART_SIZE']: String(multipartSize),\n ['ZERO_LOG_FORMAT']: config.log.format,\n ['LITESTREAM_CONFIG']: configPath,\n ['LITESTREAM_PORT']: String(port),\n ...(endpoint ? {['ZERO_LITESTREAM_ENDPOINT']: endpoint} : {}),\n ...(region ? {['ZERO_LITESTREAM_REGION']: region} : {}),\n },\n };\n}\n\nasync function tryRestore(\n lc: LogContext,\n config: ZeroConfig,\n replicaConstraints: ReplicaConstraints | null,\n) {\n let snapshotStatus: SnapshotStatus | undefined;\n if (!replicaConstraints) {\n // view-syncers fetch replica constraints from the replication-manager\n // via the snapshot protocol.\n snapshotStatus = await reserveAndGetSnapshotStatus(lc, config);\n lc.info?.(`restoring backup from ${snapshotStatus.backupURL}`);\n replicaConstraints = snapshotStatus;\n }\n\n const {litestream, env} = getLitestream(\n 'restore',\n config,\n 'debug', // Include all output from `litestream restore`, as it's minimal.\n snapshotStatus?.backupURL,\n );\n const {restoreParallelism: parallelism} = config.litestream;\n const proc = spawn(\n litestream,\n [\n 'restore',\n '-if-db-not-exists',\n '-if-replica-exists',\n '-parallelism',\n String(parallelism),\n config.replica.file,\n ],\n {env, stdio: 'inherit', windowsHide: true},\n );\n const {promise, resolve, reject} = resolver();\n proc.on('error', reject);\n proc.on('close', (code, signal) => {\n if (signal) {\n reject(`litestream killed with ${signal}`);\n } else if (code !== 0) {\n reject(`litestream exited with code ${code}`);\n } else {\n resolve();\n }\n });\n await promise;\n if (!existsSync(config.replica.file)) {\n return false;\n }\n if (!replicaIsValid(lc, config.replica.file, replicaConstraints)) {\n lc.info?.(`Deleting local replica and retrying restore`);\n deleteLiteDB(config.replica.file);\n return false;\n }\n return true;\n}\n\nfunction replicaIsValid(\n lc: LogContext,\n replica: string,\n constraints: ReplicaConstraints,\n) {\n const db = new Database(lc, replica);\n try {\n const {replicaVersion, watermark} = getSubscriptionState(\n new StatementRunner(db),\n );\n if (replicaVersion !== constraints.replicaVersion) {\n lc.warn?.(\n `Local replica version ${replicaVersion} does not match expected replicaVersion ${constraints.replicaVersion}`,\n constraints,\n );\n return false;\n }\n if (watermark < constraints.minWatermark) {\n lc.warn?.(\n `Local replica watermark ${watermark} is earlier than minWatermark ${constraints.minWatermark}`,\n );\n return false;\n }\n lc.info?.(\n `Local replica at version ${replicaVersion} and watermark ${watermark} is compatible`,\n constraints,\n );\n return true;\n } catch (e) {\n lc.error?.('Error while validating restored replica', e);\n return false;\n } finally {\n db.close();\n }\n}\n\nexport function startReplicaBackupProcess(\n lc: LogContext,\n config: ZeroConfig,\n): ChildProcess {\n const {litestream, env} = getLitestream('replicate', config);\n lc.info?.(`starting litestream backup to ${config.litestream.backupURL}`);\n return spawn(litestream, ['replicate'], {\n env,\n stdio: 'inherit',\n windowsHide: true,\n });\n}\n\n// Listing the backup state requires a few S3 LIST requests, which should\n// normally complete well within this timeout.\nconst LIST_BACKUP_TIMEOUT_MS = 30_000;\n\nconst wsRe = /\\s+/;\n\n/**\n * Returns the time of the most recent object (snapshot or WAL segment)\n * actually uploaded to the backup replica destination, as listed by the\n * bundled litestream CLI (`litestream snapshots` / `litestream wal`).\n *\n * This queries the replica destination (e.g. S3) directly, and thus serves\n * as a source of truth for backup durability. This is in contrast to the\n * `litestream_replica_progress` metric, which is exported when litestream\n * *believes* an upload has succeeded, and has been observed to advance even\n * when nothing is actually written to the destination.\n *\n * Rejects if the backup state cannot be determined (spawn error, non-zero\n * exit, timeout, or empty/unparseable listing).\n */\nexport async function getLastBackupTime(\n lc: LogContext,\n config: ZeroConfig,\n): Promise<Date> {\n const [snapshots, wal] = await Promise.all([\n listBackupCreatedTimes(lc, config, 'snapshots'),\n listBackupCreatedTimes(lc, config, 'wal'),\n ]);\n const times = [...snapshots, ...wal];\n if (times.length === 0) {\n // Note: the litestream CLI exits with code 0 and logs listing errors\n // (e.g. S3 failures) to stderr, so an empty listing cannot be\n // distinguished from a failed one. Since a valid backup always contains\n // at least one snapshot, an empty listing is treated as a failure.\n throw new Error(\n `no snapshots or WAL segments listed at ${config.litestream.backupURL}`,\n );\n }\n return new Date(Math.max(...times.map(time => time.getTime())));\n}\n\n/**\n * Runs `litestream <snapshots|wal> <replica-file>` with the same config /\n * environment used by the `litestream replicate` process (so that the\n * backupURL, endpoint, region, and credentials are identical), and parses\n * the `created` column (RFC3339) of the tab-formatted output, e.g.:\n *\n * ```\n * replica generation index size created\n * s3 1862f44967b3863f 0 4546445 2026-06-10T01:11:32Z\n * ```\n */\nasync function listBackupCreatedTimes(\n lc: LogContext,\n config: ZeroConfig,\n command: 'snapshots' | 'wal',\n): Promise<Date[]> {\n const {litestream, env} = getLitestream('replicate', config);\n const proc = spawn(litestream, [command, config.replica.file], {\n env,\n stdio: ['ignore', 'pipe', 'inherit'],\n windowsHide: true,\n });\n const {promise, resolve, reject} = resolver<string>();\n let stdout = '';\n proc.stdout.setEncoding('utf-8');\n proc.stdout.on('data', chunk => (stdout += chunk));\n proc.on('error', reject);\n proc.on('close', (code, signal) => {\n if (signal) {\n reject(new Error(`litestream ${command} killed with ${signal}`));\n } else if (code !== 0) {\n reject(new Error(`litestream ${command} exited with code ${code}`));\n } else {\n resolve(stdout);\n }\n });\n const timeout = setTimeout(() => {\n reject(new Error(`timed out listing backup state (litestream ${command})`));\n proc.kill('SIGKILL');\n }, LIST_BACKUP_TIMEOUT_MS);\n\n let output: string;\n try {\n output = await promise;\n } finally {\n clearTimeout(timeout);\n }\n\n return parseBackupCreatedTimes(lc, command, output);\n}\n\n/**\n * Parses the `created` column (the last, RFC3339-formatted column) from the\n * tab-formatted output of `litestream snapshots` / `litestream wal`. The\n * header row and any unparseable lines are skipped.\n *\n * Exported for testing.\n */\nexport function parseBackupCreatedTimes(\n lc: LogContext,\n command: 'snapshots' | 'wal',\n output: string,\n): Date[] {\n const times: Date[] = [];\n for (const line of output.split('\\n')) {\n const cols = line.trim().split(wsRe);\n const created = cols.at(-1);\n if (\n cols.length < 2 ||\n created === undefined ||\n created === 'created' /* header row */\n ) {\n continue;\n }\n const time = new Date(created);\n if (Number.isNaN(time.getTime())) {\n lc.warn?.(`unexpected line in litestream ${command} output: ${line}`);\n continue;\n }\n times.push(time);\n }\n return times;\n}\n\nfunction reserveAndGetSnapshotStatus(\n lc: LogContext,\n config: ZeroConfig,\n): Promise<SnapshotStatus> {\n const {promise: status, resolve, reject} = resolver<SnapshotStatus>();\n\n void (async function () {\n const abort = new AbortController();\n process.on('SIGINT', () => abort.abort());\n process.on('SIGTERM', () => abort.abort());\n\n for (let i = 0; ; i++) {\n let err: unknown;\n try {\n let resolved = false;\n const stream = await reserveSnapshot(lc, config);\n for await (const msg of stream) {\n // Capture the value of the status message that the change-streamer\n // backup monitor returns, and hold the connection open to\n // \"reserve\" the snapshot and prevent change log cleanup.\n resolve(msg[1]);\n resolved = true;\n }\n // The change-streamer itself closes the connection when the\n // subscription is started (or the reservation retried).\n if (resolved) {\n break;\n }\n } catch (e) {\n err = e;\n }\n // Retry in the view-syncer since it cannot proceed until it connects\n // to a (compatible) replication-manager. In particular, a\n // replication-manager that does not support the view-syncer's\n // change-streamer protocol will close the stream with an error; this\n // retry logic essentially delays the startup of a view-syncer until\n // a compatible replication-manager has been rolled out, allowing\n // replication-manager and view-syncer services to be updated in\n // parallel.\n lc.warn?.(\n `Unable to reserve snapshot (attempt ${i + 1}). Retrying in 5 seconds.`,\n String(err),\n );\n try {\n await sleep(5000, abort.signal);\n } catch (e) {\n return reject(e);\n }\n }\n })();\n\n return status;\n}\n\nfunction reserveSnapshot(\n lc: LogContext,\n config: ZeroConfig,\n): Promise<Source<SnapshotMessage>> {\n assertNormalized(config);\n const {taskID, change, changeStreamer} = config;\n const shardID = getShardConfig(config);\n\n const changeStreamerClient = new ChangeStreamerHttpClient(\n lc,\n shardID,\n change.db,\n changeStreamer.uri,\n );\n\n return changeStreamerClient.reserveSnapshot(taskID);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"commands.js","names":[],"sources":["../../../../../../zero-cache/src/services/litestream/commands.ts"],"sourcesContent":["import type {ChildProcess} from 'node:child_process';\nimport {spawn} from 'node:child_process';\nimport {existsSync} from 'node:fs';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {must} from '../../../../shared/src/must.ts';\nimport {sleep} from '../../../../shared/src/sleep.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {assertNormalized} from '../../config/normalize.ts';\nimport type {ZeroConfig} from '../../config/zero-config.ts';\nimport {deleteLiteDB} from '../../db/delete-lite-db.ts';\nimport {assertDatabaseIntegrity} from '../../db/migration-lite.ts';\nimport {StatementRunner} from '../../db/statements.ts';\nimport {getShardConfig} from '../../types/shards.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {ChangeStreamerHttpClient} from '../change-streamer/change-streamer-http.ts';\nimport type {\n SnapshotMessage,\n SnapshotStatus,\n} from '../change-streamer/snapshot.ts';\nimport {getSubscriptionState} from '../replicator/schema/replication-state.ts';\n\nconst RETRY_INTERVAL_MS = 3000;\n\ntype ReplicaConstraints = {\n replicaVersion: string;\n minWatermark: string;\n};\n\nexport class BackupNotFoundException extends Error {\n static readonly name = 'BackupNotFoundException';\n\n constructor(backupURL: string | undefined) {\n super(`backup not found at ${backupURL}`);\n }\n}\n\n/**\n * @param replicaConstraints The constraints of the restored backup when\n * restoring for the change-streamer (replication-manager). For the\n * view-syncer, this should be unspecified so that the constraints are\n * retrieved from the replication-manager via the snapshot protocol.\n */\nexport async function restoreReplica(\n lc: LogContext,\n config: ZeroConfig,\n replicaConstraints: ReplicaConstraints | null,\n) {\n // View-syncers (no replicaConstraints) wait indefinitely for the\n // replication-manager to publish a restorable backup. On a fresh stack the\n // first backup is not durable until the initial sync completes and litestream\n // uploads the initial snapshot, which can take many minutes for a large\n // replica. The platform's startup probe budget (which scales with replica\n // size) is the backstop, so restoreReplica must not impose its own shorter\n // cap and self-terminate while the backup is still being produced.\n //\n // `consecutiveErrors` distinguishes a transient restore *error* (e.g. the\n // `replicate` process compacting a snapshot mid-restore) — which is retried\n // once — from a \"backup not found\" result, which is expected while waiting.\n let consecutiveErrors = 0;\n for (;;) {\n try {\n if (await tryRestore(lc, config, replicaConstraints)) {\n return;\n }\n consecutiveErrors = 0;\n } catch (e) {\n if (++consecutiveErrors <= 1) {\n // A restore will fail if the `replicate` process creates a new\n // snapshot (and compacts old files) at the same time. Snapshots are\n // infrequent (e.g. once every 12 hours), and the scenario is\n // recoverable with a retry.\n lc.warn?.(`restore attempt failed. retrying once`, e);\n continue;\n }\n // If it fails again on the retry, though, bail.\n throw e;\n }\n if (replicaConstraints) {\n // The replication-manager restores against explicit constraints, so a\n // missing backup is fatal (e.g. the litestream URL was purposefully\n // changed to force a resync). Only the view-syncer (no constraints)\n // waits for a backup to appear.\n throw new BackupNotFoundException(config.litestream.backupURL);\n }\n lc.info?.(\n `replica not found. retrying in ${RETRY_INTERVAL_MS / 1000} seconds`,\n );\n await sleep(RETRY_INTERVAL_MS);\n }\n}\n\nfunction getLitestream(\n mode: 'restore' | 'replicate',\n config: ZeroConfig,\n logLevelOverride?: LogLevel,\n backupURLOverride?: string,\n): {\n litestream: string;\n env: NodeJS.ProcessEnv;\n} {\n const {\n executable,\n executableV5,\n restoreUsingV5,\n backupURL,\n logLevel,\n configPath,\n endpoint,\n region,\n port = config.port + 2,\n checkpointThresholdMB,\n minCheckpointPageCount = checkpointThresholdMB * 250, // SQLite page size is 4KB\n maxCheckpointPageCount = minCheckpointPageCount * 10,\n incrementalBackupIntervalMinutes,\n snapshotBackupIntervalHours,\n multipartConcurrency,\n multipartSize,\n } = config.litestream;\n\n // Set the snapshot interval to something smaller than x hours so that\n // the hourly check triggers on the hour, rather than the hour after.\n const snapshotBackupIntervalMinutes = snapshotBackupIntervalHours * 60 - 5;\n\n const litestream =\n // The v0.5.8+ litestream executable can restore from either the new LTX\n // format or the legacy WAL format, allowing forwards-compatibility /\n // rollback safety with zero-cache versions that backup to LTX.\n (mode === 'restore' && restoreUsingV5 ? executableV5 : executable) ??\n must(executable, `Missing --litestream-executable`);\n return {\n litestream,\n env: {\n ...process.env,\n ['ZERO_REPLICA_FILE']: config.replica.file,\n ['ZERO_LITESTREAM_BACKUP_URL']: must(backupURLOverride ?? backupURL),\n ['ZERO_LITESTREAM_MIN_CHECKPOINT_PAGE_COUNT']: String(\n minCheckpointPageCount,\n ),\n ['ZERO_LITESTREAM_MAX_CHECKPOINT_PAGE_COUNT']: String(\n maxCheckpointPageCount,\n ),\n ['ZERO_LITESTREAM_INCREMENTAL_BACKUP_INTERVAL_MINUTES']: String(\n incrementalBackupIntervalMinutes,\n ),\n ['ZERO_LITESTREAM_LOG_LEVEL']: logLevelOverride ?? logLevel,\n ['ZERO_LITESTREAM_SNAPSHOT_BACKUP_INTERVAL_MINUTES']: String(\n snapshotBackupIntervalMinutes,\n ),\n ['ZERO_LITESTREAM_MULTIPART_CONCURRENCY']: String(multipartConcurrency),\n ['ZERO_LITESTREAM_MULTIPART_SIZE']: String(multipartSize),\n ['ZERO_LOG_FORMAT']: config.log.format,\n ['LITESTREAM_CONFIG']: configPath,\n ['LITESTREAM_PORT']: String(port),\n ...(endpoint ? {['ZERO_LITESTREAM_ENDPOINT']: endpoint} : {}),\n ...(region ? {['ZERO_LITESTREAM_REGION']: region} : {}),\n },\n };\n}\n\nasync function tryRestore(\n lc: LogContext,\n config: ZeroConfig,\n replicaConstraints: ReplicaConstraints | null,\n) {\n let snapshotStatus: SnapshotStatus | undefined;\n if (!replicaConstraints) {\n // view-syncers fetch replica constraints from the replication-manager\n // via the snapshot protocol.\n snapshotStatus = await reserveAndGetSnapshotStatus(lc, config);\n lc.info?.(`restoring backup from ${snapshotStatus.backupURL}`);\n replicaConstraints = snapshotStatus;\n }\n\n const {litestream, env} = getLitestream(\n 'restore',\n config,\n 'debug', // Include all output from `litestream restore`, as it's minimal.\n snapshotStatus?.backupURL,\n );\n const {restoreParallelism: parallelism} = config.litestream;\n const proc = spawn(\n litestream,\n [\n 'restore',\n '-if-db-not-exists',\n '-if-replica-exists',\n '-parallelism',\n String(parallelism),\n config.replica.file,\n ],\n {env, stdio: 'inherit', windowsHide: true},\n );\n const {promise, resolve, reject} = resolver();\n proc.on('error', reject);\n proc.on('close', (code, signal) => {\n if (signal) {\n reject(`litestream killed with ${signal}`);\n } else if (code !== 0) {\n reject(`litestream exited with code ${code}`);\n } else {\n resolve();\n }\n });\n await promise;\n if (!existsSync(config.replica.file)) {\n return false;\n }\n if (!replicaIsValid(lc, config.replica.file, replicaConstraints)) {\n lc.info?.(`Deleting local replica and retrying restore`);\n deleteLiteDB(config.replica.file);\n return false;\n }\n return true;\n}\n\nfunction replicaIsValid(\n lc: LogContext,\n replica: string,\n constraints: ReplicaConstraints,\n) {\n let db: Database | undefined;\n try {\n db = new Database(lc, replica);\n assertDatabaseIntegrity(lc, 'restored replica', db);\n const {replicaVersion, watermark} = getSubscriptionState(\n new StatementRunner(db),\n );\n if (replicaVersion !== constraints.replicaVersion) {\n lc.warn?.(\n `Local replica version ${replicaVersion} does not match expected replicaVersion ${constraints.replicaVersion}`,\n constraints,\n );\n return false;\n }\n if (watermark < constraints.minWatermark) {\n lc.warn?.(\n `Local replica watermark ${watermark} is earlier than minWatermark ${constraints.minWatermark}`,\n );\n return false;\n }\n lc.info?.(\n `Local replica at version ${replicaVersion} and watermark ${watermark} is compatible`,\n constraints,\n );\n return true;\n } catch (e) {\n lc.error?.('Error while validating restored replica', e);\n return false;\n } finally {\n db?.close();\n }\n}\n\nexport function startReplicaBackupProcess(\n lc: LogContext,\n config: ZeroConfig,\n): ChildProcess {\n const {litestream, env} = getLitestream('replicate', config);\n lc.info?.(`starting litestream backup to ${config.litestream.backupURL}`);\n return spawn(litestream, ['replicate'], {\n env,\n stdio: 'inherit',\n windowsHide: true,\n });\n}\n\n// Listing the backup state requires a few S3 LIST requests, which should\n// normally complete well within this timeout.\nconst LIST_BACKUP_TIMEOUT_MS = 30_000;\n\nconst wsRe = /\\s+/;\n\n/**\n * Returns the time of the most recent object (snapshot or WAL segment)\n * actually uploaded to the backup replica destination, as listed by the\n * bundled litestream CLI (`litestream snapshots` / `litestream wal`).\n *\n * This queries the replica destination (e.g. S3) directly, and thus serves\n * as a source of truth for backup durability. This is in contrast to the\n * `litestream_replica_progress` metric, which is exported when litestream\n * *believes* an upload has succeeded, and has been observed to advance even\n * when nothing is actually written to the destination.\n *\n * Rejects if the backup state cannot be determined (spawn error, non-zero\n * exit, timeout, or empty/unparseable listing).\n */\nexport async function getLastBackupTime(\n lc: LogContext,\n config: ZeroConfig,\n): Promise<Date> {\n const [snapshots, wal] = await Promise.all([\n listBackupCreatedTimes(lc, config, 'snapshots'),\n listBackupCreatedTimes(lc, config, 'wal'),\n ]);\n const times = [...snapshots, ...wal];\n if (times.length === 0) {\n // Note: the litestream CLI exits with code 0 and logs listing errors\n // (e.g. S3 failures) to stderr, so an empty listing cannot be\n // distinguished from a failed one. Since a valid backup always contains\n // at least one snapshot, an empty listing is treated as a failure.\n throw new Error(\n `no snapshots or WAL segments listed at ${config.litestream.backupURL}`,\n );\n }\n return new Date(Math.max(...times.map(time => time.getTime())));\n}\n\n/**\n * Runs `litestream <snapshots|wal> <replica-file>` with the same config /\n * environment used by the `litestream replicate` process (so that the\n * backupURL, endpoint, region, and credentials are identical), and parses\n * the `created` column (RFC3339) of the tab-formatted output, e.g.:\n *\n * ```\n * replica generation index size created\n * s3 1862f44967b3863f 0 4546445 2026-06-10T01:11:32Z\n * ```\n */\nasync function listBackupCreatedTimes(\n lc: LogContext,\n config: ZeroConfig,\n command: 'snapshots' | 'wal',\n): Promise<Date[]> {\n const {litestream, env} = getLitestream('replicate', config);\n const proc = spawn(litestream, [command, config.replica.file], {\n env,\n stdio: ['ignore', 'pipe', 'inherit'],\n windowsHide: true,\n });\n const {promise, resolve, reject} = resolver<string>();\n let stdout = '';\n proc.stdout.setEncoding('utf-8');\n proc.stdout.on('data', chunk => (stdout += chunk));\n proc.on('error', reject);\n proc.on('close', (code, signal) => {\n if (signal) {\n reject(new Error(`litestream ${command} killed with ${signal}`));\n } else if (code !== 0) {\n reject(new Error(`litestream ${command} exited with code ${code}`));\n } else {\n resolve(stdout);\n }\n });\n const timeout = setTimeout(() => {\n reject(new Error(`timed out listing backup state (litestream ${command})`));\n proc.kill('SIGKILL');\n }, LIST_BACKUP_TIMEOUT_MS);\n\n let output: string;\n try {\n output = await promise;\n } finally {\n clearTimeout(timeout);\n }\n\n return parseBackupCreatedTimes(lc, command, output);\n}\n\n/**\n * Parses the `created` column (the last, RFC3339-formatted column) from the\n * tab-formatted output of `litestream snapshots` / `litestream wal`. The\n * header row and any unparseable lines are skipped.\n *\n * Exported for testing.\n */\nexport function parseBackupCreatedTimes(\n lc: LogContext,\n command: 'snapshots' | 'wal',\n output: string,\n): Date[] {\n const times: Date[] = [];\n for (const line of output.split('\\n')) {\n const cols = line.trim().split(wsRe);\n const created = cols.at(-1);\n if (\n cols.length < 2 ||\n created === undefined ||\n created === 'created' /* header row */\n ) {\n continue;\n }\n const time = new Date(created);\n if (Number.isNaN(time.getTime())) {\n lc.warn?.(`unexpected line in litestream ${command} output: ${line}`);\n continue;\n }\n times.push(time);\n }\n return times;\n}\n\nfunction reserveAndGetSnapshotStatus(\n lc: LogContext,\n config: ZeroConfig,\n): Promise<SnapshotStatus> {\n const {promise: status, resolve, reject} = resolver<SnapshotStatus>();\n\n void (async function () {\n const abort = new AbortController();\n process.on('SIGINT', () => abort.abort());\n process.on('SIGTERM', () => abort.abort());\n\n for (let i = 0; ; i++) {\n let err: unknown;\n try {\n let resolved = false;\n const stream = await reserveSnapshot(lc, config);\n for await (const msg of stream) {\n // Capture the value of the status message that the change-streamer\n // backup monitor returns, and hold the connection open to\n // \"reserve\" the snapshot and prevent change log cleanup.\n resolve(msg[1]);\n resolved = true;\n }\n // The change-streamer itself closes the connection when the\n // subscription is started (or the reservation retried).\n if (resolved) {\n break;\n }\n } catch (e) {\n err = e;\n }\n // Retry in the view-syncer since it cannot proceed until it connects\n // to a (compatible) replication-manager. In particular, a\n // replication-manager that does not support the view-syncer's\n // change-streamer protocol will close the stream with an error; this\n // retry logic essentially delays the startup of a view-syncer until\n // a compatible replication-manager has been rolled out, allowing\n // replication-manager and view-syncer services to be updated in\n // parallel.\n lc.warn?.(\n `Unable to reserve snapshot (attempt ${i + 1}). Retrying in 5 seconds.`,\n String(err),\n );\n try {\n await sleep(5000, abort.signal);\n } catch (e) {\n return reject(e);\n }\n }\n })();\n\n return status;\n}\n\nfunction reserveSnapshot(\n lc: LogContext,\n config: ZeroConfig,\n): Promise<Source<SnapshotMessage>> {\n assertNormalized(config);\n const {taskID, change, changeStreamer} = config;\n const shardID = getShardConfig(config);\n\n const changeStreamerClient = new ChangeStreamerHttpClient(\n lc,\n shardID,\n change.db,\n changeStreamer.uri,\n );\n\n return changeStreamerClient.reserveSnapshot(taskID);\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parse, valita_exports } from "../../../../shared/src/valita.js";
|
|
2
2
|
import { Database } from "../../../../zqlite/src/db.js";
|
|
3
|
-
import
|
|
3
|
+
import Sqlite3Database from "@rocicorp/zero-sqlite3";
|
|
4
4
|
//#region ../zero-cache/src/services/litestream/vfs-watermark-reader.ts
|
|
5
5
|
var DEFAULT_VFS_DATABASE_URI = "file:zero-backup.db?vfs=litestream&mode=ro";
|
|
6
6
|
var DEFAULT_VFS_LOG_LEVEL = "info";
|
|
@@ -27,7 +27,7 @@ var VfsBackupWatermarkReader = class {
|
|
|
27
27
|
env["LITESTREAM_REPLICA_URL"] = options.replicaURL;
|
|
28
28
|
env["LITESTREAM_LOG_LEVEL"] = (options.logLevel ?? DEFAULT_VFS_LOG_LEVEL).toUpperCase();
|
|
29
29
|
setOptionalEnv(env, "LITESTREAM_LOG_FILE", options.logFile);
|
|
30
|
-
const loaderFactory = deps.loaderFactory ?? (() => new
|
|
30
|
+
const loaderFactory = deps.loaderFactory ?? (() => new Sqlite3Database(":memory:"));
|
|
31
31
|
const databaseFactory = deps.databaseFactory ?? ((lc, uri) => new Database(lc, uri, { readonly: true }));
|
|
32
32
|
this.#loader = loaderFactory();
|
|
33
33
|
this.#loader.loadExtension(options.extensionPath);
|
|
@@ -29,6 +29,8 @@ type QueryInfo = {
|
|
|
29
29
|
readonly transformationHash: string;
|
|
30
30
|
readonly queryName?: string | undefined;
|
|
31
31
|
};
|
|
32
|
+
export type PipelineHydrationReason = 'query-set-sync' | 'unchanged-query-rehydrate';
|
|
33
|
+
type PipelineStopReason = 'replace-query' | 'remove-query' | 'reset' | 'destroy';
|
|
32
34
|
export type Timer = {
|
|
33
35
|
elapsedLap: () => number;
|
|
34
36
|
totalElapsed: () => number;
|
|
@@ -92,12 +94,12 @@ export declare class PipelineDriver {
|
|
|
92
94
|
* when yielding the thread for time-slicing).
|
|
93
95
|
* @return The rows from the initial hydration of the query.
|
|
94
96
|
*/
|
|
95
|
-
addQuery(transformationHash: string, queryID: string, query: AST, timer: Timer, queryName?: string): Iterable<RowChange | 'yield'>;
|
|
97
|
+
addQuery(transformationHash: string, queryID: string, query: AST, timer: Timer, queryName?: string, hydrationReason?: PipelineHydrationReason): Iterable<RowChange | 'yield'>;
|
|
96
98
|
/**
|
|
97
99
|
* Removes the pipeline for the query. This is a no-op if the query
|
|
98
100
|
* was not added.
|
|
99
101
|
*/
|
|
100
|
-
removeQuery(queryID: string): void;
|
|
102
|
+
removeQuery(queryID: string, stopReason?: PipelineStopReason): void;
|
|
101
103
|
/**
|
|
102
104
|
* Current XOR signature of the row-set attached to `queryID`, or
|
|
103
105
|
* `undefined` if no pipeline for the query is currently active.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline-driver.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/pipeline-driver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"pipeline-driver.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/pipeline-driver.ts"],"names":[],"mappings":"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"}
|