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}