reth_db/implementation/mdbx/
tx.rs1use super::{cursor::Cursor, utils::*, TableDbis};
4use crate::{
5 metrics::{DatabaseEnvMetrics, Operation, TransactionMode, TransactionOutcome},
6 DatabaseError, Tables,
7};
8use reth_db_api::{
9 table::{Compress, DupSort, Encode, IntoVec, Table, TableImporter},
10 transaction::{DbTx, DbTxMut},
11};
12use reth_libmdbx::{ffi::MDBX_dbi, CommitLatency, Transaction, TransactionKind, WriteFlags, RW};
13use reth_primitives_traits::FastInstant as Instant;
14use reth_storage_errors::db::{DatabaseWriteError, DatabaseWriteOperation};
15use std::{
16 backtrace::Backtrace,
17 marker::PhantomData,
18 sync::{
19 atomic::{AtomicBool, Ordering},
20 Arc,
21 },
22 time::Duration,
23};
24use tracing::{debug, instrument, trace, warn};
25
26const LONG_TRANSACTION_DURATION: Duration = Duration::from_secs(60);
28
29#[derive(Debug)]
31pub struct Tx<K: TransactionKind> {
32 inner: Transaction<K>,
34
35 dbis: Arc<TableDbis>,
37
38 metrics_handler: Option<MetricsHandler<K>>,
43 #[cfg(target_os = "linux")]
45 prefetch: AtomicBool,
46}
47
48impl<K: TransactionKind> Tx<K> {
49 #[inline]
51 #[track_caller]
52 pub(crate) fn new(
53 inner: Transaction<K>,
54 dbis: Arc<TableDbis>,
55 env_metrics: Option<Arc<DatabaseEnvMetrics>>,
56 ) -> reth_libmdbx::Result<Self> {
57 let metrics_handler = env_metrics
58 .map(|env_metrics| {
59 let handler = MetricsHandler::<K>::new(inner.id()?, env_metrics);
60 handler.env_metrics.record_opened_transaction(handler.transaction_mode());
61 handler.log_transaction_opened();
62 Ok(handler)
63 })
64 .transpose()?;
65 Ok(Self {
66 inner,
67 dbis,
68 metrics_handler,
69 #[cfg(target_os = "linux")]
70 prefetch: AtomicBool::new(false),
71 })
72 }
73
74 pub const fn inner(&self) -> &Transaction<K> {
76 &self.inner
77 }
78
79 pub fn id(&self) -> reth_libmdbx::Result<u64> {
81 self.metrics_handler.as_ref().map_or_else(|| self.inner.id(), |handler| Ok(handler.txn_id))
82 }
83
84 pub fn get_dbi_raw(&self, name: &str) -> Result<MDBX_dbi, DatabaseError> {
87 self.get_table_dbi(name.parse::<Tables>().ok(), name)
88 }
89
90 pub fn get_dbi<T: Table>(&self) -> Result<MDBX_dbi, DatabaseError> {
93 self.get_table_dbi(T::TABLE, T::NAME)
94 }
95
96 fn get_table_dbi(&self, table: Option<Tables>, name: &str) -> Result<MDBX_dbi, DatabaseError> {
99 if let Some(dbi) = self.dbis.get(table, name) {
100 Ok(dbi)
101 } else {
102 self.inner
103 .open_db(Some(name))
104 .map(|db| db.dbi())
105 .map_err(|e| DatabaseError::Open(e.into()))
106 }
107 }
108
109 pub fn new_cursor<T: Table>(&self) -> Result<Cursor<K, T>, DatabaseError> {
111 let inner = self
112 .inner
113 .cursor_with_dbi(self.get_dbi::<T>()?)
114 .map_err(|e| DatabaseError::InitCursor(e.into()))?;
115
116 Ok(Cursor::new_with_metrics(
117 inner,
118 self.metrics_handler
119 .as_ref()
120 .zip(T::TABLE)
121 .map(|(h, table)| h.env_metrics.table_operation_metrics(table)),
122 #[cfg(target_os = "linux")]
123 self.prefetch.load(Ordering::Relaxed),
124 ))
125 }
126
127 fn execute_with_close_transaction_metric<R>(
132 mut self,
133 outcome: TransactionOutcome,
134 f: impl FnOnce(Self) -> (R, Option<CommitLatency>),
135 ) -> R {
136 let run = |tx| {
137 let start = Instant::now();
138 let (result, commit_latency) = f(tx);
139 let total_duration = start.elapsed();
140
141 if outcome.is_commit() {
142 debug!(
143 target: "storage::db::mdbx",
144 ?total_duration,
145 ?commit_latency,
146 is_read_only = K::IS_READ_ONLY,
147 "Commit"
148 );
149 }
150
151 (result, commit_latency, total_duration)
152 };
153
154 if let Some(mut metrics_handler) = self.metrics_handler.take() {
155 metrics_handler.close_recorded = true;
156 metrics_handler.log_backtrace_on_long_read_transaction();
157
158 let (result, commit_latency, close_duration) = run(self);
159 let open_duration = metrics_handler.start.elapsed();
160 metrics_handler.env_metrics.record_closed_transaction(
161 metrics_handler.transaction_mode(),
162 outcome,
163 open_duration,
164 Some(close_duration),
165 commit_latency,
166 );
167
168 result
169 } else {
170 run(self).0
171 }
172 }
173
174 fn execute_with_operation_metric<T: Table, R>(
179 &self,
180 operation: Operation,
181 value_size: Option<usize>,
182 f: impl FnOnce(&Transaction<K>) -> R,
183 ) -> R {
184 if let Some(metrics_handler) = &self.metrics_handler {
185 metrics_handler.log_backtrace_on_long_read_transaction();
186 metrics_handler
187 .env_metrics
188 .record_operation(T::TABLE, operation, value_size, || f(&self.inner))
189 } else {
190 f(&self.inner)
191 }
192 }
193}
194
195#[derive(Debug)]
196struct MetricsHandler<K: TransactionKind> {
197 txn_id: u64,
199 start: Instant,
206 long_transaction_deadline: Instant,
211 close_recorded: bool,
214 record_backtrace: bool,
217 backtrace_recorded: AtomicBool,
220 env_metrics: Arc<DatabaseEnvMetrics>,
222 #[cfg(all(debug_assertions, not(feature = "fuzz")))]
225 open_backtrace: Backtrace,
226 _marker: PhantomData<K>,
227}
228
229impl<K: TransactionKind> MetricsHandler<K> {
230 fn new(txn_id: u64, env_metrics: Arc<DatabaseEnvMetrics>) -> Self {
231 let start = Instant::now();
232 Self {
233 txn_id,
234 start,
235 long_transaction_deadline: start + LONG_TRANSACTION_DURATION,
236 close_recorded: false,
237 record_backtrace: true,
238 backtrace_recorded: AtomicBool::new(false),
239 #[cfg(all(debug_assertions, not(feature = "fuzz")))]
240 open_backtrace: Backtrace::force_capture(),
241 env_metrics,
242 _marker: PhantomData,
243 }
244 }
245
246 const fn transaction_mode(&self) -> TransactionMode {
247 if K::IS_READ_ONLY {
248 TransactionMode::ReadOnly
249 } else {
250 TransactionMode::ReadWrite
251 }
252 }
253
254 #[track_caller]
256 fn log_transaction_opened(&self) {
257 trace!(
258 target: "storage::db::mdbx",
259 caller = %core::panic::Location::caller(),
260 id = %self.txn_id,
261 mode = %self.transaction_mode().as_str(),
262 "Transaction opened",
263 );
264 }
265
266 fn log_backtrace_on_long_read_transaction(&self) {
273 if self.record_backtrace &&
274 !self.backtrace_recorded.load(Ordering::Relaxed) &&
275 self.transaction_mode().is_read_only()
276 {
277 let now = Instant::now();
278 if now >= self.long_transaction_deadline {
279 let open_duration = now - self.start;
280 self.backtrace_recorded.store(true, Ordering::Relaxed);
281 #[cfg(all(debug_assertions, not(feature = "fuzz")))]
282 let open_backtrace = format_args!("{}", self.open_backtrace);
283 #[cfg(not(all(debug_assertions, not(feature = "fuzz"))))]
284 let open_backtrace = tracing::field::Empty;
285 warn!(
286 target: "storage::db::mdbx",
287 ?open_duration,
288 id=%self.txn_id,
289 backtrace=%Backtrace::force_capture(),
290 open_backtrace,
291 "A database read transaction has been open for too long"
292 );
293 }
294 }
295 }
296}
297
298impl<K: TransactionKind> Drop for MetricsHandler<K> {
299 fn drop(&mut self) {
300 if !self.close_recorded {
301 self.log_backtrace_on_long_read_transaction();
302 self.env_metrics.record_closed_transaction(
303 self.transaction_mode(),
304 TransactionOutcome::Drop,
305 self.start.elapsed(),
306 None,
307 None,
308 );
309 }
310 }
311}
312
313impl TableImporter for Tx<RW> {}
314
315impl<K: TransactionKind> DbTx for Tx<K> {
316 type Cursor<T: Table> = Cursor<K, T>;
317 type DupCursor<T: DupSort> = Cursor<K, T>;
318
319 fn get<T: Table>(&self, key: T::Key) -> Result<Option<<T as Table>::Value>, DatabaseError> {
320 self.get_by_encoded_key::<T>(&key.encode())
321 }
322
323 fn get_by_encoded_key<T: Table>(
324 &self,
325 key: &<T::Key as Encode>::Encoded,
326 ) -> Result<Option<T::Value>, DatabaseError> {
327 self.execute_with_operation_metric::<T, _>(Operation::Get, None, |tx| {
328 let dbi = self.get_dbi::<T>()?;
329 #[cfg(target_os = "linux")]
330 let value = if self.prefetch.load(Ordering::Relaxed) {
331 tx.get::<super::value_prefetch::PrefetchValue<'_>>(dbi, key.as_ref())
332 .map(|value| value.map(|value| value.0))
333 } else {
334 tx.get(dbi, key.as_ref())
335 };
336 #[cfg(not(target_os = "linux"))]
337 let value = tx.get(dbi, key.as_ref());
338 value.map_err(|e| DatabaseError::Read(e.into()))?.map(decode_one::<T>).transpose()
339 })
340 }
341
342 #[instrument(name = "Tx::commit", level = "debug", target = "providers::db", skip_all)]
343 fn commit(self) -> Result<(), DatabaseError> {
344 self.execute_with_close_transaction_metric(TransactionOutcome::Commit, |this| {
345 match this.inner.commit().map_err(|e| DatabaseError::Commit(e.into())) {
346 Ok(latency) => (Ok(()), Some(latency)),
347 Err(e) => (Err(e), None),
348 }
349 })
350 }
351
352 fn abort(self) {
353 self.execute_with_close_transaction_metric(TransactionOutcome::Abort, |this| {
354 (drop(this.inner), None)
355 })
356 }
357
358 fn cursor_read<T: Table>(&self) -> Result<Self::Cursor<T>, DatabaseError> {
360 self.new_cursor()
361 }
362
363 fn cursor_dup_read<T: DupSort>(&self) -> Result<Self::DupCursor<T>, DatabaseError> {
365 self.new_cursor()
366 }
367
368 fn entries<T: Table>(&self) -> Result<usize, DatabaseError> {
370 Ok(self
371 .inner
372 .db_stat_with_dbi(self.get_dbi::<T>()?)
373 .map_err(|e| DatabaseError::Stats(e.into()))?
374 .entries())
375 }
376
377 fn disable_long_read_transaction_safety(&mut self) {
380 if let Some(metrics_handler) = self.metrics_handler.as_mut() {
381 metrics_handler.record_backtrace = false;
382 }
383
384 self.inner.disable_timeout();
385 }
386
387 #[cfg(target_os = "linux")]
388 fn prefetch(&self, enabled: bool) -> &Self {
389 self.prefetch.store(enabled, Ordering::Relaxed);
390 self
391 }
392}
393
394#[derive(Clone, Copy)]
395enum PutKind {
396 Upsert,
398 Append,
401}
402
403impl PutKind {
404 const fn into_operation_and_flags(self) -> (Operation, DatabaseWriteOperation, WriteFlags) {
405 match self {
406 Self::Upsert => {
407 (Operation::PutUpsert, DatabaseWriteOperation::PutUpsert, WriteFlags::UPSERT)
408 }
409 Self::Append => {
410 (Operation::PutAppend, DatabaseWriteOperation::PutAppend, WriteFlags::APPEND)
411 }
412 }
413 }
414}
415
416impl Tx<RW> {
417 fn put<T: Table>(
420 &self,
421 kind: PutKind,
422 key: T::Key,
423 value: T::Value,
424 ) -> Result<(), DatabaseError> {
425 let key = key.encode();
426 let value = value.compress();
427 let (operation, write_operation, flags) = kind.into_operation_and_flags();
428 self.execute_with_operation_metric::<T, _>(operation, Some(value.as_ref().len()), |tx| {
429 tx.put(self.get_dbi::<T>()?, key.as_ref(), value, flags).map_err(|e| {
430 DatabaseWriteError {
431 info: e.into(),
432 operation: write_operation,
433 table_name: T::NAME,
434 key: key.into_vec(),
435 }
436 .into()
437 })
438 })
439 }
440}
441
442impl DbTxMut for Tx<RW> {
443 type CursorMut<T: Table> = Cursor<RW, T>;
444 type DupCursorMut<T: DupSort> = Cursor<RW, T>;
445
446 fn put<T: Table>(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError> {
447 self.put::<T>(PutKind::Upsert, key, value)
448 }
449
450 fn append<T: Table>(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError> {
451 self.put::<T>(PutKind::Append, key, value)
452 }
453
454 fn delete<T: Table>(
455 &self,
456 key: T::Key,
457 value: Option<T::Value>,
458 ) -> Result<bool, DatabaseError> {
459 let mut data = None;
460
461 let value = value.map(Compress::compress);
462 if let Some(value) = &value {
463 data = Some(value.as_ref());
464 };
465
466 self.execute_with_operation_metric::<T, _>(Operation::Delete, None, |tx| {
467 tx.del(self.get_dbi::<T>()?, key.encode(), data)
468 .map_err(|e| DatabaseError::Delete(e.into()))
469 })
470 }
471
472 fn clear<T: Table>(&self) -> Result<(), DatabaseError> {
473 self.inner.clear_db(self.get_dbi::<T>()?).map_err(|e| DatabaseError::Delete(e.into()))?;
474
475 Ok(())
476 }
477
478 fn cursor_write<T: Table>(&self) -> Result<Self::CursorMut<T>, DatabaseError> {
479 self.new_cursor()
480 }
481
482 fn cursor_dup_write<T: DupSort>(&self) -> Result<Self::DupCursorMut<T>, DatabaseError> {
483 self.new_cursor()
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use crate::{mdbx::DatabaseArguments, tables, DatabaseEnv, DatabaseEnvKind};
490 use reth_db_api::{database::Database, models::ClientVersion, transaction::DbTx};
491 use reth_libmdbx::MaxReadTransactionDuration;
492 use reth_storage_errors::db::DatabaseError;
493 use std::{sync::atomic::Ordering, thread::sleep, time::Duration};
494 use tempfile::tempdir;
495
496 #[test]
497 fn long_read_transaction_safety_disabled() {
498 const MAX_DURATION: Duration = Duration::from_secs(1);
499
500 let dir = tempdir().unwrap();
501 let args = DatabaseArguments::new(ClientVersion::default())
502 .with_max_read_transaction_duration(Some(MaxReadTransactionDuration::Set(
503 MAX_DURATION,
504 )));
505 let db = DatabaseEnv::open(dir.path(), DatabaseEnvKind::RW, args)
506 .unwrap()
507 .with_metrics()
508 .with_disable_long_read_transaction_safety();
509
510 let mut tx = db.tx().unwrap();
511 let metrics_handler = tx.metrics_handler.as_mut().unwrap();
512 metrics_handler.long_transaction_deadline = metrics_handler.start + MAX_DURATION;
513 sleep(MAX_DURATION + Duration::from_millis(100));
515
516 assert!(matches!(
518 tx.get::<tables::Transactions>(0).unwrap_err(),
519 DatabaseError::Open(err) if err == reth_libmdbx::Error::NotFound.into()));
520 assert!(!tx.metrics_handler.unwrap().backtrace_recorded.load(Ordering::Relaxed));
522 }
523
524 #[test]
525 fn long_read_transaction_safety_enabled() {
526 const MAX_DURATION: Duration = Duration::from_secs(1);
527
528 let dir = tempdir().unwrap();
529 let args = DatabaseArguments::new(ClientVersion::default())
530 .with_max_read_transaction_duration(Some(MaxReadTransactionDuration::Set(
531 MAX_DURATION,
532 )));
533 let db = DatabaseEnv::open(dir.path(), DatabaseEnvKind::RW, args).unwrap().with_metrics();
534
535 let mut tx = db.tx().unwrap();
536 let metrics_handler = tx.metrics_handler.as_mut().unwrap();
537 metrics_handler.long_transaction_deadline = metrics_handler.start + MAX_DURATION;
538 sleep(MAX_DURATION + Duration::from_millis(100));
540
541 assert!(matches!(
543 tx.get::<tables::Transactions>(0).unwrap_err(),
544 DatabaseError::Open(err) if err == reth_libmdbx::Error::ReadTransactionTimeout.into()));
545 assert!(tx.metrics_handler.unwrap().backtrace_recorded.load(Ordering::Relaxed));
547 }
548}