@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,105 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Result } from "better-result";
3
+ import type { SchemaRegistry, SearchFieldConfig } from "../schema/registry";
4
+ import { canonicalizeExactValue, extractSearchExactTermsResult, extractSearchExactValuesResult, getSearchFieldBinding } from "../search/schema";
5
+ import { schemaVersionForOffset } from "../schema/read_json";
6
+ import { resolvePointerResult } from "../util/json_pointer";
7
+
8
+ export type SecondaryIndexField = {
9
+ name: string;
10
+ config: SearchFieldConfig;
11
+ };
12
+
13
+ export type SecondaryIndexTerm = {
14
+ index: SecondaryIndexField;
15
+ canonical: string;
16
+ bytes: Uint8Array;
17
+ };
18
+
19
+ function addRawValues(out: unknown[], value: unknown): void {
20
+ if (Array.isArray(value)) {
21
+ for (const item of value) addRawValues(out, item);
22
+ return;
23
+ }
24
+ out.push(value);
25
+ }
26
+
27
+ export function getConfiguredSecondaryIndexes(registry: SchemaRegistry): SecondaryIndexField[] {
28
+ const search = registry.search;
29
+ if (!search) return [];
30
+ return Object.entries(search.fields)
31
+ .filter(([, config]) => config.exact === true)
32
+ .map(([name, config]) => ({ name, config }));
33
+ }
34
+
35
+ export function hashSecondaryIndexField(index: SecondaryIndexField): string {
36
+ return createHash("sha256")
37
+ .update(
38
+ JSON.stringify({
39
+ name: index.name,
40
+ kind: index.config.kind,
41
+ bindings: index.config.bindings,
42
+ normalizer: index.config.normalizer ?? null,
43
+ analyzer: index.config.analyzer ?? null,
44
+ exact: index.config.exact === true,
45
+ })
46
+ )
47
+ .digest("hex");
48
+ }
49
+
50
+ export function canonicalizeSecondaryIndexValue(config: SearchFieldConfig, value: unknown): string | null {
51
+ return canonicalizeExactValue(config, value);
52
+ }
53
+
54
+ export function extractSecondaryIndexTermsResult(
55
+ registry: SchemaRegistry,
56
+ offset: bigint,
57
+ value: unknown
58
+ ): Result<SecondaryIndexTerm[], { message: string }> {
59
+ const termsRes = extractSearchExactTermsResult(registry, offset, value);
60
+ if (Result.isError(termsRes)) return termsRes;
61
+ return Result.ok(
62
+ termsRes.value.map((term) => ({
63
+ index: { name: term.field, config: term.config },
64
+ canonical: term.canonical,
65
+ bytes: term.bytes,
66
+ }))
67
+ );
68
+ }
69
+
70
+ export function extractSecondaryIndexValuesResult(
71
+ registry: SchemaRegistry,
72
+ offset: bigint,
73
+ value: unknown
74
+ ): Result<Map<string, string[]>, { message: string }> {
75
+ return extractSearchExactValuesResult(registry, offset, value);
76
+ }
77
+
78
+ export function extractSecondaryIndexValuesForFieldResult(
79
+ registry: SchemaRegistry,
80
+ offset: bigint,
81
+ value: unknown,
82
+ index: SecondaryIndexField
83
+ ): Result<string[], { message: string }> {
84
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
85
+ return Result.err({ message: "search fields require JSON object records" });
86
+ }
87
+ const version = schemaVersionForOffset(registry, offset);
88
+ const binding = getSearchFieldBinding(index.config, version);
89
+ if (!binding) return Result.ok([]);
90
+ const resolvedRes = resolvePointerResult(value, binding.jsonPointer);
91
+ if (Result.isError(resolvedRes)) return Result.err({ message: resolvedRes.error.message });
92
+ if (!resolvedRes.value.exists) return Result.ok([]);
93
+
94
+ const rawValues: unknown[] = [];
95
+ addRawValues(rawValues, resolvedRes.value.value);
96
+ const out: string[] = [];
97
+ const seen = new Set<string>();
98
+ for (const rawValue of rawValues) {
99
+ const canonical = canonicalizeExactValue(index.config, rawValue);
100
+ if (canonical == null || seen.has(canonical)) continue;
101
+ seen.add(canonical);
102
+ out.push(canonical);
103
+ }
104
+ return Result.ok(out);
105
+ }
package/src/ingest.ts ADDED
@@ -0,0 +1,309 @@
1
+ import type { Config } from "./config";
2
+ import type { StatsCollector } from "./stats";
3
+ import type { BackpressureGate } from "./backpressure";
4
+ import type { Metrics } from "./metrics";
5
+ import { Result } from "better-result";
6
+ import {
7
+ type AppendRow,
8
+ type AppendSuccess,
9
+ type ProducerInfo,
10
+ type StoreAppendBatch,
11
+ type StoreAppendError,
12
+ type StoreAppendResult,
13
+ type StoreAppendTask,
14
+ } from "./store/append";
15
+ import type { WalAppendStore } from "./store/wal_store";
16
+
17
+ export type AppendError = StoreAppendError | { kind: "overloaded" | "internal" };
18
+ export type AppendResult = Result<AppendSuccess, AppendError>;
19
+ export type { AppendRow, AppendSuccess, ProducerInfo } from "./store/append";
20
+
21
+ type AppendTask = {
22
+ stream: string;
23
+ baseAppendMs: bigint;
24
+ rows: AppendRow[];
25
+ contentType: string | null;
26
+ streamSeq: string | null;
27
+ producer: ProducerInfo | null;
28
+ close: boolean;
29
+ reservedBytes: number;
30
+ enqueuedAtMs?: number;
31
+ resolve: (r: AppendResult) => void;
32
+ };
33
+
34
+ export class IngestQueue {
35
+ private readonly cfg: Config;
36
+ private readonly wal: WalAppendStore;
37
+ private readonly stats?: StatsCollector;
38
+ private readonly gate?: BackpressureGate;
39
+ private readonly metrics?: Metrics;
40
+ private readonly q: AppendTask[] = [];
41
+ private timer: any | null = null;
42
+ private scheduled = false;
43
+ private queuedBytes = 0;
44
+ private lastBacklogWarnMs = 0;
45
+ private flushPromise: Promise<void> | null = null;
46
+ private flushRequested = false;
47
+
48
+ constructor(cfg: Config, wal: WalAppendStore, stats?: StatsCollector, gate?: BackpressureGate, metrics?: Metrics) {
49
+ this.cfg = cfg;
50
+ this.wal = wal;
51
+ this.stats = stats;
52
+ this.gate = gate;
53
+ this.metrics = metrics;
54
+ }
55
+
56
+ start(): void {
57
+ if (this.timer) return;
58
+ this.timer = setInterval(() => {
59
+ void this.flush();
60
+ }, this.cfg.ingestFlushIntervalMs);
61
+ }
62
+
63
+ async stop(): Promise<void> {
64
+ if (this.timer) clearInterval(this.timer);
65
+ this.timer = null;
66
+ await this.flushPromise;
67
+ if (this.q.length > 0) await this.flush();
68
+ }
69
+
70
+ /**
71
+ * Enqueue an append. This returns after the batch containing it has committed.
72
+ */
73
+ append(args: {
74
+ stream: string;
75
+ baseAppendMs: bigint;
76
+ rows: AppendRow[];
77
+ contentType: string | null;
78
+ streamSeq?: string | null;
79
+ producer?: ProducerInfo | null;
80
+ close?: boolean;
81
+ }, opts?: { bypassBackpressure?: boolean; priority?: "high" | "normal" }): Promise<AppendResult> {
82
+ const bytes = args.rows.reduce((acc, r) => acc + r.payload.byteLength, 0);
83
+ if (!opts?.bypassBackpressure) {
84
+ if (this.q.length >= this.cfg.ingestMaxQueueRequests || this.queuedBytes + bytes > this.cfg.ingestMaxQueueBytes) {
85
+ if (this.metrics) this.metrics.record("tieredstore.backpressure.over_limit", 1, "count", { reason: "queue" });
86
+ return Promise.resolve(Result.err({ kind: "overloaded" }));
87
+ }
88
+ if (this.gate && !this.gate.reserve(bytes)) {
89
+ if (this.metrics) this.metrics.record("tieredstore.backpressure.over_limit", 1, "count", { reason: "backlog" });
90
+ this.warnBacklog();
91
+ return Promise.resolve(Result.err({ kind: "overloaded" }));
92
+ }
93
+ }
94
+ this.queuedBytes += bytes;
95
+ return new Promise((resolve) => {
96
+ const task: AppendTask = {
97
+ stream: args.stream,
98
+ baseAppendMs: args.baseAppendMs,
99
+ rows: args.rows,
100
+ contentType: args.contentType ?? null,
101
+ streamSeq: args.streamSeq ?? null,
102
+ producer: args.producer ?? null,
103
+ close: args.close ?? false,
104
+ reservedBytes: opts?.bypassBackpressure ? 0 : bytes,
105
+ enqueuedAtMs: this.stats ? Date.now() : undefined,
106
+ resolve,
107
+ };
108
+ if (opts?.priority === "high") this.q.unshift(task);
109
+ else this.q.push(task);
110
+ // Opportunistic flush if the queue gets large.
111
+ if (!this.scheduled && this.q.length >= this.cfg.ingestMaxBatchRequests) {
112
+ this.scheduled = true;
113
+ setTimeout(() => {
114
+ this.scheduled = false;
115
+ void this.flush();
116
+ }, 0);
117
+ }
118
+ });
119
+ }
120
+
121
+ appendInternal(args: {
122
+ stream: string;
123
+ baseAppendMs: bigint;
124
+ rows: AppendRow[];
125
+ contentType: string | null;
126
+ }): Promise<AppendResult> {
127
+ return this.append(args, { bypassBackpressure: true, priority: "high" });
128
+ }
129
+
130
+ getQueueStats(): { requests: number; bytes: number } {
131
+ return { requests: this.q.length, bytes: this.queuedBytes };
132
+ }
133
+
134
+ getMemoryStats(): { queuedPayloadBytes: number; queuedRequests: number } {
135
+ return {
136
+ queuedPayloadBytes: this.queuedBytes,
137
+ queuedRequests: this.q.length,
138
+ };
139
+ }
140
+
141
+ isQueueFull(): boolean {
142
+ return this.q.length >= this.cfg.ingestMaxQueueRequests || this.queuedBytes >= this.cfg.ingestMaxQueueBytes;
143
+ }
144
+
145
+ private warnBacklog(): void {
146
+ if (!this.gate) return;
147
+ const now = Date.now();
148
+ if (now - this.lastBacklogWarnMs < 10_000) return;
149
+ this.lastBacklogWarnMs = now;
150
+ const current = this.gate.getCurrentBytes();
151
+ const max = this.gate.getMaxBytes();
152
+ const msg =
153
+ `[backpressure] local backlog ${formatBytes(current)} exceeds limit ${formatBytes(max)}; rejecting appends (DS_LOCAL_BACKLOG_MAX_BYTES)`;
154
+ // eslint-disable-next-line no-console
155
+ console.warn(msg);
156
+ }
157
+
158
+ async flush(): Promise<void> {
159
+ this.flushRequested = true;
160
+ if (this.flushPromise) return this.flushPromise;
161
+ this.flushPromise = this.runFlushLoop().finally(() => {
162
+ this.flushPromise = null;
163
+ });
164
+ return this.flushPromise;
165
+ }
166
+
167
+ private async runFlushLoop(): Promise<void> {
168
+ while (this.flushRequested) {
169
+ this.flushRequested = false;
170
+ await this.flushOnce();
171
+ }
172
+ }
173
+
174
+ private async flushOnce(): Promise<void> {
175
+ if (this.q.length === 0) return;
176
+ const flushStartMs = Date.now();
177
+ let busyWaitMs = 0;
178
+
179
+ // Drain up to limits.
180
+ const batch: AppendTask[] = [];
181
+ let batchBytes = 0;
182
+ let batchReservedBytes = 0;
183
+ let drainCount = 0;
184
+ while (drainCount < this.q.length && batch.length < this.cfg.ingestMaxBatchRequests && batchBytes < this.cfg.ingestMaxBatchBytes) {
185
+ const t = this.q[drainCount]!;
186
+ batch.push(t);
187
+ drainCount += 1;
188
+ for (const r of t.rows) batchBytes += r.payload.byteLength;
189
+ batchReservedBytes += t.reservedBytes;
190
+ }
191
+ if (drainCount > 0) {
192
+ this.q.splice(0, drainCount);
193
+ }
194
+ this.queuedBytes = Math.max(0, this.queuedBytes - batchBytes);
195
+
196
+ // Compute queue wait/backpressure stats before executing the batch.
197
+ let bpOverMs = 0;
198
+ if (this.stats) {
199
+ const budgetMs = this.stats.getBackpressureBudgetMs();
200
+ const nowMs = Date.now();
201
+ for (const t of batch) {
202
+ if (t.enqueuedAtMs == null) continue;
203
+ const waitMs = Math.max(0, nowMs - t.enqueuedAtMs);
204
+ if (waitMs > budgetMs) {
205
+ bpOverMs += waitMs - budgetMs;
206
+ }
207
+ }
208
+ }
209
+
210
+ let walBytesCommitted = 0;
211
+ let results: StoreAppendResult[] = [];
212
+ const storeBatch: StoreAppendTask[] = batch.map(({ stream, baseAppendMs, rows, contentType, streamSeq, producer, close }) => ({
213
+ stream,
214
+ baseAppendMs,
215
+ rows,
216
+ contentType,
217
+ streamSeq,
218
+ producer,
219
+ close,
220
+ }));
221
+
222
+ const sleep = (ms: number): Promise<void> => new Promise((res) => setTimeout(res, ms));
223
+
224
+ try {
225
+ const maxBusyMs = Math.max(0, this.cfg.ingestBusyTimeoutMs);
226
+ const startMs = Date.now();
227
+ let attempt = 0;
228
+ let retryLimitExceeded = false;
229
+ while (true) {
230
+ const batchResult = await this.wal.appendBatch(storeBatch);
231
+ if (Result.isOk(batchResult)) {
232
+ results = batchResult.value.results;
233
+ walBytesCommitted = batchResult.value.walBytesCommitted;
234
+ break;
235
+ }
236
+ if (!isRetryableBatchResult(batchResult) || maxBusyMs <= 0) {
237
+ retryLimitExceeded = true;
238
+ break;
239
+ }
240
+ const elapsed = Date.now() - startMs;
241
+ if (elapsed >= maxBusyMs) {
242
+ retryLimitExceeded = true;
243
+ break;
244
+ }
245
+ const delay = Math.min(200, 5 * 2 ** attempt);
246
+ attempt += 1;
247
+ busyWaitMs += delay;
248
+ await sleep(delay);
249
+ }
250
+ if (retryLimitExceeded) {
251
+ if (this.gate && batchReservedBytes > 0) this.gate.release(batchReservedBytes);
252
+ for (const t of batch) t.resolve(Result.err({ kind: "internal" }));
253
+ const elapsedNs = (Date.now() - flushStartMs) * 1_000_000;
254
+ if (this.metrics) {
255
+ this.metrics.record("tieredstore.ingest.flush.latency", elapsedNs, "ns");
256
+ if (busyWaitMs > 0) this.metrics.record("tieredstore.ingest.store_retry.wait", busyWaitMs * 1_000_000, "ns");
257
+ }
258
+ return;
259
+ }
260
+ if (this.gate) {
261
+ const reservedCommitted = Math.min(batchReservedBytes, walBytesCommitted);
262
+ this.gate.commit(walBytesCommitted, reservedCommitted);
263
+ const extra = batchReservedBytes - walBytesCommitted;
264
+ if (extra > 0) this.gate.release(extra);
265
+ }
266
+ if (this.stats && walBytesCommitted > 0) this.stats.recordWalCommitBytes(walBytesCommitted);
267
+ if (this.stats && bpOverMs > 0) this.stats.recordBackpressureOverMs(bpOverMs);
268
+ for (let i = 0; i < batch.length; i++) batch[i].resolve(toAppendResult(results[i]));
269
+ const elapsedNs = (Date.now() - flushStartMs) * 1_000_000;
270
+ if (this.metrics) {
271
+ this.metrics.record("tieredstore.ingest.flush.latency", elapsedNs, "ns");
272
+ if (busyWaitMs > 0) this.metrics.record("tieredstore.ingest.store_retry.wait", busyWaitMs * 1_000_000, "ns");
273
+ }
274
+ } catch (e) {
275
+ // If the whole transaction failed, all tasks are treated as internal errors.
276
+ // eslint-disable-next-line no-console
277
+ console.error("ingest tx failed", e);
278
+ if (this.gate && batchReservedBytes > 0) this.gate.release(batchReservedBytes);
279
+ for (const t of batch) t.resolve(Result.err({ kind: "internal" }));
280
+ const elapsedNs = (Date.now() - flushStartMs) * 1_000_000;
281
+ if (this.metrics) {
282
+ this.metrics.record("tieredstore.ingest.flush.latency", elapsedNs, "ns");
283
+ if (busyWaitMs > 0) this.metrics.record("tieredstore.ingest.store_retry.wait", busyWaitMs * 1_000_000, "ns");
284
+ }
285
+ }
286
+ }
287
+ }
288
+
289
+ function isRetryableBatchResult(result: StoreAppendBatch): boolean {
290
+ return Result.isError(result) && result.error.kind === "retryable";
291
+ }
292
+
293
+ function toAppendResult(result: StoreAppendResult | undefined): AppendResult {
294
+ if (!result) return Result.err({ kind: "internal" });
295
+ if (Result.isError(result)) return Result.err(result.error);
296
+ return Result.ok(result.value);
297
+ }
298
+
299
+ function formatBytes(bytes: number): string {
300
+ const units = ["b", "kb", "mb", "gb"];
301
+ let value = Math.max(0, bytes);
302
+ let idx = 0;
303
+ while (value >= 1024 && idx < units.length - 1) {
304
+ value /= 1024;
305
+ idx += 1;
306
+ }
307
+ const digits = idx === 0 ? 0 : 1;
308
+ return `${value.toFixed(digits)}${units[idx]}`;
309
+ }