1use crate::{
2 environment::EnvPtr,
3 error::{mdbx_result, Result},
4 CommitLatency,
5};
6use std::{
7 fmt, ptr,
8 sync::mpsc::{sync_channel, Receiver, SyncSender},
9};
10
11#[derive(Copy, Clone, Debug)]
12pub(crate) struct TxnPtr(pub(crate) *mut ffi::MDBX_txn);
13unsafe impl Send for TxnPtr {}
14unsafe impl Sync for TxnPtr {}
15
16pub(crate) enum TxnManagerMessage {
17 Begin { parent: TxnPtr, flags: ffi::MDBX_txn_flags_t, sender: SyncSender<Result<TxnPtr>> },
18 Abort { tx: TxnPtr, sender: SyncSender<Result<bool>> },
19 Commit { tx: TxnPtr, sender: SyncSender<Result<(bool, CommitLatency)>> },
20}
21
22impl fmt::Debug for TxnManagerMessage {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Self::Begin { parent, flags, sender: _ } => {
26 f.debug_struct("Begin").field("parent", parent).field("flags", flags).finish()
27 }
28 Self::Abort { tx, sender: _ } => f.debug_struct("Abort").field("tx", tx).finish(),
29 Self::Commit { tx, sender: _ } => f.debug_struct("Commit").field("tx", tx).finish(),
30 }
31 }
32}
33
34#[derive(Debug)]
40pub(crate) struct TxnManager {
41 sender: SyncSender<TxnManagerMessage>,
42 #[cfg(feature = "read-tx-timeouts")]
43 read_transactions: Option<std::sync::Arc<read_transactions::ReadTransactions>>,
44}
45
46impl TxnManager {
47 pub(crate) fn new(env: EnvPtr) -> Self {
48 let (tx, rx) = sync_channel(0);
49 let txn_manager = Self {
50 sender: tx,
51 #[cfg(feature = "read-tx-timeouts")]
52 read_transactions: None,
53 };
54
55 txn_manager.start_message_listener(env, rx);
56
57 txn_manager
58 }
59
60 fn start_message_listener(&self, env: EnvPtr, rx: Receiver<TxnManagerMessage>) {
67 let task = move || {
68 let env = env;
69 loop {
70 let msg = rx.recv();
71 tracing::debug!(target: "libmdbx::txn", ?msg, "txn-mngr received");
72 match msg {
73 Ok(msg) => match msg {
74 TxnManagerMessage::Begin { parent, flags, sender } => {
75 let _span =
76 tracing::debug_span!(target: "libmdbx::txn", "begin", flags)
77 .entered();
78 let mut txn: *mut ffi::MDBX_txn = ptr::null_mut();
79 let res = mdbx_result(unsafe {
80 ffi::mdbx_txn_begin_ex(
81 env.0,
82 parent.0,
83 flags,
84 &mut txn,
85 ptr::null_mut(),
86 )
87 })
88 .map(|_| TxnPtr(txn));
89 sender.send(res).unwrap();
90 }
91 TxnManagerMessage::Abort { tx, sender } => {
92 let _span =
93 tracing::debug_span!(target: "libmdbx::txn", "abort").entered();
94 sender.send(mdbx_result(unsafe { ffi::mdbx_txn_abort(tx.0) })).unwrap();
95 }
96 TxnManagerMessage::Commit { tx, sender } => {
97 let _span =
98 tracing::debug_span!(target: "libmdbx::txn", "commit").entered();
99 sender
100 .send({
101 let mut latency = CommitLatency::new();
102 mdbx_result(unsafe {
103 ffi::mdbx_txn_commit_ex(tx.0, latency.mdbx_commit_latency())
104 })
105 .map(|v| (v, latency))
106 })
107 .unwrap();
108 }
109 },
110 Err(_) => return,
111 }
112 }
113 };
114 std::thread::Builder::new().name("mdbx-rs-txn-mgr".to_string()).spawn(task).unwrap();
115 }
116
117 pub(crate) fn send_message(&self, message: TxnManagerMessage) {
118 self.sender.send(message).unwrap()
119 }
120}
121
122#[cfg(feature = "read-tx-timeouts")]
123mod read_transactions {
124 use crate::{
125 environment::EnvPtr, error::mdbx_result, transaction::TransactionPtr,
126 txn_manager::TxnManager,
127 };
128 use dashmap::{DashMap, DashSet};
129 use std::{
130 backtrace::Backtrace,
131 sync::{mpsc::sync_channel, Arc},
132 time::{Duration, Instant},
133 };
134 use tracing::{error, trace, warn};
135
136 const READ_TRANSACTIONS_CHECK_INTERVAL: Duration = Duration::from_secs(5);
137
138 impl TxnManager {
139 pub(crate) fn new_with_max_read_transaction_duration(
142 env: EnvPtr,
143 duration: Duration,
144 ) -> Self {
145 let read_transactions = Arc::new(ReadTransactions::new(duration));
146 read_transactions.clone().start_monitor();
147
148 let (tx, rx) = sync_channel(0);
149
150 let txn_manager = Self { sender: tx, read_transactions: Some(read_transactions) };
151
152 txn_manager.start_message_listener(env, rx);
153
154 txn_manager
155 }
156
157 pub(crate) fn add_active_read_transaction(
159 &self,
160 ptr: *mut ffi::MDBX_txn,
161 tx: TransactionPtr,
162 ) {
163 if let Some(read_transactions) = &self.read_transactions {
164 read_transactions.add_active(ptr, tx);
165 }
166 }
167
168 pub(crate) fn remove_active_read_transaction(&self, ptr: *mut ffi::MDBX_txn) -> bool {
172 self.read_transactions.as_ref().is_some_and(|txs| txs.remove_active(ptr))
173 }
174
175 pub(crate) fn timed_out_not_aborted_read_transactions(&self) -> Option<usize> {
177 self.read_transactions
178 .as_ref()
179 .map(|read_transactions| read_transactions.timed_out_not_aborted())
180 }
181 }
182
183 #[derive(Debug, Default)]
184 pub(super) struct ReadTransactions {
185 max_duration: Duration,
188 active: DashMap<usize, (TransactionPtr, Instant, Option<Arc<Backtrace>>)>,
196 timed_out_not_aborted: DashSet<usize>,
199 }
200
201 impl ReadTransactions {
202 pub(super) fn new(max_duration: Duration) -> Self {
203 Self { max_duration, ..Default::default() }
204 }
205
206 pub(super) fn add_active(&self, ptr: *mut ffi::MDBX_txn, tx: TransactionPtr) {
208 let _ = self.active.insert(
209 ptr as usize,
210 (
211 tx,
212 Instant::now(),
213 cfg!(all(debug_assertions, not(feature = "dst")))
214 .then(|| Arc::new(Backtrace::force_capture())),
215 ),
216 );
217 }
218
219 pub(super) fn remove_active(&self, ptr: *mut ffi::MDBX_txn) -> bool {
221 self.timed_out_not_aborted.remove(&(ptr as usize));
222 self.active.remove(&(ptr as usize)).is_some()
223 }
224
225 pub(super) fn timed_out_not_aborted(&self) -> usize {
227 self.timed_out_not_aborted.len()
228 }
229
230 pub(super) fn start_monitor(self: Arc<Self>) {
233 let task = move || {
234 let mut timed_out_active = Vec::new();
235
236 loop {
237 let now = Instant::now();
238 let mut max_active_transaction_duration = None;
239
240 for entry in &self.active {
243 let (tx, start, backtrace) = entry.value();
244 let duration = now - *start;
245
246 if duration > self.max_duration {
247 let result = tx.txn_execute_fail_on_timeout(|txn_ptr| {
248 let result = mdbx_result(unsafe { ffi::mdbx_txn_reset(txn_ptr) });
258 if result.is_ok() {
259 tx.set_timed_out();
260 }
261 (txn_ptr, duration, result)
262 });
263
264 match result {
265 Ok((txn_ptr, duration, error)) => {
266 timed_out_active.push((
270 txn_ptr,
271 duration,
272 backtrace.clone(),
273 error,
274 ));
275 }
276 Err(err) => {
277 error!(target: "libmdbx", %err, ?backtrace, "Failed to abort the long-lived read transaction")
278 }
279 }
280 } else {
281 max_active_transaction_duration = Some(
282 duration.max(max_active_transaction_duration.unwrap_or_default()),
283 );
284 }
285 }
286
287 for (ptr, open_duration, backtrace, err) in timed_out_active.iter().cloned() {
290 let was_in_active = self.remove_active(ptr);
292 if let Err(err) = err {
293 if was_in_active {
294 error!(target: "libmdbx", %err, ?open_duration, ?backtrace, "Failed to time out the long-lived read transaction");
297 }
298 } else {
299 warn!(target: "libmdbx", ?open_duration, ?backtrace, "Long-lived read transaction has been timed out");
301 self.timed_out_not_aborted.insert(ptr as usize);
304 }
305 }
306
307 timed_out_active.clear();
310
311 if !self.active.is_empty() {
312 trace!(
313 target: "libmdbx",
314 elapsed = ?now.elapsed(),
315 active = ?self.active.iter().map(|entry| {
316 let (tx, start, _) = entry.value();
317 (tx.clone(), start.elapsed())
318 }).collect::<Vec<_>>(),
319 "Read transactions"
320 );
321 }
322
323 let sleep_duration = READ_TRANSACTIONS_CHECK_INTERVAL.min(
326 self.max_duration - max_active_transaction_duration.unwrap_or_default(),
327 );
328 trace!(target: "libmdbx", ?sleep_duration, elapsed = ?now.elapsed(), "Putting transaction monitor to sleep");
329 std::thread::sleep(sleep_duration);
330 }
331 };
332 std::thread::Builder::new()
333 .name("mdbx-rs-read-tx-timeouts".to_string())
334 .spawn(task)
335 .unwrap();
336 }
337 }
338
339 #[cfg(test)]
340 mod tests {
341 use crate::{
342 txn_manager::read_transactions::READ_TRANSACTIONS_CHECK_INTERVAL, Environment, Error,
343 MaxReadTransactionDuration,
344 };
345 use std::{
346 thread::sleep,
347 time::{Duration, Instant},
348 };
349 use tempfile::tempdir;
350
351 #[test]
352 fn txn_manager_read_transactions_duration_set() {
353 const MAX_DURATION: Duration = Duration::from_secs(1);
354
355 let dir = tempdir().unwrap();
356 let env = Environment::builder()
357 .set_max_read_transaction_duration(MaxReadTransactionDuration::Set(MAX_DURATION))
358 .open(dir.path())
359 .unwrap();
360
361 let read_transactions = env.txn_manager().read_transactions.as_ref().unwrap();
362
363 {
365 let tx = env.begin_ro_txn().unwrap();
366 let tx_ptr = tx.txn() as usize;
367 assert!(read_transactions.active.contains_key(&tx_ptr));
368 assert_eq!(
369 read_transactions.active.get(&tx_ptr).unwrap().2.is_some(),
370 cfg!(all(debug_assertions, not(feature = "dst"))),
371 );
372
373 tx.open_db(None).unwrap();
374 drop(tx);
375
376 assert!(!read_transactions.active.contains_key(&tx_ptr));
377 }
378
379 {
381 let tx = env.begin_ro_txn().unwrap();
382 let tx_ptr = tx.txn() as usize;
383 assert!(read_transactions.active.contains_key(&tx_ptr));
384
385 tx.open_db(None).unwrap();
386 tx.commit().unwrap();
387
388 assert!(!read_transactions.active.contains_key(&tx_ptr));
389 }
390
391 {
392 let tx = env.begin_ro_txn().unwrap();
395 let tx_ptr = tx.txn() as usize;
396 assert!(read_transactions.active.contains_key(&tx_ptr));
397
398 let deadline = Instant::now() + MAX_DURATION + READ_TRANSACTIONS_CHECK_INTERVAL;
400 while !read_transactions.timed_out_not_aborted.contains(&tx_ptr) {
401 assert!(Instant::now() < deadline, "transaction was not timed out");
402 sleep(Duration::from_millis(10));
403 }
404
405 assert!(!read_transactions.active.contains_key(&tx_ptr));
408 assert!(read_transactions.timed_out_not_aborted.contains(&tx_ptr));
409
410 assert_eq!(tx.open_db(None).err(), Some(Error::ReadTransactionTimeout));
412 assert!(!read_transactions.active.contains_key(&tx_ptr));
413 assert!(read_transactions.timed_out_not_aborted.contains(&tx_ptr));
414
415 assert_eq!(tx.id().err(), Some(Error::ReadTransactionTimeout));
416 assert!(!read_transactions.active.contains_key(&tx_ptr));
417 assert!(read_transactions.timed_out_not_aborted.contains(&tx_ptr));
418
419 let new_tx = env.begin_ro_txn().unwrap();
422 let new_tx_ptr = new_tx.txn() as usize;
423 assert!(read_transactions.active.contains_key(&new_tx_ptr));
424 assert_ne!(tx_ptr, new_tx_ptr);
425
426 drop(tx);
429 assert!(!read_transactions.timed_out_not_aborted.contains(&tx_ptr));
430 }
431 }
432
433 #[test]
434 fn txn_manager_read_transactions_duration_unbounded() {
435 let dir = tempdir().unwrap();
436 let env = Environment::builder()
437 .set_max_read_transaction_duration(MaxReadTransactionDuration::Unbounded)
438 .open(dir.path())
439 .unwrap();
440
441 assert!(env.txn_manager().read_transactions.is_none());
442
443 let tx = env.begin_ro_txn().unwrap();
444 assert!(tx.commit().is_ok())
445 }
446 }
447}