Describe the bug
When performing concurrent add and remove operations, the Index::size() method can return an extremely large value (e.g., 18446744073709551614).
This occurs because the underlying C++ implementation calculates size as: return typed_->size() - free_keys_.size();.
Under high concurrency, it is possible for the free_keys_ size to be larger than the typed_ size, causing a std::size_t underflow.
Debugging reveals the following state during the crash:
typed_->size(): 88
free_keys_.size(): 91
- Result: 88 - 91 = -3 -> 18446744073709551613 (approx).
Steps to reproduce
This issue was reproduced using the Rust bindings with the tokio multi-threaded runtime. Note that even when using index.read() (assuming internal thread-safety of usearch), the inconsistency between the two internal counters is exposed.
#[tokio::test(flavor = "multi_thread")]
async fn test_concurrent_usearch_add_remove_size() {
let options = IndexOptions {
dimensions: 3,
..Default::default()
};
let index = Arc::new(RwLock::new(
usearch::Index::new(&options).expect("failed to create index"),
));
const VECTORS: usize = 50;
let threads = tokio::runtime::Handle::current().metrics().num_workers();
index.write().unwrap().reserve(VECTORS * 10).expect("failed to reserve");
let mut handles = vec![];
for _ in 0..threads {
let idx = Arc::clone(&index);
// Task 1: Concurrent Adds
handles.push(tokio::spawn(async move {
for i in 0..VECTORS {
let _ = idx.read().unwrap().add(i as u64, &[i as f32; 3]);
}
}));
// Task 2: Concurrent Removes
let idx = Arc::clone(&index);
handles.push(tokio::spawn(async move {
for i in 0..VECTORS {
let _ = idx.read().unwrap().remove(i as u64);
}
}));
// Task 3: Monitor Size
let idx = Arc::clone(&index);
handles.push(tokio::spawn(async move {
for _ in 0..VECTORS * 10 {
let size = idx.read().unwrap().size();
assert!(size <= VECTORS * threads, "Underflow detected! Size: {}", size);
tokio::task::yield_now().await;
}
}));
}
for handle in handles {
handle.await.expect("Task failed");
}
}
Expected behavior
Index::size() to always return valid number of vectors inside the index.
USearch version
2.23.0
Operating System
Fedora release 42 (Adams)
Hardware architecture
x86
Which interface are you using?
Other bindings
Contact Details
No response
Are you open to being tagged as a contributor?
Is there an existing issue for this?
Code of Conduct
Describe the bug
When performing concurrent add and remove operations, the Index::size() method can return an extremely large value (e.g.,
18446744073709551614).This occurs because the underlying C++ implementation calculates size as:
return typed_->size() - free_keys_.size();.Under high concurrency, it is possible for the
free_keys_size to be larger than thetyped_size, causing a std::size_t underflow.Debugging reveals the following state during the crash:
typed_->size(): 88free_keys_.size(): 91Steps to reproduce
This issue was reproduced using the Rust bindings with the tokio multi-threaded runtime. Note that even when using index.read() (assuming internal thread-safety of usearch), the inconsistency between the two internal counters is exposed.
Expected behavior
Index::size() to always return valid number of vectors inside the index.
USearch version
2.23.0
Operating System
Fedora release 42 (Adams)
Hardware architecture
x86
Which interface are you using?
Other bindings
Contact Details
No response
Are you open to being tagged as a contributor?
.githistory as a contributorIs there an existing issue for this?
Code of Conduct