use parking_lot::{Mutex, RwLock};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Instant;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum CommsEvent {
#[serde(rename = "tuple_sample")]
TupleSample {
rule_id: String,
node_id: String,
data: Value,
timestamp: i64,
dropped_since_last: u64,
},
#[serde(rename = "connector_status")]
ConnectorStatus {
id: String,
kind: String,
status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
error: Option<String>,
},
#[serde(rename = "heartbeat")]
Heartbeat { timestamp: i64, active_taps: usize },
#[serde(rename = "alert")]
Alert {
source: String,
message: String,
level: String,
timestamp: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
details: Option<Value>,
},
}
struct ProbeRateLimiter {
tokens: f64,
last_check: Instant,
dropped: u64,
}
impl ProbeRateLimiter {
fn new(_rate_per_sec: f64, burst: f64) -> Self {
Self {
tokens: burst,
last_check: Instant::now(),
dropped: 0,
}
}
fn try_acquire(&mut self, rate_per_sec: f64, burst: f64) -> Option<u64> {
let now = Instant::now();
let elapsed = now.duration_since(self.last_check).as_secs_f64();
self.last_check = now;
self.tokens = (self.tokens + elapsed * rate_per_sec).min(burst);
if self.tokens >= 1.0 {
self.tokens -= 1.0;
let dropped = self.dropped;
self.dropped = 0;
Some(dropped)
} else {
self.dropped = self.dropped.saturating_add(1);
None
}
}
}
pub struct WireTapProbe {
pub active: Arc<AtomicBool>,
limiter: Mutex<ProbeRateLimiter>,
}
impl WireTapProbe {
pub fn new() -> Self {
Self {
active: Arc::new(AtomicBool::new(false)),
limiter: Mutex::new(ProbeRateLimiter::new(20.0, 20.0)),
}
}
#[inline]
pub fn is_active(&self) -> bool {
self.active.load(Ordering::Relaxed)
}
}
impl Default for WireTapProbe {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub struct CommsHub {
tx: tokio::sync::broadcast::Sender<CommsEvent>,
taps: Arc<RwLock<HashMap<String, Arc<WireTapProbe>>>>,
}
impl Default for CommsHub {
fn default() -> Self {
Self::new(2048)
}
}
impl CommsHub {
pub fn new(capacity: usize) -> Self {
let (tx, _) = tokio::sync::broadcast::channel(capacity.max(64));
Self {
tx,
taps: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<CommsEvent> {
self.tx.subscribe()
}
pub fn publish(&self, event: CommsEvent) {
let _ = self.tx.send(event);
}
pub fn publish_connector_status(
&self,
id: impl Into<String>,
kind: impl Into<String>,
status: impl Into<String>,
error: Option<String>,
) {
self.publish(CommsEvent::ConnectorStatus {
id: id.into(),
kind: kind.into(),
status: status.into(),
error,
});
}
pub fn publish_alert(
&self,
source: impl Into<String>,
message: impl Into<String>,
level: impl Into<String>,
details: Option<Value>,
) {
self.publish(CommsEvent::Alert {
source: source.into(),
message: message.into(),
level: level.into(),
timestamp: chrono::Utc::now().timestamp(),
details,
});
}
#[inline]
pub fn receiver_count(&self) -> usize {
self.tx.receiver_count()
}
pub fn get_or_create_probe(&self, rule_id: &str, node_id: &str) -> Arc<WireTapProbe> {
let key = format!("{}:{}", rule_id, node_id);
{
let guard = self.taps.read();
if let Some(probe) = guard.get(&key) {
return probe.clone();
}
}
let mut guard = self.taps.write();
guard
.entry(key)
.or_insert_with(|| Arc::new(WireTapProbe::new()))
.clone()
}
pub fn enable_tap(&self, rule_id: &str, node_id: &str) -> Arc<WireTapProbe> {
let probe = self.get_or_create_probe(rule_id, node_id);
probe.active.store(true, Ordering::Release);
probe
}
pub fn disable_tap(&self, rule_id: &str, node_id: &str) {
let key = format!("{}:{}", rule_id, node_id);
if let Some(probe) = self.taps.read().get(&key) {
probe.active.store(false, Ordering::Release);
}
}
#[inline]
pub fn is_tap_active(&self, rule_id: &str, node_id: &str) -> bool {
let key = format!("{}:{}", rule_id, node_id);
self.taps
.read()
.get(&key)
.map(|p| p.is_active())
.unwrap_or(false)
}
pub fn active_tap_count(&self) -> usize {
self.taps.read().values().filter(|p| p.is_active()).count()
}
#[inline]
pub fn maybe_sample(
&self,
rule_id: &str,
node_id: &str,
data: &impl serde::Serialize,
timestamp: i64,
) {
if self.tx.receiver_count() == 0 {
return;
}
let key = format!("{}:{}", rule_id, node_id);
let probe = {
let guard = self.taps.read();
match guard.get(&key) {
Some(p) if p.is_active() => p.clone(),
_ => return,
}
};
let dropped_opt = {
let mut limiter = probe.limiter.lock();
limiter.try_acquire(20.0, 20.0)
};
if let Some(dropped) = dropped_opt {
let event = CommsEvent::TupleSample {
rule_id: rule_id.to_string(),
node_id: node_id.to_string(),
data: serde_json::to_value(data).unwrap_or(Value::Null),
timestamp,
dropped_since_last: dropped,
};
let _ = self.tx.send(event);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wiretap_lifecycle_and_rate_limiter() {
let hub = CommsHub::new(64);
assert!(!hub.is_tap_active("rule1", "node_a"));
assert_eq!(hub.active_tap_count(), 0);
hub.enable_tap("rule1", "node_a");
assert!(hub.is_tap_active("rule1", "node_a"));
assert_eq!(hub.active_tap_count(), 1);
hub.disable_tap("rule1", "node_a");
assert!(!hub.is_tap_active("rule1", "node_a"));
assert_eq!(hub.active_tap_count(), 0);
}
#[tokio::test]
async fn test_comms_hub_broadcast_and_sample() {
let hub = CommsHub::new(64);
let mut rx = hub.subscribe();
hub.enable_tap("rule_test", "filter_1");
let val = serde_json::json!({"temp": 42.5});
hub.maybe_sample("rule_test", "filter_1", &val, 1000);
let received = rx.recv().await.expect("receive event");
match received {
CommsEvent::TupleSample {
rule_id,
node_id,
data,
timestamp,
dropped_since_last,
} => {
assert_eq!(rule_id, "rule_test");
assert_eq!(node_id, "filter_1");
assert_eq!(data, val);
assert_eq!(timestamp, 1000);
assert_eq!(dropped_since_last, 0);
}
other => panic!("Unexpected event: {:?}", other),
}
}
}