objectstore_service/backend/
common.rs

1//! Shared trait definition and types for all backends.
2
3use std::fmt;
4
5use objectstore_types::metadata::{ExpirationPolicy, Metadata};
6
7use bytes::Bytes;
8
9use crate::error::Result;
10use crate::id::ObjectId;
11use crate::stream::{ClientStream, PayloadStream};
12
13/// User agent string used for outgoing requests.
14///
15/// This intentionally has a "sentry" prefix so that it can easily be traced back to us.
16pub const USER_AGENT: &str = concat!("sentry-objectstore/", env!("CARGO_PKG_VERSION"));
17
18/// Backend response for put operations.
19pub type PutResponse = ();
20/// Backend response for get operations.
21pub type GetResponse = Option<(Metadata, PayloadStream)>;
22/// Backend response for metadata-only get operations.
23pub type MetadataResponse = Option<Metadata>;
24/// Backend response for delete operations.
25pub type DeleteResponse = ();
26
27/// Trait implemented by all storage backends.
28#[async_trait::async_trait]
29pub trait Backend: fmt::Debug + Send + Sync + 'static {
30    /// The backend name, used for diagnostics.
31    fn name(&self) -> &'static str;
32
33    /// Stores an object at the given path with the given metadata.
34    async fn put_object(
35        &self,
36        id: &ObjectId,
37        metadata: &Metadata,
38        stream: ClientStream,
39    ) -> Result<PutResponse>;
40
41    /// Retrieves an object at the given path, returning its metadata and a stream of bytes.
42    async fn get_object(&self, id: &ObjectId) -> Result<GetResponse>;
43
44    /// Retrieves only the metadata for an object, without the payload.
45    async fn get_metadata(&self, id: &ObjectId) -> Result<MetadataResponse> {
46        Ok(self
47            .get_object(id)
48            .await?
49            .map(|(metadata, _stream)| metadata))
50    }
51
52    /// Deletes the object at the given path.
53    async fn delete_object(&self, id: &ObjectId) -> Result<DeleteResponse>;
54
55    /// Waits for any outstanding background operations to complete before shutdown.
56    ///
57    /// The default implementation is a no-op. Backends that spawn background tasks
58    /// (such as [`TieredStorage`](super::tiered::TieredStorage)) should override this
59    /// to wait for those tasks to complete.
60    async fn join(&self) {}
61}
62
63/// Trait for backends that support tombstone-conditional operations.
64///
65/// Only backends suitable for the high-volume tier of
66/// [`TieredStorage`](super::tiered::TieredStorage) implement this trait.
67/// The conditional methods provide atomic operations to avoid overwriting
68/// redirect tombstones.
69#[async_trait::async_trait]
70pub trait HighVolumeBackend: Backend {
71    /// Writes the object only if NO redirect tombstone exists at this key.
72    ///
73    /// Returns `None` after storing the object, or `Some(tombstone)` (skipping
74    /// the write) when a redirect tombstone is present. The returned tombstone
75    /// carries the target LT `ObjectId` so the caller can route without a
76    /// second round trip.
77    ///
78    /// Takes [`Bytes`] instead of a [`ClientStream`] because callers on this
79    /// path have already fully buffered the payload.
80    async fn put_non_tombstone(
81        &self,
82        id: &ObjectId,
83        metadata: &Metadata,
84        payload: Bytes,
85    ) -> Result<Option<Tombstone>>;
86
87    /// Retrieves an object with explicit tombstone awareness.
88    ///
89    /// Returns [`TieredGet::Tombstone`] instead of synthesizing a tombstone
90    /// object, making the caller's routing logic a compile-time distinction.
91    async fn get_tiered_object(&self, id: &ObjectId) -> Result<TieredGet>;
92
93    /// Retrieves only metadata with explicit tombstone awareness.
94    ///
95    /// Implementations should skip the payload column where possible to avoid
96    /// fetching up to 1 MiB of data just to discover a tombstone.
97    async fn get_tiered_metadata(&self, id: &ObjectId) -> Result<TieredMetadata>;
98
99    /// Deletes the object only if it is NOT a redirect tombstone.
100    ///
101    /// Returns `None` after deleting the row (or if the row was already absent),
102    /// or `Some(tombstone)` (leaving the row intact) when the object is a
103    /// redirect tombstone. The returned tombstone carries the target LT
104    /// `ObjectId` so the caller can delete from long-term storage directly,
105    /// without a second round trip.
106    async fn delete_non_tombstone(&self, id: &ObjectId) -> Result<Option<Tombstone>>;
107
108    /// Atomically mutates the row if the current redirect state matches.
109    ///
110    /// `current` determines the precondition:
111    /// - `None`: succeeds only if no tombstone exists (row absent or inline).
112    /// - `Some(target)`: succeeds only if a tombstone exists whose redirect
113    ///   resolves to `target`.
114    ///
115    /// **This operation is idempotent:** if the object is already in the target
116    /// state, it returns `true`. Whether the mutation runs again is up to the
117    /// implementation.
118    ///
119    /// Returns `true` on success or idempotent match, `false` if a conflicting
120    /// state was found (another writer won the race).
121    async fn compare_and_write(
122        &self,
123        id: &ObjectId,
124        current: Option<&ObjectId>,
125        write: TieredWrite,
126    ) -> Result<bool>;
127}
128
129/// Information about a redirect tombstone in the high-volume backend.
130#[derive(Clone, Debug, PartialEq)]
131pub struct Tombstone {
132    /// The [`ObjectId`] of the object in the long-term backend.
133    ///
134    /// For legacy tombstones with an empty `r` column, the HV backend resolves
135    /// this to the HV `ObjectId` itself before surfacing the tombstone to callers.
136    pub target: ObjectId,
137
138    /// The expiration policy copied from the original object.
139    pub expiration_policy: ExpirationPolicy,
140}
141
142/// Typed response from [`HighVolumeBackend::get_tiered_object`].
143pub enum TieredGet {
144    /// A real object was found.
145    Object(Metadata, PayloadStream),
146    /// A redirect tombstone was found; the real object lives in the long-term backend.
147    Tombstone(Tombstone),
148    /// No entry exists at this key.
149    NotFound,
150}
151
152impl fmt::Debug for TieredGet {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        match self {
155            TieredGet::Object(metadata, _stream) => f
156                .debug_tuple("Object")
157                .field(metadata)
158                .finish_non_exhaustive(),
159            TieredGet::Tombstone(info) => f.debug_tuple("Tombstone").field(info).finish(),
160            TieredGet::NotFound => write!(f, "NotFound"),
161        }
162    }
163}
164
165/// Typed metadata-only response from [`HighVolumeBackend::get_tiered_metadata`].
166#[derive(Debug)]
167pub enum TieredMetadata {
168    /// Metadata for a real object was found.
169    Object(Metadata),
170    /// A redirect tombstone was found; the real object lives in the long-term backend.
171    Tombstone(Tombstone),
172    /// No entry exists at this key.
173    NotFound,
174}
175
176/// The write operation performed by [`HighVolumeBackend::compare_and_write`].
177#[derive(Clone, Debug)]
178pub enum TieredWrite {
179    /// Write a redirect tombstone.
180    Tombstone(Tombstone),
181    /// Write inline object data.
182    Object(Metadata, Bytes),
183    /// Delete the row entirely.
184    Delete,
185}
186
187impl TieredWrite {
188    /// Returns the tombstone target if this is a tombstone write, or `None` otherwise.
189    pub fn target(&self) -> Option<&ObjectId> {
190        match self {
191            TieredWrite::Tombstone(t) => Some(&t.target),
192            _ => None,
193        }
194    }
195}
196
197/// Creates a reqwest client with required defaults.
198///
199/// Automatic decompression is disabled because backends store pre-compressed
200/// payloads and manage `Content-Encoding` themselves.
201pub(super) fn reqwest_client() -> reqwest::Client {
202    reqwest::Client::builder()
203        .user_agent(USER_AGENT)
204        .hickory_dns(true)
205        .no_zstd()
206        .no_brotli()
207        .no_gzip()
208        .no_deflate()
209        .build()
210        .expect("Client::new()")
211}