@optimystic/db-p2p 1.0.0-beta.2 → 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 +172 -17
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +553 -149
- 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 +241 -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 +19 -7
- 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 +119 -5
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +181 -29
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +259 -76
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +716 -328
- 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 +137 -5
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +273 -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 +92 -11
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +246 -103
- 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 +3115 -2671
- 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 -1253
- 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 +20 -7
- 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 +1296 -1113
- package/src/repo/coordinator-repo.ts +3087 -2687
- 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 +311 -73
- package/src/storage/struct.ts +58 -0
- package/src/testing/mesh-harness.ts +329 -115
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { Libp2p, Startable } from '@libp2p/interface';
|
|
2
|
+
import type { BlockDurabilityReachedEvent, BlockId, IKeyNetwork } from '@optimystic/db-core';
|
|
3
|
+
import type { PartitionDetector } from '../cluster/partition-detector.js';
|
|
4
|
+
import type { PushBlockOutcome } from '../cluster/block-transfer-service.js';
|
|
5
|
+
import type { IUnderReplicationLedger } from './i-under-replication-ledger.js';
|
|
6
|
+
export interface UnderReplicationDrainConfig {
|
|
7
|
+
/** Debounce after a connection opens or a peer is identified, before a pass runs. Default: 5000 */
|
|
8
|
+
debounceMs: number;
|
|
9
|
+
/**
|
|
10
|
+
* Minimum interval between passes (ms). A trigger inside the window is DEFERRED to the window's
|
|
11
|
+
* end, never dropped, so a flapping peer costs at most one pass per interval and a peer that
|
|
12
|
+
* arrives mid-window is still drained to. Default: 15000
|
|
13
|
+
*/
|
|
14
|
+
minIntervalMs: number;
|
|
15
|
+
/**
|
|
16
|
+
* Self-arming re-check timer while any ledger entry is outstanding (ms). Passes otherwise fire
|
|
17
|
+
* only on connection events, so a failed push on a then-quiet network — or a shortfall recorded
|
|
18
|
+
* while its missing member was already connected — would wait for the next arrival. Cheap while
|
|
19
|
+
* nothing can be pushed: a tick with no pushable peer connected costs one connection scan and
|
|
20
|
+
* one in-memory count. `0` disables. Default: 30000
|
|
21
|
+
*/
|
|
22
|
+
recheckIntervalMs: number;
|
|
23
|
+
/**
|
|
24
|
+
* Entries examined per pass. Passes rotate through the ledger, oldest-recorded first, so a large
|
|
25
|
+
* backlog drains one budget per pass and no entry starves behind a stuck one. A deferred entry is
|
|
26
|
+
* not touched at all; it is re-examined on a later pass. Default: 64
|
|
27
|
+
*/
|
|
28
|
+
blockBudget: number;
|
|
29
|
+
/**
|
|
30
|
+
* Consecutive rounds a reachable peer may fail to confirm one block before it is ABANDONED for
|
|
31
|
+
* that block — no longer pushed to — until it reconnects, which retries it from scratch. The
|
|
32
|
+
* entry itself stays in the ledger, visible, whatever happens here. Default: 5
|
|
33
|
+
*/
|
|
34
|
+
maxAttempts: number;
|
|
35
|
+
}
|
|
36
|
+
export declare const DEFAULT_UNDER_REPLICATION_DRAIN_CONFIG: UnderReplicationDrainConfig;
|
|
37
|
+
/**
|
|
38
|
+
* The one method the drain may call on the node's storage repo: emit a full-replication event.
|
|
39
|
+
* Handed in as a function, exactly as the cluster side receives a `CommitCertificateSink`, so the
|
|
40
|
+
* drain has no way to reach anything else on the repo.
|
|
41
|
+
*/
|
|
42
|
+
export type BlockDurabilitySink = (event: BlockDurabilityReachedEvent) => void;
|
|
43
|
+
/**
|
|
44
|
+
* The push primitive, already bound to this node's own store, dialer and protocol prefix — the
|
|
45
|
+
* node binds `pushBlockToPeers` (`cluster/block-transfer-service.ts`); a unit test binds a double.
|
|
46
|
+
*/
|
|
47
|
+
export type BoundBlockPusher = (blockId: BlockId, peerIds: readonly string[]) => Promise<PushBlockOutcome>;
|
|
48
|
+
/** The slice of libp2p the drain uses: identity, the connection events, and who is connected. */
|
|
49
|
+
export type DrainLibp2p = Pick<Libp2p, 'peerId' | 'addEventListener' | 'removeEventListener' | 'getConnections'> & {
|
|
50
|
+
peerStore: Pick<Libp2p['peerStore'], 'get'>;
|
|
51
|
+
};
|
|
52
|
+
export interface UnderReplicationDrainDeps {
|
|
53
|
+
libp2p: DrainLibp2p;
|
|
54
|
+
ledger: IUnderReplicationLedger;
|
|
55
|
+
/** Re-resolves a block's cohort for an entry that could not name its missing members. */
|
|
56
|
+
keyNetwork: Pick<IKeyNetwork, 'findCluster'>;
|
|
57
|
+
partitionDetector: Pick<PartitionDetector, 'detectPartition'>;
|
|
58
|
+
pushBlock: BoundBlockPusher;
|
|
59
|
+
emit: BlockDurabilitySink;
|
|
60
|
+
/** The `/optimystic/<networkName>` prefix; a connected peer counts as pushable only once identify
|
|
61
|
+
* reports it serving this network's block-transfer protocol. Default: none (any connected peer). */
|
|
62
|
+
protocolPrefix?: string;
|
|
63
|
+
}
|
|
64
|
+
/** What woke a pass. */
|
|
65
|
+
export type DrainTrigger = 'start' | 'connection' | 'identify' | 'recheck' | 'manual';
|
|
66
|
+
/** What one pass did. Counts are per ledger ENTRY (one per block). */
|
|
67
|
+
export interface DrainPassResult {
|
|
68
|
+
trigger: DrainTrigger;
|
|
69
|
+
/** Set when the pass did no work at all, and why. */
|
|
70
|
+
skipped?: 'partition' | 'no-pushable-peer';
|
|
71
|
+
/** Entries in the ledger when the pass began. */
|
|
72
|
+
entries: number;
|
|
73
|
+
/** Entries examined this pass, up to the block budget. */
|
|
74
|
+
examined: number;
|
|
75
|
+
/** Entries the budget left for a later pass. */
|
|
76
|
+
deferred: number;
|
|
77
|
+
/** Entries whose cohort resolves to this node alone: nothing owed, nothing counted. */
|
|
78
|
+
solo: number;
|
|
79
|
+
/** Entries owing a copy to nobody currently pushable (not connected, not yet identified, or
|
|
80
|
+
* abandoned) — or whose cohort could not be resolved this pass. */
|
|
81
|
+
waiting: number;
|
|
82
|
+
/** Entries pushed to at least one peer. */
|
|
83
|
+
pushed: number;
|
|
84
|
+
/** Entries whose last shortfall cleared — one full-replication event fired for each. */
|
|
85
|
+
cleared: number;
|
|
86
|
+
/** Entries deleted because the block is gone from local storage. */
|
|
87
|
+
dropped: number;
|
|
88
|
+
/** Peers newly abandoned for a block this pass. */
|
|
89
|
+
abandoned: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Sends the copies the under-replication ledger says this node still owes, and says when a block
|
|
93
|
+
* finally has all of them.
|
|
94
|
+
*
|
|
95
|
+
* `CoordinatorRepo.commit` writes an entry per block it acknowledged below full replication, naming
|
|
96
|
+
* the cohort members that had not confirmed it (or an EMPTY set when no cohort could be named — a
|
|
97
|
+
* `local` or `unrouted` write). This service is the other half: on start, whenever a peer connects or
|
|
98
|
+
* is identified, and on a re-check timer while anything is outstanding, it walks the ledger and pushes
|
|
99
|
+
* each block to the owed peers that are reachable right now. A peer that confirms is removed from the
|
|
100
|
+
* entry; when the entry empties it is deleted and ONE {@link BlockDurabilityReachedEvent} fires through
|
|
101
|
+
* the sink — after the deletion, never before.
|
|
102
|
+
*
|
|
103
|
+
* What it deliberately does not do:
|
|
104
|
+
* - **Dial the world.** A copy goes only to a peer that is connected and identified as serving this
|
|
105
|
+
* network. An entry whose owed peers are all away simply waits; the next arrival wakes the drain.
|
|
106
|
+
* - **Re-resolve a NAMED peer.** A `majority` entry names members that were in the cohort when the
|
|
107
|
+
* write was acknowledged; a copy there is never harmful even if the cohort has since moved on.
|
|
108
|
+
* - **Count an attempt against a node that is alone.** An entry with no named members whose cohort
|
|
109
|
+
* still resolves to this node alone is left exactly as it is: nobody is owed anything yet.
|
|
110
|
+
* - **Delete an entry to tidy up.** Only a confirmed copy (or the block leaving local storage) removes
|
|
111
|
+
* one. A peer that never returns leaves its entry in place; the ledger's size cap is the backstop.
|
|
112
|
+
* - **Coordinate with the rebalance growth arm or spread-on-churn.** All three may push the same
|
|
113
|
+
* block to the same peer; the receiver is idempotent, so the overlap costs duplicate work only.
|
|
114
|
+
*
|
|
115
|
+
* NOTE: a pass that finds work SCANS the ledger (`IUnderReplicationLedger.list`, one store read per
|
|
116
|
+
* entry) and examines up to `blockBudget` entries, oldest first. Bounded by the node's owned-block
|
|
117
|
+
* count today. If a node ever holds so many under-replicated blocks that the scan shows up — a long
|
|
118
|
+
* solo run with a persistent store — index the ledger by missing peer instead of scanning, and
|
|
119
|
+
* consider draining newest-first so a host's most recent pending writes settle before old ones.
|
|
120
|
+
*/
|
|
121
|
+
export declare class UnderReplicationDrain implements Startable {
|
|
122
|
+
private readonly deps;
|
|
123
|
+
private running;
|
|
124
|
+
private readonly config;
|
|
125
|
+
private readonly blockTransferProtocol;
|
|
126
|
+
private debounceTimer;
|
|
127
|
+
private timer;
|
|
128
|
+
private timerDueAt;
|
|
129
|
+
private lastPassAt;
|
|
130
|
+
private passInFlight;
|
|
131
|
+
private pendingTrigger;
|
|
132
|
+
/** Rotation cursor into the oldest-first entry list, so a backlog wider than the budget is
|
|
133
|
+
* swept in full over successive passes rather than the same oldest slice every time. */
|
|
134
|
+
private cursor;
|
|
135
|
+
/** Consecutive unconfirmed rounds per (block, peer) — the give-up counter. */
|
|
136
|
+
private readonly failures;
|
|
137
|
+
/** Peers given up on per block; excluded from pushes until they reconnect. */
|
|
138
|
+
private readonly abandoned;
|
|
139
|
+
private readonly onConnectionOpen;
|
|
140
|
+
private readonly onPeerIdentify;
|
|
141
|
+
constructor(deps: UnderReplicationDrainDeps, config?: Partial<UnderReplicationDrainConfig>);
|
|
142
|
+
start(): Promise<void>;
|
|
143
|
+
stop(): Promise<void>;
|
|
144
|
+
/** Run one pass now, bypassing the debounce and the interval throttle. Waits for an in-flight pass first. */
|
|
145
|
+
checkNow(): Promise<DrainPassResult>;
|
|
146
|
+
/** Observability: given-up (block, peer) pairs, whether a timer is armed, and the last pass. */
|
|
147
|
+
getDiagnostics(): {
|
|
148
|
+
abandonedPairs: number;
|
|
149
|
+
timerArmed: boolean;
|
|
150
|
+
lastPassAt: number;
|
|
151
|
+
};
|
|
152
|
+
private handleConnectionOpen;
|
|
153
|
+
private schedule;
|
|
154
|
+
/** Run now, or defer to the end of the minimum interval — a trigger is never dropped. */
|
|
155
|
+
private maybeRun;
|
|
156
|
+
/** Arm the one timer, pulling an already-armed one EARLIER when `delayMs` asks for sooner. */
|
|
157
|
+
private armTimer;
|
|
158
|
+
private clearTimer;
|
|
159
|
+
private clearDebounce;
|
|
160
|
+
/** Arm the re-check while anything is outstanding; otherwise let the timer lapse. */
|
|
161
|
+
private rearmRecheck;
|
|
162
|
+
/** One pass, never overlapping another: a trigger that lands mid-pass runs one more afterwards. */
|
|
163
|
+
private run;
|
|
164
|
+
private performPass;
|
|
165
|
+
/** The next `blockBudget` entries from the rotation cursor, wrapping; the cursor advances past them. */
|
|
166
|
+
private takeBudget;
|
|
167
|
+
private drainEntry;
|
|
168
|
+
/**
|
|
169
|
+
* Take the confirmed peers off the entry. An entry that could not name its members is named now,
|
|
170
|
+
* at its own revision, with the cohort resolved for this push — so `satisfy` has a set to shrink.
|
|
171
|
+
* Both writes carry the revision guard: a newer shortfall recorded while the push was in flight
|
|
172
|
+
* is left alone (`name` touches only its own revision; `satisfy` keeps a higher one), and neither
|
|
173
|
+
* recreates an entry a concurrent `settle` removed mid-push.
|
|
174
|
+
*
|
|
175
|
+
* NOTE: an entry settled mid-push by a `full` commit at a newer revision is gone by the time
|
|
176
|
+
* `satisfy` runs, which reports that as "gone", so the event fires here too. It is true — the block
|
|
177
|
+
* IS fully replicated — but under the older `rev`/`actionId`, and a listener may also have heard
|
|
178
|
+
* `full` on that commit's own result. If a host ever needs the two told apart, have `satisfy`
|
|
179
|
+
* distinguish "deleted now" from "absent".
|
|
180
|
+
*
|
|
181
|
+
* @returns whether the entry is gone — its last missing member confirmed.
|
|
182
|
+
*/
|
|
183
|
+
private recordConfirmed;
|
|
184
|
+
/** Log each refusal so "cannot place" reads differently from "cannot reach", and count the round. */
|
|
185
|
+
private noteRefusals;
|
|
186
|
+
/** Count one unconfirmed round for a peer; abandon it at the bound. @returns whether it was abandoned now. */
|
|
187
|
+
private countFailure;
|
|
188
|
+
/**
|
|
189
|
+
* Who the entry still owes. Named members are taken as recorded. An unknown set re-resolves the
|
|
190
|
+
* cohort NOW; a cohort of this node alone is `solo` — not a failure, not an attempt.
|
|
191
|
+
*/
|
|
192
|
+
private owedPeers;
|
|
193
|
+
/**
|
|
194
|
+
* Connected peers identified as serving this network's block-transfer protocol — the only peers a
|
|
195
|
+
* push can land on. Identify fills the peer store's protocol list, so a peer that just connected
|
|
196
|
+
* is not yet pushable; the `peer:identify` trigger brings it in. With no prefix configured every
|
|
197
|
+
* connected peer counts.
|
|
198
|
+
*/
|
|
199
|
+
private pushablePeers;
|
|
200
|
+
private servesBlockTransfer;
|
|
201
|
+
private forgetPeer;
|
|
202
|
+
/** An abandoned peer that has gone is forgotten: when it returns it is retried from scratch. */
|
|
203
|
+
private forgetDisconnected;
|
|
204
|
+
private forgetBlock;
|
|
205
|
+
/** Drop state for blocks the ledger no longer lists (settled by a `full` commit, evicted, dropped). */
|
|
206
|
+
private pruneState;
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=under-replication-drain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"under-replication-drain.d.ts","sourceRoot":"","sources":["../../../src/repo/under-replication-drain.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { routingKeyForBlock } from '@optimystic/db-core';
|
|
2
|
+
import { buildBlockTransferProtocol } from '../cluster/block-transfer-service.js';
|
|
3
|
+
import { createLogger } from '../logger.js';
|
|
4
|
+
const log = createLogger('under-replication-drain');
|
|
5
|
+
export const DEFAULT_UNDER_REPLICATION_DRAIN_CONFIG = {
|
|
6
|
+
debounceMs: 5000,
|
|
7
|
+
minIntervalMs: 15_000,
|
|
8
|
+
recheckIntervalMs: 30_000,
|
|
9
|
+
blockBudget: 64,
|
|
10
|
+
maxAttempts: 5
|
|
11
|
+
};
|
|
12
|
+
const zeroCounts = (trigger) => ({
|
|
13
|
+
trigger, entries: 0, examined: 0, deferred: 0, solo: 0, waiting: 0, pushed: 0, cleared: 0, dropped: 0, abandoned: 0
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Sends the copies the under-replication ledger says this node still owes, and says when a block
|
|
17
|
+
* finally has all of them.
|
|
18
|
+
*
|
|
19
|
+
* `CoordinatorRepo.commit` writes an entry per block it acknowledged below full replication, naming
|
|
20
|
+
* the cohort members that had not confirmed it (or an EMPTY set when no cohort could be named — a
|
|
21
|
+
* `local` or `unrouted` write). This service is the other half: on start, whenever a peer connects or
|
|
22
|
+
* is identified, and on a re-check timer while anything is outstanding, it walks the ledger and pushes
|
|
23
|
+
* each block to the owed peers that are reachable right now. A peer that confirms is removed from the
|
|
24
|
+
* entry; when the entry empties it is deleted and ONE {@link BlockDurabilityReachedEvent} fires through
|
|
25
|
+
* the sink — after the deletion, never before.
|
|
26
|
+
*
|
|
27
|
+
* What it deliberately does not do:
|
|
28
|
+
* - **Dial the world.** A copy goes only to a peer that is connected and identified as serving this
|
|
29
|
+
* network. An entry whose owed peers are all away simply waits; the next arrival wakes the drain.
|
|
30
|
+
* - **Re-resolve a NAMED peer.** A `majority` entry names members that were in the cohort when the
|
|
31
|
+
* write was acknowledged; a copy there is never harmful even if the cohort has since moved on.
|
|
32
|
+
* - **Count an attempt against a node that is alone.** An entry with no named members whose cohort
|
|
33
|
+
* still resolves to this node alone is left exactly as it is: nobody is owed anything yet.
|
|
34
|
+
* - **Delete an entry to tidy up.** Only a confirmed copy (or the block leaving local storage) removes
|
|
35
|
+
* one. A peer that never returns leaves its entry in place; the ledger's size cap is the backstop.
|
|
36
|
+
* - **Coordinate with the rebalance growth arm or spread-on-churn.** All three may push the same
|
|
37
|
+
* block to the same peer; the receiver is idempotent, so the overlap costs duplicate work only.
|
|
38
|
+
*
|
|
39
|
+
* NOTE: a pass that finds work SCANS the ledger (`IUnderReplicationLedger.list`, one store read per
|
|
40
|
+
* entry) and examines up to `blockBudget` entries, oldest first. Bounded by the node's owned-block
|
|
41
|
+
* count today. If a node ever holds so many under-replicated blocks that the scan shows up — a long
|
|
42
|
+
* solo run with a persistent store — index the ledger by missing peer instead of scanning, and
|
|
43
|
+
* consider draining newest-first so a host's most recent pending writes settle before old ones.
|
|
44
|
+
*/
|
|
45
|
+
export class UnderReplicationDrain {
|
|
46
|
+
deps;
|
|
47
|
+
running = false;
|
|
48
|
+
config;
|
|
49
|
+
blockTransferProtocol;
|
|
50
|
+
debounceTimer = null;
|
|
51
|
+
timer = null;
|
|
52
|
+
timerDueAt = 0;
|
|
53
|
+
lastPassAt = 0;
|
|
54
|
+
passInFlight = null;
|
|
55
|
+
pendingTrigger = null;
|
|
56
|
+
/** Rotation cursor into the oldest-first entry list, so a backlog wider than the budget is
|
|
57
|
+
* swept in full over successive passes rather than the same oldest slice every time. */
|
|
58
|
+
cursor = 0;
|
|
59
|
+
/** Consecutive unconfirmed rounds per (block, peer) — the give-up counter. */
|
|
60
|
+
failures = new Map();
|
|
61
|
+
/** Peers given up on per block; excluded from pushes until they reconnect. */
|
|
62
|
+
abandoned = new Map();
|
|
63
|
+
onConnectionOpen;
|
|
64
|
+
onPeerIdentify;
|
|
65
|
+
constructor(deps, config = {}) {
|
|
66
|
+
this.deps = deps;
|
|
67
|
+
this.config = { ...DEFAULT_UNDER_REPLICATION_DRAIN_CONFIG, ...config };
|
|
68
|
+
this.blockTransferProtocol = buildBlockTransferProtocol(deps.protocolPrefix ?? '');
|
|
69
|
+
this.onConnectionOpen = (event) => this.handleConnectionOpen(event);
|
|
70
|
+
this.onPeerIdentify = () => this.schedule('identify');
|
|
71
|
+
}
|
|
72
|
+
// ── Startable ────────────────────────────────────────────────────
|
|
73
|
+
async start() {
|
|
74
|
+
if (this.running)
|
|
75
|
+
return;
|
|
76
|
+
this.running = true;
|
|
77
|
+
this.deps.libp2p.addEventListener('connection:open', this.onConnectionOpen);
|
|
78
|
+
this.deps.libp2p.addEventListener('peer:identify', this.onPeerIdentify);
|
|
79
|
+
log('started');
|
|
80
|
+
// The one case no in-memory mechanism ever covered: entries the ledger carried across a
|
|
81
|
+
// restart. Off the start path so a slow scan never blocks the node coming up.
|
|
82
|
+
void this.run('start').catch(err => { log('start pass error: %o', err); });
|
|
83
|
+
}
|
|
84
|
+
async stop() {
|
|
85
|
+
if (!this.running)
|
|
86
|
+
return;
|
|
87
|
+
this.running = false;
|
|
88
|
+
this.deps.libp2p.removeEventListener('connection:open', this.onConnectionOpen);
|
|
89
|
+
this.deps.libp2p.removeEventListener('peer:identify', this.onPeerIdentify);
|
|
90
|
+
this.clearDebounce();
|
|
91
|
+
this.clearTimer();
|
|
92
|
+
this.pendingTrigger = null;
|
|
93
|
+
// An in-flight pass checks `running` between entries and bails; it is not awaited here so a
|
|
94
|
+
// stop never waits out a push deadline.
|
|
95
|
+
log('stopped');
|
|
96
|
+
}
|
|
97
|
+
// ── Public API ───────────────────────────────────────────────────
|
|
98
|
+
/** Run one pass now, bypassing the debounce and the interval throttle. Waits for an in-flight pass first. */
|
|
99
|
+
async checkNow() {
|
|
100
|
+
if (this.passInFlight)
|
|
101
|
+
await this.passInFlight.catch(() => undefined);
|
|
102
|
+
return this.run('manual');
|
|
103
|
+
}
|
|
104
|
+
/** Observability: given-up (block, peer) pairs, whether a timer is armed, and the last pass. */
|
|
105
|
+
getDiagnostics() {
|
|
106
|
+
let abandonedPairs = 0;
|
|
107
|
+
for (const peers of this.abandoned.values())
|
|
108
|
+
abandonedPairs += peers.size;
|
|
109
|
+
return { abandonedPairs, timerArmed: this.timer !== null, lastPassAt: this.lastPassAt };
|
|
110
|
+
}
|
|
111
|
+
// ── Scheduling ───────────────────────────────────────────────────
|
|
112
|
+
handleConnectionOpen(event) {
|
|
113
|
+
if (!this.running)
|
|
114
|
+
return;
|
|
115
|
+
// A peer that comes back is retried from scratch: its abandonment and give-up counts go.
|
|
116
|
+
// Only a FRESH connection counts — libp2p opens parallel connections to one peer, and a second
|
|
117
|
+
// one must not wipe the record of a peer that never went away.
|
|
118
|
+
// NOTE: "fresh" is read as "this is the peer's only connection", which relies on libp2p's
|
|
119
|
+
// connection manager having registered the new connection before this listener runs. True on
|
|
120
|
+
// libp2p 3.x: the manager subscribes to `connection:open` at its own start (before any user
|
|
121
|
+
// listener) and stores the connection synchronously, before its first await. If a libp2p
|
|
122
|
+
// upgrade ever defers that store, every parallel connection would look fresh and an abandoned
|
|
123
|
+
// peer would be retried on each one — cheap, not wrong — and the check would want the
|
|
124
|
+
// `peer:connect` event instead, which fires only for a peer's first connection.
|
|
125
|
+
const remotePeer = event?.detail?.remotePeer;
|
|
126
|
+
if (remotePeer !== undefined && this.deps.libp2p.getConnections(remotePeer).length <= 1) {
|
|
127
|
+
this.forgetPeer(remotePeer.toString());
|
|
128
|
+
}
|
|
129
|
+
this.schedule('connection');
|
|
130
|
+
}
|
|
131
|
+
schedule(trigger) {
|
|
132
|
+
if (!this.running)
|
|
133
|
+
return;
|
|
134
|
+
this.clearDebounce();
|
|
135
|
+
this.debounceTimer = setTimeout(() => {
|
|
136
|
+
this.debounceTimer = null;
|
|
137
|
+
this.maybeRun(trigger);
|
|
138
|
+
}, this.config.debounceMs);
|
|
139
|
+
unref(this.debounceTimer);
|
|
140
|
+
}
|
|
141
|
+
/** Run now, or defer to the end of the minimum interval — a trigger is never dropped. */
|
|
142
|
+
maybeRun(trigger) {
|
|
143
|
+
if (!this.running)
|
|
144
|
+
return;
|
|
145
|
+
const wait = this.lastPassAt + this.config.minIntervalMs - Date.now();
|
|
146
|
+
if (wait > 0) {
|
|
147
|
+
log('throttled trigger=%s wait=%dms', trigger, wait);
|
|
148
|
+
this.armTimer(wait, trigger);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
void this.run(trigger).catch(err => { log('pass error trigger=%s: %o', trigger, err); });
|
|
152
|
+
}
|
|
153
|
+
/** Arm the one timer, pulling an already-armed one EARLIER when `delayMs` asks for sooner. */
|
|
154
|
+
armTimer(delayMs, trigger) {
|
|
155
|
+
if (!this.running)
|
|
156
|
+
return;
|
|
157
|
+
const dueAt = Date.now() + delayMs;
|
|
158
|
+
if (this.timer !== null) {
|
|
159
|
+
if (this.timerDueAt <= dueAt)
|
|
160
|
+
return;
|
|
161
|
+
this.clearTimer();
|
|
162
|
+
}
|
|
163
|
+
this.timerDueAt = dueAt;
|
|
164
|
+
this.timer = setTimeout(() => {
|
|
165
|
+
this.timer = null;
|
|
166
|
+
this.maybeRun(trigger);
|
|
167
|
+
}, delayMs);
|
|
168
|
+
unref(this.timer);
|
|
169
|
+
}
|
|
170
|
+
clearTimer() {
|
|
171
|
+
if (this.timer !== null) {
|
|
172
|
+
clearTimeout(this.timer);
|
|
173
|
+
this.timer = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
clearDebounce() {
|
|
177
|
+
if (this.debounceTimer !== null) {
|
|
178
|
+
clearTimeout(this.debounceTimer);
|
|
179
|
+
this.debounceTimer = null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Arm the re-check while anything is outstanding; otherwise let the timer lapse. */
|
|
183
|
+
async rearmRecheck() {
|
|
184
|
+
if (!this.running || this.config.recheckIntervalMs <= 0)
|
|
185
|
+
return;
|
|
186
|
+
const outstanding = await this.deps.ledger.size();
|
|
187
|
+
if (outstanding > 0)
|
|
188
|
+
this.armTimer(this.config.recheckIntervalMs, 'recheck');
|
|
189
|
+
}
|
|
190
|
+
// ── The pass ─────────────────────────────────────────────────────
|
|
191
|
+
/** One pass, never overlapping another: a trigger that lands mid-pass runs one more afterwards. */
|
|
192
|
+
run(trigger) {
|
|
193
|
+
if (this.passInFlight) {
|
|
194
|
+
this.pendingTrigger = trigger;
|
|
195
|
+
return this.passInFlight;
|
|
196
|
+
}
|
|
197
|
+
const pass = this.performPass(trigger).finally(() => {
|
|
198
|
+
this.passInFlight = null;
|
|
199
|
+
const pending = this.pendingTrigger;
|
|
200
|
+
this.pendingTrigger = null;
|
|
201
|
+
if (pending !== null)
|
|
202
|
+
this.maybeRun(pending);
|
|
203
|
+
});
|
|
204
|
+
this.passInFlight = pass;
|
|
205
|
+
return pass;
|
|
206
|
+
}
|
|
207
|
+
async performPass(trigger) {
|
|
208
|
+
const counts = zeroCounts(trigger);
|
|
209
|
+
// A pass that scanned counts for the throttle however it ends — a ledger fault mid-pass must
|
|
210
|
+
// not make the next trigger an unthrottled retry. A skipped pass is free and does not count.
|
|
211
|
+
let scanned = false;
|
|
212
|
+
try {
|
|
213
|
+
if (this.deps.partitionDetector.detectPartition()) {
|
|
214
|
+
log('pass:skip trigger=%s reason=partition', trigger);
|
|
215
|
+
return { ...counts, skipped: 'partition' };
|
|
216
|
+
}
|
|
217
|
+
const pushable = await this.pushablePeers();
|
|
218
|
+
this.forgetDisconnected(pushable);
|
|
219
|
+
if (pushable.size === 0) {
|
|
220
|
+
return { ...counts, skipped: 'no-pushable-peer' };
|
|
221
|
+
}
|
|
222
|
+
const entries = await this.deps.ledger.list();
|
|
223
|
+
scanned = true;
|
|
224
|
+
this.pruneState(entries);
|
|
225
|
+
counts.entries = entries.length;
|
|
226
|
+
const examined = this.takeBudget(entries);
|
|
227
|
+
counts.examined = examined.length;
|
|
228
|
+
counts.deferred = entries.length - examined.length;
|
|
229
|
+
for (const entry of examined) {
|
|
230
|
+
if (!this.running)
|
|
231
|
+
break;
|
|
232
|
+
await this.drainEntry(entry, pushable, counts);
|
|
233
|
+
}
|
|
234
|
+
log('pass:done trigger=%s entries=%d examined=%d deferred=%d solo=%d waiting=%d pushed=%d cleared=%d dropped=%d abandoned=%d', trigger, counts.entries, counts.examined, counts.deferred, counts.solo, counts.waiting, counts.pushed, counts.cleared, counts.dropped, counts.abandoned);
|
|
235
|
+
return counts;
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
if (scanned)
|
|
239
|
+
this.lastPassAt = Date.now();
|
|
240
|
+
await this.rearmRecheck();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** The next `blockBudget` entries from the rotation cursor, wrapping; the cursor advances past them. */
|
|
244
|
+
takeBudget(entries) {
|
|
245
|
+
const budget = Math.max(1, this.config.blockBudget);
|
|
246
|
+
if (entries.length <= budget) {
|
|
247
|
+
this.cursor = 0;
|
|
248
|
+
return entries;
|
|
249
|
+
}
|
|
250
|
+
const start = this.cursor % entries.length;
|
|
251
|
+
const taken = [...entries.slice(start, start + budget), ...entries.slice(0, Math.max(0, start + budget - entries.length))];
|
|
252
|
+
this.cursor = (start + budget) % entries.length;
|
|
253
|
+
return taken;
|
|
254
|
+
}
|
|
255
|
+
async drainEntry(entry, pushable, counts) {
|
|
256
|
+
const owed = await this.owedPeers(entry);
|
|
257
|
+
if (owed.kind === 'solo') {
|
|
258
|
+
counts.solo++;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (owed.kind === 'unresolved') {
|
|
262
|
+
counts.waiting++;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const abandoned = this.abandoned.get(entry.blockId);
|
|
266
|
+
const reachable = owed.peerIds.filter(peerId => pushable.has(peerId) && !abandoned?.has(peerId));
|
|
267
|
+
if (reachable.length === 0) {
|
|
268
|
+
counts.waiting++;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const outcome = await this.deps.pushBlock(entry.blockId, reachable);
|
|
272
|
+
if (outcome.status === 'no-local-data') {
|
|
273
|
+
// A block this node no longer holds is not one it can owe — the same call spread-on-churn
|
|
274
|
+
// makes when a tracked block has left local storage.
|
|
275
|
+
log('entry:dropped block=%s rev=%d (no local data)', entry.blockId, entry.rev);
|
|
276
|
+
await this.deps.ledger.delete(entry.blockId);
|
|
277
|
+
this.forgetBlock(entry.blockId);
|
|
278
|
+
counts.dropped++;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (outcome.status === 'unavailable') {
|
|
282
|
+
// Could not find out what this node holds — not "holds nothing". Keep the entry, count nothing.
|
|
283
|
+
log('entry:unavailable block=%s rev=%d reason=%s (keeping)', entry.blockId, entry.rev, outcome.reason);
|
|
284
|
+
counts.waiting++;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
counts.pushed++;
|
|
288
|
+
const heldRev = outcome.latest?.rev ?? entry.rev;
|
|
289
|
+
for (const peerId of outcome.confirmed) {
|
|
290
|
+
log('push:ok block=%s peer=%s rev=%d', entry.blockId, peerId, heldRev);
|
|
291
|
+
this.failures.get(entry.blockId)?.delete(peerId);
|
|
292
|
+
}
|
|
293
|
+
if (outcome.confirmed.length > 0) {
|
|
294
|
+
const cleared = await this.recordConfirmed(entry, owed, outcome.confirmed, heldRev);
|
|
295
|
+
if (cleared) {
|
|
296
|
+
counts.cleared++;
|
|
297
|
+
this.forgetBlock(entry.blockId);
|
|
298
|
+
// After the deletion, never before: a listener that re-reads the ledger sees it gone.
|
|
299
|
+
this.deps.emit({ blockIds: [entry.blockId], rev: entry.rev, actionId: entry.actionId, collectionId: outcome.collectionId });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (outcome.refusals.length > 0) {
|
|
304
|
+
await this.deps.ledger.noteAttempt(entry.blockId);
|
|
305
|
+
counts.abandoned += this.noteRefusals(entry, outcome);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Take the confirmed peers off the entry. An entry that could not name its members is named now,
|
|
310
|
+
* at its own revision, with the cohort resolved for this push — so `satisfy` has a set to shrink.
|
|
311
|
+
* Both writes carry the revision guard: a newer shortfall recorded while the push was in flight
|
|
312
|
+
* is left alone (`name` touches only its own revision; `satisfy` keeps a higher one), and neither
|
|
313
|
+
* recreates an entry a concurrent `settle` removed mid-push.
|
|
314
|
+
*
|
|
315
|
+
* NOTE: an entry settled mid-push by a `full` commit at a newer revision is gone by the time
|
|
316
|
+
* `satisfy` runs, which reports that as "gone", so the event fires here too. It is true — the block
|
|
317
|
+
* IS fully replicated — but under the older `rev`/`actionId`, and a listener may also have heard
|
|
318
|
+
* `full` on that commit's own result. If a host ever needs the two told apart, have `satisfy`
|
|
319
|
+
* distinguish "deleted now" from "absent".
|
|
320
|
+
*
|
|
321
|
+
* @returns whether the entry is gone — its last missing member confirmed.
|
|
322
|
+
*/
|
|
323
|
+
async recordConfirmed(entry, owed, confirmed, heldRev) {
|
|
324
|
+
if (!owed.named) {
|
|
325
|
+
await this.deps.ledger.name(entry.blockId, entry.rev, owed.peerIds);
|
|
326
|
+
}
|
|
327
|
+
const remaining = await this.deps.ledger.satisfy(entry.blockId, confirmed, heldRev);
|
|
328
|
+
if (remaining === undefined) {
|
|
329
|
+
log('entry:cleared block=%s rev=%d action=%s', entry.blockId, entry.rev, entry.actionId);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
/** Log each refusal so "cannot place" reads differently from "cannot reach", and count the round. */
|
|
335
|
+
noteRefusals(entry, outcome) {
|
|
336
|
+
let abandoned = 0;
|
|
337
|
+
for (const refusal of outcome.refusals) {
|
|
338
|
+
if (refusal.reason === 'rejected') {
|
|
339
|
+
// Uncertified means this node retained no proof for the revision: the receiver's
|
|
340
|
+
// default policy refuses it, and no retry changes that until the block is rewritten.
|
|
341
|
+
log('push:rejected block=%s peer=%s rev=%d certified=%s%s', entry.blockId, refusal.peerId, entry.rev, outcome.certified, outcome.certified ? '' : ' (no retained proof — cannot place)');
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
log('push:unreachable block=%s peer=%s err=%s', entry.blockId, refusal.peerId, refusal.error);
|
|
345
|
+
}
|
|
346
|
+
if (this.countFailure(entry.blockId, refusal.peerId))
|
|
347
|
+
abandoned++;
|
|
348
|
+
}
|
|
349
|
+
return abandoned;
|
|
350
|
+
}
|
|
351
|
+
/** Count one unconfirmed round for a peer; abandon it at the bound. @returns whether it was abandoned now. */
|
|
352
|
+
countFailure(blockId, peerId) {
|
|
353
|
+
let perPeer = this.failures.get(blockId);
|
|
354
|
+
if (!perPeer) {
|
|
355
|
+
perPeer = new Map();
|
|
356
|
+
this.failures.set(blockId, perPeer);
|
|
357
|
+
}
|
|
358
|
+
const count = (perPeer.get(peerId) ?? 0) + 1;
|
|
359
|
+
if (count < this.config.maxAttempts) {
|
|
360
|
+
perPeer.set(peerId, count);
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
perPeer.delete(peerId);
|
|
364
|
+
let peers = this.abandoned.get(blockId);
|
|
365
|
+
if (!peers) {
|
|
366
|
+
peers = new Set();
|
|
367
|
+
this.abandoned.set(blockId, peers);
|
|
368
|
+
}
|
|
369
|
+
peers.add(peerId);
|
|
370
|
+
log('peer:abandoned block=%s peer=%s after=%d rounds (until it reconnects)', blockId, peerId, count);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Who the entry still owes. Named members are taken as recorded. An unknown set re-resolves the
|
|
375
|
+
* cohort NOW; a cohort of this node alone is `solo` — not a failure, not an attempt.
|
|
376
|
+
*/
|
|
377
|
+
async owedPeers(entry) {
|
|
378
|
+
const selfId = this.deps.libp2p.peerId.toString();
|
|
379
|
+
if (entry.missingPeerIds.length > 0) {
|
|
380
|
+
return { kind: 'peers', peerIds: entry.missingPeerIds.filter(peerId => peerId !== selfId), named: true };
|
|
381
|
+
}
|
|
382
|
+
let cluster;
|
|
383
|
+
try {
|
|
384
|
+
cluster = await this.deps.keyNetwork.findCluster(routingKeyForBlock(entry.blockId));
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
log('cohort:unresolved block=%s err=%s', entry.blockId, err instanceof Error ? err.message : String(err));
|
|
388
|
+
return { kind: 'unresolved' };
|
|
389
|
+
}
|
|
390
|
+
const members = Object.keys(cluster).filter(peerId => peerId !== selfId);
|
|
391
|
+
return members.length === 0 ? { kind: 'solo' } : { kind: 'peers', peerIds: members, named: false };
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Connected peers identified as serving this network's block-transfer protocol — the only peers a
|
|
395
|
+
* push can land on. Identify fills the peer store's protocol list, so a peer that just connected
|
|
396
|
+
* is not yet pushable; the `peer:identify` trigger brings it in. With no prefix configured every
|
|
397
|
+
* connected peer counts.
|
|
398
|
+
*/
|
|
399
|
+
async pushablePeers() {
|
|
400
|
+
const seen = new Set();
|
|
401
|
+
const pushable = new Set();
|
|
402
|
+
for (const connection of this.deps.libp2p.getConnections()) {
|
|
403
|
+
const peerId = connection.remotePeer;
|
|
404
|
+
const peerIdStr = peerId.toString();
|
|
405
|
+
if (seen.has(peerIdStr))
|
|
406
|
+
continue;
|
|
407
|
+
seen.add(peerIdStr);
|
|
408
|
+
if (this.deps.protocolPrefix === undefined || await this.servesBlockTransfer(peerId)) {
|
|
409
|
+
pushable.add(peerIdStr);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return pushable;
|
|
413
|
+
}
|
|
414
|
+
async servesBlockTransfer(peerId) {
|
|
415
|
+
try {
|
|
416
|
+
const peer = await this.deps.libp2p.peerStore.get(peerId);
|
|
417
|
+
return peer.protocols.includes(this.blockTransferProtocol);
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// Not in the peer store (not yet identified) or a store fault: not pushable this pass.
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// ── In-memory state upkeep ───────────────────────────────────────
|
|
425
|
+
forgetPeer(peerId) {
|
|
426
|
+
for (const peers of this.failures.values())
|
|
427
|
+
peers.delete(peerId);
|
|
428
|
+
for (const [blockId, peers] of this.abandoned) {
|
|
429
|
+
if (peers.delete(peerId) && peers.size === 0)
|
|
430
|
+
this.abandoned.delete(blockId);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/** An abandoned peer that has gone is forgotten: when it returns it is retried from scratch. */
|
|
434
|
+
forgetDisconnected(pushable) {
|
|
435
|
+
for (const [blockId, peers] of this.abandoned) {
|
|
436
|
+
for (const peerId of peers) {
|
|
437
|
+
if (!pushable.has(peerId))
|
|
438
|
+
peers.delete(peerId);
|
|
439
|
+
}
|
|
440
|
+
if (peers.size === 0)
|
|
441
|
+
this.abandoned.delete(blockId);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
forgetBlock(blockId) {
|
|
445
|
+
this.failures.delete(blockId);
|
|
446
|
+
this.abandoned.delete(blockId);
|
|
447
|
+
}
|
|
448
|
+
/** Drop state for blocks the ledger no longer lists (settled by a `full` commit, evicted, dropped). */
|
|
449
|
+
pruneState(entries) {
|
|
450
|
+
const listed = new Set(entries.map(entry => entry.blockId));
|
|
451
|
+
for (const blockId of [...this.failures.keys(), ...this.abandoned.keys()]) {
|
|
452
|
+
if (!listed.has(blockId))
|
|
453
|
+
this.forgetBlock(blockId);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/** Never hold the process open for a drain timer. */
|
|
458
|
+
function unref(timer) {
|
|
459
|
+
timer.unref?.();
|
|
460
|
+
}
|
|
461
|
+
//# sourceMappingURL=under-replication-drain.js.map
|