@cloudbitmaps/core 0.7.0 → 0.8.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 +202 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +193 -2
- package/dist/index.d.ts +193 -2
- package/dist/index.js +202 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -414,6 +414,55 @@ var SegmentEngine = class {
|
|
|
414
414
|
removeMany(seg, ids) {
|
|
415
415
|
return this.mutateMany(seg, ids, applyRemoveAll);
|
|
416
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* **Atomically claim ids: add them, and return only the ones that were not already there.**
|
|
419
|
+
*
|
|
420
|
+
* The durable analogue of Redis `SETBIT` returning the prior bit — the primitive an exactly-once "have I
|
|
421
|
+
* already processed / already sent to this id?" check needs. `has()` then `add()` cannot do this: two workers
|
|
422
|
+
* can both read absent and both proceed.
|
|
423
|
+
*
|
|
424
|
+
* **Bulk on purpose.** The per-id shape is the one this library is worst at, because a Warm write rewrites a
|
|
425
|
+
* whole 64K-id chunk bitmap: claiming 5,000 ids one at a time costs ~5,000 writes and ~23 MB, and claiming
|
|
426
|
+
* them in one call costs 1 write and ~8 KB. So this takes a batch and does one OCC read-modify-write per
|
|
427
|
+
* distinct chunk — Redis's semantics at this library's cost shape, rather than Redis's cost shape.
|
|
428
|
+
*
|
|
429
|
+
* **Atomicity is per id, which is the guarantee that matters here.** Each id lives in exactly one chunk, and a
|
|
430
|
+
* chunk is one OCC row, so for any id exactly one concurrent claimer sees it as new. It is NOT atomic across
|
|
431
|
+
* chunks (neither is `addMany`) — a mid-flight failure can leave some chunks claimed. Re-running is safe:
|
|
432
|
+
* already-claimed ids simply come back as not-new.
|
|
433
|
+
*
|
|
434
|
+
* **The presence test consults Cold, not just the Warm delta.** Checking `adds` alone would report an id as
|
|
435
|
+
* newly claimed when a compaction had already folded it into a Cold generation — silently breaking
|
|
436
|
+
* exactly-once on any segment that has ever been compacted. The check is the full effective set,
|
|
437
|
+
* `(cold ∪ adds) \ removes`.
|
|
438
|
+
*
|
|
439
|
+
* Returns the claimed ids grouped nowhere in particular — order is unspecified.
|
|
440
|
+
*/
|
|
441
|
+
async claimMany(seg, ids) {
|
|
442
|
+
const byChunk = /* @__PURE__ */ new Map();
|
|
443
|
+
for (const id of ids) {
|
|
444
|
+
const { chunkKey, remainder } = splitId(id);
|
|
445
|
+
const bucket = byChunk.get(chunkKey);
|
|
446
|
+
if (bucket) bucket.push(remainder);
|
|
447
|
+
else byChunk.set(chunkKey, [remainder]);
|
|
448
|
+
}
|
|
449
|
+
const gen = await this.currentGen(seg);
|
|
450
|
+
const perChunk = await mapWithConcurrency(
|
|
451
|
+
[...byChunk],
|
|
452
|
+
this.writeConcurrency,
|
|
453
|
+
async ([chunkKey, remainders]) => {
|
|
454
|
+
const cold = await this.coldChunk({ ...seg, chunkKey }, gen) ?? this.codec.empty();
|
|
455
|
+
let claimed = [];
|
|
456
|
+
await this.readModifyWrite(seg, chunkKey, (delta) => {
|
|
457
|
+
const present = effective(cold, delta);
|
|
458
|
+
claimed = remainders.filter((r) => !present.has(r));
|
|
459
|
+
for (const r of claimed) applyAdd(delta, r);
|
|
460
|
+
});
|
|
461
|
+
return claimed.map((r) => joinId(chunkKey, r));
|
|
462
|
+
}
|
|
463
|
+
);
|
|
464
|
+
return perChunk.flat();
|
|
465
|
+
}
|
|
417
466
|
async has(seg, id, opts) {
|
|
418
467
|
const { chunkKey, remainder } = splitId(id);
|
|
419
468
|
const ref = { ...seg, chunkKey };
|
|
@@ -2528,8 +2577,10 @@ async function gcOrphanGenerations(ref, deps, options = {}) {
|
|
|
2528
2577
|
const current = record.currentGen;
|
|
2529
2578
|
const gens = [];
|
|
2530
2579
|
for await (const key of deps.cold.list(ref)) gens.push(key.generation);
|
|
2531
|
-
const
|
|
2532
|
-
|
|
2580
|
+
const toDelete = record.status === "destroyed" ? gens.sort((a, b) => a - b) : (
|
|
2581
|
+
// Delete generations below current, except the newest `keep` of them (the grace window).
|
|
2582
|
+
gens.filter((g) => g < current).sort((a, b) => b - a).slice(keep)
|
|
2583
|
+
);
|
|
2533
2584
|
for (const generation of toDelete) {
|
|
2534
2585
|
await deps.cold.delete({ namespace: ref.namespace, segment: ref.segment, generation });
|
|
2535
2586
|
}
|
|
@@ -3643,6 +3694,8 @@ var InProcessKeystore = class {
|
|
|
3643
3694
|
// src/core/erasure.ts
|
|
3644
3695
|
var MAX_CAS_ATTEMPTS = 8;
|
|
3645
3696
|
var MAX_WARM_PASSES = 4;
|
|
3697
|
+
var ERASE_CONCURRENCY = 8;
|
|
3698
|
+
var MAX_COLD_SWEEPS = 3;
|
|
3646
3699
|
async function destroySegment(ref, deps, options) {
|
|
3647
3700
|
if (options.confirmSegment !== ref.segment) {
|
|
3648
3701
|
throw new ValidationError(
|
|
@@ -3701,28 +3754,147 @@ async function eraseNamespace(namespace, deps, options) {
|
|
|
3701
3754
|
audit.onEvent({ kind: "namespace.erase", namespace, segmentsShredded });
|
|
3702
3755
|
return { destroyed };
|
|
3703
3756
|
}
|
|
3704
|
-
async function
|
|
3757
|
+
async function dropSegment(ref, deps, options) {
|
|
3758
|
+
if (options.confirmSegment !== ref.segment) {
|
|
3759
|
+
throw new ValidationError(
|
|
3760
|
+
`dropSegment: confirmSegment must equal the segment name "${ref.segment}" (guard against accidental deletion)`
|
|
3761
|
+
);
|
|
3762
|
+
}
|
|
3705
3763
|
const base = { segment: ref.segment, namespace: ref.namespace };
|
|
3764
|
+
if (options.dryRun === true) {
|
|
3765
|
+
const record = await deps.registry.get(ref);
|
|
3766
|
+
const wouldDelete = await listGenerations(deps.cold, ref);
|
|
3767
|
+
return {
|
|
3768
|
+
...base,
|
|
3769
|
+
dropped: false,
|
|
3770
|
+
warmRowsDeleted: 0,
|
|
3771
|
+
generationsDeleted: [],
|
|
3772
|
+
generationsRemaining: [],
|
|
3773
|
+
wouldDelete,
|
|
3774
|
+
wouldDeleteWarmRows: await countWarmRows(deps.warm, ref),
|
|
3775
|
+
wouldCryptoShred: record?.wrappedDeks !== void 0 && record.wrappedDeks.length > 0,
|
|
3776
|
+
cryptoShredded: false,
|
|
3777
|
+
reason: record === null ? wouldDelete.length === 0 ? "absent" : void 0 : record.status === "destroyed" ? "already" : void 0
|
|
3778
|
+
};
|
|
3779
|
+
}
|
|
3780
|
+
let shred = await shredSegment(ref, deps, true, "dropSegment");
|
|
3781
|
+
if (shred.reason === "absent") {
|
|
3782
|
+
const orphans = await listGenerations(deps.cold, ref);
|
|
3783
|
+
if (orphans.length === 0) {
|
|
3784
|
+
return {
|
|
3785
|
+
...base,
|
|
3786
|
+
dropped: false,
|
|
3787
|
+
warmRowsDeleted: shred.warmRowsDeleted,
|
|
3788
|
+
generationsDeleted: [],
|
|
3789
|
+
generationsRemaining: [],
|
|
3790
|
+
cryptoShredded: false,
|
|
3791
|
+
reason: "absent"
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
try {
|
|
3795
|
+
await deps.registry.create(ref, {
|
|
3796
|
+
// The max listed generation, so the row is consistent with what is on disk if anything reads it before
|
|
3797
|
+
// the sweep finishes. It is a tombstone, so no reader resolves through it either way.
|
|
3798
|
+
currentGen: orphans[orphans.length - 1],
|
|
3799
|
+
status: "destroyed"
|
|
3800
|
+
});
|
|
3801
|
+
shred = { ...shred, destroyed: true, reason: void 0 };
|
|
3802
|
+
} catch (err) {
|
|
3803
|
+
if (!isWriteConflictError(err)) throw err;
|
|
3804
|
+
shred = await shredSegment(ref, deps, true, "dropSegment");
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
const lateWarmRows = shred.destroyed ? await eraseWarmQuietly(deps.warm, ref) : 0;
|
|
3808
|
+
if (shred.cryptoShredded) {
|
|
3809
|
+
safeAudit(options.audit ?? NOOP_AUDIT).onEvent({
|
|
3810
|
+
kind: "segment.erase",
|
|
3811
|
+
namespace: ref.namespace,
|
|
3812
|
+
segment: ref.segment
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3815
|
+
const generationsDeleted = [];
|
|
3816
|
+
for (let pass = 0; pass < MAX_COLD_SWEEPS; pass++) {
|
|
3817
|
+
const present = await listGenerations(deps.cold, ref);
|
|
3818
|
+
if (present.length === 0) break;
|
|
3819
|
+
const outcomes = await mapWithConcurrency(present, ERASE_CONCURRENCY, async (generation) => {
|
|
3820
|
+
try {
|
|
3821
|
+
await deps.cold.delete({ ...ref, generation });
|
|
3822
|
+
return generation;
|
|
3823
|
+
} catch {
|
|
3824
|
+
return null;
|
|
3825
|
+
}
|
|
3826
|
+
});
|
|
3827
|
+
let deletedThisPass = 0;
|
|
3828
|
+
for (const g of outcomes) {
|
|
3829
|
+
if (g !== null) {
|
|
3830
|
+
generationsDeleted.push(g);
|
|
3831
|
+
deletedThisPass += 1;
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
if (deletedThisPass === 0) break;
|
|
3835
|
+
}
|
|
3836
|
+
const generationsRemaining = await listGenerations(deps.cold, ref);
|
|
3837
|
+
if (shred.destroyed) {
|
|
3838
|
+
safeAudit(options.audit ?? NOOP_AUDIT).onEvent({
|
|
3839
|
+
kind: "segment.dispose",
|
|
3840
|
+
namespace: ref.namespace,
|
|
3841
|
+
segment: ref.segment,
|
|
3842
|
+
generationsDeleted: generationsDeleted.length
|
|
3843
|
+
});
|
|
3844
|
+
}
|
|
3845
|
+
return {
|
|
3846
|
+
...base,
|
|
3847
|
+
dropped: shred.destroyed,
|
|
3848
|
+
warmRowsDeleted: shred.warmRowsDeleted + lateWarmRows,
|
|
3849
|
+
generationsDeleted,
|
|
3850
|
+
generationsRemaining,
|
|
3851
|
+
cryptoShredded: shred.cryptoShredded,
|
|
3852
|
+
reason: shred.reason
|
|
3853
|
+
};
|
|
3854
|
+
}
|
|
3855
|
+
async function listGenerations(cold, ref) {
|
|
3856
|
+
const generations = [];
|
|
3857
|
+
for await (const key of cold.list(ref)) generations.push(key.generation);
|
|
3858
|
+
return generations.sort((a, b) => a - b);
|
|
3859
|
+
}
|
|
3860
|
+
async function countWarmRows(warm, ref) {
|
|
3861
|
+
let n = 0;
|
|
3862
|
+
for await (const _row of warm.listChunks(ref)) {
|
|
3863
|
+
n += 1;
|
|
3864
|
+
}
|
|
3865
|
+
return n;
|
|
3866
|
+
}
|
|
3867
|
+
async function eraseWarmQuietly(warm, ref) {
|
|
3868
|
+
try {
|
|
3869
|
+
return await eraseWarm(warm, ref, "dropSegment");
|
|
3870
|
+
} catch (err) {
|
|
3871
|
+
if (!isWriteConflictError(err)) throw err;
|
|
3872
|
+
return 0;
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
|
|
3876
|
+
const base = { segment: ref.segment, namespace: ref.namespace };
|
|
3877
|
+
let warmRowsDeleted = 0;
|
|
3706
3878
|
for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
|
|
3707
3879
|
const record = await deps.registry.get(ref);
|
|
3708
3880
|
if (record === null) {
|
|
3709
|
-
|
|
3881
|
+
warmRowsDeleted += await eraseWarm(deps.warm, ref, op);
|
|
3710
3882
|
return {
|
|
3711
3883
|
...base,
|
|
3712
3884
|
destroyed: false,
|
|
3713
3885
|
cryptoShredded: false,
|
|
3714
3886
|
reason: "absent",
|
|
3715
|
-
warmRowsDeleted
|
|
3887
|
+
warmRowsDeleted
|
|
3716
3888
|
};
|
|
3717
3889
|
}
|
|
3718
3890
|
if (record.status === "destroyed") {
|
|
3719
|
-
|
|
3891
|
+
warmRowsDeleted += await eraseWarm(deps.warm, ref, op);
|
|
3720
3892
|
return {
|
|
3721
3893
|
...base,
|
|
3722
3894
|
destroyed: true,
|
|
3723
3895
|
cryptoShredded: false,
|
|
3724
3896
|
reason: "already",
|
|
3725
|
-
warmRowsDeleted
|
|
3897
|
+
warmRowsDeleted
|
|
3726
3898
|
};
|
|
3727
3899
|
}
|
|
3728
3900
|
const encrypted = record.wrappedDeks !== void 0 && record.wrappedDeks.length > 0;
|
|
@@ -3732,10 +3904,10 @@ async function shredSegment(ref, deps, allowCleartext) {
|
|
|
3732
3904
|
destroyed: false,
|
|
3733
3905
|
cryptoShredded: false,
|
|
3734
3906
|
reason: "cleartext",
|
|
3735
|
-
warmRowsDeleted
|
|
3907
|
+
warmRowsDeleted
|
|
3736
3908
|
};
|
|
3737
3909
|
}
|
|
3738
|
-
|
|
3910
|
+
warmRowsDeleted += await eraseWarm(deps.warm, ref, op);
|
|
3739
3911
|
try {
|
|
3740
3912
|
await deps.registry.compareAndSwap(ref, record.token, {
|
|
3741
3913
|
status: "destroyed",
|
|
@@ -3750,9 +3922,9 @@ async function shredSegment(ref, deps, allowCleartext) {
|
|
|
3750
3922
|
if (!isWriteConflictError(err)) throw err;
|
|
3751
3923
|
}
|
|
3752
3924
|
}
|
|
3753
|
-
throw new WriteConflictError(
|
|
3925
|
+
throw new WriteConflictError(`${op}: contention shredding "${ref.segment}" \u2014 retry`);
|
|
3754
3926
|
}
|
|
3755
|
-
async function eraseWarm(warm, ref) {
|
|
3927
|
+
async function eraseWarm(warm, ref, op = "destroySegment") {
|
|
3756
3928
|
let deleted = 0;
|
|
3757
3929
|
let conflicts = 0;
|
|
3758
3930
|
for (let pass = 0; pass < MAX_WARM_PASSES; pass++) {
|
|
@@ -3761,21 +3933,27 @@ async function eraseWarm(warm, ref) {
|
|
|
3761
3933
|
for await (const row of warm.listChunks(ref))
|
|
3762
3934
|
rows.push({ chunkKey: row.chunkKey, token: row.token });
|
|
3763
3935
|
if (rows.length === 0) break;
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3936
|
+
const erased = await mapWithConcurrency(
|
|
3937
|
+
rows,
|
|
3938
|
+
ERASE_CONCURRENCY,
|
|
3939
|
+
async ({ chunkKey, token }) => {
|
|
3940
|
+
const chunkRef = { namespace: ref.namespace, segment: ref.segment, chunkKey };
|
|
3941
|
+
try {
|
|
3942
|
+
await warm.deleteConditional(chunkRef, token);
|
|
3943
|
+
return true;
|
|
3944
|
+
} catch (err) {
|
|
3945
|
+
if (!isWriteConflictError(err)) throw err;
|
|
3946
|
+
return false;
|
|
3947
|
+
}
|
|
3772
3948
|
}
|
|
3773
|
-
|
|
3949
|
+
);
|
|
3950
|
+
for (const ok of erased) if (ok) deleted += 1;
|
|
3951
|
+
conflicts = erased.length - erased.filter(Boolean).length;
|
|
3774
3952
|
if (conflicts === 0) break;
|
|
3775
3953
|
}
|
|
3776
3954
|
if (conflicts > 0) {
|
|
3777
3955
|
throw new WriteConflictError(
|
|
3778
|
-
|
|
3956
|
+
`${op}: ${conflicts} warm row(s) of "${ref.segment}" were rewritten during every one of ${MAX_WARM_PASSES} erase passes \u2014 the segment was NOT destroyed and its cleartext warm deltas are still readable. Stop writes to this segment and retry.`
|
|
3779
3957
|
);
|
|
3780
3958
|
}
|
|
3781
3959
|
return deleted;
|
|
@@ -3784,7 +3962,7 @@ async function eraseWarm(warm, ref) {
|
|
|
3784
3962
|
// src/core/consistency.ts
|
|
3785
3963
|
var DEFAULT_MAX_SCAN_SEGMENTS = 25e4;
|
|
3786
3964
|
var DEFAULT_CHECK_CONCURRENCY = 8;
|
|
3787
|
-
async function
|
|
3965
|
+
async function listGenerations2(cold, ref) {
|
|
3788
3966
|
const present = /* @__PURE__ */ new Set();
|
|
3789
3967
|
for await (const key of cold.list(ref)) present.add(key.generation);
|
|
3790
3968
|
return present;
|
|
@@ -3815,7 +3993,7 @@ async function runConsistencyCheck(deps, options = {}) {
|
|
|
3815
3993
|
try {
|
|
3816
3994
|
const live = await deps.registry.get(ref);
|
|
3817
3995
|
if (!live || live.status === "destroyed") return { kind: "ok" };
|
|
3818
|
-
const present = await
|
|
3996
|
+
const present = await listGenerations2(deps.cold, ref);
|
|
3819
3997
|
if (present.has(live.currentGen)) return { kind: "ok" };
|
|
3820
3998
|
return {
|
|
3821
3999
|
kind: "issue",
|
|
@@ -4030,6 +4208,6 @@ var RetryingRegistryDriver = class {
|
|
|
4030
4208
|
}
|
|
4031
4209
|
};
|
|
4032
4210
|
|
|
4033
|
-
export { AWS_US_EAST_1_ONDEMAND, BoundedLru, BufferReader, BufferSink, CountingMetricsSink, CrbmColdChunkSource, CrbmReader, CrbmWriter, DEFAULT_BUDGET, DEFAULT_MAX_SCAN_SEGMENTS, DEFAULT_MAX_WARM_SCAN_BYTES, DEFAULT_OCC_BACKOFF, DEFAULT_PRICING, DEFAULT_RETRY_POLICY, DEFAULT_WRITE_CONCURRENCY, InProcessKeystore, LocalFsColdDriver, LocalFsRegistryDriver, LocalFsWarmDriver, MemoryColdChunkSource, MemoryColdDriver, MemoryRegistryDriver, MemoryWarmDriver, NOOP_AUDIT, NOOP_METRICS, NodeAead, RecordingAuditSink, RetryingColdChunkSource, RetryingColdDriver, RetryingRegistryDriver, RetryingWarmDriver, SegmentEngine, aadFor, bulkLoadCrbmGeneration, checkBudget, chunkRefKey, collectWithinBudget, compactSegment, destroySegment, eraseNamespace, estimateCost, findCompactable, gcOrphanGenerations, groundedReport, isTransient, joinId, publishGeneration, resolveBudget, resolvePerOpBudget, runCompactionCycle, runConsistencyCheck, runExport, safeMetrics, segmentKey, splitId, validateCompactionOptions, withRetry, writeCrbmGeneration };
|
|
4211
|
+
export { AWS_US_EAST_1_ONDEMAND, BoundedLru, BufferReader, BufferSink, CountingMetricsSink, CrbmColdChunkSource, CrbmReader, CrbmWriter, DEFAULT_BUDGET, DEFAULT_MAX_SCAN_SEGMENTS, DEFAULT_MAX_WARM_SCAN_BYTES, DEFAULT_OCC_BACKOFF, DEFAULT_PRICING, DEFAULT_RETRY_POLICY, DEFAULT_WRITE_CONCURRENCY, InProcessKeystore, LocalFsColdDriver, LocalFsRegistryDriver, LocalFsWarmDriver, MemoryColdChunkSource, MemoryColdDriver, MemoryRegistryDriver, MemoryWarmDriver, NOOP_AUDIT, NOOP_METRICS, NodeAead, RecordingAuditSink, RetryingColdChunkSource, RetryingColdDriver, RetryingRegistryDriver, RetryingWarmDriver, SegmentEngine, aadFor, bulkLoadCrbmGeneration, checkBudget, chunkRefKey, collectWithinBudget, compactSegment, destroySegment, dropSegment, eraseNamespace, estimateCost, findCompactable, gcOrphanGenerations, groundedReport, isTransient, joinId, publishGeneration, resolveBudget, resolvePerOpBudget, runCompactionCycle, runConsistencyCheck, runExport, safeMetrics, segmentKey, splitId, validateCompactionOptions, withRetry, writeCrbmGeneration };
|
|
4034
4212
|
//# sourceMappingURL=index.js.map
|
|
4035
4213
|
//# sourceMappingURL=index.js.map
|