Skip to main content

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