@prisma/streams-server 0.1.1 → 0.1.3
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/CONTRIBUTING.md +8 -0
- package/package.json +2 -1
- package/src/app.ts +290 -17
- package/src/app_core.ts +1833 -698
- package/src/app_local.ts +144 -4
- package/src/auto_tune.ts +62 -0
- package/src/bootstrap.ts +159 -1
- package/src/concurrency_gate.ts +108 -0
- package/src/config.ts +116 -14
- package/src/db/db.ts +1201 -131
- package/src/db/schema.ts +308 -8
- package/src/foreground_activity.ts +55 -0
- package/src/index/indexer.ts +254 -124
- package/src/index/lexicon_file_cache.ts +261 -0
- package/src/index/lexicon_format.ts +93 -0
- package/src/index/lexicon_indexer.ts +789 -0
- package/src/index/secondary_indexer.ts +824 -0
- package/src/index/secondary_schema.ts +105 -0
- package/src/ingest.ts +10 -12
- package/src/manifest.ts +143 -8
- package/src/memory.ts +183 -8
- package/src/metrics.ts +15 -29
- package/src/metrics_emitter.ts +26 -3
- package/src/notifier.ts +121 -5
- package/src/objectstore/accounting.ts +92 -0
- package/src/objectstore/mock_r2.ts +1 -1
- package/src/objectstore/r2.ts +17 -1
- package/src/profiles/evlog/schema.ts +234 -0
- package/src/profiles/evlog.ts +299 -0
- package/src/profiles/generic.ts +47 -0
- package/src/profiles/index.ts +205 -0
- package/src/profiles/metrics/block_format.ts +109 -0
- package/src/profiles/metrics/normalize.ts +366 -0
- package/src/profiles/metrics/schema.ts +319 -0
- package/src/profiles/metrics.ts +85 -0
- package/src/profiles/profile.ts +225 -0
- package/src/{touch/engine.ts → profiles/stateProtocol/changes.ts} +3 -20
- package/src/profiles/stateProtocol/routes.ts +389 -0
- package/src/profiles/stateProtocol/types.ts +6 -0
- package/src/profiles/stateProtocol/validation.ts +51 -0
- package/src/profiles/stateProtocol.ts +100 -0
- package/src/read_filter.ts +468 -0
- package/src/reader.ts +2151 -164
- package/src/runtime/host_runtime.ts +5 -0
- package/src/runtime_memory.ts +200 -0
- package/src/runtime_memory_sampler.ts +235 -0
- package/src/schema/read_json.ts +43 -0
- package/src/schema/registry.ts +563 -59
- package/src/search/agg_format.ts +638 -0
- package/src/search/aggregate.ts +389 -0
- package/src/search/binary/codec.ts +162 -0
- package/src/search/binary/docset.ts +67 -0
- package/src/search/binary/restart_strings.ts +181 -0
- package/src/search/binary/varint.ts +34 -0
- package/src/search/bitset.ts +19 -0
- package/src/search/col_format.ts +382 -0
- package/src/search/col_runtime.ts +59 -0
- package/src/search/column_encoding.ts +43 -0
- package/src/search/companion_file_cache.ts +319 -0
- package/src/search/companion_format.ts +313 -0
- package/src/search/companion_manager.ts +1086 -0
- package/src/search/companion_plan.ts +218 -0
- package/src/search/fts_format.ts +423 -0
- package/src/search/fts_runtime.ts +333 -0
- package/src/search/query.ts +875 -0
- package/src/search/schema.ts +245 -0
- package/src/segment/cache.ts +93 -2
- package/src/segment/cached_segment.ts +89 -0
- package/src/segment/format.ts +108 -36
- package/src/segment/segmenter.ts +79 -5
- package/src/segment/segmenter_worker.ts +35 -6
- package/src/segment/segmenter_workers.ts +42 -12
- package/src/server.ts +150 -36
- package/src/sqlite/adapter.ts +185 -14
- package/src/sqlite/runtime_stats.ts +163 -0
- package/src/stats.ts +3 -3
- package/src/stream_size_reconciler.ts +100 -0
- package/src/touch/canonical_change.ts +7 -0
- package/src/touch/live_metrics.ts +94 -64
- package/src/touch/live_templates.ts +15 -1
- package/src/touch/manager.ts +166 -88
- package/src/touch/{interpreter_worker.ts → processor_worker.ts} +19 -14
- package/src/touch/spec.ts +95 -92
- package/src/touch/touch_journal.ts +4 -0
- package/src/touch/worker_pool.ts +8 -14
- package/src/touch/worker_protocol.ts +3 -3
- package/src/uploader.ts +77 -6
- package/src/util/bloom256.ts +2 -2
- package/src/util/byte_lru.ts +73 -0
- package/src/util/lru.ts +8 -0
- package/src/util/stream_paths.ts +19 -0
package/src/touch/manager.ts
CHANGED
|
@@ -2,9 +2,9 @@ import type { Config } from "../config";
|
|
|
2
2
|
import type { SqliteDurableStore } from "../db/db";
|
|
3
3
|
import type { IngestQueue } from "../ingest";
|
|
4
4
|
import type { StreamNotifier } from "../notifier";
|
|
5
|
-
import type {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import type { StreamProfileStore } from "../profiles";
|
|
6
|
+
import { listTouchCapableProfileKinds, resolveEnabledTouchCapability, resolveTouchCapability } from "../profiles";
|
|
7
|
+
import { TouchProcessorWorkerPool } from "./worker_pool";
|
|
8
8
|
import { LruCache } from "../util/lru";
|
|
9
9
|
import type { BackpressureGate } from "../backpressure";
|
|
10
10
|
import { LiveTemplateRegistry, type TemplateDecl } from "./live_templates";
|
|
@@ -27,12 +27,12 @@ const BASE_WAL_GC_INTERVAL_MS = (() => {
|
|
|
27
27
|
|
|
28
28
|
const BASE_WAL_GC_CHUNK_OFFSETS = (() => {
|
|
29
29
|
const raw = process.env.DS_BASE_WAL_GC_CHUNK_OFFSETS;
|
|
30
|
-
if (raw == null || raw.trim() === "") return
|
|
30
|
+
if (raw == null || raw.trim() === "") return 1_000_000;
|
|
31
31
|
const n = Number(raw);
|
|
32
32
|
if (!Number.isFinite(n) || n <= 0) {
|
|
33
33
|
// eslint-disable-next-line no-console
|
|
34
34
|
console.error(`invalid DS_BASE_WAL_GC_CHUNK_OFFSETS: ${raw}`);
|
|
35
|
-
return
|
|
35
|
+
return 1_000_000;
|
|
36
36
|
}
|
|
37
37
|
return Math.floor(n);
|
|
38
38
|
})();
|
|
@@ -86,7 +86,7 @@ type StreamRuntimeTotals = {
|
|
|
86
86
|
scanRowsTotal: number;
|
|
87
87
|
scanBatchesTotal: number;
|
|
88
88
|
scannedButEmitted0BatchesTotal: number;
|
|
89
|
-
|
|
89
|
+
processedThroughDeltaTotal: number;
|
|
90
90
|
touchesEmittedTotal: number;
|
|
91
91
|
touchesTableTotal: number;
|
|
92
92
|
touchesTemplateTotal: number;
|
|
@@ -99,11 +99,37 @@ type StreamRuntimeTotals = {
|
|
|
99
99
|
waitStaleTotal: number;
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
-
export
|
|
102
|
+
export type TouchProcessorManagerMemoryStats = {
|
|
103
|
+
dirtyStreams: number;
|
|
104
|
+
journals: number;
|
|
105
|
+
journalsCreatedTotal: number;
|
|
106
|
+
journalFilterBytesTotal: number;
|
|
107
|
+
fineLagCoarseOnlyStreams: number;
|
|
108
|
+
touchModeStreams: number;
|
|
109
|
+
fineTokenBucketStreams: number;
|
|
110
|
+
hotFineStreams: number;
|
|
111
|
+
lagSourceOffsetStreams: number;
|
|
112
|
+
restrictedTemplateBucketStreams: number;
|
|
113
|
+
runtimeTotalsStreams: number;
|
|
114
|
+
zeroRowBacklogStreakStreams: number;
|
|
115
|
+
templateLastSeenEntries: number;
|
|
116
|
+
templateDirtyLastSeenEntries: number;
|
|
117
|
+
templateRateStateStreams: number;
|
|
118
|
+
liveMetricsCounterStreams: number;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type TouchTopStreamEntry = {
|
|
122
|
+
stream: string;
|
|
123
|
+
journal_filter_bytes: number;
|
|
124
|
+
dirty: boolean;
|
|
125
|
+
touch_mode: "idle" | "fine" | "restricted" | "coarseOnly" | null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export class TouchProcessorManager {
|
|
103
129
|
private readonly cfg: Config;
|
|
104
130
|
private readonly db: SqliteDurableStore;
|
|
105
|
-
private readonly
|
|
106
|
-
private readonly pool:
|
|
131
|
+
private readonly profiles: StreamProfileStore;
|
|
132
|
+
private readonly pool: TouchProcessorWorkerPool;
|
|
107
133
|
private timer: any | null = null;
|
|
108
134
|
private running = false;
|
|
109
135
|
private stopping = false;
|
|
@@ -122,6 +148,7 @@ export class TouchInterpreterManager {
|
|
|
122
148
|
private readonly restrictedTemplateBucketStateByStream = new Map<string, RestrictedTemplateBucketState>();
|
|
123
149
|
private readonly runtimeTotalsByStream = new Map<string, StreamRuntimeTotals>();
|
|
124
150
|
private readonly zeroRowBacklogStreakByStream = new Map<string, number>();
|
|
151
|
+
private journalsCreatedTotal = 0;
|
|
125
152
|
private streamScanCursor = 0;
|
|
126
153
|
private restartWorkerPoolRequested = false;
|
|
127
154
|
private lastWorkerPoolRestartAtMs = 0;
|
|
@@ -131,28 +158,31 @@ export class TouchInterpreterManager {
|
|
|
131
158
|
db: SqliteDurableStore,
|
|
132
159
|
ingest: IngestQueue,
|
|
133
160
|
notifier: StreamNotifier,
|
|
134
|
-
|
|
161
|
+
profiles: StreamProfileStore,
|
|
135
162
|
backpressure?: BackpressureGate
|
|
136
163
|
) {
|
|
137
164
|
this.cfg = cfg;
|
|
138
165
|
this.db = db;
|
|
139
|
-
this.
|
|
140
|
-
this.pool = new
|
|
166
|
+
this.profiles = profiles;
|
|
167
|
+
this.pool = new TouchProcessorWorkerPool(cfg, cfg.touchWorkers);
|
|
141
168
|
this.templates = new LiveTemplateRegistry(db);
|
|
142
|
-
this.liveMetrics = new LiveMetricsV2(db, ingest, {
|
|
169
|
+
this.liveMetrics = new LiveMetricsV2(db, ingest, profiles, {
|
|
143
170
|
getTouchJournal: (stream) => {
|
|
144
171
|
const j = this.journals.get(stream);
|
|
145
172
|
if (!j) return null;
|
|
146
173
|
return { meta: j.getMeta(), interval: j.snapshotAndResetIntervalStats() };
|
|
147
174
|
},
|
|
175
|
+
onAppended: ({ lastOffset, stream }) => {
|
|
176
|
+
notifier.notify(stream, lastOffset);
|
|
177
|
+
notifier.notifyDetailsChanged(stream);
|
|
178
|
+
},
|
|
148
179
|
});
|
|
149
180
|
}
|
|
150
181
|
|
|
151
182
|
start(): void {
|
|
152
183
|
if (this.timer) return;
|
|
153
184
|
this.stopping = false;
|
|
154
|
-
this.
|
|
155
|
-
this.seedInterpretersFromRegistry();
|
|
185
|
+
this.seedTouchStateFromProfiles();
|
|
156
186
|
const liveMetricsRes = this.liveMetrics.ensureStreamResult();
|
|
157
187
|
if (Result.isError(liveMetricsRes)) {
|
|
158
188
|
// eslint-disable-next-line no-console
|
|
@@ -160,10 +190,10 @@ export class TouchInterpreterManager {
|
|
|
160
190
|
} else {
|
|
161
191
|
this.liveMetrics.start();
|
|
162
192
|
}
|
|
163
|
-
if (this.cfg.
|
|
193
|
+
if (this.cfg.touchCheckIntervalMs > 0) {
|
|
164
194
|
this.timer = setInterval(() => {
|
|
165
195
|
void this.tick();
|
|
166
|
-
}, this.cfg.
|
|
196
|
+
}, this.cfg.touchCheckIntervalMs);
|
|
167
197
|
}
|
|
168
198
|
}
|
|
169
199
|
|
|
@@ -187,6 +217,46 @@ export class TouchInterpreterManager {
|
|
|
187
217
|
this.lastWorkerPoolRestartAtMs = 0;
|
|
188
218
|
}
|
|
189
219
|
|
|
220
|
+
getMemoryStats(): TouchProcessorManagerMemoryStats {
|
|
221
|
+
let journalFilterBytesTotal = 0;
|
|
222
|
+
for (const journal of this.journals.values()) journalFilterBytesTotal += journal.getFilterBytes();
|
|
223
|
+
const templateStats = this.templates.getMemoryStats();
|
|
224
|
+
const liveMetricsStats = this.liveMetrics.getMemoryStats();
|
|
225
|
+
return {
|
|
226
|
+
dirtyStreams: this.dirty.size,
|
|
227
|
+
journals: this.journals.size,
|
|
228
|
+
journalsCreatedTotal: this.journalsCreatedTotal,
|
|
229
|
+
journalFilterBytesTotal,
|
|
230
|
+
fineLagCoarseOnlyStreams: this.fineLagCoarseOnlyByStream.size,
|
|
231
|
+
touchModeStreams: this.touchModeByStream.size,
|
|
232
|
+
fineTokenBucketStreams: this.fineTokenBucketsByStream.size,
|
|
233
|
+
hotFineStreams: this.hotFineByStream.size,
|
|
234
|
+
lagSourceOffsetStreams: this.lagSourceOffsetsByStream.size,
|
|
235
|
+
restrictedTemplateBucketStreams: this.restrictedTemplateBucketStateByStream.size,
|
|
236
|
+
runtimeTotalsStreams: this.runtimeTotalsByStream.size,
|
|
237
|
+
zeroRowBacklogStreakStreams: this.zeroRowBacklogStreakByStream.size,
|
|
238
|
+
templateLastSeenEntries: templateStats.lastSeenEntries,
|
|
239
|
+
templateDirtyLastSeenEntries: templateStats.dirtyLastSeenEntries,
|
|
240
|
+
templateRateStateStreams: templateStats.rateStateStreams,
|
|
241
|
+
liveMetricsCounterStreams: liveMetricsStats.counterStreams,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getTopStreams(limit = 5): TouchTopStreamEntry[] {
|
|
246
|
+
const rows: TouchTopStreamEntry[] = [];
|
|
247
|
+
for (const [stream, journal] of this.journals) {
|
|
248
|
+
rows.push({
|
|
249
|
+
stream,
|
|
250
|
+
journal_filter_bytes: journal.getFilterBytes(),
|
|
251
|
+
dirty: this.dirty.has(stream),
|
|
252
|
+
touch_mode: this.touchModeByStream.get(stream) ?? null,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return rows
|
|
256
|
+
.sort((a, b) => b.journal_filter_bytes - a.journal_filter_bytes || a.stream.localeCompare(b.stream))
|
|
257
|
+
.slice(0, Math.max(0, Math.floor(limit)));
|
|
258
|
+
}
|
|
259
|
+
|
|
190
260
|
notify(stream: string): void {
|
|
191
261
|
this.dirty.add(stream);
|
|
192
262
|
}
|
|
@@ -194,14 +264,15 @@ export class TouchInterpreterManager {
|
|
|
194
264
|
async tick(): Promise<void> {
|
|
195
265
|
if (this.stopping) return;
|
|
196
266
|
if (this.running) return;
|
|
197
|
-
if (this.cfg.
|
|
267
|
+
if (this.cfg.touchWorkers <= 0) return;
|
|
198
268
|
this.running = true;
|
|
199
269
|
try {
|
|
200
270
|
const nowMs = Date.now();
|
|
201
271
|
const dirtyNow = new Set(this.dirty);
|
|
202
272
|
this.dirty.clear();
|
|
203
|
-
const states = this.db.
|
|
273
|
+
const states = this.db.listStreamTouchStates();
|
|
204
274
|
if (states.length === 0) return;
|
|
275
|
+
this.pool.start();
|
|
205
276
|
const stateByStream = new Map(states.map((s) => [s.stream, s]));
|
|
206
277
|
|
|
207
278
|
const ordered: string[] = [];
|
|
@@ -209,7 +280,7 @@ export class TouchInterpreterManager {
|
|
|
209
280
|
for (const s of stateByStream.keys()) if (!dirtyNow.has(s)) ordered.push(s);
|
|
210
281
|
const prioritized = this.prioritizeStreamsForProcessing(ordered, nowMs);
|
|
211
282
|
|
|
212
|
-
const maxConcurrent = Math.max(1, this.cfg.
|
|
283
|
+
const maxConcurrent = Math.max(1, this.cfg.touchWorkers);
|
|
213
284
|
const tasks: Promise<void>[] = [];
|
|
214
285
|
if (prioritized.length > 0) {
|
|
215
286
|
const total = prioritized.length;
|
|
@@ -220,10 +291,10 @@ export class TouchInterpreterManager {
|
|
|
220
291
|
if (this.failures.shouldSkip(stream)) continue;
|
|
221
292
|
const st = stateByStream.get(stream);
|
|
222
293
|
if (!st) continue;
|
|
223
|
-
const p = this.processOne(stream, st.
|
|
294
|
+
const p = this.processOne(stream, st.processed_through).catch((e) => {
|
|
224
295
|
this.failures.recordFailure(stream);
|
|
225
296
|
// eslint-disable-next-line no-console
|
|
226
|
-
console.error("touch
|
|
297
|
+
console.error("touch processor failed", stream, e);
|
|
227
298
|
});
|
|
228
299
|
tasks.push(p);
|
|
229
300
|
}
|
|
@@ -237,23 +308,23 @@ export class TouchInterpreterManager {
|
|
|
237
308
|
this.lastWorkerPoolRestartAtMs = Date.now();
|
|
238
309
|
} catch (e) {
|
|
239
310
|
// eslint-disable-next-line no-console
|
|
240
|
-
console.error("touch
|
|
311
|
+
console.error("touch processor worker-pool restart failed", e);
|
|
241
312
|
}
|
|
242
313
|
}
|
|
243
314
|
|
|
244
|
-
// Opportunistically GC base WAL beyond the
|
|
315
|
+
// Opportunistically GC base WAL beyond the touch-processing checkpoint.
|
|
245
316
|
//
|
|
246
317
|
// commitManifest() already GC's on upload, but it can't retroactively GC
|
|
247
|
-
// rows that were held back by
|
|
318
|
+
// rows that were held back by touch-processing lag once the processor later
|
|
248
319
|
// catches up (unless another upload happens). This loop makes GC progress
|
|
249
320
|
// deterministic for "catch up after lag" scenarios.
|
|
250
321
|
for (const stream of stateByStream.keys()) {
|
|
251
322
|
if (this.stopping) break;
|
|
252
323
|
const srow = this.db.getStream(stream);
|
|
253
324
|
if (!srow || this.db.isDeleted(srow)) continue;
|
|
254
|
-
const
|
|
255
|
-
if (!
|
|
256
|
-
this.maybeGcBaseWal(stream, srow.uploaded_through,
|
|
325
|
+
const touchState = this.db.getStreamTouchState(stream);
|
|
326
|
+
if (!touchState) continue;
|
|
327
|
+
this.maybeGcBaseWal(stream, srow.uploaded_through, touchState.processed_through);
|
|
257
328
|
}
|
|
258
329
|
|
|
259
330
|
// Template retirement GC + last-seen flush (sliding window).
|
|
@@ -261,15 +332,16 @@ export class TouchInterpreterManager {
|
|
|
261
332
|
let persistIntervalMin = Number.POSITIVE_INFINITY;
|
|
262
333
|
for (const stream of stateByStream.keys()) {
|
|
263
334
|
if (this.stopping) break;
|
|
264
|
-
const
|
|
265
|
-
if (Result.isError(
|
|
335
|
+
const profileRes = this.profiles.getProfileResult(stream);
|
|
336
|
+
if (Result.isError(profileRes)) {
|
|
266
337
|
// eslint-disable-next-line no-console
|
|
267
|
-
console.error("touch
|
|
338
|
+
console.error("touch profile read failed", stream, profileRes.error.message);
|
|
268
339
|
continue;
|
|
269
340
|
}
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
341
|
+
const profile = profileRes.value;
|
|
342
|
+
const enabledTouch = resolveEnabledTouchCapability(profile);
|
|
343
|
+
if (!enabledTouch) continue;
|
|
344
|
+
const touchCfg = enabledTouch.touchCfg;
|
|
273
345
|
touchCfgByStream.set(stream, touchCfg);
|
|
274
346
|
const persistInterval = touchCfg.templates?.lastSeenPersistIntervalMs ?? 5 * 60 * 1000;
|
|
275
347
|
if (persistInterval < persistIntervalMin) persistIntervalMin = persistInterval;
|
|
@@ -296,37 +368,38 @@ export class TouchInterpreterManager {
|
|
|
296
368
|
}
|
|
297
369
|
}
|
|
298
370
|
|
|
299
|
-
private async processOne(stream: string,
|
|
371
|
+
private async processOne(stream: string, processedThroughAtStart: bigint): Promise<void> {
|
|
300
372
|
const srow = this.db.getStream(stream);
|
|
301
373
|
if (!srow || this.db.isDeleted(srow)) {
|
|
302
|
-
this.db.
|
|
374
|
+
this.db.deleteStreamTouchState(stream);
|
|
303
375
|
return;
|
|
304
376
|
}
|
|
305
377
|
|
|
306
378
|
const next = srow.next_offset;
|
|
307
379
|
if (next <= 0n) return;
|
|
308
|
-
const fromOffset =
|
|
380
|
+
const fromOffset = processedThroughAtStart + 1n;
|
|
309
381
|
const toOffset = next - 1n;
|
|
310
382
|
if (fromOffset > toOffset) return;
|
|
311
383
|
|
|
312
|
-
const
|
|
313
|
-
if (Result.isError(
|
|
384
|
+
const profileRes = this.profiles.getProfileResult(stream, srow);
|
|
385
|
+
if (Result.isError(profileRes)) {
|
|
314
386
|
// eslint-disable-next-line no-console
|
|
315
|
-
console.error("touch
|
|
316
|
-
this.db.
|
|
387
|
+
console.error("touch profile read failed", stream, profileRes.error.message);
|
|
388
|
+
this.db.deleteStreamTouchState(stream);
|
|
317
389
|
return;
|
|
318
390
|
}
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
391
|
+
const profile = profileRes.value;
|
|
392
|
+
const enabledTouch = resolveEnabledTouchCapability(profile);
|
|
393
|
+
if (!enabledTouch) {
|
|
394
|
+
this.db.deleteStreamTouchState(stream);
|
|
322
395
|
return;
|
|
323
396
|
}
|
|
324
|
-
const touchCfg =
|
|
397
|
+
const touchCfg = enabledTouch.touchCfg;
|
|
325
398
|
const failProcessing = (message: string): void => {
|
|
326
399
|
this.failures.recordFailure(stream);
|
|
327
|
-
this.liveMetrics.
|
|
400
|
+
this.liveMetrics.recordProcessorError(stream, touchCfg);
|
|
328
401
|
// eslint-disable-next-line no-console
|
|
329
|
-
console.error("touch
|
|
402
|
+
console.error("touch processor failed", stream, message);
|
|
330
403
|
};
|
|
331
404
|
|
|
332
405
|
const nowMs = Date.now();
|
|
@@ -339,7 +412,7 @@ export class TouchInterpreterManager {
|
|
|
339
412
|
|
|
340
413
|
// Guardrail: when lag/backlog grows too large, temporarily suppress
|
|
341
414
|
// fine/template touches (coarse table touches are still emitted).
|
|
342
|
-
const lagAtStart = toOffset >=
|
|
415
|
+
const lagAtStart = toOffset >= processedThroughAtStart ? toOffset - processedThroughAtStart : 0n;
|
|
343
416
|
const suppressFineDueToLag = this.computeSuppressFineDueToLag(stream, touchCfg, lagAtStart, hasFineDemand);
|
|
344
417
|
const j = this.getOrCreateJournal(stream, touchCfg);
|
|
345
418
|
j.setCoalesceMs(this.computeAdaptiveCoalesceMs(touchCfg, lagAtStart, hasAnyWaiters));
|
|
@@ -375,7 +448,7 @@ export class TouchInterpreterManager {
|
|
|
375
448
|
if (fineGranularity !== "template") {
|
|
376
449
|
this.restrictedTemplateBucketStateByStream.delete(stream);
|
|
377
450
|
}
|
|
378
|
-
const
|
|
451
|
+
const processingMode: "full" | "hotTemplatesOnly" = fineGranularity === "template" ? "hotTemplatesOnly" : "full";
|
|
379
452
|
const touchMode: "idle" | "fine" | "restricted" | "coarseOnly" = !hasAnyWaiters ? "idle" : emitFineTouches ? (suppressFineDueToLag ? "restricted" : "fine") : "coarseOnly";
|
|
380
453
|
this.touchModeByStream.set(stream, touchMode);
|
|
381
454
|
|
|
@@ -383,13 +456,13 @@ export class TouchInterpreterManager {
|
|
|
383
456
|
stream,
|
|
384
457
|
fromOffset,
|
|
385
458
|
toOffset,
|
|
386
|
-
|
|
387
|
-
maxRows: Math.max(1, this.cfg.
|
|
388
|
-
maxBytes: Math.max(1, this.cfg.
|
|
459
|
+
profile,
|
|
460
|
+
maxRows: Math.max(1, this.cfg.touchMaxBatchRows),
|
|
461
|
+
maxBytes: Math.max(1, this.cfg.touchMaxBatchBytes),
|
|
389
462
|
emitFineTouches,
|
|
390
463
|
fineTouchBudget: emitFineTouches ? fineBudget : 0,
|
|
391
464
|
fineGranularity,
|
|
392
|
-
|
|
465
|
+
processingMode,
|
|
393
466
|
filterHotTemplates: !!(hotFine && hotFine.templateFilteringEnabled),
|
|
394
467
|
hotTemplateIds: hotFine?.hotTemplateIdsForWorker ?? null,
|
|
395
468
|
});
|
|
@@ -407,7 +480,7 @@ export class TouchInterpreterManager {
|
|
|
407
480
|
this.restartWorkerPoolRequested = true;
|
|
408
481
|
// eslint-disable-next-line no-console
|
|
409
482
|
console.error(
|
|
410
|
-
"touch
|
|
483
|
+
"touch processor produced zero-row batch despite WAL backlog; scheduling worker-pool restart",
|
|
411
484
|
stream,
|
|
412
485
|
`from=${fromOffset.toString()}`,
|
|
413
486
|
`to=${toOffset.toString()}`
|
|
@@ -477,7 +550,7 @@ export class TouchInterpreterManager {
|
|
|
477
550
|
this.lagSourceOffsetsByStream.set(stream, effectiveLag);
|
|
478
551
|
const maxSourceTsMs = Number(res.stats.maxSourceTsMs ?? 0);
|
|
479
552
|
const commitLagMs = maxSourceTsMs > 0 ? Math.max(0, Date.now() - maxSourceTsMs) : undefined;
|
|
480
|
-
this.liveMetrics.
|
|
553
|
+
this.liveMetrics.recordProcessorBatch({
|
|
481
554
|
stream,
|
|
482
555
|
touchCfg,
|
|
483
556
|
rowsRead: res.stats.rowsRead,
|
|
@@ -504,9 +577,13 @@ export class TouchInterpreterManager {
|
|
|
504
577
|
broadFineWaitersActive: hotFine?.broadFineWaitersActive ?? 0,
|
|
505
578
|
scannedButEmitted0: res.stats.rowsRead > 0 && touches.length === 0,
|
|
506
579
|
noInterestFastForward: false,
|
|
507
|
-
|
|
508
|
-
res.processedThrough >=
|
|
509
|
-
? Number(
|
|
580
|
+
processedThroughDelta:
|
|
581
|
+
res.processedThrough >= processedThroughAtStart
|
|
582
|
+
? Number(
|
|
583
|
+
(res.processedThrough - processedThroughAtStart) > BigInt(Number.MAX_SAFE_INTEGER)
|
|
584
|
+
? BigInt(Number.MAX_SAFE_INTEGER)
|
|
585
|
+
: res.processedThrough - processedThroughAtStart
|
|
586
|
+
)
|
|
510
587
|
: 0,
|
|
511
588
|
touchesEmittedDelta: touches.length,
|
|
512
589
|
});
|
|
@@ -514,15 +591,19 @@ export class TouchInterpreterManager {
|
|
|
514
591
|
// ignore
|
|
515
592
|
}
|
|
516
593
|
|
|
517
|
-
const
|
|
518
|
-
res.processedThrough >=
|
|
519
|
-
? Number(
|
|
594
|
+
const processedDelta =
|
|
595
|
+
res.processedThrough >= processedThroughAtStart
|
|
596
|
+
? Number(
|
|
597
|
+
(res.processedThrough - processedThroughAtStart) > BigInt(Number.MAX_SAFE_INTEGER)
|
|
598
|
+
? BigInt(Number.MAX_SAFE_INTEGER)
|
|
599
|
+
: res.processedThrough - processedThroughAtStart
|
|
600
|
+
)
|
|
520
601
|
: 0;
|
|
521
602
|
const totals = this.getOrCreateRuntimeTotals(stream);
|
|
522
603
|
totals.scanBatchesTotal += 1;
|
|
523
604
|
totals.scanRowsTotal += Math.max(0, res.stats.rowsRead);
|
|
524
605
|
if (res.stats.rowsRead > 0 && touches.length === 0) totals.scannedButEmitted0BatchesTotal += 1;
|
|
525
|
-
totals.
|
|
606
|
+
totals.processedThroughDeltaTotal += processedDelta;
|
|
526
607
|
totals.touchesEmittedTotal += touches.length;
|
|
527
608
|
let tableTouches = 0;
|
|
528
609
|
let templateTouches = 0;
|
|
@@ -537,13 +618,13 @@ export class TouchInterpreterManager {
|
|
|
537
618
|
totals.fineTouchesSkippedColdKeyTotal += fineSkippedColdKey;
|
|
538
619
|
totals.fineTouchesSkippedTemplateBucketTotal += fineSkippedTemplateBucket;
|
|
539
620
|
|
|
540
|
-
this.db.
|
|
621
|
+
this.db.updateStreamTouchStateThrough(stream, res.processedThrough);
|
|
541
622
|
if (res.processedThrough < toOffset) this.dirty.add(stream);
|
|
542
623
|
this.failures.recordSuccess(stream);
|
|
543
624
|
}
|
|
544
625
|
|
|
545
|
-
private maybeGcBaseWal(stream: string, uploadedThrough: bigint,
|
|
546
|
-
const gcTargetThrough =
|
|
626
|
+
private maybeGcBaseWal(stream: string, uploadedThrough: bigint, processedThrough: bigint): void {
|
|
627
|
+
const gcTargetThrough = processedThrough < uploadedThrough ? processedThrough : uploadedThrough;
|
|
547
628
|
if (gcTargetThrough < 0n) return;
|
|
548
629
|
|
|
549
630
|
const now = Date.now();
|
|
@@ -575,24 +656,20 @@ export class TouchInterpreterManager {
|
|
|
575
656
|
}
|
|
576
657
|
}
|
|
577
658
|
|
|
578
|
-
private
|
|
579
|
-
// Bootstrap support: bootstrapFromR2 restores
|
|
580
|
-
// populate
|
|
659
|
+
private seedTouchStateFromProfiles(): void {
|
|
660
|
+
// Bootstrap support: bootstrapFromR2 restores profile state but does not
|
|
661
|
+
// populate stream_touch_state. Seeding here makes touch processing start working
|
|
581
662
|
// after bootstraps and restarts without requiring a no-op config update.
|
|
582
663
|
try {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const stream
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
continue;
|
|
664
|
+
for (const kind of listTouchCapableProfileKinds()) {
|
|
665
|
+
const streams = this.db.listStreamsByProfile(kind);
|
|
666
|
+
for (const stream of streams) {
|
|
667
|
+
const profileRes = this.profiles.getProfileResult(stream);
|
|
668
|
+
if (Result.isError(profileRes)) continue;
|
|
669
|
+
const touchCapability = resolveTouchCapability(profileRes.value);
|
|
670
|
+
if (!touchCapability) continue;
|
|
671
|
+
touchCapability.syncState({ db: this.db, stream, profile: profileRes.value });
|
|
592
672
|
}
|
|
593
|
-
const enabled = !!raw?.interpreter?.touch?.enabled;
|
|
594
|
-
if (enabled) this.db.ensureStreamInterpreter(stream);
|
|
595
|
-
else this.db.deleteStreamInterpreter(stream);
|
|
596
673
|
}
|
|
597
674
|
} catch {
|
|
598
675
|
// ignore
|
|
@@ -718,7 +795,7 @@ export class TouchInterpreterManager {
|
|
|
718
795
|
scanRowsTotal: number;
|
|
719
796
|
scanBatchesTotal: number;
|
|
720
797
|
scannedButEmitted0BatchesTotal: number;
|
|
721
|
-
|
|
798
|
+
processedThroughDeltaTotal: number;
|
|
722
799
|
touchesEmittedTotal: number;
|
|
723
800
|
touchesTableTotal: number;
|
|
724
801
|
touchesTemplateTotal: number;
|
|
@@ -758,7 +835,7 @@ export class TouchInterpreterManager {
|
|
|
758
835
|
scanRowsTotal: totals.scanRowsTotal,
|
|
759
836
|
scanBatchesTotal: totals.scanBatchesTotal,
|
|
760
837
|
scannedButEmitted0BatchesTotal: totals.scannedButEmitted0BatchesTotal,
|
|
761
|
-
|
|
838
|
+
processedThroughDeltaTotal: totals.processedThroughDeltaTotal,
|
|
762
839
|
touchesEmittedTotal: totals.touchesEmittedTotal,
|
|
763
840
|
touchesTableTotal: totals.touchesTableTotal,
|
|
764
841
|
touchesTemplateTotal: totals.touchesTemplateTotal,
|
|
@@ -823,6 +900,7 @@ export class TouchInterpreterManager {
|
|
|
823
900
|
keyIndexMaxKeys: mem.keyIndexMaxKeys ?? 32,
|
|
824
901
|
});
|
|
825
902
|
this.journals.set(stream, j);
|
|
903
|
+
this.journalsCreatedTotal += 1;
|
|
826
904
|
return j;
|
|
827
905
|
}
|
|
828
906
|
|
|
@@ -867,13 +945,13 @@ export class TouchInterpreterManager {
|
|
|
867
945
|
const cold: string[] = [];
|
|
868
946
|
for (const stream of ordered) {
|
|
869
947
|
let hasActiveWaiters = false;
|
|
870
|
-
const
|
|
871
|
-
if (Result.isError(
|
|
948
|
+
const profileRes = this.profiles.getProfileResult(stream);
|
|
949
|
+
if (Result.isError(profileRes)) {
|
|
872
950
|
hasActiveWaiters = false;
|
|
873
951
|
} else {
|
|
874
|
-
const
|
|
875
|
-
if (
|
|
876
|
-
const snap = this.getHotFineSnapshot(stream,
|
|
952
|
+
const enabledTouch = resolveEnabledTouchCapability(profileRes.value);
|
|
953
|
+
if (enabledTouch) {
|
|
954
|
+
const snap = this.getHotFineSnapshot(stream, enabledTouch.touchCfg, nowMs);
|
|
877
955
|
hasActiveWaiters = snap.fineWaitersActive + snap.coarseWaitersActive > 0;
|
|
878
956
|
}
|
|
879
957
|
}
|
|
@@ -1140,7 +1218,7 @@ export class TouchInterpreterManager {
|
|
|
1140
1218
|
scanRowsTotal: 0,
|
|
1141
1219
|
scanBatchesTotal: 0,
|
|
1142
1220
|
scannedButEmitted0BatchesTotal: 0,
|
|
1143
|
-
|
|
1221
|
+
processedThroughDeltaTotal: 0,
|
|
1144
1222
|
touchesEmittedTotal: 0,
|
|
1145
1223
|
touchesTableTotal: 0,
|
|
1146
1224
|
touchesTemplateTotal: 0,
|
|
@@ -2,19 +2,23 @@ import { parentPort, workerData } from "node:worker_threads";
|
|
|
2
2
|
import { Result } from "better-result";
|
|
3
3
|
import type { Config } from "../config.ts";
|
|
4
4
|
import { SqliteDurableStore } from "../db/db.ts";
|
|
5
|
+
import { resolveEnabledTouchCapability } from "../profiles/index.ts";
|
|
6
|
+
import type { HostRuntime } from "../runtime/host_runtime.ts";
|
|
7
|
+
import { setSqliteRuntimeOverride } from "../sqlite/adapter.ts";
|
|
5
8
|
import { initConsoleLogging } from "../util/log.ts";
|
|
6
9
|
import type { ProcessRequest } from "./worker_protocol.ts";
|
|
7
|
-
import { interpretRecordToChanges } from "./engine.ts";
|
|
8
10
|
import { encodeTemplateArg, tableKeyIdFor, templateKeyIdFor, watchKeyIdFor, type TemplateEncoding } from "./live_keys.ts";
|
|
9
|
-
import { isTouchEnabled } from "./spec.ts";
|
|
10
11
|
|
|
11
12
|
initConsoleLogging();
|
|
12
13
|
|
|
13
|
-
const data = workerData as { config: Config };
|
|
14
|
+
const data = workerData as { config: Config; hostRuntime?: HostRuntime };
|
|
14
15
|
const cfg = data.config;
|
|
16
|
+
// Bun worker_threads can miss the Bun globals that the main thread sees.
|
|
17
|
+
// Use the parent host runtime hint before the worker opens SQLite.
|
|
18
|
+
setSqliteRuntimeOverride(data.hostRuntime ?? null);
|
|
15
19
|
// The main server process initializes/migrates schema; workers should avoid
|
|
16
20
|
// concurrent migrations on the same sqlite file.
|
|
17
|
-
const db = new SqliteDurableStore(cfg.dbPath, { cacheBytes: cfg.
|
|
21
|
+
const db = new SqliteDurableStore(cfg.dbPath, { cacheBytes: cfg.workerSqliteCacheBytes, skipMigrations: true });
|
|
18
22
|
|
|
19
23
|
const decoder = new TextDecoder();
|
|
20
24
|
|
|
@@ -26,12 +30,12 @@ type ActiveTemplate = {
|
|
|
26
30
|
activeFromSourceOffset: bigint;
|
|
27
31
|
};
|
|
28
32
|
|
|
29
|
-
type
|
|
33
|
+
type TouchProcessorWorkerError = { kind: "missing_old_value"; message: string };
|
|
30
34
|
|
|
31
35
|
async function handleProcess(msg: ProcessRequest): Promise<void> {
|
|
32
|
-
const { stream, fromOffset, toOffset,
|
|
36
|
+
const { stream, fromOffset, toOffset, profile, maxRows, maxBytes } = msg;
|
|
33
37
|
const failProcess = (message: string): void => {
|
|
34
|
-
const err = Result.err<never,
|
|
38
|
+
const err = Result.err<never, TouchProcessorWorkerError>({ kind: "missing_old_value", message });
|
|
35
39
|
parentPort?.postMessage({
|
|
36
40
|
type: "error",
|
|
37
41
|
id: msg.id,
|
|
@@ -39,22 +43,23 @@ async function handleProcess(msg: ProcessRequest): Promise<void> {
|
|
|
39
43
|
message: err.error.message,
|
|
40
44
|
});
|
|
41
45
|
};
|
|
42
|
-
|
|
46
|
+
const enabledTouch = resolveEnabledTouchCapability(profile);
|
|
47
|
+
if (!enabledTouch) {
|
|
43
48
|
parentPort?.postMessage({
|
|
44
49
|
type: "error",
|
|
45
50
|
id: msg.id,
|
|
46
51
|
stream,
|
|
47
|
-
message: "touch not enabled for
|
|
52
|
+
message: "touch not enabled for profile",
|
|
48
53
|
});
|
|
49
54
|
return;
|
|
50
55
|
}
|
|
51
|
-
const touch =
|
|
56
|
+
const { capability: touchCapability, touchCfg: touch } = enabledTouch;
|
|
52
57
|
|
|
53
58
|
const fineBudgetRaw = msg.fineTouchBudget ?? touch.fineTouchBudgetPerBatch;
|
|
54
59
|
const fineBudget = fineBudgetRaw == null ? null : Math.max(0, Math.floor(fineBudgetRaw));
|
|
55
60
|
const fineGranularity = msg.fineGranularity === "template" ? "template" : "key";
|
|
56
|
-
const
|
|
57
|
-
const hotTemplatesOnly = fineGranularity === "template" &&
|
|
61
|
+
const processingMode = msg.processingMode === "hotTemplatesOnly" ? "hotTemplatesOnly" : "full";
|
|
62
|
+
const hotTemplatesOnly = fineGranularity === "template" && processingMode === "hotTemplatesOnly";
|
|
58
63
|
|
|
59
64
|
const emitFineTouches = msg.emitFineTouches !== false && fineBudget !== 0;
|
|
60
65
|
let fineBudgetExhausted = fineBudget != null && fineBudget <= 0;
|
|
@@ -217,7 +222,7 @@ async function handleProcess(msg: ProcessRequest): Promise<void> {
|
|
|
217
222
|
continue;
|
|
218
223
|
}
|
|
219
224
|
|
|
220
|
-
const canonical =
|
|
225
|
+
const canonical = touchCapability.deriveCanonicalChanges(value, profile);
|
|
221
226
|
changes += canonical.length;
|
|
222
227
|
if (canonical.length === 0) continue;
|
|
223
228
|
const watermark = offset.toString();
|
|
@@ -299,7 +304,7 @@ async function handleProcess(msg: ProcessRequest): Promise<void> {
|
|
|
299
304
|
// Policy for missing/insufficient before image:
|
|
300
305
|
// - coarse: emit no fine touches (table touch already guarantees correctness)
|
|
301
306
|
// - skipBefore: emit after-only touch
|
|
302
|
-
// - error: fail the
|
|
307
|
+
// - error: fail the processing batch
|
|
303
308
|
const kAfter = compute(afterObj);
|
|
304
309
|
const kBefore = compute(beforeObj);
|
|
305
310
|
|