tuwunel_database/engine.rs
1//! RocksDB engine: database-wide operations and shared resources.
2//!
3//! `Engine` owns the opened RocksDB instance together with the worker pool, the
4//! shared open-time context, and the flags fixed at open (read-only, secondary,
5//! checksums). Per-column-family reads and writes go through `Map`; the methods
6//! here act on the database as a whole: WAL flush and sync, memtable flush,
7//! manual compaction and primary catch-up, property queries, and the cork
8//! counter that coalesces WAL writes (see the `cork` module).
9
10mod backup;
11mod cf_opts;
12pub(crate) mod context;
13mod db_opts;
14pub(crate) mod descriptor;
15mod env;
16mod events;
17mod files;
18mod logger;
19mod memory_usage;
20mod open;
21mod repair;
22#[cfg(test)]
23mod tests;
24
25use std::{
26 collections::BTreeMap,
27 ffi::CStr,
28 sync::{
29 Arc, OnceLock, Weak,
30 atomic::{AtomicU32, Ordering},
31 },
32};
33
34use rocksdb::{
35 AsColumnFamilyRef, BoundColumnFamily, DBCommon, DBWithThreadMode, MultiThreaded,
36 WaitForCompactOptions, WriteOptions,
37};
38use tuwunel_core::{Err, Result, debug, implement, info, warn};
39
40use crate::{
41 Context, Map,
42 pool::Pool,
43 util::{map_err, result},
44};
45
46pub(crate) type CfIndex = BTreeMap<u32, Weak<Map>>;
47
48/// Handle to the opened RocksDB database and its shared resources.
49///
50/// One `Engine` exists per database, shared behind an `Arc` by every `Map`.
51pub struct Engine {
52 /// The opened RocksDB instance.
53 pub(crate) db: Db,
54
55 /// Thread pool offloading uncached, blocking database requests from the
56 /// tokio workers.
57 pub(crate) pool: Arc<Pool>,
58
59 /// Resources constructed before the database is opened and outliving it
60 /// (block caches, environment, column descriptors).
61 pub(crate) ctx: Arc<Context>,
62
63 /// Database was opened read-only; writes are rejected.
64 pub(super) read_only: bool,
65
66 /// Database was opened as a secondary follower of a primary instance.
67 pub(super) secondary: bool,
68
69 /// Verify block checksums on read.
70 pub(crate) checksums: bool,
71
72 /// Shared write options for atomic batch commits.
73 pub(crate) write_options: WriteOptions,
74
75 /// Resolves catalog column ids for post-commit watcher notification.
76 /// Runtime migration column families are intentionally absent.
77 cf_index: OnceLock<CfIndex>,
78
79 /// Live cork count; nonzero suppresses the per-write WAL flush.
80 corks: AtomicU32,
81}
82
83/// Backing RocksDB type: multi-threaded column-family access, no transactions.
84pub(crate) type Db = DBWithThreadMode<MultiThreaded>;
85
86impl Engine {
87 /// Block until outstanding background compactions finish.
88 ///
89 /// Waits without a timeout and does not flush first; aborts the wait if
90 /// compaction has been paused.
91 #[tracing::instrument(
92 level = "info",
93 skip_all,
94 fields(
95 sequence = ?self.current_sequence(),
96 ),
97 )]
98 pub fn wait_compactions_blocking(&self) -> Result {
99 let mut opts = WaitForCompactOptions::default();
100 opts.set_abort_on_pause(true);
101 opts.set_flush(false);
102 opts.set_timeout(0);
103
104 self.db.wait_for_compact(&opts).map_err(map_err)
105 }
106
107 /// Flush the memtables to SST files (a RocksDB LSM-tree flush).
108 ///
109 /// Forces buffered writes out of memory into the on-disk LSM tree. An LSM
110 /// flush, not a libc `fflush(3)` or `fsync(2)`, and distinct from the
111 /// `flush` and `sync` methods here, which act on the write-ahead log.
112 #[tracing::instrument(
113 level = "info",
114 skip_all,
115 fields(
116 sequence = ?self.current_sequence(),
117 ),
118 )]
119 pub fn sort(&self) -> Result {
120 //TODO: Call flush_cfs_opt instead.
121 let flushoptions = rocksdb::FlushOptions::default();
122 result(DBCommon::flush_opt(&self.db, &flushoptions))
123 }
124
125 /// Catch a secondary instance up to the primary's latest writes.
126 ///
127 /// Replays the primary's newly appended WAL into this instance's view;
128 /// meaningful only when the database was opened as a secondary.
129 #[tracing::instrument(
130 level = "debug",
131 skip_all,
132 fields(
133 sequence = ?self.current_sequence(),
134 ),
135 )]
136 pub fn update(&self) -> Result {
137 self.db
138 .try_catch_up_with_primary()
139 .map_err(map_err)
140 }
141
142 /// Flush the write-ahead log and fsync it to disk.
143 ///
144 /// Once this returns the buffered writes survive power loss. Heavier than
145 /// `flush`, which stops at the OS page cache.
146 #[tracing::instrument(level = "info", skip_all)]
147 pub fn sync(&self) -> Result { result(DBCommon::flush_wal(&self.db, true)) }
148
149 /// Flush the buffered write-ahead log to the OS without an fsync.
150 ///
151 /// Pushes WAL bytes to the page cache (durable against process crash, not
152 /// power loss). This is the per-write flush that corking suppresses.
153 #[tracing::instrument(level = "debug", skip_all)]
154 pub fn flush(&self) -> Result { result(DBCommon::flush_wal(&self.db, false)) }
155
156 /// Increment the cork count, suppressing the per-write WAL flush.
157 #[inline]
158 pub(crate) fn cork(&self) { self.corks.fetch_add(1, Ordering::Relaxed); }
159
160 /// Decrement the cork count; the per-write flush resumes at zero.
161 #[inline]
162 pub(crate) fn uncork(&self) { self.corks.fetch_sub(1, Ordering::Relaxed); }
163
164 /// Whether any cork is currently held.
165 ///
166 /// When true, `Map` insert and remove skip their post-write WAL flush so
167 /// the records coalesce into one batch. Corking is purely a backend
168 /// write-buffering signal: it never changes application logic or any
169 /// observable database API behavior, because a write lands in the memtable
170 /// synchronously and reads back regardless of WAL flush state. See the
171 /// `cork` module.
172 #[inline]
173 pub fn corked(&self) -> bool { self.corks.load(Ordering::Relaxed) > 0 }
174
175 /// Query for database property by null-terminated name which is expected to
176 /// have a result with an integer representation. This is intended for
177 /// low-overhead programmatic use.
178 pub(crate) fn property_integer(
179 &self,
180 cf: &impl AsColumnFamilyRef,
181 name: &CStr,
182 ) -> Result<u64> {
183 result(self.db.property_int_value_cf(cf, name))
184 .and_then(|val| val.map_or_else(|| Err!("Property {name:?} not found."), Ok))
185 }
186
187 /// Query for database property by name receiving the result in a string.
188 pub(crate) fn property(&self, cf: &impl AsColumnFamilyRef, name: &str) -> Result<String> {
189 result(self.db.property_value_cf(cf, name))
190 .and_then(|val| val.map_or_else(|| Err!("Property {name:?} not found."), Ok))
191 }
192
193 /// Look up a column-family handle by name.
194 ///
195 /// The handle refers to a family opened with this database and remains tied
196 /// to the engine's lifetime.
197 ///
198 /// # Panics
199 ///
200 /// Panics if the family was not described before the database was opened.
201 pub(crate) fn cf(&self, name: &str) -> Arc<BoundColumnFamily<'_>> {
202 self.db
203 .cf_handle(name)
204 .expect("column must be described prior to database open")
205 }
206
207 /// Reports whether a column family with this name exists.
208 ///
209 /// The lookup consults the handles currently opened by RocksDB. It does not
210 /// create a missing family.
211 #[inline]
212 #[must_use]
213 pub fn has_cf(&self, name: &str) -> bool { self.db.cf_handle(name).is_some() }
214
215 /// Returns the latest RocksDB sequence number.
216 ///
217 /// RocksDB assigns sequence numbers to committed writes, so this value
218 /// marks the engine's current write position. The number is local to this
219 /// database.
220 #[inline]
221 #[must_use]
222 #[tracing::instrument(
223 name = "sequence",
224 level = "debug",
225 skip_all,
226 fields(sequence)
227 )]
228 pub fn current_sequence(&self) -> u64 {
229 let sequence = self.db.latest_sequence_number();
230
231 #[cfg(debug_assertions)]
232 tracing::Span::current().record("sequence", sequence);
233
234 sequence
235 }
236
237 /// Reports whether this engine rejects writes.
238 ///
239 /// Both read-only and secondary opens reject writes through their database
240 /// handle. A writable primary open returns false.
241 #[inline]
242 #[must_use]
243 pub fn is_read_only(&self) -> bool { self.secondary || self.read_only }
244
245 /// Reports whether the database follows a primary as a secondary.
246 ///
247 /// A secondary advances its view when [`Self::update`] catches up with the
248 /// primary. Writes through the secondary handle are rejected.
249 #[inline]
250 #[must_use]
251 pub fn is_secondary(&self) -> bool { self.secondary }
252}
253
254#[implement(Engine)]
255pub(crate) fn set_cf_index(&self, index: CfIndex) {
256 self.cf_index
257 .set(index)
258 .expect("cf_index initialized twice");
259}
260
261#[implement(Engine)]
262#[inline]
263pub(crate) fn map_by_cf_id(&self, cf_id: u32) -> Option<Arc<Map>> {
264 self.cf_index
265 .get()
266 .expect("cf_index initialized before writes")
267 .get(&cf_id)
268 .and_then(Weak::upgrade)
269}
270
271impl Drop for Engine {
272 #[cold]
273 fn drop(&mut self) {
274 const BLOCKING: bool = true;
275
276 debug!("Waiting for background tasks to finish...");
277 self.db.cancel_all_background_work(BLOCKING);
278
279 info!(
280 sequence = %self.current_sequence(),
281 "Closing database..."
282 );
283 }
284}