use std::collections::BTreeMap;
use std::ffi::{c_void, CString};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::Mutex;
use crate::buffer_pool::BufferPool;
use crate::config::GraphConfig;
use crate::status::Error;
pub const LOG_ERROR: i32 = 0;
pub const LOG_WARN: i32 = 1;
pub const LOG_INFO: i32 = 2;
pub const LOG_DEBUG: i32 = 3;
type LogFn = unsafe extern "C" fn(*mut c_void, i32, *const std::ffi::c_char);
struct LogSink {
cb: Option<LogFn>,
user: *mut c_void,
}
unsafe impl Send for LogSink {}
static LOG: Mutex<LogSink> = Mutex::new(LogSink {
cb: None,
user: std::ptr::null_mut(),
});
static LOG_SET: AtomicBool = AtomicBool::new(false);
pub fn set_log_callback(cb: Option<LogFn>, user: *mut c_void) {
let mut sink = LOG.lock().expect("log lock poisoned");
sink.cb = cb;
sink.user = user;
LOG_SET.store(cb.is_some(), Ordering::Release);
}
pub fn log_enabled() -> bool {
LOG_SET.load(Ordering::Acquire)
}
pub fn log(level: i32, msg: &str) {
if !LOG_SET.load(Ordering::Acquire) {
return;
}
let (cb, user) = {
let sink = LOG.lock().expect("log lock poisoned");
(sink.cb, sink.user)
};
if let Some(f) = cb {
if let Ok(c) = CString::new(msg) {
unsafe { f(user, level, c.as_ptr()) };
}
}
}
pub fn log_warn(msg: &str) {
log(LOG_WARN, msg);
}
pub fn log_info(msg: &str) {
log(LOG_INFO, msg);
}
#[derive(Default)]
pub struct CStrArena {
map: Mutex<BTreeMap<String, CString>>,
}
impl CStrArena {
pub fn intern(&self, s: &str) -> *const std::ffi::c_char {
let mut m = self.map.lock().expect("string pool lock poisoned");
let c = m
.entry(s.to_string())
.or_insert_with(|| CString::new(s).unwrap_or_default());
c.as_ptr()
}
}
pub mod last_error {
use std::cell::RefCell;
use std::ffi::{c_char, CString};
thread_local! {
static LAST: RefCell<CString> = RefCell::new(CString::default());
}
pub fn set(msg: &str) {
let c = CString::new(msg).unwrap_or_default();
LAST.with(|l| *l.borrow_mut() = c);
}
pub fn get() -> *const c_char {
LAST.with(|l| l.borrow().as_ptr())
}
pub fn take() -> String {
LAST.with(|l| l.borrow().to_string_lossy().into_owned())
}
}
pub const CLOSE_NORMAL: i32 = 0;
pub const CLOSE_ERROR: i32 = 1;
pub const CLOSE_CANCELLED: i32 = 2;
pub struct GraphShared {
pub config: GraphConfig,
pub(crate) buffer_pool: std::sync::Arc<BufferPool>,
error: Mutex<Option<Error>>,
error_c: CStrArena,
has_error: AtomicBool,
cancelled: AtomicBool,
total_queued: AtomicUsize,
total_queued_bytes: AtomicU64,
counters: Mutex<BTreeMap<String, i64>>,
trace: Option<crate::trace::TraceRing>,
pub strings: CStrArena,
}
impl GraphShared {
pub fn new(config: GraphConfig) -> Self {
Self {
buffer_pool: std::sync::Arc::new(BufferPool::new(config.buffer_pool_max_bytes)),
trace: (config.trace_capacity > 0)
.then(|| crate::trace::TraceRing::new(config.trace_capacity)),
config,
error: Mutex::new(None),
error_c: CStrArena::default(),
has_error: AtomicBool::new(false),
cancelled: AtomicBool::new(false),
total_queued: AtomicUsize::new(0),
total_queued_bytes: AtomicU64::new(0),
counters: Mutex::new(BTreeMap::new()),
strings: CStrArena::default(),
}
}
pub fn record_error(&self, e: Error) {
let msg = e.to_string();
let mut slot = self.error.lock().expect("error lock poisoned");
if slot.is_none() {
*slot = Some(e);
self.has_error.store(true, Ordering::SeqCst);
drop(slot); log(LOG_ERROR, &msg);
} else {
drop(slot);
log(LOG_WARN, &format!("(subsequent error, ignored) {msg}"));
}
}
pub fn reset_run_state(&self) {
*self.error.lock().expect("error lock poisoned") = None;
self.has_error.store(false, Ordering::SeqCst);
self.cancelled.store(false, Ordering::SeqCst);
self.total_queued.store(0, Ordering::Relaxed);
self.total_queued_bytes.store(0, Ordering::Relaxed);
self.counters.lock().expect("counter lock poisoned").clear();
if let Some(trace) = &self.trace {
trace.clear();
}
}
#[inline]
pub fn trace_enabled(&self) -> bool {
self.trace.is_some()
}
#[inline]
pub fn record_trace(
&self,
node: u32,
phase: crate::trace::TracePhase,
start_us: i64,
dur_us: i64,
input_ts: i64,
) {
if let Some(trace) = &self.trace {
trace.record(node, phase, start_us, dur_us, input_ts);
}
}
pub fn trace_snapshot(&self) -> Vec<crate::trace::TraceEvent> {
self.trace
.as_ref()
.map(|t| t.snapshot())
.unwrap_or_default()
}
pub fn has_error(&self) -> bool {
self.has_error.load(Ordering::SeqCst)
}
pub fn first_error(&self) -> Option<Error> {
self.error.lock().expect("error lock poisoned").clone()
}
pub fn error_cstr(&self) -> *const std::ffi::c_char {
let msg = self
.first_error()
.map(|e| e.to_string())
.unwrap_or_default();
self.error_c.intern(&msg)
}
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::SeqCst);
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::SeqCst)
}
pub fn close_reason(&self) -> i32 {
if self.is_cancelled() {
CLOSE_CANCELLED
} else if self.has_error() {
CLOSE_ERROR
} else {
CLOSE_NORMAL
}
}
pub fn on_enqueue(&self, bytes: u64) {
self.total_queued.fetch_add(1, Ordering::Relaxed);
self.total_queued_bytes.fetch_add(bytes, Ordering::Relaxed);
}
pub fn on_dequeue(&self, bytes: u64) {
let _ = self
.total_queued
.try_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
Some(v.saturating_sub(1))
});
let _ = self
.total_queued_bytes
.try_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
Some(v.saturating_sub(bytes))
});
}
pub fn total_queued(&self) -> usize {
self.total_queued.load(Ordering::Relaxed)
}
pub fn total_queued_bytes(&self) -> u64 {
self.total_queued_bytes.load(Ordering::Relaxed)
}
pub fn over_watermark(&self) -> bool {
let c = &self.config;
c.max_queued_packets > 0 && self.total_queued() >= c.max_queued_packets
}
pub fn counter_add(&self, name: &str, delta: i64) {
let mut m = self.counters.lock().expect("counter lock poisoned");
*m.entry(name.to_string()).or_insert(0) += delta;
}
pub fn counter_value(&self, name: &str) -> i64 {
self.counters
.lock()
.expect("counter lock poisoned")
.get(name)
.copied()
.unwrap_or(0)
}
pub fn counter_names(&self) -> Vec<String> {
self.counters
.lock()
.expect("counter lock poisoned")
.keys()
.cloned()
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::GraphConfig;
fn shared() -> GraphShared {
GraphShared::new(GraphConfig::from_yaml("nodes: []").unwrap())
}
#[test]
fn first_error_wins() {
let s = shared();
assert!(!s.has_error());
s.record_error(Error::Kernel("first".into()));
s.record_error(Error::Kernel("second".into()));
assert!(s.has_error());
assert!(s.first_error().unwrap().to_string().contains("first"));
}
#[test]
fn close_reason_priority() {
let s = shared();
assert_eq!(s.close_reason(), CLOSE_NORMAL);
s.record_error(Error::Kernel("x".into()));
assert_eq!(s.close_reason(), CLOSE_ERROR);
s.cancel();
assert_eq!(
s.close_reason(),
CLOSE_CANCELLED,
"cancelled takes priority over error"
);
}
#[test]
fn watermark_counts_packets_and_bytes() {
let cfg = GraphConfig::from_yaml("nodes: []\nmax_queued_packets: 2").unwrap();
let s = GraphShared::new(cfg);
assert!(!s.over_watermark());
s.on_enqueue(10);
assert!(!s.over_watermark());
s.on_enqueue(10);
assert!(
s.over_watermark(),
"reaching the limit counts as over watermark"
);
s.on_dequeue(10);
assert!(!s.over_watermark());
assert_eq!(s.total_queued(), 1);
assert_eq!(s.total_queued_bytes(), 10);
}
#[test]
fn dequeue_never_underflows() {
let s = shared();
s.on_dequeue(999);
assert_eq!(s.total_queued(), 0);
assert_eq!(s.total_queued_bytes(), 0);
}
#[test]
fn counters_accumulate() {
let s = shared();
s.counter_add("frames", 1);
s.counter_add("frames", 2);
assert_eq!(s.counter_value("frames"), 3);
assert_eq!(s.counter_value("nope"), 0);
assert_eq!(s.counter_names(), vec!["frames".to_string()]);
}
#[test]
fn interned_pointers_stay_valid_after_more_inserts() {
let a = CStrArena::default();
let p1 = a.intern("first");
for i in 0..100 {
a.intern(&format!("filler{i}"));
}
let s = unsafe { std::ffi::CStr::from_ptr(p1) };
assert_eq!(
s.to_str().unwrap(),
"first",
"interned pointer must stay valid after subsequent inserts"
);
}
}