use std::cell::{Cell, UnsafeCell};
use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex, OnceLock};
use std::thread;
use crate::error::TicklogError;
use crate::ring::RingBuffer;
const INITIAL_SCRATCH: usize = 4096;
pub(crate) struct ThreadBuf {
pub(crate) ring: Arc<RingBuffer>,
#[allow(unused)]
pub(crate) thread_id: u64,
#[allow(unused)]
pub(crate) thread_name: Option<String>,
pub(crate) scratch: Vec<u8>,
}
impl Drop for ThreadBuf {
fn drop(&mut self) {
self.ring.live.store(false, Ordering::Release);
}
}
fn get_stable_thread_id() -> u64 {
let id = thread::current().id();
let id_str = format!("{id:?}"); id_str
.strip_prefix("ThreadId(")
.and_then(|s| s.strip_suffix(')'))
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0)
}
struct ThreadSlot {
active: Cell<bool>,
buf: UnsafeCell<Option<ThreadBuf>>,
}
thread_local! {
static THREAD_SLOT: ThreadSlot = const {
ThreadSlot {
active: Cell::new(false),
buf: UnsafeCell::new(None),
}
};
}
struct ActiveGuard<'a>(&'a Cell<bool>);
impl Drop for ActiveGuard<'_> {
fn drop(&mut self) {
self.0.set(false);
}
}
pub(crate) fn with_thread_buf<F, R>(f: F) -> Option<R>
where
F: FnOnce(&mut ThreadBuf) -> R,
{
THREAD_SLOT.with(|slot| {
if slot.active.get() {
return None;
}
slot.active.set(true);
let _active = ActiveGuard(&slot.active);
let opt = unsafe { &mut *slot.buf.get() };
if opt.is_none() {
let ring = Arc::new(RingBuffer::new());
register_ring(Arc::clone(&ring));
*opt = Some(ThreadBuf {
ring,
thread_id: get_stable_thread_id(),
thread_name: thread::current().name().map(String::from),
scratch: Vec::with_capacity(INITIAL_SCRATCH),
});
}
Some(f(unsafe { opt.as_mut().unwrap_unchecked() }))
})
}
pub(crate) static REGISTRY: OnceLock<Mutex<Vec<Arc<RingBuffer>>>> = OnceLock::new();
pub(crate) fn register_ring(ring: Arc<RingBuffer>) {
let mut rings = REGISTRY
.get()
.expect("invariant: ring registry not initialized; call ticklog::builder().build() before logging")
.lock()
.expect("invariant: ring registry mutex poisoned by a panic in another thread");
rings.push(ring);
}
pub fn warm_up() -> Result<(), TicklogError> {
if REGISTRY.get().is_none() {
return Err(TicklogError::NotInitialized);
}
with_thread_buf(|_| {});
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn init_registry() {
let _ = REGISTRY.set(Mutex::new(Vec::new()));
}
#[test]
fn get_stable_thread_id_returns_nonzero() {
let id = get_stable_thread_id();
assert!(id > 0, "expected non-zero thread id, got {id}");
}
#[test]
fn thread_buf_holds_ring_and_metadata() {
let ring = Arc::new(RingBuffer::new());
let tb = ThreadBuf {
ring: Arc::clone(&ring),
thread_id: 42,
thread_name: Some("test-thread".into()),
scratch: Vec::new(),
};
assert_eq!(tb.thread_id, 42);
assert_eq!(tb.thread_name.as_deref(), Some("test-thread"));
assert!(tb.ring.live.load(Ordering::Relaxed));
}
#[test]
fn drop_sets_live_to_false() {
let ring = Arc::new(RingBuffer::new());
let tb = ThreadBuf {
ring: Arc::clone(&ring),
thread_id: 1,
thread_name: None,
scratch: Vec::new(),
};
assert!(ring.live.load(Ordering::Relaxed));
drop(tb);
assert!(!ring.live.load(Ordering::Relaxed));
}
#[test]
fn buffer_survives_thread_buf_drop_when_other_arcs_exist() {
let ring = Arc::new(RingBuffer::new());
let other = Arc::clone(&ring);
let tb = ThreadBuf {
ring: Arc::clone(&ring),
thread_id: 1,
thread_name: None,
scratch: Vec::new(),
};
drop(tb);
assert!(!ring.live.load(Ordering::Relaxed));
let still_here = Arc::clone(&other);
drop(still_here);
drop(ring);
drop(other);
}
#[test]
fn warm_up_and_with_thread_buf_lifecycle() {
init_registry();
warm_up().unwrap();
warm_up().unwrap();
with_thread_buf(|tb| {
assert!(tb.thread_id > 0);
});
}
#[test]
fn register_ring_adds_to_registry() {
init_registry();
let mut rings = REGISTRY.get().unwrap().lock().unwrap();
let count_before = rings.len();
rings.push(Arc::new(RingBuffer::new()));
assert_eq!(rings.len(), count_before + 1);
}
#[test]
fn with_thread_buf_thread_id_matches_current_thread() {
init_registry();
let expected = get_stable_thread_id();
with_thread_buf(|tb| {
assert_eq!(tb.thread_id, expected);
});
}
#[test]
fn reentrant_with_thread_buf_is_refused() {
init_registry();
with_thread_buf(|outer| {
outer.scratch.clear();
outer.scratch.push(0xAA);
with_thread_buf(|inner| inner.scratch.push(0xBB));
outer.scratch.push(0xCC);
});
with_thread_buf(|tb| {
assert_eq!(
tb.scratch,
vec![0xAA, 0xCC],
"re-entrant with_thread_buf must be refused, not run"
);
});
}
}