mod internal_docs {
#[expect(
unused_imports,
reason = "this keeps cross-references nicer for internal docs"
)]
use super::*;
}
use std::{
mem::ManuallyDrop,
num::{NonZeroU32, NonZeroUsize},
sync::atomic::Ordering,
};
use diskann::{ANNError, utils::IntoUsize};
use thiserror::Error;
use crate::{
buffer::BufferError,
epoch::{self, Registry},
freelist::{self, Freelist},
neighbors::{Neighbors, NeighborsError},
num::{Capacity, IdLimit, MaxDegree},
tag::{self, AtomicTag, Tag},
};
pub(crate) mod intrusive;
pub(crate) mod slots;
#[cfg(feature = "quantization")]
pub(crate) mod cons;
#[cfg(feature = "quantization")]
pub(crate) mod optional;
#[cfg(any(test, feature = "quantization", feature = "integration-test"))]
pub(crate) mod simple;
#[cfg(any(test, feature = "integration-test"))]
pub(crate) mod checked;
#[derive(Debug)]
pub(crate) struct Lifecycle(());
impl Lifecycle {
const fn new() -> Self {
Self(())
}
}
#[derive(Debug, Clone)]
pub struct Config {
epoch_guard_slots: NonZeroUsize,
freelist_recycle_capacity: NonZeroU32,
}
impl Config {
pub fn new() -> Self {
const DEFAULT_FREELIST_RECYCLE_CAPACITY: NonZeroU32 = NonZeroU32::new(1024).unwrap();
Self {
epoch_guard_slots: Registry::default_guard_slots(),
freelist_recycle_capacity: DEFAULT_FREELIST_RECYCLE_CAPACITY,
}
}
pub fn epoch_guard_slots(&mut self, epoch_guard_slots: NonZeroUsize) -> &mut Self {
self.epoch_guard_slots = epoch_guard_slots;
self
}
pub fn freelist_recycle_capacity(
&mut self,
freelist_recycle_capacity: NonZeroU32,
) -> &mut Self {
self.freelist_recycle_capacity = freelist_recycle_capacity;
self
}
#[cfg(any(test, feature = "integration-test"))]
#[doc(hidden)]
pub fn __exhaustive(
epoch_guard_slots: NonZeroUsize,
freelist_recycle_capacity: NonZeroU32,
) -> Self {
Self {
epoch_guard_slots,
freelist_recycle_capacity,
}
}
}
impl Default for Config {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub(crate) struct Layout {
capacity: Capacity,
max_degree: MaxDegree,
frozen: u32,
}
impl Layout {
pub(crate) fn new(capacity: Capacity, max_degree: MaxDegree, frozen: u32) -> Self {
Self {
capacity,
max_degree,
frozen,
}
}
}
#[derive(Debug)]
pub(crate) struct Store<T> {
slots: T,
unfrozen: Capacity,
tags: tag::Authoritative,
freelist: Freelist,
registry: Registry,
neighbors: Neighbors,
}
const SCAN_ADVANCE_RETRY_LIMIT: usize = 20;
const CHECK_FREELIST_EVERY: usize = 4;
impl<T> Store<T>
where
T: slots::Slots,
{
pub(crate) fn new<C>(layout: Layout, config: Config, slots: C) -> Result<Self, StoreError>
where
C: slots::SlotsConfig<Slots = T>,
{
let Layout {
capacity,
max_degree,
frozen,
} = layout;
let Config {
epoch_guard_slots,
freelist_recycle_capacity,
} = config;
let too_many_entries = || StoreError::too_many_entries(capacity, frozen);
let entries: u32 = capacity
.value()
.try_into()
.map_err(|_| too_many_entries())?;
let id_limit = IdLimit::new(entries.checked_add(frozen).ok_or_else(too_many_entries)?);
let max_degree: u32 = max_degree
.value()
.try_into()
.map_err(|_| StoreError::too_many_neighbors(max_degree))?;
let registry = Registry::with_capacity(epoch_guard_slots);
let tags = tag::Authoritative::new(id_limit);
let slots = unsafe { slots::SlotsConfig::build(slots, registry.handle(), &tags) }
.map_err(StoreError::slots)?;
let slots_id_limit = slots.id_limit();
if slots_id_limit != id_limit {
return Err(StoreError::invalid_construction(slots_id_limit, id_limit));
}
let me = Self {
slots,
unfrozen: capacity,
tags,
freelist: Freelist::new(entries, freelist_recycle_capacity),
registry,
neighbors: Neighbors::new(id_limit, max_degree)?,
};
Ok(me)
}
pub(crate) fn slots(&self) -> &T {
&self.slots
}
pub(crate) fn frozen(&self) -> std::ops::Range<u32> {
(self.unfrozen.value() as u32)..self.neighbors.entries()
}
pub(crate) fn id_limit(&self) -> IdLimit {
IdLimit::new(self.tags.len() as u32)
}
pub(crate) fn capacity(&self) -> Capacity {
self.unfrozen
}
pub(crate) fn neighbors(&self) -> &Neighbors {
&self.neighbors
}
pub(crate) fn try_drain(&self) -> Option<usize> {
let drain = self.registry.try_advance()?;
let items = drain.len();
for i in drain {
#[expect(clippy::panic, reason = "this is an unrecoverable program bug")]
let Some(tag) = self.tags.get(i.into_usize()) else {
panic!(
"received an invalid ID ({}) while reclaiming slots - max allowed is {}",
i,
self.neighbors.entries(),
);
};
unsafe { slots::Slots::reclaim(self.slots(), i, Lifecycle::new()) };
assert_eq!(
tag.load(Ordering::Relaxed),
Tag::RETIRING,
"CONCURRENCY VIOLATION",
);
tag.store(Tag::AVAILABLE, Ordering::Release);
self.freelist.push(i);
}
Some(items)
}
pub(crate) fn guard<'a, F, R>(&'a self, f: F) -> Result<R, epoch::Unavailable>
where
F: FnOnce(&'a T, epoch::Guard<'a>) -> R,
{
let guard = self.registry.guard()?;
Ok(f(self.slots(), guard))
}
pub(crate) fn acquire(&self) -> Option<Exclusive<'_, <T as slots::Slots>::Exclusive<'_>>> {
for _ in 0..SCAN_ADVANCE_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(_) => {
unsafe { slots::Slots::retire(self.slots(), i as u32, Lifecycle::new()) };
guard.retire(i as u32);
Ok(())
}
Err(_) => Err(RetireError::CouldNotClaimSlot),
}
}
fn scan_acquire(&self) -> Option<Exclusive<'_, <T as slots::Slots>::Exclusive<'_>>> {
let mut remaining = self.unfrozen.value().div_ceil(SCAN_ADVANCE_RETRY_LIMIT);
let mut chunks_since_freelist_check = 0;
let mut acquired: Option<Exclusive<'_, <T as slots::Slots>::Exclusive<'_>>> = 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 == CHECK_FREELIST_EVERY {
if let Some(id) = self.freelist.pop_recycled()
&& let Some(slot) = self.slot(id)
{
return Some(slot);
}
chunks_since_freelist_check = 0;
}
}
None
}
pub(crate) fn slot(&self, i: u32) -> Option<Exclusive<'_, <T as slots::Slots>::Exclusive<'_>>> {
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<Exclusive<'a, <T as slots::Slots>::Exclusive<'a>>> {
if tag.load(Ordering::Relaxed) != Tag::AVAILABLE {
return None;
}
match tag.compare_exchange(
Tag::AVAILABLE,
Tag::OWNED,
Ordering::Acquire,
Ordering::Relaxed,
) {
Ok(_) => {
let data = unsafe { slots::Slots::acquire(self.slots(), slot, Lifecycle::new()) };
Some(Exclusive {
tag,
data: ManuallyDrop::new(data),
slot,
})
}
Err(_) => None,
}
}
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.value() as u32
}
}
#[derive(Debug, Error)]
#[error(transparent)]
pub(crate) struct StoreError(StoreErrorInner);
impl StoreError {
fn too_many_entries(capacity: Capacity, frozen: u32) -> Self {
Self(StoreErrorInner::TooManyEntries {
entries: capacity.value(),
frozen,
})
}
fn too_many_neighbors(neighbors: MaxDegree) -> Self {
Self(StoreErrorInner::TooManyNeighbors {
neighbors: neighbors.value(),
})
}
#[track_caller]
fn slots<E>(err: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self(StoreErrorInner::SlotsError(ANNError::new(err)))
}
fn invalid_construction(got: IdLimit, expected: IdLimit) -> Self {
Self(StoreErrorInner::InvalidConstruction { got, expected })
}
}
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())
}
}
diskann::convert_error!(StoreError);
#[derive(Debug, Error)]
enum StoreErrorInner {
#[error(
"total points ({} + {} frozen) must not exceed `u32::MAX`",
entries,
frozen
)]
TooManyEntries { entries: usize, frozen: u32 },
#[error("number of neighbors ({}) may not exceed `u32::MAX`", neighbors)]
TooManyNeighbors { neighbors: usize },
#[error(transparent)]
BufferError(#[from] BufferError),
#[error(transparent)]
NeighborsError(#[from] NeighborsError),
#[error("error creating slots")]
SlotsError(ANNError),
#[error("requested {} but the slots returned {}", expected, got)]
InvalidConstruction { got: IdLimit, expected: IdLimit },
}
#[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,
}
diskann::convert_error!(RetireError);
#[derive(Debug)]
pub(crate) struct Exclusive<'a, E>
where
E: slots::Exclusive,
{
tag: &'a AtomicTag,
data: ManuallyDrop<E>,
slot: u32,
}
impl<'a, E> Exclusive<'a, E>
where
E: slots::Exclusive,
{
pub(crate) fn data(&mut self) -> &mut E {
&mut self.data
}
pub(crate) fn slot(&self) -> u32 {
self.slot
}
pub(crate) fn freeze(self) {
let mut me = ManuallyDrop::new(self);
slots::Exclusive::freeze(
unsafe { ManuallyDrop::take(&mut me.data) },
Lifecycle::new(),
);
me.tag.store(Tag::FROZEN, Ordering::Release);
}
pub(crate) fn publish(self) -> u32 {
let id = self.slot();
let mut me = ManuallyDrop::new(self);
slots::Exclusive::publish(
unsafe { ManuallyDrop::take(&mut me.data) },
Lifecycle::new(),
);
me.tag.store(Tag::PUBLISHED, Ordering::Release);
id
}
}
impl<E> Drop for Exclusive<'_, E>
where
E: slots::Exclusive,
{
fn drop(&mut self) {
slots::Exclusive::abort(
unsafe { ManuallyDrop::take(&mut self.data) },
Lifecycle::new(),
);
self.tag.store(Tag::AVAILABLE, Ordering::Release);
}
}
#[cfg(test)]
mod tests {
use super::{checked::Checked, *};
use std::assert_matches;
use diskann_utils::assert_contains;
#[derive(Debug)]
struct FaultyConfig;
impl slots::SlotsConfig for FaultyConfig {
type Slots = Checked;
type Error = diskann::error::Infallible;
unsafe fn build(
self,
handle: epoch::RegistryHandle,
tags: &tag::Authoritative,
) -> Result<Checked, diskann::error::Infallible> {
let faulty = tags.id_limit().value().checked_sub(1).unwrap_or(1);
Ok(Checked::new(handle, IdLimit::new(faulty)))
}
}
fn store(entries: usize, frozen: u32) -> Result<Store<Checked>, StoreError> {
let config =
Config::__exhaustive(NonZeroUsize::new(10).unwrap(), NonZeroU32::new(16).unwrap());
let layout = Layout::new(Capacity::new(entries), MaxDegree::new(0), frozen);
let store = Store::new(layout, config, Checked::config())?;
assert_eq!(store.frozen().len(), frozen.into_usize());
for (i, id) in store.frozen().enumerate() {
let mut slot = store.slot(id).unwrap();
slot.data().set(i as u64);
slot.freeze();
}
Ok(store)
}
fn reader(store: &Store<Checked>) -> checked::Reader<'_> {
store.guard(|checked, guard| checked.reader(guard)).unwrap()
}
#[test]
fn new_rejects_total_slot_overflow() {
let err = Store::new(
Layout::new(Capacity::new(u32::MAX as usize), MaxDegree::new(0), 1),
Config::default(),
Checked::config(),
)
.unwrap_err();
assert_matches!(err.0, StoreErrorInner::TooManyEntries { .. });
assert_contains!(err.to_string(), "must not exceed `u32::MAX`");
}
#[test]
fn new_rejects_too_many_neighbors() {
let err = Store::new(
Layout::new(
Capacity::new(4),
MaxDegree::new(u32::MAX.into_usize() + 1),
0,
),
Config::default(),
Checked::config(),
)
.unwrap_err();
assert_matches!(err.0, StoreErrorInner::TooManyNeighbors { .. });
}
#[test]
fn new_rejects_faulty_slots() {
let err = Store::new(
Layout::new(Capacity::new(4), MaxDegree::new(10), 0),
Config::default(),
FaultyConfig,
)
.unwrap_err();
assert_matches!(err.0, StoreErrorInner::InvalidConstruction { .. });
}
#[test]
fn frozen_range_follows_writable_slots() {
let s = store(4, 2).unwrap();
assert_eq!(s.frozen(), 4..6);
let reader = reader(&s);
for i in 0u32..4 {
assert!(!s.can_read_approximate(i.into_usize()).unwrap());
assert!(reader.read(i).is_none());
}
assert!(s.can_read_approximate(4).unwrap());
assert_eq!(reader.read(4).unwrap().get(), 0);
assert!(s.can_read_approximate(5).unwrap());
assert_eq!(reader.read(5).unwrap().get(), 1);
assert!(s.can_read_approximate(6).is_none());
assert!(reader.read(6).is_none());
}
#[test]
fn acquire_write_publish_read_roundtrip() {
let s = store(4, 1).unwrap();
let reader = reader(&s);
let idx = {
let mut slot = s.acquire().expect("a fresh store has free slots");
let idx = slot.slot();
slot.data().set(10);
assert!(reader.read(idx).is_none());
assert!(!s.can_read_approximate(idx.into_usize()).unwrap());
slot.publish();
idx
};
assert_eq!(reader.read(idx).unwrap().get(), 10,);
assert!(s.can_read_approximate(idx.into_usize()).unwrap());
}
#[test]
fn unpublished_slots_are_immediately_available() {
let s = store(4, 1).unwrap();
let reader = reader(&s);
let idx = {
let mut slot = s.acquire().expect("a fresh store has free slots");
let idx = slot.slot();
slot.data().set(100);
assert!(reader.read(idx).is_none());
assert!(!s.can_read_approximate(idx.into_usize()).unwrap());
idx
};
assert!(reader.read(idx).is_none());
assert!(!s.can_read_approximate(idx.into_usize()).unwrap());
}
#[test]
fn acquire_exhausts_then_reports_none() {
let s = store(2, 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, 1).unwrap();
assert!(matches!(s.retire(999), Err(RetireError::OutOfBounds)));
}
#[test]
fn retire_rejects_reserved_slots() {
let s = store(4, 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, 1).unwrap();
let idx = {
let mut slot = s.acquire().unwrap();
slot.data().set(101);
slot.publish()
};
assert!(s.retire(idx.into_usize()).is_ok());
let reader = reader(&s);
assert_matches!(reader.read(idx), None);
assert!(matches!(
s.retire(idx.into_usize()),
Err(RetireError::SlotIsReserved { .. })
));
}
#[test]
fn test_recycling() {
let entries = if cfg!(miri) { 16 } else { 2048 };
let s = store(entries, 2).unwrap();
assert_eq!(s.writable().len(), entries);
let mut count = 0;
while let Some(mut slot) = s.acquire() {
slot.data().set(count as u64);
slot.publish();
count += 1;
}
assert_eq!(count, entries);
{
let reader = reader(&s);
for i in 0..entries {
assert_eq!(reader.read(i as u32).unwrap().get(), i as u64);
}
}
for i in s.writable() {
s.retire(i.into_usize()).unwrap();
}
let mut count = 0;
while let Some(mut slot) = s.acquire() {
slot.data().set(count as u64);
slot.publish();
count += 1;
}
assert_eq!(count, entries);
}
}