@rocicorp/zero 1.8.0 → 1.9.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/shared/src/cache.d.ts +8 -1
- package/out/shared/src/cache.d.ts.map +1 -1
- package/out/shared/src/cache.js +25 -9
- package/out/shared/src/cache.js.map +1 -1
- package/out/shared/src/objects.d.ts +6 -1
- package/out/shared/src/objects.d.ts.map +1 -1
- package/out/shared/src/objects.js +19 -4
- package/out/shared/src/objects.js.map +1 -1
- package/out/zero/package.js +2 -19
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +4 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +21 -6
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.d.ts +6 -0
- package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.js +8 -0
- package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
- package/out/zero-cache/src/db/migration-lite.d.ts.map +1 -1
- package/out/zero-cache/src/db/migration-lite.js +5 -2
- package/out/zero-cache/src/db/migration-lite.js.map +1 -1
- package/out/zero-cache/src/db/migration.d.ts.map +1 -1
- package/out/zero-cache/src/db/migration.js +3 -1
- package/out/zero-cache/src/db/migration.js.map +1 -1
- package/out/zero-cache/src/db/sqlite-corruption.d.ts +11 -0
- package/out/zero-cache/src/db/sqlite-corruption.d.ts.map +1 -0
- package/out/zero-cache/src/db/sqlite-corruption.js +191 -0
- package/out/zero-cache/src/db/sqlite-corruption.js.map +1 -0
- package/out/zero-cache/src/server/anonymous-otel-start.d.ts +1 -1
- package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
- package/out/zero-cache/src/server/backup-watermark-reader.js +1 -1
- package/out/zero-cache/src/server/backup-watermark-reader.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +7 -2
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/logging.d.ts +8 -0
- package/out/zero-cache/src/server/logging.d.ts.map +1 -1
- package/out/zero-cache/src/server/logging.js +27 -5
- package/out/zero-cache/src/server/logging.js.map +1 -1
- package/out/zero-cache/src/server/main.d.ts.map +1 -1
- package/out/zero-cache/src/server/main.js +6 -1
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/mutator.js +1 -1
- package/out/zero-cache/src/server/mutator.js.map +1 -1
- package/out/zero-cache/src/server/otel-log-sink.d.ts +7 -1
- package/out/zero-cache/src/server/otel-log-sink.d.ts.map +1 -1
- package/out/zero-cache/src/server/otel-log-sink.js +7 -2
- package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
- package/out/zero-cache/src/server/reaper.js +1 -1
- package/out/zero-cache/src/server/reaper.js.map +1 -1
- package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/server/replicator.js +11 -1
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/shadow-syncer.js +1 -1
- package/out/zero-cache/src/server/shadow-syncer.js.map +1 -1
- package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/server/syncer.js +6 -1
- 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 +8 -5
- package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts +36 -2
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +76 -33
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +15 -0
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js +41 -10
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts +21 -0
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js +34 -5
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts +2 -2
- package/out/zero-cache/src/services/change-source/pg/replication-slots.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +61 -87
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +17 -12
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +15 -3
- package/out/zero-cache/src/services/life-cycle.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 +20 -3
- 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/mutagen/mutagen.d.ts.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +4 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.js +28 -15
- package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.js +5 -12
- package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
- package/out/zero-cache/src/services/replicator/write-worker.js +38 -7
- package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +8 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- 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 +66 -26
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/query-covering.d.ts +28 -0
- package/out/zero-cache/src/services/view-syncer/query-covering.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/query-covering.js +159 -0
- package/out/zero-cache/src/services/view-syncer/query-covering.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +2 -0
- 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 +71 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/lite.d.ts.map +1 -1
- package/out/zero-cache/src/types/lite.js +4 -1
- package/out/zero-cache/src/types/lite.js.map +1 -1
- package/out/zero-cache/src/types/pg.d.ts +18 -0
- package/out/zero-cache/src/types/pg.d.ts.map +1 -1
- package/out/zero-cache/src/types/pg.js +50 -0
- package/out/zero-cache/src/types/pg.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +1 -0
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/crud.d.ts.map +1 -1
- package/out/zero-client/src/client/crud.js +3 -4
- package/out/zero-client/src/client/crud.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-schema/src/builder/relationship-builder.d.ts.map +1 -1
- package/out/zero-schema/src/builder/relationship-builder.js +3 -2
- package/out/zero-schema/src/builder/relationship-builder.js.map +1 -1
- package/out/zero-schema/src/builder/schema-builder.d.ts.map +1 -1
- package/out/zero-schema/src/builder/schema-builder.js +7 -7
- package/out/zero-schema/src/builder/schema-builder.js.map +1 -1
- package/out/zero-schema/src/builder/table-builder.d.ts.map +1 -1
- package/out/zero-schema/src/builder/table-builder.js +2 -1
- package/out/zero-schema/src/builder/table-builder.js.map +1 -1
- package/out/zero-server/src/custom.js +2 -1
- package/out/zero-server/src/custom.js.map +1 -1
- package/out/zql/src/ivm/view-apply-change.d.ts.map +1 -1
- package/out/zql/src/ivm/view-apply-change.js +2 -1
- package/out/zql/src/ivm/view-apply-change.js.map +1 -1
- package/out/zql/src/mutate/crud.d.ts +10 -8
- package/out/zql/src/mutate/crud.d.ts.map +1 -1
- package/out/zql/src/mutate/crud.js +3 -4
- package/out/zql/src/mutate/crud.js.map +1 -1
- package/out/zqlite/src/db.js +2 -2
- package/out/zqlite/src/internal/statement-cache.d.ts +25 -1
- package/out/zqlite/src/internal/statement-cache.d.ts.map +1 -1
- package/out/zqlite/src/internal/statement-cache.js +48 -18
- package/out/zqlite/src/internal/statement-cache.js.map +1 -1
- package/out/zqlite/src/query-builder.js +3 -3
- 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 +2 -33
|
@@ -5,6 +5,7 @@ import { assertNormalized } from "../../config/normalize.js";
|
|
|
5
5
|
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
|
+
import { isSQLiteCorruption, logSQLiteCorruptionDiagnostics } from "../../db/sqlite-corruption.js";
|
|
8
9
|
import { deleteLiteDB } from "../../db/delete-lite-db.js";
|
|
9
10
|
import { ChangeStreamerHttpClient } from "../change-streamer/change-streamer-http.js";
|
|
10
11
|
import { litestreamBackupListDuration, litestreamBackupMetricAttrs, litestreamBackupProcessDuration, litestreamBackupProcessMetricAttrs, litestreamBackupProcessRuns, litestreamRestoreAttempts, litestreamRestoreDuration, litestreamRestoreMetricAttrs, litestreamRestoreProcessDuration, litestreamRestoreRuns, litestreamRestoreValidationDuration, litestreamRestoreWaitDuration, litestreamRestoredDbBytes } from "./metrics.js";
|
|
@@ -34,7 +35,7 @@ async function restoreReplica(lc, config, replicaConstraints) {
|
|
|
34
35
|
try {
|
|
35
36
|
for (;;) {
|
|
36
37
|
try {
|
|
37
|
-
const attempt = await tryRestore(lc, config, replicaConstraints, role);
|
|
38
|
+
const attempt = await tryRestore(lc, config, replicaConstraints, role, consecutiveErrors >= 1);
|
|
38
39
|
backupURL = attempt.backupURL;
|
|
39
40
|
if (attempt.restored) {
|
|
40
41
|
result = attempt.result;
|
|
@@ -92,7 +93,7 @@ function getLitestream(mode, config, logLevelOverride, backupURLOverride) {
|
|
|
92
93
|
}
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
|
-
async function tryRestore(lc, config, replicaConstraints, role) {
|
|
96
|
+
async function tryRestore(lc, config, replicaConstraints, role, isFinalAttempt) {
|
|
96
97
|
let snapshotStatus;
|
|
97
98
|
if (!replicaConstraints) {
|
|
98
99
|
const waitStart = performance.now();
|
|
@@ -122,9 +123,19 @@ async function tryRestore(lc, config, replicaConstraints, role) {
|
|
|
122
123
|
config.replica.file
|
|
123
124
|
], {
|
|
124
125
|
env,
|
|
125
|
-
stdio:
|
|
126
|
+
stdio: [
|
|
127
|
+
"ignore",
|
|
128
|
+
"pipe",
|
|
129
|
+
"pipe"
|
|
130
|
+
],
|
|
126
131
|
windowsHide: true
|
|
127
132
|
});
|
|
133
|
+
let stdout = "";
|
|
134
|
+
let stderr = "";
|
|
135
|
+
proc.stdout.setEncoding("utf-8");
|
|
136
|
+
proc.stderr.setEncoding("utf-8");
|
|
137
|
+
proc.stdout.on("data", (chunk) => stdout += chunk);
|
|
138
|
+
proc.stderr.on("data", (chunk) => stderr += chunk);
|
|
128
139
|
const { promise, resolve, reject } = resolver();
|
|
129
140
|
proc.on("error", reject);
|
|
130
141
|
proc.on("close", (code, signal) => {
|
|
@@ -139,11 +150,16 @@ async function tryRestore(lc, config, replicaConstraints, role) {
|
|
|
139
150
|
...attrs,
|
|
140
151
|
result: "success"
|
|
141
152
|
});
|
|
153
|
+
if (stdout) process.stdout.write(stdout);
|
|
154
|
+
if (stderr) process.stderr.write(stderr);
|
|
142
155
|
} catch (e) {
|
|
143
156
|
litestreamRestoreProcessDuration().recordMs(performance.now() - processStart, {
|
|
144
157
|
...attrs,
|
|
145
158
|
result: "error"
|
|
146
159
|
});
|
|
160
|
+
const output = (stdout + stderr).trim();
|
|
161
|
+
if (isFinalAttempt) lc.error?.(`litestream restore failed after retry`, output || e);
|
|
162
|
+
else lc.error?.(`litestream restore attempt failed, will retry`, output || e);
|
|
147
163
|
throw e;
|
|
148
164
|
}
|
|
149
165
|
if (!existsSync(config.replica.file)) {
|
|
@@ -203,6 +219,7 @@ function replicaIsValid(lc, replica, constraints) {
|
|
|
203
219
|
lc.info?.(`Local replica at version ${replicaVersion} and watermark ${watermark} is compatible`, constraints);
|
|
204
220
|
return true;
|
|
205
221
|
} catch (e) {
|
|
222
|
+
if (isSQLiteCorruption(e)) logSQLiteCorruptionDiagnostics(lc, "restored replica", replica, e);
|
|
206
223
|
lc.error?.("Error while validating restored replica", e);
|
|
207
224
|
return false;
|
|
208
225
|
} finally {
|
|
@@ -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, statSync} 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';\nimport {\n litestreamBackupListDuration,\n litestreamBackupMetricAttrs,\n litestreamBackupProcessMetricAttrs,\n litestreamBackupProcessDuration,\n litestreamBackupProcessRuns,\n litestreamRestoreAttempts,\n litestreamRestoredDbBytes,\n litestreamRestoreDuration,\n litestreamRestoreMetricAttrs,\n litestreamRestoreProcessDuration,\n litestreamRestoreRuns,\n litestreamRestoreValidationDuration,\n litestreamRestoreWaitDuration,\n type LitestreamRole,\n} from './metrics.ts';\n\nconst RETRY_INTERVAL_MS = 3000;\n\ntype ReplicaConstraints = {\n replicaVersion: string;\n minWatermark: string;\n};\n\ntype RestoreResult = 'success' | 'no_backup' | 'invalid_replica' | 'error';\n\ntype RestoreAttempt = {\n restored: boolean;\n backupURL: string | undefined;\n result: RestoreResult;\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 const start = performance.now();\n const role: LitestreamRole = replicaConstraints\n ? 'replication_manager'\n : 'view_syncer';\n let backupURL = config.litestream.backupURL;\n let result: RestoreResult | undefined;\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 try {\n for (;;) {\n try {\n const attempt = await tryRestore(lc, config, replicaConstraints, role);\n backupURL = attempt.backupURL;\n if (attempt.restored) {\n result = attempt.result;\n return;\n }\n consecutiveErrors = 0;\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 result = attempt.result;\n throw new BackupNotFoundException(config.litestream.backupURL);\n }\n } catch (e) {\n if (e instanceof BackupNotFoundException) {\n throw e;\n }\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 lc.info?.(\n `replica not found. retrying in ${RETRY_INTERVAL_MS / 1000} seconds`,\n );\n await sleep(RETRY_INTERVAL_MS);\n }\n } catch (e) {\n if (e instanceof BackupNotFoundException && result === undefined) {\n result = 'no_backup';\n }\n throw e;\n } finally {\n const attrs = litestreamRestoreMetricAttrs(config, role, backupURL);\n const labels = {...attrs, result: result ?? 'error'};\n litestreamRestoreRuns().add(1, labels);\n litestreamRestoreDuration().recordMs(performance.now() - start, labels);\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 role: LitestreamRole,\n): Promise<RestoreAttempt> {\n let snapshotStatus: SnapshotStatus | undefined;\n if (!replicaConstraints) {\n // view-syncers fetch replica constraints from the replication-manager\n // via the snapshot protocol.\n const waitStart = performance.now();\n snapshotStatus = await reserveAndGetSnapshotStatus(lc, config);\n litestreamRestoreWaitDuration().recordMs(\n performance.now() - waitStart,\n litestreamRestoreMetricAttrs(config, role, snapshotStatus.backupURL),\n );\n lc.info?.(`restoring backup from ${snapshotStatus.backupURL}`);\n replicaConstraints = snapshotStatus;\n }\n\n const backupURL = snapshotStatus?.backupURL ?? config.litestream.backupURL;\n const attrs = litestreamRestoreMetricAttrs(config, role, backupURL);\n let result: RestoreResult = 'error';\n try {\n const replicaExistedBeforeRestore = existsSync(config.replica.file);\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 {\n restoreParallelism: parallelism,\n multipartConcurrency,\n multipartSize,\n } = config.litestream;\n lc.info?.(`starting litestream restore`, {\n restoreParallelism: parallelism,\n multipartConcurrency,\n multipartSize,\n });\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 const processStart = performance.now();\n try {\n await promise;\n litestreamRestoreProcessDuration().recordMs(\n performance.now() - processStart,\n {...attrs, result: 'success'},\n );\n } catch (e) {\n litestreamRestoreProcessDuration().recordMs(\n performance.now() - processStart,\n {...attrs, result: 'error'},\n );\n throw e;\n }\n if (!existsSync(config.replica.file)) {\n result = 'no_backup';\n return {restored: false, backupURL, result};\n }\n const validationStart = performance.now();\n const valid = replicaIsValid(lc, config.replica.file, replicaConstraints);\n litestreamRestoreValidationDuration().recordMs(\n performance.now() - validationStart,\n {...attrs, result: valid ? 'success' : 'invalid_replica'},\n );\n if (!valid) {\n result = 'invalid_replica';\n lc.info?.(`Deleting local replica and retrying restore`);\n deleteLiteDB(config.replica.file);\n return {restored: false, backupURL, result};\n }\n result = 'success';\n if (!replicaExistedBeforeRestore) {\n litestreamRestoredDbBytes().add(statSync(config.replica.file).size, {\n ...attrs,\n result: 'success',\n });\n }\n return {restored: true, backupURL, result};\n } finally {\n litestreamRestoreAttempts().add(1, {\n ...attrs,\n result,\n });\n }\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 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 const attrs = litestreamBackupProcessMetricAttrs(config);\n lc.info?.(`starting litestream backup to ${config.litestream.backupURL}`);\n const start = performance.now();\n const proc = spawn(litestream, ['replicate'], {\n env,\n stdio: 'inherit',\n windowsHide: true,\n });\n let recorded = false;\n const record = (result: 'success' | 'error' | 'stopped') => {\n if (recorded) {\n return;\n }\n recorded = true;\n const labels = {...attrs, result};\n litestreamBackupProcessRuns().add(1, labels);\n litestreamBackupProcessDuration().recordMs(\n performance.now() - start,\n labels,\n );\n };\n proc.on('error', e => {\n lc.warn?.(`litestream backup process error`, e);\n record('error');\n });\n proc.on('close', (code, signal) => {\n if (signal) {\n lc.info?.(`litestream backup process stopped`, {signal});\n record('stopped');\n } else if (code === 0) {\n record('success');\n } else {\n lc.warn?.(`litestream backup process exited with code ${code}`);\n record('error');\n }\n });\n return proc;\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 start = performance.now();\n let result: 'success' | 'empty' | 'timeout' | 'error' = 'error';\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 result = 'timeout';\n reject(new Error(`timed out listing backup state (litestream ${command})`));\n proc.kill('SIGKILL');\n }, LIST_BACKUP_TIMEOUT_MS);\n\n try {\n const output = await promise;\n const times = parseBackupCreatedTimes(lc, command, output);\n result = times.length ? 'success' : 'empty';\n return times;\n } finally {\n clearTimeout(timeout);\n litestreamBackupListDuration().recordMs(performance.now() - start, {\n ...litestreamBackupMetricAttrs(config),\n command,\n result,\n });\n }\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, statSync} 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 {\n isSQLiteCorruption,\n logSQLiteCorruptionDiagnostics,\n} from '../../db/sqlite-corruption.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';\nimport {\n litestreamBackupListDuration,\n litestreamBackupMetricAttrs,\n litestreamBackupProcessMetricAttrs,\n litestreamBackupProcessDuration,\n litestreamBackupProcessRuns,\n litestreamRestoreAttempts,\n litestreamRestoredDbBytes,\n litestreamRestoreDuration,\n litestreamRestoreMetricAttrs,\n litestreamRestoreProcessDuration,\n litestreamRestoreRuns,\n litestreamRestoreValidationDuration,\n litestreamRestoreWaitDuration,\n type LitestreamRole,\n} from './metrics.ts';\n\nconst RETRY_INTERVAL_MS = 3000;\n\ntype ReplicaConstraints = {\n replicaVersion: string;\n minWatermark: string;\n};\n\ntype RestoreResult = 'success' | 'no_backup' | 'invalid_replica' | 'error';\n\ntype RestoreAttempt = {\n restored: boolean;\n backupURL: string | undefined;\n result: RestoreResult;\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 const start = performance.now();\n const role: LitestreamRole = replicaConstraints\n ? 'replication_manager'\n : 'view_syncer';\n let backupURL = config.litestream.backupURL;\n let result: RestoreResult | undefined;\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 try {\n for (;;) {\n try {\n // `consecutiveErrors` is 0 on a fresh attempt and 1 on the retry, so\n // `consecutiveErrors >= 1` identifies the final attempt of a streak.\n // tryRestore surfaces litestream's failure output with a different\n // message on the final attempt (which the log classifier routes to a\n // paging alert) than on a retriable earlier attempt (routed to a\n // non-paging warning). See INC-961.\n const attempt = await tryRestore(\n lc,\n config,\n replicaConstraints,\n role,\n consecutiveErrors >= 1,\n );\n backupURL = attempt.backupURL;\n if (attempt.restored) {\n result = attempt.result;\n return;\n }\n consecutiveErrors = 0;\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 result = attempt.result;\n throw new BackupNotFoundException(config.litestream.backupURL);\n }\n } catch (e) {\n if (e instanceof BackupNotFoundException) {\n throw e;\n }\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 lc.info?.(\n `replica not found. retrying in ${RETRY_INTERVAL_MS / 1000} seconds`,\n );\n await sleep(RETRY_INTERVAL_MS);\n }\n } catch (e) {\n if (e instanceof BackupNotFoundException && result === undefined) {\n result = 'no_backup';\n }\n throw e;\n } finally {\n const attrs = litestreamRestoreMetricAttrs(config, role, backupURL);\n const labels = {...attrs, result: result ?? 'error'};\n litestreamRestoreRuns().add(1, labels);\n litestreamRestoreDuration().recordMs(performance.now() - start, labels);\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 role: LitestreamRole,\n isFinalAttempt: boolean,\n): Promise<RestoreAttempt> {\n let snapshotStatus: SnapshotStatus | undefined;\n if (!replicaConstraints) {\n // view-syncers fetch replica constraints from the replication-manager\n // via the snapshot protocol.\n const waitStart = performance.now();\n snapshotStatus = await reserveAndGetSnapshotStatus(lc, config);\n litestreamRestoreWaitDuration().recordMs(\n performance.now() - waitStart,\n litestreamRestoreMetricAttrs(config, role, snapshotStatus.backupURL),\n );\n lc.info?.(`restoring backup from ${snapshotStatus.backupURL}`);\n replicaConstraints = snapshotStatus;\n }\n\n const backupURL = snapshotStatus?.backupURL ?? config.litestream.backupURL;\n const attrs = litestreamRestoreMetricAttrs(config, role, backupURL);\n let result: RestoreResult = 'error';\n try {\n const replicaExistedBeforeRestore = existsSync(config.replica.file);\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 {\n restoreParallelism: parallelism,\n multipartConcurrency,\n multipartSize,\n } = config.litestream;\n lc.info?.(`starting litestream restore`, {\n restoreParallelism: parallelism,\n multipartConcurrency,\n multipartSize,\n });\n // Pipe (rather than inherit) litestream's stdout/stderr so that its own\n // `\"level\":\"ERROR\"` output on a failed restore does not go straight to the\n // pod's stdout — where a log-scraper alert would page on it — before our\n // code has decided whether the failure is retriable. The captured output is\n // re-surfaced through `lc` below with a distinct message per attempt, so a\n // retriable failure is routed to a non-paging warning and only a post-retry\n // failure pages. See INC-961.\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: ['ignore', 'pipe', 'pipe'], windowsHide: true},\n );\n let stdout = '';\n let stderr = '';\n proc.stdout.setEncoding('utf-8');\n proc.stderr.setEncoding('utf-8');\n proc.stdout.on('data', chunk => (stdout += chunk));\n proc.stderr.on('data', chunk => (stderr += chunk));\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 const processStart = performance.now();\n try {\n await promise;\n litestreamRestoreProcessDuration().recordMs(\n performance.now() - processStart,\n {...attrs, result: 'success'},\n );\n // A successful restore does not emit ERROR-level output; forward\n // litestream's own (minimal, debug-level) restore logging to the pod's\n // stdout/stderr, matching the previous `stdio: 'inherit'` behavior.\n if (stdout) {\n process.stdout.write(stdout);\n }\n if (stderr) {\n process.stderr.write(stderr);\n }\n } catch (e) {\n litestreamRestoreProcessDuration().recordMs(\n performance.now() - processStart,\n {...attrs, result: 'error'},\n );\n // litestream logs its restore failure at ERROR. Re-surface that captured\n // output through our logger with a distinct message per attempt, so the\n // cloudzero log classifier can route them differently: the retriable\n // earlier attempt to a non-paging warning, and the post-retry failure to\n // a paging critical. Both are emitted at ERROR because the log scraper\n // only ingests error-level lines, and both carry our `worker` context,\n // distinguishing them from litestream's untagged raw output. See INC-961.\n const output = (stdout + stderr).trim();\n if (isFinalAttempt) {\n lc.error?.(`litestream restore failed after retry`, output || e);\n } else {\n lc.error?.(\n `litestream restore attempt failed, will retry`,\n output || e,\n );\n }\n throw e;\n }\n if (!existsSync(config.replica.file)) {\n result = 'no_backup';\n return {restored: false, backupURL, result};\n }\n const validationStart = performance.now();\n const valid = replicaIsValid(lc, config.replica.file, replicaConstraints);\n litestreamRestoreValidationDuration().recordMs(\n performance.now() - validationStart,\n {...attrs, result: valid ? 'success' : 'invalid_replica'},\n );\n if (!valid) {\n result = 'invalid_replica';\n lc.info?.(`Deleting local replica and retrying restore`);\n deleteLiteDB(config.replica.file);\n return {restored: false, backupURL, result};\n }\n result = 'success';\n if (!replicaExistedBeforeRestore) {\n litestreamRestoredDbBytes().add(statSync(config.replica.file).size, {\n ...attrs,\n result: 'success',\n });\n }\n return {restored: true, backupURL, result};\n } finally {\n litestreamRestoreAttempts().add(1, {\n ...attrs,\n result,\n });\n }\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 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 if (isSQLiteCorruption(e)) {\n logSQLiteCorruptionDiagnostics(lc, 'restored replica', replica, e);\n }\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 const attrs = litestreamBackupProcessMetricAttrs(config);\n lc.info?.(`starting litestream backup to ${config.litestream.backupURL}`);\n const start = performance.now();\n const proc = spawn(litestream, ['replicate'], {\n env,\n stdio: 'inherit',\n windowsHide: true,\n });\n let recorded = false;\n const record = (result: 'success' | 'error' | 'stopped') => {\n if (recorded) {\n return;\n }\n recorded = true;\n const labels = {...attrs, result};\n litestreamBackupProcessRuns().add(1, labels);\n litestreamBackupProcessDuration().recordMs(\n performance.now() - start,\n labels,\n );\n };\n proc.on('error', e => {\n lc.warn?.(`litestream backup process error`, e);\n record('error');\n });\n proc.on('close', (code, signal) => {\n if (signal) {\n lc.info?.(`litestream backup process stopped`, {signal});\n record('stopped');\n } else if (code === 0) {\n record('success');\n } else {\n lc.warn?.(`litestream backup process exited with code ${code}`);\n record('error');\n }\n });\n return proc;\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 start = performance.now();\n let result: 'success' | 'empty' | 'timeout' | 'error' = 'error';\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 result = 'timeout';\n reject(new Error(`timed out listing backup state (litestream ${command})`));\n proc.kill('SIGKILL');\n }, LIST_BACKUP_TIMEOUT_MS);\n\n try {\n const output = await promise;\n const times = parseBackupCreatedTimes(lc, command, output);\n result = times.length ? 'success' : 'empty';\n return times;\n } finally {\n clearTimeout(timeout);\n litestreamBackupListDuration().recordMs(performance.now() - start, {\n ...litestreamBackupMetricAttrs(config),\n command,\n result,\n });\n }\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);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutagen.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/mutagen/mutagen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,MAAM,CAAC;AACrC,OAAO,QAAQ,MAAM,UAAU,CAAC;AAGhC,OAAO,EAAC,SAAS,EAAC,MAAM,6CAA6C,CAAC;AAOtE,OAAO,EACL,KAAK,YAAY,EAEjB,KAAK,QAAQ,EACb,KAAK,QAAQ,EAEb,KAAK,QAAQ,EACd,MAAM,2CAA2C,CAAC;AAKnD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,4CAA4C,CAAC;AAEhF,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,6BAA6B,CAAC;AAK5D,OAAO,KAAK,EAAC,UAAU,EAAE,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAiB,KAAK,OAAO,EAAC,MAAM,uBAAuB,CAAC;AAEnE,OAAO,KAAK,EAAC,iBAAiB,EAAE,OAAO,EAAC,MAAM,eAAe,CAAC;AAK9D,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,SAAS,CAAC,cAAc,GAAG,SAAS,CAAC,mBAAmB;IAC9D,IAAI,EAAE,MAAM;CACb,CAAC;AAEF,MAAM,WAAW,OAAQ,SAAQ,iBAAiB;IAChD,eAAe,CACb,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,UAAU,GAAG,SAAS,EAChC,qBAAqB,EAAE,OAAO,GAC7B,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;CACvC;AAED,qBAAa,cAAe,YAAW,OAAO,EAAE,OAAO;;IACrD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;gBAkBlB,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,OAAO,EACd,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,UAAU,EACpB,MAAM,EAAE,UAAU,EAClB,iBAAiB,EAAE,eAAe;IA0BpC,GAAG;IAKH,KAAK;IAQL,OAAO,IAAI,OAAO;IAIlB,eAAe,CACb,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,UAAU,GAAG,SAAS,EAChC,qBAAqB,UAAQ,GAC5B,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAuBrC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAIpB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAStB;AAID,wBAAsB,eAAe,CACnC,EAAE,EAAE,UAAU,EACd,QAAQ,EAAE,UAAU,GAAG,SAAS,EAChC,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,OAAO,EACd,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,QAAQ,EAClB,eAAe,EAAE,eAAe,EAChC,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAAE,cAAc;AACtD,qBAAqB,UAAQ,GAC5B,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAqIpC;AAED,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,mBAAmB,EACvB,QAAQ,EAAE,UAAU,GAAG,SAAS,EAChC,KAAK,EAAE,OAAO,EACd,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,YAAY,EACtB,SAAS,EAAE,OAAO,EAClB,UAAU,EAAE,eAAe,iBA8D5B;AAED,wBAAgB,YAAY,CAC1B,EAAE,EAAE,QAAQ,CAAC,cAAc,EAC3B,MAAM,EAAE,QAAQ,GACf,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"mutagen.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/mutagen/mutagen.ts"],"names":[],"mappings":"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"}
|
|
@@ -156,7 +156,10 @@ async function processMutationWithTx(lc, tx, authData, shard, clientGroupID, mut
|
|
|
156
156
|
await Promise.all(tasks.map((task) => task()));
|
|
157
157
|
}
|
|
158
158
|
function getInsertSQL(tx, create) {
|
|
159
|
-
return tx`
|
|
159
|
+
return tx`
|
|
160
|
+
INSERT INTO ${tx(create.tableName)} ${tx(create.value)}
|
|
161
|
+
ON CONFLICT (${tx(create.primaryKey)}) DO NOTHING
|
|
162
|
+
`;
|
|
160
163
|
}
|
|
161
164
|
function getUpsertSQL(tx, set) {
|
|
162
165
|
const { tableName, primaryKey, value } = set;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutagen.js","names":["#lc","#upstream","#shard","#stopped","#replica","#writeAuthorizer","#limiter","#crudMutations","#isStopped","#refCount"],"sources":["../../../../../../zero-cache/src/services/mutagen/mutagen.ts"],"sourcesContent":["import {PG_SERIALIZATION_FAILURE} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport type {JWTPayload} from 'jose';\nimport postgres from 'postgres';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {\n isProtocolError,\n ProtocolError,\n} from '../../../../zero-protocol/src/error.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {\n type CRUDMutation,\n type DeleteOp,\n type InsertOp,\n type Mutation,\n type UpdateOp,\n type UpsertOp,\n} from '../../../../zero-protocol/src/mutation.ts';\nimport {\n primaryKeyValueSchema,\n type PrimaryKeyValue,\n} from '../../../../zero-protocol/src/primary-key.ts';\nimport type {DatabaseStorage} from '../../../../zqlite/src/database-storage.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {\n WriteAuthorizerImpl,\n type WriteAuthorizer,\n} from '../../auth/write-authorizer.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport type {PostgresDB, PostgresTransaction} from '../../types/pg.ts';\nimport {upstreamSchema, type ShardID} from '../../types/shards.ts';\nimport {SlidingWindowLimiter} from '../limiter/sliding-window-limiter.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport {MutationAlreadyProcessedError} from './error.ts';\n\n// An error encountered processing a mutation.\n// Returned back to application for display to user.\nexport type MutationError = [\n kind: ErrorKind.MutationFailed | ErrorKind.MutationRateLimited,\n desc: string,\n];\n\nexport interface Mutagen extends RefCountedService {\n processMutation(\n mutation: Mutation,\n authData: JWTPayload | undefined,\n customMutatorsEnabled: boolean,\n ): Promise<MutationError | undefined>;\n}\n\nexport class MutagenService implements Mutagen, Service {\n readonly id: string;\n readonly #lc: LogContext;\n readonly #upstream: PostgresDB;\n readonly #shard: ShardID;\n readonly #stopped = resolver();\n readonly #replica: Database;\n readonly #writeAuthorizer: WriteAuthorizerImpl;\n readonly #limiter: SlidingWindowLimiter | undefined;\n #refCount = 0;\n #isStopped = false;\n\n readonly #crudMutations = getOrCreateCounter(\n 'mutation',\n 'crud',\n 'Number of CRUD mutations processed',\n );\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n clientGroupID: string,\n upstream: PostgresDB,\n config: ZeroConfig,\n writeAuthzStorage: DatabaseStorage,\n ) {\n this.id = clientGroupID;\n this.#lc = lc;\n this.#upstream = upstream;\n this.#shard = shard;\n this.#replica = new Database(this.#lc, config.replica.file, {\n fileMustExist: true,\n });\n this.#writeAuthorizer = new WriteAuthorizerImpl(\n this.#lc,\n config,\n this.#replica,\n shard.appID,\n clientGroupID,\n writeAuthzStorage,\n );\n\n if (config.perUserMutationLimit.max !== undefined) {\n this.#limiter = new SlidingWindowLimiter(\n config.perUserMutationLimit.windowMs,\n config.perUserMutationLimit.max,\n );\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'MutagenService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'MutagenService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n processMutation(\n mutation: Mutation,\n authData: JWTPayload | undefined,\n customMutatorsEnabled = false,\n ): Promise<MutationError | undefined> {\n if (this.#limiter?.canDo() === false) {\n return Promise.resolve([\n ErrorKind.MutationRateLimited,\n 'Rate limit exceeded',\n ]);\n }\n this.#crudMutations.add(1, {\n clientGroupID: this.id,\n });\n return processMutation(\n this.#lc,\n authData,\n this.#upstream,\n this.#shard,\n this.id,\n mutation,\n this.#writeAuthorizer,\n undefined,\n customMutatorsEnabled,\n );\n }\n\n run(): Promise<void> {\n return this.#stopped.promise;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return this.#stopped.promise;\n }\n this.#writeAuthorizer.destroy();\n this.#isStopped = true;\n this.#stopped.resolve();\n return this.#stopped.promise;\n }\n}\n\nconst MAX_SERIALIZATION_ATTEMPTS = 10;\n\nexport async function processMutation(\n lc: LogContext,\n authData: JWTPayload | undefined,\n db: PostgresDB,\n shard: ShardID,\n clientGroupID: string,\n mutation: Mutation,\n writeAuthorizer: WriteAuthorizer,\n onTxStart?: () => void | Promise<void>, // for testing\n customMutatorsEnabled = false,\n): Promise<MutationError | undefined> {\n assert(\n mutation.type === MutationType.CRUD,\n 'Only CRUD mutations are supported',\n );\n lc = lc.withContext('mutationID', mutation.id);\n lc = lc.withContext('processMutation');\n lc.debug?.('Process mutation start', mutation);\n\n // Record mutation processing attempt for telemetry (regardless of success/failure)\n recordMutation('crud');\n\n let result: MutationError | undefined;\n\n const start = Date.now();\n try {\n // Mutations can fail for a variety of reasons:\n //\n // - application error\n // - network/db error\n // - zero bug\n //\n // For application errors what we want is to re-run the mutation in\n // \"error mode\", which skips the actual mutation and just updates the\n // lastMutationID. Then return the error to the app.\n //\n // However, it's hard to tell the difference between application errors\n // and the other types.\n //\n // A reasonable policy ends up being to just retry every mutation once\n // in error mode. If the error mode mutation succeeds then we assume it\n // was an application error and return the error to the app. Otherwise,\n // we know it was something internal and we log it.\n //\n // This is not 100% correct - there are theoretical cases where we\n // return an internal error to the app that shouldn't have been. But it\n // would have to be a crazy coincidence: we'd have to have a network\n // error on the first attempt that resolves by the second attempt.\n //\n // One might ask why not try/catch just the calls to the mutators and\n // consider those application errors. That is actually what we do in\n // Replicache:\n //\n // https://github.com/rocicorp/todo-row-versioning/blob/9a0a79dc2d2de32c4fac61b5d1634bd9a9e66b7c/server/src/push.ts#L131\n //\n // We don't do it here because:\n //\n // 1. It's still not perfect. It's hard to isolate SQL errors in\n // mutators due to app developer mistakes from SQL errors due to\n // Zero mistakes.\n // 2. It's not possible to do this with the pg library we're using in\n // Zero anyway: https://github.com/porsager/postgres/issues/455.\n //\n // Personally I think this simple retry policy is nice.\n let errorMode = false;\n for (let i = 0; i < MAX_SERIALIZATION_ATTEMPTS; i++) {\n try {\n await runTx(\n db,\n async tx => {\n // Simulates a concurrent request for testing. In production this is a noop.\n const done = onTxStart?.();\n try {\n return await processMutationWithTx(\n lc,\n tx,\n authData,\n shard,\n clientGroupID,\n mutation,\n errorMode,\n writeAuthorizer,\n );\n } finally {\n await done;\n }\n },\n {mode: Mode.SERIALIZABLE},\n );\n if (errorMode) {\n lc.debug?.('Ran mutation successfully in error mode');\n }\n break;\n } catch (e) {\n if (e instanceof MutationAlreadyProcessedError) {\n lc.debug?.(e.message);\n // Don't double-count already processed mutations, but they were counted above\n return undefined;\n }\n if (\n isProtocolError(e) &&\n !errorMode &&\n e.kind === ErrorKind.InvalidPush &&\n customMutatorsEnabled &&\n i < 2\n ) {\n // We're temporarily supporting custom mutators AND CRUD mutators at the same time.\n // This can create a lot of OOO mutation errors since we do not know when the API server\n // has applied a custom mutation before moving on to process CRUD mutations.\n // The temporary workaround (since CRUD is being deprecated) is to retry the mutation\n // after a small delay. Users are not expected to be running both CRUD and Custom mutators.\n // They should migrate completely to custom mutators.\n lc.info?.(\n 'Both CRUD and Custom mutators are being used at once. This is supported for now but IS NOT RECOMMENDED. Migrate completely to custom mutators.',\n e,\n );\n await new Promise(resolve => setTimeout(resolve, 100));\n continue;\n }\n if (isProtocolError(e) || errorMode) {\n lc.error?.('Process mutation error', e);\n throw e;\n }\n if (\n e instanceof postgres.PostgresError &&\n e.code === PG_SERIALIZATION_FAILURE\n ) {\n lc.info?.(`attempt ${i + 1}: ${String(e)}`, e);\n continue; // Retry up to MAX_SERIALIZATION_ATTEMPTS.\n }\n result = [ErrorKind.MutationFailed, String(e)];\n if (errorMode) {\n break;\n }\n lc.error?.('Got error running mutation, re-running in error mode', e);\n errorMode = true;\n i--;\n }\n }\n } finally {\n lc.debug?.('Process mutation complete in', Date.now() - start);\n }\n return result;\n}\n\nexport async function processMutationWithTx(\n lc: LogContext,\n tx: PostgresTransaction,\n authData: JWTPayload | undefined,\n shard: ShardID,\n clientGroupID: string,\n mutation: CRUDMutation,\n errorMode: boolean,\n authorizer: WriteAuthorizer,\n) {\n const tasks: (() => Promise<unknown>)[] = [];\n\n async function execute(stmt: postgres.PendingQuery<postgres.Row[]>) {\n try {\n return await stmt.execute();\n } finally {\n const q = stmt as unknown as Query;\n lc.debug?.(`${q.string}: ${JSON.stringify(q.parameters)}`);\n }\n }\n\n authorizer.reloadPermissions();\n\n if (!errorMode) {\n const {ops} = mutation.args[0];\n\n // Validate all table names before processing\n authorizer.validateTableNames(ops);\n\n const normalizedOps = authorizer.normalizeOps(ops);\n const [canPre, canPost] = await Promise.all([\n authorizer.canPreMutation(authData, normalizedOps),\n authorizer.canPostMutation(authData, normalizedOps),\n ]);\n if (canPre && canPost) {\n for (const op of ops) {\n switch (op.op) {\n case 'insert':\n tasks.push(() => execute(getInsertSQL(tx, op)));\n break;\n case 'upsert':\n tasks.push(() => execute(getUpsertSQL(tx, op)));\n break;\n case 'update':\n tasks.push(() => execute(getUpdateSQL(tx, op)));\n break;\n case 'delete':\n tasks.push(() => execute(getDeleteSQL(tx, op)));\n break;\n default:\n unreachable(op);\n }\n }\n }\n }\n\n // Confirm the mutation even though it may have been blocked by the authorizer.\n // Authorizer blocking a mutation is not an error but the correct result of the mutation.\n tasks.unshift(() =>\n checkSchemaVersionAndIncrementLastMutationID(\n tx,\n shard,\n clientGroupID,\n mutation.clientID,\n mutation.id,\n ),\n );\n\n // Note: An error thrown from any Promise aborts the entire transaction.\n await Promise.all(tasks.map(task => task()));\n}\n\nexport function getInsertSQL(\n tx: postgres.TransactionSql,\n create: InsertOp,\n): postgres.PendingQuery<postgres.Row[]> {\n return tx`INSERT INTO ${tx(create.tableName)} ${tx(create.value)}`;\n}\n\nexport function getUpsertSQL(\n tx: postgres.TransactionSql,\n set: UpsertOp,\n): postgres.PendingQuery<postgres.Row[]> {\n const {tableName, primaryKey, value} = set;\n return tx`\n INSERT INTO ${tx(tableName)} ${tx(value)}\n ON CONFLICT (${tx(primaryKey)})\n DO UPDATE SET ${tx(value)}\n `;\n}\n\nfunction getUpdateSQL(\n tx: postgres.TransactionSql,\n update: UpdateOp,\n): postgres.PendingQuery<postgres.Row[]> {\n const table = update.tableName;\n const {primaryKey, value} = update;\n const id: Record<string, PrimaryKeyValue> = {};\n for (const key of primaryKey) {\n id[key] = v.parse(value[key], primaryKeyValueSchema);\n }\n return tx`UPDATE ${tx(table)} SET ${tx(value)} WHERE ${Object.entries(\n id,\n ).flatMap(([key, value], i) =>\n i ? [tx`AND`, tx`${tx(key)} = ${value}`] : tx`${tx(key)} = ${value}`,\n )}`;\n}\n\nfunction getDeleteSQL(\n tx: postgres.TransactionSql,\n deleteOp: DeleteOp,\n): postgres.PendingQuery<postgres.Row[]> {\n const {tableName, primaryKey, value} = deleteOp;\n\n const conditions = [];\n for (const key of primaryKey) {\n if (conditions.length > 0) {\n conditions.push(tx`AND`);\n }\n conditions.push(tx`${tx(key)} = ${value[key]}`);\n }\n\n return tx`DELETE FROM ${tx(tableName)} WHERE ${conditions}`;\n}\n\nasync function checkSchemaVersionAndIncrementLastMutationID(\n tx: PostgresTransaction,\n shard: ShardID,\n clientGroupID: string,\n clientID: string,\n receivedMutationID: number,\n) {\n const [{lastMutationID}] = await tx<{lastMutationID: bigint}[]>`\n INSERT INTO ${tx(upstreamSchema(shard))}.clients \n as current (\"clientGroupID\", \"clientID\", \"lastMutationID\")\n VALUES (${clientGroupID}, ${clientID}, ${1})\n ON CONFLICT (\"clientGroupID\", \"clientID\")\n DO UPDATE SET \"lastMutationID\" = current.\"lastMutationID\" + 1\n RETURNING \"lastMutationID\"\n `;\n\n // ABORT if the resulting lastMutationID is not equal to the receivedMutationID.\n if (receivedMutationID < lastMutationID) {\n throw new MutationAlreadyProcessedError(\n clientID,\n receivedMutationID,\n lastMutationID,\n );\n } else if (receivedMutationID > lastMutationID) {\n throw new ProtocolError({\n kind: ErrorKind.InvalidPush,\n message: `Push contains unexpected mutation id ${receivedMutationID} for client ${clientID}. Expected mutation id ${lastMutationID.toString()}.`,\n origin: ErrorOrigin.ZeroCache,\n });\n }\n}\n\n// The slice of information from the Query object in Postgres.js that gets logged for debugging.\n// https://github.com/porsager/postgres/blob/f58cd4f3affd3e8ce8f53e42799672d86cd2c70b/src/connection.js#L219\ntype Query = {string: string; parameters: object[]};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA0DA,IAAa,iBAAb,MAAwD;CACtD;CACA;CACA;CACA;CACA,WAAoB,SAAS;CAC7B;CACA;CACA;CACA,YAAY;CACZ,aAAa;CAEb,iBAA0B,mBACxB,YACA,QACA,oCACF;CAEA,YACE,IACA,OACA,eACA,UACA,QACA,mBACA;EACA,KAAK,KAAK;EACV,KAAKA,MAAM;EACX,KAAKC,YAAY;EACjB,KAAKC,SAAS;EACd,KAAKE,WAAW,IAAI,SAAS,KAAKJ,KAAK,OAAO,QAAQ,MAAM,EAC1D,eAAe,KACjB,CAAC;EACD,KAAKK,mBAAmB,IAAI,oBAC1B,KAAKL,KACL,QACA,KAAKI,UACL,MAAM,OACN,eACA,iBACF;EAEA,IAAI,OAAO,qBAAqB,QAAQ,KAAA,GACtC,KAAKE,WAAW,IAAI,qBAClB,OAAO,qBAAqB,UAC5B,OAAO,qBAAqB,GAC9B;CAEJ;CAEA,MAAM;EACJ,OAAO,CAAC,KAAKE,YAAY,mCAAmC;EAC5D,EAAE,KAAKC;CACT;CAEA,QAAQ;EACN,OAAO,CAAC,KAAKD,YAAY,mCAAmC;EAC5D,EAAE,KAAKC;EACP,IAAI,KAAKA,aAAa,GACpB,KAAU,KAAK;CAEnB;CAEA,UAAmB;EACjB,OAAO,KAAKA,YAAY;CAC1B;CAEA,gBACE,UACA,UACA,wBAAwB,OACY;EACpC,IAAI,KAAKH,UAAU,MAAM,MAAM,OAC7B,OAAO,QAAQ,QAAQ,CACrB,qBACA,qBACF,CAAC;EAEH,KAAKC,eAAe,IAAI,GAAG,EACzB,eAAe,KAAK,GACtB,CAAC;EACD,OAAO,gBACL,KAAKP,KACL,UACA,KAAKC,WACL,KAAKC,QACL,KAAK,IACL,UACA,KAAKG,kBACL,KAAA,GACA,qBACF;CACF;CAEA,MAAqB;EACnB,OAAO,KAAKF,SAAS;CACvB;CAEA,OAAsB;EACpB,IAAI,KAAKK,YACP,OAAO,KAAKL,SAAS;EAEvB,KAAKE,iBAAiB,QAAQ;EAC9B,KAAKG,aAAa;EAClB,KAAKL,SAAS,QAAQ;EACtB,OAAO,KAAKA,SAAS;CACvB;AACF;AAEA,IAAM,6BAA6B;AAEnC,eAAsB,gBACpB,IACA,UACA,IACA,OACA,eACA,UACA,iBACA,WACA,wBAAwB,OACY;CACpC,OACE,SAAS,SAAS,MAClB,mCACF;CACA,KAAK,GAAG,YAAY,cAAc,SAAS,EAAE;CAC7C,KAAK,GAAG,YAAY,iBAAiB;CACrC,GAAG,QAAQ,0BAA0B,QAAQ;CAG7C,eAAe,MAAM;CAErB,IAAI;CAEJ,MAAM,QAAQ,KAAK,IAAI;CACvB,IAAI;EAuCF,IAAI,YAAY;EAChB,KAAK,IAAI,IAAI,GAAG,IAAI,4BAA4B,KAC9C,IAAI;GACF,MAAM,MACJ,IACA,OAAM,OAAM;IAEV,MAAM,OAAO,YAAY;IACzB,IAAI;KACF,OAAO,MAAM,sBACX,IACA,IACA,UACA,OACA,eACA,UACA,WACA,eACF;IACF,UAAU;KACR,MAAM;IACR;GACF,GACA,EAAC,MAAM,aAAiB,CAC1B;GACA,IAAI,WACF,GAAG,QAAQ,yCAAyC;GAEtD;EACF,SAAS,GAAG;GACV,IAAI,aAAa,+BAA+B;IAC9C,GAAG,QAAQ,EAAE,OAAO;IAEpB;GACF;GACA,IACE,gBAAgB,CAAC,KACjB,CAAC,aACD,EAAE,SAAS,iBACX,yBACA,IAAI,GACJ;IAOA,GAAG,OACD,kJACA,CACF;IACA,MAAM,IAAI,SAAQ,YAAW,WAAW,SAAS,GAAG,CAAC;IACrD;GACF;GACA,IAAI,gBAAgB,CAAC,KAAK,WAAW;IACnC,GAAG,QAAQ,0BAA0B,CAAC;IACtC,MAAM;GACR;GACA,IACE,aAAa,SAAS,iBACtB,EAAE,SAAS,0BACX;IACA,GAAG,OAAO,WAAW,IAAI,EAAE,IAAI,OAAO,CAAC,KAAK,CAAC;IAC7C;GACF;GACA,SAAS,CAAC,gBAA0B,OAAO,CAAC,CAAC;GAC7C,IAAI,WACF;GAEF,GAAG,QAAQ,wDAAwD,CAAC;GACpE,YAAY;GACZ;EACF;CAEJ,UAAU;EACR,GAAG,QAAQ,gCAAgC,KAAK,IAAI,IAAI,KAAK;CAC/D;CACA,OAAO;AACT;AAEA,eAAsB,sBACpB,IACA,IACA,UACA,OACA,eACA,UACA,WACA,YACA;CACA,MAAM,QAAoC,CAAC;CAE3C,eAAe,QAAQ,MAA6C;EAClE,IAAI;GACF,OAAO,MAAM,KAAK,QAAQ;EAC5B,UAAU;GACR,MAAM,IAAI;GACV,GAAG,QAAQ,GAAG,EAAE,OAAO,IAAI,KAAK,UAAU,EAAE,UAAU,GAAG;EAC3D;CACF;CAEA,WAAW,kBAAkB;CAE7B,IAAI,CAAC,WAAW;EACd,MAAM,EAAC,QAAO,SAAS,KAAK;EAG5B,WAAW,mBAAmB,GAAG;EAEjC,MAAM,gBAAgB,WAAW,aAAa,GAAG;EACjD,MAAM,CAAC,QAAQ,WAAW,MAAM,QAAQ,IAAI,CAC1C,WAAW,eAAe,UAAU,aAAa,GACjD,WAAW,gBAAgB,UAAU,aAAa,CACpD,CAAC;EACD,IAAI,UAAU,SACZ,KAAK,MAAM,MAAM,KACf,QAAQ,GAAG,IAAX;GACE,KAAK;IACH,MAAM,WAAW,QAAQ,aAAa,IAAI,EAAE,CAAC,CAAC;IAC9C;GACF,KAAK;IACH,MAAM,WAAW,QAAQ,aAAa,IAAI,EAAE,CAAC,CAAC;IAC9C;GACF,KAAK;IACH,MAAM,WAAW,QAAQ,aAAa,IAAI,EAAE,CAAC,CAAC;IAC9C;GACF,KAAK;IACH,MAAM,WAAW,QAAQ,aAAa,IAAI,EAAE,CAAC,CAAC;IAC9C;GACF,SACE,YAAY,EAAE;EAClB;CAGN;CAIA,MAAM,cACJ,6CACE,IACA,OACA,eACA,SAAS,UACT,SAAS,EACX,CACF;CAGA,MAAM,QAAQ,IAAI,MAAM,KAAI,SAAQ,KAAK,CAAC,CAAC;AAC7C;AAEA,SAAgB,aACd,IACA,QACuC;CACvC,OAAO,EAAE,eAAe,GAAG,OAAO,SAAS,EAAE,GAAG,GAAG,OAAO,KAAK;AACjE;AAEA,SAAgB,aACd,IACA,KACuC;CACvC,MAAM,EAAC,WAAW,YAAY,UAAS;CACvC,OAAO,EAAE;kBACO,GAAG,SAAS,EAAE,GAAG,GAAG,KAAK,EAAE;mBAC1B,GAAG,UAAU,EAAE;oBACd,GAAG,KAAK,EAAE;;AAE9B;AAEA,SAAS,aACP,IACA,QACuC;CACvC,MAAM,QAAQ,OAAO;CACrB,MAAM,EAAC,YAAY,UAAS;CAC5B,MAAM,KAAsC,CAAC;CAC7C,KAAK,MAAM,OAAO,YAChB,GAAG,OAAO,MAAQ,MAAM,MAAM,qBAAqB;CAErD,OAAO,EAAE,UAAU,GAAG,KAAK,EAAE,OAAO,GAAG,KAAK,EAAE,SAAS,OAAO,QAC5D,EACF,EAAE,SAAS,CAAC,KAAK,QAAQ,MACvB,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,GAAG,EAAE,KAAK,OAAO,IAAI,EAAE,GAAG,GAAG,GAAG,EAAE,KAAK,OAC/D;AACF;AAEA,SAAS,aACP,IACA,UACuC;CACvC,MAAM,EAAC,WAAW,YAAY,UAAS;CAEvC,MAAM,aAAa,CAAC;CACpB,KAAK,MAAM,OAAO,YAAY;EAC5B,IAAI,WAAW,SAAS,GACtB,WAAW,KAAK,EAAE,KAAK;EAEzB,WAAW,KAAK,EAAE,GAAG,GAAG,GAAG,EAAE,KAAK,MAAM,MAAM;CAChD;CAEA,OAAO,EAAE,eAAe,GAAG,SAAS,EAAE,SAAS;AACjD;AAEA,eAAe,6CACb,IACA,OACA,eACA,UACA,oBACA;CACA,MAAM,CAAC,EAAC,oBAAmB,MAAM,EAA8B;kBAC/C,GAAG,eAAe,KAAK,CAAC,EAAE;;oBAExB,cAAc,IAAI,SAAS,IAAI,EAAE;;;;;CAOnD,IAAI,qBAAqB,gBACvB,MAAM,IAAI,8BACR,UACA,oBACA,cACF;MACK,IAAI,qBAAqB,gBAC9B,MAAM,IAAI,cAAc;EACtB,MAAM;EACN,SAAS,wCAAwC,mBAAmB,cAAc,SAAS,yBAAyB,eAAe,SAAS,EAAE;EAC9I,QAAQ;CACV,CAAC;AAEL"}
|
|
1
|
+
{"version":3,"file":"mutagen.js","names":["#lc","#upstream","#shard","#stopped","#replica","#writeAuthorizer","#limiter","#crudMutations","#isStopped","#refCount"],"sources":["../../../../../../zero-cache/src/services/mutagen/mutagen.ts"],"sourcesContent":["import {PG_SERIALIZATION_FAILURE} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport type {JWTPayload} from 'jose';\nimport postgres from 'postgres';\nimport {assert, unreachable} from '../../../../shared/src/asserts.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {ErrorKind} from '../../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../../zero-protocol/src/error-origin.ts';\nimport {\n isProtocolError,\n ProtocolError,\n} from '../../../../zero-protocol/src/error.ts';\nimport * as MutationType from '../../../../zero-protocol/src/mutation-type-enum.ts';\nimport {\n type CRUDMutation,\n type DeleteOp,\n type InsertOp,\n type Mutation,\n type UpdateOp,\n type UpsertOp,\n} from '../../../../zero-protocol/src/mutation.ts';\nimport {\n primaryKeyValueSchema,\n type PrimaryKeyValue,\n} from '../../../../zero-protocol/src/primary-key.ts';\nimport type {DatabaseStorage} from '../../../../zqlite/src/database-storage.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {\n WriteAuthorizerImpl,\n type WriteAuthorizer,\n} from '../../auth/write-authorizer.ts';\nimport {type ZeroConfig} from '../../config/zero-config.ts';\nimport * as Mode from '../../db/mode-enum.ts';\nimport {runTx} from '../../db/run-transaction.ts';\nimport {getOrCreateCounter} from '../../observability/metrics.ts';\nimport {recordMutation} from '../../server/anonymous-otel-start.ts';\nimport type {PostgresDB, PostgresTransaction} from '../../types/pg.ts';\nimport {upstreamSchema, type ShardID} from '../../types/shards.ts';\nimport {SlidingWindowLimiter} from '../limiter/sliding-window-limiter.ts';\nimport type {RefCountedService, Service} from '../service.ts';\nimport {MutationAlreadyProcessedError} from './error.ts';\n\n// An error encountered processing a mutation.\n// Returned back to application for display to user.\nexport type MutationError = [\n kind: ErrorKind.MutationFailed | ErrorKind.MutationRateLimited,\n desc: string,\n];\n\nexport interface Mutagen extends RefCountedService {\n processMutation(\n mutation: Mutation,\n authData: JWTPayload | undefined,\n customMutatorsEnabled: boolean,\n ): Promise<MutationError | undefined>;\n}\n\nexport class MutagenService implements Mutagen, Service {\n readonly id: string;\n readonly #lc: LogContext;\n readonly #upstream: PostgresDB;\n readonly #shard: ShardID;\n readonly #stopped = resolver();\n readonly #replica: Database;\n readonly #writeAuthorizer: WriteAuthorizerImpl;\n readonly #limiter: SlidingWindowLimiter | undefined;\n #refCount = 0;\n #isStopped = false;\n\n readonly #crudMutations = getOrCreateCounter(\n 'mutation',\n 'crud',\n 'Number of CRUD mutations processed',\n );\n\n constructor(\n lc: LogContext,\n shard: ShardID,\n clientGroupID: string,\n upstream: PostgresDB,\n config: ZeroConfig,\n writeAuthzStorage: DatabaseStorage,\n ) {\n this.id = clientGroupID;\n this.#lc = lc;\n this.#upstream = upstream;\n this.#shard = shard;\n this.#replica = new Database(this.#lc, config.replica.file, {\n fileMustExist: true,\n });\n this.#writeAuthorizer = new WriteAuthorizerImpl(\n this.#lc,\n config,\n this.#replica,\n shard.appID,\n clientGroupID,\n writeAuthzStorage,\n );\n\n if (config.perUserMutationLimit.max !== undefined) {\n this.#limiter = new SlidingWindowLimiter(\n config.perUserMutationLimit.windowMs,\n config.perUserMutationLimit.max,\n );\n }\n }\n\n ref() {\n assert(!this.#isStopped, 'MutagenService is already stopped');\n ++this.#refCount;\n }\n\n unref() {\n assert(!this.#isStopped, 'MutagenService is already stopped');\n --this.#refCount;\n if (this.#refCount <= 0) {\n void this.stop();\n }\n }\n\n hasRefs(): boolean {\n return this.#refCount > 0;\n }\n\n processMutation(\n mutation: Mutation,\n authData: JWTPayload | undefined,\n customMutatorsEnabled = false,\n ): Promise<MutationError | undefined> {\n if (this.#limiter?.canDo() === false) {\n return Promise.resolve([\n ErrorKind.MutationRateLimited,\n 'Rate limit exceeded',\n ]);\n }\n this.#crudMutations.add(1, {\n clientGroupID: this.id,\n });\n return processMutation(\n this.#lc,\n authData,\n this.#upstream,\n this.#shard,\n this.id,\n mutation,\n this.#writeAuthorizer,\n undefined,\n customMutatorsEnabled,\n );\n }\n\n run(): Promise<void> {\n return this.#stopped.promise;\n }\n\n stop(): Promise<void> {\n if (this.#isStopped) {\n return this.#stopped.promise;\n }\n this.#writeAuthorizer.destroy();\n this.#isStopped = true;\n this.#stopped.resolve();\n return this.#stopped.promise;\n }\n}\n\nconst MAX_SERIALIZATION_ATTEMPTS = 10;\n\nexport async function processMutation(\n lc: LogContext,\n authData: JWTPayload | undefined,\n db: PostgresDB,\n shard: ShardID,\n clientGroupID: string,\n mutation: Mutation,\n writeAuthorizer: WriteAuthorizer,\n onTxStart?: () => void | Promise<void>, // for testing\n customMutatorsEnabled = false,\n): Promise<MutationError | undefined> {\n assert(\n mutation.type === MutationType.CRUD,\n 'Only CRUD mutations are supported',\n );\n lc = lc.withContext('mutationID', mutation.id);\n lc = lc.withContext('processMutation');\n lc.debug?.('Process mutation start', mutation);\n\n // Record mutation processing attempt for telemetry (regardless of success/failure)\n recordMutation('crud');\n\n let result: MutationError | undefined;\n\n const start = Date.now();\n try {\n // Mutations can fail for a variety of reasons:\n //\n // - application error\n // - network/db error\n // - zero bug\n //\n // For application errors what we want is to re-run the mutation in\n // \"error mode\", which skips the actual mutation and just updates the\n // lastMutationID. Then return the error to the app.\n //\n // However, it's hard to tell the difference between application errors\n // and the other types.\n //\n // A reasonable policy ends up being to just retry every mutation once\n // in error mode. If the error mode mutation succeeds then we assume it\n // was an application error and return the error to the app. Otherwise,\n // we know it was something internal and we log it.\n //\n // This is not 100% correct - there are theoretical cases where we\n // return an internal error to the app that shouldn't have been. But it\n // would have to be a crazy coincidence: we'd have to have a network\n // error on the first attempt that resolves by the second attempt.\n //\n // One might ask why not try/catch just the calls to the mutators and\n // consider those application errors. That is actually what we do in\n // Replicache:\n //\n // https://github.com/rocicorp/todo-row-versioning/blob/9a0a79dc2d2de32c4fac61b5d1634bd9a9e66b7c/server/src/push.ts#L131\n //\n // We don't do it here because:\n //\n // 1. It's still not perfect. It's hard to isolate SQL errors in\n // mutators due to app developer mistakes from SQL errors due to\n // Zero mistakes.\n // 2. It's not possible to do this with the pg library we're using in\n // Zero anyway: https://github.com/porsager/postgres/issues/455.\n //\n // Personally I think this simple retry policy is nice.\n let errorMode = false;\n for (let i = 0; i < MAX_SERIALIZATION_ATTEMPTS; i++) {\n try {\n await runTx(\n db,\n async tx => {\n // Simulates a concurrent request for testing. In production this is a noop.\n const done = onTxStart?.();\n try {\n return await processMutationWithTx(\n lc,\n tx,\n authData,\n shard,\n clientGroupID,\n mutation,\n errorMode,\n writeAuthorizer,\n );\n } finally {\n await done;\n }\n },\n {mode: Mode.SERIALIZABLE},\n );\n if (errorMode) {\n lc.debug?.('Ran mutation successfully in error mode');\n }\n break;\n } catch (e) {\n if (e instanceof MutationAlreadyProcessedError) {\n lc.debug?.(e.message);\n // Don't double-count already processed mutations, but they were counted above\n return undefined;\n }\n if (\n isProtocolError(e) &&\n !errorMode &&\n e.kind === ErrorKind.InvalidPush &&\n customMutatorsEnabled &&\n i < 2\n ) {\n // We're temporarily supporting custom mutators AND CRUD mutators at the same time.\n // This can create a lot of OOO mutation errors since we do not know when the API server\n // has applied a custom mutation before moving on to process CRUD mutations.\n // The temporary workaround (since CRUD is being deprecated) is to retry the mutation\n // after a small delay. Users are not expected to be running both CRUD and Custom mutators.\n // They should migrate completely to custom mutators.\n lc.info?.(\n 'Both CRUD and Custom mutators are being used at once. This is supported for now but IS NOT RECOMMENDED. Migrate completely to custom mutators.',\n e,\n );\n await new Promise(resolve => setTimeout(resolve, 100));\n continue;\n }\n if (isProtocolError(e) || errorMode) {\n lc.error?.('Process mutation error', e);\n throw e;\n }\n if (\n e instanceof postgres.PostgresError &&\n e.code === PG_SERIALIZATION_FAILURE\n ) {\n lc.info?.(`attempt ${i + 1}: ${String(e)}`, e);\n continue; // Retry up to MAX_SERIALIZATION_ATTEMPTS.\n }\n result = [ErrorKind.MutationFailed, String(e)];\n if (errorMode) {\n break;\n }\n lc.error?.('Got error running mutation, re-running in error mode', e);\n errorMode = true;\n i--;\n }\n }\n } finally {\n lc.debug?.('Process mutation complete in', Date.now() - start);\n }\n return result;\n}\n\nexport async function processMutationWithTx(\n lc: LogContext,\n tx: PostgresTransaction,\n authData: JWTPayload | undefined,\n shard: ShardID,\n clientGroupID: string,\n mutation: CRUDMutation,\n errorMode: boolean,\n authorizer: WriteAuthorizer,\n) {\n const tasks: (() => Promise<unknown>)[] = [];\n\n async function execute(stmt: postgres.PendingQuery<postgres.Row[]>) {\n try {\n return await stmt.execute();\n } finally {\n const q = stmt as unknown as Query;\n lc.debug?.(`${q.string}: ${JSON.stringify(q.parameters)}`);\n }\n }\n\n authorizer.reloadPermissions();\n\n if (!errorMode) {\n const {ops} = mutation.args[0];\n\n // Validate all table names before processing\n authorizer.validateTableNames(ops);\n\n const normalizedOps = authorizer.normalizeOps(ops);\n const [canPre, canPost] = await Promise.all([\n authorizer.canPreMutation(authData, normalizedOps),\n authorizer.canPostMutation(authData, normalizedOps),\n ]);\n if (canPre && canPost) {\n for (const op of ops) {\n switch (op.op) {\n case 'insert':\n tasks.push(() => execute(getInsertSQL(tx, op)));\n break;\n case 'upsert':\n tasks.push(() => execute(getUpsertSQL(tx, op)));\n break;\n case 'update':\n tasks.push(() => execute(getUpdateSQL(tx, op)));\n break;\n case 'delete':\n tasks.push(() => execute(getDeleteSQL(tx, op)));\n break;\n default:\n unreachable(op);\n }\n }\n }\n }\n\n // Confirm the mutation even though it may have been blocked by the authorizer.\n // Authorizer blocking a mutation is not an error but the correct result of the mutation.\n tasks.unshift(() =>\n checkSchemaVersionAndIncrementLastMutationID(\n tx,\n shard,\n clientGroupID,\n mutation.clientID,\n mutation.id,\n ),\n );\n\n // Note: An error thrown from any Promise aborts the entire transaction.\n await Promise.all(tasks.map(task => task()));\n}\n\nexport function getInsertSQL(\n tx: postgres.TransactionSql,\n create: InsertOp,\n): postgres.PendingQuery<postgres.Row[]> {\n return tx`\n INSERT INTO ${tx(create.tableName)} ${tx(create.value)}\n ON CONFLICT (${tx(create.primaryKey)}) DO NOTHING\n `;\n}\n\nexport function getUpsertSQL(\n tx: postgres.TransactionSql,\n set: UpsertOp,\n): postgres.PendingQuery<postgres.Row[]> {\n const {tableName, primaryKey, value} = set;\n return tx`\n INSERT INTO ${tx(tableName)} ${tx(value)}\n ON CONFLICT (${tx(primaryKey)})\n DO UPDATE SET ${tx(value)}\n `;\n}\n\nfunction getUpdateSQL(\n tx: postgres.TransactionSql,\n update: UpdateOp,\n): postgres.PendingQuery<postgres.Row[]> {\n const table = update.tableName;\n const {primaryKey, value} = update;\n const id: Record<string, PrimaryKeyValue> = {};\n for (const key of primaryKey) {\n id[key] = v.parse(value[key], primaryKeyValueSchema);\n }\n return tx`UPDATE ${tx(table)} SET ${tx(value)} WHERE ${Object.entries(\n id,\n ).flatMap(([key, value], i) =>\n i ? [tx`AND`, tx`${tx(key)} = ${value}`] : tx`${tx(key)} = ${value}`,\n )}`;\n}\n\nfunction getDeleteSQL(\n tx: postgres.TransactionSql,\n deleteOp: DeleteOp,\n): postgres.PendingQuery<postgres.Row[]> {\n const {tableName, primaryKey, value} = deleteOp;\n\n const conditions = [];\n for (const key of primaryKey) {\n if (conditions.length > 0) {\n conditions.push(tx`AND`);\n }\n conditions.push(tx`${tx(key)} = ${value[key]}`);\n }\n\n return tx`DELETE FROM ${tx(tableName)} WHERE ${conditions}`;\n}\n\nasync function checkSchemaVersionAndIncrementLastMutationID(\n tx: PostgresTransaction,\n shard: ShardID,\n clientGroupID: string,\n clientID: string,\n receivedMutationID: number,\n) {\n const [{lastMutationID}] = await tx<{lastMutationID: bigint}[]>`\n INSERT INTO ${tx(upstreamSchema(shard))}.clients \n as current (\"clientGroupID\", \"clientID\", \"lastMutationID\")\n VALUES (${clientGroupID}, ${clientID}, ${1})\n ON CONFLICT (\"clientGroupID\", \"clientID\")\n DO UPDATE SET \"lastMutationID\" = current.\"lastMutationID\" + 1\n RETURNING \"lastMutationID\"\n `;\n\n // ABORT if the resulting lastMutationID is not equal to the receivedMutationID.\n if (receivedMutationID < lastMutationID) {\n throw new MutationAlreadyProcessedError(\n clientID,\n receivedMutationID,\n lastMutationID,\n );\n } else if (receivedMutationID > lastMutationID) {\n throw new ProtocolError({\n kind: ErrorKind.InvalidPush,\n message: `Push contains unexpected mutation id ${receivedMutationID} for client ${clientID}. Expected mutation id ${lastMutationID.toString()}.`,\n origin: ErrorOrigin.ZeroCache,\n });\n }\n}\n\n// The slice of information from the Query object in Postgres.js that gets logged for debugging.\n// https://github.com/porsager/postgres/blob/f58cd4f3affd3e8ce8f53e42799672d86cd2c70b/src/connection.js#L219\ntype Query = {string: string; parameters: object[]};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-processor.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/replicator/change-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"change-processor.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/replicator/change-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAOjD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,uCAAuC,CAAC;AAiB1E,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,wBAAwB,CAAC;AAiC5D,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iDAAiD,CAAC;AACtF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,iBAAiB,CAAC;AASpD,MAAM,MAAM,mBAAmB,GAAG,cAAc,GAAG,cAAc,CAAC;AAElE,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,cAAc,GAAG,SAAS,CAAC;IAC9C,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,qBAAa,eAAe;;gBAiBxB,EAAE,EAAE,eAAe,EACnB,IAAI,EAAE,mBAAmB,EACzB,WAAW,EAAE,CAAC,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI;IAgCrD,KAAK,CAAC,EAAE,EAAE,UAAU;IAIpB,8CAA8C;IAC9C,cAAc,CACZ,EAAE,EAAE,UAAU,EACd,UAAU,EAAE,gBAAgB,GAC3B,YAAY,GAAG,IAAI;CAyJvB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assert, unreachable } from "../../../../shared/src/asserts.js";
|
|
2
2
|
import { AbortError } from "../../../../shared/src/abort-error.js";
|
|
3
3
|
import { must } from "../../../../shared/src/must.js";
|
|
4
|
+
import { mapEntries } from "../../../../shared/src/objects.js";
|
|
4
5
|
import { stringify } from "../../../../shared/src/bigint-json.js";
|
|
5
6
|
import { ZERO_VERSION_COLUMN_NAME } from "./schema/constants.js";
|
|
6
7
|
import { liteRow } from "../../types/lite.js";
|
|
@@ -338,25 +339,37 @@ var TransactionProcessor = class {
|
|
|
338
339
|
const oldSpec = mapPostgresToLiteColumn(table, msg.old, "ignore-default");
|
|
339
340
|
const newSpec = mapPostgresToLiteColumn(table, msg.new, "ignore-default");
|
|
340
341
|
if (oldName === newName && oldSpec.dataType === newSpec.dataType) {
|
|
341
|
-
this.#
|
|
342
|
+
this.#columnMetadata.update(table, msg.old.name, msg.new.name, msg.new.spec);
|
|
343
|
+
this.#lc.info?.(msg.tag, "updated metadata only", oldSpec, newSpec);
|
|
342
344
|
return;
|
|
343
345
|
}
|
|
344
346
|
if (oldSpec.dataType !== newSpec.dataType) {
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
347
|
+
const tableSpec = must(listTables(this.#db.db, false, false).find((tableSpec) => tableSpec.name === table));
|
|
348
|
+
const indexes = listIndexes(this.#db.db).filter((idx) => idx.tableName === table);
|
|
349
|
+
const tmpTable = `tmp.${table}`;
|
|
350
|
+
const newColumns = mapEntries(tableSpec.columns, (column, spec) => [column === oldName ? newName : column, column === oldName ? {
|
|
351
|
+
...newSpec,
|
|
352
|
+
pos: spec.pos
|
|
353
|
+
} : spec]);
|
|
354
|
+
const sourceColumns = Object.keys(tableSpec.columns);
|
|
355
|
+
const destinationColumns = Object.keys(newColumns);
|
|
356
|
+
const stmts = [
|
|
357
|
+
createLiteTableStatement({
|
|
358
|
+
...tableSpec,
|
|
359
|
+
name: tmpTable,
|
|
360
|
+
columns: newColumns
|
|
361
|
+
}),
|
|
362
|
+
`INSERT INTO ${id(tmpTable)} (${destinationColumns.map(id).join(",")})
|
|
363
|
+
SELECT ${sourceColumns.map(id).join(",")} FROM ${id(table)};`,
|
|
364
|
+
`DROP TABLE ${id(table)};`,
|
|
365
|
+
`ALTER TABLE ${id(tmpTable)} RENAME TO ${id(table)};`,
|
|
366
|
+
...indexes.map((idx) => createLiteIndexStatement({
|
|
367
|
+
...idx,
|
|
368
|
+
columns: mapEntries(idx.columns, (column, direction) => [column === oldName ? newName : column, direction])
|
|
369
|
+
}))
|
|
370
|
+
];
|
|
358
371
|
this.#db.db.exec(stmts.join(""));
|
|
359
|
-
oldName =
|
|
372
|
+
oldName = newName;
|
|
360
373
|
}
|
|
361
374
|
if (oldName !== newName) this.#db.db.exec(`ALTER TABLE ${id(table)} RENAME ${id(oldName)} TO ${id(newName)}`);
|
|
362
375
|
this.#columnMetadata.update(table, msg.old.name, msg.new.name, msg.new.spec);
|