use std::sync::Arc;
use std::task::Poll;
use async_trait::async_trait;
use futures::FutureExt;
use futures::future::poll_fn;
use moka::future::Cache;
use moka::future::CacheBuilder;
use moka::policy::EvictionPolicy;
use rustc_hash::FxBuildHasher;
use vortex_array::buffer::BufferHandle;
use vortex_buffer::ByteBuffer;
use vortex_error::VortexExpect;
use vortex_error::VortexResult;
use vortex_metrics::Counter;
use vortex_metrics::Label;
use vortex_metrics::MetricBuilder;
use vortex_metrics::MetricsRegistry;
use crate::segments::SegmentFuture;
use crate::segments::SegmentId;
use crate::segments::SegmentSource;
#[async_trait]
pub trait SegmentCache: Send + Sync {
async fn get(&self, id: SegmentId) -> VortexResult<Option<ByteBuffer>>;
async fn put(&self, id: SegmentId, buffer: ByteBuffer) -> VortexResult<()>;
}
pub struct NoOpSegmentCache;
#[async_trait]
impl SegmentCache for NoOpSegmentCache {
async fn get(&self, _id: SegmentId) -> VortexResult<Option<ByteBuffer>> {
Ok(None)
}
async fn put(&self, _id: SegmentId, _buffer: ByteBuffer) -> VortexResult<()> {
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SegmentEviction {
#[default]
Lru,
TinyLfu,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SegmentSourceId(Arc<str>);
impl SegmentSourceId {
pub fn new(id: impl Into<Arc<str>>) -> Self {
Self(id.into())
}
}
impl From<&str> for SegmentSourceId {
fn from(id: &str) -> Self {
Self::new(id)
}
}
impl From<String> for SegmentSourceId {
fn from(id: String) -> Self {
Self::new(id)
}
}
impl From<Arc<str>> for SegmentSourceId {
fn from(id: Arc<str>) -> Self {
Self(id)
}
}
#[derive(Clone)]
pub struct MokaSegmentCache(Cache<(SegmentSourceId, SegmentId), ByteBuffer, FxBuildHasher>);
impl MokaSegmentCache {
pub fn new(max_capacity_bytes: u64, eviction: SegmentEviction) -> Self {
let eviction_policy = match eviction {
SegmentEviction::Lru => EvictionPolicy::lru(),
SegmentEviction::TinyLfu => EvictionPolicy::tiny_lfu(),
};
Self(
CacheBuilder::new(max_capacity_bytes)
.name("vortex-segment-cache")
.weigher(|_, buffer: &ByteBuffer| {
u32::try_from(buffer.len().min(u32::MAX as usize)).vortex_expect("must fit")
})
.eviction_policy(eviction_policy)
.build_with_hasher(FxBuildHasher),
)
}
pub fn for_file(&self, source_id: impl Into<SegmentSourceId>) -> FileSegmentCache {
FileSegmentCache {
cache: self.clone(),
source_id: source_id.into(),
}
}
pub fn weighted_size(&self) -> u64 {
self.0.weighted_size()
}
pub fn entry_count(&self) -> u64 {
self.0.entry_count()
}
pub async fn run_pending_tasks(&self) {
self.0.run_pending_tasks().await;
}
pub fn invalidate_all(&self) {
self.0.invalidate_all();
}
}
#[derive(Clone)]
pub struct FileSegmentCache {
cache: MokaSegmentCache,
source_id: SegmentSourceId,
}
#[async_trait]
impl SegmentCache for FileSegmentCache {
async fn get(&self, id: SegmentId) -> VortexResult<Option<ByteBuffer>> {
Ok(self.cache.0.get(&(self.source_id.clone(), id)).await)
}
async fn put(&self, id: SegmentId, buffer: ByteBuffer) -> VortexResult<()> {
self.cache
.0
.insert((self.source_id.clone(), id), buffer)
.await;
Ok(())
}
}
pub struct InstrumentedSegmentCache<C> {
segment_cache: C,
hits: Counter,
misses: Counter,
stores: Counter,
}
impl<C: SegmentCache> InstrumentedSegmentCache<C> {
pub fn new(
segment_cache: C,
metrics_registry: &dyn MetricsRegistry,
labels: Vec<Label>,
) -> Self {
Self {
segment_cache,
hits: MetricBuilder::new(metrics_registry)
.add_labels(labels.clone())
.counter("vortex.file.segments.cache.hits"),
misses: MetricBuilder::new(metrics_registry)
.add_labels(labels.clone())
.counter("vortex.file.segments.cache.misses"),
stores: MetricBuilder::new(metrics_registry)
.add_labels(labels)
.counter("vortex.file.segments.cache.stores"),
}
}
}
#[async_trait]
impl<C: SegmentCache> SegmentCache for InstrumentedSegmentCache<C> {
async fn get(&self, id: SegmentId) -> VortexResult<Option<ByteBuffer>> {
let result = self.segment_cache.get(id).await?;
if result.is_some() {
self.hits.add(1);
} else {
self.misses.add(1);
}
Ok(result)
}
async fn put(&self, id: SegmentId, buffer: ByteBuffer) -> VortexResult<()> {
self.segment_cache.put(id, buffer).await?;
self.stores.add(1);
Ok(())
}
}
pub struct SegmentCacheSourceAdapter {
cache: Arc<dyn SegmentCache>,
source: Arc<dyn SegmentSource>,
}
impl SegmentCacheSourceAdapter {
pub fn new(cache: Arc<dyn SegmentCache>, source: Arc<dyn SegmentSource>) -> Self {
Self { cache, source }
}
}
impl SegmentSource for SegmentCacheSourceAdapter {
fn request(&self, id: SegmentId) -> SegmentFuture {
let cache = Arc::clone(&self.cache);
let source = Arc::clone(&self.source);
let mut request = async move {
if let Ok(Some(segment)) = cache.get(id).await {
tracing::debug!("Resolved segment {} from cache", id);
return Ok(BufferHandle::new_host(segment));
}
let delegate = source.request(id);
let mut defer = true;
poll_fn(move |cx| {
if std::mem::take(&mut defer) {
cx.waker().wake_by_ref();
Poll::Pending
} else {
Poll::Ready(())
}
})
.await;
let result = delegate.await?;
if let Some(buffer) = result.as_host_opt()
&& let Err(e) = cache.put(id, buffer.clone()).await
{
tracing::warn!("Failed to store segment {} in cache: {}", id, e);
}
Ok(result)
}
.boxed();
match request.as_mut().now_or_never() {
Some(result) => async move { result }.boxed(),
None => request,
}
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use futures::channel::oneshot;
use parking_lot::Mutex;
use rstest::rstest;
use vortex_buffer::ByteBuffer;
use super::*;
#[rstest]
#[case::lru(SegmentEviction::Lru)]
#[case::tiny_lfu(SegmentEviction::TinyLfu)]
#[tokio::test]
async fn shared_cache_separates_files(#[case] eviction: SegmentEviction) -> VortexResult<()> {
let shared = MokaSegmentCache::new(1 << 20, eviction);
let a = shared.for_file("a");
let b = shared.for_file("b");
let id = SegmentId::from(0);
a.put(id, ByteBuffer::copy_from(b"from a")).await?;
assert_eq!(a.get(id).await?.as_deref(), Some(b"from a".as_slice()));
assert!(b.get(id).await?.is_none());
let a_again = shared.for_file("a");
assert_eq!(
a_again.get(id).await?.as_deref(),
Some(b"from a".as_slice())
);
Ok(())
}
#[rstest]
#[case::lru(SegmentEviction::Lru)]
#[case::tiny_lfu(SegmentEviction::TinyLfu)]
#[tokio::test]
async fn shared_cache_is_capped_by_bytes(
#[case] eviction: SegmentEviction,
) -> VortexResult<()> {
let shared = MokaSegmentCache::new(1000, eviction);
let file = shared.for_file("file");
for i in 0..10u32 {
file.put(SegmentId::from(i), ByteBuffer::copy_from(vec![0u8; 400]))
.await?;
}
shared.run_pending_tasks().await;
assert!(shared.weighted_size() <= 1000);
assert!(shared.entry_count() <= 2);
if eviction == SegmentEviction::Lru {
assert!(file.get(SegmentId::from(9)).await?.is_some());
}
Ok(())
}
#[derive(Default)]
struct CountingSource(AtomicUsize, Arc<AtomicUsize>);
impl SegmentSource for CountingSource {
fn request(&self, _id: SegmentId) -> SegmentFuture {
self.0.fetch_add(1, Ordering::SeqCst);
let demanded = Arc::clone(&self.1);
async move {
demanded.fetch_add(1, Ordering::SeqCst);
Ok(BufferHandle::new_host(ByteBuffer::copy_from(b"source")))
}
.boxed()
}
}
#[tokio::test]
async fn cache_lookup_preserves_registration_and_demand_order() -> VortexResult<()> {
let cache = MokaSegmentCache::new(1024, SegmentEviction::Lru).for_file("file");
let hit = SegmentId::from(0);
cache.put(hit, ByteBuffer::copy_from(b"cached")).await?;
let source = Arc::new(CountingSource::default());
let cached =
SegmentCacheSourceAdapter::new(Arc::new(cache), Arc::<CountingSource>::clone(&source));
assert_eq!(
cached.request(hit).await?.unwrap_host(),
ByteBuffer::copy_from(b"cached")
);
assert_eq!(source.0.load(Ordering::SeqCst), 0);
let miss = cached.request(SegmentId::from(1));
assert_eq!(source.0.load(Ordering::SeqCst), 1);
assert_eq!(source.1.load(Ordering::SeqCst), 0);
assert_eq!(miss.await?.unwrap_host(), ByteBuffer::copy_from(b"source"));
assert_eq!(source.1.load(Ordering::SeqCst), 1);
Ok(())
}
struct DelayedCache(Mutex<Option<oneshot::Receiver<Option<ByteBuffer>>>>);
#[async_trait]
impl SegmentCache for DelayedCache {
async fn get(&self, _id: SegmentId) -> VortexResult<Option<ByteBuffer>> {
let lookup = self
.0
.lock()
.take()
.vortex_expect("lookup is not restarted");
Ok(lookup.await.expect("test supplies lookup result"))
}
async fn put(&self, _id: SegmentId, _buffer: ByteBuffer) -> VortexResult<()> {
Ok(())
}
}
#[rstest]
#[case::hit(true)]
#[case::miss(false)]
#[tokio::test]
async fn pending_lookup_finishes_before_registering_reads(
#[case] hit: bool,
) -> VortexResult<()> {
let (sender, receiver) = oneshot::channel();
let cache = Arc::new(DelayedCache(Mutex::new(Some(receiver))));
let source = Arc::new(CountingSource::default());
let cached = SegmentCacheSourceAdapter::new(cache, Arc::<CountingSource>::clone(&source));
let mut request = cached.request(SegmentId::from(0));
assert_eq!(source.0.load(Ordering::SeqCst), 0);
assert!(futures::poll!(request.as_mut()).is_pending());
assert_eq!(source.0.load(Ordering::SeqCst), 0);
sender
.send(hit.then(|| ByteBuffer::copy_from(b"cached")))
.unwrap();
let expected: &[u8] = if hit { b"cached" } else { b"source" };
assert_eq!(request.await?.unwrap_host().as_ref(), expected);
assert_eq!(source.0.load(Ordering::SeqCst), usize::from(!hit));
Ok(())
}
}