1use std::sync::Arc;
10
11use async_trait::async_trait;
12use khive_db::stores::blob_s3::{S3BlobStore, S3BlobStoreConfig};
13use khive_db::{SqliteError, StorageBackend};
14use khive_storage::{
15 BlobOrphanSweepConfig, BlobOrphanSweepResult, BlobStore, ContentRef, SqlAccess,
16 StorageCapability, StorageError, StorageResult, UploadId,
17};
18
19use crate::engine_config::BlobConfig;
20use crate::{KhiveConfig, RuntimeError, RuntimeResult};
21
22pub const DEFAULT_BLOB_HYDRATION_BYTES: u64 = 4 * khive_storage::MAX_BLOB_WHOLE_BYTES;
24
25#[derive(Debug)]
27pub struct BlobHydrator {
28 store: Arc<dyn BlobStore>,
29 raw_store: Arc<dyn BlobStore>,
33 read_only: bool,
39 governed: bool,
46 admission: Arc<tokio::sync::Semaphore>,
47 budget_bytes: u64,
48}
49
50#[derive(Debug)]
55pub enum GovernedBlobError {
56 Resolve(SqliteError),
58 Construct(RuntimeError),
60}
61
62impl std::fmt::Display for GovernedBlobError {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 Self::Resolve(error) => write!(f, "blob store resolution: {error}"),
66 Self::Construct(error) => write!(f, "blob hydrator construction: {error}"),
67 }
68 }
69}
70
71impl std::error::Error for GovernedBlobError {}
72
73impl BlobHydrator {
74 pub(crate) fn new_read_only(
79 store: Arc<dyn BlobStore>,
80 budget_bytes: u64,
81 ) -> RuntimeResult<Self> {
82 let mut hydrator =
83 Self::new_inner(wrap_read_only(Arc::clone(&store)), store, budget_bytes)?;
84 hydrator.read_only = true;
85 Ok(hydrator)
86 }
87
88 pub(crate) fn enforces_read_only(&self) -> bool {
90 self.read_only
91 }
92
93 pub(crate) fn raw_store(&self) -> Arc<dyn BlobStore> {
95 Arc::clone(&self.raw_store)
96 }
97
98 pub fn for_mode(
107 store: Arc<dyn BlobStore>,
108 budget_bytes: u64,
109 read_only: bool,
110 ) -> RuntimeResult<Self> {
111 if read_only {
112 Self::new_read_only(store, budget_bytes)
113 } else {
114 Self::new(store, budget_bytes)
115 }
116 }
117
118 pub fn resolve_for_governing_backend(
141 cfg: &KhiveConfig,
142 resolve_backend: &StorageBackend,
143 governing_backend: &StorageBackend,
144 budget_bytes: u64,
145 ) -> Result<Self, GovernedBlobError> {
146 let read_only = governing_backend.is_read_only();
147 let store = resolve_blob_store_for_mode(cfg, resolve_backend, read_only)
148 .map_err(GovernedBlobError::Resolve)?;
149 let mut hydrator =
150 Self::for_mode(store, budget_bytes, read_only).map_err(GovernedBlobError::Construct)?;
151 hydrator.governed = true;
152 Ok(hydrator)
153 }
154
155 pub(crate) fn is_governed(&self) -> bool {
158 self.governed
159 }
160
161 pub fn new(store: Arc<dyn BlobStore>, budget_bytes: u64) -> RuntimeResult<Self> {
163 let raw = Arc::clone(&store);
164 Self::new_inner(store, raw, budget_bytes)
165 }
166
167 fn new_inner(
168 store: Arc<dyn BlobStore>,
169 raw_store: Arc<dyn BlobStore>,
170 budget_bytes: u64,
171 ) -> RuntimeResult<Self> {
172 if budget_bytes < khive_storage::MAX_BLOB_WHOLE_BYTES {
173 return Err(RuntimeError::InvalidInput(format!(
174 "blob hydration budget must be at least {} bytes, got {budget_bytes}",
175 khive_storage::MAX_BLOB_WHOLE_BYTES
176 )));
177 }
178 let permits = usize::try_from(budget_bytes).map_err(|_| {
179 RuntimeError::InvalidInput(format!(
180 "blob hydration budget {budget_bytes} does not fit this platform"
181 ))
182 })?;
183 if permits > tokio::sync::Semaphore::MAX_PERMITS {
184 return Err(RuntimeError::InvalidInput(format!(
185 "blob hydration budget {budget_bytes} exceeds the runtime maximum {}",
186 tokio::sync::Semaphore::MAX_PERMITS
187 )));
188 }
189 Ok(Self {
190 store,
191 raw_store,
192 read_only: false,
193 governed: false,
194 admission: Arc::new(tokio::sync::Semaphore::new(permits)),
195 budget_bytes,
196 })
197 }
198
199 pub fn budget_bytes(&self) -> u64 {
201 self.budget_bytes
202 }
203
204 pub(crate) fn store(&self) -> Arc<dyn BlobStore> {
208 Arc::clone(&self.store)
209 }
210
211 pub async fn hydrate_verified(
213 &self,
214 content_ref: &ContentRef,
215 max_bytes: u64,
216 ) -> RuntimeResult<VerifiedBlob> {
217 if max_bytes > khive_storage::MAX_BLOB_WHOLE_BYTES {
218 return Err(RuntimeError::InvalidInput(format!(
219 "blob hydration max_bytes must not exceed {} bytes, got {max_bytes}",
220 khive_storage::MAX_BLOB_WHOLE_BYTES
221 )));
222 }
223
224 let acquire = Arc::clone(&self.admission).acquire_many_owned(max_bytes as u32);
225 let admission =
226 khive_storage::await_request_read_phase("blob_hydration_admission", acquire)
227 .await?
228 .map_err(|error| {
229 RuntimeError::Internal(format!("blob hydration admission closed: {error}"))
230 })?;
231
232 let (sender, receiver) = tokio::sync::oneshot::channel::<RuntimeResult<VerifiedBlob>>();
233 let store = Arc::clone(&self.store);
234 let content_ref = content_ref.clone();
235 crate::track_named_background_task("blob_hydration", async move {
236 let result = match store.get_bounded_verified(&content_ref, max_bytes).await {
240 Ok(bytes) => Ok(VerifiedBlob {
241 bytes,
242 _admission: admission,
243 }),
244 Err(error) => Err(RuntimeError::Storage(error)),
245 };
246 let _ = sender.send(result);
247 });
248
249 khive_storage::await_request_read_phase("blob_hydration", receiver)
250 .await?
251 .map_err(|_| {
252 RuntimeError::Internal(
253 "blob hydration supervisor ended without delivering a result".to_string(),
254 )
255 })?
256 }
257}
258
259pub struct VerifiedBlob {
288 bytes: Vec<u8>,
289 _admission: tokio::sync::OwnedSemaphorePermit,
290}
291
292impl VerifiedBlob {
293 pub fn bytes(&self) -> &[u8] {
295 &self.bytes
296 }
297}
298
299impl AsRef<[u8]> for VerifiedBlob {
300 fn as_ref(&self) -> &[u8] {
301 self.bytes()
302 }
303}
304
305impl std::fmt::Debug for VerifiedBlob {
306 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307 formatter
308 .debug_struct("VerifiedBlob")
309 .field("len", &self.bytes.len())
310 .finish_non_exhaustive()
311 }
312}
313
314pub fn resolve_blob_store(
323 cfg: &KhiveConfig,
324 backend: &StorageBackend,
325) -> Result<Arc<dyn BlobStore>, SqliteError> {
326 match &cfg.storage.blob {
327 None => backend.blob_store(None, None),
328 Some(BlobConfig::Fs { root, floor_bytes }) => {
329 let root_path = root.as_ref().map(std::path::PathBuf::from);
330 backend.blob_store(root_path.as_deref(), *floor_bytes)
331 }
332 Some(BlobConfig::S3 {
333 bucket,
334 region,
335 endpoint,
336 prefix,
337 allow_http,
338 }) => {
339 let mut s3_cfg = S3BlobStoreConfig::new(bucket.clone(), region.clone());
340 if let Some(endpoint) = endpoint {
341 s3_cfg = s3_cfg.with_endpoint(endpoint.clone());
342 }
343 if let Some(prefix) = prefix {
344 s3_cfg = s3_cfg.with_prefix(prefix.clone());
345 }
346 if let Some(allow_http) = allow_http {
347 s3_cfg = s3_cfg.with_allow_http(*allow_http);
348 }
349 let store = S3BlobStore::new(s3_cfg)?;
350 Ok(Arc::new(store))
351 }
352 }
353}
354
355pub fn resolve_blob_store_for_mode(
363 cfg: &KhiveConfig,
364 backend: &StorageBackend,
365 read_only: bool,
366) -> Result<Arc<dyn BlobStore>, SqliteError> {
367 if !read_only {
368 return resolve_blob_store(cfg, backend);
369 }
370
371 let inner: Arc<dyn BlobStore> = match &cfg.storage.blob {
372 None => backend.blob_store_read_only(None, None)?,
373 Some(BlobConfig::Fs { root, floor_bytes }) => {
374 let root_path = root.as_ref().map(std::path::PathBuf::from);
375 backend.blob_store_read_only(root_path.as_deref(), *floor_bytes)?
376 }
377 Some(BlobConfig::S3 {
378 bucket,
379 region,
380 endpoint,
381 prefix,
382 allow_http,
383 }) => {
384 let mut s3_cfg = S3BlobStoreConfig::new(bucket.clone(), region.clone());
385 if let Some(endpoint) = endpoint {
386 s3_cfg = s3_cfg.with_endpoint(endpoint.clone());
387 }
388 if let Some(prefix) = prefix {
389 s3_cfg = s3_cfg.with_prefix(prefix.clone());
390 }
391 if let Some(allow_http) = allow_http {
392 s3_cfg = s3_cfg.with_allow_http(*allow_http);
393 }
394 Arc::new(S3BlobStore::new(s3_cfg)?)
395 }
396 };
397 Ok(Arc::new(ReadOnlyBlobStore { inner }))
398}
399
400pub(crate) fn wrap_read_only(inner: Arc<dyn BlobStore>) -> Arc<dyn BlobStore> {
406 Arc::new(ReadOnlyBlobStore { inner })
407}
408
409#[derive(Debug)]
410struct ReadOnlyBlobStore {
411 inner: Arc<dyn BlobStore>,
412}
413
414impl ReadOnlyBlobStore {
415 fn mutation_error(operation: &'static str) -> StorageError {
416 StorageError::Unsupported {
417 capability: StorageCapability::Blob,
418 operation: operation.into(),
419 message: "blob storage is read-only for this pack runtime".to_string(),
420 }
421 }
422}
423
424#[async_trait]
425impl BlobStore for ReadOnlyBlobStore {
426 async fn put(&self, _bytes: Vec<u8>) -> StorageResult<ContentRef> {
427 Err(Self::mutation_error("put"))
428 }
429
430 fn upload_lease_idle_cap(&self) -> Option<std::time::Duration> {
431 self.inner.upload_lease_idle_cap()
432 }
433
434 async fn begin_upload_with_lease(
435 &self,
436 _size: u64,
437 _config: khive_storage::blob::UploadLeaseConfig,
438 ) -> StorageResult<UploadId> {
439 Err(Self::mutation_error("begin_upload_with_lease"))
440 }
441
442 async fn renew_upload(&self, _id: &UploadId) -> StorageResult<()> {
443 Err(Self::mutation_error("renew_upload"))
444 }
445
446 async fn begin_upload(&self, _declared_size: u64) -> StorageResult<UploadId> {
447 Err(Self::mutation_error("begin_upload"))
448 }
449
450 async fn append_part(&self, _id: &UploadId, _bytes: Vec<u8>) -> StorageResult<u64> {
451 Err(Self::mutation_error("append_part"))
452 }
453
454 async fn commit_upload(&self, _id: &UploadId, _content_ref: &ContentRef) -> StorageResult<()> {
455 Err(Self::mutation_error("commit_upload"))
456 }
457
458 async fn abort_upload(&self, _id: &UploadId) -> StorageResult<()> {
459 Err(Self::mutation_error("abort_upload"))
460 }
461
462 async fn sweep_uploads(&self, _idle_for: std::time::Duration) -> StorageResult<u64> {
463 Err(Self::mutation_error("sweep_uploads"))
464 }
465
466 async fn get_bounded_verified(
467 &self,
468 content_ref: &ContentRef,
469 max_bytes: u64,
470 ) -> StorageResult<Vec<u8>> {
471 self.inner
472 .get_bounded_verified(content_ref, max_bytes)
473 .await
474 }
475
476 async fn exists(&self, content_ref: &ContentRef) -> StorageResult<bool> {
477 self.inner.exists(content_ref).await
478 }
479
480 async fn size(&self, content_ref: &ContentRef) -> StorageResult<Option<u64>> {
481 self.inner.size(content_ref).await
482 }
483
484 async fn delete(&self, _content_ref: &ContentRef) -> StorageResult<bool> {
485 Err(Self::mutation_error("delete"))
486 }
487
488 async fn orphan_sweep(
489 &self,
490 _config: &BlobOrphanSweepConfig,
491 ) -> StorageResult<BlobOrphanSweepResult> {
492 Err(Self::mutation_error("orphan_sweep"))
493 }
494
495 async fn transactional_orphan_sweep(
496 &self,
497 _sql: &dyn SqlAccess,
498 _dry_run: bool,
499 ) -> StorageResult<BlobOrphanSweepResult> {
500 Err(Self::mutation_error("transactional_orphan_sweep"))
501 }
502}
503
504#[cfg(test)]
505#[path = "blob_tests.rs"]
506mod tests;