use ipfrs_core::Cid;
use std::collections::{BinaryHeap, HashMap};
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct WantListConfig {
pub max_wants: usize,
pub default_timeout: Duration,
pub max_retries: u32,
pub base_retry_delay: Duration,
pub max_retry_delay: Duration,
}
impl Default for WantListConfig {
fn default() -> Self {
Self {
max_wants: 1024,
default_timeout: Duration::from_secs(30),
max_retries: 3,
base_retry_delay: Duration::from_millis(100),
max_retry_delay: Duration::from_secs(10),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Priority {
Low = 0,
Normal = 50,
High = 100,
Urgent = 200,
Critical = 300,
}
impl From<i32> for Priority {
fn from(value: i32) -> Self {
match value {
v if v >= 300 => Priority::Critical,
v if v >= 200 => Priority::Urgent,
v if v >= 100 => Priority::High,
v if v >= 50 => Priority::Normal,
_ => Priority::Low,
}
}
}
impl From<Priority> for i32 {
fn from(value: Priority) -> Self {
value as i32
}
}
#[derive(Debug, Clone)]
pub struct WantEntry {
pub cid: Cid,
pub priority: i32,
pub created_at: Instant,
pub expires_at: Instant,
pub retry_count: u32,
pub last_attempt: Option<Instant>,
pub send_dont_have: bool,
pub deadline: Option<Instant>,
pub tag: Option<String>,
}
impl WantEntry {
pub fn new(cid: Cid, priority: i32, timeout: Duration) -> Self {
let now = Instant::now();
Self {
cid,
priority,
created_at: now,
expires_at: now + timeout,
retry_count: 0,
last_attempt: None,
send_dont_have: false,
deadline: None,
tag: None,
}
}
pub fn with_deadline(mut self, deadline: Instant) -> Self {
self.deadline = Some(deadline);
self
}
pub fn with_tag(mut self, tag: String) -> Self {
self.tag = Some(tag);
self
}
pub fn with_dont_have(mut self) -> Self {
self.send_dont_have = true;
self
}
pub fn is_expired(&self) -> bool {
Instant::now() >= self.expires_at
}
pub fn can_retry(&self, max_retries: u32) -> bool {
self.retry_count < max_retries
}
pub fn effective_priority(&self) -> i32 {
let mut priority = self.priority;
if let Some(deadline) = self.deadline {
let now = Instant::now();
if now >= deadline {
priority = priority.max(Priority::Critical as i32);
} else {
let time_left = deadline.duration_since(now);
if time_left < Duration::from_secs(1) {
priority = priority.max(Priority::Urgent as i32);
} else if time_left < Duration::from_secs(5) {
priority = priority.max(Priority::High as i32);
}
}
}
priority
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
struct HeapEntry {
cid: Cid,
priority: i32,
created_at: Instant,
version: u64,
}
impl Ord for HeapEntry {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.priority
.cmp(&other.priority)
.then_with(|| other.created_at.cmp(&self.created_at))
}
}
impl PartialOrd for HeapEntry {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
pub struct WantList {
heap: BinaryHeap<HeapEntry>,
entries: HashMap<Cid, (WantEntry, u64)>,
version_counter: u64,
config: WantListConfig,
}
impl WantList {
pub fn new(config: WantListConfig) -> Self {
Self {
heap: BinaryHeap::with_capacity(config.max_wants),
entries: HashMap::with_capacity(config.max_wants),
version_counter: 0,
config,
}
}
pub fn with_defaults() -> Self {
Self::new(WantListConfig::default())
}
pub fn add(&mut self, entry: WantEntry) -> bool {
if self.entries.contains_key(&entry.cid) {
return false;
}
if self.entries.len() >= self.config.max_wants {
return false;
}
let cid = entry.cid;
let priority = entry.effective_priority();
let created_at = entry.created_at;
self.version_counter += 1;
let version = self.version_counter;
self.entries.insert(cid, (entry, version));
self.heap.push(HeapEntry {
cid,
priority,
created_at,
version,
});
true
}
pub fn add_simple(&mut self, cid: Cid, priority: i32) -> bool {
let entry = WantEntry::new(cid, priority, self.config.default_timeout);
self.add(entry)
}
pub fn remove(&mut self, cid: &Cid) -> Option<WantEntry> {
self.entries.remove(cid).map(|(entry, _)| entry)
}
pub fn update_priority(&mut self, cid: &Cid, new_priority: i32) -> bool {
if let Some((entry, _old_version)) = self.entries.get_mut(cid) {
entry.priority = new_priority;
self.version_counter += 1;
let version = self.version_counter;
let created_at = entry.created_at;
if let Some((_, v)) = self.entries.get_mut(cid) {
*v = version;
}
self.heap.push(HeapEntry {
cid: *cid,
priority: new_priority,
created_at,
version,
});
true
} else {
false
}
}
pub fn boost_deadline_priorities(&mut self) {
let now = Instant::now();
let mut updates = Vec::new();
for (cid, (entry, _)) in &self.entries {
if let Some(deadline) = entry.deadline {
let effective = entry.effective_priority();
if effective > entry.priority {
updates.push((*cid, effective));
}
if now >= deadline && entry.priority < Priority::Critical as i32 {
updates.push((*cid, Priority::Critical as i32));
}
}
}
for (cid, priority) in updates {
self.update_priority(&cid, priority);
}
}
pub fn pop(&mut self) -> Option<WantEntry> {
loop {
let heap_entry = self.heap.pop()?;
if let Some((_entry, version)) = self.entries.get(&heap_entry.cid) {
if *version == heap_entry.version {
return self.entries.remove(&heap_entry.cid).map(|(e, _)| e);
}
}
}
}
pub fn peek(&self) -> Option<&WantEntry> {
self.entries
.values()
.max_by_key(|(e, _)| e.effective_priority())
.map(|(e, _)| e)
}
pub fn contains(&self, cid: &Cid) -> bool {
self.entries.contains_key(cid)
}
pub fn get(&self, cid: &Cid) -> Option<&WantEntry> {
self.entries.get(cid).map(|(e, _)| e)
}
pub fn get_mut(&mut self, cid: &Cid) -> Option<&mut WantEntry> {
self.entries.get_mut(cid).map(|(e, _)| e)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn cleanup_expired(&mut self) -> Vec<WantEntry> {
let mut expired = Vec::new();
let cids_to_remove: Vec<Cid> = self
.entries
.iter()
.filter(|(_, (entry, _))| entry.is_expired())
.map(|(cid, _)| *cid)
.collect();
for cid in cids_to_remove {
if let Some(entry) = self.remove(&cid) {
expired.push(entry);
}
}
expired
}
pub fn get_retry_candidates(&self) -> Vec<Cid> {
self.entries
.iter()
.filter(|(_, (entry, _))| {
entry.can_retry(self.config.max_retries) && entry.last_attempt.is_some()
})
.map(|(cid, _)| *cid)
.collect()
}
pub fn mark_attempted(&mut self, cid: &Cid) {
if let Some((entry, _)) = self.entries.get_mut(cid) {
entry.retry_count += 1;
entry.last_attempt = Some(Instant::now());
}
}
pub fn retry_delay(&self, retry_count: u32) -> Duration {
let base = self.config.base_retry_delay.as_millis() as u64;
let max = self.config.max_retry_delay.as_millis() as u64;
let delay_ms = base.saturating_mul(1 << retry_count.min(10));
let delay_ms = delay_ms.min(max);
let jitter = (delay_ms / 10) as i64;
let jitter_offset = if jitter > 0 {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as i64)
.unwrap_or(0);
(now % (jitter * 2)) - jitter
} else {
0
};
Duration::from_millis((delay_ms as i64 + jitter_offset).max(0) as u64)
}
pub fn cids(&self) -> Vec<Cid> {
self.entries.keys().copied().collect()
}
pub fn entries_by_priority(&self) -> Vec<&WantEntry> {
let mut entries: Vec<_> = self.entries.values().map(|(e, _)| e).collect();
entries.sort_by_key(|b| std::cmp::Reverse(b.effective_priority()));
entries
}
pub fn entries_with_tag(&self, tag: &str) -> Vec<&WantEntry> {
self.entries
.values()
.filter_map(|(e, _)| {
if e.tag.as_deref() == Some(tag) {
Some(e)
} else {
None
}
})
.collect()
}
pub fn clear(&mut self) {
self.heap.clear();
self.entries.clear();
}
pub fn config(&self) -> &WantListConfig {
&self.config
}
pub fn add_batch(&mut self, entries: &[(Cid, i32)]) -> usize {
let mut added = 0;
for (cid, priority) in entries {
if self.add_simple(*cid, *priority) {
added += 1;
}
}
added
}
pub fn add_batch_same_priority(&mut self, cids: &[Cid], priority: i32) -> usize {
let mut added = 0;
for cid in cids {
if self.add_simple(*cid, priority) {
added += 1;
}
}
added
}
pub fn remove_batch(&mut self, cids: &[Cid]) -> Vec<WantEntry> {
let mut removed = Vec::with_capacity(cids.len());
for cid in cids {
if let Some(entry) = self.remove(cid) {
removed.push(entry);
}
}
removed
}
pub fn update_priorities_batch(&mut self, updates: &[(Cid, i32)]) -> usize {
let mut updated = 0;
for (cid, priority) in updates {
if self.update_priority(cid, *priority) {
updated += 1;
}
}
updated
}
pub fn contains_any(&self, cids: &[Cid]) -> bool {
cids.iter().any(|cid| self.contains(cid))
}
pub fn contains_all(&self, cids: &[Cid]) -> bool {
cids.iter().all(|cid| self.contains(cid))
}
pub fn get_batch(&self, cids: &[Cid]) -> Vec<&WantEntry> {
cids.iter().filter_map(|cid| self.get(cid)).collect()
}
}
pub struct ConcurrentWantList {
inner: Arc<RwLock<WantList>>,
}
impl ConcurrentWantList {
pub fn new(config: WantListConfig) -> Self {
Self {
inner: Arc::new(RwLock::new(WantList::new(config))),
}
}
pub fn with_defaults() -> Self {
Self::new(WantListConfig::default())
}
pub fn add(&self, entry: WantEntry) -> bool {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.add(entry)
}
pub fn add_simple(&self, cid: Cid, priority: i32) -> bool {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.add_simple(cid, priority)
}
pub fn remove(&self, cid: &Cid) -> Option<WantEntry> {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.remove(cid)
}
pub fn update_priority(&self, cid: &Cid, new_priority: i32) -> bool {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.update_priority(cid, new_priority)
}
pub fn pop(&self) -> Option<WantEntry> {
self.inner.write().unwrap_or_else(|e| e.into_inner()).pop()
}
pub fn contains(&self, cid: &Cid) -> bool {
self.inner
.read()
.unwrap_or_else(|e| e.into_inner())
.contains(cid)
}
pub fn len(&self) -> usize {
self.inner.read().unwrap_or_else(|e| e.into_inner()).len()
}
pub fn is_empty(&self) -> bool {
self.inner
.read()
.unwrap_or_else(|e| e.into_inner())
.is_empty()
}
pub fn cleanup_expired(&self) -> Vec<WantEntry> {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.cleanup_expired()
}
pub fn boost_deadline_priorities(&self) {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.boost_deadline_priorities()
}
pub fn cids(&self) -> Vec<Cid> {
self.inner.read().unwrap_or_else(|e| e.into_inner()).cids()
}
pub fn mark_attempted(&self, cid: &Cid) {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.mark_attempted(cid)
}
pub fn retry_delay(&self, retry_count: u32) -> Duration {
self.inner
.read()
.unwrap_or_else(|e| e.into_inner())
.retry_delay(retry_count)
}
pub fn clone_inner(&self) -> Arc<RwLock<WantList>> {
Arc::clone(&self.inner)
}
pub fn add_batch(&self, entries: &[(Cid, i32)]) -> usize {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.add_batch(entries)
}
pub fn add_batch_same_priority(&self, cids: &[Cid], priority: i32) -> usize {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.add_batch_same_priority(cids, priority)
}
pub fn remove_batch(&self, cids: &[Cid]) -> Vec<WantEntry> {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.remove_batch(cids)
}
pub fn update_priorities_batch(&self, updates: &[(Cid, i32)]) -> usize {
self.inner
.write()
.unwrap_or_else(|e| e.into_inner())
.update_priorities_batch(updates)
}
pub fn contains_any(&self, cids: &[Cid]) -> bool {
self.inner
.read()
.unwrap_or_else(|e| e.into_inner())
.contains_any(cids)
}
pub fn contains_all(&self, cids: &[Cid]) -> bool {
self.inner
.read()
.unwrap_or_else(|e| e.into_inner())
.contains_all(cids)
}
pub fn get_batch(&self, cids: &[Cid]) -> Vec<WantEntry> {
let lock = self.inner.read().unwrap_or_else(|e| e.into_inner());
cids.iter()
.filter_map(|cid| lock.get(cid).cloned())
.collect()
}
}
impl Clone for ConcurrentWantList {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use multihash::Multihash;
fn test_cid() -> Cid {
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse()
.expect("test: parse known-good CID string")
}
fn test_cid2() -> Cid {
"bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354"
.parse()
.expect("test: parse known-good CID string")
}
#[test]
fn test_want_list_add_remove() {
let mut list = WantList::with_defaults();
let cid = test_cid();
assert!(list.add_simple(cid, 50));
assert!(list.contains(&cid));
assert_eq!(list.len(), 1);
assert!(!list.add_simple(cid, 100));
let entry = list.remove(&cid);
assert!(entry.is_some());
assert!(!list.contains(&cid));
}
#[test]
fn test_priority_ordering() {
let mut list = WantList::with_defaults();
let cid1 = test_cid();
let cid2 = test_cid2();
list.add_simple(cid1, 10);
list.add_simple(cid2, 100);
let first = list
.pop()
.expect("test: pop highest-priority entry from non-empty want list");
assert_eq!(first.cid, cid2);
assert_eq!(first.priority, 100);
let second = list.pop().expect("test: pop second entry from want list");
assert_eq!(second.cid, cid1);
}
#[test]
fn test_priority_update() {
let mut list = WantList::with_defaults();
let cid1 = test_cid();
let cid2 = test_cid2();
list.add_simple(cid1, 10);
list.add_simple(cid2, 20);
assert!(list.update_priority(&cid1, 100));
let first = list
.pop()
.expect("test: pop updated-priority entry from non-empty want list");
assert_eq!(first.cid, cid1);
}
#[test]
fn test_deadline_priority_boost() {
let mut list = WantList::with_defaults();
let cid = test_cid();
let entry = WantEntry::new(cid, Priority::Low as i32, Duration::from_secs(60))
.with_deadline(Instant::now() + Duration::from_millis(100));
list.add(entry);
std::thread::sleep(Duration::from_millis(50));
let entry = list
.get(&cid)
.expect("test: get entry that was just added to want list");
assert!(entry.effective_priority() > Priority::Low as i32);
}
#[test]
fn test_retry_delay_exponential_backoff() {
let list = WantList::with_defaults();
let delay0 = list.retry_delay(0);
let delay1 = list.retry_delay(1);
let delay2 = list.retry_delay(2);
assert!(delay1 >= delay0);
assert!(delay2 >= delay1);
assert!(delay2 <= list.config.max_retry_delay);
}
#[test]
fn test_concurrent_want_list() {
let list = ConcurrentWantList::with_defaults();
let cid = test_cid();
assert!(list.add_simple(cid, 50));
assert!(list.contains(&cid));
assert_eq!(list.len(), 1);
let entry = list.pop();
assert!(entry.is_some());
assert!(list.is_empty());
}
#[test]
fn test_add_batch() {
let mut list = WantList::with_defaults();
let cids: Vec<_> = (0u64..10)
.map(|i| {
let data = i.to_le_bytes();
let hash =
Multihash::wrap(0x12, &data).expect("test: wrap raw bytes into Multihash");
Cid::new_v1(0x55, hash)
})
.collect();
let entries: Vec<_> = cids.iter().map(|cid| (*cid, 100)).collect();
let added = list.add_batch(&entries);
assert_eq!(added, 10);
assert_eq!(list.len(), 10);
let added = list.add_batch(&entries);
assert_eq!(added, 0); assert_eq!(list.len(), 10);
}
#[test]
fn test_add_batch_same_priority() {
let mut list = WantList::with_defaults();
let cids: Vec<_> = (0u64..5)
.map(|i| {
let data = i.to_le_bytes();
let hash =
Multihash::wrap(0x12, &data).expect("test: wrap raw bytes into Multihash");
Cid::new_v1(0x55, hash)
})
.collect();
let added = list.add_batch_same_priority(&cids, 200);
assert_eq!(added, 5);
assert_eq!(list.len(), 5);
for cid in &cids {
let entry = list
.get(cid)
.expect("test: get entry that was batch-added to want list");
assert_eq!(entry.priority, 200);
}
}
#[test]
fn test_remove_batch() {
let mut list = WantList::with_defaults();
let cids: Vec<_> = (0u64..8)
.map(|i| {
let data = i.to_le_bytes();
let hash =
Multihash::wrap(0x12, &data).expect("test: wrap raw bytes into Multihash");
Cid::new_v1(0x55, hash)
})
.collect();
for cid in &cids {
list.add_simple(*cid, 100);
}
assert_eq!(list.len(), 8);
let removed = list.remove_batch(&cids[0..4]);
assert_eq!(removed.len(), 4);
assert_eq!(list.len(), 4);
for cid in &cids[0..4] {
assert!(!list.contains(cid));
}
for cid in &cids[4..] {
assert!(list.contains(cid));
}
}
#[test]
fn test_update_priorities_batch() {
let mut list = WantList::with_defaults();
let cids: Vec<_> = (0u64..6)
.map(|i| {
let data = i.to_le_bytes();
let hash =
Multihash::wrap(0x12, &data).expect("test: wrap raw bytes into Multihash");
Cid::new_v1(0x55, hash)
})
.collect();
for cid in &cids {
list.add_simple(*cid, 100);
}
let updates: Vec<_> = cids
.iter()
.enumerate()
.map(|(i, cid)| (*cid, 200 + i as i32))
.collect();
let updated = list.update_priorities_batch(&updates);
assert_eq!(updated, 6);
for (i, cid) in cids.iter().enumerate() {
let entry = list
.get(cid)
.expect("test: get entry after batch priority update");
assert_eq!(entry.priority, 200 + i as i32);
}
}
#[test]
fn test_contains_any_all() {
let mut list = WantList::with_defaults();
let cids: Vec<_> = (0u64..5)
.map(|i| {
let data = i.to_le_bytes();
let hash =
Multihash::wrap(0x12, &data).expect("test: wrap raw bytes into Multihash");
Cid::new_v1(0x55, hash)
})
.collect();
for cid in &cids[0..3] {
list.add_simple(*cid, 100);
}
assert!(list.contains_any(&cids)); assert!(!list.contains_all(&cids)); assert!(list.contains_all(&cids[0..3])); assert!(!list.contains_any(&cids[4..5])); }
#[test]
fn test_get_batch() {
let mut list = WantList::with_defaults();
let cids: Vec<_> = (0u64..7)
.map(|i| {
let data = i.to_le_bytes();
let hash =
Multihash::wrap(0x12, &data).expect("test: wrap raw bytes into Multihash");
Cid::new_v1(0x55, hash)
})
.collect();
for (i, cid) in cids.iter().enumerate() {
if i % 2 == 0 {
list.add_simple(*cid, 100);
}
}
let entries = list.get_batch(&cids);
assert_eq!(entries.len(), 4);
for entry in entries {
assert_eq!(entry.priority, 100);
}
}
#[test]
fn test_concurrent_batch_operations() {
let list = ConcurrentWantList::with_defaults();
let cids: Vec<_> = (0u64..10)
.map(|i| {
let data = i.to_le_bytes();
let hash =
Multihash::wrap(0x12, &data).expect("test: wrap raw bytes into Multihash");
Cid::new_v1(0x55, hash)
})
.collect();
let added = list.add_batch_same_priority(&cids, 150);
assert_eq!(added, 10);
assert_eq!(list.len(), 10);
assert!(list.contains_all(&cids));
let updates: Vec<_> = cids.iter().map(|cid| (*cid, 250)).collect();
let updated = list.update_priorities_batch(&updates);
assert_eq!(updated, 10);
let entries = list.get_batch(&cids);
assert_eq!(entries.len(), 10);
for entry in entries {
assert_eq!(entry.priority, 250);
}
let removed = list.remove_batch(&cids[0..5]);
assert_eq!(removed.len(), 5);
assert_eq!(list.len(), 5);
}
}