@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/app_core.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import type { Config } from "./config";
|
|
3
|
-
import { SqliteDurableStore } from "./db/db";
|
|
4
|
-
import { IngestQueue, type ProducerInfo, type AppendRow } from "./ingest";
|
|
4
|
+
import { SqliteDurableStore, type StreamRow } from "./db/db";
|
|
5
|
+
import { IngestQueue, type ProducerInfo, type AppendRow, type AppendResult } from "./ingest";
|
|
5
6
|
import type { ObjectStore } from "./objectstore/interface";
|
|
6
|
-
import type { StreamReader, ReadBatch, ReaderError } from "./reader";
|
|
7
|
+
import type { StreamReader, ReadBatch, ReaderError, SearchResultBatch } from "./reader";
|
|
7
8
|
import { StreamNotifier } from "./notifier";
|
|
8
9
|
import { encodeOffset, parseOffsetResult, offsetToSeqOrNeg1, canonicalizeOffset, type ParsedOffset } from "./offset";
|
|
9
10
|
import { parseDurationMsResult } from "./util/duration";
|
|
@@ -11,22 +12,51 @@ import { Metrics } from "./metrics";
|
|
|
11
12
|
import { parseTimestampMsResult } from "./util/time";
|
|
12
13
|
import { cleanupTempSegments } from "./util/cleanup";
|
|
13
14
|
import { MetricsEmitter } from "./metrics_emitter";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
SchemaRegistryStore,
|
|
17
|
+
parseSchemaUpdateResult,
|
|
18
|
+
type SchemaRegistry,
|
|
19
|
+
type SearchConfig,
|
|
20
|
+
type SchemaRegistryMutationError,
|
|
21
|
+
type SchemaRegistryReadError,
|
|
22
|
+
} from "./schema/registry";
|
|
23
|
+
import { decodeJsonPayloadResult } from "./schema/read_json";
|
|
15
24
|
import { resolvePointerResult } from "./util/json_pointer";
|
|
16
|
-
import { applyLensChainResult } from "./lens/lens";
|
|
17
25
|
import { ExpirySweeper } from "./expiry_sweeper";
|
|
18
26
|
import type { StatsCollector } from "./stats";
|
|
19
27
|
import { BackpressureGate } from "./backpressure";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
28
|
+
import { MemoryPressureMonitor } from "./memory";
|
|
29
|
+
import { RuntimeMemorySampler } from "./runtime_memory_sampler";
|
|
30
|
+
import { TouchProcessorManager } from "./touch/manager";
|
|
31
|
+
import type { SegmentDiskCache } from "./segment/cache";
|
|
32
|
+
import { StreamSizeReconciler } from "./stream_size_reconciler";
|
|
33
|
+
import { ConcurrencyGate } from "./concurrency_gate";
|
|
34
|
+
import {
|
|
35
|
+
buildProcessMemoryBreakdown,
|
|
36
|
+
type RuntimeHighWaterMark,
|
|
37
|
+
type RuntimeMemoryHighWaterSnapshot,
|
|
38
|
+
type RuntimeMemorySubsystemSnapshot,
|
|
39
|
+
type RuntimeMemorySnapshot,
|
|
40
|
+
} from "./runtime_memory";
|
|
26
41
|
import type { SegmenterController } from "./segment/segmenter_workers";
|
|
27
42
|
import type { UploaderController } from "./uploader";
|
|
28
|
-
import type {
|
|
43
|
+
import type { StreamIndexLookup } from "./index/indexer";
|
|
44
|
+
import { ForegroundActivityTracker } from "./foreground_activity";
|
|
29
45
|
import { Result } from "better-result";
|
|
46
|
+
import { parseReadFilterResult } from "./read_filter";
|
|
47
|
+
import { hashSecondaryIndexField } from "./index/secondary_schema";
|
|
48
|
+
import { buildDesiredSearchCompanionPlan, hashSearchCompanionPlan } from "./search/companion_plan";
|
|
49
|
+
import { parseSearchRequestBodyResult, parseSearchRequestQueryResult } from "./search/query";
|
|
50
|
+
import { parseAggregateRequestBodyResult } from "./search/aggregate";
|
|
51
|
+
import {
|
|
52
|
+
StreamProfileStore,
|
|
53
|
+
parseProfileUpdateResult,
|
|
54
|
+
resolveJsonIngestCapability,
|
|
55
|
+
resolveTouchCapability,
|
|
56
|
+
type StreamTouchRoute,
|
|
57
|
+
} from "./profiles";
|
|
58
|
+
import { dsError } from "./util/ds_error.ts";
|
|
59
|
+
import { streamHash16Hex } from "./util/stream_paths";
|
|
30
60
|
|
|
31
61
|
function withNosniff(headers: HeadersInit = {}): HeadersInit {
|
|
32
62
|
return {
|
|
@@ -46,6 +76,66 @@ function json(status: number, body: any, headers: HeadersInit = {}): Response {
|
|
|
46
76
|
});
|
|
47
77
|
}
|
|
48
78
|
|
|
79
|
+
const OVERLOAD_RETRY_AFTER_SECONDS = "1";
|
|
80
|
+
const UNAVAILABLE_RETRY_AFTER_SECONDS = "5";
|
|
81
|
+
const APPEND_REQUEST_TIMEOUT_MS = 3_000;
|
|
82
|
+
const HTTP_RESOLVER_TIMEOUT_MS = 5_000;
|
|
83
|
+
const SEARCH_REQUEST_TIMEOUT_MS = 3_000;
|
|
84
|
+
const TIMEOUT_SENTINEL = Symbol("request-timeout");
|
|
85
|
+
const DEFAULT_TOUCH_JOURNAL_FILTER_BYTES = 4 * (1 << 22);
|
|
86
|
+
|
|
87
|
+
type TimeoutSentinel = typeof TIMEOUT_SENTINEL;
|
|
88
|
+
|
|
89
|
+
function retryAfterHeaders(seconds: string, headers: HeadersInit = {}): HeadersInit {
|
|
90
|
+
return {
|
|
91
|
+
"retry-after": seconds,
|
|
92
|
+
...headers,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function clampSearchRequestTimeoutMs(timeoutMs: number | null): number {
|
|
97
|
+
return timeoutMs == null ? SEARCH_REQUEST_TIMEOUT_MS : Math.min(timeoutMs, SEARCH_REQUEST_TIMEOUT_MS);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function awaitWithCooperativeTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | TimeoutSentinel> {
|
|
101
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
102
|
+
try {
|
|
103
|
+
return await Promise.race([
|
|
104
|
+
promise,
|
|
105
|
+
new Promise<TimeoutSentinel>((resolve) => {
|
|
106
|
+
timer = setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs);
|
|
107
|
+
}),
|
|
108
|
+
]);
|
|
109
|
+
} finally {
|
|
110
|
+
if (timer != null) clearTimeout(timer);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isAbortLikeError(error: unknown): boolean {
|
|
115
|
+
return typeof error === "object" && error != null && "name" in error && (error as { name?: unknown }).name === "AbortError";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function searchResponseHeaders(search: SearchResultBatch): HeadersInit {
|
|
119
|
+
return {
|
|
120
|
+
"search-timed-out": search.timedOut ? "true" : "false",
|
|
121
|
+
"search-timeout-ms": String(search.timeoutMs ?? SEARCH_REQUEST_TIMEOUT_MS),
|
|
122
|
+
"search-took-ms": String(search.tookMs),
|
|
123
|
+
"search-total-relation": search.total.relation,
|
|
124
|
+
"search-coverage-complete": search.coverage.complete ? "true" : "false",
|
|
125
|
+
"search-indexed-segments": String(search.coverage.indexedSegments),
|
|
126
|
+
"search-indexed-segment-time-ms": String(search.coverage.indexedSegmentTimeMs),
|
|
127
|
+
"search-fts-section-get-ms": String(search.coverage.ftsSectionGetMs),
|
|
128
|
+
"search-fts-decode-ms": String(search.coverage.ftsDecodeMs),
|
|
129
|
+
"search-fts-clause-estimate-ms": String(search.coverage.ftsClauseEstimateMs),
|
|
130
|
+
"search-scanned-segments": String(search.coverage.scannedSegments),
|
|
131
|
+
"search-scanned-segment-time-ms": String(search.coverage.scannedSegmentTimeMs),
|
|
132
|
+
"search-scanned-tail-docs": String(search.coverage.scannedTailDocs),
|
|
133
|
+
"search-scanned-tail-time-ms": String(search.coverage.scannedTailTimeMs),
|
|
134
|
+
"search-exact-candidate-time-ms": String(search.coverage.exactCandidateTimeMs),
|
|
135
|
+
"search-index-families-used": search.coverage.indexFamiliesUsed.join(","),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
49
139
|
function internalError(message = "internal server error"): Response {
|
|
50
140
|
return json(500, { error: { code: "internal", message } });
|
|
51
141
|
}
|
|
@@ -82,6 +172,44 @@ function tooLarge(msg: string): Response {
|
|
|
82
172
|
return json(413, { error: { code: "payload_too_large", message: msg } });
|
|
83
173
|
}
|
|
84
174
|
|
|
175
|
+
function unavailable(msg = "server shutting down"): Response {
|
|
176
|
+
return json(503, { error: { code: "unavailable", message: msg } }, retryAfterHeaders(UNAVAILABLE_RETRY_AFTER_SECONDS));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function overloaded(msg = "ingest queue full", code = "overloaded"): Response {
|
|
180
|
+
return json(429, { error: { code, message: msg } }, retryAfterHeaders(OVERLOAD_RETRY_AFTER_SECONDS));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function requestTimeout(msg = "request timed out"): Response {
|
|
184
|
+
return json(408, { error: { code: "request_timeout", message: msg } });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function appendTimeout(): Response {
|
|
188
|
+
return json(408, {
|
|
189
|
+
error: {
|
|
190
|
+
code: "append_timeout",
|
|
191
|
+
message: "append timed out; append outcome is unknown, check Stream-Next-Offset before retrying",
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function cancelRequestBody(req: Request): Promise<void> {
|
|
197
|
+
const body = req.body;
|
|
198
|
+
if (!body) return;
|
|
199
|
+
try {
|
|
200
|
+
await body.cancel("request rejected");
|
|
201
|
+
return;
|
|
202
|
+
} catch {
|
|
203
|
+
// ignore and try a reader-based cancel below
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const reader = body.getReader();
|
|
207
|
+
await reader.cancel("request rejected");
|
|
208
|
+
} catch {
|
|
209
|
+
// ignore
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
85
213
|
function normalizeContentType(value: string | null): string | null {
|
|
86
214
|
if (!value) return null;
|
|
87
215
|
const base = value.split(";")[0]?.trim().toLowerCase();
|
|
@@ -143,6 +271,25 @@ function encodeSseEvent(eventType: string, data: string): string {
|
|
|
143
271
|
return out;
|
|
144
272
|
}
|
|
145
273
|
|
|
274
|
+
const INTERNAL_METRICS_STREAM = "__stream_metrics__";
|
|
275
|
+
|
|
276
|
+
function clearInternalMetricsAccelerationState(db: SqliteDurableStore): void {
|
|
277
|
+
db.deleteAccelerationState(INTERNAL_METRICS_STREAM);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function reconcileDeletedStreamAccelerationState(db: SqliteDurableStore): void {
|
|
281
|
+
let offset = 0;
|
|
282
|
+
const pageSize = 1000;
|
|
283
|
+
for (;;) {
|
|
284
|
+
const streams = db.listDeletedStreams(pageSize, offset);
|
|
285
|
+
for (const stream of streams) {
|
|
286
|
+
db.deleteAccelerationState(stream);
|
|
287
|
+
}
|
|
288
|
+
if (streams.length < pageSize) break;
|
|
289
|
+
offset += streams.length;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
146
293
|
function computeCursor(nowMs: number, provided: string | null): string {
|
|
147
294
|
let cursor = Math.floor(nowMs / 1000);
|
|
148
295
|
if (provided && /^[0-9]+$/.test(provided)) {
|
|
@@ -152,16 +299,16 @@ function computeCursor(nowMs: number, provided: string | null): string {
|
|
|
152
299
|
return String(cursor);
|
|
153
300
|
}
|
|
154
301
|
|
|
155
|
-
function concatPayloads(parts: Uint8Array[]):
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
302
|
+
function concatPayloads(parts: Uint8Array[]): Buffer {
|
|
303
|
+
return Buffer.concat(parts.map((part) => Buffer.from(part.buffer, part.byteOffset, part.byteLength)));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function bodyBufferFromBytes(bytes: Uint8Array): ArrayBuffer {
|
|
307
|
+
const buffer = bytes.buffer;
|
|
308
|
+
if (bytes.byteOffset === 0 && bytes.byteLength === buffer.byteLength) {
|
|
309
|
+
return buffer as ArrayBuffer;
|
|
163
310
|
}
|
|
164
|
-
return
|
|
311
|
+
return buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
|
|
165
312
|
}
|
|
166
313
|
|
|
167
314
|
function keyBytesFromString(s: string | null): Uint8Array | null {
|
|
@@ -183,15 +330,78 @@ function extractRoutingKey(reg: SchemaRegistry, value: any): Result<Uint8Array |
|
|
|
183
330
|
return Result.ok(keyBytesFromString(resolved.value));
|
|
184
331
|
}
|
|
185
332
|
|
|
186
|
-
function
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
333
|
+
function timestampToIsoString(value: bigint | null): string | null {
|
|
334
|
+
return value == null ? null : new Date(Number(value)).toISOString();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function weakEtag(namespace: string, body: string): string {
|
|
338
|
+
const hash = createHash("sha1").update(body).digest("hex");
|
|
339
|
+
return `W/"${namespace}:${hash}"`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function configuredExactIndexes(search: SearchConfig | undefined): Array<{ name: string; kind: string; configHash: string }> {
|
|
343
|
+
if (!search) return [];
|
|
344
|
+
return Object.entries(search.fields)
|
|
345
|
+
.filter(([, field]) => field.exact === true && field.kind !== "text")
|
|
346
|
+
.map(([name, field]) => ({
|
|
347
|
+
name,
|
|
348
|
+
kind: field.kind,
|
|
349
|
+
configHash: hashSecondaryIndexField({ name, config: field }),
|
|
350
|
+
}))
|
|
351
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function configuredSearchFamilies(search: SearchConfig | undefined): Array<{ family: "col" | "fts" | "agg" | "mblk"; fields: string[] }> {
|
|
355
|
+
if (!search) return [];
|
|
356
|
+
const out: Array<{ family: "col" | "fts" | "agg" | "mblk"; fields: string[] }> = [];
|
|
357
|
+
const colFields = Object.entries(search.fields)
|
|
358
|
+
.filter(([, field]) => field.column === true)
|
|
359
|
+
.map(([name]) => name)
|
|
360
|
+
.sort((a, b) => a.localeCompare(b));
|
|
361
|
+
if (colFields.length > 0) out.push({ family: "col", fields: colFields });
|
|
362
|
+
const ftsFields = Object.entries(search.fields)
|
|
363
|
+
.filter(([, field]) => field.kind === "text" || (field.kind === "keyword" && field.prefix === true))
|
|
364
|
+
.map(([name]) => name)
|
|
365
|
+
.sort((a, b) => a.localeCompare(b));
|
|
366
|
+
if (ftsFields.length > 0) out.push({ family: "fts", fields: ftsFields });
|
|
367
|
+
const aggRollups = Object.keys(search.rollups ?? {}).sort((a, b) => a.localeCompare(b));
|
|
368
|
+
if (aggRollups.length > 0) out.push({ family: "agg", fields: aggRollups });
|
|
369
|
+
if (search.profile === "metrics") out.push({ family: "mblk", fields: ["metrics"] });
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function parseCompanionSections(value: string): Set<string> {
|
|
374
|
+
try {
|
|
375
|
+
const parsed = JSON.parse(value);
|
|
376
|
+
return new Set(Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : []);
|
|
377
|
+
} catch {
|
|
378
|
+
return new Set();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function parseCompanionSectionSizes(value: string): Record<string, number> {
|
|
383
|
+
try {
|
|
384
|
+
const parsed = JSON.parse(value);
|
|
385
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
386
|
+
const out: Record<string, number> = {};
|
|
387
|
+
for (const [key, raw] of Object.entries(parsed)) {
|
|
388
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) out[key] = raw;
|
|
389
|
+
}
|
|
390
|
+
return out;
|
|
391
|
+
} catch {
|
|
392
|
+
return {};
|
|
193
393
|
}
|
|
194
|
-
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function contiguousCoveredSegmentCount(rows: Array<{ segment_index: number; sections_json: string }>, family: string): number {
|
|
397
|
+
let expected = 0;
|
|
398
|
+
for (const row of rows) {
|
|
399
|
+
if (row.segment_index < expected) continue;
|
|
400
|
+
if (row.segment_index > expected) break;
|
|
401
|
+
if (!parseCompanionSections(row.sections_json).has(family)) break;
|
|
402
|
+
expected += 1;
|
|
403
|
+
}
|
|
404
|
+
return expected;
|
|
195
405
|
}
|
|
196
406
|
|
|
197
407
|
export type App = {
|
|
@@ -206,13 +416,21 @@ export type App = {
|
|
|
206
416
|
reader: StreamReader;
|
|
207
417
|
segmenter: SegmenterController;
|
|
208
418
|
uploader: UploaderController;
|
|
209
|
-
indexer?:
|
|
419
|
+
indexer?: StreamIndexLookup;
|
|
210
420
|
metrics: Metrics;
|
|
211
421
|
registry: SchemaRegistryStore;
|
|
212
|
-
|
|
422
|
+
profiles: StreamProfileStore;
|
|
423
|
+
touch: TouchProcessorManager;
|
|
213
424
|
stats?: StatsCollector;
|
|
214
425
|
backpressure?: BackpressureGate;
|
|
215
|
-
memory?:
|
|
426
|
+
memory?: MemoryPressureMonitor;
|
|
427
|
+
concurrency?: {
|
|
428
|
+
ingest: ConcurrencyGate;
|
|
429
|
+
read: ConcurrencyGate;
|
|
430
|
+
search: ConcurrencyGate;
|
|
431
|
+
asyncIndex: ConcurrencyGate;
|
|
432
|
+
};
|
|
433
|
+
memorySampler?: RuntimeMemorySampler;
|
|
216
434
|
};
|
|
217
435
|
};
|
|
218
436
|
|
|
@@ -222,11 +440,15 @@ export type CreateAppRuntimeArgs = {
|
|
|
222
440
|
ingest: IngestQueue;
|
|
223
441
|
notifier: StreamNotifier;
|
|
224
442
|
registry: SchemaRegistryStore;
|
|
225
|
-
|
|
443
|
+
profiles: StreamProfileStore;
|
|
444
|
+
touch: TouchProcessorManager;
|
|
226
445
|
stats?: StatsCollector;
|
|
227
446
|
backpressure?: BackpressureGate;
|
|
228
|
-
memory:
|
|
447
|
+
memory: MemoryPressureMonitor;
|
|
448
|
+
asyncIndexGate: ConcurrencyGate;
|
|
449
|
+
foregroundActivity: ForegroundActivityTracker;
|
|
229
450
|
metrics: Metrics;
|
|
451
|
+
memorySampler?: RuntimeMemorySampler;
|
|
230
452
|
};
|
|
231
453
|
|
|
232
454
|
type AppRuntimeDeps = {
|
|
@@ -234,8 +456,17 @@ type AppRuntimeDeps = {
|
|
|
234
456
|
reader: StreamReader;
|
|
235
457
|
segmenter: SegmenterController;
|
|
236
458
|
uploader: UploaderController;
|
|
237
|
-
indexer?:
|
|
459
|
+
indexer?: StreamIndexLookup;
|
|
460
|
+
segmentDiskCache?: SegmentDiskCache;
|
|
238
461
|
uploadSchemaRegistry: (stream: string, registry: SchemaRegistry) => Promise<void>;
|
|
462
|
+
getRuntimeMemorySnapshot?: () => RuntimeMemorySubsystemSnapshot;
|
|
463
|
+
getLocalStorageUsage?: (stream: string) => {
|
|
464
|
+
segment_cache_bytes: number;
|
|
465
|
+
routing_index_cache_bytes: number;
|
|
466
|
+
exact_index_cache_bytes: number;
|
|
467
|
+
lexicon_index_cache_bytes: number;
|
|
468
|
+
companion_cache_bytes: number;
|
|
469
|
+
};
|
|
239
470
|
start(): void;
|
|
240
471
|
};
|
|
241
472
|
|
|
@@ -244,51 +475,582 @@ export type CreateAppCoreOptions = {
|
|
|
244
475
|
createRuntime(args: CreateAppRuntimeArgs): AppRuntimeDeps;
|
|
245
476
|
};
|
|
246
477
|
|
|
478
|
+
function reduceConcurrencyLimit(limit: number): number {
|
|
479
|
+
return Math.max(1, Math.ceil(Math.max(1, limit) / 2));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function gateSnapshot(configuredLimit: number, gate: ConcurrencyGate) {
|
|
483
|
+
return {
|
|
484
|
+
configured_limit: configuredLimit,
|
|
485
|
+
current_limit: gate.getLimit(),
|
|
486
|
+
active: gate.getActive(),
|
|
487
|
+
queued: gate.getQueued(),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
247
491
|
export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
248
492
|
mkdirSync(cfg.rootDir, { recursive: true });
|
|
249
493
|
cleanupTempSegments(cfg.rootDir);
|
|
250
494
|
|
|
251
495
|
const db = new SqliteDurableStore(cfg.dbPath, { cacheBytes: cfg.sqliteCacheBytes });
|
|
252
496
|
db.resetSegmentInProgress();
|
|
497
|
+
reconcileDeletedStreamAccelerationState(db);
|
|
253
498
|
const stats = opts.stats;
|
|
499
|
+
const metrics = new Metrics();
|
|
254
500
|
const backpressure =
|
|
255
501
|
cfg.localBacklogMaxBytes > 0
|
|
256
502
|
? new BackpressureGate(cfg.localBacklogMaxBytes, db.sumPendingBytes() + db.sumPendingSegmentBytes())
|
|
257
503
|
: undefined;
|
|
258
|
-
const
|
|
504
|
+
const memorySampler =
|
|
505
|
+
cfg.memorySamplerPath != null
|
|
506
|
+
? new RuntimeMemorySampler(cfg.memorySamplerPath, {
|
|
507
|
+
intervalMs: cfg.memorySamplerIntervalMs,
|
|
508
|
+
scope: "main",
|
|
509
|
+
})
|
|
510
|
+
: undefined;
|
|
511
|
+
memorySampler?.start();
|
|
512
|
+
const ingestGate = new ConcurrencyGate(cfg.ingestConcurrency);
|
|
513
|
+
const readGate = new ConcurrencyGate(cfg.readConcurrency);
|
|
514
|
+
const searchGate = new ConcurrencyGate(cfg.searchConcurrency);
|
|
515
|
+
const asyncIndexGate = new ConcurrencyGate(cfg.asyncIndexConcurrency);
|
|
516
|
+
const foregroundActivity = new ForegroundActivityTracker();
|
|
517
|
+
const memory = new MemoryPressureMonitor(cfg.memoryLimitBytes, {
|
|
259
518
|
onSample: (rss, overLimit) => {
|
|
260
519
|
metrics.record("process.rss.bytes", rss, "bytes");
|
|
261
520
|
if (overLimit) metrics.record("process.rss.over_limit", 1, "count");
|
|
521
|
+
searchGate.setLimit(overLimit ? reduceConcurrencyLimit(cfg.searchConcurrency) : cfg.searchConcurrency);
|
|
522
|
+
asyncIndexGate.setLimit(overLimit ? reduceConcurrencyLimit(cfg.asyncIndexConcurrency) : cfg.asyncIndexConcurrency);
|
|
262
523
|
},
|
|
263
|
-
heapSnapshotPath:
|
|
524
|
+
heapSnapshotPath: cfg.heapSnapshotPath ?? undefined,
|
|
264
525
|
});
|
|
265
526
|
memory.start();
|
|
266
|
-
const
|
|
267
|
-
const ingest = new IngestQueue(cfg, db, stats, backpressure, memory, metrics);
|
|
527
|
+
const ingest = new IngestQueue(cfg, db, stats, backpressure, metrics);
|
|
268
528
|
const notifier = new StreamNotifier();
|
|
269
529
|
const registry = new SchemaRegistryStore(db);
|
|
270
|
-
const
|
|
530
|
+
const profiles = new StreamProfileStore(db, registry);
|
|
531
|
+
const touch = new TouchProcessorManager(cfg, db, ingest, notifier, profiles, backpressure);
|
|
271
532
|
const runtime = opts.createRuntime({
|
|
272
533
|
config: cfg,
|
|
273
534
|
db,
|
|
274
535
|
ingest,
|
|
275
536
|
notifier,
|
|
276
537
|
registry,
|
|
538
|
+
profiles,
|
|
277
539
|
touch,
|
|
278
540
|
stats,
|
|
279
541
|
backpressure,
|
|
280
542
|
memory,
|
|
543
|
+
asyncIndexGate,
|
|
544
|
+
foregroundActivity,
|
|
281
545
|
metrics,
|
|
546
|
+
memorySampler,
|
|
282
547
|
});
|
|
283
|
-
const { store, reader, segmenter, uploader, indexer, uploadSchemaRegistry } = runtime;
|
|
284
|
-
const
|
|
285
|
-
|
|
548
|
+
const { store, reader, segmenter, uploader, indexer, uploadSchemaRegistry, getRuntimeMemorySnapshot, getLocalStorageUsage } = runtime;
|
|
549
|
+
const runtimeHighWater: RuntimeMemoryHighWaterSnapshot = {
|
|
550
|
+
process: {},
|
|
551
|
+
process_breakdown: {},
|
|
552
|
+
sqlite: {},
|
|
553
|
+
runtime_bytes: {},
|
|
554
|
+
runtime_totals: {},
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const observeHighWaterValue = (target: Record<string, RuntimeHighWaterMark>, key: string, value: number, at: string): void => {
|
|
558
|
+
const next = Math.max(0, Math.floor(value));
|
|
559
|
+
const existing = target[key];
|
|
560
|
+
if (!existing || next > existing.value) {
|
|
561
|
+
target[key] = {
|
|
562
|
+
value: next,
|
|
563
|
+
at,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const buildRuntimeBytes = (runtimeMemory: RuntimeMemorySnapshot): Record<string, Record<string, number>> => {
|
|
569
|
+
const groups: Record<string, Record<string, number>> = {};
|
|
570
|
+
for (const [kind, values] of Object.entries(runtimeMemory.subsystems)) {
|
|
571
|
+
if (kind === "counts") continue;
|
|
572
|
+
groups[kind] = values;
|
|
573
|
+
}
|
|
574
|
+
return groups;
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const buildTopStreamContributors = (limit = 5) => {
|
|
578
|
+
const safeLimit = Math.max(1, Math.min(limit, 20));
|
|
579
|
+
const localStorageRows: Array<{
|
|
580
|
+
stream: string;
|
|
581
|
+
bytes: number;
|
|
582
|
+
wal_retained_bytes: number;
|
|
583
|
+
segment_cache_bytes: number;
|
|
584
|
+
index_cache_bytes: number;
|
|
585
|
+
}> = [];
|
|
586
|
+
const pendingWalRows: Array<{ stream: string; pending_wal_bytes: number; pending_rows: number }> = [];
|
|
587
|
+
let offset = 0;
|
|
588
|
+
const pageSize = 1000;
|
|
589
|
+
for (;;) {
|
|
590
|
+
const rows = db.listStreams(pageSize, offset);
|
|
591
|
+
if (rows.length === 0) break;
|
|
592
|
+
for (const row of rows) {
|
|
593
|
+
if (db.isDeleted(row)) continue;
|
|
594
|
+
const usage = getLocalStorageUsage?.(row.stream) ?? { segment_cache_bytes: 0 };
|
|
595
|
+
const walRetainedBytes = Number(row.pending_bytes);
|
|
596
|
+
const segmentCacheBytes = Math.max(0, Math.floor(Number((usage as Record<string, number>).segment_cache_bytes ?? 0)));
|
|
597
|
+
const indexCacheBytes = Math.max(
|
|
598
|
+
0,
|
|
599
|
+
Math.floor(
|
|
600
|
+
Number((usage as Record<string, number>).routing_index_cache_bytes ?? 0) +
|
|
601
|
+
Number((usage as Record<string, number>).exact_index_cache_bytes ?? 0) +
|
|
602
|
+
Number((usage as Record<string, number>).lexicon_index_cache_bytes ?? 0) +
|
|
603
|
+
Number((usage as Record<string, number>).companion_cache_bytes ?? 0)
|
|
604
|
+
)
|
|
605
|
+
);
|
|
606
|
+
localStorageRows.push({
|
|
607
|
+
stream: row.stream,
|
|
608
|
+
bytes: Math.max(0, walRetainedBytes + segmentCacheBytes + indexCacheBytes),
|
|
609
|
+
wal_retained_bytes: Math.max(0, walRetainedBytes),
|
|
610
|
+
segment_cache_bytes: segmentCacheBytes,
|
|
611
|
+
index_cache_bytes: indexCacheBytes,
|
|
612
|
+
});
|
|
613
|
+
pendingWalRows.push({
|
|
614
|
+
stream: row.stream,
|
|
615
|
+
pending_wal_bytes: Math.max(0, walRetainedBytes),
|
|
616
|
+
pending_rows: Math.max(0, Number(row.pending_rows)),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
if (rows.length < pageSize) break;
|
|
620
|
+
offset += rows.length;
|
|
621
|
+
}
|
|
622
|
+
localStorageRows.sort((a, b) => b.bytes - a.bytes || a.stream.localeCompare(b.stream));
|
|
623
|
+
pendingWalRows.sort((a, b) => b.pending_wal_bytes - a.pending_wal_bytes || a.stream.localeCompare(b.stream));
|
|
624
|
+
return {
|
|
625
|
+
local_storage_bytes: localStorageRows.slice(0, safeLimit),
|
|
626
|
+
pending_wal_bytes: pendingWalRows.slice(0, safeLimit),
|
|
627
|
+
touch_journal_filter_bytes: touch.getTopStreams(safeLimit),
|
|
628
|
+
notifier_waiters: notifier.getTopStreams(safeLimit),
|
|
629
|
+
};
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const buildRuntimeMemorySnapshot = (): RuntimeMemorySnapshot => {
|
|
633
|
+
const processUsage = process.memoryUsage();
|
|
634
|
+
const subsystemSnapshot = getRuntimeMemorySnapshot?.() ?? {
|
|
635
|
+
subsystems: {
|
|
636
|
+
heap_estimates: {},
|
|
637
|
+
mapped_files: {},
|
|
638
|
+
disk_caches: {},
|
|
639
|
+
configured_budgets: {},
|
|
640
|
+
pipeline_buffers: {},
|
|
641
|
+
sqlite_runtime: {},
|
|
642
|
+
counts: {},
|
|
643
|
+
},
|
|
644
|
+
totals: {
|
|
645
|
+
heap_estimate_bytes: 0,
|
|
646
|
+
mapped_file_bytes: 0,
|
|
647
|
+
disk_cache_bytes: 0,
|
|
648
|
+
configured_budget_bytes: 0,
|
|
649
|
+
pipeline_buffer_bytes: 0,
|
|
650
|
+
sqlite_runtime_bytes: 0,
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
const sqliteRuntimeBytes = subsystemSnapshot.subsystems.sqlite_runtime ?? {};
|
|
654
|
+
const runtimeCounts = subsystemSnapshot.subsystems.counts ?? {};
|
|
655
|
+
const snapshot: RuntimeMemorySnapshot = {
|
|
656
|
+
process: {
|
|
657
|
+
rss_bytes: processUsage.rss,
|
|
658
|
+
heap_total_bytes: processUsage.heapTotal,
|
|
659
|
+
heap_used_bytes: processUsage.heapUsed,
|
|
660
|
+
external_bytes: processUsage.external,
|
|
661
|
+
array_buffers_bytes: processUsage.arrayBuffers,
|
|
662
|
+
},
|
|
663
|
+
process_breakdown: buildProcessMemoryBreakdown({
|
|
664
|
+
process: {
|
|
665
|
+
rss_bytes: processUsage.rss,
|
|
666
|
+
heap_total_bytes: processUsage.heapTotal,
|
|
667
|
+
heap_used_bytes: processUsage.heapUsed,
|
|
668
|
+
external_bytes: processUsage.external,
|
|
669
|
+
array_buffers_bytes: processUsage.arrayBuffers,
|
|
670
|
+
},
|
|
671
|
+
mappedFileBytes: subsystemSnapshot.totals.mapped_file_bytes,
|
|
672
|
+
sqliteRuntimeBytes: Number(sqliteRuntimeBytes["sqlite_memory_used_bytes"] ?? 0),
|
|
673
|
+
}),
|
|
674
|
+
sqlite: {
|
|
675
|
+
available: Number(runtimeCounts["sqlite_open_connections"] ?? 0) > 0 || Number(sqliteRuntimeBytes["sqlite_memory_used_bytes"] ?? 0) > 0,
|
|
676
|
+
source:
|
|
677
|
+
Number(runtimeCounts["sqlite_open_connections"] ?? 0) > 0 || Number(sqliteRuntimeBytes["sqlite_memory_used_bytes"] ?? 0) > 0
|
|
678
|
+
? "sqlite3_status64"
|
|
679
|
+
: "unavailable",
|
|
680
|
+
memory_used_bytes: Math.max(0, Math.floor(Number(sqliteRuntimeBytes["sqlite_memory_used_bytes"] ?? 0))),
|
|
681
|
+
memory_highwater_bytes: Math.max(
|
|
682
|
+
0,
|
|
683
|
+
Math.floor(Number(sqliteRuntimeBytes["sqlite_memory_highwater_bytes"] ?? 0))
|
|
684
|
+
),
|
|
685
|
+
pagecache_used_slots: Math.max(0, Math.floor(Number(runtimeCounts["sqlite_pagecache_used_slots"] ?? 0))),
|
|
686
|
+
pagecache_used_slots_highwater: Math.max(
|
|
687
|
+
0,
|
|
688
|
+
Math.floor(Number(runtimeCounts["sqlite_pagecache_used_slots_highwater"] ?? 0))
|
|
689
|
+
),
|
|
690
|
+
pagecache_overflow_bytes: Math.max(
|
|
691
|
+
0,
|
|
692
|
+
Math.floor(Number(sqliteRuntimeBytes["sqlite_pagecache_overflow_bytes"] ?? 0))
|
|
693
|
+
),
|
|
694
|
+
pagecache_overflow_highwater_bytes: Math.max(
|
|
695
|
+
0,
|
|
696
|
+
Math.floor(Number(sqliteRuntimeBytes["sqlite_pagecache_overflow_highwater_bytes"] ?? 0))
|
|
697
|
+
),
|
|
698
|
+
malloc_count: Math.max(0, Math.floor(Number(runtimeCounts["sqlite_malloc_count"] ?? 0))),
|
|
699
|
+
malloc_count_highwater: Math.max(
|
|
700
|
+
0,
|
|
701
|
+
Math.floor(Number(runtimeCounts["sqlite_malloc_count_highwater"] ?? 0))
|
|
702
|
+
),
|
|
703
|
+
open_connections: Math.max(0, Math.floor(Number(runtimeCounts["sqlite_open_connections"] ?? 0))),
|
|
704
|
+
prepared_statements: Math.max(
|
|
705
|
+
0,
|
|
706
|
+
Math.floor(Number(runtimeCounts["sqlite_prepared_statements"] ?? 0))
|
|
707
|
+
),
|
|
708
|
+
},
|
|
709
|
+
gc: memory.getGcStats(),
|
|
710
|
+
subsystems: subsystemSnapshot.subsystems,
|
|
711
|
+
totals: subsystemSnapshot.totals,
|
|
712
|
+
};
|
|
713
|
+
const ts = new Date().toISOString();
|
|
714
|
+
for (const [key, value] of Object.entries(snapshot.process)) observeHighWaterValue(runtimeHighWater.process, key, value, ts);
|
|
715
|
+
for (const [key, value] of Object.entries(snapshot.process_breakdown)) {
|
|
716
|
+
if (typeof value === "number") observeHighWaterValue(runtimeHighWater.process_breakdown, key, value, ts);
|
|
717
|
+
}
|
|
718
|
+
for (const [key, value] of Object.entries(snapshot.sqlite)) {
|
|
719
|
+
if (typeof value === "number") observeHighWaterValue(runtimeHighWater.sqlite, key, value, ts);
|
|
720
|
+
}
|
|
721
|
+
for (const [kind, values] of Object.entries(buildRuntimeBytes(snapshot))) {
|
|
722
|
+
const bucket = (runtimeHighWater.runtime_bytes[kind] ??= {});
|
|
723
|
+
for (const [key, value] of Object.entries(values)) observeHighWaterValue(bucket, key, value, ts);
|
|
724
|
+
}
|
|
725
|
+
for (const [key, value] of Object.entries(snapshot.totals)) observeHighWaterValue(runtimeHighWater.runtime_totals, key, value, ts);
|
|
726
|
+
if (snapshot.sqlite.memory_used_bytes > 0) observeHighWaterValue(runtimeHighWater.sqlite, "memory_used_bytes", snapshot.sqlite.memory_used_bytes, ts);
|
|
727
|
+
if (snapshot.sqlite.pagecache_overflow_bytes > 0)
|
|
728
|
+
observeHighWaterValue(runtimeHighWater.sqlite, "pagecache_overflow_bytes", snapshot.sqlite.pagecache_overflow_bytes, ts);
|
|
729
|
+
if (snapshot.sqlite.pagecache_used_slots > 0)
|
|
730
|
+
observeHighWaterValue(runtimeHighWater.sqlite, "pagecache_used_slots", snapshot.sqlite.pagecache_used_slots, ts);
|
|
731
|
+
if (snapshot.sqlite.malloc_count > 0) observeHighWaterValue(runtimeHighWater.sqlite, "malloc_count", snapshot.sqlite.malloc_count, ts);
|
|
732
|
+
return snapshot;
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
const buildLeakCandidateCounters = (): Record<string, number> => {
|
|
736
|
+
const runtimeMemory = buildRuntimeMemorySnapshot();
|
|
737
|
+
const runtimeCounts = runtimeMemory.subsystems.counts ?? {};
|
|
738
|
+
const countValue = (name: string): number => {
|
|
739
|
+
const raw = Number(runtimeCounts[name] ?? 0);
|
|
740
|
+
if (!Number.isFinite(raw)) return 0;
|
|
741
|
+
return Math.max(0, Math.floor(raw));
|
|
742
|
+
};
|
|
743
|
+
const touchMemory = touch.getMemoryStats();
|
|
744
|
+
const notifierMemory = notifier.getMemoryStats();
|
|
745
|
+
const metricsMemory = metrics.getMemoryStats();
|
|
746
|
+
return {
|
|
747
|
+
"tieredstore.mem.leak_candidate.segment_cache.pinned_entries": countValue("segment_pinned_files"),
|
|
748
|
+
"tieredstore.mem.leak_candidate.lexicon_file_cache.pinned_entries": countValue("lexicon_pinned_files"),
|
|
749
|
+
"tieredstore.mem.leak_candidate.companion_file_cache.pinned_entries": countValue("companion_pinned_files"),
|
|
750
|
+
"tieredstore.mem.leak_candidate.routing_run_disk_cache.pinned_entries": countValue("routing_run_disk_cache_pinned_entries"),
|
|
751
|
+
"tieredstore.mem.leak_candidate.exact_run_disk_cache.pinned_entries": countValue("exact_run_disk_cache_pinned_entries"),
|
|
752
|
+
"tieredstore.mem.leak_candidate.touch.journals.active_count": touchMemory.journals,
|
|
753
|
+
"tieredstore.mem.leak_candidate.touch.journals.created_total": touchMemory.journalsCreatedTotal,
|
|
754
|
+
"tieredstore.mem.leak_candidate.touch.journals.filter_bytes_total": touchMemory.journalFilterBytesTotal,
|
|
755
|
+
"tieredstore.mem.leak_candidate.touch.journal.default_filter_bytes": DEFAULT_TOUCH_JOURNAL_FILTER_BYTES,
|
|
756
|
+
"tieredstore.mem.leak_candidate.touch.maps.fine_lag_coarse_only_streams": touchMemory.fineLagCoarseOnlyStreams,
|
|
757
|
+
"tieredstore.mem.leak_candidate.touch.maps.touch_mode_streams": touchMemory.touchModeStreams,
|
|
758
|
+
"tieredstore.mem.leak_candidate.touch.maps.fine_token_bucket_streams": touchMemory.fineTokenBucketStreams,
|
|
759
|
+
"tieredstore.mem.leak_candidate.touch.maps.hot_fine_streams": touchMemory.hotFineStreams,
|
|
760
|
+
"tieredstore.mem.leak_candidate.touch.maps.lag_source_offset_streams": touchMemory.lagSourceOffsetStreams,
|
|
761
|
+
"tieredstore.mem.leak_candidate.touch.maps.restricted_template_bucket_streams": touchMemory.restrictedTemplateBucketStreams,
|
|
762
|
+
"tieredstore.mem.leak_candidate.touch.maps.runtime_totals_streams": touchMemory.runtimeTotalsStreams,
|
|
763
|
+
"tieredstore.mem.leak_candidate.touch.maps.zero_row_backlog_streams": touchMemory.zeroRowBacklogStreakStreams,
|
|
764
|
+
"tieredstore.mem.leak_candidate.live_template.last_seen_entries": touchMemory.templateLastSeenEntries,
|
|
765
|
+
"tieredstore.mem.leak_candidate.live_template.dirty_last_seen_entries": touchMemory.templateDirtyLastSeenEntries,
|
|
766
|
+
"tieredstore.mem.leak_candidate.live_template.rate_state_streams": touchMemory.templateRateStateStreams,
|
|
767
|
+
"tieredstore.mem.leak_candidate.live_metrics.counter_streams": touchMemory.liveMetricsCounterStreams,
|
|
768
|
+
"tieredstore.mem.leak_candidate.notifier.latest_seq_streams": notifierMemory.latestSeqStreams,
|
|
769
|
+
"tieredstore.mem.leak_candidate.notifier.details_version_streams": notifierMemory.detailsVersionStreams,
|
|
770
|
+
"tieredstore.mem.leak_candidate.metrics.series": metricsMemory.seriesCount,
|
|
771
|
+
"tieredstore.mem.leak_candidate.secondary_index.stream_idle_ticks_streams": countValue("secondary_index_stream_idle_ticks"),
|
|
772
|
+
"tieredstore.mem.leak_candidate.mock_r2.in_memory_bytes": countValue("mock_r2_in_memory_bytes"),
|
|
773
|
+
"tieredstore.mem.leak_candidate.mock_r2.object_count": countValue("mock_r2_object_count"),
|
|
774
|
+
};
|
|
775
|
+
};
|
|
286
776
|
|
|
287
|
-
|
|
777
|
+
const buildServerMem = () => {
|
|
778
|
+
const runtimeMemory = buildRuntimeMemorySnapshot();
|
|
779
|
+
return {
|
|
780
|
+
ts: new Date().toISOString(),
|
|
781
|
+
process: runtimeMemory.process,
|
|
782
|
+
process_breakdown: runtimeMemory.process_breakdown,
|
|
783
|
+
sqlite: runtimeMemory.sqlite,
|
|
784
|
+
gc: runtimeMemory.gc,
|
|
785
|
+
high_water: runtimeHighWater,
|
|
786
|
+
counters: buildLeakCandidateCounters(),
|
|
787
|
+
runtime_counts: runtimeMemory.subsystems.counts,
|
|
788
|
+
runtime_bytes: buildRuntimeBytes(runtimeMemory),
|
|
789
|
+
runtime_totals: runtimeMemory.totals,
|
|
790
|
+
top_streams: buildTopStreamContributors(),
|
|
791
|
+
};
|
|
792
|
+
};
|
|
793
|
+
memorySampler?.setSubsystemProvider(() => buildRuntimeMemorySnapshot().subsystems);
|
|
794
|
+
const buildServerDetails = () => {
|
|
795
|
+
const runtimeMemory = buildRuntimeMemorySnapshot();
|
|
796
|
+
return {
|
|
797
|
+
auto_tune: {
|
|
798
|
+
enabled: cfg.autoTunePresetMb != null,
|
|
799
|
+
requested_memory_mb: cfg.autoTuneRequestedMemoryMb,
|
|
800
|
+
preset_mb: cfg.autoTunePresetMb,
|
|
801
|
+
effective_memory_limit_mb: cfg.autoTuneEffectiveMemoryLimitMb,
|
|
802
|
+
},
|
|
803
|
+
configured_limits: {
|
|
804
|
+
caches: {
|
|
805
|
+
sqlite_cache_bytes: cfg.sqliteCacheBytes,
|
|
806
|
+
worker_sqlite_cache_bytes: cfg.workerSqliteCacheBytes,
|
|
807
|
+
index_run_memory_cache_bytes: cfg.indexRunMemoryCacheBytes,
|
|
808
|
+
index_run_disk_cache_bytes: cfg.indexRunCacheMaxBytes,
|
|
809
|
+
lexicon_index_cache_bytes: cfg.lexiconIndexCacheMaxBytes,
|
|
810
|
+
segment_cache_bytes: cfg.segmentCacheMaxBytes,
|
|
811
|
+
companion_toc_cache_bytes: cfg.searchCompanionTocCacheBytes,
|
|
812
|
+
companion_section_cache_bytes: cfg.searchCompanionSectionCacheBytes,
|
|
813
|
+
companion_file_cache_bytes: cfg.searchCompanionFileCacheMaxBytes,
|
|
814
|
+
},
|
|
815
|
+
concurrency: {
|
|
816
|
+
ingest: cfg.ingestConcurrency,
|
|
817
|
+
read: cfg.readConcurrency,
|
|
818
|
+
search: cfg.searchConcurrency,
|
|
819
|
+
async_index: cfg.asyncIndexConcurrency,
|
|
820
|
+
upload: cfg.uploadConcurrency,
|
|
821
|
+
index_build: cfg.indexBuildConcurrency,
|
|
822
|
+
index_compact: cfg.indexCompactionConcurrency,
|
|
823
|
+
},
|
|
824
|
+
ingest: {
|
|
825
|
+
max_batch_requests: cfg.ingestMaxBatchRequests,
|
|
826
|
+
max_batch_bytes: cfg.ingestMaxBatchBytes,
|
|
827
|
+
max_queue_requests: cfg.ingestMaxQueueRequests,
|
|
828
|
+
max_queue_bytes: cfg.ingestMaxQueueBytes,
|
|
829
|
+
busy_timeout_ms: cfg.ingestBusyTimeoutMs,
|
|
830
|
+
local_backlog_max_bytes: cfg.localBacklogMaxBytes,
|
|
831
|
+
},
|
|
832
|
+
search: {
|
|
833
|
+
companion_batch_segments: cfg.searchCompanionBuildBatchSegments,
|
|
834
|
+
companion_yield_blocks: cfg.searchCompanionYieldBlocks,
|
|
835
|
+
wal_overlay_quiet_period_ms: cfg.searchWalOverlayQuietPeriodMs,
|
|
836
|
+
wal_overlay_max_bytes: cfg.searchWalOverlayMaxBytes,
|
|
837
|
+
},
|
|
838
|
+
segmenting: {
|
|
839
|
+
segment_max_bytes: cfg.segmentMaxBytes,
|
|
840
|
+
segment_target_rows: cfg.segmentTargetRows,
|
|
841
|
+
segmenter_workers: cfg.segmenterWorkers,
|
|
842
|
+
},
|
|
843
|
+
timeouts: {
|
|
844
|
+
append_request_timeout_ms: APPEND_REQUEST_TIMEOUT_MS,
|
|
845
|
+
search_request_timeout_ms: SEARCH_REQUEST_TIMEOUT_MS,
|
|
846
|
+
resolver_timeout_ms: HTTP_RESOLVER_TIMEOUT_MS,
|
|
847
|
+
object_store_timeout_ms: cfg.objectStoreTimeoutMs,
|
|
848
|
+
},
|
|
849
|
+
memory: {
|
|
850
|
+
pressure_limit_bytes: cfg.memoryLimitBytes,
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
runtime: {
|
|
854
|
+
memory: {
|
|
855
|
+
pressure_active: memory.isOverLimit(),
|
|
856
|
+
pressure_limit_bytes: memory.getLimitBytes(),
|
|
857
|
+
last_rss_bytes: memory.getLastRssBytes(),
|
|
858
|
+
max_rss_bytes: memory.getMaxRssBytes(),
|
|
859
|
+
process: runtimeMemory.process,
|
|
860
|
+
process_breakdown: runtimeMemory.process_breakdown,
|
|
861
|
+
sqlite: runtimeMemory.sqlite,
|
|
862
|
+
gc: runtimeMemory.gc,
|
|
863
|
+
subsystems: runtimeMemory.subsystems,
|
|
864
|
+
totals: runtimeMemory.totals,
|
|
865
|
+
high_water: runtimeHighWater,
|
|
866
|
+
},
|
|
867
|
+
ingest_queue: {
|
|
868
|
+
requests: ingest.getQueueStats().requests,
|
|
869
|
+
bytes: ingest.getQueueStats().bytes,
|
|
870
|
+
full: ingest.isQueueFull(),
|
|
871
|
+
},
|
|
872
|
+
local_backpressure: {
|
|
873
|
+
enabled: backpressure?.enabled() ?? false,
|
|
874
|
+
current_bytes: backpressure?.getCurrentBytes() ?? 0,
|
|
875
|
+
max_bytes: backpressure?.getMaxBytes() ?? 0,
|
|
876
|
+
over_limit: backpressure?.isOverLimit() ?? false,
|
|
877
|
+
},
|
|
878
|
+
uploads: {
|
|
879
|
+
pending_segments: uploader.countSegmentsWaiting(),
|
|
880
|
+
},
|
|
881
|
+
concurrency: {
|
|
882
|
+
ingest: gateSnapshot(cfg.ingestConcurrency, ingestGate),
|
|
883
|
+
read: gateSnapshot(cfg.readConcurrency, readGate),
|
|
884
|
+
search: gateSnapshot(cfg.searchConcurrency, searchGate),
|
|
885
|
+
async_index: gateSnapshot(cfg.asyncIndexConcurrency, asyncIndexGate),
|
|
886
|
+
},
|
|
887
|
+
top_streams: buildTopStreamContributors(),
|
|
888
|
+
},
|
|
889
|
+
};
|
|
890
|
+
};
|
|
891
|
+
const collectRuntimeMetrics = () => {
|
|
892
|
+
const queue = ingest.getQueueStats();
|
|
893
|
+
const emitGate = (name: string, configuredLimit: number, gate: ConcurrencyGate) => {
|
|
894
|
+
metrics.record("tieredstore.concurrency.limit", configuredLimit, "count", { gate: name, kind: "configured" });
|
|
895
|
+
metrics.record("tieredstore.concurrency.limit", gate.getLimit(), "count", { gate: name, kind: "effective" });
|
|
896
|
+
metrics.record("tieredstore.concurrency.active", gate.getActive(), "count", { gate: name });
|
|
897
|
+
metrics.record("tieredstore.concurrency.queued", gate.getQueued(), "count", { gate: name });
|
|
898
|
+
};
|
|
899
|
+
emitGate("ingest", cfg.ingestConcurrency, ingestGate);
|
|
900
|
+
emitGate("read", cfg.readConcurrency, readGate);
|
|
901
|
+
emitGate("search", cfg.searchConcurrency, searchGate);
|
|
902
|
+
emitGate("async_index", cfg.asyncIndexConcurrency, asyncIndexGate);
|
|
903
|
+
metrics.record("tieredstore.ingest.queue.capacity.requests", cfg.ingestMaxQueueRequests, "count");
|
|
904
|
+
metrics.record("tieredstore.ingest.queue.capacity.bytes", cfg.ingestMaxQueueBytes, "bytes");
|
|
905
|
+
metrics.record("tieredstore.upload.pending_segments", uploader.countSegmentsWaiting(), "count");
|
|
906
|
+
metrics.record("tieredstore.upload.concurrency.limit", cfg.uploadConcurrency, "count");
|
|
907
|
+
if (cfg.memoryLimitBytes > 0) metrics.record("process.memory.limit.bytes", cfg.memoryLimitBytes, "bytes");
|
|
908
|
+
const lastRss = memory.getLastRssBytes();
|
|
909
|
+
if (lastRss > 0) metrics.record("process.rss.current.bytes", lastRss, "bytes");
|
|
910
|
+
const maxRss = memory.snapshotMaxRssBytes();
|
|
911
|
+
if (maxRss > 0) metrics.record("process.rss.max_interval.bytes", maxRss, "bytes");
|
|
912
|
+
const runtimeMemory = buildRuntimeMemorySnapshot();
|
|
913
|
+
metrics.record("process.heap.total.bytes", runtimeMemory.process.heap_total_bytes, "bytes");
|
|
914
|
+
metrics.record("process.heap.used.bytes", runtimeMemory.process.heap_used_bytes, "bytes");
|
|
915
|
+
metrics.record("process.external.bytes", runtimeMemory.process.external_bytes, "bytes");
|
|
916
|
+
metrics.record("process.array_buffers.bytes", runtimeMemory.process.array_buffers_bytes, "bytes");
|
|
917
|
+
if (runtimeMemory.process_breakdown.rss_anon_bytes != null) {
|
|
918
|
+
metrics.record("process.memory.rss.anon.bytes", runtimeMemory.process_breakdown.rss_anon_bytes, "bytes");
|
|
919
|
+
}
|
|
920
|
+
if (runtimeMemory.process_breakdown.rss_file_bytes != null) {
|
|
921
|
+
metrics.record("process.memory.rss.file.bytes", runtimeMemory.process_breakdown.rss_file_bytes, "bytes");
|
|
922
|
+
}
|
|
923
|
+
if (runtimeMemory.process_breakdown.rss_shmem_bytes != null) {
|
|
924
|
+
metrics.record("process.memory.rss.shmem.bytes", runtimeMemory.process_breakdown.rss_shmem_bytes, "bytes");
|
|
925
|
+
}
|
|
926
|
+
if (runtimeMemory.process_breakdown.unattributed_anon_bytes != null) {
|
|
927
|
+
metrics.record("process.memory.unattributed_anon.bytes", runtimeMemory.process_breakdown.unattributed_anon_bytes, "bytes");
|
|
928
|
+
}
|
|
929
|
+
metrics.record("process.memory.js_managed.bytes", runtimeMemory.process_breakdown.js_managed_bytes, "bytes");
|
|
930
|
+
metrics.record(
|
|
931
|
+
"process.memory.js_external_non_array_buffers.bytes",
|
|
932
|
+
runtimeMemory.process_breakdown.js_external_non_array_buffers_bytes,
|
|
933
|
+
"bytes"
|
|
934
|
+
);
|
|
935
|
+
metrics.record("process.memory.unattributed.bytes", runtimeMemory.process_breakdown.unattributed_rss_bytes, "bytes");
|
|
936
|
+
metrics.record("tieredstore.sqlite.memory.used.bytes", runtimeMemory.sqlite.memory_used_bytes, "bytes");
|
|
937
|
+
metrics.record("tieredstore.sqlite.memory.high_water.bytes", runtimeMemory.sqlite.memory_highwater_bytes, "bytes");
|
|
938
|
+
metrics.record("tieredstore.sqlite.pagecache.used", runtimeMemory.sqlite.pagecache_used_slots, "count");
|
|
939
|
+
metrics.record("tieredstore.sqlite.pagecache.high_water", runtimeMemory.sqlite.pagecache_used_slots_highwater, "count");
|
|
940
|
+
metrics.record("tieredstore.sqlite.pagecache.overflow.bytes", runtimeMemory.sqlite.pagecache_overflow_bytes, "bytes");
|
|
941
|
+
metrics.record(
|
|
942
|
+
"tieredstore.sqlite.pagecache.overflow.high_water.bytes",
|
|
943
|
+
runtimeMemory.sqlite.pagecache_overflow_highwater_bytes,
|
|
944
|
+
"bytes"
|
|
945
|
+
);
|
|
946
|
+
metrics.record("tieredstore.sqlite.malloc.count", runtimeMemory.sqlite.malloc_count, "count");
|
|
947
|
+
metrics.record("tieredstore.sqlite.malloc.high_water.count", runtimeMemory.sqlite.malloc_count_highwater, "count");
|
|
948
|
+
metrics.record("tieredstore.sqlite.open_connections", runtimeMemory.sqlite.open_connections, "count");
|
|
949
|
+
metrics.record("tieredstore.sqlite.prepared_statements", runtimeMemory.sqlite.prepared_statements, "count");
|
|
950
|
+
for (const [kind, values] of Object.entries(runtimeMemory.subsystems)) {
|
|
951
|
+
if (kind === "counts") continue;
|
|
952
|
+
for (const [subsystem, value] of Object.entries(values)) {
|
|
953
|
+
metrics.record("tieredstore.memory.subsystem.bytes", value, "bytes", { kind, subsystem });
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
for (const [subsystem, value] of Object.entries(runtimeMemory.subsystems.counts)) {
|
|
957
|
+
metrics.record("tieredstore.memory.subsystem.count", value, "count", { subsystem });
|
|
958
|
+
}
|
|
959
|
+
metrics.record("tieredstore.memory.tracked.bytes", runtimeMemory.totals.heap_estimate_bytes, "bytes", { kind: "heap_estimate" });
|
|
960
|
+
metrics.record("tieredstore.memory.tracked.bytes", runtimeMemory.totals.mapped_file_bytes, "bytes", { kind: "mapped_file" });
|
|
961
|
+
metrics.record("tieredstore.memory.tracked.bytes", runtimeMemory.totals.disk_cache_bytes, "bytes", { kind: "disk_cache" });
|
|
962
|
+
metrics.record("tieredstore.memory.tracked.bytes", runtimeMemory.totals.configured_budget_bytes, "bytes", { kind: "configured_budget" });
|
|
963
|
+
metrics.record("tieredstore.memory.tracked.bytes", runtimeMemory.totals.pipeline_buffer_bytes, "bytes", { kind: "pipeline_buffer" });
|
|
964
|
+
metrics.record("tieredstore.memory.tracked.bytes", runtimeMemory.totals.sqlite_runtime_bytes, "bytes", { kind: "sqlite_runtime" });
|
|
965
|
+
const memLeakCounters = buildLeakCandidateCounters();
|
|
966
|
+
for (const [metricName, value] of Object.entries(memLeakCounters)) {
|
|
967
|
+
const unit = metricName.endsWith("_bytes") ? "bytes" : "count";
|
|
968
|
+
metrics.record(metricName, value, unit);
|
|
969
|
+
}
|
|
970
|
+
metrics.record("process.gc.forced.count", runtimeMemory.gc.forced_gc_count, "count");
|
|
971
|
+
metrics.record("process.gc.reclaimed.bytes", runtimeMemory.gc.forced_gc_reclaimed_bytes_total, "bytes", { kind: "total" });
|
|
972
|
+
if (runtimeMemory.gc.last_forced_gc_reclaimed_bytes != null) {
|
|
973
|
+
metrics.record("process.gc.reclaimed.bytes", runtimeMemory.gc.last_forced_gc_reclaimed_bytes, "bytes", { kind: "last" });
|
|
974
|
+
}
|
|
975
|
+
if (runtimeMemory.gc.last_forced_gc_at_ms != null) {
|
|
976
|
+
metrics.record("process.gc.last_forced_at_ms", runtimeMemory.gc.last_forced_gc_at_ms, "count");
|
|
977
|
+
}
|
|
978
|
+
metrics.record("process.heap.snapshot.count", runtimeMemory.gc.heap_snapshots_written, "count");
|
|
979
|
+
if (runtimeMemory.gc.last_heap_snapshot_at_ms != null) {
|
|
980
|
+
metrics.record("process.heap.snapshot.last_at_ms", runtimeMemory.gc.last_heap_snapshot_at_ms, "count");
|
|
981
|
+
}
|
|
982
|
+
for (const [metricName, entry] of Object.entries(runtimeHighWater.process)) {
|
|
983
|
+
metrics.record("process.memory.high_water.bytes", entry.value, "bytes", { metric: metricName });
|
|
984
|
+
}
|
|
985
|
+
for (const [metricName, entry] of Object.entries(runtimeHighWater.process_breakdown)) {
|
|
986
|
+
metrics.record("process.memory.high_water.bytes", entry.value, "bytes", { metric: metricName });
|
|
987
|
+
}
|
|
988
|
+
for (const [metricName, entry] of Object.entries(runtimeHighWater.runtime_totals)) {
|
|
989
|
+
metrics.record("tieredstore.memory.high_water.bytes", entry.value, "bytes", { kind: "runtime_total", metric: metricName });
|
|
990
|
+
}
|
|
991
|
+
for (const [kind, entries] of Object.entries(runtimeHighWater.runtime_bytes)) {
|
|
992
|
+
for (const [metricName, entry] of Object.entries(entries)) {
|
|
993
|
+
metrics.record("tieredstore.memory.high_water.bytes", entry.value, "bytes", {
|
|
994
|
+
kind: "runtime_subsystem",
|
|
995
|
+
subsystem_kind: kind,
|
|
996
|
+
metric: metricName,
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
for (const [metricName, entry] of Object.entries(runtimeHighWater.sqlite)) {
|
|
1001
|
+
const unit =
|
|
1002
|
+
metricName.includes("bytes") || metricName.includes("memory") || metricName.includes("overflow") ? "bytes" : "count";
|
|
1003
|
+
metrics.record("tieredstore.sqlite.high_water", entry.value, unit, { metric: metricName });
|
|
1004
|
+
}
|
|
1005
|
+
metrics.record("process.memory.pressure", memory.isOverLimit() ? 1 : 0, "count");
|
|
1006
|
+
if (backpressure) {
|
|
1007
|
+
metrics.record("tieredstore.backpressure.current.bytes", backpressure.getCurrentBytes(), "bytes");
|
|
1008
|
+
metrics.record("tieredstore.backpressure.limit.bytes", backpressure.getMaxBytes(), "bytes");
|
|
1009
|
+
metrics.record("tieredstore.backpressure.pressure", backpressure.isOverLimit() ? 1 : 0, "count");
|
|
1010
|
+
}
|
|
1011
|
+
if (cfg.autoTunePresetMb != null) {
|
|
1012
|
+
metrics.record("tieredstore.auto_tune.preset_mb", cfg.autoTunePresetMb, "count");
|
|
1013
|
+
}
|
|
1014
|
+
if (cfg.autoTuneEffectiveMemoryLimitMb != null) {
|
|
1015
|
+
metrics.record("tieredstore.auto_tune.effective_memory_limit_mb", cfg.autoTuneEffectiveMemoryLimitMb, "count");
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
const metricsEmitter = new MetricsEmitter(metrics, ingest, cfg.metricsFlushIntervalMs, {
|
|
1019
|
+
onAppended: ({ lastOffset, stream }) => {
|
|
1020
|
+
notifier.notify(stream, lastOffset);
|
|
1021
|
+
notifier.notifyDetailsChanged(stream);
|
|
1022
|
+
},
|
|
1023
|
+
collectRuntimeMetrics,
|
|
1024
|
+
});
|
|
1025
|
+
const expirySweeper = new ExpirySweeper(cfg, db);
|
|
1026
|
+
const streamSizeReconciler = new StreamSizeReconciler(
|
|
1027
|
+
db,
|
|
1028
|
+
store,
|
|
1029
|
+
runtime.segmentDiskCache,
|
|
1030
|
+
(stream) => notifier.notifyDetailsChanged(stream)
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1033
|
+
const metricsStreamRow = db.ensureStream(INTERNAL_METRICS_STREAM, { contentType: "application/json", profile: "metrics" });
|
|
1034
|
+
const metricsProfileRes = profiles.updateProfileResult(INTERNAL_METRICS_STREAM, metricsStreamRow, { kind: "metrics" });
|
|
1035
|
+
if (Result.isError(metricsProfileRes)) {
|
|
1036
|
+
throw dsError(`failed to initialize ${INTERNAL_METRICS_STREAM} profile: ${metricsProfileRes.error.message}`);
|
|
1037
|
+
}
|
|
1038
|
+
clearInternalMetricsAccelerationState(db);
|
|
288
1039
|
runtime.start();
|
|
1040
|
+
if (metricsProfileRes.value.schemaRegistry) {
|
|
1041
|
+
void (async () => {
|
|
1042
|
+
try {
|
|
1043
|
+
await uploadSchemaRegistry(INTERNAL_METRICS_STREAM, metricsProfileRes.value.schemaRegistry!);
|
|
1044
|
+
await uploader.publishManifest(INTERNAL_METRICS_STREAM);
|
|
1045
|
+
} catch {
|
|
1046
|
+
// background best-effort; next manifest publication will reconcile
|
|
1047
|
+
}
|
|
1048
|
+
})();
|
|
1049
|
+
}
|
|
289
1050
|
metricsEmitter.start();
|
|
290
1051
|
expirySweeper.start();
|
|
291
1052
|
touch.start();
|
|
1053
|
+
streamSizeReconciler.start();
|
|
292
1054
|
|
|
293
1055
|
const buildJsonRows = (
|
|
294
1056
|
stream: string,
|
|
@@ -300,7 +1062,12 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
300
1062
|
if (Result.isError(regRes)) {
|
|
301
1063
|
return Result.err({ status: 500, message: regRes.error.message });
|
|
302
1064
|
}
|
|
1065
|
+
const profileRes = profiles.getProfileResult(stream);
|
|
1066
|
+
if (Result.isError(profileRes)) {
|
|
1067
|
+
return Result.err({ status: 500, message: profileRes.error.message });
|
|
1068
|
+
}
|
|
303
1069
|
const reg = regRes.value;
|
|
1070
|
+
const jsonIngest = resolveJsonIngestCapability(profileRes.value);
|
|
304
1071
|
const text = new TextDecoder().decode(bodyBytes);
|
|
305
1072
|
let arr: any;
|
|
306
1073
|
try {
|
|
@@ -321,16 +1088,26 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
321
1088
|
|
|
322
1089
|
const rows: AppendRow[] = [];
|
|
323
1090
|
for (const v of arr) {
|
|
324
|
-
|
|
1091
|
+
let value = v;
|
|
1092
|
+
let profileRoutingKey: Uint8Array | null = null;
|
|
1093
|
+
if (jsonIngest) {
|
|
1094
|
+
const preparedRes = jsonIngest.prepareRecordResult({ stream, profile: profileRes.value, value: v });
|
|
1095
|
+
if (Result.isError(preparedRes)) return Result.err({ status: 400, message: preparedRes.error.message });
|
|
1096
|
+
value = preparedRes.value.value;
|
|
1097
|
+
profileRoutingKey = keyBytesFromString(preparedRes.value.routingKey);
|
|
1098
|
+
}
|
|
1099
|
+
if (validator && !validator(value)) {
|
|
325
1100
|
const msg = validator.errors ? validator.errors.map((e) => e.message).join("; ") : "schema validation failed";
|
|
326
1101
|
return Result.err({ status: 400, message: msg });
|
|
327
1102
|
}
|
|
328
|
-
const rkRes = reg.routingKey
|
|
1103
|
+
const rkRes = reg.routingKey
|
|
1104
|
+
? extractRoutingKey(reg, value)
|
|
1105
|
+
: Result.ok(routingKeyHeader != null ? keyBytesFromString(routingKeyHeader) : profileRoutingKey);
|
|
329
1106
|
if (Result.isError(rkRes)) return Result.err({ status: 400, message: rkRes.error.message });
|
|
330
1107
|
rows.push({
|
|
331
1108
|
routingKey: rkRes.value,
|
|
332
1109
|
contentType: "application/json",
|
|
333
|
-
payload: new TextEncoder().encode(JSON.stringify(
|
|
1110
|
+
payload: new TextEncoder().encode(JSON.stringify(value)),
|
|
334
1111
|
});
|
|
335
1112
|
}
|
|
336
1113
|
return Result.ok({ rows });
|
|
@@ -380,6 +1157,11 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
380
1157
|
close: args.close,
|
|
381
1158
|
});
|
|
382
1159
|
|
|
1160
|
+
const awaitAppendWithTimeout = async (appendPromise: Promise<AppendResult>): Promise<AppendResult | Response> => {
|
|
1161
|
+
const appendResult = await awaitWithCooperativeTimeout(appendPromise, APPEND_REQUEST_TIMEOUT_MS);
|
|
1162
|
+
return appendResult === TIMEOUT_SENTINEL ? appendTimeout() : appendResult;
|
|
1163
|
+
};
|
|
1164
|
+
|
|
383
1165
|
const recordAppendOutcome = (args: {
|
|
384
1166
|
stream: string;
|
|
385
1167
|
lastOffset: bigint;
|
|
@@ -392,52 +1174,393 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
392
1174
|
if (args.appendedRows > 0) {
|
|
393
1175
|
metrics.recordAppend(args.metricsBytes, args.appendedRows);
|
|
394
1176
|
notifier.notify(args.stream, args.lastOffset);
|
|
1177
|
+
notifier.notifyDetailsChanged(args.stream);
|
|
395
1178
|
touch.notify(args.stream);
|
|
396
1179
|
}
|
|
397
1180
|
if (stats) {
|
|
398
1181
|
if (args.touched) stats.recordStreamTouched(args.stream);
|
|
399
1182
|
if (args.appendedRows > 0) stats.recordIngested(args.ingestedBytes);
|
|
400
1183
|
}
|
|
401
|
-
if (args.closed)
|
|
1184
|
+
if (args.closed) {
|
|
1185
|
+
notifier.notifyDetailsChanged(args.stream);
|
|
1186
|
+
notifier.notifyClose(args.stream);
|
|
1187
|
+
}
|
|
402
1188
|
};
|
|
403
1189
|
|
|
404
1190
|
const decodeJsonRecords = (
|
|
405
1191
|
stream: string,
|
|
406
1192
|
records: Array<{ offset: bigint; payload: Uint8Array }>
|
|
407
1193
|
): Result<{ values: any[] }, { status: 400 | 500; message: string }> => {
|
|
408
|
-
const regRes = registry.getRegistryResult(stream);
|
|
409
|
-
if (Result.isError(regRes)) return Result.err({ status: 500, message: regRes.error.message });
|
|
410
|
-
const reg = regRes.value;
|
|
411
1194
|
const values: any[] = [];
|
|
412
1195
|
for (const r of records) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (reg.currentVersion > 0) {
|
|
417
|
-
const version = schemaVersionForOffset(reg, r.offset);
|
|
418
|
-
if (version < reg.currentVersion) {
|
|
419
|
-
const chainRes = registry.getLensChainResult(reg, version, reg.currentVersion);
|
|
420
|
-
if (Result.isError(chainRes)) return Result.err({ status: 500, message: chainRes.error.message });
|
|
421
|
-
const chain = chainRes.value;
|
|
422
|
-
const transformedRes = applyLensChainResult(chain, value);
|
|
423
|
-
if (Result.isError(transformedRes)) return Result.err({ status: 400, message: transformedRes.error.message });
|
|
424
|
-
value = transformedRes.value;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
values.push(value);
|
|
428
|
-
} catch (e: any) {
|
|
429
|
-
return Result.err({ status: 400, message: String(e?.message ?? e) });
|
|
430
|
-
}
|
|
1196
|
+
const valueRes = decodeJsonPayloadResult(registry, stream, r.offset, r.payload);
|
|
1197
|
+
if (Result.isError(valueRes)) return valueRes;
|
|
1198
|
+
values.push(valueRes.value);
|
|
431
1199
|
}
|
|
432
1200
|
return Result.ok({ values });
|
|
433
1201
|
};
|
|
434
1202
|
|
|
1203
|
+
const encodeStoredJsonArrayResult = (
|
|
1204
|
+
stream: string,
|
|
1205
|
+
records: Array<{ payload: Uint8Array }>
|
|
1206
|
+
): Result<Buffer | null, { status: 400 | 500; message: string }> => {
|
|
1207
|
+
const regRes = registry.getRegistryResult(stream);
|
|
1208
|
+
if (Result.isError(regRes)) return Result.err({ status: 500, message: regRes.error.message });
|
|
1209
|
+
if (regRes.value.currentVersion !== 0) return Result.ok(null);
|
|
1210
|
+
const parts: Buffer[] = [Buffer.from("[")];
|
|
1211
|
+
for (let i = 0; i < records.length; i++) {
|
|
1212
|
+
if (i > 0) parts.push(Buffer.from(","));
|
|
1213
|
+
const payload = records[i]!.payload;
|
|
1214
|
+
parts.push(Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength));
|
|
1215
|
+
}
|
|
1216
|
+
parts.push(Buffer.from("]"));
|
|
1217
|
+
return Result.ok(Buffer.concat(parts));
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
const buildStreamSummary = (stream: string, row: StreamRow, profileKind: string) => ({
|
|
1221
|
+
name: stream,
|
|
1222
|
+
content_type: normalizeContentType(row.content_type) ?? row.content_type,
|
|
1223
|
+
profile: profileKind,
|
|
1224
|
+
created_at: timestampToIsoString(row.created_at_ms),
|
|
1225
|
+
updated_at: timestampToIsoString(row.updated_at_ms),
|
|
1226
|
+
expires_at: timestampToIsoString(row.expires_at_ms),
|
|
1227
|
+
ttl_seconds: row.ttl_seconds,
|
|
1228
|
+
stream_seq: row.stream_seq,
|
|
1229
|
+
closed: row.closed !== 0,
|
|
1230
|
+
epoch: row.epoch,
|
|
1231
|
+
next_offset: row.next_offset.toString(),
|
|
1232
|
+
sealed_through: row.sealed_through.toString(),
|
|
1233
|
+
uploaded_through: row.uploaded_through.toString(),
|
|
1234
|
+
segment_count: db.countSegmentsForStream(stream),
|
|
1235
|
+
uploaded_segment_count: db.countUploadedSegments(stream),
|
|
1236
|
+
pending_rows: row.pending_rows.toString(),
|
|
1237
|
+
pending_bytes: row.pending_bytes.toString(),
|
|
1238
|
+
total_size_bytes: row.logical_size_bytes.toString(),
|
|
1239
|
+
wal_rows: row.wal_rows.toString(),
|
|
1240
|
+
wal_bytes: row.wal_bytes.toString(),
|
|
1241
|
+
last_append_at: timestampToIsoString(row.last_append_ms),
|
|
1242
|
+
last_segment_cut_at: timestampToIsoString(row.last_segment_cut_ms),
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
const buildIndexLagMs = (stream: string, headRow: StreamRow, coveredSegmentCount: number): string | null => {
|
|
1246
|
+
if (coveredSegmentCount <= 0) return null;
|
|
1247
|
+
const coveredLastAppendMs = db.getSegmentLastAppendMsFromMeta(stream, coveredSegmentCount - 1);
|
|
1248
|
+
if (coveredLastAppendMs == null) return null;
|
|
1249
|
+
const lagMs = headRow.last_append_ms > coveredLastAppendMs ? headRow.last_append_ms - coveredLastAppendMs : 0n;
|
|
1250
|
+
return lagMs.toString();
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
const buildStorageBreakdown = (
|
|
1254
|
+
stream: string,
|
|
1255
|
+
row: StreamRow,
|
|
1256
|
+
currentCompanionRows: Array<{
|
|
1257
|
+
sections_json: string;
|
|
1258
|
+
section_sizes_json: string;
|
|
1259
|
+
size_bytes: number;
|
|
1260
|
+
}>,
|
|
1261
|
+
indexStatus: any
|
|
1262
|
+
) => {
|
|
1263
|
+
const manifest = db.getManifestRow(stream);
|
|
1264
|
+
const schemaRow = db.getSchemaRegistry(stream);
|
|
1265
|
+
const uploadedSegmentBytes = db.getUploadedSegmentBytes(stream);
|
|
1266
|
+
const pendingSealedSegmentBytes = db.getPendingSealedSegmentBytes(stream);
|
|
1267
|
+
const routingIndexStorage = db.getRoutingIndexStorage(stream);
|
|
1268
|
+
const routingLexiconStorage =
|
|
1269
|
+
db
|
|
1270
|
+
.getLexiconIndexStorage(stream)
|
|
1271
|
+
.find((entry) => entry.source_kind === "routing_key" && entry.source_name === "") ?? { object_count: 0, bytes: 0n };
|
|
1272
|
+
const secondaryIndexStorage = new Map(db.getSecondaryIndexStorage(stream).map((entry) => [entry.index_name, entry]));
|
|
1273
|
+
const companionStorage = db.getBundledCompanionStorage(stream);
|
|
1274
|
+
const localStorageUsage = {
|
|
1275
|
+
segment_cache_bytes: 0,
|
|
1276
|
+
routing_index_cache_bytes: 0,
|
|
1277
|
+
exact_index_cache_bytes: 0,
|
|
1278
|
+
lexicon_index_cache_bytes: 0,
|
|
1279
|
+
companion_cache_bytes: 0,
|
|
1280
|
+
...(getLocalStorageUsage?.(stream) ?? {}),
|
|
1281
|
+
};
|
|
1282
|
+
const sqliteSharedBytes = BigInt(db.getWalDbSizeBytes() + db.getMetaDbSizeBytes());
|
|
1283
|
+
const exactIndexBytes = indexStatus.exact_indexes.reduce((sum: bigint, entry: any) => sum + BigInt(entry.bytes_at_rest ?? 0), 0n);
|
|
1284
|
+
const familyBytes = new Map<string, bigint>();
|
|
1285
|
+
for (const row of currentCompanionRows) {
|
|
1286
|
+
const sizes = parseCompanionSectionSizes(row.section_sizes_json);
|
|
1287
|
+
for (const [kind, size] of Object.entries(sizes)) {
|
|
1288
|
+
familyBytes.set(kind, (familyBytes.get(kind) ?? 0n) + BigInt(size));
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return {
|
|
1292
|
+
object_storage: {
|
|
1293
|
+
total_bytes: (
|
|
1294
|
+
uploadedSegmentBytes +
|
|
1295
|
+
routingIndexStorage.bytes +
|
|
1296
|
+
routingLexiconStorage.bytes +
|
|
1297
|
+
exactIndexBytes +
|
|
1298
|
+
companionStorage.bytes +
|
|
1299
|
+
(manifest.last_uploaded_size_bytes ?? 0n) +
|
|
1300
|
+
(schemaRow?.uploaded_size_bytes ?? 0n)
|
|
1301
|
+
).toString(),
|
|
1302
|
+
segments_bytes: uploadedSegmentBytes.toString(),
|
|
1303
|
+
indexes_bytes: (routingIndexStorage.bytes + routingLexiconStorage.bytes + exactIndexBytes + companionStorage.bytes).toString(),
|
|
1304
|
+
manifest_and_meta_bytes: ((manifest.last_uploaded_size_bytes ?? 0n) + (schemaRow?.uploaded_size_bytes ?? 0n)).toString(),
|
|
1305
|
+
manifest_bytes: (manifest.last_uploaded_size_bytes ?? 0n).toString(),
|
|
1306
|
+
schema_registry_bytes: (schemaRow?.uploaded_size_bytes ?? 0n).toString(),
|
|
1307
|
+
segment_object_count: indexStatus.segments.uploaded_count,
|
|
1308
|
+
routing_index_object_count: routingIndexStorage.object_count,
|
|
1309
|
+
routing_lexicon_object_count: routingLexiconStorage.object_count,
|
|
1310
|
+
exact_index_object_count: indexStatus.exact_indexes.reduce((sum: number, entry: any) => sum + Number(entry.object_count ?? 0), 0),
|
|
1311
|
+
bundled_companion_object_count: companionStorage.object_count,
|
|
1312
|
+
},
|
|
1313
|
+
local_storage: {
|
|
1314
|
+
total_bytes: (
|
|
1315
|
+
row.wal_bytes +
|
|
1316
|
+
pendingSealedSegmentBytes +
|
|
1317
|
+
BigInt(localStorageUsage.segment_cache_bytes) +
|
|
1318
|
+
BigInt(localStorageUsage.routing_index_cache_bytes) +
|
|
1319
|
+
BigInt(localStorageUsage.exact_index_cache_bytes) +
|
|
1320
|
+
BigInt(localStorageUsage.lexicon_index_cache_bytes) +
|
|
1321
|
+
BigInt(localStorageUsage.companion_cache_bytes)
|
|
1322
|
+
).toString(),
|
|
1323
|
+
wal_retained_bytes: row.wal_bytes.toString(),
|
|
1324
|
+
pending_tail_bytes: row.pending_bytes.toString(),
|
|
1325
|
+
pending_sealed_segment_bytes: pendingSealedSegmentBytes.toString(),
|
|
1326
|
+
segment_cache_bytes: String(localStorageUsage.segment_cache_bytes),
|
|
1327
|
+
routing_index_cache_bytes: String(localStorageUsage.routing_index_cache_bytes),
|
|
1328
|
+
exact_index_cache_bytes: String(localStorageUsage.exact_index_cache_bytes),
|
|
1329
|
+
lexicon_index_cache_bytes: String(localStorageUsage.lexicon_index_cache_bytes),
|
|
1330
|
+
companion_cache_bytes: String(localStorageUsage.companion_cache_bytes),
|
|
1331
|
+
sqlite_shared_total_bytes: sqliteSharedBytes.toString(),
|
|
1332
|
+
},
|
|
1333
|
+
companion_families: {
|
|
1334
|
+
col_bytes: String(familyBytes.get("col") ?? 0n),
|
|
1335
|
+
fts_bytes: String(familyBytes.get("fts") ?? 0n),
|
|
1336
|
+
agg_bytes: String(familyBytes.get("agg") ?? 0n),
|
|
1337
|
+
mblk_bytes: String(familyBytes.get("mblk") ?? 0n),
|
|
1338
|
+
},
|
|
1339
|
+
};
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
const buildObjectStoreRequestSummary = (stream: string) => {
|
|
1343
|
+
const summary = db.getObjectStoreRequestSummaryByHash(streamHash16Hex(stream));
|
|
1344
|
+
return {
|
|
1345
|
+
puts: summary.puts.toString(),
|
|
1346
|
+
reads: summary.reads.toString(),
|
|
1347
|
+
gets: summary.gets.toString(),
|
|
1348
|
+
heads: summary.heads.toString(),
|
|
1349
|
+
lists: summary.lists.toString(),
|
|
1350
|
+
deletes: summary.deletes.toString(),
|
|
1351
|
+
by_artifact: summary.by_artifact.map((entry) => ({
|
|
1352
|
+
artifact: entry.artifact,
|
|
1353
|
+
puts: entry.puts.toString(),
|
|
1354
|
+
gets: entry.gets.toString(),
|
|
1355
|
+
heads: entry.heads.toString(),
|
|
1356
|
+
lists: entry.lists.toString(),
|
|
1357
|
+
deletes: entry.deletes.toString(),
|
|
1358
|
+
reads: entry.reads.toString(),
|
|
1359
|
+
})),
|
|
1360
|
+
};
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
const buildIndexStatus = (stream: string, row: StreamRow, reg: SchemaRegistry, profileKind: string) => {
|
|
1364
|
+
const segmentCount = db.countSegmentsForStream(stream);
|
|
1365
|
+
const uploadedSegmentCount = db.countUploadedSegments(stream);
|
|
1366
|
+
const manifest = db.getManifestRow(stream);
|
|
1367
|
+
|
|
1368
|
+
const routingState = db.getIndexState(stream);
|
|
1369
|
+
const routingRuns = db.listIndexRuns(stream);
|
|
1370
|
+
const retiredRoutingRuns = db.listRetiredIndexRuns(stream);
|
|
1371
|
+
const routingStorage = db.getRoutingIndexStorage(stream);
|
|
1372
|
+
const routingLexiconState = db.getLexiconIndexState(stream, "routing_key", "");
|
|
1373
|
+
const routingLexiconRuns = db.listLexiconIndexRuns(stream, "routing_key", "");
|
|
1374
|
+
const retiredRoutingLexiconRuns = db.listRetiredLexiconIndexRuns(stream, "routing_key", "");
|
|
1375
|
+
const routingLexiconStorage =
|
|
1376
|
+
db
|
|
1377
|
+
.getLexiconIndexStorage(stream)
|
|
1378
|
+
.find((entry) => entry.source_kind === "routing_key" && entry.source_name === "") ?? { object_count: 0, bytes: 0n };
|
|
1379
|
+
const secondaryIndexStorage = new Map(db.getSecondaryIndexStorage(stream).map((entry) => [entry.index_name, entry]));
|
|
1380
|
+
|
|
1381
|
+
const exactIndexes = configuredExactIndexes(reg.search).map(({ name, kind, configHash }) => {
|
|
1382
|
+
const state = db.getSecondaryIndexState(stream, name);
|
|
1383
|
+
const configMatches = state?.config_hash === configHash;
|
|
1384
|
+
const indexedSegmentCount = configMatches ? (state?.indexed_through ?? 0) : 0;
|
|
1385
|
+
const storage = secondaryIndexStorage.get(name);
|
|
1386
|
+
return {
|
|
1387
|
+
name,
|
|
1388
|
+
kind,
|
|
1389
|
+
indexed_segment_count: indexedSegmentCount,
|
|
1390
|
+
lag_segments: Math.max(0, uploadedSegmentCount - indexedSegmentCount),
|
|
1391
|
+
lag_ms: buildIndexLagMs(stream, row, indexedSegmentCount),
|
|
1392
|
+
bytes_at_rest: String(storage?.bytes ?? 0n),
|
|
1393
|
+
object_count: storage?.object_count ?? 0,
|
|
1394
|
+
active_run_count: db.listSecondaryIndexRuns(stream, name).length,
|
|
1395
|
+
retired_run_count: db.listRetiredSecondaryIndexRuns(stream, name).length,
|
|
1396
|
+
fully_indexed_uploaded_segments: configMatches && indexedSegmentCount >= uploadedSegmentCount,
|
|
1397
|
+
stale_configuration: !configMatches,
|
|
1398
|
+
updated_at: timestampToIsoString(state?.updated_at_ms ?? null),
|
|
1399
|
+
};
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
const desiredCompanionPlan = buildDesiredSearchCompanionPlan(reg);
|
|
1403
|
+
const desiredCompanionHash = hashSearchCompanionPlan(desiredCompanionPlan);
|
|
1404
|
+
const companionPlanRow = db.getSearchCompanionPlan(stream);
|
|
1405
|
+
const desiredIndexPlanGeneration =
|
|
1406
|
+
Object.values(desiredCompanionPlan.families).some(Boolean)
|
|
1407
|
+
? companionPlanRow
|
|
1408
|
+
? companionPlanRow.plan_hash === desiredCompanionHash
|
|
1409
|
+
? companionPlanRow.generation
|
|
1410
|
+
: companionPlanRow.generation + 1
|
|
1411
|
+
: 1
|
|
1412
|
+
: 0;
|
|
1413
|
+
const companionRows = db.listSearchSegmentCompanions(stream);
|
|
1414
|
+
const currentCompanionRows = companionRows.filter((row) => row.plan_generation === desiredIndexPlanGeneration);
|
|
1415
|
+
const currentCompanionBytes = currentCompanionRows.reduce((sum, entry) => sum + BigInt(entry.size_bytes), 0n);
|
|
1416
|
+
const searchFamilies = configuredSearchFamilies(reg.search).map(({ family, fields }) => {
|
|
1417
|
+
const coveredSegmentCount = currentCompanionRows.filter((row) => parseCompanionSections(row.sections_json).has(family)).length;
|
|
1418
|
+
const contiguousCoveredCount = contiguousCoveredSegmentCount(currentCompanionRows, family);
|
|
1419
|
+
let familyBytes = 0n;
|
|
1420
|
+
let familyObjectCount = 0;
|
|
1421
|
+
for (const row of currentCompanionRows) {
|
|
1422
|
+
const size = parseCompanionSectionSizes(row.section_sizes_json)[family];
|
|
1423
|
+
if (size == null) continue;
|
|
1424
|
+
familyBytes += BigInt(size);
|
|
1425
|
+
familyObjectCount += 1;
|
|
1426
|
+
}
|
|
1427
|
+
return {
|
|
1428
|
+
family,
|
|
1429
|
+
fields,
|
|
1430
|
+
plan_generation: desiredIndexPlanGeneration,
|
|
1431
|
+
covered_segment_count: coveredSegmentCount,
|
|
1432
|
+
contiguous_covered_segment_count: contiguousCoveredCount,
|
|
1433
|
+
lag_segments: Math.max(0, uploadedSegmentCount - contiguousCoveredCount),
|
|
1434
|
+
lag_ms: buildIndexLagMs(stream, row, contiguousCoveredCount),
|
|
1435
|
+
bytes_at_rest: familyBytes.toString(),
|
|
1436
|
+
object_count: familyObjectCount,
|
|
1437
|
+
stale_segment_count: Math.max(0, uploadedSegmentCount - coveredSegmentCount),
|
|
1438
|
+
fully_indexed_uploaded_segments: coveredSegmentCount >= uploadedSegmentCount,
|
|
1439
|
+
updated_at: timestampToIsoString(companionPlanRow?.updated_at_ms ?? null),
|
|
1440
|
+
};
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
return {
|
|
1444
|
+
stream,
|
|
1445
|
+
profile: profileKind,
|
|
1446
|
+
desired_index_plan_generation: desiredIndexPlanGeneration,
|
|
1447
|
+
segments: {
|
|
1448
|
+
total_count: segmentCount,
|
|
1449
|
+
uploaded_count: uploadedSegmentCount,
|
|
1450
|
+
},
|
|
1451
|
+
manifest: {
|
|
1452
|
+
generation: manifest.generation,
|
|
1453
|
+
uploaded_generation: manifest.uploaded_generation,
|
|
1454
|
+
last_uploaded_at: timestampToIsoString(manifest.last_uploaded_at_ms),
|
|
1455
|
+
last_uploaded_etag: manifest.last_uploaded_etag,
|
|
1456
|
+
last_uploaded_size_bytes: manifest.last_uploaded_size_bytes?.toString() ?? null,
|
|
1457
|
+
},
|
|
1458
|
+
routing_key_index: {
|
|
1459
|
+
configured: reg.routingKey != null,
|
|
1460
|
+
indexed_segment_count: routingState?.indexed_through ?? 0,
|
|
1461
|
+
lag_segments: Math.max(0, uploadedSegmentCount - (routingState?.indexed_through ?? 0)),
|
|
1462
|
+
lag_ms: buildIndexLagMs(stream, row, routingState?.indexed_through ?? 0),
|
|
1463
|
+
bytes_at_rest: routingStorage.bytes.toString(),
|
|
1464
|
+
object_count: routingStorage.object_count,
|
|
1465
|
+
active_run_count: routingRuns.length,
|
|
1466
|
+
retired_run_count: retiredRoutingRuns.length,
|
|
1467
|
+
fully_indexed_uploaded_segments: reg.routingKey == null ? true : (routingState?.indexed_through ?? 0) >= uploadedSegmentCount,
|
|
1468
|
+
updated_at: timestampToIsoString(routingState?.updated_at_ms ?? null),
|
|
1469
|
+
},
|
|
1470
|
+
routing_key_lexicon: {
|
|
1471
|
+
configured: reg.routingKey != null,
|
|
1472
|
+
indexed_segment_count: routingLexiconState?.indexed_through ?? 0,
|
|
1473
|
+
lag_segments: Math.max(0, uploadedSegmentCount - (routingLexiconState?.indexed_through ?? 0)),
|
|
1474
|
+
lag_ms: buildIndexLagMs(stream, row, routingLexiconState?.indexed_through ?? 0),
|
|
1475
|
+
bytes_at_rest: routingLexiconStorage.bytes.toString(),
|
|
1476
|
+
object_count: routingLexiconStorage.object_count,
|
|
1477
|
+
active_run_count: routingLexiconRuns.length,
|
|
1478
|
+
retired_run_count: retiredRoutingLexiconRuns.length,
|
|
1479
|
+
fully_indexed_uploaded_segments: reg.routingKey == null ? true : (routingLexiconState?.indexed_through ?? 0) >= uploadedSegmentCount,
|
|
1480
|
+
updated_at: timestampToIsoString(routingLexiconState?.updated_at_ms ?? null),
|
|
1481
|
+
},
|
|
1482
|
+
exact_indexes: exactIndexes,
|
|
1483
|
+
bundled_companions: {
|
|
1484
|
+
object_count: currentCompanionRows.length,
|
|
1485
|
+
bytes_at_rest: currentCompanionBytes.toString(),
|
|
1486
|
+
fully_indexed_uploaded_segments: currentCompanionRows.length >= uploadedSegmentCount,
|
|
1487
|
+
},
|
|
1488
|
+
search_families: searchFamilies,
|
|
1489
|
+
current_companion_rows: currentCompanionRows,
|
|
1490
|
+
};
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
type DetailsSnapshot = { etag: string; body: string; version: bigint };
|
|
1494
|
+
|
|
1495
|
+
const buildDetailsSnapshotResult = (
|
|
1496
|
+
stream: string,
|
|
1497
|
+
mode: "details" | "index_status"
|
|
1498
|
+
): Result<DetailsSnapshot, { status: 404 | 500; message: string }> => {
|
|
1499
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1500
|
+
const beforeVersion = notifier.currentDetailsVersion(stream);
|
|
1501
|
+
const srow = db.getStream(stream);
|
|
1502
|
+
if (!srow || db.isDeleted(srow)) return Result.err({ status: 404, message: "not_found" });
|
|
1503
|
+
if (srow.expires_at_ms != null && db.nowMs() > srow.expires_at_ms) return Result.err({ status: 404, message: "stream expired" });
|
|
1504
|
+
|
|
1505
|
+
const regRes = registry.getRegistryResult(stream);
|
|
1506
|
+
if (Result.isError(regRes)) return Result.err({ status: 500, message: regRes.error.message });
|
|
1507
|
+
const profileRes = profiles.getProfileResourceResult(stream, srow);
|
|
1508
|
+
if (Result.isError(profileRes)) return Result.err({ status: 500, message: profileRes.error.message });
|
|
1509
|
+
|
|
1510
|
+
const profileKind = profileRes.value.profile.kind;
|
|
1511
|
+
const indexStatus = buildIndexStatus(stream, srow, regRes.value, profileKind);
|
|
1512
|
+
const storage = buildStorageBreakdown(stream, srow, indexStatus.current_companion_rows, indexStatus);
|
|
1513
|
+
const objectStoreRequests = buildObjectStoreRequestSummary(stream);
|
|
1514
|
+
delete (indexStatus as any).current_companion_rows;
|
|
1515
|
+
const payload =
|
|
1516
|
+
mode === "index_status"
|
|
1517
|
+
? indexStatus
|
|
1518
|
+
: {
|
|
1519
|
+
stream: buildStreamSummary(stream, srow, profileKind),
|
|
1520
|
+
profile: profileRes.value,
|
|
1521
|
+
schema: regRes.value,
|
|
1522
|
+
index_status: indexStatus,
|
|
1523
|
+
storage,
|
|
1524
|
+
object_store_requests: objectStoreRequests,
|
|
1525
|
+
};
|
|
1526
|
+
const body = JSON.stringify(payload);
|
|
1527
|
+
const afterVersion = notifier.currentDetailsVersion(stream);
|
|
1528
|
+
if (beforeVersion === afterVersion) {
|
|
1529
|
+
return Result.ok({
|
|
1530
|
+
etag: weakEtag(mode, body),
|
|
1531
|
+
body,
|
|
1532
|
+
version: afterVersion,
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
return Result.err({ status: 500, message: "details changed too quickly" });
|
|
1538
|
+
};
|
|
1539
|
+
|
|
435
1540
|
let closing = false;
|
|
436
1541
|
const fetch = async (req: Request): Promise<Response> => {
|
|
437
1542
|
if (closing) {
|
|
438
|
-
return
|
|
1543
|
+
return unavailable();
|
|
439
1544
|
}
|
|
1545
|
+
const requestAbortController = new AbortController();
|
|
1546
|
+
const abortFromClient = () => requestAbortController.abort(req.signal.reason);
|
|
1547
|
+
let timedOut = false;
|
|
1548
|
+
if (req.signal.aborted) requestAbortController.abort(req.signal.reason);
|
|
1549
|
+
else req.signal.addEventListener("abort", abortFromClient, { once: true });
|
|
440
1550
|
try {
|
|
1551
|
+
const runWithGate = async <T>(gate: ConcurrencyGate, fn: () => Promise<T>): Promise<T> =>
|
|
1552
|
+
gate.run(fn, requestAbortController.signal);
|
|
1553
|
+
const runForeground = async <T>(fn: () => Promise<T>): Promise<T> => {
|
|
1554
|
+
const leaveForeground = foregroundActivity.enter();
|
|
1555
|
+
try {
|
|
1556
|
+
return await fn();
|
|
1557
|
+
} finally {
|
|
1558
|
+
leaveForeground();
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
const runForegroundWithGate = async <T>(gate: ConcurrencyGate, fn: () => Promise<T>): Promise<T> =>
|
|
1562
|
+
runForeground(() => runWithGate(gate, fn));
|
|
1563
|
+
const requestPromise = (async (): Promise<Response> => {
|
|
441
1564
|
let url: URL;
|
|
442
1565
|
try {
|
|
443
1566
|
url = new URL(req.url, "http://localhost");
|
|
@@ -452,33 +1575,38 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
452
1575
|
if (path === "/metrics") {
|
|
453
1576
|
return json(200, metrics.snapshot());
|
|
454
1577
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
return json(429, { error: { code: "overloaded", message: "ingest queue full" } });
|
|
462
|
-
};
|
|
1578
|
+
if (req.method === "GET" && path === "/v1/server/_details") {
|
|
1579
|
+
return json(200, buildServerDetails());
|
|
1580
|
+
}
|
|
1581
|
+
if (req.method === "GET" && path === "/v1/server/_mem") {
|
|
1582
|
+
return json(200, buildServerMem());
|
|
1583
|
+
}
|
|
463
1584
|
|
|
464
1585
|
// /v1/streams
|
|
465
1586
|
if (req.method === "GET" && path === "/v1/streams") {
|
|
466
1587
|
const limit = Number(url.searchParams.get("limit") ?? "100");
|
|
467
1588
|
const offset = Number(url.searchParams.get("offset") ?? "0");
|
|
468
1589
|
const rows = db.listStreams(Math.max(0, Math.min(limit, 1000)), Math.max(0, offset));
|
|
469
|
-
const out =
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
1590
|
+
const out = [];
|
|
1591
|
+
for (const r of rows) {
|
|
1592
|
+
const profileRes = profiles.getProfileResult(r.stream, r);
|
|
1593
|
+
if (Result.isError(profileRes)) return internalError("invalid stream profile");
|
|
1594
|
+
const profile = profileRes.value;
|
|
1595
|
+
out.push({
|
|
1596
|
+
name: r.stream,
|
|
1597
|
+
created_at: new Date(Number(r.created_at_ms)).toISOString(),
|
|
1598
|
+
expires_at: r.expires_at_ms == null ? null : new Date(Number(r.expires_at_ms)).toISOString(),
|
|
1599
|
+
epoch: r.epoch,
|
|
1600
|
+
next_offset: r.next_offset.toString(),
|
|
1601
|
+
sealed_through: r.sealed_through.toString(),
|
|
1602
|
+
uploaded_through: r.uploaded_through.toString(),
|
|
1603
|
+
profile: profile.kind,
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
478
1606
|
return json(200, out);
|
|
479
1607
|
}
|
|
480
1608
|
|
|
481
|
-
// /v1/stream/:name[/_schema] (accept encoded or raw slashes in name)
|
|
1609
|
+
// /v1/stream/:name[/_schema|/_profile|/_details|/_index_status] (accept encoded or raw slashes in name)
|
|
482
1610
|
const streamPrefix = "/v1/stream/";
|
|
483
1611
|
if (path.startsWith(streamPrefix)) {
|
|
484
1612
|
const rawRest = path.slice(streamPrefix.length);
|
|
@@ -486,15 +1614,35 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
486
1614
|
if (rest.length === 0) return badRequest("missing stream name");
|
|
487
1615
|
const segments = rest.split("/");
|
|
488
1616
|
let isSchema = false;
|
|
1617
|
+
let isProfile = false;
|
|
1618
|
+
let isSearch = false;
|
|
1619
|
+
let isAggregate = false;
|
|
1620
|
+
let isDetails = false;
|
|
1621
|
+
let isIndexStatus = false;
|
|
1622
|
+
let isRoutingKeys = false;
|
|
489
1623
|
let pathKeyParam: string | null = null;
|
|
490
|
-
let touchMode:
|
|
491
|
-
| null
|
|
492
|
-
| { kind: "meta" }
|
|
493
|
-
| { kind: "wait" }
|
|
494
|
-
| { kind: "templates_activate" } = null;
|
|
1624
|
+
let touchMode: StreamTouchRoute | null = null;
|
|
495
1625
|
if (segments[segments.length - 1] === "_schema") {
|
|
496
1626
|
isSchema = true;
|
|
497
1627
|
segments.pop();
|
|
1628
|
+
} else if (segments[segments.length - 1] === "_profile") {
|
|
1629
|
+
isProfile = true;
|
|
1630
|
+
segments.pop();
|
|
1631
|
+
} else if (segments[segments.length - 1] === "_search") {
|
|
1632
|
+
isSearch = true;
|
|
1633
|
+
segments.pop();
|
|
1634
|
+
} else if (segments[segments.length - 1] === "_aggregate") {
|
|
1635
|
+
isAggregate = true;
|
|
1636
|
+
segments.pop();
|
|
1637
|
+
} else if (segments[segments.length - 1] === "_details") {
|
|
1638
|
+
isDetails = true;
|
|
1639
|
+
segments.pop();
|
|
1640
|
+
} else if (segments[segments.length - 1] === "_index_status") {
|
|
1641
|
+
isIndexStatus = true;
|
|
1642
|
+
segments.pop();
|
|
1643
|
+
} else if (segments[segments.length - 1] === "_routing_keys") {
|
|
1644
|
+
isRoutingKeys = true;
|
|
1645
|
+
segments.pop();
|
|
498
1646
|
} else if (
|
|
499
1647
|
segments.length >= 3 &&
|
|
500
1648
|
segments[segments.length - 3] === "touch" &&
|
|
@@ -528,74 +1676,16 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
528
1676
|
return json(200, regRes.value);
|
|
529
1677
|
}
|
|
530
1678
|
if (req.method === "POST") {
|
|
531
|
-
let body:
|
|
1679
|
+
let body: unknown;
|
|
532
1680
|
try {
|
|
533
1681
|
body = await req.json();
|
|
534
1682
|
} catch {
|
|
535
1683
|
return badRequest("schema update must be valid JSON");
|
|
536
1684
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const isSchemaObject =
|
|
542
|
-
update &&
|
|
543
|
-
(update.schema === true ||
|
|
544
|
-
update.schema === false ||
|
|
545
|
-
(typeof update.schema === "object" && update.schema !== null && !Array.isArray(update.schema)));
|
|
546
|
-
if (!isSchemaObject && update && typeof update === "object" && update.schemas && typeof update.schemas === "object") {
|
|
547
|
-
const versions = Object.keys(update.schemas)
|
|
548
|
-
.map((v) => Number(v))
|
|
549
|
-
.filter((v) => Number.isFinite(v) && v >= 0);
|
|
550
|
-
const currentVersion =
|
|
551
|
-
typeof update.currentVersion === "number" && Number.isFinite(update.currentVersion)
|
|
552
|
-
? update.currentVersion
|
|
553
|
-
: versions.length > 0
|
|
554
|
-
? Math.max(...versions)
|
|
555
|
-
: null;
|
|
556
|
-
if (currentVersion != null) {
|
|
557
|
-
const schema = update.schemas[String(currentVersion)];
|
|
558
|
-
const lens =
|
|
559
|
-
update.lens ??
|
|
560
|
-
(update.lenses && typeof update.lenses === "object" ? update.lenses[String(currentVersion - 1)] : undefined);
|
|
561
|
-
update = {
|
|
562
|
-
schema,
|
|
563
|
-
lens,
|
|
564
|
-
routingKey: update.routingKey,
|
|
565
|
-
interpreter: (update as any).interpreter,
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
if (update && typeof update === "object") {
|
|
570
|
-
if (update.schema === null) {
|
|
571
|
-
delete update.schema;
|
|
572
|
-
}
|
|
573
|
-
if (update.routingKey === undefined) {
|
|
574
|
-
const raw = update as any;
|
|
575
|
-
const candidate =
|
|
576
|
-
raw.routing_key ?? raw.routingKeyPointer ?? raw.routing_key_pointer ?? raw.routingKey;
|
|
577
|
-
if (typeof candidate === "string") {
|
|
578
|
-
update.routingKey = { jsonPointer: candidate, required: true };
|
|
579
|
-
} else if (candidate && typeof candidate === "object") {
|
|
580
|
-
const jsonPointer = candidate.jsonPointer ?? candidate.json_pointer;
|
|
581
|
-
if (typeof jsonPointer === "string") {
|
|
582
|
-
update.routingKey = {
|
|
583
|
-
jsonPointer,
|
|
584
|
-
required: typeof candidate.required === "boolean" ? candidate.required : true,
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
} else if (update.routingKey && typeof update.routingKey === "object") {
|
|
589
|
-
const rk = update.routingKey as any;
|
|
590
|
-
if (rk.jsonPointer === undefined && typeof rk.json_pointer === "string") {
|
|
591
|
-
update.routingKey = {
|
|
592
|
-
jsonPointer: rk.json_pointer,
|
|
593
|
-
required: typeof rk.required === "boolean" ? rk.required : true,
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
if (update.schema === undefined && update.routingKey !== undefined && update.interpreter === undefined) {
|
|
1685
|
+
const updateRes = parseSchemaUpdateResult(body);
|
|
1686
|
+
if (Result.isError(updateRes)) return badRequest(updateRes.error.message);
|
|
1687
|
+
const update = updateRes.value;
|
|
1688
|
+
if (update.schema === undefined && update.routingKey !== undefined && update.search === undefined) {
|
|
599
1689
|
const regRes = registry.updateRoutingKeyResult(stream, update.routingKey ?? null);
|
|
600
1690
|
if (Result.isError(regRes)) return schemaMutationErrorResponse(regRes.error);
|
|
601
1691
|
try {
|
|
@@ -603,408 +1693,354 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
603
1693
|
} catch {
|
|
604
1694
|
return json(500, { error: { code: "internal", message: "schema upload failed" } });
|
|
605
1695
|
}
|
|
1696
|
+
indexer?.enqueue(stream);
|
|
1697
|
+
notifier.notifyDetailsChanged(stream);
|
|
606
1698
|
return json(200, regRes.value);
|
|
607
1699
|
}
|
|
608
|
-
if (update.schema === undefined && update.
|
|
609
|
-
const regRes = registry.
|
|
1700
|
+
if (update.schema === undefined && update.search !== undefined && update.routingKey === undefined) {
|
|
1701
|
+
const regRes = registry.updateSearchResult(stream, update.search ?? null);
|
|
610
1702
|
if (Result.isError(regRes)) return schemaMutationErrorResponse(regRes.error);
|
|
611
1703
|
try {
|
|
612
1704
|
await uploadSchemaRegistry(stream, regRes.value);
|
|
613
1705
|
} catch {
|
|
614
1706
|
return json(500, { error: { code: "internal", message: "schema upload failed" } });
|
|
615
1707
|
}
|
|
1708
|
+
indexer?.enqueue(stream);
|
|
1709
|
+
notifier.notifyDetailsChanged(stream);
|
|
616
1710
|
return json(200, regRes.value);
|
|
617
1711
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
try {
|
|
625
|
-
await uploadSchemaRegistry(stream, interpreterRes.value);
|
|
626
|
-
} catch {
|
|
627
|
-
return json(500, { error: { code: "internal", message: "schema upload failed" } });
|
|
628
|
-
}
|
|
629
|
-
return json(200, interpreterRes.value);
|
|
630
|
-
}
|
|
631
|
-
const regRes = registry.updateRegistryResult(stream, srow, update);
|
|
1712
|
+
const regRes = registry.updateRegistryResult(stream, srow, {
|
|
1713
|
+
schema: update.schema,
|
|
1714
|
+
lens: update.lens,
|
|
1715
|
+
routingKey: update.routingKey ?? undefined,
|
|
1716
|
+
search: update.search,
|
|
1717
|
+
});
|
|
632
1718
|
if (Result.isError(regRes)) return schemaMutationErrorResponse(regRes.error);
|
|
633
1719
|
try {
|
|
634
1720
|
await uploadSchemaRegistry(stream, regRes.value);
|
|
635
1721
|
} catch {
|
|
636
1722
|
return json(500, { error: { code: "internal", message: "schema upload failed" } });
|
|
637
1723
|
}
|
|
1724
|
+
indexer?.enqueue(stream);
|
|
1725
|
+
notifier.notifyDetailsChanged(stream);
|
|
638
1726
|
return json(200, regRes.value);
|
|
639
1727
|
}
|
|
640
1728
|
return badRequest("unsupported method");
|
|
641
1729
|
}
|
|
642
1730
|
|
|
643
|
-
if (
|
|
1731
|
+
if (isProfile) {
|
|
644
1732
|
const srow = db.getStream(stream);
|
|
645
1733
|
if (!srow || db.isDeleted(srow)) return notFound();
|
|
646
1734
|
if (srow.expires_at_ms != null && db.nowMs() > srow.expires_at_ms) return notFound("stream expired");
|
|
647
1735
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const touchCfg = reg.interpreter.touch;
|
|
1736
|
+
if (req.method === "GET") {
|
|
1737
|
+
const profileRes = profiles.getProfileResourceResult(stream, srow);
|
|
1738
|
+
if (Result.isError(profileRes)) return internalError("invalid stream profile");
|
|
1739
|
+
return json(200, profileRes.value);
|
|
1740
|
+
}
|
|
654
1741
|
|
|
655
|
-
if (
|
|
656
|
-
if (req.method !== "POST") return badRequest("unsupported method");
|
|
1742
|
+
if (req.method === "POST") {
|
|
657
1743
|
let body: any;
|
|
658
1744
|
try {
|
|
659
1745
|
body = await req.json();
|
|
660
1746
|
} catch {
|
|
661
|
-
return badRequest("
|
|
662
|
-
}
|
|
663
|
-
const templatesRaw = body?.templates;
|
|
664
|
-
if (!Array.isArray(templatesRaw) || templatesRaw.length === 0) {
|
|
665
|
-
return badRequest("activate.templates must be a non-empty array");
|
|
1747
|
+
return badRequest("profile update must be valid JSON");
|
|
666
1748
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
? Math.floor(ttlRaw)
|
|
675
|
-
: null;
|
|
676
|
-
if (inactivityTtlMs == null) return badRequest("activate.inactivityTtlMs must be a non-negative number (ms)");
|
|
677
|
-
|
|
678
|
-
const templates: Array<{ entity: string; fields: Array<{ name: string; encoding: any }> }> = [];
|
|
679
|
-
for (const t of templatesRaw) {
|
|
680
|
-
const entity = typeof t?.entity === "string" ? t.entity.trim() : "";
|
|
681
|
-
const fieldsRaw = t?.fields;
|
|
682
|
-
if (entity === "" || !Array.isArray(fieldsRaw) || fieldsRaw.length === 0 || fieldsRaw.length > 3) continue;
|
|
683
|
-
const fields: Array<{ name: string; encoding: any }> = [];
|
|
684
|
-
for (const f of fieldsRaw) {
|
|
685
|
-
const name = typeof f?.name === "string" ? f.name.trim() : "";
|
|
686
|
-
const encoding = f?.encoding;
|
|
687
|
-
if (name === "") continue;
|
|
688
|
-
fields.push({ name, encoding });
|
|
1749
|
+
const nextProfileRes = parseProfileUpdateResult(body);
|
|
1750
|
+
if (Result.isError(nextProfileRes)) return badRequest(nextProfileRes.error.message);
|
|
1751
|
+
const profileRes = profiles.updateProfileResult(stream, srow, nextProfileRes.value);
|
|
1752
|
+
if (Result.isError(profileRes)) return badRequest(profileRes.error.message);
|
|
1753
|
+
try {
|
|
1754
|
+
if (profileRes.value.schemaRegistry) {
|
|
1755
|
+
await uploadSchemaRegistry(stream, profileRes.value.schemaRegistry);
|
|
689
1756
|
}
|
|
690
|
-
|
|
691
|
-
|
|
1757
|
+
await uploader.publishManifest(stream);
|
|
1758
|
+
} catch {
|
|
1759
|
+
return json(500, { error: { code: "internal", message: "profile upload failed" } });
|
|
692
1760
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
maxActiveTemplatesPerEntity: touchCfg.templates?.maxActiveTemplatesPerEntity ?? 256,
|
|
698
|
-
};
|
|
699
|
-
|
|
700
|
-
const activeFromTouchOffset = touch.getOrCreateJournal(stream, touchCfg).getCursor();
|
|
1761
|
+
indexer?.enqueue(stream);
|
|
1762
|
+
notifier.notifyDetailsChanged(stream);
|
|
1763
|
+
return json(200, profileRes.value.resource);
|
|
1764
|
+
}
|
|
701
1765
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
touchCfg,
|
|
705
|
-
baseStreamNextOffset: srow.next_offset,
|
|
706
|
-
activeFromTouchOffset,
|
|
707
|
-
templates,
|
|
708
|
-
inactivityTtlMs,
|
|
709
|
-
});
|
|
1766
|
+
return badRequest("unsupported method");
|
|
1767
|
+
}
|
|
710
1768
|
|
|
711
|
-
|
|
712
|
-
|
|
1769
|
+
if (isDetails || isIndexStatus) {
|
|
1770
|
+
if (req.method !== "GET") return badRequest("unsupported method");
|
|
1771
|
+
const liveParam = url.searchParams.get("live") ?? "";
|
|
1772
|
+
let longPoll = false;
|
|
1773
|
+
if (liveParam === "" || liveParam === "false" || liveParam === "0") longPoll = false;
|
|
1774
|
+
else if (liveParam === "long-poll" || liveParam === "true" || liveParam === "1") longPoll = true;
|
|
1775
|
+
else return badRequest("invalid live mode");
|
|
713
1776
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1777
|
+
const timeout = url.searchParams.get("timeout") ?? url.searchParams.get("timeout_ms");
|
|
1778
|
+
let timeoutMs: number | null = null;
|
|
1779
|
+
if (timeout) {
|
|
1780
|
+
if (/^[0-9]+$/.test(timeout)) {
|
|
1781
|
+
timeoutMs = Number(timeout);
|
|
1782
|
+
} else {
|
|
1783
|
+
const timeoutRes = parseDurationMsResult(timeout);
|
|
1784
|
+
if (Result.isError(timeoutRes)) return badRequest("invalid timeout");
|
|
1785
|
+
timeoutMs = timeoutRes.value;
|
|
722
1786
|
}
|
|
723
|
-
const meta = touch.getOrCreateJournal(stream, touchCfg).getMeta();
|
|
724
|
-
const runtime = touch.getTouchRuntimeSnapshot({ stream, touchCfg });
|
|
725
|
-
const interp = db.getStreamInterpreter(stream);
|
|
726
|
-
return json(200, {
|
|
727
|
-
...meta,
|
|
728
|
-
coarseIntervalMs: touchCfg.coarseIntervalMs ?? 100,
|
|
729
|
-
touchCoalesceWindowMs: touchCfg.touchCoalesceWindowMs ?? 100,
|
|
730
|
-
activeTemplates,
|
|
731
|
-
lagSourceOffsets: runtime.lagSourceOffsets,
|
|
732
|
-
touchMode: runtime.touchMode,
|
|
733
|
-
walScannedThrough: interp ? encodeOffset(srow.epoch, interp.interpreted_through) : null,
|
|
734
|
-
bucketMaxSourceOffsetSeq: meta.bucketMaxSourceOffsetSeq,
|
|
735
|
-
hotFineKeys: runtime.hotFineKeys,
|
|
736
|
-
hotTemplates: runtime.hotTemplates,
|
|
737
|
-
hotFineKeysActive: runtime.hotFineKeysActive,
|
|
738
|
-
hotFineKeysGrace: runtime.hotFineKeysGrace,
|
|
739
|
-
hotTemplatesActive: runtime.hotTemplatesActive,
|
|
740
|
-
hotTemplatesGrace: runtime.hotTemplatesGrace,
|
|
741
|
-
fineWaitersActive: runtime.fineWaitersActive,
|
|
742
|
-
coarseWaitersActive: runtime.coarseWaitersActive,
|
|
743
|
-
broadFineWaitersActive: runtime.broadFineWaitersActive,
|
|
744
|
-
hotKeyFilteringEnabled: runtime.hotKeyFilteringEnabled,
|
|
745
|
-
hotTemplateFilteringEnabled: runtime.hotTemplateFilteringEnabled,
|
|
746
|
-
scanRowsTotal: runtime.scanRowsTotal,
|
|
747
|
-
scanBatchesTotal: runtime.scanBatchesTotal,
|
|
748
|
-
scannedButEmitted0BatchesTotal: runtime.scannedButEmitted0BatchesTotal,
|
|
749
|
-
interpretedThroughDeltaTotal: runtime.interpretedThroughDeltaTotal,
|
|
750
|
-
touchesEmittedTotal: runtime.touchesEmittedTotal,
|
|
751
|
-
touchesTableTotal: runtime.touchesTableTotal,
|
|
752
|
-
touchesTemplateTotal: runtime.touchesTemplateTotal,
|
|
753
|
-
fineTouchesDroppedDueToBudgetTotal: runtime.fineTouchesDroppedDueToBudgetTotal,
|
|
754
|
-
fineTouchesSkippedColdTemplateTotal: runtime.fineTouchesSkippedColdTemplateTotal,
|
|
755
|
-
fineTouchesSkippedColdKeyTotal: runtime.fineTouchesSkippedColdKeyTotal,
|
|
756
|
-
fineTouchesSkippedTemplateBucketTotal: runtime.fineTouchesSkippedTemplateBucketTotal,
|
|
757
|
-
waitTouchedTotal: runtime.waitTouchedTotal,
|
|
758
|
-
waitTimeoutTotal: runtime.waitTimeoutTotal,
|
|
759
|
-
waitStaleTotal: runtime.waitStaleTotal,
|
|
760
|
-
journalFlushesTotal: runtime.journalFlushesTotal,
|
|
761
|
-
journalNotifyWakeupsTotal: runtime.journalNotifyWakeupsTotal,
|
|
762
|
-
journalNotifyWakeMsTotal: runtime.journalNotifyWakeMsTotal,
|
|
763
|
-
journalNotifyWakeMsMax: runtime.journalNotifyWakeMsMax,
|
|
764
|
-
journalTimeoutsFiredTotal: runtime.journalTimeoutsFiredTotal,
|
|
765
|
-
journalTimeoutSweepMsTotal: runtime.journalTimeoutSweepMsTotal,
|
|
766
|
-
});
|
|
767
1787
|
}
|
|
768
1788
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
return badRequest("wait body must be valid JSON");
|
|
777
|
-
}
|
|
778
|
-
const keysRaw = body?.keys;
|
|
779
|
-
const cursorRaw = body?.cursor;
|
|
780
|
-
const timeoutMsRaw = body?.timeoutMs;
|
|
781
|
-
if (keysRaw !== undefined && (!Array.isArray(keysRaw) || !keysRaw.every((k: any) => typeof k === "string" && k.trim() !== ""))) {
|
|
782
|
-
return badRequest("wait.keys must be a non-empty string array when provided");
|
|
783
|
-
}
|
|
784
|
-
const keys = Array.isArray(keysRaw) ? Array.from(new Set(keysRaw.map((k: string) => k.trim()))) : [];
|
|
785
|
-
if (keys.length > 1024) return badRequest("wait.keys too large (max 1024)");
|
|
786
|
-
const keyIdsRaw = body?.keyIds;
|
|
787
|
-
const keyIds =
|
|
788
|
-
Array.isArray(keyIdsRaw) && keyIdsRaw.length > 0
|
|
789
|
-
? Array.from(
|
|
790
|
-
new Set(
|
|
791
|
-
keyIdsRaw.map((x: any) => Number(x)).filter((n: number) => Number.isFinite(n) && Number.isInteger(n) && n >= 0 && n <= 0xffffffff)
|
|
792
|
-
)
|
|
793
|
-
).map((n) => n >>> 0)
|
|
794
|
-
: [];
|
|
795
|
-
if (Array.isArray(keyIdsRaw) && keyIds.length !== keyIdsRaw.length) {
|
|
796
|
-
return badRequest("wait.keyIds must be a non-empty uint32 array when provided");
|
|
797
|
-
}
|
|
798
|
-
if (keys.length === 0 && keyIds.length === 0) return badRequest("wait requires keys or keyIds");
|
|
799
|
-
if (keyIds.length > 1024) return badRequest("wait.keyIds too large (max 1024)");
|
|
800
|
-
if (typeof cursorRaw !== "string" || cursorRaw.trim() === "") return badRequest("wait.cursor must be a non-empty string");
|
|
801
|
-
const cursor = cursorRaw.trim();
|
|
802
|
-
|
|
803
|
-
const timeoutMs =
|
|
804
|
-
timeoutMsRaw === undefined ? 30_000 : typeof timeoutMsRaw === "number" && Number.isFinite(timeoutMsRaw) ? Math.max(0, Math.min(120_000, timeoutMsRaw)) : null;
|
|
805
|
-
if (timeoutMs == null) return badRequest("wait.timeoutMs must be a number (ms)");
|
|
806
|
-
|
|
807
|
-
const templateIdsUsedRaw = body?.templateIdsUsed;
|
|
808
|
-
if (Array.isArray(templateIdsUsedRaw) && !templateIdsUsedRaw.every((x: any) => typeof x === "string" && x.trim() !== "")) {
|
|
809
|
-
return badRequest("wait.templateIdsUsed must be a string array");
|
|
810
|
-
}
|
|
811
|
-
const templateIdsUsed =
|
|
812
|
-
Array.isArray(templateIdsUsedRaw) && templateIdsUsedRaw.length > 0
|
|
813
|
-
? Array.from(new Set(templateIdsUsedRaw.map((s: any) => (typeof s === "string" ? s.trim() : "")).filter((s: string) => s !== "")))
|
|
814
|
-
: [];
|
|
815
|
-
const interestModeRaw = body?.interestMode;
|
|
816
|
-
if (interestModeRaw !== undefined && interestModeRaw !== "fine" && interestModeRaw !== "coarse") {
|
|
817
|
-
return badRequest("wait.interestMode must be 'fine' or 'coarse'");
|
|
1789
|
+
const loadSnapshot = (): Response | DetailsSnapshot => {
|
|
1790
|
+
const snapshotRes = buildDetailsSnapshotResult(stream, isIndexStatus ? "index_status" : "details");
|
|
1791
|
+
if (Result.isError(snapshotRes)) {
|
|
1792
|
+
if (snapshotRes.error.status === 404) {
|
|
1793
|
+
return snapshotRes.error.message === "stream expired" ? notFound("stream expired") : notFound();
|
|
1794
|
+
}
|
|
1795
|
+
return internalError(snapshotRes.error.message);
|
|
818
1796
|
}
|
|
819
|
-
|
|
1797
|
+
return snapshotRes.value;
|
|
1798
|
+
};
|
|
820
1799
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1800
|
+
let snapshotOrResponse = loadSnapshot();
|
|
1801
|
+
if (snapshotOrResponse instanceof Response) return snapshotOrResponse;
|
|
1802
|
+
let snapshot = snapshotOrResponse;
|
|
1803
|
+
const ifNoneMatch = req.headers.get("if-none-match");
|
|
824
1804
|
|
|
825
|
-
|
|
826
|
-
if (
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
ttlRaw === undefined
|
|
831
|
-
? touchCfg.templates?.defaultInactivityTtlMs ?? 60 * 60 * 1000
|
|
832
|
-
: typeof ttlRaw === "number" && Number.isFinite(ttlRaw) && ttlRaw >= 0
|
|
833
|
-
? Math.floor(ttlRaw)
|
|
834
|
-
: null;
|
|
835
|
-
if (inactivityTtlMs == null) return badRequest("wait.inactivityTtlMs must be a non-negative number (ms)");
|
|
836
|
-
|
|
837
|
-
const templates: Array<{ entity: string; fields: Array<{ name: string; encoding: any }> }> = [];
|
|
838
|
-
for (const t of declareTemplatesRaw) {
|
|
839
|
-
const entity = typeof t?.entity === "string" ? t.entity.trim() : "";
|
|
840
|
-
const fieldsRaw = t?.fields;
|
|
841
|
-
if (entity === "" || !Array.isArray(fieldsRaw) || fieldsRaw.length === 0 || fieldsRaw.length > 3) continue;
|
|
842
|
-
const fields: Array<{ name: string; encoding: any }> = [];
|
|
843
|
-
for (const f of fieldsRaw) {
|
|
844
|
-
const name = typeof f?.name === "string" ? f.name.trim() : "";
|
|
845
|
-
const encoding = f?.encoding;
|
|
846
|
-
if (name === "") continue;
|
|
847
|
-
fields.push({ name, encoding });
|
|
848
|
-
}
|
|
849
|
-
if (fields.length !== fieldsRaw.length) continue;
|
|
850
|
-
templates.push({ entity, fields });
|
|
851
|
-
}
|
|
852
|
-
if (templates.length !== declareTemplatesRaw.length) return badRequest("wait.declareTemplates contains invalid template definitions");
|
|
853
|
-
const activeFromTouchOffset = touch.getOrCreateJournal(stream, touchCfg).getCursor();
|
|
854
|
-
touch.activateTemplates({
|
|
855
|
-
stream,
|
|
856
|
-
touchCfg,
|
|
857
|
-
baseStreamNextOffset: srow.next_offset,
|
|
858
|
-
activeFromTouchOffset,
|
|
859
|
-
templates,
|
|
860
|
-
inactivityTtlMs,
|
|
1805
|
+
if (!longPoll) {
|
|
1806
|
+
if (ifNoneMatch && ifNoneMatch === snapshot.etag) {
|
|
1807
|
+
return new Response(null, {
|
|
1808
|
+
status: 304,
|
|
1809
|
+
headers: withNosniff({ "cache-control": "no-store", etag: snapshot.etag }),
|
|
861
1810
|
});
|
|
862
1811
|
}
|
|
1812
|
+
return new Response(snapshot.body, {
|
|
1813
|
+
status: 200,
|
|
1814
|
+
headers: withNosniff({
|
|
1815
|
+
"content-type": "application/json; charset=utf-8",
|
|
1816
|
+
"cache-control": "no-store",
|
|
1817
|
+
etag: snapshot.etag,
|
|
1818
|
+
}),
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
863
1821
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
rawFineKeyIds = parsedKeyIds;
|
|
875
|
-
}
|
|
876
|
-
const templateWaitKeyIds =
|
|
877
|
-
templateIdsUsed.length > 0
|
|
878
|
-
? Array.from(new Set(templateIdsUsed.map((templateId) => templateKeyIdFor(templateId) >>> 0)))
|
|
879
|
-
: [];
|
|
880
|
-
let waitKeyIds = rawFineKeyIds;
|
|
881
|
-
let effectiveWaitKind: "fineKey" | "templateKey" | "tableKey" = "fineKey";
|
|
882
|
-
|
|
883
|
-
if (interestMode === "coarse") {
|
|
884
|
-
effectiveWaitKind = "tableKey";
|
|
885
|
-
} else if (runtime.touchMode === "restricted" && templateIdsUsed.length > 0) {
|
|
886
|
-
effectiveWaitKind = "templateKey";
|
|
887
|
-
} else if (runtime.touchMode === "coarseOnly" && templateIdsUsed.length > 0) {
|
|
888
|
-
effectiveWaitKind = "tableKey";
|
|
889
|
-
}
|
|
1822
|
+
if (!ifNoneMatch || ifNoneMatch !== snapshot.etag) {
|
|
1823
|
+
return new Response(snapshot.body, {
|
|
1824
|
+
status: 200,
|
|
1825
|
+
headers: withNosniff({
|
|
1826
|
+
"content-type": "application/json; charset=utf-8",
|
|
1827
|
+
"cache-control": "no-store",
|
|
1828
|
+
etag: snapshot.etag,
|
|
1829
|
+
}),
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
890
1832
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1833
|
+
const deadline = Date.now() + (timeoutMs ?? 3000);
|
|
1834
|
+
while (ifNoneMatch === snapshot.etag) {
|
|
1835
|
+
const remaining = deadline - Date.now();
|
|
1836
|
+
if (remaining <= 0) {
|
|
1837
|
+
return new Response(null, {
|
|
1838
|
+
status: 304,
|
|
1839
|
+
headers: withNosniff({ "cache-control": "no-store", etag: snapshot.etag }),
|
|
1840
|
+
});
|
|
896
1841
|
}
|
|
1842
|
+
await notifier.waitForDetailsChange(stream, snapshot.version, remaining, req.signal);
|
|
1843
|
+
if (req.signal.aborted) return new Response(null, { status: 204 });
|
|
1844
|
+
snapshotOrResponse = loadSnapshot();
|
|
1845
|
+
if (snapshotOrResponse instanceof Response) return snapshotOrResponse;
|
|
1846
|
+
snapshot = snapshotOrResponse;
|
|
1847
|
+
}
|
|
897
1848
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
}
|
|
1849
|
+
return new Response(snapshot.body, {
|
|
1850
|
+
status: 200,
|
|
1851
|
+
headers: withNosniff({
|
|
1852
|
+
"content-type": "application/json; charset=utf-8",
|
|
1853
|
+
"cache-control": "no-store",
|
|
1854
|
+
etag: snapshot.etag,
|
|
1855
|
+
}),
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
908
1858
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1859
|
+
if (isRoutingKeys) {
|
|
1860
|
+
const srow = db.getStream(stream);
|
|
1861
|
+
if (!srow || db.isDeleted(srow)) return notFound();
|
|
1862
|
+
if (srow.expires_at_ms != null && db.nowMs() > srow.expires_at_ms) return notFound("stream expired");
|
|
1863
|
+
if (req.method !== "GET") return badRequest("unsupported method");
|
|
1864
|
+
const regRes = registry.getRegistryResult(stream);
|
|
1865
|
+
if (Result.isError(regRes)) return internalError();
|
|
1866
|
+
if (regRes.value.routingKey == null) return badRequest("routing key not configured");
|
|
1867
|
+
const limitRaw = url.searchParams.get("limit");
|
|
1868
|
+
const limit = limitRaw == null ? 100 : Number(limitRaw);
|
|
1869
|
+
if (!Number.isFinite(limit) || limit <= 0 || !Number.isInteger(limit)) return badRequest("invalid limit");
|
|
1870
|
+
const after = url.searchParams.get("after");
|
|
1871
|
+
const listRes = indexer?.listRoutingKeysResult
|
|
1872
|
+
? await runForeground(() => indexer.listRoutingKeysResult!(stream, after, limit))
|
|
1873
|
+
: Result.err({ kind: "invalid_lexicon_index", message: "routing key lexicon unavailable" });
|
|
1874
|
+
if (Result.isError(listRes)) return internalError(listRes.error.message);
|
|
1875
|
+
return json(200, {
|
|
1876
|
+
stream,
|
|
1877
|
+
source: {
|
|
1878
|
+
kind: "routing_key",
|
|
1879
|
+
name: "",
|
|
1880
|
+
},
|
|
1881
|
+
took_ms: listRes.value.tookMs,
|
|
1882
|
+
coverage: {
|
|
1883
|
+
complete: listRes.value.coverage.complete,
|
|
1884
|
+
indexed_segments: listRes.value.coverage.indexedSegments,
|
|
1885
|
+
scanned_uploaded_segments: listRes.value.coverage.scannedUploadedSegments,
|
|
1886
|
+
scanned_local_segments: listRes.value.coverage.scannedLocalSegments,
|
|
1887
|
+
scanned_wal_rows: listRes.value.coverage.scannedWalRows,
|
|
1888
|
+
possible_missing_uploaded_segments: listRes.value.coverage.possibleMissingUploadedSegments,
|
|
1889
|
+
possible_missing_local_segments: listRes.value.coverage.possibleMissingLocalSegments,
|
|
1890
|
+
},
|
|
1891
|
+
timing: {
|
|
1892
|
+
lexicon_run_get_ms: listRes.value.timing.lexiconRunGetMs,
|
|
1893
|
+
lexicon_decode_ms: listRes.value.timing.lexiconDecodeMs,
|
|
1894
|
+
lexicon_enumerate_ms: listRes.value.timing.lexiconEnumerateMs,
|
|
1895
|
+
lexicon_merge_ms: listRes.value.timing.lexiconMergeMs,
|
|
1896
|
+
fallback_scan_ms: listRes.value.timing.fallbackScanMs,
|
|
1897
|
+
fallback_segment_get_ms: listRes.value.timing.fallbackSegmentGetMs,
|
|
1898
|
+
fallback_wal_scan_ms: listRes.value.timing.fallbackWalScanMs,
|
|
1899
|
+
lexicon_runs_loaded: listRes.value.timing.lexiconRunsLoaded,
|
|
1900
|
+
},
|
|
1901
|
+
keys: listRes.value.keys,
|
|
1902
|
+
next_after: listRes.value.nextAfter,
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
if (isSearch) {
|
|
1907
|
+
const srow = db.getStream(stream);
|
|
1908
|
+
if (!srow || db.isDeleted(srow)) return notFound();
|
|
1909
|
+
if (srow.expires_at_ms != null && db.nowMs() > srow.expires_at_ms) return notFound("stream expired");
|
|
1910
|
+
|
|
1911
|
+
const regRes = registry.getRegistryResult(stream);
|
|
1912
|
+
if (Result.isError(regRes)) return internalError();
|
|
1913
|
+
|
|
1914
|
+
const respondSearch = async (requestBody: unknown, fromQuery: boolean): Promise<Response> => {
|
|
1915
|
+
const requestRes = fromQuery
|
|
1916
|
+
? parseSearchRequestQueryResult(regRes.value, url.searchParams)
|
|
1917
|
+
: parseSearchRequestBodyResult(regRes.value, requestBody);
|
|
1918
|
+
if (Result.isError(requestRes)) return badRequest(requestRes.error.message);
|
|
1919
|
+
const request = {
|
|
1920
|
+
...requestRes.value,
|
|
1921
|
+
timeoutMs: clampSearchRequestTimeoutMs(requestRes.value.timeoutMs),
|
|
1922
|
+
};
|
|
1923
|
+
const searchRes = await runForegroundWithGate(searchGate, () => reader.searchResult({ stream, request }));
|
|
1924
|
+
if (Result.isError(searchRes)) return readerErrorResponse(searchRes.error);
|
|
1925
|
+
const status = searchRes.value.timedOut ? 408 : 200;
|
|
1926
|
+
return json(status, {
|
|
915
1927
|
stream,
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1928
|
+
snapshot_end_offset: searchRes.value.snapshotEndOffset,
|
|
1929
|
+
took_ms: searchRes.value.tookMs,
|
|
1930
|
+
timed_out: searchRes.value.timedOut,
|
|
1931
|
+
timeout_ms: searchRes.value.timeoutMs,
|
|
1932
|
+
coverage: {
|
|
1933
|
+
mode: searchRes.value.coverage.mode,
|
|
1934
|
+
complete: searchRes.value.coverage.complete,
|
|
1935
|
+
stream_head_offset: searchRes.value.coverage.streamHeadOffset,
|
|
1936
|
+
visible_through_offset: searchRes.value.coverage.visibleThroughOffset,
|
|
1937
|
+
visible_through_primary_timestamp_max: searchRes.value.coverage.visibleThroughPrimaryTimestampMax,
|
|
1938
|
+
oldest_omitted_append_at: searchRes.value.coverage.oldestOmittedAppendAt,
|
|
1939
|
+
possible_missing_events_upper_bound: searchRes.value.coverage.possibleMissingEventsUpperBound,
|
|
1940
|
+
possible_missing_uploaded_segments: searchRes.value.coverage.possibleMissingUploadedSegments,
|
|
1941
|
+
possible_missing_sealed_rows: searchRes.value.coverage.possibleMissingSealedRows,
|
|
1942
|
+
possible_missing_wal_rows: searchRes.value.coverage.possibleMissingWalRows,
|
|
1943
|
+
indexed_segments: searchRes.value.coverage.indexedSegments,
|
|
1944
|
+
indexed_segment_time_ms: searchRes.value.coverage.indexedSegmentTimeMs,
|
|
1945
|
+
fts_section_get_ms: searchRes.value.coverage.ftsSectionGetMs,
|
|
1946
|
+
fts_decode_ms: searchRes.value.coverage.ftsDecodeMs,
|
|
1947
|
+
fts_clause_estimate_ms: searchRes.value.coverage.ftsClauseEstimateMs,
|
|
1948
|
+
scanned_segments: searchRes.value.coverage.scannedSegments,
|
|
1949
|
+
scanned_segment_time_ms: searchRes.value.coverage.scannedSegmentTimeMs,
|
|
1950
|
+
scanned_tail_docs: searchRes.value.coverage.scannedTailDocs,
|
|
1951
|
+
scanned_tail_time_ms: searchRes.value.coverage.scannedTailTimeMs,
|
|
1952
|
+
exact_candidate_time_ms: searchRes.value.coverage.exactCandidateTimeMs,
|
|
1953
|
+
index_families_used: searchRes.value.coverage.indexFamiliesUsed,
|
|
1954
|
+
},
|
|
1955
|
+
total: searchRes.value.total,
|
|
1956
|
+
hits: searchRes.value.hits,
|
|
1957
|
+
next_search_after: searchRes.value.nextSearchAfter,
|
|
1958
|
+
}, searchResponseHeaders(searchRes.value));
|
|
1959
|
+
};
|
|
945
1960
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
if (j.maybeTouchedSinceAny(waitKeyIds, sinceGen)) {
|
|
950
|
-
const latencyMs = Date.now() - waitStartMs;
|
|
951
|
-
touch.recordWaitMetrics({ stream, touchCfg, keysCount: waitKeyIds.length, outcome: "touched", latencyMs });
|
|
952
|
-
return json(200, {
|
|
953
|
-
touched: true,
|
|
954
|
-
cursor: j.getCursor(),
|
|
955
|
-
effectiveWaitKind,
|
|
956
|
-
bucketMaxSourceOffsetSeq: j.getLastFlushedSourceOffsetSeq().toString(),
|
|
957
|
-
flushAtMs: j.getLastFlushAtMs(),
|
|
958
|
-
bucketStartMs: j.getLastBucketStartMs(),
|
|
959
|
-
});
|
|
960
|
-
}
|
|
1961
|
+
if (req.method === "GET") {
|
|
1962
|
+
return respondSearch(null, true);
|
|
1963
|
+
}
|
|
961
1964
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
bucketMaxSourceOffsetSeq: j.getLastFlushedSourceOffsetSeq().toString(),
|
|
972
|
-
flushAtMs: j.getLastFlushAtMs(),
|
|
973
|
-
bucketStartMs: j.getLastBucketStartMs(),
|
|
974
|
-
});
|
|
975
|
-
}
|
|
1965
|
+
if (req.method === "POST") {
|
|
1966
|
+
let body: unknown;
|
|
1967
|
+
try {
|
|
1968
|
+
body = await req.json();
|
|
1969
|
+
} catch {
|
|
1970
|
+
return badRequest("search request must be valid JSON");
|
|
1971
|
+
}
|
|
1972
|
+
return respondSearch(body, false);
|
|
1973
|
+
}
|
|
976
1974
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
if (req.signal.aborted) return new Response(null, { status: 204 });
|
|
1975
|
+
return badRequest("unsupported method");
|
|
1976
|
+
}
|
|
980
1977
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
cursor: j.getCursor(),
|
|
987
|
-
effectiveWaitKind,
|
|
988
|
-
bucketMaxSourceOffsetSeq: j.getLastFlushedSourceOffsetSeq().toString(),
|
|
989
|
-
flushAtMs: j.getLastFlushAtMs(),
|
|
990
|
-
bucketStartMs: j.getLastBucketStartMs(),
|
|
991
|
-
});
|
|
992
|
-
}
|
|
1978
|
+
if (isAggregate) {
|
|
1979
|
+
const srow = db.getStream(stream);
|
|
1980
|
+
if (!srow || db.isDeleted(srow)) return notFound();
|
|
1981
|
+
if (srow.expires_at_ms != null && db.nowMs() > srow.expires_at_ms) return notFound("stream expired");
|
|
1982
|
+
if (req.method !== "POST") return badRequest("unsupported method");
|
|
993
1983
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
bucketStartMs: hit.bucketStartMs,
|
|
1003
|
-
});
|
|
1004
|
-
} finally {
|
|
1005
|
-
releaseHotInterest();
|
|
1006
|
-
}
|
|
1984
|
+
const regRes = registry.getRegistryResult(stream);
|
|
1985
|
+
if (Result.isError(regRes)) return internalError();
|
|
1986
|
+
|
|
1987
|
+
let body: unknown;
|
|
1988
|
+
try {
|
|
1989
|
+
body = await req.json();
|
|
1990
|
+
} catch {
|
|
1991
|
+
return badRequest("aggregate request must be valid JSON");
|
|
1007
1992
|
}
|
|
1993
|
+
|
|
1994
|
+
const requestRes = parseAggregateRequestBodyResult(regRes.value, body);
|
|
1995
|
+
if (Result.isError(requestRes)) return badRequest(requestRes.error.message);
|
|
1996
|
+
const aggregateRes = await runForegroundWithGate(searchGate, () => reader.aggregateResult({ stream, request: requestRes.value }));
|
|
1997
|
+
if (Result.isError(aggregateRes)) return readerErrorResponse(aggregateRes.error);
|
|
1998
|
+
return json(200, {
|
|
1999
|
+
stream,
|
|
2000
|
+
rollup: aggregateRes.value.rollup,
|
|
2001
|
+
from: aggregateRes.value.from,
|
|
2002
|
+
to: aggregateRes.value.to,
|
|
2003
|
+
interval: aggregateRes.value.interval,
|
|
2004
|
+
coverage: {
|
|
2005
|
+
mode: aggregateRes.value.coverage.mode,
|
|
2006
|
+
complete: aggregateRes.value.coverage.complete,
|
|
2007
|
+
stream_head_offset: aggregateRes.value.coverage.streamHeadOffset,
|
|
2008
|
+
visible_through_offset: aggregateRes.value.coverage.visibleThroughOffset,
|
|
2009
|
+
visible_through_primary_timestamp_max: aggregateRes.value.coverage.visibleThroughPrimaryTimestampMax,
|
|
2010
|
+
oldest_omitted_append_at: aggregateRes.value.coverage.oldestOmittedAppendAt,
|
|
2011
|
+
possible_missing_events_upper_bound: aggregateRes.value.coverage.possibleMissingEventsUpperBound,
|
|
2012
|
+
possible_missing_uploaded_segments: aggregateRes.value.coverage.possibleMissingUploadedSegments,
|
|
2013
|
+
possible_missing_sealed_rows: aggregateRes.value.coverage.possibleMissingSealedRows,
|
|
2014
|
+
possible_missing_wal_rows: aggregateRes.value.coverage.possibleMissingWalRows,
|
|
2015
|
+
used_rollups: aggregateRes.value.coverage.usedRollups,
|
|
2016
|
+
indexed_segments: aggregateRes.value.coverage.indexedSegments,
|
|
2017
|
+
scanned_segments: aggregateRes.value.coverage.scannedSegments,
|
|
2018
|
+
scanned_tail_docs: aggregateRes.value.coverage.scannedTailDocs,
|
|
2019
|
+
index_families_used: aggregateRes.value.coverage.indexFamiliesUsed,
|
|
2020
|
+
},
|
|
2021
|
+
buckets: aggregateRes.value.buckets,
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
if (touchMode) {
|
|
2026
|
+
const srow = db.getStream(stream);
|
|
2027
|
+
if (!srow || db.isDeleted(srow)) return notFound();
|
|
2028
|
+
if (srow.expires_at_ms != null && db.nowMs() > srow.expires_at_ms) return notFound("stream expired");
|
|
2029
|
+
|
|
2030
|
+
const profileRes = profiles.getProfileResult(stream, srow);
|
|
2031
|
+
if (Result.isError(profileRes)) return internalError("invalid stream profile");
|
|
2032
|
+
const touchCapability = resolveTouchCapability(profileRes.value);
|
|
2033
|
+
if (!touchCapability?.handleRoute) return notFound("touch not enabled");
|
|
2034
|
+
return touchCapability.handleRoute({
|
|
2035
|
+
route: touchMode,
|
|
2036
|
+
req,
|
|
2037
|
+
stream,
|
|
2038
|
+
streamRow: srow,
|
|
2039
|
+
profile: profileRes.value,
|
|
2040
|
+
db,
|
|
2041
|
+
touchManager: touch,
|
|
2042
|
+
respond: { json, badRequest, internalError, notFound },
|
|
2043
|
+
});
|
|
1008
2044
|
}
|
|
1009
2045
|
|
|
1010
2046
|
// Stream lifecycle.
|
|
@@ -1029,115 +2065,130 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1029
2065
|
|
|
1030
2066
|
const contentType = normalizeContentType(req.headers.get("content-type")) ?? "application/octet-stream";
|
|
1031
2067
|
const routingKeyHeader = req.headers.get("stream-key");
|
|
2068
|
+
const leaveAppendPhase = memorySampler?.enter("append", {
|
|
2069
|
+
route: "put",
|
|
2070
|
+
stream,
|
|
2071
|
+
content_type: contentType,
|
|
2072
|
+
});
|
|
2073
|
+
try {
|
|
2074
|
+
return await runWithGate(ingestGate, async () => {
|
|
2075
|
+
const ab = await req.arrayBuffer();
|
|
2076
|
+
if (ab.byteLength > cfg.appendMaxBodyBytes) return tooLarge(`body too large (max ${cfg.appendMaxBodyBytes})`);
|
|
2077
|
+
const bodyBytes = new Uint8Array(ab);
|
|
2078
|
+
|
|
2079
|
+
let srow = db.getStream(stream);
|
|
2080
|
+
if (srow && db.isDeleted(srow)) {
|
|
2081
|
+
db.hardDeleteStream(stream);
|
|
2082
|
+
srow = null;
|
|
2083
|
+
}
|
|
2084
|
+
if (srow && srow.expires_at_ms != null && db.nowMs() > srow.expires_at_ms) {
|
|
2085
|
+
db.hardDeleteStream(stream);
|
|
2086
|
+
srow = null;
|
|
2087
|
+
}
|
|
1032
2088
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
db.hardDeleteStream(stream);
|
|
1046
|
-
srow = null;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
if (srow) {
|
|
1050
|
-
const existingClosed = srow.closed !== 0;
|
|
1051
|
-
const existingContentType = normalizeContentType(srow.content_type) ?? srow.content_type;
|
|
1052
|
-
const ttlMatch =
|
|
1053
|
-
ttlSeconds != null
|
|
1054
|
-
? srow.ttl_seconds != null && srow.ttl_seconds === ttlSeconds
|
|
1055
|
-
: expiresAtMs != null
|
|
1056
|
-
? srow.ttl_seconds == null && srow.expires_at_ms != null && srow.expires_at_ms === expiresAtMs
|
|
1057
|
-
: srow.ttl_seconds == null && srow.expires_at_ms == null;
|
|
1058
|
-
if (existingContentType !== contentType || existingClosed !== streamClosed || !ttlMatch) {
|
|
1059
|
-
return conflict("stream config mismatch");
|
|
1060
|
-
}
|
|
2089
|
+
if (srow) {
|
|
2090
|
+
const existingClosed = srow.closed !== 0;
|
|
2091
|
+
const existingContentType = normalizeContentType(srow.content_type) ?? srow.content_type;
|
|
2092
|
+
const ttlMatch =
|
|
2093
|
+
ttlSeconds != null
|
|
2094
|
+
? srow.ttl_seconds != null && srow.ttl_seconds === ttlSeconds
|
|
2095
|
+
: expiresAtMs != null
|
|
2096
|
+
? srow.ttl_seconds == null && srow.expires_at_ms != null && srow.expires_at_ms === expiresAtMs
|
|
2097
|
+
: srow.ttl_seconds == null && srow.expires_at_ms == null;
|
|
2098
|
+
if (existingContentType !== contentType || existingClosed !== streamClosed || !ttlMatch) {
|
|
2099
|
+
return conflict("stream config mismatch");
|
|
2100
|
+
}
|
|
1061
2101
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
2102
|
+
const tailOffset = encodeOffset(srow.epoch, srow.next_offset - 1n);
|
|
2103
|
+
const headers: Record<string, string> = {
|
|
2104
|
+
"content-type": existingContentType,
|
|
2105
|
+
"stream-next-offset": tailOffset,
|
|
2106
|
+
};
|
|
2107
|
+
if (existingClosed) headers["stream-closed"] = "true";
|
|
2108
|
+
if (srow.expires_at_ms != null) headers["stream-expires-at"] = new Date(Number(srow.expires_at_ms)).toISOString();
|
|
2109
|
+
return new Response(null, { status: 200, headers: withNosniff(headers) });
|
|
2110
|
+
}
|
|
1071
2111
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
2112
|
+
db.ensureStream(stream, { contentType, expiresAtMs, ttlSeconds, closed: false });
|
|
2113
|
+
notifier.notifyDetailsChanged(stream);
|
|
2114
|
+
let lastOffset = -1n;
|
|
2115
|
+
let appendedRows = 0;
|
|
2116
|
+
let closedNow = false;
|
|
2117
|
+
|
|
2118
|
+
if (bodyBytes.byteLength > 0) {
|
|
2119
|
+
const rowsRes = buildAppendRowsResult(stream, bodyBytes, contentType, routingKeyHeader, true);
|
|
2120
|
+
if (Result.isError(rowsRes)) {
|
|
2121
|
+
if (rowsRes.error.status === 500) return internalError();
|
|
2122
|
+
return badRequest(rowsRes.error.message);
|
|
2123
|
+
}
|
|
2124
|
+
const rows = rowsRes.value.rows;
|
|
2125
|
+
appendedRows = rows.length;
|
|
2126
|
+
if (rows.length > 0 || streamClosed) {
|
|
2127
|
+
const appendResOrResponse = await awaitAppendWithTimeout(enqueueAppend({
|
|
2128
|
+
stream,
|
|
2129
|
+
baseAppendMs: db.nowMs(),
|
|
2130
|
+
rows,
|
|
2131
|
+
contentType,
|
|
2132
|
+
close: streamClosed,
|
|
2133
|
+
}));
|
|
2134
|
+
if (appendResOrResponse instanceof Response) return appendResOrResponse;
|
|
2135
|
+
const appendRes = appendResOrResponse;
|
|
2136
|
+
if (Result.isError(appendRes)) {
|
|
2137
|
+
if (appendRes.error.kind === "overloaded") return overloaded();
|
|
2138
|
+
return json(500, { error: { code: "internal", message: "append failed" } });
|
|
2139
|
+
}
|
|
2140
|
+
lastOffset = appendRes.value.lastOffset;
|
|
2141
|
+
closedNow = appendRes.value.closed;
|
|
2142
|
+
}
|
|
2143
|
+
} else if (streamClosed) {
|
|
2144
|
+
const appendResOrResponse = await awaitAppendWithTimeout(enqueueAppend({
|
|
2145
|
+
stream,
|
|
2146
|
+
baseAppendMs: db.nowMs(),
|
|
2147
|
+
rows: [],
|
|
2148
|
+
contentType,
|
|
2149
|
+
close: true,
|
|
2150
|
+
}));
|
|
2151
|
+
if (appendResOrResponse instanceof Response) return appendResOrResponse;
|
|
2152
|
+
const appendRes = appendResOrResponse;
|
|
2153
|
+
if (Result.isError(appendRes)) {
|
|
2154
|
+
if (appendRes.error.kind === "overloaded") return overloaded();
|
|
2155
|
+
return json(500, { error: { code: "internal", message: "close failed" } });
|
|
2156
|
+
}
|
|
2157
|
+
lastOffset = appendRes.value.lastOffset;
|
|
2158
|
+
closedNow = appendRes.value.closed;
|
|
2159
|
+
}
|
|
1076
2160
|
|
|
1077
|
-
|
|
1078
|
-
const rowsRes = buildAppendRowsResult(stream, bodyBytes, contentType, routingKeyHeader, true);
|
|
1079
|
-
if (Result.isError(rowsRes)) {
|
|
1080
|
-
if (rowsRes.error.status === 500) return internalError();
|
|
1081
|
-
return badRequest(rowsRes.error.message);
|
|
1082
|
-
}
|
|
1083
|
-
const rows = rowsRes.value.rows;
|
|
1084
|
-
appendedRows = rows.length;
|
|
1085
|
-
if (rows.length > 0 || streamClosed) {
|
|
1086
|
-
const appendRes = await enqueueAppend({
|
|
2161
|
+
recordAppendOutcome({
|
|
1087
2162
|
stream,
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
2163
|
+
lastOffset,
|
|
2164
|
+
appendedRows,
|
|
2165
|
+
metricsBytes: bodyBytes.byteLength,
|
|
2166
|
+
ingestedBytes: bodyBytes.byteLength,
|
|
2167
|
+
touched: bodyBytes.byteLength > 0 || streamClosed,
|
|
2168
|
+
closed: closedNow,
|
|
1092
2169
|
});
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
stream
|
|
1103
|
-
|
|
1104
|
-
rows: [],
|
|
1105
|
-
contentType,
|
|
1106
|
-
close: true,
|
|
2170
|
+
|
|
2171
|
+
const createdRow = db.getStream(stream)!;
|
|
2172
|
+
const tailOffset = encodeOffset(createdRow.epoch, createdRow.next_offset - 1n);
|
|
2173
|
+
const headers: Record<string, string> = {
|
|
2174
|
+
"content-type": contentType,
|
|
2175
|
+
"stream-next-offset": appendedRows > 0 || streamClosed ? encodeOffset(createdRow.epoch, lastOffset) : tailOffset,
|
|
2176
|
+
location: req.url,
|
|
2177
|
+
};
|
|
2178
|
+
if (streamClosed || closedNow) headers["stream-closed"] = "true";
|
|
2179
|
+
if (createdRow.expires_at_ms != null) headers["stream-expires-at"] = new Date(Number(createdRow.expires_at_ms)).toISOString();
|
|
2180
|
+
return new Response(null, { status: 201, headers: withNosniff(headers) });
|
|
1107
2181
|
});
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
return json(500, { error: { code: "internal", message: "close failed" } });
|
|
1111
|
-
}
|
|
1112
|
-
lastOffset = appendRes.value.lastOffset;
|
|
1113
|
-
closedNow = appendRes.value.closed;
|
|
2182
|
+
} finally {
|
|
2183
|
+
leaveAppendPhase?.();
|
|
1114
2184
|
}
|
|
1115
|
-
|
|
1116
|
-
recordAppendOutcome({
|
|
1117
|
-
stream,
|
|
1118
|
-
lastOffset,
|
|
1119
|
-
appendedRows,
|
|
1120
|
-
metricsBytes: bodyBytes.byteLength,
|
|
1121
|
-
ingestedBytes: bodyBytes.byteLength,
|
|
1122
|
-
touched: bodyBytes.byteLength > 0 || streamClosed,
|
|
1123
|
-
closed: closedNow,
|
|
1124
|
-
});
|
|
1125
|
-
|
|
1126
|
-
const createdRow = db.getStream(stream)!;
|
|
1127
|
-
const tailOffset = encodeOffset(createdRow.epoch, createdRow.next_offset - 1n);
|
|
1128
|
-
const headers: Record<string, string> = {
|
|
1129
|
-
"content-type": contentType,
|
|
1130
|
-
"stream-next-offset": appendedRows > 0 || streamClosed ? encodeOffset(createdRow.epoch, lastOffset) : tailOffset,
|
|
1131
|
-
location: req.url,
|
|
1132
|
-
};
|
|
1133
|
-
if (streamClosed || closedNow) headers["stream-closed"] = "true";
|
|
1134
|
-
if (createdRow.expires_at_ms != null) headers["stream-expires-at"] = new Date(Number(createdRow.expires_at_ms)).toISOString();
|
|
1135
|
-
return new Response(null, { status: 201, headers: withNosniff(headers) });
|
|
1136
2185
|
}
|
|
1137
2186
|
|
|
1138
2187
|
if (req.method === "DELETE") {
|
|
1139
2188
|
const deleted = db.deleteStream(stream);
|
|
1140
2189
|
if (!deleted) return notFound();
|
|
2190
|
+
notifier.notifyDetailsChanged(stream);
|
|
2191
|
+
notifier.notifyClose(stream);
|
|
1141
2192
|
await uploader.publishManifest(stream);
|
|
1142
2193
|
return new Response(null, { status: 204, headers: withNosniff() });
|
|
1143
2194
|
}
|
|
@@ -1197,100 +2248,111 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1197
2248
|
baseAppendMs = tsRes.value;
|
|
1198
2249
|
}
|
|
1199
2250
|
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
const ab = await req.arrayBuffer();
|
|
1203
|
-
if (ab.byteLength > cfg.appendMaxBodyBytes) return tooLarge(`body too large (max ${cfg.appendMaxBodyBytes})`);
|
|
1204
|
-
const bodyBytes = new Uint8Array(ab);
|
|
1205
|
-
|
|
1206
|
-
const isCloseOnly = streamClosed && bodyBytes.byteLength === 0;
|
|
1207
|
-
if (bodyBytes.byteLength === 0 && !streamClosed) return badRequest("empty body");
|
|
1208
|
-
|
|
1209
|
-
let reqContentType = normalizeContentType(req.headers.get("content-type"));
|
|
1210
|
-
if (!isCloseOnly && !reqContentType) return badRequest("missing content-type");
|
|
1211
|
-
|
|
1212
|
-
const routingKeyHeader = req.headers.get("stream-key");
|
|
1213
|
-
let rows: AppendRow[] = [];
|
|
1214
|
-
if (!isCloseOnly) {
|
|
1215
|
-
const rowsRes = buildAppendRowsResult(stream, bodyBytes, reqContentType!, routingKeyHeader, false);
|
|
1216
|
-
if (Result.isError(rowsRes)) {
|
|
1217
|
-
if (rowsRes.error.status === 500) return internalError();
|
|
1218
|
-
return badRequest(rowsRes.error.message);
|
|
1219
|
-
}
|
|
1220
|
-
rows = rowsRes.value.rows;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
const appendRes = await enqueueAppend({
|
|
2251
|
+
const leaveAppendPhase = memorySampler?.enter("append", {
|
|
2252
|
+
route: "post",
|
|
1224
2253
|
stream,
|
|
1225
|
-
|
|
1226
|
-
rows,
|
|
1227
|
-
contentType: reqContentType ?? streamContentType,
|
|
1228
|
-
streamSeq,
|
|
1229
|
-
producer,
|
|
1230
|
-
close: streamClosed,
|
|
2254
|
+
stream_content_type: streamContentType,
|
|
1231
2255
|
});
|
|
2256
|
+
try {
|
|
2257
|
+
return await runWithGate(ingestGate, async () => {
|
|
2258
|
+
const ab = await req.arrayBuffer();
|
|
2259
|
+
if (ab.byteLength > cfg.appendMaxBodyBytes) return tooLarge(`body too large (max ${cfg.appendMaxBodyBytes})`);
|
|
2260
|
+
const bodyBytes = new Uint8Array(ab);
|
|
2261
|
+
|
|
2262
|
+
const isCloseOnly = streamClosed && bodyBytes.byteLength === 0;
|
|
2263
|
+
if (bodyBytes.byteLength === 0 && !streamClosed) return badRequest("empty body");
|
|
2264
|
+
|
|
2265
|
+
let reqContentType = normalizeContentType(req.headers.get("content-type"));
|
|
2266
|
+
if (!isCloseOnly && !reqContentType) return badRequest("missing content-type");
|
|
2267
|
+
|
|
2268
|
+
const routingKeyHeader = req.headers.get("stream-key");
|
|
2269
|
+
let rows: AppendRow[] = [];
|
|
2270
|
+
if (!isCloseOnly) {
|
|
2271
|
+
const rowsRes = buildAppendRowsResult(stream, bodyBytes, reqContentType!, routingKeyHeader, false);
|
|
2272
|
+
if (Result.isError(rowsRes)) {
|
|
2273
|
+
if (rowsRes.error.status === 500) return internalError();
|
|
2274
|
+
return badRequest(rowsRes.error.message);
|
|
2275
|
+
}
|
|
2276
|
+
rows = rowsRes.value.rows;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
const appendResOrResponse = await awaitAppendWithTimeout(enqueueAppend({
|
|
2280
|
+
stream,
|
|
2281
|
+
baseAppendMs,
|
|
2282
|
+
rows,
|
|
2283
|
+
contentType: reqContentType ?? streamContentType,
|
|
2284
|
+
streamSeq,
|
|
2285
|
+
producer,
|
|
2286
|
+
close: streamClosed,
|
|
2287
|
+
}));
|
|
2288
|
+
if (appendResOrResponse instanceof Response) return appendResOrResponse;
|
|
2289
|
+
const appendRes = appendResOrResponse;
|
|
1232
2290
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
2291
|
+
if (Result.isError(appendRes)) {
|
|
2292
|
+
const err = appendRes.error;
|
|
2293
|
+
if (err.kind === "overloaded") return overloaded();
|
|
2294
|
+
if (err.kind === "gone") return notFound("stream expired");
|
|
2295
|
+
if (err.kind === "not_found") return notFound();
|
|
2296
|
+
if (err.kind === "content_type_mismatch") return conflict("content-type mismatch");
|
|
2297
|
+
if (err.kind === "stream_seq") {
|
|
2298
|
+
return conflict("sequence mismatch", {
|
|
2299
|
+
"stream-expected-seq": err.expected,
|
|
2300
|
+
"stream-received-seq": err.received,
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
if (err.kind === "closed") {
|
|
2304
|
+
const headers: Record<string, string> = {
|
|
2305
|
+
"stream-next-offset": encodeOffset(srow.epoch, err.lastOffset),
|
|
2306
|
+
"stream-closed": "true",
|
|
2307
|
+
};
|
|
2308
|
+
return new Response(null, { status: 409, headers: withNosniff(headers) });
|
|
2309
|
+
}
|
|
2310
|
+
if (err.kind === "producer_stale_epoch") {
|
|
2311
|
+
return new Response(null, {
|
|
2312
|
+
status: 403,
|
|
2313
|
+
headers: withNosniff({ "producer-epoch": String(err.producerEpoch) }),
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
if (err.kind === "producer_gap") {
|
|
2317
|
+
return new Response(null, {
|
|
2318
|
+
status: 409,
|
|
2319
|
+
headers: withNosniff({
|
|
2320
|
+
"producer-expected-seq": String(err.expected),
|
|
2321
|
+
"producer-received-seq": String(err.received),
|
|
2322
|
+
}),
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
if (err.kind === "producer_epoch_seq") return badRequest("invalid producer sequence");
|
|
2326
|
+
return json(500, { error: { code: "internal", message: "append failed" } });
|
|
2327
|
+
}
|
|
2328
|
+
const res = appendRes.value;
|
|
2329
|
+
|
|
2330
|
+
const appendBytes = rows.reduce((acc, r) => acc + r.payload.byteLength, 0);
|
|
2331
|
+
recordAppendOutcome({
|
|
2332
|
+
stream,
|
|
2333
|
+
lastOffset: res.lastOffset,
|
|
2334
|
+
appendedRows: res.appendedRows,
|
|
2335
|
+
metricsBytes: appendBytes,
|
|
2336
|
+
ingestedBytes: bodyBytes.byteLength,
|
|
2337
|
+
touched: true,
|
|
2338
|
+
closed: res.closed,
|
|
1243
2339
|
});
|
|
1244
|
-
|
|
1245
|
-
if (err.kind === "closed") {
|
|
2340
|
+
|
|
1246
2341
|
const headers: Record<string, string> = {
|
|
1247
|
-
"stream-next-offset": encodeOffset(srow.epoch,
|
|
1248
|
-
"stream-closed": "true",
|
|
2342
|
+
"stream-next-offset": encodeOffset(srow.epoch, res.lastOffset),
|
|
1249
2343
|
};
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
headers: withNosniff({ "producer-epoch": String(err.producerEpoch) }),
|
|
1256
|
-
});
|
|
1257
|
-
}
|
|
1258
|
-
if (err.kind === "producer_gap") {
|
|
1259
|
-
return new Response(null, {
|
|
1260
|
-
status: 409,
|
|
1261
|
-
headers: withNosniff({
|
|
1262
|
-
"producer-expected-seq": String(err.expected),
|
|
1263
|
-
"producer-received-seq": String(err.received),
|
|
1264
|
-
}),
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
1267
|
-
if (err.kind === "producer_epoch_seq") return badRequest("invalid producer sequence");
|
|
1268
|
-
return json(500, { error: { code: "internal", message: "append failed" } });
|
|
1269
|
-
}
|
|
1270
|
-
const res = appendRes.value;
|
|
1271
|
-
|
|
1272
|
-
const appendBytes = rows.reduce((acc, r) => acc + r.payload.byteLength, 0);
|
|
1273
|
-
recordAppendOutcome({
|
|
1274
|
-
stream,
|
|
1275
|
-
lastOffset: res.lastOffset,
|
|
1276
|
-
appendedRows: res.appendedRows,
|
|
1277
|
-
metricsBytes: appendBytes,
|
|
1278
|
-
ingestedBytes: bodyBytes.byteLength,
|
|
1279
|
-
touched: true,
|
|
1280
|
-
closed: res.closed,
|
|
1281
|
-
});
|
|
2344
|
+
if (res.closed) headers["stream-closed"] = "true";
|
|
2345
|
+
if (producer && res.producer) {
|
|
2346
|
+
headers["producer-epoch"] = String(res.producer.epoch);
|
|
2347
|
+
headers["producer-seq"] = String(res.producer.seq);
|
|
2348
|
+
}
|
|
1282
2349
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
headers["producer-epoch"] = String(res.producer.epoch);
|
|
1289
|
-
headers["producer-seq"] = String(res.producer.seq);
|
|
2350
|
+
const status = producer && res.appendedRows > 0 ? 200 : 204;
|
|
2351
|
+
return new Response(null, { status, headers: withNosniff(headers) });
|
|
2352
|
+
});
|
|
2353
|
+
} finally {
|
|
2354
|
+
leaveAppendPhase?.();
|
|
1290
2355
|
}
|
|
1291
|
-
|
|
1292
|
-
const status = producer && res.appendedRows > 0 ? 200 : 204;
|
|
1293
|
-
return new Response(null, { status, headers: withNosniff(headers) });
|
|
1294
2356
|
}
|
|
1295
2357
|
|
|
1296
2358
|
if (req.method === "GET") {
|
|
@@ -1311,6 +2373,18 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1311
2373
|
|
|
1312
2374
|
const pathKey = pathKeyParam ?? null;
|
|
1313
2375
|
const key = pathKey ?? url.searchParams.get("key");
|
|
2376
|
+
const rawFilter = url.searchParams.get("filter");
|
|
2377
|
+
let filterInput: string | null = null;
|
|
2378
|
+
let filter = null;
|
|
2379
|
+
if (rawFilter != null) {
|
|
2380
|
+
if (!isJsonStream) return badRequest("filter requires application/json stream content-type");
|
|
2381
|
+
filterInput = rawFilter.trim();
|
|
2382
|
+
const regRes = registry.getRegistryResult(stream);
|
|
2383
|
+
if (Result.isError(regRes)) return internalError();
|
|
2384
|
+
const filterRes = parseReadFilterResult(regRes.value, filterInput);
|
|
2385
|
+
if (Result.isError(filterRes)) return badRequest(filterRes.error.message);
|
|
2386
|
+
filter = filterRes.value;
|
|
2387
|
+
}
|
|
1314
2388
|
|
|
1315
2389
|
const liveParam = url.searchParams.get("live") ?? "";
|
|
1316
2390
|
const cursorParam = url.searchParams.get("cursor");
|
|
@@ -1319,6 +2393,7 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1319
2393
|
else if (liveParam === "long-poll" || liveParam === "true" || liveParam === "1") mode = "long-poll";
|
|
1320
2394
|
else if (liveParam === "sse") mode = "sse";
|
|
1321
2395
|
else return badRequest("invalid live mode");
|
|
2396
|
+
if (filter && mode === "sse") return badRequest("filter does not support live=sse");
|
|
1322
2397
|
|
|
1323
2398
|
const timeout = url.searchParams.get("timeout") ?? url.searchParams.get("timeout_ms");
|
|
1324
2399
|
let timeoutMs: number | null = null;
|
|
@@ -1362,7 +2437,7 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1362
2437
|
const upToDate = batch.nextOffsetSeq === batch.endOffsetSeq;
|
|
1363
2438
|
const closedAtTail = srow.closed !== 0 && upToDate;
|
|
1364
2439
|
const etag = includeEtag
|
|
1365
|
-
? `W/\"slice:${canonicalizeOffset(offset!)}:${batch.nextOffset}:key=${key ?? ""}:fmt=${format}\"`
|
|
2440
|
+
? `W/\"slice:${canonicalizeOffset(offset!)}:${batch.nextOffset}:key=${key ?? ""}:fmt=${format}:filter=${filterInput ? encodeURIComponent(filterInput) : ""}\"`
|
|
1366
2441
|
: null;
|
|
1367
2442
|
const baseHeaders: Record<string, string> = {
|
|
1368
2443
|
"stream-next-offset": batch.nextOffset,
|
|
@@ -1374,12 +2449,31 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1374
2449
|
if (cacheControl) baseHeaders["cache-control"] = cacheControl;
|
|
1375
2450
|
if (etag) baseHeaders["etag"] = etag;
|
|
1376
2451
|
if (srow.expires_at_ms != null) baseHeaders["stream-expires-at"] = new Date(Number(srow.expires_at_ms)).toISOString();
|
|
2452
|
+
if (batch.filterScanLimitReached) {
|
|
2453
|
+
baseHeaders["stream-filter-scan-limit-reached"] = "true";
|
|
2454
|
+
baseHeaders["stream-filter-scan-limit-bytes"] = String(batch.filterScanLimitBytes ?? 0);
|
|
2455
|
+
baseHeaders["stream-filter-scanned-bytes"] = String(batch.filterScannedBytes ?? 0);
|
|
2456
|
+
}
|
|
1377
2457
|
|
|
1378
2458
|
if (etag && ifNoneMatch && ifNoneMatch === etag) {
|
|
1379
2459
|
return new Response(null, { status: 304, headers: withNosniff(baseHeaders) });
|
|
1380
2460
|
}
|
|
1381
2461
|
|
|
1382
2462
|
if (format === "json") {
|
|
2463
|
+
const encodedRes = encodeStoredJsonArrayResult(stream, batch.records);
|
|
2464
|
+
if (Result.isError(encodedRes)) {
|
|
2465
|
+
if (encodedRes.error.status === 500) return internalError();
|
|
2466
|
+
return badRequest(encodedRes.error.message);
|
|
2467
|
+
}
|
|
2468
|
+
if (encodedRes.value) {
|
|
2469
|
+
metrics.recordRead(encodedRes.value.byteLength, batch.records.length);
|
|
2470
|
+
const headers: Record<string, string> = {
|
|
2471
|
+
"content-type": "application/json",
|
|
2472
|
+
...baseHeaders,
|
|
2473
|
+
};
|
|
2474
|
+
return new Response(bodyBufferFromBytes(encodedRes.value), { status: 200, headers: withNosniff(headers) });
|
|
2475
|
+
}
|
|
2476
|
+
|
|
1383
2477
|
const decoded = decodeJsonRecords(stream, batch.records);
|
|
1384
2478
|
if (Result.isError(decoded)) {
|
|
1385
2479
|
if (decoded.error.status === 500) return internalError();
|
|
@@ -1400,9 +2494,7 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1400
2494
|
"content-type": streamContentType,
|
|
1401
2495
|
...baseHeaders,
|
|
1402
2496
|
};
|
|
1403
|
-
|
|
1404
|
-
outBody.set(outBytes);
|
|
1405
|
-
return new Response(outBody, { status: 200, headers: withNosniff(headers) });
|
|
2497
|
+
return new Response(bodyBufferFromBytes(outBytes), { status: 200, headers: withNosniff(headers) });
|
|
1406
2498
|
};
|
|
1407
2499
|
|
|
1408
2500
|
if (mode === "sse") {
|
|
@@ -1441,7 +2533,9 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1441
2533
|
records: [],
|
|
1442
2534
|
};
|
|
1443
2535
|
} else {
|
|
1444
|
-
const batchRes = await
|
|
2536
|
+
const batchRes = await runForegroundWithGate(readGate, () =>
|
|
2537
|
+
reader.readResult({ stream, offset: currentOffset, key: key ?? null, format, filter })
|
|
2538
|
+
);
|
|
1445
2539
|
if (Result.isError(batchRes)) {
|
|
1446
2540
|
fail(batchRes.error.message);
|
|
1447
2541
|
return;
|
|
@@ -1455,12 +2549,21 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1455
2549
|
if (batch.records.length > 0) {
|
|
1456
2550
|
let dataPayload = "";
|
|
1457
2551
|
if (format === "json") {
|
|
1458
|
-
const
|
|
1459
|
-
if (Result.isError(
|
|
1460
|
-
fail(
|
|
2552
|
+
const encodedRes = encodeStoredJsonArrayResult(stream, batch.records);
|
|
2553
|
+
if (Result.isError(encodedRes)) {
|
|
2554
|
+
fail(encodedRes.error.message);
|
|
1461
2555
|
return;
|
|
1462
2556
|
}
|
|
1463
|
-
|
|
2557
|
+
if (encodedRes.value) {
|
|
2558
|
+
dataPayload = new TextDecoder().decode(encodedRes.value);
|
|
2559
|
+
} else {
|
|
2560
|
+
const decoded = decodeJsonRecords(stream, batch.records);
|
|
2561
|
+
if (Result.isError(decoded)) {
|
|
2562
|
+
fail(decoded.error.message);
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
dataPayload = JSON.stringify(decoded.value.values);
|
|
2566
|
+
}
|
|
1464
2567
|
} else {
|
|
1465
2568
|
const outBytes = concatPayloads(batch.records.map((r) => r.payload));
|
|
1466
2569
|
dataPayload =
|
|
@@ -1546,10 +2649,12 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1546
2649
|
const deadline = Date.now() + (timeoutMs ?? defaultLongPollTimeoutMs);
|
|
1547
2650
|
let currentOffset = tailOffset;
|
|
1548
2651
|
while (true) {
|
|
1549
|
-
const batchRes = await
|
|
2652
|
+
const batchRes = await runForegroundWithGate(readGate, () =>
|
|
2653
|
+
reader.readResult({ stream, offset: currentOffset, key: key ?? null, format, filter })
|
|
2654
|
+
);
|
|
1550
2655
|
if (Result.isError(batchRes)) return readerErrorResponse(batchRes.error);
|
|
1551
2656
|
const batch = batchRes.value;
|
|
1552
|
-
if (batch.records.length > 0) {
|
|
2657
|
+
if (batch.records.length > 0 || batch.filterScanLimitReached) {
|
|
1553
2658
|
const cursor = computeCursor(Date.now(), cursorParam);
|
|
1554
2659
|
const resp = await sendBatch(batch, "no-store", false);
|
|
1555
2660
|
const headers = new Headers(resp.headers);
|
|
@@ -1605,10 +2710,12 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1605
2710
|
const deadline = Date.now() + (timeoutMs ?? defaultLongPollTimeoutMs);
|
|
1606
2711
|
let currentOffset = offset;
|
|
1607
2712
|
while (true) {
|
|
1608
|
-
const batchRes = await
|
|
2713
|
+
const batchRes = await runForegroundWithGate(readGate, () =>
|
|
2714
|
+
reader.readResult({ stream, offset: currentOffset, key: key ?? null, format, filter })
|
|
2715
|
+
);
|
|
1609
2716
|
if (Result.isError(batchRes)) return readerErrorResponse(batchRes.error);
|
|
1610
2717
|
const batch = batchRes.value;
|
|
1611
|
-
if (batch.records.length > 0) {
|
|
2718
|
+
if (batch.records.length > 0 || batch.filterScanLimitReached) {
|
|
1612
2719
|
const cursor = computeCursor(Date.now(), cursorParam);
|
|
1613
2720
|
const resp = await sendBatch(batch, "no-store", false);
|
|
1614
2721
|
const headers = new Headers(resp.headers);
|
|
@@ -1648,7 +2755,9 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1648
2755
|
return new Response(null, { status: 204, headers: withNosniff(headers) });
|
|
1649
2756
|
}
|
|
1650
2757
|
|
|
1651
|
-
const batchRes = await
|
|
2758
|
+
const batchRes = await runForegroundWithGate(readGate, () =>
|
|
2759
|
+
reader.readResult({ stream, offset, key: key ?? null, format, filter })
|
|
2760
|
+
);
|
|
1652
2761
|
if (Result.isError(batchRes)) return readerErrorResponse(batchRes.error);
|
|
1653
2762
|
const batch = batchRes.value;
|
|
1654
2763
|
const cacheControl = "immutable, max-age=31536000";
|
|
@@ -1659,13 +2768,29 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1659
2768
|
}
|
|
1660
2769
|
|
|
1661
2770
|
return notFound();
|
|
2771
|
+
})();
|
|
2772
|
+
const resolved = await awaitWithCooperativeTimeout(requestPromise, HTTP_RESOLVER_TIMEOUT_MS);
|
|
2773
|
+
if (resolved === TIMEOUT_SENTINEL) {
|
|
2774
|
+
timedOut = true;
|
|
2775
|
+
requestAbortController.abort(new Error("request timed out"));
|
|
2776
|
+
void requestPromise.catch(() => {});
|
|
2777
|
+
await cancelRequestBody(req);
|
|
2778
|
+
return requestTimeout();
|
|
2779
|
+
}
|
|
2780
|
+
return resolved;
|
|
1662
2781
|
} catch (e: any) {
|
|
2782
|
+
if (isAbortLikeError(e)) {
|
|
2783
|
+
if (timedOut) return requestTimeout();
|
|
2784
|
+
return new Response(null, { status: 204 });
|
|
2785
|
+
}
|
|
1663
2786
|
const msg = String(e?.message ?? e);
|
|
1664
2787
|
if (!closing && !msg.includes("Statement has finalized")) {
|
|
1665
2788
|
// eslint-disable-next-line no-console
|
|
1666
2789
|
console.error("request failed", e);
|
|
1667
2790
|
}
|
|
1668
2791
|
return internalError();
|
|
2792
|
+
} finally {
|
|
2793
|
+
req.signal.removeEventListener("abort", abortFromClient);
|
|
1669
2794
|
}
|
|
1670
2795
|
};
|
|
1671
2796
|
|
|
@@ -1677,7 +2802,9 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1677
2802
|
segmenter.stop(true);
|
|
1678
2803
|
metricsEmitter.stop();
|
|
1679
2804
|
expirySweeper.stop();
|
|
2805
|
+
streamSizeReconciler.stop();
|
|
1680
2806
|
ingest.stop();
|
|
2807
|
+
memorySampler?.stop();
|
|
1681
2808
|
memory.stop();
|
|
1682
2809
|
db.close();
|
|
1683
2810
|
};
|
|
@@ -1697,10 +2824,18 @@ export function createAppCore(cfg: Config, opts: CreateAppCoreOptions): App {
|
|
|
1697
2824
|
indexer,
|
|
1698
2825
|
metrics,
|
|
1699
2826
|
registry,
|
|
2827
|
+
profiles,
|
|
1700
2828
|
touch,
|
|
1701
2829
|
stats,
|
|
1702
2830
|
backpressure,
|
|
1703
2831
|
memory,
|
|
2832
|
+
concurrency: {
|
|
2833
|
+
ingest: ingestGate,
|
|
2834
|
+
read: readGate,
|
|
2835
|
+
search: searchGate,
|
|
2836
|
+
asyncIndex: asyncIndexGate,
|
|
2837
|
+
},
|
|
2838
|
+
memorySampler,
|
|
1704
2839
|
},
|
|
1705
2840
|
};
|
|
1706
2841
|
}
|