Skip to main content

tuwunel_database/
txn.rs

1//! Atomic database writes backed by one RocksDB write batch.
2//!
3//! A transaction queues operations for maps owned by one database engine and
4//! commits them only when [`Txn::execute`] consumes it. Typed operations use
5//! the database codec, while raw operations preserve caller-provided bytes.
6
7use std::{fmt::Debug, iter::once, sync::Arc};
8
9use rocksdb::WriteBatch;
10use serde::Serialize;
11use tuwunel_core::implement;
12
13use crate::{
14	Engine, Map,
15	keyval::{serialize_key, serialize_val},
16	util::or_else,
17};
18
19/// Atomic write batch spanning one or more column families from one database.
20///
21/// Every queued map must belong to the captured engine because column family
22/// identifiers are interpreted within that database. Dropping an unexecuted
23/// transaction leaves the database unchanged.
24#[must_use = "does nothing until execute()"]
25pub struct Txn {
26	batch: WriteBatch,
27	engine: Arc<Engine>,
28}
29
30/// Record parser yielding each queued key with its resolved map.
31struct Keys<'a> {
32	engine: &'a Engine,
33	data: &'a [u8],
34}
35
36/// Batch representation header: a fixed64 sequence then a fixed32 count.
37const HEADER: usize = 12;
38
39/// Worst-case per-record overhead: a type tag and three varint32s.
40const PER_OP: usize = 16;
41
42/// Record tags per rocksdb `write_batch.cc`; puts and deletes against
43/// column family id 0 encode as the legacy untagged types.
44#[derive(Clone, Copy)]
45enum Tag {
46	Deletion = 0x0,
47	Value = 0x1,
48	CfDeletion = 0x4,
49	CfValue = 0x5,
50}
51
52impl TryFrom<u8> for Tag {
53	type Error = u8;
54
55	fn try_from(byte: u8) -> Result<Self, Self::Error> {
56		match byte {
57			| 0x0 => Ok(Self::Deletion),
58			| 0x1 => Ok(Self::Value),
59			| 0x4 => Ok(Self::CfDeletion),
60			| 0x5 => Ok(Self::CfValue),
61			| unrecognized => Err(unrecognized),
62		}
63	}
64}
65
66/// Creates an empty transaction for one database engine.
67///
68/// Operations can be appended through the typed or raw queueing methods. The
69/// transaction remains inert until [`Txn::execute`] consumes it.
70#[implement(Txn)]
71pub fn new(engine: &Arc<Engine>) -> Self {
72	Self {
73		batch: WriteBatch::default(),
74		engine: engine.clone(),
75	}
76}
77
78/// Creates an empty transaction with reserved batch capacity.
79///
80/// `capacity_bytes` reserves storage for the serialized RocksDB batch
81/// representation. The reservation affects allocation only and does not queue
82/// an operation.
83#[implement(Txn)]
84pub fn with_capacity_bytes(engine: &Arc<Engine>, capacity_bytes: usize) -> Self {
85	Self {
86		batch: WriteBatch::with_capacity_bytes(capacity_bytes),
87		engine: engine.clone(),
88	}
89}
90
91/// Queues raw key and value pairs for one map from a single pass.
92///
93/// The database codec is not applied, and the write batch copies each supplied
94/// byte sequence. Empty input produces an empty transaction whose execution is
95/// a no-op.
96#[implement(Txn)]
97pub fn insert<I, K, V>(map: &Map, items: I) -> Self
98where
99	I: IntoIterator<Item = (K, V)>,
100	K: AsRef<[u8]>,
101	V: AsRef<[u8]>,
102{
103	items
104		.into_iter()
105		.fold(Self::new(map.engine()), |mut txn, (key, val)| {
106			txn.insert_raw(map, key, val);
107			txn
108		})
109}
110
111/// Queues a raw slice for one map with a precomputed capacity estimate.
112///
113/// The estimate includes payload lengths and worst-case record overhead before
114/// the items are copied into the write batch. Empty input produces an empty
115/// transaction.
116#[implement(Txn)]
117pub fn insert_slice<K, V>(map: &Map, items: &[(K, V)]) -> Self
118where
119	K: AsRef<[u8]>,
120	V: AsRef<[u8]>,
121{
122	let capacity_bytes = size_hint(items.iter().map(|(key, val)| (key, val)));
123
124	items.iter().fold(
125		Self::with_capacity_bytes(map.engine(), capacity_bytes),
126		|mut txn, (key, val)| {
127			txn.insert_raw(map, key, val);
128			txn
129		},
130	)
131}
132
133/// Queues raw entries across maps from a nonempty single pass.
134///
135/// The first item selects the database engine, and every subsequent map must
136/// belong to that same engine. The database codec is not applied to keys or
137/// values.
138///
139/// # Panics
140///
141/// Panics when `items` is empty or when any map belongs to a different database
142/// engine.
143#[implement(Txn)]
144pub fn insert_each<'a, I, K, V>(items: I) -> Self
145where
146	I: IntoIterator<Item = (&'a Map, K, V)>,
147	K: AsRef<[u8]>,
148	V: AsRef<[u8]>,
149{
150	let mut items = items.into_iter();
151	let (map, key, val) = items
152		.next()
153		.expect("insert_each: at least one item");
154
155	let mut txn = Self::new(map.engine());
156
157	txn.insert_raw(map, key, val);
158	txn.extend(items);
159
160	txn
161}
162
163/// Queues a nonempty raw slice across maps with a capacity estimate.
164///
165/// The first item selects the database engine, and every map must belong to
166/// that same engine. The database codec is not applied to keys or values.
167///
168/// # Panics
169///
170/// Panics when `items` is empty or when any map belongs to a different database
171/// engine.
172#[implement(Txn)]
173pub fn insert_each_slice<K, V>(items: &[(&Map, K, V)]) -> Self
174where
175	K: AsRef<[u8]>,
176	V: AsRef<[u8]>,
177{
178	let map = items
179		.first()
180		.expect("insert_each_slice: at least one item")
181		.0;
182
183	let capacity_bytes = size_hint(items.iter().map(|(_, key, val)| (key, val)));
184
185	let mut txn = Self::with_capacity_bytes(map.engine(), capacity_bytes);
186
187	txn.extend(
188		items
189			.iter()
190			.map(|(map, key, val)| (*map, key, val)),
191	);
192
193	txn
194}
195
196/// Serializes and queues entries across maps from a nonempty pass.
197///
198/// The first item selects the database engine, and every map must belong to
199/// that same engine. All keys and values are encoded with the database record
200/// codec before being copied into the batch.
201///
202/// # Panics
203///
204/// Panics when `items` is empty, a map belongs to another database engine, or
205/// serialization of a key or value fails.
206#[implement(Txn)]
207pub fn put_each<'a, I, K, V>(items: I) -> Self
208where
209	I: IntoIterator<Item = (&'a Map, K, V)>,
210	K: Serialize + Debug,
211	V: Serialize,
212{
213	let mut items = items.into_iter();
214	let (map, key, val) = items.next().expect("put_each: at least one item");
215	let txn = Self::new(map.engine());
216
217	once((map, key, val))
218		.chain(items)
219		.fold(txn, |mut txn, (map, key, val)| {
220			txn.put(map, key, val);
221			txn
222		})
223}
224
225/// Serializes and queues one insertion.
226///
227/// The key and value use the database record codec, and the operation remains
228/// pending until [`Txn::execute`]. The map must belong to the transaction's
229/// database engine.
230///
231/// # Panics
232///
233/// Panics when the map belongs to another database engine or serialization of
234/// the key or value fails.
235#[implement(Txn)]
236pub fn put<K, V>(&mut self, map: &Map, key: K, val: V)
237where
238	K: Serialize + Debug,
239	V: Serialize,
240{
241	self.assert_map(map);
242
243	let key = serialize_key(key).expect("failed to serialize batch key");
244	let val = serialize_val(val).expect("failed to serialize batch val");
245
246	self.batch.put_cf(&map.cf(), key, val);
247}
248
249/// Serializes the key and queues one raw-value insertion.
250///
251/// The key uses the database record codec, while the value bytes are copied
252/// unchanged into the batch. The operation remains pending until
253/// [`Txn::execute`], and the map must belong to the transaction's database
254/// engine.
255///
256/// # Panics
257///
258/// Panics when the map belongs to another database engine or serialization of
259/// the key fails.
260#[implement(Txn)]
261pub fn put_raw<K, V>(&mut self, map: &Map, key: K, val: V)
262where
263	K: Serialize + Debug,
264	V: AsRef<[u8]>,
265{
266	self.assert_map(map);
267
268	let key = serialize_key(key).expect("failed to serialize batch key");
269
270	self.batch.put_cf(&map.cf(), key, val);
271}
272
273/// Queues one raw-key insertion after serializing the value.
274///
275/// The key bytes are copied unchanged into the batch, while the value uses the
276/// database record codec. The operation remains pending until [`Txn::execute`],
277/// and the map must belong to the transaction's database engine.
278///
279/// # Panics
280///
281/// Panics when the map belongs to another database engine or serialization of
282/// the value fails.
283#[implement(Txn)]
284pub fn raw_put<K, V>(&mut self, map: &Map, key: K, val: V)
285where
286	K: AsRef<[u8]>,
287	V: Serialize,
288{
289	self.assert_map(map);
290
291	let val = serialize_val(val).expect("failed to serialize batch val");
292
293	self.batch.put_cf(&map.cf(), key, val);
294}
295
296/// Serializes and queues one deletion.
297///
298/// The key uses the database record codec, and the operation remains pending
299/// until [`Txn::execute`]. The map must belong to the transaction's database
300/// engine.
301///
302/// # Panics
303///
304/// Panics when the map belongs to another database engine or serialization of
305/// the key fails.
306#[implement(Txn)]
307pub fn del<K>(&mut self, map: &Map, key: K)
308where
309	K: Serialize + Debug,
310{
311	self.assert_map(map);
312
313	let key = serialize_key(key).expect("failed to serialize batch key");
314
315	self.batch.delete_cf(&map.cf(), key);
316}
317
318/// Queues one deletion for an already serialized key.
319///
320/// The key bytes are copied into the write batch without invoking the database
321/// codec. The map must belong to the transaction's database engine.
322///
323/// # Panics
324///
325/// Panics when the map belongs to another database engine.
326#[implement(Txn)]
327pub fn del_raw<K>(&mut self, map: &Map, key: K)
328where
329	K: AsRef<[u8]>,
330{
331	self.assert_map(map);
332	self.batch.delete_cf(&map.cf(), key);
333}
334
335/// Commits the batch atomically, flushes unless corked, and notifies matching
336/// watchers.
337///
338/// An empty transaction returns without touching the engine. For a nonempty
339/// batch, notifications occur only after the write and any required flush
340/// succeed.
341///
342/// # Panics
343///
344/// Panics when RocksDB rejects the batch write or when the required database
345/// flush fails.
346#[implement(Txn)]
347#[tracing::instrument(
348	level = "trace",
349	skip_all,
350	fields(
351		ops = self.len(),
352		bytes = self.size_in_bytes(),
353	)
354)]
355pub fn execute(self) {
356	if self.is_empty() {
357		return;
358	}
359
360	self.engine
361		.db
362		.write_opt(&self.batch, &self.engine.write_options)
363		.or_else(or_else)
364		.expect("database transaction execute error");
365
366	if !self.engine.corked() {
367		self.engine.flush().expect("database flush error");
368	}
369
370	self.notify();
371}
372
373/// Notifies watchers after a successful commit for queued keys that resolve to
374/// catalog maps.
375///
376/// Keys are parsed lazily from the batch representation and consumed in queue
377/// order. Operations without a live map in the engine's startup catalog are
378/// skipped.
379#[implement(Txn)]
380fn notify(&self) {
381	for (map, key) in self.keys() {
382		map.notify(key);
383	}
384}
385
386/// Iterate queued put and delete keys in insertion order.
387///
388/// The iterator borrows keys directly from the serialized write batch without
389/// materializing a container. Keys whose column families are outside the
390/// startup map catalog are omitted.
391///
392/// # Panics
393///
394/// Iteration panics if a record has an unsupported operation tag, is truncated,
395/// or contains a varint whose fifth byte retains its continuation bit.
396#[implement(Txn)]
397pub fn keys(&self) -> impl Iterator<Item = (Arc<Map>, &[u8])> + '_ {
398	let data = self.batch.data();
399
400	Keys {
401		engine: &self.engine,
402		data: data.get(HEADER..).unwrap_or_default(),
403	}
404}
405
406/// Returns the number of operations queued in the batch.
407///
408/// Both insertions and deletions count as one operation. Inspecting the count
409/// does not execute the transaction.
410#[implement(Txn)]
411#[inline]
412#[must_use]
413pub fn len(&self) -> usize { self.batch.len() }
414
415/// Reports whether the batch contains no queued operations.
416///
417/// A newly created or cleared transaction is empty. Executing an empty
418/// transaction performs no database work.
419#[implement(Txn)]
420#[inline]
421#[must_use]
422pub fn is_empty(&self) -> bool { self.batch.is_empty() }
423
424/// Returns the encoded size of the RocksDB write batch in bytes.
425///
426/// The size includes batch metadata and queued record data. Inspecting it does
427/// not execute the transaction.
428#[implement(Txn)]
429#[inline]
430#[must_use]
431pub fn size_in_bytes(&self) -> usize { self.batch.size_in_bytes() }
432
433/// Removes every queued operation from the transaction.
434///
435/// The captured database engine remains attached, so the transaction can be
436/// populated again. Executing it before another operation is queued is a no-op.
437#[implement(Txn)]
438#[inline]
439pub fn clear(&mut self) { self.batch.clear(); }
440
441/// Queue one unencoded key and value after enforcing map ownership.
442///
443/// Both byte sequences are copied into the write batch without invoking the
444/// database codec. The operation remains pending until [`Txn::execute`].
445///
446/// # Panics
447///
448/// Panics when the map belongs to another database engine.
449#[implement(Txn)]
450pub fn insert_raw<K, V>(&mut self, map: &Map, key: K, val: V)
451where
452	K: AsRef<[u8]>,
453	V: AsRef<[u8]>,
454{
455	self.assert_map(map);
456	self.batch.put_cf(&map.cf(), key, val);
457}
458
459/// Verifies that a map belongs to the transaction's database engine.
460///
461/// RocksDB identifies column families numerically within one database, so
462/// accepting a foreign map could target a same-numbered column family in the
463/// captured engine.
464///
465/// # Panics
466///
467/// Panics when `map` belongs to a different database engine.
468#[implement(Txn)]
469#[inline]
470fn assert_map(&self, map: &Map) {
471	assert!(
472		Arc::ptr_eq(&self.engine, map.engine()),
473		"transaction map belongs to a different database"
474	);
475}
476
477impl<'a> Iterator for Keys<'a> {
478	type Item = (Arc<Map>, &'a [u8]);
479
480	fn next(&mut self) -> Option<Self::Item> {
481		while !self.data.is_empty() {
482			let (cf_id, key) =
483				next_record(&mut self.data).expect("malformed write batch representation");
484
485			if let Some(map) = self.engine.map_by_cf_id(cf_id) {
486				return Some((map, key));
487			}
488		}
489
490		None
491	}
492}
493
494/// Extends this transaction with raw insertions across maps.
495///
496/// Each tuple queues its raw key and value through [`Txn::insert_raw`]. Use
497/// [`Txn::put_each`] when the keys and values need serialization. Every map
498/// must belong to the transaction's database engine.
499///
500/// # Panics
501///
502/// Panics when any map belongs to another database engine.
503impl<'a, K, V> Extend<(&'a Map, K, V)> for Txn
504where
505	K: AsRef<[u8]>,
506	V: AsRef<[u8]>,
507{
508	fn extend<I>(&mut self, items: I)
509	where
510		I: IntoIterator<Item = (&'a Map, K, V)>,
511	{
512		for (map, key, val) in items {
513			self.insert_raw(map, key, val);
514		}
515	}
516}
517
518/// Extends this transaction with raw-key deletions across maps.
519///
520/// Each tuple queues its raw key through [`Txn::del_raw`]. Every map must
521/// belong to the transaction's database engine.
522///
523/// # Panics
524///
525/// Panics when any map belongs to another database engine.
526impl<'a, K> Extend<(&'a Map, K)> for Txn
527where
528	K: AsRef<[u8]>,
529{
530	fn extend<I>(&mut self, items: I)
531	where
532		I: IntoIterator<Item = (&'a Map, K)>,
533	{
534		for (map, key) in items {
535			self.del_raw(map, key);
536		}
537	}
538}
539
540/// Decodes one record into its column family identifier and borrowed key.
541///
542/// Value payloads are skipped after their lengths are consumed. Unsupported
543/// tags, truncated fields, and varints whose fifth byte retains its
544/// continuation bit return `None` and may leave the input advanced through the
545/// parsed prefix.
546pub(crate) fn next_record<'a>(data: &mut &'a [u8]) -> Option<(u32, &'a [u8])> {
547	let (&tag, rest) = data.split_first()?;
548	*data = rest;
549
550	let tag = Tag::try_from(tag).ok()?;
551
552	let cf_id = match tag {
553		| Tag::Value | Tag::Deletion => 0,
554		| Tag::CfValue | Tag::CfDeletion => take_varint32(data)?,
555	};
556
557	let key = take_varstring(data)?;
558
559	if matches!(tag, Tag::Value | Tag::CfValue) {
560		take_varstring(data)?;
561	}
562
563	Some((cf_id, key))
564}
565
566/// Takes one length-prefixed byte string from the front of a batch record.
567///
568/// The returned slice borrows the original batch representation, and `data`
569/// advances past it. Invalid lengths or truncated input return `None`.
570fn take_varstring<'a>(data: &mut &'a [u8]) -> Option<&'a [u8]> {
571	let len = take_varint32(data)?.try_into().ok()?;
572
573	let (string, rest) = data.split_at_checked(len)?;
574	*data = rest;
575
576	Some(string)
577}
578
579/// Takes one RocksDB varint32 from the front of a batch record.
580///
581/// The parser consumes at most five bytes and advances `data` as bytes are
582/// read. A missing byte or a continuation bit on the fifth byte returns `None`.
583fn take_varint32(data: &mut &[u8]) -> Option<u32> {
584	let mut result = 0_u32;
585
586	for shift in (0_u32..32).step_by(7) {
587		let (&byte, rest) = data.split_first()?;
588		*data = rest;
589		result |= u32::from(byte & 0x7F).checked_shl(shift)?;
590
591		if byte & 0x80 == 0 {
592			return Some(result);
593		}
594	}
595
596	None
597}
598
599/// Estimates write-batch capacity for a reusable sequence of raw pairs.
600///
601/// The estimate includes the fixed header, worst-case per-operation metadata,
602/// and payload lengths. Saturating arithmetic prevents an oversized input from
603/// wrapping the reservation.
604fn size_hint<'a, K, V, I>(items: I) -> usize
605where
606	I: Iterator<Item = (&'a K, &'a V)>,
607	K: AsRef<[u8]> + 'a,
608	V: AsRef<[u8]> + 'a,
609{
610	items.fold(HEADER, |capacity_bytes, (key, val)| {
611		capacity_bytes
612			.saturating_add(PER_OP)
613			.saturating_add(key.as_ref().len())
614			.saturating_add(val.as_ref().len())
615	})
616}