Skip to main content

syncweb_core/sync/
peer_tracker.rs

1use std::{
2    collections::{HashMap, HashSet},
3    time::{Duration, Instant},
4};
5
6use iroh::PublicKey;
7use iroh_blobs::Hash;
8
9/// Policy used when the peer availability cache reaches its size limit.
10#[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/// Bounded cache of peers observed serving individual blobs.
26#[derive(Clone, Debug)]
27pub struct PeerTracker {
28    entries: HashMap<Hash, Availability>,
29    max_cache_size: usize,
30    strategy: EvictionStrategy,
31    expiry: Duration,
32    clock: u64,
33}
34
35impl PeerTracker {
36    #[must_use]
37    pub fn new(max_cache_size: usize, strategy: EvictionStrategy) -> Self {
38        Self::with_expiry(max_cache_size, strategy, Duration::from_mins(5))
39    }
40
41    #[must_use]
42    pub fn with_expiry(max_cache_size: usize, strategy: EvictionStrategy, expiry: Duration) -> Self {
43        Self {
44            entries: HashMap::new(),
45            max_cache_size,
46            strategy,
47            expiry,
48            clock: 0,
49        }
50    }
51
52    pub fn record_peer(&mut self, blob_hash: Hash, node_id: PublicKey) {
53        self.clock = self.clock.saturating_add(1);
54        let now = Instant::now();
55        let entry = self.entries.entry(blob_hash).or_insert_with(|| Availability {
56            peers: HashSet::new(),
57            inserted_at: self.clock,
58            accessed_at: self.clock,
59            last_seen: now,
60        });
61        entry.peers.insert(node_id);
62        entry.accessed_at = self.clock;
63        entry.last_seen = now;
64        self.evict_to_limit();
65    }
66
67    /// Record a peer observed during a blob transfer.
68    pub fn on_blob_fetched(&mut self, blob_hash: Hash, node_id: PublicKey) {
69        self.record_peer(blob_hash, node_id);
70    }
71
72    #[must_use]
73    pub fn get_peers(&mut self, blob_hash: &Hash) -> Vec<PublicKey> {
74        self.clock = self.clock.saturating_add(1);
75        self.entries.get_mut(blob_hash).map_or_else(Vec::new, |entry| {
76            if self.strategy == EvictionStrategy::Lru {
77                entry.accessed_at = self.clock;
78            }
79            entry.peers.iter().copied().collect()
80        })
81    }
82
83    #[must_use]
84    pub fn peers(&self, blob_hash: &Hash) -> Vec<PublicKey> {
85        self.entries
86            .get(blob_hash)
87            .map_or_else(Vec::new, |entry| entry.peers.iter().copied().collect())
88    }
89
90    #[must_use]
91    pub fn peer_count(&self, blob_hash: &Hash) -> usize {
92        self.entries.get(blob_hash).map_or(0, |entry| entry.peers.len())
93    }
94
95    #[must_use]
96    pub fn contains(&self, blob_hash: &Hash, node_id: &PublicKey) -> bool {
97        self.entries
98            .get(blob_hash)
99            .is_some_and(|entry| entry.peers.contains(node_id))
100    }
101
102    /// Remove a peer from all cached blob availability records.
103    pub fn on_peer_disconnected(&mut self, node_id: &PublicKey) {
104        self.entries.retain(|_, entry| {
105            entry.peers.remove(node_id);
106            !entry.peers.is_empty()
107        });
108    }
109
110    #[must_use]
111    pub fn is_fresh(&self, blob_hash: &Hash) -> bool {
112        self.entries
113            .get(blob_hash)
114            .is_some_and(|entry| entry.last_seen.elapsed() <= self.expiry)
115    }
116
117    #[must_use]
118    pub fn len(&self) -> usize {
119        self.entries.len()
120    }
121
122    #[must_use]
123    pub fn is_empty(&self) -> bool {
124        self.entries.is_empty()
125    }
126
127    pub fn tick_and_maybe_evict(&mut self) {
128        let now = Instant::now();
129        self.entries
130            .retain(|_, entry| now.saturating_duration_since(entry.last_seen) <= self.expiry);
131        self.evict_to_limit();
132    }
133
134    fn evict_to_limit(&mut self) {
135        while self.entries.len() > self.max_cache_size {
136            let oldest = self
137                .entries
138                .iter()
139                .min_by_key(|(_, entry)| match self.strategy {
140                    EvictionStrategy::Lru => entry.accessed_at,
141                    EvictionStrategy::Fifo => entry.inserted_at,
142                })
143                .map(|(hash, _)| *hash);
144            if let Some(hash) = oldest {
145                self.entries.remove(&hash);
146            } else {
147                break;
148            }
149        }
150    }
151}
152
153/// Compact peer presence cache using stable integer indices.
154#[derive(Clone, Debug, Default)]
155pub struct EfficientPeerCache {
156    active_indices: HashMap<Hash, Vec<u16>>,
157    presence: HashMap<Hash, Vec<u64>>,
158    peer_index: HashMap<PublicKey, u16>,
159    index_peer: Vec<PublicKey>,
160}
161
162impl EfficientPeerCache {
163    #[must_use]
164    pub fn new() -> Self {
165        Self::default()
166    }
167
168    /// Record that a peer can serve a blob.
169    ///
170    /// # Errors
171    ///
172    /// Returns an error after all `u16` peer indices have been allocated.
173    pub fn add_presence(&mut self, hash: Hash, peer: PublicKey) -> crate::Result<()> {
174        let index = if let Some(index) = self.peer_index.get(&peer) {
175            *index
176        } else {
177            let index = u16::try_from(self.index_peer.len())
178                .map_err(|error| crate::SyncwebError::operation("peer cache capacity exceeded", error))?;
179            self.index_peer.push(peer);
180            self.peer_index.insert(peer, index);
181            index
182        };
183        let indices = self.active_indices.entry(hash).or_default();
184        match indices.binary_search(&index) {
185            Ok(_) => {}
186            Err(position) => indices.insert(position, index),
187        }
188        let index_usize = usize::from(index);
189        let word_index = index_usize.checked_div(64).unwrap_or(0);
190        let bit_index = index_usize.checked_rem(64).unwrap_or(0);
191        let mask = 1_u64.checked_shl(u32::try_from(bit_index).unwrap_or(0)).unwrap_or(0);
192        let presence = self.presence.entry(hash).or_default();
193        presence.resize(word_index.saturating_add(1), 0);
194        if let Some(word) = presence.get_mut(word_index) {
195            *word |= mask;
196        }
197        Ok(())
198    }
199
200    pub fn remove_presence(&mut self, hash: &Hash, peer: &PublicKey) -> bool {
201        let Some(index) = self.peer_index.get(peer) else {
202            return false;
203        };
204        let Some(indices) = self.active_indices.get_mut(hash) else {
205            return false;
206        };
207        let removed = indices.binary_search(index).is_ok_and(|position| {
208            indices.remove(position);
209            true
210        });
211        if indices.is_empty() {
212            self.active_indices.remove(hash);
213        }
214        if removed {
215            let index_usize = usize::from(*index);
216            let word_index = index_usize.checked_div(64).unwrap_or(0);
217            let bit_index = index_usize.checked_rem(64).unwrap_or(0);
218            let mask = 1_u64.checked_shl(u32::try_from(bit_index).unwrap_or(0)).unwrap_or(0);
219            if let Some(word) = self.presence.get_mut(hash).and_then(|words| words.get_mut(word_index)) {
220                *word &= !mask;
221            }
222        }
223        removed
224    }
225
226    #[must_use]
227    pub fn has_peer(&self, hash: &Hash, peer: &PublicKey) -> bool {
228        self.peer_index.get(peer).is_some_and(|index| {
229            let index_usize = usize::from(*index);
230            let word_index = index_usize.checked_div(64).unwrap_or(0);
231            let bit_index = index_usize.checked_rem(64).unwrap_or(0);
232            let mask = 1_u64.checked_shl(u32::try_from(bit_index).unwrap_or(0)).unwrap_or(0);
233            self.presence
234                .get(hash)
235                .and_then(|words| words.get(word_index))
236                .is_some_and(|word| *word & mask != 0)
237        })
238    }
239
240    #[must_use]
241    pub fn peers(&self, hash: &Hash) -> Vec<PublicKey> {
242        self.active_indices.get(hash).map_or_else(Vec::new, |indices| {
243            indices
244                .iter()
245                .filter_map(|index| self.index_peer.get(usize::from(*index)).copied())
246                .collect()
247        })
248    }
249
250    #[must_use]
251    pub const fn peer_count(&self) -> usize {
252        self.index_peer.len()
253    }
254
255    #[must_use]
256    pub fn active_peer_count(&self, hash: &Hash) -> usize {
257        self.active_indices.get(hash).map_or(0, Vec::len)
258    }
259
260    #[must_use]
261    pub fn memory_usage(&self) -> usize {
262        let word_bytes = self
263            .presence
264            .values()
265            .map(|words| words.len().saturating_mul(8))
266            .sum::<usize>();
267        let index_bytes = self
268            .active_indices
269            .values()
270            .map(|indices| indices.len().saturating_mul(2))
271            .sum::<usize>();
272        word_bytes
273            .saturating_add(index_bytes)
274            .saturating_add(
275                self.peer_index
276                    .len()
277                    .saturating_mul(std::mem::size_of::<PublicKey>().saturating_add(2)),
278            )
279            .saturating_add(self.index_peer.len().saturating_mul(std::mem::size_of::<PublicKey>()))
280    }
281
282    /// Remove a peer from every blob while retaining stable indices for peers
283    /// that may be observed again later.
284    pub fn remove_peer(&mut self, peer: &PublicKey) {
285        if !self.peer_index.contains_key(peer) {
286            return;
287        }
288        let hashes = self.active_indices.keys().copied().collect::<Vec<_>>();
289        for hash in hashes {
290            let _removed = self.remove_presence(&hash, peer);
291        }
292    }
293}