@optimystic/db-p2p 0.25.1 → 0.26.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/src/cluster/cluster-repo.d.ts +47 -91
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +138 -200
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/race-resolution.d.ts +89 -0
- package/dist/src/cluster/race-resolution.d.ts.map +1 -0
- package/dist/src/cluster/race-resolution.js +140 -0
- package/dist/src/cluster/race-resolution.js.map +1 -0
- package/dist/src/cluster/record-operations.d.ts +25 -0
- package/dist/src/cluster/record-operations.d.ts.map +1 -0
- package/dist/src/cluster/record-operations.js +56 -0
- package/dist/src/cluster/record-operations.js.map +1 -0
- package/dist/src/cohort-topic/host.d.ts +133 -51
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +290 -83
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +7 -4
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +5 -5
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/storage/block-latch.d.ts +5 -4
- package/dist/src/storage/block-latch.d.ts.map +1 -1
- package/dist/src/storage/block-latch.js +5 -4
- package/dist/src/storage/block-latch.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts +1 -1
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +11 -3
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +17 -1
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/cached-raw-storage.js +8 -1
- package/dist/src/storage/cached-raw-storage.js.map +1 -1
- package/dist/src/storage/cached-store-driver.d.ts +7 -0
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -1
- package/dist/src/storage/cached-store-driver.js +20 -0
- package/dist/src/storage/cached-store-driver.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +45 -2
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js +29 -0
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +16 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.d.ts +5 -1
- package/dist/src/storage/kv-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.js +8 -1
- package/dist/src/storage/kv-raw-storage.js.map +1 -1
- package/dist/src/storage/raw-store-driver.d.ts +15 -0
- package/dist/src/storage/raw-store-driver.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +5 -0
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.js +6 -1
- package/dist/src/storage/shared-cache-pool.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +141 -99
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/with-read-cache.d.ts +9 -5
- package/dist/src/storage/with-read-cache.d.ts.map +1 -1
- package/dist/src/storage/with-read-cache.js +16 -6
- package/dist/src/storage/with-read-cache.js.map +1 -1
- package/package.json +2 -2
- package/{README.md → readme.md} +2 -2
- package/src/cluster/cluster-repo.ts +187 -211
- package/src/cluster/race-resolution.ts +158 -0
- package/src/cluster/record-operations.ts +59 -0
- package/src/cohort-topic/host.ts +423 -118
- package/src/libp2p-node-base.ts +12 -9
- package/src/storage/block-latch.ts +5 -4
- package/src/storage/block-storage.ts +11 -3
- package/src/storage/cached-raw-storage.ts +21 -1
- package/src/storage/cached-store-driver.ts +23 -0
- package/src/storage/i-block-storage.ts +47 -2
- package/src/storage/i-raw-storage.ts +17 -0
- package/src/storage/kv-raw-storage.ts +8 -1
- package/src/storage/raw-store-driver.ts +26 -0
- package/src/storage/shared-cache-pool.ts +6 -1
- package/src/storage/storage-repo.ts +143 -103
- package/src/storage/with-read-cache.ts +16 -6
|
@@ -520,6 +520,8 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
520
520
|
};
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
+
// Already deduped: `blockIdsForTransforms` builds its result through a Set. So the pass-2 save
|
|
524
|
+
// loop below cannot write one block twice, and the echoed `blockIds` carries no duplicate.
|
|
523
525
|
const blockIds = blockIdsForTransforms(request.transforms);
|
|
524
526
|
log('pend actionId=%s blockIds=%d rev=%s', request.actionId, blockIds.length, request.rev);
|
|
525
527
|
const pendings: ActionPending[] = [];
|
|
@@ -531,124 +533,162 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
531
533
|
// Blocks this action ALREADY committed at exactly the requested revision — the durable half
|
|
532
534
|
// of a torn action whose retry reuses the same actionId. Sibling of the `alreadyDone`
|
|
533
535
|
// partition in `commit` below: satisfied, not merely non-stale, so no pending is recorded
|
|
534
|
-
// for them (see
|
|
536
|
+
// for them (see pass 2).
|
|
535
537
|
const satisfied = new Set<BlockId>();
|
|
536
|
-
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
538
|
+
// Blocks observed at or past the requested revision under a DIFFERENT action — a real stale
|
|
539
|
+
// loss. Counted separately from `missing` because the two are not the same question: `missing`
|
|
540
|
+
// is the catch-up the loser is handed, and a node whose revision index is sparse over
|
|
541
|
+
// [request.rev, latest.rev] hands back an empty one while still having lost. Gating the
|
|
542
|
+
// refusal on the enumeration would then let a block pass classification that pass 2 cannot
|
|
543
|
+
// write (`savePendingTransaction` refuses it), turning a stale answer into a throw. `commit`
|
|
544
|
+
// takes the same position — it pushes a `missedCommits` entry "even if transforms is empty,
|
|
545
|
+
// because we want to reject the older version".
|
|
546
|
+
let staleCount = 0;
|
|
547
|
+
|
|
548
|
+
// Classifying and saving are ONE atomic step per pend: both passes below run inside a single
|
|
549
|
+
// multi-block write-latch hold, so no commit can land between deciding a block is pendable
|
|
550
|
+
// and writing its pending record. That is the whole property — a pend never writes a pending
|
|
551
|
+
// record for a revision already taken. Such a record could never be promoted (`commit`
|
|
552
|
+
// partitions the block as already-done or refuses it as stale, and promotion is the only
|
|
553
|
+
// thing that removes a record on the success path), and would then be reported as a
|
|
554
|
+
// conflicting in-flight action to every later writer of the block. See docs/repository.md,
|
|
555
|
+
// Invariant P; `BlockStorage.savePendingTransaction` refuses such a write outright.
|
|
556
|
+
//
|
|
557
|
+
// TWO passes, not one interleaved loop: with a single loop a block refused partway through
|
|
558
|
+
// would leave records already written for its predecessors, and retracting those under the
|
|
559
|
+
// hold could delete a record an EARLIER pend of the same action legitimately left. Classify
|
|
560
|
+
// everything before writing anything, and no record is ever written that must be taken back.
|
|
561
|
+
//
|
|
562
|
+
// Everything inside the hold is local storage I/O. No network I/O and no caller-supplied
|
|
563
|
+
// code may enter it — `checkPendValidation` above can call the caller's validation hook,
|
|
564
|
+
// which is precisely why it stays outside. `commit` keeps the same rule. Acquiring through
|
|
565
|
+
// `acquireBlockWriteLatches` (deduped, sorted) is what keeps the three multi-latch holders —
|
|
566
|
+
// this, `commit`, and `applyInvalidation` — free of deadlock, and no caller of `pend`
|
|
567
|
+
// (`ClusterRepo`, `CoordinatorRepo`, `service.ts`) holds a block latch, so the hold cannot
|
|
568
|
+
// re-enter itself.
|
|
569
|
+
//
|
|
570
|
+
// NOTE: a pend now blocks concurrent commits on its blocks for the span of BOTH passes, not
|
|
571
|
+
// just its writes. Accepted: every call inside is local storage I/O, and `commit` already
|
|
572
|
+
// holds the same set for a comparable span. If pend latency on contended blocks ever shows
|
|
573
|
+
// up in a profile, two things inside the hold scale with width and are the ones to look at:
|
|
574
|
+
// the policy-'r' arm reads one transform per rival, and pass 2 awaits its saves one block at
|
|
575
|
+
// a time (where the pre-latch code fanned out with `Promise.all`). Sequential is the
|
|
576
|
+
// deliberate choice — a throw mid-pass then strands records for FEWER blocks, not more — so
|
|
577
|
+
// batch or fan out only with that tradeoff in hand.
|
|
578
|
+
const { latches, release } = await acquireBlockWriteLatches(blockIds);
|
|
579
|
+
try {
|
|
580
|
+
// --- Pass 1: classify. Every read below runs under the hold. ---
|
|
581
|
+
for (const blockId of blockIds) {
|
|
582
|
+
const blockStorage = this.createBlockStorage(blockId);
|
|
583
|
+
const transforms = transformForBlockId(request.transforms, blockId);
|
|
584
|
+
|
|
585
|
+
// Handle any conflicting revisions FIRST: a block this same action already committed at
|
|
586
|
+
// exactly the requested revision is satisfied, and skips both this check and the
|
|
587
|
+
// pending-action listing below.
|
|
588
|
+
if (request.rev !== undefined || transforms.insert) {
|
|
589
|
+
const latest = await blockStorage.getLatest();
|
|
590
|
+
// Our own already-durable work, met again by a retry (see {@link isOwnRevision}):
|
|
591
|
+
// treating it as a stale rival would refuse the writer with its own commit.
|
|
592
|
+
// NOTE: a rev-less pend (`request.rev === undefined`, an insert-only claim) can
|
|
593
|
+
// never match, so a torn action retried WITHOUT a revision is still refused by its
|
|
594
|
+
// own insert. No production caller sends one — `TransactorSource.transact` and the
|
|
595
|
+
// multi-collection coordinator both require a rev — so this is unreachable today;
|
|
596
|
+
// if a rev-less write path ever appears, match on `latest.actionId` alone here.
|
|
597
|
+
if (isOwnRevision(latest, request.rev, request.actionId)) {
|
|
598
|
+
satisfied.add(blockId);
|
|
599
|
+
continue;
|
|
571
600
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
601
|
+
if (latest && latest.rev >= (request.rev ?? 0)) {
|
|
602
|
+
// Only a real revision race yields a meaningful `staleAt`. When `request.rev` is
|
|
603
|
+
// undefined this same branch fires for an insert collision (the comparison degrades
|
|
604
|
+
// to `latest.rev >= 0`, true for any existing block), and reporting that block's
|
|
605
|
+
// revision would be a number that answers a question nobody asked.
|
|
606
|
+
if (request.rev !== undefined) {
|
|
607
|
+
staleAt = highestStaleAt([staleAt, { blockId, rev: latest.rev }]);
|
|
608
|
+
}
|
|
609
|
+
staleCount++;
|
|
610
|
+
const missedRevisions = await asyncIteratorToArray(blockStorage.listRevisions(request.rev ?? 0, latest.rev));
|
|
611
|
+
for (const actionRev of missedRevisions) {
|
|
612
|
+
const transform = await blockStorage.getTransaction(actionRev.actionId);
|
|
613
|
+
if (!transform) {
|
|
614
|
+
throw new Error(`Missing action ${actionRev.actionId} for block ${blockId}`);
|
|
615
|
+
}
|
|
616
|
+
missing.push({
|
|
617
|
+
actionId: actionRev.actionId,
|
|
618
|
+
rev: actionRev.rev,
|
|
619
|
+
transforms: transformsFromTransform(transform, blockId)
|
|
620
|
+
});
|
|
577
621
|
}
|
|
578
|
-
missing.push({
|
|
579
|
-
actionId: actionRev.actionId,
|
|
580
|
-
rev: actionRev.rev,
|
|
581
|
-
transforms: transformsFromTransform(transform, blockId)
|
|
582
|
-
});
|
|
583
622
|
}
|
|
584
623
|
}
|
|
585
|
-
}
|
|
586
624
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
if (missing.length) {
|
|
593
|
-
log('pend:stale actionId=%s missing=%d', request.actionId, missing.length);
|
|
594
|
-
return {
|
|
595
|
-
success: false,
|
|
596
|
-
conflict: true,
|
|
597
|
-
missing,
|
|
598
|
-
...(staleAt === undefined ? {} : { staleAt })
|
|
599
|
-
};
|
|
600
|
-
}
|
|
625
|
+
// Then handle any pending actions
|
|
626
|
+
const pending = await asyncIteratorToArray(blockStorage.listPendingTransactions());
|
|
627
|
+
pendings.push(...pending.map(actionId => ({ blockId, actionId })));
|
|
628
|
+
}
|
|
601
629
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
630
|
+
// Every refusal below returns having written ZERO pending records — that is what pass 1
|
|
631
|
+
// finishing before pass 2 begins buys.
|
|
632
|
+
if (staleCount > 0) {
|
|
633
|
+
log('pend:stale actionId=%s stale=%d missing=%d', request.actionId, staleCount, missing.length);
|
|
606
634
|
return {
|
|
607
635
|
success: false,
|
|
608
636
|
conflict: true,
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
return {
|
|
612
|
-
blockId: action.blockId,
|
|
613
|
-
actionId: action.actionId,
|
|
614
|
-
transform: (await blockStorage.getPendingTransaction(action.actionId))
|
|
615
|
-
?? (await blockStorage.getTransaction(action.actionId))! // Possible that since enumeration, the action has been promoted
|
|
616
|
-
}
|
|
617
|
-
}))
|
|
637
|
+
missing,
|
|
638
|
+
...(staleAt === undefined ? {} : { staleAt })
|
|
618
639
|
};
|
|
619
640
|
}
|
|
620
|
-
}
|
|
621
641
|
|
|
642
|
+
if (pendings.length > 0) {
|
|
643
|
+
if (request.policy === 'f') { // Fail on pending actions
|
|
644
|
+
return { success: false, conflict: true, pending: pendings };
|
|
645
|
+
} else if (request.policy === 'r') { // Return populated pending actions
|
|
646
|
+
return {
|
|
647
|
+
success: false,
|
|
648
|
+
conflict: true,
|
|
649
|
+
pending: await Promise.all(pendings.map(async action => {
|
|
650
|
+
const blockStorage = this.createBlockStorage(action.blockId);
|
|
651
|
+
return {
|
|
652
|
+
blockId: action.blockId,
|
|
653
|
+
actionId: action.actionId,
|
|
654
|
+
// The fallback stays: a rival enumerated on a block we hold cannot be promoted
|
|
655
|
+
// out from under us mid-hold, but a partially-overlapping pend can still have
|
|
656
|
+
// promoted one on a block outside this hold.
|
|
657
|
+
transform: (await blockStorage.getPendingTransaction(action.actionId))
|
|
658
|
+
?? (await blockStorage.getTransaction(action.actionId))!
|
|
659
|
+
}
|
|
660
|
+
}))
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
}
|
|
622
664
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
// ride in the returned `blockIds` so `cancel` covers them (deleting an absent pending is a
|
|
640
|
-
// no-op that writes no metadata).
|
|
641
|
-
await Promise.all(blockIds.filter(blockId => !satisfied.has(blockId)).map(blockId => {
|
|
642
|
-
const blockStorage = this.createBlockStorage(blockId);
|
|
643
|
-
const blockTransform = transformForBlockId(request.transforms, blockId);
|
|
644
|
-
return withBlockWriteLatch(blockId, latch => blockStorage.savePendingTransaction(request.actionId, blockTransform, latch));
|
|
645
|
-
}));
|
|
665
|
+
// --- Pass 2: save. Same hold, so nothing advanced a block since pass 1 observed it. ---
|
|
666
|
+
//
|
|
667
|
+
// `satisfied` blocks are skipped: `commit`'s `alreadyDone` arm skips `internalCommit`, the
|
|
668
|
+
// only thing that promotes (and thereby removes) a pending record, so a pending saved here
|
|
669
|
+
// would never clear — a permanent durable reservation that the rival-pending checks (this
|
|
670
|
+
// method's listPendingTransactions scan, and `ClusterMember.validatePendOperations`) refuse
|
|
671
|
+
// every future writer against. They still ride in the returned `blockIds` so `cancel`
|
|
672
|
+
// covers them (deleting an absent pending is a no-op that writes no metadata).
|
|
673
|
+
for (const blockId of blockIds) {
|
|
674
|
+
if (satisfied.has(blockId)) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
const blockStorage = this.createBlockStorage(blockId);
|
|
678
|
+
const blockTransform = transformForBlockId(request.transforms, blockId);
|
|
679
|
+
await blockStorage.savePendingTransaction(request.actionId, blockTransform, request.rev, latches.get(blockId)!);
|
|
680
|
+
}
|
|
646
681
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
682
|
+
return {
|
|
683
|
+
success: true,
|
|
684
|
+
pending: pendings,
|
|
685
|
+
blockIds
|
|
686
|
+
} as PendSuccess;
|
|
687
|
+
} finally {
|
|
688
|
+
// Releases on every path, including the early returns above and the
|
|
689
|
+
// `Missing action … for block …` throw inside pass 1.
|
|
690
|
+
release();
|
|
691
|
+
}
|
|
652
692
|
}
|
|
653
693
|
|
|
654
694
|
async cancel(actionRef: ActionBlocks, _options?: MessageOptions): Promise<void> {
|
|
@@ -102,8 +102,12 @@ class Lease implements ReadCacheLease {
|
|
|
102
102
|
* Returns the storage **unchanged**, with no `lease`, when caching would not pay:
|
|
103
103
|
* - `MemoryRawStorage` is already in memory; the cache would duplicate every map entry's
|
|
104
104
|
* bookkeeping with nothing to save (see `CachedStoreDriver`'s class doc).
|
|
105
|
-
* -
|
|
106
|
-
* and stays the host's to dispose.
|
|
105
|
+
* - A storage that reports {@link IRawStorage.readCached} (a host that attached a cache before
|
|
106
|
+
* handing it over) is not wrapped twice, and stays the host's to dispose. The capability, not
|
|
107
|
+
* a class check: BOTH documented constructions — `new CachedRawStorage(inner)` and
|
|
108
|
+
* `new KvRawStorage(new CachedStoreDriver(driver))` — report it, and only the first is a
|
|
109
|
+
* `CachedRawStorage`. Checking the class instead sent the driver-level shape down the
|
|
110
|
+
* construct path, where the pool's identity guard threw and the host's node failed to start.
|
|
107
111
|
*
|
|
108
112
|
* Why this is needed at all: `BlockStorage` re-reads block metadata on essentially every
|
|
109
113
|
* operation and `StorageRepo` builds a fresh `BlockStorage` per block per call, so nothing above
|
|
@@ -122,9 +126,9 @@ class Lease implements ReadCacheLease {
|
|
|
122
126
|
* unwrapped instance handed to two consumers). Identity is one-directional — equal proves
|
|
123
127
|
* sameness, unequal proves nothing — so backends whose identity under-approximates (path
|
|
124
128
|
* aliases, two handles over one database; each backend's `NOTE:` lists its gaps) can still end
|
|
125
|
-
* up with two caches; and a host that builds its own
|
|
126
|
-
* registry, so a second consumer wrapping a fresh instance over that store still gets
|
|
127
|
-
* cache. What remains of Invariant 5 (`packages/db-p2p/docs/storage.md`) is the cross-process
|
|
129
|
+
* up with two caches; and a host that builds its own cache — in either construction — never
|
|
130
|
+
* enters the registry, so a second consumer wrapping a fresh instance over that store still gets
|
|
131
|
+
* a second cache. What remains of Invariant 5 (`packages/db-p2p/docs/storage.md`) is the cross-process
|
|
128
132
|
* case: the filesystem driver takes no lock, and a second process's writes bypass this cache.
|
|
129
133
|
*
|
|
130
134
|
* On a dedupe hit the FIRST caller's `label` and `pool` stick: `pool.stats()` shows whoever
|
|
@@ -148,7 +152,13 @@ class Lease implements ReadCacheLease {
|
|
|
148
152
|
* dedupe hit.
|
|
149
153
|
*/
|
|
150
154
|
export function withReadCache(storage: IRawStorage, label?: string, pool?: SharedCachePool): ResolvedReadCache {
|
|
151
|
-
|
|
155
|
+
// `readCached` is a plain property read, which keeps this whole function synchronous — see
|
|
156
|
+
// the "no `await` between lookup and insert" note above; never make it an accessor that
|
|
157
|
+
// could become async. `MemoryRawStorage` never carries the marker (its driver is uncached);
|
|
158
|
+
// it is excluded for a different reason and keeps its own class check. Note a plain
|
|
159
|
+
// `KvRawStorage` over a bare `MemoryStoreDriver` is NOT a `MemoryRawStorage` and is still
|
|
160
|
+
// wrapped — deliberate, and pinned by a test.
|
|
161
|
+
if (storage instanceof MemoryRawStorage || storage.readCached) {
|
|
152
162
|
return { storage, lease: undefined };
|
|
153
163
|
}
|
|
154
164
|
|