Skip to content

Commit 6287142

Browse files
Add DataFusion parquet range cache
1 parent 04b4cd3 commit 6287142

4 files changed

Lines changed: 205 additions & 9 deletions

File tree

quickwit/quickwit-datafusion/src/object_store_registry.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ use datafusion::common::{DataFusionError, Result as DFResult};
5151
use datafusion::execution::object_store::{DefaultObjectStoreRegistry, ObjectStoreRegistry};
5252
use object_store::ObjectStore;
5353
use quickwit_common::uri::Uri;
54-
use quickwit_storage::StorageResolver;
54+
use quickwit_storage::{StorageCache, StorageResolver};
5555
use url::Url;
5656

5757
use crate::storage_bridge::QuickwitObjectStore;
@@ -69,6 +69,7 @@ pub struct QuickwitObjectStoreRegistry {
6969
/// fallback.
7070
default: DefaultObjectStoreRegistry,
7171
storage_resolver: StorageResolver,
72+
storage_cache: Option<Arc<dyn StorageCache>>,
7273
/// Lazy wrappers constructed by `get_store` on demand, keyed by
7374
/// `scheme://authority`. Plain `RwLock<HashMap>` is fine — contention
7475
/// is negligible because the write lock is only taken once per unique
@@ -92,10 +93,16 @@ impl QuickwitObjectStoreRegistry {
9293
Self {
9394
default: DefaultObjectStoreRegistry::new(),
9495
storage_resolver,
96+
storage_cache: None,
9597
lazy_stores: RwLock::new(HashMap::new()),
9698
}
9799
}
98100

101+
pub fn with_storage_cache(mut self, storage_cache: Arc<dyn StorageCache>) -> Self {
102+
self.storage_cache = Some(storage_cache);
103+
self
104+
}
105+
99106
/// Canonical cache key mirroring DataFusion's `DefaultObjectStoreRegistry`:
100107
/// `scheme://authority`. Preserves the authority so indexes in
101108
/// different buckets stay distinct; paths within an authority share
@@ -138,8 +145,11 @@ impl ObjectStoreRegistry for QuickwitObjectStoreRegistry {
138145
"failed to build Quickwit URI from `{key}`: {err}"
139146
))))
140147
})?;
141-
let store: Arc<dyn ObjectStore> =
142-
Arc::new(QuickwitObjectStore::new(uri, self.storage_resolver.clone()));
148+
let mut quickwit_store = QuickwitObjectStore::new(uri, self.storage_resolver.clone());
149+
if let Some(storage_cache) = &self.storage_cache {
150+
quickwit_store = quickwit_store.with_storage_cache(Arc::clone(storage_cache));
151+
}
152+
let store: Arc<dyn ObjectStore> = Arc::new(quickwit_store);
143153
let mut write = self
144154
.lazy_stores
145155
.write()

quickwit/quickwit-datafusion/src/storage_bridge.rs

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
//! operations return `NotSupported` — DataFusion only reads parquet files
3131
//! through this store.
3232
33+
use std::ops::Range;
34+
use std::path::{Path, PathBuf};
3335
use std::sync::Arc;
3436

3537
use async_trait::async_trait;
@@ -43,7 +45,9 @@ use object_store::{
4345
Result as ObjectStoreResult,
4446
};
4547
use quickwit_common::uri::Uri;
46-
use quickwit_storage::{Storage, StorageResolver};
48+
use quickwit_storage::{
49+
OwnedBytes, Storage, StorageCache, StorageResolver, wrap_storage_with_cache,
50+
};
4751
use tokio::sync::OnceCell;
4852

4953
/// Adapts Quickwit's `Storage` trait to DataFusion's `ObjectStore` interface.
@@ -54,6 +58,7 @@ use tokio::sync::OnceCell;
5458
pub struct QuickwitObjectStore {
5559
index_uri: Uri,
5660
storage_resolver: StorageResolver,
61+
storage_cache: Option<Arc<dyn StorageCache>>,
5762
storage: OnceCell<Arc<dyn Storage>>,
5863
}
5964

@@ -62,27 +67,80 @@ impl QuickwitObjectStore {
6267
Self {
6368
index_uri,
6469
storage_resolver,
70+
storage_cache: None,
6571
storage: OnceCell::new(),
6672
}
6773
}
6874

75+
pub fn with_storage_cache(mut self, storage_cache: Arc<dyn StorageCache>) -> Self {
76+
self.storage_cache = Some(storage_cache);
77+
self
78+
}
79+
6980
/// Returns the handle to the underlying `Storage`, resolving it via the
7081
/// `StorageResolver` if this is the first call.
7182
async fn storage(&self) -> ObjectStoreResult<&Arc<dyn Storage>> {
7283
self.storage
7384
.get_or_try_init(|| async {
74-
self.storage_resolver
85+
let storage = self
86+
.storage_resolver
7587
.resolve(&self.index_uri)
7688
.await
7789
.map_err(|err| object_store::Error::Generic {
7890
store: "QuickwitObjectStore",
7991
source: Box::new(err),
80-
})
92+
})?;
93+
Ok(match &self.storage_cache {
94+
Some(cache) => wrap_storage_with_cache(
95+
Arc::new(ScopedStorageCache::new(
96+
self.index_uri.as_str().to_string(),
97+
Arc::clone(cache),
98+
)),
99+
storage,
100+
),
101+
None => storage,
102+
})
81103
})
82104
.await
83105
}
84106
}
85107

108+
struct ScopedStorageCache {
109+
scope: String,
110+
inner: Arc<dyn StorageCache>,
111+
}
112+
113+
impl ScopedStorageCache {
114+
fn new(scope: String, inner: Arc<dyn StorageCache>) -> Self {
115+
Self { scope, inner }
116+
}
117+
118+
fn scoped_path(&self, path: &Path) -> PathBuf {
119+
PathBuf::from(format!("{}#{}", self.scope, path.to_string_lossy()))
120+
}
121+
}
122+
123+
#[async_trait]
124+
impl StorageCache for ScopedStorageCache {
125+
async fn get(&self, path: &Path, byte_range: Range<usize>) -> Option<OwnedBytes> {
126+
self.inner.get(&self.scoped_path(path), byte_range).await
127+
}
128+
129+
async fn get_all(&self, path: &Path) -> Option<OwnedBytes> {
130+
self.inner.get_all(&self.scoped_path(path)).await
131+
}
132+
133+
async fn put(&self, path: PathBuf, byte_range: Range<usize>, bytes: OwnedBytes) {
134+
self.inner
135+
.put(self.scoped_path(&path), byte_range, bytes)
136+
.await
137+
}
138+
139+
async fn put_all(&self, path: PathBuf, bytes: OwnedBytes) {
140+
self.inner.put_all(self.scoped_path(&path), bytes).await
141+
}
142+
}
143+
86144
impl std::fmt::Debug for QuickwitObjectStore {
87145
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88146
f.debug_struct("QuickwitObjectStore")
@@ -264,3 +322,73 @@ impl ObjectStore for QuickwitObjectStore {
264322
})
265323
}
266324
}
325+
326+
#[cfg(test)]
327+
mod tests {
328+
use std::sync::Mutex;
329+
330+
use super::*;
331+
332+
#[derive(Default)]
333+
struct RecordingStorageCache {
334+
paths: Mutex<Vec<PathBuf>>,
335+
}
336+
337+
impl RecordingStorageCache {
338+
fn paths(&self) -> Vec<PathBuf> {
339+
self.paths.lock().unwrap().clone()
340+
}
341+
342+
fn record(&self, path: &Path) {
343+
self.paths.lock().unwrap().push(path.to_path_buf());
344+
}
345+
}
346+
347+
#[async_trait]
348+
impl StorageCache for RecordingStorageCache {
349+
async fn get(&self, path: &Path, _byte_range: Range<usize>) -> Option<OwnedBytes> {
350+
self.record(path);
351+
None
352+
}
353+
354+
async fn get_all(&self, path: &Path) -> Option<OwnedBytes> {
355+
self.record(path);
356+
None
357+
}
358+
359+
async fn put(&self, path: PathBuf, _byte_range: Range<usize>, _bytes: OwnedBytes) {
360+
self.record(&path);
361+
}
362+
363+
async fn put_all(&self, path: PathBuf, _bytes: OwnedBytes) {
364+
self.record(&path);
365+
}
366+
}
367+
368+
#[tokio::test]
369+
async fn scoped_storage_cache_prefixes_cache_keys() {
370+
let inner = Arc::new(RecordingStorageCache::default());
371+
let inner_cache: Arc<dyn StorageCache> = inner.clone();
372+
let cache = ScopedStorageCache::new("s3://metrics-bucket".to_string(), inner_cache);
373+
374+
cache
375+
.get(Path::new("indexes/metrics.parquet"), 10..20)
376+
.await;
377+
cache
378+
.put(
379+
Path::new("indexes/metrics.parquet").to_path_buf(),
380+
10..20,
381+
OwnedBytes::new(&b"bytes"[..]),
382+
)
383+
.await;
384+
385+
let paths = inner.paths();
386+
assert_eq!(
387+
paths,
388+
vec![
389+
PathBuf::from("s3://metrics-bucket#indexes/metrics.parquet"),
390+
PathBuf::from("s3://metrics-bucket#indexes/metrics.parquet"),
391+
]
392+
);
393+
}
394+
}

quickwit/quickwit-serve/src/datafusion_api/setup.rs

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,19 @@
2020
2121
use std::collections::BTreeSet;
2222
use std::net::SocketAddr;
23+
use std::ops::Range;
24+
use std::path::{Path, PathBuf};
2325
use std::sync::Arc;
2426
use std::time::Duration;
2527

2628
use anyhow::Context;
29+
use async_trait::async_trait;
2730
use bytesize::ByteSize;
2831
use futures::{StreamExt, stream};
2932
use quickwit_cluster::{ClusterChange, ClusterChangeStream, ClusterNode};
3033
use quickwit_common::tower::Change;
31-
use quickwit_config::NodeConfig;
3234
use quickwit_config::service::QuickwitService;
35+
use quickwit_config::{CacheConfig, NodeConfig};
3336
use quickwit_datafusion::grpc::DataFusionServiceGrpcImpl;
3437
use quickwit_datafusion::proto::data_fusion_service_server::{
3538
DataFusionServiceServer, SERVICE_NAME as DATAFUSION_SERVICE_NAME,
@@ -41,7 +44,7 @@ use quickwit_datafusion::{
4144
};
4245
use quickwit_proto::metastore::MetastoreServiceClient;
4346
use quickwit_search::{SearchServiceClient, SearcherPool, create_search_client_from_grpc_addr};
44-
use quickwit_storage::StorageResolver;
47+
use quickwit_storage::{MemorySizedCache, OwnedBytes, StorageCache, StorageResolver};
4548
use tokio::time::timeout;
4649
use tonic::transport::server::Router;
4750
use tonic_reflection::pb::v1::ServerReflectionRequest;
@@ -51,6 +54,10 @@ use tonic_reflection::pb::v1::server_reflection_response::MessageResponse;
5154

5255
use crate::QuickwitServices;
5356

57+
const DATAFUSION_PARQUET_RANGE_CACHE_CAPACITY_ENV: &str =
58+
"QW_DATAFUSION_PARQUET_RANGE_CACHE_CAPACITY";
59+
const FULL_SLICE: Range<usize> = 0..usize::MAX;
60+
5461
/// Build the generic DataFusion session builder for this node.
5562
///
5663
/// Returns `None` if the searcher role is disabled or the endpoint toggle is
@@ -84,7 +91,13 @@ pub(crate) fn build_datafusion_session_builder(
8491
);
8592
let worker_resolver = QuickwitWorkerResolver::new(datafusion_worker_pool)
8693
.with_tls(node_config.grpc_config.tls.is_some());
87-
let registry = Arc::new(QuickwitObjectStoreRegistry::new(storage_resolver));
94+
let datafusion_parquet_range_cache = datafusion_parquet_range_cache_config();
95+
let datafusion_storage_cache =
96+
DataFusionParquetRangeCache::from_config(&datafusion_parquet_range_cache);
97+
let registry = Arc::new(
98+
QuickwitObjectStoreRegistry::new(storage_resolver)
99+
.with_storage_cache(Arc::new(datafusion_storage_cache)),
100+
);
88101
let builder = DataFusionSessionBuilder::new()
89102
.with_object_store_registry(registry)
90103
.context("failed to install DataFusion object store registry")?
@@ -97,6 +110,49 @@ pub(crate) fn build_datafusion_session_builder(
97110
Ok(Some(Arc::new(builder)))
98111
}
99112

113+
fn datafusion_parquet_range_cache_config() -> CacheConfig {
114+
let capacity = quickwit_common::get_from_env(
115+
DATAFUSION_PARQUET_RANGE_CACHE_CAPACITY_ENV,
116+
ByteSize::gb(4),
117+
false,
118+
);
119+
CacheConfig::default_with_capacity(capacity)
120+
}
121+
122+
struct DataFusionParquetRangeCache {
123+
inner: MemorySizedCache,
124+
}
125+
126+
impl DataFusionParquetRangeCache {
127+
fn from_config(cache_config: &CacheConfig) -> Self {
128+
Self {
129+
inner: MemorySizedCache::from_config(
130+
cache_config,
131+
&quickwit_storage::STORAGE_METRICS.datafusion_parquet_range_cache,
132+
),
133+
}
134+
}
135+
}
136+
137+
#[async_trait]
138+
impl StorageCache for DataFusionParquetRangeCache {
139+
async fn get(&self, path: &Path, byte_range: Range<usize>) -> Option<OwnedBytes> {
140+
self.inner.get_slice(path, byte_range)
141+
}
142+
143+
async fn get_all(&self, path: &Path) -> Option<OwnedBytes> {
144+
self.inner.get_slice(path, FULL_SLICE)
145+
}
146+
147+
async fn put(&self, path: PathBuf, byte_range: Range<usize>, bytes: OwnedBytes) {
148+
self.inner.put_slice(path, byte_range, bytes);
149+
}
150+
151+
async fn put_all(&self, path: PathBuf, bytes: OwnedBytes) {
152+
self.inner.put_slice(path, FULL_SLICE, bytes);
153+
}
154+
}
155+
100156
fn setup_datafusion_worker_pool(
101157
cluster_change_stream: ClusterChangeStream,
102158
max_message_size: ByteSize,

quickwit/quickwit-storage/src/metrics.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub struct StorageMetrics {
3030
pub predicate_cache: CacheMetrics,
3131
pub fd_cache_metrics: CacheMetrics,
3232
pub fast_field_cache: CacheMetrics,
33+
pub datafusion_parquet_range_cache: CacheMetrics,
3334
pub split_footer_cache: CacheMetrics,
3435
pub searcher_split_cache: CacheMetrics,
3536
pub get_slice_timeout_successes: [IntCounter; 3],
@@ -94,6 +95,7 @@ impl Default for StorageMetrics {
9495

9596
StorageMetrics {
9697
fast_field_cache: CacheMetrics::for_component("fastfields"),
98+
datafusion_parquet_range_cache: CacheMetrics::for_component("datafusion_parquet_range"),
9799
fd_cache_metrics: CacheMetrics::for_component("fd"),
98100
partial_request_cache: CacheMetrics::for_component("partial_request"),
99101
predicate_cache: CacheMetrics::for_component("predicate"),

0 commit comments

Comments
 (0)