Integrate new bitflags structure (#10123)

* Add `FlagsMode::from_feature_flags`, the mode for newly created flags

Compact in serverless-compatible deployments, dynamic otherwise. Only
creation consults it; opening existing flags detects their mode from
disk.

* Support the compact mode in the read-only flags types

Add `ReadOnlyFlags`, the mode-dispatching union of the two read-only
counterparts, serving the shared `RoaringFlagsRead` surface. Teach
`InMemoryBitvecFlags` to detect the mode it opens; its compact
`reload_appended` decodes the whole (small) file, as the format has no
random access.

* Create flags through mode selection in storages and indexes

Vector storage deleted flags and the bool/null indexes now open through
`open_or_create` with the mode from the feature flags: serverless
deployments create compact flags, dedicated ones keep creating dynamic
flags, and existing flags are opened in their detected mode either way.

* Read flags in either mode in the read-only bool and null indexes

`ReadOnlyFlags` shares the `RoaringFlagsRead` surface and the lifecycle
signatures of the roaring type it replaces, so the swap is a type
rename.

* Add TODO to not lock bitmask structure during flush

* `MutableStoredBitmask::save` returns the number of bytes written

Zero when the skip-clean save wrote nothing. Lets wrappers charge the
actual write to a hardware counter.

* Refuse to open compact flags in a dynamic-mode directory

Creating the compact file next to dynamic files would leave a directory
of both modes behind, which every later open rejects — refuse up front
instead. Both production callers already rule the case out through
`FlagsMode::detect`, so this only removes a foot-gun for future callers.

The open-or-eagerly-create logic moves into
`open_or_create_compact_mask`, shared with the update-only writer next.

* Rewrite `UpdateOnlyStoredFlags` onto the compact bitmask

The update-only flags writer now writes the compact mode — a single
roaring-encoded `compact_flags.dat` through `MutableStoredBitmask` —
instead of rewriting the whole padded dynamic file pair every batch. A
flush with no effective changes now writes nothing at all, where the old
writer rewrote the full mask on any `set`.

This also fixes opening serverless-created segments: the old open
eagerly wrote a `status.dat` into directories the writable side had
created in the compact mode, leaving files of both modes behind and
poisoning the directory for every later open.

A directory already holding dynamic-mode flags is refused loudly rather
than kept current or migrated; rebuild the segment to migrate its flags.
Migration may come later.

Drops the now-dead `InMemoryBitvecFlags::into_bitvec` and
`DynamicFlagsStatus::new`, and demotes `file_size_for` to private.

* Run edge tests with serverless feature flags

The edge fixtures ran with default feature flags, building leader shards
with dynamic-mode flags — a configuration edge never serves in
production, and one the update-only flags writer now refuses. It also
hid that the writer poisoned compact directories: no test exercised
update-only writes over a serverless-created shard.

Feature flags are process-global and first-init-wins, so every fixture
in the binary initializes the same serverless set; the manifest test
folds into it, since serverless implies `write_segment_manifest`.

* Don't use sequencial mode for one shot reads
This commit is contained in:
Tim Visée
2026-09-03 12:41:06 +02:00
committed by timvisee
parent 4a993d76d0
commit 34d52c4493
25 changed files with 728 additions and 213 deletions
@@ -150,10 +150,12 @@ impl MutableStoredBitmask {
/// the write entirely when nothing changed since the mask was opened or
/// last saved.
///
/// Returns the number of bytes written, zero when the write was skipped.
///
/// A failed save leaves the mask dirty, so a later retry writes again.
pub fn save(&mut self, fs: &impl UniversalWriteFileOps, path: &Path) -> UioResult<()> {
pub fn save(&mut self, fs: &impl UniversalWriteFileOps, path: &Path) -> UioResult<usize> {
if !self.is_dirty() {
return Ok(());
return Ok(0);
}
// Run-optimize in place so the encoder can serialize a borrow.
@@ -163,6 +165,6 @@ impl MutableStoredBitmask {
self.changed = false;
self.persisted_len = Some(self.logical_len);
Ok(())
Ok(bytes.len())
}
}
@@ -119,6 +119,7 @@ impl Populate {
#[derive(Copy, Clone, Debug)]
pub struct OpenOptions {
pub writeable: bool,
// Not needed for one shot reeds
pub need_sequential: bool,
/// Populate RAM cache on open, if applicable for this implementation.
pub populate: Populate,
+17 -9
View File
@@ -28,7 +28,20 @@ use crate::{CountRequest, EdgeConfig, EdgeShard, RetrieveRequestBuilder, ScrollR
pub(crate) const VECTOR_NAME: &str = "edge-ro-test-vector";
/// Initialize the process-global feature flags the way a serverless
/// deployment runs, which is what every edge shard serves in production.
///
/// Feature flags are first-init-wins for the whole process, so every fixture
/// in this binary initializes the same set to stay deterministic regardless
/// of test order. The field is private, set through the same route a config
/// file takes.
pub(crate) fn init_serverless_feature_flags() {
let flags: FeatureFlags = serde_json::from_str(r#"{ "serverless_compatible": true }"#).unwrap();
init_feature_flags(flags);
}
pub(crate) fn test_config() -> EdgeConfig {
init_serverless_feature_flags();
EdgeConfig {
on_disk_payload: Some(false),
vectors: HashMap::from([(
@@ -493,15 +506,10 @@ fn manifest_enumerator_requires_manifest() {
/// its segments, and a follower opened over the same directory loads through it.
#[test]
fn leader_writes_manifest_and_follower_loads_it() {
let mut flags = FeatureFlags::default();
flags.write_segment_manifest = true;
init_feature_flags(flags);
// Another test in this process may have initialized flags first; the assertions below only hold
// with the flag enabled.
if !common::flags::feature_flags().write_segment_manifest {
return;
}
// Serverless mode implies `write_segment_manifest`, which the assertions
// below rely on.
init_serverless_feature_flags();
assert!(common::flags::feature_flags().write_segment_manifest);
let dir = tempfile::Builder::new()
.prefix("edge-ro-manifest-write")
@@ -6,17 +6,57 @@ use common::mmap::AdviceSetting;
use common::stored_bitmask::MutableStoredBitmask;
use common::types::PointOffsetType;
use common::universal_io::{
OkNotFound, OpenOptions, Populate, UniversalRead, UniversalWrite, UniversalWriteFileOps,
OkNotFound, OpenOptions, Populate, UniversalRead, UniversalReadFs, UniversalWrite,
UniversalWriteFileOps,
};
use parking_lot::Mutex;
use roaring::RoaringBitmap;
use super::mode::FlagsMode;
use crate::common::Flusher;
use crate::common::operation_error::{OperationError, OperationResult};
/// Name of the single file holding the mask, inside the storage directory.
pub(super) const COMPACT_FLAGS_FILE: &str = "compact_flags.dat";
/// Open the compact-mode mask in `directory`, creating the directory and a
/// persisted empty mask when none exists yet.
///
/// The eager persist is deliberate: flags directories double as the marker
/// that their storage exists, and an empty directory would leave the next
/// open to pick a mode from feature flags rather than from disk.
///
/// The caller must have ruled out dynamic-mode flags in the directory first —
/// see [`FlagsMode::detect`]. Creating the compact file next to dynamic files
/// would leave a directory of both modes behind, which every later open
/// rejects.
pub(super) fn open_or_create_compact_mask<Fs>(
fs: &Fs,
directory: &Path,
populate: Populate,
) -> OperationResult<MutableStoredBitmask>
where
Fs: UniversalReadFs + UniversalWriteFileOps,
{
fs.create_dir(directory)?;
let path = directory.join(COMPACT_FLAGS_FILE);
let options = OpenOptions {
writeable: false,
need_sequential: true,
populate,
advice: AdviceSetting::Global,
};
match MutableStoredBitmask::open(fs, &path, options, Default::default()).ok_not_found()? {
Some(mask) => Ok(mask),
None => {
let mut mask = MutableStoredBitmask::new(0);
mask.save(fs, &path)?;
Ok(mask)
}
}
}
/// Flags over a single compact stored-bitmask file, rewritten whole on flush.
///
/// The serverless-compatible counterpart of `DynamicStoredFlags` +
@@ -47,32 +87,22 @@ where
/// Open the flags in `directory`, materializing the whole mask into RAM.
///
/// Creates the directory and persists an empty mask when the file is
/// missing, so [`Self::files`] always exist on disk.
/// missing, so [`Self::files`] always exist on disk. Errors when the
/// directory holds dynamic-mode flags instead.
pub fn open(fs: S::Fs, directory: &Path, populate: Populate) -> OperationResult<Self> {
fs.create_dir(directory)?;
let path = directory.join(COMPACT_FLAGS_FILE);
if FlagsMode::detect(&fs, directory)? == Some(FlagsMode::Dynamic) {
return Err(OperationError::service_error(format!(
"cannot open compact flags in {}: the directory holds flags of the dynamic mode",
directory.display(),
)));
}
let options = OpenOptions {
writeable: false,
need_sequential: true,
populate,
advice: AdviceSetting::Global,
};
let mask = match MutableStoredBitmask::open(&fs, &path, options, Default::default())
.ok_not_found()?
{
Some(mask) => mask,
None => {
let mut mask = MutableStoredBitmask::new(0);
mask.save(&fs, &path)?;
mask
}
};
let mask = open_or_create_compact_mask(&fs, directory, populate)?;
Ok(Self {
mask: Arc::new(Mutex::new(mask)),
fs: Arc::new(fs),
path,
path: directory.join(COMPACT_FLAGS_FILE),
is_alive_flush_lock: IsAliveLock::new(),
})
}
@@ -149,6 +179,8 @@ where
));
};
// TODO(serverless): we should not lock here during save
// TODO(serverless): currently acceptable because readers and writers are completely separate
mask_arc.lock().save(&*fs, &path)?;
drop(is_alive_flush_guard);
@@ -187,12 +219,21 @@ mod tests_mod {
use tempfile::TempDir;
use crate::common::flags::compact_stored_flags::CompactStoredFlags;
use crate::common::flags::dynamic_stored_flags::DynamicStoredFlags;
use crate::common::operation_error::OperationError;
fn open(dir: &std::path::Path) -> CompactStoredFlags<S> {
CompactStoredFlags::open(Fs::default(), dir, Populate::Blocking).unwrap()
}
#[test]
fn open_refuses_dynamic_directory() {
let dir = TempDir::new().unwrap();
DynamicStoredFlags::<S>::open(&Fs::default(), dir.path(), Populate::No).unwrap();
let result = CompactStoredFlags::<S>::open(Fs::default(), dir.path(), Populate::No);
assert!(result.is_err(), "expected refusal on a dynamic directory");
}
#[test]
fn open_creates_file_and_lists_it() {
let dir = TempDir::new().unwrap();
@@ -41,14 +41,6 @@ pub struct DynamicFlagsStatus {
}
impl DynamicFlagsStatus {
/// The status of a storage holding `len` flags.
pub(super) fn new(len: usize) -> Self {
Self {
len,
current_file_id: 0,
}
}
/// Number of logical flags (bits) stored.
pub fn len(&self) -> usize {
self.len
@@ -92,7 +84,7 @@ impl<S: fmt::Debug> fmt::Debug for DynamicStoredFlags<S> {
}
/// Based on the number of flags determines the size in bytes for the storage file.
pub(super) fn file_size_for(num_flags: usize) -> usize {
fn file_size_for(num_flags: usize) -> usize {
let number_of_bytes = num_flags.div_ceil(u8::BITS as usize);
max(MINIMAL_MMAP_SIZE, number_of_bytes.next_power_of_two())
@@ -3,13 +3,16 @@ use std::path::{Path, PathBuf};
use common::bitvec::{BitSlice, BitVec};
use common::mmap::AdviceSetting;
use common::sorted_slice::SortedSlice;
use common::stored_bitmask::StoredBitmask;
use common::stored_bitslice::StoredBitSlice;
use common::types::PointOffsetType;
use common::universal_io::{
CachedReadFs, OpenOptions, Populate, TypedStorage, UniversalRead, UniversalReadFs,
};
use super::compact_stored_flags::COMPACT_FLAGS_FILE;
use super::dynamic_stored_flags::{DynamicFlagsStatus, FLAGS_FILE, status_file};
use super::mode::FlagsMode;
use crate::common::operation_error::{OperationError, OperationResult};
/// In-memory counterpart of `BitvecFlags`: persisted flags materialized into an
@@ -30,9 +33,10 @@ pub struct InMemoryBitvecFlags {
bitvec: BitVec,
/// Set-flag count, kept in sync with `bitvec`.
count: usize,
/// Backing directory of the dynamic flags file, so [`Self::reload_appended`]
/// can reopen it. `None` for flags built via [`Self::from_bitvec`].
directory: Option<PathBuf>,
/// Backing directory and [mode](FlagsMode) of the persisted flags, so
/// [`Self::reload_appended`] can reopen them. `None` for flags built via
/// [`Self::from_bitvec`].
backing: Option<(PathBuf, FlagsMode)>,
}
/// Read-only mmap options: never writable, lazily paged, nothing populated.
@@ -45,22 +49,45 @@ fn bitslice_open_options(populate: Populate) -> OpenOptions {
}
}
impl InMemoryBitvecFlags {
/// Schedule background prefetch of the two files [`Self::open`] reads.
pub fn preopen(fs: &impl CachedReadFs, directory: &Path) -> OperationResult<()> {
// Status file
fs.schedule_prefetch(
&status_file(directory),
Some(bitslice_open_options(Populate::PreferBackground)),
None,
)?;
/// Read-only options for the compact bitmask, which is decoded whole.
fn compact_open_options(populate: Populate) -> OpenOptions {
OpenOptions {
writeable: false,
// Not needed for one shot reeds
need_sequential: false,
populate,
advice: AdviceSetting::Global,
}
}
// Bitslice
fs.schedule_prefetch(
&directory.join(FLAGS_FILE),
Some(bitslice_open_options(Populate::PreferBackground)),
None,
)?;
impl InMemoryBitvecFlags {
/// Schedule background prefetch of the files [`Self::open`] reads, in the
/// detected [mode](FlagsMode).
pub fn preopen(fs: &impl CachedReadFs, directory: &Path) -> OperationResult<()> {
match FlagsMode::detect(fs, directory)?.unwrap_or(FlagsMode::Dynamic) {
FlagsMode::Dynamic => {
// Status file
fs.schedule_prefetch(
&status_file(directory),
Some(bitslice_open_options(Populate::PreferBackground)),
None,
)?;
// Bitslice
fs.schedule_prefetch(
&directory.join(FLAGS_FILE),
Some(bitslice_open_options(Populate::PreferBackground)),
None,
)?;
}
FlagsMode::Compact => {
fs.schedule_prefetch(
&directory.join(COMPACT_FLAGS_FILE),
Some(compact_open_options(Populate::PreferBackground)),
None,
)?;
}
}
Ok(())
}
@@ -82,12 +109,24 @@ impl InMemoryBitvecFlags {
.map_or(0, DynamicFlagsStatus::len))
}
/// Open persisted flags read-only into an owned `BitVec`; creates and writes
/// nothing. The flags file is padded past the logical length (held in the
/// status file), so the bitvec is truncated to it and `count` is exact.
/// Open persisted flags read-only into an owned `BitVec`, in their
/// detected [mode](FlagsMode); creates and writes nothing.
pub fn open<S: UniversalRead>(
fs: &impl UniversalReadFs<File = S>,
directory: &Path,
) -> OperationResult<Self> {
match FlagsMode::detect(fs, directory)?.unwrap_or(FlagsMode::Dynamic) {
FlagsMode::Dynamic => Self::open_dynamic(fs, directory),
FlagsMode::Compact => Self::open_compact(fs, directory),
}
}
/// [`Self::open`] for the dynamic mode. The flags file is padded past the
/// logical length (held in the status file), so the bitvec is truncated to
/// it and `count` is exact.
fn open_dynamic<S: UniversalRead>(
fs: &impl UniversalReadFs<File = S>,
directory: &Path,
) -> OperationResult<Self> {
let len = Self::persisted_len(fs, directory)?;
@@ -110,7 +149,33 @@ impl InMemoryBitvecFlags {
Ok(Self {
bitvec,
count,
directory: Some(directory.to_path_buf()),
backing: Some((directory.to_path_buf(), FlagsMode::Dynamic)),
})
}
/// [`Self::open`] for the compact mode: the single bitmask file is decoded
/// whole into the bitvec.
fn open_compact<S: UniversalRead>(
fs: &impl UniversalReadFs<File = S>,
directory: &Path,
) -> OperationResult<Self> {
let mask = StoredBitmask::<S>::open(
fs,
directory.join(COMPACT_FLAGS_FILE),
compact_open_options(Populate::No),
Default::default(),
)?;
let ones = mask.read_ones()?;
let mut bitvec = BitVec::repeat(false, mask.bit_len() as usize);
for index in &ones {
bitvec.set(index as usize, true);
}
Ok(Self {
bitvec,
count: ones.len() as usize,
backing: Some((directory.to_path_buf(), FlagsMode::Compact)),
})
}
@@ -122,7 +187,7 @@ impl InMemoryBitvecFlags {
Self {
bitvec,
count,
directory: None,
backing: None,
}
}
@@ -141,11 +206,6 @@ impl InMemoryBitvecFlags {
self.bitvec.as_bitslice()
}
/// Set flags as an owned `BitVec`, dropping the count.
pub fn into_bitvec(self) -> BitVec {
self.bitvec
}
/// Set `points`, growing as needed and keeping `count` in sync. Folds a
/// live-reload deletion delta; live offsets aren't passed (they read unset).
pub fn insert_all(&mut self, points: &[PointOffsetType]) {
@@ -171,14 +231,29 @@ impl InMemoryBitvecFlags {
fs: &impl UniversalReadFs<File = S>,
new_points: &SortedSlice<'_, PointOffsetType>,
) -> OperationResult<()> {
let Some(directory) = self.directory.clone() else {
let Some((directory, mode)) = self.backing.clone() else {
return Ok(());
};
match mode {
FlagsMode::Dynamic => self.reload_appended_dynamic(fs, &directory, new_points),
FlagsMode::Compact => self.reload_appended_compact(fs, &directory, new_points),
}
}
/// [`Self::reload_appended`] for the dynamic mode. `new_points` is sorted,
/// so the covering range up to the persisted length is read in one batched
/// read.
fn reload_appended_dynamic<S: UniversalRead>(
&mut self,
fs: &impl UniversalReadFs<File = S>,
directory: &Path,
new_points: &SortedSlice<'_, PointOffsetType>,
) -> OperationResult<()> {
let (Some(&first), Some(&last)) = (new_points.first(), new_points.last()) else {
return Ok(());
};
let len = Self::persisted_len(fs, &directory)? as u64;
let len = Self::persisted_len(fs, directory)? as u64;
let start = u64::from(first);
let end = u64::from(last).saturating_add(1).min(len);
if start >= end {
@@ -204,6 +279,37 @@ impl InMemoryBitvecFlags {
Ok(())
}
/// [`Self::reload_appended`] for the compact mode. The bitmask has no
/// random access, so the (small) file is decoded whole and the appended
/// offsets' bits are folded in from it.
fn reload_appended_compact<S: UniversalRead>(
&mut self,
fs: &impl UniversalReadFs<File = S>,
directory: &Path,
new_points: &SortedSlice<'_, PointOffsetType>,
) -> OperationResult<()> {
if new_points.is_empty() {
return Ok(());
}
let mask = StoredBitmask::<S>::open(
fs,
directory.join(COMPACT_FLAGS_FILE),
compact_open_options(Populate::No),
Default::default(),
)?;
let ones = mask.read_ones()?;
let deleted: Vec<_> = new_points
.iter()
.copied()
.filter(|&point| ones.contains(point))
.collect();
self.insert_all(&deleted);
Ok(())
}
}
#[allow(clippy::default_constructed_unit_structs)]
@@ -224,6 +330,7 @@ mod tests_mod {
use tempfile::Builder;
use super::*;
use crate::common::flags::compact_stored_flags::CompactStoredFlags;
use crate::common::flags::dynamic_stored_flags::DynamicStoredFlags;
/// Persist `flags` via the writable storage so the read-only path can open it.
@@ -284,4 +391,37 @@ mod tests_mod {
assert!(flags.get(beyond));
assert!(!flags.get(beyond + 1));
}
#[test]
fn open_compact_mode_and_reload_appended() {
let dir = Builder::new().prefix("storage_dir").tempdir().unwrap();
// Persist compact-mode flags via the writable wrapper.
let writer =
CompactStoredFlags::<S>::open(Fs::default(), dir.path(), Populate::No).unwrap();
writer.set(3, true);
writer.set(9, false); // grows the flags to 10
writer.flusher()().unwrap();
let mut flags = InMemoryBitvecFlags::open::<S>(&Fs::default(), dir.path()).unwrap();
assert_eq!(flags.count(), 1);
assert!(flags.get(3));
assert!(!flags.get(9));
assert_eq!(flags.as_bitslice().len(), 10);
// Append points 10..=12; one carries a deletion recorded only in the
// persisted flags.
writer.set(11, true);
writer.set(12, false);
writer.flusher()().unwrap();
let new_points = [10, 11, 12];
flags
.reload_appended::<S>(&Fs::default(), &SortedSlice::new(&new_points).unwrap())
.unwrap();
assert!(flags.get(11));
assert!(!flags.get(10));
assert!(!flags.get(12));
assert_eq!(flags.count(), 2);
}
}
+5 -1
View File
@@ -11,10 +11,13 @@
//! rewritten whole on flush; serverless-compatible counterpart of the dynamic + buffered stack.
//! - `read_only_compact_flags`: read-only counterpart of `compact_stored_flags`, bound to
//! `UniversalRead`.
//! - `read_only_flags`: mode-dispatching union of `read_only_roaring_flags` and
//! `read_only_compact_flags`.
//!
//! `bitvec_flags` and `roaring_flags` persist either through the dynamic stack or through
//! `compact_stored_flags`, selected by [`FlagsMode`] when flags are created and detected
//! automatically when opening existing flags.
//! automatically when opening existing flags. The read-only types (`read_only_flags`,
//! `in_memory_bitvec_flags`) likewise detect the mode of the flags they open.
pub mod bitvec_flags;
mod buffered_dynamic_flags;
@@ -23,6 +26,7 @@ pub mod dynamic_stored_flags;
pub mod in_memory_bitvec_flags;
mod mode;
pub mod read_only_compact_flags;
pub mod read_only_flags;
pub mod read_only_roaring_flags;
pub mod roaring_flags;
mod storage;
+18 -1
View File
@@ -1,5 +1,6 @@
use std::path::Path;
use common::flags::feature_flags;
use common::universal_io::UniversalReadFileOps;
use super::compact_stored_flags::COMPACT_FLAGS_FILE;
@@ -26,6 +27,19 @@ pub enum FlagsMode {
}
impl FlagsMode {
/// Mode for newly created flags, per the deployment's feature flags:
/// compact in serverless-compatible deployments, dynamic otherwise.
///
/// Only relevant when creating flags; opening existing flags detects
/// their mode from disk regardless of this value.
pub fn from_feature_flags() -> Self {
if feature_flags().serverless_compatible() {
Self::Compact
} else {
Self::Dynamic
}
}
/// Detect the mode of the flags in `directory` from the files present,
/// `None` when no flags exist there yet.
///
@@ -97,9 +111,12 @@ mod tests_mod {
#[test]
fn errors_on_files_of_both_modes() {
// Compact first: opening compact flags in a dynamic directory is
// refused outright, so this order is the only way to end up with
// files of both modes.
let dir = TempDir::new().unwrap();
DynamicStoredFlags::<S>::open(&Fs::default(), dir.path(), Populate::No).unwrap();
CompactStoredFlags::<S>::open(Fs::default(), dir.path(), Populate::No).unwrap();
DynamicStoredFlags::<S>::open(&Fs::default(), dir.path(), Populate::No).unwrap();
assert!(FlagsMode::detect(&Fs::default(), dir.path()).is_err());
}
}
@@ -0,0 +1,156 @@
use std::path::{Path, PathBuf};
use common::universal_io::{CachedReadFs, Populate, UniversalRead, UniversalReadFs};
use roaring::RoaringBitmap;
use super::mode::FlagsMode;
use super::read_only_compact_flags::ReadOnlyCompactFlags;
use super::read_only_roaring_flags::ReadOnlyRoaringFlags;
use super::roaring_flags::RoaringFlagsRead;
use crate::common::operation_error::OperationResult;
/// Read-only flags in either [storage mode](FlagsMode), serving the
/// [`RoaringFlagsRead`] surface.
///
/// Read-side counterpart of the mode dispatch inside the writable wrappers:
/// [`Self::open`] detects the mode from the files present and every operation
/// forwards to that variant.
pub enum ReadOnlyFlags<S: UniversalRead> {
/// Flags in the dynamic (mmapped, mutated in place) format.
Dynamic(ReadOnlyRoaringFlags<S>),
/// Flags in the compact stored-bitmask format.
Compact(ReadOnlyCompactFlags<S>),
}
impl<S: UniversalRead> ReadOnlyFlags<S> {
/// Schedule background prefetch of the files [`Self::open`] reads, in the
/// detected mode.
///
/// Returns whether the flags exist.
pub fn preopen(
fs: &impl CachedReadFs<File = S>,
directory: &Path,
populate: Populate,
) -> OperationResult<bool> {
match FlagsMode::detect(fs, directory)? {
None => Ok(false),
Some(FlagsMode::Dynamic) => ReadOnlyRoaringFlags::<S>::preopen(fs, directory, populate),
Some(FlagsMode::Compact) => ReadOnlyCompactFlags::<S>::preopen(fs, directory, populate),
}
}
/// Open persisted flags read-only, in their detected mode.
///
/// Returns [`Ok(None)`] when no flags exist in `directory`, matching the
/// read path's never-create contract.
pub fn open(
fs: &impl UniversalReadFs<File = S>,
directory: &Path,
) -> OperationResult<Option<Self>> {
match FlagsMode::detect(fs, directory)? {
None => Ok(None),
Some(FlagsMode::Dynamic) => {
Ok(ReadOnlyRoaringFlags::open(fs, directory)?.map(Self::Dynamic))
}
Some(FlagsMode::Compact) => {
Ok(ReadOnlyCompactFlags::open(fs, directory)?.map(Self::Compact))
}
}
}
/// Refresh to the current on-disk state; see the variants' impls for the
/// respective reload semantics.
pub fn live_reload(&mut self, fs: &impl UniversalReadFs<File = S>) -> OperationResult<()> {
match self {
Self::Dynamic(flags) => flags.live_reload(fs),
Self::Compact(flags) => flags.live_reload(fs),
}
}
}
impl<S: UniversalRead> RoaringFlagsRead for ReadOnlyFlags<S> {
fn len(&self) -> usize {
match self {
Self::Dynamic(flags) => flags.len(),
Self::Compact(flags) => flags.len(),
}
}
fn get_bitmap(&self) -> OperationResult<&RoaringBitmap> {
match self {
Self::Dynamic(flags) => flags.get_bitmap(),
Self::Compact(flags) => flags.get_bitmap(),
}
}
fn bitmap_if_materialized(&self) -> Option<&RoaringBitmap> {
match self {
Self::Dynamic(flags) => flags.bitmap_if_materialized(),
Self::Compact(flags) => flags.bitmap_if_materialized(),
}
}
fn files(&self) -> Vec<PathBuf> {
match self {
Self::Dynamic(flags) => flags.files(),
Self::Compact(flags) => flags.files(),
}
}
}
#[cfg(test)]
mod tests {
use common::universal_io::{MmapFile, MmapFs};
use tempfile::TempDir;
use super::*;
use crate::common::flags::compact_stored_flags::CompactStoredFlags;
use crate::common::flags::dynamic_stored_flags::DynamicStoredFlags;
#[test]
fn open_dispatches_on_detected_mode() {
let tmp = TempDir::new().unwrap();
// Nothing on disk: no flags.
let missing = tmp.path().join("missing");
assert!(
ReadOnlyFlags::<MmapFile>::open(&MmapFs, &missing)
.unwrap()
.is_none()
);
// Dynamic-mode flags.
let dynamic_dir = tmp.path().join("dynamic");
{
let mut writer =
DynamicStoredFlags::<MmapFile>::open(&MmapFs, &dynamic_dir, Populate::No).unwrap();
writer.set_len(&MmapFs, 10).unwrap();
writer.set(3, true).unwrap();
writer.flusher()().unwrap();
}
let flags = ReadOnlyFlags::<MmapFile>::open(&MmapFs, &dynamic_dir)
.unwrap()
.unwrap();
assert!(matches!(flags, ReadOnlyFlags::Dynamic(_)));
assert_eq!(flags.len(), 10);
assert!(flags.get(3).unwrap());
assert_eq!(flags.count_trues().unwrap(), 1);
// Compact-mode flags.
let compact_dir = tmp.path().join("compact");
{
let writer =
CompactStoredFlags::<MmapFile>::open(MmapFs, &compact_dir, Populate::No).unwrap();
writer.set(7, true);
writer.flusher()().unwrap();
}
let flags = ReadOnlyFlags::<MmapFile>::open(&MmapFs, &compact_dir)
.unwrap()
.unwrap();
assert!(matches!(flags, ReadOnlyFlags::Compact(_)));
assert_eq!(flags.len(), 8);
assert!(flags.get(7).unwrap());
assert_eq!(flags.files().len(), 1);
}
}
@@ -1,61 +1,58 @@
use std::path::{Path, PathBuf};
use common::bitvec::BitVec;
use common::counter::hardware_counter::HardwareCounterCell;
use common::stored_bitmask::MutableStoredBitmask;
use common::types::PointOffsetType;
use common::universal_io::{
OkNotFound as _, UniversalAppend, UniversalReadFileOps as _, UniversalWriteFileOps as _,
};
use common::universal_io::{Populate, UniversalAppend};
use super::dynamic_stored_flags::{DynamicFlagsStatus, FLAGS_FILE, file_size_for, status_file};
use super::in_memory_bitvec_flags::InMemoryBitvecFlags;
use crate::common::operation_error::OperationResult;
use super::compact_stored_flags::{COMPACT_FLAGS_FILE, open_or_create_compact_mask};
use super::mode::FlagsMode;
use crate::common::operation_error::{OperationError, OperationResult};
/// Short-lived writer for the persisted flags of an update-only segment,
/// opened for one batch and dropped with it.
///
/// Writes the same two files [`DynamicStoredFlags`][1] does, but rewrites the
/// mask whole: a bitmask over all points cannot be kept current by appending.
/// The cost is one bit per point of the segment per batch, which is why only
/// the bitmask-backed indexes use this.
/// The update-only counterpart of [`CompactStoredFlags`][1]: the mask is
/// fully resident in RAM, mutations collect there, and a flush rewrites the
/// single compact file in one atomic whole-file write — a bitmask over all
/// points cannot be kept current by appending. A flush with no effective
/// changes writes nothing.
///
/// [1]: super::dynamic_stored_flags::DynamicStoredFlags
/// Only the compact storage mode is supported. Opening a directory that
/// holds dynamic-mode flags fails loudly rather than going stale or leaving
/// files of both modes behind; rebuild such a segment to migrate its flags.
///
/// [1]: super::compact_stored_flags::CompactStoredFlags
pub struct UpdateOnlyStoredFlags<S: UniversalAppend + 'static> {
fs: S::Fs,
directory: PathBuf,
/// Every flag the storage holds, materialized on open. Its length is the
/// logical flag count, which the status file publishes.
flags: BitVec,
dirty: bool,
/// Path of the mask file inside the flags directory.
path: PathBuf,
/// The mask, resident in RAM; tracks its own effective dirtiness.
mask: MutableStoredBitmask,
}
impl<S: UniversalAppend + 'static> UpdateOnlyStoredFlags<S> {
/// Read the flags at `directory` into memory, ready to be extended. A
/// directory that holds none yet opens empty, and is created — status file
/// included — right away.
/// directory that holds none yet opens empty, and is created — mask file
/// included — right away, because storages take the directory as the
/// marker that they exist at all.
///
/// Errors when the directory holds flags of the dynamic mode.
pub fn open(fs: S::Fs, directory: &Path) -> OperationResult<Self> {
// Materialize the directory on the first open rather than on the first
// flag. Storages use it as the marker that they exist at all — the
// sparse one takes its absence for "not created yet" and starts over —
// so a batch that flags nothing must still leave it behind.
if !fs.exists(&status_file(directory))? {
fs.create_dir(directory)?;
fs.atomic_save(
&status_file(directory),
bytemuck::bytes_of(&DynamicFlagsStatus::new(0)),
)?;
if FlagsMode::detect(&fs, directory)? == Some(FlagsMode::Dynamic) {
return Err(OperationError::service_error(format!(
"flags in {} are in the dynamic mode, which the update-only writer does not \
support; rebuild the segment to migrate its flags",
directory.display(),
)));
}
let flags = InMemoryBitvecFlags::open::<S>(&fs, directory)
.ok_not_found()?
.map(InMemoryBitvecFlags::into_bitvec)
.unwrap_or_default();
let mask = open_or_create_compact_mask(&fs, directory, Populate::Blocking)?;
Ok(Self {
fs,
directory: directory.to_owned(),
flags,
dirty: false,
path: directory.join(COMPACT_FLAGS_FILE),
mask,
})
}
@@ -64,40 +61,180 @@ impl<S: UniversalAppend + 'static> UpdateOnlyStoredFlags<S> {
/// Extends for a `false` as much as for a `true`: the mask's length is what
/// says which points it has an answer for.
pub fn set(&mut self, slot: PointOffsetType, value: bool) {
let slot = slot as usize;
if slot >= self.flags.len() {
self.flags.resize(slot + 1, false);
if u64::from(slot) >= self.mask.bit_len() {
self.mask.set_len(u64::from(slot) + 1);
}
self.flags.set(slot, value);
self.dirty = true;
self.mask.set(slot, value);
}
/// Write both files, the mask before the length that publishes it, so that
/// a torn batch never leaves a length pointing past the written flags.
/// Persist the mask in one atomic whole-file write, or write nothing when
/// it has not effectively changed since it was opened or last flushed.
pub fn flush(&mut self, hw_counter: &HardwareCounterCell) -> OperationResult<()> {
if !self.dirty {
return Ok(());
}
self.fs.create_dir(&self.directory)?;
// Padded to the size the writable side would have allocated, so that a
// writer of either kind can go on from here without resizing.
let len = self.flags.len();
let mut bytes = bytemuck::cast_slice(self.flags.as_raw_slice()).to_vec();
bytes.resize(file_size_for(len), 0);
self.fs
.atomic_save(&self.directory.join(FLAGS_FILE), &bytes)?;
let status = DynamicFlagsStatus::new(len);
self.fs
.atomic_save(&status_file(&self.directory), bytemuck::bytes_of(&status))?;
let bytes_written = self.mask.save(&self.fs, &self.path)?;
hw_counter
.payload_index_io_write_counter()
.incr_delta(bytes.len() + size_of::<DynamicFlagsStatus>());
self.dirty = false;
.incr_delta(bytes_written);
Ok(())
}
}
#[allow(clippy::default_constructed_unit_structs)]
#[duplicate::duplicate_item(
tests_mod S Fs cfg_predicate;
[tests_mmap] [MmapFile] [MmapFs] [cfg(all())];
[tests_uring] [IoUringFile] [IoUringFs] [cfg(target_os = "linux")];
)]
#[cfg_predicate]
#[cfg(test)]
mod tests_mod {
use common::counter::hardware_counter::HardwareCounterCell;
use common::universal_io::Populate;
#[cfg_predicate]
use common::universal_io::{Fs, S};
use tempfile::TempDir;
use crate::common::flags::FlagsMode;
use crate::common::flags::compact_stored_flags::{COMPACT_FLAGS_FILE, CompactStoredFlags};
use crate::common::flags::dynamic_stored_flags::DynamicStoredFlags;
use crate::common::flags::update_only_stored_flags::UpdateOnlyStoredFlags;
fn open(dir: &std::path::Path) -> UpdateOnlyStoredFlags<S> {
UpdateOnlyStoredFlags::open(Fs::default(), dir).unwrap()
}
fn flush(flags: &mut UpdateOnlyStoredFlags<S>) {
flags.flush(&HardwareCounterCell::new()).unwrap();
}
/// Reader for what the writer left behind, through the type the writable
/// segment side uses on the same directory.
fn read_back(dir: &std::path::Path) -> CompactStoredFlags<S> {
CompactStoredFlags::open(Fs::default(), dir, Populate::Blocking).unwrap()
}
#[test]
fn open_creates_mask_file_eagerly() {
// The flags directory is the marker that its storage exists, so a
// batch that flags nothing must still leave it behind, mask included.
let dir = TempDir::new().unwrap();
let _flags = open(dir.path());
assert!(dir.path().join(COMPACT_FLAGS_FILE).exists());
assert_eq!(
FlagsMode::detect(&Fs::default(), dir.path()).unwrap(),
Some(FlagsMode::Compact),
);
}
#[test]
fn set_flush_reopen_roundtrip() {
let dir = TempDir::new().unwrap();
{
let mut flags = open(dir.path());
flags.set(3, true);
flags.set(100, false); // grows to 101 without setting anything
flush(&mut flags);
}
let reader = read_back(dir.path());
assert_eq!(reader.len(), 101);
assert_eq!(reader.count_flags(), 1);
assert!(reader.get(3));
assert!(!reader.get(100));
}
#[test]
fn clean_flush_writes_nothing() {
let dir = TempDir::new().unwrap();
let mut flags = open(dir.path());
flags.set(1, true);
flush(&mut flags);
// A clean flush must not touch storage: with the file deleted behind
// its back, only an actual write could bring it back.
fs_err::remove_file(dir.path().join(COMPACT_FLAGS_FILE)).unwrap();
flush(&mut flags);
assert!(!dir.path().join(COMPACT_FLAGS_FILE).exists());
// Re-setting a flag to its value is not an effective change either.
flags.set(1, true);
flush(&mut flags);
assert!(!dir.path().join(COMPACT_FLAGS_FILE).exists());
// The next effective change rewrites the whole mask.
flags.set(0, true);
flush(&mut flags);
let reader = read_back(dir.path());
assert!(reader.get(0));
assert!(reader.get(1));
}
#[test]
fn reopen_extends_previous_batch() {
let dir = TempDir::new().unwrap();
{
let mut flags = open(dir.path());
flags.set(0, true);
flags.set(5, true);
flush(&mut flags);
}
{
let mut flags = open(dir.path());
flags.set(5, false);
flags.set(9, true);
flush(&mut flags);
}
let reader = read_back(dir.path());
assert_eq!(reader.len(), 10);
assert!(reader.get(0));
assert!(!reader.get(5));
assert!(reader.get(9));
}
#[test]
fn extends_flags_created_by_the_writable_side() {
// A serverless-created segment arrives with compact flags written by
// the writable side; the update-only writer must pick them up and
// leave the directory in the compact mode.
let dir = TempDir::new().unwrap();
{
let writable: CompactStoredFlags<S> =
CompactStoredFlags::open(Fs::default(), dir.path(), Populate::Blocking).unwrap();
writable.set(2, true);
writable.flusher()().unwrap();
}
{
let mut flags = open(dir.path());
flags.set(4, true);
flush(&mut flags);
}
assert_eq!(
FlagsMode::detect(&Fs::default(), dir.path()).unwrap(),
Some(FlagsMode::Compact),
);
let reader = read_back(dir.path());
assert_eq!(reader.len(), 5);
assert!(reader.get(2));
assert!(reader.get(4));
}
#[test]
fn refuses_dynamic_directory() {
let dir = TempDir::new().unwrap();
DynamicStoredFlags::<S>::open(&Fs::default(), dir.path(), Populate::No).unwrap();
let result = UpdateOnlyStoredFlags::<S>::open(Fs::default(), dir.path());
let message = result
.err()
.expect("dynamic flags must be refused")
.to_string();
assert!(
message.contains("dynamic"),
"refusal should name the dynamic mode: {message}",
);
// The refused directory is left untouched: still cleanly dynamic.
assert_eq!(
FlagsMode::detect(&Fs::default(), dir.path()).unwrap(),
Some(FlagsMode::Dynamic),
);
}
}
@@ -8,7 +8,7 @@ use fs_err as fs;
use super::super::read_ops::BoolIndexRead;
use super::{FALSES_DIRNAME, MutableBoolIndex, Storage, TRUES_DIRNAME};
use crate::common::flags::dynamic_stored_flags::DynamicStoredFlags;
use crate::common::flags::FlagsMode;
use crate::common::flags::roaring_flags::{RoaringFlags, RoaringFlagsRead};
use crate::common::operation_error::{OperationError, OperationResult};
use crate::index::field_index::{FieldIndexBuilderTrait, PayloadFieldIndex, ValueIndexer};
@@ -46,15 +46,21 @@ impl MutableBoolIndex {
))
})?;
// Trues bitslice
let trues_path = path.join(TRUES_DIRNAME);
let trues_slice = DynamicStoredFlags::open(&MmapFs, &trues_path, Populate::No)?;
let trues_flags = RoaringFlags::new(MmapFs, trues_slice)?;
// Trues flags
let trues_flags = RoaringFlags::open_or_create(
MmapFs,
&path.join(TRUES_DIRNAME),
FlagsMode::from_feature_flags(),
Populate::No,
)?;
// Falses bitslice
let falses_path = path.join(FALSES_DIRNAME);
let falses_slice = DynamicStoredFlags::open(&MmapFs, &falses_path, Populate::No)?;
let falses_flags = RoaringFlags::new(MmapFs, falses_slice)?;
// Falses flags
let falses_flags = RoaringFlags::open_or_create(
MmapFs,
&path.join(FALSES_DIRNAME),
FlagsMode::from_feature_flags(),
Populate::No,
)?;
// Infallible for the writable variant: its bitmaps are materialized by
// `RoaringFlags::new` above.
@@ -5,7 +5,7 @@ use common::universal_io::{CachedReadFs, Populate, UniversalReadFs};
use super::super::mutable_bool_index::{FALSES_DIRNAME, TRUES_DIRNAME};
use super::{ReadOnlyBoolIndex, ReadOnlyStorage};
use crate::common::flags::read_only_roaring_flags::ReadOnlyRoaringFlags;
use crate::common::flags::read_only_flags::ReadOnlyFlags;
use crate::common::operation_error::{OperationError, OperationResult};
use crate::index::UniversalReadExt;
@@ -22,8 +22,8 @@ impl<S: UniversalReadExt> ReadOnlyBoolIndex<S> {
path: &Path,
populate: Populate,
) -> OperationResult<bool> {
let trues = ReadOnlyRoaringFlags::<S>::preopen(fs, &path.join(TRUES_DIRNAME), populate)?;
let falses = ReadOnlyRoaringFlags::<S>::preopen(fs, &path.join(FALSES_DIRNAME), populate)?;
let trues = ReadOnlyFlags::<S>::preopen(fs, &path.join(TRUES_DIRNAME), populate)?;
let falses = ReadOnlyFlags::<S>::preopen(fs, &path.join(FALSES_DIRNAME), populate)?;
Ok(trues || falses)
}
@@ -46,8 +46,8 @@ impl<S: UniversalReadExt> ReadOnlyBoolIndex<S> {
pub fn open(fs: &impl UniversalReadFs<File = S>, path: &Path) -> OperationResult<Option<Self>> {
// Open both directories first so a partial layout can be distinguished
// from a genuinely absent index, regardless of which half is missing.
let trues_flags = ReadOnlyRoaringFlags::<S>::open(fs, &path.join(TRUES_DIRNAME))?;
let falses_flags = ReadOnlyRoaringFlags::<S>::open(fs, &path.join(FALSES_DIRNAME))?;
let trues_flags = ReadOnlyFlags::<S>::open(fs, &path.join(TRUES_DIRNAME))?;
let falses_flags = ReadOnlyFlags::<S>::open(fs, &path.join(FALSES_DIRNAME))?;
match (trues_flags, falses_flags) {
// Neither directory exists: the index isn't present on disk.
@@ -3,7 +3,7 @@ use std::sync::OnceLock;
use roaring::RoaringBitmap;
use crate::common::flags::read_only_roaring_flags::ReadOnlyRoaringFlags;
use crate::common::flags::read_only_flags::ReadOnlyFlags;
use crate::common::flags::roaring_flags::RoaringFlagsRead;
use crate::common::operation_error::OperationResult;
use crate::index::UniversalReadExt;
@@ -21,9 +21,9 @@ mod read_ops;
/// condition checker / faceting) is shared with the writable variants via
/// [`BoolIndexRead`][4].
///
/// Each [`ReadOnlyRoaringFlags`] retains its backend `S` so a [`LiveReload`][5]
/// can reopen the bitslice and apply only the changed positions, instead of
/// re-materializing the bitmaps from scratch.
/// Each [`ReadOnlyFlags`] retains its backend `S` so a [`LiveReload`][5]
/// can reopen the backing files and resync, instead of re-materializing the
/// bitmaps from scratch.
///
/// [1]: super::mutable_bool_index::MutableBoolIndex
/// [2]: super::immutable_bool_index::ImmutableBoolIndex
@@ -37,7 +37,7 @@ pub struct ReadOnlyBoolIndex<S: UniversalReadExt> {
///
/// Deriving them eagerly would force both bitmaps to materialize at open,
/// scanning each flags file end to end — exactly what the lazy
/// [`ReadOnlyRoaringFlags`] bitmap exists to avoid. [`LiveReload`][1]
/// [`ReadOnlyFlags`] bitmap exists to avoid. [`LiveReload`][1]
/// refreshes them in place if they are present, and leaves them unset
/// otherwise.
///
@@ -59,9 +59,9 @@ pub(super) struct BoolCounts {
pub(super) struct ReadOnlyStorage<S: UniversalReadExt> {
/// Points which have at least one `true` value
pub(super) trues_flags: ReadOnlyRoaringFlags<S>,
pub(super) trues_flags: ReadOnlyFlags<S>,
/// Points which have at least one `false` value
pub(super) falses_flags: ReadOnlyRoaringFlags<S>,
pub(super) falses_flags: ReadOnlyFlags<S>,
}
impl<S: UniversalReadExt> ReadOnlyBoolIndex<S> {
@@ -4,7 +4,7 @@ use common::types::PointOffsetType;
use super::super::read_ops::{self, BoolIndexRead};
use super::ReadOnlyBoolIndex;
use crate::common::flags::read_only_roaring_flags::ReadOnlyRoaringFlags;
use crate::common::flags::read_only_flags::ReadOnlyFlags;
use crate::common::operation_error::OperationResult;
use crate::data_types::facets::{FacetHit, FacetValue, FacetValueRef};
use crate::index::UniversalReadExt;
@@ -28,7 +28,7 @@ impl<S: UniversalReadExt> ReadOnlyBoolIndex<S> {
}
impl<S: UniversalReadExt> BoolIndexRead for ReadOnlyBoolIndex<S> {
type Flags = ReadOnlyRoaringFlags<S>;
type Flags = ReadOnlyFlags<S>;
fn trues_flags(&self) -> &Self::Flags {
&self.storage.trues_flags
@@ -225,8 +225,9 @@ fn rewriting_a_slot_is_rejected() {
assert!(writer.add_point(0, &[&json!(0)], &hw_counter).is_err());
}
/// The boolean index is mask-backed: its writer rewrites both masks whole, and
/// leaves behind the same two files the mutable index writes.
/// The boolean index is mask-backed: its writer rewrites both masks whole,
/// leaving compact-mode flags the mutable index picks up through mode
/// detection.
#[test]
fn bool_index_round_trip() {
let dir = TempDir::with_prefix("update_only_index").unwrap();
@@ -10,7 +10,7 @@ use serde_json::Value;
use super::super::read_ops::NullIndexRead;
use super::{HAS_VALUES_DIRNAME, IS_NULL_DIRNAME, MutableNullIndex, Storage};
use crate::common::Flusher;
use crate::common::flags::dynamic_stored_flags::DynamicStoredFlags;
use crate::common::flags::FlagsMode;
use crate::common::flags::roaring_flags::RoaringFlags;
use crate::common::operation_error::{OperationError, OperationResult};
use crate::index::field_index::{FieldIndexBuilderTrait, PayloadFieldIndex};
@@ -76,13 +76,19 @@ impl MutableNullIndex {
))
})?;
let has_values_path = path.join(HAS_VALUES_DIRNAME);
let has_values_mmap = DynamicStoredFlags::open(&MmapFs, &has_values_path, Populate::No)?;
let has_values_flags = RoaringFlags::new(MmapFs, has_values_mmap)?;
let has_values_flags = RoaringFlags::open_or_create(
MmapFs,
&path.join(HAS_VALUES_DIRNAME),
FlagsMode::from_feature_flags(),
Populate::No,
)?;
let is_null_path = path.join(IS_NULL_DIRNAME);
let is_null_mmap = DynamicStoredFlags::open(&MmapFs, &is_null_path, Populate::No)?;
let is_null_flags = RoaringFlags::new(MmapFs, is_null_mmap)?;
let is_null_flags = RoaringFlags::open_or_create(
MmapFs,
&path.join(IS_NULL_DIRNAME),
FlagsMode::from_feature_flags(),
Populate::No,
)?;
let storage = Storage {
has_values_flags,
@@ -4,7 +4,7 @@ use common::universal_io::{CachedReadFs, Populate, UniversalRead, UniversalReadF
use super::super::mutable_null_index::{HAS_VALUES_DIRNAME, IS_NULL_DIRNAME};
use super::{ReadOnlyNullIndex, ReadOnlyStorage};
use crate::common::flags::read_only_roaring_flags::ReadOnlyRoaringFlags;
use crate::common::flags::read_only_flags::ReadOnlyFlags;
use crate::common::operation_error::{OperationError, OperationResult};
impl<S: UniversalRead> ReadOnlyNullIndex<S> {
@@ -20,10 +20,8 @@ impl<S: UniversalRead> ReadOnlyNullIndex<S> {
path: &Path,
populate: Populate,
) -> OperationResult<bool> {
let has_values =
ReadOnlyRoaringFlags::<S>::preopen(fs, &path.join(HAS_VALUES_DIRNAME), populate)?;
let is_null =
ReadOnlyRoaringFlags::<S>::preopen(fs, &path.join(IS_NULL_DIRNAME), populate)?;
let has_values = ReadOnlyFlags::<S>::preopen(fs, &path.join(HAS_VALUES_DIRNAME), populate)?;
let is_null = ReadOnlyFlags::<S>::preopen(fs, &path.join(IS_NULL_DIRNAME), populate)?;
Ok(has_values || is_null)
}
@@ -50,8 +48,8 @@ impl<S: UniversalRead> ReadOnlyNullIndex<S> {
) -> OperationResult<Option<Self>> {
// Open both directories first so a partial layout can be distinguished
// from a genuinely absent index, regardless of which half is missing.
let has_values_flags = ReadOnlyRoaringFlags::<S>::open(fs, &path.join(HAS_VALUES_DIRNAME))?;
let is_null_flags = ReadOnlyRoaringFlags::<S>::open(fs, &path.join(IS_NULL_DIRNAME))?;
let has_values_flags = ReadOnlyFlags::<S>::open(fs, &path.join(HAS_VALUES_DIRNAME))?;
let is_null_flags = ReadOnlyFlags::<S>::open(fs, &path.join(IS_NULL_DIRNAME))?;
match (has_values_flags, is_null_flags) {
// Neither directory exists: the index isn't present on disk.
@@ -2,7 +2,7 @@ use std::path::PathBuf;
use common::universal_io::UniversalRead;
use crate::common::flags::read_only_roaring_flags::ReadOnlyRoaringFlags;
use crate::common::flags::read_only_flags::ReadOnlyFlags;
use crate::index::payload_config::IndexMutability;
mod lifecycle;
@@ -16,9 +16,9 @@ mod read_ops;
/// bound to [`UniversalRead`][3] only — no buffer, no flusher, no write path. Query logic (filter / cardinality / condition checker) is
/// shared with the writable variant via the [`NullIndexRead`][4] trait.
///
/// Each [`ReadOnlyRoaringFlags`] retains its backend `S` so a [`LiveReload`][5]
/// can reopen the bitslice and apply only the changed positions on reload,
/// instead of re-materializing the bitmaps from scratch.
/// Each [`ReadOnlyFlags`] retains its backend `S` so a [`LiveReload`][5]
/// can reopen the backing files and resync on reload, instead of
/// re-materializing the bitmaps from scratch.
///
/// [1]: super::mutable_null_index::MutableNullIndex
/// [2]: super::immutable_null_index::ImmutableNullIndex
@@ -33,9 +33,9 @@ pub struct ReadOnlyNullIndex<S: UniversalRead> {
pub(super) struct ReadOnlyStorage<S: UniversalRead> {
/// Points which have at least one value
pub(super) has_values_flags: ReadOnlyRoaringFlags<S>,
pub(super) has_values_flags: ReadOnlyFlags<S>,
/// Points which have null values
pub(super) is_null_flags: ReadOnlyRoaringFlags<S>,
pub(super) is_null_flags: ReadOnlyFlags<S>,
}
impl<S: UniversalRead> ReadOnlyNullIndex<S> {
@@ -5,7 +5,7 @@ use common::universal_io::UniversalRead;
use super::super::read_ops::{self, NullIndexRead};
use super::ReadOnlyNullIndex;
use crate::common::flags::read_only_roaring_flags::ReadOnlyRoaringFlags;
use crate::common::flags::read_only_flags::ReadOnlyFlags;
use crate::common::operation_error::OperationResult;
use crate::index::UniversalReadExt;
use crate::index::condition_checker::ConditionCheckerEnum;
@@ -15,7 +15,7 @@ use crate::index::field_index::{
use crate::types::{FieldCondition, PayloadKeyType};
impl<S: UniversalRead> NullIndexRead for ReadOnlyNullIndex<S> {
type Flags = ReadOnlyRoaringFlags<S>;
type Flags = ReadOnlyFlags<S>;
fn has_values_flags(&self) -> &Self::Flags {
&self.storage.has_values_flags
@@ -12,8 +12,8 @@ use common::universal_io::{MmapFile, MmapFs, Populate, UserData};
use fs_err as fs;
use crate::common::Flusher;
use crate::common::flags::FlagsMode;
use crate::common::flags::bitvec_flags::BitvecFlags;
use crate::common::flags::dynamic_stored_flags::DynamicStoredFlags;
use crate::common::operation_error::{OperationResult, check_process_stopped};
use crate::data_types::named_vectors::CowVector;
use crate::data_types::primitive::PrimitiveVectorElement;
@@ -318,9 +318,11 @@ pub fn open_appendable_memmap_vector_storage_impl<T: PrimitiveVectorElement>(
Populate::from(populate),
)?;
let deleted = BitvecFlags::new(
let deleted = BitvecFlags::open_or_create(
MmapFs,
DynamicStoredFlags::open(&MmapFs, &deleted_path, Populate::from(populate))?,
&deleted_path,
FlagsMode::from_feature_flags(),
Populate::from(populate),
)?;
let deleted_count = deleted.count_trues();
@@ -13,8 +13,8 @@ use fs_err as fs;
use super::buffered_offsets::BufferedOffsets;
use crate::common::Flusher;
use crate::common::flags::FlagsMode;
use crate::common::flags::bitvec_flags::BitvecFlags;
use crate::common::flags::dynamic_stored_flags::DynamicStoredFlags;
use crate::common::operation_error::{OperationError, OperationResult, check_process_stopped};
use crate::data_types::named_vectors::{CowMultiVector, CowVector};
use crate::data_types::primitive::PrimitiveVectorElement;
@@ -614,9 +614,11 @@ pub fn open_appendable_memmap_multi_vector_storage_impl<T: PrimitiveVectorElemen
// the durable `vectors` length, which would corrupt points on reload.
let offsets = BufferedOffsets::new(offsets);
let deleted = BitvecFlags::new(
let deleted = BitvecFlags::open_or_create(
MmapFs,
DynamicStoredFlags::open(&MmapFs, &deleted_path, Populate::from(populate))?,
&deleted_path,
FlagsMode::from_feature_flags(),
Populate::from(populate),
)?;
let deleted_count = deleted.count_trues();
@@ -16,8 +16,8 @@ use common::universal_io::{MmapFile, MmapFs, Populate, UserData};
use fs_err as fs;
use sparse::common::sparse_vector::SparseVector;
use crate::common::flags::FlagsMode;
use crate::common::flags::bitvec_flags::BitvecFlags;
use crate::common::flags::dynamic_stored_flags::DynamicStoredFlags;
use crate::common::operation_error::{OperationError, OperationResult, check_process_stopped};
use crate::data_types::named_vectors::CowVector;
use crate::data_types::vectors::VectorRef;
@@ -71,9 +71,11 @@ impl MmapSparseVectorStorage {
// Deleted flags
let deleted_path = path.join(DELETED_DIRNAME);
let deleted = BitvecFlags::new(
let deleted = BitvecFlags::open_or_create(
MmapFs,
DynamicStoredFlags::open(&MmapFs, &deleted_path, populate)?,
&deleted_path,
FlagsMode::from_feature_flags(),
populate,
)?;
let deleted_count = deleted.count_trues();
@@ -117,9 +119,11 @@ impl MmapSparseVectorStorage {
// Deleted flags
let deleted_path = path.join(DELETED_DIRNAME);
let deleted = BitvecFlags::new(
let deleted = BitvecFlags::open_or_create(
MmapFs,
DynamicStoredFlags::open(&MmapFs, &deleted_path, Populate::from(populate))?,
&deleted_path,
FlagsMode::from_feature_flags(),
Populate::from(populate),
)?;
Ok(Self {
@@ -22,8 +22,8 @@ use quantization::{EncodedStorage, EncodedStorageWrite};
use super::shared::{self, DELETED_DIR_PATH, VECTORS_DIR_PATH};
use crate::common::Flusher;
use crate::common::flags::FlagsMode;
use crate::common::flags::bitvec_flags::BitvecFlags;
use crate::common::flags::dynamic_stored_flags::DynamicStoredFlags;
use crate::common::operation_error::{OperationError, OperationResult, check_process_stopped};
use crate::data_types::named_vectors::CowVector;
use crate::data_types::vectors::{DenseVector, VectorElementType, VectorRef};
@@ -85,13 +85,11 @@ impl AppendableMmapTurboVectorStorage {
in_ram,
)?;
let deleted = BitvecFlags::new(
let deleted = BitvecFlags::open_or_create(
MmapFs,
DynamicStoredFlags::open(
&MmapFs,
&path.join(DELETED_DIR_PATH),
Populate::from(in_ram),
)?,
&path.join(DELETED_DIR_PATH),
FlagsMode::from_feature_flags(),
Populate::from(in_ram),
)?;
let deleted_count = deleted.count_trues();
let quantization_buffer = vec![0.0; quantizer.get_padded_dim()];
@@ -29,8 +29,8 @@ use super::super::shared::{
DELETED_DIR_PATH, TQDT_BITS, TQDT_MODE, TQDT_ROTATION, VECTORS_DIR_PATH,
};
use crate::common::Flusher;
use crate::common::flags::FlagsMode;
use crate::common::flags::bitvec_flags::BitvecFlags;
use crate::common::flags::dynamic_stored_flags::DynamicStoredFlags;
use crate::common::operation_error::{OperationError, OperationResult, check_process_stopped};
use crate::data_types::named_vectors::{CowMultiVector, CowVector};
use crate::data_types::vectors::{
@@ -135,9 +135,11 @@ pub fn open_appendable_turbo_multi_vector_storage(
populate,
)?;
let deleted = BitvecFlags::new(
let deleted = BitvecFlags::open_or_create(
MmapFs,
DynamicStoredFlags::open(&MmapFs, &path.join(DELETED_DIR_PATH), populate)?,
&path.join(DELETED_DIR_PATH),
FlagsMode::from_feature_flags(),
populate,
)?;
let deleted_count = deleted.count_trues();
@@ -27,8 +27,8 @@ use quantization::{EncodedStorage, EncodedStorageWrite};
use super::shared::{self, DELETED_DIR_PATH, VECTORS_PATH};
use crate::common::Flusher;
use crate::common::flags::FlagsMode;
use crate::common::flags::bitvec_flags::BitvecFlags;
use crate::common::flags::dynamic_stored_flags::DynamicStoredFlags;
use crate::common::io_uring::{IoUringFallback, use_io_uring};
use crate::common::operation_error::{OperationError, OperationResult, check_process_stopped};
use crate::data_types::named_vectors::CowVector;
@@ -164,13 +164,11 @@ impl<S: UniversalRead> TurboVectorStorageImpl<S> {
populate: bool,
) -> OperationResult<Self> {
fs_err::create_dir_all(path)?;
let deleted = BitvecFlags::new(
let deleted = BitvecFlags::open_or_create(
MmapFs,
DynamicStoredFlags::open(
&MmapFs,
&path.join(DELETED_DIR_PATH),
Populate::from(populate),
)?,
&path.join(DELETED_DIR_PATH),
FlagsMode::from_feature_flags(),
Populate::from(populate),
)?;
let deleted_count = deleted.count_trues();
let quantization_buffer = vec![0.0; quantizer.get_padded_dim()];