1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
use super::{
Arc, ContentReclamationMode, Db, DbInnerParts, DbOptions, DurabilityMode, DurabilitySubstrate,
Error, FilesystemSubstrate, ManifestStore, ObjectClient, ObjectClientTrustMode,
ObjectLeaseState, ObjectStoreBackend, ObjectStoreInnerParts, ObjectStoreSubstrate,
ObjectWriterLease, PathBuf, Result, Runtime, buckets_from_manifest_async, canonical_object_key,
canonical_object_prefix, ensure_default_bucket_in_manifest_async, ensure_default_bucket_loaded,
manifest, object_store_committed_wal_batches, object_store_wal_batches_after_replay_floor,
recovery, validate_common_options, verify_object_client_contract_for_open,
};
impl Db {
/// Opens an object-storage database at the bucket root (async-only).
///
/// Equivalent to [`Db::open_object_store_at`] with an empty key prefix. Use
/// the prefixed form to host several databases in one bucket.
///
/// # Errors
///
/// See [`Db::open_object_store_at`].
pub async fn open_object_store(
client: Arc<dyn ObjectClient>,
options: DbOptions,
) -> Result<Self> {
Self::open_object_store_at(client, "", options).await
}
/// Opens an object-storage database whose write-ahead log uses a separate
/// object client.
///
/// `storage_client` stores table/blob objects and the manifest CAS.
/// `wal_client` stores the writer lease, remote WAL head, and WAL segment
/// bytes. A confirmed durable write is acknowledged only after the WAL tier
/// stores the frame and publishes the WAL head. Flush later copies covered
/// memtable data into the storage tier's table objects and advances the
/// storage-tier manifest replay floor.
///
/// Use this when object storage is the shared bulk tier but commit latency
/// should be bounded by a lower-latency durable WAL tier. The two clients may
/// point at different buckets, services, or adapters; they share the same
/// key prefix string so recovery can address the corresponding objects in
/// each tier.
///
/// # Parameters
///
/// - `storage_client`: object client for manifest, tables, blobs, and bulk
/// cleanup.
/// - `wal_client`: object client for the writer lease and remote WAL
/// objects. It must provide real conditional writes and same-key
/// read-after-write visibility.
/// - `options`: must be [`DbOptions::object_store`], optionally marked
/// read-only.
///
/// # Errors
///
/// Returns [`Error::InvalidOptions`] when `options` is not object-store mode,
/// [`Error::LeaseUnavailable`] when another live writer owns the WAL-tier
/// lease, [`Error::Fenced`] when this writer has been superseded by a newer
/// epoch, or storage/`ObjectClient` errors from either tier.
///
/// # Examples
///
/// ```rust
/// use std::sync::Arc;
///
/// use trine_kv::{Db, DbOptions, InMemoryObjectStore};
///
/// async fn example() -> trine_kv::Result<()> {
/// let storage = Arc::new(InMemoryObjectStore::new());
/// let wal = Arc::new(InMemoryObjectStore::new());
/// let db = Db::open_object_store_with_wal(storage, wal, DbOptions::object_store()).await?;
///
/// db.put(b"k", b"v").await?;
/// assert_eq!(db.get(b"k").await?, Some(b"v".to_vec()));
/// Ok(())
/// }
/// ```
pub async fn open_object_store_with_wal(
storage_client: Arc<dyn ObjectClient>,
wal_client: Arc<dyn ObjectClient>,
options: DbOptions,
) -> Result<Self> {
Self::open_object_store_with_wal_at(storage_client, wal_client, "", options).await
}
/// Opens an object-storage database under a key `prefix` (async-only).
///
/// Provide any [`ObjectClient`] implementation (S3 and compatible, or the
/// in-memory fake) and object-store options ([`DbOptions::object_store`]).
/// All of the database's object keys live under `prefix`
/// (`<prefix>/MANIFEST`, `<prefix>/LOCK`, `<prefix>/table-*.trinet`, …), so
/// several databases can share one bucket. An empty `prefix` uses the bucket
/// root.
///
/// The byte IO, the manifest (conditional-PUT CAS), the writer lease, and
/// the remote WAL all ride `client`. Durable writes publish WAL frames into
/// the current remote segment before they become visible; flush later
/// converts covered memtables into table objects, advances the manifest
/// replay floor, and cleans old WAL objects.
///
/// # Errors
///
/// Returns [`Error::InvalidOptions`] when `options` is not object-store mode,
/// [`Error::LeaseUnavailable`] when another live writer owns the prefix, or
/// storage/`ObjectClient` errors from reading the manifest and acquiring
/// the writer lease.
#[cfg_attr(
all(target_arch = "wasm32", target_os = "unknown"),
allow(clippy::arc_with_non_send_sync)
)]
#[allow(clippy::too_many_lines)]
pub async fn open_object_store_at(
client: Arc<dyn ObjectClient>,
prefix: impl Into<String>,
options: DbOptions,
) -> Result<Self> {
Self::open_object_store_with_wal_at(Arc::clone(&client), client, prefix, options).await
}
/// Opens an object-storage database under `prefix` with an explicit WAL tier.
///
/// This is the prefixed form of [`Db::open_object_store_with_wal`]. The
/// prefix is applied independently to both clients: storage keys such as
/// `<prefix>/MANIFEST` live in `storage_client`, while WAL-tier keys such as
/// `<prefix>/LOCK` and `<prefix>/wal-...` live in `wal_client`.
///
/// A read-only open reads the storage manifest from `storage_client` and the
/// remote WAL head/segment from `wal_client`, then serves a point-in-time
/// view until [`Db::refresh_object_store`] is called.
///
/// # Errors
///
/// Returns [`Error::InvalidOptions`] when `options` is not object-store mode,
/// [`Error::UnsupportedBackend`] when a reclamation capability belongs to a
/// different storage client or prefix, [`Error::LeaseUnavailable`] when
/// another live writer owns the WAL-tier lease, or any error returned by
/// the storage or WAL client while opening or recovering.
#[cfg_attr(
all(target_arch = "wasm32", target_os = "unknown"),
allow(clippy::arc_with_non_send_sync)
)]
#[allow(clippy::too_many_lines)]
pub async fn open_object_store_with_wal_at(
storage_client: Arc<dyn ObjectClient>,
wal_client: Arc<dyn ObjectClient>,
prefix: impl Into<String>,
options: DbOptions,
) -> Result<Self> {
if !options.storage_mode.is_object_store_persistent() {
return Err(Error::invalid_options(
"object-store open requires the object-store storage mode",
));
}
validate_common_options(&options)?;
if matches!(
options.durability,
DurabilityMode::SyncData | DurabilityMode::SyncAll | DurabilityMode::SyncAllStrict
) {
return Err(Error::unsupported_durability(options.durability));
}
let prefix = canonical_object_prefix(&prefix.into())?;
let db_path = PathBuf::from(prefix);
crate::substrate::validate_object_lease_wal_key_capacity(&db_path)?;
if let ContentReclamationMode::QualifiedObjectStore(qualification) =
&options.content_reclamation
{
if !qualification.matches_prefix(&db_path) {
return Err(Error::unsupported_backend(
"object-store reclamation qualification names a different database prefix",
));
}
if !qualification.matches_client(&storage_client) {
return Err(Error::unsupported_backend(
"object-store reclamation qualification belongs to a different client instance",
));
}
}
let backend = ObjectStoreBackend::new(Arc::clone(&storage_client));
let wal_backend = ObjectStoreBackend::new(Arc::clone(&wal_client));
// The default trust mode assumes adapter qualification happened outside
// open (CI/startup/health check). VerifyOnOpen is the explicit
// fail-closed mode for deployments that want this open call to probe.
if !options.read_only
&& matches!(
options.object_client_trust,
ObjectClientTrustMode::VerifyOnOpen
)
{
verify_object_client_contract_for_open(&storage_client, &db_path, "storage").await?;
if !Arc::ptr_eq(&storage_client, &wal_client) {
verify_object_client_contract_for_open(&wal_client, &db_path, "wal").await?;
}
}
let manifest_key = canonical_object_key(&manifest::manifest_path(&db_path))?;
// Acquire the writer lease before the manifest, so its fencing epoch is
// known and can be stamped into every publish.
let lease_key = canonical_object_key(&db_path.join(recovery::PROCESS_LOCK_FILE_NAME))?;
let (substrate, remote_wal_state) = if options.read_only {
let remote_wal_state =
ObjectWriterLease::read_current(Arc::clone(&wal_client), lease_key)
.await?
.unwrap_or_else(ObjectLeaseState::empty);
(
DurabilitySubstrate::Filesystem(FilesystemSubstrate::new(None, None)),
remote_wal_state,
)
} else {
let lease = ObjectWriterLease::acquire(Arc::clone(&wal_client), lease_key).await?;
let remote_wal_state = lease.lease_state();
(
DurabilitySubstrate::ObjectStore(ObjectStoreSubstrate::new(
lease,
db_path.clone(),
)?),
remote_wal_state,
)
};
let writer_epoch = substrate.object_fencing_epoch().unwrap_or(0);
let mut manifest = ManifestStore::open_object_store_async(
Arc::clone(&storage_client),
manifest_key,
writer_epoch,
)
.await?;
ensure_default_bucket_in_manifest_async(&mut manifest, &options).await?;
// Stamp our fencing epoch into the manifest now (writable opens), so a
// just-displaced prior owner is fenced before we issue our first flush —
// not only after it. Fails with `Error::Fenced` if a newer owner exists.
if !options.read_only {
manifest.claim_object_epoch_async().await?;
}
let mut buckets =
buckets_from_manifest_async(&backend, &db_path, manifest.state(), true).await?;
ensure_default_bucket_loaded(&mut buckets, &options)?;
let replay_floor = manifest.state().wal_replay_floor();
let wal_batches = object_store_wal_batches_after_replay_floor(
Arc::clone(&wal_client),
&db_path,
&remote_wal_state,
replay_floor,
)
.await?;
let batches = object_store_committed_wal_batches(
wal_batches,
replay_floor,
remote_wal_state.committed_sequence,
)?;
let runtime = Runtime::new(options.runtime);
let db = Self::from_inner_parts(DbInnerParts::object_store(
options,
buckets,
manifest,
ObjectStoreInnerParts {
substrate,
storage: backend,
wal_storage: wal_backend,
prefix: db_path,
},
runtime,
));
db.replay_wal_batches(batches, replay_floor)?;
Ok(db)
}
}