1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
//! Piece and peer selection for swarm fetches (ADR-099 section 6, card
//! mesh-placement-25). Pure logic, no I/O.
//!
//! - [`PiecePicker`] hands each peer the **rarest** piece it holds that is
//! still needed and not already requested from someone else. Rarity is
//! the number of live peers that hold the piece; ties go to the lowest
//! index so a fetch is reproducible. When a peer is lost its in-flight
//! pieces return to the pool and its holdings stop counting.
//! - [`order_peers`] ranks fetch candidates: higher trust tier first (a LAN
//! id is self-asserted), then same LAN as the local node, then by measured link speed (peers never measured rank as an
//! assumed default, so they get tried), then by id.
//! - [`LinkStats`] keeps a smoothed bytes-per-second figure per peer from
//! the transfers this node actually performed. It is locally measured:
//! a peer's own claims about its link are never used.
use std::collections::{BTreeMap, HashMap};
use dashmap::DashMap;
use clawft_types::placement::TrustTier;
use crate::mesh_artifact_types::Bitfield;
/// What the picker tells one peer's worker to do next.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Pick {
/// Request this piece from the peer.
Piece(u32),
/// Nothing free for this peer now, but pieces it holds are in flight
/// elsewhere and may come back: wait for a change.
Wait,
/// The peer holds nothing still needed: done with it.
Nothing,
}
/// Which pieces are held, in flight and available where.
#[derive(Debug, Clone)]
pub struct PiecePicker {
have: Bitfield,
in_flight: BTreeMap<u32, String>,
peers: HashMap<String, Bitfield>,
}
impl PiecePicker {
/// Picker over `have` (pieces already held locally).
pub fn new(have: Bitfield) -> Self {
Self {
have,
in_flight: BTreeMap::new(),
peers: HashMap::new(),
}
}
/// A peer joined with this holding.
pub fn add_peer(&mut self, peer: &str, has: Bitfield) {
self.peers.insert(peer.to_string(), has);
}
/// A peer is gone: its holdings stop counting and its in-flight pieces
/// return to the pool.
pub fn remove_peer(&mut self, peer: &str) {
self.peers.remove(peer);
self.in_flight.retain(|_, p| p != peer);
}
/// The peer says it cannot supply piece `i` after all.
pub fn peer_lacks(&mut self, peer: &str, i: u32) {
if let Some(b) = self.peers.get_mut(peer) {
b.set(i, false);
}
if self.in_flight.get(&i).is_some_and(|p| p == peer) {
self.in_flight.remove(&i);
}
}
/// Live peers holding piece `i`.
pub fn availability(&self, i: u32) -> usize {
self.peers.values().filter(|b| b.get(i)).count()
}
/// Next piece for `peer`, claiming it (see [`Pick`]).
pub fn pick(&mut self, peer: &str) -> Pick {
let Some(has) = self.peers.get(peer) else {
return Pick::Nothing;
};
let mut best: Option<(usize, u32)> = None;
let mut blocked = false;
for i in self.have.missing() {
if !has.get(i) {
continue;
}
if self.in_flight.contains_key(&i) {
blocked = true;
continue;
}
let key = (self.availability(i), i);
if best.is_none_or(|b| key < b) {
best = Some(key);
}
}
match best {
Some((_, i)) => {
self.in_flight.insert(i, peer.to_string());
Pick::Piece(i)
}
None if blocked => Pick::Wait,
None => Pick::Nothing,
}
}
/// Piece `i` arrived and verified.
pub fn complete(&mut self, i: u32) {
self.have.set(i, true);
self.in_flight.remove(&i);
}
/// Piece `i` was not delivered: back to the pool.
pub fn release(&mut self, i: u32) {
self.in_flight.remove(&i);
}
/// True when every piece is held.
pub fn is_complete(&self) -> bool {
self.have.is_complete()
}
/// Pieces still missing.
pub fn missing(&self) -> u32 {
self.have.len() - self.have.count()
}
/// Missing pieces no live peer holds.
pub fn unavailable(&self) -> usize {
self.have.missing().filter(|&i| self.availability(i) == 0).count()
}
}
/// A peer that may hold an artifact.
#[derive(Debug, Clone, PartialEq)]
pub struct PeerCandidate {
/// Peer node id.
pub peer_id: String,
/// LAN the peer advertises (`net.lan` fact), for locality. Self-asserted,
/// so it only orders peers of the same tier.
pub lan_id: Option<String>,
/// Trust tier this node assigned the peer (`Discovered` when unknown).
pub tier: TrustTier,
}
impl PeerCandidate {
/// Candidate with no locality information.
pub fn new(peer_id: impl Into<String>) -> Self {
Self {
peer_id: peer_id.into(),
lan_id: None,
tier: TrustTier::Discovered,
}
}
/// Candidate with trust tier `tier`.
pub fn with_tier(mut self, tier: TrustTier) -> Self {
self.tier = tier;
self
}
/// Candidate on `lan`.
pub fn on_lan(mut self, lan: impl Into<String>) -> Self {
self.lan_id = Some(lan.into());
self
}
}
/// Link speed assumed for a peer this node never transferred from, so it
/// is tried rather than starved (bytes per second).
pub const ASSUMED_LINK_BPS: f64 = 10.0 * 1024.0 * 1024.0;
/// Rank `candidates`: higher trust tier first, then same LAN as `local_lan`,
/// then faster measured link, then peer id. Banned peers are dropped.
pub fn order_peers(
candidates: Vec<PeerCandidate>,
local_lan: Option<&str>,
links: &LinkStats,
is_banned: &dyn Fn(&str) -> bool,
) -> Vec<PeerCandidate> {
let mut v: Vec<(bool, f64, PeerCandidate)> = candidates
.into_iter()
.filter(|c| !is_banned(&c.peer_id))
.map(|c| {
let same_lan = matches!((local_lan, c.lan_id.as_deref()), (Some(a), Some(b)) if a == b);
let bps = links.bytes_per_sec(&c.peer_id).unwrap_or(ASSUMED_LINK_BPS);
(same_lan, bps, c)
})
.collect();
v.sort_by(|a, b| {
b.2.tier
.cmp(&a.2.tier)
.then(b.0.cmp(&a.0))
.then(b.1.total_cmp(&a.1))
.then(a.2.peer_id.cmp(&b.2.peer_id))
});
v.into_iter().map(|(_, _, c)| c).collect()
}
/// Smoothed per-peer throughput measured by this node.
#[derive(Debug, Default)]
pub struct LinkStats {
peers: DashMap<String, f64>,
}
/// Weight of the newest sample in the smoothed figure.
const EWMA_ALPHA: f64 = 0.3;
impl LinkStats {
/// Record `bytes` received from `peer` in `secs` seconds.
pub fn record(&self, peer: &str, bytes: u64, secs: f64) {
if bytes == 0 || secs <= 0.0 {
return;
}
let sample = bytes as f64 / secs;
self.peers
.entry(peer.to_string())
.and_modify(|v| *v = EWMA_ALPHA * sample + (1.0 - EWMA_ALPHA) * *v)
.or_insert(sample);
}
/// Smoothed bytes per second from `peer`, if ever measured.
pub fn bytes_per_sec(&self, peer: &str) -> Option<f64> {
self.peers.get(peer).map(|v| *v)
}
}
#[cfg(test)]
#[path = "mesh_swarm_picker_tests.rs"]
mod tests;