@optimystic/db-p2p 1.0.0-beta.3 → 1.1.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 +155 -9
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +446 -58
- 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 +96 -11
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +164 -25
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +240 -122
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +627 -473
- 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 +493 -65
- 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 +183 -25
- package/src/repo/coordinator-repo.ts +656 -506
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { peerIdFromString } from "@libp2p/peer-id";
|
|
2
2
|
import type { ClusterRecord, IKeyNetwork, RepoMessage, BlockId, ClusterPeers, MessageOptions, ClusterConsensusConfig, ICluster, PendResult, CommitResult, StaleFailure } from "@optimystic/db-core";
|
|
3
|
-
import { CURRENT_MEMBERSHIP_VERSION, computeClusterMessageHash, isConflictFailure, membershipDigest } from "@optimystic/db-core";
|
|
3
|
+
import { CURRENT_MEMBERSHIP_VERSION, computeClusterMessageHash, isConflictFailure, membershipDigest, routingKeyForBlock } from "@optimystic/db-core";
|
|
4
4
|
import { Pending } from "@optimystic/db-core";
|
|
5
5
|
import type { PeerId } from "@libp2p/interface";
|
|
6
6
|
import { createLogger, verbose } from '../logger.js'
|
|
@@ -9,6 +9,7 @@ import type { FretService } from "p2p-fret";
|
|
|
9
9
|
import type { IPeerReputation } from "../reputation/types.js";
|
|
10
10
|
import { PenaltyReason } from "../reputation/types.js";
|
|
11
11
|
import type { ITransactionStateStore } from "../cluster/i-transaction-state-store.js";
|
|
12
|
+
import { ResponsibilityRefusalError } from "./responsibility.js";
|
|
12
13
|
|
|
13
14
|
const log = createLogger('cluster')
|
|
14
15
|
|
|
@@ -91,6 +92,33 @@ export class ConflictRaceLostError extends Error {
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The transaction's pend could not proceed because one or more members answered with a signed `held`
|
|
97
|
+
* vote: the requested blocks are reserved by a different unresolved action in that member's durable
|
|
98
|
+
* storage. Sibling of {@link ConflictRaceLostError} and retryable for the same reason — nobody judged
|
|
99
|
+
* this write invalid; it queued behind a reservation that disappears when the holder commits or
|
|
100
|
+
* cancels.
|
|
101
|
+
*
|
|
102
|
+
* The two are separate because they name different things. A conflict vote names the winning rival's
|
|
103
|
+
* `messageHash`, which the member holds whole; a held vote can only name the rival's **action id**,
|
|
104
|
+
* because it fires in the window where the rival has left the member's in-memory table but not yet its
|
|
105
|
+
* storage. `CoordinatorRepo.pend` converts this into a `StaleFailure` with `conflict: true` so the
|
|
106
|
+
* normal retry machinery (`isConflictFailure`) absorbs it, exactly as it does a lost race.
|
|
107
|
+
*
|
|
108
|
+
* Only a PEND record can produce it: `held` votes come from `ClusterMember.validatePendOperations`,
|
|
109
|
+
* which inspects pend operations only, so `CoordinatorRepo.commit` never meets one.
|
|
110
|
+
*/
|
|
111
|
+
export class BlocksHeldError extends Error {
|
|
112
|
+
constructor(
|
|
113
|
+
message: string,
|
|
114
|
+
/** peerId → actionId of the unresolved action that member's storage says holds the blocks. */
|
|
115
|
+
readonly heldBy: Record<string, string>
|
|
116
|
+
) {
|
|
117
|
+
super(message);
|
|
118
|
+
this.name = 'BlocksHeldError';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
94
122
|
/** Cancel handle for an injected timer; cancels a not-yet-fired timer (safe no-op after fire/cancel). */
|
|
95
123
|
export type TimerCancel = () => void;
|
|
96
124
|
|
|
@@ -140,6 +168,16 @@ interface ClusterTransactionState {
|
|
|
140
168
|
}
|
|
141
169
|
|
|
142
170
|
/** Manages distributed transactions across clusters */
|
|
171
|
+
/**
|
|
172
|
+
* What a cohort lookup established about a block's cohort. `resolved: false` covers BOTH a lookup
|
|
173
|
+
* that threw and one that answered with nobody: neither names a destination for a write, and the
|
|
174
|
+
* durability class both produce is the same (`unrouted`). `reason` is for logs only — never branch
|
|
175
|
+
* on it.
|
|
176
|
+
*/
|
|
177
|
+
export type CohortResolution =
|
|
178
|
+
| { readonly resolved: true; readonly peerIds: readonly string[] }
|
|
179
|
+
| { readonly resolved: false; readonly reason: string };
|
|
180
|
+
|
|
143
181
|
export class ClusterCoordinator {
|
|
144
182
|
private transactions: Map<string, ClusterTransactionState> = new Map();
|
|
145
183
|
private readonly retryInitialIntervalMs: number;
|
|
@@ -165,6 +203,8 @@ export class ClusterCoordinator {
|
|
|
165
203
|
getExecutedPendResult?: (messageHash: string) => PendResult | undefined;
|
|
166
204
|
/** Local storage's verdict for a commit applied during consensus; see ClusterMember.getExecutedCommitResult. */
|
|
167
205
|
getExecutedCommitResult?: (messageHash: string) => CommitResult | undefined;
|
|
206
|
+
/** One more reconcile for a behind-refused commit, once remote members hold it; see ClusterMember.reconcileRefusedCommit. */
|
|
207
|
+
reconcileRefusedCommit?: (record: ClusterRecord) => Promise<void>;
|
|
168
208
|
},
|
|
169
209
|
private readonly fretService?: FretService,
|
|
170
210
|
private readonly reputation?: IPeerReputation,
|
|
@@ -236,21 +276,76 @@ export class ClusterCoordinator {
|
|
|
236
276
|
}
|
|
237
277
|
|
|
238
278
|
/**
|
|
239
|
-
*
|
|
279
|
+
* The ONE cohort lookup every accessor on this class derives from: the raw peer map when the key
|
|
280
|
+
* network answered, otherwise the reason it did not. A thrown `findCluster` is logged here and
|
|
281
|
+
* nowhere else. Callers that need the map (`executeClusterTransaction`, which builds the record's
|
|
282
|
+
* `peers`) go through {@link getClusterForBlock}; callers that need to know whether the cohort
|
|
283
|
+
* RESOLVED go through {@link resolveCohort}.
|
|
240
284
|
*/
|
|
241
|
-
private async
|
|
242
|
-
const blockIdBytes = new TextEncoder().encode(blockId);
|
|
285
|
+
private async lookupCluster(blockId: BlockId): Promise<{ peers: ClusterPeers } | { reason: string }> {
|
|
243
286
|
try {
|
|
244
|
-
const peers = await this.keyNetwork.findCluster(
|
|
287
|
+
const peers = await this.keyNetwork.findCluster(routingKeyForBlock(blockId));
|
|
245
288
|
const peerIds = Object.keys(peers ?? {});
|
|
246
289
|
log('cluster-tx:cluster-members', { blockId, peerIds });
|
|
247
|
-
return peers;
|
|
290
|
+
return { peers: peers ?? {} };
|
|
248
291
|
} catch (e) {
|
|
249
292
|
log('WARN findCluster failed for %s: %o', blockId, e)
|
|
250
|
-
return {
|
|
293
|
+
return { reason: `findCluster threw: ${(e as Error)?.message ?? String(e)}` };
|
|
251
294
|
}
|
|
252
295
|
}
|
|
253
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Gets all peers in the cluster for a specific block ID. Empty when the lookup failed — the
|
|
299
|
+
* consensus path treats "no cohort" and "lookup failed" alike (there is nobody to run consensus
|
|
300
|
+
* with either way); a caller that must tell them apart uses {@link resolveCohort}.
|
|
301
|
+
*/
|
|
302
|
+
private async getClusterForBlock(blockId: BlockId): Promise<ClusterPeers> {
|
|
303
|
+
const outcome = await this.lookupCluster(blockId);
|
|
304
|
+
return 'peers' in outcome ? outcome.peers : {};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Whether the block's cohort could be established, and who it is. The primitive behind
|
|
309
|
+
* {@link getClusterPeerIds} and {@link getClusterSize}: a lookup that threw and a lookup that named
|
|
310
|
+
* nobody used to reach every caller as the same empty list, and `CoordinatorRepo`'s solo
|
|
311
|
+
* short-circuit then acknowledged a write it had no idea where to send exactly as it acknowledged a
|
|
312
|
+
* write to a genuine cohort of one (GitHub #19). Both shapes are still `resolved: false` here —
|
|
313
|
+
* neither names a destination — but they are distinguishable from a resolved cohort, which is what
|
|
314
|
+
* the write's durability class needs (`unrouted` vs `local`).
|
|
315
|
+
*/
|
|
316
|
+
async resolveCohort(blockId: BlockId): Promise<CohortResolution> {
|
|
317
|
+
const outcome = await this.lookupCluster(blockId);
|
|
318
|
+
if ('reason' in outcome) return { resolved: false, reason: outcome.reason };
|
|
319
|
+
const peerIds = Object.keys(outcome.peers);
|
|
320
|
+
if (peerIds.length === 0) return { resolved: false, reason: 'findCluster named nobody' };
|
|
321
|
+
return { resolved: true, peerIds };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* A node never runs a cluster transaction for a cohort it is not in. Behind members reconcile from the
|
|
326
|
+
* coordinator's own proof-carrying copy (its member applies before the merged record fans out), and a
|
|
327
|
+
* coordinator outside `record.peers` is not a reconcile target — so a cohort with no holder would stay
|
|
328
|
+
* behind and the commit durability gate would refuse, having first put this node's vote and storage
|
|
329
|
+
* where the cohort does not look. The invariant is held here, at the one place a record's `peers` is
|
|
330
|
+
* chosen, rather than left to the routing convention.
|
|
331
|
+
*
|
|
332
|
+
* Fires only on a RESOLVED cohort (at least one peer) that excludes the wired local member. An empty
|
|
333
|
+
* cohort is a failed lookup, not a cohort this node is outside of, so it is left to `executeTransaction`'s
|
|
334
|
+
* size checks; `CoordinatorRepo`'s solo short-circuit keeps unresolved and single-peer cohorts away from
|
|
335
|
+
* this method altogether in any case. After its responsibility check, what remains is a multi-member
|
|
336
|
+
* cohort that changed inside the responsibility cache's staleness window. With no local member wired the guard does not apply: that
|
|
337
|
+
* bypass exists for wiring without an identity (direct constructors, some tests), never for production.
|
|
338
|
+
*/
|
|
339
|
+
private assertLocalMemberInCohort(blockId: BlockId, peers: ClusterPeers): void {
|
|
340
|
+
if (!this.localCluster) return;
|
|
341
|
+
const peerIds = Object.keys(peers);
|
|
342
|
+
const selfId = this.localCluster.peerId.toString();
|
|
343
|
+
if (peerIds.length === 0 || peerIds.includes(selfId)) return;
|
|
344
|
+
log('cluster-tx:not-in-cohort', { blockId, selfId, peerIds });
|
|
345
|
+
throw new ResponsibilityRefusalError('not-responsible', [blockId],
|
|
346
|
+
`refusing to coordinate a cluster transaction for a cohort this node is not in: ${peerIds.join(', ')}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
254
349
|
private makeRecord(peers: ClusterPeers, messageHash: string, message: RepoMessage, membershipDigestValue: string): ClusterRecord {
|
|
255
350
|
const peerCount = Object.keys(peers ?? {}).length;
|
|
256
351
|
const record: ClusterRecord = {
|
|
@@ -302,7 +397,9 @@ export class ClusterCoordinator {
|
|
|
302
397
|
* during consensus, when the member retained one. Same availability contract as
|
|
303
398
|
* `localPendResult`. `CoordinatorRepo.commit` uses a retained refusal to detect a rival's
|
|
304
399
|
* win swallowed by the member-side ahead-divergence tolerance, instead of fabricating a
|
|
305
|
-
* success no member durably stored.
|
|
400
|
+
* success no member durably stored. Read after the commit broadcast, so a behind member's
|
|
401
|
+
* verdict already reflects the second reconcile `broadcastMergedRecord` gives it once a
|
|
402
|
+
* remote member holds the revision.
|
|
306
403
|
*/
|
|
307
404
|
localCommitResult?: CommitResult;
|
|
308
405
|
/**
|
|
@@ -356,6 +453,7 @@ export class ClusterCoordinator {
|
|
|
356
453
|
|
|
357
454
|
// Get the cluster peers for this block
|
|
358
455
|
const peers = await this.getClusterForBlock(blockId);
|
|
456
|
+
this.assertLocalMemberInCohort(blockId, peers);
|
|
359
457
|
|
|
360
458
|
// Bind the responsible membership into the transaction identity (v2): the digest is folded into
|
|
361
459
|
// the messageHash below, so two different peer sets produce two different messageHashes rather
|
|
@@ -494,18 +592,24 @@ export class ClusterCoordinator {
|
|
|
494
592
|
const promised = await this.collectPromises(peers, record);
|
|
495
593
|
const superMajority = Math.ceil(peerCount * this.cfg.superMajorityThreshold);
|
|
496
594
|
|
|
497
|
-
// Count approvals, rejections and
|
|
498
|
-
// saying "not now — I hold the race winner"
|
|
499
|
-
//
|
|
500
|
-
//
|
|
595
|
+
// Count approvals, rejections and the two RETRYABLE refusals separately. A `conflict` vote is a
|
|
596
|
+
// member saying "not now — I hold the race winner"; a `held` vote is a member saying "not now —
|
|
597
|
+
// a different unresolved action holds these blocks in my storage". Neither may count toward
|
|
598
|
+
// approvals OR rejections, or a transient refusal would masquerade as a validator rejection
|
|
599
|
+
// (permanent) or as silence (indistinguishable from an unreachable cohort) — both wrong.
|
|
501
600
|
const promises = promised.record.promises;
|
|
502
601
|
const approvalCount = Object.values(promises).filter(sig => sig.type === 'approve').length;
|
|
503
602
|
const rejectionCount = Object.values(promises).filter(sig => sig.type === 'reject').length;
|
|
504
603
|
const conflictCount = Object.values(promises).filter(sig => sig.type === 'conflict').length;
|
|
604
|
+
const heldCount = Object.values(promises).filter(sig => sig.type === 'held').length;
|
|
505
605
|
|
|
506
606
|
// Check if rejections make super-majority impossible
|
|
507
607
|
// If more than (peerCount - superMajority) nodes reject, we can never reach super-majority
|
|
508
608
|
const maxAllowedRejections = peerCount - superMajority;
|
|
609
|
+
// Whether the merged record itself PROVES super-majority unreachable — the same sum a member
|
|
610
|
+
// re-derives as `ConflictSuperseded`/`Rejected` from the signed votes, which is what makes an
|
|
611
|
+
// abandonment broadcast proof-carrying rather than an unauthenticated "forget this".
|
|
612
|
+
const refusalsProveUnreachable = rejectionCount + conflictCount + heldCount > maxAllowedRejections;
|
|
509
613
|
if (rejectionCount > maxAllowedRejections) {
|
|
510
614
|
const rejectReasonsByPeer = Object.fromEntries(Object.entries(promises)
|
|
511
615
|
.flatMap(([peerId, sig]) => sig.type === 'reject' ? [[peerId, sig.rejectReason ?? 'unknown'] as const] : []));
|
|
@@ -552,7 +656,7 @@ export class ClusterCoordinator {
|
|
|
552
656
|
// super-majority (members re-derive ConflictSuperseded/Rejected from the signed votes and
|
|
553
657
|
// clear their reservations immediately). Below that bar the record proves nothing and a
|
|
554
658
|
// broadcast would be the unauthenticated "forget this" the shortfall NOTE below refuses.
|
|
555
|
-
if (
|
|
659
|
+
if (refusalsProveUnreachable) {
|
|
556
660
|
this.broadcastAbandonment(promised.record, 'conflict-race-lost');
|
|
557
661
|
}
|
|
558
662
|
throw new ConflictRaceLostError(
|
|
@@ -560,6 +664,31 @@ export class ClusterCoordinator {
|
|
|
560
664
|
conflicts);
|
|
561
665
|
}
|
|
562
666
|
|
|
667
|
+
// A `held`-answered shortfall is the OTHER retryable refusal: the pend queued behind a rival's
|
|
668
|
+
// unresolved reservation. Checked after the conflict branch so a lost race still wins when both
|
|
669
|
+
// answer — a conflict vote names the winning transaction's messageHash, which is strictly more
|
|
670
|
+
// actionable than an action id — and, like it, before the generic shortfall, which must stay
|
|
671
|
+
// reserved for the genuinely-silent cohort.
|
|
672
|
+
if (heldCount > 0 && approvalCount < superMajority) {
|
|
673
|
+
const heldBy = Object.fromEntries(Object.entries(promises)
|
|
674
|
+
.flatMap(([peerId, sig]) => sig.type === 'held' ? [[peerId, sig.heldBy] as const] : []));
|
|
675
|
+
log('cluster-tx:pend-blocks-held', {
|
|
676
|
+
messageHash: record.messageHash,
|
|
677
|
+
peerCount,
|
|
678
|
+
approvals: approvalCount,
|
|
679
|
+
rejections: rejectionCount,
|
|
680
|
+
heldBy,
|
|
681
|
+
superMajority
|
|
682
|
+
});
|
|
683
|
+
this.updateTransactionRecord(promised.record, 'pend-blocks-held');
|
|
684
|
+
if (refusalsProveUnreachable) {
|
|
685
|
+
this.broadcastAbandonment(promised.record, 'pend-blocks-held');
|
|
686
|
+
}
|
|
687
|
+
throw new BlocksHeldError(
|
|
688
|
+
`Pend blocks held: ${heldCount}/${peerCount} member(s) hold an unresolved rival action (${approvalCount}/${superMajority} approvals)`,
|
|
689
|
+
heldBy);
|
|
690
|
+
}
|
|
691
|
+
|
|
563
692
|
if (peerCount > 1 && approvalCount < superMajority) {
|
|
564
693
|
log('cluster-tx:supermajority-failed', {
|
|
565
694
|
messageHash: record.messageHash,
|
|
@@ -578,7 +707,8 @@ export class ClusterCoordinator {
|
|
|
578
707
|
// their own staleness sweep instead.
|
|
579
708
|
// NOTE: the message below is load-bearing wire text — the consuming repo
|
|
580
709
|
// (sereus cadre-core control-write-retry) matches it verbatim to retry a genuinely-silent
|
|
581
|
-
// cohort. Keep it byte-identical, and never fold conflict votes into its
|
|
710
|
+
// cohort. Keep it byte-identical, and never fold `conflict` or `held` votes into its
|
|
711
|
+
// rejection count.
|
|
582
712
|
throw new Error(`Failed to get super-majority: ${approvalCount}/${peerCount} approvals (needed ${superMajority}, ${rejectionCount} rejections)`);
|
|
583
713
|
}
|
|
584
714
|
|
|
@@ -614,14 +744,14 @@ export class ClusterCoordinator {
|
|
|
614
744
|
}
|
|
615
745
|
|
|
616
746
|
/**
|
|
617
|
-
* The block's cohort peer ids as currently derivable. Empty when
|
|
618
|
-
* (
|
|
619
|
-
* the degraded-routing branch
|
|
620
|
-
*
|
|
747
|
+
* The block's cohort peer ids as currently derivable. Empty when the cohort did not resolve
|
|
748
|
+
* ({@link resolveCohort}: `findCluster` threw, or named nobody), so a caller branching on
|
|
749
|
+
* `length <= 1` is also taking the degraded-routing branch. Derived from `resolveCohort` rather
|
|
750
|
+
* than re-deriving the cohort, so there is exactly one lookup rule.
|
|
621
751
|
*/
|
|
622
752
|
async getClusterPeerIds(blockId: BlockId): Promise<string[]> {
|
|
623
|
-
const
|
|
624
|
-
return
|
|
753
|
+
const cohort = await this.resolveCohort(blockId);
|
|
754
|
+
return cohort.resolved ? [...cohort.peerIds] : [];
|
|
625
755
|
}
|
|
626
756
|
|
|
627
757
|
/** {@link getClusterPeerIds}, counted. Derived from it rather than re-deriving the cohort, so the
|
|
@@ -922,11 +1052,14 @@ export class ClusterCoordinator {
|
|
|
922
1052
|
* reconcile target and gains nothing from this ordering; the durability gate in
|
|
923
1053
|
* `CoordinatorRepo.commit` is what makes that shape refuse rather than acknowledge.
|
|
924
1054
|
*
|
|
925
|
-
*
|
|
926
|
-
* runs
|
|
927
|
-
*
|
|
928
|
-
*
|
|
929
|
-
*
|
|
1055
|
+
* The mirror case — the coordinating member is ITSELF behind (no pend, or no base for the block)
|
|
1056
|
+
* — is the price of that order: its reconcile runs before any remote member has applied, finds
|
|
1057
|
+
* no holder, and retains a refusal. So once the remote members have answered, and at least one
|
|
1058
|
+
* reported holding the revision, this node's own member gets one more reconcile
|
|
1059
|
+
* (`reconcileRefusedCommit`). The member skips it unless its retained refusal has the behind
|
|
1060
|
+
* shape, so only a behind coordinator pays the extra fetch. It finishes before this method
|
|
1061
|
+
* returns, so `executeClusterTransaction` reads the refreshed verdict, and a two-member cohort
|
|
1062
|
+
* whose members both end up holding the commit is no longer refused as not durable.
|
|
930
1063
|
*/
|
|
931
1064
|
private async broadcastMergedRecord(record: ClusterRecord, peerIds: string[]): Promise<{ failures: string[]; applyOutcomes?: ClusterRecord['applyOutcomes'] }> {
|
|
932
1065
|
const deliver = async (peerIdStr: string) => {
|
|
@@ -958,9 +1091,34 @@ export class ClusterCoordinator {
|
|
|
958
1091
|
// asked, so a member cannot report an outcome on another member's behalf by echoing a record
|
|
959
1092
|
// full of entries. Unsigned and advisory either way — see ClusterRecord.applyOutcomes.
|
|
960
1093
|
const applyOutcomes = collectApplyOutcomes(results);
|
|
1094
|
+
// NOTE: after a healing second reconcile, `applyOutcomes[selfId].commit` still carries the
|
|
1095
|
+
// pre-reconcile refusal. Nothing reads the self entry today (the gate reads
|
|
1096
|
+
// `localCommitResult`); if anything starts to, re-stamp it from `getExecutedCommitResult` here.
|
|
1097
|
+
// NOTE: in a 3+ cohort this also runs when the remote holders already form a majority without
|
|
1098
|
+
// this member — one extra fetch that heals its copy; gate on the remote count if it ever shows up.
|
|
1099
|
+
const remoteHolds = remote.some(id => applyOutcomes?.[id]?.commit?.success === true);
|
|
1100
|
+
if (remoteHolds && localResults.some(r => r.success)) {
|
|
1101
|
+
await this.reconcileLocalMemberAgain(record);
|
|
1102
|
+
}
|
|
961
1103
|
return { failures, ...(applyOutcomes === undefined ? {} : { applyOutcomes }) };
|
|
962
1104
|
}
|
|
963
1105
|
|
|
1106
|
+
/**
|
|
1107
|
+
* Give this node's own member its second reconcile (see {@link broadcastMergedRecord}). The
|
|
1108
|
+
* member contract is never to throw; the catch keeps a broken seam from failing a transaction
|
|
1109
|
+
* the remote members already applied.
|
|
1110
|
+
*/
|
|
1111
|
+
private async reconcileLocalMemberAgain(record: ClusterRecord): Promise<void> {
|
|
1112
|
+
try {
|
|
1113
|
+
await this.localCluster?.reconcileRefusedCommit?.(record);
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
log('cluster-tx:local-reconcile-again-error', {
|
|
1116
|
+
messageHash: record.messageHash,
|
|
1117
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
964
1122
|
/**
|
|
965
1123
|
* Fire-and-forget replay of an abandoned transaction's record to every peer in its cohort.
|
|
966
1124
|
*
|