@optimystic/db-p2p 1.0.0-beta.3 → 1.0.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 +73 -1
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +70 -3
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +16 -1
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +73 -72
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/client.d.ts +4 -6
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +8 -13
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +2 -1
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +126 -6
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +378 -50
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.js +2 -2
- package/dist/src/cluster/race-resolution.d.ts +51 -7
- package/dist/src/cluster/race-resolution.d.ts.map +1 -1
- package/dist/src/cluster/race-resolution.js +71 -7
- package/dist/src/cluster/race-resolution.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +67 -5
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +120 -26
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +34 -47
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -6
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/membership-source.js +1 -1
- package/dist/src/cohort-topic/membership-source.js.map +1 -1
- package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/peer-codec.js +5 -2
- package/dist/src/cohort-topic/peer-codec.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +24 -3
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +36 -3
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/cohort-topic/topic-router.d.ts +9 -0
- package/dist/src/cohort-topic/topic-router.d.ts.map +1 -1
- package/dist/src/cohort-topic/topic-router.js +12 -2
- package/dist/src/cohort-topic/topic-router.js.map +1 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +15 -3
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +78 -17
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +234 -147
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +35 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +151 -26
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/libp2p-node-rn.d.ts +2 -2
- package/dist/src/libp2p-node-rn.d.ts.map +1 -1
- package/dist/src/libp2p-node-rn.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +2 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +6 -1
- package/dist/src/logger.js.map +1 -1
- package/dist/src/matchmaking/query-transport.d.ts +3 -4
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
- package/dist/src/matchmaking/query-transport.js +9 -9
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +10 -5
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +11 -2
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/network/relay-reservation.d.ts +170 -0
- package/dist/src/network/relay-reservation.d.ts.map +1 -0
- package/dist/src/network/relay-reservation.js +527 -0
- package/dist/src/network/relay-reservation.js.map +1 -0
- package/dist/src/pend-validation.d.ts +3 -2
- package/dist/src/pend-validation.d.ts.map +1 -1
- package/dist/src/pend-validation.js +3 -2
- package/dist/src/pend-validation.js.map +1 -1
- package/dist/src/protocol-client.d.ts +8 -1
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +11 -0
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/reactivity/forwarder-host.d.ts +2 -1
- package/dist/src/reactivity/forwarder-host.d.ts.map +1 -1
- package/dist/src/reactivity/forwarder-host.js +2 -1
- package/dist/src/reactivity/forwarder-host.js.map +1 -1
- package/dist/src/reactivity/origination-manager.d.ts +3 -3
- package/dist/src/reactivity/origination-manager.js +3 -3
- package/dist/src/reactivity/recover-transport.d.ts +22 -14
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +32 -19
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/reactivity/subscription-manager.d.ts +2 -2
- package/dist/src/reactivity/topic-bytes.d.ts +6 -7
- package/dist/src/reactivity/topic-bytes.d.ts.map +1 -1
- package/dist/src/reactivity/topic-bytes.js +8 -9
- package/dist/src/reactivity/topic-bytes.js.map +1 -1
- package/dist/src/repo/client.d.ts +5 -2
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +49 -40
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +77 -5
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +131 -20
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +238 -122
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +625 -472
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/i-under-replication-ledger.d.ts +87 -0
- package/dist/src/repo/i-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/i-under-replication-ledger.js +2 -0
- package/dist/src/repo/i-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts +91 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.js +287 -0
- package/dist/src/repo/kv-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/responsibility.d.ts +37 -0
- package/dist/src/repo/responsibility.d.ts.map +1 -0
- package/dist/src/repo/responsibility.js +37 -0
- package/dist/src/repo/responsibility.js.map +1 -0
- package/dist/src/repo/service.d.ts +44 -15
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +64 -27
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/repo/stuck-reservation.d.ts +127 -0
- package/dist/src/repo/stuck-reservation.d.ts.map +1 -0
- package/dist/src/repo/stuck-reservation.js +179 -0
- package/dist/src/repo/stuck-reservation.js.map +1 -0
- package/dist/src/repo/under-replication-drain.d.ts +208 -0
- package/dist/src/repo/under-replication-drain.d.ts.map +1 -0
- package/dist/src/repo/under-replication-drain.js +461 -0
- package/dist/src/repo/under-replication-drain.js.map +1 -0
- package/dist/src/rn.d.ts +5 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +5 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-latch.d.ts +3 -2
- package/dist/src/storage/block-latch.d.ts.map +1 -1
- package/dist/src/storage/block-latch.js +7 -4
- package/dist/src/storage/block-latch.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts +45 -3
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +156 -9
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +57 -5
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/pending-claim.d.ts +167 -0
- package/dist/src/storage/pending-claim.d.ts.map +1 -0
- package/dist/src/storage/pending-claim.js +156 -0
- package/dist/src/storage/pending-claim.js.map +1 -0
- package/dist/src/storage/raw-store-codec.d.ts +13 -0
- package/dist/src/storage/raw-store-codec.d.ts.map +1 -1
- package/dist/src/storage/raw-store-codec.js +24 -3
- package/dist/src/storage/raw-store-codec.js.map +1 -1
- package/dist/src/storage/restoration-coordinator.d.ts.map +1 -1
- package/dist/src/storage/restoration-coordinator.js +2 -1
- package/dist/src/storage/restoration-coordinator.js.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.js +3 -3
- package/dist/src/storage/ring-shift-coordinator.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +122 -5
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +245 -68
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/struct.d.ts +58 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +77 -11
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +226 -100
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +27 -3
- package/src/cluster/block-transfer-service.ts +132 -4
- package/src/cluster/block-transfer.ts +79 -84
- package/src/cluster/client.ts +115 -119
- package/src/cluster/cluster-policy.ts +2 -1
- package/src/cluster/cluster-repo.ts +422 -56
- package/src/cluster/commit-proof.ts +2 -2
- package/src/cluster/race-resolution.ts +76 -7
- package/src/cluster/rebalance-monitor.ts +677 -539
- package/src/cluster/spread-on-churn.ts +34 -54
- package/src/cohort-topic/host.ts +8 -6
- package/src/cohort-topic/membership-source.ts +1 -1
- package/src/cohort-topic/peer-codec.ts +34 -31
- package/src/cohort-topic/stream-util.ts +39 -4
- package/src/cohort-topic/topic-router.ts +12 -2
- package/src/dispute/client.ts +17 -7
- package/src/index.ts +64 -59
- package/src/libp2p-key-network.ts +1377 -1260
- package/src/libp2p-node-base.ts +186 -27
- package/src/libp2p-node-rn.ts +31 -30
- package/src/libp2p-node.ts +37 -36
- package/src/logger.ts +6 -1
- package/src/matchmaking/query-transport.ts +9 -9
- package/src/network/network-manager-service.ts +16 -5
- package/src/network/relay-reservation.ts +636 -0
- package/src/pend-validation.ts +3 -2
- package/src/protocol-client.ts +13 -1
- package/src/reactivity/forwarder-host.ts +2 -1
- package/src/reactivity/origination-manager.ts +3 -3
- package/src/reactivity/recover-transport.ts +39 -22
- package/src/reactivity/subscription-manager.ts +2 -2
- package/src/reactivity/topic-bytes.ts +35 -37
- package/src/repo/client.ts +172 -173
- package/src/repo/cluster-coordinator.ts +145 -19
- package/src/repo/coordinator-repo.ts +652 -505
- package/src/repo/i-under-replication-ledger.ts +89 -0
- package/src/repo/kv-under-replication-ledger.ts +302 -0
- package/src/repo/responsibility.ts +48 -0
- package/src/repo/service.ts +76 -39
- package/src/repo/stuck-reservation.ts +224 -0
- package/src/repo/under-replication-drain.ts +582 -0
- package/src/rn.ts +74 -69
- package/src/storage/block-latch.ts +7 -4
- package/src/storage/block-storage.ts +161 -10
- package/src/storage/i-block-storage.ts +60 -5
- package/src/storage/pending-claim.ts +200 -0
- package/src/storage/raw-store-codec.ts +26 -3
- package/src/storage/restoration-coordinator.ts +212 -211
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/storage-repo.ts +280 -72
- package/src/storage/struct.ts +58 -0
- package/src/testing/mesh-harness.ts +293 -112
package/dist/src/repo/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { blockIdsForTransforms
|
|
1
|
+
import { blockIdsForTransforms } from "@optimystic/db-core";
|
|
2
2
|
import { ProtocolClient } from "../protocol-client.js";
|
|
3
3
|
import { MAX_BLOCK_MESSAGE_BYTES } from "../protocol-limits.js";
|
|
4
4
|
import { peerIdFromString } from "@libp2p/peer-id";
|
|
@@ -59,9 +59,28 @@ export class RepoClient extends ProtocolClient {
|
|
|
59
59
|
// ResponseTimeoutError and mask the caller-facing 'RepoClient timeout' message.
|
|
60
60
|
const deadlineController = new AbortController();
|
|
61
61
|
const timer = setTimeout(() => deadlineController.abort(new Error('RepoClient timeout')), deadlineMs);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Explicit combinator rather than the native `AbortSignal.any`, which Hermes
|
|
63
|
+
// (React Native's JS engine) does not provide. This has to forward whichever
|
|
64
|
+
// source signal's *reason* fired — that's how the caller-facing 'RepoClient
|
|
65
|
+
// timeout' message above (and any reason a caller's own `options.signal` carries)
|
|
66
|
+
// survives the combine. `any-signal` (the package libp2p itself uses for this) was
|
|
67
|
+
// tried and rejected here: it calls the composite `AbortController.abort()` with no
|
|
68
|
+
// argument, which discards the source reason and replaces it with a generic
|
|
69
|
+
// AbortError — confirmed against both Node's native `AbortSignal.any` and
|
|
70
|
+
// `any-signal@4.1.1` directly. Listeners are removed in the `finally` below so a
|
|
71
|
+
// long-lived caller `options.signal` never accumulates one per call.
|
|
72
|
+
const abortController = new AbortController();
|
|
73
|
+
const forwardAbort = (source) => () => abortController.abort(source.reason);
|
|
74
|
+
const onCallerAbort = options?.signal ? forwardAbort(options.signal) : undefined;
|
|
75
|
+
const onDeadlineAbort = forwardAbort(deadlineController.signal);
|
|
76
|
+
deadlineController.signal.addEventListener('abort', onDeadlineAbort, { once: true });
|
|
77
|
+
if (options?.signal) {
|
|
78
|
+
if (options.signal.aborted)
|
|
79
|
+
abortController.abort(options.signal.reason);
|
|
80
|
+
else
|
|
81
|
+
options.signal.addEventListener('abort', onCallerAbort, { once: true });
|
|
82
|
+
}
|
|
83
|
+
const combinedSignal = abortController.signal;
|
|
65
84
|
let response;
|
|
66
85
|
try {
|
|
67
86
|
response = await super.processMessage(message, preferred, {
|
|
@@ -74,6 +93,9 @@ export class RepoClient extends ProtocolClient {
|
|
|
74
93
|
}
|
|
75
94
|
finally {
|
|
76
95
|
clearTimeout(timer);
|
|
96
|
+
deadlineController.signal.removeEventListener('abort', onDeadlineAbort);
|
|
97
|
+
if (onCallerAbort)
|
|
98
|
+
options?.signal?.removeEventListener('abort', onCallerAbort);
|
|
77
99
|
}
|
|
78
100
|
// Type the redirect branch against the payload the service actually produces
|
|
79
101
|
// (`RedirectPayload`) rather than reading it off the `any` response — the untyped read
|
|
@@ -94,50 +116,37 @@ export class RepoClient extends ProtocolClient {
|
|
|
94
116
|
// notice we get that this peer matters, and if it is relay-only we have no address
|
|
95
117
|
// for it at all. Merging after the dial would help only some later hop.
|
|
96
118
|
this.peerNetwork.recordPeerAddresses?.(nextId, next.addrs ?? []);
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
// Cache hint: a follow-up op on this block can dial the target directly.
|
|
120
|
+
const coordinated = this.coordinatedBlockId(operations);
|
|
121
|
+
if (coordinated)
|
|
122
|
+
this.recordCoordinatorHint(coordinated, nextId);
|
|
101
123
|
// single-hop retry against target peer using repo protocol
|
|
102
124
|
const nextClient = RepoClient.create(nextId, this.peerNetwork, this.protocolPrefix);
|
|
103
125
|
return await nextClient.processRepoMessage(operations, options, hop + 1);
|
|
104
126
|
}
|
|
105
127
|
return response;
|
|
106
128
|
}
|
|
107
|
-
|
|
129
|
+
/**
|
|
130
|
+
* The block a redirected op is coordinated on — the block `RepoService.deriveBlockKey` redirected
|
|
131
|
+
* it by, and so the block its coordinator hint belongs to.
|
|
132
|
+
*/
|
|
133
|
+
coordinatedBlockId(ops) {
|
|
108
134
|
const op = ops[0];
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if ('commit' in op) {
|
|
124
|
-
// Anchor on blockIds[0] (where CoordinatorRepo.commit runs consensus +
|
|
125
|
-
// verifyResponsibility), NOT tailId — they differ for a non-tail batch.
|
|
126
|
-
const id = op.commit.blockIds[0];
|
|
127
|
-
return id ? await blockIdToBytes(id) : undefined;
|
|
128
|
-
}
|
|
129
|
-
if ('cancel' in op) {
|
|
130
|
-
const id = op.cancel.actionRef.blockIds[0];
|
|
131
|
-
return id ? await blockIdToBytes(id) : undefined;
|
|
132
|
-
}
|
|
135
|
+
if (!op)
|
|
136
|
+
return undefined;
|
|
137
|
+
if ('get' in op)
|
|
138
|
+
return op.get.blockIds[0];
|
|
139
|
+
// Key on a real block id the pend touches, NOT a structural transforms field
|
|
140
|
+
// name ('inserts'/'updates'/'deletes'); see RepoService.deriveBlockKey.
|
|
141
|
+
if ('pend' in op)
|
|
142
|
+
return blockIdsForTransforms(op.pend.transforms)[0];
|
|
143
|
+
// Anchor on blockIds[0] (where CoordinatorRepo.commit runs consensus +
|
|
144
|
+
// verifyResponsibility), NOT tailId — they differ for a non-tail batch.
|
|
145
|
+
if ('commit' in op)
|
|
146
|
+
return op.commit.blockIds[0];
|
|
147
|
+
if ('cancel' in op)
|
|
148
|
+
return op.cancel.actionRef.blockIds[0];
|
|
133
149
|
return undefined;
|
|
134
150
|
}
|
|
135
|
-
async recordCoordinatorForOpsIfSupported(ops, peerId) {
|
|
136
|
-
const keyBytes = await this.extractKeyFromOperations(ops);
|
|
137
|
-
const pn = this.peerNetwork;
|
|
138
|
-
if (keyBytes != null && typeof pn?.recordCoordinator === 'function') {
|
|
139
|
-
pn.recordCoordinator(keyBytes, peerId);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
151
|
}
|
|
143
152
|
//# sourceMappingURL=client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/repo/client.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,qBAAqB,EAAE,
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/repo/client.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,OAAO,UAAW,SAAQ,cAAc;IAC2B;IAAxE,YAAoB,MAAc,EAAE,WAAyB,EAAW,cAAuB;QAC9F,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAD4C,mBAAc,GAAd,cAAc,CAAS;IAE/F,CAAC;IAED,mCAAmC;IAC5B,MAAM,CAAC,MAAM,CAAC,MAAc,EAAE,WAAyB,EAAE,cAAuB;QACtF,OAAO,IAAI,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,SAAoB,EAAE,OAAuB;QACtD,OAAO,IAAI,CAAC,kBAAkB,CAC7B,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,EACpB,OAAO,CACP,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAoB,EAAE,OAAuB;QACvD,OAAO,IAAI,CAAC,kBAAkB,CAC7B,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EACnB,OAAO,CACP,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,SAAuB,EAAE,OAAuB;QAC5D,OAAO,IAAI,CAAC,kBAAkB,CAC7B,CAAC,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,EAAE,CAAC,EAC3B,OAAO,CACP,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,OAAsB,EAAE,OAAuB;QAC3D,OAAO,IAAI,CAAC,kBAAkB,CAC7B,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EACrB,OAAO,CACP,CAAC;IACH,CAAC;IAEO,oBAAoB,CAAC,UAAqC;QACjE,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,CAAC,EAAE;YAAE,OAAO,SAAS,CAAC;QAC1B,IAAI,MAAM,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;QAC1C,IAAI,QAAQ,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC;QAC9C,IAAI,QAAQ,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC;QACxD,OAAO,SAAS,CAAC;IAClB,CAAC;IAEO,KAAK,CAAC,kBAAkB,CAC/B,UAAqC,EACrC,OAAuB,EACvB,MAAc,CAAC;QAEf,MAAM,OAAO,GAAgB;YAC5B,UAAU;YACV,UAAU,EAAE,OAAO,CAAC,UAAU;SAC9B,CAAC;QACF,MAAM,aAAa,GAAG,IAAI,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC;QAC5D,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,CAAA;QAC5D,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;QACrD,MAAM,SAAS,GAAG,CAAC,IAAI,CAAC,cAAc,IAAI,SAAS,CAAC,GAAG,aAAa,CAAA;QAEpE,2EAA2E;QAC3E,6EAA6E;QAC7E,4EAA4E;QAC5E,+EAA+E;QAC/E,yEAAyE;QACzE,gFAAgF;QAChF,4EAA4E;QAC5E,qEAAqE;QACrE,EAAE;QACF,+EAA+E;QAC/E,+DAA+D;QAC/D,gFAAgF;QAChF,MAAM,kBAAkB,GAAG,IAAI,eAAe,EAAE,CAAA;QAChD,MAAM,KAAK,GAAG,UAAU,CACvB,GAAG,EAAE,CAAC,kBAAkB,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC,EAC/D,UAAU,CACV,CAAA;QACD,6EAA6E;QAC7E,6EAA6E;QAC7E,4EAA4E;QAC5E,kFAAkF;QAClF,mFAAmF;QACnF,oFAAoF;QACpF,4EAA4E;QAC5E,0EAA0E;QAC1E,iFAAiF;QACjF,qEAAqE;QACrE,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAA;QAC7C,MAAM,YAAY,GAAG,CAAC,MAAmB,EAAE,EAAE,CAAC,GAAS,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAC9F,MAAM,aAAa,GAAG,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAChF,MAAM,eAAe,GAAG,YAAY,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAA;QAC/D,kBAAkB,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QACpF,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;YACrB,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO;gBAAE,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;;gBACnE,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,aAAc,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QAC9E,CAAC;QACD,MAAM,cAAc,GAAG,eAAe,CAAC,MAAM,CAAA;QAC7C,IAAI,QAAa,CAAA;QACjB,IAAI,CAAC;YACJ,QAAQ,GAAG,MAAM,KAAK,CAAC,cAAc,CAAM,OAAO,EAAE,SAAS,EAAE;gBAC9D,MAAM,EAAE,cAAc;gBACtB,aAAa;gBACb,aAAa,EAAE,OAAO,EAAE,aAAa;gBACrC,+DAA+D;gBAC/D,aAAa,EAAE,uBAAuB;aACtC,CAAC,CAAA;QACH,CAAC;gBAAS,CAAC;YACV,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,kBAAkB,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,eAAe,CAAC,CAAA;YACvE,IAAI,aAAa;gBAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,aAAa,CAAC,CAAA;QAChF,CAAC;QAED,6EAA6E;QAC7E,uFAAuF;QACvF,iEAAiE;QACjE,MAAM,gBAAgB,GAAG,QAAoC,CAAA;QAC7D,IAAI,gBAAgB,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;YAC/C,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;gBACd,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAA;YAC3E,CAAC;YACD,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAA;YAC3C,MAAM,KAAK,GAAG,gBAAgB,CAAC,QAAQ,CAAC,KAAK,CAAA;YAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,YAAY,CAAC,IAAI,KAAK,CAAC,CAAC,CAAE,CAAA;YAClE,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;YACxC,IAAI,IAAI,CAAC,EAAE,KAAK,YAAY,EAAE,CAAC;gBAC9B,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;YACpE,CAAC;YACD,oFAAoF;YACpF,mFAAmF;YACnF,wEAAwE;YACxE,IAAI,CAAC,WAAW,CAAC,mBAAmB,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAA;YAChE,yEAAyE;YACzE,MAAM,WAAW,GAAG,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAA;YACvD,IAAI,WAAW;gBAAE,IAAI,CAAC,qBAAqB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;YAChE,2DAA2D;YAC3D,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC,CAAA;YACnF,OAAO,MAAM,UAAU,CAAC,kBAAkB,CAAI,UAAU,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,CAAC,CAAA;QAC5E,CAAC;QACD,OAAO,QAAa,CAAC;IACtB,CAAC;IAED;;;OAGG;IACK,kBAAkB,CAAC,GAA8B;QACxD,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QAClB,IAAI,CAAC,EAAE;YAAE,OAAO,SAAS,CAAC;QAC1B,IAAI,KAAK,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC3C,6EAA6E;QAC7E,wEAAwE;QACxE,IAAI,MAAM,IAAI,EAAE;YAAE,OAAO,qBAAqB,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACtE,uEAAuE;QACvE,wEAAwE;QACxE,IAAI,QAAQ,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACjD,IAAI,QAAQ,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC3D,OAAO,SAAS,CAAC;IAClB,CAAC;CAED"}
|
|
@@ -43,6 +43,29 @@ export declare class ConflictRaceLostError extends Error {
|
|
|
43
43
|
/** peerId → messageHash of the rival transaction that member holds as the race winner. */
|
|
44
44
|
conflicts: Record<string, string>);
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The transaction's pend could not proceed because one or more members answered with a signed `held`
|
|
48
|
+
* vote: the requested blocks are reserved by a different unresolved action in that member's durable
|
|
49
|
+
* storage. Sibling of {@link ConflictRaceLostError} and retryable for the same reason — nobody judged
|
|
50
|
+
* this write invalid; it queued behind a reservation that disappears when the holder commits or
|
|
51
|
+
* cancels.
|
|
52
|
+
*
|
|
53
|
+
* The two are separate because they name different things. A conflict vote names the winning rival's
|
|
54
|
+
* `messageHash`, which the member holds whole; a held vote can only name the rival's **action id**,
|
|
55
|
+
* because it fires in the window where the rival has left the member's in-memory table but not yet its
|
|
56
|
+
* storage. `CoordinatorRepo.pend` converts this into a `StaleFailure` with `conflict: true` so the
|
|
57
|
+
* normal retry machinery (`isConflictFailure`) absorbs it, exactly as it does a lost race.
|
|
58
|
+
*
|
|
59
|
+
* Only a PEND record can produce it: `held` votes come from `ClusterMember.validatePendOperations`,
|
|
60
|
+
* which inspects pend operations only, so `CoordinatorRepo.commit` never meets one.
|
|
61
|
+
*/
|
|
62
|
+
export declare class BlocksHeldError extends Error {
|
|
63
|
+
/** peerId → actionId of the unresolved action that member's storage says holds the blocks. */
|
|
64
|
+
readonly heldBy: Record<string, string>;
|
|
65
|
+
constructor(message: string,
|
|
66
|
+
/** peerId → actionId of the unresolved action that member's storage says holds the blocks. */
|
|
67
|
+
heldBy: Record<string, string>);
|
|
68
|
+
}
|
|
46
69
|
/** Cancel handle for an injected timer; cancels a not-yet-fired timer (safe no-op after fire/cancel). */
|
|
47
70
|
export type TimerCancel = () => void;
|
|
48
71
|
/**
|
|
@@ -57,6 +80,19 @@ export interface ClusterCoordinatorClock {
|
|
|
57
80
|
setTimer?: (fn: () => void, delayMs: number) => TimerCancel;
|
|
58
81
|
}
|
|
59
82
|
/** Manages distributed transactions across clusters */
|
|
83
|
+
/**
|
|
84
|
+
* What a cohort lookup established about a block's cohort. `resolved: false` covers BOTH a lookup
|
|
85
|
+
* that threw and one that answered with nobody: neither names a destination for a write, and the
|
|
86
|
+
* durability class both produce is the same (`unrouted`). `reason` is for logs only — never branch
|
|
87
|
+
* on it.
|
|
88
|
+
*/
|
|
89
|
+
export type CohortResolution = {
|
|
90
|
+
readonly resolved: true;
|
|
91
|
+
readonly peerIds: readonly string[];
|
|
92
|
+
} | {
|
|
93
|
+
readonly resolved: false;
|
|
94
|
+
readonly reason: string;
|
|
95
|
+
};
|
|
60
96
|
export declare class ClusterCoordinator {
|
|
61
97
|
private readonly keyNetwork;
|
|
62
98
|
/** Factory for a per-peer cluster RPC handle; only `update` is ever called, hence `ICluster`. */
|
|
@@ -116,9 +152,45 @@ export declare class ClusterCoordinator {
|
|
|
116
152
|
*/
|
|
117
153
|
private createMessageHash;
|
|
118
154
|
/**
|
|
119
|
-
*
|
|
155
|
+
* The ONE cohort lookup every accessor on this class derives from: the raw peer map when the key
|
|
156
|
+
* network answered, otherwise the reason it did not. A thrown `findCluster` is logged here and
|
|
157
|
+
* nowhere else. Callers that need the map (`executeClusterTransaction`, which builds the record's
|
|
158
|
+
* `peers`) go through {@link getClusterForBlock}; callers that need to know whether the cohort
|
|
159
|
+
* RESOLVED go through {@link resolveCohort}.
|
|
160
|
+
*/
|
|
161
|
+
private lookupCluster;
|
|
162
|
+
/**
|
|
163
|
+
* Gets all peers in the cluster for a specific block ID. Empty when the lookup failed — the
|
|
164
|
+
* consensus path treats "no cohort" and "lookup failed" alike (there is nobody to run consensus
|
|
165
|
+
* with either way); a caller that must tell them apart uses {@link resolveCohort}.
|
|
120
166
|
*/
|
|
121
167
|
private getClusterForBlock;
|
|
168
|
+
/**
|
|
169
|
+
* Whether the block's cohort could be established, and who it is. The primitive behind
|
|
170
|
+
* {@link getClusterPeerIds} and {@link getClusterSize}: a lookup that threw and a lookup that named
|
|
171
|
+
* nobody used to reach every caller as the same empty list, and `CoordinatorRepo`'s solo
|
|
172
|
+
* short-circuit then acknowledged a write it had no idea where to send exactly as it acknowledged a
|
|
173
|
+
* write to a genuine cohort of one (GitHub #19). Both shapes are still `resolved: false` here —
|
|
174
|
+
* neither names a destination — but they are distinguishable from a resolved cohort, which is what
|
|
175
|
+
* the write's durability class needs (`unrouted` vs `local`).
|
|
176
|
+
*/
|
|
177
|
+
resolveCohort(blockId: BlockId): Promise<CohortResolution>;
|
|
178
|
+
/**
|
|
179
|
+
* A node never runs a cluster transaction for a cohort it is not in. Behind members reconcile from the
|
|
180
|
+
* coordinator's own proof-carrying copy (its member applies before the merged record fans out), and a
|
|
181
|
+
* coordinator outside `record.peers` is not a reconcile target — so a cohort with no holder would stay
|
|
182
|
+
* behind and the commit durability gate would refuse, having first put this node's vote and storage
|
|
183
|
+
* where the cohort does not look. The invariant is held here, at the one place a record's `peers` is
|
|
184
|
+
* chosen, rather than left to the routing convention.
|
|
185
|
+
*
|
|
186
|
+
* Fires only on a RESOLVED cohort (at least one peer) that excludes the wired local member. An empty
|
|
187
|
+
* cohort is a failed lookup, not a cohort this node is outside of, so it is left to `executeTransaction`'s
|
|
188
|
+
* size checks; `CoordinatorRepo`'s solo short-circuit keeps unresolved and single-peer cohorts away from
|
|
189
|
+
* this method altogether in any case. After its responsibility check, what remains is a multi-member
|
|
190
|
+
* cohort that changed inside the responsibility cache's staleness window. With no local member wired the guard does not apply: that
|
|
191
|
+
* bypass exists for wiring without an identity (direct constructors, some tests), never for production.
|
|
192
|
+
*/
|
|
193
|
+
private assertLocalMemberInCohort;
|
|
122
194
|
private makeRecord;
|
|
123
195
|
/**
|
|
124
196
|
* Initiates a 2-phase transaction for a specific block ID.
|
|
@@ -181,10 +253,10 @@ export declare class ClusterCoordinator {
|
|
|
181
253
|
*/
|
|
182
254
|
private executeTransaction;
|
|
183
255
|
/**
|
|
184
|
-
* The block's cohort peer ids as currently derivable. Empty when
|
|
185
|
-
* (
|
|
186
|
-
* the degraded-routing branch
|
|
187
|
-
*
|
|
256
|
+
* The block's cohort peer ids as currently derivable. Empty when the cohort did not resolve
|
|
257
|
+
* ({@link resolveCohort}: `findCluster` threw, or named nobody), so a caller branching on
|
|
258
|
+
* `length <= 1` is also taking the degraded-routing branch. Derived from `resolveCohort` rather
|
|
259
|
+
* than re-deriving the cohort, so there is exactly one lookup rule.
|
|
188
260
|
*/
|
|
189
261
|
getClusterPeerIds(blockId: BlockId): Promise<string[]>;
|
|
190
262
|
/** {@link getClusterPeerIds}, counted. Derived from it rather than re-deriving the cohort, so the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cluster-coordinator.d.ts","sourceRoot":"","sources":["../../../src/repo/cluster-coordinator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,EAAgB,cAAc,EAAE,sBAAsB,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGpM,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"cluster-coordinator.d.ts","sourceRoot":"","sources":["../../../src/repo/cluster-coordinator.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { peerIdFromString } from "@libp2p/peer-id";
|
|
2
|
-
import { CURRENT_MEMBERSHIP_VERSION, computeClusterMessageHash, isConflictFailure, membershipDigest } from "@optimystic/db-core";
|
|
2
|
+
import { CURRENT_MEMBERSHIP_VERSION, computeClusterMessageHash, isConflictFailure, membershipDigest, routingKeyForBlock } from "@optimystic/db-core";
|
|
3
3
|
import { Pending } from "@optimystic/db-core";
|
|
4
4
|
import { createLogger, verbose } from '../logger.js';
|
|
5
5
|
import { PenaltyReason } from "../reputation/types.js";
|
|
6
|
+
import { ResponsibilityRefusalError } from "./responsibility.js";
|
|
6
7
|
const log = createLogger('cluster');
|
|
7
8
|
/**
|
|
8
9
|
* Pick each peer's OWN {@link ClusterRecord.applyOutcomes} entry out of the record that peer answered
|
|
@@ -79,6 +80,32 @@ export class ConflictRaceLostError extends Error {
|
|
|
79
80
|
this.name = 'ConflictRaceLostError';
|
|
80
81
|
}
|
|
81
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The transaction's pend could not proceed because one or more members answered with a signed `held`
|
|
85
|
+
* vote: the requested blocks are reserved by a different unresolved action in that member's durable
|
|
86
|
+
* storage. Sibling of {@link ConflictRaceLostError} and retryable for the same reason — nobody judged
|
|
87
|
+
* this write invalid; it queued behind a reservation that disappears when the holder commits or
|
|
88
|
+
* cancels.
|
|
89
|
+
*
|
|
90
|
+
* The two are separate because they name different things. A conflict vote names the winning rival's
|
|
91
|
+
* `messageHash`, which the member holds whole; a held vote can only name the rival's **action id**,
|
|
92
|
+
* because it fires in the window where the rival has left the member's in-memory table but not yet its
|
|
93
|
+
* storage. `CoordinatorRepo.pend` converts this into a `StaleFailure` with `conflict: true` so the
|
|
94
|
+
* normal retry machinery (`isConflictFailure`) absorbs it, exactly as it does a lost race.
|
|
95
|
+
*
|
|
96
|
+
* Only a PEND record can produce it: `held` votes come from `ClusterMember.validatePendOperations`,
|
|
97
|
+
* which inspects pend operations only, so `CoordinatorRepo.commit` never meets one.
|
|
98
|
+
*/
|
|
99
|
+
export class BlocksHeldError extends Error {
|
|
100
|
+
heldBy;
|
|
101
|
+
constructor(message,
|
|
102
|
+
/** peerId → actionId of the unresolved action that member's storage says holds the blocks. */
|
|
103
|
+
heldBy) {
|
|
104
|
+
super(message);
|
|
105
|
+
this.heldBy = heldBy;
|
|
106
|
+
this.name = 'BlocksHeldError';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
82
109
|
/**
|
|
83
110
|
* Production timer binding: a one-shot `setTimeout` whose handle is **unref'd** so a pending
|
|
84
111
|
* commit-retry (or the deferred transaction cleanup) never keeps an otherwise-idle process alive.
|
|
@@ -91,7 +118,6 @@ function defaultSetTimer(fn, delayMs) {
|
|
|
91
118
|
handle.unref?.();
|
|
92
119
|
return () => clearTimeout(handle);
|
|
93
120
|
}
|
|
94
|
-
/** Manages distributed transactions across clusters */
|
|
95
121
|
export class ClusterCoordinator {
|
|
96
122
|
keyNetwork;
|
|
97
123
|
createClusterClient;
|
|
@@ -183,21 +209,76 @@ export class ClusterCoordinator {
|
|
|
183
209
|
return computeClusterMessageHash(message, membershipDigestValue);
|
|
184
210
|
}
|
|
185
211
|
/**
|
|
186
|
-
*
|
|
212
|
+
* The ONE cohort lookup every accessor on this class derives from: the raw peer map when the key
|
|
213
|
+
* network answered, otherwise the reason it did not. A thrown `findCluster` is logged here and
|
|
214
|
+
* nowhere else. Callers that need the map (`executeClusterTransaction`, which builds the record's
|
|
215
|
+
* `peers`) go through {@link getClusterForBlock}; callers that need to know whether the cohort
|
|
216
|
+
* RESOLVED go through {@link resolveCohort}.
|
|
187
217
|
*/
|
|
188
|
-
async
|
|
189
|
-
const blockIdBytes = new TextEncoder().encode(blockId);
|
|
218
|
+
async lookupCluster(blockId) {
|
|
190
219
|
try {
|
|
191
|
-
const peers = await this.keyNetwork.findCluster(
|
|
220
|
+
const peers = await this.keyNetwork.findCluster(routingKeyForBlock(blockId));
|
|
192
221
|
const peerIds = Object.keys(peers ?? {});
|
|
193
222
|
log('cluster-tx:cluster-members', { blockId, peerIds });
|
|
194
|
-
return peers;
|
|
223
|
+
return { peers: peers ?? {} };
|
|
195
224
|
}
|
|
196
225
|
catch (e) {
|
|
197
226
|
log('WARN findCluster failed for %s: %o', blockId, e);
|
|
198
|
-
return {};
|
|
227
|
+
return { reason: `findCluster threw: ${e?.message ?? String(e)}` };
|
|
199
228
|
}
|
|
200
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Gets all peers in the cluster for a specific block ID. Empty when the lookup failed — the
|
|
232
|
+
* consensus path treats "no cohort" and "lookup failed" alike (there is nobody to run consensus
|
|
233
|
+
* with either way); a caller that must tell them apart uses {@link resolveCohort}.
|
|
234
|
+
*/
|
|
235
|
+
async getClusterForBlock(blockId) {
|
|
236
|
+
const outcome = await this.lookupCluster(blockId);
|
|
237
|
+
return 'peers' in outcome ? outcome.peers : {};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Whether the block's cohort could be established, and who it is. The primitive behind
|
|
241
|
+
* {@link getClusterPeerIds} and {@link getClusterSize}: a lookup that threw and a lookup that named
|
|
242
|
+
* nobody used to reach every caller as the same empty list, and `CoordinatorRepo`'s solo
|
|
243
|
+
* short-circuit then acknowledged a write it had no idea where to send exactly as it acknowledged a
|
|
244
|
+
* write to a genuine cohort of one (GitHub #19). Both shapes are still `resolved: false` here —
|
|
245
|
+
* neither names a destination — but they are distinguishable from a resolved cohort, which is what
|
|
246
|
+
* the write's durability class needs (`unrouted` vs `local`).
|
|
247
|
+
*/
|
|
248
|
+
async resolveCohort(blockId) {
|
|
249
|
+
const outcome = await this.lookupCluster(blockId);
|
|
250
|
+
if ('reason' in outcome)
|
|
251
|
+
return { resolved: false, reason: outcome.reason };
|
|
252
|
+
const peerIds = Object.keys(outcome.peers);
|
|
253
|
+
if (peerIds.length === 0)
|
|
254
|
+
return { resolved: false, reason: 'findCluster named nobody' };
|
|
255
|
+
return { resolved: true, peerIds };
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* A node never runs a cluster transaction for a cohort it is not in. Behind members reconcile from the
|
|
259
|
+
* coordinator's own proof-carrying copy (its member applies before the merged record fans out), and a
|
|
260
|
+
* coordinator outside `record.peers` is not a reconcile target — so a cohort with no holder would stay
|
|
261
|
+
* behind and the commit durability gate would refuse, having first put this node's vote and storage
|
|
262
|
+
* where the cohort does not look. The invariant is held here, at the one place a record's `peers` is
|
|
263
|
+
* chosen, rather than left to the routing convention.
|
|
264
|
+
*
|
|
265
|
+
* Fires only on a RESOLVED cohort (at least one peer) that excludes the wired local member. An empty
|
|
266
|
+
* cohort is a failed lookup, not a cohort this node is outside of, so it is left to `executeTransaction`'s
|
|
267
|
+
* size checks; `CoordinatorRepo`'s solo short-circuit keeps unresolved and single-peer cohorts away from
|
|
268
|
+
* this method altogether in any case. After its responsibility check, what remains is a multi-member
|
|
269
|
+
* cohort that changed inside the responsibility cache's staleness window. With no local member wired the guard does not apply: that
|
|
270
|
+
* bypass exists for wiring without an identity (direct constructors, some tests), never for production.
|
|
271
|
+
*/
|
|
272
|
+
assertLocalMemberInCohort(blockId, peers) {
|
|
273
|
+
if (!this.localCluster)
|
|
274
|
+
return;
|
|
275
|
+
const peerIds = Object.keys(peers);
|
|
276
|
+
const selfId = this.localCluster.peerId.toString();
|
|
277
|
+
if (peerIds.length === 0 || peerIds.includes(selfId))
|
|
278
|
+
return;
|
|
279
|
+
log('cluster-tx:not-in-cohort', { blockId, selfId, peerIds });
|
|
280
|
+
throw new ResponsibilityRefusalError('not-responsible', [blockId], `refusing to coordinate a cluster transaction for a cohort this node is not in: ${peerIds.join(', ')}`);
|
|
281
|
+
}
|
|
201
282
|
makeRecord(peers, messageHash, message, membershipDigestValue) {
|
|
202
283
|
const peerCount = Object.keys(peers ?? {}).length;
|
|
203
284
|
const record = {
|
|
@@ -253,6 +334,7 @@ export class ClusterCoordinator {
|
|
|
253
334
|
: { ...message, coordinatingBlockIds: [blockId] };
|
|
254
335
|
// Get the cluster peers for this block
|
|
255
336
|
const peers = await this.getClusterForBlock(blockId);
|
|
337
|
+
this.assertLocalMemberInCohort(blockId, peers);
|
|
256
338
|
// Bind the responsible membership into the transaction identity (v2): the digest is folded into
|
|
257
339
|
// the messageHash below, so two different peer sets produce two different messageHashes rather
|
|
258
340
|
// than one hash with a silent internal disagreement about who is responsible.
|
|
@@ -382,17 +464,23 @@ export class ClusterCoordinator {
|
|
|
382
464
|
// Collect promises with super-majority requirement
|
|
383
465
|
const promised = await this.collectPromises(peers, record);
|
|
384
466
|
const superMajority = Math.ceil(peerCount * this.cfg.superMajorityThreshold);
|
|
385
|
-
// Count approvals, rejections and
|
|
386
|
-
// saying "not now — I hold the race winner"
|
|
387
|
-
//
|
|
388
|
-
//
|
|
467
|
+
// Count approvals, rejections and the two RETRYABLE refusals separately. A `conflict` vote is a
|
|
468
|
+
// member saying "not now — I hold the race winner"; a `held` vote is a member saying "not now —
|
|
469
|
+
// a different unresolved action holds these blocks in my storage". Neither may count toward
|
|
470
|
+
// approvals OR rejections, or a transient refusal would masquerade as a validator rejection
|
|
471
|
+
// (permanent) or as silence (indistinguishable from an unreachable cohort) — both wrong.
|
|
389
472
|
const promises = promised.record.promises;
|
|
390
473
|
const approvalCount = Object.values(promises).filter(sig => sig.type === 'approve').length;
|
|
391
474
|
const rejectionCount = Object.values(promises).filter(sig => sig.type === 'reject').length;
|
|
392
475
|
const conflictCount = Object.values(promises).filter(sig => sig.type === 'conflict').length;
|
|
476
|
+
const heldCount = Object.values(promises).filter(sig => sig.type === 'held').length;
|
|
393
477
|
// Check if rejections make super-majority impossible
|
|
394
478
|
// If more than (peerCount - superMajority) nodes reject, we can never reach super-majority
|
|
395
479
|
const maxAllowedRejections = peerCount - superMajority;
|
|
480
|
+
// Whether the merged record itself PROVES super-majority unreachable — the same sum a member
|
|
481
|
+
// re-derives as `ConflictSuperseded`/`Rejected` from the signed votes, which is what makes an
|
|
482
|
+
// abandonment broadcast proof-carrying rather than an unauthenticated "forget this".
|
|
483
|
+
const refusalsProveUnreachable = rejectionCount + conflictCount + heldCount > maxAllowedRejections;
|
|
396
484
|
if (rejectionCount > maxAllowedRejections) {
|
|
397
485
|
const rejectReasonsByPeer = Object.fromEntries(Object.entries(promises)
|
|
398
486
|
.flatMap(([peerId, sig]) => sig.type === 'reject' ? [[peerId, sig.rejectReason ?? 'unknown']] : []));
|
|
@@ -436,11 +524,33 @@ export class ClusterCoordinator {
|
|
|
436
524
|
// super-majority (members re-derive ConflictSuperseded/Rejected from the signed votes and
|
|
437
525
|
// clear their reservations immediately). Below that bar the record proves nothing and a
|
|
438
526
|
// broadcast would be the unauthenticated "forget this" the shortfall NOTE below refuses.
|
|
439
|
-
if (
|
|
527
|
+
if (refusalsProveUnreachable) {
|
|
440
528
|
this.broadcastAbandonment(promised.record, 'conflict-race-lost');
|
|
441
529
|
}
|
|
442
530
|
throw new ConflictRaceLostError(`Conflict race lost: ${conflictCount}/${peerCount} member(s) hold a conflicting winner (${approvalCount}/${superMajority} approvals)`, conflicts);
|
|
443
531
|
}
|
|
532
|
+
// A `held`-answered shortfall is the OTHER retryable refusal: the pend queued behind a rival's
|
|
533
|
+
// unresolved reservation. Checked after the conflict branch so a lost race still wins when both
|
|
534
|
+
// answer — a conflict vote names the winning transaction's messageHash, which is strictly more
|
|
535
|
+
// actionable than an action id — and, like it, before the generic shortfall, which must stay
|
|
536
|
+
// reserved for the genuinely-silent cohort.
|
|
537
|
+
if (heldCount > 0 && approvalCount < superMajority) {
|
|
538
|
+
const heldBy = Object.fromEntries(Object.entries(promises)
|
|
539
|
+
.flatMap(([peerId, sig]) => sig.type === 'held' ? [[peerId, sig.heldBy]] : []));
|
|
540
|
+
log('cluster-tx:pend-blocks-held', {
|
|
541
|
+
messageHash: record.messageHash,
|
|
542
|
+
peerCount,
|
|
543
|
+
approvals: approvalCount,
|
|
544
|
+
rejections: rejectionCount,
|
|
545
|
+
heldBy,
|
|
546
|
+
superMajority
|
|
547
|
+
});
|
|
548
|
+
this.updateTransactionRecord(promised.record, 'pend-blocks-held');
|
|
549
|
+
if (refusalsProveUnreachable) {
|
|
550
|
+
this.broadcastAbandonment(promised.record, 'pend-blocks-held');
|
|
551
|
+
}
|
|
552
|
+
throw new BlocksHeldError(`Pend blocks held: ${heldCount}/${peerCount} member(s) hold an unresolved rival action (${approvalCount}/${superMajority} approvals)`, heldBy);
|
|
553
|
+
}
|
|
444
554
|
if (peerCount > 1 && approvalCount < superMajority) {
|
|
445
555
|
log('cluster-tx:supermajority-failed', {
|
|
446
556
|
messageHash: record.messageHash,
|
|
@@ -459,7 +569,8 @@ export class ClusterCoordinator {
|
|
|
459
569
|
// their own staleness sweep instead.
|
|
460
570
|
// NOTE: the message below is load-bearing wire text — the consuming repo
|
|
461
571
|
// (sereus cadre-core control-write-retry) matches it verbatim to retry a genuinely-silent
|
|
462
|
-
// cohort. Keep it byte-identical, and never fold conflict votes into its
|
|
572
|
+
// cohort. Keep it byte-identical, and never fold `conflict` or `held` votes into its
|
|
573
|
+
// rejection count.
|
|
463
574
|
throw new Error(`Failed to get super-majority: ${approvalCount}/${peerCount} approvals (needed ${superMajority}, ${rejectionCount} rejections)`);
|
|
464
575
|
}
|
|
465
576
|
// Mark as disputed when minority rejections exist but super-majority approves
|
|
@@ -492,14 +603,14 @@ export class ClusterCoordinator {
|
|
|
492
603
|
return await this.commitTransaction(promised.record);
|
|
493
604
|
}
|
|
494
605
|
/**
|
|
495
|
-
* The block's cohort peer ids as currently derivable. Empty when
|
|
496
|
-
* (
|
|
497
|
-
* the degraded-routing branch
|
|
498
|
-
*
|
|
606
|
+
* The block's cohort peer ids as currently derivable. Empty when the cohort did not resolve
|
|
607
|
+
* ({@link resolveCohort}: `findCluster` threw, or named nobody), so a caller branching on
|
|
608
|
+
* `length <= 1` is also taking the degraded-routing branch. Derived from `resolveCohort` rather
|
|
609
|
+
* than re-deriving the cohort, so there is exactly one lookup rule.
|
|
499
610
|
*/
|
|
500
611
|
async getClusterPeerIds(blockId) {
|
|
501
|
-
const
|
|
502
|
-
return
|
|
612
|
+
const cohort = await this.resolveCohort(blockId);
|
|
613
|
+
return cohort.resolved ? [...cohort.peerIds] : [];
|
|
503
614
|
}
|
|
504
615
|
/** {@link getClusterPeerIds}, counted. Derived from it rather than re-deriving the cohort, so the
|
|
505
616
|
* size a caller branches on and the ids it logs can never come from two different rules. */
|