Skip to main content

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}