use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc;
use parking_lot::Mutex;
use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Bucket {
pub capacity: u64,
pub refill_tokens: u64,
pub refill_interval_ms: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Acquire {
pub allowed: bool,
pub remaining: u64,
pub retry_after_ms: u64,
}
pub trait KvBackend: Send + Sync {
fn get(&self, key: &[u8]) -> Option<Vec<u8>>;
fn set(&self, key: &[u8], value: Vec<u8>);
fn delete(&self, key: &[u8]);
fn increment(&self, key: &[u8], delta: i64) -> i64;
fn try_acquire(&self, key: &[u8], cost: u64, spec: Bucket, now_ms: u64) -> Acquire;
}
pub fn apply_bucket(
state: Option<(u64, u64)>,
cost: u64,
spec: Bucket,
now_ms: u64,
) -> ((u64, u64), Acquire) {
let no_refill = spec.refill_interval_ms == 0 || spec.refill_tokens == 0;
let (tokens, last_refill) = match state {
None => (spec.capacity, now_ms),
Some((tokens, last)) if no_refill => (tokens.min(spec.capacity), last),
Some((tokens, last)) => {
let intervals = now_ms.saturating_sub(last) / spec.refill_interval_ms;
let refilled = tokens
.saturating_add(intervals.saturating_mul(spec.refill_tokens))
.min(spec.capacity);
let advanced = last.saturating_add(intervals.saturating_mul(spec.refill_interval_ms));
(refilled, advanced)
}
};
if tokens >= cost {
let remaining = tokens - cost;
(
(remaining, last_refill),
Acquire {
allowed: true,
remaining,
retry_after_ms: 0,
},
)
} else {
let retry_after_ms = if no_refill {
u64::MAX
} else {
let needed = cost - tokens;
needed
.div_ceil(spec.refill_tokens)
.saturating_mul(spec.refill_interval_ms)
};
(
(tokens, last_refill),
Acquire {
allowed: false,
remaining: tokens,
retry_after_ms,
},
)
}
}
#[allow(clippy::indexing_slicing)] fn decode_i64(bytes: &[u8]) -> i64 {
let mut buf = [0u8; 8];
let n = bytes.len().min(8);
buf[..n].copy_from_slice(&bytes[..n]);
i64::from_le_bytes(buf)
}
#[allow(clippy::indexing_slicing)] fn decode_bucket(bytes: &[u8]) -> Option<(u64, u64)> {
if bytes.len() != 16 {
return None;
}
let tokens = u64::from_le_bytes(bytes[0..8].try_into().ok()?);
let last = u64::from_le_bytes(bytes[8..16].try_into().ok()?);
Some((tokens, last))
}
fn encode_bucket(state: (u64, u64)) -> Vec<u8> {
let mut out = Vec::with_capacity(16);
out.extend_from_slice(&state.0.to_le_bytes());
out.extend_from_slice(&state.1.to_le_bytes());
out
}
fn bucket_input(raw: Option<&[u8]>, now_ms: u64) -> Option<(u64, u64)> {
raw.map(|bytes| decode_bucket(bytes).unwrap_or((0, now_ms)))
}
#[derive(Default)]
pub struct MemoryBackend {
map: Mutex<HashMap<Vec<u8>, Vec<u8>>>,
}
impl KvBackend for MemoryBackend {
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
self.map.lock().get(key).cloned()
}
fn set(&self, key: &[u8], value: Vec<u8>) {
self.map.lock().insert(key.to_vec(), value);
}
fn delete(&self, key: &[u8]) {
self.map.lock().remove(key);
}
fn increment(&self, key: &[u8], delta: i64) -> i64 {
let mut map = self.map.lock();
let cur = decode_i64(map.get(key).map(Vec::as_slice).unwrap_or(&[]));
if delta == 0 {
return cur;
}
let next = cur.saturating_add(delta);
map.insert(key.to_vec(), next.to_le_bytes().to_vec());
next
}
fn try_acquire(&self, key: &[u8], cost: u64, spec: Bucket, now_ms: u64) -> Acquire {
let mut map = self.map.lock();
let prev = bucket_input(map.get(key).map(Vec::as_slice), now_ms);
let (next, result) = apply_bucket(prev, cost, spec, now_ms);
map.insert(key.to_vec(), encode_bucket(next));
result
}
}
const STATE_TABLE: TableDefinition<'_, &[u8], &[u8]> = TableDefinition::new("plecto_state");
const REDB_CACHE_BYTES: usize = 64 << 20;
const MAX_COMBINE_ROUNDS: u32 = 16;
const MAX_BATCH_OPS: usize = 64;
const DURABLE_FLUSH_EVERY: u64 = 1024;
pub struct RedbBackend {
db: Database,
flush_every: u64,
non_durable_run: AtomicU64,
forced_flushes: AtomicU64,
jobs: mpsc::Sender<WriteJob>,
combine: Mutex<mpsc::Receiver<WriteJob>>,
#[cfg(test)]
test: CombineTestHooks,
}
#[cfg(test)]
struct CombineTestHooks {
combine_rounds: u32,
max_batch_ops: usize,
fail_next_commits: AtomicU64,
max_batch_seen: AtomicU64,
}
enum WriteOp {
Set {
key: Vec<u8>,
value: Vec<u8>,
},
Delete {
key: Vec<u8>,
},
Increment {
key: Vec<u8>,
delta: i64,
},
TryAcquire {
key: Vec<u8>,
cost: u64,
spec: Bucket,
now_ms: u64,
},
}
impl WriteOp {
fn needs_durable_commit(&self) -> bool {
matches!(self, WriteOp::Set { .. } | WriteOp::Delete { .. })
}
}
enum WriteOutcome {
Done,
Counter(i64),
Acquired(Acquire),
}
struct WriteJob {
op: WriteOp,
reply: mpsc::Sender<anyhow::Result<WriteOutcome>>,
}
impl RedbBackend {
pub fn open(path: impl AsRef<std::path::Path>) -> anyhow::Result<Self> {
Self::open_inner(path, DURABLE_FLUSH_EVERY)
}
#[cfg(test)]
fn open_with_flush_every(
path: impl AsRef<std::path::Path>,
flush_every: u64,
) -> anyhow::Result<Self> {
Self::open_with_combine_limits(path, flush_every, MAX_COMBINE_ROUNDS, MAX_BATCH_OPS)
}
#[cfg(test)]
fn open_with_combine_limits(
path: impl AsRef<std::path::Path>,
flush_every: u64,
combine_rounds: u32,
max_batch_ops: usize,
) -> anyhow::Result<Self> {
let mut b = Self::open_inner(path, flush_every)?;
b.test.combine_rounds = combine_rounds.max(1);
b.test.max_batch_ops = max_batch_ops.max(1);
Ok(b)
}
fn open_inner(path: impl AsRef<std::path::Path>, flush_every: u64) -> anyhow::Result<Self> {
let db = redb::Builder::new()
.set_cache_size(REDB_CACHE_BYTES)
.create(path)?;
let (jobs, queue) = mpsc::channel();
Ok(Self {
db,
flush_every,
non_durable_run: AtomicU64::new(0),
forced_flushes: AtomicU64::new(0),
jobs,
combine: Mutex::new(queue),
#[cfg(test)]
test: CombineTestHooks {
combine_rounds: MAX_COMBINE_ROUNDS,
max_batch_ops: MAX_BATCH_OPS,
fail_next_commits: AtomicU64::new(0),
max_batch_seen: AtomicU64::new(0),
},
})
}
#[cfg(test)]
fn forced_flushes(&self) -> u64 {
self.forced_flushes.load(Ordering::Relaxed)
}
#[cfg(test)]
fn non_durable_run(&self) -> u64 {
self.non_durable_run.load(Ordering::Relaxed)
}
#[cfg(test)]
fn max_batch_seen(&self) -> u64 {
self.test.max_batch_seen.load(Ordering::Relaxed)
}
#[cfg(test)]
fn fail_next_n_commits(&self, n: u64) {
self.test.fail_next_commits.store(n, Ordering::Relaxed);
}
#[cfg(test)]
fn apply_ops_as_batch(&self, ops: Vec<WriteOp>) -> anyhow::Result<Vec<WriteOutcome>> {
let batch: Vec<_> = ops
.into_iter()
.map(|op| {
let (reply, _rx) = mpsc::channel();
WriteJob { op, reply }
})
.collect();
self.apply_batch(&batch)
}
fn get_inner(&self, key: &[u8]) -> anyhow::Result<Option<Vec<u8>>> {
let rtxn = self.db.begin_read()?;
let table = match rtxn.open_table(STATE_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
Err(e) => return Err(e.into()),
};
Ok(table.get(key)?.map(|g| g.value().to_vec()))
}
fn submit(&self, op: WriteOp) -> anyhow::Result<WriteOutcome> {
let (reply, outcome) = mpsc::channel();
self.jobs
.send(WriteJob { op, reply })
.map_err(|_| anyhow::anyhow!("kv write queue closed"))?;
loop {
match outcome.try_recv() {
Ok(result) => return result,
Err(mpsc::TryRecvError::Empty) => {}
Err(mpsc::TryRecvError::Disconnected) => {
anyhow::bail!("kv combiner dropped a queued write")
}
}
let Some(queue) = self.combine.try_lock() else {
match outcome.recv_timeout(std::time::Duration::from_micros(100)) {
Ok(result) => return result,
Err(mpsc::RecvTimeoutError::Timeout) => continue,
Err(mpsc::RecvTimeoutError::Disconnected) => {
anyhow::bail!("kv combiner dropped a queued write")
}
}
};
#[cfg(test)]
let rounds = self.test.combine_rounds;
#[cfg(not(test))]
let rounds = MAX_COMBINE_ROUNDS;
#[cfg(test)]
let max_batch = self.test.max_batch_ops;
#[cfg(not(test))]
let max_batch = MAX_BATCH_OPS;
for _ in 0..rounds {
let mut batch = Vec::new();
while batch.len() < max_batch {
match queue.try_recv() {
Ok(job) => batch.push(job),
Err(_) => break,
}
}
if batch.is_empty() {
break;
}
match self.apply_batch(&batch) {
Ok(outcomes) => {
for (job, out) in batch.iter().zip(outcomes) {
let _ = job.reply.send(Ok(out));
}
}
Err(e) => {
tracing::error!(error = %e, ops = batch.len(), "redb batch commit failed");
for job in &batch {
let _ = job
.reply
.send(Err(anyhow::anyhow!("batch commit failed: {e:#}")));
}
break;
}
}
}
}
}
fn durability_for_batch(&self, batch: &[WriteJob]) -> redb::Durability {
if batch.iter().any(|j| j.op.needs_durable_commit()) {
return redb::Durability::Immediate;
}
let ops = batch.len() as u64;
let run = self.non_durable_run.load(Ordering::Relaxed) + ops;
if run >= self.flush_every {
redb::Durability::Immediate
} else {
redb::Durability::None
}
}
fn record_successful_commit(&self, batch: &[WriteJob], durability: redb::Durability) {
if batch.iter().any(|j| j.op.needs_durable_commit()) {
self.non_durable_run.store(0, Ordering::Relaxed);
return;
}
let ops = batch.len() as u64;
match durability {
redb::Durability::Immediate => {
self.non_durable_run.store(0, Ordering::Relaxed);
self.forced_flushes.fetch_add(1, Ordering::Relaxed);
}
redb::Durability::None => {
self.non_durable_run.fetch_add(ops, Ordering::Relaxed);
}
_ => {
self.non_durable_run.store(0, Ordering::Relaxed);
self.forced_flushes.fetch_add(1, Ordering::Relaxed);
}
}
}
fn apply_batch(&self, batch: &[WriteJob]) -> anyhow::Result<Vec<WriteOutcome>> {
#[cfg(test)]
{
let len = batch.len() as u64;
self.test.max_batch_seen.fetch_max(len, Ordering::Relaxed);
}
let durability = self.durability_for_batch(batch);
let mut wtxn = self.db.begin_write()?;
wtxn.set_durability(durability)?;
let mut outcomes = Vec::with_capacity(batch.len());
{
let mut table = wtxn.open_table(STATE_TABLE)?;
for job in batch {
outcomes.push(apply_op(&mut table, &job.op)?);
}
}
#[cfg(test)]
if self
.test
.fail_next_commits
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_sub(1))
.is_ok()
{
drop(wtxn);
anyhow::bail!("injected commit failure");
}
wtxn.commit()?;
self.record_successful_commit(batch, durability);
Ok(outcomes)
}
}
fn apply_op(
table: &mut redb::Table<'_, &'static [u8], &'static [u8]>,
op: &WriteOp,
) -> anyhow::Result<WriteOutcome> {
match op {
WriteOp::Set { key, value } => {
table.insert(key.as_slice(), value.as_slice())?;
Ok(WriteOutcome::Done)
}
WriteOp::Delete { key } => {
table.remove(key.as_slice())?;
Ok(WriteOutcome::Done)
}
WriteOp::Increment { key, delta } => {
let cur = table
.get(key.as_slice())?
.map(|g| decode_i64(g.value()))
.unwrap_or(0);
let next = cur.saturating_add(*delta);
table.insert(key.as_slice(), next.to_le_bytes().as_slice())?;
Ok(WriteOutcome::Counter(next))
}
WriteOp::TryAcquire {
key,
cost,
spec,
now_ms,
} => {
let prev = {
let guard = table.get(key.as_slice())?;
bucket_input(guard.as_ref().map(|g| g.value()), *now_ms)
};
let (next, result) = apply_bucket(prev, *cost, *spec, *now_ms);
table.insert(key.as_slice(), encode_bucket(next).as_slice())?;
Ok(WriteOutcome::Acquired(result))
}
}
}
impl KvBackend for RedbBackend {
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
match self.get_inner(key) {
Ok(v) => v,
Err(e) => {
tracing::error!(error = %e, "redb get failed; treating key as absent");
None
}
}
}
fn set(&self, key: &[u8], value: Vec<u8>) {
if let Err(e) = self.submit(WriteOp::Set {
key: key.to_vec(),
value,
}) {
tracing::error!(error = %e, "redb set failed; value dropped");
}
}
fn delete(&self, key: &[u8]) {
if let Err(e) = self.submit(WriteOp::Delete { key: key.to_vec() }) {
tracing::error!(error = %e, "redb delete failed");
}
}
fn increment(&self, key: &[u8], delta: i64) -> i64 {
if delta == 0 {
return match self.get_inner(key) {
Ok(opt) => decode_i64(opt.as_deref().unwrap_or_default()),
Err(e) => {
tracing::error!(error = %e, "redb increment failed; returning 0");
0
}
};
}
match self.submit(WriteOp::Increment {
key: key.to_vec(),
delta,
}) {
Ok(WriteOutcome::Counter(v)) => v,
Ok(WriteOutcome::Done | WriteOutcome::Acquired(_)) => {
tracing::error!("redb increment got a mismatched outcome; returning 0");
0
}
Err(e) => {
tracing::error!(error = %e, "redb increment failed; returning 0");
0
}
}
}
fn try_acquire(&self, key: &[u8], cost: u64, spec: Bucket, now_ms: u64) -> Acquire {
let denied = Acquire {
allowed: false,
remaining: 0,
retry_after_ms: spec.refill_interval_ms,
};
match self.submit(WriteOp::TryAcquire {
key: key.to_vec(),
cost,
spec,
now_ms,
}) {
Ok(WriteOutcome::Acquired(r)) => r,
Ok(WriteOutcome::Done | WriteOutcome::Counter(_)) => {
tracing::error!("redb try_acquire got a mismatched outcome; denying");
denied
}
Err(e) => {
tracing::error!(error = %e, "redb try_acquire failed; denying");
denied
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
fn kv_roundtrip(backend: &dyn KvBackend) {
assert_eq!(backend.get(b"k"), None);
backend.set(b"k", b"v".to_vec());
assert_eq!(backend.get(b"k"), Some(b"v".to_vec()));
backend.delete(b"k");
assert_eq!(backend.get(b"k"), None);
}
fn counter_is_atomic_add_and_get(backend: &dyn KvBackend) {
assert_eq!(backend.increment(b"c", 1), 1);
assert_eq!(backend.increment(b"c", 4), 5);
assert_eq!(backend.increment(b"c", -2), 3);
assert_eq!(decode_i64(&backend.get(b"c").unwrap()), 3);
}
fn token_bucket_drains_then_refills(backend: &dyn KvBackend) {
let spec = Bucket {
capacity: 2,
refill_tokens: 1,
refill_interval_ms: 1000,
};
assert!(backend.try_acquire(b"rl", 1, spec, 0).allowed);
assert!(backend.try_acquire(b"rl", 1, spec, 0).allowed);
let denied = backend.try_acquire(b"rl", 1, spec, 0);
assert!(!denied.allowed);
assert_eq!(denied.remaining, 0);
assert_eq!(denied.retry_after_ms, 1000, "1 token needs one interval");
assert!(backend.try_acquire(b"rl", 1, spec, 1000).allowed);
}
fn counter_read_via_zero_delta_does_not_create_key(backend: &dyn KvBackend) {
assert_eq!(backend.increment(b"zc", 0), 0, "unset counter reads as 0");
assert_eq!(
backend.get(b"zc"),
None,
"a zero-delta read must not create the counter key"
);
assert_eq!(backend.increment(b"zc", 5), 5);
assert_eq!(
backend.increment(b"zc", 0),
5,
"zero-delta still reads the live value"
);
}
fn token_bucket_corrupt_state_fails_closed(backend: &dyn KvBackend) {
let spec = Bucket {
capacity: 5,
refill_tokens: 1,
refill_interval_ms: 1000,
};
backend.set(b"cb", vec![0xff; 3]); assert!(
!backend.try_acquire(b"cb", 1, spec, 0).allowed,
"corrupt bucket must fail closed, not start full"
);
assert!(backend.try_acquire(b"cb", 1, spec, 1000).allowed);
}
#[test]
fn memory_backend_behaviour() {
let b = MemoryBackend::default();
kv_roundtrip(&b);
counter_is_atomic_add_and_get(&b);
counter_read_via_zero_delta_does_not_create_key(&b);
token_bucket_drains_then_refills(&b);
token_bucket_corrupt_state_fails_closed(&b);
}
#[test]
fn redb_backend_behaviour() {
let dir = tempfile::tempdir().unwrap();
let b = RedbBackend::open(dir.path().join("state.redb")).unwrap();
kv_roundtrip(&b);
counter_is_atomic_add_and_get(&b);
counter_read_via_zero_delta_does_not_create_key(&b);
token_bucket_drains_then_refills(&b);
token_bucket_corrupt_state_fails_closed(&b);
}
#[test]
fn redb_periodic_durable_flush_caps_a_non_durable_run() {
let dir = tempfile::tempdir().unwrap();
let b = RedbBackend::open_with_flush_every(dir.path().join("state.redb"), 4).unwrap();
let spec = Bucket {
capacity: 1000,
refill_tokens: 0,
refill_interval_ms: 0,
};
b.increment(b"c", 1);
b.increment(b"c", 1);
b.increment(b"c", 0); b.try_acquire(b"rl", 1, spec, 0);
assert_eq!(b.forced_flushes(), 0, "three hot-path ops stay non-durable");
b.try_acquire(b"rl", 1, spec, 0);
assert_eq!(
b.forced_flushes(),
1,
"the 4th op in a run is upgraded to durable"
);
for _ in 0..4 {
b.increment(b"c", 1);
}
assert_eq!(b.forced_flushes(), 2, "the cadence repeats");
}
#[test]
fn redb_durable_kv_write_resets_the_flush_cadence() {
let dir = tempfile::tempdir().unwrap();
let b = RedbBackend::open_with_flush_every(dir.path().join("state.redb"), 4).unwrap();
b.increment(b"c", 1);
b.increment(b"c", 1);
b.increment(b"c", 1);
b.set(b"k", b"v".to_vec()); b.increment(b"c", 1);
b.increment(b"c", 1);
b.increment(b"c", 1);
assert_eq!(
b.forced_flushes(),
0,
"a durable set resets the non-durable run"
);
b.increment(b"c", 1);
assert_eq!(b.forced_flushes(), 1);
}
#[test]
fn redb_concurrent_writes_are_exact_under_combining() {
let dir = tempfile::tempdir().unwrap();
let b = Arc::new(RedbBackend::open(dir.path().join("state.redb")).unwrap());
let spec = Bucket {
capacity: 100,
refill_tokens: 0,
refill_interval_ms: 0,
};
let admitted = Arc::new(AtomicU64::new(0));
let workers: Vec<_> = (0..8)
.map(|_| {
let b = Arc::clone(&b);
let admitted = Arc::clone(&admitted);
std::thread::spawn(move || {
for _ in 0..50 {
b.increment(b"c", 1);
if b.try_acquire(b"rl", 1, spec, 0).allowed {
admitted.fetch_add(1, Ordering::Relaxed);
}
}
})
})
.collect();
for w in workers {
w.join().unwrap();
}
assert_eq!(b.increment(b"c", 0), 400, "no increment lost or doubled");
assert_eq!(
admitted.load(Ordering::Relaxed),
100,
"the bucket admits exactly its capacity"
);
}
#[test]
fn redb_failed_durable_commit_does_not_reset_flush_cadence() {
let dir = tempfile::tempdir().unwrap();
let b = RedbBackend::open_with_flush_every(dir.path().join("state.redb"), 4).unwrap();
b.increment(b"c", 1);
b.increment(b"c", 1);
b.increment(b"c", 1);
assert_eq!(b.non_durable_run(), 3);
b.fail_next_n_commits(1);
b.set(b"k", b"v".to_vec()); assert!(b.get(b"k").is_none(), "failed set must not be observable");
assert_eq!(
b.non_durable_run(),
3,
"failed durable commit must leave the hot-path run intact"
);
b.increment(b"c", 1);
assert_eq!(
b.forced_flushes(),
1,
"the next hot op must still trip the cadence at flush_every"
);
}
#[test]
fn redb_flush_cadence_counts_ops_not_commits() {
let dir = tempfile::tempdir().unwrap();
let b = RedbBackend::open_with_flush_every(dir.path().join("state.redb"), 4).unwrap();
let ops = (0..4)
.map(|_| WriteOp::Increment {
key: b"c".to_vec(),
delta: 1,
})
.collect();
b.apply_ops_as_batch(ops).unwrap();
assert_eq!(
b.forced_flushes(),
1,
"4 ops in one commit trip flush_every=4"
);
assert_eq!(b.increment(b"c", 0), 4);
let ops = (0..3)
.map(|_| WriteOp::Increment {
key: b"c".to_vec(),
delta: 1,
})
.collect();
b.apply_ops_as_batch(ops).unwrap();
assert_eq!(
b.forced_flushes(),
1,
"3 more ops stay under the next threshold"
);
b.increment(b"c", 1);
assert_eq!(
b.forced_flushes(),
2,
"the 4th op of the new run forces a flush"
);
}
#[test]
fn redb_batch_commit_failure_fails_closed_for_every_op() {
let dir = tempfile::tempdir().unwrap();
let b = RedbBackend::open(dir.path().join("state.redb")).unwrap();
b.fail_next_n_commits(1);
let ops = vec![
WriteOp::Increment {
key: b"c".to_vec(),
delta: 1,
},
WriteOp::Increment {
key: b"c".to_vec(),
delta: 1,
},
WriteOp::Set {
key: b"k".to_vec(),
value: b"v".to_vec(),
},
];
assert!(b.apply_ops_as_batch(ops).is_err());
assert_eq!(b.increment(b"c", 0), 0, "no partial counter apply");
assert!(b.get(b"k").is_none(), "no partial kv apply");
}
#[test]
fn redb_mixed_durable_and_hot_batch_resets_cadence_without_forced_flush() {
let dir = tempfile::tempdir().unwrap();
let b = RedbBackend::open_with_flush_every(dir.path().join("state.redb"), 4).unwrap();
b.increment(b"c", 1);
b.increment(b"c", 1);
b.increment(b"c", 1);
b.apply_ops_as_batch(vec![
WriteOp::Increment {
key: b"c".to_vec(),
delta: 1,
},
WriteOp::Set {
key: b"k".to_vec(),
value: b"v".to_vec(),
},
])
.unwrap();
assert_eq!(b.get(b"k"), Some(b"v".to_vec()));
assert_eq!(b.forced_flushes(), 0);
assert_eq!(b.non_durable_run(), 0);
for _ in 0..3 {
b.increment(b"c", 1);
}
assert_eq!(b.forced_flushes(), 0);
b.increment(b"c", 1);
assert_eq!(b.forced_flushes(), 1);
}
#[test]
fn redb_per_round_batch_cap_bounds_combiner_work() {
let dir = tempfile::tempdir().unwrap();
let b = Arc::new(
RedbBackend::open_with_combine_limits(dir.path().join("state.redb"), 1024, 16, 2)
.unwrap(),
);
let workers: Vec<_> = (0..8)
.map(|_| {
let b = Arc::clone(&b);
std::thread::spawn(move || {
for _ in 0..40 {
b.increment(b"c", 1);
}
})
})
.collect();
for w in workers {
w.join().unwrap();
}
assert_eq!(b.increment(b"c", 0), 320);
assert!(
b.max_batch_seen() <= 2,
"max_batch_seen={} exceeded cap 2",
b.max_batch_seen()
);
}
#[test]
fn redb_residual_jobs_after_round_cap_still_commit() {
let dir = tempfile::tempdir().unwrap();
let b = Arc::new(
RedbBackend::open_with_combine_limits(dir.path().join("state.redb"), 1024, 1, 1)
.unwrap(),
);
let workers: Vec<_> = (0..8)
.map(|_| {
let b = Arc::clone(&b);
std::thread::spawn(move || {
for _ in 0..25 {
b.increment(b"c", 1);
}
})
})
.collect();
for w in workers {
w.join().unwrap();
}
assert_eq!(b.increment(b"c", 0), 200);
}
#[test]
fn redb_concurrent_set_delete_are_exact_under_combining() {
let dir = tempfile::tempdir().unwrap();
let b = Arc::new(RedbBackend::open(dir.path().join("state.redb")).unwrap());
let workers: Vec<_> = (0..8)
.map(|i| {
let b = Arc::clone(&b);
std::thread::spawn(move || {
let key = format!("k{i}").into_bytes();
for n in 0..20u8 {
b.set(&key, vec![n]);
}
b.delete(&key);
})
})
.collect();
for w in workers {
w.join().unwrap();
}
for i in 0..8 {
assert_eq!(b.get(format!("k{i}").as_bytes()), None);
}
}
fn split_iters_across_threads(threads: u64, iters: u64) -> Vec<u64> {
let threads = threads.max(1);
let base = iters / threads;
let rem = iters % threads;
(0..threads).map(|t| base + u64::from(t < rem)).collect()
}
#[test]
fn split_iters_across_threads_sums_exactly_to_iters() {
for threads in [1u64, 2, 8] {
for iters in [0u64, 1, 3, 7, 8, 100] {
let parts = split_iters_across_threads(threads, iters);
assert_eq!(parts.len(), threads as usize);
assert_eq!(
parts.iter().copied().sum::<u64>(),
iters,
"threads={threads} iters={iters} parts={parts:?}"
);
}
}
}
#[test]
fn redb_persists_across_reopen() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("state.redb");
{
let b = RedbBackend::open(&path).unwrap();
assert_eq!(b.increment(b"hits", 3), 3);
}
let b = RedbBackend::open(&path).unwrap();
assert_eq!(b.increment(b"hits", 1), 4);
}
#[test]
fn token_bucket_cost_zero_always_allowed() {
let spec = Bucket {
capacity: 1,
refill_tokens: 1,
refill_interval_ms: 1000,
};
let (_state, r) = apply_bucket(Some((0, 0)), 0, spec, 0);
assert!(r.allowed, "a zero-cost acquire never blocks");
}
}