use std::sync::Arc;
use std::time::Duration;
use dashmap::DashMap;
use tokio::sync::Notify;
use crate::proxy::cdc::{CdcEventBuilder, CdcEventType, SharedEventSender};
struct FetchState {
#[allow(dead_code)]
origin_node_id: String,
notify: Arc<Notify>,
}
pub enum CoalesceDecision {
Proceed,
Wait(Arc<Notify>),
}
pub struct DistributedCoalescer {
active_fetches: DashMap<String, FetchState>,
cdc_sender: SharedEventSender,
wait_timeout: Duration,
}
impl DistributedCoalescer {
pub fn new(cdc_sender: SharedEventSender, wait_timeout: Duration) -> Self {
Self {
active_fetches: DashMap::new(),
cdc_sender,
wait_timeout,
}
}
pub fn check(&self, query_key: &str) -> CoalesceDecision {
if let Some(state) = self.active_fetches.get(query_key) {
CoalesceDecision::Wait(state.notify.clone())
} else {
CoalesceDecision::Proceed
}
}
pub fn register_fetch(&self, query_key: &str) {
self.cdc_sender.send(
CdcEventType::FetchStart,
CdcEventBuilder::fetch_start(query_key.to_string()),
);
}
pub fn complete_fetch(&self, query_key: &str) {
if let Some((_, state)) = self.active_fetches.remove(query_key) {
state.notify.notify_waiters();
}
}
pub fn cancel_fetch(&self, query_key: &str) {
self.active_fetches.remove(query_key);
self.cdc_sender.send(
CdcEventType::FetchCancel,
CdcEventBuilder::fetch_cancel(query_key.to_string()),
);
}
pub fn on_remote_fetch_start(&self, query_key: &str, origin_node_id: &str) {
let notify = Arc::new(Notify::new());
self.active_fetches.insert(
query_key.to_string(),
FetchState {
origin_node_id: origin_node_id.to_string(),
notify,
},
);
}
pub fn on_remote_fetch_cancel(&self, query_key: &str) {
if let Some((_, state)) = self.active_fetches.remove(query_key) {
state.notify.notify_waiters();
}
}
pub fn on_remote_insert(&self, query_key: &str) {
if let Some((_, state)) = self.active_fetches.remove(query_key) {
state.notify.notify_waiters();
}
}
pub async fn wait_for_fetch(&self, notify: Arc<Notify>) -> bool {
tokio::select! {
_ = notify.notified() => true,
_ = tokio::time::sleep(self.wait_timeout) => false,
}
}
pub fn active_fetch_count(&self) -> usize {
self.active_fetches.len()
}
pub fn wait_timeout(&self) -> Duration {
self.wait_timeout
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proxy::cdc::EventSender;
fn make_coalescer() -> DistributedCoalescer {
let sender = Arc::new(EventSender::new(100, "test-node".to_string()));
DistributedCoalescer::new(sender, Duration::from_millis(100))
}
#[test]
fn test_check_empty() {
let c = make_coalescer();
assert!(matches!(c.check("q1"), CoalesceDecision::Proceed));
}
#[test]
fn test_check_after_remote_start() {
let c = make_coalescer();
c.on_remote_fetch_start("q1", "node-b");
assert!(matches!(c.check("q1"), CoalesceDecision::Wait(_)));
}
#[test]
fn test_remote_insert_clears_fetch() {
let c = make_coalescer();
c.on_remote_fetch_start("q1", "node-b");
assert_eq!(c.active_fetch_count(), 1);
c.on_remote_insert("q1");
assert_eq!(c.active_fetch_count(), 0);
}
#[test]
fn test_remote_cancel_clears_fetch() {
let c = make_coalescer();
c.on_remote_fetch_start("q1", "node-b");
c.on_remote_fetch_cancel("q1");
assert_eq!(c.active_fetch_count(), 0);
}
#[test]
fn test_complete_fetch_clears() {
let c = make_coalescer();
c.on_remote_fetch_start("q1", "node-b");
c.complete_fetch("q1");
assert_eq!(c.active_fetch_count(), 0);
}
#[tokio::test]
async fn test_wait_for_fetch_timeout() {
let c = make_coalescer();
let notify = Arc::new(Notify::new());
let result = c.wait_for_fetch(notify).await;
assert!(!result);
}
#[tokio::test]
async fn test_wait_for_fetch_notified() {
let c = make_coalescer();
let notify = Arc::new(Notify::new());
let notify_clone = notify.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(10)).await;
notify_clone.notify_waiters();
});
let result = c.wait_for_fetch(notify).await;
assert!(result);
}
#[test]
fn test_register_fetch_publishes_event() {
let c = make_coalescer();
c.register_fetch("q1");
assert_eq!(c.active_fetch_count(), 0);
}
#[test]
fn test_cancel_fetch_publishes_event() {
let c = make_coalescer();
c.on_remote_fetch_start("q1", "node-b");
assert_eq!(c.active_fetch_count(), 1);
c.cancel_fetch("q1");
assert_eq!(c.active_fetch_count(), 0);
}
#[test]
fn test_cancel_fetch_nonexistent_key() {
let c = make_coalescer();
c.cancel_fetch("nonexistent");
assert_eq!(c.active_fetch_count(), 0);
}
#[test]
fn test_wait_timeout_accessor() {
let c = make_coalescer();
assert_eq!(c.wait_timeout(), Duration::from_millis(100));
}
#[test]
fn test_multiple_keys_tracked_independently() {
let c = make_coalescer();
c.on_remote_fetch_start("q1", "node-b");
c.on_remote_fetch_start("q2", "node-c");
assert_eq!(c.active_fetch_count(), 2);
c.complete_fetch("q1");
assert_eq!(c.active_fetch_count(), 1);
assert!(matches!(c.check("q1"), CoalesceDecision::Proceed));
assert!(matches!(c.check("q2"), CoalesceDecision::Wait(_)));
}
#[test]
fn test_overwrite_same_key_different_origin() {
let c = make_coalescer();
c.on_remote_fetch_start("q1", "node-b");
assert_eq!(c.active_fetch_count(), 1);
c.on_remote_fetch_start("q1", "node-c");
assert_eq!(
c.active_fetch_count(),
1,
"same key should not double-count"
);
}
#[test]
fn test_on_remote_insert_unknown_key_no_panic() {
let c = make_coalescer();
c.on_remote_insert("never-registered");
assert_eq!(c.active_fetch_count(), 0);
}
#[test]
fn test_on_remote_cancel_unknown_key_no_panic() {
let c = make_coalescer();
c.on_remote_fetch_cancel("never-registered");
assert_eq!(c.active_fetch_count(), 0);
}
#[test]
fn test_complete_fetch_unknown_key_no_panic() {
let c = make_coalescer();
c.complete_fetch("never-registered");
assert_eq!(c.active_fetch_count(), 0);
}
#[test]
fn test_check_after_complete_returns_proceed() {
let c = make_coalescer();
c.on_remote_fetch_start("q1", "node-b");
assert!(matches!(c.check("q1"), CoalesceDecision::Wait(_)));
c.complete_fetch("q1");
assert!(matches!(c.check("q1"), CoalesceDecision::Proceed));
}
#[tokio::test]
async fn test_concurrent_waits_all_resolve_on_complete() {
let c = make_coalescer();
c.on_remote_fetch_start("q1", "node-b");
let CoalesceDecision::Wait(notify) = c.check("q1") else {
panic!("expected Wait");
};
let n1 = notify.clone();
let n2 = notify.clone();
let h1 = tokio::spawn(async move { n1.notified().await });
let h2 = tokio::spawn(async move { n2.notified().await });
tokio::time::sleep(Duration::from_millis(20)).await;
c.complete_fetch("q1");
let r1 = tokio::time::timeout(Duration::from_millis(500), h1).await;
let r2 = tokio::time::timeout(Duration::from_millis(500), h2).await;
assert!(r1.is_ok(), "waiter 1 should resolve");
assert!(r2.is_ok(), "waiter 2 should resolve");
}
}