@optimystic/db-core 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/structs.d.ts +30 -6
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +5 -3
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts +2 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js +10 -3
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js.map +1 -1
- package/dist/src/cohort-topic/wire/codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/codec.js +8 -2
- package/dist/src/cohort-topic/wire/codec.js.map +1 -1
- package/dist/src/collection/collection.d.ts +439 -34
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +843 -148
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collection/struct.d.ts +109 -4
- package/dist/src/collection/struct.d.ts.map +1 -1
- package/dist/src/collection/struct.js +88 -1
- package/dist/src/collection/struct.js.map +1 -1
- package/dist/src/collections/diary/diary.d.ts +6 -2
- package/dist/src/collections/diary/diary.d.ts.map +1 -1
- package/dist/src/collections/diary/diary.js +5 -2
- package/dist/src/collections/diary/diary.js.map +1 -1
- package/dist/src/collections/tree/struct.d.ts +124 -16
- package/dist/src/collections/tree/struct.d.ts.map +1 -1
- package/dist/src/collections/tree/struct.js +86 -11
- package/dist/src/collections/tree/struct.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +13 -3
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +106 -40
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/matchmaking/wire.d.ts.map +1 -1
- package/dist/src/matchmaking/wire.js +8 -2
- package/dist/src/matchmaking/wire.js.map +1 -1
- package/dist/src/network/durability.d.ts +45 -0
- package/dist/src/network/durability.d.ts.map +1 -0
- package/dist/src/network/durability.js +114 -0
- package/dist/src/network/durability.js.map +1 -0
- package/dist/src/network/i-key-network.d.ts +14 -6
- package/dist/src/network/i-key-network.d.ts.map +1 -1
- package/dist/src/network/index.d.ts +3 -0
- package/dist/src/network/index.d.ts.map +1 -1
- package/dist/src/network/index.js +3 -0
- package/dist/src/network/index.js.map +1 -1
- package/dist/src/network/lineage.d.ts +39 -0
- package/dist/src/network/lineage.d.ts.map +1 -0
- package/dist/src/network/lineage.js +61 -0
- package/dist/src/network/lineage.js.map +1 -0
- package/dist/src/network/routing-key.d.ts +24 -0
- package/dist/src/network/routing-key.d.ts.map +1 -0
- package/dist/src/network/routing-key.js +9 -0
- package/dist/src/network/routing-key.js.map +1 -0
- package/dist/src/network/stale-failure.d.ts +5 -1
- package/dist/src/network/stale-failure.d.ts.map +1 -1
- package/dist/src/network/stale-failure.js +5 -1
- package/dist/src/network/stale-failure.js.map +1 -1
- package/dist/src/network/struct.d.ts +151 -8
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/reactivity/wire.d.ts.map +1 -1
- package/dist/src/reactivity/wire.js +8 -2
- package/dist/src/reactivity/wire.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +64 -7
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +150 -23
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts +141 -21
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +408 -122
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/errors.d.ts +39 -10
- package/dist/src/transaction/errors.d.ts.map +1 -1
- package/dist/src/transaction/errors.js +34 -9
- package/dist/src/transaction/errors.js.map +1 -1
- package/dist/src/transaction/index.d.ts +1 -0
- package/dist/src/transaction/index.d.ts.map +1 -1
- package/dist/src/transaction/index.js.map +1 -1
- package/dist/src/transactor/block-floors.d.ts +78 -0
- package/dist/src/transactor/block-floors.d.ts.map +1 -0
- package/dist/src/transactor/block-floors.js +62 -0
- package/dist/src/transactor/block-floors.js.map +1 -0
- package/dist/src/transactor/change-notifier.d.ts +43 -0
- package/dist/src/transactor/change-notifier.d.ts.map +1 -1
- package/dist/src/transactor/change-notifier.js +3 -0
- package/dist/src/transactor/change-notifier.js.map +1 -1
- package/dist/src/transactor/index.d.ts +1 -0
- package/dist/src/transactor/index.d.ts.map +1 -1
- package/dist/src/transactor/index.js +1 -0
- package/dist/src/transactor/index.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts +43 -6
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +279 -86
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +94 -4
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +120 -42
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transactor/transactor.d.ts +14 -1
- package/dist/src/transactor/transactor.d.ts.map +1 -1
- package/dist/src/transform/atomic.d.ts.map +1 -1
- package/dist/src/transform/atomic.js +12 -9
- package/dist/src/transform/atomic.js.map +1 -1
- package/dist/src/transform/base-pins.d.ts +50 -17
- package/dist/src/transform/base-pins.d.ts.map +1 -1
- package/dist/src/transform/base-pins.js +58 -11
- package/dist/src/transform/base-pins.js.map +1 -1
- package/dist/src/transform/cache-source.d.ts +75 -11
- package/dist/src/transform/cache-source.d.ts.map +1 -1
- package/dist/src/transform/cache-source.js +162 -36
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/digest.d.ts +8 -1
- package/dist/src/transform/digest.d.ts.map +1 -1
- package/dist/src/transform/digest.js +19 -9
- package/dist/src/transform/digest.js.map +1 -1
- package/dist/src/transform/helpers.d.ts +6 -2
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +8 -3
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/tracker.d.ts +94 -14
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +222 -68
- package/dist/src/transform/tracker.js.map +1 -1
- package/dist/src/utility/structural-equals.d.ts +38 -0
- package/dist/src/utility/structural-equals.d.ts.map +1 -0
- package/dist/src/utility/structural-equals.js +77 -0
- package/dist/src/utility/structural-equals.js.map +1 -0
- package/package.json +2 -2
- package/src/cluster/structs.ts +28 -7
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +260 -253
- package/src/cohort-topic/wire/codec.ts +222 -216
- package/src/collection/collection.ts +960 -157
- package/src/collection/struct.ts +99 -4
- package/src/collections/diary/diary.ts +71 -68
- package/src/collections/tree/struct.ts +155 -23
- package/src/collections/tree/tree.ts +127 -40
- package/src/index.ts +1 -1
- package/src/matchmaking/wire.ts +8 -2
- package/src/network/durability.ts +123 -0
- package/src/network/i-key-network.ts +60 -52
- package/src/network/index.ts +10 -7
- package/src/network/lineage.ts +80 -0
- package/src/network/routing-key.ts +27 -0
- package/src/network/stale-failure.ts +5 -1
- package/src/network/struct.ts +156 -8
- package/src/reactivity/wire.ts +230 -224
- package/src/testing/test-transactor.ts +152 -24
- package/src/transaction/coordinator.ts +470 -131
- package/src/transaction/errors.ts +34 -9
- package/src/transaction/index.ts +1 -0
- package/src/transactor/block-floors.ts +105 -0
- package/src/transactor/change-notifier.ts +129 -80
- package/src/transactor/index.ts +1 -0
- package/src/transactor/network-transactor.ts +297 -89
- package/src/transactor/transactor-source.ts +131 -42
- package/src/transactor/transactor.ts +13 -1
- package/src/transform/atomic.ts +25 -22
- package/src/transform/base-pins.ts +76 -18
- package/src/transform/cache-source.ts +327 -194
- package/src/transform/digest.ts +21 -10
- package/src/transform/helpers.ts +8 -3
- package/src/transform/tracker.ts +423 -270
- package/src/utility/structural-equals.ts +72 -0
- package/dist/src/utility/block-id-to-bytes.d.ts +0 -3
- package/dist/src/utility/block-id-to-bytes.d.ts.map +0 -1
- package/dist/src/utility/block-id-to-bytes.js +0 -7
- package/dist/src/utility/block-id-to-bytes.js.map +0 -1
- package/src/utility/block-id-to-bytes.ts +0 -8
|
@@ -2,16 +2,18 @@ import { peerIdFromString } from "../network/types.js";
|
|
|
2
2
|
import type { PeerId } from "../network/types.js";
|
|
3
3
|
import { highestStaleAt, isConflictFailure } from "../network/stale-failure.js";
|
|
4
4
|
import { BlockUnavailableError, BlockPossiblyStaleError } from "../network/struct.js";
|
|
5
|
-
import
|
|
5
|
+
import { mergeDurability, withTornBlocks } from "../network/durability.js";
|
|
6
|
+
import { judgeCohortLineage, type CohortLineage, type MemberLineage } from "../network/lineage.js";
|
|
7
|
+
import type { ActionTransforms, ActionBlocks, ActionLineage, BlockActionStatus, ITransactor, PendSuccess, CommitSuccess, StaleFailure, IKeyNetwork, BlockId, GetBlockResults, PendResult, CommitResult, PendRequest, IRepo, BlockGets, Transforms, CommitRequest, ActionId, RepoCommitRequest, ClusterNomineesResult, CollectionId, IBlock, GetBlockResult, CoordinatorIntent, BlockUnavailableReason, BlockContentDigests, BlockBaseRevs, WriteDurability } from "../index.js";
|
|
6
8
|
import type { IBlockChangeNotifier, CollectionChangeListener } from "./change-notifier.js";
|
|
7
9
|
import { transformForBlockId, concatTransforms, concatTransform, transformsFromTransform, blockIdsForTransforms } from "../transform/helpers.js";
|
|
8
10
|
import { Tracker } from "../transform/tracker.js";
|
|
9
|
-
import { blockDigestsField } from "../transform/digest.js";
|
|
11
|
+
import { blockDigestsField, baseRevsField } from "../transform/digest.js";
|
|
10
12
|
import { CacheSource } from "../transform/cache-source.js";
|
|
11
|
-
import { TransactorSource } from "./transactor-source.js";
|
|
13
|
+
import { TransactorSource, servedRevision } from "./transactor-source.js";
|
|
12
14
|
import { Log } from "../log/log.js";
|
|
13
15
|
import { groupBy } from "../utility/groupby.js";
|
|
14
|
-
import {
|
|
16
|
+
import { routingKeyForBlock } from "../network/routing-key.js";
|
|
15
17
|
import { isRecordEmpty } from "../utility/is-record-empty.js";
|
|
16
18
|
import { type CoordinatorBatch, makeBatchesByPeer, incompleteBatches, everyBatch, allBatches, mergeBlocks, processBatches, createBatchesForPayload } from "../utility/batch-coordinator.js";
|
|
17
19
|
import { abortableDelay, jitteredBackoffMs } from "../utility/backoff.js";
|
|
@@ -41,6 +43,17 @@ type NetworkTransactorInit = {
|
|
|
41
43
|
* absent, `onCollectionChange` is a logged no-op.
|
|
42
44
|
*/
|
|
43
45
|
localChangeNotifier?: IBlockChangeNotifier;
|
|
46
|
+
/**
|
|
47
|
+
* The peer id of the node this transactor runs on, when `getRepo` hands that id the node's own
|
|
48
|
+
* co-located repo rather than a network client — the same "co-located node" idea as
|
|
49
|
+
* `localChangeNotifier`. It only breaks ties when a pend's blocks are grouped onto coordinators:
|
|
50
|
+
* among cohort members covering equally many blocks, this node wins, so a write it is responsible
|
|
51
|
+
* for is coordinated without a network hop. It never wins over a member covering more blocks
|
|
52
|
+
* (fewer batches come first), and it never makes this node a coordinator for a block whose cohort
|
|
53
|
+
* does not include it. Omit when nothing is co-located; ties then go to the member seen first,
|
|
54
|
+
* which is the nearest.
|
|
55
|
+
*/
|
|
56
|
+
localPeerId?: PeerId;
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
/**
|
|
@@ -59,6 +72,8 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
59
72
|
private readonly dialTimeoutMs: number | undefined;
|
|
60
73
|
private readonly getRepo: (peerId: PeerId) => IRepo;
|
|
61
74
|
private readonly localChangeNotifier: IBlockChangeNotifier | undefined;
|
|
75
|
+
/** `NetworkTransactorInit.localPeerId` as a string, the form cohort peer ids arrive in. */
|
|
76
|
+
private readonly localPeerId: string | undefined;
|
|
62
77
|
|
|
63
78
|
/**
|
|
64
79
|
* Per-transaction coordinator cache: `actionId → (blockId → resolved coordinator)`.
|
|
@@ -93,6 +108,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
93
108
|
: (init.dialTimeoutMs > 0 ? init.dialTimeoutMs : undefined);
|
|
94
109
|
this.getRepo = init.getRepo;
|
|
95
110
|
this.localChangeNotifier = init.localChangeNotifier;
|
|
111
|
+
this.localPeerId = init.localPeerId?.toString();
|
|
96
112
|
}
|
|
97
113
|
|
|
98
114
|
/**
|
|
@@ -137,7 +153,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
137
153
|
batch => batch.payload,
|
|
138
154
|
(gets, blockId, mergeWithGets) => [...(mergeWithGets ?? []), ...gets.filter(bid => bid === blockId)],
|
|
139
155
|
expiration,
|
|
140
|
-
|
|
156
|
+
(blockId, options) => this.keyNetwork.findCoordinator(routingKeyForBlock(blockId), { ...options, intent: 'read' })
|
|
141
157
|
);
|
|
142
158
|
} catch (e) {
|
|
143
159
|
error = e as Error;
|
|
@@ -152,35 +168,53 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
152
168
|
// present and unflagged) — retrying it doubles the round-trips on the common
|
|
153
169
|
// createOrOpen "does this block exist?" probe. Cross-member reconciliation for a
|
|
154
170
|
// missing block has already happened one layer down: CoordinatorRepo.get detects
|
|
155
|
-
// `isMissing` and consults cluster peers before it responds —
|
|
156
|
-
// consult FAILS, the entry now says so via `unavailable` instead of
|
|
157
|
-
// authoritative absent.
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// transactor-level retry to discover: another coordinator would find the same
|
|
163
|
-
// cohort's same answer. A flagged entry earns the retry against a different peer
|
|
164
|
-
// that an absent deliberately does not.
|
|
171
|
+
// `isMissing` and consults cluster peers on every such read before it responds —
|
|
172
|
+
// and when that consult FAILS, the entry now says so via `unavailable` instead of
|
|
173
|
+
// posing as an authoritative absent. So by the time an unflagged absent reaches
|
|
174
|
+
// here there is nothing left for a transactor-level retry to discover, while a
|
|
175
|
+
// flagged entry earns the retry against a different peer that an absent
|
|
176
|
+
// deliberately does not. (The coordinator never remembers an absence across reads:
|
|
177
|
+
// one that did served a just-committed block as never created — GitHub issue #20.)
|
|
165
178
|
// See tickets txn-perf-authoritative-notfound and repo-reports-unavailable-vs-absent.
|
|
166
179
|
const hasValidResponse = (b: CoordinatorBatch<BlockId[], GetBlockResults>) => {
|
|
167
180
|
return b.request?.isResponse === true && b.request.response != null;
|
|
168
181
|
};
|
|
169
182
|
|
|
183
|
+
// Content the CALLER can prove is too old, from the floor it carried on the request
|
|
184
|
+
// (see {@link BlockGets.floors}): the caller walked a log entry saying this block changed at
|
|
185
|
+
// that revision, so anything below it is not the view the read asked for, however current
|
|
186
|
+
// the answering repo honestly believes its copy to be. Only a block-carrying entry can be
|
|
187
|
+
// below a floor — an absent answer is judged by its own rules above, and treating one as
|
|
188
|
+
// below-floor would put the one-round `createOrOpen` probe back on the retry path.
|
|
189
|
+
// Reported here rather than left to the reader: `TransactorSource` reports
|
|
190
|
+
// `collection:block-below-floor` from the MERGED answer, so a read the retry below repaired
|
|
191
|
+
// is the one case that leaves no trace anywhere else — the extra round would show up in a
|
|
192
|
+
// field log as an unexplained second consult. Costs nothing on an unfloored read, which
|
|
193
|
+
// leaves on the first clause.
|
|
194
|
+
const belowFloor = (bid: BlockId, entry: GetBlockResult): boolean => {
|
|
195
|
+
const floor = blockGets.floors?.[bid];
|
|
196
|
+
if (floor === undefined || entry.block == null || servedRevision(entry) >= floor) return false;
|
|
197
|
+
log('get:below-floor blockId=%s served=%d floor=%d', bid, servedRevision(entry), floor);
|
|
198
|
+
return true;
|
|
199
|
+
};
|
|
200
|
+
|
|
170
201
|
// A batch is answered when its response carries an entry for EVERY requested
|
|
171
|
-
// block id and none of those entries carries a doubt marker
|
|
172
|
-
// with only `state` (no `block`) is an
|
|
173
|
-
// answered — not a gap. An `unavailable`
|
|
174
|
-
// find out whether the block EXISTS; an
|
|
175
|
-
// saying it could not confirm the content
|
|
176
|
-
// sits ahead of it, unsettled)
|
|
177
|
-
//
|
|
202
|
+
// block id and none of those entries carries a doubt marker or falls below the
|
|
203
|
+
// caller's floor. An entry present with only `state` (no `block`) is an
|
|
204
|
+
// authoritative "absent", which counts as answered — not a gap. An `unavailable`
|
|
205
|
+
// entry is the peer saying it could not find out whether the block EXISTS; an
|
|
206
|
+
// `unconfirmedAheadRev` entry is the peer saying it could not confirm the content
|
|
207
|
+
// it served is CURRENT (a cohort claim sits ahead of it, unsettled); a below-floor
|
|
208
|
+
// entry is the CALLER knowing the content is behind a revision it has already seen
|
|
209
|
+
// the log commit. None counts as answered, so each earns the second-chance retry
|
|
210
|
+
// against a different coordinator.
|
|
178
211
|
const isAuthoritative = (b: CoordinatorBatch<BlockId[], GetBlockResults>) => {
|
|
179
212
|
if (!hasValidResponse(b)) return false;
|
|
180
213
|
const resp = b.request!.response! as GetBlockResults;
|
|
181
214
|
return b.payload.every(bid => resp[bid] !== undefined
|
|
182
215
|
&& resp[bid]!.unavailable === undefined
|
|
183
|
-
&& resp[bid]!.unconfirmedAheadRev === undefined
|
|
216
|
+
&& resp[bid]!.unconfirmedAheadRev === undefined
|
|
217
|
+
&& !belowFloor(bid, resp[bid]!));
|
|
184
218
|
};
|
|
185
219
|
|
|
186
220
|
// Retry only genuine no-response / partial-response batches. An authoritative
|
|
@@ -191,6 +225,13 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
191
225
|
// today — one extra bounded consult on an already failing read. If isolated-node
|
|
192
226
|
// read latency ever matters, skip the retry for that reason rather than widening
|
|
193
227
|
// isAuthoritative.
|
|
228
|
+
// This is ONE extra round, not a loop: a below-floor answer that every reachable
|
|
229
|
+
// coordinator repeats survives the merge below as the highest revision anyone
|
|
230
|
+
// served, UNFLAGGED. Deliberate — a log entry is not proof its blocks landed, so a
|
|
231
|
+
// floor no machine can meet may simply describe an abandoned write, in which case
|
|
232
|
+
// the below-floor content is the correct content and every machine agrees on it
|
|
233
|
+
// (see the accepted-tradeoff NOTE at `TransactorSource.mayRetain`). The reader
|
|
234
|
+
// judges it again and returns it uncached.
|
|
194
235
|
const retryable = Array.from(allBatches(batches)).filter(b =>
|
|
195
236
|
!isAuthoritative(b as any)
|
|
196
237
|
) as CoordinatorBatch<BlockId[], GetBlockResults>[];
|
|
@@ -207,7 +248,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
207
248
|
b.payload,
|
|
208
249
|
(gets, blockId, mergeWithGets) => [...(mergeWithGets ?? []), ...gets.filter(id => id === blockId)],
|
|
209
250
|
Array.from(excluded),
|
|
210
|
-
|
|
251
|
+
(blockId, options) => this.keyNetwork.findCoordinator(routingKeyForBlock(blockId), { ...options, intent: 'read' })
|
|
211
252
|
);
|
|
212
253
|
if (retries.length > 0) {
|
|
213
254
|
b.subsumedBy = [...(b.subsumedBy ?? []), ...retries];
|
|
@@ -217,7 +258,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
217
258
|
batch => batch.payload,
|
|
218
259
|
(gets, blockId, mergeWithGets) => [...(mergeWithGets ?? []), ...gets.filter(id => id === blockId)],
|
|
219
260
|
expiration,
|
|
220
|
-
|
|
261
|
+
(blockId, options) => this.keyNetwork.findCoordinator(routingKeyForBlock(blockId), { ...options, intent: 'read' })
|
|
221
262
|
);
|
|
222
263
|
}
|
|
223
264
|
}));
|
|
@@ -244,13 +285,20 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
244
285
|
// — and only strictly-greater rank replaces, so first-arrival (the stale one)
|
|
245
286
|
// would win the very merge the retry exists to fix. Non-object junk ranks below
|
|
246
287
|
// everything so any real entry replaces it.
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
288
|
+
// Rank alone does not order two peers that both served content: a confirmed block ranks 6
|
|
289
|
+
// whatever revision it is. Cohort peers were expected to agree — they share the block's
|
|
290
|
+
// revision log, so they agree on the highest committed revision at or below a pin — but a
|
|
291
|
+
// peer inside its read-repair window answers from its own copy without consulting anyone,
|
|
292
|
+
// and two cohort members answered one pinned read at revisions 6 and 7. Since only a
|
|
293
|
+
// strictly greater rank replaces, first arrival (the stale one) would win the very merge a
|
|
294
|
+
// below-floor retry exists to fix. So equally-ranked entries that BOTH carry a block break
|
|
295
|
+
// the tie on the newer content, measured by `servedRevision` — the same number the floor is
|
|
296
|
+
// judged against and the same number the reader records as its read dependency, so the
|
|
297
|
+
// merge cannot prefer one revision while the reader believes it read another. The tie-break
|
|
298
|
+
// is deliberately inside a rank rather than across ranks: a confirmed older block still
|
|
299
|
+
// beats an unconfirmed newer one, because "the peer could not confirm this is current" is
|
|
300
|
+
// a statement about the ANSWER, not about the revision, and adopting the unconfirmed one
|
|
301
|
+
// would re-open the doubt the rank split exists to resolve.
|
|
254
302
|
// `unavailable` answers rank among THEMSELVES by how much they establish, so the merged
|
|
255
303
|
// entry never presents a weaker doubt than some peer actually returned. This matters
|
|
256
304
|
// because the reason travels out verbatim on `BlockUnavailableError` and callers act on
|
|
@@ -269,6 +317,33 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
269
317
|
if (entry.unavailable !== undefined) return unavailableRank(entry.unavailable);
|
|
270
318
|
return entry.unconfirmedAheadRev === undefined ? 4 : 3;
|
|
271
319
|
};
|
|
320
|
+
const carriesBlock = (r: unknown): r is GetBlockResult =>
|
|
321
|
+
!!r && typeof r === 'object' && (r as GetBlockResult).block != null;
|
|
322
|
+
/** Whether `candidate` displaces the entry already held for the same block: a strictly
|
|
323
|
+
* better rank, or — at equal rank, both carrying content — strictly newer content.
|
|
324
|
+
*
|
|
325
|
+
* NOTE: `servedRevision` falls back to `state.latest` for a producer that omits
|
|
326
|
+
* `materialized`, and on a PINNED read that fallback overstates — `state.latest` is the
|
|
327
|
+
* newest revision the repo holds, not the revision it served. Every in-tree producer
|
|
328
|
+
* populates `materialized` (`StorageRepo`, `TestTransactor`; `CoordinatorRepo` forwards it
|
|
329
|
+
* verbatim), so the fallback only reaches a third-party `IRepo`, and `belowFloor` above
|
|
330
|
+
* reads the same number, so the two stay consistent. If such a repo ever answers pinned
|
|
331
|
+
* reads beside one that does report `materialized`, it can win this tie with a number
|
|
332
|
+
* describing content it did not serve: cap the comparison at `blockGets.context.rev` then
|
|
333
|
+
* (a correct answer to a pinned read is never above the pin), in both places at once.
|
|
334
|
+
*
|
|
335
|
+
* NOTE: `servedRevision` also reads `state`, which the type requires but a decoded wire
|
|
336
|
+
* response could still omit. Ranking never touched it, so an entry like that used to fault
|
|
337
|
+
* only the reader of ITS block; it now faults the whole `get`, siblings included. Left
|
|
338
|
+
* alone because the reader called `servedRevision` on such an entry anyway — this widens
|
|
339
|
+
* the blast radius, it does not create the fault. If a third-party `IRepo` ever makes it
|
|
340
|
+
* real, make `servedRevision` tolerate a missing `state` rather than guarding here. */
|
|
341
|
+
const beats = (candidate: unknown, held: unknown): boolean => {
|
|
342
|
+
const byRank = rankOf(candidate) - rankOf(held);
|
|
343
|
+
return byRank !== 0
|
|
344
|
+
? byRank > 0
|
|
345
|
+
: carriesBlock(candidate) && carriesBlock(held) && servedRevision(candidate) > servedRevision(held);
|
|
346
|
+
};
|
|
272
347
|
|
|
273
348
|
// Create a lookup map from successful responses only
|
|
274
349
|
const resultEntries = new Map<string, any>();
|
|
@@ -276,7 +351,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
276
351
|
const resp = batch.request!.response! as any;
|
|
277
352
|
for (const [bid, res] of Object.entries(resp)) {
|
|
278
353
|
const existing = resultEntries.get(bid);
|
|
279
|
-
if (!existing ||
|
|
354
|
+
if (!existing || beats(res, existing)) {
|
|
280
355
|
resultEntries.set(bid, res);
|
|
281
356
|
}
|
|
282
357
|
}
|
|
@@ -425,6 +500,57 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
425
500
|
}
|
|
426
501
|
}
|
|
427
502
|
|
|
503
|
+
/**
|
|
504
|
+
* See {@link ITransactor.getLineage}. Asks EVERY member of each block's cohort, directly, and
|
|
505
|
+
* folds their answers with {@link judgeCohortLineage} — not the coordinator round {@link get}
|
|
506
|
+
* runs, because no single member can answer for the cohort: a member that took a later revision
|
|
507
|
+
* as a replica cannot say what it was built from, while the member it came from can.
|
|
508
|
+
*
|
|
509
|
+
* The per-member `get` carries no context, so it pins nothing and promotes nothing; asking is
|
|
510
|
+
* never what makes a write land.
|
|
511
|
+
*
|
|
512
|
+
* NOTE: one request per cohort member per block. Reached only when a writer's own half-landed
|
|
513
|
+
* write was refused as superseded, which is rare, so the fan-out is not batched by member. If a
|
|
514
|
+
* write path ever calls this routinely, group the blocks a member is in the cohort of into one
|
|
515
|
+
* request per member.
|
|
516
|
+
*/
|
|
517
|
+
async getLineage(ref: ActionBlocks & { rev: number }): Promise<ActionLineage> {
|
|
518
|
+
const target = { actionId: ref.actionId, rev: ref.rev };
|
|
519
|
+
const expiration = Date.now() + this.timeoutMs;
|
|
520
|
+
const answers = await Promise.all(ref.blockIds.map(blockId => this.cohortLineage(blockId, target, expiration)));
|
|
521
|
+
const durabilities = answers.flatMap(answer => answer.durability === undefined ? [] : [answer.durability]);
|
|
522
|
+
const saved = answers.length > 0 && durabilities.length === answers.length;
|
|
523
|
+
log('getLineage actionId=%s rev=%d blocks=%o', ref.actionId, ref.rev, answers.map(answer => answer.lineage));
|
|
524
|
+
return {
|
|
525
|
+
blocks: answers.map(answer => answer.lineage),
|
|
526
|
+
...(saved ? { durability: mergeDurability(durabilities) } : {})
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** One block's cohort answer: every member asked in parallel, a member that cannot be reached
|
|
531
|
+
* (or whose cohort cannot be resolved at all) counted as `unknown` rather than left out. */
|
|
532
|
+
private async cohortLineage(blockId: BlockId, target: { actionId: ActionId; rev: number }, expiration: number): Promise<CohortLineage> {
|
|
533
|
+
let cohort: string[];
|
|
534
|
+
try {
|
|
535
|
+
cohort = Object.keys(await this.keyNetwork.findCluster(routingKeyForBlock(blockId)));
|
|
536
|
+
} catch (err) {
|
|
537
|
+
log('getLineage:cohort-unresolved blockId=%s error=%s', blockId, errorMessage(err));
|
|
538
|
+
return { lineage: 'unknown' };
|
|
539
|
+
}
|
|
540
|
+
const members = await Promise.all(cohort.map(async (peerId): Promise<MemberLineage> => {
|
|
541
|
+
try {
|
|
542
|
+
const results = await this.getRepo(peerIdFromString(peerId)).get(
|
|
543
|
+
{ blockIds: [blockId], lineageOf: target }, { expiration, dialTimeoutMs: this.dialTimeoutMs });
|
|
544
|
+
const entry = results?.[blockId];
|
|
545
|
+
return { peerId, lineage: entry?.lineage ?? 'unknown', ...(entry?.state?.latest ? { latest: entry.state.latest } : {}) };
|
|
546
|
+
} catch (err) {
|
|
547
|
+
log('getLineage:member-silent blockId=%s peer=%s error=%s', blockId, peerId, errorMessage(err));
|
|
548
|
+
return { peerId, lineage: 'unknown' };
|
|
549
|
+
}
|
|
550
|
+
}));
|
|
551
|
+
return judgeCohortLineage(members);
|
|
552
|
+
}
|
|
553
|
+
|
|
428
554
|
private async consolidateCoordinators(
|
|
429
555
|
blockIds: BlockId[],
|
|
430
556
|
transforms: Transforms,
|
|
@@ -441,7 +567,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
441
567
|
|
|
442
568
|
await Promise.all(blockIds.map(async bid => {
|
|
443
569
|
try {
|
|
444
|
-
const clusterPeers = await this.keyNetwork.findCluster(
|
|
570
|
+
const clusterPeers = await this.keyNetwork.findCluster(routingKeyForBlock(bid));
|
|
445
571
|
blockClusterPeerIds.set(bid, new Set(Object.keys(clusterPeers)));
|
|
446
572
|
} catch {
|
|
447
573
|
fallbackBlocks.push(bid);
|
|
@@ -463,22 +589,10 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
463
589
|
const assignments = new Map<string, BlockId[]>(); // peerIdStr → assigned blockIds
|
|
464
590
|
|
|
465
591
|
while (uncovered.size > 0) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
for (const
|
|
470
|
-
const coverCount = blocks.filter(bid => uncovered.has(bid)).length;
|
|
471
|
-
if (coverCount > bestCount) {
|
|
472
|
-
bestCount = coverCount;
|
|
473
|
-
bestPeer = peerId;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (!bestPeer || bestCount === 0) break;
|
|
478
|
-
|
|
479
|
-
const covered = peerBlocks.get(bestPeer)!.filter(bid => uncovered.has(bid));
|
|
480
|
-
assignments.set(bestPeer, covered);
|
|
481
|
-
for (const bid of covered) uncovered.delete(bid);
|
|
592
|
+
const best = bestCoveringPeer(peerBlocks, uncovered, this.localPeerId);
|
|
593
|
+
if (!best) break;
|
|
594
|
+
assignments.set(best.peerId, best.covered);
|
|
595
|
+
for (const bid of best.covered) uncovered.delete(bid);
|
|
482
596
|
}
|
|
483
597
|
|
|
484
598
|
// Step 4: Any remaining uncovered blocks fall back to findCoordinator
|
|
@@ -487,7 +601,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
487
601
|
const fallbackCoordinators = await Promise.all(
|
|
488
602
|
fallbackBlocks.map(async bid => ({
|
|
489
603
|
blockId: bid,
|
|
490
|
-
coordinator: await this.keyNetwork.findCoordinator(
|
|
604
|
+
coordinator: await this.keyNetwork.findCoordinator(routingKeyForBlock(bid), { excludedPeers: [] })
|
|
491
605
|
}))
|
|
492
606
|
);
|
|
493
607
|
for (const { blockId, coordinator } of fallbackCoordinators) {
|
|
@@ -549,7 +663,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
549
663
|
await processBatches(
|
|
550
664
|
batches,
|
|
551
665
|
(batch) => this.getRepo(batch.peerId).pend(
|
|
552
|
-
|
|
666
|
+
pendRequestForBatch(blockAction, batch.payload),
|
|
553
667
|
{
|
|
554
668
|
expiration,
|
|
555
669
|
dialTimeoutMs: this.dialTimeoutMs,
|
|
@@ -559,12 +673,12 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
559
673
|
batch => blockIdsForTransforms(batch.payload),
|
|
560
674
|
transformForBlock,
|
|
561
675
|
expiration,
|
|
562
|
-
|
|
676
|
+
(blockId, options) => this.keyNetwork.findCoordinator(routingKeyForBlock(blockId), options)
|
|
563
677
|
);
|
|
564
678
|
// Cache resolved coordinators for follow-up commit to hit the same peers
|
|
565
679
|
try {
|
|
566
680
|
for (const b of Array.from(allBatches(batches))) {
|
|
567
|
-
this.keyNetwork.recordCoordinator?.(
|
|
681
|
+
this.keyNetwork.recordCoordinator?.(routingKeyForBlock(b.blockId), b.peerId);
|
|
568
682
|
}
|
|
569
683
|
} catch (e) { log('WARN: Failed to record coordinator hint %o', e); }
|
|
570
684
|
} catch (e) {
|
|
@@ -667,7 +781,15 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
667
781
|
return {
|
|
668
782
|
success: true,
|
|
669
783
|
pending: completed.flatMap(b => (b.request!.response! as PendSuccess).pending),
|
|
670
|
-
blockIds: blockIdsForTransforms(blockAction.transforms)
|
|
784
|
+
blockIds: blockIdsForTransforms(blockAction.transforms),
|
|
785
|
+
// The weakest coordinator's answer is the action's answer (`mergeDurability`). A pend has
|
|
786
|
+
// no torn case: a batch that did not succeed fails the whole pend above.
|
|
787
|
+
// NOTE: a pend naming NO blocks now throws here (`mergeDurability` refuses an empty input rather
|
|
788
|
+
// than fabricate a class), where it used to answer success. No caller produces one today —
|
|
789
|
+
// `Collection.sync` skips an empty change set, and the multi-collection coordinator pends only
|
|
790
|
+
// collections that appended a log entry. If one ever appears, decide what an empty write's
|
|
791
|
+
// durability means at that caller; do not make the merge invent one.
|
|
792
|
+
durability: mergeDurability(completed.map(b => (b.request!.response! as PendSuccess).durability))
|
|
671
793
|
};
|
|
672
794
|
}
|
|
673
795
|
|
|
@@ -688,8 +810,8 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
688
810
|
}
|
|
689
811
|
|
|
690
812
|
async queryClusterNominees(blockId: BlockId): Promise<ClusterNomineesResult> {
|
|
691
|
-
const
|
|
692
|
-
const clusterPeers = await this.keyNetwork.findCluster(
|
|
813
|
+
const routingKey = routingKeyForBlock(blockId);
|
|
814
|
+
const clusterPeers = await this.keyNetwork.findCluster(routingKey);
|
|
693
815
|
const nominees = Object.keys(clusterPeers).map(idStr => peerIdFromString(idStr));
|
|
694
816
|
return { nominees };
|
|
695
817
|
}
|
|
@@ -709,8 +831,25 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
709
831
|
// Commit the tail block
|
|
710
832
|
const tailResult = await this.commitBlock(request.tailId, request.actionId, request.rev, request.tailId, request.blockDigests);
|
|
711
833
|
if (!tailResult.success) {
|
|
834
|
+
// NOTE: a refused tail is NOT an absent tail. The coordinator's durability gate answers
|
|
835
|
+
// `commit-not-durable` whenever fewer than a majority hold the revision — even though
|
|
836
|
+
// some members stored it — and this return means the sweep below never runs, so every
|
|
837
|
+
// other block of the action is left uncommitted while the log entry may already be
|
|
838
|
+
// readable. This layer cannot tell "tail absent" from "tail present on a minority", so it
|
|
839
|
+
// does not sweep anyway (that would commit data blocks under a tail that may never reach
|
|
840
|
+
// a majority). The writer closes the gap instead: its retry keeps the SAME action id,
|
|
841
|
+
// and if its refresh finds this action's own log entry it re-sends this attempt — same
|
|
842
|
+
// transforms, same revision — to land what was left behind before treating the write as
|
|
843
|
+
// saved (`Collection.completeOwnEntry`). A write is saved only when every block its log
|
|
844
|
+
// entry names holds its revision; the entry alone proves nothing but the tail.
|
|
712
845
|
return tailResult;
|
|
713
846
|
}
|
|
847
|
+
// Every coordinator that confirmed part of this action reports who holds its part; the
|
|
848
|
+
// action-level answer is the WEAKEST of them (`mergeDurability`), so a caller reading only the
|
|
849
|
+
// scalar fields reads the binding constraint. The blocks a torn sweep abandons are on nobody,
|
|
850
|
+
// and `torn` names them: they never heal by replication, only by re-driving the action.
|
|
851
|
+
const cohortReports: WriteDurability[] = [tailResult.durability];
|
|
852
|
+
let torn: BlockId[] = [];
|
|
714
853
|
|
|
715
854
|
// Sweep every non-tail block (the header, when the action touches it, lands here too — after
|
|
716
855
|
// the tail, like any other touched block). The tail is the only exclusion needed.
|
|
@@ -724,6 +863,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
724
863
|
const remainingBlocks = request.blockIds.filter(bid => bid !== request.tailId);
|
|
725
864
|
if (remainingBlocks.length > 0) {
|
|
726
865
|
const { batches, error } = await this.commitBlocks({ blockIds: remainingBlocks, actionId: request.actionId, rev: request.rev, tailId: request.tailId, blockDigests: request.blockDigests });
|
|
866
|
+
cohortReports.push(...confirmedDurabilities(batches));
|
|
727
867
|
if (error) {
|
|
728
868
|
// Split by the failure's NATURE, exactly as commitBlock does for the tail: a RETURNED
|
|
729
869
|
// `success:false` from a cohort coordinator is a confirmed optimistic-concurrency loss
|
|
@@ -747,28 +887,33 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
747
887
|
// latest.actionId === request.actionId` (StorageRepo.pend, which also skips saving
|
|
748
888
|
// a pending record for such a block; ClusterMember.validatePendOperations; and
|
|
749
889
|
// CoordinatorRepo's stale classification). The retry no longer wedges.
|
|
750
|
-
// - its retry
|
|
751
|
-
// (Collection.updateInternal's `inFlightActionId` arm,
|
|
752
|
-
//
|
|
753
|
-
// a duplicate entry.
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
//
|
|
758
|
-
//
|
|
759
|
-
//
|
|
890
|
+
// - its retry recognises its own already-committed log entry instead of replaying it
|
|
891
|
+
// (Collection.updateInternal's `inFlightActionId` arm, set by both write paths —
|
|
892
|
+
// syncInternal and TransactionCoordinator.commitOnce — which reuse one action id
|
|
893
|
+
// across all of a write's attempts). It no longer appends a duplicate entry.
|
|
894
|
+
// - and before consuming that entry it FINISHES the action: the blocks this return
|
|
895
|
+
// abandons are on nobody (the caller cancels their pending records next), so the
|
|
896
|
+
// refresh re-sends the refused attempt verbatim at the same revision, which the
|
|
897
|
+
// carve-outs above turn into "land exactly what is missing"
|
|
898
|
+
// (Collection.completeOwnEntry). The entry alone never counts as saved. If a rival
|
|
899
|
+
// has since taken a revision one of those blocks needed, the writer is told so by
|
|
900
|
+
// name (TornActionError) rather than acknowledged.
|
|
760
901
|
//
|
|
761
902
|
// Transport-shaped failures (throws, no returned refusal) keep the tolerance for the
|
|
762
903
|
// RESULT — the tail committed durably, and reporting failure now would disown an
|
|
763
904
|
// acknowledged write — but NOT for the state the sweep abandoned, which
|
|
764
905
|
// `cancelAbandonedSweepBlocks` below repairs before this returns.
|
|
765
906
|
try { log('WARN: non-tail commit had errors; cancelling unconfirmed blocks, proceeding after tail commit: %s', error.message); } catch { /* ignore */ }
|
|
766
|
-
await this.cancelAbandonedSweepBlocks(request.actionId, remainingBlocks, batches);
|
|
907
|
+
torn = await this.cancelAbandonedSweepBlocks(request.actionId, remainingBlocks, batches);
|
|
767
908
|
}
|
|
768
909
|
}
|
|
769
910
|
|
|
770
|
-
log('commit:done actionId=%s ms=%d', request.actionId, Date.now() - t0);
|
|
771
|
-
|
|
911
|
+
log('commit:done actionId=%s ms=%d torn=%d', request.actionId, Date.now() - t0, torn.length);
|
|
912
|
+
// `withTornBlocks` clamps the class below `full` when the sweep abandoned anything — a cohort
|
|
913
|
+
// can hold every block it was asked for while the action as a whole is incomplete — and never
|
|
914
|
+
// raises it. `isFullyDurable` then answers false; a consumer that needs to know WHICH blocks
|
|
915
|
+
// reads `torn`.
|
|
916
|
+
return { success: true, durability: withTornBlocks(mergeDurability(cohortReports), torn) };
|
|
772
917
|
}
|
|
773
918
|
|
|
774
919
|
/**
|
|
@@ -800,8 +945,11 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
800
945
|
* `debt-unpromotable-pending-records-need-a-sweep`); and this covers only the sweep's abandonment
|
|
801
946
|
* — `StorageRepo.commit`'s genuine-fault arm deliberately KEEPS a failed batch's pendings for a
|
|
802
947
|
* retry, so it is a second producer of the same durable state whenever that retry never comes.
|
|
948
|
+
*
|
|
949
|
+
* @returns the abandoned block ids — exactly the set cancelled here — so {@link commit} can name
|
|
950
|
+
* them as `torn` on the acknowledgement without re-deriving the set by a second rule.
|
|
803
951
|
*/
|
|
804
|
-
private async cancelAbandonedSweepBlocks(actionId: ActionId, sweptBlocks: BlockId[], batches: CoordinatorBatch<BlockId[], CommitResult>[]): Promise<
|
|
952
|
+
private async cancelAbandonedSweepBlocks(actionId: ActionId, sweptBlocks: BlockId[], batches: CoordinatorBatch<BlockId[], CommitResult>[]): Promise<BlockId[]> {
|
|
805
953
|
// NOTE: `confirmed` is only ever non-empty when the sweep spans MORE THAN ONE batch, and it
|
|
806
954
|
// does so only when no single peer covers every swept block — `consolidateCoordinators`'
|
|
807
955
|
// greedy set cover collapses the pend onto one coordinator otherwise, and commit reuses that
|
|
@@ -818,13 +966,14 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
818
966
|
}
|
|
819
967
|
const abandoned = sweptBlocks.filter(bid => !confirmed.has(bid));
|
|
820
968
|
if (abandoned.length === 0) {
|
|
821
|
-
return;
|
|
969
|
+
return abandoned;
|
|
822
970
|
}
|
|
823
971
|
try {
|
|
824
972
|
await this.cancel({ actionId, blockIds: abandoned });
|
|
825
973
|
} catch (cancelError) {
|
|
826
974
|
try { log('WARN: cancel of abandoned sweep blocks failed — pending records may wedge until the node-side sweep lands: %o', cancelError); } catch { /* ignore */ }
|
|
827
975
|
}
|
|
976
|
+
return abandoned;
|
|
828
977
|
}
|
|
829
978
|
|
|
830
979
|
private async commitBlock(blockId: BlockId, actionId: ActionId, rev: number, tailId?: BlockId, blockDigests?: BlockContentDigests): Promise<CommitResult> {
|
|
@@ -841,7 +990,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
841
990
|
}
|
|
842
991
|
throw tailError;
|
|
843
992
|
}
|
|
844
|
-
return { success: true };
|
|
993
|
+
return { success: true, durability: mergeDurability(confirmedDurabilities(tailBatches)) };
|
|
845
994
|
}
|
|
846
995
|
|
|
847
996
|
/**
|
|
@@ -851,21 +1000,26 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
851
1000
|
* both must distinguish a confirmed conflict (return it; the caller cancels and re-drives) from
|
|
852
1001
|
* a transient fault (throw / tolerate).
|
|
853
1002
|
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
* `
|
|
857
|
-
*
|
|
858
|
-
*
|
|
1003
|
+
* Rebuilt the same way {@link pend}'s aggregate is: `reason` is the first one any batch gave
|
|
1004
|
+
* (the only diagnostic that survives into the coordinator's error text — a refusal whose reason
|
|
1005
|
+
* is `commit-not-durable` must not read as "stale commit", a rival's win), `conflict` holds when
|
|
1006
|
+
* any batch was a classified conflict, and `staleAt` is the highest confirmed revision. A
|
|
1007
|
+
* reason-only StaleFailure (success:false, no `missing`) lands here too and comes out with
|
|
1008
|
+
* `missing: []` and its reason intact.
|
|
859
1009
|
*/
|
|
860
1010
|
private staleFromBatches(batches: CoordinatorBatch<BlockId[], CommitResult>[]): StaleFailure | undefined {
|
|
861
1011
|
const stale = Array.from(allBatches(batches, b => b.request?.isResponse as boolean && !b.request!.response!.success));
|
|
862
1012
|
if (stale.length === 0) {
|
|
863
1013
|
return undefined;
|
|
864
1014
|
}
|
|
865
|
-
const
|
|
1015
|
+
const responses = stale.map(b => b.request!.response! as StaleFailure);
|
|
1016
|
+
const staleAt = highestStaleAt(responses.map(r => r.staleAt));
|
|
1017
|
+
const reason = responses.map(r => r.reason).find(r => r !== undefined);
|
|
866
1018
|
return {
|
|
867
|
-
missing: distinctBlockActionTransforms(
|
|
1019
|
+
missing: distinctBlockActionTransforms(responses.flatMap(r => r.missing).filter((x): x is ActionTransforms => x !== undefined)),
|
|
868
1020
|
...(staleAt === undefined ? {} : { staleAt }),
|
|
1021
|
+
...(reason === undefined ? {} : { reason }),
|
|
1022
|
+
conflict: responses.some(isConflictFailure),
|
|
869
1023
|
success: false as const
|
|
870
1024
|
};
|
|
871
1025
|
}
|
|
@@ -954,7 +1108,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
954
1108
|
return cached;
|
|
955
1109
|
}
|
|
956
1110
|
}
|
|
957
|
-
return this.keyNetwork.findCoordinator(
|
|
1111
|
+
return this.keyNetwork.findCoordinator(routingKeyForBlock(blockId), { ...options, intent });
|
|
958
1112
|
}
|
|
959
1113
|
|
|
960
1114
|
/**
|
|
@@ -1091,7 +1245,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
1091
1245
|
batch => batch.payload,
|
|
1092
1246
|
mergeBlocks,
|
|
1093
1247
|
deadline,
|
|
1094
|
-
|
|
1248
|
+
(blockId, options) => this.keyNetwork.findCoordinator(routingKeyForBlock(blockId), options)
|
|
1095
1249
|
);
|
|
1096
1250
|
} catch (e) {
|
|
1097
1251
|
lastError = asError(e);
|
|
@@ -1146,6 +1300,28 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
1146
1300
|
}
|
|
1147
1301
|
|
|
1148
1302
|
|
|
1303
|
+
/**
|
|
1304
|
+
* One round of `consolidateCoordinators`' greedy cover: the cohort member covering the most still-uncovered
|
|
1305
|
+
* blocks, with the blocks it covers, or undefined when no member covers any.
|
|
1306
|
+
*
|
|
1307
|
+
* A tie goes to `localPeerId` when it is among the tied members, otherwise to the member seen first. Cohorts
|
|
1308
|
+
* arrive in proximity order, so "seen first" is the nearest; without the local preference a node that is in
|
|
1309
|
+
* a block's cohort but not nearest would send its own write over the network, and on a network no wider than
|
|
1310
|
+
* a cohort (every node in every cohort) that would be nearly every write.
|
|
1311
|
+
*/
|
|
1312
|
+
function bestCoveringPeer(peerBlocks: Map<string, BlockId[]>, uncovered: Set<BlockId>, localPeerId: string | undefined): { peerId: string; covered: BlockId[] } | undefined {
|
|
1313
|
+
let best: { peerId: string; covered: BlockId[] } | undefined;
|
|
1314
|
+
for (const [peerId, blocks] of peerBlocks) {
|
|
1315
|
+
const covered = blocks.filter(bid => uncovered.has(bid));
|
|
1316
|
+
if (covered.length === 0) continue;
|
|
1317
|
+
const bestCount = best?.covered.length ?? 0;
|
|
1318
|
+
if (covered.length > bestCount || (covered.length === bestCount && peerId === localPeerId)) {
|
|
1319
|
+
best = { peerId, covered };
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return best;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1149
1325
|
/**
|
|
1150
1326
|
* The block ids some batch in the tree got an ANSWER for. A cancel batch that errored, or never
|
|
1151
1327
|
* responded, discharged nothing — its blocks' pending records are still standing. Batch payloads
|
|
@@ -1160,18 +1336,50 @@ function dischargedBlocks(batches: CoordinatorBatch<BlockId[], void>[]): Set<Blo
|
|
|
1160
1336
|
return discharged;
|
|
1161
1337
|
}
|
|
1162
1338
|
|
|
1339
|
+
/**
|
|
1340
|
+
* The durability report off every commit batch in the tree that answered `success: true` — the
|
|
1341
|
+
* coordinators' own answers for the blocks they drove, one per confirmed batch. Retry batches
|
|
1342
|
+
* (`subsumedBy`) are included, so a block re-homed after a failed first attempt reports through the
|
|
1343
|
+
* peer that actually committed it. Batches that threw or returned a refusal contribute nothing.
|
|
1344
|
+
*/
|
|
1345
|
+
function confirmedDurabilities(batches: CoordinatorBatch<BlockId[], CommitResult>[]): WriteDurability[] {
|
|
1346
|
+
return Array.from(allBatches(batches, b => b.request?.isResponse === true && b.request!.response!.success))
|
|
1347
|
+
.map(b => (b.request!.response! as CommitSuccess).durability);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/** The entries of `all` whose ids appear in `batchBlockIds` — the per-batch narrowing both
|
|
1351
|
+
* per-block declarations share ({@link digestsFor}, {@link baseRevsFor}). */
|
|
1352
|
+
function subsetOf<T>(all: Record<BlockId, T>, batchBlockIds: BlockId[]): Record<BlockId, T> {
|
|
1353
|
+
const subset: Record<BlockId, T> = {};
|
|
1354
|
+
for (const id of batchBlockIds) {
|
|
1355
|
+
const entry = all[id];
|
|
1356
|
+
if (entry !== undefined) subset[id] = entry;
|
|
1357
|
+
}
|
|
1358
|
+
return subset;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1163
1361
|
/** The subset of `all` whose ids appear in `batchBlockIds`, wrapped (via {@link blockDigestsField})
|
|
1164
1362
|
* so it spreads to nothing when the batch declares no digests. Called at SEND time, once per attempt,
|
|
1165
1363
|
* because `processBatches` re-batches failed blocks onto different coordinators — a subset computed
|
|
1166
1364
|
* up front would follow the wrong batch on retry. */
|
|
1167
1365
|
function digestsFor(all: BlockContentDigests | undefined, batchBlockIds: BlockId[]): { blockDigests?: BlockContentDigests } {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1366
|
+
return all ? blockDigestsField(subsetOf(all, batchBlockIds)) : {};
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/** The pend-side sibling of {@link digestsFor}: the subset of `all` whose ids appear in
|
|
1370
|
+
* `batchBlockIds`, wrapped (via {@link baseRevsField}) so it spreads to nothing when no block in the
|
|
1371
|
+
* batch names a base. Same send-time rule, for the same reason. */
|
|
1372
|
+
function baseRevsFor(all: BlockBaseRevs | undefined, batchBlockIds: BlockId[]): { baseRevs?: BlockBaseRevs } {
|
|
1373
|
+
return all ? baseRevsField(subsetOf(all, batchBlockIds)) : {};
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/** `action` re-scoped to one coordinator batch: the batch's own transforms, and the bases for the
|
|
1377
|
+
* blocks in it alone (see {@link PendRequest.baseRevs}). The action-wide `baseRevs` key is dropped
|
|
1378
|
+
* BEFORE the subset is spread, so a batch none of whose blocks names a base carries no key at all
|
|
1379
|
+
* rather than the whole action's map — and no cohort signs for a block it is not responsible for. */
|
|
1380
|
+
function pendRequestForBatch(action: PendRequest, payload: Transforms): PendRequest {
|
|
1381
|
+
const { baseRevs, ...rest } = action;
|
|
1382
|
+
return { ...rest, transforms: payload, ...baseRevsFor(baseRevs, blockIdsForTransforms(payload)) };
|
|
1175
1383
|
}
|
|
1176
1384
|
|
|
1177
1385
|
/**
|