@optimystic/db-core 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/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
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { peerIdFromString } from "../network/types.js";
|
|
2
2
|
import { highestStaleAt, isConflictFailure } from "../network/stale-failure.js";
|
|
3
3
|
import { BlockUnavailableError, BlockPossiblyStaleError } from "../network/struct.js";
|
|
4
|
+
import { mergeDurability, withTornBlocks } from "../network/durability.js";
|
|
5
|
+
import { judgeCohortLineage } from "../network/lineage.js";
|
|
4
6
|
import { transformForBlockId, concatTransforms, concatTransform, transformsFromTransform, blockIdsForTransforms } from "../transform/helpers.js";
|
|
5
7
|
import { Tracker } from "../transform/tracker.js";
|
|
6
|
-
import { blockDigestsField } from "../transform/digest.js";
|
|
8
|
+
import { blockDigestsField, baseRevsField } from "../transform/digest.js";
|
|
7
9
|
import { CacheSource } from "../transform/cache-source.js";
|
|
8
|
-
import { TransactorSource } from "./transactor-source.js";
|
|
10
|
+
import { TransactorSource, servedRevision } from "./transactor-source.js";
|
|
9
11
|
import { Log } from "../log/log.js";
|
|
10
12
|
import { groupBy } from "../utility/groupby.js";
|
|
11
|
-
import {
|
|
13
|
+
import { routingKeyForBlock } from "../network/routing-key.js";
|
|
12
14
|
import { isRecordEmpty } from "../utility/is-record-empty.js";
|
|
13
15
|
import { makeBatchesByPeer, incompleteBatches, everyBatch, allBatches, mergeBlocks, processBatches, createBatchesForPayload } from "../utility/batch-coordinator.js";
|
|
14
16
|
import { abortableDelay, jitteredBackoffMs } from "../utility/backoff.js";
|
|
@@ -29,6 +31,8 @@ export class NetworkTransactor {
|
|
|
29
31
|
dialTimeoutMs;
|
|
30
32
|
getRepo;
|
|
31
33
|
localChangeNotifier;
|
|
34
|
+
/** `NetworkTransactorInit.localPeerId` as a string, the form cohort peer ids arrive in. */
|
|
35
|
+
localPeerId;
|
|
32
36
|
/**
|
|
33
37
|
* Per-transaction coordinator cache: `actionId → (blockId → resolved coordinator)`.
|
|
34
38
|
* {@link pend} populates it from its final (retry-adjusted) batch assignment; commit
|
|
@@ -59,6 +63,7 @@ export class NetworkTransactor {
|
|
|
59
63
|
: (init.dialTimeoutMs > 0 ? init.dialTimeoutMs : undefined);
|
|
60
64
|
this.getRepo = init.getRepo;
|
|
61
65
|
this.localChangeNotifier = init.localChangeNotifier;
|
|
66
|
+
this.localPeerId = init.localPeerId?.toString();
|
|
62
67
|
}
|
|
63
68
|
/**
|
|
64
69
|
* Subscribe to commits landing on the local node for `collectionId`, delegating
|
|
@@ -85,7 +90,7 @@ export class NetworkTransactor {
|
|
|
85
90
|
const expiration = Date.now() + this.timeoutMs;
|
|
86
91
|
let error;
|
|
87
92
|
try {
|
|
88
|
-
await processBatches(batches, (batch) => this.getRepo(batch.peerId).get({ blockIds: batch.payload, context: blockGets.context }, { expiration, dialTimeoutMs: this.dialTimeoutMs }), batch => batch.payload, (gets, blockId, mergeWithGets) => [...(mergeWithGets ?? []), ...gets.filter(bid => bid === blockId)], expiration,
|
|
93
|
+
await processBatches(batches, (batch) => this.getRepo(batch.peerId).get({ blockIds: batch.payload, context: blockGets.context }, { expiration, dialTimeoutMs: this.dialTimeoutMs }), batch => batch.payload, (gets, blockId, mergeWithGets) => [...(mergeWithGets ?? []), ...gets.filter(bid => bid === blockId)], expiration, (blockId, options) => this.keyNetwork.findCoordinator(routingKeyForBlock(blockId), { ...options, intent: 'read' }));
|
|
89
94
|
}
|
|
90
95
|
catch (e) {
|
|
91
96
|
error = e;
|
|
@@ -99,35 +104,53 @@ export class NetworkTransactor {
|
|
|
99
104
|
// present and unflagged) — retrying it doubles the round-trips on the common
|
|
100
105
|
// createOrOpen "does this block exist?" probe. Cross-member reconciliation for a
|
|
101
106
|
// missing block has already happened one layer down: CoordinatorRepo.get detects
|
|
102
|
-
// `isMissing` and consults cluster peers before it responds —
|
|
103
|
-
// consult FAILS, the entry now says so via `unavailable` instead of
|
|
104
|
-
// authoritative absent.
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// transactor-level retry to discover: another coordinator would find the same
|
|
110
|
-
// cohort's same answer. A flagged entry earns the retry against a different peer
|
|
111
|
-
// that an absent deliberately does not.
|
|
107
|
+
// `isMissing` and consults cluster peers on every such read before it responds —
|
|
108
|
+
// and when that consult FAILS, the entry now says so via `unavailable` instead of
|
|
109
|
+
// posing as an authoritative absent. So by the time an unflagged absent reaches
|
|
110
|
+
// here there is nothing left for a transactor-level retry to discover, while a
|
|
111
|
+
// flagged entry earns the retry against a different peer that an absent
|
|
112
|
+
// deliberately does not. (The coordinator never remembers an absence across reads:
|
|
113
|
+
// one that did served a just-committed block as never created — GitHub issue #20.)
|
|
112
114
|
// See tickets txn-perf-authoritative-notfound and repo-reports-unavailable-vs-absent.
|
|
113
115
|
const hasValidResponse = (b) => {
|
|
114
116
|
return b.request?.isResponse === true && b.request.response != null;
|
|
115
117
|
};
|
|
118
|
+
// Content the CALLER can prove is too old, from the floor it carried on the request
|
|
119
|
+
// (see {@link BlockGets.floors}): the caller walked a log entry saying this block changed at
|
|
120
|
+
// that revision, so anything below it is not the view the read asked for, however current
|
|
121
|
+
// the answering repo honestly believes its copy to be. Only a block-carrying entry can be
|
|
122
|
+
// below a floor — an absent answer is judged by its own rules above, and treating one as
|
|
123
|
+
// below-floor would put the one-round `createOrOpen` probe back on the retry path.
|
|
124
|
+
// Reported here rather than left to the reader: `TransactorSource` reports
|
|
125
|
+
// `collection:block-below-floor` from the MERGED answer, so a read the retry below repaired
|
|
126
|
+
// is the one case that leaves no trace anywhere else — the extra round would show up in a
|
|
127
|
+
// field log as an unexplained second consult. Costs nothing on an unfloored read, which
|
|
128
|
+
// leaves on the first clause.
|
|
129
|
+
const belowFloor = (bid, entry) => {
|
|
130
|
+
const floor = blockGets.floors?.[bid];
|
|
131
|
+
if (floor === undefined || entry.block == null || servedRevision(entry) >= floor)
|
|
132
|
+
return false;
|
|
133
|
+
log('get:below-floor blockId=%s served=%d floor=%d', bid, servedRevision(entry), floor);
|
|
134
|
+
return true;
|
|
135
|
+
};
|
|
116
136
|
// A batch is answered when its response carries an entry for EVERY requested
|
|
117
|
-
// block id and none of those entries carries a doubt marker
|
|
118
|
-
// with only `state` (no `block`) is an
|
|
119
|
-
// answered — not a gap. An `unavailable`
|
|
120
|
-
// find out whether the block EXISTS; an
|
|
121
|
-
// saying it could not confirm the content
|
|
122
|
-
// sits ahead of it, unsettled)
|
|
123
|
-
//
|
|
137
|
+
// block id and none of those entries carries a doubt marker or falls below the
|
|
138
|
+
// caller's floor. An entry present with only `state` (no `block`) is an
|
|
139
|
+
// authoritative "absent", which counts as answered — not a gap. An `unavailable`
|
|
140
|
+
// entry is the peer saying it could not find out whether the block EXISTS; an
|
|
141
|
+
// `unconfirmedAheadRev` entry is the peer saying it could not confirm the content
|
|
142
|
+
// it served is CURRENT (a cohort claim sits ahead of it, unsettled); a below-floor
|
|
143
|
+
// entry is the CALLER knowing the content is behind a revision it has already seen
|
|
144
|
+
// the log commit. None counts as answered, so each earns the second-chance retry
|
|
145
|
+
// against a different coordinator.
|
|
124
146
|
const isAuthoritative = (b) => {
|
|
125
147
|
if (!hasValidResponse(b))
|
|
126
148
|
return false;
|
|
127
149
|
const resp = b.request.response;
|
|
128
150
|
return b.payload.every(bid => resp[bid] !== undefined
|
|
129
151
|
&& resp[bid].unavailable === undefined
|
|
130
|
-
&& resp[bid].unconfirmedAheadRev === undefined
|
|
152
|
+
&& resp[bid].unconfirmedAheadRev === undefined
|
|
153
|
+
&& !belowFloor(bid, resp[bid]));
|
|
131
154
|
};
|
|
132
155
|
// Retry only genuine no-response / partial-response batches. An authoritative
|
|
133
156
|
// absent answer is not retried.
|
|
@@ -137,6 +160,13 @@ export class NetworkTransactor {
|
|
|
137
160
|
// today — one extra bounded consult on an already failing read. If isolated-node
|
|
138
161
|
// read latency ever matters, skip the retry for that reason rather than widening
|
|
139
162
|
// isAuthoritative.
|
|
163
|
+
// This is ONE extra round, not a loop: a below-floor answer that every reachable
|
|
164
|
+
// coordinator repeats survives the merge below as the highest revision anyone
|
|
165
|
+
// served, UNFLAGGED. Deliberate — a log entry is not proof its blocks landed, so a
|
|
166
|
+
// floor no machine can meet may simply describe an abandoned write, in which case
|
|
167
|
+
// the below-floor content is the correct content and every machine agrees on it
|
|
168
|
+
// (see the accepted-tradeoff NOTE at `TransactorSource.mayRetain`). The reader
|
|
169
|
+
// judges it again and returns it uncached.
|
|
140
170
|
const retryable = Array.from(allBatches(batches)).filter(b => !isAuthoritative(b));
|
|
141
171
|
if (retryable.length > 0 && Date.now() < expiration) {
|
|
142
172
|
log('get:retry retryable=%d', retryable.length);
|
|
@@ -145,10 +175,10 @@ export class NetworkTransactor {
|
|
|
145
175
|
// independent per root and safe to run in parallel.
|
|
146
176
|
const retryOutcomes = await Promise.allSettled(retryable.map(async (b) => {
|
|
147
177
|
const excluded = new Set([b.peerId, ...(b.excludedPeers ?? [])]);
|
|
148
|
-
const retries = await createBatchesForPayload(b.payload, b.payload, (gets, blockId, mergeWithGets) => [...(mergeWithGets ?? []), ...gets.filter(id => id === blockId)], Array.from(excluded),
|
|
178
|
+
const retries = await createBatchesForPayload(b.payload, b.payload, (gets, blockId, mergeWithGets) => [...(mergeWithGets ?? []), ...gets.filter(id => id === blockId)], Array.from(excluded), (blockId, options) => this.keyNetwork.findCoordinator(routingKeyForBlock(blockId), { ...options, intent: 'read' }));
|
|
149
179
|
if (retries.length > 0) {
|
|
150
180
|
b.subsumedBy = [...(b.subsumedBy ?? []), ...retries];
|
|
151
|
-
await processBatches(retries, (batch) => this.getRepo(batch.peerId).get({ blockIds: batch.payload, context: blockGets.context }, { expiration, dialTimeoutMs: this.dialTimeoutMs }), batch => batch.payload, (gets, blockId, mergeWithGets) => [...(mergeWithGets ?? []), ...gets.filter(id => id === blockId)], expiration,
|
|
181
|
+
await processBatches(retries, (batch) => this.getRepo(batch.peerId).get({ blockIds: batch.payload, context: blockGets.context }, { expiration, dialTimeoutMs: this.dialTimeoutMs }), batch => batch.payload, (gets, blockId, mergeWithGets) => [...(mergeWithGets ?? []), ...gets.filter(id => id === blockId)], expiration, (blockId, options) => this.keyNetwork.findCoordinator(routingKeyForBlock(blockId), { ...options, intent: 'read' }));
|
|
152
182
|
}
|
|
153
183
|
}));
|
|
154
184
|
// First-error-wins: keep any pre-existing error, otherwise adopt the first
|
|
@@ -171,13 +201,20 @@ export class NetworkTransactor {
|
|
|
171
201
|
// — and only strictly-greater rank replaces, so first-arrival (the stale one)
|
|
172
202
|
// would win the very merge the retry exists to fix. Non-object junk ranks below
|
|
173
203
|
// everything so any real entry replaces it.
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
204
|
+
// Rank alone does not order two peers that both served content: a confirmed block ranks 6
|
|
205
|
+
// whatever revision it is. Cohort peers were expected to agree — they share the block's
|
|
206
|
+
// revision log, so they agree on the highest committed revision at or below a pin — but a
|
|
207
|
+
// peer inside its read-repair window answers from its own copy without consulting anyone,
|
|
208
|
+
// and two cohort members answered one pinned read at revisions 6 and 7. Since only a
|
|
209
|
+
// strictly greater rank replaces, first arrival (the stale one) would win the very merge a
|
|
210
|
+
// below-floor retry exists to fix. So equally-ranked entries that BOTH carry a block break
|
|
211
|
+
// the tie on the newer content, measured by `servedRevision` — the same number the floor is
|
|
212
|
+
// judged against and the same number the reader records as its read dependency, so the
|
|
213
|
+
// merge cannot prefer one revision while the reader believes it read another. The tie-break
|
|
214
|
+
// is deliberately inside a rank rather than across ranks: a confirmed older block still
|
|
215
|
+
// beats an unconfirmed newer one, because "the peer could not confirm this is current" is
|
|
216
|
+
// a statement about the ANSWER, not about the revision, and adopting the unconfirmed one
|
|
217
|
+
// would re-open the doubt the rank split exists to resolve.
|
|
181
218
|
// `unavailable` answers rank among THEMSELVES by how much they establish, so the merged
|
|
182
219
|
// entry never presents a weaker doubt than some peer actually returned. This matters
|
|
183
220
|
// because the reason travels out verbatim on `BlockUnavailableError` and callers act on
|
|
@@ -198,13 +235,39 @@ export class NetworkTransactor {
|
|
|
198
235
|
return unavailableRank(entry.unavailable);
|
|
199
236
|
return entry.unconfirmedAheadRev === undefined ? 4 : 3;
|
|
200
237
|
};
|
|
238
|
+
const carriesBlock = (r) => !!r && typeof r === 'object' && r.block != null;
|
|
239
|
+
/** Whether `candidate` displaces the entry already held for the same block: a strictly
|
|
240
|
+
* better rank, or — at equal rank, both carrying content — strictly newer content.
|
|
241
|
+
*
|
|
242
|
+
* NOTE: `servedRevision` falls back to `state.latest` for a producer that omits
|
|
243
|
+
* `materialized`, and on a PINNED read that fallback overstates — `state.latest` is the
|
|
244
|
+
* newest revision the repo holds, not the revision it served. Every in-tree producer
|
|
245
|
+
* populates `materialized` (`StorageRepo`, `TestTransactor`; `CoordinatorRepo` forwards it
|
|
246
|
+
* verbatim), so the fallback only reaches a third-party `IRepo`, and `belowFloor` above
|
|
247
|
+
* reads the same number, so the two stay consistent. If such a repo ever answers pinned
|
|
248
|
+
* reads beside one that does report `materialized`, it can win this tie with a number
|
|
249
|
+
* describing content it did not serve: cap the comparison at `blockGets.context.rev` then
|
|
250
|
+
* (a correct answer to a pinned read is never above the pin), in both places at once.
|
|
251
|
+
*
|
|
252
|
+
* NOTE: `servedRevision` also reads `state`, which the type requires but a decoded wire
|
|
253
|
+
* response could still omit. Ranking never touched it, so an entry like that used to fault
|
|
254
|
+
* only the reader of ITS block; it now faults the whole `get`, siblings included. Left
|
|
255
|
+
* alone because the reader called `servedRevision` on such an entry anyway — this widens
|
|
256
|
+
* the blast radius, it does not create the fault. If a third-party `IRepo` ever makes it
|
|
257
|
+
* real, make `servedRevision` tolerate a missing `state` rather than guarding here. */
|
|
258
|
+
const beats = (candidate, held) => {
|
|
259
|
+
const byRank = rankOf(candidate) - rankOf(held);
|
|
260
|
+
return byRank !== 0
|
|
261
|
+
? byRank > 0
|
|
262
|
+
: carriesBlock(candidate) && carriesBlock(held) && servedRevision(candidate) > servedRevision(held);
|
|
263
|
+
};
|
|
201
264
|
// Create a lookup map from successful responses only
|
|
202
265
|
const resultEntries = new Map();
|
|
203
266
|
for (const batch of completedBatches) {
|
|
204
267
|
const resp = batch.request.response;
|
|
205
268
|
for (const [bid, res] of Object.entries(resp)) {
|
|
206
269
|
const existing = resultEntries.get(bid);
|
|
207
|
-
if (!existing ||
|
|
270
|
+
if (!existing || beats(res, existing)) {
|
|
208
271
|
resultEntries.set(bid, res);
|
|
209
272
|
}
|
|
210
273
|
}
|
|
@@ -338,6 +401,56 @@ export class NetworkTransactor {
|
|
|
338
401
|
return false;
|
|
339
402
|
}
|
|
340
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* See {@link ITransactor.getLineage}. Asks EVERY member of each block's cohort, directly, and
|
|
406
|
+
* folds their answers with {@link judgeCohortLineage} — not the coordinator round {@link get}
|
|
407
|
+
* runs, because no single member can answer for the cohort: a member that took a later revision
|
|
408
|
+
* as a replica cannot say what it was built from, while the member it came from can.
|
|
409
|
+
*
|
|
410
|
+
* The per-member `get` carries no context, so it pins nothing and promotes nothing; asking is
|
|
411
|
+
* never what makes a write land.
|
|
412
|
+
*
|
|
413
|
+
* NOTE: one request per cohort member per block. Reached only when a writer's own half-landed
|
|
414
|
+
* write was refused as superseded, which is rare, so the fan-out is not batched by member. If a
|
|
415
|
+
* write path ever calls this routinely, group the blocks a member is in the cohort of into one
|
|
416
|
+
* request per member.
|
|
417
|
+
*/
|
|
418
|
+
async getLineage(ref) {
|
|
419
|
+
const target = { actionId: ref.actionId, rev: ref.rev };
|
|
420
|
+
const expiration = Date.now() + this.timeoutMs;
|
|
421
|
+
const answers = await Promise.all(ref.blockIds.map(blockId => this.cohortLineage(blockId, target, expiration)));
|
|
422
|
+
const durabilities = answers.flatMap(answer => answer.durability === undefined ? [] : [answer.durability]);
|
|
423
|
+
const saved = answers.length > 0 && durabilities.length === answers.length;
|
|
424
|
+
log('getLineage actionId=%s rev=%d blocks=%o', ref.actionId, ref.rev, answers.map(answer => answer.lineage));
|
|
425
|
+
return {
|
|
426
|
+
blocks: answers.map(answer => answer.lineage),
|
|
427
|
+
...(saved ? { durability: mergeDurability(durabilities) } : {})
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/** One block's cohort answer: every member asked in parallel, a member that cannot be reached
|
|
431
|
+
* (or whose cohort cannot be resolved at all) counted as `unknown` rather than left out. */
|
|
432
|
+
async cohortLineage(blockId, target, expiration) {
|
|
433
|
+
let cohort;
|
|
434
|
+
try {
|
|
435
|
+
cohort = Object.keys(await this.keyNetwork.findCluster(routingKeyForBlock(blockId)));
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
log('getLineage:cohort-unresolved blockId=%s error=%s', blockId, errorMessage(err));
|
|
439
|
+
return { lineage: 'unknown' };
|
|
440
|
+
}
|
|
441
|
+
const members = await Promise.all(cohort.map(async (peerId) => {
|
|
442
|
+
try {
|
|
443
|
+
const results = await this.getRepo(peerIdFromString(peerId)).get({ blockIds: [blockId], lineageOf: target }, { expiration, dialTimeoutMs: this.dialTimeoutMs });
|
|
444
|
+
const entry = results?.[blockId];
|
|
445
|
+
return { peerId, lineage: entry?.lineage ?? 'unknown', ...(entry?.state?.latest ? { latest: entry.state.latest } : {}) };
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
log('getLineage:member-silent blockId=%s peer=%s error=%s', blockId, peerId, errorMessage(err));
|
|
449
|
+
return { peerId, lineage: 'unknown' };
|
|
450
|
+
}
|
|
451
|
+
}));
|
|
452
|
+
return judgeCohortLineage(members);
|
|
453
|
+
}
|
|
341
454
|
async consolidateCoordinators(blockIds, transforms, transformForBlock) {
|
|
342
455
|
// Use cluster intersections to minimize the number of coordinators.
|
|
343
456
|
// For each block, find its full cluster, then greedily assign blocks to
|
|
@@ -348,7 +461,7 @@ export class NetworkTransactor {
|
|
|
348
461
|
const fallbackBlocks = [];
|
|
349
462
|
await Promise.all(blockIds.map(async (bid) => {
|
|
350
463
|
try {
|
|
351
|
-
const clusterPeers = await this.keyNetwork.findCluster(
|
|
464
|
+
const clusterPeers = await this.keyNetwork.findCluster(routingKeyForBlock(bid));
|
|
352
465
|
blockClusterPeerIds.set(bid, new Set(Object.keys(clusterPeers)));
|
|
353
466
|
}
|
|
354
467
|
catch {
|
|
@@ -368,20 +481,11 @@ export class NetworkTransactor {
|
|
|
368
481
|
const uncovered = new Set(blockClusterPeerIds.keys());
|
|
369
482
|
const assignments = new Map(); // peerIdStr → assigned blockIds
|
|
370
483
|
while (uncovered.size > 0) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
for (const [peerId, blocks] of peerBlocks) {
|
|
374
|
-
const coverCount = blocks.filter(bid => uncovered.has(bid)).length;
|
|
375
|
-
if (coverCount > bestCount) {
|
|
376
|
-
bestCount = coverCount;
|
|
377
|
-
bestPeer = peerId;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
if (!bestPeer || bestCount === 0)
|
|
484
|
+
const best = bestCoveringPeer(peerBlocks, uncovered, this.localPeerId);
|
|
485
|
+
if (!best)
|
|
381
486
|
break;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
for (const bid of covered)
|
|
487
|
+
assignments.set(best.peerId, best.covered);
|
|
488
|
+
for (const bid of best.covered)
|
|
385
489
|
uncovered.delete(bid);
|
|
386
490
|
}
|
|
387
491
|
// Step 4: Any remaining uncovered blocks fall back to findCoordinator
|
|
@@ -389,7 +493,7 @@ export class NetworkTransactor {
|
|
|
389
493
|
fallbackBlocks.push(bid);
|
|
390
494
|
const fallbackCoordinators = await Promise.all(fallbackBlocks.map(async (bid) => ({
|
|
391
495
|
blockId: bid,
|
|
392
|
-
coordinator: await this.keyNetwork.findCoordinator(
|
|
496
|
+
coordinator: await this.keyNetwork.findCoordinator(routingKeyForBlock(bid), { excludedPeers: [] })
|
|
393
497
|
})));
|
|
394
498
|
for (const { blockId, coordinator } of fallbackCoordinators) {
|
|
395
499
|
const key = coordinator.toString();
|
|
@@ -441,15 +545,15 @@ export class NetworkTransactor {
|
|
|
441
545
|
let error;
|
|
442
546
|
try {
|
|
443
547
|
// Process all batches, noting all outstanding peers
|
|
444
|
-
await processBatches(batches, (batch) => this.getRepo(batch.peerId).pend(
|
|
548
|
+
await processBatches(batches, (batch) => this.getRepo(batch.peerId).pend(pendRequestForBatch(blockAction, batch.payload), {
|
|
445
549
|
expiration,
|
|
446
550
|
dialTimeoutMs: this.dialTimeoutMs,
|
|
447
551
|
coordinatingBlockIds: batch.coordinatingBlockIds
|
|
448
|
-
}), batch => blockIdsForTransforms(batch.payload), transformForBlock, expiration,
|
|
552
|
+
}), batch => blockIdsForTransforms(batch.payload), transformForBlock, expiration, (blockId, options) => this.keyNetwork.findCoordinator(routingKeyForBlock(blockId), options));
|
|
449
553
|
// Cache resolved coordinators for follow-up commit to hit the same peers
|
|
450
554
|
try {
|
|
451
555
|
for (const b of Array.from(allBatches(batches))) {
|
|
452
|
-
this.keyNetwork.recordCoordinator?.(
|
|
556
|
+
this.keyNetwork.recordCoordinator?.(routingKeyForBlock(b.blockId), b.peerId);
|
|
453
557
|
}
|
|
454
558
|
}
|
|
455
559
|
catch (e) {
|
|
@@ -551,7 +655,15 @@ export class NetworkTransactor {
|
|
|
551
655
|
return {
|
|
552
656
|
success: true,
|
|
553
657
|
pending: completed.flatMap(b => b.request.response.pending),
|
|
554
|
-
blockIds: blockIdsForTransforms(blockAction.transforms)
|
|
658
|
+
blockIds: blockIdsForTransforms(blockAction.transforms),
|
|
659
|
+
// The weakest coordinator's answer is the action's answer (`mergeDurability`). A pend has
|
|
660
|
+
// no torn case: a batch that did not succeed fails the whole pend above.
|
|
661
|
+
// NOTE: a pend naming NO blocks now throws here (`mergeDurability` refuses an empty input rather
|
|
662
|
+
// than fabricate a class), where it used to answer success. No caller produces one today —
|
|
663
|
+
// `Collection.sync` skips an empty change set, and the multi-collection coordinator pends only
|
|
664
|
+
// collections that appended a log entry. If one ever appears, decide what an empty write's
|
|
665
|
+
// durability means at that caller; do not make the merge invent one.
|
|
666
|
+
durability: mergeDurability(completed.map(b => b.request.response.durability))
|
|
555
667
|
};
|
|
556
668
|
}
|
|
557
669
|
/**
|
|
@@ -570,8 +682,8 @@ export class NetworkTransactor {
|
|
|
570
682
|
await this.dischargeCancel(actionRef);
|
|
571
683
|
}
|
|
572
684
|
async queryClusterNominees(blockId) {
|
|
573
|
-
const
|
|
574
|
-
const clusterPeers = await this.keyNetwork.findCluster(
|
|
685
|
+
const routingKey = routingKeyForBlock(blockId);
|
|
686
|
+
const clusterPeers = await this.keyNetwork.findCluster(routingKey);
|
|
575
687
|
const nominees = Object.keys(clusterPeers).map(idStr => peerIdFromString(idStr));
|
|
576
688
|
return { nominees };
|
|
577
689
|
}
|
|
@@ -588,8 +700,25 @@ export class NetworkTransactor {
|
|
|
588
700
|
// Commit the tail block
|
|
589
701
|
const tailResult = await this.commitBlock(request.tailId, request.actionId, request.rev, request.tailId, request.blockDigests);
|
|
590
702
|
if (!tailResult.success) {
|
|
703
|
+
// NOTE: a refused tail is NOT an absent tail. The coordinator's durability gate answers
|
|
704
|
+
// `commit-not-durable` whenever fewer than a majority hold the revision — even though
|
|
705
|
+
// some members stored it — and this return means the sweep below never runs, so every
|
|
706
|
+
// other block of the action is left uncommitted while the log entry may already be
|
|
707
|
+
// readable. This layer cannot tell "tail absent" from "tail present on a minority", so it
|
|
708
|
+
// does not sweep anyway (that would commit data blocks under a tail that may never reach
|
|
709
|
+
// a majority). The writer closes the gap instead: its retry keeps the SAME action id,
|
|
710
|
+
// and if its refresh finds this action's own log entry it re-sends this attempt — same
|
|
711
|
+
// transforms, same revision — to land what was left behind before treating the write as
|
|
712
|
+
// saved (`Collection.completeOwnEntry`). A write is saved only when every block its log
|
|
713
|
+
// entry names holds its revision; the entry alone proves nothing but the tail.
|
|
591
714
|
return tailResult;
|
|
592
715
|
}
|
|
716
|
+
// Every coordinator that confirmed part of this action reports who holds its part; the
|
|
717
|
+
// action-level answer is the WEAKEST of them (`mergeDurability`), so a caller reading only the
|
|
718
|
+
// scalar fields reads the binding constraint. The blocks a torn sweep abandons are on nobody,
|
|
719
|
+
// and `torn` names them: they never heal by replication, only by re-driving the action.
|
|
720
|
+
const cohortReports = [tailResult.durability];
|
|
721
|
+
let torn = [];
|
|
593
722
|
// Sweep every non-tail block (the header, when the action touches it, lands here too — after
|
|
594
723
|
// the tail, like any other touched block). The tail is the only exclusion needed.
|
|
595
724
|
//
|
|
@@ -602,6 +731,7 @@ export class NetworkTransactor {
|
|
|
602
731
|
const remainingBlocks = request.blockIds.filter(bid => bid !== request.tailId);
|
|
603
732
|
if (remainingBlocks.length > 0) {
|
|
604
733
|
const { batches, error } = await this.commitBlocks({ blockIds: remainingBlocks, actionId: request.actionId, rev: request.rev, tailId: request.tailId, blockDigests: request.blockDigests });
|
|
734
|
+
cohortReports.push(...confirmedDurabilities(batches));
|
|
605
735
|
if (error) {
|
|
606
736
|
// Split by the failure's NATURE, exactly as commitBlock does for the tail: a RETURNED
|
|
607
737
|
// `success:false` from a cohort coordinator is a confirmed optimistic-concurrency loss
|
|
@@ -625,16 +755,17 @@ export class NetworkTransactor {
|
|
|
625
755
|
// latest.actionId === request.actionId` (StorageRepo.pend, which also skips saving
|
|
626
756
|
// a pending record for such a block; ClusterMember.validatePendOperations; and
|
|
627
757
|
// CoordinatorRepo's stale classification). The retry no longer wedges.
|
|
628
|
-
// - its retry
|
|
629
|
-
// (Collection.updateInternal's `inFlightActionId` arm,
|
|
630
|
-
//
|
|
631
|
-
// a duplicate entry.
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
//
|
|
637
|
-
//
|
|
758
|
+
// - its retry recognises its own already-committed log entry instead of replaying it
|
|
759
|
+
// (Collection.updateInternal's `inFlightActionId` arm, set by both write paths —
|
|
760
|
+
// syncInternal and TransactionCoordinator.commitOnce — which reuse one action id
|
|
761
|
+
// across all of a write's attempts). It no longer appends a duplicate entry.
|
|
762
|
+
// - and before consuming that entry it FINISHES the action: the blocks this return
|
|
763
|
+
// abandons are on nobody (the caller cancels their pending records next), so the
|
|
764
|
+
// refresh re-sends the refused attempt verbatim at the same revision, which the
|
|
765
|
+
// carve-outs above turn into "land exactly what is missing"
|
|
766
|
+
// (Collection.completeOwnEntry). The entry alone never counts as saved. If a rival
|
|
767
|
+
// has since taken a revision one of those blocks needed, the writer is told so by
|
|
768
|
+
// name (TornActionError) rather than acknowledged.
|
|
638
769
|
//
|
|
639
770
|
// Transport-shaped failures (throws, no returned refusal) keep the tolerance for the
|
|
640
771
|
// RESULT — the tail committed durably, and reporting failure now would disown an
|
|
@@ -644,11 +775,15 @@ export class NetworkTransactor {
|
|
|
644
775
|
log('WARN: non-tail commit had errors; cancelling unconfirmed blocks, proceeding after tail commit: %s', error.message);
|
|
645
776
|
}
|
|
646
777
|
catch { /* ignore */ }
|
|
647
|
-
await this.cancelAbandonedSweepBlocks(request.actionId, remainingBlocks, batches);
|
|
778
|
+
torn = await this.cancelAbandonedSweepBlocks(request.actionId, remainingBlocks, batches);
|
|
648
779
|
}
|
|
649
780
|
}
|
|
650
|
-
log('commit:done actionId=%s ms=%d', request.actionId, Date.now() - t0);
|
|
651
|
-
|
|
781
|
+
log('commit:done actionId=%s ms=%d torn=%d', request.actionId, Date.now() - t0, torn.length);
|
|
782
|
+
// `withTornBlocks` clamps the class below `full` when the sweep abandoned anything — a cohort
|
|
783
|
+
// can hold every block it was asked for while the action as a whole is incomplete — and never
|
|
784
|
+
// raises it. `isFullyDurable` then answers false; a consumer that needs to know WHICH blocks
|
|
785
|
+
// reads `torn`.
|
|
786
|
+
return { success: true, durability: withTornBlocks(mergeDurability(cohortReports), torn) };
|
|
652
787
|
}
|
|
653
788
|
/**
|
|
654
789
|
* Cancels every sweep block whose commit batch never confirmed success, so an acknowledged
|
|
@@ -679,6 +814,9 @@ export class NetworkTransactor {
|
|
|
679
814
|
* `debt-unpromotable-pending-records-need-a-sweep`); and this covers only the sweep's abandonment
|
|
680
815
|
* — `StorageRepo.commit`'s genuine-fault arm deliberately KEEPS a failed batch's pendings for a
|
|
681
816
|
* retry, so it is a second producer of the same durable state whenever that retry never comes.
|
|
817
|
+
*
|
|
818
|
+
* @returns the abandoned block ids — exactly the set cancelled here — so {@link commit} can name
|
|
819
|
+
* them as `torn` on the acknowledgement without re-deriving the set by a second rule.
|
|
682
820
|
*/
|
|
683
821
|
async cancelAbandonedSweepBlocks(actionId, sweptBlocks, batches) {
|
|
684
822
|
// NOTE: `confirmed` is only ever non-empty when the sweep spans MORE THAN ONE batch, and it
|
|
@@ -698,7 +836,7 @@ export class NetworkTransactor {
|
|
|
698
836
|
}
|
|
699
837
|
const abandoned = sweptBlocks.filter(bid => !confirmed.has(bid));
|
|
700
838
|
if (abandoned.length === 0) {
|
|
701
|
-
return;
|
|
839
|
+
return abandoned;
|
|
702
840
|
}
|
|
703
841
|
try {
|
|
704
842
|
await this.cancel({ actionId, blockIds: abandoned });
|
|
@@ -709,6 +847,7 @@ export class NetworkTransactor {
|
|
|
709
847
|
}
|
|
710
848
|
catch { /* ignore */ }
|
|
711
849
|
}
|
|
850
|
+
return abandoned;
|
|
712
851
|
}
|
|
713
852
|
async commitBlock(blockId, actionId, rev, tailId, blockDigests) {
|
|
714
853
|
const { batches: tailBatches, error: tailError } = await this.commitBlocks({ blockIds: [blockId], actionId, rev, tailId, blockDigests });
|
|
@@ -724,7 +863,7 @@ export class NetworkTransactor {
|
|
|
724
863
|
}
|
|
725
864
|
throw tailError;
|
|
726
865
|
}
|
|
727
|
-
return { success: true };
|
|
866
|
+
return { success: true, durability: mergeDurability(confirmedDurabilities(tailBatches)) };
|
|
728
867
|
}
|
|
729
868
|
/**
|
|
730
869
|
* Merge the RETURNED `success:false` responses out of a set of commit batches into one
|
|
@@ -733,21 +872,26 @@ export class NetworkTransactor {
|
|
|
733
872
|
* both must distinguish a confirmed conflict (return it; the caller cancels and re-drives) from
|
|
734
873
|
* a transient fault (throw / tolerate).
|
|
735
874
|
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
* `
|
|
739
|
-
*
|
|
740
|
-
*
|
|
875
|
+
* Rebuilt the same way {@link pend}'s aggregate is: `reason` is the first one any batch gave
|
|
876
|
+
* (the only diagnostic that survives into the coordinator's error text — a refusal whose reason
|
|
877
|
+
* is `commit-not-durable` must not read as "stale commit", a rival's win), `conflict` holds when
|
|
878
|
+
* any batch was a classified conflict, and `staleAt` is the highest confirmed revision. A
|
|
879
|
+
* reason-only StaleFailure (success:false, no `missing`) lands here too and comes out with
|
|
880
|
+
* `missing: []` and its reason intact.
|
|
741
881
|
*/
|
|
742
882
|
staleFromBatches(batches) {
|
|
743
883
|
const stale = Array.from(allBatches(batches, b => b.request?.isResponse && !b.request.response.success));
|
|
744
884
|
if (stale.length === 0) {
|
|
745
885
|
return undefined;
|
|
746
886
|
}
|
|
747
|
-
const
|
|
887
|
+
const responses = stale.map(b => b.request.response);
|
|
888
|
+
const staleAt = highestStaleAt(responses.map(r => r.staleAt));
|
|
889
|
+
const reason = responses.map(r => r.reason).find(r => r !== undefined);
|
|
748
890
|
return {
|
|
749
|
-
missing: distinctBlockActionTransforms(
|
|
891
|
+
missing: distinctBlockActionTransforms(responses.flatMap(r => r.missing).filter((x) => x !== undefined)),
|
|
750
892
|
...(staleAt === undefined ? {} : { staleAt }),
|
|
893
|
+
...(reason === undefined ? {} : { reason }),
|
|
894
|
+
conflict: responses.some(isConflictFailure),
|
|
751
895
|
success: false
|
|
752
896
|
};
|
|
753
897
|
}
|
|
@@ -810,7 +954,7 @@ export class NetworkTransactor {
|
|
|
810
954
|
return cached;
|
|
811
955
|
}
|
|
812
956
|
}
|
|
813
|
-
return this.keyNetwork.findCoordinator(
|
|
957
|
+
return this.keyNetwork.findCoordinator(routingKeyForBlock(blockId), { ...options, intent });
|
|
814
958
|
}
|
|
815
959
|
/**
|
|
816
960
|
* Get (creating if absent) the per-transaction coordinator map for `actionId`,
|
|
@@ -928,7 +1072,7 @@ export class NetworkTransactor {
|
|
|
928
1072
|
roundBatches = round === 0 && seedBatches
|
|
929
1073
|
? seedBatches
|
|
930
1074
|
: await this.batchesForPayload(outstanding, outstanding, mergeBlocks, []);
|
|
931
|
-
await processBatches(roundBatches, (batch) => this.getRepo(batch.peerId).cancel({ actionId: actionRef.actionId, blockIds: batch.payload }, { expiration: deadline, dialTimeoutMs: this.dialTimeoutMs }), batch => batch.payload, mergeBlocks, deadline,
|
|
1075
|
+
await processBatches(roundBatches, (batch) => this.getRepo(batch.peerId).cancel({ actionId: actionRef.actionId, blockIds: batch.payload }, { expiration: deadline, dialTimeoutMs: this.dialTimeoutMs }), batch => batch.payload, mergeBlocks, deadline, (blockId, options) => this.keyNetwork.findCoordinator(routingKeyForBlock(blockId), options));
|
|
932
1076
|
}
|
|
933
1077
|
catch (e) {
|
|
934
1078
|
lastError = asError(e);
|
|
@@ -973,6 +1117,28 @@ export class NetworkTransactor {
|
|
|
973
1117
|
return details;
|
|
974
1118
|
}
|
|
975
1119
|
}
|
|
1120
|
+
/**
|
|
1121
|
+
* One round of `consolidateCoordinators`' greedy cover: the cohort member covering the most still-uncovered
|
|
1122
|
+
* blocks, with the blocks it covers, or undefined when no member covers any.
|
|
1123
|
+
*
|
|
1124
|
+
* A tie goes to `localPeerId` when it is among the tied members, otherwise to the member seen first. Cohorts
|
|
1125
|
+
* arrive in proximity order, so "seen first" is the nearest; without the local preference a node that is in
|
|
1126
|
+
* a block's cohort but not nearest would send its own write over the network, and on a network no wider than
|
|
1127
|
+
* a cohort (every node in every cohort) that would be nearly every write.
|
|
1128
|
+
*/
|
|
1129
|
+
function bestCoveringPeer(peerBlocks, uncovered, localPeerId) {
|
|
1130
|
+
let best;
|
|
1131
|
+
for (const [peerId, blocks] of peerBlocks) {
|
|
1132
|
+
const covered = blocks.filter(bid => uncovered.has(bid));
|
|
1133
|
+
if (covered.length === 0)
|
|
1134
|
+
continue;
|
|
1135
|
+
const bestCount = best?.covered.length ?? 0;
|
|
1136
|
+
if (covered.length > bestCount || (covered.length === bestCount && peerId === localPeerId)) {
|
|
1137
|
+
best = { peerId, covered };
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return best;
|
|
1141
|
+
}
|
|
976
1142
|
/**
|
|
977
1143
|
* The block ids some batch in the tree got an ANSWER for. A cancel batch that errored, or never
|
|
978
1144
|
* responded, discharged nothing — its blocks' pending records are still standing. Batch payloads
|
|
@@ -987,20 +1153,47 @@ function dischargedBlocks(batches) {
|
|
|
987
1153
|
}
|
|
988
1154
|
return discharged;
|
|
989
1155
|
}
|
|
1156
|
+
/**
|
|
1157
|
+
* The durability report off every commit batch in the tree that answered `success: true` — the
|
|
1158
|
+
* coordinators' own answers for the blocks they drove, one per confirmed batch. Retry batches
|
|
1159
|
+
* (`subsumedBy`) are included, so a block re-homed after a failed first attempt reports through the
|
|
1160
|
+
* peer that actually committed it. Batches that threw or returned a refusal contribute nothing.
|
|
1161
|
+
*/
|
|
1162
|
+
function confirmedDurabilities(batches) {
|
|
1163
|
+
return Array.from(allBatches(batches, b => b.request?.isResponse === true && b.request.response.success))
|
|
1164
|
+
.map(b => b.request.response.durability);
|
|
1165
|
+
}
|
|
1166
|
+
/** The entries of `all` whose ids appear in `batchBlockIds` — the per-batch narrowing both
|
|
1167
|
+
* per-block declarations share ({@link digestsFor}, {@link baseRevsFor}). */
|
|
1168
|
+
function subsetOf(all, batchBlockIds) {
|
|
1169
|
+
const subset = {};
|
|
1170
|
+
for (const id of batchBlockIds) {
|
|
1171
|
+
const entry = all[id];
|
|
1172
|
+
if (entry !== undefined)
|
|
1173
|
+
subset[id] = entry;
|
|
1174
|
+
}
|
|
1175
|
+
return subset;
|
|
1176
|
+
}
|
|
990
1177
|
/** The subset of `all` whose ids appear in `batchBlockIds`, wrapped (via {@link blockDigestsField})
|
|
991
1178
|
* so it spreads to nothing when the batch declares no digests. Called at SEND time, once per attempt,
|
|
992
1179
|
* because `processBatches` re-batches failed blocks onto different coordinators — a subset computed
|
|
993
1180
|
* up front would follow the wrong batch on retry. */
|
|
994
1181
|
function digestsFor(all, batchBlockIds) {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1182
|
+
return all ? blockDigestsField(subsetOf(all, batchBlockIds)) : {};
|
|
1183
|
+
}
|
|
1184
|
+
/** The pend-side sibling of {@link digestsFor}: the subset of `all` whose ids appear in
|
|
1185
|
+
* `batchBlockIds`, wrapped (via {@link baseRevsField}) so it spreads to nothing when no block in the
|
|
1186
|
+
* batch names a base. Same send-time rule, for the same reason. */
|
|
1187
|
+
function baseRevsFor(all, batchBlockIds) {
|
|
1188
|
+
return all ? baseRevsField(subsetOf(all, batchBlockIds)) : {};
|
|
1189
|
+
}
|
|
1190
|
+
/** `action` re-scoped to one coordinator batch: the batch's own transforms, and the bases for the
|
|
1191
|
+
* blocks in it alone (see {@link PendRequest.baseRevs}). The action-wide `baseRevs` key is dropped
|
|
1192
|
+
* BEFORE the subset is spread, so a batch none of whose blocks names a base carries no key at all
|
|
1193
|
+
* rather than the whole action's map — and no cohort signs for a block it is not responsible for. */
|
|
1194
|
+
function pendRequestForBatch(action, payload) {
|
|
1195
|
+
const { baseRevs, ...rest } = action;
|
|
1196
|
+
return { ...rest, transforms: payload, ...baseRevsFor(baseRevs, blockIdsForTransforms(payload)) };
|
|
1004
1197
|
}
|
|
1005
1198
|
/**
|
|
1006
1199
|
* The owning collection id for an action ref, read from any fetched block's header. A
|