vortex_layout/segments/
cache.rs1use std::sync::Arc;
5
6use async_trait::async_trait;
7use futures::FutureExt;
8use moka::future::Cache;
9use moka::future::CacheBuilder;
10use moka::policy::EvictionPolicy;
11use rustc_hash::FxBuildHasher;
12use vortex_array::buffer::BufferHandle;
13use vortex_buffer::ByteBuffer;
14use vortex_error::VortexExpect;
15use vortex_error::VortexResult;
16use vortex_metrics::Counter;
17use vortex_metrics::Label;
18use vortex_metrics::MetricBuilder;
19use vortex_metrics::MetricsRegistry;
20
21use crate::segments::SegmentFuture;
22use crate::segments::SegmentId;
23use crate::segments::SegmentSource;
24
25#[async_trait]
50pub trait SegmentCache: Send + Sync {
51 async fn get(&self, id: SegmentId) -> VortexResult<Option<ByteBuffer>>;
53 async fn put(&self, id: SegmentId, buffer: ByteBuffer) -> VortexResult<()>;
55}
56
57pub struct NoOpSegmentCache;
59
60#[async_trait]
61impl SegmentCache for NoOpSegmentCache {
62 async fn get(&self, _id: SegmentId) -> VortexResult<Option<ByteBuffer>> {
63 Ok(None)
64 }
65
66 async fn put(&self, _id: SegmentId, _buffer: ByteBuffer) -> VortexResult<()> {
67 Ok(())
68 }
69}
70
71#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
73pub enum SegmentEviction {
74 #[default]
77 Lru,
78 TinyLfu,
82}
83
84#[derive(Clone, Debug, PartialEq, Eq, Hash)]
90pub struct SegmentSourceId(Arc<str>);
91
92impl SegmentSourceId {
93 pub fn new(id: impl Into<Arc<str>>) -> Self {
95 Self(id.into())
96 }
97}
98
99impl From<&str> for SegmentSourceId {
100 fn from(id: &str) -> Self {
101 Self::new(id)
102 }
103}
104
105impl From<String> for SegmentSourceId {
106 fn from(id: String) -> Self {
107 Self::new(id)
108 }
109}
110
111impl From<Arc<str>> for SegmentSourceId {
112 fn from(id: Arc<str>) -> Self {
113 Self(id)
114 }
115}
116
117#[derive(Clone)]
124pub struct MokaSegmentCache(Cache<(SegmentSourceId, SegmentId), ByteBuffer, FxBuildHasher>);
125
126impl MokaSegmentCache {
127 pub fn new(max_capacity_bytes: u64, eviction: SegmentEviction) -> Self {
129 let eviction_policy = match eviction {
130 SegmentEviction::Lru => EvictionPolicy::lru(),
131 SegmentEviction::TinyLfu => EvictionPolicy::tiny_lfu(),
132 };
133 Self(
134 CacheBuilder::new(max_capacity_bytes)
135 .name("vortex-segment-cache")
136 .weigher(|_, buffer: &ByteBuffer| {
138 u32::try_from(buffer.len().min(u32::MAX as usize)).vortex_expect("must fit")
139 })
140 .eviction_policy(eviction_policy)
141 .build_with_hasher(FxBuildHasher),
142 )
143 }
144
145 pub fn for_file(&self, source_id: impl Into<SegmentSourceId>) -> FileSegmentCache {
147 FileSegmentCache {
148 cache: self.clone(),
149 source_id: source_id.into(),
150 }
151 }
152
153 pub fn weighted_size(&self) -> u64 {
158 self.0.weighted_size()
159 }
160
161 pub fn entry_count(&self) -> u64 {
163 self.0.entry_count()
164 }
165
166 pub async fn run_pending_tasks(&self) {
168 self.0.run_pending_tasks().await;
169 }
170
171 pub fn invalidate_all(&self) {
173 self.0.invalidate_all();
174 }
175}
176
177#[derive(Clone)]
179pub struct FileSegmentCache {
180 cache: MokaSegmentCache,
181 source_id: SegmentSourceId,
182}
183
184#[async_trait]
185impl SegmentCache for FileSegmentCache {
186 async fn get(&self, id: SegmentId) -> VortexResult<Option<ByteBuffer>> {
187 Ok(self.cache.0.get(&(self.source_id.clone(), id)).await)
188 }
189
190 async fn put(&self, id: SegmentId, buffer: ByteBuffer) -> VortexResult<()> {
191 self.cache
192 .0
193 .insert((self.source_id.clone(), id), buffer)
194 .await;
195 Ok(())
196 }
197}
198
199pub struct InstrumentedSegmentCache<C> {
201 segment_cache: C,
202
203 hits: Counter,
204 misses: Counter,
205 stores: Counter,
206}
207
208impl<C: SegmentCache> InstrumentedSegmentCache<C> {
209 pub fn new(
211 segment_cache: C,
212 metrics_registry: &dyn MetricsRegistry,
213 labels: Vec<Label>,
214 ) -> Self {
215 Self {
216 segment_cache,
217 hits: MetricBuilder::new(metrics_registry)
218 .add_labels(labels.clone())
219 .counter("vortex.file.segments.cache.hits"),
220 misses: MetricBuilder::new(metrics_registry)
221 .add_labels(labels.clone())
222 .counter("vortex.file.segments.cache.misses"),
223 stores: MetricBuilder::new(metrics_registry)
224 .add_labels(labels)
225 .counter("vortex.file.segments.cache.stores"),
226 }
227 }
228}
229
230#[async_trait]
231impl<C: SegmentCache> SegmentCache for InstrumentedSegmentCache<C> {
232 async fn get(&self, id: SegmentId) -> VortexResult<Option<ByteBuffer>> {
233 let result = self.segment_cache.get(id).await?;
234 if result.is_some() {
235 self.hits.add(1);
236 } else {
237 self.misses.add(1);
238 }
239 Ok(result)
240 }
241
242 async fn put(&self, id: SegmentId, buffer: ByteBuffer) -> VortexResult<()> {
243 self.segment_cache.put(id, buffer).await?;
244 self.stores.add(1);
245 Ok(())
246 }
247}
248
249pub struct SegmentCacheSourceAdapter {
251 cache: Arc<dyn SegmentCache>,
252 source: Arc<dyn SegmentSource>,
253}
254
255impl SegmentCacheSourceAdapter {
256 pub fn new(cache: Arc<dyn SegmentCache>, source: Arc<dyn SegmentSource>) -> Self {
258 Self { cache, source }
259 }
260}
261
262impl SegmentSource for SegmentCacheSourceAdapter {
263 fn request(&self, id: SegmentId) -> SegmentFuture {
264 let cache = Arc::clone(&self.cache);
265 let delegate = self.source.request(id);
266
267 async move {
268 if let Ok(Some(segment)) = cache.get(id).await {
269 tracing::debug!("Resolved segment {} from cache", id);
270 return Ok(BufferHandle::new_host(segment));
271 }
272 let result = delegate.await?;
273 if let Some(buffer) = result.as_host_opt()
275 && let Err(e) = cache.put(id, buffer.clone()).await
276 {
277 tracing::warn!("Failed to store segment {} in cache: {}", id, e);
278 }
279 Ok(result)
280 }
281 .boxed()
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use rstest::rstest;
288 use vortex_buffer::ByteBuffer;
289
290 use super::*;
291
292 #[rstest]
293 #[case::lru(SegmentEviction::Lru)]
294 #[case::tiny_lfu(SegmentEviction::TinyLfu)]
295 #[tokio::test]
296 async fn shared_cache_separates_files(#[case] eviction: SegmentEviction) -> VortexResult<()> {
297 let shared = MokaSegmentCache::new(1 << 20, eviction);
298 let a = shared.for_file("a");
299 let b = shared.for_file("b");
300 let id = SegmentId::from(0);
301
302 a.put(id, ByteBuffer::copy_from(b"from a")).await?;
303 assert_eq!(a.get(id).await?.as_deref(), Some(b"from a".as_slice()));
304 assert!(b.get(id).await?.is_none());
305
306 let a_again = shared.for_file("a");
308 assert_eq!(
309 a_again.get(id).await?.as_deref(),
310 Some(b"from a".as_slice())
311 );
312 Ok(())
313 }
314
315 #[rstest]
316 #[case::lru(SegmentEviction::Lru)]
317 #[case::tiny_lfu(SegmentEviction::TinyLfu)]
318 #[tokio::test]
319 async fn shared_cache_is_capped_by_bytes(
320 #[case] eviction: SegmentEviction,
321 ) -> VortexResult<()> {
322 let shared = MokaSegmentCache::new(1000, eviction);
323 let file = shared.for_file("file");
324 for i in 0..10u32 {
325 file.put(SegmentId::from(i), ByteBuffer::copy_from(vec![0u8; 400]))
326 .await?;
327 }
328
329 shared.run_pending_tasks().await;
330 assert!(shared.weighted_size() <= 1000);
331 assert!(shared.entry_count() <= 2);
332
333 if eviction == SegmentEviction::Lru {
335 assert!(file.get(SegmentId::from(9)).await?.is_some());
336 }
337 Ok(())
338 }
339}