@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/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
|
-
|
|
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?:
|
|
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?:
|
|
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:
|
|
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.
|
|
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) {
|
package/src/util/bloom256.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { dsError } from "./ds_error.ts";
|
|
2
2
|
/**
|
|
3
|
-
* A tiny 256-bit bloom filter
|
|
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 =
|
|
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
package/src/util/stream_paths.ts
CHANGED
|
@@ -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
|
}
|