@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.
Files changed (91) hide show
  1. package/CONTRIBUTING.md +8 -0
  2. package/package.json +2 -1
  3. package/src/app.ts +290 -17
  4. package/src/app_core.ts +1833 -698
  5. package/src/app_local.ts +144 -4
  6. package/src/auto_tune.ts +62 -0
  7. package/src/bootstrap.ts +159 -1
  8. package/src/concurrency_gate.ts +108 -0
  9. package/src/config.ts +116 -14
  10. package/src/db/db.ts +1201 -131
  11. package/src/db/schema.ts +308 -8
  12. package/src/foreground_activity.ts +55 -0
  13. package/src/index/indexer.ts +254 -124
  14. package/src/index/lexicon_file_cache.ts +261 -0
  15. package/src/index/lexicon_format.ts +93 -0
  16. package/src/index/lexicon_indexer.ts +789 -0
  17. package/src/index/secondary_indexer.ts +824 -0
  18. package/src/index/secondary_schema.ts +105 -0
  19. package/src/ingest.ts +10 -12
  20. package/src/manifest.ts +143 -8
  21. package/src/memory.ts +183 -8
  22. package/src/metrics.ts +15 -29
  23. package/src/metrics_emitter.ts +26 -3
  24. package/src/notifier.ts +121 -5
  25. package/src/objectstore/accounting.ts +92 -0
  26. package/src/objectstore/mock_r2.ts +1 -1
  27. package/src/objectstore/r2.ts +17 -1
  28. package/src/profiles/evlog/schema.ts +234 -0
  29. package/src/profiles/evlog.ts +299 -0
  30. package/src/profiles/generic.ts +47 -0
  31. package/src/profiles/index.ts +205 -0
  32. package/src/profiles/metrics/block_format.ts +109 -0
  33. package/src/profiles/metrics/normalize.ts +366 -0
  34. package/src/profiles/metrics/schema.ts +319 -0
  35. package/src/profiles/metrics.ts +85 -0
  36. package/src/profiles/profile.ts +225 -0
  37. package/src/{touch/engine.ts → profiles/stateProtocol/changes.ts} +3 -20
  38. package/src/profiles/stateProtocol/routes.ts +389 -0
  39. package/src/profiles/stateProtocol/types.ts +6 -0
  40. package/src/profiles/stateProtocol/validation.ts +51 -0
  41. package/src/profiles/stateProtocol.ts +100 -0
  42. package/src/read_filter.ts +468 -0
  43. package/src/reader.ts +2151 -164
  44. package/src/runtime/host_runtime.ts +5 -0
  45. package/src/runtime_memory.ts +200 -0
  46. package/src/runtime_memory_sampler.ts +235 -0
  47. package/src/schema/read_json.ts +43 -0
  48. package/src/schema/registry.ts +563 -59
  49. package/src/search/agg_format.ts +638 -0
  50. package/src/search/aggregate.ts +389 -0
  51. package/src/search/binary/codec.ts +162 -0
  52. package/src/search/binary/docset.ts +67 -0
  53. package/src/search/binary/restart_strings.ts +181 -0
  54. package/src/search/binary/varint.ts +34 -0
  55. package/src/search/bitset.ts +19 -0
  56. package/src/search/col_format.ts +382 -0
  57. package/src/search/col_runtime.ts +59 -0
  58. package/src/search/column_encoding.ts +43 -0
  59. package/src/search/companion_file_cache.ts +319 -0
  60. package/src/search/companion_format.ts +313 -0
  61. package/src/search/companion_manager.ts +1086 -0
  62. package/src/search/companion_plan.ts +218 -0
  63. package/src/search/fts_format.ts +423 -0
  64. package/src/search/fts_runtime.ts +333 -0
  65. package/src/search/query.ts +875 -0
  66. package/src/search/schema.ts +245 -0
  67. package/src/segment/cache.ts +93 -2
  68. package/src/segment/cached_segment.ts +89 -0
  69. package/src/segment/format.ts +108 -36
  70. package/src/segment/segmenter.ts +79 -5
  71. package/src/segment/segmenter_worker.ts +35 -6
  72. package/src/segment/segmenter_workers.ts +42 -12
  73. package/src/server.ts +150 -36
  74. package/src/sqlite/adapter.ts +185 -14
  75. package/src/sqlite/runtime_stats.ts +163 -0
  76. package/src/stats.ts +3 -3
  77. package/src/stream_size_reconciler.ts +100 -0
  78. package/src/touch/canonical_change.ts +7 -0
  79. package/src/touch/live_metrics.ts +94 -64
  80. package/src/touch/live_templates.ts +15 -1
  81. package/src/touch/manager.ts +166 -88
  82. package/src/touch/{interpreter_worker.ts → processor_worker.ts} +19 -14
  83. package/src/touch/spec.ts +95 -92
  84. package/src/touch/touch_journal.ts +4 -0
  85. package/src/touch/worker_pool.ts +8 -14
  86. package/src/touch/worker_protocol.ts +3 -3
  87. package/src/uploader.ts +77 -6
  88. package/src/util/bloom256.ts +2 -2
  89. package/src/util/byte_lru.ts +73 -0
  90. package/src/util/lru.ts +8 -0
  91. package/src/util/stream_paths.ts +19 -0
package/src/uploader.ts CHANGED
@@ -13,15 +13,26 @@ import { LruCache } from "./util/lru";
13
13
  import type { StatsCollector } from "./stats";
14
14
  import type { BackpressureGate } from "./backpressure";
15
15
  import { dsError } from "./util/ds_error.ts";
16
+ import { RuntimeMemorySampler } from "./runtime_memory_sampler";
16
17
 
17
18
  export type UploaderController = {
18
19
  start(): void;
19
20
  stop(hard?: boolean): void;
20
21
  countSegmentsWaiting(): number;
21
- setHooks(hooks: { onSegmentsUploaded?: (stream: string) => void } | undefined): void;
22
+ getMemoryStats?: () => {
23
+ inflight_segments: number;
24
+ inflight_segment_bytes: number;
25
+ manifest_inflight_streams: number;
26
+ };
27
+ setHooks(hooks: UploaderHooks | undefined): void;
22
28
  publishManifest(stream: string): Promise<void>;
23
29
  };
24
30
 
31
+ export type UploaderHooks = {
32
+ onSegmentsUploaded?: (stream: string) => void;
33
+ onMetadataChanged?: (stream: string) => void;
34
+ };
35
+
25
36
  export class Uploader {
26
37
  private readonly config: Config;
27
38
  private readonly db: SqliteDurableStore;
@@ -29,13 +40,15 @@ export class Uploader {
29
40
  private readonly diskCache?: SegmentDiskCache;
30
41
  private readonly stats?: StatsCollector;
31
42
  private readonly gate?: BackpressureGate;
43
+ private readonly memorySampler?: RuntimeMemorySampler;
32
44
  private timer: any | null = null;
33
45
  private running = false;
34
46
  private stopping = false;
35
47
  private readonly inflight = new Set<string>();
36
48
  private readonly failures = new FailureTracker(1024);
37
- private hooks?: { onSegmentsUploaded?: (stream: string) => void };
49
+ private hooks?: UploaderHooks;
38
50
  private readonly manifestInflight = new Set<string>();
51
+ private inflightSegmentBytes = 0;
39
52
 
40
53
  constructor(
41
54
  config: Config,
@@ -44,7 +57,8 @@ export class Uploader {
44
57
  diskCache?: SegmentDiskCache,
45
58
  stats?: StatsCollector,
46
59
  gate?: BackpressureGate,
47
- hooks?: { onSegmentsUploaded?: (stream: string) => void }
60
+ hooks?: UploaderHooks,
61
+ memorySampler?: RuntimeMemorySampler
48
62
  ) {
49
63
  this.config = config;
50
64
  this.db = db;
@@ -53,9 +67,10 @@ export class Uploader {
53
67
  this.stats = stats;
54
68
  this.gate = gate;
55
69
  this.hooks = hooks;
70
+ this.memorySampler = memorySampler;
56
71
  }
57
72
 
58
- setHooks(hooks: { onSegmentsUploaded?: (stream: string) => void } | undefined): void {
73
+ setHooks(hooks: UploaderHooks | undefined): void {
59
74
  this.hooks = hooks;
60
75
  }
61
76
 
@@ -78,12 +93,20 @@ export class Uploader {
78
93
  return this.db.countPendingSegments();
79
94
  }
80
95
 
96
+ getMemoryStats(): { inflight_segments: number; inflight_segment_bytes: number; manifest_inflight_streams: number } {
97
+ return {
98
+ inflight_segments: this.inflight.size,
99
+ inflight_segment_bytes: this.inflightSegmentBytes,
100
+ manifest_inflight_streams: this.manifestInflight.size,
101
+ };
102
+ }
103
+
81
104
  private async tick(): Promise<void> {
82
105
  if (this.stopping) return;
83
106
  if (this.running) return;
84
107
  this.running = true;
85
108
  try {
86
- const pending = this.db.pendingUploadSegments(1000);
109
+ const pending = this.db.pendingUploadHeads(1000);
87
110
  if (pending.length === 0) return;
88
111
 
89
112
  // Upload with bounded concurrency.
@@ -141,6 +164,7 @@ export class Uploader {
141
164
  if (!seg) return;
142
165
  if (this.inflight.has(seg.segment_id)) continue;
143
166
  this.inflight.add(seg.segment_id);
167
+ this.inflightSegmentBytes += Math.max(0, seg.size_bytes);
144
168
  try {
145
169
  try {
146
170
  await this.uploadOne(seg);
@@ -155,6 +179,7 @@ export class Uploader {
155
179
  }
156
180
  } finally {
157
181
  this.inflight.delete(seg.segment_id);
182
+ this.inflightSegmentBytes = Math.max(0, this.inflightSegmentBytes - Math.max(0, seg.size_bytes));
158
183
  }
159
184
  }
160
185
  }
@@ -163,6 +188,11 @@ export class Uploader {
163
188
  if (this.stopping) return;
164
189
  const shash = streamHash16Hex(seg.stream);
165
190
  const objectKey = segmentObjectKey(shash, seg.segment_index);
191
+ const leaveUploadPhase = this.memorySampler?.enter("upload", {
192
+ stream: seg.stream,
193
+ segment_index: seg.segment_index,
194
+ size_bytes: seg.size_bytes,
195
+ });
166
196
  try {
167
197
  const res = await retry(
168
198
  async () => {
@@ -180,11 +210,14 @@ export class Uploader {
180
210
  }
181
211
  );
182
212
  this.db.markSegmentUploaded(seg.segment_id, res.etag, this.db.nowMs());
213
+ this.hooks?.onMetadataChanged?.(seg.stream);
183
214
  if (this.stats) this.stats.recordUploadedBytes(seg.size_bytes);
184
215
  if (this.gate) this.gate.adjustOnUpload(seg.size_bytes);
185
216
  } catch (e) {
186
217
  this.failures.recordFailure(seg.stream);
187
218
  throw e;
219
+ } finally {
220
+ leaveUploadPhase?.();
188
221
  }
189
222
  }
190
223
 
@@ -218,6 +251,9 @@ export class Uploader {
218
251
 
219
252
  const uploadedThrough =
220
253
  uploadedPrefix === 0 ? -1n : readU64LE(meta.segment_offsets, (uploadedPrefix - 1) * 8) - 1n;
254
+ const unpublishedWalBytes = this.db.getWalBytesAfterOffset(stream, uploadedThrough);
255
+ const publishedLogicalSizeBytes =
256
+ srow.logical_size_bytes > unpublishedWalBytes ? srow.logical_size_bytes - unpublishedWalBytes : 0n;
221
257
 
222
258
  const manifestRow = this.db.getManifestRow(stream);
223
259
  const generation = manifestRow.generation + 1;
@@ -225,15 +261,49 @@ export class Uploader {
225
261
  const indexState = this.db.getIndexState(stream);
226
262
  const indexRuns = this.db.listIndexRuns(stream);
227
263
  const retiredRuns = this.db.listRetiredIndexRuns(stream);
264
+ const secondaryIndexStates = this.db.listSecondaryIndexStates(stream);
265
+ const secondaryIndexRuns = secondaryIndexStates.flatMap((state) => this.db.listSecondaryIndexRuns(stream, state.index_name));
266
+ const retiredSecondaryIndexRuns = secondaryIndexStates.flatMap((state) =>
267
+ this.db.listRetiredSecondaryIndexRuns(stream, state.index_name)
268
+ );
269
+ const lexiconIndexStates = this.db.listLexiconIndexStates(stream);
270
+ const lexiconIndexRuns = lexiconIndexStates.flatMap((state) =>
271
+ this.db.listLexiconIndexRuns(stream, state.source_kind, state.source_name)
272
+ );
273
+ const retiredLexiconIndexRuns = lexiconIndexStates.flatMap((state) =>
274
+ this.db.listRetiredLexiconIndexRuns(stream, state.source_kind, state.source_name)
275
+ );
276
+ const searchCompanionPlan = this.db.getSearchCompanionPlan(stream);
277
+ const searchSegmentCompanions = this.db.listSearchSegmentCompanions(stream);
278
+ let profileJson: Record<string, any> | null = null;
279
+ const profileRow = this.db.getStreamProfile(stream);
280
+ if (profileRow) {
281
+ try {
282
+ profileJson = JSON.parse(profileRow.profile_json);
283
+ } catch {
284
+ this.failures.recordFailure(stream);
285
+ throw dsError(`invalid profile_json for ${stream}`);
286
+ }
287
+ }
228
288
  const manifestRes = buildManifestResult({
229
289
  streamName: stream,
230
290
  streamRow: srow,
291
+ publishedLogicalSizeBytes,
292
+ profileJson,
231
293
  segmentMeta: meta,
232
294
  uploadedPrefixCount: uploadedPrefix,
233
295
  generation,
234
296
  indexState,
235
297
  indexRuns,
236
298
  retiredRuns,
299
+ secondaryIndexStates,
300
+ secondaryIndexRuns,
301
+ retiredSecondaryIndexRuns,
302
+ lexiconIndexStates,
303
+ lexiconIndexRuns,
304
+ retiredLexiconIndexRuns,
305
+ searchCompanionPlan,
306
+ searchSegmentCompanions,
237
307
  });
238
308
  if (Result.isError(manifestRes)) {
239
309
  this.failures.recordFailure(stream);
@@ -261,7 +331,8 @@ export class Uploader {
261
331
  }
262
332
 
263
333
  // Commit point: advance uploaded_through and delete WAL prefix.
264
- this.db.commitManifest(stream, generation, putRes.etag, this.db.nowMs(), uploadedThrough);
334
+ this.db.commitManifest(stream, generation, putRes.etag, this.db.nowMs(), uploadedThrough, body.byteLength);
335
+ this.hooks?.onMetadataChanged?.(stream);
265
336
 
266
337
  // Local disk cleanup: delete newly uploaded segment files.
267
338
  if (uploadedPrefix > prevPrefix) {
@@ -1,13 +1,13 @@
1
1
  import { dsError } from "./ds_error.ts";
2
2
  /**
3
- * A tiny 256-bit bloom filter (2048 bits) with 3 hash probes.
3
+ * A tiny 256-bit bloom filter with 3 hash probes.
4
4
  *
5
5
  * This is used per DSB3 block to cheaply skip blocks during key-filtered reads.
6
6
  *
7
7
  * Correctness rule: bloom filter may have false positives but MUST NOT have false negatives.
8
8
  */
9
9
 
10
- const BITS = 2048n;
10
+ const BITS = 256n;
11
11
  const MASK64 = (1n << 64n) - 1n;
12
12
 
13
13
  function fnv1a64(data: Uint8Array): bigint {
@@ -0,0 +1,73 @@
1
+ export class ByteLruCache<K, V> {
2
+ private readonly map = new Map<K, { value: V; weight: number }>();
3
+ private readonly capacityBytes: number;
4
+ private readonly weigh: (value: V) => number;
5
+ private usedBytes = 0;
6
+
7
+ constructor(maxBytes: number, weigh: (value: V) => number) {
8
+ this.capacityBytes = Math.max(0, Math.floor(maxBytes));
9
+ this.weigh = weigh;
10
+ }
11
+
12
+ get maxBytes(): number {
13
+ return this.capacityBytes;
14
+ }
15
+
16
+ get size(): number {
17
+ return this.map.size;
18
+ }
19
+
20
+ get totalBytes(): number {
21
+ return this.usedBytes;
22
+ }
23
+
24
+ get(key: K): V | undefined {
25
+ const entry = this.map.get(key);
26
+ if (!entry) return undefined;
27
+ this.map.delete(key);
28
+ this.map.set(key, entry);
29
+ return entry.value;
30
+ }
31
+
32
+ set(key: K, value: V): boolean {
33
+ const weight = Math.max(0, Math.floor(this.weigh(value)));
34
+ if (this.capacityBytes <= 0 || weight > this.capacityBytes) {
35
+ this.delete(key);
36
+ return false;
37
+ }
38
+ const existing = this.map.get(key);
39
+ if (existing) {
40
+ this.usedBytes -= existing.weight;
41
+ this.map.delete(key);
42
+ }
43
+ this.map.set(key, { value, weight });
44
+ this.usedBytes += weight;
45
+ this.evict();
46
+ return true;
47
+ }
48
+
49
+ has(key: K): boolean {
50
+ return this.map.has(key);
51
+ }
52
+
53
+ delete(key: K): boolean {
54
+ const existing = this.map.get(key);
55
+ if (!existing) return false;
56
+ this.usedBytes -= existing.weight;
57
+ this.map.delete(key);
58
+ return true;
59
+ }
60
+
61
+ clear(): void {
62
+ this.map.clear();
63
+ this.usedBytes = 0;
64
+ }
65
+
66
+ private evict(): void {
67
+ while (this.usedBytes > this.capacityBytes && this.map.size > 0) {
68
+ const oldest = this.map.keys().next();
69
+ if (oldest.done) break;
70
+ this.delete(oldest.value);
71
+ }
72
+ }
73
+ }
package/src/util/lru.ts CHANGED
@@ -42,4 +42,12 @@ export class LruCache<K, V> {
42
42
  clear(): void {
43
43
  this.map.clear();
44
44
  }
45
+
46
+ values(): IterableIterator<V> {
47
+ return this.map.values();
48
+ }
49
+
50
+ entries(): IterableIterator<[K, V]> {
51
+ return this.map.entries();
52
+ }
45
53
  }
@@ -26,6 +26,25 @@ export function indexRunObjectKey(streamHash: string, runId: string): string {
26
26
  return `streams/${streamHash}/index/${runId}.idx`;
27
27
  }
28
28
 
29
+ export function secondaryIndexRunObjectKey(streamHash: string, indexName: string, runId: string): string {
30
+ return `streams/${streamHash}/secondary-index/${encodeURIComponent(indexName)}/${runId}.idx`;
31
+ }
32
+
33
+ export function lexiconRunObjectKey(
34
+ streamHash: string,
35
+ sourceKind: string,
36
+ sourceName: string,
37
+ runId: string
38
+ ): string {
39
+ const encodedKind = encodeURIComponent(sourceKind);
40
+ const encodedName = encodeURIComponent(sourceName === "" ? "__default__" : sourceName);
41
+ return `streams/${streamHash}/lexicon/${encodedKind}/${encodedName}/${runId}.lex`;
42
+ }
43
+
44
+ export function searchCompanionObjectKey(streamHash: string, segmentIndex: number, objectId: string): string {
45
+ return `streams/${streamHash}/segments/${pad16(segmentIndex)}-${objectId}.cix`;
46
+ }
47
+
29
48
  export function localSegmentPath(rootDir: string, streamHash: string, segmentIndex: number): string {
30
49
  return `${rootDir}/local/streams/${streamHash}/segments/${pad16(segmentIndex)}.bin`;
31
50
  }