@rocicorp/zero 1.11.0-canary.21 → 1.11.0-canary.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
- package/out/zero-cache/src/config/normalize.js +1 -2
- package/out/zero-cache/src/config/normalize.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +0 -22
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +38 -42
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +90 -73
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js +1 -1
- package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/init-cleanup.d.ts +24 -0
- package/out/zero-cache/src/services/change-source/common/init-cleanup.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-source/common/init-cleanup.js +44 -0
- package/out/zero-cache/src/services/change-source/common/init-cleanup.js.map +1 -0
- package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts +49 -1
- package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.d.ts +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +7 -3
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts +10 -5
- package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js +65 -21
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
- 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 +2 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js +3 -1
- 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 +31 -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 +100 -2
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.d.ts +6 -0
- package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js +8 -0
- package/out/zero-cache/src/services/change-streamer/backfill-state-tracker.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js +3 -16
- package/out/zero-cache/src/services/change-streamer/backup-cleanup-monitor-factory.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.d.ts +17 -0
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.js +44 -1
- package/out/zero-cache/src/services/change-streamer/change-log-initializer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts +7 -0
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +27 -8
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts +57 -0
- package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.js +103 -23
- package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.d.ts +86 -1
- package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js +272 -77
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts +7 -5
- package/out/zero-cache/src/services/change-streamer/upstream-acker.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/upstream-acker.js +9 -5
- package/out/zero-cache/src/services/change-streamer/upstream-acker.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts +3 -11
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +29 -23
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/litestream/config-v5.yml +0 -7
- package/out/zero-cache/src/services/profz.d.ts.map +1 -1
- package/out/zero-cache/src/services/profz.js +19 -1
- package/out/zero-cache/src/services/profz.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replication-status.js +1 -1
- package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/advancement-metrics.d.ts +26 -0
- package/out/zero-cache/src/services/view-syncer/advancement-metrics.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/advancement-metrics.js +26 -0
- package/out/zero-cache/src/services/view-syncer/advancement-metrics.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +10 -2
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +102 -122
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +8 -9
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +15 -3
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +50 -28
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +2 -1
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-react/src/use-query.js +12 -3
- package/out/zero-react/src/use-query.js.map +1 -1
- package/out/zql/src/ivm/array-view.d.ts.map +1 -1
- package/out/zql/src/ivm/array-view.js +21 -12
- package/out/zql/src/ivm/array-view.js.map +1 -1
- package/out/zqlite/src/db.d.ts.map +1 -1
- package/out/zqlite/src/db.js +34 -6
- package/out/zqlite/src/db.js.map +1 -1
- package/package.json +1 -1
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.d.ts +0 -34
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.d.ts.map +0 -1
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.js +0 -184
- package/out/zero-cache/src/services/change-streamer/vfs-watermark-poller.js.map +0 -1
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { assert } from "../../../../shared/src/asserts.js";
|
|
2
|
-
import { parse, valita_exports } from "../../../../shared/src/valita.js";
|
|
3
|
-
import { Database } from "../../../../zqlite/src/db.js";
|
|
4
|
-
import { RunningState } from "../running-state.js";
|
|
5
|
-
import { Subscription } from "../../types/subscription.js";
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
7
|
-
import { createInterface } from "node:readline";
|
|
8
|
-
//#region ../zero-cache/src/services/change-streamer/vfs-watermark-poller.ts
|
|
9
|
-
var LOCAL_CHECK_INTERVAL_MS = 1e3;
|
|
10
|
-
var queryResultRowSchema = valita_exports.object({
|
|
11
|
-
stateVersion: valita_exports.string(),
|
|
12
|
-
writeTimeMs: valita_exports.number()
|
|
13
|
-
});
|
|
14
|
-
var remoteQueryMetadataSchema = valita_exports.object({ litestream_time: valita_exports.string().map((str) => new Date(str).getTime()).optional() });
|
|
15
|
-
var remoteQueryLineSchema = valita_exports.object({
|
|
16
|
-
query_result: valita_exports.tuple([queryResultRowSchema]),
|
|
17
|
-
metadata: remoteQueryMetadataSchema
|
|
18
|
-
});
|
|
19
|
-
var INITIAL_BACKOFF_MS = 2e3;
|
|
20
|
-
var MAX_BACKOFF_MS = 3e4;
|
|
21
|
-
var VFS_QUERY_MEMORY_LIMIT_BASE_BYTES = 64 * 1024 ** 2;
|
|
22
|
-
var VFS_QUERY_MEMORY_LIMIT_BYTES_PER_PAGE = 150;
|
|
23
|
-
/**
|
|
24
|
-
* Returns the GOMEMLIMIT for a vfs-query process watching a replica of
|
|
25
|
-
* `pageCount` pages.
|
|
26
|
-
*/
|
|
27
|
-
function vfsQueryMemoryLimitBytes(pageCount) {
|
|
28
|
-
return VFS_QUERY_MEMORY_LIMIT_BASE_BYTES + pageCount * VFS_QUERY_MEMORY_LIMIT_BYTES_PER_PAGE;
|
|
29
|
-
}
|
|
30
|
-
var VfsWatermarkPoller = class {
|
|
31
|
-
#lc;
|
|
32
|
-
#state = new RunningState("vfs-watermark-poller");
|
|
33
|
-
#pollerConfig;
|
|
34
|
-
#readLocalWatermark;
|
|
35
|
-
#readPageCount;
|
|
36
|
-
#stream;
|
|
37
|
-
#localWatermark;
|
|
38
|
-
#remoteWatermark;
|
|
39
|
-
#localPollTimer;
|
|
40
|
-
#shouldPollRemote = false;
|
|
41
|
-
#remotePoller;
|
|
42
|
-
#remotePollerBackoffMs = INITIAL_BACKOFF_MS;
|
|
43
|
-
#remotePollerBackoffTimer;
|
|
44
|
-
constructor(lc, replicaFile, pollerConfig) {
|
|
45
|
-
this.#lc = lc.withContext("component", "vfs-watermark-poller");
|
|
46
|
-
this.#pollerConfig = pollerConfig;
|
|
47
|
-
const db = new Database(this.#lc, replicaFile, { readonly: true });
|
|
48
|
-
this.#readLocalWatermark = db.prepare(`SELECT stateVersion, writeTimeMs FROM "_zero.replicationState"`);
|
|
49
|
-
this.#readPageCount = db.prepare(`PRAGMA page_count`);
|
|
50
|
-
this.#localWatermark = parse(this.#readLocalWatermark.get(), queryResultRowSchema);
|
|
51
|
-
this.#stream = Subscription.create({ cleanup: () => {
|
|
52
|
-
this.#disableRemotePoller();
|
|
53
|
-
clearInterval(this.#localPollTimer);
|
|
54
|
-
clearTimeout(this.#remotePollerBackoffTimer);
|
|
55
|
-
this.#state.stop(this.#lc);
|
|
56
|
-
db.close();
|
|
57
|
-
} });
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Starts polling for watermarks and pushes new ones to returned stream.
|
|
61
|
-
* Must only be called once.
|
|
62
|
-
*/
|
|
63
|
-
start() {
|
|
64
|
-
assert(this.#localPollTimer === void 0, `Already called start()`);
|
|
65
|
-
this.#lc.info?.(`monitoring backups at ${this.#pollerConfig.backupURL}`);
|
|
66
|
-
this.#localPollTimer = setInterval(this.checkLocalWatermark, LOCAL_CHECK_INTERVAL_MS);
|
|
67
|
-
this.checkLocalWatermark();
|
|
68
|
-
return this.#stream;
|
|
69
|
-
}
|
|
70
|
-
checkLocalWatermark = () => {
|
|
71
|
-
try {
|
|
72
|
-
this.#localWatermark = parse(this.#readLocalWatermark.get(), queryResultRowSchema);
|
|
73
|
-
} catch (e) {
|
|
74
|
-
this.#lc.warn?.(`unable to read local watermark`, e);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
if (this.#localWatermark.stateVersion !== this.#remoteWatermark?.watermark) this.#enableRemotePoller();
|
|
78
|
-
else this.#disableRemotePoller();
|
|
79
|
-
};
|
|
80
|
-
#enableRemotePoller() {
|
|
81
|
-
if (this.#remotePoller || this.#remotePollerBackoffTimer) return;
|
|
82
|
-
this.#shouldPollRemote = true;
|
|
83
|
-
if (this.#remoteWatermark) this.#lc.debug?.(`local and remote watermarks differ. resuming backup polling`, {
|
|
84
|
-
local: this.#localWatermark,
|
|
85
|
-
remote: this.#remoteWatermark
|
|
86
|
-
});
|
|
87
|
-
const child = spawn(this.#pollerConfig.executable, [
|
|
88
|
-
"--replica-url",
|
|
89
|
-
buildLitestreamVfsReplicaURL(this.#pollerConfig),
|
|
90
|
-
"--query",
|
|
91
|
-
`SELECT stateVersion, writeTimeMs FROM "_zero.replicationState"`,
|
|
92
|
-
"--remote-poll-interval",
|
|
93
|
-
`${this.#pollerConfig.remotePollIntervalMs}ms`,
|
|
94
|
-
...this.#pollerConfig.logLevel ? ["--log-level", this.#pollerConfig.logLevel] : [],
|
|
95
|
-
...this.#pollerConfig.logFormat ? ["--log-format", this.#pollerConfig.logFormat] : [],
|
|
96
|
-
...this.#pollerConfig.remoteQueryTimeoutMs ? ["--query-timeout", `${this.#pollerConfig.remoteQueryTimeoutMs}ms`] : []
|
|
97
|
-
], {
|
|
98
|
-
stdio: [
|
|
99
|
-
"pipe",
|
|
100
|
-
"pipe",
|
|
101
|
-
"inherit"
|
|
102
|
-
],
|
|
103
|
-
env: this.#vfsQueryEnv()
|
|
104
|
-
}).on("error", (err) => {
|
|
105
|
-
this.#lc.error?.(`received error from vfs-query process`, err);
|
|
106
|
-
}).on("close", (code, signal) => {
|
|
107
|
-
if (this.#remotePoller !== child) return;
|
|
108
|
-
this.#remotePoller = void 0;
|
|
109
|
-
if (this.#shouldPollRemote) {
|
|
110
|
-
if (code !== 0) this.#lc.warn?.(`vfs-query exited with ${code}, ${signal}. retrying in ${this.#remotePollerBackoffMs}ms`);
|
|
111
|
-
if (!this.#remotePollerBackoffTimer) {
|
|
112
|
-
this.#remotePollerBackoffTimer = setTimeout(() => {
|
|
113
|
-
this.#remotePollerBackoffTimer = void 0;
|
|
114
|
-
if (this.#shouldPollRemote) this.#enableRemotePoller();
|
|
115
|
-
}, this.#remotePollerBackoffMs);
|
|
116
|
-
this.#remotePollerBackoffMs = Math.min(this.#remotePollerBackoffMs * 2, MAX_BACKOFF_MS);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
createInterface({ input: child.stdout }).on("line", this.#readRemotePollerLine);
|
|
121
|
-
this.#remotePoller = child;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* The environment for a vfs-query process: this process's environment
|
|
125
|
-
* plus a GOMEMLIMIT sized to the replica's current page count. An
|
|
126
|
-
* explicitly configured GOMEMLIMIT is passed through unchanged.
|
|
127
|
-
*/
|
|
128
|
-
#vfsQueryEnv() {
|
|
129
|
-
if (process.env.GOMEMLIMIT) return process.env;
|
|
130
|
-
const { page_count: pageCount } = this.#readPageCount.get();
|
|
131
|
-
const limit = vfsQueryMemoryLimitBytes(pageCount);
|
|
132
|
-
this.#lc.info?.(`spawning vfs-query with GOMEMLIMIT=${limit}B for ${pageCount} pages`);
|
|
133
|
-
return {
|
|
134
|
-
...process.env,
|
|
135
|
-
GOMEMLIMIT: `${limit}B`
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
#readRemotePollerLine = (line) => {
|
|
139
|
-
try {
|
|
140
|
-
const update = parse(JSON.parse(line), remoteQueryLineSchema, "passthrough");
|
|
141
|
-
const [{ stateVersion: watermark, writeTimeMs }] = update.query_result;
|
|
142
|
-
const remoteWatermark = {
|
|
143
|
-
watermark,
|
|
144
|
-
writeTimeMs,
|
|
145
|
-
backupTimeMs: update.metadata.litestream_time ?? Date.now()
|
|
146
|
-
};
|
|
147
|
-
if (remoteWatermark.watermark !== this.#remoteWatermark?.watermark) {
|
|
148
|
-
this.#lc.debug?.(`backup watermark advanced from ${this.#remoteWatermark?.watermark} to ${remoteWatermark.watermark}`, {
|
|
149
|
-
local: this.#localWatermark,
|
|
150
|
-
remote: remoteWatermark
|
|
151
|
-
});
|
|
152
|
-
this.#stream.push(remoteWatermark);
|
|
153
|
-
}
|
|
154
|
-
this.#remoteWatermark = remoteWatermark;
|
|
155
|
-
this.#remotePollerBackoffMs = INITIAL_BACKOFF_MS;
|
|
156
|
-
} catch (e) {
|
|
157
|
-
this.#lc.error?.(`unable to parse vfs-query output: ${line}`, e);
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
#disableRemotePoller() {
|
|
161
|
-
if (!this.#shouldPollRemote) return;
|
|
162
|
-
this.#shouldPollRemote = false;
|
|
163
|
-
if (this.#remotePoller || this.#remotePollerBackoffTimer) this.#lc.debug?.(`local and remote watermarks match. pausing backup polling`, {
|
|
164
|
-
local: this.#localWatermark,
|
|
165
|
-
remote: this.#remoteWatermark
|
|
166
|
-
});
|
|
167
|
-
this.#remotePoller?.kill("SIGTERM");
|
|
168
|
-
this.#remotePoller = void 0;
|
|
169
|
-
clearTimeout(this.#remotePollerBackoffTimer);
|
|
170
|
-
this.#remotePollerBackoffTimer = void 0;
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
function buildLitestreamVfsReplicaURL({ backupURL, endpoint, region }) {
|
|
174
|
-
if (endpoint === void 0 && region === void 0) return backupURL;
|
|
175
|
-
const url = new URL(backupURL);
|
|
176
|
-
if (url.protocol !== "s3:") return backupURL;
|
|
177
|
-
if (endpoint !== void 0 && !url.searchParams.has("endpoint")) url.searchParams.set("endpoint", endpoint);
|
|
178
|
-
if (region !== void 0 && !url.searchParams.has("region")) url.searchParams.set("region", region);
|
|
179
|
-
return url.toString();
|
|
180
|
-
}
|
|
181
|
-
//#endregion
|
|
182
|
-
export { VfsWatermarkPoller };
|
|
183
|
-
|
|
184
|
-
//# sourceMappingURL=vfs-watermark-poller.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"vfs-watermark-poller.js","names":["#lc","#state","#pollerConfig","#readLocalWatermark","#readPageCount","#stream","#localWatermark","#disableRemotePoller","#localPollTimer","#remotePollerBackoffTimer","#remoteWatermark","#enableRemotePoller","#remotePoller","#shouldPollRemote","#vfsQueryEnv","#remotePollerBackoffMs","#readRemotePollerLine"],"sources":["../../../../../../zero-cache/src/services/change-streamer/vfs-watermark-poller.ts"],"sourcesContent":["import {spawn, type ChildProcess} from 'node:child_process';\nimport {createInterface} from 'node:readline';\nimport type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../shared/src/asserts.ts';\nimport * as v from '../../../../shared/src/valita.ts';\nimport {Database, type Statement} from '../../../../zqlite/src/db.ts';\nimport type {Source} from '../../types/streams.ts';\nimport {Subscription} from '../../types/subscription.ts';\nimport {RunningState} from '../running-state.ts';\nimport type {BackedUpWatermark} from './backup-monitor.ts';\n\nconst LOCAL_CHECK_INTERVAL_MS = 1_000;\n\ntype LocalWatermark = {\n stateVersion: string;\n writeTimeMs: number;\n};\n\ntype RemoteWatermark = {\n watermark: string;\n writeTimeMs: number;\n backupTimeMs: number;\n};\n\nexport type VfsPollerConfig = {\n executable: string;\n remotePollIntervalMs: number;\n remoteQueryTimeoutMs?: number;\n backupURL: string;\n region?: string | undefined;\n endpoint?: string | undefined;\n logLevel?: string;\n logFormat?: string;\n};\n\nconst queryResultRowSchema = v.object({\n stateVersion: v.string(),\n writeTimeMs: v.number(),\n});\n\nconst remoteQueryMetadataSchema = v.object({\n litestream_time: v\n .string()\n .map(str => new Date(str).getTime())\n .optional(),\n});\n\n// Expected format of each line of stdout from the vfs-query, e.g.\n// {\"query_result\":[{\"stateVersion\":\"51y50ko\",\"writeTimeMs\":1787259951121}],\"metadata\":{\"litestream_time\":\"2026-08-20T21:05:57.36439Z\"}}\n// {\"query_result\":[{\"stateVersion\":\"51y7360\",\"writeTimeMs\":1787259966137}],\"metadata\":{\"litestream_time\":\"2026-08-20T21:06:12.384531Z\"}}\nconst remoteQueryLineSchema = v.object({\n query_result: v.tuple([queryResultRowSchema]),\n metadata: remoteQueryMetadataSchema,\n});\n\nconst INITIAL_BACKOFF_MS = 2_000;\nconst MAX_BACKOFF_MS = 30_000;\n\nconst MiB = 1024 ** 2;\n\n// vfs-query holds a page index for the entire replica in memory: ~117 bytes\n// per page once built, peaking at ~235 bytes per page while the snapshot's\n// index is merged into it. With Go's default GC pacing, the heap (and RSS)\n// settles at ~2x the live index (~315 bytes per page). A soft memory limit\n// just above the live index halves that steady state, at the cost of extra\n// GC work while the index is built (the limit cannot push the build peak\n// below its live size).\nconst VFS_QUERY_MEMORY_LIMIT_BASE_BYTES = 64 * MiB;\nconst VFS_QUERY_MEMORY_LIMIT_BYTES_PER_PAGE = 150;\n\n/**\n * Returns the GOMEMLIMIT for a vfs-query process watching a replica of\n * `pageCount` pages.\n */\nexport function vfsQueryMemoryLimitBytes(pageCount: number): number {\n return (\n VFS_QUERY_MEMORY_LIMIT_BASE_BYTES +\n pageCount * VFS_QUERY_MEMORY_LIMIT_BYTES_PER_PAGE\n );\n}\n\nexport class VfsWatermarkPoller {\n readonly #lc: LogContext;\n readonly #state = new RunningState('vfs-watermark-poller');\n readonly #pollerConfig: VfsPollerConfig;\n readonly #readLocalWatermark: Statement;\n readonly #readPageCount: Statement;\n\n readonly #stream: Subscription<BackedUpWatermark>;\n\n #localWatermark: LocalWatermark;\n #remoteWatermark: RemoteWatermark | undefined;\n #localPollTimer: NodeJS.Timeout | undefined;\n\n #shouldPollRemote = false;\n\n #remotePoller: ChildProcess | undefined;\n #remotePollerBackoffMs = INITIAL_BACKOFF_MS;\n #remotePollerBackoffTimer: NodeJS.Timeout | undefined;\n\n constructor(\n lc: LogContext,\n replicaFile: string,\n pollerConfig: VfsPollerConfig,\n ) {\n this.#lc = lc.withContext('component', 'vfs-watermark-poller');\n this.#pollerConfig = pollerConfig;\n\n const db = new Database(this.#lc, replicaFile, {readonly: true});\n this.#readLocalWatermark = db.prepare(\n `SELECT stateVersion, writeTimeMs FROM \"_zero.replicationState\"`,\n );\n this.#readPageCount = db.prepare(`PRAGMA page_count`);\n // Purely to type the member variable as always defined\n this.#localWatermark = v.parse(\n this.#readLocalWatermark.get(),\n queryResultRowSchema,\n );\n this.#stream = Subscription.create<BackedUpWatermark>({\n cleanup: () => {\n this.#disableRemotePoller();\n clearInterval(this.#localPollTimer);\n clearTimeout(this.#remotePollerBackoffTimer);\n this.#state.stop(this.#lc);\n db.close();\n },\n });\n }\n\n /**\n * Starts polling for watermarks and pushes new ones to returned stream.\n * Must only be called once.\n */\n start(): Source<BackedUpWatermark> {\n assert(this.#localPollTimer === undefined, `Already called start()`);\n this.#lc.info?.(`monitoring backups at ${this.#pollerConfig.backupURL}`);\n this.#localPollTimer = setInterval(\n this.checkLocalWatermark,\n LOCAL_CHECK_INTERVAL_MS,\n );\n this.checkLocalWatermark();\n return this.#stream;\n }\n\n // Exported for testing\n readonly checkLocalWatermark = () => {\n try {\n this.#localWatermark = v.parse(\n this.#readLocalWatermark.get(),\n queryResultRowSchema,\n );\n } catch (e) {\n this.#lc.warn?.(`unable to read local watermark`, e);\n return;\n }\n if (\n this.#localWatermark.stateVersion !== this.#remoteWatermark?.watermark\n ) {\n this.#enableRemotePoller();\n } else {\n this.#disableRemotePoller();\n }\n };\n\n #enableRemotePoller() {\n if (this.#remotePoller || this.#remotePollerBackoffTimer) {\n return; // already polling or backing off\n }\n this.#shouldPollRemote = true;\n if (this.#remoteWatermark) {\n this.#lc.debug?.(\n `local and remote watermarks differ. resuming backup polling`,\n {local: this.#localWatermark, remote: this.#remoteWatermark},\n );\n }\n const child = spawn(\n this.#pollerConfig.executable,\n [\n '--replica-url',\n buildLitestreamVfsReplicaURL(this.#pollerConfig),\n '--query',\n `SELECT stateVersion, writeTimeMs FROM \"_zero.replicationState\"`,\n '--remote-poll-interval',\n `${this.#pollerConfig.remotePollIntervalMs}ms`,\n ...(this.#pollerConfig.logLevel\n ? ['--log-level', this.#pollerConfig.logLevel]\n : []),\n ...(this.#pollerConfig.logFormat\n ? ['--log-format', this.#pollerConfig.logFormat]\n : []),\n ...(this.#pollerConfig.remoteQueryTimeoutMs\n ? ['--query-timeout', `${this.#pollerConfig.remoteQueryTimeoutMs}ms`]\n : []),\n ],\n {stdio: ['pipe', 'pipe', 'inherit'], env: this.#vfsQueryEnv()},\n )\n .on('error', err => {\n this.#lc.error?.(`received error from vfs-query process`, err);\n })\n .on('close', (code, signal) => {\n if (this.#remotePoller !== child) {\n // This child was already stopped by #disableRemotePoller(), and a\n // replacement may have been spawned since. Its exit must neither\n // untrack that replacement nor schedule a respawn next to it.\n return;\n }\n this.#remotePoller = undefined;\n if (this.#shouldPollRemote) {\n if (code !== 0) {\n this.#lc.warn?.(\n `vfs-query exited with ${code}, ${signal}. retrying in ${this.#remotePollerBackoffMs}ms`,\n );\n }\n if (!this.#remotePollerBackoffTimer) {\n this.#remotePollerBackoffTimer = setTimeout(() => {\n this.#remotePollerBackoffTimer = undefined;\n if (this.#shouldPollRemote) {\n this.#enableRemotePoller();\n }\n }, this.#remotePollerBackoffMs);\n this.#remotePollerBackoffMs = Math.min(\n this.#remotePollerBackoffMs * 2,\n MAX_BACKOFF_MS,\n );\n }\n }\n });\n createInterface({input: child.stdout}).on(\n 'line',\n this.#readRemotePollerLine,\n );\n this.#remotePoller = child;\n }\n\n /**\n * The environment for a vfs-query process: this process's environment\n * plus a GOMEMLIMIT sized to the replica's current page count. An\n * explicitly configured GOMEMLIMIT is passed through unchanged.\n */\n #vfsQueryEnv(): NodeJS.ProcessEnv {\n if (process.env.GOMEMLIMIT) {\n return process.env;\n }\n const {page_count: pageCount} = this.#readPageCount.get<{\n page_count: number;\n }>();\n const limit = vfsQueryMemoryLimitBytes(pageCount);\n this.#lc.info?.(\n `spawning vfs-query with GOMEMLIMIT=${limit}B for ${pageCount} pages`,\n );\n return {...process.env, GOMEMLIMIT: `${limit}B`};\n }\n\n readonly #readRemotePollerLine = (line: string) => {\n try {\n const update = v.parse(\n JSON.parse(line),\n remoteQueryLineSchema,\n 'passthrough',\n );\n const [{stateVersion: watermark, writeTimeMs}] = update.query_result;\n const remoteWatermark: RemoteWatermark = {\n watermark,\n writeTimeMs,\n backupTimeMs: update.metadata.litestream_time ?? Date.now(),\n };\n if (remoteWatermark.watermark !== this.#remoteWatermark?.watermark) {\n this.#lc.debug?.(\n `backup watermark advanced from ${this.#remoteWatermark?.watermark} to ${remoteWatermark.watermark}`,\n {local: this.#localWatermark, remote: remoteWatermark},\n );\n this.#stream.push(remoteWatermark);\n }\n this.#remoteWatermark = remoteWatermark;\n\n // Reset backoff on successful output from the poller\n this.#remotePollerBackoffMs = INITIAL_BACKOFF_MS;\n } catch (e) {\n this.#lc.error?.(`unable to parse vfs-query output: ${line}`, e);\n }\n };\n\n #disableRemotePoller() {\n if (!this.#shouldPollRemote) {\n return;\n }\n this.#shouldPollRemote = false;\n if (this.#remotePoller || this.#remotePollerBackoffTimer) {\n this.#lc.debug?.(\n `local and remote watermarks match. pausing backup polling`,\n {local: this.#localWatermark, remote: this.#remoteWatermark},\n );\n }\n this.#remotePoller?.kill('SIGTERM');\n this.#remotePoller = undefined;\n clearTimeout(this.#remotePollerBackoffTimer);\n this.#remotePollerBackoffTimer = undefined;\n }\n}\n\nexport function buildLitestreamVfsReplicaURL({\n backupURL,\n endpoint,\n region,\n}: {\n readonly backupURL: string;\n readonly endpoint?: string | undefined;\n readonly region?: string | undefined;\n}): string {\n if (endpoint === undefined && region === undefined) {\n return backupURL;\n }\n\n const url = new URL(backupURL);\n if (url.protocol !== 's3:') {\n return backupURL;\n }\n\n if (endpoint !== undefined && !url.searchParams.has('endpoint')) {\n url.searchParams.set('endpoint', endpoint);\n }\n if (region !== undefined && !url.searchParams.has('region')) {\n url.searchParams.set('region', region);\n }\n return url.toString();\n}\n"],"mappings":"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"}
|