use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use crate::common::mmap::AdviceSetting;
use crate::common::stored_bitslice::StoredBitSlice;
use crate::common::types::PointOffsetType;
use crate::common::universal_io::{
CachedReadFs, OkNotFound, OpenOptions, Populate, TypedStorage, UniversalRead, UniversalReadFs,
};
use roaring::RoaringBitmap;
use super::dynamic_stored_flags::{DynamicFlagsStatus, FLAGS_FILE, status_file};
use super::roaring_flags::RoaringFlagsRead;
use crate::segment::common::operation_error::OperationResult;
pub struct ReadOnlyRoaringFlags<S: UniversalRead> {
bitmap: OnceLock<RoaringBitmap>,
storage: StoredBitSlice<S>,
len: usize,
directory: PathBuf,
}
fn open_options(populate: Populate) -> OpenOptions {
OpenOptions {
writeable: false,
need_sequential: false,
populate,
advice: AdviceSetting::Global,
}
}
fn read_status_len<S: UniversalRead>(
fs: &impl UniversalReadFs<File = S>,
directory: &Path,
) -> OperationResult<usize> {
let file = fs.open(
status_file(directory),
open_options(Populate::No),
Default::default(),
)?;
let status = TypedStorage::<S, DynamicFlagsStatus>::new(file);
Ok(status.read_whole()?[0].len())
}
impl<S: UniversalRead> ReadOnlyRoaringFlags<S> {
pub fn preopen(
fs: &impl CachedReadFs<File = S>,
directory: &Path,
populate: Populate,
) -> OperationResult<bool> {
if fs
.schedule_prefetch(
&status_file(directory),
Some(open_options(Populate::PreferBackground)),
None,
)
.ok_not_found()?
.is_none()
{
return Ok(false);
}
fs.schedule_prefetch(
&directory.join(FLAGS_FILE),
Some(open_options(populate)),
None,
)?;
Ok(true)
}
pub fn open(
fs: &impl UniversalReadFs<File = S>,
directory: &Path,
) -> OperationResult<Option<Self>> {
let Some(len) = read_status_len::<S>(fs, directory).ok_not_found()? else {
return Ok(None);
};
let storage = StoredBitSlice::<S>::open(
fs,
directory.join(FLAGS_FILE),
open_options(Populate::No),
Default::default(),
)?;
Ok(Some(Self {
bitmap: OnceLock::new(),
storage,
len,
directory: directory.to_path_buf(),
}))
}
fn bitmap(&self) -> OperationResult<&RoaringBitmap> {
if let Some(bitmap) = self.bitmap.get() {
return Ok(bitmap);
}
let bitmap = RoaringBitmap::from_sorted_iter(
self.storage.iter_ones()?.map(|i| i as PointOffsetType),
)
.expect("iter_ones iterates in sorted order");
Ok(self.bitmap.get_or_init(|| bitmap))
}
pub fn live_reload(&mut self, fs: &impl UniversalReadFs<File = S>) -> OperationResult<()> {
let storage = StoredBitSlice::<S>::open(
fs,
self.directory.join(FLAGS_FILE),
open_options(Populate::No),
Default::default(),
)?;
if let Some(bitmap) = self.bitmap.get_mut() {
*bitmap =
RoaringBitmap::from_sorted_iter(storage.iter_ones()?.map(|i| i as PointOffsetType))
.expect("iter_ones iterates in sorted order");
}
self.storage = storage;
self.len = read_status_len::<S>(fs, &self.directory)?;
Ok(())
}
}
impl<S: UniversalRead> RoaringFlagsRead for ReadOnlyRoaringFlags<S> {
fn len(&self) -> usize {
self.len
}
fn get_bitmap(&self) -> OperationResult<&RoaringBitmap> {
self.bitmap()
}
fn bitmap_if_materialized(&self) -> Option<&RoaringBitmap> {
self.bitmap.get()
}
fn files(&self) -> Vec<PathBuf> {
vec![
status_file(&self.directory),
self.directory.join(FLAGS_FILE),
]
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::common::universal_io::{
DiskCache, DiskCacheConfig, DiskCacheFs, DiskCacheFsContext, MmapFile, MmapFs,
UniversalReadFileOps,
};
use tempfile::Builder;
use super::*;
use crate::segment::common::flags::dynamic_stored_flags::DynamicStoredFlags;
#[test]
fn live_reload_over_disk_cache_sees_in_place_bit_writes() {
let tmp = Builder::new().prefix("roaring_reload").tempdir().unwrap();
let remote_root = tmp.path().join("remote");
let local_root = tmp.path().join("local");
let dir = remote_root.join("flags");
fs_err::create_dir_all(&dir).unwrap();
fs_err::create_dir_all(&local_root).unwrap();
let mut writer = DynamicStoredFlags::<MmapFile>::open(&MmapFs, &dir, Populate::No).unwrap();
writer.set_len(&MmapFs, 100).unwrap();
writer.set(5, true).unwrap();
writer.flusher()().unwrap();
let cache_fs = DiskCacheFs::<MmapFile>::from_context(DiskCacheFsContext {
config: Arc::new(DiskCacheConfig::new(remote_root, local_root).unwrap()),
remote: Default::default(),
})
.unwrap();
let mut flags = ReadOnlyRoaringFlags::<DiskCache<MmapFile>>::open(&cache_fs, &dir)
.unwrap()
.unwrap();
assert!(flags.get_bitmap().unwrap().contains(5));
assert_eq!(flags.len(), 100);
writer.set(6, true).unwrap();
writer.set(50, true).unwrap();
writer.set(5, false).unwrap();
writer.set_len(&MmapFs, 120).unwrap();
writer.flusher()().unwrap();
flags.live_reload(&cache_fs).unwrap();
let bitmap = flags.get_bitmap().unwrap();
assert!(bitmap.contains(6));
assert!(bitmap.contains(50));
assert!(
!bitmap.contains(5),
"cleared flag must not survive a reload"
);
assert_eq!(flags.len(), 120);
}
#[test]
fn live_reload_before_materialization_scans_fresh_state() {
let tmp = Builder::new().prefix("roaring_reload").tempdir().unwrap();
let remote_root = tmp.path().join("remote");
let local_root = tmp.path().join("local");
let dir = remote_root.join("flags");
fs_err::create_dir_all(&dir).unwrap();
fs_err::create_dir_all(&local_root).unwrap();
let mut writer = DynamicStoredFlags::<MmapFile>::open(&MmapFs, &dir, Populate::No).unwrap();
writer.set_len(&MmapFs, 100).unwrap();
writer.set(5, true).unwrap();
writer.flusher()().unwrap();
let cache_fs = DiskCacheFs::<MmapFile>::from_context(DiskCacheFsContext {
config: Arc::new(DiskCacheConfig::new(remote_root, local_root).unwrap()),
remote: Default::default(),
})
.unwrap();
let mut flags = ReadOnlyRoaringFlags::<DiskCache<MmapFile>>::open(&cache_fs, &dir)
.unwrap()
.unwrap();
writer.set(6, true).unwrap();
writer.flusher()().unwrap();
flags.live_reload(&cache_fs).unwrap();
let bitmap = flags.get_bitmap().unwrap();
assert!(bitmap.contains(5));
assert!(bitmap.contains(6));
}
}