use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
pub const PRODUCER_FLUSH_OPS_THRESHOLD: u64 = 256;
pub const PRODUCER_FLUSH_ELAPSED_THRESHOLD: Duration = Duration::from_millis(500);
#[derive(Debug, thiserror::Error)]
pub enum ProducerAllocError {
#[error("producer-id space exhausted (u64::MAX reached)")]
Exhausted,
#[error("producer-id flush failed: {detail}")]
FlushFailed { detail: String },
}
pub trait ProducerHwmPersist: Send + Sync {
fn checkpoint(&self, hwm: u64) -> crate::Result<()>;
fn load(&self) -> crate::Result<u64>;
}
pub struct ProducerIdAllocator {
counter: AtomicU64,
allocs_since_flush: AtomicU64,
last_flush_at: Mutex<Instant>,
}
impl ProducerIdAllocator {
pub fn new() -> Self {
Self::from_persisted_hwm(0)
}
pub fn from_persisted_hwm(hwm: u64) -> Self {
Self {
counter: AtomicU64::new(hwm.saturating_add(1)),
allocs_since_flush: AtomicU64::new(0),
last_flush_at: Mutex::new(Instant::now()),
}
}
pub fn alloc_one(&self) -> Result<u64, ProducerAllocError> {
let prev = self.counter.fetch_add(1, Ordering::AcqRel);
if prev == u64::MAX {
self.counter.store(u64::MAX, Ordering::Release);
return Err(ProducerAllocError::Exhausted);
}
self.allocs_since_flush.fetch_add(1, Ordering::AcqRel);
Ok(prev)
}
pub fn current_hwm(&self) -> u64 {
let next = self.counter.load(Ordering::Acquire);
next.saturating_sub(1)
}
pub fn should_flush(&self) -> bool {
if self.allocs_since_flush.load(Ordering::Acquire) >= PRODUCER_FLUSH_OPS_THRESHOLD {
return true;
}
if let Ok(last) = self.last_flush_at.lock() {
return last.elapsed() >= PRODUCER_FLUSH_ELAPSED_THRESHOLD;
}
false
}
pub fn restore_hwm(&self, new_hwm: u64) -> Result<(), ProducerAllocError> {
let target = new_hwm.saturating_add(1);
let mut current = self.counter.load(Ordering::Acquire);
loop {
if target <= current {
return Ok(());
}
match self.counter.compare_exchange_weak(
current,
target,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return Ok(()),
Err(actual) => current = actual,
}
}
}
pub fn flush(&self, persist: &dyn ProducerHwmPersist) -> Result<(), ProducerAllocError> {
let hwm = self.current_hwm();
persist
.checkpoint(hwm)
.map_err(|e| ProducerAllocError::FlushFailed {
detail: e.to_string(),
})?;
self.allocs_since_flush.store(0, Ordering::Release);
if let Ok(mut guard) = self.last_flush_at.lock() {
*guard = Instant::now();
}
Ok(())
}
#[cfg(test)]
fn rewind_flush_clock(&self, by: Duration) {
if let Ok(mut guard) = self.last_flush_at.lock()
&& let Some(earlier) = guard.checked_sub(by)
{
*guard = earlier;
}
}
}
impl Default for ProducerIdAllocator {
fn default() -> Self {
Self::new()
}
}
impl From<ProducerAllocError> for crate::Error {
fn from(e: ProducerAllocError) -> Self {
match e {
ProducerAllocError::Exhausted => crate::Error::Internal {
detail: "producer-id space exhausted (u64::MAX reached)".into(),
},
ProducerAllocError::FlushFailed { detail } => crate::Error::Storage {
engine: "sync_producer".into(),
detail,
},
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use super::*;
struct MemPersist {
last: std::sync::Mutex<Option<u64>>,
calls: AtomicU64,
}
impl MemPersist {
fn new() -> Self {
Self {
last: std::sync::Mutex::new(None),
calls: AtomicU64::new(0),
}
}
fn last(&self) -> Option<u64> {
*self.last.lock().unwrap()
}
fn calls(&self) -> u64 {
self.calls.load(Ordering::Acquire)
}
}
impl ProducerHwmPersist for MemPersist {
fn checkpoint(&self, hwm: u64) -> crate::Result<()> {
*self.last.lock().unwrap() = Some(hwm);
self.calls.fetch_add(1, Ordering::AcqRel);
Ok(())
}
fn load(&self) -> crate::Result<u64> {
Ok(self.last().unwrap_or(0))
}
}
#[test]
fn first_alloc_returns_one() {
let a = ProducerIdAllocator::new();
assert_eq!(a.alloc_one().unwrap(), 1);
assert_eq!(a.current_hwm(), 1);
}
#[test]
fn monotonic_10k() {
let a = ProducerIdAllocator::new();
let mut prev = 0u64;
for _ in 0..10_000 {
let id = a.alloc_one().unwrap();
assert!(id > prev, "expected monotonic, got {prev} then {id}");
prev = id;
}
assert_eq!(a.current_hwm(), 10_000);
}
#[test]
fn restart_survives_hwm() {
let a = ProducerIdAllocator::from_persisted_hwm(5000);
assert_eq!(a.alloc_one().unwrap(), 5001);
assert_eq!(a.current_hwm(), 5001);
}
#[test]
fn concurrent_16x1000_unique() {
let a = Arc::new(ProducerIdAllocator::new());
let mut handles = Vec::with_capacity(16);
for _ in 0..16 {
let r = a.clone();
handles.push(std::thread::spawn(move || {
let mut local = Vec::with_capacity(1000);
for _ in 0..1000 {
local.push(r.alloc_one().unwrap());
}
local
}));
}
let mut all = Vec::with_capacity(16_000);
for h in handles {
all.extend(h.join().unwrap());
}
all.sort_unstable();
all.dedup();
assert_eq!(all.len(), 16_000, "expected 16000 unique producer-ids");
assert!(a.current_hwm() >= 16_000);
}
#[test]
fn flush_threshold_ops() {
let a = ProducerIdAllocator::new();
assert!(!a.should_flush());
for _ in 0..(PRODUCER_FLUSH_OPS_THRESHOLD - 1) {
a.alloc_one().unwrap();
}
assert!(!a.should_flush(), "below threshold");
a.alloc_one().unwrap();
assert!(a.should_flush(), "at threshold");
let p = MemPersist::new();
a.flush(&p).unwrap();
assert_eq!(p.calls(), 1);
assert_eq!(p.last(), Some(PRODUCER_FLUSH_OPS_THRESHOLD));
assert!(!a.should_flush(), "post-flush");
}
#[test]
fn flush_threshold_elapsed() {
let a = ProducerIdAllocator::new();
a.alloc_one().unwrap();
assert!(!a.should_flush());
a.rewind_flush_clock(PRODUCER_FLUSH_ELAPSED_THRESHOLD * 2);
assert!(a.should_flush(), "rewound clock fires elapsed");
let p = MemPersist::new();
a.flush(&p).unwrap();
assert!(!a.should_flush(), "post-flush resets clock");
}
#[test]
fn restore_hwm_never_lowers() {
let a = ProducerIdAllocator::from_persisted_hwm(100);
a.restore_hwm(50).unwrap();
assert_eq!(a.current_hwm(), 100);
a.restore_hwm(200).unwrap();
assert_eq!(a.current_hwm(), 200);
}
}