@optimystic/db-p2p 0.17.0 → 0.18.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/block-transfer-service.d.ts +10 -0
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +2 -1
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +112 -0
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -0
- package/dist/src/cluster/cluster-policy.js +88 -0
- package/dist/src/cluster/cluster-policy.js.map +1 -0
- package/dist/src/cluster/cluster-repo.d.ts +35 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +88 -19
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/quorum-restore.d.ts +25 -3
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +27 -3
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/reconcile-block.d.ts +10 -2
- package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
- package/dist/src/cluster/reconcile-block.js +38 -18
- package/dist/src/cluster/reconcile-block.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +8 -0
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/inbound-authorization.d.ts +6 -0
- package/dist/src/inbound-authorization.d.ts.map +1 -1
- package/dist/src/inbound-authorization.js +6 -0
- package/dist/src/inbound-authorization.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +14 -0
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +54 -4
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +22 -23
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +22 -19
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +21 -3
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +27 -5
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +60 -26
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +218 -65
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +9 -0
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +56 -4
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +10 -0
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +14 -1
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +20 -0
- package/src/cluster/block-transfer-service.ts +9 -1
- package/src/cluster/cluster-policy.ts +152 -0
- package/src/cluster/cluster-repo.ts +93 -22
- package/src/cluster/quorum-restore.ts +28 -3
- package/src/cluster/reconcile-block.ts +52 -19
- package/src/cluster/spread-on-churn.ts +8 -0
- package/src/inbound-authorization.ts +6 -0
- package/src/libp2p-key-network.ts +54 -4
- package/src/libp2p-node-base.ts +42 -43
- package/src/repo/cluster-coordinator.ts +30 -6
- package/src/repo/coordinator-repo.ts +235 -73
- package/src/storage/storage-repo.ts +58 -6
- package/src/testing/mesh-harness.ts +15 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
IRepo, MessageOptions, BlockId, CommitRequest, CommitResult, GetBlockResults, PendRequest, PendResult, ActionBlocks,
|
|
3
3
|
ActionId, BlockGets, ActionPending, PendSuccess, ActionTransform, ActionTransforms,
|
|
4
|
-
GetBlockResult, IBlock, ActionRev,
|
|
4
|
+
GetBlockResult, IBlock, ActionRev, BlockUnavailableReason,
|
|
5
5
|
PendValidationHook,
|
|
6
6
|
CollectionId, IBlockChangeNotifier, CollectionChangeListener, CollectionChangeEvent
|
|
7
7
|
} from "@optimystic/db-core";
|
|
@@ -186,6 +186,11 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
186
186
|
const promotions: { collectionId: CollectionId, blockId: BlockId, actionId: ActionId, rev: number }[] = [];
|
|
187
187
|
const results = await Promise.all(distinctBlockIds.map(async (blockId) => {
|
|
188
188
|
const blockStorage = this.createBlockStorage(blockId);
|
|
189
|
+
// Set when this node KNOWS its answer for the block is a guess: the promotion
|
|
190
|
+
// below refused for a missing base, or getBlock() threw (truncated history /
|
|
191
|
+
// failed restore). An absent-reading block then reports `unavailable` instead of
|
|
192
|
+
// posing as an authoritative "never existed" — see BlockUnavailableReason.
|
|
193
|
+
let unavailable: BlockUnavailableReason | undefined;
|
|
189
194
|
|
|
190
195
|
// Ensure that all outstanding transactions in the context are committed.
|
|
191
196
|
// This promotes a landed-elsewhere pending via internalCommit, which mutates
|
|
@@ -235,6 +240,10 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
235
240
|
if (!(err instanceof MissingBaseRevisionError)) {
|
|
236
241
|
throw err;
|
|
237
242
|
}
|
|
243
|
+
// This node holds records PROVING the block exists (a pending it could not
|
|
244
|
+
// promote); if the block then reads as absent below, the answer is a guess,
|
|
245
|
+
// not an authoritative "never existed".
|
|
246
|
+
unavailable = 'unmaterializable';
|
|
238
247
|
log('get:promote-skipped-missing-base blockId=%s rev=%d reason=%s', blockId, err.rev, err.message);
|
|
239
248
|
}
|
|
240
249
|
});
|
|
@@ -248,7 +257,26 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
248
257
|
// (actionId, rev) self-heals it via storage.recover() in commit(). Not repaired lazily on
|
|
249
258
|
// the read path because get() holds no commit latch; if stale reads on unwritten blocks
|
|
250
259
|
// ever become a problem, add a latched lazy recover() here.
|
|
251
|
-
|
|
260
|
+
//
|
|
261
|
+
// getBlock() THROWS when this node holds a `latest` it cannot materialize (truncated
|
|
262
|
+
// history: "Failed to find materialized block", or a failed restore). Caught PER BLOCK so
|
|
263
|
+
// one broken block cannot fail the whole batch's Promise.all and take healthy siblings
|
|
264
|
+
// down with it. The read still fails for THIS block — TransactorSource throws
|
|
265
|
+
// BlockUnavailableError on the flagged entry — so nothing is swallowed.
|
|
266
|
+
let blockRev: Awaited<ReturnType<IBlockStorage['getBlock']>>;
|
|
267
|
+
try {
|
|
268
|
+
blockRev = await blockStorage.getBlock(context?.rev);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
// NOTE: the entry drops `state.latest`, which this node does know (getLatest() does not
|
|
271
|
+
// materialize, so it does not throw). Empty state is what makes CoordinatorRepo treat the
|
|
272
|
+
// block as missing and consult the cohort — exactly the repair this block needs. If a
|
|
273
|
+
// consumer ever needs the revision behind an unavailable answer (e.g. to ask the cohort
|
|
274
|
+
// for a specific rev instead of the whole block), carry `latest` here and widen the
|
|
275
|
+
// coordinator's consult trigger to `isMissing || unavailable` so repair still fires.
|
|
276
|
+
log('get:unmaterializable blockId=%s error=%s', blockId,
|
|
277
|
+
err instanceof Error ? err.message : String(err));
|
|
278
|
+
return [blockId, { state: {}, unavailable: 'unmaterializable' } as GetBlockResult];
|
|
279
|
+
}
|
|
252
280
|
|
|
253
281
|
// Include pending action if requested — handled first so a pending-only
|
|
254
282
|
// insert (no committed revision yet) can still be served by applying the
|
|
@@ -256,6 +284,8 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
256
284
|
if (context?.actionId !== undefined) {
|
|
257
285
|
const pendingTransform = await blockStorage.getPendingTransaction(context.actionId);
|
|
258
286
|
if (!pendingTransform) {
|
|
287
|
+
// Caller-contract violation (the caller asserted a pending this repo never had, or
|
|
288
|
+
// cancelled) — an error, not an availability question. Deliberately NOT `unavailable`.
|
|
259
289
|
throw new Error(`Pending action ${context.actionId} not found`);
|
|
260
290
|
}
|
|
261
291
|
const block = applyTransform(blockRev?.block, pendingTransform);
|
|
@@ -264,12 +294,21 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
264
294
|
state: {
|
|
265
295
|
latest: await blockStorage.getLatest(),
|
|
266
296
|
pendings: [context.actionId]
|
|
267
|
-
}
|
|
268
|
-
|
|
297
|
+
},
|
|
298
|
+
// A pending applied to a missing base can materialize nothing (applyTransform drops
|
|
299
|
+
// updates with no block to apply them to) — that absence is a guess, and is flagged.
|
|
300
|
+
// A materialized block is a real answer regardless of the earlier refusal.
|
|
301
|
+
...(unavailable !== undefined && block === undefined ? { unavailable } : {})
|
|
302
|
+
} as GetBlockResult];
|
|
269
303
|
}
|
|
270
304
|
|
|
271
305
|
if (!blockRev) {
|
|
272
|
-
|
|
306
|
+
// `unavailable` distinguishes "never existed" (the common insert-probe case, no flag)
|
|
307
|
+
// from "this node cannot reconstruct it" (the promotion above refused for a missing
|
|
308
|
+
// base). A tombstoned block also lands here with meta.latest set, but it never enters
|
|
309
|
+
// the missing-base catch, so it stays an authoritative absent — keyed off the explicit
|
|
310
|
+
// flag, not off "no block".
|
|
311
|
+
return [blockId, { state: {}, ...(unavailable !== undefined ? { unavailable } : {}) } as GetBlockResult];
|
|
273
312
|
}
|
|
274
313
|
|
|
275
314
|
const pendings = await asyncIteratorToArray(blockStorage.listPendingTransactions());
|
|
@@ -322,6 +361,8 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
322
361
|
if (this.validatePend && request.transaction && request.operationsHash) {
|
|
323
362
|
const validationResult = await this.validatePend(request.transaction, request.operationsHash);
|
|
324
363
|
if (!validationResult.valid) {
|
|
364
|
+
// Hard rejection: the transaction itself is invalid, so no `conflict` flag — a
|
|
365
|
+
// re-read and re-pend would fail the same way and only burn the retry budget.
|
|
325
366
|
return {
|
|
326
367
|
success: false,
|
|
327
368
|
reason: validationResult.reason ?? 'Transaction validation failed'
|
|
@@ -373,16 +414,18 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
373
414
|
log('pend:stale actionId=%s missing=%d', request.actionId, missing.length);
|
|
374
415
|
return {
|
|
375
416
|
success: false,
|
|
417
|
+
conflict: true,
|
|
376
418
|
missing
|
|
377
419
|
};
|
|
378
420
|
}
|
|
379
421
|
|
|
380
422
|
if (pendings.length > 0) {
|
|
381
423
|
if (request.policy === 'f') { // Fail on pending actions
|
|
382
|
-
return { success: false, pending: pendings };
|
|
424
|
+
return { success: false, conflict: true, pending: pendings };
|
|
383
425
|
} else if (request.policy === 'r') { // Return populated pending actions
|
|
384
426
|
return {
|
|
385
427
|
success: false,
|
|
428
|
+
conflict: true,
|
|
386
429
|
pending: await Promise.all(pendings.map(async action => {
|
|
387
430
|
const blockStorage = this.createBlockStorage(action.blockId);
|
|
388
431
|
return {
|
|
@@ -745,6 +788,15 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
745
788
|
* all, so it is translated into {@link MissingBaseRevisionError} rather than surfacing as an opaque
|
|
746
789
|
* storage fault: the healing path can then repair the block instead of the fault resetting the
|
|
747
790
|
* cluster stream, and a wedged node recovers on the next write touching the block.
|
|
791
|
+
*
|
|
792
|
+
* The catch is deliberately UNNARROWED — it also absorbs a transient fault (a raw-storage read
|
|
793
|
+
* error, a `restoreCallback` timeout on a block whose `ranges` do not cover its own `latest`).
|
|
794
|
+
* BlockStorage reports every one of these as a bare `Error`, so they cannot be told apart here,
|
|
795
|
+
* and treating them as divergence is the safe default: this node genuinely cannot materialize the
|
|
796
|
+
* base right now, and the cluster's policy is to heal rather than throw out of consensus. The
|
|
797
|
+
* price is that a transient fault ALSO drops the pending (see {@link refuseMissingBase}), so the
|
|
798
|
+
* block converges by replication instead of by a replay the retry could have done. Narrowing this
|
|
799
|
+
* would require typed faults out of BlockStorage; until then, prefer the tolerant reading.
|
|
748
800
|
*/
|
|
749
801
|
private async readCommitBase(
|
|
750
802
|
blockId: BlockId,
|
|
@@ -248,7 +248,7 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
248
248
|
};
|
|
249
249
|
const factory = coordinatorRepo(
|
|
250
250
|
nodeKeyNetwork,
|
|
251
|
-
|
|
251
|
+
createClusterClient,
|
|
252
252
|
{
|
|
253
253
|
clusterSize: options.clusterSize ?? nodeCount,
|
|
254
254
|
superMajorityThreshold: options.superMajorityThreshold ?? DEFAULT_SUPER_MAJORITY_THRESHOLD,
|
|
@@ -272,6 +272,20 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
272
272
|
return { nodes, failures, keyNetwork };
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
/**
|
|
276
|
+
* The nodes the key network keeps OUT of `blockId`'s cohort — peers that receive none of the
|
|
277
|
+
* block's cluster traffic, and so hold none of its content until something repairs them.
|
|
278
|
+
*
|
|
279
|
+
* Peer ids are generated fresh per mesh, so which node is responsible for a given block is random
|
|
280
|
+
* from run to run: in a 3-node `responsibilityK: 1` mesh, `nodes[1]` is the block's sole responsible
|
|
281
|
+
* peer about a third of the time, and then it receives the writer's commit directly. A test that
|
|
282
|
+
* needs a genuinely non-responsible node has to ask the routing layer rather than assume an index.
|
|
283
|
+
*/
|
|
284
|
+
export async function nonResponsibleNodes(mesh: Mesh, blockId: string): Promise<MeshNode[]> {
|
|
285
|
+
const cohort = await mesh.keyNetwork.findCluster(new TextEncoder().encode(blockId));
|
|
286
|
+
return mesh.nodes.filter(node => !(node.peerId.toString() in cohort));
|
|
287
|
+
}
|
|
288
|
+
|
|
275
289
|
export interface BuildTransactorOptions {
|
|
276
290
|
timeoutMs?: number;
|
|
277
291
|
abortOrCancelTimeoutMs?: number;
|