use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use super::SSTableReader;
pub(crate) struct ScanLifetime {
inner: Option<Inner>,
}
struct Inner {
#[cfg_attr(not(unix), allow(dead_code))]
mmap: Option<Arc<memmap2::Mmap>>,
in_flight: std::sync::Mutex<u32>,
willneed: AtomicU64,
dontneed: AtomicU64,
}
impl ScanLifetime {
pub(crate) fn disabled() -> Arc<Self> {
Arc::new(Self { inner: None })
}
#[cfg(unix)]
pub(crate) fn for_scan_mapping(mmap: Arc<memmap2::Mmap>) -> Arc<Self> {
Arc::new(Self {
inner: Some(Inner::new(Some(mmap))),
})
}
#[cfg(test)]
pub(crate) fn counting_only() -> Arc<Self> {
Arc::new(Self {
inner: Some(Inner::new(None)),
})
}
pub(crate) fn begin(self: &Arc<Self>) -> ScanLifetimeGuard {
match &self.inner {
None => ScanLifetimeGuard { lifetime: None },
Some(inner) => {
let mut count = inner
.in_flight
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
*count = count.saturating_add(1);
if *count == 1 {
inner.advise_willneed();
}
ScanLifetimeGuard {
lifetime: Some(Arc::clone(self)),
}
}
}
}
pub(crate) fn in_flight(&self) -> u32 {
match &self.inner {
None => 0,
Some(inner) => *inner
.in_flight
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()),
}
}
pub(crate) fn advice_counts(&self) -> (u64, u64) {
match &self.inner {
None => (0, 0),
Some(inner) => (
inner.willneed.load(Ordering::Relaxed),
inner.dontneed.load(Ordering::Relaxed),
),
}
}
}
impl Inner {
fn new(mmap: Option<Arc<memmap2::Mmap>>) -> Self {
Self {
mmap,
in_flight: std::sync::Mutex::new(0),
willneed: AtomicU64::new(0),
dontneed: AtomicU64::new(0),
}
}
fn advise_willneed(&self) {
self.willneed.fetch_add(1, Ordering::Relaxed);
#[cfg(unix)]
if let Some(mmap) = &self.mmap {
if let Err(e) = mmap.advise(memmap2::Advice::WillNeed) {
tracing::debug!("madvise(WILLNEED) on scan mapping failed: {}", e);
}
}
}
fn advise_dontneed(&self) {
self.dontneed.fetch_add(1, Ordering::Relaxed);
#[cfg(unix)]
if let Some(mmap) = &self.mmap {
if let Err(e) = unsafe { mmap.unchecked_advise(memmap2::UncheckedAdvice::DontNeed) } {
tracing::debug!("madvise(DONTNEED) on scan mapping failed: {}", e);
}
}
}
}
pub(crate) struct ScanLifetimeGuard {
lifetime: Option<Arc<ScanLifetime>>,
}
impl Drop for ScanLifetimeGuard {
fn drop(&mut self) {
let Some(lifetime) = self.lifetime.take() else {
return;
};
let Some(inner) = &lifetime.inner else {
return;
};
let mut count = inner
.in_flight
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
*count = count.saturating_sub(1);
if *count == 0 {
inner.advise_dontneed();
}
}
}
impl SSTableReader {
pub(crate) fn begin_scan(&self) -> ScanLifetimeGuard {
self.scan_lifetime.begin()
}
pub fn scan_lifetime_advice_counts(&self) -> (u64, u64) {
self.scan_lifetime.advice_counts()
}
pub fn scan_lifetime_in_flight(&self) -> u32 {
self.scan_lifetime.in_flight()
}
pub fn scan_lifetime_enabled(&self) -> bool {
self.scan_lifetime.is_enabled()
}
}
impl ScanLifetime {
pub(crate) fn is_enabled(&self) -> bool {
self.inner.is_some()
}
}
#[cfg(unix)]
pub(crate) fn resolve(
scan_source: &super::source::ScanSource,
prefetch: crate::config::PrefetchMode,
point_plane_mmap: Option<&Arc<memmap2::Mmap>>,
) -> Arc<ScanLifetime> {
let scan_mmap = match scan_source {
super::source::ScanSource::Mapped(mmap) => mmap,
_ => return ScanLifetime::disabled(),
};
if !matches!(prefetch, crate::config::PrefetchMode::WillNeed) {
return ScanLifetime::disabled();
}
match point_plane_mmap {
Some(point) if !Arc::ptr_eq(point, scan_mmap) => {
ScanLifetime::for_scan_mapping(Arc::clone(scan_mmap))
}
_ => ScanLifetime::disabled(),
}
}
#[cfg(not(unix))]
pub(crate) fn resolve(
_scan_source: &super::source::ScanSource,
_prefetch: crate::config::PrefetchMode,
_point_plane_mmap: Option<&Arc<memmap2::Mmap>>,
) -> Arc<ScanLifetime> {
ScanLifetime::disabled()
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn disabled_never_advises() {
let lifetime = ScanLifetime::disabled();
let guard = lifetime.begin();
assert_eq!(lifetime.in_flight(), 0);
drop(guard);
assert_eq!(lifetime.advice_counts(), (0, 0));
}
#[test]
fn first_begin_advises_willneed_and_last_end_advises_dontneed() {
let lifetime = ScanLifetime::counting_only();
let outer = lifetime.begin();
assert_eq!(lifetime.advice_counts(), (1, 0));
assert_eq!(lifetime.in_flight(), 1);
let inner = lifetime.begin();
assert_eq!(
lifetime.advice_counts(),
(1, 0),
"nested begin must not re-advise"
);
assert_eq!(lifetime.in_flight(), 2);
drop(inner);
assert_eq!(
lifetime.advice_counts(),
(1, 0),
"inner end must not release"
);
drop(outer);
assert_eq!(lifetime.advice_counts(), (1, 1));
assert_eq!(lifetime.in_flight(), 0);
drop(lifetime.begin());
assert_eq!(lifetime.advice_counts(), (2, 2), "a later scan re-advises");
}
#[test]
fn overlapping_threads_advise_exactly_once() {
const THREADS: usize = 8;
let lifetime = ScanLifetime::counting_only();
let barrier = Arc::new(std::sync::Barrier::new(THREADS));
let mut handles = Vec::with_capacity(THREADS);
for _ in 0..THREADS {
let lifetime = Arc::clone(&lifetime);
let barrier = Arc::clone(&barrier);
handles.push(std::thread::spawn(move || {
let guard = lifetime.begin();
barrier.wait();
drop(guard);
}));
}
for h in handles {
assert!(h.join().is_ok());
}
assert_eq!(lifetime.advice_counts(), (1, 1));
assert_eq!(lifetime.in_flight(), 0);
}
}
#[cfg(all(test, unix))]
mod resolve_tests {
use super::*;
use crate::config::PrefetchMode;
use std::io::Write;
fn two_mappings() -> (
Arc<memmap2::Mmap>,
Arc<memmap2::Mmap>,
tempfile::NamedTempFile,
) {
let mut tmp = tempfile::NamedTempFile::new().expect("temp file");
tmp.write_all(&[7u8; 4096]).expect("write");
tmp.flush().expect("flush");
let map = || {
let f = std::fs::File::open(tmp.path()).expect("open");
Arc::new(unsafe { memmap2::MmapOptions::new().map(&f).expect("map") })
};
let scan = map();
let point = map();
assert!(!Arc::ptr_eq(&scan, &point), "two distinct mappings");
(scan, point, tmp)
}
#[test]
fn enabled_only_for_mapped_willneed_with_a_distinct_point_plane() {
let (scan, point, _tmp) = two_mappings();
let mapped = super::super::source::ScanSource::Mapped(Arc::clone(&scan));
assert!(
resolve(&mapped, PrefetchMode::WillNeed, Some(&point)).is_enabled(),
"mmap + WillNeed + distinct point mapping must arm the seam"
);
for mode in [
PrefetchMode::Auto,
PrefetchMode::Off,
PrefetchMode::Sequential,
] {
assert!(
!resolve(&mapped, mode, Some(&point)).is_enabled(),
"prefetch {:?} must leave the seam disabled",
mode
);
}
assert!(
!resolve(&mapped, PrefetchMode::WillNeed, Some(&scan)).is_enabled(),
"a shared point/scan mapping must disable the seam"
);
assert!(
!resolve(&mapped, PrefetchMode::WillNeed, None).is_enabled(),
"no recorded point mapping must disable the seam"
);
let buffered = super::super::source::ScanSource::Buffered { file_len: 4096 };
assert!(!resolve(&buffered, PrefetchMode::WillNeed, Some(&point)).is_enabled());
}
}