@cloudbitmaps/core 0.16.0 → 0.17.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/{chunk-LFUNPMXZ.js → chunk-CV7GJO3B.js} +363 -23
- package/dist/chunk-CV7GJO3B.js.map +7 -0
- package/dist/core/cost.d.ts +3 -3
- package/dist/core/crbm-storage-source.d.ts +73 -65
- package/dist/core/engine.d.ts +23 -0
- package/dist/core/erasure.d.ts +2 -1
- package/dist/core/errors.d.ts +23 -0
- package/dist/core/generation-gc.d.ts +69 -45
- package/dist/core/kept-generations.d.ts +40 -0
- package/dist/core/leases.d.ts +119 -0
- package/dist/core/load-input.d.ts +12 -0
- package/dist/core/load.d.ts +13 -11
- package/dist/core/metrics.d.ts +24 -0
- package/dist/core/ports.d.ts +99 -2
- package/dist/core/registry-reap.d.ts +22 -0
- package/dist/core/retention.d.ts +2 -1
- package/dist/core/rollback.d.ts +2 -1
- package/dist/driver-kit.d.ts +1 -0
- package/dist/driver-kit.js +100 -1
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/object-registry.d.ts +19 -1
- package/dist/drivers/_shared/registry.d.ts +7 -9
- package/dist/export/index.d.ts +20 -8
- package/dist/index.d.ts +9 -3
- package/dist/index.js +527 -86
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-LFUNPMXZ.js.map +0 -7
package/dist/index.js
CHANGED
|
@@ -5,6 +5,12 @@ import {
|
|
|
5
5
|
DEFAULT_NAMESPACE,
|
|
6
6
|
IntegrityError,
|
|
7
7
|
KeyUnavailableError,
|
|
8
|
+
LEASE_SKEW_MS,
|
|
9
|
+
LeaseExpiredError,
|
|
10
|
+
LeaseLimitError,
|
|
11
|
+
MAX_KEPT_GENERATIONS,
|
|
12
|
+
MAX_LEASES_PER_SEGMENT,
|
|
13
|
+
MAX_LEASE_MS,
|
|
8
14
|
MAX_METADATA_BYTES,
|
|
9
15
|
MAX_NAME_LENGTH,
|
|
10
16
|
NotFoundError,
|
|
@@ -25,25 +31,35 @@ import {
|
|
|
25
31
|
encodeNameForPath,
|
|
26
32
|
encodedNameLength,
|
|
27
33
|
entropyIsAvailable,
|
|
34
|
+
heldGenerations,
|
|
28
35
|
incarnationOf,
|
|
29
36
|
isCloudRoaringError,
|
|
30
37
|
isIntegrityError,
|
|
38
|
+
isLeaseExpiredError,
|
|
39
|
+
isLeaseLimitError,
|
|
40
|
+
isLive,
|
|
31
41
|
isNotFoundError,
|
|
32
42
|
isReservedNamespace,
|
|
33
43
|
isStorageBackend,
|
|
34
44
|
isTransientError,
|
|
35
45
|
isValidationError,
|
|
36
46
|
isWriteConflictError,
|
|
47
|
+
leaseChurn,
|
|
37
48
|
mapWithConcurrency,
|
|
38
49
|
metadataBytes,
|
|
39
50
|
metadataFromBytes,
|
|
40
51
|
namespacePathPart,
|
|
41
52
|
newIncarnationToken,
|
|
53
|
+
nextKept,
|
|
42
54
|
nextRegistryToken,
|
|
55
|
+
onlyLeasesDiffer,
|
|
43
56
|
parseRegistryEnvelope,
|
|
44
57
|
recordFromNew,
|
|
58
|
+
releaseLease,
|
|
45
59
|
sameIncarnation,
|
|
46
60
|
serializeRegistryEnvelope,
|
|
61
|
+
takeLease,
|
|
62
|
+
usableKeptGens,
|
|
47
63
|
validateChunkKeyOrder,
|
|
48
64
|
validateChunkRef,
|
|
49
65
|
validateNewRegistryRecord,
|
|
@@ -52,7 +68,7 @@ import {
|
|
|
52
68
|
validateUserNamespace,
|
|
53
69
|
validateUserRef,
|
|
54
70
|
webCryptoEntropy
|
|
55
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-CV7GJO3B.js";
|
|
56
72
|
|
|
57
73
|
// src/core/bit-route.ts
|
|
58
74
|
var U32_MAX = 4294967295;
|
|
@@ -332,6 +348,8 @@ var CountingMetricsSink = class _CountingMetricsSink {
|
|
|
332
348
|
op.totalMs += event.ms;
|
|
333
349
|
break;
|
|
334
350
|
}
|
|
351
|
+
case "advisory":
|
|
352
|
+
break;
|
|
335
353
|
default: {
|
|
336
354
|
const exhaustive = event;
|
|
337
355
|
return exhaustive;
|
|
@@ -675,6 +693,104 @@ var SegmentEngine = class {
|
|
|
675
693
|
chunks.close();
|
|
676
694
|
}
|
|
677
695
|
}
|
|
696
|
+
/**
|
|
697
|
+
* The ids at 1-based ranks `n`, `2n`, `3n` … counted over the ids in `range`, ascending. The rank of each boundary
|
|
698
|
+
* is placed from the index's per-chunk counts, so only the chunks that hold a boundary are read, each once however
|
|
699
|
+
* many boundaries it holds, through the same coalesced stream, window and budget unit as {@link iterate}. A range with
|
|
700
|
+
* `after` inside a chunk reads that chunk too, to count the ids at or below `after`. A last partial window yields
|
|
701
|
+
* nothing.
|
|
702
|
+
*
|
|
703
|
+
* The counts place ids, and are trusted as `count()` trusts them: a chunk that is read must decode to the size its
|
|
704
|
+
* index says, or the read throws {@link IntegrityError} (invariant 5), but a chunk that is not read is not checked, so
|
|
705
|
+
* an index that lies consistently about one (on an unencrypted object, the sum still matches the footer's total,
|
|
706
|
+
* which opening the object checks before any boundary is placed) shifts the ranks after it. An encrypted object's
|
|
707
|
+
* index is authenticated and bound to the object, so a wrong count there needs the key. The index and the chunks must come from one
|
|
708
|
+
* generation (invariant 3): the caller reads through a source that holds one, a pinned one, and a source with no
|
|
709
|
+
* per-chunk counts is refused.
|
|
710
|
+
*
|
|
711
|
+
* Nothing is fetched when the chunks in range hold fewer than `n` ids. The budget is charged before the first fetch
|
|
712
|
+
* with an upper bound, the cut chunk plus one chunk per boundary the counts allow (capped by the chunks in range),
|
|
713
|
+
* which can exceed the chunks actually read. A range that cuts its first chunk reads it even when the ids above
|
|
714
|
+
* `after` turn out to be fewer than `n`, and one that cuts its last chunk may read it and yield nothing.
|
|
715
|
+
*/
|
|
716
|
+
async *everyNth(seg, n, range) {
|
|
717
|
+
if (!Number.isSafeInteger(n) || n < 1) {
|
|
718
|
+
throw new ValidationError(`n must be a positive integer; got ${String(n)}`);
|
|
719
|
+
}
|
|
720
|
+
const epoch = this.invalidations;
|
|
721
|
+
const w = windowOf(range);
|
|
722
|
+
if (w === "empty") return;
|
|
723
|
+
const { loKey, loRem, hiKey, hiRem } = w ?? WHOLE_ID_SPACE;
|
|
724
|
+
if (this.storage.cardinalities === void 0) {
|
|
725
|
+
throw new UnsupportedError("everyNth needs a storage source that reports per-chunk counts");
|
|
726
|
+
}
|
|
727
|
+
const counts = await this.storage.cardinalities(seg);
|
|
728
|
+
if (counts === null) return;
|
|
729
|
+
const keys = keysWithin(
|
|
730
|
+
[...counts.keys()].sort((a, b) => a - b),
|
|
731
|
+
{ loKey, loRem, hiKey, hiRem }
|
|
732
|
+
);
|
|
733
|
+
for (const k of keys) {
|
|
734
|
+
this.assertChunkKeyInRange(k);
|
|
735
|
+
const c = counts.get(k);
|
|
736
|
+
if (!Number.isInteger(c) || c < 1 || c > CHUNK_COUNT) {
|
|
737
|
+
throw new IntegrityError(`chunk cardinality from a tier is out of range: ${c}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const gen = await this.cacheVersion(seg);
|
|
741
|
+
const cutsFirst = loRem > 0 && keys[0] === loKey;
|
|
742
|
+
let upper = 0;
|
|
743
|
+
for (const k of keys) upper += counts.get(k);
|
|
744
|
+
if (upper < n) return;
|
|
745
|
+
const cut = cutsFirst ? 1 : 0;
|
|
746
|
+
checkBudget(this.budget, cut + Math.min(keys.length - cut, Math.floor(upper / n)), "everyNth");
|
|
747
|
+
let first;
|
|
748
|
+
let below = 0;
|
|
749
|
+
if (cutsFirst) {
|
|
750
|
+
first = this.chunkRemainders(
|
|
751
|
+
await this.storageChunk({ ...seg, chunkKey: loKey }, gen),
|
|
752
|
+
loKey,
|
|
753
|
+
counts
|
|
754
|
+
);
|
|
755
|
+
below = firstAbove(first, loRem - 1);
|
|
756
|
+
}
|
|
757
|
+
const hits = [];
|
|
758
|
+
let before = 0;
|
|
759
|
+
for (const key of keys) {
|
|
760
|
+
const skipped = key === loKey && cutsFirst ? below : 0;
|
|
761
|
+
const size = counts.get(key) - skipped;
|
|
762
|
+
const next = (Math.floor(before / n) + 1) * n;
|
|
763
|
+
if (next <= before + size) hits.push({ key, index: skipped + next - before - 1 });
|
|
764
|
+
before += size;
|
|
765
|
+
}
|
|
766
|
+
if (hits.length === 0) return;
|
|
767
|
+
const streamed = hits.filter((h) => !(cutsFirst && h.key === loKey)).map((h) => h.key);
|
|
768
|
+
const chunks = streamed.length > 0 ? this.chunkSequence(seg, streamed, gen, epoch) : void 0;
|
|
769
|
+
try {
|
|
770
|
+
for (const { key, index } of hits) {
|
|
771
|
+
const rem = cutsFirst && key === loKey && first !== void 0 ? first : this.chunkRemainders(await chunks.take(key), key, counts);
|
|
772
|
+
const base = key * CHUNK_COUNT;
|
|
773
|
+
for (let i = index; i < rem.length; i += n) {
|
|
774
|
+
const remainder = rem[i] & MAX_REMAINDER;
|
|
775
|
+
if (key === hiKey && remainder > hiRem) return;
|
|
776
|
+
yield base + remainder;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
} finally {
|
|
780
|
+
chunks?.close();
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/** A chunk's remainders ascending, once its size is confirmed against the index's count for it (invariant 5). */
|
|
784
|
+
chunkRemainders(chunk, chunkKey, counts) {
|
|
785
|
+
const expected = counts.get(chunkKey);
|
|
786
|
+
if (chunk === null || chunk.size !== expected) {
|
|
787
|
+
const got = chunk === null ? "no bytes" : `${chunk.size} ids`;
|
|
788
|
+
throw new IntegrityError(
|
|
789
|
+
`chunk ${chunkKey} holds ${got}, but the object's index says ${expected}`
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
return chunk.toUint32Array ? chunk.toUint32Array() : Uint32Array.from(chunk);
|
|
793
|
+
}
|
|
678
794
|
/**
|
|
679
795
|
* `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
|
|
680
796
|
*
|
|
@@ -1444,63 +1560,121 @@ async function nextLoadGeneration(ref, deps, row) {
|
|
|
1444
1560
|
}
|
|
1445
1561
|
var LIST_COLLECTION_CADENCE = 16;
|
|
1446
1562
|
async function gcOrphanGenerations(ref, deps, options = {}) {
|
|
1563
|
+
return (await listingPass(ref, deps, { keep: options.keep })).deleted;
|
|
1564
|
+
}
|
|
1565
|
+
async function listingPass(ref, deps, options) {
|
|
1447
1566
|
const keep = options.keep ?? 1;
|
|
1567
|
+
const quiet = options.quiet === true;
|
|
1448
1568
|
if (!Number.isInteger(keep) || keep < 0) {
|
|
1449
1569
|
throw new ValidationError(`keep must be a non-negative integer; got ${String(keep)}`);
|
|
1450
1570
|
}
|
|
1451
1571
|
const record = await deps.registry.get(ref);
|
|
1452
|
-
if (record === null) return [];
|
|
1572
|
+
if (record === null) return { deleted: [], kept: [], complete: true };
|
|
1453
1573
|
const current = record.currentGen;
|
|
1454
1574
|
const seen = /* @__PURE__ */ new Set();
|
|
1455
1575
|
for await (const key of deps.storage.list(ref)) seen.add(key.generation);
|
|
1456
1576
|
const gens = [...seen];
|
|
1457
1577
|
const after = await deps.registry.get(ref);
|
|
1458
|
-
if (after === null)
|
|
1578
|
+
if (after === null) {
|
|
1579
|
+
if (quiet) return { deleted: [], kept: [], complete: false };
|
|
1459
1580
|
throw new WriteConflictError(`registry row for segment ${ref.segment} was purged mid-pass`);
|
|
1581
|
+
}
|
|
1460
1582
|
if (record.status === "destroyed" && after.token !== record.token) {
|
|
1583
|
+
if (quiet) return { deleted: [], kept: [], complete: false };
|
|
1461
1584
|
throw new WriteConflictError(
|
|
1462
1585
|
`segment ${ref.segment} changed incarnation while its generations were being listed`
|
|
1463
1586
|
);
|
|
1464
1587
|
}
|
|
1465
1588
|
const cutoff = current === null || after.currentGen === null ? null : Math.min(current, after.currentGen);
|
|
1466
|
-
const
|
|
1589
|
+
const named = record.status === "destroyed" ? void 0 : usableKeptGens(after);
|
|
1590
|
+
const protect = options.protect === void 0 || named === void 0 ? void 0 : /* @__PURE__ */ new Set([...options.protect, ...named]);
|
|
1591
|
+
const stillCollectable = async (generation) => {
|
|
1467
1592
|
const still = await deps.registry.get(ref);
|
|
1468
1593
|
if (still === null) {
|
|
1594
|
+
if (quiet) return "stop";
|
|
1469
1595
|
throw new WriteConflictError(`registry row for segment ${ref.segment} was purged mid-pass`);
|
|
1470
1596
|
}
|
|
1471
1597
|
const ok = record.status === "destroyed" ? still.token === after.token : cutoff === null || still.currentGen !== null && still.currentGen >= cutoff;
|
|
1472
|
-
|
|
1598
|
+
const now = protect === void 0 ? void 0 : usableKeptGens(still);
|
|
1599
|
+
const unnamed = protect === void 0 || record.status === "destroyed" || now !== void 0 && !now.includes(generation);
|
|
1600
|
+
if (!ok || !unnamed) {
|
|
1601
|
+
if (quiet) return "stop";
|
|
1473
1602
|
throw new WriteConflictError(
|
|
1474
1603
|
`segment ${ref.segment} changed incarnation while its generations were being collected`
|
|
1475
1604
|
);
|
|
1476
1605
|
}
|
|
1606
|
+
if (guard !== void 0 && heldGenerations(still, guard.now()).has(generation)) return "skip";
|
|
1607
|
+
return "delete";
|
|
1477
1608
|
};
|
|
1478
|
-
const
|
|
1609
|
+
const guard = record.status === "destroyed" ? void 0 : options.leases;
|
|
1610
|
+
const held = guard === void 0 ? void 0 : heldGenerations(after, guard.now());
|
|
1611
|
+
const below = record.status === "destroyed" || cutoff === null ? [] : gens.filter((g) => g < cutoff).sort((a, b) => b - a);
|
|
1612
|
+
const candidates = record.status === "destroyed" ? gens.sort((a, b) => a - b) : cutoff === null ? (
|
|
1479
1613
|
// No Storage pointer yet, so "below current" selects nothing and there is nothing safe to infer: an object
|
|
1480
1614
|
// here is either a load about to publish or an orphan we cannot tell apart from it. Deleting would race
|
|
1481
1615
|
// that publish into a dangling pointer. It is collected once a pointer exists.
|
|
1482
1616
|
[]
|
|
1617
|
+
) : protect !== void 0 ? (
|
|
1618
|
+
// The row's list is the window: everything below the pointer it does not name is garbage.
|
|
1619
|
+
below.filter((g) => !protect.has(g))
|
|
1483
1620
|
) : (
|
|
1484
1621
|
// Delete generations below the cutoff, except the newest `keep` of them (the grace window).
|
|
1485
|
-
|
|
1622
|
+
below.slice(keep)
|
|
1486
1623
|
);
|
|
1624
|
+
const leasedSpared = new Set(candidates.filter((g) => held?.has(g) === true));
|
|
1625
|
+
const toDelete = candidates.filter((g) => !leasedSpared.has(g));
|
|
1626
|
+
const done = [];
|
|
1487
1627
|
for (const generation of toDelete) {
|
|
1488
|
-
await stillCollectable();
|
|
1628
|
+
const verdict = await stillCollectable(generation);
|
|
1629
|
+
if (verdict === "stop") return { deleted: done, kept: [], complete: false };
|
|
1630
|
+
if (verdict === "skip") {
|
|
1631
|
+
leasedSpared.add(generation);
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1489
1634
|
await deps.storage.delete({ namespace: ref.namespace, segment: ref.segment, generation });
|
|
1635
|
+
done.push(generation);
|
|
1490
1636
|
}
|
|
1491
|
-
|
|
1637
|
+
const deleted = new Set(done);
|
|
1638
|
+
return {
|
|
1639
|
+
deleted: done,
|
|
1640
|
+
kept: below.filter((g) => !deleted.has(g) && !leasedSpared.has(g)).reverse(),
|
|
1641
|
+
complete: true
|
|
1642
|
+
};
|
|
1492
1643
|
}
|
|
1493
1644
|
async function collectAfterLoad(ref, deps, options) {
|
|
1494
|
-
const { generation, keep, byName, proveCurrent } = options;
|
|
1645
|
+
const { generation, keep, byName, currentGone, kept, proveCurrent, leases } = options;
|
|
1495
1646
|
if (keep >= generation) return [];
|
|
1647
|
+
const list = kept?.list;
|
|
1648
|
+
if (list === void 0 || currentGone === true)
|
|
1649
|
+
return reconcile(ref, deps, { keep, kept, leases });
|
|
1496
1650
|
const periodic = generation % LIST_COLLECTION_CADENCE === 0;
|
|
1497
|
-
if (byName &&
|
|
1498
|
-
if (keep
|
|
1499
|
-
return
|
|
1651
|
+
if (byName && !periodic) {
|
|
1652
|
+
if (keep >= 1 && proveCurrent !== void 0 && !await objectIsThere(deps.storage, proveCurrent)) {
|
|
1653
|
+
return reconcile(ref, deps, { keep, kept, leases });
|
|
1654
|
+
}
|
|
1655
|
+
return deleteEvicted(ref, deps, { generation, evict: kept.evict, leases });
|
|
1656
|
+
}
|
|
1657
|
+
return (await listingPass(ref, deps, { keep, protect: list, quiet: true, leases })).deleted;
|
|
1658
|
+
}
|
|
1659
|
+
async function reconcile(ref, deps, options) {
|
|
1660
|
+
const { keep, kept, leases } = options;
|
|
1661
|
+
const {
|
|
1662
|
+
deleted,
|
|
1663
|
+
kept: present,
|
|
1664
|
+
complete
|
|
1665
|
+
} = await listingPass(ref, deps, { keep, quiet: true, leases });
|
|
1666
|
+
const token = kept?.token;
|
|
1667
|
+
if (complete && token !== void 0 && keep <= MAX_KEPT_GENERATIONS && !sameList(kept?.list, present)) {
|
|
1668
|
+
try {
|
|
1669
|
+
await deps.registry.compareAndSwap(ref, token, { keptGens: present });
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
if (!(err instanceof WriteConflictError)) throw err;
|
|
1500
1672
|
}
|
|
1501
|
-
return collectByName(ref, deps, { generation, keep });
|
|
1502
1673
|
}
|
|
1503
|
-
return
|
|
1674
|
+
return deleted;
|
|
1675
|
+
}
|
|
1676
|
+
function sameList(a, b) {
|
|
1677
|
+
return a !== void 0 && a.length === b.length && a.every((g, i) => g === b[i]);
|
|
1504
1678
|
}
|
|
1505
1679
|
async function objectIsThere(storage, key) {
|
|
1506
1680
|
try {
|
|
@@ -1510,21 +1684,20 @@ async function objectIsThere(storage, key) {
|
|
|
1510
1684
|
return false;
|
|
1511
1685
|
}
|
|
1512
1686
|
}
|
|
1513
|
-
async function
|
|
1514
|
-
const
|
|
1515
|
-
|
|
1516
|
-
|
|
1687
|
+
async function deleteEvicted(ref, deps, options) {
|
|
1688
|
+
const deleted = [];
|
|
1689
|
+
for (const generation of options.evict) {
|
|
1690
|
+
const still = await deps.registry.get(ref);
|
|
1691
|
+
if (still === null || still.currentGen === null || still.currentGen < options.generation) break;
|
|
1692
|
+
const named = usableKeptGens(still);
|
|
1693
|
+
if (named === void 0 || generation >= still.currentGen || named.includes(generation)) break;
|
|
1694
|
+
if (options.leases !== void 0 && heldGenerations(still, options.leases.now()).has(generation)) {
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
await deps.storage.delete({ namespace: ref.namespace, segment: ref.segment, generation });
|
|
1698
|
+
deleted.push(generation);
|
|
1517
1699
|
}
|
|
1518
|
-
|
|
1519
|
-
if (evicted < 0) return [];
|
|
1520
|
-
const still = await deps.registry.get(ref);
|
|
1521
|
-
if (still === null || still.currentGen === null || still.currentGen < generation) return [];
|
|
1522
|
-
await deps.storage.delete({
|
|
1523
|
-
namespace: ref.namespace,
|
|
1524
|
-
segment: ref.segment,
|
|
1525
|
-
generation: evicted
|
|
1526
|
-
});
|
|
1527
|
-
return [evicted];
|
|
1700
|
+
return deleted;
|
|
1528
1701
|
}
|
|
1529
1702
|
|
|
1530
1703
|
// src/core/reader-defaults.ts
|
|
@@ -2030,13 +2203,30 @@ async function runExport(reader, registry, sink, options = {}) {
|
|
|
2030
2203
|
const segments = [];
|
|
2031
2204
|
const failed = [];
|
|
2032
2205
|
let totalIds = 0;
|
|
2206
|
+
if (typeof reader?.segment !== "function") {
|
|
2207
|
+
throw new ValidationError(
|
|
2208
|
+
"runExport: the reader must have a segment(name, { namespace? }) method returning a handle with pin(), as a store's does"
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
let probed = false;
|
|
2212
|
+
let unusable;
|
|
2033
2213
|
const exportRef = async (ref) => {
|
|
2034
2214
|
let writer;
|
|
2035
2215
|
let count = 0;
|
|
2036
2216
|
let bytes = 0;
|
|
2037
2217
|
try {
|
|
2038
2218
|
validateSegmentRef(ref);
|
|
2039
|
-
const
|
|
2219
|
+
const handle = reader.segment(ref.segment, { namespace: ref.namespace });
|
|
2220
|
+
if (!probed) {
|
|
2221
|
+
probed = true;
|
|
2222
|
+
if (typeof handle?.pin !== "function") {
|
|
2223
|
+
unusable = new ValidationError(
|
|
2224
|
+
"runExport: the reader's segment(name, { namespace? }) must return a handle with a pin() method; a segment is exported at one pinned generation"
|
|
2225
|
+
);
|
|
2226
|
+
throw unusable;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
const ids = (await handle.pin()).iterate();
|
|
2040
2230
|
writer = await sink.open(ref, ext);
|
|
2041
2231
|
if (format === "roaring") {
|
|
2042
2232
|
const bm = requireCodec(options.codec, "runExport({ format: 'roaring' })").empty();
|
|
@@ -2067,6 +2257,7 @@ async function runExport(reader, registry, sink, options = {}) {
|
|
|
2067
2257
|
}
|
|
2068
2258
|
}
|
|
2069
2259
|
} catch (err) {
|
|
2260
|
+
if (err === unusable) throw err;
|
|
2070
2261
|
if (writer !== void 0) {
|
|
2071
2262
|
try {
|
|
2072
2263
|
await writer.abort?.();
|
|
@@ -3848,7 +4039,7 @@ function isShared(buffer) {
|
|
|
3848
4039
|
}
|
|
3849
4040
|
function overCap(what, length) {
|
|
3850
4041
|
return new ValidationError(
|
|
3851
|
-
`${what} is ${length} bytes, more than any canonical 32-bit bitmap serializes to (${MAX_SERIALIZED_LOAD_BYTES}).
|
|
4042
|
+
`${what} is ${length} bytes, more than any canonical 32-bit bitmap serializes to (${MAX_SERIALIZED_LOAD_BYTES}). If it was serialized from a bitmap, call runOptimize() on the bitmap first.`
|
|
3852
4043
|
);
|
|
3853
4044
|
}
|
|
3854
4045
|
function prepareLoadInput(input, codec) {
|
|
@@ -3866,7 +4057,9 @@ function prepareLoadInput(input, codec) {
|
|
|
3866
4057
|
if (Symbol.iterator in input || Symbol.asyncIterator in input) return input;
|
|
3867
4058
|
const keys = Object.keys(input);
|
|
3868
4059
|
if (keys.length === 1 && keys[0] === "serialized") {
|
|
3869
|
-
return
|
|
4060
|
+
return new DecodedLoadInput(
|
|
4061
|
+
decodeSerialized(input.serialized, codec, "{ serialized }")
|
|
4062
|
+
);
|
|
3870
4063
|
}
|
|
3871
4064
|
if (keys.length === 1 && keys[0] === "bitmap") {
|
|
3872
4065
|
const bitmap = input.bitmap;
|
|
@@ -3878,13 +4071,13 @@ function prepareLoadInput(input, codec) {
|
|
|
3878
4071
|
const size = bitmap.getSerializationSizeInBytes("portable");
|
|
3879
4072
|
if (typeof size === "number" && size > MAX_SERIALIZED_LOAD_BYTES) throw overCap(what, size);
|
|
3880
4073
|
}
|
|
3881
|
-
return
|
|
4074
|
+
return new DecodedLoadInput(decodeSerialized(bitmap.serialize("portable"), codec, what));
|
|
3882
4075
|
}
|
|
3883
4076
|
throw new ValidationError(
|
|
3884
4077
|
`a load takes ids (an iterable of integers), { serialized } or { bitmap }, as the only key; got an object with keys [${keys.join(", ")}]. Load options go in the next argument.`
|
|
3885
4078
|
);
|
|
3886
4079
|
}
|
|
3887
|
-
function
|
|
4080
|
+
function decodeSerialized(bytes, codec, what = "the buffer") {
|
|
3888
4081
|
if (typeof bytes !== "object" || bytes === null || tagOf(bytes) !== "Uint8Array") {
|
|
3889
4082
|
throw new ValidationError(`${what} must be a Uint8Array of portable Roaring bytes`);
|
|
3890
4083
|
}
|
|
@@ -3892,9 +4085,7 @@ function decode(bytes, what, codec) {
|
|
|
3892
4085
|
if (view.byteLength > MAX_SERIALIZED_LOAD_BYTES) throw overCap(what, view.byteLength);
|
|
3893
4086
|
const own = isShared(view.buffer) ? new Uint8Array(view) : view;
|
|
3894
4087
|
try {
|
|
3895
|
-
return
|
|
3896
|
-
codec.safeDeserialize(own, MAX_SERIALIZED_LOAD_BYTES, { whole: true })
|
|
3897
|
-
);
|
|
4088
|
+
return codec.safeDeserialize(own, MAX_SERIALIZED_LOAD_BYTES, { whole: true });
|
|
3898
4089
|
} catch (err) {
|
|
3899
4090
|
if (!isIntegrityError(err)) throw err;
|
|
3900
4091
|
throw new ValidationError(`${what} is not a valid bitmap: ${err.message}`);
|
|
@@ -4037,6 +4228,8 @@ var notThePinned = (ref, generation) => new NotFoundError(
|
|
|
4037
4228
|
function versionOf(generation, lineage) {
|
|
4038
4229
|
return lineage === void 0 ? String(generation) : `${generation}:${String(lineage)}`;
|
|
4039
4230
|
}
|
|
4231
|
+
var PinMoved = class extends Error {
|
|
4232
|
+
};
|
|
4040
4233
|
function summaryOfGeneration(generation, description) {
|
|
4041
4234
|
const { cardinality, metadata } = description;
|
|
4042
4235
|
return metadata === void 0 || Object.keys(metadata).length === 0 ? { generation, cardinality } : { generation, cardinality, metadata: Object.freeze({ ...metadata }) };
|
|
@@ -4403,17 +4596,102 @@ var CrbmStorageChunkSource = class {
|
|
|
4403
4596
|
* and a `keep: 0` sweep can land in that gap, and nothing has been handed out yet, so the generation current
|
|
4404
4597
|
* once the swept one is gone is exactly what a pin taken now should hold. One retry, then propagate.
|
|
4405
4598
|
*/
|
|
4406
|
-
async pinGeneration(ref) {
|
|
4599
|
+
async pinGeneration(ref, lease) {
|
|
4600
|
+
for (let moved = 0; ; moved++) {
|
|
4601
|
+
try {
|
|
4602
|
+
try {
|
|
4603
|
+
return await this.pinOnce(ref, lease);
|
|
4604
|
+
} catch (err) {
|
|
4605
|
+
if (!isNotFoundError(err)) throw err;
|
|
4606
|
+
return await this.pinOnce(ref, lease);
|
|
4607
|
+
}
|
|
4608
|
+
} catch (err) {
|
|
4609
|
+
if (!(err instanceof PinMoved)) throw err;
|
|
4610
|
+
if (moved >= 2) {
|
|
4611
|
+
throw new WriteConflictError(
|
|
4612
|
+
`pin: the pointer of "${ref.segment}" moved on every attempt to lease its current generation`
|
|
4613
|
+
);
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
/**
|
|
4619
|
+
* Open one named generation and verify it is the object `at.fingerprint` names, for a pin the caller reopens. One
|
|
4620
|
+
* row read and one tail read with a registry (none of the row for a store without one): the open is the one a
|
|
4621
|
+
* pinned read makes ({@link readerAt}), memoised under the version the pin will read by, so the pin's first read
|
|
4622
|
+
* finds it open. A generation that is gone, above the row's pointer, on a destroyed or absent row, or another
|
|
4623
|
+
* object than the fingerprint names, throws `NotFoundError`; nothing here reads empty.
|
|
4624
|
+
*
|
|
4625
|
+
* A generation counts as published while the pointer is at or above it. A rollback deletes nothing, so after one
|
|
4626
|
+
* and a later load, a generation it rolled back from can be reopened while its object is still stored and the
|
|
4627
|
+
* fingerprint matches. Like `pinGeneration`'s pinned reads, this opens the object without the live open's check
|
|
4628
|
+
* of the row's summary against it, since that check is made where the row's own generation is read.
|
|
4629
|
+
*/
|
|
4630
|
+
async pinGenerationAt(ref, at, lease) {
|
|
4631
|
+
const { generation, fingerprint } = at;
|
|
4632
|
+
const gone = (why) => new NotFoundError(
|
|
4633
|
+
`segment "${ref.segment}" generation ${generation} cannot be pinned: ${why}`
|
|
4634
|
+
);
|
|
4635
|
+
let version = versionOf(generation, void 0);
|
|
4636
|
+
let installed;
|
|
4637
|
+
let pinKey;
|
|
4638
|
+
let memoised = false;
|
|
4639
|
+
if (this.registry !== void 0) {
|
|
4640
|
+
const record = await this.registry.get(ref);
|
|
4641
|
+
if (record === null || record.status === "destroyed")
|
|
4642
|
+
throw gone("the segment has no readable row");
|
|
4643
|
+
if (record.currentGen === null || generation > record.currentGen) {
|
|
4644
|
+
throw gone("it is not a published generation of this segment");
|
|
4645
|
+
}
|
|
4646
|
+
let lineage = record.token;
|
|
4647
|
+
if (lease !== void 0) {
|
|
4648
|
+
const taken = await lease.take(record);
|
|
4649
|
+
if (taken === "moved") throw gone("the segment moved while its lease was taken");
|
|
4650
|
+
lineage = taken.token;
|
|
4651
|
+
}
|
|
4652
|
+
version = versionOf(generation, lineage);
|
|
4653
|
+
const key = this.pinnedKey(ref, version);
|
|
4654
|
+
memoised = this.snapshots.peek(key) !== void 0;
|
|
4655
|
+
if (!memoised) {
|
|
4656
|
+
installed = this.install(
|
|
4657
|
+
key,
|
|
4658
|
+
Snapshot.eager(
|
|
4659
|
+
this.openForTarget(ref, {
|
|
4660
|
+
generation,
|
|
4661
|
+
lineage,
|
|
4662
|
+
wrappedDeks: record.wrappedDeks
|
|
4663
|
+
})
|
|
4664
|
+
)
|
|
4665
|
+
);
|
|
4666
|
+
}
|
|
4667
|
+
pinKey = key;
|
|
4668
|
+
}
|
|
4669
|
+
if (this.registry === void 0) {
|
|
4670
|
+
memoised = this.snapshots.peek(this.pinnedKey(ref, version, fingerprint)) !== void 0;
|
|
4671
|
+
}
|
|
4407
4672
|
try {
|
|
4408
|
-
|
|
4673
|
+
await this.readerAt(ref, generation, version, fingerprint);
|
|
4409
4674
|
} catch (err) {
|
|
4410
|
-
if (
|
|
4411
|
-
|
|
4675
|
+
if (installed !== void 0 && pinKey !== void 0 && this.snapshots.peek(pinKey) === installed) {
|
|
4676
|
+
this.snapshots.delete(pinKey);
|
|
4677
|
+
}
|
|
4678
|
+
throw err;
|
|
4679
|
+
}
|
|
4680
|
+
if (!memoised) {
|
|
4681
|
+
const held = this.heldKey(ref, version, fingerprint);
|
|
4682
|
+
this.replacedPins.delete(held);
|
|
4683
|
+
this.checking.delete(held);
|
|
4412
4684
|
}
|
|
4685
|
+
return { generation, version, fingerprint };
|
|
4413
4686
|
}
|
|
4414
|
-
async pinOnce(ref) {
|
|
4415
|
-
|
|
4687
|
+
async pinOnce(ref, lease) {
|
|
4688
|
+
let live = await this.resolveLive(ref);
|
|
4416
4689
|
if (live === null) return null;
|
|
4690
|
+
if (lease !== void 0 && live.row !== void 0) {
|
|
4691
|
+
const taken = await lease.take(live.row);
|
|
4692
|
+
if (taken === "moved") throw new PinMoved();
|
|
4693
|
+
live = { ...live, target: { ...live.target, lineage: taken.token } };
|
|
4694
|
+
}
|
|
4417
4695
|
const target = live.target;
|
|
4418
4696
|
const version = versionOf(target.generation, target.lineage);
|
|
4419
4697
|
let reader;
|
|
@@ -4709,6 +4987,7 @@ var CrbmStorageChunkSource = class {
|
|
|
4709
4987
|
const unwrap = keys === void 0 || keys.length === 0 || this.keystore === void 0 ? void 0 : prior?.unwrap !== void 0 && sameKeys(prior.target.wrappedDeks, keys) ? prior.unwrap : once(() => this.keystore.openDek(keys));
|
|
4710
4988
|
const crypto = once(() => this.cryptoForRead(ref, generation, keys, unwrap));
|
|
4711
4989
|
return {
|
|
4990
|
+
row: record,
|
|
4712
4991
|
target: { generation, lineage: record.token, wrappedDeks: record.wrappedDeks },
|
|
4713
4992
|
crypto,
|
|
4714
4993
|
unwrap,
|
|
@@ -5146,11 +5425,24 @@ function refuseCleartextOntoKey(key, record, cleartext) {
|
|
|
5146
5425
|
}
|
|
5147
5426
|
var UNANSWERED_RESENDS = 3;
|
|
5148
5427
|
var UNANSWERED_RESEND_BASE_MS = 500;
|
|
5149
|
-
|
|
5428
|
+
function leasesPatch(record, options) {
|
|
5429
|
+
const held = record.leases;
|
|
5430
|
+
if (held === void 0) return {};
|
|
5431
|
+
if (options.clearLeases === true) return { leases: void 0 };
|
|
5432
|
+
if (options.leasesNow === void 0) return {};
|
|
5433
|
+
const now = options.leasesNow();
|
|
5434
|
+
const live = held.filter((e) => isLive(e, now));
|
|
5435
|
+
if (live.length === held.length) return {};
|
|
5436
|
+
return { leases: live.length === 0 ? void 0 : live };
|
|
5437
|
+
}
|
|
5438
|
+
async function publishGenerationKept(registry, key, options = {}) {
|
|
5150
5439
|
let fresh;
|
|
5151
5440
|
let failedOn;
|
|
5152
5441
|
let unanswered;
|
|
5153
5442
|
let resends = 0;
|
|
5443
|
+
const churn = leaseChurn({ clock: options.clock, rng: options.rng });
|
|
5444
|
+
let throughChurn = false;
|
|
5445
|
+
let waited;
|
|
5154
5446
|
let sawUnanswered = false;
|
|
5155
5447
|
let lastRecord;
|
|
5156
5448
|
for (let attempt = 0; attempt < 5; attempt++) {
|
|
@@ -5161,6 +5453,8 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
5161
5453
|
fresh = void 0;
|
|
5162
5454
|
unanswered = void 0;
|
|
5163
5455
|
lastRecord = record;
|
|
5456
|
+
let next;
|
|
5457
|
+
let wrote;
|
|
5164
5458
|
if (failedOn !== void 0) {
|
|
5165
5459
|
let landed;
|
|
5166
5460
|
try {
|
|
@@ -5168,57 +5462,78 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
5168
5462
|
} catch (proofErr) {
|
|
5169
5463
|
throw outcomeUnknown(key, proofErr);
|
|
5170
5464
|
}
|
|
5171
|
-
if (landed)
|
|
5465
|
+
if (landed)
|
|
5466
|
+
return {
|
|
5467
|
+
published: true,
|
|
5468
|
+
kept: { ...nextKept(failedOn, key.generation, options.keep), token: void 0 }
|
|
5469
|
+
};
|
|
5172
5470
|
if (record !== null && record.status === "active" && record.currentGen === key.generation) {
|
|
5173
|
-
return
|
|
5471
|
+
return REFUSED;
|
|
5174
5472
|
}
|
|
5175
5473
|
failedOn = void 0;
|
|
5176
5474
|
}
|
|
5177
5475
|
try {
|
|
5178
5476
|
if (options.expectFrom !== void 0 && record?.currentGen !== options.expectFrom) {
|
|
5179
|
-
return
|
|
5477
|
+
return REFUSED;
|
|
5180
5478
|
}
|
|
5181
5479
|
if (options.expectAbsent === true && record !== null) {
|
|
5182
|
-
return
|
|
5480
|
+
return REFUSED;
|
|
5183
5481
|
}
|
|
5482
|
+
throughChurn = false;
|
|
5184
5483
|
if (options.expectToken !== void 0 && record?.token !== options.expectToken) {
|
|
5185
|
-
|
|
5484
|
+
const held = options.expectRow;
|
|
5485
|
+
if (record !== null && held != null && record === waited && onlyLeasesDiffer(held, record)) {
|
|
5486
|
+
throughChurn = true;
|
|
5487
|
+
} else {
|
|
5488
|
+
const settled2 = record === waited ? void 0 : await churn.settle(held, record, () => registry.get(key));
|
|
5489
|
+
if (settled2 !== void 0) {
|
|
5490
|
+
waited = settled2.row;
|
|
5491
|
+
fresh = settled2.row;
|
|
5492
|
+
attempt -= 1;
|
|
5493
|
+
continue;
|
|
5494
|
+
}
|
|
5495
|
+
return REFUSED;
|
|
5496
|
+
}
|
|
5186
5497
|
}
|
|
5187
5498
|
if (record === null && options.summary !== void 0 && "sealed" in options.summary && options.wrappedDeks === void 0) {
|
|
5188
5499
|
if (reused) continue;
|
|
5189
|
-
return
|
|
5500
|
+
return REFUSED;
|
|
5190
5501
|
}
|
|
5191
5502
|
if (record === null) {
|
|
5192
|
-
|
|
5503
|
+
next = nextKept(null, key.generation, options.keep);
|
|
5504
|
+
wrote = (await registry.create(
|
|
5193
5505
|
key,
|
|
5194
5506
|
{
|
|
5195
5507
|
currentGen: key.generation,
|
|
5196
5508
|
wrappedDeks: options.wrappedDeks,
|
|
5197
|
-
...options.summary === void 0 ? {} : { summary: options.summary }
|
|
5509
|
+
...options.summary === void 0 ? {} : { summary: options.summary },
|
|
5510
|
+
...next.list === void 0 ? {} : { keptGens: next.list }
|
|
5198
5511
|
},
|
|
5199
5512
|
write
|
|
5200
|
-
);
|
|
5513
|
+
)).token;
|
|
5201
5514
|
} else if (record.status === "destroyed") {
|
|
5202
5515
|
throw new ValidationError(
|
|
5203
5516
|
`segment "${key.segment}" was destroyed (crypto-shredded) while generation ${key.generation} was being written \u2014 refusing to publish it; the written object is unreadable. Use a new segment.`
|
|
5204
5517
|
);
|
|
5205
5518
|
} else if (record.currentGen === null) {
|
|
5206
5519
|
refuseCleartextOntoKey(key, record, options.cleartext);
|
|
5207
|
-
|
|
5520
|
+
next = nextKept(record, key.generation, options.keep);
|
|
5521
|
+
wrote = (await registry.compareAndSwap(
|
|
5208
5522
|
key,
|
|
5209
5523
|
record.token,
|
|
5210
5524
|
{
|
|
5211
5525
|
currentGen: key.generation,
|
|
5212
5526
|
...options.wrappedDeks === void 0 ? {} : { wrappedDeks: options.wrappedDeks },
|
|
5213
|
-
...options.summary === void 0 ? {} : { summary: options.summary }
|
|
5527
|
+
...options.summary === void 0 ? {} : { summary: options.summary },
|
|
5528
|
+
...next.list === void 0 ? {} : { keptGens: next.list }
|
|
5214
5529
|
},
|
|
5215
5530
|
write
|
|
5216
|
-
);
|
|
5531
|
+
)).token;
|
|
5217
5532
|
} else if (record.currentGen > key.generation) {
|
|
5218
|
-
return
|
|
5533
|
+
return REFUSED;
|
|
5219
5534
|
} else if (record.currentGen === key.generation) {
|
|
5220
5535
|
if (reused) continue;
|
|
5221
|
-
return true;
|
|
5536
|
+
return { published: true };
|
|
5222
5537
|
} else {
|
|
5223
5538
|
if (options.wrappedDeks !== void 0 && options.wrappedDeks.length > 0) {
|
|
5224
5539
|
throw new ValidationError(
|
|
@@ -5226,20 +5541,27 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
5226
5541
|
);
|
|
5227
5542
|
}
|
|
5228
5543
|
refuseCleartextOntoKey(key, record, options.cleartext);
|
|
5229
|
-
|
|
5544
|
+
next = nextKept(record, key.generation, options.keep);
|
|
5545
|
+
wrote = (await registry.compareAndSwap(
|
|
5230
5546
|
key,
|
|
5231
5547
|
record.token,
|
|
5232
5548
|
{
|
|
5233
5549
|
currentGen: key.generation,
|
|
5234
|
-
...options.summary === void 0 ? {} : { summary: options.summary }
|
|
5550
|
+
...options.summary === void 0 ? {} : { summary: options.summary },
|
|
5551
|
+
...next.list === void 0 ? {} : { keptGens: next.list },
|
|
5552
|
+
...leasesPatch(record, options)
|
|
5235
5553
|
},
|
|
5236
5554
|
write
|
|
5237
|
-
);
|
|
5555
|
+
)).token;
|
|
5238
5556
|
}
|
|
5239
|
-
return
|
|
5557
|
+
return {
|
|
5558
|
+
published: true,
|
|
5559
|
+
kept: { ...next ?? { list: void 0, evict: [] }, token: wrote }
|
|
5560
|
+
};
|
|
5240
5561
|
} catch (err) {
|
|
5241
5562
|
if (isWriteConflictError(err)) {
|
|
5242
5563
|
failedOn = record;
|
|
5564
|
+
if (throughChurn) attempt -= 1;
|
|
5243
5565
|
continue;
|
|
5244
5566
|
}
|
|
5245
5567
|
if (!isTransientError(err)) throw err;
|
|
@@ -5268,10 +5590,12 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
5268
5590
|
}
|
|
5269
5591
|
const final = await registry.get(key);
|
|
5270
5592
|
if (final !== null && final.currentGen !== null && final.currentGen >= key.generation) {
|
|
5271
|
-
if (final.currentGen !== key.generation) return
|
|
5272
|
-
if (!sawUnanswered) return true;
|
|
5593
|
+
if (final.currentGen !== key.generation) return REFUSED;
|
|
5594
|
+
if (!sawUnanswered) return { published: true };
|
|
5273
5595
|
try {
|
|
5274
|
-
return
|
|
5596
|
+
return {
|
|
5597
|
+
published: await landedHere(final, lastRecord ?? null, key, options.holdsOwnObject)
|
|
5598
|
+
};
|
|
5275
5599
|
} catch (proofErr) {
|
|
5276
5600
|
throw outcomeUnknown(key, proofErr);
|
|
5277
5601
|
}
|
|
@@ -5281,6 +5605,10 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
5281
5605
|
`publishGeneration: contention setting currentGen for ${key.segment}`
|
|
5282
5606
|
);
|
|
5283
5607
|
}
|
|
5608
|
+
var REFUSED = { published: false };
|
|
5609
|
+
async function publishGeneration(registry, key, options = {}) {
|
|
5610
|
+
return (await publishGenerationKept(registry, key, options)).published;
|
|
5611
|
+
}
|
|
5284
5612
|
async function landedHere(now, before, key, holdsOwnObject) {
|
|
5285
5613
|
if (now === null || now.status === "destroyed" || now.currentGen !== key.generation) return false;
|
|
5286
5614
|
if (before !== null && !sameIncarnation(before, now)) return false;
|
|
@@ -5838,7 +6166,7 @@ async function runLoad(ref, prepare, deps, options) {
|
|
|
5838
6166
|
const key = { namespace: ref.namespace, segment: ref.segment, generation };
|
|
5839
6167
|
const reclaim = async () => {
|
|
5840
6168
|
const now = await deps.registry.get(ref);
|
|
5841
|
-
if (now !== null && (now.token === fromToken || now.status === "destroyed")) {
|
|
6169
|
+
if (now !== null && (now.token === fromToken || now.status === "destroyed" || row !== null && onlyLeasesDiffer(row, now))) {
|
|
5842
6170
|
await deps.storage.delete(key);
|
|
5843
6171
|
return;
|
|
5844
6172
|
}
|
|
@@ -5880,14 +6208,17 @@ async function runLoad(ref, prepare, deps, options) {
|
|
|
5880
6208
|
if (guard?.minRetained !== void 0 && before !== null) {
|
|
5881
6209
|
if (written.cardinality < before * guard.minRetained) return refuse("min-retained");
|
|
5882
6210
|
}
|
|
6211
|
+
const leasesNow = deps.clock === void 0 ? void 0 : () => deps.clock.now();
|
|
5883
6212
|
let published;
|
|
5884
6213
|
try {
|
|
5885
|
-
published = await
|
|
6214
|
+
published = await publishGenerationKept(deps.registry, key, {
|
|
6215
|
+
keep,
|
|
6216
|
+
leasesNow,
|
|
5886
6217
|
row,
|
|
5887
6218
|
wrappedDeks: written.wrappedDeks,
|
|
5888
6219
|
summary: written.summary,
|
|
5889
6220
|
cleartext: !written.encrypted,
|
|
5890
|
-
...fromToken === void 0 ? {} : { expectToken: fromToken },
|
|
6221
|
+
...fromToken === void 0 ? {} : { expectToken: fromToken, expectRow: row },
|
|
5891
6222
|
...needsBefore && fromGeneration !== void 0 ? { expectFrom: fromGeneration } : {},
|
|
5892
6223
|
// The third case, and the one the two fences above structurally cannot cover: the guard judged a segment
|
|
5893
6224
|
// that had NO ROW. Both `expectFrom` and `expectToken` compare against a value read from a row, so with no
|
|
@@ -5911,7 +6242,7 @@ async function runLoad(ref, prepare, deps, options) {
|
|
|
5911
6242
|
if (isValidationError(err) || err instanceof KeyUnavailableError) await reclaim();
|
|
5912
6243
|
throw err;
|
|
5913
6244
|
}
|
|
5914
|
-
if (!published) return refuse("superseded");
|
|
6245
|
+
if (!published.published) return refuse("superseded");
|
|
5915
6246
|
audit.onEvent({
|
|
5916
6247
|
kind: "segment.publish",
|
|
5917
6248
|
segment: ref.segment,
|
|
@@ -5922,7 +6253,10 @@ async function runLoad(ref, prepare, deps, options) {
|
|
|
5922
6253
|
const collected = await collectAfterLoad(ref, deps, {
|
|
5923
6254
|
generation,
|
|
5924
6255
|
keep,
|
|
5925
|
-
byName: checked &&
|
|
6256
|
+
byName: checked && deps.collectByListing !== true,
|
|
6257
|
+
currentGone: currentObjectGone,
|
|
6258
|
+
...leasesNow === void 0 ? {} : { leases: { now: leasesNow } },
|
|
6259
|
+
kept: published.kept,
|
|
5926
6260
|
...current.fromSummary && fromGeneration !== void 0 ? { proveCurrent: { ...ref, generation: fromGeneration } } : {}
|
|
5927
6261
|
});
|
|
5928
6262
|
return {
|
|
@@ -6021,10 +6355,25 @@ async function rollbackSegment(ref, toGeneration, deps, options = {}) {
|
|
|
6021
6355
|
{ cardinality: reader.count(), metadata: reader.metadata },
|
|
6022
6356
|
aead
|
|
6023
6357
|
);
|
|
6024
|
-
const
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6358
|
+
const churn = leaseChurn(deps);
|
|
6359
|
+
let against = record.token;
|
|
6360
|
+
let token;
|
|
6361
|
+
for (; ; ) {
|
|
6362
|
+
try {
|
|
6363
|
+
({ token } = await deps.registry.compareAndSwap(ref, against, {
|
|
6364
|
+
currentGen: toGeneration,
|
|
6365
|
+
summary
|
|
6366
|
+
}));
|
|
6367
|
+
break;
|
|
6368
|
+
} catch (err) {
|
|
6369
|
+
if (!isWriteConflictError(err)) throw err;
|
|
6370
|
+
const seen = await deps.registry.get(ref);
|
|
6371
|
+
const settled2 = await churn.settle(record, seen, () => deps.registry.get(ref));
|
|
6372
|
+
const now = settled2?.row ?? null;
|
|
6373
|
+
if (settled2 === void 0 || now === null || !onlyLeasesDiffer(record, now)) throw err;
|
|
6374
|
+
against = now.token;
|
|
6375
|
+
}
|
|
6376
|
+
}
|
|
6028
6377
|
let stillThere = false;
|
|
6029
6378
|
for await (const key of deps.storage.list(ref)) {
|
|
6030
6379
|
if (key.generation === toGeneration) {
|
|
@@ -6047,13 +6396,31 @@ async function rollbackSegment(ref, toGeneration, deps, options = {}) {
|
|
|
6047
6396
|
}
|
|
6048
6397
|
if (!stillThere || replaced) {
|
|
6049
6398
|
let undone = false;
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6399
|
+
let undoAgainst = token;
|
|
6400
|
+
for (; ; ) {
|
|
6401
|
+
try {
|
|
6402
|
+
await deps.registry.compareAndSwap(ref, undoAgainst, {
|
|
6403
|
+
currentGen: record.currentGen,
|
|
6404
|
+
summary: describingSummary(record)
|
|
6405
|
+
});
|
|
6406
|
+
undone = true;
|
|
6407
|
+
break;
|
|
6408
|
+
} catch (err) {
|
|
6409
|
+
if (!isWriteConflictError(err)) break;
|
|
6410
|
+
const moved = ["currentGen", "summary", "keptGens"];
|
|
6411
|
+
let now;
|
|
6412
|
+
try {
|
|
6413
|
+
const seen = await deps.registry.get(ref);
|
|
6414
|
+
if (seen === null || seen.currentGen !== toGeneration) break;
|
|
6415
|
+
const settled2 = await churn.settle(record, seen, () => deps.registry.get(ref), moved);
|
|
6416
|
+
now = settled2?.row ?? null;
|
|
6417
|
+
} catch {
|
|
6418
|
+
break;
|
|
6419
|
+
}
|
|
6420
|
+
if (now === null || now.currentGen !== toGeneration || !onlyLeasesDiffer(record, now, moved))
|
|
6421
|
+
break;
|
|
6422
|
+
undoAgainst = now.token;
|
|
6423
|
+
}
|
|
6057
6424
|
}
|
|
6058
6425
|
throw new NotFoundError(
|
|
6059
6426
|
`rollback: generation ${toGeneration} of "${ref.segment}" was ${replaced ? "replaced" : "collected"} while the pointer was moving` + (undone ? " \u2014 the pointer was put back" : `, and the move back failed, so the pointer may still name ${toGeneration} (a write that landed and lost its response reads as a failure here). Check which generation is current, then re-run a rollback to a generation that exists, or restore the object.`)
|
|
@@ -6117,7 +6484,7 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
6117
6484
|
if (row === null) return "absent";
|
|
6118
6485
|
if (row.status === "destroyed") return "destroyed";
|
|
6119
6486
|
if (row.currentGen === null) return "no-generation";
|
|
6120
|
-
return row.currentGen === from && row.token === fromToken ? null : "superseded";
|
|
6487
|
+
return row.currentGen === from && (row.token === fromToken || onlyLeasesDiffer(record, row)) ? null : "superseded";
|
|
6121
6488
|
};
|
|
6122
6489
|
const holds = async (generation2) => {
|
|
6123
6490
|
const key2 = { ...base, generation: generation2 };
|
|
@@ -6288,7 +6655,13 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
|
|
|
6288
6655
|
const published = await publishGeneration(deps.registry, key, {
|
|
6289
6656
|
expectFrom: from,
|
|
6290
6657
|
expectToken: fromToken,
|
|
6658
|
+
// A lease written while the rewrite streams moves the token and nothing it derived from: it does not refuse it.
|
|
6659
|
+
expectRow: record,
|
|
6291
6660
|
summary,
|
|
6661
|
+
// The row then records that no generation below the new pointer is kept, as the collection below leaves it.
|
|
6662
|
+
keep: 0,
|
|
6663
|
+
// Every generation below the new pointer is deleted whatever a lease says, so the row drops its leases.
|
|
6664
|
+
clearLeases: true,
|
|
6292
6665
|
// A write that ends without an answer is settled by reading the row, and a pointer at this number is this
|
|
6293
6666
|
// rewrite's only over the object it wrote: the footer proves it, so another incarnation's cannot pass for it.
|
|
6294
6667
|
holdsOwnObject: () => provesOwnObject(deps.storage, key, fingerprint),
|
|
@@ -6621,8 +6994,17 @@ async function listGenerations2(storage, ref) {
|
|
|
6621
6994
|
}
|
|
6622
6995
|
async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
|
|
6623
6996
|
const base = { segment: ref.segment, namespace: ref.namespace };
|
|
6997
|
+
const churn = leaseChurn(deps);
|
|
6998
|
+
let lost;
|
|
6624
6999
|
for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
|
|
6625
|
-
|
|
7000
|
+
let record = await deps.registry.get(ref);
|
|
7001
|
+
if (lost !== void 0) {
|
|
7002
|
+
const settled2 = await churn.settle(lost, record, () => deps.registry.get(ref));
|
|
7003
|
+
if (settled2 !== void 0) {
|
|
7004
|
+
record = settled2.row;
|
|
7005
|
+
attempt -= 1;
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
6626
7008
|
if (record === null) {
|
|
6627
7009
|
return { ...base, destroyed: false, cryptoShredded: false, reason: "absent" };
|
|
6628
7010
|
}
|
|
@@ -6641,11 +7023,14 @@ async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
|
|
|
6641
7023
|
keyId: void 0,
|
|
6642
7024
|
// The current generation's cached count and metadata go with it: sealed, they cannot be opened without
|
|
6643
7025
|
// the wrappings; clear, they would outlive the segment on its tombstone.
|
|
6644
|
-
summary: void 0
|
|
7026
|
+
summary: void 0,
|
|
7027
|
+
// A tombstone holds nothing: a lease names a generation of a segment that now resolves none.
|
|
7028
|
+
leases: void 0
|
|
6645
7029
|
});
|
|
6646
7030
|
return { ...base, destroyed: true, cryptoShredded: encrypted };
|
|
6647
7031
|
} catch (err) {
|
|
6648
7032
|
if (!isWriteConflictError(err)) throw err;
|
|
7033
|
+
lost = record;
|
|
6649
7034
|
}
|
|
6650
7035
|
}
|
|
6651
7036
|
throw new WriteConflictError(`${op}: contention shredding "${ref.segment}" \u2014 retry`);
|
|
@@ -6713,8 +7098,17 @@ async function setSegmentRetention(ref, deps, policy) {
|
|
|
6713
7098
|
validateRetentionPolicy(policy);
|
|
6714
7099
|
const base = { segment: ref.segment, namespace: ref.namespace };
|
|
6715
7100
|
let previousExpiresAt;
|
|
7101
|
+
const churn = leaseChurn(deps);
|
|
7102
|
+
let lost;
|
|
6716
7103
|
for (let attempt = 0; attempt < RETENTION_CAS_ATTEMPTS; attempt += 1) {
|
|
6717
|
-
|
|
7104
|
+
let record = await deps.registry.get(ref);
|
|
7105
|
+
if (lost !== void 0) {
|
|
7106
|
+
const settled2 = await churn.settle(lost, record, () => deps.registry.get(ref));
|
|
7107
|
+
if (settled2 !== void 0) {
|
|
7108
|
+
record = settled2.row;
|
|
7109
|
+
attempt -= 1;
|
|
7110
|
+
}
|
|
7111
|
+
}
|
|
6718
7112
|
try {
|
|
6719
7113
|
previousExpiresAt = readExpiresAt(record);
|
|
6720
7114
|
if (record === null) {
|
|
@@ -6741,6 +7135,7 @@ async function setSegmentRetention(ref, deps, policy) {
|
|
|
6741
7135
|
};
|
|
6742
7136
|
} catch (err) {
|
|
6743
7137
|
if (!isWriteConflictError(err)) throw err;
|
|
7138
|
+
lost = record ?? void 0;
|
|
6744
7139
|
}
|
|
6745
7140
|
}
|
|
6746
7141
|
throw new WriteConflictError(
|
|
@@ -6749,8 +7144,17 @@ async function setSegmentRetention(ref, deps, policy) {
|
|
|
6749
7144
|
}
|
|
6750
7145
|
async function clearSegmentRetention(ref, deps) {
|
|
6751
7146
|
validateUserRef(ref);
|
|
7147
|
+
const churn = leaseChurn(deps);
|
|
7148
|
+
let lost;
|
|
6752
7149
|
for (let attempt = 0; attempt < RETENTION_CAS_ATTEMPTS; attempt += 1) {
|
|
6753
|
-
|
|
7150
|
+
let record = await deps.registry.get(ref);
|
|
7151
|
+
if (lost !== void 0) {
|
|
7152
|
+
const settled2 = await churn.settle(lost, record, () => deps.registry.get(ref));
|
|
7153
|
+
if (settled2 !== void 0) {
|
|
7154
|
+
record = settled2.row;
|
|
7155
|
+
attempt -= 1;
|
|
7156
|
+
}
|
|
7157
|
+
}
|
|
6754
7158
|
if (record === null) return false;
|
|
6755
7159
|
if (record.status === "destroyed") return false;
|
|
6756
7160
|
if (record.retention === void 0 || !(EXPIRES_AT in record.retention)) return false;
|
|
@@ -6764,6 +7168,7 @@ async function clearSegmentRetention(ref, deps) {
|
|
|
6764
7168
|
return true;
|
|
6765
7169
|
} catch (err) {
|
|
6766
7170
|
if (!isWriteConflictError(err)) throw err;
|
|
7171
|
+
lost = record;
|
|
6767
7172
|
}
|
|
6768
7173
|
}
|
|
6769
7174
|
throw new WriteConflictError(
|
|
@@ -7135,6 +7540,31 @@ function failureReason(err) {
|
|
|
7135
7540
|
return `failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
7136
7541
|
}
|
|
7137
7542
|
|
|
7543
|
+
// src/core/registry-reap.ts
|
|
7544
|
+
var DEFAULT_REAP_LIMIT = 1e3;
|
|
7545
|
+
async function reapRegistryTombstones(registry, options = {}) {
|
|
7546
|
+
const dryRun = options.dryRun === true;
|
|
7547
|
+
if (!dryRun && options.confirmNoLegacyWriters !== true) {
|
|
7548
|
+
throw new ValidationError(
|
|
7549
|
+
"reapRegistryTombstones: a real run needs `confirmNoLegacyWriters: true`, your statement that no process on a release before 0.12 writes this registry (one that did would issue a removed row\u2019s tokens again). Pass `dryRun: true` to see what would be removed."
|
|
7550
|
+
);
|
|
7551
|
+
}
|
|
7552
|
+
const limit = options.limit ?? DEFAULT_REAP_LIMIT;
|
|
7553
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
7554
|
+
throw new ValidationError(
|
|
7555
|
+
`reapRegistryTombstones: \`limit\` must be a positive integer; got ${String(limit)}`
|
|
7556
|
+
);
|
|
7557
|
+
}
|
|
7558
|
+
if (options.namespace !== void 0) validateUserNamespace(options.namespace);
|
|
7559
|
+
if (registry.reapLegacyTombstones === void 0) {
|
|
7560
|
+
throw new UnsupportedError(
|
|
7561
|
+
"reapRegistryTombstones: this registry keeps no deleted rows of a release before 0.12 to remove (only the object-store registries, S3, GCS and Azure Blob, do)"
|
|
7562
|
+
);
|
|
7563
|
+
}
|
|
7564
|
+
const request = options.namespace === void 0 ? { dryRun, limit } : { namespace: options.namespace, dryRun, limit };
|
|
7565
|
+
return registry.reapLegacyTombstones(request);
|
|
7566
|
+
}
|
|
7567
|
+
|
|
7138
7568
|
// src/core/consistency.ts
|
|
7139
7569
|
var DEFAULT_CHECK_CONCURRENCY = 8;
|
|
7140
7570
|
async function generationsPresent(storage, ref) {
|
|
@@ -7338,7 +7768,12 @@ export {
|
|
|
7338
7768
|
InProcessKeystore,
|
|
7339
7769
|
IntegrityError,
|
|
7340
7770
|
KeyUnavailableError,
|
|
7771
|
+
LEASE_SKEW_MS,
|
|
7772
|
+
LeaseExpiredError,
|
|
7773
|
+
LeaseLimitError,
|
|
7341
7774
|
LocalFsStorage,
|
|
7775
|
+
MAX_LEASES_PER_SEGMENT,
|
|
7776
|
+
MAX_LEASE_MS,
|
|
7342
7777
|
MIN_EXPIRES_AT_MS,
|
|
7343
7778
|
MemoryStorage,
|
|
7344
7779
|
NOOP_METRICS,
|
|
@@ -7357,6 +7792,7 @@ export {
|
|
|
7357
7792
|
checkBudget,
|
|
7358
7793
|
clearSegmentRetention,
|
|
7359
7794
|
collectWithinBudget,
|
|
7795
|
+
decodeSerialized,
|
|
7360
7796
|
destroySegment,
|
|
7361
7797
|
dropSegment,
|
|
7362
7798
|
eraseIdFromSegment,
|
|
@@ -7367,6 +7803,8 @@ export {
|
|
|
7367
7803
|
groundedReport,
|
|
7368
7804
|
isCloudRoaringError,
|
|
7369
7805
|
isIntegrityError,
|
|
7806
|
+
isLeaseExpiredError,
|
|
7807
|
+
isLeaseLimitError,
|
|
7370
7808
|
isNotFoundError,
|
|
7371
7809
|
isStorageBackend,
|
|
7372
7810
|
isTransientError,
|
|
@@ -7378,6 +7816,8 @@ export {
|
|
|
7378
7816
|
loadSegmentChunks,
|
|
7379
7817
|
mapWithConcurrency,
|
|
7380
7818
|
readRetentionPolicy,
|
|
7819
|
+
reapRegistryTombstones,
|
|
7820
|
+
releaseLease,
|
|
7381
7821
|
resolveBudget,
|
|
7382
7822
|
resolvePerOpBudget,
|
|
7383
7823
|
retireExpired,
|
|
@@ -7389,6 +7829,7 @@ export {
|
|
|
7389
7829
|
segmentKey,
|
|
7390
7830
|
setSegmentRetention,
|
|
7391
7831
|
splitId,
|
|
7832
|
+
takeLease,
|
|
7392
7833
|
withRetry
|
|
7393
7834
|
};
|
|
7394
7835
|
//# sourceMappingURL=index.js.map
|