use std::sync::LazyLock;
use tokio::sync::Mutex;
use super::KeyspaceHandle;
use crate::error::AppError;
static COUNTER_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
pub async fn allocate_u32(ks: &KeyspaceHandle, counter_key: &str) -> Result<u32, AppError> {
let _guard = COUNTER_LOCK.lock().await;
let current: u32 = match ks.get_raw(counter_key).await? {
Some(bytes) => {
let arr: [u8; 4] = bytes
.try_into()
.map_err(|_| AppError::Internal(format!("corrupt counter at {counter_key}")))?;
u32::from_le_bytes(arr)
}
None => 0,
};
ks.insert_raw(counter_key, (current + 1).to_le_bytes().to_vec())
.await?;
ks.persist().await?;
crate::integrity::reseal_if_active().await?;
Ok(current)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::StoreConfig;
use crate::store::Store;
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn allocate_u32_is_collision_free_under_concurrency() {
let dir = tempfile::tempdir().expect("tempdir");
let store = Store::open(&StoreConfig {
data_dir: dir.path().to_path_buf(),
})
.expect("open store");
let ks = store.keyspace("test").expect("keyspace");
let n = 64usize;
let mut handles = Vec::with_capacity(n);
for _ in 0..n {
let ks = ks.clone();
handles.push(tokio::spawn(async move {
allocate_u32(&ks, "counter:x").await.expect("alloc")
}));
}
let mut seen = std::collections::HashSet::with_capacity(n);
for h in handles {
let v = h.await.expect("join");
assert!(seen.insert(v), "duplicate counter value {v}");
}
assert_eq!(seen.len(), n);
}
#[tokio::test]
async fn allocate_u32_starts_at_zero_and_is_sequential() {
let dir = tempfile::tempdir().expect("tempdir");
let store = Store::open(&StoreConfig {
data_dir: dir.path().to_path_buf(),
})
.expect("open store");
let ks = store.keyspace("test").expect("keyspace");
for expect in 0..3u32 {
assert_eq!(allocate_u32(&ks, "counter:y").await.unwrap(), expect);
}
}
}