use bytes::Bytes;
use dashmap::DashMap;
use ipfrs_core::Cid;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{broadcast, RwLock};
#[derive(Debug, Clone)]
pub struct CoalescerConfig {
pub coalesce_window: Duration,
pub max_waiters_per_request: usize,
pub broadcast_capacity: usize,
pub enable_stats: bool,
}
impl Default for CoalescerConfig {
fn default() -> Self {
Self {
coalesce_window: Duration::from_millis(10),
max_waiters_per_request: 100,
broadcast_capacity: 128,
enable_stats: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CoalescerStats {
pub total_requests: u64,
pub coalesced_requests: u64,
pub unique_requests: u64,
pub avg_waiters_per_request: f64,
pub max_waiters_seen: usize,
}
impl CoalescerStats {
pub fn efficiency(&self) -> f64 {
if self.total_requests == 0 {
return 0.0;
}
self.coalesced_requests as f64 / self.total_requests as f64
}
pub fn reduction_ratio(&self) -> f64 {
if self.total_requests == 0 {
return 0.0;
}
1.0 - (self.unique_requests as f64 / self.total_requests as f64)
}
}
struct PendingRequest {
tx: broadcast::Sender<Result<Bytes, String>>,
created_at: Instant,
waiter_count: usize,
}
pub struct RequestCoalescer {
config: CoalescerConfig,
pending: Arc<DashMap<Cid, PendingRequest>>,
stats: Arc<RwLock<CoalescerStats>>,
}
impl RequestCoalescer {
pub fn new(config: CoalescerConfig) -> Self {
Self {
config,
pending: Arc::new(DashMap::new()),
stats: Arc::new(RwLock::new(CoalescerStats::default())),
}
}
pub async fn register_request(
&self,
cid: &Cid,
) -> Result<Option<broadcast::Receiver<Result<Bytes, String>>>, String> {
if self.config.enable_stats {
let mut stats = self.stats.write().await;
stats.total_requests += 1;
}
if let Some(mut entry) = self.pending.get_mut(cid) {
entry.waiter_count += 1;
let rx = entry.tx.subscribe();
if self.config.enable_stats {
let mut stats = self.stats.write().await;
stats.coalesced_requests += 1;
if entry.waiter_count > stats.max_waiters_seen {
stats.max_waiters_seen = entry.waiter_count;
}
}
if entry.waiter_count >= self.config.max_waiters_per_request {
drop(entry); self.pending.remove(cid);
return Ok(None); }
return Ok(Some(rx));
}
let (tx, _rx) = broadcast::channel(self.config.broadcast_capacity);
self.pending.insert(
*cid,
PendingRequest {
tx,
created_at: Instant::now(),
waiter_count: 1,
},
);
if self.config.enable_stats {
let mut stats = self.stats.write().await;
stats.unique_requests += 1;
}
Ok(None) }
pub async fn complete_request(&self, cid: &Cid, data: Bytes) {
if let Some((_, pending)) = self.pending.remove(cid) {
let waiter_count = pending.waiter_count;
let _ = pending.tx.send(Ok(data));
if self.config.enable_stats && waiter_count > 1 {
let mut stats = self.stats.write().await;
let total_waiters = stats.avg_waiters_per_request * stats.coalesced_requests as f64;
stats.avg_waiters_per_request =
(total_waiters + waiter_count as f64) / (stats.coalesced_requests + 1) as f64;
}
}
}
pub async fn fail_request(&self, cid: &Cid, error: String) {
if let Some((_, pending)) = self.pending.remove(cid) {
let _ = pending.tx.send(Err(error));
}
}
pub async fn cancel_request(&self, cid: &Cid) {
self.pending.remove(cid);
}
pub async fn stats(&self) -> CoalescerStats {
self.stats.read().await.clone()
}
pub async fn reset_stats(&self) {
let mut stats = self.stats.write().await;
*stats = CoalescerStats::default();
}
pub async fn cleanup_expired(&self) {
let timeout = self.config.coalesce_window * 10; self.pending.retain(|_, pending| {
if pending.created_at.elapsed() > timeout {
let _ = pending
.tx
.send(Err("Request timeout during coalescing".to_string()));
false
} else {
true
}
});
}
pub fn pending_count(&self) -> usize {
self.pending.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_cid(seed: u64) -> Cid {
use multihash::Multihash;
let data = seed.to_le_bytes();
let hash = Multihash::wrap(0x12, &data).expect("test: wrap multihash from seed bytes");
Cid::new_v1(0x55, hash)
}
#[tokio::test]
async fn test_request_coalescer_creation() {
let config = CoalescerConfig::default();
let coalescer = RequestCoalescer::new(config);
assert_eq!(coalescer.pending_count(), 0);
}
#[tokio::test]
async fn test_first_request_returns_none() {
let coalescer = RequestCoalescer::new(CoalescerConfig::default());
let cid = test_cid(1);
let result = coalescer
.register_request(&cid)
.await
.expect("test: register request");
assert!(result.is_none()); assert_eq!(coalescer.pending_count(), 1);
}
#[tokio::test]
async fn test_duplicate_request_returns_receiver() {
let coalescer = RequestCoalescer::new(CoalescerConfig::default());
let cid = test_cid(1);
let first = coalescer
.register_request(&cid)
.await
.expect("test: register request");
assert!(first.is_none());
let second = coalescer
.register_request(&cid)
.await
.expect("test: register request");
assert!(second.is_some());
assert_eq!(coalescer.pending_count(), 1); }
#[tokio::test]
async fn test_complete_request_broadcasts_to_waiters() {
let coalescer = RequestCoalescer::new(CoalescerConfig::default());
let cid = test_cid(1);
let first = coalescer
.register_request(&cid)
.await
.expect("test: register request");
assert!(first.is_none());
let mut second_rx = coalescer
.register_request(&cid)
.await
.expect("test: register request")
.expect("test: receiver should be Some");
let mut third_rx = coalescer
.register_request(&cid)
.await
.expect("test: register request")
.expect("test: receiver should be Some");
let data = Bytes::from("test data");
coalescer.complete_request(&cid, data.clone()).await;
let result2 = second_rx
.recv()
.await
.expect("test: receive result")
.expect("test: inner result");
let result3 = third_rx
.recv()
.await
.expect("test: receive result")
.expect("test: inner result");
assert_eq!(result2, data);
assert_eq!(result3, data);
assert_eq!(coalescer.pending_count(), 0);
}
#[tokio::test]
async fn test_fail_request_broadcasts_error() {
let coalescer = RequestCoalescer::new(CoalescerConfig::default());
let cid = test_cid(1);
coalescer
.register_request(&cid)
.await
.expect("test: register request");
let mut rx = coalescer
.register_request(&cid)
.await
.expect("test: register request")
.expect("test: receiver should be Some");
coalescer
.fail_request(&cid, "Network error".to_string())
.await;
let result = rx.recv().await.expect("test: receive result");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Network error");
assert_eq!(coalescer.pending_count(), 0);
}
#[tokio::test]
async fn test_cancel_request_removes_pending() {
let coalescer = RequestCoalescer::new(CoalescerConfig::default());
let cid = test_cid(1);
coalescer
.register_request(&cid)
.await
.expect("test: register request");
assert_eq!(coalescer.pending_count(), 1);
coalescer.cancel_request(&cid).await;
assert_eq!(coalescer.pending_count(), 0);
}
#[tokio::test]
async fn test_stats_tracking() {
let config = CoalescerConfig {
enable_stats: true,
..Default::default()
};
let coalescer = RequestCoalescer::new(config);
let cid = test_cid(1);
coalescer
.register_request(&cid)
.await
.expect("test: register request");
coalescer
.register_request(&cid)
.await
.expect("test: register request");
coalescer
.register_request(&cid)
.await
.expect("test: register request");
let stats = coalescer.stats().await;
assert_eq!(stats.total_requests, 3);
assert_eq!(stats.unique_requests, 1);
assert_eq!(stats.coalesced_requests, 2);
}
#[tokio::test]
async fn test_efficiency_calculation() {
let stats = CoalescerStats {
total_requests: 100,
coalesced_requests: 80,
unique_requests: 20,
avg_waiters_per_request: 4.0,
max_waiters_seen: 10,
};
assert_eq!(stats.efficiency(), 0.8); assert_eq!(stats.reduction_ratio(), 0.8); }
#[tokio::test]
async fn test_max_waiters_limit() {
let config = CoalescerConfig {
max_waiters_per_request: 3,
..Default::default()
};
let coalescer = RequestCoalescer::new(config);
let cid = test_cid(1);
coalescer
.register_request(&cid)
.await
.expect("test: register request");
coalescer
.register_request(&cid)
.await
.expect("test: register request");
coalescer
.register_request(&cid)
.await
.expect("test: register request");
let result = coalescer
.register_request(&cid)
.await
.expect("test: register request");
assert!(result.is_none());
}
#[tokio::test]
async fn test_reset_stats() {
let coalescer = RequestCoalescer::new(CoalescerConfig::default());
let cid = test_cid(1);
coalescer
.register_request(&cid)
.await
.expect("test: register request");
coalescer
.register_request(&cid)
.await
.expect("test: register request");
let stats = coalescer.stats().await;
assert!(stats.total_requests > 0);
coalescer.reset_stats().await;
let stats = coalescer.stats().await;
assert_eq!(stats.total_requests, 0);
}
#[tokio::test]
async fn test_concurrent_different_cids() {
let coalescer = RequestCoalescer::new(CoalescerConfig::default());
let cid1 = test_cid(1);
let cid2 = test_cid(2);
let r1 = coalescer
.register_request(&cid1)
.await
.expect("test: register request");
let r2 = coalescer
.register_request(&cid2)
.await
.expect("test: register request");
assert!(r1.is_none());
assert!(r2.is_none());
assert_eq!(coalescer.pending_count(), 2);
}
}