1use std::{
2 collections::{HashMap, HashSet},
3 time::{Duration, Instant},
4};
5
6use iroh::PublicKey;
7use iroh_blobs::Hash;
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11#[non_exhaustive]
12pub enum EvictionStrategy {
13 Lru,
14 Fifo,
15}
16
17#[derive(Clone, Debug)]
18struct Availability {
19 peers: HashSet<PublicKey>,
20 inserted_at: u64,
21 accessed_at: u64,
22 last_seen: Instant,
23}
24
25#[derive(Clone, Debug)]
30pub struct PeerTracker {
31 entries: HashMap<Hash, Availability>,
32 max_cache_size: usize,
33 strategy: EvictionStrategy,
34 expiry: Duration,
35 clock: u64,
36}
37
38impl PeerTracker {
39 #[must_use]
40 pub fn new(max_cache_size: usize, strategy: EvictionStrategy) -> Self {
41 Self::with_expiry(max_cache_size, strategy, Duration::from_mins(5))
42 }
43
44 #[must_use]
45 pub fn with_expiry(max_cache_size: usize, strategy: EvictionStrategy, expiry: Duration) -> Self {
46 Self {
47 entries: HashMap::new(),
48 max_cache_size,
49 strategy,
50 expiry,
51 clock: 0,
52 }
53 }
54
55 pub fn record_peer(&mut self, blob_hash: Hash, node_id: PublicKey) {
56 self.clock = self.clock.saturating_add(1);
57 let now = Instant::now();
58 let entry = self.entries.entry(blob_hash).or_insert_with(|| Availability {
59 peers: HashSet::new(),
60 inserted_at: self.clock,
61 accessed_at: self.clock,
62 last_seen: now,
63 });
64 entry.peers.insert(node_id);
65 entry.accessed_at = self.clock;
66 entry.last_seen = now;
67 self.evict_to_limit();
68 }
69
70 pub fn on_blob_fetched(&mut self, blob_hash: Hash, node_id: PublicKey) {
72 self.record_peer(blob_hash, node_id);
73 }
74
75 #[must_use]
76 pub fn get_peers(&mut self, blob_hash: &Hash) -> Vec<PublicKey> {
77 self.clock = self.clock.saturating_add(1);
78 self.entries.get_mut(blob_hash).map_or_else(Vec::new, |entry| {
79 if self.strategy == EvictionStrategy::Lru {
80 entry.accessed_at = self.clock;
81 }
82 entry.peers.iter().copied().collect()
83 })
84 }
85
86 #[must_use]
87 pub fn peers(&mut self, blob_hash: &Hash) -> Vec<PublicKey> {
88 self.clock = self.clock.saturating_add(1);
89 self.entries.get_mut(blob_hash).map_or_else(Vec::new, |entry| {
90 if self.strategy == EvictionStrategy::Lru {
91 entry.accessed_at = self.clock;
92 }
93 entry.peers.iter().copied().collect()
94 })
95 }
96
97 #[must_use]
98 pub fn peer_count(&self, blob_hash: &Hash) -> usize {
99 self.entries.get(blob_hash).map_or(0, |entry| entry.peers.len())
100 }
101
102 #[must_use]
103 pub fn contains(&self, blob_hash: &Hash, node_id: &PublicKey) -> bool {
104 self.entries
105 .get(blob_hash)
106 .is_some_and(|entry| entry.peers.contains(node_id))
107 }
108
109 pub fn on_peer_disconnected(&mut self, node_id: &PublicKey) {
111 self.entries.retain(|_, entry| {
112 entry.peers.remove(node_id);
113 !entry.peers.is_empty()
114 });
115 }
116
117 #[must_use]
118 pub fn is_fresh(&self, blob_hash: &Hash) -> bool {
119 self.entries
120 .get(blob_hash)
121 .is_some_and(|entry| entry.last_seen.elapsed() <= self.expiry)
122 }
123
124 #[must_use]
125 pub fn len(&self) -> usize {
126 self.entries.len()
127 }
128
129 #[must_use]
130 pub fn is_empty(&self) -> bool {
131 self.entries.is_empty()
132 }
133
134 pub fn tick_and_maybe_evict(&mut self) {
135 let now = Instant::now();
136 self.entries
137 .retain(|_, entry| now.saturating_duration_since(entry.last_seen) <= self.expiry);
138 self.evict_to_limit();
139 }
140
141 fn evict_to_limit(&mut self) {
142 while self.entries.len() > self.max_cache_size {
143 let oldest = self
144 .entries
145 .iter()
146 .min_by_key(|(_, entry)| match self.strategy {
147 EvictionStrategy::Lru => entry.accessed_at,
148 EvictionStrategy::Fifo => entry.inserted_at,
149 })
150 .map(|(hash, _)| *hash);
151 if let Some(hash) = oldest {
152 self.entries.remove(&hash);
153 } else {
154 break;
155 }
156 }
157 }
158}
159
160#[derive(Clone, Debug, Default)]
162pub struct EfficientPeerCache {
163 hash_to_indices: HashMap<Hash, Vec<u16>>,
164 presence: HashMap<Hash, Vec<u64>>,
165 peer_to_index: HashMap<PublicKey, u16>,
166 index_peer: Vec<PublicKey>,
167 last_seen: HashMap<PublicKey, Instant>,
168}
169
170impl EfficientPeerCache {
171 #[must_use]
172 pub fn new() -> Self {
173 Self::default()
174 }
175
176 pub fn add_presence(&mut self, hash: Hash, peer: PublicKey) -> crate::Result<()> {
182 let index = if let Some(index) = self.peer_to_index.get(&peer) {
183 *index
184 } else {
185 let index = u16::try_from(self.index_peer.len())
186 .map_err(|error| crate::SyncwebError::operation("peer cache capacity exceeded", error))?;
187 self.index_peer.push(peer);
188 self.peer_to_index.insert(peer, index);
189 index
190 };
191 self.last_seen.insert(peer, Instant::now());
192 let indices = self.hash_to_indices.entry(hash).or_default();
193 match indices.binary_search(&index) {
194 Ok(_) => {}
195 Err(position) => indices.insert(position, index),
196 }
197 let index_usize = usize::from(index);
198 let word_index = index_usize.checked_div(64).unwrap_or(0);
199 let bit_index = index_usize.checked_rem(64).unwrap_or(0);
200 let mask = 1_u64.checked_shl(u32::try_from(bit_index).unwrap_or(0)).unwrap_or(0);
201 let presence = self.presence.entry(hash).or_default();
202 presence.resize(word_index.saturating_add(1), 0);
203 if let Some(word) = presence.get_mut(word_index) {
204 *word |= mask;
205 }
206 Ok(())
207 }
208
209 pub fn remove_presence(&mut self, hash: &Hash, peer: &PublicKey) -> bool {
210 let Some(index) = self.peer_to_index.get(peer) else {
211 return false;
212 };
213 let Some(indices) = self.hash_to_indices.get_mut(hash) else {
214 return false;
215 };
216 let removed = indices.binary_search(index).is_ok_and(|position| {
217 indices.remove(position);
218 true
219 });
220 if indices.is_empty() {
221 self.hash_to_indices.remove(hash);
222 }
223 if removed {
224 let index_usize = usize::from(*index);
225 let word_index = index_usize.checked_div(64).unwrap_or(0);
226 let bit_index = index_usize.checked_rem(64).unwrap_or(0);
227 let mask = 1_u64.checked_shl(u32::try_from(bit_index).unwrap_or(0)).unwrap_or(0);
228 if let Some(word) = self.presence.get_mut(hash).and_then(|words| words.get_mut(word_index)) {
229 *word &= !mask;
230 }
231 }
232 removed
233 }
234
235 #[must_use]
236 pub fn has_peer(&self, hash: &Hash, peer: &PublicKey) -> bool {
237 self.peer_to_index.get(peer).is_some_and(|index| {
238 let index_usize = usize::from(*index);
239 let word_index = index_usize.checked_div(64).unwrap_or(0);
240 let bit_index = index_usize.checked_rem(64).unwrap_or(0);
241 let mask = 1_u64.checked_shl(u32::try_from(bit_index).unwrap_or(0)).unwrap_or(0);
242 self.presence
243 .get(hash)
244 .and_then(|words| words.get(word_index))
245 .is_some_and(|word| *word & mask != 0)
246 })
247 }
248
249 #[must_use]
250 pub fn peers(&self, hash: &Hash) -> Vec<PublicKey> {
251 self.hash_to_indices.get(hash).map_or_else(Vec::new, |indices| {
252 indices
253 .iter()
254 .filter_map(|index| self.index_peer.get(usize::from(*index)).copied())
255 .collect()
256 })
257 }
258
259 #[must_use]
262 pub const fn total_peers(&self) -> usize {
263 self.index_peer.len()
264 }
265
266 #[must_use]
267 pub fn active_peer_count(&self, hash: &Hash) -> usize {
268 self.hash_to_indices.get(hash).map_or(0, Vec::len)
269 }
270
271 #[must_use]
272 pub fn memory_usage(&self) -> usize {
273 let word_bytes = self
274 .presence
275 .values()
276 .map(|words| words.len().saturating_mul(8))
277 .sum::<usize>();
278 let index_bytes = self
279 .hash_to_indices
280 .values()
281 .map(|indices| indices.len().saturating_mul(2))
282 .sum::<usize>();
283 word_bytes
284 .saturating_add(index_bytes)
285 .saturating_add(
286 self.peer_to_index
287 .len()
288 .saturating_mul(std::mem::size_of::<PublicKey>().saturating_add(2)),
289 )
290 .saturating_add(self.index_peer.len().saturating_mul(std::mem::size_of::<PublicKey>()))
291 }
292
293 pub fn remove_peer(&mut self, peer: &PublicKey) {
296 if !self.peer_to_index.contains_key(peer) {
297 return;
298 }
299 let hashes = self.hash_to_indices.keys().copied().collect::<Vec<_>>();
300 for hash in hashes {
301 let _removed = self.remove_presence(&hash, peer);
302 }
303 self.last_seen.remove(peer);
304 }
305
306 pub fn retain_active(&mut self, ttl: Duration) -> usize {
313 self.retain_active_at(ttl, Instant::now())
314 }
315
316 fn retain_active_at(&mut self, ttl: Duration, now: Instant) -> usize {
319 let cutoff = now.checked_sub(ttl).unwrap_or(now);
320 let stale: Vec<PublicKey> = self
321 .last_seen
322 .iter()
323 .filter(|(_, last_seen)| **last_seen < cutoff)
324 .map(|(peer, _)| *peer)
325 .collect();
326 let count = stale.len();
327 for peer in stale {
328 self.remove_peer(&peer);
329 if let Some(stale_index) = self.peer_to_index.remove(&peer) {
330 let last_index = self.index_peer.len().saturating_sub(1);
331 if usize::from(stale_index) != last_index
332 && let Some(last_peer) = self.index_peer.get(last_index).copied()
333 {
334 self.remap_presence(last_index, usize::from(stale_index));
337 if let Some(slot) = self.index_peer.get_mut(usize::from(stale_index)) {
338 *slot = last_peer;
339 }
340 self.peer_to_index.insert(last_peer, stale_index);
341 }
342 self.index_peer.pop();
343 }
344 }
345 count
346 }
347
348 fn remap_presence(&mut self, old_index: usize, new_index: usize) {
351 let old_u16 = u16::try_from(old_index).unwrap_or(u16::MAX);
352 let new_u16 = u16::try_from(new_index).unwrap_or(u16::MAX);
353 for (hash, indices) in &mut self.hash_to_indices {
354 let Ok(pos) = indices.binary_search(&old_u16) else {
356 continue;
357 };
358 if let Some(slot) = indices.get_mut(pos) {
359 *slot = new_u16;
360 }
361 let mut p = pos;
362 while p > 0 {
363 let prev = p.wrapping_sub(1);
364 if indices.get(prev).is_none_or(|&v| v <= new_u16) {
365 break;
366 }
367 indices.swap(prev, p);
368 p = prev;
369 }
370 if let Some(words) = self.presence.get_mut(hash) {
372 let old_word = old_index.checked_div(64).unwrap_or(0);
373 let old_bit = old_index.checked_rem(64).unwrap_or(0);
374 if let Some(word) = words.get_mut(old_word) {
375 *word &= !(1_u64 << old_bit);
376 }
377 let new_word = new_index.checked_div(64).unwrap_or(0);
378 let new_bit = new_index.checked_rem(64).unwrap_or(0);
379 if new_word >= words.len() {
380 words.resize(new_word.saturating_add(1), 0);
381 }
382 if let Some(word) = words.get_mut(new_word) {
383 *word |= 1_u64 << new_bit;
384 }
385 }
386 }
387 }
388}