Skip to main content

diskann_bftree/
neighbors.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! Bf-Tree neighbor list provider.
7
8use std::marker::PhantomData;
9
10use bf_tree::{BfTree, Config};
11use bytemuck::{cast_slice, cast_slice_mut};
12use diskann::{
13    graph::AdjacencyList,
14    provider::{self, HasId},
15    utils::{IntoUsize, VectorId},
16    ANNError, ANNResult,
17};
18
19use super::ConfigError;
20use crate::locks::StripedLocks;
21use crate::{bftree_insert, TestCallCount};
22
23pub struct NeighborProvider<I: VectorId + IntoUsize> {
24    adjacency_list_index: BfTree,
25    dim: usize, // Max number of neighbors in a neighbor list + 1 for the neighbor count
26    #[allow(dead_code)]
27    pub(crate) num_get_calls: TestCallCount,
28    _phantom: PhantomData<I>,
29}
30
31impl<I: VectorId + IntoUsize> HasId for NeighborProvider<I> {
32    type Id = I;
33}
34
35impl<I: VectorId + IntoUsize> NeighborProvider<I> {
36    /// Create a new instance based on bf-tree Config directly.
37    pub fn new_with_config(max_degree: u32, config: Config) -> ANNResult<Self> {
38        let key_size = std::mem::size_of::<u32>();
39        let value_size = (max_degree as usize + 1) * std::mem::size_of::<u32>();
40        crate::validate_record_size("neighbor_provider", &config, key_size, value_size)?;
41
42        let adj_list_index = BfTree::with_config(config, None).map_err(ConfigError)?;
43
44        Self::new(max_degree, adj_list_index)
45    }
46
47    fn new(max_degree: u32, adjacency_list_index: BfTree) -> ANNResult<Self> {
48        let dim = 1 + max_degree.into_usize();
49
50        Ok(Self {
51            adjacency_list_index,
52            dim,
53            num_get_calls: TestCallCount::default(),
54            _phantom: PhantomData,
55        })
56    }
57
58    /// Access the BfTree config
59    pub(crate) fn config(&self) -> &Config {
60        self.adjacency_list_index.config()
61    }
62
63    /// Access the underlying BfTree
64    pub(crate) fn bftree(&self) -> &BfTree {
65        &self.adjacency_list_index
66    }
67
68    /// Return the maximum degree (number of neighbors per vector)
69    ///
70    pub fn max_degree(&self) -> u32 {
71        (self.dim - 1) as u32
72    }
73
74    /// Create a new instance from an existing BfTree (for loading from snapshot)
75    ///
76    pub(crate) fn new_from_bftree(
77        max_degree: u32,
78        adjacency_list_index: BfTree,
79    ) -> ANNResult<Self> {
80        Self::new(max_degree, adjacency_list_index)
81    }
82
83    /// Retrieve the neighbor list of a vector.
84    ///
85    /// Does not acquire any lock. Callers must ensure appropriate synchronization.
86    pub fn get_neighbors(&self, vector_id: I, neighbors: &mut AdjacencyList<I>) -> ANNResult<()> {
87        #[cfg(test)]
88        self.num_get_calls.increment();
89
90        // Clear the output before any early returns so callers always see
91        // a deterministic empty state on error.
92        neighbors.clear();
93
94        self.get_neighbors_unlocked(vector_id, neighbors)
95    }
96
97    /// Retrieve the neighbor list without acquiring any lock.
98    ///
99    /// Callers must ensure they already hold the appropriate lock.
100    fn get_neighbors_unlocked(
101        &self,
102        vector_id: I,
103        neighbors: &mut AdjacencyList<I>,
104    ) -> ANNResult<()> {
105        // Resize 'neighbors' to hold any full-size neighbor list + I-cell for length
106        let mut guard = neighbors.resize(self.dim);
107
108        // Serialize the key, vector_id, into a byte string, &[u8]
109        let key = bytemuck::bytes_of(&vector_id);
110
111        // Search and retrieve the corresponding neighbor list data as a byte string,
112        // &[u8], in the format described in [`Self::set_neighbors_internal`].
113        let value = cast_slice_mut::<I, u8>(&mut guard);
114        match self.adjacency_list_index.read(key, value) {
115            bf_tree::LeafReadResult::Found(read_size) => {
116                // If found, then re-construct the neighbor list (valid neighbors only)
117                if read_size > 0 {
118                    // A retrieved neighbor list should be exactly dim length
119                    if read_size as usize != self.dim * std::mem::size_of::<I>() {
120                        return Err(ANNError::log_index_error(
121                            "Retrieved neighbor list is not expected length = max degree + 1",
122                        ));
123                    }
124
125                    // The last I-cell stores the neighbor count (see `cell_to_len`).
126                    //
127                    // SAFETY: we know this will not panic since
128                    // read_size == self.dim * std::mem::size_of::<I>().
129                    let count = cell_to_len(&guard[self.dim - 1]);
130
131                    // The specified list length must be smaller than the retrieved data length
132                    if count > self.max_degree() {
133                        return Err(ANNError::log_index_error(
134                            "Size of retrieved neighbor list is shorter than the stored neighbor count",
135                        ));
136                    }
137
138                    guard.finish(count as usize);
139                }
140            }
141            bf_tree::LeafReadResult::Deleted => {
142                return Err(ANNError::log_index_error(
143                    "The bf-tree entry for the vector is marked as deleted",
144                ));
145            }
146            bf_tree::LeafReadResult::InvalidKey => {
147                return Err(ANNError::log_index_error(
148                    "The bf-tree entry for the vector key is marked as invalid",
149                ));
150            }
151            bf_tree::LeafReadResult::NotFound => {
152                return Err(ANNError::log_index_error(
153                    "The bf-tree entry for the vector key is marked as not found",
154                ));
155            }
156        };
157
158        Ok(())
159    }
160
161    /// Internal function for setting the neighbors for a vector id.
162    ///
163    /// Each key is a `VectorId`, written in bytes. The value is a
164    /// neighbor list of length exactly `self.dim`. Specifically,
165    /// an array of exactly `n` neighbors is written as :
166    /// ```text
167    /// |  I0  | ... |  In  | padding |  ...  | [n; 0] |
168    ///
169    /// -----------------------------------------------
170    ///                     |
171    ///                 self.dim
172    /// ```
173    /// where `[n; 0]` represents the u32 count `n` byte-packed
174    /// into a type `I`, and 'padding' indicates unfilled empty slots.
175    ///
176    /// Note: assuming all neighbors in the input list, 'neighbors',
177    /// are valid.
178    fn set_neighbors_internal(
179        &self,
180        vector_id: I,
181        neighbors: &[I],
182        buf: &mut [I],
183    ) -> ANNResult<()> {
184        #[cfg(test)]
185        self.num_get_calls.increment();
186
187        if buf.len() < self.dim {
188            return Err(ANNError::log_index_error(
189                "The provided buffer is not long enough",
190            ));
191        }
192
193        // Serialize the value into the reusable buffer.
194        buf[..neighbors.len()].copy_from_slice(neighbors);
195        buf[self.dim - 1] = len_to_cell(neighbors.len() as u32);
196
197        let key = bytemuck::bytes_of(&vector_id);
198        let value = cast_slice::<I, u8>(&buf[..self.dim]);
199
200        bftree_insert(&self.adjacency_list_index, key, value)?;
201
202        Ok(())
203    }
204
205    /// Insert a neighbor list of a vector in bf-tree as a (K, V) pair.
206    ///
207    /// Does not acquire any lock. Callers must ensure appropriate synchronization.
208    ///
209    /// The list of neighbors and their length is written into the passed buffer
210    /// `buf` before writing the leading `self.dim * std::mem::size_of::<I>()`
211    /// bytes of it into the bf-tree.
212    ///
213    /// # Errors
214    ///
215    ///  - Neighbor length is larger than `self.max_degree()`
216    ///  - Buffer length (in `I` cells) is smaller than `max_degree() + 1`
217    pub fn set_neighbors(&self, vector_id: I, neighbors: &[I], buf: &mut [I]) -> ANNResult<()> {
218        if neighbors.len() > self.dim - 1 {
219            return Err(ANNError::log_index_error(
220                "The provided neighbor list is longer than the max degree",
221            ));
222        };
223
224        self.set_neighbors_internal(vector_id, neighbors, buf)
225    }
226
227    /// Append unique vectors into a neighbor list.
228    ///
229    /// Does not acquire any lock. Callers must ensure appropriate synchronization
230    /// for the entire read-modify-write cycle.
231    ///
232    /// The newly appended neighbor list will always be extended to 'dim'
233    /// long to avoid frequent mem copy in bf-tree.
234    ///
235    /// Note: assuming all neighbors in the input list, 'new_neighbor_ids', are valid
236    pub fn append_vector(
237        &self,
238        vector_id: I,
239        new_neighbor_ids: &[I],
240        buf: &mut [I],
241    ) -> ANNResult<()> {
242        // Retrieve existing neighborlist
243        let mut neighbor_list = AdjacencyList::with_capacity(self.dim);
244        self.get_neighbors_unlocked(vector_id, &mut neighbor_list)?;
245
246        // Append the new neighbors
247        let mut new_neighbor_added = false;
248        for new_neighbor_id in new_neighbor_ids {
249            if neighbor_list.len() == self.dim - 1 {
250                break;
251            }
252            new_neighbor_added |= neighbor_list.push(*new_neighbor_id);
253        }
254
255        // If unique new neighbors are appended, write back using the reusable buffer
256        if new_neighbor_added {
257            self.set_neighbors_internal(vector_id, &neighbor_list, buf)?;
258        }
259
260        Ok(())
261    }
262
263    pub fn delete_vector(&self, vector_id: I) -> ANNResult<()> {
264        let key = bytemuck::bytes_of(&vector_id);
265        self.adjacency_list_index.delete(key);
266        Ok(())
267    }
268
269    pub(crate) fn scratch<'a>(&'a self, locks: &'a StripedLocks) -> NeighborAccessor<'a, I> {
270        NeighborAccessor {
271            provider: self,
272            locks,
273            buf: vec![I::zeroed(); self.dim],
274        }
275    }
276}
277
278pub struct NeighborAccessor<'a, I>
279where
280    I: VectorId + IntoUsize,
281{
282    provider: &'a NeighborProvider<I>,
283    locks: &'a StripedLocks,
284    buf: Vec<I>,
285}
286
287impl<'a, I> NeighborAccessor<'a, I>
288where
289    I: VectorId + IntoUsize,
290{
291    pub fn write_neighbors(&mut self, id: I, neighbors: &[I]) -> ANNResult<()> {
292        let _guard = self.locks.lock(id.into_usize());
293        self.provider.set_neighbors(id, neighbors, &mut self.buf)
294    }
295    pub fn write_append(&mut self, id: I, neighbors: &[I]) -> ANNResult<()> {
296        let _guard = self.locks.lock(id.into_usize());
297        self.provider.append_vector(id, neighbors, &mut self.buf)
298    }
299}
300
301impl<'a, I> HasId for NeighborAccessor<'a, I>
302where
303    I: VectorId + IntoUsize,
304{
305    type Id = I;
306}
307
308impl<'a, I> provider::NeighborAccessor for NeighborAccessor<'a, I>
309where
310    I: VectorId + IntoUsize,
311{
312    fn get_neighbors(
313        &mut self,
314        id: Self::Id,
315        neighbors: &mut AdjacencyList<Self::Id>,
316    ) -> impl std::future::Future<Output = ANNResult<()>> + Send {
317        std::future::ready(self.provider.get_neighbors(id, neighbors))
318    }
319}
320
321impl<'a, I> provider::NeighborAccessorMut for NeighborAccessor<'a, I>
322where
323    I: VectorId + IntoUsize,
324{
325    fn set_neighbors(
326        &mut self,
327        id: Self::Id,
328        neighbors: &[Self::Id],
329    ) -> impl std::future::Future<Output = ANNResult<()>> + Send {
330        let _guard = self.locks.lock(id.into_usize());
331        std::future::ready(self.provider.set_neighbors(id, neighbors, &mut self.buf))
332    }
333    fn append_vector(
334        &mut self,
335        id: Self::Id,
336        neighbors: &[Self::Id],
337    ) -> impl std::future::Future<Output = ANNResult<()>> + Send {
338        let _guard = self.locks.lock(id.into_usize());
339        std::future::ready(self.provider.append_vector(id, neighbors, &mut self.buf))
340    }
341}
342
343/// Const for size in bytes of a `u32`
344const FOUR: usize = std::mem::size_of::<u32>();
345
346/// Encode a neighbor-list length as an `I`-sized cell.
347///
348/// The count is stored as a little-endian `u32` in the low 4 bytes of the cell; any
349/// remaining bytes are zero. The compile-time assertion guarantees that `I` is wide
350/// enough.
351fn len_to_cell<I: bytemuck::Pod>(len: u32) -> I {
352    const { assert!(std::mem::size_of::<I>() >= FOUR) };
353    let mut cell = I::zeroed();
354    bytemuck::bytes_of_mut(&mut cell)[..FOUR].copy_from_slice(&len.to_le_bytes());
355    cell
356}
357
358/// Decode a neighbor-list length from a cell produced by [`len_to_cell`].
359fn cell_to_len<I: bytemuck::Pod>(cell: &I) -> u32 {
360    const { assert!(std::mem::size_of::<I>() >= FOUR) };
361
362    let mut low = [0u8; FOUR];
363    low.copy_from_slice(&bytemuck::bytes_of(cell)[..FOUR]);
364
365    u32::from_le_bytes(low)
366}
367
368///////////
369// Tests //
370///////////
371
372/// These unit tests target the functionality of Bf-Tree neighbor list provider alone
373#[cfg(test)]
374mod tests {
375    use std::sync::Arc;
376
377    use tokio::task::JoinSet;
378
379    use super::*;
380
381    /// Number of concurrent worker tasks for stress tests, scaled to the host's
382    /// available parallelism with a floor so contention is guaranteed even on
383    /// low-core CI runners. Falls back to the floor if parallelism is unknown.
384    fn stress_thread_count() -> u32 {
385        std::thread::available_parallelism()
386            .map(|n| n.get() as u32)
387            .unwrap_or(8)
388            .max(8)
389    }
390
391    /// Build a `NeighborProvider<u32>` with a default bf-tree config.
392    fn new_provider(max_degree: u32) -> NeighborProvider<u32> {
393        NeighborProvider::<u32>::new_with_config(max_degree, Config::default()).unwrap()
394    }
395
396    /// Build a shared `NeighborProvider<u32>` with a default bf-tree config.
397    fn new_shared_provider(max_degree: u32) -> Arc<NeighborProvider<u32>> {
398        Arc::new(new_provider(max_degree))
399    }
400
401    /// Length round-trips through an `I`-sized cell .
402    #[test]
403    fn len_cell_round_trip() {
404        for len in [0u32, 1, 42, u32::MAX] {
405            assert_eq!(cell_to_len::<u32>(&len_to_cell::<u32>(len)), len);
406            assert_eq!(cell_to_len::<u64>(&len_to_cell::<u64>(len)), len);
407        }
408    }
409
410    /// Test corner cases of appending to neighbor list
411    #[tokio::test]
412    async fn test_neighbor_accessors() {
413        let locks = Arc::new(StripedLocks::new());
414        let neighbor_provider = new_provider(6);
415        let mut scratch = neighbor_provider.scratch(&locks);
416
417        // Set the neighbor list of a vector
418        let adj_list = vec![1, 2, 3];
419        scratch.write_neighbors(1, &adj_list).unwrap();
420
421        let mut result = AdjacencyList::with_capacity(10);
422        neighbor_provider.get_neighbors(1, &mut result).unwrap();
423        assert_eq!(&*adj_list, &*result);
424
425        // Append two neighbors, one of which is a duplicate
426        let new_neighbors = vec![9, 2, 9];
427        scratch.write_append(1, &new_neighbors).unwrap();
428
429        neighbor_provider.get_neighbors(1, &mut result).unwrap();
430
431        let adj_list_new = vec![1, 2, 3, 9];
432        assert_eq!(&*adj_list_new, &*result);
433
434        // Append three more neighbors, and the last one should be ignored due to max degree
435        let new_neighbors = vec![5, 6, 7];
436        scratch.write_append(1, &new_neighbors).unwrap();
437
438        neighbor_provider.get_neighbors(1, &mut result).unwrap();
439
440        let adj_list_new = vec![1, 2, 3, 9, 5, 6];
441        assert_eq!(&*adj_list_new, &*result);
442
443        // Overwrite the neighbor list of the vector to empty
444        scratch.write_neighbors(1, &[]).unwrap();
445        neighbor_provider.get_neighbors(1, &mut result).unwrap();
446        assert!(result.is_empty());
447
448        // Append to an emptied neighbor list
449        let new_neighbors = vec![3, 4, 5];
450        scratch.write_append(1, &new_neighbors).unwrap();
451
452        neighbor_provider.get_neighbors(1, &mut result).unwrap();
453        assert_eq!(&*new_neighbors, &*result);
454
455        neighbor_provider.delete_vector(1).unwrap();
456        assert!(neighbor_provider.get_neighbors(1, &mut result).is_err());
457    }
458
459    /// Test the interleaved and parallel traversal of the Bf-Tree
460    /// by invoking the async accessors of the neighbor list provider
461    #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
462    async fn test_parallel_tree_traversal() {
463        let locks = Arc::new(StripedLocks::new());
464        let neighbor_provider = new_shared_provider(120);
465
466        let mut set = JoinSet::new();
467        for i in 0..100 {
468            let neighbor_list = vec![i as u32, (i + 1) as u32, (i + 2) as u32];
469            let neighbor_provider_clone = Arc::clone(&neighbor_provider);
470            let locks = locks.clone();
471            set.spawn(async move {
472                let mut scratch = neighbor_provider_clone.scratch(&locks);
473                scratch.write_neighbors(i as u32, &neighbor_list).unwrap();
474            });
475        }
476
477        while let Some(res) = set.join_next().await {
478            res.unwrap();
479        }
480
481        let mut result = AdjacencyList::with_capacity(121);
482        for i in 0..100 {
483            neighbor_provider
484                .get_neighbors(i as u32, &mut result)
485                .unwrap();
486
487            let neighbor_list = vec![i as u32, (i + 1) as u32, (i + 2) as u32];
488            assert_eq!(&*neighbor_list, &*result);
489        }
490    }
491
492    /// Test that cpr_snapshot can be called without errors
493    #[tokio::test]
494    async fn test_snapshot() {
495        let locks = Arc::new(StripedLocks::new());
496        // Use a temporary directory that gets cleaned up when dropped
497        let temp_dir = tempfile::tempdir().unwrap();
498        let snapshot_path = temp_dir.path().join("test_neighbor_snapshot.bftree");
499        let snapshot_output = temp_dir.path().join("test_neighbor_snapshot_out.bftree");
500
501        let mut bf_tree_config = Config::new(&snapshot_path, 16384 * 16);
502        bf_tree_config.storage_backend(bf_tree::StorageBackend::Std);
503        bf_tree_config.use_snapshot(true);
504
505        let neighbor_provider =
506            NeighborProvider::<u32>::new_with_config(6, bf_tree_config).unwrap();
507        let mut scratch = neighbor_provider.scratch(&locks);
508
509        // Set some neighbor lists
510        scratch.write_neighbors(1, &[2, 3, 4]).unwrap();
511        scratch.write_neighbors(2, &[1, 3, 5]).unwrap();
512
513        // Call cpr_snapshot - should not panic
514        neighbor_provider
515            .adjacency_list_index
516            .cpr_snapshot(&snapshot_output);
517
518        // Verify data is still accessible after snapshot
519        let mut result = AdjacencyList::with_capacity(10);
520        neighbor_provider.get_neighbors(1, &mut result).unwrap();
521        assert_eq!(&[2, 3, 4], &*result);
522
523        neighbor_provider.get_neighbors(2, &mut result).unwrap();
524        assert_eq!(&[1, 3, 5], &*result);
525
526        // temp_dir is automatically cleaned up when it goes out of scope
527    }
528
529    /// Test that max_degree returns the correct value
530    #[tokio::test]
531    async fn test_max_degree() {
532        // Test with various max_degree values
533        let neighbor_provider = new_provider(6);
534        assert_eq!(neighbor_provider.max_degree(), 6);
535
536        let neighbor_provider = new_provider(120);
537        assert_eq!(neighbor_provider.max_degree(), 120);
538
539        let neighbor_provider = new_provider(1);
540        assert_eq!(neighbor_provider.max_degree(), 1);
541    }
542
543    /// Test new_from_bftree constructor
544    #[tokio::test]
545    async fn test_new_from_bftree() {
546        let locks = Arc::new(StripedLocks::new());
547        let bftree = BfTree::with_config(Config::default(), None).expect("Failed to create BfTree");
548        let neighbor_provider = NeighborProvider::<u32>::new_from_bftree(10, bftree).unwrap();
549
550        assert_eq!(neighbor_provider.max_degree(), 10);
551
552        // Verify the provider is functional
553        let mut scratch = neighbor_provider.scratch(&locks);
554        scratch.write_neighbors(1, &[2, 3]).unwrap();
555        let mut result = AdjacencyList::with_capacity(11);
556        neighbor_provider.get_neighbors(1, &mut result).unwrap();
557        assert_eq!(&[2, 3], &*result);
558    }
559
560    /// Test other methods and edge cases of the vector provider and synchronization mechanism of Bf-Tree
561    #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
562    async fn test_parallel_neighbor_access() {
563        let locks = Arc::new(StripedLocks::new());
564        let neighbor_provider = new_shared_provider(120);
565
566        let mut set = JoinSet::new();
567        for _ in 0..5 {
568            let neighbor_provider_clone = Arc::clone(&neighbor_provider);
569            let locks = locks.clone();
570            set.spawn(async move {
571                let mut scratch = neighbor_provider_clone.scratch(&locks);
572                for i in 0..5 {
573                    scratch.write_neighbors(i as u32, &[1, 2, 3, 4, 5]).unwrap();
574                }
575
576                let mut result = AdjacencyList::with_capacity(121);
577                for i in 0..5 {
578                    neighbor_provider_clone
579                        .get_neighbors(i as u32, &mut result)
580                        .unwrap();
581
582                    assert_eq!(&[1, 2, 3, 4, 5], &*result);
583                }
584            });
585        }
586
587        while let Some(res) = set.join_next().await {
588            res.unwrap();
589        }
590    }
591
592    /// Stress test: many threads concurrently append to the SAME vertex.
593    ///
594    /// Without per-vertex locking, this test would non-deterministically lose edges
595    /// due to the TOCTOU race in append_vector's read-modify-write cycle. With the
596    /// per-vertex write lock, every appended edge must be present in the final list.
597    ///
598    /// Lost-update races are probabilistic, so a single pass may not surface a
599    /// regression. The scenario is therefore repeated over many outer iterations,
600    /// and the writer count scales with the host's available parallelism (with a
601    /// floor) to keep contention meaningful on CI boxes of any core count. The
602    /// `multi_thread` runtime is left unsized so tokio scales its worker pool to
603    /// the number of CPUs.
604    #[tokio::test(flavor = "multi_thread")]
605    async fn test_concurrent_append_no_lost_edges() {
606        let num_threads = stress_thread_count();
607        let edges_per_thread = 20u32;
608        // Size the degree to the workload so every distinct edge can be retained.
609        let max_degree = num_threads * edges_per_thread;
610
611        // Repeat the whole scenario many times: each iteration is a fresh chance
612        // to interleave threads such that a broken lock would drop an edge.
613        let outer_iterations = 100;
614        for _ in 0..outer_iterations {
615            let locks = Arc::new(StripedLocks::new());
616            let provider = new_shared_provider(max_degree);
617
618            // Initialize vertex 0 with an empty neighbor list.
619            let mut scratch = provider.scratch(&locks);
620            scratch.write_neighbors(0, &[]).unwrap();
621
622            // Each thread appends a unique set of neighbor IDs to vertex 0.
623            // Thread t appends IDs: [t*edges+1, ..., t*edges+edges]
624            let mut set = JoinSet::new();
625            for t in 0..num_threads {
626                let provider_clone = Arc::clone(&provider);
627                let locks = locks.clone();
628                set.spawn(async move {
629                    let mut scratch = provider_clone.scratch(&locks);
630                    let base = t * edges_per_thread + 1;
631                    for offset in 0..edges_per_thread {
632                        let neighbor_id = base + offset;
633                        scratch.write_append(0, &[neighbor_id]).unwrap();
634                    }
635                });
636            }
637
638            while let Some(res) = set.join_next().await {
639                res.unwrap();
640            }
641
642            // Verify ALL edges from all threads are present.
643            let mut result = AdjacencyList::with_capacity(max_degree as usize + 1);
644            provider.get_neighbors(0, &mut result).unwrap();
645
646            let expected_count = (num_threads * edges_per_thread) as usize;
647            assert_eq!(
648                result.len(),
649                expected_count,
650                "Expected {} edges but found {} — edges were lost!",
651                expected_count,
652                result.len()
653            );
654
655            // Verify every expected ID is present.
656            for t in 0..num_threads {
657                let base = t * edges_per_thread + 1;
658                for offset in 0..edges_per_thread {
659                    let expected_id = base + offset;
660                    assert!(
661                        result.contains(expected_id),
662                        "Missing edge {} from thread {}",
663                        expected_id,
664                        t
665                    );
666                }
667            }
668        }
669    }
670
671    /// Stress test: concurrent appends to DIFFERENT vertices (no contention).
672    ///
673    /// Validates that the lock table doesn't introduce false sharing or deadlocks
674    /// when independent vertices are mutated in parallel. The writer count scales
675    /// with the host's available parallelism (with a floor), and the `multi_thread`
676    /// runtime is left unsized so tokio scales its worker pool to the CPU count.
677    #[tokio::test(flavor = "multi_thread")]
678    async fn test_concurrent_append_independent_vertices() {
679        let locks = Arc::new(StripedLocks::new());
680        let max_degree = 64u32;
681        let num_threads = stress_thread_count();
682        let vertices_per_thread = 50u32;
683        let num_vertices = num_threads * vertices_per_thread; // disjoint ranges, no overlap
684        let provider = new_shared_provider(max_degree);
685
686        // Initialize all vertices with empty neighbor lists.
687        {
688            let mut scratch = provider.scratch(&locks);
689            for v in 0..num_vertices {
690                scratch.write_neighbors(v, &[]).unwrap();
691            }
692        }
693
694        let mut set = JoinSet::new();
695
696        for t in 0..num_threads {
697            let provider_clone = Arc::clone(&provider);
698            let locks = locks.clone();
699            set.spawn(async move {
700                let mut scratch = provider_clone.scratch(&locks);
701                // Each thread owns a disjoint range of vertices
702                let base_vertex = t * vertices_per_thread;
703                for i in 0..vertices_per_thread {
704                    let vertex = base_vertex + i;
705                    let neighbor_id = t * 1000 + i;
706                    scratch.write_append(vertex, &[neighbor_id]).unwrap();
707                }
708            });
709        }
710
711        while let Some(res) = set.join_next().await {
712            res.unwrap();
713        }
714
715        // Verify total edge count across all vertices matches expectation.
716        let mut total_edges = 0usize;
717        let mut result = AdjacencyList::with_capacity(max_degree as usize + 1);
718        for v in 0..num_vertices {
719            provider.get_neighbors(v, &mut result).unwrap();
720            total_edges += result.len();
721        }
722
723        let expected_total = (num_threads * vertices_per_thread) as usize;
724        assert_eq!(
725            total_edges, expected_total,
726            "Expected {} total edges but found {} — edges were lost or duplicated!",
727            expected_total, total_edges
728        );
729    }
730
731    /// Stress test: mixed concurrent reads and writes on the same vertex.
732    ///
733    /// Writers continuously append while readers verify the neighbor list is
734    /// always in a consistent state (length matches actual content, no torn reads).
735    ///
736    /// The read/write phase is repeated over many outer iterations to surface
737    /// torn-read races more reliably, the writer/reader counts scale with the
738    /// host's available parallelism (with a floor), and the `multi_thread`
739    /// runtime is left unsized so tokio scales its worker pool to the CPU count.
740    #[tokio::test(flavor = "multi_thread")]
741    async fn test_concurrent_read_write_consistency() {
742        let parallelism = stress_thread_count();
743        let num_writers = parallelism;
744        let num_readers = parallelism;
745        let edges_per_writer = 20u32;
746        // Size the degree to the workload so every distinct edge can be retained.
747        let max_degree = num_writers * edges_per_writer;
748        // Largest neighbor ID any writer can append (IDs run 1..=num_writers*edges).
749        let max_valid_id = num_writers * edges_per_writer;
750
751        let outer_iterations = 100;
752        for _ in 0..outer_iterations {
753            let locks = Arc::new(StripedLocks::new());
754            let provider = new_shared_provider(max_degree);
755
756            // Initialize vertex 0.
757            let mut scratch = provider.scratch(&locks);
758            scratch.write_neighbors(0, &[]).unwrap();
759
760            let done = Arc::new(std::sync::atomic::AtomicBool::new(false));
761            let writers_remaining = Arc::new(std::sync::atomic::AtomicU32::new(num_writers));
762
763            // Spawn writer threads that append edges.
764            let mut set = JoinSet::new();
765
766            for t in 0..num_writers {
767                let provider_clone = Arc::clone(&provider);
768                let done_clone = Arc::clone(&done);
769                let remaining_clone = Arc::clone(&writers_remaining);
770                let locks = locks.clone();
771                set.spawn(async move {
772                    let mut scratch = provider_clone.scratch(&locks);
773                    let base = t * edges_per_writer + 1;
774                    for offset in 0..edges_per_writer {
775                        scratch.write_append(0, &[base + offset]).unwrap();
776                        tokio::task::yield_now().await;
777                    }
778                    // Signal done only when ALL writers have finished.
779                    if remaining_clone.fetch_sub(1, std::sync::atomic::Ordering::AcqRel) == 1 {
780                        done_clone.store(true, std::sync::atomic::Ordering::Release);
781                    }
782                });
783            }
784
785            // Spawn reader threads that continuously read and validate consistency.
786            for _ in 0..num_readers {
787                let provider_clone = Arc::clone(&provider);
788                let done_clone = Arc::clone(&done);
789                set.spawn(async move {
790                    let mut result = AdjacencyList::with_capacity(max_degree as usize + 1);
791                    let mut iterations = 0u64;
792                    // Validate at least once before observing `done`. A reader that is
793                    // first scheduled only after every writer has already finished would
794                    // otherwise see `done == true` immediately, read nothing, and trip the
795                    // liveness assertion below — a scheduling artifact, not a real defect.
796                    // Reading the fully-written state is still a valid, consistent
797                    // observation, so the invariants hold regardless of timing.
798                    loop {
799                        provider_clone.get_neighbors(0, &mut result).unwrap();
800                        // The list must never exceed max_degree
801                        assert!(
802                            result.len() <= max_degree as usize,
803                            "Neighbor list exceeded max_degree"
804                        );
805                        // Every ID must be a real appended edge: non-zero and within
806                        // the range any writer could have produced. Anything else
807                        // signals a torn or garbage read.
808                        for &id in result.iter() {
809                            assert!(id > 0, "Found invalid zero ID in neighbor list");
810                            assert!(
811                                id <= max_valid_id,
812                                "Found out-of-bounds ID {id} (max valid is {max_valid_id})"
813                            );
814                        }
815                        iterations += 1;
816                        if done_clone.load(std::sync::atomic::Ordering::Acquire) {
817                            break;
818                        }
819                        tokio::task::yield_now().await;
820                    }
821                    assert!(
822                        iterations > 0,
823                        "Reader never got a chance to run — test is not validating anything"
824                    );
825                });
826            }
827
828            while let Some(res) = set.join_next().await {
829                res.unwrap();
830            }
831
832            // Final check: all edges from all writers present.
833            let mut result = AdjacencyList::with_capacity(max_degree as usize + 1);
834            provider.get_neighbors(0, &mut result).unwrap();
835            let expected = (num_writers * edges_per_writer) as usize;
836            assert_eq!(
837                result.len(),
838                expected,
839                "Expected {} edges but found {}",
840                expected,
841                result.len()
842            );
843        }
844    }
845}