@cloudbitmaps/core 0.2.0 → 0.4.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.
- package/dist/index.cjs +342 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +229 -9
- package/dist/index.d.ts +229 -9
- package/dist/index.js +339 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -91,6 +91,14 @@ function applyRemove(delta, remainder) {
|
|
|
91
91
|
delta.removes.add(remainder);
|
|
92
92
|
delta.adds.remove(remainder);
|
|
93
93
|
}
|
|
94
|
+
function applyAddAll(delta, ids) {
|
|
95
|
+
delta.adds.orInPlace(ids);
|
|
96
|
+
delta.removes.andNotInPlace(ids);
|
|
97
|
+
}
|
|
98
|
+
function applyRemoveAll(delta, ids) {
|
|
99
|
+
delta.removes.orInPlace(ids);
|
|
100
|
+
delta.adds.andNotInPlace(ids);
|
|
101
|
+
}
|
|
94
102
|
function effective(cold, delta) {
|
|
95
103
|
const out = cold.clone();
|
|
96
104
|
out.orInPlace(delta.adds);
|
|
@@ -171,6 +179,18 @@ function checkBudget(budget, projectedRequests, op) {
|
|
|
171
179
|
);
|
|
172
180
|
}
|
|
173
181
|
}
|
|
182
|
+
async function collectWithinBudget(source, budget, op) {
|
|
183
|
+
const out = [];
|
|
184
|
+
for await (const item of source) {
|
|
185
|
+
out.push(item);
|
|
186
|
+
if (budget !== null && out.length > budget.maxRequests) {
|
|
187
|
+
throw new BudgetExceededError(
|
|
188
|
+
`${op} would fan out to more than ${budget.maxRequests} units, over the per-op budget \u2014 the scan was abandoned at that point rather than completed, so no exact total is available. Raise \`budget.maxRequests\`, override it per-op, or set \`budget: false\``
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
174
194
|
|
|
175
195
|
// src/core/concurrency.ts
|
|
176
196
|
async function mapWithConcurrency(items, limit, fn) {
|
|
@@ -202,6 +222,19 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
202
222
|
return results;
|
|
203
223
|
}
|
|
204
224
|
|
|
225
|
+
// src/core/cooperative.ts
|
|
226
|
+
var YIELD_EVERY = 1024;
|
|
227
|
+
function yieldEvery(clock, everyN = YIELD_EVERY) {
|
|
228
|
+
if (clock === void 0) return () => null;
|
|
229
|
+
const pause = clock.yieldNow !== void 0 ? () => clock.yieldNow() : () => clock.sleep(1);
|
|
230
|
+
let n = 0;
|
|
231
|
+
return () => {
|
|
232
|
+
if (++n < everyN) return null;
|
|
233
|
+
n = 0;
|
|
234
|
+
return pause();
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
205
238
|
// src/core/keys.ts
|
|
206
239
|
var FIELD = " ";
|
|
207
240
|
var NO_NAMESPACE = "/";
|
|
@@ -384,6 +417,8 @@ async function withRetry(op, policy, deps) {
|
|
|
384
417
|
var DEFAULT_MAX_BITMAP_BYTES = 1 << 20;
|
|
385
418
|
var DEFAULT_MAX_RETRIES = 16;
|
|
386
419
|
var DEFAULT_INTERSECT_CONCURRENCY = 8;
|
|
420
|
+
var MUTATE_STAGE_IDS = 1 << 20;
|
|
421
|
+
var YIELD_EVERY_IDS = 1 << 14;
|
|
387
422
|
var InstantClock = class {
|
|
388
423
|
now() {
|
|
389
424
|
return 0;
|
|
@@ -397,6 +432,8 @@ var ZeroRng = class {
|
|
|
397
432
|
return 0;
|
|
398
433
|
}
|
|
399
434
|
};
|
|
435
|
+
var DEFAULT_MAX_WARM_SCAN_BYTES = 64 * 1024 * 1024;
|
|
436
|
+
var DEFAULT_WRITE_CONCURRENCY = 4;
|
|
400
437
|
var SegmentEngine = class {
|
|
401
438
|
warm;
|
|
402
439
|
cold;
|
|
@@ -412,6 +449,7 @@ var SegmentEngine = class {
|
|
|
412
449
|
/** Consistency for READ-path Warm fetches (precomputed; OCC RMW passes strong explicitly, see `warmGet`). */
|
|
413
450
|
readOpts;
|
|
414
451
|
writeConcurrency;
|
|
452
|
+
maxWarmScanBytes;
|
|
415
453
|
/** Resolved per-op budget (null = disabled); undefined deps ⇒ the generous default. See {@link checkBudget}. */
|
|
416
454
|
budget;
|
|
417
455
|
constructor(deps) {
|
|
@@ -425,7 +463,13 @@ var SegmentEngine = class {
|
|
|
425
463
|
this.rng = deps.rng ?? new ZeroRng();
|
|
426
464
|
this.occBackoff = deps.occBackoff ?? DEFAULT_OCC_BACKOFF;
|
|
427
465
|
this.readOpts = { consistent: (deps.warmReadConsistency ?? "strong") !== "eventual" };
|
|
428
|
-
this.writeConcurrency = deps.writeConcurrency ??
|
|
466
|
+
this.writeConcurrency = deps.writeConcurrency ?? DEFAULT_WRITE_CONCURRENCY;
|
|
467
|
+
this.maxWarmScanBytes = deps.maxWarmScanBytes ?? DEFAULT_MAX_WARM_SCAN_BYTES;
|
|
468
|
+
if (!Number.isFinite(this.maxWarmScanBytes) || this.maxWarmScanBytes < 1) {
|
|
469
|
+
throw new ValidationError(
|
|
470
|
+
`maxWarmScanBytes must be a finite number >= 1 (got ${String(deps.maxWarmScanBytes)})`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
429
473
|
this.budget = deps.budget === void 0 ? DEFAULT_BUDGET : deps.budget;
|
|
430
474
|
this.metrics = safeMetrics(deps.metrics ?? NOOP_METRICS);
|
|
431
475
|
this.metricsOn = this.metrics !== NOOP_METRICS;
|
|
@@ -439,10 +483,10 @@ var SegmentEngine = class {
|
|
|
439
483
|
return this.readModifyWrite(seg, chunkKey, (d) => applyRemove(d, remainder));
|
|
440
484
|
}
|
|
441
485
|
addMany(seg, ids) {
|
|
442
|
-
return this.mutateMany(seg, ids,
|
|
486
|
+
return this.mutateMany(seg, ids, applyAddAll);
|
|
443
487
|
}
|
|
444
488
|
removeMany(seg, ids) {
|
|
445
|
-
return this.mutateMany(seg, ids,
|
|
489
|
+
return this.mutateMany(seg, ids, applyRemoveAll);
|
|
446
490
|
}
|
|
447
491
|
async has(seg, id, opts) {
|
|
448
492
|
const { chunkKey, remainder } = splitId(id);
|
|
@@ -457,7 +501,7 @@ var SegmentEngine = class {
|
|
|
457
501
|
return cold ? cold.has(remainder) : false;
|
|
458
502
|
}
|
|
459
503
|
async count(seg) {
|
|
460
|
-
const warmRows = await this.collectWarm(seg);
|
|
504
|
+
const warmRows = await this.collectWarm(seg, "count", this.budget?.maxRequests ?? null);
|
|
461
505
|
const cardinalities = this.cold.cardinalities ? await this.cold.cardinalities(seg) : null;
|
|
462
506
|
if (!cardinalities) {
|
|
463
507
|
const chunkKeys = await this.chunkKeys(seg, warmRows);
|
|
@@ -489,7 +533,7 @@ var SegmentEngine = class {
|
|
|
489
533
|
return this.cold.sizeOf ? this.cold.sizeOf(seg) : Promise.resolve(null);
|
|
490
534
|
}
|
|
491
535
|
async *iterate(seg) {
|
|
492
|
-
const warmRows = await this.collectWarm(seg);
|
|
536
|
+
const warmRows = await this.collectWarm(seg, "iterate", this.budget?.maxRequests ?? null);
|
|
493
537
|
const chunkKeys = await this.chunkKeys(seg, warmRows);
|
|
494
538
|
checkBudget(this.budget, chunkKeys.length, "iterate");
|
|
495
539
|
const gen = await this.currentGen(seg);
|
|
@@ -523,9 +567,44 @@ var SegmentEngine = class {
|
|
|
523
567
|
* re-resolves fresh (bypassing the TTL), which can hop generations even sub-TTL — still whole/immutable per
|
|
524
568
|
* read, never torn. Full intra-op snapshot isolation is deferred (gap #4).
|
|
525
569
|
*/
|
|
526
|
-
|
|
570
|
+
/**
|
|
571
|
+
* `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
|
|
572
|
+
*
|
|
573
|
+
* The `exclude` operands are what make suppression **compose without materialising**. Applying suppression
|
|
574
|
+
* afterwards (intersect into a temp segment, then remove) writes an intermediate segment that nobody wants;
|
|
575
|
+
* applying it here folds it into the same chunk-aligned pass. It is also cheaper than it looks: an exclude
|
|
576
|
+
* operand is read **only at the keys that survived the intersection**, so a 61,000-chunk global opt-out list
|
|
577
|
+
* intersected against a narrow audience costs a handful of reads, not 61,000.
|
|
578
|
+
*/
|
|
579
|
+
intersect(segs, options) {
|
|
580
|
+
return this.combine(segs, options?.exclude ?? [], "all", "intersect", options);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* `segs[0] ∪ segs[1] ∪ …`, minus every id in `options.exclude` — streamed ascending.
|
|
584
|
+
*
|
|
585
|
+
* **Union is the one composite read with no chunk-skipping**, and that is a property of the operation rather
|
|
586
|
+
* than of this implementation: an id in *any* operand belongs to the result, so every chunk of every operand
|
|
587
|
+
* has to be read. `intersect` can prune a key absent from any operand; union cannot prune anything. It is
|
|
588
|
+
* budgeted exactly like `intersect` so a wide union is refused rather than quietly billed.
|
|
589
|
+
*/
|
|
590
|
+
union(segs, options) {
|
|
591
|
+
return this.combine(segs, options?.exclude ?? [], "any", "union", options);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* `seg \ (excludes[0] ∪ excludes[1] ∪ …)` — streamed ascending. Suppression on its own.
|
|
595
|
+
*
|
|
596
|
+
* Reads every chunk of `seg` (any of them may survive) but each exclude **only where it overlaps `seg`**, so
|
|
597
|
+
* the cost scales with the segment being filtered rather than with the size of the suppression list.
|
|
598
|
+
*/
|
|
599
|
+
andNot(seg, excludes, options) {
|
|
600
|
+
return this.combine([seg], excludes, "all", "andNot", options);
|
|
601
|
+
}
|
|
602
|
+
async *combine(segs, excludeSegs, mode, op, options) {
|
|
527
603
|
if (segs.length === 0) {
|
|
528
|
-
throw new ValidationError(
|
|
604
|
+
throw new ValidationError(`${op} requires at least one segment`);
|
|
605
|
+
}
|
|
606
|
+
if (op === "andNot" && excludeSegs.length === 0) {
|
|
607
|
+
throw new ValidationError("andNot requires at least one segment to subtract");
|
|
529
608
|
}
|
|
530
609
|
const limit = options?.concurrency ?? DEFAULT_INTERSECT_CONCURRENCY;
|
|
531
610
|
if (!Number.isInteger(limit) || limit < 1) {
|
|
@@ -534,34 +613,52 @@ var SegmentEngine = class {
|
|
|
534
613
|
);
|
|
535
614
|
}
|
|
536
615
|
const budget = resolvePerOpBudget(options?.budget, this.budget);
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
616
|
+
const extract = async (seg) => {
|
|
617
|
+
const warmRows = await this.collectWarm(seg, op, null);
|
|
618
|
+
const keys = await this.chunkKeys(seg, warmRows);
|
|
619
|
+
const gen = await this.currentGen(seg);
|
|
620
|
+
return { seg, warmRows, keys: new Set(keys), gen };
|
|
621
|
+
};
|
|
622
|
+
const [operands, excludes] = await Promise.all([
|
|
623
|
+
Promise.all(segs.map(extract)),
|
|
624
|
+
Promise.all(excludeSegs.map(extract))
|
|
625
|
+
]);
|
|
626
|
+
const candidates = [];
|
|
627
|
+
if (mode === "all") {
|
|
628
|
+
const pivot = operands.reduce((a, b) => a.keys.size <= b.keys.size ? a : b);
|
|
629
|
+
for (const k of pivot.keys) {
|
|
630
|
+
if (operands.every((o) => o.keys.has(k))) candidates.push(k);
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
const seen = /* @__PURE__ */ new Set();
|
|
634
|
+
for (const o of operands) for (const k of o.keys) seen.add(k);
|
|
635
|
+
for (const k of seen) candidates.push(k);
|
|
636
|
+
}
|
|
637
|
+
candidates.sort((a, b) => a - b);
|
|
638
|
+
const common = candidates;
|
|
639
|
+
let units = 0;
|
|
640
|
+
for (const key of common) {
|
|
641
|
+
for (const o of operands) if (mode === "all" || o.keys.has(key)) units += 1;
|
|
642
|
+
for (const e of excludes) if (e.keys.has(key)) units += 1;
|
|
643
|
+
}
|
|
644
|
+
checkBudget(budget, units, op);
|
|
552
645
|
if (this.metricsOn) {
|
|
553
646
|
const distinctKeys = /* @__PURE__ */ new Set();
|
|
554
|
-
for (const
|
|
555
|
-
for (const k of
|
|
647
|
+
for (const op2 of operands) {
|
|
648
|
+
for (const k of op2.keys) distinctKeys.add(k);
|
|
556
649
|
}
|
|
650
|
+
for (const e of excludes) for (const k of e.keys) distinctKeys.add(k);
|
|
557
651
|
this.metrics.onEvent({
|
|
558
652
|
kind: "intersect",
|
|
559
|
-
|
|
653
|
+
op,
|
|
654
|
+
operands: segs.length + excludes.length,
|
|
560
655
|
fetchedChunks: common.length,
|
|
656
|
+
// Never negative: under 'any' every candidate key came from some operand, so `distinctKeys` is a
|
|
657
|
+
// superset of `common`; excludes only ever add keys here.
|
|
561
658
|
skippedChunks: distinctKeys.size - common.length
|
|
562
659
|
});
|
|
563
660
|
}
|
|
564
|
-
const startAt = (key) => this.
|
|
661
|
+
const startAt = (key) => this.combineChunk(operands, excludes, mode, key).then(
|
|
565
662
|
(result) => ({ key, result }),
|
|
566
663
|
(error) => ({ key, result: null, error })
|
|
567
664
|
);
|
|
@@ -583,16 +680,33 @@ var SegmentEngine = class {
|
|
|
583
680
|
* first operand's effective chunk and mutates it in place — safe because `effectiveChunk` always returns
|
|
584
681
|
* a FRESH bitmap (never a cached/shared Cold instance), so this can't poison the HOT cache.
|
|
585
682
|
*/
|
|
586
|
-
async
|
|
683
|
+
async combineChunk(operands, excludes, mode, chunkKey) {
|
|
684
|
+
const present = mode === "all" ? operands : operands.filter((o) => o.keys.has(chunkKey));
|
|
685
|
+
if (present.length === 0) return null;
|
|
587
686
|
const chunks = await Promise.all(
|
|
588
|
-
|
|
687
|
+
present.map((op) => this.effectiveChunk(op.seg, chunkKey, op.warmRows, op.gen))
|
|
589
688
|
);
|
|
590
689
|
const acc = chunks[0];
|
|
591
690
|
for (let i = 1; i < chunks.length; i++) {
|
|
592
|
-
|
|
593
|
-
|
|
691
|
+
if (mode === "all") {
|
|
692
|
+
acc.andInPlace(chunks[i]);
|
|
693
|
+
if (acc.isEmpty) return null;
|
|
694
|
+
} else {
|
|
695
|
+
acc.orInPlace(chunks[i]);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (acc.isEmpty) return null;
|
|
699
|
+
const relevant = excludes.filter((e) => e.keys.has(chunkKey));
|
|
700
|
+
if (relevant.length > 0) {
|
|
701
|
+
const cuts = await Promise.all(
|
|
702
|
+
relevant.map((e) => this.effectiveChunk(e.seg, chunkKey, e.warmRows, e.gen))
|
|
703
|
+
);
|
|
704
|
+
for (const cut of cuts) {
|
|
705
|
+
acc.andNotInPlace(cut);
|
|
706
|
+
if (acc.isEmpty) return null;
|
|
707
|
+
}
|
|
594
708
|
}
|
|
595
|
-
return acc
|
|
709
|
+
return acc;
|
|
596
710
|
}
|
|
597
711
|
/**
|
|
598
712
|
* Materialize `seg[0] ∩ seg[1] ∩ …` into `dest` via batched `addMany` (streaming, bounded memory).
|
|
@@ -600,9 +714,30 @@ var SegmentEngine = class {
|
|
|
600
714
|
* `dest` is added to, not replaced.
|
|
601
715
|
*/
|
|
602
716
|
async intersectInto(dest, segs, options) {
|
|
603
|
-
|
|
717
|
+
await this.drainInto(dest, this.intersect(segs, options), options?.batchSize);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Materialize `segs[0] ∪ segs[1] ∪ …` (minus `options.exclude`) into `dest`. **Not atomic**, like
|
|
721
|
+
* {@link intersectInto}. See {@link union} for why this one cannot skip any chunk.
|
|
722
|
+
*/
|
|
723
|
+
async unionInto(dest, segs, options) {
|
|
724
|
+
await this.drainInto(dest, this.union(segs, options), options?.batchSize);
|
|
725
|
+
}
|
|
726
|
+
/** Materialize `seg \ (excludes…)` into `dest`. **Not atomic**, like {@link intersectInto}. */
|
|
727
|
+
async andNotInto(dest, seg, excludes, options) {
|
|
728
|
+
await this.drainInto(dest, this.andNot(seg, excludes, options), options?.batchSize);
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Stream a combine's results into `dest` in bounded batches.
|
|
732
|
+
*
|
|
733
|
+
* `dest` is **added to, not replaced**, and this is **not atomic** — ids land chunk-by-chunk, so a failure
|
|
734
|
+
* mid-stream leaves a partial result, exactly as `addMany` does. Batching keeps memory bounded no matter how
|
|
735
|
+
* large the result is.
|
|
736
|
+
*/
|
|
737
|
+
async drainInto(dest, source, batchSizeOption) {
|
|
738
|
+
const batchSize = Math.max(1, batchSizeOption ?? 4096);
|
|
604
739
|
let batch = [];
|
|
605
|
-
for await (const id of
|
|
740
|
+
for await (const id of source) {
|
|
606
741
|
batch.push(id);
|
|
607
742
|
if (batch.length >= batchSize) {
|
|
608
743
|
await this.addMany(dest, batch);
|
|
@@ -611,19 +746,77 @@ var SegmentEngine = class {
|
|
|
611
746
|
}
|
|
612
747
|
if (batch.length > 0) await this.addMany(dest, batch);
|
|
613
748
|
}
|
|
749
|
+
/**
|
|
750
|
+
* Group ids by chunk, then apply each chunk's whole batch in a single OCC read-modify-write.
|
|
751
|
+
*
|
|
752
|
+
* Accepts a **sync or async** iterable, so an Athena/BigQuery cursor streams straight in without the caller
|
|
753
|
+
* hand-batching `page → addMany(page)`.
|
|
754
|
+
*
|
|
755
|
+
* **The pending ids are held as BITMAPS, not as JS numbers, and that is the load-bearing choice.** Streaming
|
|
756
|
+
* input is unbounded by definition, so something has to give. The obvious answer — buffer N ids, flush them to
|
|
757
|
+
* the backend, repeat — bounds memory and is quietly catastrophic on cost: with ids arriving in arbitrary
|
|
758
|
+
* order, every flush touches nearly every chunk again, so an 11M-id stream at a 1M-id buffer would issue
|
|
759
|
+
* **11x** the OCC round-trips of a single pass. In a library whose headline claim is honest cost, silently
|
|
760
|
+
* multiplying someone's write bill by the size of their input is not a bounded-memory strategy; it is a
|
|
761
|
+
* different bug.
|
|
762
|
+
*
|
|
763
|
+
* So the staging buffer is capped, but it folds into per-chunk **compressed bitmaps** rather than being
|
|
764
|
+
* written out. Peak memory is the roaring representation of the ids seen so far — which is what the segment
|
|
765
|
+
* itself costs, and the floor for any implementation that writes each chunk exactly once — while the backend
|
|
766
|
+
* still sees exactly one read-modify-write per distinct chunk regardless of stream length. This is strictly
|
|
767
|
+
* less memory than the previous sync-only version, which held every remainder as an uncompressed JS number.
|
|
768
|
+
*
|
|
769
|
+
* There is no way to do better without a sorted stream: a chunk can only be written once it is known that no
|
|
770
|
+
* further ids for it will arrive, and unsorted input never gives that guarantee. Callers streaming *very*
|
|
771
|
+
* large sets want `bulkLoadCrbmGeneration` instead — it writes one immutable Cold object rather than tens of
|
|
772
|
+
* thousands of warm rows. The crossover is documented in the guide.
|
|
773
|
+
*/
|
|
614
774
|
async mutateMany(seg, ids, apply) {
|
|
615
775
|
const byChunk = /* @__PURE__ */ new Map();
|
|
616
|
-
|
|
776
|
+
const staged = /* @__PURE__ */ new Map();
|
|
777
|
+
let pending = 0;
|
|
778
|
+
const tickId = yieldEvery(this.clock, YIELD_EVERY_IDS);
|
|
779
|
+
const compact = () => {
|
|
780
|
+
for (const [chunkKey, rems] of staged) {
|
|
781
|
+
if (rems.length === 0) continue;
|
|
782
|
+
const existing = byChunk.get(chunkKey);
|
|
783
|
+
if (existing === void 0) byChunk.set(chunkKey, this.codec.fromValues(rems));
|
|
784
|
+
else existing.addMany(rems);
|
|
785
|
+
rems.length = 0;
|
|
786
|
+
}
|
|
787
|
+
pending = 0;
|
|
788
|
+
};
|
|
789
|
+
const stage = (id) => {
|
|
617
790
|
const { chunkKey, remainder } = splitId(id);
|
|
618
|
-
|
|
791
|
+
let rems = staged.get(chunkKey);
|
|
792
|
+
if (rems === void 0) {
|
|
793
|
+
rems = [];
|
|
794
|
+
staged.set(chunkKey, rems);
|
|
795
|
+
}
|
|
619
796
|
rems.push(remainder);
|
|
620
|
-
|
|
797
|
+
pending += 1;
|
|
798
|
+
};
|
|
799
|
+
if (Symbol.asyncIterator in ids) {
|
|
800
|
+
for await (const id of ids) {
|
|
801
|
+
stage(id);
|
|
802
|
+
if (pending >= MUTATE_STAGE_IDS) compact();
|
|
803
|
+
const pause = tickId();
|
|
804
|
+
if (pause !== null) await pause;
|
|
805
|
+
}
|
|
806
|
+
} else {
|
|
807
|
+
for (const id of ids) {
|
|
808
|
+
stage(id);
|
|
809
|
+
if (pending >= MUTATE_STAGE_IDS) compact();
|
|
810
|
+
const pause = tickId();
|
|
811
|
+
if (pause !== null) await pause;
|
|
812
|
+
}
|
|
621
813
|
}
|
|
814
|
+
compact();
|
|
622
815
|
await mapWithConcurrency(
|
|
623
816
|
[...byChunk],
|
|
624
817
|
this.writeConcurrency,
|
|
625
|
-
([chunkKey,
|
|
626
|
-
|
|
818
|
+
([chunkKey, bitmap]) => this.readModifyWrite(seg, chunkKey, (d) => {
|
|
819
|
+
apply(d, bitmap);
|
|
627
820
|
})
|
|
628
821
|
);
|
|
629
822
|
}
|
|
@@ -677,12 +870,22 @@ var SegmentEngine = class {
|
|
|
677
870
|
}
|
|
678
871
|
return row;
|
|
679
872
|
}
|
|
680
|
-
async collectWarm(seg) {
|
|
873
|
+
async collectWarm(seg, op, rowCeiling) {
|
|
681
874
|
const rows = /* @__PURE__ */ new Map();
|
|
682
875
|
let bytes = 0;
|
|
683
876
|
for await (const row of this.warm.listChunks(seg, this.readOpts)) {
|
|
684
877
|
rows.set(row.chunkKey, row.bytes);
|
|
685
878
|
bytes += row.bytes.length;
|
|
879
|
+
if (bytes > this.maxWarmScanBytes) {
|
|
880
|
+
throw new BudgetExceededError(
|
|
881
|
+
`${op} would hold more than ${this.maxWarmScanBytes} bytes of warm deltas resident for one segment \u2014 the scan was abandoned there rather than completed. Raise \`maxWarmScanBytes\`, narrow the segment, or compact it so fewer chunks carry warm deltas. (This ceiling is independent of \`budget\` and stays in force when \`budget: false\`.)`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
if (rowCeiling !== null && rows.size > rowCeiling) {
|
|
885
|
+
throw new BudgetExceededError(
|
|
886
|
+
`${op} would read more than ${rowCeiling} warm chunk rows, over the per-op budget \u2014 the scan was abandoned at that point rather than completed, so no exact total is available. Raise \`budget.maxRequests\`, override it per-op, or set \`budget: false\``
|
|
887
|
+
);
|
|
888
|
+
}
|
|
686
889
|
}
|
|
687
890
|
if (this.metricsOn) {
|
|
688
891
|
this.metrics.onEvent({
|
|
@@ -1193,22 +1396,44 @@ function validateChunkRef(ref) {
|
|
|
1193
1396
|
}
|
|
1194
1397
|
|
|
1195
1398
|
// src/core/crbm/crc32c.ts
|
|
1196
|
-
var
|
|
1399
|
+
var TABLES = (() => {
|
|
1197
1400
|
const POLY = 2197175160;
|
|
1198
|
-
const
|
|
1401
|
+
const t0 = new Uint32Array(256);
|
|
1199
1402
|
for (let n = 0; n < 256; n++) {
|
|
1200
1403
|
let c = n;
|
|
1201
|
-
for (let k = 0; k < 8; k++)
|
|
1202
|
-
|
|
1404
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? POLY ^ c >>> 1 : c >>> 1;
|
|
1405
|
+
t0[n] = c >>> 0;
|
|
1406
|
+
}
|
|
1407
|
+
const tables = [t0];
|
|
1408
|
+
for (let k = 1; k < 8; k++) {
|
|
1409
|
+
const prev = tables[k - 1];
|
|
1410
|
+
const next = new Uint32Array(256);
|
|
1411
|
+
for (let n = 0; n < 256; n++) {
|
|
1412
|
+
const v = prev[n];
|
|
1413
|
+
next[n] = (v >>> 8 ^ t0[v & 255]) >>> 0;
|
|
1203
1414
|
}
|
|
1204
|
-
|
|
1415
|
+
tables.push(next);
|
|
1205
1416
|
}
|
|
1206
|
-
return
|
|
1417
|
+
return tables;
|
|
1207
1418
|
})();
|
|
1419
|
+
var T0 = TABLES[0];
|
|
1420
|
+
var T1 = TABLES[1];
|
|
1421
|
+
var T2 = TABLES[2];
|
|
1422
|
+
var T3 = TABLES[3];
|
|
1423
|
+
var T4 = TABLES[4];
|
|
1424
|
+
var T5 = TABLES[5];
|
|
1425
|
+
var T6 = TABLES[6];
|
|
1426
|
+
var T7 = TABLES[7];
|
|
1208
1427
|
function crc32c(bytes, seed = 0) {
|
|
1209
1428
|
let crc = (seed ^ 4294967295) >>> 0;
|
|
1210
|
-
|
|
1211
|
-
|
|
1429
|
+
let i = 0;
|
|
1430
|
+
const n8 = bytes.length - bytes.length % 8;
|
|
1431
|
+
for (; i < n8; i += 8) {
|
|
1432
|
+
crc ^= bytes[i] | bytes[i + 1] << 8 | bytes[i + 2] << 16 | bytes[i + 3] << 24;
|
|
1433
|
+
crc = (T7[crc & 255] ^ T6[crc >>> 8 & 255] ^ T5[crc >>> 16 & 255] ^ T4[crc >>> 24 & 255] ^ T3[bytes[i + 4]] ^ T2[bytes[i + 5]] ^ T1[bytes[i + 6]] ^ T0[bytes[i + 7]]) >>> 0;
|
|
1434
|
+
}
|
|
1435
|
+
for (; i < bytes.length; i++) {
|
|
1436
|
+
crc = (T0[(crc ^ bytes[i]) & 255] ^ crc >>> 8) >>> 0;
|
|
1212
1437
|
}
|
|
1213
1438
|
return (crc ^ 4294967295) >>> 0;
|
|
1214
1439
|
}
|
|
@@ -1719,6 +1944,8 @@ function coldBlobReader(driver, key) {
|
|
|
1719
1944
|
};
|
|
1720
1945
|
}
|
|
1721
1946
|
var DEFAULT_CURRENT_GEN_TTL_MS = 2e3;
|
|
1947
|
+
var BULK_FLUSH_IDS = 1 << 20;
|
|
1948
|
+
var YIELD_EVERY_IDS2 = 1 << 14;
|
|
1722
1949
|
var DEFAULT_MAX_OPEN_SEGMENTS = 1024;
|
|
1723
1950
|
var DEFAULT_MAX_OPEN_INDEX_BYTES = 64 * 1024 * 1024;
|
|
1724
1951
|
var CrbmColdChunkSource = class {
|
|
@@ -1937,11 +2164,14 @@ var CrbmColdChunkSource = class {
|
|
|
1937
2164
|
};
|
|
1938
2165
|
function writeCrbmGeneration(driver, key, chunks, options = {}) {
|
|
1939
2166
|
const sorted = [...chunks].sort((a, b) => a.chunkKey - b.chunkKey);
|
|
2167
|
+
const tick = yieldEvery(options.clock);
|
|
1940
2168
|
return driver.putImmutable(key, async (sink) => {
|
|
1941
2169
|
const writer = new CrbmWriter(sink, { generation: key.generation, crypto: options.crypto });
|
|
1942
2170
|
for (const { chunkKey, bitmap } of sorted) {
|
|
1943
2171
|
if (bitmap.isEmpty) continue;
|
|
1944
2172
|
await writer.addChunk(chunkKey, bitmap.serialize(), bitmap.size);
|
|
2173
|
+
const pause = tick();
|
|
2174
|
+
if (pause !== null) await pause;
|
|
1945
2175
|
}
|
|
1946
2176
|
await writer.finish();
|
|
1947
2177
|
});
|
|
@@ -1949,6 +2179,7 @@ function writeCrbmGeneration(driver, key, chunks, options = {}) {
|
|
|
1949
2179
|
async function writeCrbmGenerationStream(driver, key, chunks, options = {}) {
|
|
1950
2180
|
const chunkKeys = [];
|
|
1951
2181
|
let cardinality = 0;
|
|
2182
|
+
const tick = yieldEvery(options.clock);
|
|
1952
2183
|
const { size, sha256 } = await driver.putImmutable(key, async (sink) => {
|
|
1953
2184
|
const writer = new CrbmWriter(sink, { generation: key.generation, crypto: options.crypto });
|
|
1954
2185
|
for await (const { chunkKey, bitmap } of chunks) {
|
|
@@ -1956,6 +2187,8 @@ async function writeCrbmGenerationStream(driver, key, chunks, options = {}) {
|
|
|
1956
2187
|
await writer.addChunk(chunkKey, bitmap.serialize(), bitmap.size);
|
|
1957
2188
|
chunkKeys.push(chunkKey);
|
|
1958
2189
|
cardinality += bitmap.size;
|
|
2190
|
+
const pause = tick();
|
|
2191
|
+
if (pause !== null) await pause;
|
|
1959
2192
|
}
|
|
1960
2193
|
await writer.finish();
|
|
1961
2194
|
});
|
|
@@ -1996,20 +2229,53 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
1996
2229
|
}
|
|
1997
2230
|
const codec = requireCodec(options.codec, "bulkLoadCrbmGeneration");
|
|
1998
2231
|
const byChunk = /* @__PURE__ */ new Map();
|
|
1999
|
-
|
|
2232
|
+
const pendingByChunk = /* @__PURE__ */ new Map();
|
|
2233
|
+
let pending = 0;
|
|
2234
|
+
const tickChunk = yieldEvery(options.clock);
|
|
2235
|
+
const tickId = yieldEvery(options.clock, YIELD_EVERY_IDS2);
|
|
2236
|
+
const flushPending = async () => {
|
|
2237
|
+
for (const [chunkKey, rems] of pendingByChunk) {
|
|
2238
|
+
if (rems.length === 0) continue;
|
|
2239
|
+
const existingBitmap = byChunk.get(chunkKey);
|
|
2240
|
+
if (existingBitmap === void 0) byChunk.set(chunkKey, codec.fromValues(rems));
|
|
2241
|
+
else existingBitmap.addMany(rems);
|
|
2242
|
+
rems.length = 0;
|
|
2243
|
+
const pause = tickChunk();
|
|
2244
|
+
if (pause !== null) await pause;
|
|
2245
|
+
}
|
|
2246
|
+
pending = 0;
|
|
2247
|
+
};
|
|
2248
|
+
const ingest = (id) => {
|
|
2000
2249
|
const { chunkKey, remainder } = splitId(id);
|
|
2001
|
-
let
|
|
2002
|
-
if (
|
|
2003
|
-
|
|
2004
|
-
|
|
2250
|
+
let bucket = pendingByChunk.get(chunkKey);
|
|
2251
|
+
if (bucket === void 0) {
|
|
2252
|
+
bucket = [];
|
|
2253
|
+
pendingByChunk.set(chunkKey, bucket);
|
|
2254
|
+
}
|
|
2255
|
+
bucket.push(remainder);
|
|
2256
|
+
return ++pending >= BULK_FLUSH_IDS;
|
|
2257
|
+
};
|
|
2258
|
+
if (Symbol.asyncIterator in ids) {
|
|
2259
|
+
for await (const id of ids) {
|
|
2260
|
+
if (ingest(id)) await flushPending();
|
|
2261
|
+
const pause = tickId();
|
|
2262
|
+
if (pause !== null) await pause;
|
|
2263
|
+
}
|
|
2264
|
+
} else {
|
|
2265
|
+
for (const id of ids) {
|
|
2266
|
+
if (ingest(id)) await flushPending();
|
|
2267
|
+
const pause = tickId();
|
|
2268
|
+
if (pause !== null) await pause;
|
|
2005
2269
|
}
|
|
2006
|
-
bitmap.add(remainder);
|
|
2007
2270
|
}
|
|
2271
|
+
await flushPending();
|
|
2008
2272
|
let cardinality = 0;
|
|
2009
2273
|
const chunks = [];
|
|
2010
2274
|
for (const [chunkKey, bitmap] of byChunk) {
|
|
2011
2275
|
cardinality += bitmap.size;
|
|
2012
2276
|
chunks.push({ chunkKey, bitmap });
|
|
2277
|
+
const pause = tickChunk();
|
|
2278
|
+
if (pause !== null) await pause;
|
|
2013
2279
|
}
|
|
2014
2280
|
if (options.keystore !== void 0 && options.registry === void 0) {
|
|
2015
2281
|
throw new ValidationError("bulk-load encryption requires a registry to store the wrapped DEK");
|
|
@@ -2032,7 +2298,10 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
2032
2298
|
crypto = { aead: minted.aead, aadFor: (scope) => aadFor(key, key.generation, scope) };
|
|
2033
2299
|
}
|
|
2034
2300
|
}
|
|
2035
|
-
const { size, sha256 } = await writeCrbmGeneration(driver, key, chunks, {
|
|
2301
|
+
const { size, sha256 } = await writeCrbmGeneration(driver, key, chunks, {
|
|
2302
|
+
crypto,
|
|
2303
|
+
clock: options.clock
|
|
2304
|
+
});
|
|
2036
2305
|
if (options.registry !== void 0) {
|
|
2037
2306
|
const becameCurrent = await publishGeneration(options.registry, key, {
|
|
2038
2307
|
wrappedDeks: newWrapped
|
|
@@ -2048,13 +2317,6 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
2048
2317
|
}
|
|
2049
2318
|
return { size, sha256, chunkCount: chunks.length, cardinality };
|
|
2050
2319
|
}
|
|
2051
|
-
async function* toAsyncIterable(src) {
|
|
2052
|
-
if (Symbol.asyncIterator in src) {
|
|
2053
|
-
yield* src;
|
|
2054
|
-
} else {
|
|
2055
|
-
yield* src;
|
|
2056
|
-
}
|
|
2057
|
-
}
|
|
2058
2320
|
|
|
2059
2321
|
// src/core/compaction.ts
|
|
2060
2322
|
var DEFAULT_MAX_BITMAP_BYTES2 = 1 << 20;
|
|
@@ -3788,6 +4050,7 @@ async function eraseWarm(warm, ref) {
|
|
|
3788
4050
|
}
|
|
3789
4051
|
|
|
3790
4052
|
// src/core/consistency.ts
|
|
4053
|
+
var DEFAULT_MAX_SCAN_SEGMENTS = 25e4;
|
|
3791
4054
|
var DEFAULT_CHECK_CONCURRENCY = 8;
|
|
3792
4055
|
async function listGenerations(cold, ref) {
|
|
3793
4056
|
const present = /* @__PURE__ */ new Set();
|
|
@@ -3799,8 +4062,21 @@ async function runConsistencyCheck(deps, options = {}) {
|
|
|
3799
4062
|
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
3800
4063
|
throw new ValidationError(`concurrency must be a positive integer; got ${concurrency}`);
|
|
3801
4064
|
}
|
|
4065
|
+
const maxScanSegments = options.maxScanSegments ?? DEFAULT_MAX_SCAN_SEGMENTS;
|
|
4066
|
+
if (!Number.isFinite(maxScanSegments) || maxScanSegments < 1) {
|
|
4067
|
+
throw new ValidationError(
|
|
4068
|
+
`maxScanSegments must be a finite number >= 1; got ${String(options.maxScanSegments)}`
|
|
4069
|
+
);
|
|
4070
|
+
}
|
|
3802
4071
|
const recs = [];
|
|
3803
|
-
for await (const rec of deps.registry.list(options.namespace))
|
|
4072
|
+
for await (const rec of deps.registry.list(options.namespace)) {
|
|
4073
|
+
recs.push(rec);
|
|
4074
|
+
if (recs.length > maxScanSegments) {
|
|
4075
|
+
throw new BudgetExceededError(
|
|
4076
|
+
`checkConsistency would enumerate more than ${maxScanSegments} segments \u2014 the scan was abandoned there rather than completed. Narrow it with \`namespace\`, or raise \`maxScanSegments\` if the fleet really is that large and the memory is available.`
|
|
4077
|
+
);
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
3804
4080
|
const results = await mapWithConcurrency(recs, concurrency, async (rec) => {
|
|
3805
4081
|
if (rec.status === "destroyed") return { kind: "ok" };
|
|
3806
4082
|
const ref = { segment: rec.segment, namespace: rec.namespace };
|
|
@@ -4004,9 +4280,12 @@ exports.CrbmColdChunkSource = CrbmColdChunkSource;
|
|
|
4004
4280
|
exports.CrbmReader = CrbmReader;
|
|
4005
4281
|
exports.CrbmWriter = CrbmWriter;
|
|
4006
4282
|
exports.DEFAULT_BUDGET = DEFAULT_BUDGET;
|
|
4283
|
+
exports.DEFAULT_MAX_SCAN_SEGMENTS = DEFAULT_MAX_SCAN_SEGMENTS;
|
|
4284
|
+
exports.DEFAULT_MAX_WARM_SCAN_BYTES = DEFAULT_MAX_WARM_SCAN_BYTES;
|
|
4007
4285
|
exports.DEFAULT_OCC_BACKOFF = DEFAULT_OCC_BACKOFF;
|
|
4008
4286
|
exports.DEFAULT_PRICING = DEFAULT_PRICING;
|
|
4009
4287
|
exports.DEFAULT_RETRY_POLICY = DEFAULT_RETRY_POLICY;
|
|
4288
|
+
exports.DEFAULT_WRITE_CONCURRENCY = DEFAULT_WRITE_CONCURRENCY;
|
|
4010
4289
|
exports.InProcessKeystore = InProcessKeystore;
|
|
4011
4290
|
exports.IntegrityError = IntegrityError;
|
|
4012
4291
|
exports.KeyUnavailableError = KeyUnavailableError;
|
|
@@ -4037,6 +4316,7 @@ exports.aadFor = aadFor;
|
|
|
4037
4316
|
exports.bulkLoadCrbmGeneration = bulkLoadCrbmGeneration;
|
|
4038
4317
|
exports.checkBudget = checkBudget;
|
|
4039
4318
|
exports.chunkRefKey = chunkRefKey;
|
|
4319
|
+
exports.collectWithinBudget = collectWithinBudget;
|
|
4040
4320
|
exports.compactSegment = compactSegment;
|
|
4041
4321
|
exports.destroySegment = destroySegment;
|
|
4042
4322
|
exports.eraseNamespace = eraseNamespace;
|