use std::{
iter::repeat_n,
num::{NonZeroU32, NonZeroUsize},
sync::atomic::Ordering,
};
use diskann::utils::IntoUsize;
use diskann_utils::views::MatrixView;
use thiserror::Error;
use crate::{
buffer::{Buffer, BufferError, RawSlice},
epoch::{self, Registry},
freelist::{self, Freelist},
neighbors::{Neighbors, NeighborsError},
num::{Align, Bytes},
tag::{AtomicTag, Tag},
};
#[derive(Debug)]
pub(crate) struct Config {
entries: usize,
bytes: Bytes,
max_neighbors: usize,
epoch_guard_slots: NonZeroUsize,
freelist_recycle_capacity: NonZeroU32,
}
impl Config {
pub(crate) fn new(entries: usize, bytes: Bytes, max_neighbors: usize) -> Self {
const DEFAULT_FREELIST_RECYCLE_CAPACITY: NonZeroU32 = NonZeroU32::new(1024).unwrap();
Self {
entries,
bytes,
max_neighbors,
epoch_guard_slots: Registry::default_guard_slots(),
freelist_recycle_capacity: DEFAULT_FREELIST_RECYCLE_CAPACITY,
}
}
pub(crate) fn epoch_guard_slots(&mut self, epoch_guard_slots: NonZeroUsize) -> &mut Self {
self.epoch_guard_slots = epoch_guard_slots;
self
}
pub(crate) fn freelist_recycle_capacity(
&mut self,
freelist_recycle_capacity: NonZeroU32,
) -> &mut Self {
self.freelist_recycle_capacity = freelist_recycle_capacity;
self
}
}
#[derive(Debug)]
pub(crate) struct Store {
buffer: Buffer,
unpadded: Bytes,
unfrozen: usize,
tags: Vec<AtomicTag>,
freelist: Freelist,
registry: Registry,
neighbors: Neighbors,
}
pub(crate) const TAG_SIZE: Bytes = Bytes::size_of::<AtomicTag>();
const TWO: NonZeroUsize = NonZeroUsize::new(2).unwrap();
const RETRY_LIMIT: usize = 20;
impl Store {
pub(crate) fn new(
config: Config,
init: MatrixView<'_, u8>,
) -> Result<Self, StoreError> {
let Config {
entries,
bytes,
max_neighbors,
epoch_guard_slots,
freelist_recycle_capacity,
} = config;
if init.ncols() != bytes.value() {
return Err(StoreError::mismatched_frozen_point_dim(init.ncols(), bytes));
}
if init.nrows() == 0 {
return Err(StoreError::need_frozen_point());
}
#[expect(
clippy::expect_used,
reason = "we expect `init` to have at least one row, so this should never happen"
)]
let unpadded = bytes
.checked_add(TAG_SIZE)
.expect("unreachable because `init` cannot exceed `isize::MAX` bytes");
#[expect(
clippy::expect_used,
reason = "we expect `init` to have at least one row, so this should never happen"
)]
let padded_bytes = unpadded
.checked_next_multiple_of(Bytes::CACHELINE.div(TWO))
.expect("unreachable because `init` cannot exceed `isize::MAX` bytes");
let too_many_entries = || StoreError::too_many_entries(entries, init.nrows());
let entries: u32 = entries.try_into().map_err(|_| too_many_entries())?;
let frozen: u32 = init.nrows().try_into().map_err(|_| too_many_entries())?;
let total: u32 = entries.checked_add(frozen).ok_or_else(too_many_entries)?;
let max_neighbors: u32 = max_neighbors
.try_into()
.map_err(|_| StoreError::too_many_neighbors(max_neighbors))?;
let me = Self {
buffer: Buffer::new(total.into_usize(), padded_bytes, Align::_128)?,
unpadded,
unfrozen: entries.into_usize(),
tags: repeat_n(Tag::AVAILABLE, total.into_usize())
.map(AtomicTag::new)
.collect(),
freelist: Freelist::new(entries, freelist_recycle_capacity),
registry: Registry::with_capacity(epoch_guard_slots),
neighbors: Neighbors::new(total, max_neighbors)?,
};
for (i, data) in init.row_iter().enumerate() {
#[expect(clippy::expect_used, reason = "this should always succeed")]
let mut slot = me
.slot(entries + (i as u32))
.expect("store was just created - claiming the slot must succeed");
slot.as_mut_slice().copy_from_slice(data);
slot.freeze();
}
Ok(me)
}
pub(crate) fn frozen(&self) -> std::ops::Range<u32> {
(self.unfrozen as u32)..(self.buffer.len() as u32)
}
pub(crate) fn bytes(&self) -> Bytes {
self.unpadded
}
pub(crate) fn max_degree(&self) -> usize {
self.neighbors.max_length()
}
pub(crate) fn try_drain(&self) -> Option<usize> {
fn release(tag: &AtomicTag, kind: &'static str) {
assert_eq!(
tag.load(Ordering::Relaxed),
Tag::RETIRING,
"CONCURRENCY VIOLATION: {}",
kind,
);
tag.store(Tag::AVAILABLE, Ordering::Release);
}
let drain = self.registry.try_advance()?;
let items = drain.len();
for i in drain {
assert!(
i.into_usize() < self.buffer.len(),
"received an invalid ID ({}) while reclaiming slots - max allowed is {}",
i,
self.buffer.len(),
);
let (mirror, _) = unsafe { self.data_unchecked(i.into_usize()) };
release(mirror, "mirror");
release(&self.tags[i.into_usize()], "tag");
self.freelist.push(i);
}
Some(items)
}
pub(crate) fn reader(&self) -> Result<Reader<'_>, epoch::Unavailable> {
Ok(Reader {
buffer: &self.buffer,
unpadded: self.unpadded,
neighbors: &self.neighbors,
_guard: self.registry.guard()?,
})
}
pub(crate) fn acquire(&self) -> Option<Slot<'_>> {
for _ in 0..RETRY_LIMIT {
match self.freelist.pop() {
freelist::Id::Found(id) => {
if let Some(slot) = self.slot(id) {
return Some(slot);
}
}
freelist::Id::Scan => match self.scan_acquire() {
Some(slot) => return Some(slot),
None => {
self.try_drain();
}
},
}
}
None
}
pub(crate) fn retire(&self, i: usize) -> Result<(), RetireError> {
let tag = self.tags.get(i).ok_or(RetireError::OutOfBounds)?;
let current = tag.load(Ordering::Relaxed);
if current.is_reserved() {
return Err(RetireError::SlotIsReserved { tag: current });
}
let guard = self
.registry
.guard()
.map_err(RetireError::GuardUnavailable)?;
let retiring = Tag::RETIRING;
match tag.compare_exchange(current, retiring, Ordering::Relaxed, Ordering::Relaxed) {
Ok(_) => {
let (mirror, _) = unsafe { self.data_unchecked(i) };
mirror.store(retiring, Ordering::Relaxed);
guard.retire(i as u32);
Ok(())
}
Err(_) => Err(RetireError::CouldNotClaimSlot),
}
}
fn scan_acquire(&self) -> Option<Slot<'_>> {
let mut remaining = self.unfrozen.div_ceil(RETRY_LIMIT);
let mut chunks_since_freelist_check = 0;
let mut acquired: Option<Slot<'_>> = None;
while remaining != 0 {
let chunk = self.freelist.scan();
remaining = remaining.saturating_sub(chunk.len());
for slot in chunk {
#[expect(
clippy::expect_used,
reason = "this is a serious bug with the freelist"
)]
let tag = self
.tags
.get(slot.into_usize())
.expect("freelist scan should not give out invalid IDs");
if tag.load(Ordering::Relaxed) == Tag::AVAILABLE {
if acquired.is_none() {
acquired = unsafe { self.try_acquire(tag, slot) };
} else {
self.freelist.push(slot);
}
}
}
if acquired.is_some() {
return acquired;
}
chunks_since_freelist_check += 1;
if chunks_since_freelist_check == 4 {
if let Some(id) = self.freelist.pop_recycled()
&& let Some(slot) = self.slot(id)
{
return Some(slot);
}
chunks_since_freelist_check = 0;
}
}
None
}
fn slot(&self, i: u32) -> Option<Slot<'_>> {
let tag = &self.tags.get(i.into_usize())?;
unsafe { self.try_acquire(tag, i) }
}
unsafe fn try_acquire<'a>(&'a self, tag: &'a AtomicTag, slot: u32) -> Option<Slot<'a>> {
if tag.load(Ordering::Relaxed) != Tag::AVAILABLE {
return None;
}
match tag.compare_exchange(
Tag::AVAILABLE,
Tag::OWNED,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => {
let (mirror, data) = unsafe { self.data_unchecked(slot.into_usize()) };
Some(Slot {
tag,
mirror,
data,
slot,
})
}
Err(_) => None,
}
}
unsafe fn data_unchecked(&self, i: usize) -> (&AtomicTag, RawSlice<'_>) {
let (data, mirror) = unsafe { self.buffer.get_unchecked(i) }
.truncate(self.unpadded)
.split(self.unpadded.unchecked_sub(TAG_SIZE));
(
unsafe { AtomicTag::from_ptr(mirror.as_mut_ptr().cast()) },
data,
)
}
pub(crate) fn can_read_approximate(&self, i: usize) -> Option<bool> {
self.tags
.get(i)
.map(|tag| tag.load(Ordering::Relaxed).can_read())
}
#[cfg(test)]
fn writable(&self) -> std::ops::Range<u32> {
0..self.unfrozen as u32
}
}
#[derive(Debug, Error)]
#[error(transparent)]
pub(crate) struct StoreError(StoreErrorInner);
impl StoreError {
fn mismatched_frozen_point_dim(dim: usize, bytes: Bytes) -> Self {
Self(StoreErrorInner::MismatchedFrozenPointDim { dim, bytes })
}
fn need_frozen_point() -> Self {
Self(StoreErrorInner::NeedFrozenPoint)
}
fn too_many_entries(entries: usize, frozen: usize) -> Self {
Self(StoreErrorInner::TooManyEntries { entries, frozen })
}
fn too_many_neighbors(neighbors: usize) -> Self {
Self(StoreErrorInner::TooManyNeighbors { neighbors })
}
}
impl From<BufferError> for StoreError {
fn from(err: BufferError) -> Self {
Self(err.into())
}
}
impl From<NeighborsError> for StoreError {
fn from(err: NeighborsError) -> Self {
Self(err.into())
}
}
#[derive(Debug, Error)]
enum StoreErrorInner {
#[error(
"frozen point dim ({}) must have the same dimensionality as requested bytes ({})",
dim,
bytes
)]
MismatchedFrozenPointDim { dim: usize, bytes: Bytes },
#[error("at least one frozen point must be provided")]
NeedFrozenPoint,
#[error(
"total points ({} + {} frozen) must not exceed `u32::MAX`",
entries,
frozen
)]
TooManyEntries { entries: usize, frozen: usize },
#[error("number of neighbors ({}) may not exceed `u32::MAX`", neighbors)]
TooManyNeighbors { neighbors: usize },
#[error(transparent)]
BufferError(#[from] BufferError),
#[error(transparent)]
NeighborsError(#[from] NeighborsError),
}
#[derive(Debug, Error)]
pub(crate) enum RetireError {
#[error("index out of bounds")]
OutOfBounds,
#[error("slot is reserved: {}", tag)]
SlotIsReserved { tag: Tag },
#[error(transparent)]
GuardUnavailable(epoch::Unavailable),
#[error("could not claim slot")]
CouldNotClaimSlot,
}
#[derive(Debug)]
pub(crate) struct Reader<'a> {
buffer: &'a Buffer,
unpadded: Bytes,
neighbors: &'a Neighbors,
_guard: epoch::Guard<'a>,
}
impl<'a> Reader<'a> {
#[inline]
pub(crate) fn read(&self, i: usize) -> Option<&[u8]> {
if self.is_in_bounds(i) {
unsafe { self.read_in_bounds(i) }
} else {
None
}
}
#[inline]
#[must_use = "this function has no side-effects"]
pub(crate) fn is_in_bounds(&self, i: usize) -> bool {
i < self.buffer.len()
}
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "this is non-trivial method that likely be used in the future"
)
)]
pub(crate) fn can_read(&self, i: usize) -> Option<bool> {
if !self.is_in_bounds(i) {
return None;
}
let tag_ptr = unsafe {
self.buffer
.get_unchecked(i)
.as_mut_ptr()
.add(self.unpadded.unchecked_sub(TAG_SIZE).value())
};
let can_read = unsafe { AtomicTag::from_ptr(tag_ptr.cast()) }
.load(Ordering::Acquire)
.can_read();
Some(can_read)
}
#[inline]
pub(crate) unsafe fn read_in_bounds(&self, i: usize) -> Option<&[u8]> {
debug_assert!(self.is_in_bounds(i));
let (data, tag_ptr) = unsafe {
self.buffer
.get_unchecked(i)
.truncate_unchecked(self.unpadded)
.split_unchecked(self.unpadded.unchecked_sub(TAG_SIZE))
};
let can_read = unsafe { AtomicTag::from_ptr(tag_ptr.as_mut_ptr().cast()) }
.load(Ordering::Acquire)
.can_read();
if can_read {
Some(unsafe { data.as_slice() })
} else {
None
}
}
#[inline]
pub(crate) unsafe fn read_raw_unchecked(&self, i: usize) -> RawSlice<'_> {
unsafe { self.buffer.get_unchecked(i) }.truncate(self.unpadded)
}
pub(crate) fn bytes(&self) -> Bytes {
self.unpadded
}
pub(crate) fn neighbors(&self) -> &Neighbors {
self.neighbors
}
}
#[derive(Debug)]
pub(crate) struct Slot<'a> {
tag: &'a AtomicTag,
mirror: &'a AtomicTag,
data: RawSlice<'a>,
slot: u32,
}
impl<'a> Slot<'a> {
pub(crate) fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { self.data.as_mut_slice() }
}
pub(crate) fn slot(&self) -> u32 {
self.slot
}
fn freeze(self) {
let me = std::mem::ManuallyDrop::new(self);
me.mirror.store(Tag::FROZEN, Ordering::Release);
me.tag.store(Tag::FROZEN, Ordering::Release);
}
pub(crate) fn publish(self) -> u32 {
let id = self.slot();
let me = std::mem::ManuallyDrop::new(self);
me.mirror.store(Tag::PUBLISHED, Ordering::Release);
me.tag.store(Tag::PUBLISHED, Ordering::Release);
id
}
}
impl Drop for Slot<'_> {
fn drop(&mut self) {
self.mirror.store(Tag::AVAILABLE, Ordering::Release);
self.tag.store(Tag::AVAILABLE, Ordering::Release);
}
}
#[cfg(test)]
mod tests {
use super::*;
use diskann_utils::views::Matrix;
fn store(entries: usize, entry_bytes: usize, frozen: usize) -> Result<Store, StoreError> {
let mut data = Matrix::new(0u8, frozen, entry_bytes);
let mut base = 0u8;
for row in data.row_iter_mut() {
row.fill(base);
base = base.wrapping_add(1);
}
let mut config = Config::new(entries, Bytes::new(entry_bytes), 0);
config.epoch_guard_slots(NonZeroUsize::new(10).unwrap());
config.freelist_recycle_capacity(NonZeroU32::new(16).unwrap());
Store::new(config, data.as_view())
}
#[test]
fn new_rejects_mismatched_frozen_dim() {
let data = Matrix::new(0u8, 1, 8);
let err = Store::new(Config::new(4, Bytes::new(16), 0), data.as_view()).unwrap_err();
assert!(matches!(
err.0,
StoreErrorInner::MismatchedFrozenPointDim { dim: 8, .. }
));
}
#[test]
fn new_requires_a_frozen_point() {
let err = store(4, 8, 0).unwrap_err();
assert!(matches!(err.0, StoreErrorInner::NeedFrozenPoint));
}
#[test]
fn new_rejects_total_slot_overflow() {
let data = Matrix::new(0u8, 1, 8);
let err = Store::new(
Config::new(u32::MAX as usize, Bytes::new(8), 0),
data.as_view(),
)
.unwrap_err();
assert!(matches!(err.0, StoreErrorInner::TooManyEntries { .. }));
}
#[test]
fn new_rejects_too_many_neighbors() {
let data = Matrix::new(0u8, 1, 8);
let err = Store::new(
Config::new(4, Bytes::new(8), u32::MAX.into_usize() + 1),
data.as_view(),
)
.unwrap_err();
assert!(matches!(err.0, StoreErrorInner::TooManyNeighbors { .. }));
}
#[test]
fn frozen_range_follows_writable_slots() {
let s = store(4, 8, 2).unwrap();
assert_eq!(s.frozen(), 4..6);
let reader = s.reader().unwrap();
for i in 0..4 {
assert!(!s.can_read_approximate(i).unwrap());
assert!(!reader.can_read(i).unwrap());
assert!(reader.read(i).is_none());
}
assert!(s.can_read_approximate(4).unwrap());
assert!(reader.can_read(4).unwrap());
assert_eq!(reader.read(4).unwrap(), &[0, 0, 0, 0, 0, 0, 0, 0]);
assert!(s.can_read_approximate(5).unwrap());
assert!(reader.can_read(5).unwrap());
assert_eq!(reader.read(5).unwrap(), &[1, 1, 1, 1, 1, 1, 1, 1]);
assert!(s.can_read_approximate(6).is_none());
assert!(reader.can_read(6).is_none());
assert!(reader.read(6).is_none());
}
#[test]
fn acquire_write_publish_read_roundtrip() {
let s = store(4, 8, 1).unwrap();
let reader = s.reader().expect("reader guard available");
let idx = {
let mut slot = s.acquire().expect("a fresh store has free slots");
let idx = slot.slot() as usize;
slot.as_mut_slice()
.copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
assert!(reader.read(idx).is_none());
assert!(!s.can_read_approximate(idx).unwrap());
slot.publish();
idx
};
assert_eq!(reader.read(idx), Some([1, 2, 3, 4, 5, 6, 7, 8].as_slice()));
assert!(s.can_read_approximate(idx).unwrap());
}
#[test]
fn unpublished_slots_are_immediately_available() {
let s = store(4, 8, 1).unwrap();
let reader = s.reader().expect("reader guard available");
let idx = {
let mut slot = s.acquire().expect("a fresh store has free slots");
let idx = slot.slot() as usize;
slot.as_mut_slice()
.copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
assert!(reader.read(idx).is_none());
assert!(!s.can_read_approximate(idx).unwrap());
idx
};
assert!(reader.read(idx).is_none());
assert!(!s.can_read_approximate(idx).unwrap());
}
#[test]
fn acquire_exhausts_then_reports_none() {
let s = store(2, 8, 1).unwrap();
let _a = s.acquire().expect("first writable slot");
let _b = s.acquire().expect("second writable slot");
assert!(
s.acquire().is_none(),
"all writable slots are owned, so acquire must fail"
);
}
#[test]
fn retire_out_of_bounds() {
let s = store(4, 8, 1).unwrap();
assert!(matches!(s.retire(999), Err(RetireError::OutOfBounds)));
}
#[test]
fn retire_rejects_reserved_slots() {
let s = store(4, 8, 1).unwrap();
assert!(matches!(
s.retire(0),
Err(RetireError::SlotIsReserved { .. })
));
let frozen = s.frozen().start as usize;
assert!(matches!(
s.retire(frozen),
Err(RetireError::SlotIsReserved { .. })
));
let slot = s.acquire().unwrap();
assert!(matches!(
s.retire(slot.slot() as usize),
Err(RetireError::SlotIsReserved { .. })
));
}
#[test]
fn retire_published_slot_then_unreadable() {
let s = store(4, 8, 1).unwrap();
let idx = {
let slot = s.acquire().unwrap();
slot.publish() as usize
};
assert!(s.retire(idx).is_ok());
let reader = s.reader().unwrap();
assert_eq!(reader.read(idx), None);
assert_eq!(reader.can_read(idx), Some(false));
assert!(matches!(
s.retire(idx),
Err(RetireError::SlotIsReserved { .. })
));
}
#[test]
fn test_recycling() {
let entries = if cfg!(miri) { 16 } else { 2048 };
let s = store(entries, 4, 2).unwrap();
let mut count = 0;
while let Some(slot) = s.acquire() {
slot.publish();
count += 1;
}
assert_eq!(count, s.writable().len());
for i in s.writable() {
s.retire(i.into_usize()).unwrap();
}
let mut count = 0;
while let Some(slot) = s.acquire() {
slot.publish();
count += 1;
}
assert_eq!(count, s.writable().len());
}
}