@prisma/streams-server 0.0.1 → 0.1.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/CODE_OF_CONDUCT.md +45 -0
- package/CONTRIBUTING.md +68 -0
- package/LICENSE +201 -0
- package/README.md +39 -2
- package/SECURITY.md +33 -0
- package/bin/prisma-streams-server +2 -0
- package/package.json +29 -34
- package/src/app.ts +74 -0
- package/src/app_core.ts +1983 -0
- package/src/app_local.ts +46 -0
- package/src/backpressure.ts +66 -0
- package/src/bootstrap.ts +239 -0
- package/src/config.ts +251 -0
- package/src/db/db.ts +1440 -0
- package/src/db/schema.ts +619 -0
- package/src/expiry_sweeper.ts +44 -0
- package/src/hist.ts +169 -0
- package/src/index/binary_fuse.ts +379 -0
- package/src/index/indexer.ts +745 -0
- package/src/index/run_cache.ts +84 -0
- package/src/index/run_format.ts +213 -0
- package/src/ingest.ts +655 -0
- package/src/lens/lens.ts +501 -0
- package/src/manifest.ts +114 -0
- package/src/memory.ts +155 -0
- package/src/metrics.ts +161 -0
- package/src/metrics_emitter.ts +50 -0
- package/src/notifier.ts +64 -0
- package/src/objectstore/interface.ts +13 -0
- package/src/objectstore/mock_r2.ts +269 -0
- package/src/objectstore/null.ts +32 -0
- package/src/objectstore/r2.ts +128 -0
- package/src/offset.ts +70 -0
- package/src/reader.ts +454 -0
- package/src/runtime/hash.ts +156 -0
- package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
- package/src/runtime/hash_vendor/NOTICE.md +8 -0
- package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
- package/src/schema/lens_schema.ts +290 -0
- package/src/schema/proof.ts +547 -0
- package/src/schema/registry.ts +405 -0
- package/src/segment/cache.ts +179 -0
- package/src/segment/format.ts +331 -0
- package/src/segment/segmenter.ts +326 -0
- package/src/segment/segmenter_worker.ts +43 -0
- package/src/segment/segmenter_workers.ts +94 -0
- package/src/server.ts +326 -0
- package/src/sqlite/adapter.ts +164 -0
- package/src/stats.ts +205 -0
- package/src/touch/engine.ts +41 -0
- package/src/touch/interpreter_worker.ts +459 -0
- package/src/touch/live_keys.ts +118 -0
- package/src/touch/live_metrics.ts +858 -0
- package/src/touch/live_templates.ts +619 -0
- package/src/touch/manager.ts +1341 -0
- package/src/touch/naming.ts +13 -0
- package/src/touch/routing_key_notifier.ts +275 -0
- package/src/touch/spec.ts +526 -0
- package/src/touch/touch_journal.ts +671 -0
- package/src/touch/touch_key_id.ts +20 -0
- package/src/touch/worker_pool.ts +189 -0
- package/src/touch/worker_protocol.ts +58 -0
- package/src/types/proper-lockfile.d.ts +1 -0
- package/src/uploader.ts +317 -0
- package/src/util/base32_crockford.ts +81 -0
- package/src/util/bloom256.ts +67 -0
- package/src/util/cleanup.ts +22 -0
- package/src/util/crc32c.ts +29 -0
- package/src/util/ds_error.ts +15 -0
- package/src/util/duration.ts +17 -0
- package/src/util/endian.ts +53 -0
- package/src/util/json_pointer.ts +148 -0
- package/src/util/log.ts +25 -0
- package/src/util/lru.ts +45 -0
- package/src/util/retry.ts +35 -0
- package/src/util/siphash.ts +71 -0
- package/src/util/stream_paths.ts +31 -0
- package/src/util/time.ts +14 -0
- package/src/util/yield.ts +3 -0
- package/build/index.d.mts +0 -1
- package/build/index.d.ts +0 -1
- package/build/index.js +0 -0
- package/build/index.mjs +0 -1
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
import type { IngestQueue } from "../ingest";
|
|
2
|
+
import type { SqliteDurableStore } from "../db/db";
|
|
3
|
+
import { STREAM_FLAG_TOUCH } from "../db/db";
|
|
4
|
+
import { encodeOffset } from "../offset";
|
|
5
|
+
import type { TouchConfig } from "./spec";
|
|
6
|
+
import type { TemplateLifecycleEvent } from "./live_templates";
|
|
7
|
+
import { resolveTouchStreamName } from "./naming";
|
|
8
|
+
import type { RoutingKeyNotifier } from "./routing_key_notifier";
|
|
9
|
+
import type { TouchJournalIntervalStats, TouchJournalMeta } from "./touch_journal";
|
|
10
|
+
import { Result } from "better-result";
|
|
11
|
+
|
|
12
|
+
export type TouchKind = "table" | "template";
|
|
13
|
+
|
|
14
|
+
export type TouchEventPayload = {
|
|
15
|
+
sourceOffset: string;
|
|
16
|
+
entity: string;
|
|
17
|
+
kind: TouchKind;
|
|
18
|
+
templateId?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type WaitOutcome = "touched" | "timeout" | "stale";
|
|
22
|
+
type EnsureLiveMetricsStreamError = {
|
|
23
|
+
kind: "live_metrics_stream_content_type_mismatch";
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type LatencyHistogram = {
|
|
28
|
+
bounds: number[];
|
|
29
|
+
counts: number[];
|
|
30
|
+
record: (ms: number) => void;
|
|
31
|
+
p50: () => number;
|
|
32
|
+
p95: () => number;
|
|
33
|
+
p99: () => number;
|
|
34
|
+
reset: () => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function makeLatencyHistogram(): LatencyHistogram {
|
|
38
|
+
const bounds = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10_000, 30_000, 120_000];
|
|
39
|
+
const counts = new Array(bounds.length + 1).fill(0);
|
|
40
|
+
const record = (ms: number) => {
|
|
41
|
+
const x = Math.max(0, Math.floor(ms));
|
|
42
|
+
let i = 0;
|
|
43
|
+
while (i < bounds.length && x > bounds[i]) i++;
|
|
44
|
+
counts[i] += 1;
|
|
45
|
+
};
|
|
46
|
+
const quantile = (q: number) => {
|
|
47
|
+
const total = counts.reduce((a, b) => a + b, 0);
|
|
48
|
+
if (total === 0) return 0;
|
|
49
|
+
const target = Math.ceil(total * q);
|
|
50
|
+
let acc = 0;
|
|
51
|
+
for (let i = 0; i < counts.length; i++) {
|
|
52
|
+
acc += counts[i];
|
|
53
|
+
if (acc >= target) {
|
|
54
|
+
return i < bounds.length ? bounds[i] : bounds[bounds.length - 1];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return bounds[bounds.length - 1];
|
|
58
|
+
};
|
|
59
|
+
const reset = () => {
|
|
60
|
+
for (let i = 0; i < counts.length; i++) counts[i] = 0;
|
|
61
|
+
};
|
|
62
|
+
return { bounds, counts, record, p50: () => quantile(0.5), p95: () => quantile(0.95), p99: () => quantile(0.99), reset };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function nowIso(ms: number): string {
|
|
66
|
+
return new Date(ms).toISOString();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function envString(name: string): string | null {
|
|
70
|
+
const v = process.env[name];
|
|
71
|
+
return v && v.trim() !== "" ? v.trim() : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getInstanceId(): string {
|
|
75
|
+
return envString("DS_INSTANCE_ID") ?? envString("HOSTNAME") ?? "local";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getRegion(): string {
|
|
79
|
+
return envString("DS_REGION") ?? "local";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type StreamCounters = {
|
|
83
|
+
touch: {
|
|
84
|
+
coarseIntervalMs: number;
|
|
85
|
+
coalesceWindowMs: number;
|
|
86
|
+
mode: "idle" | "fine" | "restricted" | "coarseOnly";
|
|
87
|
+
hotFineKeys: number;
|
|
88
|
+
hotTemplates: number;
|
|
89
|
+
hotFineKeysActive: number;
|
|
90
|
+
hotFineKeysGrace: number;
|
|
91
|
+
hotTemplatesActive: number;
|
|
92
|
+
hotTemplatesGrace: number;
|
|
93
|
+
fineWaitersActive: number;
|
|
94
|
+
coarseWaitersActive: number;
|
|
95
|
+
broadFineWaitersActive: number;
|
|
96
|
+
touchesEmitted: number;
|
|
97
|
+
uniqueKeysTouched: number;
|
|
98
|
+
tableTouchesEmitted: number;
|
|
99
|
+
templateTouchesEmitted: number;
|
|
100
|
+
staleResponses: number;
|
|
101
|
+
fineTouchesDroppedDueToBudget: number;
|
|
102
|
+
fineTouchesSkippedColdTemplate: number;
|
|
103
|
+
fineTouchesSkippedColdKey: number;
|
|
104
|
+
fineTouchesSkippedTemplateBucket: number;
|
|
105
|
+
fineTouchesSuppressedBatchesDueToLag: number;
|
|
106
|
+
fineTouchesSuppressedMsDueToLag: number;
|
|
107
|
+
fineTouchesSuppressedBatchesDueToBudget: number;
|
|
108
|
+
};
|
|
109
|
+
gc: {
|
|
110
|
+
baseWalGcCalls: number;
|
|
111
|
+
baseWalGcDeletedRows: number;
|
|
112
|
+
baseWalGcDeletedBytes: number;
|
|
113
|
+
baseWalGcMsSum: number;
|
|
114
|
+
baseWalGcMsMax: number;
|
|
115
|
+
};
|
|
116
|
+
templates: {
|
|
117
|
+
activated: number;
|
|
118
|
+
retired: number;
|
|
119
|
+
evicted: number;
|
|
120
|
+
activationDenied: number;
|
|
121
|
+
};
|
|
122
|
+
wait: {
|
|
123
|
+
calls: number;
|
|
124
|
+
keysWatchedTotal: number;
|
|
125
|
+
touched: number;
|
|
126
|
+
timeout: number;
|
|
127
|
+
stale: number;
|
|
128
|
+
latencySumMs: number;
|
|
129
|
+
latencyHist: LatencyHistogram;
|
|
130
|
+
};
|
|
131
|
+
interpreter: {
|
|
132
|
+
eventsIn: number;
|
|
133
|
+
changesOut: number;
|
|
134
|
+
errors: number;
|
|
135
|
+
lagSourceOffsets: number;
|
|
136
|
+
scannedBatches: number;
|
|
137
|
+
scannedButEmitted0Batches: number;
|
|
138
|
+
noInterestFastForwardBatches: number;
|
|
139
|
+
interpretedThroughDelta: number;
|
|
140
|
+
touchesEmittedDelta: number;
|
|
141
|
+
commitLagSamples: number;
|
|
142
|
+
commitLagMsSum: number;
|
|
143
|
+
commitLagHist: LatencyHistogram;
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function defaultCounters(touchCfg: TouchConfig): StreamCounters {
|
|
148
|
+
return {
|
|
149
|
+
touch: {
|
|
150
|
+
coarseIntervalMs: touchCfg.coarseIntervalMs ?? 100,
|
|
151
|
+
coalesceWindowMs: touchCfg.touchCoalesceWindowMs ?? 100,
|
|
152
|
+
mode: "idle",
|
|
153
|
+
hotFineKeys: 0,
|
|
154
|
+
hotTemplates: 0,
|
|
155
|
+
hotFineKeysActive: 0,
|
|
156
|
+
hotFineKeysGrace: 0,
|
|
157
|
+
hotTemplatesActive: 0,
|
|
158
|
+
hotTemplatesGrace: 0,
|
|
159
|
+
fineWaitersActive: 0,
|
|
160
|
+
coarseWaitersActive: 0,
|
|
161
|
+
broadFineWaitersActive: 0,
|
|
162
|
+
touchesEmitted: 0,
|
|
163
|
+
uniqueKeysTouched: 0,
|
|
164
|
+
tableTouchesEmitted: 0,
|
|
165
|
+
templateTouchesEmitted: 0,
|
|
166
|
+
staleResponses: 0,
|
|
167
|
+
fineTouchesDroppedDueToBudget: 0,
|
|
168
|
+
fineTouchesSkippedColdTemplate: 0,
|
|
169
|
+
fineTouchesSkippedColdKey: 0,
|
|
170
|
+
fineTouchesSkippedTemplateBucket: 0,
|
|
171
|
+
fineTouchesSuppressedBatchesDueToLag: 0,
|
|
172
|
+
fineTouchesSuppressedMsDueToLag: 0,
|
|
173
|
+
fineTouchesSuppressedBatchesDueToBudget: 0,
|
|
174
|
+
},
|
|
175
|
+
gc: {
|
|
176
|
+
baseWalGcCalls: 0,
|
|
177
|
+
baseWalGcDeletedRows: 0,
|
|
178
|
+
baseWalGcDeletedBytes: 0,
|
|
179
|
+
baseWalGcMsSum: 0,
|
|
180
|
+
baseWalGcMsMax: 0,
|
|
181
|
+
},
|
|
182
|
+
templates: { activated: 0, retired: 0, evicted: 0, activationDenied: 0 },
|
|
183
|
+
wait: { calls: 0, keysWatchedTotal: 0, touched: 0, timeout: 0, stale: 0, latencySumMs: 0, latencyHist: makeLatencyHistogram() },
|
|
184
|
+
interpreter: {
|
|
185
|
+
eventsIn: 0,
|
|
186
|
+
changesOut: 0,
|
|
187
|
+
errors: 0,
|
|
188
|
+
lagSourceOffsets: 0,
|
|
189
|
+
scannedBatches: 0,
|
|
190
|
+
scannedButEmitted0Batches: 0,
|
|
191
|
+
noInterestFastForwardBatches: 0,
|
|
192
|
+
interpretedThroughDelta: 0,
|
|
193
|
+
touchesEmittedDelta: 0,
|
|
194
|
+
commitLagSamples: 0,
|
|
195
|
+
commitLagMsSum: 0,
|
|
196
|
+
commitLagHist: makeLatencyHistogram(),
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export class LiveMetricsV2 {
|
|
202
|
+
private readonly db: SqliteDurableStore;
|
|
203
|
+
private readonly ingest: IngestQueue;
|
|
204
|
+
private readonly metricsStream: string;
|
|
205
|
+
private readonly enabled: boolean;
|
|
206
|
+
private readonly intervalMs: number;
|
|
207
|
+
private readonly snapshotIntervalMs: number;
|
|
208
|
+
private readonly snapshotChunkSize: number;
|
|
209
|
+
private readonly retentionMs: number;
|
|
210
|
+
private readonly routingKeyNotifier?: RoutingKeyNotifier;
|
|
211
|
+
private readonly getTouchJournal?: (derivedStream: string) => { meta: TouchJournalMeta; interval: TouchJournalIntervalStats } | null;
|
|
212
|
+
private timer: any | null = null;
|
|
213
|
+
private snapshotTimer: any | null = null;
|
|
214
|
+
private retentionTimer: any | null = null;
|
|
215
|
+
private lagTimer: any | null = null;
|
|
216
|
+
|
|
217
|
+
private readonly instanceId = getInstanceId();
|
|
218
|
+
private readonly region = getRegion();
|
|
219
|
+
|
|
220
|
+
private readonly counters = new Map<string, StreamCounters>();
|
|
221
|
+
|
|
222
|
+
private lagExpectedMs = 0;
|
|
223
|
+
private lagMaxMs = 0;
|
|
224
|
+
private lagSumMs = 0;
|
|
225
|
+
private lagSamples = 0;
|
|
226
|
+
|
|
227
|
+
constructor(
|
|
228
|
+
db: SqliteDurableStore,
|
|
229
|
+
ingest: IngestQueue,
|
|
230
|
+
opts?: {
|
|
231
|
+
enabled?: boolean;
|
|
232
|
+
stream?: string;
|
|
233
|
+
intervalMs?: number;
|
|
234
|
+
snapshotIntervalMs?: number;
|
|
235
|
+
snapshotChunkSize?: number;
|
|
236
|
+
retentionMs?: number;
|
|
237
|
+
routingKeyNotifier?: RoutingKeyNotifier;
|
|
238
|
+
getTouchJournal?: (derivedStream: string) => { meta: TouchJournalMeta; interval: TouchJournalIntervalStats } | null;
|
|
239
|
+
}
|
|
240
|
+
) {
|
|
241
|
+
this.db = db;
|
|
242
|
+
this.ingest = ingest;
|
|
243
|
+
this.enabled = opts?.enabled !== false;
|
|
244
|
+
this.metricsStream = opts?.stream ?? "live.metrics";
|
|
245
|
+
this.intervalMs = opts?.intervalMs ?? 1000;
|
|
246
|
+
this.snapshotIntervalMs = opts?.snapshotIntervalMs ?? 60_000;
|
|
247
|
+
this.snapshotChunkSize = opts?.snapshotChunkSize ?? 200;
|
|
248
|
+
this.retentionMs = opts?.retentionMs ?? 7 * 24 * 60 * 60 * 1000;
|
|
249
|
+
this.routingKeyNotifier = opts?.routingKeyNotifier;
|
|
250
|
+
this.getTouchJournal = opts?.getTouchJournal;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
start(): void {
|
|
254
|
+
if (!this.enabled) return;
|
|
255
|
+
if (this.timer) return;
|
|
256
|
+
this.timer = setInterval(() => {
|
|
257
|
+
void this.flushTick();
|
|
258
|
+
}, this.intervalMs);
|
|
259
|
+
this.snapshotTimer = setInterval(() => {
|
|
260
|
+
void this.emitSnapshots();
|
|
261
|
+
}, this.snapshotIntervalMs);
|
|
262
|
+
// Retention trims are best-effort; 60s granularity is fine.
|
|
263
|
+
this.retentionTimer = setInterval(() => {
|
|
264
|
+
try {
|
|
265
|
+
this.db.trimWalByAge(this.metricsStream, this.retentionMs);
|
|
266
|
+
} catch {
|
|
267
|
+
// ignore
|
|
268
|
+
}
|
|
269
|
+
}, 60_000);
|
|
270
|
+
|
|
271
|
+
// Track event-loop lag at a tighter cadence than the tick interval to
|
|
272
|
+
// debug cases where timeouts/fire events are delayed under load.
|
|
273
|
+
const lagIntervalMs = 100;
|
|
274
|
+
this.lagExpectedMs = Date.now() + lagIntervalMs;
|
|
275
|
+
this.lagMaxMs = 0;
|
|
276
|
+
this.lagSumMs = 0;
|
|
277
|
+
this.lagSamples = 0;
|
|
278
|
+
this.lagTimer = setInterval(() => {
|
|
279
|
+
const now = Date.now();
|
|
280
|
+
const lag = Math.max(0, now - this.lagExpectedMs);
|
|
281
|
+
this.lagMaxMs = Math.max(this.lagMaxMs, lag);
|
|
282
|
+
this.lagSumMs += lag;
|
|
283
|
+
this.lagSamples += 1;
|
|
284
|
+
this.lagExpectedMs += lagIntervalMs;
|
|
285
|
+
// If the loop was paused for a long time, avoid building up a huge debt.
|
|
286
|
+
if (this.lagExpectedMs < now - 5 * lagIntervalMs) this.lagExpectedMs = now + lagIntervalMs;
|
|
287
|
+
}, lagIntervalMs);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
stop(): void {
|
|
291
|
+
if (this.timer) clearInterval(this.timer);
|
|
292
|
+
if (this.snapshotTimer) clearInterval(this.snapshotTimer);
|
|
293
|
+
if (this.retentionTimer) clearInterval(this.retentionTimer);
|
|
294
|
+
if (this.lagTimer) clearInterval(this.lagTimer);
|
|
295
|
+
this.timer = null;
|
|
296
|
+
this.snapshotTimer = null;
|
|
297
|
+
this.retentionTimer = null;
|
|
298
|
+
this.lagTimer = null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
ensureStreamResult(): Result<void, EnsureLiveMetricsStreamError> {
|
|
302
|
+
if (!this.enabled) return Result.ok(undefined);
|
|
303
|
+
const existing = this.db.getStream(this.metricsStream);
|
|
304
|
+
if (existing) {
|
|
305
|
+
if (String(existing.content_type) !== "application/json") {
|
|
306
|
+
return Result.err({
|
|
307
|
+
kind: "live_metrics_stream_content_type_mismatch",
|
|
308
|
+
message: `live metrics stream content-type mismatch: ${existing.content_type}`,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if ((existing.stream_flags & STREAM_FLAG_TOUCH) === 0) this.db.addStreamFlags(this.metricsStream, STREAM_FLAG_TOUCH);
|
|
312
|
+
return Result.ok(undefined);
|
|
313
|
+
}
|
|
314
|
+
// Treat live.metrics as WAL-only (like touch streams) so age-based retention
|
|
315
|
+
// is enforceable without segment/object-store GC.
|
|
316
|
+
this.db.ensureStream(this.metricsStream, { contentType: "application/json", streamFlags: STREAM_FLAG_TOUCH });
|
|
317
|
+
return Result.ok(undefined);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private get(stream: string, touchCfg: TouchConfig): StreamCounters {
|
|
321
|
+
const existing = this.counters.get(stream);
|
|
322
|
+
if (existing) return existing;
|
|
323
|
+
const c = defaultCounters(touchCfg);
|
|
324
|
+
this.counters.set(stream, c);
|
|
325
|
+
return c;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private ensure(stream: string): StreamCounters {
|
|
329
|
+
const existing = this.counters.get(stream);
|
|
330
|
+
if (existing) return existing;
|
|
331
|
+
// Use defaults; actual config values will be filled in when we observe the stream.
|
|
332
|
+
const c = defaultCounters({ enabled: true } as TouchConfig);
|
|
333
|
+
this.counters.set(stream, c);
|
|
334
|
+
return c;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
recordInterpreterError(stream: string, touchCfg: TouchConfig): void {
|
|
338
|
+
const c = this.get(stream, touchCfg);
|
|
339
|
+
c.interpreter.errors += 1;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
recordInterpreterBatch(args: {
|
|
343
|
+
stream: string;
|
|
344
|
+
touchCfg: TouchConfig;
|
|
345
|
+
rowsRead: number;
|
|
346
|
+
changes: number;
|
|
347
|
+
touches: Array<{ keyId: number; kind: TouchKind }>;
|
|
348
|
+
lagSourceOffsets: number;
|
|
349
|
+
touchMode: "idle" | "fine" | "restricted" | "coarseOnly";
|
|
350
|
+
hotFineKeys?: number;
|
|
351
|
+
hotTemplates?: number;
|
|
352
|
+
hotFineKeysActive?: number;
|
|
353
|
+
hotFineKeysGrace?: number;
|
|
354
|
+
hotTemplatesActive?: number;
|
|
355
|
+
hotTemplatesGrace?: number;
|
|
356
|
+
fineWaitersActive?: number;
|
|
357
|
+
coarseWaitersActive?: number;
|
|
358
|
+
broadFineWaitersActive?: number;
|
|
359
|
+
commitLagMs?: number;
|
|
360
|
+
fineTouchesDroppedDueToBudget?: number;
|
|
361
|
+
fineTouchesSkippedColdTemplate?: number;
|
|
362
|
+
fineTouchesSkippedColdKey?: number;
|
|
363
|
+
fineTouchesSkippedTemplateBucket?: number;
|
|
364
|
+
fineTouchesSuppressedDueToLag?: boolean;
|
|
365
|
+
fineTouchesSuppressedDueToLagMs?: number;
|
|
366
|
+
fineTouchesSuppressedDueToBudget?: boolean;
|
|
367
|
+
scannedButEmitted0?: boolean;
|
|
368
|
+
noInterestFastForward?: boolean;
|
|
369
|
+
interpretedThroughDelta?: number;
|
|
370
|
+
touchesEmittedDelta?: number;
|
|
371
|
+
}): void {
|
|
372
|
+
const c = this.get(args.stream, args.touchCfg);
|
|
373
|
+
c.touch.coarseIntervalMs = args.touchCfg.coarseIntervalMs ?? c.touch.coarseIntervalMs;
|
|
374
|
+
c.touch.coalesceWindowMs = args.touchCfg.touchCoalesceWindowMs ?? c.touch.coalesceWindowMs;
|
|
375
|
+
c.touch.mode = args.touchMode;
|
|
376
|
+
c.touch.hotFineKeys = Math.max(c.touch.hotFineKeys, Math.max(0, Math.floor(args.hotFineKeys ?? 0)));
|
|
377
|
+
c.touch.hotTemplates = Math.max(c.touch.hotTemplates, Math.max(0, Math.floor(args.hotTemplates ?? 0)));
|
|
378
|
+
c.touch.hotFineKeysActive = Math.max(c.touch.hotFineKeysActive, Math.max(0, Math.floor(args.hotFineKeysActive ?? 0)));
|
|
379
|
+
c.touch.hotFineKeysGrace = Math.max(c.touch.hotFineKeysGrace, Math.max(0, Math.floor(args.hotFineKeysGrace ?? 0)));
|
|
380
|
+
c.touch.hotTemplatesActive = Math.max(c.touch.hotTemplatesActive, Math.max(0, Math.floor(args.hotTemplatesActive ?? 0)));
|
|
381
|
+
c.touch.hotTemplatesGrace = Math.max(c.touch.hotTemplatesGrace, Math.max(0, Math.floor(args.hotTemplatesGrace ?? 0)));
|
|
382
|
+
c.touch.fineWaitersActive = Math.max(c.touch.fineWaitersActive, Math.max(0, Math.floor(args.fineWaitersActive ?? 0)));
|
|
383
|
+
c.touch.coarseWaitersActive = Math.max(c.touch.coarseWaitersActive, Math.max(0, Math.floor(args.coarseWaitersActive ?? 0)));
|
|
384
|
+
c.touch.broadFineWaitersActive = Math.max(c.touch.broadFineWaitersActive, Math.max(0, Math.floor(args.broadFineWaitersActive ?? 0)));
|
|
385
|
+
c.interpreter.eventsIn += Math.max(0, args.rowsRead);
|
|
386
|
+
c.interpreter.changesOut += Math.max(0, args.changes);
|
|
387
|
+
c.interpreter.lagSourceOffsets = Math.max(c.interpreter.lagSourceOffsets, Math.max(0, args.lagSourceOffsets));
|
|
388
|
+
c.interpreter.scannedBatches += 1;
|
|
389
|
+
if (args.scannedButEmitted0) c.interpreter.scannedButEmitted0Batches += 1;
|
|
390
|
+
if (args.noInterestFastForward) c.interpreter.noInterestFastForwardBatches += 1;
|
|
391
|
+
c.interpreter.interpretedThroughDelta += Math.max(0, Math.floor(args.interpretedThroughDelta ?? 0));
|
|
392
|
+
c.interpreter.touchesEmittedDelta += Math.max(0, Math.floor(args.touchesEmittedDelta ?? 0));
|
|
393
|
+
if (args.commitLagMs != null && Number.isFinite(args.commitLagMs) && args.commitLagMs >= 0) {
|
|
394
|
+
c.interpreter.commitLagSamples += 1;
|
|
395
|
+
c.interpreter.commitLagMsSum += args.commitLagMs;
|
|
396
|
+
c.interpreter.commitLagHist.record(args.commitLagMs);
|
|
397
|
+
}
|
|
398
|
+
c.touch.fineTouchesDroppedDueToBudget += Math.max(0, args.fineTouchesDroppedDueToBudget ?? 0);
|
|
399
|
+
c.touch.fineTouchesSkippedColdTemplate += Math.max(0, args.fineTouchesSkippedColdTemplate ?? 0);
|
|
400
|
+
c.touch.fineTouchesSkippedColdKey += Math.max(0, args.fineTouchesSkippedColdKey ?? 0);
|
|
401
|
+
c.touch.fineTouchesSkippedTemplateBucket += Math.max(0, args.fineTouchesSkippedTemplateBucket ?? 0);
|
|
402
|
+
if (args.fineTouchesSuppressedDueToLag) c.touch.fineTouchesSuppressedBatchesDueToLag += 1;
|
|
403
|
+
c.touch.fineTouchesSuppressedMsDueToLag += Math.max(0, args.fineTouchesSuppressedDueToLagMs ?? 0);
|
|
404
|
+
if (args.fineTouchesSuppressedDueToBudget) c.touch.fineTouchesSuppressedBatchesDueToBudget += 1;
|
|
405
|
+
|
|
406
|
+
const unique = new Set<number>();
|
|
407
|
+
let table = 0;
|
|
408
|
+
let tpl = 0;
|
|
409
|
+
for (const t of args.touches) {
|
|
410
|
+
unique.add(t.keyId >>> 0);
|
|
411
|
+
if (t.kind === "table") table++;
|
|
412
|
+
else tpl++;
|
|
413
|
+
}
|
|
414
|
+
c.touch.touchesEmitted += args.touches.length;
|
|
415
|
+
c.touch.uniqueKeysTouched += unique.size;
|
|
416
|
+
c.touch.tableTouchesEmitted += table;
|
|
417
|
+
c.touch.templateTouchesEmitted += tpl;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
recordWait(stream: string, touchCfg: TouchConfig, keysCount: number, outcome: WaitOutcome, latencyMs: number): void {
|
|
421
|
+
const c = this.get(stream, touchCfg);
|
|
422
|
+
c.wait.calls += 1;
|
|
423
|
+
c.wait.keysWatchedTotal += Math.max(0, keysCount);
|
|
424
|
+
c.wait.latencySumMs += Math.max(0, latencyMs);
|
|
425
|
+
c.wait.latencyHist.record(latencyMs);
|
|
426
|
+
if (outcome === "touched") c.wait.touched += 1;
|
|
427
|
+
else if (outcome === "timeout") c.wait.timeout += 1;
|
|
428
|
+
else c.wait.stale += 1;
|
|
429
|
+
if (outcome === "stale") c.touch.staleResponses += 1;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
recordBaseWalGc(stream: string, args: { deletedRows: number; deletedBytes: number; durationMs: number }): void {
|
|
433
|
+
const c = this.ensure(stream);
|
|
434
|
+
c.gc.baseWalGcCalls += 1;
|
|
435
|
+
c.gc.baseWalGcDeletedRows += Math.max(0, args.deletedRows);
|
|
436
|
+
c.gc.baseWalGcDeletedBytes += Math.max(0, args.deletedBytes);
|
|
437
|
+
c.gc.baseWalGcMsSum += Math.max(0, args.durationMs);
|
|
438
|
+
c.gc.baseWalGcMsMax = Math.max(c.gc.baseWalGcMsMax, Math.max(0, args.durationMs));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async emitLifecycle(events: TemplateLifecycleEvent[]): Promise<void> {
|
|
442
|
+
if (!this.enabled) return;
|
|
443
|
+
if (events.length === 0) return;
|
|
444
|
+
|
|
445
|
+
const rows = events.map((e) => ({
|
|
446
|
+
routingKey: new TextEncoder().encode(`${e.stream}|${e.type}`),
|
|
447
|
+
contentType: "application/json",
|
|
448
|
+
payload: new TextEncoder().encode(
|
|
449
|
+
JSON.stringify({
|
|
450
|
+
...e,
|
|
451
|
+
liveSystemVersion: "v2",
|
|
452
|
+
instanceId: this.instanceId,
|
|
453
|
+
region: this.region,
|
|
454
|
+
})
|
|
455
|
+
),
|
|
456
|
+
}));
|
|
457
|
+
|
|
458
|
+
for (const e of events) {
|
|
459
|
+
const c = this.ensure(e.stream);
|
|
460
|
+
if (e.type === "live.template_activated") c.templates.activated += 1;
|
|
461
|
+
else if (e.type === "live.template_retired") c.templates.retired += 1;
|
|
462
|
+
else if (e.type === "live.template_evicted") c.templates.evicted += 1;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
await this.ingest.appendInternal({
|
|
467
|
+
stream: this.metricsStream,
|
|
468
|
+
baseAppendMs: BigInt(Date.now()),
|
|
469
|
+
rows,
|
|
470
|
+
contentType: "application/json",
|
|
471
|
+
});
|
|
472
|
+
} catch {
|
|
473
|
+
// best-effort
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
recordActivationDenied(stream: string, touchCfg: TouchConfig, n = 1): void {
|
|
478
|
+
const c = this.get(stream, touchCfg);
|
|
479
|
+
c.templates.activationDenied += Math.max(0, n);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private async flushTick(): Promise<void> {
|
|
483
|
+
if (!this.enabled) return;
|
|
484
|
+
const nowMs = Date.now();
|
|
485
|
+
const clampBigInt = (v: bigint): number => {
|
|
486
|
+
if (v <= 0n) return 0;
|
|
487
|
+
const max = BigInt(Number.MAX_SAFE_INTEGER);
|
|
488
|
+
return v > max ? Number.MAX_SAFE_INTEGER : Number(v);
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const states = this.db.listStreamInterpreters();
|
|
492
|
+
if (states.length === 0) return;
|
|
493
|
+
|
|
494
|
+
const rows: Array<{ routingKey: Uint8Array | null; contentType: string; payload: Uint8Array }> = [];
|
|
495
|
+
const encoder = new TextEncoder();
|
|
496
|
+
|
|
497
|
+
const loopLagMax = this.lagMaxMs;
|
|
498
|
+
const loopLagAvg = this.lagSamples > 0 ? this.lagSumMs / this.lagSamples : 0;
|
|
499
|
+
this.lagMaxMs = 0;
|
|
500
|
+
this.lagSumMs = 0;
|
|
501
|
+
this.lagSamples = 0;
|
|
502
|
+
|
|
503
|
+
const rkInterval = this.routingKeyNotifier?.snapshotAndResetIntervalStats() ?? null;
|
|
504
|
+
|
|
505
|
+
for (const st of states) {
|
|
506
|
+
const stream = st.stream;
|
|
507
|
+
const regRow = this.db.getStream(stream);
|
|
508
|
+
if (!regRow) continue;
|
|
509
|
+
|
|
510
|
+
const touchCfg = ((): TouchConfig | null => {
|
|
511
|
+
try {
|
|
512
|
+
const row = this.db.getSchemaRegistry(stream);
|
|
513
|
+
if (!row) return null;
|
|
514
|
+
const raw = JSON.parse(row.registry_json);
|
|
515
|
+
const cfg = raw?.interpreter?.touch;
|
|
516
|
+
if (!cfg || !cfg.enabled) return null;
|
|
517
|
+
return cfg as TouchConfig;
|
|
518
|
+
} catch {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
})();
|
|
522
|
+
if (!touchCfg) continue;
|
|
523
|
+
|
|
524
|
+
const c = this.get(stream, touchCfg);
|
|
525
|
+
const storage = (touchCfg.storage ?? "memory") as "memory" | "sqlite";
|
|
526
|
+
const derived = resolveTouchStreamName(stream, touchCfg);
|
|
527
|
+
const journal = storage === "memory" ? this.getTouchJournal?.(derived) ?? null : null;
|
|
528
|
+
const trow = (() => {
|
|
529
|
+
try {
|
|
530
|
+
return this.db.getStream(derived);
|
|
531
|
+
} catch {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
})();
|
|
535
|
+
const touchTailSeq = trow ? (trow.next_offset > 0n ? trow.next_offset - 1n : -1n) : -1n;
|
|
536
|
+
let touchWalOldestOffset: string | null = null;
|
|
537
|
+
try {
|
|
538
|
+
const oldest = this.db.getWalOldestOffset(derived);
|
|
539
|
+
touchWalOldestOffset = oldest == null || !trow ? null : encodeOffset(trow.epoch, oldest);
|
|
540
|
+
} catch {
|
|
541
|
+
touchWalOldestOffset = null;
|
|
542
|
+
}
|
|
543
|
+
const waitActive =
|
|
544
|
+
storage === "memory" ? (journal?.meta.activeWaiters ?? 0) : this.routingKeyNotifier ? this.routingKeyNotifier.getActiveWaiters(derived) : 0;
|
|
545
|
+
const tailSeq = regRow.next_offset > 0n ? regRow.next_offset - 1n : -1n;
|
|
546
|
+
const interpretedThrough = st.interpreted_through;
|
|
547
|
+
const gcThrough = interpretedThrough < regRow.uploaded_through ? interpretedThrough : regRow.uploaded_through;
|
|
548
|
+
const backlog = tailSeq >= interpretedThrough ? tailSeq - interpretedThrough : 0n;
|
|
549
|
+
const backlogNum = backlog > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(backlog);
|
|
550
|
+
let walOldestOffset: string | null = null;
|
|
551
|
+
try {
|
|
552
|
+
const oldest = this.db.getWalOldestOffset(stream);
|
|
553
|
+
walOldestOffset = oldest == null ? null : encodeOffset(regRow.epoch, oldest);
|
|
554
|
+
} catch {
|
|
555
|
+
walOldestOffset = null;
|
|
556
|
+
}
|
|
557
|
+
const activeTemplates = (() => {
|
|
558
|
+
try {
|
|
559
|
+
const row = this.db.db.query(`SELECT COUNT(*) as cnt FROM live_templates WHERE stream=? AND state='active';`).get(stream) as any;
|
|
560
|
+
return Number(row?.cnt ?? 0);
|
|
561
|
+
} catch {
|
|
562
|
+
return 0;
|
|
563
|
+
}
|
|
564
|
+
})();
|
|
565
|
+
|
|
566
|
+
const tick = {
|
|
567
|
+
type: "live.tick",
|
|
568
|
+
ts: nowIso(nowMs),
|
|
569
|
+
stream,
|
|
570
|
+
liveSystemVersion: "v2",
|
|
571
|
+
instanceId: this.instanceId,
|
|
572
|
+
region: this.region,
|
|
573
|
+
touch: {
|
|
574
|
+
storage,
|
|
575
|
+
coarseIntervalMs: c.touch.coarseIntervalMs,
|
|
576
|
+
coalesceWindowMs: c.touch.coalesceWindowMs,
|
|
577
|
+
mode: c.touch.mode,
|
|
578
|
+
hotFineKeys: c.touch.hotFineKeys,
|
|
579
|
+
hotTemplates: c.touch.hotTemplates,
|
|
580
|
+
hotFineKeysActive: c.touch.hotFineKeysActive,
|
|
581
|
+
hotFineKeysGrace: c.touch.hotFineKeysGrace,
|
|
582
|
+
hotTemplatesActive: c.touch.hotTemplatesActive,
|
|
583
|
+
hotTemplatesGrace: c.touch.hotTemplatesGrace,
|
|
584
|
+
fineWaitersActive: c.touch.fineWaitersActive,
|
|
585
|
+
coarseWaitersActive: c.touch.coarseWaitersActive,
|
|
586
|
+
broadFineWaitersActive: c.touch.broadFineWaitersActive,
|
|
587
|
+
touchesEmitted: c.touch.touchesEmitted,
|
|
588
|
+
uniqueKeysTouched: c.touch.uniqueKeysTouched,
|
|
589
|
+
tableTouchesEmitted: c.touch.tableTouchesEmitted,
|
|
590
|
+
templateTouchesEmitted: c.touch.templateTouchesEmitted,
|
|
591
|
+
staleResponses: c.touch.staleResponses,
|
|
592
|
+
fineTouchesDroppedDueToBudget: c.touch.fineTouchesDroppedDueToBudget,
|
|
593
|
+
fineTouchesSkippedColdTemplate: c.touch.fineTouchesSkippedColdTemplate,
|
|
594
|
+
fineTouchesSkippedColdKey: c.touch.fineTouchesSkippedColdKey,
|
|
595
|
+
fineTouchesSkippedTemplateBucket: c.touch.fineTouchesSkippedTemplateBucket,
|
|
596
|
+
fineTouchesSuppressedBatchesDueToLag: c.touch.fineTouchesSuppressedBatchesDueToLag,
|
|
597
|
+
fineTouchesSuppressedSecondsDueToLag: c.touch.fineTouchesSuppressedMsDueToLag / 1000,
|
|
598
|
+
fineTouchesSuppressedBatchesDueToBudget: c.touch.fineTouchesSuppressedBatchesDueToBudget,
|
|
599
|
+
cursor: storage === "memory" ? (journal?.meta.cursor ?? null) : null,
|
|
600
|
+
epoch: storage === "memory" ? (journal?.meta.epoch ?? null) : null,
|
|
601
|
+
generation: storage === "memory" ? (journal?.meta.generation ?? null) : null,
|
|
602
|
+
pendingKeys: storage === "memory" ? (journal?.meta.pendingKeys ?? 0) : 0,
|
|
603
|
+
overflowBuckets: storage === "memory" ? (journal?.meta.overflowBuckets ?? 0) : 0,
|
|
604
|
+
walTailOffset: trow ? encodeOffset(trow.epoch, touchTailSeq) : null,
|
|
605
|
+
walNextOffset: trow ? encodeOffset(trow.epoch, trow.next_offset) : null,
|
|
606
|
+
walOldestOffset: touchWalOldestOffset,
|
|
607
|
+
walRetainedRows: trow ? clampBigInt(trow.wal_rows) : 0,
|
|
608
|
+
walRetainedBytes: trow ? clampBigInt(trow.wal_bytes) : 0,
|
|
609
|
+
},
|
|
610
|
+
templates: {
|
|
611
|
+
active: activeTemplates,
|
|
612
|
+
activated: c.templates.activated,
|
|
613
|
+
retired: c.templates.retired,
|
|
614
|
+
evicted: c.templates.evicted,
|
|
615
|
+
activationDenied: c.templates.activationDenied,
|
|
616
|
+
},
|
|
617
|
+
wait: {
|
|
618
|
+
calls: c.wait.calls,
|
|
619
|
+
keysWatchedTotal: c.wait.keysWatchedTotal,
|
|
620
|
+
avgKeysPerCall: c.wait.calls > 0 ? c.wait.keysWatchedTotal / c.wait.calls : 0,
|
|
621
|
+
touched: c.wait.touched,
|
|
622
|
+
timeout: c.wait.timeout,
|
|
623
|
+
stale: c.wait.stale,
|
|
624
|
+
avgLatencyMs: c.wait.calls > 0 ? c.wait.latencySumMs / c.wait.calls : 0,
|
|
625
|
+
p95LatencyMs: c.wait.latencyHist.p95(),
|
|
626
|
+
activeWaiters: waitActive,
|
|
627
|
+
timeoutsFired: storage === "memory" ? (journal?.interval.timeoutsFired ?? 0) : rkInterval?.timeoutsFired ?? 0,
|
|
628
|
+
timeoutSweeps: storage === "memory" ? (journal?.interval.timeoutSweeps ?? 0) : rkInterval?.timeoutSweeps ?? 0,
|
|
629
|
+
timeoutSweepMsSum: storage === "memory" ? (journal?.interval.timeoutSweepMsSum ?? 0) : rkInterval?.timeoutSweepMsSum ?? 0,
|
|
630
|
+
timeoutSweepMsMax: storage === "memory" ? (journal?.interval.timeoutSweepMsMax ?? 0) : rkInterval?.timeoutSweepMsMax ?? 0,
|
|
631
|
+
notifyWakeups: storage === "memory" ? (journal?.interval.notifyWakeups ?? 0) : 0,
|
|
632
|
+
notifyFlushes: storage === "memory" ? (journal?.interval.notifyFlushes ?? 0) : 0,
|
|
633
|
+
notifyWakeMsSum: storage === "memory" ? (journal?.interval.notifyWakeMsSum ?? 0) : 0,
|
|
634
|
+
notifyWakeMsMax: storage === "memory" ? (journal?.interval.notifyWakeMsMax ?? 0) : 0,
|
|
635
|
+
timeoutHeapSize: storage === "memory" ? (journal?.interval.heapSize ?? 0) : rkInterval?.heapSize ?? 0,
|
|
636
|
+
},
|
|
637
|
+
interpreter: {
|
|
638
|
+
eventsIn: c.interpreter.eventsIn,
|
|
639
|
+
changesOut: c.interpreter.changesOut,
|
|
640
|
+
errors: c.interpreter.errors,
|
|
641
|
+
lagSourceOffsets: c.interpreter.lagSourceOffsets,
|
|
642
|
+
scannedBatches: c.interpreter.scannedBatches,
|
|
643
|
+
scannedButEmitted0Batches: c.interpreter.scannedButEmitted0Batches,
|
|
644
|
+
noInterestFastForwardBatches: c.interpreter.noInterestFastForwardBatches,
|
|
645
|
+
interpretedThroughDelta: c.interpreter.interpretedThroughDelta,
|
|
646
|
+
touchesEmittedDelta: c.interpreter.touchesEmittedDelta,
|
|
647
|
+
commitLagMsAvg: c.interpreter.commitLagSamples > 0 ? c.interpreter.commitLagMsSum / c.interpreter.commitLagSamples : 0,
|
|
648
|
+
commitLagMsP50: c.interpreter.commitLagHist.p50(),
|
|
649
|
+
commitLagMsP95: c.interpreter.commitLagHist.p95(),
|
|
650
|
+
commitLagMsP99: c.interpreter.commitLagHist.p99(),
|
|
651
|
+
},
|
|
652
|
+
base: {
|
|
653
|
+
tailOffset: encodeOffset(regRow.epoch, tailSeq),
|
|
654
|
+
nextOffset: encodeOffset(regRow.epoch, regRow.next_offset),
|
|
655
|
+
sealedThrough: encodeOffset(regRow.epoch, regRow.sealed_through),
|
|
656
|
+
uploadedThrough: encodeOffset(regRow.epoch, regRow.uploaded_through),
|
|
657
|
+
interpretedThrough: encodeOffset(regRow.epoch, interpretedThrough),
|
|
658
|
+
gcThrough: encodeOffset(regRow.epoch, gcThrough),
|
|
659
|
+
walOldestOffset,
|
|
660
|
+
walRetainedRows: clampBigInt(regRow.wal_rows),
|
|
661
|
+
walRetainedBytes: clampBigInt(regRow.wal_bytes),
|
|
662
|
+
gc: {
|
|
663
|
+
calls: c.gc.baseWalGcCalls,
|
|
664
|
+
deletedRows: c.gc.baseWalGcDeletedRows,
|
|
665
|
+
deletedBytes: c.gc.baseWalGcDeletedBytes,
|
|
666
|
+
msSum: c.gc.baseWalGcMsSum,
|
|
667
|
+
msMax: c.gc.baseWalGcMsMax,
|
|
668
|
+
},
|
|
669
|
+
backlogSourceOffsets: backlogNum,
|
|
670
|
+
},
|
|
671
|
+
process: {
|
|
672
|
+
eventLoopLagMsMax: loopLagMax,
|
|
673
|
+
eventLoopLagMsAvg: loopLagAvg,
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
rows.push({
|
|
678
|
+
routingKey: encoder.encode(`${stream}|live.tick`),
|
|
679
|
+
contentType: "application/json",
|
|
680
|
+
payload: encoder.encode(JSON.stringify(tick)),
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// Reset interval counters (keep config).
|
|
684
|
+
c.touch.hotFineKeys = 0;
|
|
685
|
+
c.touch.hotTemplates = 0;
|
|
686
|
+
c.touch.hotFineKeysActive = 0;
|
|
687
|
+
c.touch.hotFineKeysGrace = 0;
|
|
688
|
+
c.touch.hotTemplatesActive = 0;
|
|
689
|
+
c.touch.hotTemplatesGrace = 0;
|
|
690
|
+
c.touch.fineWaitersActive = 0;
|
|
691
|
+
c.touch.coarseWaitersActive = 0;
|
|
692
|
+
c.touch.broadFineWaitersActive = 0;
|
|
693
|
+
c.touch.touchesEmitted = 0;
|
|
694
|
+
c.touch.uniqueKeysTouched = 0;
|
|
695
|
+
c.touch.tableTouchesEmitted = 0;
|
|
696
|
+
c.touch.templateTouchesEmitted = 0;
|
|
697
|
+
c.touch.staleResponses = 0;
|
|
698
|
+
c.touch.fineTouchesDroppedDueToBudget = 0;
|
|
699
|
+
c.touch.fineTouchesSkippedColdTemplate = 0;
|
|
700
|
+
c.touch.fineTouchesSkippedColdKey = 0;
|
|
701
|
+
c.touch.fineTouchesSkippedTemplateBucket = 0;
|
|
702
|
+
c.touch.fineTouchesSuppressedBatchesDueToLag = 0;
|
|
703
|
+
c.touch.fineTouchesSuppressedMsDueToLag = 0;
|
|
704
|
+
c.touch.fineTouchesSuppressedBatchesDueToBudget = 0;
|
|
705
|
+
c.touch.mode = "idle";
|
|
706
|
+
c.templates.activated = 0;
|
|
707
|
+
c.templates.retired = 0;
|
|
708
|
+
c.templates.evicted = 0;
|
|
709
|
+
c.templates.activationDenied = 0;
|
|
710
|
+
c.wait.calls = 0;
|
|
711
|
+
c.wait.keysWatchedTotal = 0;
|
|
712
|
+
c.wait.touched = 0;
|
|
713
|
+
c.wait.timeout = 0;
|
|
714
|
+
c.wait.stale = 0;
|
|
715
|
+
c.wait.latencySumMs = 0;
|
|
716
|
+
c.wait.latencyHist.reset();
|
|
717
|
+
c.interpreter.eventsIn = 0;
|
|
718
|
+
c.interpreter.changesOut = 0;
|
|
719
|
+
c.interpreter.errors = 0;
|
|
720
|
+
c.interpreter.lagSourceOffsets = 0;
|
|
721
|
+
c.interpreter.scannedBatches = 0;
|
|
722
|
+
c.interpreter.scannedButEmitted0Batches = 0;
|
|
723
|
+
c.interpreter.noInterestFastForwardBatches = 0;
|
|
724
|
+
c.interpreter.interpretedThroughDelta = 0;
|
|
725
|
+
c.interpreter.touchesEmittedDelta = 0;
|
|
726
|
+
c.interpreter.commitLagSamples = 0;
|
|
727
|
+
c.interpreter.commitLagMsSum = 0;
|
|
728
|
+
c.interpreter.commitLagHist.reset();
|
|
729
|
+
c.gc.baseWalGcCalls = 0;
|
|
730
|
+
c.gc.baseWalGcDeletedRows = 0;
|
|
731
|
+
c.gc.baseWalGcDeletedBytes = 0;
|
|
732
|
+
c.gc.baseWalGcMsSum = 0;
|
|
733
|
+
c.gc.baseWalGcMsMax = 0;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (rows.length === 0) return;
|
|
737
|
+
try {
|
|
738
|
+
await this.ingest.appendInternal({
|
|
739
|
+
stream: this.metricsStream,
|
|
740
|
+
baseAppendMs: BigInt(nowMs),
|
|
741
|
+
rows,
|
|
742
|
+
contentType: "application/json",
|
|
743
|
+
});
|
|
744
|
+
} catch {
|
|
745
|
+
// best-effort
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private async emitSnapshots(): Promise<void> {
|
|
750
|
+
if (!this.enabled) return;
|
|
751
|
+
const nowMs = Date.now();
|
|
752
|
+
const streams = this.db.listStreamInterpreters().map((r) => r.stream);
|
|
753
|
+
if (streams.length === 0) return;
|
|
754
|
+
|
|
755
|
+
const encoder = new TextEncoder();
|
|
756
|
+
const rows: Array<{ routingKey: Uint8Array | null; contentType: string; payload: Uint8Array }> = [];
|
|
757
|
+
|
|
758
|
+
for (const stream of streams) {
|
|
759
|
+
let templates: any[] = [];
|
|
760
|
+
try {
|
|
761
|
+
templates = this.db.db
|
|
762
|
+
.query(
|
|
763
|
+
`SELECT template_id, entity, fields_json, last_seen_at_ms, state
|
|
764
|
+
FROM live_templates
|
|
765
|
+
WHERE stream=? AND state='active'
|
|
766
|
+
ORDER BY entity ASC, template_id ASC;`
|
|
767
|
+
)
|
|
768
|
+
.all(stream) as any[];
|
|
769
|
+
} catch {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const snapshotId = `s-${stream}-${nowMs}`;
|
|
774
|
+
const activeTemplates = templates.length;
|
|
775
|
+
rows.push({
|
|
776
|
+
routingKey: encoder.encode(`${stream}|live.templates_snapshot_start`),
|
|
777
|
+
contentType: "application/json",
|
|
778
|
+
payload: encoder.encode(
|
|
779
|
+
JSON.stringify({
|
|
780
|
+
type: "live.templates_snapshot_start",
|
|
781
|
+
ts: nowIso(nowMs),
|
|
782
|
+
stream,
|
|
783
|
+
liveSystemVersion: "v2",
|
|
784
|
+
instanceId: this.instanceId,
|
|
785
|
+
region: this.region,
|
|
786
|
+
snapshotId,
|
|
787
|
+
activeTemplates,
|
|
788
|
+
chunkSize: this.snapshotChunkSize,
|
|
789
|
+
})
|
|
790
|
+
),
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
let chunkIndex = 0;
|
|
794
|
+
for (let i = 0; i < templates.length; i += this.snapshotChunkSize) {
|
|
795
|
+
const slice = templates.slice(i, i + this.snapshotChunkSize);
|
|
796
|
+
const payloadTemplates = slice.map((t) => {
|
|
797
|
+
const templateId = String(t.template_id);
|
|
798
|
+
const entity = String(t.entity);
|
|
799
|
+
let fields: string[] = [];
|
|
800
|
+
try {
|
|
801
|
+
const f = JSON.parse(String(t.fields_json));
|
|
802
|
+
if (Array.isArray(f)) fields = f.map(String);
|
|
803
|
+
} catch {
|
|
804
|
+
// ignore
|
|
805
|
+
}
|
|
806
|
+
const lastSeenAgoMs = Math.max(0, nowMs - Number(t.last_seen_at_ms));
|
|
807
|
+
return { templateId, entity, fields, lastSeenAgoMs, state: "active" };
|
|
808
|
+
});
|
|
809
|
+
rows.push({
|
|
810
|
+
routingKey: encoder.encode(`${stream}|live.templates_snapshot_chunk`),
|
|
811
|
+
contentType: "application/json",
|
|
812
|
+
payload: encoder.encode(
|
|
813
|
+
JSON.stringify({
|
|
814
|
+
type: "live.templates_snapshot_chunk",
|
|
815
|
+
ts: nowIso(nowMs),
|
|
816
|
+
stream,
|
|
817
|
+
liveSystemVersion: "v2",
|
|
818
|
+
instanceId: this.instanceId,
|
|
819
|
+
region: this.region,
|
|
820
|
+
snapshotId,
|
|
821
|
+
chunkIndex,
|
|
822
|
+
templates: payloadTemplates,
|
|
823
|
+
})
|
|
824
|
+
),
|
|
825
|
+
});
|
|
826
|
+
chunkIndex++;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
rows.push({
|
|
830
|
+
routingKey: encoder.encode(`${stream}|live.templates_snapshot_end`),
|
|
831
|
+
contentType: "application/json",
|
|
832
|
+
payload: encoder.encode(
|
|
833
|
+
JSON.stringify({
|
|
834
|
+
type: "live.templates_snapshot_end",
|
|
835
|
+
ts: nowIso(nowMs),
|
|
836
|
+
stream,
|
|
837
|
+
liveSystemVersion: "v2",
|
|
838
|
+
instanceId: this.instanceId,
|
|
839
|
+
region: this.region,
|
|
840
|
+
snapshotId,
|
|
841
|
+
})
|
|
842
|
+
),
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (rows.length === 0) return;
|
|
847
|
+
try {
|
|
848
|
+
await this.ingest.appendInternal({
|
|
849
|
+
stream: this.metricsStream,
|
|
850
|
+
baseAppendMs: BigInt(nowMs),
|
|
851
|
+
rows,
|
|
852
|
+
contentType: "application/json",
|
|
853
|
+
});
|
|
854
|
+
} catch {
|
|
855
|
+
// best-effort
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|