use std::cell::Cell;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Default, Clone, Copy)]
struct LocalBatch {
next: u64,
end: u64,
}
impl LocalBatch {
const EMPTY: Self = Self { next: 0, end: 0 };
#[inline]
fn is_exhausted(&self) -> bool {
self.next >= self.end
}
}
thread_local! {
static LOCAL_READ_TS: Cell<(usize, LocalBatch)> = const {
Cell::new((0, LocalBatch::EMPTY))
};
}
#[derive(Debug)]
pub struct ReadTsBatcher {
shared: AtomicU64,
batch_size: u64,
}
impl ReadTsBatcher {
#[must_use]
pub const fn new(start: u64, batch_size: u64) -> Self {
let b = if batch_size == 0 { 1 } else { batch_size };
Self {
shared: AtomicU64::new(start),
batch_size: b,
}
}
#[must_use]
pub const fn with_default() -> Self {
Self::new(1, 64)
}
pub fn next_read_ts(&self) -> u64 {
let self_key = std::ptr::from_ref::<Self>(self) as usize;
LOCAL_READ_TS.with(|slot| {
let (key, mut batch) = slot.get();
if key != self_key || batch.is_exhausted() {
batch = self.reserve_batch();
}
debug_assert!(!batch.is_exhausted());
let v = batch.next;
batch.next = batch.next.saturating_add(1);
slot.set((self_key, batch));
v
})
}
fn reserve_batch(&self) -> LocalBatch {
let start = self.shared.fetch_add(self.batch_size, Ordering::Relaxed);
LocalBatch {
next: start,
end: start.saturating_add(self.batch_size),
}
}
#[must_use]
pub fn watermark(&self) -> u64 {
self.shared.load(Ordering::Relaxed)
}
#[must_use]
pub const fn batch_size(&self) -> u64 {
self.batch_size
}
}
impl Default for ReadTsBatcher {
fn default() -> Self {
Self::with_default()
}
}
#[derive(Debug)]
pub struct TidGap {
start: u64,
end: u64,
next: Cell<u64>,
}
impl TidGap {
#[must_use]
pub const fn start(&self) -> u64 {
self.start
}
#[must_use]
pub const fn end(&self) -> u64 {
self.end
}
#[must_use]
pub const fn capacity(&self) -> u64 {
self.end - self.start
}
pub fn next_tid(&self) -> Option<u64> {
let v = self.next.get();
if v >= self.end {
return None;
}
self.next.set(v + 1);
Some(v)
}
#[must_use]
pub fn remaining(&self) -> u64 {
self.end.saturating_sub(self.next.get())
}
}
#[derive(Debug)]
pub struct TidGapAllocator {
shared: AtomicU64,
gap_size: u64,
}
impl TidGapAllocator {
#[must_use]
pub const fn new(start: u64, gap_size: u64) -> Self {
let g = if gap_size == 0 { 1 } else { gap_size };
Self {
shared: AtomicU64::new(start),
gap_size: g,
}
}
#[must_use]
pub const fn with_default() -> Self {
Self::new(1, 16)
}
#[must_use]
pub fn reserve_gap(&self) -> TidGap {
let start = self.shared.fetch_add(self.gap_size, Ordering::Relaxed);
let end = start.saturating_add(self.gap_size);
TidGap {
start,
end,
next: Cell::new(start),
}
}
#[must_use]
pub fn watermark(&self) -> u64 {
self.shared.load(Ordering::Relaxed)
}
#[must_use]
pub const fn gap_size(&self) -> u64 {
self.gap_size
}
}
impl Default for TidGapAllocator {
fn default() -> Self {
Self::with_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::mpsc;
use std::thread;
#[test]
fn cicada_single_thread_unique_monotonic() {
let batcher = ReadTsBatcher::new(1, 16);
let mut last: u64 = 0;
let mut seen: HashSet<u64> = HashSet::with_capacity(1000);
for _ in 0..1000 {
let v = batcher.next_read_ts();
assert!(
v > last,
"expected strictly monotonic, got {v} after {last}"
);
assert!(seen.insert(v), "duplicate read-ts value {v}");
last = v;
}
assert_eq!(seen.len(), 1000);
let max = *seen.iter().max().unwrap();
assert!(max >= 1000, "max value {max} unexpectedly small");
}
#[test]
fn cicada_multi_thread_unique() {
let batcher = Arc::new(ReadTsBatcher::new(1, 16));
let (tx, rx) = mpsc::channel::<Vec<u64>>();
let threads: Vec<_> = (0..4)
.map(|_| {
let b = Arc::clone(&batcher);
let tx = tx.clone();
thread::spawn(move || {
let mut local = Vec::with_capacity(250);
for _ in 0..250 {
local.push(b.next_read_ts());
}
tx.send(local).unwrap();
})
})
.collect();
drop(tx);
for t in threads {
t.join().unwrap();
}
let mut all: HashSet<u64> = HashSet::with_capacity(1000);
while let Ok(batch) = rx.recv() {
assert_eq!(batch.len(), 250);
for pair in batch.windows(2) {
assert!(
pair[0] < pair[1],
"non-monotonic within thread: {} then {}",
pair[0],
pair[1]
);
}
for v in batch {
assert!(all.insert(v), "duplicate read-ts across threads: {v}");
}
}
assert_eq!(
all.len(),
1000,
"expected 1000 unique values, got {}",
all.len()
);
}
#[test]
fn cicada_zero_batch_clamped() {
let batcher = ReadTsBatcher::new(100, 0);
assert_eq!(batcher.batch_size(), 1);
let a = batcher.next_read_ts();
let b = batcher.next_read_ts();
assert!(b > a);
}
#[test]
fn hekaton_ten_gaps_non_overlapping() {
let alloc = TidGapAllocator::new(1, 8);
let gaps: Vec<TidGap> = (0..10).map(|_| alloc.reserve_gap()).collect();
let mut ranges: Vec<(u64, u64)> = gaps.iter().map(|g| (g.start(), g.end())).collect();
ranges.sort_by_key(|&(s, _)| s);
for pair in ranges.windows(2) {
let (_, e0) = pair[0];
let (s1, _) = pair[1];
assert!(e0 <= s1, "overlapping gaps: [_, {e0}) and [{s1}, _)");
}
for g in &gaps {
assert_eq!(g.capacity(), 8);
let mut tids = Vec::new();
while let Some(t) = g.next_tid() {
tids.push(t);
}
assert_eq!(tids.len(), 8);
for w in tids.windows(2) {
assert!(w[0] < w[1]);
}
}
assert_eq!(gaps[0].next_tid(), None);
}
#[test]
fn hekaton_multi_thread_non_overlapping() {
let alloc = Arc::new(TidGapAllocator::new(1, 8));
let (tx, rx) = mpsc::channel::<Vec<(u64, u64)>>();
let threads: Vec<_> = (0..4)
.map(|_| {
let a = Arc::clone(&alloc);
let tx = tx.clone();
thread::spawn(move || {
let mut local = Vec::with_capacity(25);
for _ in 0..25 {
let g = a.reserve_gap();
local.push((g.start(), g.end()));
}
tx.send(local).unwrap();
})
})
.collect();
drop(tx);
for t in threads {
t.join().unwrap();
}
let mut all: Vec<(u64, u64)> = Vec::with_capacity(100);
while let Ok(batch) = rx.recv() {
assert_eq!(batch.len(), 25);
all.extend(batch);
}
assert_eq!(all.len(), 100);
all.sort_by_key(|&(s, _)| s);
for pair in all.windows(2) {
let (s0, e0) = pair[0];
let (s1, e1) = pair[1];
assert!(
e0 <= s1,
"overlapping gaps in multi-thread trace: [{s0}, {e0}) and [{s1}, {e1})",
);
assert_eq!(e0 - s0, 8);
}
let starts: HashSet<u64> = all.iter().map(|&(s, _)| s).collect();
assert_eq!(starts.len(), 100);
}
#[test]
fn hekaton_gap_exhaustion() {
let alloc = TidGapAllocator::new(1000, 3);
let g = alloc.reserve_gap();
assert_eq!(g.start(), 1000);
assert_eq!(g.end(), 1003);
assert_eq!(g.next_tid(), Some(1000));
assert_eq!(g.next_tid(), Some(1001));
assert_eq!(g.next_tid(), Some(1002));
assert_eq!(g.next_tid(), None);
assert_eq!(g.next_tid(), None); assert_eq!(g.remaining(), 0);
}
#[test]
fn hekaton_zero_gap_clamped() {
let alloc = TidGapAllocator::new(5, 0);
assert_eq!(alloc.gap_size(), 1);
let g = alloc.reserve_gap();
assert_eq!(g.capacity(), 1);
assert_eq!(g.next_tid(), Some(5));
assert_eq!(g.next_tid(), None);
}
}