@optimystic/db-p2p 1.0.0-beta.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cluster/block-transfer-service.d.ts +73 -1
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +70 -3
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +16 -1
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +73 -72
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/client.d.ts +4 -6
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +8 -13
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +2 -1
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +126 -6
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +378 -50
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.js +2 -2
- package/dist/src/cluster/race-resolution.d.ts +51 -7
- package/dist/src/cluster/race-resolution.d.ts.map +1 -1
- package/dist/src/cluster/race-resolution.js +71 -7
- package/dist/src/cluster/race-resolution.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +67 -5
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +120 -26
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +34 -47
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -6
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/membership-source.js +1 -1
- package/dist/src/cohort-topic/membership-source.js.map +1 -1
- package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/peer-codec.js +5 -2
- package/dist/src/cohort-topic/peer-codec.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +24 -3
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +36 -3
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/cohort-topic/topic-router.d.ts +9 -0
- package/dist/src/cohort-topic/topic-router.d.ts.map +1 -1
- package/dist/src/cohort-topic/topic-router.js +12 -2
- package/dist/src/cohort-topic/topic-router.js.map +1 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +15 -3
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +78 -17
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +234 -147
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +35 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +151 -26
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/libp2p-node-rn.d.ts +2 -2
- package/dist/src/libp2p-node-rn.d.ts.map +1 -1
- package/dist/src/libp2p-node-rn.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +2 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +6 -1
- package/dist/src/logger.js.map +1 -1
- package/dist/src/matchmaking/query-transport.d.ts +3 -4
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
- package/dist/src/matchmaking/query-transport.js +9 -9
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +10 -5
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +11 -2
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/network/relay-reservation.d.ts +170 -0
- package/dist/src/network/relay-reservation.d.ts.map +1 -0
- package/dist/src/network/relay-reservation.js +527 -0
- package/dist/src/network/relay-reservation.js.map +1 -0
- package/dist/src/pend-validation.d.ts +3 -2
- package/dist/src/pend-validation.d.ts.map +1 -1
- package/dist/src/pend-validation.js +3 -2
- package/dist/src/pend-validation.js.map +1 -1
- package/dist/src/protocol-client.d.ts +8 -1
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +11 -0
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/reactivity/forwarder-host.d.ts +2 -1
- package/dist/src/reactivity/forwarder-host.d.ts.map +1 -1
- package/dist/src/reactivity/forwarder-host.js +2 -1
- package/dist/src/reactivity/forwarder-host.js.map +1 -1
- package/dist/src/reactivity/origination-manager.d.ts +3 -3
- package/dist/src/reactivity/origination-manager.js +3 -3
- package/dist/src/reactivity/recover-transport.d.ts +22 -14
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +32 -19
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/reactivity/subscription-manager.d.ts +2 -2
- package/dist/src/reactivity/topic-bytes.d.ts +6 -7
- package/dist/src/reactivity/topic-bytes.d.ts.map +1 -1
- package/dist/src/reactivity/topic-bytes.js +8 -9
- package/dist/src/reactivity/topic-bytes.js.map +1 -1
- package/dist/src/repo/client.d.ts +5 -2
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +49 -40
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +77 -5
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +131 -20
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +238 -122
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +625 -472
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/i-under-replication-ledger.d.ts +87 -0
- package/dist/src/repo/i-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/i-under-replication-ledger.js +2 -0
- package/dist/src/repo/i-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts +91 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.js +287 -0
- package/dist/src/repo/kv-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/responsibility.d.ts +37 -0
- package/dist/src/repo/responsibility.d.ts.map +1 -0
- package/dist/src/repo/responsibility.js +37 -0
- package/dist/src/repo/responsibility.js.map +1 -0
- package/dist/src/repo/service.d.ts +44 -15
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +64 -27
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/repo/stuck-reservation.d.ts +127 -0
- package/dist/src/repo/stuck-reservation.d.ts.map +1 -0
- package/dist/src/repo/stuck-reservation.js +179 -0
- package/dist/src/repo/stuck-reservation.js.map +1 -0
- package/dist/src/repo/under-replication-drain.d.ts +208 -0
- package/dist/src/repo/under-replication-drain.d.ts.map +1 -0
- package/dist/src/repo/under-replication-drain.js +461 -0
- package/dist/src/repo/under-replication-drain.js.map +1 -0
- package/dist/src/rn.d.ts +5 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +5 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-latch.d.ts +3 -2
- package/dist/src/storage/block-latch.d.ts.map +1 -1
- package/dist/src/storage/block-latch.js +7 -4
- package/dist/src/storage/block-latch.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts +45 -3
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +156 -9
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +57 -5
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/pending-claim.d.ts +167 -0
- package/dist/src/storage/pending-claim.d.ts.map +1 -0
- package/dist/src/storage/pending-claim.js +156 -0
- package/dist/src/storage/pending-claim.js.map +1 -0
- package/dist/src/storage/raw-store-codec.d.ts +13 -0
- package/dist/src/storage/raw-store-codec.d.ts.map +1 -1
- package/dist/src/storage/raw-store-codec.js +24 -3
- package/dist/src/storage/raw-store-codec.js.map +1 -1
- package/dist/src/storage/restoration-coordinator.d.ts.map +1 -1
- package/dist/src/storage/restoration-coordinator.js +2 -1
- package/dist/src/storage/restoration-coordinator.js.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.js +3 -3
- package/dist/src/storage/ring-shift-coordinator.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +122 -5
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +245 -68
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/struct.d.ts +58 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +77 -11
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +226 -100
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +27 -3
- package/src/cluster/block-transfer-service.ts +132 -4
- package/src/cluster/block-transfer.ts +79 -84
- package/src/cluster/client.ts +115 -119
- package/src/cluster/cluster-policy.ts +2 -1
- package/src/cluster/cluster-repo.ts +422 -56
- package/src/cluster/commit-proof.ts +2 -2
- package/src/cluster/race-resolution.ts +76 -7
- package/src/cluster/rebalance-monitor.ts +677 -539
- package/src/cluster/spread-on-churn.ts +34 -54
- package/src/cohort-topic/host.ts +8 -6
- package/src/cohort-topic/membership-source.ts +1 -1
- package/src/cohort-topic/peer-codec.ts +34 -31
- package/src/cohort-topic/stream-util.ts +39 -4
- package/src/cohort-topic/topic-router.ts +12 -2
- package/src/dispute/client.ts +17 -7
- package/src/index.ts +64 -59
- package/src/libp2p-key-network.ts +1377 -1260
- package/src/libp2p-node-base.ts +186 -27
- package/src/libp2p-node-rn.ts +31 -30
- package/src/libp2p-node.ts +37 -36
- package/src/logger.ts +6 -1
- package/src/matchmaking/query-transport.ts +9 -9
- package/src/network/network-manager-service.ts +16 -5
- package/src/network/relay-reservation.ts +636 -0
- package/src/pend-validation.ts +3 -2
- package/src/protocol-client.ts +13 -1
- package/src/reactivity/forwarder-host.ts +2 -1
- package/src/reactivity/origination-manager.ts +3 -3
- package/src/reactivity/recover-transport.ts +39 -22
- package/src/reactivity/subscription-manager.ts +2 -2
- package/src/reactivity/topic-bytes.ts +35 -37
- package/src/repo/client.ts +172 -173
- package/src/repo/cluster-coordinator.ts +145 -19
- package/src/repo/coordinator-repo.ts +652 -505
- package/src/repo/i-under-replication-ledger.ts +89 -0
- package/src/repo/kv-under-replication-ledger.ts +302 -0
- package/src/repo/responsibility.ts +48 -0
- package/src/repo/service.ts +76 -39
- package/src/repo/stuck-reservation.ts +224 -0
- package/src/repo/under-replication-drain.ts +582 -0
- package/src/rn.ts +74 -69
- package/src/storage/block-latch.ts +7 -4
- package/src/storage/block-storage.ts +161 -10
- package/src/storage/i-block-storage.ts +60 -5
- package/src/storage/pending-claim.ts +200 -0
- package/src/storage/raw-store-codec.ts +26 -3
- package/src/storage/restoration-coordinator.ts +212 -211
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/storage-repo.ts +280 -72
- package/src/storage/struct.ts +58 -0
- package/src/testing/mesh-harness.ts +293 -112
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { LruMap, type ActionId, type BlockId } from "@optimystic/db-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What one block's pending-conflict refusals have added up to, for ONE unchanged set of holders.
|
|
5
|
+
*
|
|
6
|
+
* A block is reserved by an unresolved pending action for the span between that action's pend and
|
|
7
|
+
* its commit or cancel, and while the reservation stands every OTHER writer's pend for the block is
|
|
8
|
+
* refused. That is the healthy optimistic-concurrency loss. The unhealthy case has the identical
|
|
9
|
+
* per-refusal shape and differs only in repetition: the same holder refusing DISTINCT later actions
|
|
10
|
+
* without end, because the holder is never going to commit or cancel (see
|
|
11
|
+
* {@link StuckReservationTracker}).
|
|
12
|
+
*
|
|
13
|
+
* Counted per (block, holders) rather than per block: a holder that changes is the healthy cycle —
|
|
14
|
+
* the previous reservation resolved and another writer took the block — so a new holder starts a new
|
|
15
|
+
* count and gets its own chance to speak.
|
|
16
|
+
*/
|
|
17
|
+
interface StuckReservationWatch {
|
|
18
|
+
/** The rival action ids the refusals in this episode named, sorted so the comparison is stable. */
|
|
19
|
+
holders: readonly ActionId[];
|
|
20
|
+
/**
|
|
21
|
+
* Distinct action ids these holders have refused. Distinct ACTIONS, not refusals: one writer
|
|
22
|
+
* retrying is one writer, because a sync reuses a single action id across all of its retry
|
|
23
|
+
* attempts (`Collection.syncInternal` mints the id once and `syncAttempts` reuses it for every
|
|
24
|
+
* attempt of that cycle). Emptied at the
|
|
25
|
+
* moment the episode is reported — the count is in the line, and nothing reads the ids again —
|
|
26
|
+
* so the set is bounded by {@link STUCK_RESERVATION_DISTINCT_ACTIONS}.
|
|
27
|
+
*/
|
|
28
|
+
refused: Set<ActionId>;
|
|
29
|
+
/** True once this episode has been named; suppresses every later refusal against these holders. */
|
|
30
|
+
reported: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* How many DISTINCT later actions one unchanged holder must refuse on a block before the refusals are
|
|
35
|
+
* named as a stuck reservation rather than as an ordinary lost race.
|
|
36
|
+
*
|
|
37
|
+
* **Why a count of distinct actions and not something else.** Elapsed time answers the wrong question
|
|
38
|
+
* — a slow writer is not a stuck one, and a holder legitimately keeps its reservation for as long as
|
|
39
|
+
* its own commit takes. A raw refusal count answers the wrong question too: a single writer retrying
|
|
40
|
+
* a lost race produces a run of refusals under ONE action id (see {@link StuckReservationWatch.refused}).
|
|
41
|
+
* What no healthy holder can produce is an unbounded stream of *different* writers all losing to it,
|
|
42
|
+
* because a healthy holder's reservation lasts one pend-to-commit window.
|
|
43
|
+
*
|
|
44
|
+
* **Why 8.** The bound to clear is how many distinct actions can honestly be refused inside one such
|
|
45
|
+
* window. Measured on the in-process mesh, in the healthy-contention arm of
|
|
46
|
+
* `test/stuck-reservation-named.spec.ts`: a holder that pends, is raced by other writers, and then
|
|
47
|
+
* commits refuses **2** distinct actions per episode — the two rivals — and the count resets on every
|
|
48
|
+
* holder change. `concurrent-diary-append-acknowledgement.spec.ts` races three writers at one diary
|
|
49
|
+
* and cannot exceed that either, for the same reason: at most (writers - 1) rivals can lose to one
|
|
50
|
+
* winner. 8 is four times the measured healthy figure, and it is a floor a genuinely stuck block
|
|
51
|
+
* clears trivially (the field instance refused hundreds).
|
|
52
|
+
*
|
|
53
|
+
* **The bound stated exactly.** It is distinct SYNC CYCLES, not distinct writers: one writer that
|
|
54
|
+
* exhausts a sync's retry budget and is re-driven by its caller mints a fresh id for the next cycle,
|
|
55
|
+
* so it can contribute more than one. That does not widen the window much — a cycle only ends in
|
|
56
|
+
* exhaustion after `DefaultMaxAttempts` (10) attempts of backoff, roughly 21s (see the exhaustion
|
|
57
|
+
* NOTE in `Collection.syncAttempts`), so a lone writer needs a holder to keep the block for upwards
|
|
58
|
+
* of two and a half minutes before it reaches 8 by itself, which is not a healthy holder.
|
|
59
|
+
*
|
|
60
|
+
* **What the margin does NOT cover, stated honestly.** A block with more than 8 distinct writers
|
|
61
|
+
* racing it inside a single pend-to-commit round trip could reach 8 with a perfectly healthy holder.
|
|
62
|
+
* That is a diagnostic false positive on a log line and nothing else — this counter never refuses,
|
|
63
|
+
* expires, or deletes anything (see {@link StuckReservationTracker}) — and the remedy if
|
|
64
|
+
* a deployment ever hits it is to raise this number, not to add a control path. Raising it costs
|
|
65
|
+
* detection latency on low-traffic blocks, which need this many distinct write ATTEMPTS before the
|
|
66
|
+
* condition can be named at all.
|
|
67
|
+
*/
|
|
68
|
+
export const STUCK_RESERVATION_DISTINCT_ACTIONS = 8;
|
|
69
|
+
|
|
70
|
+
/** Whether two sorted holder lists name the same reservation — i.e. whether a refusal continues an
|
|
71
|
+
* existing episode or starts a new one. Both sides come from the same sort, so this is a plain
|
|
72
|
+
* element-wise comparison; a block normally has exactly one holder, since a member's own pend refuses
|
|
73
|
+
* a second one (`ClusterMember.validatePendOperations`). */
|
|
74
|
+
function sameHolders(a: readonly ActionId[], b: readonly ActionId[]): boolean {
|
|
75
|
+
return a.length === b.length && a.every((id, i) => id === b[i]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The stuck-reservation wording: written for an operator reading logs, in the same register as
|
|
80
|
+
* `cohortTooSmallMessage` and `soleHolderMessage` in `coordinator-repo.ts` — what is stuck, what will
|
|
81
|
+
* and will not clear it, and what to do next.
|
|
82
|
+
*
|
|
83
|
+
* The claim is deliberately about the RESERVATION, not about the writer's intent: this node cannot
|
|
84
|
+
* see whether the holding process is alive, only that it has held the block across enough unrelated
|
|
85
|
+
* later actions that no retry is going to win. So the line says what is provable (the block accepts
|
|
86
|
+
* no writes while this record stands, and nothing on the node removes it) and points at the one check
|
|
87
|
+
* that settles the rest.
|
|
88
|
+
*/
|
|
89
|
+
export function stuckReservationMessage(holders: readonly ActionId[], refusedActions: number): string {
|
|
90
|
+
const held = holders.join(', ');
|
|
91
|
+
return `This block is WEDGED BEHIND A PENDING WRITE THAT IS NOT COMPLETING, and retrying will never ` +
|
|
92
|
+
`clear it: action(s) ${held} reserved the block and have now refused ${refusedActions} DISTINCT, ` +
|
|
93
|
+
`unrelated later actions. Each of those refusals on its own looks exactly like an ordinary ` +
|
|
94
|
+
`optimistic-concurrency loss, which is normal and healthy — the repetition is what is not. A ` +
|
|
95
|
+
`healthy rival holds a block only for its own pend-to-commit window and then releases it by ` +
|
|
96
|
+
`committing or cancelling; a reservation that keeps refusing NEW writers is holding the block ` +
|
|
97
|
+
`against every writer on every machine, and each of them loses again identically. EXACTLY TWO ` +
|
|
98
|
+
`THINGS CLEAR IT: a cancel for action(s) ${held} on this block (route it through the cohort so ` +
|
|
99
|
+
`every member drops the record), or that same action's own commit landing. Nothing on the node ` +
|
|
100
|
+
`expires it — there is no sweep for abandoned pending records — so until one of those two happens ` +
|
|
101
|
+
`the block takes NO writes while continuing to serve reads and to look healthy in every other ` +
|
|
102
|
+
`respect. The usual cause is a writer that went away between a failed or half-applied commit and ` +
|
|
103
|
+
`the cancel it owed, so check whether whatever ran ${held} still exists before cancelling on its ` +
|
|
104
|
+
`behalf. This line is a diagnosis and nothing more: this node does not expire, refuse, or delete ` +
|
|
105
|
+
`the record on the strength of it.`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* One episode that just crossed the threshold — what the caller logs, verbatim, under its own tag.
|
|
110
|
+
* The ids an operator needs to grep for and to cancel are kept as data beside the prose so a log
|
|
111
|
+
* search finds the block and the action without parsing English.
|
|
112
|
+
*/
|
|
113
|
+
export interface StuckReservationEpisode {
|
|
114
|
+
blockId: BlockId;
|
|
115
|
+
holdingActionIds: readonly ActionId[];
|
|
116
|
+
distinctRefusedActions: number;
|
|
117
|
+
message: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Count pending-conflict refusals against the holder(s) of each block they name, and say ONCE — in
|
|
122
|
+
* words, at the moment it becomes provable — when a block is wedged behind a reservation that is not
|
|
123
|
+
* going to clear.
|
|
124
|
+
*
|
|
125
|
+
* **Why this needs saying at all.** Every individual refusal is indistinguishable from an ordinary
|
|
126
|
+
* lost race, which is a normal and healthy event, so the logs of a permanently wedged block read
|
|
127
|
+
* exactly like the logs of a busy one. Finding the difference otherwise means noticing that the SAME
|
|
128
|
+
* rival action id keeps appearing across unrelated writers for as long as the process lives — a
|
|
129
|
+
* pattern nothing points at, and one that cost a downstream project several tickets and weeks to
|
|
130
|
+
* re-derive from raw traces. The node has the fact in hand at every refusal; this makes it sayable.
|
|
131
|
+
*
|
|
132
|
+
* **The signal, and the two things that are NOT the signal.** The discriminator is repetition
|
|
133
|
+
* against an unchanged holder — see {@link STUCK_RESERVATION_DISTINCT_ACTIONS} for why distinct
|
|
134
|
+
* refused actions is the right counter and for the measured threshold. Two cheaper-looking tests
|
|
135
|
+
* were tried and do not work: the members' in-memory reservation table
|
|
136
|
+
* (`ClusterMember.activeTransactions`) clears the moment a rival's pend reaches consensus, so a
|
|
137
|
+
* perfectly healthy rival inside its pend-to-commit window is absent from it too and absence there
|
|
138
|
+
* says nothing; and "the block already passed this pending record's revision" catches a different
|
|
139
|
+
* orphan class entirely — in the verified instance the wedged block sat at revision 1 while the
|
|
140
|
+
* orphaned record was for revision 2, still nominally promotable. (That second class is now handled
|
|
141
|
+
* at its own site — a record the incoming writer has built on no longer refuses at all, see
|
|
142
|
+
* `isReservationAgainst` — so what reaches this counter is a holder whose change the block has not
|
|
143
|
+
* taken: one whose slot every later writer still wants, or one past the base every later writer
|
|
144
|
+
* declares, such as an abandoned action's record on a block no later write touched.)
|
|
145
|
+
*
|
|
146
|
+
* **Never a control path.** This classifies and reports; it never refuses, expires, or deletes
|
|
147
|
+
* anything. Deciding when a durable pending record may be removed is precisely the hard problem
|
|
148
|
+
* backlog `debt-unpromotable-pending-records-need-a-sweep` exists for — deleting a live reservation
|
|
149
|
+
* is worse than the leak — and a counter accurate enough for a log line is not evidence enough to
|
|
150
|
+
* destroy state.
|
|
151
|
+
*
|
|
152
|
+
* **Two instances, two vantage points.** The coordinator keeps one (`CoordinatorRepo.noteStuckReservation`),
|
|
153
|
+
* fed by cohort-wide `held`-answered refusals whose holder its OWN storage corroborates, and every
|
|
154
|
+
* member keeps one (`ClusterMember.validatePendOperations`), fed by its own `held` votes — so a
|
|
155
|
+
* reservation only remote members hold is named by those members, where the record actually lives,
|
|
156
|
+
* rather than going unnamed at a coordinator that cannot see it.
|
|
157
|
+
*
|
|
158
|
+
* LRU-bounded: an eviction under more than `capacity` conflicted blocks loses an episode's say-once
|
|
159
|
+
* flag, so the line can repeat once for that block — bounded duplication, far cheaper than the
|
|
160
|
+
* unbounded silence it replaces.
|
|
161
|
+
*/
|
|
162
|
+
export class StuckReservationTracker {
|
|
163
|
+
private readonly watches: LruMap<string, StuckReservationWatch>;
|
|
164
|
+
|
|
165
|
+
constructor(capacity = 1000) {
|
|
166
|
+
this.watches = new LruMap<string, StuckReservationWatch>(capacity);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Count one refusal of `refusedActionId` against the rival holder(s) of each block in `rivalsByBlock`.
|
|
171
|
+
* Returns the highest distinct-refusal count any of these blocks has now reached — for a
|
|
172
|
+
* classification line to carry, saturating at the threshold once an episode has been reported,
|
|
173
|
+
* since the ids are dropped at that point — and every episode that crossed the threshold on this
|
|
174
|
+
* refusal, which the caller must log.
|
|
175
|
+
*/
|
|
176
|
+
note(rivalsByBlock: ReadonlyMap<BlockId, readonly ActionId[]>, refusedActionId: ActionId): { highest: number; named: StuckReservationEpisode[] } {
|
|
177
|
+
let highest = 0;
|
|
178
|
+
const named: StuckReservationEpisode[] = [];
|
|
179
|
+
for (const [blockId, rivals] of rivalsByBlock) {
|
|
180
|
+
const holders = [...new Set(rivals)].sort();
|
|
181
|
+
const prior = this.watches.get(blockId);
|
|
182
|
+
// A different holder set is a DIFFERENT episode — the block changed hands, which is the
|
|
183
|
+
// healthy cycle — so the count starts over and the new holder gets its own chance to speak.
|
|
184
|
+
const watch: StuckReservationWatch = prior !== undefined && sameHolders(prior.holders, holders)
|
|
185
|
+
? prior
|
|
186
|
+
: { holders, refused: new Set<ActionId>(), reported: false };
|
|
187
|
+
if (watch !== prior) this.watches.set(blockId, watch);
|
|
188
|
+
if (watch.reported) {
|
|
189
|
+
highest = Math.max(highest, STUCK_RESERVATION_DISTINCT_ACTIONS);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
watch.refused.add(refusedActionId);
|
|
193
|
+
highest = Math.max(highest, watch.refused.size);
|
|
194
|
+
if (watch.refused.size < STUCK_RESERVATION_DISTINCT_ACTIONS) continue;
|
|
195
|
+
named.push({
|
|
196
|
+
blockId,
|
|
197
|
+
holdingActionIds: holders,
|
|
198
|
+
distinctRefusedActions: watch.refused.size,
|
|
199
|
+
message: stuckReservationMessage(holders, watch.refused.size)
|
|
200
|
+
});
|
|
201
|
+
watch.reported = true;
|
|
202
|
+
// Said once per episode: from here the flag alone suppresses, and the ids have done their
|
|
203
|
+
// work (their count is in the line above), so drop them rather than growing a set for the
|
|
204
|
+
// unbounded remainder of a permanent condition.
|
|
205
|
+
watch.refused.clear();
|
|
206
|
+
}
|
|
207
|
+
return { highest, named };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Forget any episode recorded for these blocks, optionally only when `holderActionId` is one of
|
|
212
|
+
* the actions that episode named. Called from the events the message names as the cures — a write
|
|
213
|
+
* the block accepted, a cancel for the holding action — and it is LRU hygiene rather than
|
|
214
|
+
* behaviour: a later wedge is named by a DIFFERENT holder, which {@link note}'s holder comparison
|
|
215
|
+
* already treats as a new episode whether the old entry is still there or not. Kept because a
|
|
216
|
+
* settled episode holding an LRU slot can only evict a live one.
|
|
217
|
+
*/
|
|
218
|
+
forget(blockIds: readonly BlockId[], holderActionId?: ActionId): void {
|
|
219
|
+
for (const blockId of blockIds) {
|
|
220
|
+
if (holderActionId !== undefined && !this.watches.peek(blockId)?.holders.includes(holderActionId)) continue;
|
|
221
|
+
this.watches.delete(blockId);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|