1use std::time::{Duration, Instant};
12
13use dashmap::DashMap;
14use parking_lot::RwLock;
15
16#[derive(Debug, Clone)]
18pub struct NodeEntry {
19 pub node_id: String,
20 pub last_seen: Instant,
21}
22
23#[derive(Default)]
27pub struct PrefixIndex {
28 inner: DashMap<[u8; 32], RwLock<Vec<NodeEntry>>>,
30 ttl: Duration,
32}
33
34impl PrefixIndex {
35 pub fn new(ttl: Duration) -> Self {
36 Self {
37 inner: DashMap::new(),
38 ttl,
39 }
40 }
41
42 pub fn insert(&self, digest: [u8; 32], node_id: &str) {
44 let now = Instant::now();
45 let entry = self.inner.entry(digest).or_default();
46 let mut v = entry.write();
47 if let Some(e) = v.iter_mut().find(|e| e.node_id == node_id) {
48 e.last_seen = now;
49 } else {
50 v.push(NodeEntry {
51 node_id: node_id.to_string(),
52 last_seen: now,
53 });
54 }
55 }
56
57 pub fn forget_node(&self, node_id: &str) {
59 for mut e in self.inner.iter_mut() {
60 e.value_mut().write().retain(|n| n.node_id != node_id);
61 }
62 self.inner.retain(|_, v| !v.read().is_empty());
64 }
65
66 pub fn lookup(&self, digest: &[u8; 32]) -> Vec<String> {
68 let Some(entry) = self.inner.get(digest) else {
69 return Vec::new();
70 };
71 let now = Instant::now();
72 let g = entry.read();
73 g.iter()
74 .filter(|n| now.duration_since(n.last_seen) < self.ttl)
75 .map(|n| n.node_id.clone())
76 .collect()
77 }
78
79 pub fn overlap(&self, digests: &[[u8; 32]]) -> std::collections::HashMap<String, usize> {
88 let mut out: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
89 let now = Instant::now();
90 for d in digests {
91 if let Some(entry) = self.inner.get(d) {
92 let g = entry.read();
93 for n in g.iter() {
94 if now.duration_since(n.last_seen) < self.ttl {
95 *out.entry(n.node_id.clone()).or_insert(0) += 1;
96 }
97 }
98 }
99 }
100 out
101 }
102
103 pub fn longest_prefix_overlap(
117 &self,
118 digests: &[[u8; 32]],
119 ) -> std::collections::HashMap<String, usize> {
120 let mut out: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
121 if digests.is_empty() {
122 return out;
123 }
124 let now = Instant::now();
125
126 let first = match self.inner.get(&digests[0]) {
128 Some(e) => e,
129 None => return out,
130 };
131 let mut active: std::collections::HashSet<String> = first
132 .read()
133 .iter()
134 .filter(|n| now.duration_since(n.last_seen) < self.ttl)
135 .map(|n| n.node_id.clone())
136 .collect();
137 for n in &active {
138 out.insert(n.clone(), 1);
139 }
140 drop(first);
141
142 for (i, d) in digests.iter().enumerate().skip(1) {
145 if active.is_empty() {
146 break;
147 }
148 let holders: std::collections::HashSet<String> = match self.inner.get(d) {
149 Some(e) => e
150 .read()
151 .iter()
152 .filter(|n| now.duration_since(n.last_seen) < self.ttl)
153 .map(|n| n.node_id.clone())
154 .collect(),
155 None => Default::default(),
156 };
157 active.retain(|n| holders.contains(n));
158 for n in &active {
159 out.insert(n.clone(), i + 1);
160 }
161 }
162 out
163 }
164
165 pub fn len(&self) -> usize {
167 self.inner.len()
168 }
169 pub fn is_empty(&self) -> bool {
170 self.inner.is_empty()
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn d(byte: u8) -> [u8; 32] {
179 let mut x = [0u8; 32];
180 x[0] = byte;
181 x
182 }
183
184 #[test]
185 fn insert_and_lookup() {
186 let ix = PrefixIndex::new(Duration::from_secs(60));
187 ix.insert(d(1), "node-a");
188 ix.insert(d(1), "node-b");
189 ix.insert(d(2), "node-a");
190 assert_eq!(ix.len(), 2);
191
192 let mut nodes = ix.lookup(&d(1));
193 nodes.sort();
194 assert_eq!(nodes, vec!["node-a".to_string(), "node-b".to_string()]);
195 }
196
197 #[test]
198 fn forget_node_purges_entries() {
199 let ix = PrefixIndex::new(Duration::from_secs(60));
200 ix.insert(d(1), "node-a");
201 ix.insert(d(2), "node-a");
202 ix.forget_node("node-a");
203 assert!(ix.is_empty());
204 }
205
206 #[test]
207 fn overlap_counts() {
208 let ix = PrefixIndex::new(Duration::from_secs(60));
209 ix.insert(d(1), "n1");
210 ix.insert(d(2), "n1");
211 ix.insert(d(3), "n2");
212 let counts = ix.overlap(&[d(1), d(2), d(3), d(4)]);
213 assert_eq!(counts.get("n1").copied(), Some(2));
214 assert_eq!(counts.get("n2").copied(), Some(1));
215 }
216
217 #[test]
218 fn longest_prefix_overlap_walks_in_order() {
219 let ix = PrefixIndex::new(Duration::from_secs(60));
223 ix.insert(d(1), "n1");
224 ix.insert(d(2), "n1");
225 ix.insert(d(3), "n1");
226 ix.insert(d(1), "n2");
227 ix.insert(d(3), "n2");
228 ix.insert(d(7), "n3");
229
230 let req = [d(1), d(2), d(3), d(4)];
231 let lp = ix.longest_prefix_overlap(&req);
232 assert_eq!(lp.get("n1").copied(), Some(3));
233 assert_eq!(lp.get("n2").copied(), Some(1));
234 assert!(!lp.contains_key("n3"));
235 }
236
237 #[test]
238 fn longest_prefix_overlap_empty_when_no_chunk0() {
239 let ix = PrefixIndex::new(Duration::from_secs(60));
240 ix.insert(d(2), "n1");
241 ix.insert(d(3), "n1");
242 let lp = ix.longest_prefix_overlap(&[d(1), d(2), d(3)]);
243 assert!(lp.is_empty(), "no node holds chunk 0 → no prefix overlap");
244 }
245}