pub mod slot;
use std::hint;
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;
use std::time::Duration;
use slot::Slot;
use crate::config::QueueFullPolicy;
use crate::error::AppendError;
#[repr(align(64))]
pub(crate) struct CachePadded<T> {
pub(crate) inner: T,
}
impl<T> CachePadded<T> {
pub(crate) fn new(val: T) -> Self {
Self { inner: val }
}
}
pub struct Ring {
slots: Box<[Slot]>,
mask: u64,
ring_size: u64,
pub(crate) producer_cursor: CachePadded<AtomicU64>,
pub(crate) sealed_cursor: AtomicU64,
pub(crate) committed_cursor: AtomicU64,
pub(crate) durable_cursor: AtomicU64,
hash_enabled: bool,
}
impl Ring {
pub fn new(ring_size: usize, hash_enabled: bool, initial: u64) -> Self {
assert!(ring_size.is_power_of_two() && ring_size >= 16);
let ring_size_u64 = ring_size as u64;
Self {
slots: (0..ring_size)
.map(|_| Slot::new())
.collect::<Vec<_>>()
.into_boxed_slice(),
mask: ring_size_u64 - 1,
ring_size: ring_size_u64,
producer_cursor: CachePadded::new(AtomicU64::new(initial)),
sealed_cursor: AtomicU64::new(initial),
committed_cursor: AtomicU64::new(initial),
durable_cursor: AtomicU64::new(initial),
hash_enabled,
}
}
#[inline]
pub fn ring_size(&self) -> usize {
self.ring_size as usize
}
#[inline]
pub fn producer_cursor_value(&self) -> u64 {
self.producer_cursor.inner.load(Ordering::Acquire)
}
#[inline]
#[doc(hidden)]
pub fn set_committed_cursor(&self, val: u64) {
self.committed_cursor.store(val, Ordering::Release);
}
#[inline]
#[doc(hidden)]
pub fn set_sealed_cursor(&self, val: u64) {
self.sealed_cursor.store(val, Ordering::Release);
}
#[inline]
pub fn consume_watermark(&self) -> u64 {
let committed = self.committed_cursor.load(Ordering::Acquire);
if self.hash_enabled {
let sealed = self.sealed_cursor.load(Ordering::Acquire);
committed.min(sealed)
} else {
committed
}
}
#[inline]
pub fn claim(&self, policy: QueueFullPolicy) -> Result<u64, AppendError> {
let mut spins: u32 = 0;
loop {
let seq = self.producer_cursor.inner.load(Ordering::Acquire);
let wm = self.consume_watermark();
if seq.wrapping_sub(wm) >= self.ring_size {
match policy {
QueueFullPolicy::Drop => {
return Err(AppendError::QueueFull);
}
QueueFullPolicy::Block => {
backoff(&mut spins);
continue;
}
}
}
match self.producer_cursor.inner.compare_exchange_weak(
seq,
seq + 1,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => {
return Ok(seq);
}
Err(_) => {
hint::spin_loop();
continue;
}
}
}
}
#[inline]
pub fn slot(&self, seq: u64) -> &Slot {
&self.slots[(seq & self.mask) as usize]
}
pub fn claim_batch(&self, n: u64, policy: QueueFullPolicy) -> Result<u64, AppendError> {
assert!(n >= 1, "claim_batch requires n >= 1");
if n > self.ring_size {
return Err(AppendError::QueueFull);
}
let mut spins: u32 = 0;
loop {
let seq = self.producer_cursor.inner.load(Ordering::Acquire);
let wm = self.consume_watermark();
let in_flight = seq.wrapping_sub(wm);
if in_flight + n > self.ring_size {
match policy {
QueueFullPolicy::Drop => return Err(AppendError::QueueFull),
QueueFullPolicy::Block => {
backoff(&mut spins);
continue;
}
}
}
match self.producer_cursor.inner.compare_exchange_weak(
seq,
seq + n,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return Ok(seq),
Err(_) => {
hint::spin_loop();
continue;
}
}
}
}
#[inline]
pub fn highest_published_contiguous(&self, from_seq: u64) -> u64 {
let mut seq = from_seq;
loop {
if !self.slot(seq).is_published(seq) {
return seq.wrapping_sub(1);
}
seq = seq.wrapping_add(1);
}
}
#[inline]
pub fn hash_enabled(&self) -> bool {
self.hash_enabled
}
}
#[inline]
fn backoff(spins: &mut u32) {
*spins = spins.saturating_add(1);
if *spins <= 64 {
hint::spin_loop();
} else if *spins <= 256 {
thread::yield_now();
} else {
thread::sleep(Duration::from_micros(100));
*spins = 128;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::thread;
#[test]
fn new_ring_cursors_equal_initial() {
let ring = Ring::new(16, false, 0);
assert_eq!(ring.producer_cursor_value(), 0);
assert_eq!(ring.consume_watermark(), 0);
}
#[test]
fn new_ring_with_nonzero_initial() {
let ring = Ring::new(16, false, 100);
assert_eq!(ring.producer_cursor_value(), 100);
assert_eq!(ring.consume_watermark(), 100);
}
#[test]
fn claim_advances_cursor() {
let ring = Ring::new(16, false, 0);
let seq0 = ring.claim(QueueFullPolicy::Block).unwrap();
assert_eq!(seq0, 0);
assert_eq!(ring.producer_cursor_value(), 1);
let seq1 = ring.claim(QueueFullPolicy::Block).unwrap();
assert_eq!(seq1, 1);
assert_eq!(ring.producer_cursor_value(), 2);
}
#[test]
fn claim_returns_queue_full_when_drop() {
let ring = Ring::new(16, false, 0);
for i in 0..16 {
let seq = ring.claim(QueueFullPolicy::Block).unwrap();
assert_eq!(seq, i);
}
let err = ring.claim(QueueFullPolicy::Drop).unwrap_err();
assert_eq!(err, AppendError::QueueFull);
}
#[test]
fn claim_unblocks_after_consume() {
let ring = Arc::new(Ring::new(16, false, 0));
for _i in 0..16 {
ring.claim(QueueFullPolicy::Block).unwrap();
}
let r = Arc::clone(&ring);
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(50));
r.committed_cursor.store(10, Ordering::Release);
});
let seq = ring.claim(QueueFullPolicy::Block).unwrap();
assert_eq!(seq, 16);
handle.join().unwrap();
}
#[test]
fn slot_index_wraps_correctly() {
let ring = Ring::new(16, false, 0);
assert_eq!(ring.slot(0) as *const Slot, ring.slot(16) as *const Slot);
assert_eq!(ring.slot(5) as *const Slot, ring.slot(21) as *const Slot);
}
#[test]
fn consume_watermark_without_hash() {
let ring = Ring::new(16, false, 0);
assert_eq!(ring.consume_watermark(), 0);
ring.committed_cursor.store(5, Ordering::Release);
assert_eq!(ring.consume_watermark(), 5);
}
#[test]
fn consume_watermark_with_hash() {
let ring = Ring::new(16, true, 0);
assert_eq!(ring.consume_watermark(), 0);
ring.sealed_cursor.store(3, Ordering::Release);
ring.committed_cursor.store(5, Ordering::Release);
assert_eq!(ring.consume_watermark(), 3);
ring.sealed_cursor.store(10, Ordering::Release);
assert_eq!(ring.consume_watermark(), 5);
}
#[test]
fn highest_published_contiguous_all_published() {
let ring = Ring::new(16, false, 0);
for seq in 0..3 {
unsafe {
ring.slot(seq).producer_write(seq, 0, b"x");
}
ring.slot(seq).publish(seq);
}
let hi = ring.highest_published_contiguous(0);
assert_eq!(hi, 2);
}
#[test]
fn highest_published_contiguous_with_gap() {
let ring = Ring::new(16, false, 0);
for seq in 0..2 {
unsafe {
ring.slot(seq).producer_write(seq, 0, b"x");
}
ring.slot(seq).publish(seq);
}
let hi = ring.highest_published_contiguous(0);
assert_eq!(hi, 1);
}
#[test]
fn highest_published_contiguous_from_mid() {
let ring = Ring::new(16, false, 0);
for seq in 0..4 {
unsafe {
ring.slot(seq).producer_write(seq, 0, b"x");
}
ring.slot(seq).publish(seq);
}
let hi = ring.highest_published_contiguous(2);
assert_eq!(hi, 3);
}
#[test]
fn highest_published_contiguous_none() {
let ring = Ring::new(16, false, 0);
let hi = ring.highest_published_contiguous(0);
assert_eq!(hi, u64::MAX); }
#[test]
fn multi_thread_claim_no_duplicates() {
use std::collections::HashSet;
let ring = Arc::new(Ring::new(1024, false, 0));
let num_threads = 8;
let claims_per_thread = 100;
let mut handles = vec![];
for _ in 0..num_threads {
let r = Arc::clone(&ring);
handles.push(thread::spawn(move || {
let mut claimed = Vec::with_capacity(claims_per_thread);
for _ in 0..claims_per_thread {
let seq = r.claim(QueueFullPolicy::Block).unwrap();
claimed.push(seq);
}
claimed
}));
}
let mut all_seqs = HashSet::new();
for h in handles {
let claimed = h.join().unwrap();
for seq in claimed {
assert!(all_seqs.insert(seq), "duplicate seq: {}", seq);
}
}
let total = (num_threads * claims_per_thread) as u64;
for i in 0..total {
assert!(all_seqs.contains(&i), "missing seq: {}", i);
}
}
#[test]
fn full_write_read_cycle() {
let ring = Ring::new(16, false, 0);
let content = b"integration test";
let seq = ring.claim(QueueFullPolicy::Block).unwrap();
unsafe {
ring.slot(seq).producer_write(seq, 5000, content);
}
ring.slot(seq).publish(seq);
assert!(ring.slot(seq).is_published(seq));
unsafe {
let view = ring.slot(seq).read();
assert_eq!(view.record_id, seq);
assert_eq!(view.timestamp_ns, 5000);
assert_eq!(view.content, content);
}
}
#[test]
fn ring_size_power_of_two_assert() {
Ring::new(16, false, 0);
Ring::new(32, false, 0);
Ring::new(1024, false, 0);
Ring::new(8192, false, 0);
}
#[test]
#[should_panic]
fn ring_size_non_power_of_two_panics() {
Ring::new(100, false, 0);
}
#[test]
#[should_panic]
fn ring_size_too_small_panics() {
Ring::new(8, false, 0);
}
#[test]
fn producer_cursor_cache_line_isolated() {
let ring = Ring::new(64, false, 0);
let base = &ring as *const Ring as usize;
let pc_offset = {
let pc = &ring.producer_cursor as *const CachePadded<AtomicU64> as usize;
pc - base
};
let sc_offset = {
let sc = &ring.sealed_cursor as *const AtomicU64 as usize;
sc - base
};
let cache_line = 64usize;
assert_ne!(
pc_offset / cache_line,
sc_offset / cache_line,
"producer_cursor shares cache line with sealed_cursor (false sharing!)"
);
assert_eq!(
pc_offset % cache_line,
0,
"producer_cursor is not cache-line aligned"
);
}
}