Skip to main content

query_runtime/runtime/context/
lakehouse.rs

1//! Lakehouse cache-fill and object-store accessors for execution contexts.
2
3use std::{
4    future::Future,
5    sync,
6    sync::Arc,
7    time::{Duration, Instant},
8};
9
10use ::object_store::ObjectStoreExt;
11#[cfg(any(test, feature = "test-hooks"))]
12use bytes::Bytes;
13use object_store::{aws::AwsCredential, path::Path as ObjectStorePath};
14use query_planning::{
15    error::{QueryError, QueryErrorCode, Result},
16    source::redacted_object_store_path_diagnostic,
17};
18use tokio::{runtime::Runtime as TokioRuntime, task::JoinHandle};
19
20use super::{
21    super::{
22        CacheFillByteBudget, CacheFillBytePermit, SourceStorageProfile,
23        cache_fill::CacheFillOperationPermit,
24        object_store::{ObjectStoreCustomFetchResult, ObjectStoreFetchResult, S3ObjectStoreKey},
25    },
26    ExecutionContext, QueryServiceStreamPool,
27};
28
29impl ExecutionContext {
30    /// Returns the service-level CUDA stream pool used by source-read workers.
31    ///
32    /// The pool is wrapped with this context's service epoch and query anchor,
33    /// so a lease taken through it is revoked with the attempt and keeps the
34    /// service from reaching idle. A standalone context wraps neither.
35    pub fn service_stream_pool(&self) -> QueryServiceStreamPool {
36        QueryServiceStreamPool {
37            pool: self.device.service_stream_pool.clone(),
38            service_epoch: self.admitted().map(|attempt| attempt.service_epoch.clone()),
39            query_anchor: self.plan.query_anchor.clone(),
40        }
41    }
42
43    /// Timeout used by blocking lakehouse cache maintenance resource acquisition.
44    pub fn lakehouse_cache_fill_acquire_timeout(&self) -> Duration {
45        self.device.lakehouse_cache_fill_config.acquire_timeout
46    }
47
48    /// Returns the service-wide background lakehouse cache-fill byte budget.
49    pub fn lakehouse_cache_fill_budget(&self) -> CacheFillByteBudget {
50        self.device.lakehouse_cache_fill_budget.clone()
51    }
52
53    /// Maximum background lakehouse cache-fill operations admitted concurrently.
54    #[cfg(test)]
55    pub(crate) fn lakehouse_cache_fill_max_concurrent_ops(&self) -> usize {
56        self.device.lakehouse_cache_fill_concurrency.max_concurrent_ops()
57    }
58
59    #[doc(hidden)]
60    pub fn acquire_lakehouse_cache_fill_operation_with_timeout(
61        &self,
62        timeout: Duration,
63        operation: &'static str,
64    ) -> Result<Option<CacheFillOperationPermit>> {
65        self.device.lakehouse_cache_fill_concurrency.acquire_with_timeout(self.cancellation(), timeout, operation)
66    }
67
68    #[doc(hidden)]
69    pub fn acquire_lakehouse_cache_fill_budget_with_timeout(
70        &self,
71        bytes: u64,
72        timeout: Duration,
73        operation: &'static str,
74    ) -> Result<Option<CacheFillBytePermit>> {
75        self.device.lakehouse_cache_fill_budget.acquire_with_timeout(self.cancellation(), bytes, timeout, operation)
76    }
77
78    #[doc(hidden)]
79    pub fn s3_object_store(&self, key: S3ObjectStoreKey) -> Result<Arc<dyn ::object_store::ObjectStore>> {
80        self.device.source_object_stores.s3_store(key)
81    }
82
83    #[doc(hidden)]
84    pub fn source_storage_profile(&self, id: &str) -> Result<&SourceStorageProfile> {
85        self.device.source_object_stores.profile(id)
86    }
87
88    /// Resolves default-chain AWS credentials from the object-store provider.
89    ///
90    /// Remote KvikIO reads use this to pass resolved identity into lower layers
91    /// that lack a default chain. Recent results are reused briefly; on a cache
92    /// miss, resolution runs on the object-store control runtime while this call
93    /// blocks for the result.
94    #[doc(hidden)]
95    pub fn resolve_s3_default_chain_credential(
96        &self,
97        key: S3ObjectStoreKey,
98        operation: &'static str,
99    ) -> Result<Arc<AwsCredential>> {
100        if let Some(credential) = self.device.source_object_stores.recently_resolved_s3_credential(&key) {
101            return Ok(credential);
102        }
103        let storage_profile_id = key.storage_profile_id.clone();
104        let provider = self.device.source_object_stores.s3_credential_provider(key.clone())?;
105        // The receive below blocks this thread. The control runtime is a
106        // separate multi-thread runtime, so a GPU pool worker or host thread
107        // can wait here without holding up the task that resolves the
108        // credential.
109        let (sender, receiver) = sync::mpsc::channel();
110        self.device.object_store_control_runtime.spawn(async move {
111            let credential = provider.get_credential().await;
112            let _ = sender.send(credential);
113        });
114        let credential_unavailable = |query_error: QueryError| {
115            let query_error = query_error
116                .with_fact("resource", "remote_parquet_source")
117                .with_fact("credential_source", "default_chain")
118                .with_fact("reason", "default_chain_credentials_unavailable");
119            match storage_profile_id.as_deref() {
120                Some(profile_id) => query_error.with_fact("storage_profile_id", profile_id),
121                None => query_error,
122            }
123        };
124        let credential = receiver.recv().map_err(|_error| {
125            credential_unavailable(QueryError::new(
126                QueryErrorCode::Internal,
127                operation,
128                "source credential resolution worker stopped before returning a result",
129            ))
130        })?;
131        let credential = credential.map_err(|error| {
132            credential_unavailable(
133                QueryError::new(
134                    QueryErrorCode::CudfDependencyFailure,
135                    operation,
136                    "failed to resolve AWS default-chain credentials for native remote-direct read",
137                )
138                .with_source(error),
139            )
140        })?;
141        self.device.source_object_stores.record_resolved_s3_credential(key, Arc::clone(&credential));
142        Ok(credential)
143    }
144
145    #[doc(hidden)]
146    pub fn spawn_cache_maintenance_fetch(
147        &self,
148        store: Arc<dyn ::object_store::ObjectStore>,
149        location: ObjectStorePath,
150        on_result: impl FnOnce(ObjectStoreFetchResult) + Send + 'static,
151    ) {
152        Self::spawn_cache_maintenance_fetch_on_runtime(
153            self.device.object_store_control_runtime.as_ref(),
154            store,
155            location,
156            on_result,
157        )
158    }
159
160    /// Spawns caller-provided object-store control-plane work.
161    ///
162    /// The caller composes any combination of `get_range` / `get_ranges` calls
163    /// inside the future. Elapsed time covers the entire future, including any
164    /// rewrite work the caller performs inside it.
165    #[doc(hidden)]
166    pub fn spawn_object_store_control_task<F, Fut, T>(
167        &self,
168        future_factory: F,
169        on_result: impl FnOnce(ObjectStoreCustomFetchResult<T>) + Send + 'static,
170    ) -> JoinHandle<()>
171    where
172        F: FnOnce() -> Fut + Send + 'static,
173        Fut: Future<Output = Result<T>> + Send + 'static,
174        T: Send + 'static,
175    {
176        self.device.object_store_control_runtime.spawn(async move {
177            let timer = Instant::now();
178            let value = future_factory().await;
179            on_result(ObjectStoreCustomFetchResult { value, elapsed: timer.elapsed() });
180        })
181    }
182
183    /// Spawns caller-provided asynchronous lakehouse cache maintenance.
184    #[doc(hidden)]
185    pub fn spawn_lakehouse_cache_maintenance<F, Fut, T>(
186        &self,
187        future_factory: F,
188        on_result: impl FnOnce(ObjectStoreCustomFetchResult<T>) + Send + 'static,
189    ) -> JoinHandle<()>
190    where
191        F: FnOnce() -> Fut + Send + 'static,
192        Fut: Future<Output = Result<T>> + Send + 'static,
193        T: Send + 'static,
194    {
195        self.device.object_store_control_runtime.spawn(async move {
196            let timer = Instant::now();
197            let value = future_factory().await;
198            on_result(ObjectStoreCustomFetchResult { value, elapsed: timer.elapsed() });
199        })
200    }
201
202    #[cfg(any(test, feature = "test-hooks"))]
203    #[cfg_attr(not(test), expect(dead_code, reason = "used by in-crate runtime source tests"))]
204    pub(in crate::runtime) fn fetch_object_store_bytes_on_runtime(
205        object_store_control_runtime: &TokioRuntime,
206        store: Arc<dyn ::object_store::ObjectStore>,
207        location: ObjectStorePath,
208    ) -> Result<Bytes> {
209        let (sender, receiver) = sync::mpsc::channel();
210        Self::spawn_cache_maintenance_fetch_on_runtime(object_store_control_runtime, store, location, move |result| {
211            let _ = sender.send(result.bytes);
212        });
213        receiver.recv().map_err(|error| {
214            QueryError::new(
215                QueryErrorCode::Internal,
216                "query_engine.lakehouse_cache.maintenance.receive",
217                "object-store cache maintenance worker stopped before returning a result",
218            )
219            .with_source(error)
220        })?
221    }
222
223    fn spawn_cache_maintenance_fetch_on_runtime(
224        object_store_control_runtime: &TokioRuntime,
225        store: Arc<dyn ::object_store::ObjectStore>,
226        location: ObjectStorePath,
227        on_result: impl FnOnce(ObjectStoreFetchResult) + Send + 'static,
228    ) {
229        object_store_control_runtime.spawn(async move {
230            let path_fact = redacted_object_store_path_diagnostic(location.as_ref());
231            let bytes = async move {
232                let result = store.get(&location).await?;
233                result.bytes().await
234            }
235            .await
236            .map_err(|_error| {
237                QueryError::new(
238                    QueryErrorCode::CudfDependencyFailure,
239                    "query_engine.lakehouse_cache.maintenance.object_store",
240                    "failed to fetch lakehouse cache object through object_store",
241                )
242                .with_fact("path", path_fact)
243            });
244            on_result(ObjectStoreFetchResult { bytes });
245        });
246    }
247}