@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/sqlite/adapter.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import type { HostRuntime } from "../runtime/host_runtime.ts";
|
|
3
|
+
import { detectHostRuntime } from "../runtime/host_runtime.ts";
|
|
1
4
|
import { dsError } from "../util/ds_error.ts";
|
|
2
5
|
export interface SqliteStatement {
|
|
3
6
|
get(...params: any[]): any;
|
|
@@ -10,15 +13,49 @@ export interface SqliteStatement {
|
|
|
10
13
|
export interface SqliteDatabase {
|
|
11
14
|
exec(sql: string): void;
|
|
12
15
|
query(sql: string): SqliteStatement;
|
|
16
|
+
prepare(sql: string): SqliteStatement;
|
|
13
17
|
transaction<T>(fn: () => T): () => T;
|
|
14
18
|
close(): void;
|
|
15
19
|
}
|
|
16
20
|
|
|
21
|
+
type SqliteAdapterRuntimeCounts = {
|
|
22
|
+
open_connections: number;
|
|
23
|
+
prepared_statements: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const sqliteAdapterRuntimeCounts: SqliteAdapterRuntimeCounts = {
|
|
27
|
+
open_connections: 0,
|
|
28
|
+
prepared_statements: 0,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function incrementSqliteConnection(): void {
|
|
32
|
+
sqliteAdapterRuntimeCounts.open_connections += 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function decrementSqliteConnection(): void {
|
|
36
|
+
sqliteAdapterRuntimeCounts.open_connections = Math.max(0, sqliteAdapterRuntimeCounts.open_connections - 1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function incrementPreparedStatement(): void {
|
|
40
|
+
sqliteAdapterRuntimeCounts.prepared_statements += 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function decrementPreparedStatement(): void {
|
|
44
|
+
sqliteAdapterRuntimeCounts.prepared_statements = Math.max(0, sqliteAdapterRuntimeCounts.prepared_statements - 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getSqliteAdapterRuntimeCounts(): SqliteAdapterRuntimeCounts {
|
|
48
|
+
return { ...sqliteAdapterRuntimeCounts };
|
|
49
|
+
}
|
|
50
|
+
|
|
17
51
|
class BunStatementAdapter implements SqliteStatement {
|
|
18
52
|
private readonly stmt: any;
|
|
53
|
+
private readonly onFinalize?: () => void;
|
|
54
|
+
private finalized = false;
|
|
19
55
|
|
|
20
|
-
constructor(stmt: any) {
|
|
56
|
+
constructor(stmt: any, onFinalize?: () => void) {
|
|
21
57
|
this.stmt = stmt;
|
|
58
|
+
this.onFinalize = onFinalize;
|
|
22
59
|
}
|
|
23
60
|
|
|
24
61
|
get(...params: any[]): any {
|
|
@@ -38,23 +75,58 @@ class BunStatementAdapter implements SqliteStatement {
|
|
|
38
75
|
}
|
|
39
76
|
|
|
40
77
|
finalize(): void {
|
|
41
|
-
if (
|
|
78
|
+
if (this.finalized) return;
|
|
79
|
+
this.finalized = true;
|
|
80
|
+
try {
|
|
81
|
+
if (typeof this.stmt.finalize === "function") this.stmt.finalize();
|
|
82
|
+
} finally {
|
|
83
|
+
this.onFinalize?.();
|
|
84
|
+
}
|
|
42
85
|
}
|
|
43
86
|
}
|
|
44
87
|
|
|
45
88
|
class BunDatabaseAdapter implements SqliteDatabase {
|
|
46
89
|
private readonly db: any;
|
|
90
|
+
private preparedStatementCount = 0;
|
|
91
|
+
private closed = false;
|
|
92
|
+
private readonly statementCache = new Map<string, BunStatementAdapter>();
|
|
47
93
|
|
|
48
94
|
constructor(db: any) {
|
|
49
95
|
this.db = db;
|
|
96
|
+
incrementSqliteConnection();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private trackStatement(): () => void {
|
|
100
|
+
let released = false;
|
|
101
|
+
this.preparedStatementCount += 1;
|
|
102
|
+
incrementPreparedStatement();
|
|
103
|
+
return () => {
|
|
104
|
+
if (released) return;
|
|
105
|
+
released = true;
|
|
106
|
+
this.preparedStatementCount = Math.max(0, this.preparedStatementCount - 1);
|
|
107
|
+
decrementPreparedStatement();
|
|
108
|
+
};
|
|
50
109
|
}
|
|
51
110
|
|
|
52
111
|
exec(sql: string): void {
|
|
53
112
|
this.db.exec(sql);
|
|
54
113
|
}
|
|
55
114
|
|
|
115
|
+
prepare(sql: string): SqliteStatement {
|
|
116
|
+
return new BunStatementAdapter(this.db.query(sql), this.trackStatement());
|
|
117
|
+
}
|
|
118
|
+
|
|
56
119
|
query(sql: string): SqliteStatement {
|
|
57
|
-
|
|
120
|
+
const cached = this.statementCache.get(sql);
|
|
121
|
+
if (cached) return cached;
|
|
122
|
+
let adapter: BunStatementAdapter;
|
|
123
|
+
const release = this.trackStatement();
|
|
124
|
+
adapter = new BunStatementAdapter(this.db.query(sql), () => {
|
|
125
|
+
this.statementCache.delete(sql);
|
|
126
|
+
release();
|
|
127
|
+
});
|
|
128
|
+
this.statementCache.set(sql, adapter);
|
|
129
|
+
return adapter;
|
|
58
130
|
}
|
|
59
131
|
|
|
60
132
|
transaction<T>(fn: () => T): () => T {
|
|
@@ -62,15 +134,37 @@ class BunDatabaseAdapter implements SqliteDatabase {
|
|
|
62
134
|
}
|
|
63
135
|
|
|
64
136
|
close(): void {
|
|
65
|
-
this.
|
|
137
|
+
if (this.closed) return;
|
|
138
|
+
this.closed = true;
|
|
139
|
+
const cachedStatements = Array.from(this.statementCache.values());
|
|
140
|
+
this.statementCache.clear();
|
|
141
|
+
for (const stmt of cachedStatements) {
|
|
142
|
+
try {
|
|
143
|
+
stmt.finalize?.();
|
|
144
|
+
} catch {
|
|
145
|
+
// Ignore finalizer failures during shutdown.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
while (this.preparedStatementCount > 0) {
|
|
149
|
+
this.preparedStatementCount -= 1;
|
|
150
|
+
decrementPreparedStatement();
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
this.db.close();
|
|
154
|
+
} finally {
|
|
155
|
+
decrementSqliteConnection();
|
|
156
|
+
}
|
|
66
157
|
}
|
|
67
158
|
}
|
|
68
159
|
|
|
69
160
|
class NodeStatementAdapter implements SqliteStatement {
|
|
70
161
|
private readonly stmt: any;
|
|
162
|
+
private readonly onFinalize?: () => void;
|
|
163
|
+
private finalized = false;
|
|
71
164
|
|
|
72
|
-
constructor(stmt: any) {
|
|
165
|
+
constructor(stmt: any, onFinalize?: () => void) {
|
|
73
166
|
this.stmt = stmt;
|
|
167
|
+
this.onFinalize = onFinalize;
|
|
74
168
|
}
|
|
75
169
|
|
|
76
170
|
get(...params: any[]): any {
|
|
@@ -90,7 +184,13 @@ class NodeStatementAdapter implements SqliteStatement {
|
|
|
90
184
|
}
|
|
91
185
|
|
|
92
186
|
finalize(): void {
|
|
93
|
-
if (
|
|
187
|
+
if (this.finalized) return;
|
|
188
|
+
this.finalized = true;
|
|
189
|
+
try {
|
|
190
|
+
if (typeof this.stmt.finalize === "function") this.stmt.finalize();
|
|
191
|
+
} finally {
|
|
192
|
+
this.onFinalize?.();
|
|
193
|
+
}
|
|
94
194
|
}
|
|
95
195
|
}
|
|
96
196
|
|
|
@@ -98,19 +198,50 @@ class NodeDatabaseAdapter implements SqliteDatabase {
|
|
|
98
198
|
private txDepth = 0;
|
|
99
199
|
private txCounter = 0;
|
|
100
200
|
private readonly db: any;
|
|
201
|
+
private preparedStatementCount = 0;
|
|
202
|
+
private closed = false;
|
|
203
|
+
private readonly statementCache = new Map<string, NodeStatementAdapter>();
|
|
101
204
|
|
|
102
205
|
constructor(db: any) {
|
|
103
206
|
this.db = db;
|
|
207
|
+
incrementSqliteConnection();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private trackStatement(): () => void {
|
|
211
|
+
let released = false;
|
|
212
|
+
this.preparedStatementCount += 1;
|
|
213
|
+
incrementPreparedStatement();
|
|
214
|
+
return () => {
|
|
215
|
+
if (released) return;
|
|
216
|
+
released = true;
|
|
217
|
+
this.preparedStatementCount = Math.max(0, this.preparedStatementCount - 1);
|
|
218
|
+
decrementPreparedStatement();
|
|
219
|
+
};
|
|
104
220
|
}
|
|
105
221
|
|
|
106
222
|
exec(sql: string): void {
|
|
107
223
|
this.db.exec(sql);
|
|
108
224
|
}
|
|
109
225
|
|
|
226
|
+
prepare(sql: string): SqliteStatement {
|
|
227
|
+
const stmt = this.db.prepare(sql);
|
|
228
|
+
if (typeof stmt?.setReadBigInts === "function") stmt.setReadBigInts(true);
|
|
229
|
+
return new NodeStatementAdapter(stmt, this.trackStatement());
|
|
230
|
+
}
|
|
231
|
+
|
|
110
232
|
query(sql: string): SqliteStatement {
|
|
233
|
+
const cached = this.statementCache.get(sql);
|
|
234
|
+
if (cached) return cached;
|
|
111
235
|
const stmt = this.db.prepare(sql);
|
|
112
236
|
if (typeof stmt?.setReadBigInts === "function") stmt.setReadBigInts(true);
|
|
113
|
-
|
|
237
|
+
let adapter: NodeStatementAdapter;
|
|
238
|
+
const release = this.trackStatement();
|
|
239
|
+
adapter = new NodeStatementAdapter(stmt, () => {
|
|
240
|
+
this.statementCache.delete(sql);
|
|
241
|
+
release();
|
|
242
|
+
});
|
|
243
|
+
this.statementCache.set(sql, adapter);
|
|
244
|
+
return adapter;
|
|
114
245
|
}
|
|
115
246
|
|
|
116
247
|
transaction<T>(fn: () => T): () => T {
|
|
@@ -144,21 +275,61 @@ class NodeDatabaseAdapter implements SqliteDatabase {
|
|
|
144
275
|
}
|
|
145
276
|
|
|
146
277
|
close(): void {
|
|
147
|
-
this.
|
|
278
|
+
if (this.closed) return;
|
|
279
|
+
this.closed = true;
|
|
280
|
+
const cachedStatements = Array.from(this.statementCache.values());
|
|
281
|
+
this.statementCache.clear();
|
|
282
|
+
for (const stmt of cachedStatements) {
|
|
283
|
+
try {
|
|
284
|
+
stmt.finalize?.();
|
|
285
|
+
} catch {
|
|
286
|
+
// Ignore finalizer failures during shutdown.
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
while (this.preparedStatementCount > 0) {
|
|
290
|
+
this.preparedStatementCount -= 1;
|
|
291
|
+
decrementPreparedStatement();
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
this.db.close();
|
|
295
|
+
} finally {
|
|
296
|
+
decrementSqliteConnection();
|
|
297
|
+
}
|
|
148
298
|
}
|
|
149
299
|
}
|
|
150
300
|
|
|
151
301
|
let openImpl: ((path: string) => SqliteDatabase) | null = null;
|
|
302
|
+
let openImplRuntime: HostRuntime | null = null;
|
|
303
|
+
let runtimeOverride: HostRuntime | null = null;
|
|
304
|
+
const require = createRequire(import.meta.url);
|
|
152
305
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
306
|
+
function selectedRuntime(): HostRuntime {
|
|
307
|
+
return runtimeOverride ?? detectHostRuntime();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function buildOpenImpl(runtime: HostRuntime): (path: string) => SqliteDatabase {
|
|
311
|
+
if (runtime === "bun") {
|
|
312
|
+
const { Database } = require("bun:sqlite") as { Database: new (path: string) => any };
|
|
313
|
+
return (path: string) => new BunDatabaseAdapter(new Database(path));
|
|
314
|
+
}
|
|
315
|
+
const { DatabaseSync } = require("node:sqlite") as { DatabaseSync: new (path: string) => any };
|
|
316
|
+
return (path: string) => new NodeDatabaseAdapter(new DatabaseSync(path));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function setSqliteRuntimeOverride(runtime: HostRuntime | null): void {
|
|
320
|
+
runtimeOverride = runtime;
|
|
321
|
+
if (runtimeOverride && openImplRuntime && runtimeOverride !== openImplRuntime) {
|
|
322
|
+
openImpl = null;
|
|
323
|
+
openImplRuntime = null;
|
|
324
|
+
}
|
|
159
325
|
}
|
|
160
326
|
|
|
161
327
|
export function openSqliteDatabase(path: string): SqliteDatabase {
|
|
328
|
+
const runtime = selectedRuntime();
|
|
329
|
+
if (!openImpl || openImplRuntime !== runtime) {
|
|
330
|
+
openImpl = buildOpenImpl(runtime);
|
|
331
|
+
openImplRuntime = runtime;
|
|
332
|
+
}
|
|
162
333
|
if (!openImpl) throw dsError("sqlite adapter not initialized");
|
|
163
334
|
return openImpl(path);
|
|
164
335
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { detectHostRuntime } from "../runtime/host_runtime.ts";
|
|
3
|
+
import { getSqliteAdapterRuntimeCounts } from "./adapter.ts";
|
|
4
|
+
import type { SqliteRuntimeMemoryStats } from "../runtime_memory.ts";
|
|
5
|
+
|
|
6
|
+
const SQLITE_STATUS_MEMORY_USED = 0;
|
|
7
|
+
const SQLITE_STATUS_PAGECACHE_USED = 1;
|
|
8
|
+
const SQLITE_STATUS_PAGECACHE_OVERFLOW = 2;
|
|
9
|
+
const SQLITE_STATUS_MALLOC_COUNT = 9;
|
|
10
|
+
|
|
11
|
+
type SqliteStatus64Fn = (op: number, currentPtr: number, highwaterPtr: number, resetFlag: number) => number;
|
|
12
|
+
type BunFfiModule = {
|
|
13
|
+
dlopen: (
|
|
14
|
+
path: string,
|
|
15
|
+
symbols: {
|
|
16
|
+
sqlite3_status64: {
|
|
17
|
+
args: ["i32", "ptr", "ptr", "i32"];
|
|
18
|
+
returns: "i32";
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
) => { symbols: { sqlite3_status64: SqliteStatus64Fn }; close: () => void };
|
|
22
|
+
ptr: (value: TypedArray) => number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type TypedArray = BigInt64Array | BigUint64Array | Int32Array | Uint32Array;
|
|
26
|
+
|
|
27
|
+
type SqliteLibraryHandle = {
|
|
28
|
+
sqlite3_status64: SqliteStatus64Fn;
|
|
29
|
+
close: () => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type CachedSqliteRuntimeStats = {
|
|
33
|
+
at_ms: number;
|
|
34
|
+
value: SqliteRuntimeMemoryStats;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let ffiModule: BunFfiModule | null | undefined;
|
|
38
|
+
let sqliteLibrary: SqliteLibraryHandle | null | undefined;
|
|
39
|
+
let cachedRuntimeStats: CachedSqliteRuntimeStats | null = null;
|
|
40
|
+
const require = createRequire(import.meta.url);
|
|
41
|
+
|
|
42
|
+
function sqliteLibraryNames(): string[] {
|
|
43
|
+
if (process.platform === "darwin") {
|
|
44
|
+
return ["libsqlite3.dylib", "/usr/lib/libsqlite3.dylib"];
|
|
45
|
+
}
|
|
46
|
+
if (process.platform === "linux") {
|
|
47
|
+
return ["libsqlite3.so.0", "libsqlite3.so", "/usr/lib/x86_64-linux-gnu/libsqlite3.so.0", "/lib/x86_64-linux-gnu/libsqlite3.so.0"];
|
|
48
|
+
}
|
|
49
|
+
return ["libsqlite3.so", "libsqlite3.dylib"];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadBunFfi(): BunFfiModule | null {
|
|
53
|
+
if (ffiModule !== undefined) return ffiModule;
|
|
54
|
+
if (detectHostRuntime() !== "bun") {
|
|
55
|
+
ffiModule = null;
|
|
56
|
+
return ffiModule;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
ffiModule = require("bun:ffi") as BunFfiModule;
|
|
60
|
+
} catch {
|
|
61
|
+
ffiModule = null;
|
|
62
|
+
}
|
|
63
|
+
return ffiModule;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function loadSqliteLibrary(): SqliteLibraryHandle | null {
|
|
67
|
+
if (sqliteLibrary !== undefined) return sqliteLibrary;
|
|
68
|
+
const ffi = loadBunFfi();
|
|
69
|
+
if (!ffi) {
|
|
70
|
+
sqliteLibrary = null;
|
|
71
|
+
return sqliteLibrary;
|
|
72
|
+
}
|
|
73
|
+
for (const name of sqliteLibraryNames()) {
|
|
74
|
+
try {
|
|
75
|
+
const lib = ffi.dlopen(name, {
|
|
76
|
+
sqlite3_status64: {
|
|
77
|
+
args: ["i32", "ptr", "ptr", "i32"],
|
|
78
|
+
returns: "i32",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
sqliteLibrary = {
|
|
82
|
+
sqlite3_status64: lib.symbols.sqlite3_status64,
|
|
83
|
+
close: () => lib.close(),
|
|
84
|
+
};
|
|
85
|
+
return sqliteLibrary;
|
|
86
|
+
} catch {
|
|
87
|
+
// Try the next library name.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
sqliteLibrary = null;
|
|
91
|
+
return sqliteLibrary;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readStatus64(
|
|
95
|
+
lib: SqliteLibraryHandle,
|
|
96
|
+
ffi: BunFfiModule,
|
|
97
|
+
op: number
|
|
98
|
+
): { current: number; highwater: number } | null {
|
|
99
|
+
const current = new BigInt64Array(1);
|
|
100
|
+
const highwater = new BigInt64Array(1);
|
|
101
|
+
try {
|
|
102
|
+
const rc = lib.sqlite3_status64(op, ffi.ptr(current), ffi.ptr(highwater), 0);
|
|
103
|
+
if (rc !== 0) return null;
|
|
104
|
+
return {
|
|
105
|
+
current: Number(current[0]),
|
|
106
|
+
highwater: Number(highwater[0]),
|
|
107
|
+
};
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function readSqliteRuntimeMemoryStats(ttlMs = 1_000): SqliteRuntimeMemoryStats {
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
if (cachedRuntimeStats && now - cachedRuntimeStats.at_ms < Math.max(0, ttlMs)) {
|
|
116
|
+
return cachedRuntimeStats.value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const adapterCounts = getSqliteAdapterRuntimeCounts();
|
|
120
|
+
const unavailable: SqliteRuntimeMemoryStats = {
|
|
121
|
+
available: false,
|
|
122
|
+
source: "unavailable",
|
|
123
|
+
memory_used_bytes: 0,
|
|
124
|
+
memory_highwater_bytes: 0,
|
|
125
|
+
pagecache_used_slots: 0,
|
|
126
|
+
pagecache_used_slots_highwater: 0,
|
|
127
|
+
pagecache_overflow_bytes: 0,
|
|
128
|
+
pagecache_overflow_highwater_bytes: 0,
|
|
129
|
+
malloc_count: 0,
|
|
130
|
+
malloc_count_highwater: 0,
|
|
131
|
+
open_connections: adapterCounts.open_connections,
|
|
132
|
+
prepared_statements: adapterCounts.prepared_statements,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const ffi = loadBunFfi();
|
|
136
|
+
const lib = loadSqliteLibrary();
|
|
137
|
+
if (!ffi || !lib) {
|
|
138
|
+
cachedRuntimeStats = { at_ms: now, value: unavailable };
|
|
139
|
+
return unavailable;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const memoryUsed = readStatus64(lib, ffi, SQLITE_STATUS_MEMORY_USED);
|
|
143
|
+
const pagecacheUsed = readStatus64(lib, ffi, SQLITE_STATUS_PAGECACHE_USED);
|
|
144
|
+
const pagecacheOverflow = readStatus64(lib, ffi, SQLITE_STATUS_PAGECACHE_OVERFLOW);
|
|
145
|
+
const mallocCount = readStatus64(lib, ffi, SQLITE_STATUS_MALLOC_COUNT);
|
|
146
|
+
|
|
147
|
+
const stats: SqliteRuntimeMemoryStats = {
|
|
148
|
+
available: memoryUsed != null,
|
|
149
|
+
source: memoryUsed != null ? "sqlite3_status64" : "unavailable",
|
|
150
|
+
memory_used_bytes: Math.max(0, Math.floor(memoryUsed?.current ?? 0)),
|
|
151
|
+
memory_highwater_bytes: Math.max(0, Math.floor(memoryUsed?.highwater ?? 0)),
|
|
152
|
+
pagecache_used_slots: Math.max(0, Math.floor(pagecacheUsed?.current ?? 0)),
|
|
153
|
+
pagecache_used_slots_highwater: Math.max(0, Math.floor(pagecacheUsed?.highwater ?? 0)),
|
|
154
|
+
pagecache_overflow_bytes: Math.max(0, Math.floor(pagecacheOverflow?.current ?? 0)),
|
|
155
|
+
pagecache_overflow_highwater_bytes: Math.max(0, Math.floor(pagecacheOverflow?.highwater ?? 0)),
|
|
156
|
+
malloc_count: Math.max(0, Math.floor(mallocCount?.current ?? 0)),
|
|
157
|
+
malloc_count_highwater: Math.max(0, Math.floor(mallocCount?.highwater ?? 0)),
|
|
158
|
+
open_connections: adapterCounts.open_connections,
|
|
159
|
+
prepared_statements: adapterCounts.prepared_statements,
|
|
160
|
+
};
|
|
161
|
+
cachedRuntimeStats = { at_ms: now, value: stats };
|
|
162
|
+
return stats;
|
|
163
|
+
}
|
package/src/stats.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SqliteDurableStore } from "./db/db";
|
|
2
2
|
import type { UploaderController } from "./uploader";
|
|
3
|
-
import type {
|
|
3
|
+
import type { MemoryPressureMonitor } from "./memory";
|
|
4
4
|
import type { BackpressureGate } from "./backpressure";
|
|
5
5
|
import type { IngestQueue } from "./ingest";
|
|
6
6
|
|
|
@@ -110,7 +110,7 @@ export class StatsReporter {
|
|
|
110
110
|
private readonly uploader: UploaderController;
|
|
111
111
|
private readonly ingest?: IngestQueue;
|
|
112
112
|
private readonly backpressure?: BackpressureGate;
|
|
113
|
-
private readonly memory?:
|
|
113
|
+
private readonly memory?: MemoryPressureMonitor;
|
|
114
114
|
|
|
115
115
|
constructor(
|
|
116
116
|
stats: StatsCollector,
|
|
@@ -118,7 +118,7 @@ export class StatsReporter {
|
|
|
118
118
|
uploader: UploaderController,
|
|
119
119
|
ingest?: IngestQueue,
|
|
120
120
|
backpressure?: BackpressureGate,
|
|
121
|
-
memory?:
|
|
121
|
+
memory?: MemoryPressureMonitor,
|
|
122
122
|
intervalMs = 60_000
|
|
123
123
|
) {
|
|
124
124
|
this.stats = stats;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { Result } from "better-result";
|
|
4
|
+
import type { SqliteDurableStore, SegmentRow } from "./db/db";
|
|
5
|
+
import type { ObjectStore } from "./objectstore/interface";
|
|
6
|
+
import type { SegmentDiskCache } from "./segment/cache";
|
|
7
|
+
import { loadSegmentBytesCached } from "./segment/cached_segment";
|
|
8
|
+
import { iterateBlocksResult } from "./segment/format";
|
|
9
|
+
import { dsError } from "./util/ds_error";
|
|
10
|
+
import { yieldToEventLoop } from "./util/yield";
|
|
11
|
+
|
|
12
|
+
export class StreamSizeReconciler {
|
|
13
|
+
private stopped = false;
|
|
14
|
+
private running: Promise<void> | null = null;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly db: SqliteDurableStore,
|
|
18
|
+
private readonly os: ObjectStore,
|
|
19
|
+
private readonly segmentCache?: SegmentDiskCache,
|
|
20
|
+
private readonly onMetadataChanged?: (stream: string) => void
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
start(): void {
|
|
24
|
+
if (this.running) return;
|
|
25
|
+
this.running = this.run().finally(() => {
|
|
26
|
+
this.running = null;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
stop(): void {
|
|
31
|
+
this.stopped = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async run(): Promise<void> {
|
|
35
|
+
while (!this.stopped) {
|
|
36
|
+
const streams = this.db.listStreamsMissingLogicalSize(8);
|
|
37
|
+
if (streams.length === 0) return;
|
|
38
|
+
for (const stream of streams) {
|
|
39
|
+
if (this.stopped) return;
|
|
40
|
+
try {
|
|
41
|
+
await this.reconcileStream(stream);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
const msg = String((e as any)?.message ?? e);
|
|
44
|
+
if (!this.stopped && !msg.includes("Statement has finalized")) {
|
|
45
|
+
// eslint-disable-next-line no-console
|
|
46
|
+
console.error("stream size reconcile failed", stream, e);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async reconcileStream(stream: string): Promise<void> {
|
|
54
|
+
for (let attempt = 0; attempt < 3 && !this.stopped; attempt++) {
|
|
55
|
+
const before = this.db.getStream(stream);
|
|
56
|
+
if (!before || this.db.isDeleted(before) || before.next_offset <= 0n) return;
|
|
57
|
+
if (before.logical_size_bytes > 0n) return;
|
|
58
|
+
|
|
59
|
+
const segments = this.db.listSegmentsForStream(stream);
|
|
60
|
+
let total = 0n;
|
|
61
|
+
for (const segment of segments) {
|
|
62
|
+
if (this.stopped) return;
|
|
63
|
+
total += await this.sumSegmentPayloadBytes(segment);
|
|
64
|
+
await yieldToEventLoop();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const after = this.db.getStream(stream);
|
|
68
|
+
if (!after || this.db.isDeleted(after)) return;
|
|
69
|
+
if (after.logical_size_bytes > 0n) return;
|
|
70
|
+
|
|
71
|
+
if (segments.length !== this.db.countSegmentsForStream(stream)) continue;
|
|
72
|
+
|
|
73
|
+
const finalTotal = total + after.wal_bytes;
|
|
74
|
+
if (finalTotal > after.logical_size_bytes) {
|
|
75
|
+
this.db.setStreamLogicalSizeBytes(stream, finalTotal);
|
|
76
|
+
this.onMetadataChanged?.(stream);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async sumSegmentPayloadBytes(segment: SegmentRow): Promise<bigint> {
|
|
83
|
+
const bytes = await this.loadSegmentBytes(segment);
|
|
84
|
+
let total = 0n;
|
|
85
|
+
for (const blockRes of iterateBlocksResult(bytes)) {
|
|
86
|
+
if (Result.isError(blockRes)) throw dsError(blockRes.error.message);
|
|
87
|
+
for (const record of blockRes.value.decoded.records) {
|
|
88
|
+
total += BigInt(record.payload.byteLength);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return total;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async loadSegmentBytes(segment: SegmentRow): Promise<Uint8Array> {
|
|
95
|
+
if (existsSync(segment.local_path)) {
|
|
96
|
+
return new Uint8Array(await readFile(segment.local_path));
|
|
97
|
+
}
|
|
98
|
+
return loadSegmentBytesCached(this.os, segment, this.segmentCache);
|
|
99
|
+
}
|
|
100
|
+
}
|