@tungthedev/streams-server 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CODE_OF_CONDUCT.md +45 -0
  2. package/CONTRIBUTING.md +76 -0
  3. package/LICENSE +201 -0
  4. package/README.md +58 -0
  5. package/SECURITY.md +42 -0
  6. package/bin/prisma-streams-server +2 -0
  7. package/package.json +46 -0
  8. package/src/app.ts +583 -0
  9. package/src/app_core.ts +3144 -0
  10. package/src/app_local.ts +206 -0
  11. package/src/auth.ts +124 -0
  12. package/src/auto_tune.ts +69 -0
  13. package/src/backpressure.ts +66 -0
  14. package/src/bootstrap.ts +613 -0
  15. package/src/compute/demo_entry.ts +415 -0
  16. package/src/compute/demo_site.ts +1242 -0
  17. package/src/compute/entry.ts +19 -0
  18. package/src/compute/package_entry.ts +4 -0
  19. package/src/compute/virtual-modules.d.ts +15 -0
  20. package/src/compute/worker_module_url.ts +9 -0
  21. package/src/concurrency_gate.ts +108 -0
  22. package/src/config.ts +402 -0
  23. package/src/db/bootstrap_store.ts +9 -0
  24. package/src/db/db.ts +2424 -0
  25. package/src/db/schema.ts +925 -0
  26. package/src/db/sqlite_manifest_snapshot.ts +81 -0
  27. package/src/db/sqlite_touch_store.ts +491 -0
  28. package/src/db/sqlite_wal_store.ts +472 -0
  29. package/src/details/full_mode_details.ts +568 -0
  30. package/src/expiry_sweeper.ts +47 -0
  31. package/src/foreground_activity.ts +55 -0
  32. package/src/hist.ts +169 -0
  33. package/src/index/binary_fuse.ts +379 -0
  34. package/src/index/indexer.ts +947 -0
  35. package/src/index/lexicon_file_cache.ts +261 -0
  36. package/src/index/lexicon_format.ts +93 -0
  37. package/src/index/lexicon_indexer.ts +863 -0
  38. package/src/index/run_cache.ts +84 -0
  39. package/src/index/run_format.ts +213 -0
  40. package/src/index/schedule.ts +28 -0
  41. package/src/index/secondary_indexer.ts +901 -0
  42. package/src/index/secondary_schema.ts +105 -0
  43. package/src/ingest.ts +309 -0
  44. package/src/lens/lens.ts +501 -0
  45. package/src/manifest.ts +249 -0
  46. package/src/memory.ts +334 -0
  47. package/src/metrics.ts +147 -0
  48. package/src/metrics_emitter.ts +83 -0
  49. package/src/notifier.ts +180 -0
  50. package/src/objectstore/accounting.ts +151 -0
  51. package/src/objectstore/interface.ts +13 -0
  52. package/src/objectstore/mock_r2.ts +269 -0
  53. package/src/objectstore/null.ts +32 -0
  54. package/src/objectstore/r2.ts +318 -0
  55. package/src/observe/pairing.ts +61 -0
  56. package/src/observe/request.ts +772 -0
  57. package/src/offset.ts +70 -0
  58. package/src/postgres/bootstrap.ts +269 -0
  59. package/src/postgres/companions.ts +197 -0
  60. package/src/postgres/control_restore.ts +109 -0
  61. package/src/postgres/details.ts +189 -0
  62. package/src/postgres/lexicon_index.ts +260 -0
  63. package/src/postgres/routing_index.ts +189 -0
  64. package/src/postgres/rows.ts +132 -0
  65. package/src/postgres/schema.ts +355 -0
  66. package/src/postgres/secondary_index.ts +238 -0
  67. package/src/postgres/segments.ts +900 -0
  68. package/src/postgres/stats.ts +103 -0
  69. package/src/postgres/store.ts +947 -0
  70. package/src/postgres/touch.ts +591 -0
  71. package/src/postgres/types.ts +32 -0
  72. package/src/profiles/evlog/schema.ts +234 -0
  73. package/src/profiles/evlog.ts +473 -0
  74. package/src/profiles/generic.ts +51 -0
  75. package/src/profiles/index.ts +237 -0
  76. package/src/profiles/metrics/block_format.ts +109 -0
  77. package/src/profiles/metrics/normalize.ts +366 -0
  78. package/src/profiles/metrics/schema.ts +319 -0
  79. package/src/profiles/metrics.ts +83 -0
  80. package/src/profiles/otelTraces/normalize.ts +955 -0
  81. package/src/profiles/otelTraces/otlp.ts +1002 -0
  82. package/src/profiles/otelTraces/schema.ts +408 -0
  83. package/src/profiles/otelTraces.ts +390 -0
  84. package/src/profiles/profile.ts +284 -0
  85. package/src/profiles/stateProtocol/change_event_conformance.typecheck.ts +35 -0
  86. package/src/profiles/stateProtocol/changes.ts +24 -0
  87. package/src/profiles/stateProtocol/ingest.ts +115 -0
  88. package/src/profiles/stateProtocol/routes.ts +511 -0
  89. package/src/profiles/stateProtocol/types.ts +6 -0
  90. package/src/profiles/stateProtocol/validation.ts +51 -0
  91. package/src/profiles/stateProtocol.ts +107 -0
  92. package/src/read_filter.ts +468 -0
  93. package/src/reader.ts +2986 -0
  94. package/src/runtime/hash.ts +156 -0
  95. package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
  96. package/src/runtime/hash_vendor/NOTICE.md +8 -0
  97. package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
  98. package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
  99. package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
  100. package/src/runtime/host_runtime.ts +5 -0
  101. package/src/runtime_memory.ts +200 -0
  102. package/src/runtime_memory_sampler.ts +237 -0
  103. package/src/schema/lens_schema.ts +290 -0
  104. package/src/schema/proof.ts +547 -0
  105. package/src/schema/read_json.ts +51 -0
  106. package/src/schema/registry.ts +966 -0
  107. package/src/search/agg_format.ts +638 -0
  108. package/src/search/aggregate.ts +409 -0
  109. package/src/search/binary/codec.ts +162 -0
  110. package/src/search/binary/docset.ts +67 -0
  111. package/src/search/binary/restart_strings.ts +181 -0
  112. package/src/search/binary/varint.ts +34 -0
  113. package/src/search/bitset.ts +19 -0
  114. package/src/search/col_format.ts +382 -0
  115. package/src/search/col_runtime.ts +59 -0
  116. package/src/search/column_encoding.ts +43 -0
  117. package/src/search/companion_file_cache.ts +319 -0
  118. package/src/search/companion_format.ts +327 -0
  119. package/src/search/companion_manager.ts +1305 -0
  120. package/src/search/companion_plan.ts +229 -0
  121. package/src/search/exact_format.ts +281 -0
  122. package/src/search/exact_runtime.ts +55 -0
  123. package/src/search/fts_format.ts +423 -0
  124. package/src/search/fts_runtime.ts +333 -0
  125. package/src/search/query.ts +875 -0
  126. package/src/search/schema.ts +245 -0
  127. package/src/segment/cache.ts +270 -0
  128. package/src/segment/cached_segment.ts +89 -0
  129. package/src/segment/format.ts +403 -0
  130. package/src/segment/segmenter.ts +412 -0
  131. package/src/segment/segmenter_worker.ts +72 -0
  132. package/src/segment/segmenter_workers.ts +130 -0
  133. package/src/server.ts +264 -0
  134. package/src/server_auto_tune.ts +158 -0
  135. package/src/sqlite/adapter.ts +335 -0
  136. package/src/sqlite/runtime_stats.ts +163 -0
  137. package/src/stats.ts +205 -0
  138. package/src/store/append.ts +50 -0
  139. package/src/store/bootstrap_restore_store.ts +71 -0
  140. package/src/store/capabilities.ts +86 -0
  141. package/src/store/full_mode_details_store.ts +71 -0
  142. package/src/store/index_store.ts +104 -0
  143. package/src/store/profile_touch_store.ts +1 -0
  144. package/src/store/rows.ts +144 -0
  145. package/src/store/schema_profile_store.ts +73 -0
  146. package/src/store/schema_publication.ts +6 -0
  147. package/src/store/segment_manifest_store.ts +129 -0
  148. package/src/store/segment_read_store.ts +22 -0
  149. package/src/store/stats_accounting_store.ts +83 -0
  150. package/src/store/touch_store.ts +98 -0
  151. package/src/store/wal_store.ts +21 -0
  152. package/src/stream_size_reconciler.ts +100 -0
  153. package/src/touch/canonical_change.ts +7 -0
  154. package/src/touch/live_keys.ts +158 -0
  155. package/src/touch/live_metrics.ts +841 -0
  156. package/src/touch/live_templates.ts +449 -0
  157. package/src/touch/manager.ts +1292 -0
  158. package/src/touch/process_batch.ts +576 -0
  159. package/src/touch/processor_worker.ts +85 -0
  160. package/src/touch/spec.ts +459 -0
  161. package/src/touch/touch_journal.ts +771 -0
  162. package/src/touch/touch_key_id.ts +20 -0
  163. package/src/touch/worker_pool.ts +191 -0
  164. package/src/touch/worker_protocol.ts +57 -0
  165. package/src/types/proper-lockfile.d.ts +1 -0
  166. package/src/uploader.ts +358 -0
  167. package/src/util/base32_crockford.ts +81 -0
  168. package/src/util/bloom256.ts +67 -0
  169. package/src/util/byte_lru.ts +73 -0
  170. package/src/util/cleanup.ts +22 -0
  171. package/src/util/crc32c.ts +29 -0
  172. package/src/util/ds_error.ts +15 -0
  173. package/src/util/duration.ts +17 -0
  174. package/src/util/endian.ts +53 -0
  175. package/src/util/json_pointer.ts +148 -0
  176. package/src/util/log.ts +25 -0
  177. package/src/util/lru.ts +53 -0
  178. package/src/util/retry.ts +35 -0
  179. package/src/util/siphash.ts +71 -0
  180. package/src/util/stream_paths.ts +50 -0
  181. package/src/util/time.ts +14 -0
  182. package/src/util/yield.ts +3 -0
  183. package/src/util/zstd.ts +24 -0
@@ -0,0 +1,947 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { Result } from "better-result";
4
+ import type { Config } from "../config";
5
+ import type { IndexRunRow, SegmentRow } from "../store/rows";
6
+ import type { RoutingIndexStore } from "../store/index_store";
7
+ import type { ObjectStore } from "../objectstore/interface";
8
+ import { SegmentDiskCache } from "../segment/cache";
9
+ import { loadSegmentBytesCached } from "../segment/cached_segment";
10
+ import { iterateBlockRecordsResult } from "../segment/format";
11
+ import { siphash24 } from "../util/siphash";
12
+ import { retry } from "../util/retry";
13
+ import { indexRunObjectKey, segmentObjectKey, streamHash16Hex } from "../util/stream_paths";
14
+ import { binaryFuseContains, buildBinaryFuseResult } from "./binary_fuse";
15
+ import { decodeIndexRunResult, encodeIndexRunResult, RUN_TYPE_MASK16, RUN_TYPE_POSTINGS, type IndexRun } from "./run_format";
16
+ import { IndexRunCache } from "./run_cache";
17
+ import type { Metrics } from "../metrics";
18
+ import { dsError } from "../util/ds_error.ts";
19
+ import { yieldToEventLoop } from "../util/yield";
20
+ import { RuntimeMemorySampler } from "../runtime_memory_sampler";
21
+ import { ConcurrencyGate } from "../concurrency_gate";
22
+ import type { ForegroundActivityTracker } from "../foreground_activity";
23
+ import { LOW_MEMORY_INDEX_ENQUEUE_QUIET_MS, shouldDeferEnqueuedIndexWork, shouldWaitForLowMemoryIndexQuiet } from "./schedule";
24
+ import type { AggSectionView } from "../search/agg_format";
25
+ import type { ColSectionView } from "../search/col_format";
26
+ import type { ExactSectionView } from "../search/exact_format";
27
+ import type { FtsSectionView } from "../search/fts_format";
28
+ import type { MetricsBlockSectionView } from "../profiles/metrics/block_format";
29
+ import type { SchemaRegistryStore } from "../schema/registry";
30
+ import type { RoutingKeyLexiconListResult } from "./lexicon_indexer";
31
+
32
+ export type IndexCandidate = { segments: Set<number>; indexedThrough: number };
33
+ type IndexBuildError = { kind: "invalid_index_build"; message: string };
34
+ export type CompanionSectionLookupStats = {
35
+ sectionGetMs: number;
36
+ decodeMs: number;
37
+ };
38
+
39
+ export type StreamIndexLookup = {
40
+ start(): void;
41
+ stop(): Promise<void>;
42
+ enqueue(stream: string): void;
43
+ candidateSegmentsForRoutingKey(stream: string, keyBytes: Uint8Array): Promise<IndexCandidate | null>;
44
+ candidateSegmentsForSecondaryIndex(stream: string, indexName: string, keyBytes: Uint8Array): Promise<IndexCandidate | null>;
45
+ getAggSegmentCompanion(stream: string, segmentIndex: number): Promise<AggSectionView | null>;
46
+ getColSegmentCompanion(stream: string, segmentIndex: number): Promise<ColSectionView | null>;
47
+ getExactSegmentCompanion(stream: string, segmentIndex: number): Promise<ExactSectionView | null>;
48
+ getFtsSegmentCompanion(stream: string, segmentIndex: number): Promise<FtsSectionView | null>;
49
+ getFtsSegmentCompanionWithStats?(
50
+ stream: string,
51
+ segmentIndex: number
52
+ ): Promise<{ companion: FtsSectionView | null; stats: CompanionSectionLookupStats }>;
53
+ getMetricsBlockSegmentCompanion(stream: string, segmentIndex: number): Promise<MetricsBlockSectionView | null>;
54
+ listRoutingKeysResult?(stream: string, after: string | null, limit: number): Promise<Result<RoutingKeyLexiconListResult, { kind: string; message: string }>>;
55
+ getLocalStorageUsage?(stream: string): {
56
+ routing_index_cache_bytes: number;
57
+ exact_index_cache_bytes: number;
58
+ companion_cache_bytes: number;
59
+ lexicon_index_cache_bytes: number;
60
+ };
61
+ };
62
+
63
+ function invalidIndexBuild<T = never>(message: string): Result<T, IndexBuildError> {
64
+ return Result.err({ kind: "invalid_index_build", message });
65
+ }
66
+
67
+ function errorMessage(e: unknown): string {
68
+ return String((e as any)?.message ?? e);
69
+ }
70
+
71
+ export class IndexManager {
72
+ private readonly cfg: Config;
73
+ private readonly db: RoutingIndexStore;
74
+ private readonly os: ObjectStore;
75
+ private readonly segmentCache?: SegmentDiskCache;
76
+ private readonly runDiskCache?: SegmentDiskCache;
77
+ private readonly runCache: IndexRunCache;
78
+ private readonly span: number;
79
+ private readonly buildConcurrency: number;
80
+ private readonly compactionFanout: number;
81
+ private readonly maxLevel: number;
82
+ private readonly compactionConcurrency: number;
83
+ private readonly retireGenWindow: number;
84
+ private readonly retireMinMs: number;
85
+ private readonly queue = new Set<string>();
86
+ private readonly building = new Set<string>();
87
+ private readonly compacting = new Set<string>();
88
+ private readonly metrics?: Metrics;
89
+ private lastRunCacheHits = 0;
90
+ private lastRunCacheMisses = 0;
91
+ private lastRunCacheEvictions = 0;
92
+ private lastDiskHits = 0;
93
+ private lastDiskMisses = 0;
94
+ private lastDiskEvictions = 0;
95
+ private lastDiskBytesAdded = 0;
96
+ private timer: any | null = null;
97
+ private wakeTimer: any | null = null;
98
+ private running = false;
99
+ private stopped = false;
100
+ private tickPromise: Promise<void> | null = null;
101
+ private readonly publishManifest?: (stream: string) => Promise<void>;
102
+ private readonly onMetadataChanged?: (stream: string) => void;
103
+ private readonly memorySampler?: RuntimeMemorySampler;
104
+ private readonly registry?: SchemaRegistryStore;
105
+ private readonly asyncGate: ConcurrencyGate;
106
+ private readonly foregroundActivity?: ForegroundActivityTracker;
107
+ private firstQueuedAtMs: number | null = null;
108
+
109
+ constructor(
110
+ cfg: Config,
111
+ db: RoutingIndexStore,
112
+ os: ObjectStore,
113
+ segmentCache: SegmentDiskCache | undefined,
114
+ publishManifest?: (stream: string) => Promise<void>,
115
+ metrics?: Metrics,
116
+ onMetadataChanged?: (stream: string) => void,
117
+ memorySampler?: RuntimeMemorySampler,
118
+ registry?: SchemaRegistryStore,
119
+ asyncGate?: ConcurrencyGate,
120
+ foregroundActivity?: ForegroundActivityTracker
121
+ ) {
122
+ this.cfg = cfg;
123
+ this.db = db;
124
+ this.os = os;
125
+ this.segmentCache = segmentCache;
126
+ this.publishManifest = publishManifest;
127
+ this.span = cfg.indexL0SpanSegments;
128
+ this.buildConcurrency = Math.max(1, cfg.indexBuildConcurrency);
129
+ this.compactionFanout = cfg.indexCompactionFanout;
130
+ this.maxLevel = cfg.indexMaxLevel;
131
+ this.compactionConcurrency = Math.max(1, cfg.indexCompactionConcurrency);
132
+ this.retireGenWindow = Math.max(0, cfg.indexRetireGenWindow);
133
+ this.retireMinMs = Math.max(0, cfg.indexRetireMinMs);
134
+ this.metrics = metrics;
135
+ this.onMetadataChanged = onMetadataChanged;
136
+ this.memorySampler = memorySampler;
137
+ this.registry = registry;
138
+ this.asyncGate = asyncGate ?? new ConcurrencyGate(1);
139
+ this.foregroundActivity = foregroundActivity;
140
+ this.runCache = new IndexRunCache(cfg.indexRunMemoryCacheBytes);
141
+ this.runDiskCache = cfg.indexRunCacheMaxBytes > 0 ? new SegmentDiskCache(`${cfg.rootDir}/cache/index`, cfg.indexRunCacheMaxBytes) : undefined;
142
+ }
143
+
144
+ private async yieldBackgroundWork(): Promise<void> {
145
+ if (this.foregroundActivity) {
146
+ await this.foregroundActivity.yieldBackgroundWork();
147
+ return;
148
+ }
149
+ await yieldToEventLoop();
150
+ }
151
+
152
+ start(): void {
153
+ if (this.span <= 0) return;
154
+ if (this.timer) return;
155
+ this.stopped = false;
156
+ this.timer = setInterval(() => {
157
+ if (!this.stopped) this.runTick();
158
+ }, this.cfg.indexCheckIntervalMs);
159
+ }
160
+
161
+ async stop(): Promise<void> {
162
+ this.stopped = true;
163
+ if (this.timer) clearInterval(this.timer);
164
+ if (this.wakeTimer) clearTimeout(this.wakeTimer);
165
+ this.timer = null;
166
+ this.wakeTimer = null;
167
+ while (this.tickPromise) await this.tickPromise;
168
+ this.firstQueuedAtMs = null;
169
+ }
170
+
171
+ enqueue(stream: string): void {
172
+ if (this.span <= 0 || this.stopped) return;
173
+ if (this.firstQueuedAtMs == null) this.firstQueuedAtMs = Date.now();
174
+ this.queue.add(stream);
175
+ if (shouldDeferEnqueuedIndexWork(this.cfg)) {
176
+ this.scheduleTick(LOW_MEMORY_INDEX_ENQUEUE_QUIET_MS);
177
+ return;
178
+ }
179
+ this.scheduleTick();
180
+ }
181
+
182
+ private scheduleTick(delayMs = 0): void {
183
+ if (this.stopped || !this.timer || this.wakeTimer) return;
184
+ this.wakeTimer = setTimeout(() => {
185
+ this.wakeTimer = null;
186
+ if (this.stopped) return;
187
+ if (
188
+ shouldWaitForLowMemoryIndexQuiet(
189
+ this.cfg,
190
+ this.firstQueuedAtMs,
191
+ this.foregroundActivity?.wasActiveWithin(LOW_MEMORY_INDEX_ENQUEUE_QUIET_MS) ?? false
192
+ )
193
+ ) {
194
+ this.scheduleTick(LOW_MEMORY_INDEX_ENQUEUE_QUIET_MS);
195
+ return;
196
+ }
197
+ if (this.running) {
198
+ this.scheduleTick(250);
199
+ return;
200
+ }
201
+ this.runTick();
202
+ }, delayMs);
203
+ (this.wakeTimer as { unref?: () => void }).unref?.();
204
+ }
205
+
206
+ private runTick(): void {
207
+ if (this.tickPromise) return;
208
+ const promise = this.tick()
209
+ .catch((e) => {
210
+ const lower = errorMessage(e).toLowerCase();
211
+ const shutdownError =
212
+ lower.includes("database has closed") ||
213
+ lower.includes("closed database") ||
214
+ lower.includes("statement has finalized") ||
215
+ lower.includes("disk i/o error");
216
+ if (!this.stopped || !shutdownError) {
217
+ // eslint-disable-next-line no-console
218
+ console.error("index tick failed", e);
219
+ }
220
+ })
221
+ .finally(() => {
222
+ if (this.tickPromise === promise) this.tickPromise = null;
223
+ });
224
+ this.tickPromise = promise;
225
+ }
226
+
227
+ async candidateSegmentsForRoutingKey(stream: string, keyBytes: Uint8Array): Promise<IndexCandidate | null> {
228
+ if (this.span <= 0) return null;
229
+ if (!(await this.isRoutingConfigured(stream))) return null;
230
+ const state = await this.db.getIndexState(stream);
231
+ if (!state) return null;
232
+ const runs = await this.db.listIndexRuns(stream);
233
+ if (runs.length === 0 && state.indexed_through === 0) return null;
234
+
235
+ const fp = siphash24(state.index_secret, keyBytes);
236
+ const segments = new Set<number>();
237
+ for (const meta of runs) {
238
+ const runRes = await this.loadRunResult(meta);
239
+ if (Result.isError(runRes)) continue;
240
+ const run = runRes.value;
241
+ if (!run) continue;
242
+ if (run.filter && !binaryFuseContains(run.filter, fp)) continue;
243
+ if (run.runType === RUN_TYPE_MASK16 && run.masks) {
244
+ const idx = binarySearch(run.fingerprints, fp);
245
+ if (idx >= 0) {
246
+ const mask = run.masks[idx];
247
+ for (let bit = 0; bit < 16; bit++) {
248
+ if ((mask & (1 << bit)) !== 0) segments.add(run.meta.startSegment + bit);
249
+ }
250
+ }
251
+ } else if (run.postings) {
252
+ const idx = binarySearch(run.fingerprints, fp);
253
+ if (idx >= 0) {
254
+ for (const seg of run.postings[idx]) segments.add(seg);
255
+ }
256
+ }
257
+ }
258
+ return { segments, indexedThrough: state.indexed_through };
259
+ }
260
+
261
+ async candidateSegmentsForSecondaryIndex(_stream: string, _indexName: string, _keyBytes: Uint8Array): Promise<IndexCandidate | null> {
262
+ return null;
263
+ }
264
+
265
+ async getColSegmentCompanion(_stream: string, _segmentIndex: number): Promise<ColSectionView | null> {
266
+ return null;
267
+ }
268
+
269
+ async getAggSegmentCompanion(_stream: string, _segmentIndex: number): Promise<AggSectionView | null> {
270
+ return null;
271
+ }
272
+
273
+ async getFtsSegmentCompanion(_stream: string, _segmentIndex: number): Promise<FtsSectionView | null> {
274
+ return null;
275
+ }
276
+
277
+ async getMetricsBlockSegmentCompanion(_stream: string, _segmentIndex: number): Promise<MetricsBlockSectionView | null> {
278
+ return null;
279
+ }
280
+
281
+ getLocalCacheBytes(stream: string): number {
282
+ if (!this.runDiskCache) return 0;
283
+ return this.runDiskCache.bytesForObjectKeyPrefix(`streams/${streamHash16Hex(stream)}/index/`);
284
+ }
285
+
286
+ getMemoryStats(): {
287
+ runCacheBytes: number;
288
+ runCacheEntries: number;
289
+ runDiskCacheBytes: number;
290
+ runDiskCacheEntries: number;
291
+ runDiskMappedBytes: number;
292
+ runDiskMappedEntries: number;
293
+ runDiskPinnedEntries: number;
294
+ } {
295
+ const mem = this.runCache.stats();
296
+ const disk = this.runDiskCache?.stats();
297
+ return {
298
+ runCacheBytes: mem.usedBytes,
299
+ runCacheEntries: mem.entries,
300
+ runDiskCacheBytes: disk?.usedBytes ?? 0,
301
+ runDiskCacheEntries: disk?.entryCount ?? 0,
302
+ runDiskMappedBytes: disk?.mappedBytes ?? 0,
303
+ runDiskMappedEntries: disk?.mappedEntryCount ?? 0,
304
+ runDiskPinnedEntries: disk?.pinnedEntryCount ?? 0,
305
+ };
306
+ }
307
+
308
+ private async tick(): Promise<void> {
309
+ if (this.running || this.stopped) return;
310
+ this.running = true;
311
+ try {
312
+ if (this.metrics) {
313
+ this.metrics.record("tieredstore.index.build.queue_len", this.queue.size, "count");
314
+ this.metrics.record("tieredstore.index.builds_inflight", this.building.size, "count");
315
+ }
316
+ const streams = Array.from(this.queue);
317
+ this.queue.clear();
318
+ for (const stream of streams) {
319
+ if (this.stopped) break;
320
+ if (!(await this.isRoutingConfigured(stream))) {
321
+ const hadRoutingState = !!(await this.db.getIndexState(stream)) || (await this.db.listIndexRunsAll(stream)).length > 0;
322
+ if (hadRoutingState) {
323
+ await this.db.deleteIndex(stream);
324
+ this.onMetadataChanged?.(stream);
325
+ if (this.publishManifest) {
326
+ try {
327
+ await this.publishManifest(stream);
328
+ } catch {
329
+ // ignore and retry on next enqueue
330
+ }
331
+ }
332
+ }
333
+ continue;
334
+ }
335
+ try {
336
+ const buildRes = await this.maybeBuildRuns(stream);
337
+ if (Result.isError(buildRes)) {
338
+ // eslint-disable-next-line no-console
339
+ console.error("index build failed", stream, buildRes.error.message);
340
+ this.queue.add(stream);
341
+ continue;
342
+ }
343
+ const compactRes = await this.maybeCompactRuns(stream);
344
+ if (Result.isError(compactRes)) {
345
+ // eslint-disable-next-line no-console
346
+ console.error("index compaction failed", stream, compactRes.error.message);
347
+ this.queue.add(stream);
348
+ continue;
349
+ }
350
+ } catch (e) {
351
+ const msg = String((e as any)?.message ?? e);
352
+ const lower = msg.toLowerCase();
353
+ if (lower.includes("database has closed") || lower.includes("closed database") || lower.includes("statement has finalized")) {
354
+ continue;
355
+ }
356
+ // eslint-disable-next-line no-console
357
+ console.error("index build failed", stream, e);
358
+ this.queue.add(stream);
359
+ }
360
+ }
361
+ this.recordCacheStats();
362
+ } finally {
363
+ this.running = false;
364
+ if (!this.stopped && this.queue.size > 0) {
365
+ if (this.firstQueuedAtMs == null) this.firstQueuedAtMs = Date.now();
366
+ this.scheduleTick(shouldDeferEnqueuedIndexWork(this.cfg) ? LOW_MEMORY_INDEX_ENQUEUE_QUIET_MS : 0);
367
+ } else {
368
+ this.firstQueuedAtMs = null;
369
+ }
370
+ }
371
+ }
372
+
373
+ private async maybeBuildRuns(stream: string): Promise<Result<void, IndexBuildError>> {
374
+ if (this.span <= 0) return Result.ok(undefined);
375
+ if (this.building.has(stream)) return Result.ok(undefined);
376
+ this.building.add(stream);
377
+ try {
378
+ return await this.asyncGate.run(async () => {
379
+ let state = await this.db.getIndexState(stream);
380
+ if (!state) {
381
+ const secret = randomBytes(16);
382
+ await this.db.upsertIndexState(stream, secret, 0);
383
+ state = await this.db.getIndexState(stream);
384
+ }
385
+ if (!state) return Result.ok(undefined);
386
+ if (this.metrics) {
387
+ const lag = Math.max(0, (await this.db.countUploadedSegments(stream)) - state.indexed_through);
388
+ this.metrics.record("tieredstore.index.lag.segments", lag, "count", undefined, stream);
389
+ }
390
+ const indexedThrough = state.indexed_through;
391
+ const uploadedCount = await this.db.countUploadedSegments(stream);
392
+ if (uploadedCount < indexedThrough + this.span) return Result.ok(undefined);
393
+ const start = indexedThrough;
394
+ const end = start + this.span - 1;
395
+ const segments: SegmentRow[] = [];
396
+ for (let i = start; i <= end; i++) {
397
+ const seg = await this.db.getSegmentByIndex(stream, i);
398
+ if (!seg || !seg.r2_etag) return Result.ok(undefined);
399
+ segments.push(seg);
400
+ }
401
+ const t0 = Date.now();
402
+ const runRes = this.memorySampler
403
+ ? await this.memorySampler.track(
404
+ "routing_l0",
405
+ { stream, start_segment: start, end_segment: end },
406
+ () => this.buildL0RunResult(stream, start, segments, state.index_secret)
407
+ )
408
+ : await this.buildL0RunResult(stream, start, segments, state.index_secret);
409
+ if (Result.isError(runRes)) return runRes;
410
+ const run = runRes.value;
411
+ const elapsedNs = BigInt(Date.now() - t0) * 1_000_000n;
412
+ const persistRes = await this.persistRunResult(run, stream);
413
+ if (Result.isError(persistRes)) return persistRes;
414
+ const sizeBytes = persistRes.value;
415
+ await this.db.insertIndexRun({
416
+ run_id: run.meta.runId,
417
+ stream,
418
+ level: run.meta.level,
419
+ start_segment: run.meta.startSegment,
420
+ end_segment: run.meta.endSegment,
421
+ object_key: run.meta.objectKey,
422
+ size_bytes: sizeBytes,
423
+ filter_len: run.meta.filterLen,
424
+ record_count: run.meta.recordCount,
425
+ });
426
+ if (this.metrics) {
427
+ this.metrics.record("tieredstore.index.build.latency", Number(elapsedNs), "ns", { level: String(run.meta.level) }, stream);
428
+ this.metrics.record("tieredstore.index.runs.built", 1, "count", { level: String(run.meta.level) }, stream);
429
+ await this.recordActiveRuns(stream);
430
+ }
431
+ const nextIndexedThrough = end + 1;
432
+ await this.db.updateIndexedThrough(stream, nextIndexedThrough);
433
+ state.indexed_through = nextIndexedThrough;
434
+ this.onMetadataChanged?.(stream);
435
+ if (this.publishManifest) {
436
+ try {
437
+ await this.publishManifest(stream);
438
+ } catch {
439
+ // ignore manifest publish errors; will be retried by uploader/indexer
440
+ }
441
+ }
442
+ if ((await this.db.countUploadedSegments(stream)) >= nextIndexedThrough + this.span) this.queue.add(stream);
443
+ return Result.ok(undefined);
444
+ });
445
+ } finally {
446
+ this.building.delete(stream);
447
+ }
448
+ }
449
+
450
+ private async maybeCompactRuns(stream: string): Promise<Result<void, IndexBuildError>> {
451
+ if (this.span <= 0) return Result.ok(undefined);
452
+ if (this.compactionFanout <= 1) return Result.ok(undefined);
453
+ if (this.compacting.has(stream)) return Result.ok(undefined);
454
+ if (this.foregroundActivity?.wasActiveWithin(2000)) {
455
+ this.queue.add(stream);
456
+ return Result.ok(undefined);
457
+ }
458
+ this.compacting.add(stream);
459
+ try {
460
+ return await this.asyncGate.run(async () => {
461
+ const group = await this.findCompactionGroup(stream);
462
+ if (!group) {
463
+ await this.gcRetiredRuns(stream);
464
+ return Result.ok(undefined);
465
+ }
466
+ const t0 = Date.now();
467
+ const { level, runs } = group;
468
+ const runRes = await this.buildCompactedRunResult(stream, level + 1, runs);
469
+ if (Result.isError(runRes)) return runRes;
470
+ const run = runRes.value;
471
+ const elapsedNs = BigInt(Date.now() - t0) * 1_000_000n;
472
+ const persistRes = await this.persistRunResult(run, stream);
473
+ if (Result.isError(persistRes)) return persistRes;
474
+ const sizeBytes = persistRes.value;
475
+ await this.db.insertIndexRun({
476
+ run_id: run.meta.runId,
477
+ stream,
478
+ level: run.meta.level,
479
+ start_segment: run.meta.startSegment,
480
+ end_segment: run.meta.endSegment,
481
+ object_key: run.meta.objectKey,
482
+ size_bytes: sizeBytes,
483
+ filter_len: run.meta.filterLen,
484
+ record_count: run.meta.recordCount,
485
+ });
486
+ const state = await this.db.getIndexState(stream);
487
+ if (state && run.meta.endSegment + 1 > state.indexed_through) {
488
+ await this.db.updateIndexedThrough(stream, run.meta.endSegment + 1);
489
+ state.indexed_through = run.meta.endSegment + 1;
490
+ }
491
+ const manifestRow = await this.db.getManifestRow(stream);
492
+ const retiredGen = manifestRow.generation + 1;
493
+ const nowMs = this.db.nowMs();
494
+ await this.db.retireIndexRuns(
495
+ runs.map((r) => r.run_id),
496
+ retiredGen,
497
+ nowMs
498
+ );
499
+ this.onMetadataChanged?.(stream);
500
+ if (this.metrics) {
501
+ this.metrics.record("tieredstore.index.compact.latency", Number(elapsedNs), "ns", { level: String(run.meta.level) }, stream);
502
+ this.metrics.record("tieredstore.index.runs.compacted", 1, "count", { level: String(run.meta.level) }, stream);
503
+ await this.recordActiveRuns(stream);
504
+ }
505
+ for (const r of runs) {
506
+ this.runCache.remove(r.object_key);
507
+ this.runDiskCache?.remove(r.object_key);
508
+ }
509
+ if (this.publishManifest) {
510
+ try {
511
+ await this.publishManifest(stream);
512
+ } catch {
513
+ // ignore manifest publish errors; will be retried
514
+ }
515
+ }
516
+ await this.gcRetiredRuns(stream);
517
+ this.queue.add(stream);
518
+ return Result.ok(undefined);
519
+ });
520
+ } finally {
521
+ this.compacting.delete(stream);
522
+ }
523
+ }
524
+
525
+ private async findCompactionGroup(stream: string): Promise<{ level: number; runs: IndexRunRow[] } | null> {
526
+ const runs = await this.db.listIndexRuns(stream);
527
+ if (runs.length < this.compactionFanout) return null;
528
+ const byLevel = new Map<number, IndexRunRow[]>();
529
+ for (const r of runs) {
530
+ const arr = byLevel.get(r.level) ?? [];
531
+ arr.push(r);
532
+ byLevel.set(r.level, arr);
533
+ }
534
+ for (let level = 0; level <= this.maxLevel; level++) {
535
+ const levelRuns = byLevel.get(level);
536
+ if (!levelRuns || levelRuns.length < this.compactionFanout) continue;
537
+ const span = this.levelSpan(level);
538
+ for (let i = 0; i + this.compactionFanout <= levelRuns.length; i++) {
539
+ const base = levelRuns[i].start_segment;
540
+ let ok = true;
541
+ for (let j = 0; j < this.compactionFanout; j++) {
542
+ const r = levelRuns[i + j];
543
+ const expectStart = base + j * span;
544
+ if (r.level !== level || r.start_segment !== expectStart || r.end_segment !== expectStart + span - 1) {
545
+ ok = false;
546
+ break;
547
+ }
548
+ }
549
+ if (ok) return { level, runs: levelRuns.slice(i, i + this.compactionFanout) };
550
+ }
551
+ }
552
+ return null;
553
+ }
554
+
555
+ private levelSpan(level: number): number {
556
+ let span = this.span;
557
+ for (let i = 0; i < level; i++) span *= this.compactionFanout;
558
+ return span;
559
+ }
560
+
561
+ private async buildCompactedRunResult(
562
+ stream: string,
563
+ level: number,
564
+ inputs: IndexRunRow[]
565
+ ): Promise<Result<IndexRun, IndexBuildError>> {
566
+ if (inputs.length === 0) return invalidIndexBuild("compact: missing inputs");
567
+ const segments = new Map<bigint, number[]>();
568
+ const addSegment = (fp: bigint, seg: number) => {
569
+ let list = segments.get(fp);
570
+ if (!list) {
571
+ list = [];
572
+ segments.set(fp, list);
573
+ }
574
+ list.push(seg);
575
+ };
576
+ const mergeRun = (meta: IndexRunRow, run: IndexRun): void => {
577
+ if (run.runType === RUN_TYPE_MASK16 && run.masks) {
578
+ for (let i = 0; i < run.fingerprints.length; i++) {
579
+ const fp = run.fingerprints[i];
580
+ const mask = run.masks[i];
581
+ for (let bit = 0; bit < 16; bit++) {
582
+ if ((mask & (1 << bit)) === 0) continue;
583
+ addSegment(fp, meta.start_segment + bit);
584
+ }
585
+ }
586
+ return;
587
+ }
588
+ if (run.runType === RUN_TYPE_POSTINGS && run.postings) {
589
+ for (let i = 0; i < run.fingerprints.length; i++) {
590
+ const fp = run.fingerprints[i];
591
+ const postings = run.postings[i];
592
+ for (const rel of postings) addSegment(fp, meta.start_segment + rel);
593
+ }
594
+ return;
595
+ }
596
+ throw dsError(`unknown run type ${run.runType}`);
597
+ };
598
+
599
+ const pending = inputs.slice();
600
+ const workers = Math.min(this.compactionConcurrency, pending.length);
601
+ let buildError: string | null = null;
602
+ const workerTasks: Promise<void>[] = [];
603
+ for (let w = 0; w < workers; w++) {
604
+ workerTasks.push(
605
+ (async () => {
606
+ for (;;) {
607
+ if (buildError) return;
608
+ const meta = pending.shift();
609
+ if (!meta) return;
610
+ const runRes = await this.loadRunResult(meta);
611
+ if (Result.isError(runRes)) {
612
+ buildError = runRes.error.message;
613
+ return;
614
+ }
615
+ const run = runRes.value;
616
+ if (!run) {
617
+ buildError = `missing run ${meta.run_id}`;
618
+ return;
619
+ }
620
+ try {
621
+ mergeRun(meta, run);
622
+ } catch (e: unknown) {
623
+ buildError = errorMessage(e);
624
+ return;
625
+ }
626
+ await this.yieldBackgroundWork();
627
+ }
628
+ })()
629
+ );
630
+ }
631
+ await Promise.all(workerTasks);
632
+ if (buildError) return invalidIndexBuild(buildError);
633
+
634
+ const startSegment = inputs[0].start_segment;
635
+ const endSegment = inputs[inputs.length - 1].end_segment;
636
+ const fingerprints = Array.from(segments.keys()).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
637
+ const postings: number[][] = new Array(fingerprints.length);
638
+ for (let i = 0; i < fingerprints.length; i++) {
639
+ const fp = fingerprints[i]!;
640
+ const list = segments.get(fp) ?? [];
641
+ list.sort((a, b) => a - b);
642
+ const rel: number[] = [];
643
+ let lastSeg = Number.NaN;
644
+ for (const seg of list) {
645
+ if (seg === lastSeg) continue;
646
+ rel.push(seg - startSegment);
647
+ lastSeg = seg;
648
+ }
649
+ postings[i] = rel;
650
+ }
651
+
652
+ const fuseRes = buildBinaryFuseResult(fingerprints);
653
+ if (Result.isError(fuseRes)) return invalidIndexBuild(fuseRes.error.message);
654
+ const { filter, bytes } = fuseRes.value;
655
+ const shash = streamHash16Hex(stream);
656
+ const runId = `l${level}-${startSegment.toString().padStart(16, "0")}-${endSegment.toString().padStart(16, "0")}-${Date.now()}`;
657
+ const objectKey = indexRunObjectKey(shash, runId);
658
+ return Result.ok({
659
+ meta: {
660
+ runId,
661
+ level,
662
+ startSegment,
663
+ endSegment,
664
+ objectKey,
665
+ filterLen: bytes.byteLength,
666
+ recordCount: fingerprints.length,
667
+ },
668
+ runType: RUN_TYPE_POSTINGS,
669
+ filterBytes: bytes,
670
+ filter,
671
+ fingerprints,
672
+ postings,
673
+ });
674
+ }
675
+
676
+ private async gcRetiredRuns(stream: string): Promise<void> {
677
+ const retired = await this.db.listRetiredIndexRuns(stream);
678
+ if (retired.length === 0) return;
679
+ const manifest = await this.db.getManifestRow(stream);
680
+ const nowMs = this.db.nowMs();
681
+ const cutoffGen = this.retireGenWindow > 0 && manifest.generation > this.retireGenWindow ? manifest.generation - this.retireGenWindow : 0;
682
+ const toDelete: IndexRunRow[] = [];
683
+ for (const r of retired) {
684
+ const expiredByGen = r.retired_gen != null && r.retired_gen > 0 && r.retired_gen <= cutoffGen;
685
+ const expiredByTTL = r.retired_at_ms != null && r.retired_at_ms + BigInt(this.retireMinMs) <= nowMs;
686
+ if (expiredByGen || expiredByTTL) toDelete.push(r);
687
+ }
688
+ if (toDelete.length === 0) return;
689
+ for (const r of toDelete) {
690
+ try {
691
+ await this.os.delete(r.object_key);
692
+ } catch {
693
+ // ignore deletion errors
694
+ }
695
+ this.runCache.remove(r.object_key);
696
+ this.runDiskCache?.remove(r.object_key);
697
+ }
698
+ await this.db.deleteIndexRuns(toDelete.map((r) => r.run_id));
699
+ }
700
+
701
+ private async buildL0RunResult(
702
+ stream: string,
703
+ startSegment: number,
704
+ segments: SegmentRow[],
705
+ secret: Uint8Array
706
+ ): Promise<Result<IndexRun, IndexBuildError>> {
707
+ const maskByFp = new Map<bigint, number>();
708
+ const pending = segments.slice();
709
+ const concurrency = Math.max(1, Math.min(this.buildConcurrency, pending.length));
710
+ let buildError: string | null = null;
711
+ const workers: Promise<void>[] = [];
712
+ for (let i = 0; i < concurrency; i++) {
713
+ workers.push(
714
+ (async () => {
715
+ for (;;) {
716
+ if (buildError) return;
717
+ const seg = pending.shift();
718
+ if (!seg) return;
719
+ const segBytesRes = await this.loadSegmentBytesResult(seg);
720
+ if (Result.isError(segBytesRes)) {
721
+ buildError = segBytesRes.error.message;
722
+ return;
723
+ }
724
+ const segBytes = segBytesRes.value;
725
+ const bit = seg.segment_index - startSegment;
726
+ const maskBit = 1 << bit;
727
+ const local = new Map<bigint, number>();
728
+ let processedRecords = 0;
729
+ for (const recRes of iterateBlockRecordsResult(segBytes)) {
730
+ if (Result.isError(recRes)) {
731
+ buildError = recRes.error.message;
732
+ return;
733
+ }
734
+ if (recRes.value.routingKey.byteLength === 0) continue;
735
+ const fp = siphash24(secret, recRes.value.routingKey);
736
+ const prev = local.get(fp) ?? 0;
737
+ local.set(fp, prev | maskBit);
738
+ processedRecords += 1;
739
+ if (processedRecords % 256 === 0) {
740
+ await this.yieldBackgroundWork();
741
+ }
742
+ }
743
+ for (const [fp, mask] of local.entries()) {
744
+ const prev = maskByFp.get(fp) ?? 0;
745
+ maskByFp.set(fp, prev | mask);
746
+ }
747
+ local.clear();
748
+ await this.yieldBackgroundWork();
749
+ }
750
+ })()
751
+ );
752
+ }
753
+ await Promise.all(workers);
754
+ if (buildError) return invalidIndexBuild(buildError);
755
+ const fingerprints = Array.from(maskByFp.keys()).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
756
+ const masks = fingerprints.map((fp) => maskByFp.get(fp) ?? 0);
757
+ const fuseRes = buildBinaryFuseResult(fingerprints);
758
+ if (Result.isError(fuseRes)) return invalidIndexBuild(fuseRes.error.message);
759
+ const { filter, bytes } = fuseRes.value;
760
+ const shash = streamHash16Hex(stream);
761
+ const endSegment = startSegment + this.span - 1;
762
+ const runId = `l0-${startSegment.toString().padStart(16, "0")}-${endSegment.toString().padStart(16, "0")}-${Date.now()}`;
763
+ const objectKey = indexRunObjectKey(shash, runId);
764
+ const run: IndexRun = {
765
+ meta: {
766
+ runId,
767
+ level: 0,
768
+ startSegment,
769
+ endSegment,
770
+ objectKey,
771
+ filterLen: bytes.byteLength,
772
+ recordCount: fingerprints.length,
773
+ },
774
+ runType: RUN_TYPE_MASK16,
775
+ filterBytes: bytes,
776
+ filter,
777
+ fingerprints,
778
+ masks,
779
+ };
780
+ return Result.ok(run);
781
+ }
782
+
783
+ private async isRoutingConfigured(stream: string): Promise<boolean> {
784
+ const streamRow = await this.db.getStream(stream);
785
+ const contentType = streamRow?.content_type.split(";")[0]?.trim().toLowerCase() ?? null;
786
+ if (contentType != null && contentType !== "application/json") return true;
787
+ if (!this.registry) return false;
788
+ const regRes = await this.registry.getRegistryResult(stream);
789
+ if (Result.isError(regRes)) return false;
790
+ return !!regRes.value.routingKey;
791
+ }
792
+
793
+ private async persistRunResult(run: IndexRun, stream?: string): Promise<Result<number, IndexBuildError>> {
794
+ const payloadRes = encodeIndexRunResult(run);
795
+ if (Result.isError(payloadRes)) return invalidIndexBuild(payloadRes.error.message);
796
+ const payload = payloadRes.value;
797
+ if (this.metrics) {
798
+ this.metrics.record("tieredstore.index.bytes.written", payload.byteLength, "bytes", { level: String(run.meta.level) }, stream);
799
+ }
800
+ try {
801
+ await retry(
802
+ () => this.os.put(run.meta.objectKey, payload, { contentLength: payload.byteLength }),
803
+ {
804
+ retries: this.cfg.objectStoreRetries,
805
+ baseDelayMs: this.cfg.objectStoreBaseDelayMs,
806
+ maxDelayMs: this.cfg.objectStoreMaxDelayMs,
807
+ timeoutMs: this.cfg.objectStoreTimeoutMs,
808
+ }
809
+ );
810
+ } catch (e: any) {
811
+ return invalidIndexBuild(String(e?.message ?? e));
812
+ }
813
+ this.runDiskCache?.put(run.meta.objectKey, payload);
814
+ this.runCache.put(run.meta.objectKey, run, payload.byteLength);
815
+ return Result.ok(payload.byteLength);
816
+ }
817
+
818
+ private async loadRunResult(meta: IndexRunRow): Promise<Result<IndexRun | null, IndexBuildError>> {
819
+ const cached = this.runCache.get(meta.object_key);
820
+ if (cached) return Result.ok(cached);
821
+ let bytes: Uint8Array | null = null;
822
+ if (this.runDiskCache) {
823
+ try {
824
+ bytes = this.runDiskCache.get(meta.object_key);
825
+ } catch {
826
+ this.runDiskCache.remove(meta.object_key);
827
+ }
828
+ }
829
+ if (!bytes) {
830
+ try {
831
+ bytes = await retry(
832
+ async () => {
833
+ const data = await this.os.get(meta.object_key);
834
+ if (!data) throw dsError(`missing index run ${meta.object_key}`);
835
+ return data;
836
+ },
837
+ {
838
+ retries: this.cfg.objectStoreRetries,
839
+ baseDelayMs: this.cfg.objectStoreBaseDelayMs,
840
+ maxDelayMs: this.cfg.objectStoreMaxDelayMs,
841
+ timeoutMs: this.cfg.objectStoreTimeoutMs,
842
+ }
843
+ );
844
+ } catch (e: unknown) {
845
+ return invalidIndexBuild(errorMessage(e));
846
+ }
847
+ if (this.metrics) {
848
+ this.metrics.record("tieredstore.index.bytes.read", bytes.byteLength, "bytes", { level: String(meta.level) }, meta.stream);
849
+ }
850
+ this.runDiskCache?.put(meta.object_key, bytes);
851
+ }
852
+ const runRes = decodeIndexRunResult(bytes);
853
+ if (Result.isError(runRes)) {
854
+ this.runDiskCache?.remove(meta.object_key);
855
+ return Result.ok(null);
856
+ }
857
+ const run = runRes.value;
858
+ run.meta.runId = meta.run_id;
859
+ run.meta.objectKey = meta.object_key;
860
+ run.meta.level = meta.level;
861
+ run.meta.startSegment = meta.start_segment;
862
+ run.meta.endSegment = meta.end_segment;
863
+ run.meta.filterLen = meta.filter_len;
864
+ run.meta.recordCount = meta.record_count;
865
+ this.runCache.put(meta.object_key, run, meta.size_bytes);
866
+ return Result.ok(run);
867
+ }
868
+
869
+ private async loadSegmentBytesResult(seg: SegmentRow): Promise<Result<Uint8Array, IndexBuildError>> {
870
+ try {
871
+ const data = await loadSegmentBytesCached(
872
+ this.os,
873
+ seg,
874
+ this.segmentCache,
875
+ {
876
+ retries: this.cfg.objectStoreRetries,
877
+ baseDelayMs: this.cfg.objectStoreBaseDelayMs,
878
+ maxDelayMs: this.cfg.objectStoreMaxDelayMs,
879
+ timeoutMs: this.cfg.objectStoreTimeoutMs,
880
+ }
881
+ );
882
+ return Result.ok(data);
883
+ } catch (e: unknown) {
884
+ return invalidIndexBuild(errorMessage(e));
885
+ }
886
+ }
887
+
888
+ private recordCacheStats(): void {
889
+ if (!this.metrics) return;
890
+ const mem = this.runCache.stats();
891
+ this.metrics.record("tieredstore.index.run_cache.used_bytes", mem.usedBytes, "bytes", { cache: "mem" });
892
+ this.metrics.record("tieredstore.index.run_cache.entries", mem.entries, "count", { cache: "mem" });
893
+ const deltaHits = mem.hits - this.lastRunCacheHits;
894
+ const deltaMisses = mem.misses - this.lastRunCacheMisses;
895
+ const deltaEvict = mem.evictions - this.lastRunCacheEvictions;
896
+ if (deltaHits > 0) this.metrics.record("tieredstore.index.run_cache.hits", deltaHits, "count", { cache: "mem" });
897
+ if (deltaMisses > 0) this.metrics.record("tieredstore.index.run_cache.misses", deltaMisses, "count", { cache: "mem" });
898
+ if (deltaEvict > 0) this.metrics.record("tieredstore.index.run_cache.evictions", deltaEvict, "count", { cache: "mem" });
899
+ this.lastRunCacheHits = mem.hits;
900
+ this.lastRunCacheMisses = mem.misses;
901
+ this.lastRunCacheEvictions = mem.evictions;
902
+
903
+ if (this.runDiskCache) {
904
+ const disk = this.runDiskCache.stats();
905
+ this.metrics.record("tieredstore.index.run_cache.used_bytes", disk.usedBytes, "bytes", { cache: "disk" });
906
+ this.metrics.record("tieredstore.index.run_cache.entries", disk.entryCount, "count", { cache: "disk" });
907
+ const dh = disk.hits - this.lastDiskHits;
908
+ const dm = disk.misses - this.lastDiskMisses;
909
+ const de = disk.evictions - this.lastDiskEvictions;
910
+ const db = disk.bytesAdded - this.lastDiskBytesAdded;
911
+ if (dh > 0) this.metrics.record("tieredstore.index.run_cache.hits", dh, "count", { cache: "disk" });
912
+ if (dm > 0) this.metrics.record("tieredstore.index.run_cache.misses", dm, "count", { cache: "disk" });
913
+ if (de > 0) this.metrics.record("tieredstore.index.run_cache.evictions", de, "count", { cache: "disk" });
914
+ if (db > 0) this.metrics.record("tieredstore.index.run_cache.bytes_added", db, "bytes", { cache: "disk" });
915
+ this.lastDiskHits = disk.hits;
916
+ this.lastDiskMisses = disk.misses;
917
+ this.lastDiskEvictions = disk.evictions;
918
+ this.lastDiskBytesAdded = disk.bytesAdded;
919
+ }
920
+ }
921
+
922
+ private async recordActiveRuns(stream: string): Promise<void> {
923
+ if (!this.metrics) return;
924
+ const runs = await this.db.listIndexRuns(stream);
925
+ this.metrics.record("tieredstore.index.active_runs", runs.length, "count", undefined, stream);
926
+ const byLevel = new Map<number, number>();
927
+ for (const r of runs) byLevel.set(r.level, (byLevel.get(r.level) ?? 0) + 1);
928
+ for (const [level, count] of byLevel.entries()) {
929
+ this.metrics.record("tieredstore.index.active_runs", count, "count", { level: String(level) }, stream);
930
+ }
931
+ }
932
+ }
933
+
934
+ function binarySearch(arr: bigint[], target: bigint): number {
935
+ let lo = 0;
936
+ let hi = arr.length - 1;
937
+ while (lo <= hi) {
938
+ const mid = (lo + hi) >> 1;
939
+ const v = arr[mid];
940
+ if (v === target) return mid;
941
+ if (v < target) lo = mid + 1;
942
+ else hi = mid - 1;
943
+ }
944
+ return -1;
945
+ }
946
+
947
+ // segmentObjectKey handles stream hash + path.