Skip to main content

trine_kv/
object_store.rs

1//! Provider-agnostic object-store client and an in-memory fake.
2//!
3//! This is the seam the (planned) object-storage backend is written against —
4//! see `docs/object-storage-backend.md`. Real providers (S3 and compatible)
5//! implement [`ObjectClient`]; the [`InMemoryObjectStore`] here reproduces the
6//! semantics that matter — whole-object `put`/`get`, range reads, listing by
7//! prefix, idempotent delete, and **conditional writes with `ETags`** — so the
8//! backend's harder pieces (segmented WAL, manifest CAS, writer-lease fencing)
9//! can be built and tested deterministically with no cloud dependency.
10//!
11//! ETag/conditional-write semantics mirror object stores: every store assigns a
12//! fresh `ETag`, `IfNoneMatch` creates only when absent, and `IfMatch` stores only
13//! when the current `ETag` matches. A failed precondition reports the current
14//! `ETag` so a compare-and-swap caller (the manifest commit) can retry.
15//!
16//! This trait is the public "bring your own object store" seam: implement
17//! [`ObjectClient`] for your provider (S3 and compatible) and open a database
18//! with [`crate::Db::open_object_store`]. The crate's manifest commit and remote
19//! WAL head rely on `put_if` providing a real conditional write
20//! (compare-and-swap); a backend that cannot honor `If-None-Match` /
21//! `If-Match` is unsafe for concurrent writers. After a successful conditional
22//! write, later `get`/`head` calls for that same key must observe that version
23//! or a newer one. Recovery and read-only refresh follow the lease/head and
24//! manifest keys directly; object listing is used for cleanup, so eventually
25//! consistent listings may delay garbage collection but must not define
26//! committed state.
27
28use std::{
29    collections::BTreeMap,
30    future::Future,
31    path::{Path, PathBuf},
32    pin::Pin,
33    sync::{
34        Arc, Mutex, MutexGuard,
35        atomic::{AtomicU64, Ordering},
36    },
37    time::{SystemTime, UNIX_EPOCH},
38};
39
40use crate::error::{Error, Result};
41use crate::options::DurabilityMode;
42use crate::storage::{
43    StorageCapabilities, StorageFuture, StorageObjectDeleteBackend, StorageObjectId,
44    StorageObjectListBackend, StorageObjectListRequest, StorageObjectReadBackend,
45    StorageObjectWriteBackend, StorageReadBackend, StorageReadFuture, StorageReadObject,
46    ensure_whole_object_read_len,
47};
48
49/// Boxed future returned by [`ObjectClient`] methods. Mirrors the storage
50/// layer's `StorageFuture`: object stores are used through `dyn`, so the async
51/// methods return a boxed future rather than `async fn`. The `Send` bound is
52/// dropped only on the single-threaded wasm target, matching `StorageFuture`.
53#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
54pub type ObjectFuture<'op, T> = Pin<Box<dyn Future<Output = Result<T>> + 'op>>;
55
56/// See the wasm variant above.
57#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
58pub type ObjectFuture<'op, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'op>>;
59
60/// An opaque entity tag identifying a specific stored version of an object. A
61/// new value is minted on every store, so an unchanged `ETag` means the object
62/// has not been overwritten since it was observed.
63#[derive(Debug, Clone, PartialEq, Eq, Hash)]
64pub struct ETag(Arc<str>);
65
66impl ETag {
67    /// Wraps a provider's entity-tag string (e.g. an S3 `ETag` response header).
68    #[must_use]
69    pub fn new(tag: impl Into<Arc<str>>) -> Self {
70        Self(tag.into())
71    }
72
73    /// The underlying entity-tag string.
74    #[must_use]
75    pub fn as_str(&self) -> &str {
76        &self.0
77    }
78}
79
80/// Precondition for a conditional write ([`ObjectClient::put_if`]).
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum Precondition {
83    /// Store only if the object does not exist (create). `If-None-Match: *`.
84    IfNoneMatch,
85    /// Store only if the object exists and its current `ETag` equals this one
86    /// (compare-and-swap). `If-Match: <etag>`.
87    IfMatch(ETag),
88}
89
90/// Outcome of a conditional write. A failed precondition is **not** an error:
91/// it is the expected, retryable result of losing a compare-and-swap race, and
92/// carries the current `ETag` (or `None` when the object is absent) so the caller
93/// can re-read and retry.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum PutIf {
96    /// The write was applied; the object now has this `ETag`.
97    Stored {
98        /// The new entity tag of the stored object.
99        etag: ETag,
100    },
101    /// The precondition did not hold; the object was left unchanged.
102    PreconditionFailed {
103        /// The object's current entity tag, or `None` if it does not exist.
104        current: Option<ETag>,
105    },
106}
107
108/// Metadata for one object returned by [`ObjectClient::list`] / [`ObjectClient::head`].
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct ObjectMeta {
111    /// Object key.
112    pub key: String,
113    /// Object size in bytes.
114    pub size: u64,
115    /// Current entity tag.
116    pub etag: ETag,
117}
118
119/// A flat key/value object store: keys are strings, values are immutable byte
120/// blobs with an `ETag`. All methods are async (real providers are network I/O);
121/// the in-memory fake completes synchronously.
122///
123/// Contract the backend relies on:
124/// - `put` always stores and returns a fresh `ETag` (overwrites bump the `ETag`).
125/// - `get` returns `None` for an absent key; `get_range` errors for an absent
126///   key or an out-of-bounds range.
127/// - After `put`, or after a `put_if` returns [`PutIf::Stored`], later `get` and
128///   `head` calls for the same key observe that object version or a newer one.
129/// - `delete` is idempotent (deleting an absent key succeeds).
130/// - `list` returns objects whose key starts with the prefix, in key order.
131///   Listing drives orphan cleanup only; recovery and read-only refresh do not
132///   infer committed state from a listing result.
133/// - `head` returns an object's metadata (size + `ETag`) without its bytes, or
134///   `None` when the key is absent (like S3 `HEAD`).
135/// - `put_if` applies the write only when the precondition holds, otherwise
136///   reports [`PutIf::PreconditionFailed`] with the current `ETag`.
137///
138/// Object-store opens trust this contract by default. That keeps open cheap and
139/// predictable, but it also means a faulty adapter can pass open and fail only
140/// later when WAL, lease, manifest, or recovery checks observe the bad behavior.
141/// Run [`verify_object_client_contract`] in CI, process startup, or a deployment
142/// health check before trusting a custom adapter in production. If you need open
143/// itself to fail closed while developing or diagnosing an adapter, configure
144/// [`ObjectClientTrustMode::VerifyOnOpen`](crate::ObjectClientTrustMode).
145///
146/// # WAL durability sink and split tiers
147///
148/// A database opened with [`Db::open_object_store_at`](crate::Db::open_object_store_at)
149/// writes everything through one client — `SSTable` segments, blobs, the
150/// manifest CAS, the writer lease, and the **write-ahead log** — and a commit is
151/// acknowledged only after its WAL bytes and WAL head are durable.
152///
153/// A database opened with
154/// [`Db::open_object_store_with_wal_at`](crate::Db::open_object_store_with_wal_at)
155/// splits that responsibility: the storage client stores bulk objects and the
156/// manifest, while the WAL client stores the writer lease, remote WAL head, and
157/// WAL segments. The WAL client is then the commit-latency and commit-durability
158/// sink; the storage client remains the long-term table/blob tier.
159///
160/// Because `Arc<C>` is itself an `ObjectClient`, one client can be **shared
161/// across many open databases**. A higher layer (e.g. a multi-tenant service)
162/// can either provide an explicit WAL client through the split-tier open API or
163/// supply a custom shared client that recognizes WAL writes with
164/// [`is_wal_object_key`](crate::is_wal_object_key) and coalesces them across
165/// databases. In both forms, the client handling WAL keys must provide the
166/// conditional-write and same-key visibility guarantees described above.
167pub trait ObjectClient: Send + Sync {
168    /// Reads the whole object, or `None` when the key is absent.
169    fn get<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<Arc<[u8]>>>;
170
171    /// Reads `len` bytes starting at `offset`; errors for an absent key or an
172    /// out-of-bounds range.
173    fn get_range<'op>(&'op self, key: &str, offset: u64, len: u64) -> ObjectFuture<'op, Arc<[u8]>>;
174
175    /// Stores the object unconditionally, returning its new `ETag`.
176    fn put<'op>(&'op self, key: &str, bytes: Arc<[u8]>) -> ObjectFuture<'op, ETag>;
177
178    /// Deletes the object (idempotent: deleting an absent key succeeds).
179    fn delete<'op>(&'op self, key: &str) -> ObjectFuture<'op, ()>;
180
181    /// Lists objects whose key starts with `prefix`, in key order.
182    fn list<'op>(&'op self, prefix: &str) -> ObjectFuture<'op, Vec<ObjectMeta>>;
183
184    /// Returns the object's metadata (size + `ETag`) without its bytes, or `None`
185    /// when the key is absent (like S3 `HEAD`).
186    fn head<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<ObjectMeta>>;
187
188    /// Conditional write (compare-and-swap): stores `bytes` only if `precondition`
189    /// holds, otherwise reports [`PutIf::PreconditionFailed`] with the current
190    /// `ETag`. This is the manifest commit point; it must be a real CAS.
191    fn put_if<'op>(
192        &'op self,
193        key: &str,
194        bytes: Arc<[u8]>,
195        precondition: Precondition,
196    ) -> ObjectFuture<'op, PutIf>;
197}
198
199static OBJECT_CLIENT_CONTRACT_PROBE_COUNTER: AtomicU64 = AtomicU64::new(0);
200
201/// Verifies the same-key semantics Trine requires from an [`ObjectClient`].
202///
203/// This is a health-check helper for object-store adapters. It writes, reads,
204/// conditionally overwrites, and deletes one temporary probe object under
205/// `prefix`, then returns [`Ok(())`](std::result::Result::Ok) only if the client
206/// behaves like a real compare-and-swap object store for that key.
207///
208/// # What This Checks
209///
210/// The probe verifies:
211///
212/// - `put` stores bytes and returns an `ETag` visible through `head` and `get`;
213/// - `put_if(..., IfNoneMatch)` refuses to overwrite an existing object;
214/// - `put_if(..., IfMatch(wrong_etag))` refuses to overwrite;
215/// - `put_if(..., IfMatch(current_etag))` stores and returns a new `ETag`;
216/// - same-key `head` and `get` observe the bytes from the successful write.
217///
218/// # Limits
219///
220/// This is not a proof that every future request will be correct. It validates
221/// one temporary key at one moment. Use it before deployment, during process
222/// startup, or as a service health check; Trine's open path defaults to trusting
223/// the client so normal opens do not pay these extra object-store requests.
224///
225/// # Parameters
226///
227/// - `client`: object-store adapter to validate.
228/// - `prefix`: object-key prefix where the temporary probe object may be
229///   created. The function appends a unique hidden key and deletes it before
230///   returning. The caller must grant read, write, conditional-write, metadata,
231///   and delete permissions for this prefix.
232///
233/// # Errors
234///
235/// Returns any error from the client. Returns [`Error::Corruption`] when the
236/// observed behavior violates Trine's required object-store semantics. If the
237/// final cleanup delete fails after a successful probe, that cleanup error is
238/// returned so the caller can treat the health check as failed.
239pub async fn verify_object_client_contract(
240    client: Arc<dyn ObjectClient>,
241    prefix: impl Into<String>,
242) -> Result<()> {
243    let key = object_client_contract_probe_key(Path::new(&prefix.into()))?;
244    let result = verify_object_client_contract_at_key(&client, &key).await;
245    let cleanup = client.delete(&key).await;
246    match (result, cleanup) {
247        (Ok(()), Ok(())) => Ok(()),
248        (Err(error), _) | (Ok(()), Err(error)) => Err(error),
249    }
250}
251
252pub(crate) async fn verify_object_client_contract_for_open(
253    client: &Arc<dyn ObjectClient>,
254    db_path: &Path,
255    role: &str,
256) -> Result<()> {
257    let key = object_client_contract_probe_key_for_role(db_path, role)?;
258    let result = verify_object_client_contract_at_key(client, &key).await;
259    let cleanup = client.delete(&key).await;
260    match (result, cleanup) {
261        (Ok(()), Ok(())) => Ok(()),
262        (Err(error), _) | (Ok(()), Err(error)) => Err(error),
263    }
264}
265
266async fn verify_object_client_contract_at_key(
267    client: &Arc<dyn ObjectClient>,
268    key: &str,
269) -> Result<()> {
270    client.delete(key).await?;
271
272    let first = Arc::<[u8]>::from(b"trine-object-client-contract:first".as_slice());
273    let second = Arc::<[u8]>::from(b"trine-object-client-contract:second".as_slice());
274    let first_etag = client.put(key, Arc::clone(&first)).await?;
275    verify_object_client_observed_bytes(client, key, &first, &first_etag, "put").await?;
276
277    match client
278        .put_if(key, Arc::clone(&second), Precondition::IfNoneMatch)
279        .await?
280    {
281        PutIf::PreconditionFailed { current } if current.as_ref() == Some(&first_etag) => {}
282        PutIf::PreconditionFailed { current } => {
283            return Err(Error::Corruption {
284                message: format!(
285                    "object client contract probe for {key} returned wrong IfNoneMatch ETag: {current:?}"
286                ),
287            });
288        }
289        PutIf::Stored { .. } => {
290            return Err(Error::Corruption {
291                message: format!(
292                    "object client contract probe for {key} stored despite IfNoneMatch on an existing object"
293                ),
294            });
295        }
296    }
297
298    let mismatched = ETag::new("trine-object-client-contract-mismatch");
299    match client
300        .put_if(key, Arc::clone(&second), Precondition::IfMatch(mismatched))
301        .await?
302    {
303        PutIf::PreconditionFailed { .. } => {}
304        PutIf::Stored { .. } => {
305            return Err(Error::Corruption {
306                message: format!(
307                    "object client contract probe for {key} stored despite a mismatched IfMatch ETag"
308                ),
309            });
310        }
311    }
312
313    let second_etag = match client
314        .put_if(
315            key,
316            Arc::clone(&second),
317            Precondition::IfMatch(first_etag.clone()),
318        )
319        .await?
320    {
321        PutIf::Stored { etag } => etag,
322        PutIf::PreconditionFailed { current } => {
323            return Err(Error::Corruption {
324                message: format!(
325                    "object client contract probe for {key} rejected a matching IfMatch ETag: {current:?}"
326                ),
327            });
328        }
329    };
330    if second_etag == first_etag {
331        return Err(Error::Corruption {
332            message: format!(
333                "object client contract probe for {key} reused an ETag after overwriting bytes"
334            ),
335        });
336    }
337    verify_object_client_observed_bytes(client, key, &second, &second_etag, "put_if").await
338}
339
340async fn verify_object_client_observed_bytes(
341    client: &Arc<dyn ObjectClient>,
342    key: &str,
343    expected: &Arc<[u8]>,
344    expected_etag: &ETag,
345    operation: &str,
346) -> Result<()> {
347    let head = client.head(key).await?.ok_or_else(|| Error::Corruption {
348        message: format!("object client contract probe for {key} lost head after {operation}"),
349    })?;
350    if &head.etag != expected_etag || head.size != expected.len() as u64 {
351        return Err(Error::Corruption {
352            message: format!(
353                "object client contract probe for {key} observed stale head after {operation}"
354            ),
355        });
356    }
357    let bytes = client.get(key).await?.ok_or_else(|| Error::Corruption {
358        message: format!("object client contract probe for {key} lost bytes after {operation}"),
359    })?;
360    if bytes.as_ref() != expected.as_ref() {
361        return Err(Error::Corruption {
362            message: format!(
363                "object client contract probe for {key} observed stale bytes after {operation}"
364            ),
365        });
366    }
367    Ok(())
368}
369
370fn object_client_contract_probe_key(prefix: &Path) -> Result<String> {
371    object_client_contract_probe_key_for_role(prefix, "health")
372}
373
374fn object_client_contract_probe_key_for_role(db_path: &Path, role: &str) -> Result<String> {
375    let now = SystemTime::now()
376        .duration_since(UNIX_EPOCH)
377        .map_err(|error| Error::Corruption {
378            message: format!("system clock is before UNIX_EPOCH: {error}"),
379        })?;
380    let counter = OBJECT_CLIENT_CONTRACT_PROBE_COUNTER.fetch_add(1, Ordering::Relaxed);
381    Ok(db_path
382        .join(format!(
383            ".trine-object-client-contract-{role}-{}-{counter}",
384            now.as_nanos()
385        ))
386        .to_string_lossy()
387        .into_owned())
388}
389
390/// One stored object: its bytes and current `ETag`.
391#[derive(Debug, Clone)]
392struct StoredObject {
393    bytes: Arc<[u8]>,
394    etag: ETag,
395}
396
397/// An in-memory [`ObjectClient`] with real `ETag` and conditional-write
398/// semantics, for building and testing the object-storage backend without a
399/// cloud dependency.
400#[derive(Debug, Default)]
401pub struct InMemoryObjectStore {
402    objects: Mutex<BTreeMap<String, StoredObject>>,
403    next_etag: AtomicU64,
404}
405
406impl InMemoryObjectStore {
407    /// Creates an empty in-memory object store.
408    #[must_use]
409    pub fn new() -> Self {
410        Self::default()
411    }
412
413    fn mint_etag(&self) -> ETag {
414        let value = self.next_etag.fetch_add(1, Ordering::Relaxed);
415        ETag(Arc::from(format!("etag-{value}")))
416    }
417
418    fn lock(&self) -> Result<MutexGuard<'_, BTreeMap<String, StoredObject>>> {
419        self.objects.lock().map_err(|_| Error::Corruption {
420            message: "in-memory object store lock poisoned".to_owned(),
421        })
422    }
423
424    fn get_inner(&self, key: &str) -> Result<Option<Arc<[u8]>>> {
425        Ok(self
426            .lock()?
427            .get(key)
428            .map(|object| Arc::clone(&object.bytes)))
429    }
430
431    fn get_range_inner(&self, key: &str, offset: u64, len: u64) -> Result<Arc<[u8]>> {
432        let objects = self.lock()?;
433        let object = objects.get(key).ok_or_else(|| Error::Corruption {
434            message: format!("object {key} not found for range read"),
435        })?;
436        let offset = usize::try_from(offset)
437            .map_err(|_| Error::invalid_options("object range offset overflow"))?;
438        let len = usize::try_from(len)
439            .map_err(|_| Error::invalid_options("object range length overflow"))?;
440        let end = offset
441            .checked_add(len)
442            .ok_or_else(|| Error::invalid_options("object range end overflow"))?;
443        let slice = object
444            .bytes
445            .get(offset..end)
446            .ok_or_else(|| Error::Corruption {
447                message: format!("object {key} short read for range {offset}..{end}"),
448            })?;
449        Ok(Arc::from(slice))
450    }
451
452    fn put_inner(&self, key: &str, bytes: Arc<[u8]>) -> Result<ETag> {
453        let etag = self.mint_etag();
454        self.lock()?.insert(
455            key.to_owned(),
456            StoredObject {
457                bytes,
458                etag: etag.clone(),
459            },
460        );
461        Ok(etag)
462    }
463
464    fn delete_inner(&self, key: &str) -> Result<()> {
465        self.lock()?.remove(key);
466        Ok(())
467    }
468
469    fn list_inner(&self, prefix: &str) -> Result<Vec<ObjectMeta>> {
470        let objects = self.lock()?;
471        Ok(objects
472            .range(prefix.to_owned()..)
473            .take_while(|(key, _)| key.starts_with(prefix))
474            .map(|(key, object)| ObjectMeta {
475                key: key.clone(),
476                size: object.bytes.len() as u64,
477                etag: object.etag.clone(),
478            })
479            .collect())
480    }
481
482    fn head_inner(&self, key: &str) -> Result<Option<ObjectMeta>> {
483        Ok(self.lock()?.get(key).map(|object| ObjectMeta {
484            key: key.to_owned(),
485            size: object.bytes.len() as u64,
486            etag: object.etag.clone(),
487        }))
488    }
489
490    fn put_if_inner(
491        &self,
492        key: &str,
493        bytes: Arc<[u8]>,
494        precondition: &Precondition,
495    ) -> Result<PutIf> {
496        let mut objects = self.lock()?;
497        let current = objects.get(key).map(|object| object.etag.clone());
498        let allowed = match (precondition, &current) {
499            (Precondition::IfNoneMatch, None) => true,
500            (Precondition::IfMatch(expected), Some(actual)) => expected == actual,
501            (Precondition::IfNoneMatch, Some(_)) | (Precondition::IfMatch(_), None) => false,
502        };
503        if !allowed {
504            return Ok(PutIf::PreconditionFailed { current });
505        }
506        let etag = self.mint_etag();
507        objects.insert(
508            key.to_owned(),
509            StoredObject {
510                bytes,
511                etag: etag.clone(),
512            },
513        );
514        Ok(PutIf::Stored { etag })
515    }
516}
517
518impl ObjectClient for InMemoryObjectStore {
519    fn get<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<Arc<[u8]>>> {
520        let key = key.to_owned();
521        Box::pin(async move { self.get_inner(&key) })
522    }
523
524    fn get_range<'op>(&'op self, key: &str, offset: u64, len: u64) -> ObjectFuture<'op, Arc<[u8]>> {
525        let key = key.to_owned();
526        Box::pin(async move { self.get_range_inner(&key, offset, len) })
527    }
528
529    fn put<'op>(&'op self, key: &str, bytes: Arc<[u8]>) -> ObjectFuture<'op, ETag> {
530        let key = key.to_owned();
531        Box::pin(async move { self.put_inner(&key, bytes) })
532    }
533
534    fn delete<'op>(&'op self, key: &str) -> ObjectFuture<'op, ()> {
535        let key = key.to_owned();
536        Box::pin(async move { self.delete_inner(&key) })
537    }
538
539    fn list<'op>(&'op self, prefix: &str) -> ObjectFuture<'op, Vec<ObjectMeta>> {
540        let prefix = prefix.to_owned();
541        Box::pin(async move { self.list_inner(&prefix) })
542    }
543
544    fn head<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<ObjectMeta>> {
545        let key = key.to_owned();
546        Box::pin(async move { self.head_inner(&key) })
547    }
548
549    fn put_if<'op>(
550        &'op self,
551        key: &str,
552        bytes: Arc<[u8]>,
553        precondition: Precondition,
554    ) -> ObjectFuture<'op, PutIf> {
555        let key = key.to_owned();
556        Box::pin(async move { self.put_if_inner(&key, bytes, &precondition) })
557    }
558}
559
560/// A shared [`ObjectClient`] is itself an `ObjectClient`, so several components
561/// (e.g. the manifest store and the byte backend) can share one client by
562/// holding `Arc<C>` clones.
563impl<C: ObjectClient + ?Sized> ObjectClient for Arc<C> {
564    fn get<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<Arc<[u8]>>> {
565        (**self).get(key)
566    }
567
568    fn get_range<'op>(&'op self, key: &str, offset: u64, len: u64) -> ObjectFuture<'op, Arc<[u8]>> {
569        (**self).get_range(key, offset, len)
570    }
571
572    fn put<'op>(&'op self, key: &str, bytes: Arc<[u8]>) -> ObjectFuture<'op, ETag> {
573        (**self).put(key, bytes)
574    }
575
576    fn delete<'op>(&'op self, key: &str) -> ObjectFuture<'op, ()> {
577        (**self).delete(key)
578    }
579
580    fn list<'op>(&'op self, prefix: &str) -> ObjectFuture<'op, Vec<ObjectMeta>> {
581        (**self).list(prefix)
582    }
583
584    fn head<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<ObjectMeta>> {
585        (**self).head(key)
586    }
587
588    fn put_if<'op>(
589        &'op self,
590        key: &str,
591        bytes: Arc<[u8]>,
592        precondition: Precondition,
593    ) -> ObjectFuture<'op, PutIf> {
594        (**self).put_if(key, bytes, precondition)
595    }
596}
597
598/// An object-storage **byte** backend: `SSTable` and blob object IO over an
599/// [`ObjectClient`].
600///
601/// It implements the async `Storage*Backend` byte traits the already-generic
602/// table/blob async helpers are written against, so flush, compaction, and reads
603/// work over object storage. The WAL, the manifest CAS, and the writer lease are
604/// deliberately **not** here — those are the object-storage durability
605/// substrate's job (manifest CAS lives in [`crate::manifest::ObjectManifestStore`]).
606///
607/// A [`StorageObjectId`]'s path is used directly as the object key, so keys are
608/// consistent across read / write / list / delete (the open path joins file
609/// names under the database's key prefix, mirroring the filesystem layout).
610#[derive(Clone)]
611pub(crate) struct ObjectStoreBackend {
612    client: Arc<dyn ObjectClient>,
613}
614
615impl std::fmt::Debug for ObjectStoreBackend {
616    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
617        formatter
618            .debug_struct("ObjectStoreBackend")
619            .finish_non_exhaustive()
620    }
621}
622
623impl ObjectStoreBackend {
624    pub(crate) fn new(client: Arc<dyn ObjectClient>) -> Self {
625        Self { client }
626    }
627
628    pub(crate) fn client(&self) -> Arc<dyn ObjectClient> {
629        Arc::clone(&self.client)
630    }
631
632    fn object_key(object: &StorageObjectId) -> String {
633        object.path().to_string_lossy().into_owned()
634    }
635}
636
637/// A whole-object read handle: the bytes are fetched eagerly on `open_read`
638/// (object stores favour whole-object GETs) and random-access reads are served
639/// from the in-memory buffer — mirroring `MemoryStorageObject`.
640#[derive(Debug, Clone)]
641pub(crate) struct ObjectStoreReadObject {
642    object: StorageObjectId,
643    bytes: Arc<[u8]>,
644}
645
646impl ObjectStoreReadObject {
647    fn read_exact_at_offset(&self, offset: usize, out: &mut [u8]) -> Result<()> {
648        let end = offset
649            .checked_add(out.len())
650            .ok_or_else(|| Error::invalid_options("object read offset overflow"))?;
651        let source = self
652            .bytes
653            .get(offset..end)
654            .ok_or_else(|| Error::Corruption {
655                message: format!("object {} short read", self.object.path().display()),
656            })?;
657        out.copy_from_slice(source);
658        Ok(())
659    }
660}
661
662impl StorageReadObject for ObjectStoreReadObject {
663    fn object(&self) -> &StorageObjectId {
664        &self.object
665    }
666
667    fn len(&self) -> StorageReadFuture<'_, u64> {
668        let len = self.bytes.len();
669        Box::pin(async move {
670            u64::try_from(len).map_err(|_| Error::invalid_options("object length overflow"))
671        })
672    }
673
674    fn read_exact_at<'op>(
675        &'op self,
676        offset: usize,
677        bytes: &'op mut [u8],
678    ) -> StorageReadFuture<'op, ()> {
679        Box::pin(async move { self.read_exact_at_offset(offset, bytes) })
680    }
681}
682
683impl StorageReadBackend for ObjectStoreBackend {
684    type ReadObject = ObjectStoreReadObject;
685
686    fn capabilities(&self) -> StorageCapabilities {
687        StorageCapabilities::object_store()
688    }
689
690    fn open_read(&self, object: StorageObjectId) -> StorageReadFuture<'_, Self::ReadObject> {
691        Box::pin(async move {
692            let key = Self::object_key(&object);
693            let meta = self
694                .client
695                .head(&key)
696                .await?
697                .ok_or_else(|| Error::Corruption {
698                    message: format!("referenced object {key} cannot be opened"),
699                })?;
700            let bytes =
701                read_object_bytes_by_meta(self.client.as_ref(), &key, &object, &meta).await?;
702            Ok(ObjectStoreReadObject { object, bytes })
703        })
704    }
705}
706
707impl StorageObjectReadBackend for ObjectStoreBackend {
708    fn read_object_bytes(&self, object: StorageObjectId) -> StorageFuture<'_, Option<Arc<[u8]>>> {
709        Box::pin(async move {
710            let key = Self::object_key(&object);
711            let Some(meta) = self.client.head(&key).await? else {
712                return Ok(None);
713            };
714            read_object_bytes_by_meta(self.client.as_ref(), &key, &object, &meta)
715                .await
716                .map(Some)
717        })
718    }
719}
720
721impl StorageObjectWriteBackend for ObjectStoreBackend {
722    fn write_object(
723        &self,
724        object: StorageObjectId,
725        bytes: Arc<[u8]>,
726        _durability: DurabilityMode,
727    ) -> StorageFuture<'_, ()> {
728        // A PUT is durable once the store acknowledges it, so durability hints do
729        // not apply (there is no separate flush/fsync step).
730        Box::pin(async move {
731            self.client.put(&Self::object_key(&object), bytes).await?;
732            Ok(())
733        })
734    }
735}
736
737impl StorageObjectDeleteBackend for ObjectStoreBackend {
738    fn delete_object(&self, object: StorageObjectId) -> StorageFuture<'_, ()> {
739        Box::pin(async move { self.client.delete(&Self::object_key(&object)).await })
740    }
741}
742
743impl StorageObjectListBackend for ObjectStoreBackend {
744    fn list_objects(
745        &self,
746        request: StorageObjectListRequest,
747    ) -> StorageFuture<'_, Vec<StorageObjectId>> {
748        Box::pin(async move {
749            let root = request.root().to_path_buf();
750            let kind = request.kind();
751            let extension = request.file_extension();
752            // Prefix-list under the database root, then keep only the direct
753            // children matching the requested extension — mirroring the
754            // filesystem backend's non-recursive, extension-filtered listing.
755            let prefix = root.to_string_lossy().into_owned();
756            let mut objects: Vec<StorageObjectId> = self
757                .client
758                .list(&prefix)
759                .await?
760                .into_iter()
761                .map(|meta| PathBuf::from(meta.key))
762                .filter(|path| path.parent() == Some(root.as_path()))
763                .filter(|path| path_matches_extension(path, extension))
764                .map(|path| StorageObjectId::native_file(kind, path))
765                .collect();
766            objects.sort_unstable();
767            Ok(objects)
768        })
769    }
770}
771
772fn path_matches_extension(path: &Path, expected: Option<&str>) -> bool {
773    expected.is_none_or(|expected| {
774        path.extension()
775            .and_then(|extension| extension.to_str())
776            .is_some_and(|extension| extension.eq_ignore_ascii_case(expected))
777    })
778}
779
780fn ensure_object_meta_read_len(object: &StorageObjectId, meta: &ObjectMeta) -> Result<()> {
781    let len = usize::try_from(meta.size).map_err(|_| Error::Corruption {
782        message: format!("object {} length exceeds usize", object.path().display()),
783    })?;
784    ensure_whole_object_read_len(object, len)
785}
786
787async fn read_object_bytes_by_meta(
788    client: &dyn ObjectClient,
789    key: &str,
790    object: &StorageObjectId,
791    meta: &ObjectMeta,
792) -> Result<Arc<[u8]>> {
793    ensure_object_meta_read_len(object, meta)?;
794    let expected_len = usize::try_from(meta.size).map_err(|_| Error::Corruption {
795        message: format!("object {} length exceeds usize", object.path().display()),
796    })?;
797    if expected_len == 0 {
798        return Ok(Arc::from([]));
799    }
800    let bytes = client.get_range(key, 0, meta.size).await?;
801    if bytes.len() != expected_len {
802        return Err(Error::Corruption {
803            message: format!(
804                "object {} range read returned {} bytes for declared length {expected_len}",
805                object.path().display(),
806                bytes.len()
807            ),
808        });
809    }
810    ensure_whole_object_read_len(object, bytes.len())?;
811    Ok(bytes)
812}
813
814#[cfg(test)]
815mod tests {
816    use super::*;
817    use std::sync::atomic::{AtomicU64, Ordering};
818
819    fn bytes(data: &[u8]) -> Arc<[u8]> {
820        Arc::from(data)
821    }
822
823    /// Drives an [`ObjectFuture`] to completion. The in-memory store never
824    /// yields, so a single poll with a no-op waker suffices.
825    fn block_on<T>(future: ObjectFuture<'_, T>) -> Result<T> {
826        use std::task::{Context, Poll, Wake, Waker};
827
828        struct NoopWaker;
829        impl Wake for NoopWaker {
830            fn wake(self: Arc<Self>) {}
831        }
832
833        let waker = Waker::from(Arc::new(NoopWaker));
834        let mut context = Context::from_waker(&waker);
835        let mut future = future;
836        match future.as_mut().poll(&mut context) {
837            Poll::Ready(result) => result,
838            Poll::Pending => panic!("in-memory object store future should be ready immediately"),
839        }
840    }
841
842    #[derive(Debug, Default)]
843    struct OversizedHeadClient {
844        get_calls: AtomicU64,
845    }
846
847    impl ObjectClient for OversizedHeadClient {
848        fn get<'op>(&'op self, _key: &str) -> ObjectFuture<'op, Option<Arc<[u8]>>> {
849            Box::pin(async move {
850                self.get_calls.fetch_add(1, Ordering::Relaxed);
851                Ok(Some(bytes(b"unreachable")))
852            })
853        }
854
855        fn get_range<'op>(
856            &'op self,
857            _key: &str,
858            _offset: u64,
859            _len: u64,
860        ) -> ObjectFuture<'op, Arc<[u8]>> {
861            Box::pin(async move { Err(Error::invalid_options("unexpected range read")) })
862        }
863
864        fn put<'op>(&'op self, _key: &str, _bytes: Arc<[u8]>) -> ObjectFuture<'op, ETag> {
865            Box::pin(async move { Err(Error::invalid_options("unexpected put")) })
866        }
867
868        fn delete<'op>(&'op self, _key: &str) -> ObjectFuture<'op, ()> {
869            Box::pin(async move { Err(Error::invalid_options("unexpected delete")) })
870        }
871
872        fn list<'op>(&'op self, _prefix: &str) -> ObjectFuture<'op, Vec<ObjectMeta>> {
873            Box::pin(async move { Err(Error::invalid_options("unexpected list")) })
874        }
875
876        fn head<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<ObjectMeta>> {
877            let key = key.to_owned();
878            Box::pin(async move {
879                Ok(Some(ObjectMeta {
880                    key,
881                    size: u64::MAX,
882                    etag: ETag::new("oversized"),
883                }))
884            })
885        }
886
887        fn put_if<'op>(
888            &'op self,
889            _key: &str,
890            _bytes: Arc<[u8]>,
891            _precondition: Precondition,
892        ) -> ObjectFuture<'op, PutIf> {
893            Box::pin(async move { Err(Error::invalid_options("unexpected put_if")) })
894        }
895    }
896
897    #[derive(Debug, Default)]
898    struct ShortRangeClient {
899        get_calls: AtomicU64,
900    }
901
902    impl ObjectClient for ShortRangeClient {
903        fn get<'op>(&'op self, _key: &str) -> ObjectFuture<'op, Option<Arc<[u8]>>> {
904            Box::pin(async move {
905                self.get_calls.fetch_add(1, Ordering::Relaxed);
906                Ok(Some(bytes(b"unreachable")))
907            })
908        }
909
910        fn get_range<'op>(
911            &'op self,
912            _key: &str,
913            _offset: u64,
914            _len: u64,
915        ) -> ObjectFuture<'op, Arc<[u8]>> {
916            Box::pin(async move { Ok(bytes(b"abc")) })
917        }
918
919        fn put<'op>(&'op self, _key: &str, _bytes: Arc<[u8]>) -> ObjectFuture<'op, ETag> {
920            Box::pin(async move { Err(Error::invalid_options("unexpected put")) })
921        }
922
923        fn delete<'op>(&'op self, _key: &str) -> ObjectFuture<'op, ()> {
924            Box::pin(async move { Err(Error::invalid_options("unexpected delete")) })
925        }
926
927        fn list<'op>(&'op self, _prefix: &str) -> ObjectFuture<'op, Vec<ObjectMeta>> {
928            Box::pin(async move { Err(Error::invalid_options("unexpected list")) })
929        }
930
931        fn head<'op>(&'op self, key: &str) -> ObjectFuture<'op, Option<ObjectMeta>> {
932            let key = key.to_owned();
933            Box::pin(async move {
934                Ok(Some(ObjectMeta {
935                    key,
936                    size: 5,
937                    etag: ETag::new("short-range"),
938                }))
939            })
940        }
941
942        fn put_if<'op>(
943            &'op self,
944            _key: &str,
945            _bytes: Arc<[u8]>,
946            _precondition: Precondition,
947        ) -> ObjectFuture<'op, PutIf> {
948            Box::pin(async move { Err(Error::invalid_options("unexpected put_if")) })
949        }
950    }
951
952    #[test]
953    fn put_then_get_roundtrips_and_overwrite_changes_etag() {
954        let store = InMemoryObjectStore::new();
955        let first = block_on(store.put("k", bytes(b"hello"))).unwrap();
956        assert_eq!(
957            block_on(store.get("k")).unwrap().as_deref(),
958            Some(b"hello".as_slice())
959        );
960        let second = block_on(store.put("k", bytes(b"world"))).unwrap();
961        assert_ne!(first, second, "overwrite mints a new ETag");
962        assert_eq!(
963            block_on(store.get("k")).unwrap().as_deref(),
964            Some(b"world".as_slice())
965        );
966    }
967
968    #[test]
969    fn get_absent_is_none_and_range_reads_a_window() {
970        let store = InMemoryObjectStore::new();
971        assert!(block_on(store.get("missing")).unwrap().is_none());
972        block_on(store.put("k", bytes(b"abcdef"))).unwrap();
973        assert_eq!(
974            block_on(store.get_range("k", 2, 3)).unwrap().as_ref(),
975            b"cde"
976        );
977        // Absent key and out-of-bounds range are both errors.
978        assert!(block_on(store.get_range("missing", 0, 1)).is_err());
979        assert!(block_on(store.get_range("k", 4, 10)).is_err());
980    }
981
982    #[test]
983    fn delete_is_idempotent() {
984        let store = InMemoryObjectStore::new();
985        block_on(store.put("k", bytes(b"x"))).unwrap();
986        block_on(store.delete("k")).unwrap();
987        assert!(block_on(store.get("k")).unwrap().is_none());
988        // Deleting an absent key still succeeds.
989        block_on(store.delete("k")).unwrap();
990    }
991
992    #[test]
993    fn list_returns_prefix_matches_in_key_order() {
994        let store = InMemoryObjectStore::new();
995        block_on(store.put("wal/2", bytes(b"b"))).unwrap();
996        block_on(store.put("wal/1", bytes(b"aa"))).unwrap();
997        block_on(store.put("table/9", bytes(b"c"))).unwrap();
998        let listed = block_on(store.list("wal/")).unwrap();
999        let keys: Vec<&str> = listed.iter().map(|meta| meta.key.as_str()).collect();
1000        assert_eq!(keys, ["wal/1", "wal/2"], "prefix-filtered, key-ordered");
1001        assert_eq!(listed[0].size, 2);
1002        assert_eq!(listed[1].size, 1);
1003    }
1004
1005    #[test]
1006    fn head_returns_metadata_without_bytes_and_none_when_absent() {
1007        let store = InMemoryObjectStore::new();
1008        assert!(block_on(store.head("k")).unwrap().is_none());
1009        let etag = block_on(store.put("k", bytes(b"hello"))).unwrap();
1010        let meta = block_on(store.head("k")).unwrap().expect("present");
1011        assert_eq!(meta.key, "k");
1012        assert_eq!(meta.size, 5);
1013        assert_eq!(meta.etag, etag);
1014    }
1015
1016    #[test]
1017    fn put_if_none_match_creates_only_when_absent() {
1018        let store = InMemoryObjectStore::new();
1019        let created = block_on(store.put_if("k", bytes(b"v1"), Precondition::IfNoneMatch)).unwrap();
1020        let etag = match created {
1021            PutIf::Stored { etag } => etag,
1022            PutIf::PreconditionFailed { .. } => panic!("create should succeed when absent"),
1023        };
1024        // A second create is refused and reports the current ETag.
1025        match block_on(store.put_if("k", bytes(b"v2"), Precondition::IfNoneMatch)).unwrap() {
1026            PutIf::PreconditionFailed { current } => assert_eq!(current, Some(etag)),
1027            PutIf::Stored { .. } => panic!("create should fail when present"),
1028        }
1029        assert_eq!(
1030            block_on(store.get("k")).unwrap().as_deref(),
1031            Some(b"v1".as_slice()),
1032            "refused create left the object unchanged"
1033        );
1034    }
1035
1036    #[test]
1037    fn put_if_match_is_a_compare_and_swap() {
1038        let store = InMemoryObjectStore::new();
1039        let v1 = block_on(store.put("k", bytes(b"v1"))).unwrap();
1040
1041        // CAS with the current ETag wins and advances the ETag.
1042        let v2 = match block_on(store.put_if("k", bytes(b"v2"), Precondition::IfMatch(v1.clone())))
1043            .unwrap()
1044        {
1045            PutIf::Stored { etag } => etag,
1046            PutIf::PreconditionFailed { .. } => panic!("CAS with current ETag should win"),
1047        };
1048        assert_ne!(v1, v2);
1049
1050        // A second CAS with the now-stale ETag loses and reports the current one
1051        // — this is the manifest-commit retry signal.
1052        match block_on(store.put_if("k", bytes(b"v3"), Precondition::IfMatch(v1))).unwrap() {
1053            PutIf::PreconditionFailed { current } => assert_eq!(current, Some(v2)),
1054            PutIf::Stored { .. } => panic!("CAS with stale ETag should lose"),
1055        }
1056        assert_eq!(
1057            block_on(store.get("k")).unwrap().as_deref(),
1058            Some(b"v2".as_slice()),
1059            "the losing CAS left v2 in place"
1060        );
1061    }
1062
1063    #[test]
1064    fn put_if_match_on_absent_object_fails() {
1065        let store = InMemoryObjectStore::new();
1066        let phantom = ETag(Arc::from("etag-phantom"));
1067        match block_on(store.put_if("k", bytes(b"v"), Precondition::IfMatch(phantom))).unwrap() {
1068            PutIf::PreconditionFailed { current } => assert_eq!(current, None),
1069            PutIf::Stored { .. } => panic!("IfMatch cannot match a missing object"),
1070        }
1071    }
1072
1073    #[test]
1074    fn object_store_backend_round_trips_an_object() {
1075        use crate::storage::StorageObjectKind;
1076
1077        let backend = ObjectStoreBackend::new(Arc::new(InMemoryObjectStore::new()));
1078        let id = StorageObjectId::native_file(StorageObjectKind::Table, "/db/0001.trinet");
1079
1080        block_on(backend.write_object(id.clone(), bytes(b"hello world"), DurabilityMode::Flush))
1081            .unwrap();
1082
1083        // Whole-object read.
1084        assert_eq!(
1085            block_on(backend.read_object_bytes(id.clone()))
1086                .unwrap()
1087                .as_deref(),
1088            Some(b"hello world".as_slice())
1089        );
1090
1091        // Random-access read via the eager read handle.
1092        let object = block_on(backend.open_read(id.clone())).unwrap();
1093        assert_eq!(block_on(StorageReadObject::len(&object)).unwrap(), 11);
1094        let mut window = [0_u8; 5];
1095        block_on(StorageReadObject::read_exact_at(&object, 6, &mut window)).unwrap();
1096        assert_eq!(&window, b"world");
1097
1098        // Delete, then it is gone.
1099        block_on(backend.delete_object(id.clone())).unwrap();
1100        assert!(block_on(backend.read_object_bytes(id)).unwrap().is_none());
1101    }
1102
1103    #[test]
1104    fn object_store_backend_rejects_oversized_head_before_get() {
1105        use crate::storage::StorageObjectKind;
1106
1107        let client = Arc::new(OversizedHeadClient::default());
1108        let backend = ObjectStoreBackend::new(client.clone());
1109        let id = StorageObjectId::native_file(StorageObjectKind::Table, "/db/huge.trinet");
1110
1111        let error =
1112            block_on(backend.read_object_bytes(id.clone())).expect_err("oversized head fails");
1113        assert!(error.to_string().contains("exceeds maximum"));
1114        assert_eq!(
1115            client.get_calls.load(Ordering::Relaxed),
1116            0,
1117            "oversized object should fail on HEAD before GET"
1118        );
1119
1120        let error = block_on(backend.open_read(id)).expect_err("oversized head fails");
1121        assert!(error.to_string().contains("exceeds maximum"));
1122        assert_eq!(
1123            client.get_calls.load(Ordering::Relaxed),
1124            0,
1125            "open_read should also fail on HEAD before GET"
1126        );
1127    }
1128
1129    #[test]
1130    fn object_store_backend_rejects_short_range_without_whole_get() {
1131        use crate::storage::StorageObjectKind;
1132
1133        let client = Arc::new(ShortRangeClient::default());
1134        let backend = ObjectStoreBackend::new(client.clone());
1135        let id = StorageObjectId::native_file(StorageObjectKind::Table, "/db/short.trinet");
1136
1137        let error = block_on(backend.read_object_bytes(id)).expect_err("short range fails");
1138        assert!(error.to_string().contains("declared length"));
1139        assert_eq!(
1140            client.get_calls.load(Ordering::Relaxed),
1141            0,
1142            "bounded range reads should not fall back to whole-object GET"
1143        );
1144    }
1145
1146    #[test]
1147    fn object_store_backend_lists_direct_children_by_extension() {
1148        use crate::storage::{StorageObjectKind, StorageObjectListRequest};
1149
1150        let backend = ObjectStoreBackend::new(Arc::new(InMemoryObjectStore::new()));
1151        let write = |key: &'static str| {
1152            block_on(backend.write_object(
1153                StorageObjectId::native_file(StorageObjectKind::Table, key),
1154                bytes(b"x"),
1155                DurabilityMode::Flush,
1156            ))
1157            .unwrap();
1158        };
1159        write("/db/0002.trinet");
1160        write("/db/0001.trinet");
1161        write("/db/MANIFEST"); // wrong extension
1162        write("/db/sub/9999.trinet"); // not a direct child of /db
1163
1164        let listed = block_on(
1165            backend.list_objects(
1166                StorageObjectListRequest::native_file(StorageObjectKind::Table, "/db")
1167                    .with_file_extension("trinet"),
1168            ),
1169        )
1170        .unwrap();
1171        let paths: Vec<String> = listed
1172            .iter()
1173            .map(|id| id.path().to_string_lossy().into_owned())
1174            .collect();
1175        assert_eq!(
1176            paths,
1177            ["/db/0001.trinet", "/db/0002.trinet"],
1178            "only direct .trinet children, in key order"
1179        );
1180    }
1181}