1use bytes::Bytes;
19use hashiverse_lib::protocol::payload::payload::CacheRequestTokenV1;
20use hashiverse_lib::protocol::peer::Peer;
21use hashiverse_lib::tools::buckets::BucketLocation;
22use hashiverse_lib::tools::server_id::ServerId;
23use hashiverse_lib::tools::time::{TimeMillis, MILLIS_IN_MINUTE};
24use hashiverse_lib::tools::time_provider::moka_clock::TimeProviderMokaClock;
25use hashiverse_lib::tools::time_provider::time_provider::TimeProvider;
26use hashiverse_lib::tools::tools::leading_agreement_bits_xor;
27use hashiverse_lib::tools::types::Id;
28use moka::sync::Cache;
29use std::collections::HashMap;
30use std::sync::{Arc, Mutex};
31
32use crate::server::post_bundle_caching_shared::{CachedBundle, GetCacheResult, CACHE_HIT_THRESHOLD, CACHE_LOCATION_TTI, CACHE_REQUEST_TOKEN_TTL_DURATION, CACHE_REQUEST_TOKEN_TTL_DURATION_MILLIS};
33
34const POST_BUNDLE_PLACEHOLDER_WEIGHT: u32 = 4 * 1024 * 1024;
37
38struct CachedPostBundle {
40 bundle_time_millis: TimeMillis,
41 cached_bundle: CachedBundle,
42}
43
44struct CachedPostBundleLocationEntry {
51 bundles: HashMap<Id, CachedPostBundle>,
53 hit_count: u32,
54}
55
56impl CachedPostBundleLocationEntry {
57 fn placeholder() -> Self {
58 Self { bundles: HashMap::new(), hit_count: 0 }
59 }
60
61 fn weight(&self) -> u32 {
62 let total: u32 = self.bundles.values().map(|cached_post_bundle| cached_post_bundle.cached_bundle.bytes.len() as u32).sum();
63 if total == 0 { POST_BUNDLE_PLACEHOLDER_WEIGHT } else { total }
64 }
65}
66
67pub struct PostBundleCache {
81 max_originators_per_location: usize,
82 bundles: Cache<Id, Arc<Mutex<CachedPostBundleLocationEntry>>>,
83 inflight: Cache<Id, ()>,
84}
85
86impl PostBundleCache {
87 pub fn new(max_originators_per_location: usize, max_bytes: u64, time_provider: Arc<dyn TimeProvider>) -> Self {
88 let clock = Arc::new(TimeProviderMokaClock::new(time_provider));
90
91 let bundles = Cache::builder()
92 .weigher(|_key: &Id, entry: &Arc<Mutex<CachedPostBundleLocationEntry>>| {
93 entry.lock().map(|e| e.weight()).unwrap_or(POST_BUNDLE_PLACEHOLDER_WEIGHT)
94 })
95 .max_capacity(max_bytes)
96 .time_to_idle(CACHE_LOCATION_TTI)
97 .external_clock(clock.clone())
98 .build();
99
100 let inflight = Cache::builder()
101 .time_to_live(CACHE_REQUEST_TOKEN_TTL_DURATION)
102 .external_clock(clock)
103 .build();
104
105 Self { max_originators_per_location, bundles, inflight }
106 }
107
108 pub fn on_get(
115 &self,
116 bucket_location: &BucketLocation,
117 already_retrieved_peer_ids: &[Id],
118 peer_self: &Peer,
119 server_id: &ServerId,
120 now: TimeMillis,
121 ) -> GetCacheResult {
122 let location_id = bucket_location.location_id;
123 let entry_arc = self.bundles.get_with(location_id, || Arc::new(Mutex::new(CachedPostBundleLocationEntry::placeholder())));
124
125 let (cached_items, already_cached_peer_ids, should_issue_token) = {
126 let mut entry = entry_arc.lock().unwrap();
127 entry.hit_count += 1;
128
129 let already_retrieved_set: std::collections::HashSet<Id> = already_retrieved_peer_ids.iter().copied().collect();
130 let cached_items: Vec<Bytes> = entry.bundles
131 .iter()
132 .filter(|(originator_id, cached_post_bundle)| !already_retrieved_set.contains(originator_id) && !cached_post_bundle.cached_bundle.is_stale(now))
133 .map(|(_, cached_post_bundle)| cached_post_bundle.cached_bundle.bytes.clone())
134 .collect();
135
136 let already_cached_peer_ids: Vec<Id> = entry.bundles.keys().copied().collect();
137 let should_issue_token = entry.hit_count >= CACHE_HIT_THRESHOLD && !self.inflight.contains_key(&location_id);
138 (cached_items, already_cached_peer_ids, should_issue_token)
139 };
140
141 let cache_request_token = if should_issue_token {
142 self.inflight.insert(location_id, ());
143 let expires_at = now + CACHE_REQUEST_TOKEN_TTL_DURATION_MILLIS;
144 Some(CacheRequestTokenV1::new(peer_self.clone(), bucket_location.clone(), expires_at, already_cached_peer_ids, &server_id.keys.signature_key))
145 } else {
146 None
147 };
148
149 GetCacheResult { cached_items, cache_request_token }
150 }
151
152 pub fn on_upload(
159 &self,
160 location_id: Id,
161 originator_peer_id: Id,
162 bundle_bytes: Bytes,
163 bundle_time_millis: TimeMillis,
164 server_time: TimeMillis,
165 is_sealed: bool,
166 ) -> bool {
167 let entry_arc = match self.bundles.get(&location_id) {
168 Some(e) => e,
169 None => return false, };
171
172 let mut entry = entry_arc.lock().unwrap();
173 if let Some(existing_cached_post_bundle) = entry.bundles.get(&originator_peer_id)
174 && bundle_time_millis < existing_cached_post_bundle.bundle_time_millis
175 {
176 return false;
177 }
178
179 let expires_at = if is_sealed { None } else { Some(server_time + MILLIS_IN_MINUTE.const_mul(5)) };
180 let cached_post_bundle = CachedPostBundle { bundle_time_millis, cached_bundle: CachedBundle { bytes: bundle_bytes, expires_at } };
181
182 entry.bundles.insert(originator_peer_id, cached_post_bundle);
184
185 while entry.bundles.len() > self.max_originators_per_location {
190 let evict_key = entry.bundles
191 .iter()
192 .min_by(|(id_a, cached_post_bundle_a), (id_b, cached_post_bundle_b)| {
193 let distance_a = leading_agreement_bits_xor(id_a.as_ref(), location_id.as_ref());
194 let distance_b = leading_agreement_bits_xor(id_b.as_ref(), location_id.as_ref());
195 distance_a.cmp(&distance_b).then_with(|| {
196 let expires_a = cached_post_bundle_a.cached_bundle.expires_at.unwrap_or(TimeMillis(i64::MAX));
197 let expires_b = cached_post_bundle_b.cached_bundle.expires_at.unwrap_or(TimeMillis(i64::MAX));
198 expires_a.cmp(&expires_b)
199 })
200 })
201 .map(|(id, _)| *id);
202 if let Some(k) = evict_key {
203 entry.bundles.remove(&k);
204 }
205 }
206
207 entry.bundles.contains_key(&originator_peer_id)
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use bytes::Bytes;
216 use hashiverse_lib::tools::buckets::{BucketLocation, BucketType, BUCKET_DURATIONS};
217 use hashiverse_lib::tools::server_id::ServerId;
218 use hashiverse_lib::tools::time::TimeMillis;
219 use hashiverse_lib::tools::time_provider::time_provider::RealTimeProvider;
220 use hashiverse_lib::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
221 use hashiverse_lib::tools::types::{Id, Pow};
222
223 async fn make_test_server_and_peer() -> anyhow::Result<(ServerId, hashiverse_lib::protocol::peer::Peer)> {
224 let time_provider = RealTimeProvider;
225 let pow_generator = SingleThreadedPowGenerator::new();
226 let server_id = ServerId::new("own_pow", &time_provider, Pow(0), true, &pow_generator).await?;
227 let peer = server_id.to_peer(&time_provider)?;
228 Ok((server_id, peer))
229 }
230
231 fn make_test_bucket_location() -> BucketLocation {
232 BucketLocation::new(BucketType::User, Id::random(), BUCKET_DURATIONS[0], TimeMillis(1_000_000)).unwrap()
233 }
234
235 #[tokio::test]
236 async fn test_below_threshold_no_token() -> anyhow::Result<()> {
237 let (server_id, peer_self) = make_test_server_and_peer().await?;
238 let cache = PostBundleCache::new(5, 64 * 1024 * 1024, Arc::new(RealTimeProvider));
239 let bucket_location = make_test_bucket_location();
240 let now = TimeMillis(1_000_000);
241
242 for _ in 0..(CACHE_HIT_THRESHOLD - 1) {
243 let result = cache.on_get(&bucket_location, &[], &peer_self, &server_id, now);
244 assert!(result.cache_request_token.is_none());
245 assert!(result.cached_items.is_empty());
246 }
247
248 Ok(())
249 }
250
251 #[tokio::test]
252 async fn test_at_threshold_token_issued_then_deduplicated() -> anyhow::Result<()> {
253 let (server_id, peer_self) = make_test_server_and_peer().await?;
254 let cache = PostBundleCache::new(5, 64 * 1024 * 1024, Arc::new(RealTimeProvider));
255 let bucket_location = make_test_bucket_location();
256 let now = TimeMillis(1_000_000);
257
258 for _ in 0..(CACHE_HIT_THRESHOLD - 1) {
259 let result = cache.on_get(&bucket_location, &[], &peer_self, &server_id, now);
260 assert!(result.cache_request_token.is_none());
261 }
262
263 let result = cache.on_get(&bucket_location, &[], &peer_self, &server_id, now);
265 assert!(result.cache_request_token.is_some());
266
267 let result2 = cache.on_get(&bucket_location, &[], &peer_self, &server_id, now);
269 assert!(result2.cache_request_token.is_none());
270
271 Ok(())
272 }
273
274 #[tokio::test]
275 async fn test_upload_and_retrieval() -> anyhow::Result<()> {
276 let (server_id, peer_self) = make_test_server_and_peer().await?;
277 let cache = PostBundleCache::new(5, 64 * 1024 * 1024, Arc::new(RealTimeProvider));
278 let bucket_location = make_test_bucket_location();
279 let location_id = bucket_location.location_id;
280 let now = TimeMillis(1_000_000);
281 let originator_id = Id::random();
282 let bundle_bytes = Bytes::from_static(b"test_bundle");
283
284 cache.on_get(&bucket_location, &[], &peer_self, &server_id, now);
286
287 let accepted = cache.on_upload(location_id, originator_id, bundle_bytes.clone(), now, now, false);
288 assert!(accepted);
289
290 let result = cache.on_get(&bucket_location, &[], &peer_self, &server_id, now);
291 assert_eq!(result.cached_items, vec![bundle_bytes]);
292
293 Ok(())
294 }
295
296 #[tokio::test]
297 async fn test_already_retrieved_filtered() -> anyhow::Result<()> {
298 let (server_id, peer_self) = make_test_server_and_peer().await?;
299 let cache = PostBundleCache::new(5, 64 * 1024 * 1024, Arc::new(RealTimeProvider));
300 let bucket_location = make_test_bucket_location();
301 let location_id = bucket_location.location_id;
302 let now = TimeMillis(1_000_000);
303 let originator_id = Id::random();
304
305 cache.on_get(&bucket_location, &[], &peer_self, &server_id, now);
306 cache.on_upload(location_id, originator_id, Bytes::from_static(b"bundle"), now, now, false);
307
308 let result = cache.on_get(&bucket_location, &[originator_id], &peer_self, &server_id, now);
309 assert!(result.cached_items.is_empty());
310
311 Ok(())
312 }
313
314 #[tokio::test]
315 async fn test_upload_returns_false_when_not_in_cache() -> anyhow::Result<()> {
316 let cache = PostBundleCache::new(5, 64 * 1024 * 1024, Arc::new(RealTimeProvider));
317 let location_id = Id::random();
318 let originator_id = Id::random();
319
320 let accepted = cache.on_upload(location_id, originator_id, Bytes::from_static(b"bundle"), TimeMillis(1_000_000), TimeMillis(1_000_000), false);
322 assert!(!accepted);
323
324 Ok(())
325 }
326
327 #[tokio::test]
330 async fn test_overflow_keeps_closest_originators() -> anyhow::Result<()> {
331 let (server_id, peer_self) = make_test_server_and_peer().await?;
332 let cache = PostBundleCache::new(3, 64 * 1024 * 1024, Arc::new(RealTimeProvider)); let bucket_location = make_test_bucket_location();
334 let location_id = bucket_location.location_id;
335 let now = TimeMillis(1_000_000);
336
337 cache.on_get(&bucket_location, &[], &peer_self, &server_id, now);
339
340 let originator_at = |flip_bit: usize| -> Id {
343 let mut bytes = location_id.0;
344 bytes[flip_bit / 8] ^= 1 << (7 - (flip_bit % 8));
345 Id(bytes)
346 };
347
348 for &p in &[20usize, 40, 60, 80, 100] {
350 let bytes = Bytes::from(format!("bundle-agreement-{}", p));
351 cache.on_upload(location_id, originator_at(p), bytes, now, now, true);
353 }
354
355 let result = cache.on_get(&bucket_location, &[], &peer_self, &server_id, now);
356 let cached: std::collections::HashSet<Vec<u8>> = result.cached_items.iter().map(|b| b.to_vec()).collect();
357 assert_eq!(3, cached.len(), "cache must keep exactly max_originators_per_location entries");
358 for &p in &[60usize, 80, 100] {
359 assert!(cached.contains(format!("bundle-agreement-{}", p).as_bytes()), "closest originator (agreement {}) must be kept", p);
360 }
361 for &p in &[20usize, 40] {
362 assert!(!cached.contains(format!("bundle-agreement-{}", p).as_bytes()), "furthest originator (agreement {}) must be evicted", p);
363 }
364 Ok(())
365 }
366
367 #[tokio::test]
370 async fn test_upload_keeps_newer_bundle_for_same_originator() -> anyhow::Result<()> {
371 let (server_id, peer_self) = make_test_server_and_peer().await?;
372 let cache = PostBundleCache::new(5, 64 * 1024 * 1024, Arc::new(RealTimeProvider));
373 let bucket_location = make_test_bucket_location();
374 let location_id = bucket_location.location_id;
375 let now = TimeMillis(1_000_000);
376 let originator_id = Id::random();
377 let other_originator_id = Id::random();
378 let newer_bundle_bytes = Bytes::from_static(b"newer_bundle");
379 let older_bundle_bytes = Bytes::from_static(b"older_bundle");
380 let newest_bundle_bytes = Bytes::from_static(b"newest_bundle");
381 let other_originator_bundle_bytes = Bytes::from_static(b"other_originator_old_bundle");
382
383 cache.on_get(&bucket_location, &[], &peer_self, &server_id, now);
384
385 assert!(cache.on_upload(location_id, originator_id, newer_bundle_bytes.clone(), TimeMillis(900_000), now, true));
386 assert!(!cache.on_upload(location_id, originator_id, older_bundle_bytes, TimeMillis(800_000), now, true), "older bundle from the same originator must be rejected");
387 let result = cache.on_get(&bucket_location, &[], &peer_self, &server_id, now);
388 assert_eq!(result.cached_items, vec![newer_bundle_bytes]);
389
390 assert!(cache.on_upload(location_id, originator_id, newest_bundle_bytes.clone(), TimeMillis(950_000), now, true), "newer bundle from the same originator must replace the cached one");
391 assert!(cache.on_upload(location_id, other_originator_id, other_originator_bundle_bytes.clone(), TimeMillis(100_000), now, true), "an old bundle from another originator is still cacheable");
392 let result = cache.on_get(&bucket_location, &[], &peer_self, &server_id, now);
393 let cached_items: std::collections::HashSet<Bytes> = result.cached_items.into_iter().collect();
394 assert_eq!(cached_items, std::collections::HashSet::from([newest_bundle_bytes, other_originator_bundle_bytes]));
395
396 Ok(())
397 }
398
399 #[tokio::test]
407 async fn test_cache_request_token_expiry() -> anyhow::Result<()> {
408 let (server_id, peer_self) = make_test_server_and_peer().await?;
409 let cache = PostBundleCache::new(5, 64 * 1024 * 1024, Arc::new(RealTimeProvider));
410 let bucket_location = make_test_bucket_location();
411 let now = TimeMillis(1_000_000);
412
413 let mut token = None;
415 for _ in 0..CACHE_HIT_THRESHOLD {
416 token = cache.on_get(&bucket_location, &[], &peer_self, &server_id, now).cache_request_token.or(token);
417 }
418 let token = token.expect("server issues a token at the hit threshold");
419
420 assert!(!token.is_expired(now), "token must be valid at issue time");
421 assert!(!token.is_expired(TimeMillis(now.0 + CACHE_REQUEST_TOKEN_TTL_DURATION_MILLIS.0 - 1)), "token must be valid just before its TTL elapses");
422 assert!(token.is_expired(TimeMillis(now.0 + CACHE_REQUEST_TOKEN_TTL_DURATION_MILLIS.0 + 1)), "token must be expired once its TTL has elapsed");
423
424 Ok(())
425 }
426}