objstore_r/lib.rs
1// Author: spicyzhug@gmail.com
2// Copyright: lituus-io, all rights reserved
3//
4//! # objstore-r
5//!
6//! Object stores with **generation preconditions**, for data that is published
7//! as immutable snapshots plus append-only segments (graph-r, link-r).
8//!
9//! Every object carries a *generation* (a `u64`, never zero) that changes on
10//! each write. Writes take a [`Cond`]:
11//!
12//! - [`Cond::CreateOnly`]: succeed only if the key does not exist. Immutable
13//! objects (snapshots, log segments) are written this way, so a retry after a
14//! lost response is harmless and a second writer can never clobber the first.
15//! - [`Cond::IfGeneration`]: succeed only if the object is still at that
16//! generation. A small `HEAD` object updated this way gives single-writer
17//! compare-and-swap across machines, with no lock service.
18//!
19//! ## Backends
20//!
21//! | Backend | Feature | Use |
22//! |---|---|---|
23//! | [`LocalDir`] | always | a directory; atomic rename, fsync, `File::lock` for conditions |
24//! | [`Mem`] | always | tests; deterministic, with scripted fault injection |
25//! | `Gcs` | `gcs` | Google Cloud Storage (JSON API), ADC tokens, retries, ranged reads |
26//!
27//! ## Design tenets
28//!
29//! - **Named futures, no boxing.** [`ObjectStore`] methods return associated
30//! future types (GATs), so callers compose them without `Box<dyn Future>`
31//! and without `Send`/`'static` bounds imposed by this crate. Futures are
32//! `Send` whenever the backend is, so multi-threaded runtimes work too.
33//! - **Enums over `dyn`.** Choose a backend at compile time, or wrap several in
34//! your own enum; nothing here needs a trait object.
35//! - **Zero-copy bodies.** Objects are [`bytes::Bytes`]; a body is read or
36//! written once and shared by reference count from there on.
37//! - **No unsafe code.**
38//!
39//! ## Content encoding
40//!
41//! Opt in per object with [`ObjectStore::put_encoded`] (feature `gzip`): the
42//! body is stored compressed and [`get`](ObjectStore::get) decodes it once
43//! into the returned buffer, bounded against decompression bombs; a ranged
44//! read of an encoded object is refused ([`Error::EncodedRange`]).
45//! [`ObjectStore::stat`] and [`ObjectStore::list_stat`] report the encoding.
46//! See [`encoding`].
47//!
48//! ## Example
49//!
50//! ```
51//! use objstore_r::{Cond, Mem, ObjectStore};
52//! # futures::executor::block_on(async {
53//! let store = Mem::default();
54//! let g1 = store.put("snap/0001", "v1".into(), Cond::CreateOnly).await?;
55//! assert!(store.put("snap/0001", "again".into(), Cond::CreateOnly).await.is_err());
56//! let head = store.put("HEAD", "0001".into(), Cond::None).await?;
57//! store.put("HEAD", "0002".into(), Cond::IfGeneration(head)).await?; // CAS
58//! assert_eq!(store.get("snap/0001").await?.unwrap().bytes, "v1");
59//! # let _ = g1;
60//! # Ok::<(), objstore_r::Error>(()) }).unwrap();
61//! ```
62
63#![cfg_attr(feature = "http", feature(impl_trait_in_assoc_type))]
64#![forbid(unsafe_code)]
65#![deny(missing_docs)]
66#![deny(missing_debug_implementations)]
67#![deny(rust_2018_idioms)]
68#![deny(unreachable_pub)]
69#![warn(clippy::all, clippy::pedantic, clippy::cargo)]
70#![allow(
71 clippy::module_name_repetitions,
72 clippy::missing_errors_doc,
73 clippy::missing_panics_doc,
74 clippy::multiple_crate_versions,
75 clippy::cast_possible_truncation
76)]
77
78pub mod encoding;
79pub mod error;
80pub mod fenced;
81pub mod join;
82pub mod key;
83pub mod local;
84pub mod mem;
85#[cfg(feature = "mirror")]
86pub mod mirror;
87pub mod net;
88pub mod sse;
89pub mod transport;
90
91#[cfg(feature = "gcs")]
92pub mod gcs;
93#[cfg(feature = "http")]
94pub mod http;
95#[cfg(feature = "http")]
96pub mod policy;
97#[cfg(feature = "http")]
98pub mod token;
99
100#[cfg(feature = "testing")]
101pub mod testing;
102
103use std::future::Future;
104use std::ops::Range;
105
106pub use bytes::Bytes;
107pub use encoding::{ContentEncoding, MAX_INFLATED_BYTES};
108pub use error::{Error, PolicyError, Result};
109pub use key::Key;
110pub use local::LocalDir;
111pub use mem::{Fault, Mem};
112
113#[cfg(feature = "gcs")]
114pub use gcs::{Gcs, GcsConfig};
115
116/// An object's content and generation.
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct Object {
119 /// The whole body (shared, zero-copy).
120 pub bytes: Bytes,
121 /// The generation it was read at (never zero).
122 pub generation: u64,
123}
124
125/// A listed object.
126#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
127pub struct Meta {
128 /// The full key.
129 pub key: String,
130 /// Body size in bytes.
131 pub size: u64,
132 /// Current generation (never zero).
133 pub generation: u64,
134}
135
136/// [`Meta`] and the object's content encoding, from [`ObjectStore::stat`]
137/// and [`ObjectStore::list_stat`].
138#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
139pub struct Stat {
140 /// Key, generation, and the **stored** size (for an encoded object, the
141 /// encoded bytes).
142 pub meta: Meta,
143 /// How the stored bytes are encoded (`None`: as written).
144 pub content_encoding: Option<ContentEncoding>,
145}
146
147/// A write precondition.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
149pub enum Cond {
150 /// Write unconditionally (replace whatever is there).
151 #[default]
152 None,
153 /// Write only if no object exists under the key ([`Error::Conflict`] otherwise).
154 CreateOnly,
155 /// Write only if the object is at exactly this generation
156 /// ([`Error::PreconditionFailed`] otherwise, including when it does not exist).
157 IfGeneration(u64),
158}
159
160/// An object store with generation preconditions.
161///
162/// Keys are validated by [`Key::parse`]: `/`-separated segments of
163/// `[A-Za-z0-9._-]`, no empty segment, none starting with `.`, at most 1024 bytes.
164///
165/// Implementations must be linearizable per key: a successful conditional
166/// write is observed by every later read.
167pub trait ObjectStore {
168 /// Future of [`get`](ObjectStore::get).
169 type Get<'a>: Future<Output = Result<Option<Object>>> + 'a
170 where
171 Self: 'a;
172 /// Future of [`get_range`](ObjectStore::get_range).
173 type GetRange<'a>: Future<Output = Result<Bytes>> + 'a
174 where
175 Self: 'a;
176 /// Future of [`head`](ObjectStore::head).
177 type Head<'a>: Future<Output = Result<Option<Meta>>> + 'a
178 where
179 Self: 'a;
180 /// Future of [`put`](ObjectStore::put).
181 type Put<'a>: Future<Output = Result<u64>> + 'a
182 where
183 Self: 'a;
184 /// Future of [`list`](ObjectStore::list).
185 type List<'a>: Future<Output = Result<Vec<Meta>>> + 'a
186 where
187 Self: 'a;
188 /// Future of [`delete`](ObjectStore::delete).
189 type Delete<'a>: Future<Output = Result<()>> + 'a
190 where
191 Self: 'a;
192
193 /// The whole object, or `None` if it does not exist.
194 fn get<'a>(&'a self, key: &'a str) -> Self::Get<'a>;
195
196 /// Bytes `range` of the object. A range running past the end is clipped;
197 /// a range starting at or past the end is an empty body. A missing object
198 /// is [`Error::NotFound`].
199 fn get_range<'a>(&'a self, key: &'a str, range: Range<u64>) -> Self::GetRange<'a>;
200
201 /// Size and generation, without the body.
202 fn head<'a>(&'a self, key: &'a str) -> Self::Head<'a>;
203
204 /// Write `body` under `key` if `cond` holds; returns the new generation.
205 fn put<'a>(&'a self, key: &'a str, body: Bytes, cond: Cond) -> Self::Put<'a>;
206
207 /// Every object whose key starts with `prefix`, sorted by key. An empty
208 /// prefix lists everything; a non-empty prefix is validated like a key
209 /// (a trailing `/` is allowed).
210 fn list<'a>(&'a self, prefix: &'a str) -> Self::List<'a>;
211
212 /// Remove the object. Deleting a missing key succeeds.
213 fn delete<'a>(&'a self, key: &'a str) -> Self::Delete<'a>;
214
215 /// Like [`put`](ObjectStore::put), storing `body` compressed with
216 /// `encoding` and recording the encoding, so [`get`](ObjectStore::get)
217 /// returns `body` again and [`get_range`](ObjectStore::get_range) refuses
218 /// with [`Error::EncodedRange`]. Conditions behave exactly as for `put`.
219 ///
220 /// Every backend in this crate implements it (feature `gzip`). The
221 /// default, for stores that cannot record an encoding, refuses with
222 /// [`Error::UnsupportedEncoding`] and writes nothing.
223 fn put_encoded<'a>(
224 &'a self,
225 key: &'a str,
226 body: Bytes,
227 cond: Cond,
228 encoding: ContentEncoding,
229 ) -> impl Future<Output = Result<u64>> + 'a {
230 let _ = (key, body, cond);
231 std::future::ready(Err(Error::UnsupportedEncoding { encoding: encoding.as_str().into() }))
232 }
233
234 /// [`head`](ObjectStore::head) plus the content encoding. The default
235 /// reports none, which is exact for a store that cannot record one.
236 fn stat<'a>(&'a self, key: &'a str) -> impl Future<Output = Result<Option<Stat>>> + 'a {
237 let head = self.head(key);
238 async move { Ok(head.await?.map(|meta| Stat { meta, content_encoding: None })) }
239 }
240
241 /// [`list`](ObjectStore::list) plus each object's content encoding. The
242 /// default reports none, which is exact for a store that cannot record
243 /// one.
244 fn list_stat<'a>(&'a self, prefix: &'a str) -> impl Future<Output = Result<Vec<Stat>>> + 'a {
245 let list = self.list(prefix);
246 async move {
247 Ok(list.await?.into_iter().map(|meta| Stat { meta, content_encoding: None }).collect())
248 }
249 }
250}
251
252/// Clip `range` to an object of `len` bytes, as `get_range` specifies.
253pub(crate) fn clip(range: &Range<u64>, len: u64) -> Range<usize> {
254 let start = range.start.min(len);
255 let end = range.end.clamp(start, len);
256 start as usize..end as usize
257}