syncweb_core/sync/
peer_tracker.rs1use 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)]
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 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 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#[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 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 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}