1#![cfg(feature = "sql-data-source")]
14use std::{cmp::min, fmt::Debug, future::Future, str::FromStr, time::Duration};
15
16use anyhow::Context;
17use async_trait::async_trait;
18use chrono::Utc;
19#[cfg(not(feature = "embedded-db"))]
20use futures::future::FutureExt;
21use hotshot_types::{
22 data::VidShare,
23 traits::{metrics::Metrics, node_implementation::NodeType},
24};
25use itertools::Itertools;
26use log::LevelFilter;
27use rand::Rng;
28#[cfg(not(feature = "embedded-db"))]
29use sqlx::postgres::{PgConnectOptions, PgSslMode};
30#[cfg(feature = "embedded-db")]
31use sqlx::sqlite::SqliteConnectOptions;
32use sqlx::{
33 ConnectOptions, Row,
34 pool::{Pool, PoolOptions},
35};
36use tokio::time::sleep;
37use tracing::instrument;
38
39use crate::{
40 Header, QueryError, QueryResult,
41 availability::{QueryableHeader, QueryablePayload, VidCommonMetadata, VidCommonQueryData},
42 data_source::{
43 VersionedDataSource,
44 storage::{
45 SerializableRetry,
46 pruning::{PruneStorage, PrunedHeightStorage, PrunerCfg, PrunerConfig},
47 },
48 update::Transaction as _,
49 },
50 metrics::PrometheusMetrics,
51 node::BlockId,
52 status::HasMetrics,
53};
54pub extern crate sqlx;
55pub use sqlx::{Database, Sqlite};
56
57mod db;
58mod migrate;
59mod queries;
60mod transaction;
61
62pub use anyhow::Error;
63pub use db::*;
64pub use include_dir::include_dir;
65pub use queries::QueryBuilder;
66pub use refinery::Migration;
67pub use transaction::*;
68
69use self::{migrate::Migrator, transaction::PoolMetrics};
70use super::{AvailabilityStorage, NodeStorage};
71pub use crate::include_migrations;
75
76#[macro_export]
119macro_rules! include_migrations {
120 ($dir:tt) => {
121 $crate::data_source::storage::sql::include_dir!($dir)
122 .files()
123 .map(|file| {
124 let path = file.path();
125 let name = path
126 .file_name()
127 .and_then(std::ffi::OsStr::to_str)
128 .unwrap_or_else(|| {
129 panic!(
130 "migration file {} must have a non-empty UTF-8 name",
131 path.display()
132 )
133 });
134 let sql = file
135 .contents_utf8()
136 .unwrap_or_else(|| panic!("migration file {name} must use UTF-8 encoding"));
137 $crate::data_source::storage::sql::Migration::unapplied(name, sql)
138 .expect("invalid migration")
139 })
140 };
141}
142
143pub fn default_migrations() -> Vec<Migration> {
145 #[cfg(not(feature = "embedded-db"))]
146 let mut migrations =
147 include_migrations!("$CARGO_MANIFEST_DIR/migrations/postgres").collect::<Vec<_>>();
148
149 #[cfg(feature = "embedded-db")]
150 let mut migrations =
151 include_migrations!("$CARGO_MANIFEST_DIR/migrations/sqlite").collect::<Vec<_>>();
152
153 validate_migrations(&mut migrations).expect("default migrations are invalid");
155
156 for m in &migrations {
159 if m.version() <= 30 {
160 assert!(
164 m.version() > 0 && m.version() % 10 == 0,
165 "legacy default migration version {} is not a positive multiple of 10",
166 m.version()
167 );
168 } else {
169 assert!(
170 m.version() % 100 == 0,
171 "default migration version {} is not a multiple of 100",
172 m.version()
173 );
174 }
175 }
176
177 migrations
178}
179
180fn validate_migrations(migrations: &mut [Migration]) -> Result<(), Error> {
185 migrations.sort_by_key(|m| m.version());
186
187 for (prev, next) in migrations.iter().zip(migrations.iter().skip(1)) {
189 if next <= prev {
190 return Err(Error::msg(format!(
191 "migration versions are not strictly increasing ({prev}->{next})"
192 )));
193 }
194 }
195
196 Ok(())
197}
198
199fn add_custom_migrations(
206 default: impl IntoIterator<Item = Migration>,
207 custom: impl IntoIterator<Item = Migration>,
208) -> impl Iterator<Item = Migration> {
209 default
210 .into_iter()
211 .merge_join_by(custom, |l, r| l.version().cmp(&r.version()))
213 .map(|pair| pair.reduce(|_, custom| custom))
216}
217
218#[derive(Clone)]
219pub struct Config {
220 #[cfg(feature = "embedded-db")]
221 db_opt: SqliteConnectOptions,
222
223 #[cfg(not(feature = "embedded-db"))]
224 db_opt: PgConnectOptions,
225
226 pool_opt: PoolOptions<Db>,
227
228 #[cfg(not(feature = "embedded-db"))]
230 pool_opt_query: PoolOptions<Db>,
231
232 #[cfg(not(feature = "embedded-db"))]
233 schema: String,
234 reset: bool,
235 migrations: Vec<Migration>,
236 no_migrations: bool,
237 pruner_cfg: Option<PrunerCfg>,
238 archive: bool,
239 serializable_retry_config: SerializableRetryConfig,
240 pool: Option<Pool<Db>>,
241}
242
243#[cfg(not(feature = "embedded-db"))]
244impl Default for Config {
245 fn default() -> Self {
246 PgConnectOptions::default()
247 .username("postgres")
248 .password("password")
249 .host("localhost")
250 .port(5432)
251 .into()
252 }
253}
254
255#[cfg(feature = "embedded-db")]
256impl Default for Config {
257 fn default() -> Self {
258 crate::sqlite_options::sqlite_options().into()
259 }
260}
261
262#[cfg(feature = "embedded-db")]
263impl From<SqliteConnectOptions> for Config {
264 fn from(db_opt: SqliteConnectOptions) -> Self {
265 Self {
266 db_opt,
267 pool_opt: PoolOptions::default(),
268 reset: false,
269 migrations: vec![],
270 no_migrations: false,
271 pruner_cfg: None,
272 archive: false,
273 serializable_retry_config: SerializableRetryConfig::default(),
274 pool: None,
275 }
276 }
277}
278
279#[cfg(not(feature = "embedded-db"))]
280impl From<PgConnectOptions> for Config {
281 fn from(db_opt: PgConnectOptions) -> Self {
282 Self {
283 db_opt,
284 pool_opt: PoolOptions::default(),
285 pool_opt_query: PoolOptions::default(),
286 schema: "hotshot".into(),
287 reset: false,
288 migrations: vec![],
289 no_migrations: false,
290 pruner_cfg: None,
291 archive: false,
292 serializable_retry_config: SerializableRetryConfig::default(),
293 pool: None,
294 }
295 }
296}
297
298#[cfg(not(feature = "embedded-db"))]
299impl FromStr for Config {
300 type Err = <PgConnectOptions as FromStr>::Err;
301
302 fn from_str(s: &str) -> Result<Self, Self::Err> {
303 Ok(PgConnectOptions::from_str(s)?.into())
304 }
305}
306
307#[cfg(feature = "embedded-db")]
308impl FromStr for Config {
309 type Err = <SqliteConnectOptions as FromStr>::Err;
310
311 fn from_str(s: &str) -> Result<Self, Self::Err> {
312 Ok(SqliteConnectOptions::from_str(s)?.into())
313 }
314}
315
316#[cfg(feature = "embedded-db")]
317impl Config {
318 pub fn busy_timeout(mut self, timeout: Duration) -> Self {
319 self.db_opt = self.db_opt.busy_timeout(timeout);
320 self
321 }
322
323 pub fn db_path(mut self, path: std::path::PathBuf) -> Self {
324 self.db_opt = self.db_opt.filename(path);
325 self
326 }
327}
328
329#[cfg(not(feature = "embedded-db"))]
330impl Config {
331 pub fn host(mut self, host: impl Into<String>) -> Self {
335 self.db_opt = self.db_opt.host(&host.into());
336 self
337 }
338
339 pub fn port(mut self, port: u16) -> Self {
343 self.db_opt = self.db_opt.port(port);
344 self
345 }
346
347 pub fn user(mut self, user: &str) -> Self {
349 self.db_opt = self.db_opt.username(user);
350 self
351 }
352
353 pub fn password(mut self, password: &str) -> Self {
355 self.db_opt = self.db_opt.password(password);
356 self
357 }
358
359 pub fn database(mut self, database: &str) -> Self {
361 self.db_opt = self.db_opt.database(database);
362 self
363 }
364
365 pub fn tls(mut self) -> Self {
371 self.db_opt = self.db_opt.ssl_mode(PgSslMode::Require);
372 self
373 }
374
375 pub fn schema(mut self, schema: impl Into<String>) -> Self {
379 self.schema = schema.into();
380 self
381 }
382}
383
384impl Config {
385 pub fn pool(mut self, pool: Pool<Db>) -> Self {
388 self.pool = Some(pool);
389 self
390 }
391
392 pub fn serializable_retry(mut self, cfg: SerializableRetryConfig) -> Self {
394 self.serializable_retry_config = cfg;
395 self
396 }
397
398 pub fn reset_schema(mut self) -> Self {
409 self.reset = true;
410 self
411 }
412
413 pub fn migrations(mut self, migrations: impl IntoIterator<Item = Migration>) -> Self {
415 self.migrations.extend(migrations);
416 self
417 }
418
419 pub fn no_migrations(mut self) -> Self {
421 self.no_migrations = true;
422 self
423 }
424
425 pub fn pruner_cfg(mut self, cfg: PrunerCfg) -> Result<Self, Error> {
429 cfg.validate()?;
430 self.pruner_cfg = Some(cfg);
431 self.archive = false;
432 Ok(self)
433 }
434
435 pub fn archive(mut self) -> Self {
444 self.pruner_cfg = None;
445 self.archive = true;
446 self
447 }
448
449 pub fn idle_connection_timeout(mut self, timeout: Duration) -> Self {
454 self.pool_opt = self.pool_opt.idle_timeout(Some(timeout));
455
456 #[cfg(not(feature = "embedded-db"))]
457 {
458 self.pool_opt_query = self.pool_opt_query.idle_timeout(Some(timeout));
459 }
460
461 self
462 }
463
464 pub fn connection_timeout(mut self, timeout: Duration) -> Self {
471 self.pool_opt = self.pool_opt.max_lifetime(Some(timeout));
472
473 #[cfg(not(feature = "embedded-db"))]
474 {
475 self.pool_opt = self.pool_opt.max_lifetime(Some(timeout));
476 }
477
478 self
479 }
480
481 pub fn min_connections(mut self, min: u32) -> Self {
487 self.pool_opt = self.pool_opt.min_connections(min);
488 self
489 }
490
491 #[cfg(not(feature = "embedded-db"))]
492 pub fn query_min_connections(mut self, min: u32) -> Self {
493 self.pool_opt_query = self.pool_opt_query.min_connections(min);
494 self
495 }
496
497 pub fn max_connections(mut self, max: u32) -> Self {
502 self.pool_opt = self.pool_opt.max_connections(max);
503 self
504 }
505
506 #[cfg(not(feature = "embedded-db"))]
507 pub fn query_max_connections(mut self, max: u32) -> Self {
508 self.pool_opt_query = self.pool_opt_query.max_connections(max);
509 self
510 }
511
512 pub fn slow_statement_threshold(mut self, threshold: Duration) -> Self {
516 self.db_opt = self
517 .db_opt
518 .log_slow_statements(LevelFilter::Warn, threshold);
519 self
520 }
521
522 #[cfg(not(feature = "embedded-db"))]
526 pub fn statement_timeout(mut self, timeout: Duration) -> Self {
527 let timeout_ms = timeout.as_millis();
530 self.db_opt = self
531 .db_opt
532 .options([("statement_timeout", timeout_ms.to_string())]);
533 self
534 }
535
536 #[cfg(feature = "embedded-db")]
538 pub fn statement_timeout(self, _timeout: Duration) -> Self {
539 self
540 }
541}
542
543#[derive(Clone, Debug)]
545pub struct SqlStorage {
546 pool: Pool<Db>,
547 metrics: PrometheusMetrics,
548 pool_metrics: PoolMetrics,
549 pruner_cfg: Option<PrunerCfg>,
550 serializable_retry_config: SerializableRetryConfig,
551}
552
553#[derive(Debug)]
554struct PruneState {
555 min_height: u64,
556 target_height: u64,
557 minimum_retention_height: u64,
558}
559
560impl PruneState {
561 fn next_target_batch(&self, batch_size: u64) -> Option<u64> {
562 if self.min_height < self.target_height {
563 Some(min(self.min_height + batch_size, self.target_height) - 1)
564 } else {
565 None
566 }
567 }
568
569 fn next_extra_batch(&self, batch_size: u64) -> Option<u64> {
570 if self.min_height < self.minimum_retention_height {
571 Some(min(self.min_height + batch_size, self.minimum_retention_height) - 1)
572 } else {
573 None
574 }
575 }
576}
577
578#[derive(Debug)]
579pub struct Pruner<'a> {
580 data: PruneState,
581 state: PruneState,
582 cfg: &'a PrunerCfg,
583 extra_pruning: bool,
584}
585
586#[derive(Clone, Copy, Debug)]
587enum PruneCategory {
588 Data,
589 State,
590}
591
592impl<'a> Pruner<'a> {
593 fn next_target_batch(&self) -> Option<(PruneCategory, u64)> {
598 if let Some(batch) = self.state.next_target_batch(self.cfg.batch_size()) {
602 return Some((PruneCategory::State, batch));
603 }
604 self.data
605 .next_target_batch(self.cfg.batch_size())
606 .map(|batch| (PruneCategory::Data, batch))
607 }
608
609 fn next_extra_batch(&self) -> Option<(PruneCategory, u64)> {
614 if let Some(batch) = self.state.next_extra_batch(self.cfg.batch_size()) {
615 return Some((PruneCategory::State, batch));
616 }
617 self.data
618 .next_extra_batch(self.cfg.batch_size())
619 .map(|batch| (PruneCategory::Data, batch))
620 }
621
622 fn set_pruned_height(&mut self, category: PruneCategory, height: u64) {
623 match category {
624 PruneCategory::State => self.state.min_height = height + 1,
625 PruneCategory::Data => self.data.min_height = height + 1,
626 }
627 }
628}
629
630#[derive(PartialEq)]
631pub enum StorageConnectionType {
632 Sequencer,
633 Query,
634}
635
636impl SqlStorage {
637 pub fn pool(&self) -> Pool<Db> {
638 self.pool.clone()
639 }
640
641 #[allow(unused_variables)]
643 pub async fn connect(
644 mut config: Config,
645 connection_type: StorageConnectionType,
646 ) -> Result<Self, Error> {
647 let metrics = PrometheusMetrics::default();
648 let pool_metrics = PoolMetrics::new(&*metrics.subgroup("sql".into()));
649
650 #[cfg(feature = "embedded-db")]
651 let pool = config.pool_opt.clone();
652 #[cfg(not(feature = "embedded-db"))]
653 let pool = match connection_type {
654 StorageConnectionType::Sequencer => config.pool_opt.clone(),
655 StorageConnectionType::Query => config.pool_opt_query.clone(),
656 };
657
658 let pruner_cfg = config.pruner_cfg;
659 let serializable_retry_config = config.serializable_retry_config;
660
661 if cfg!(feature = "embedded-db") || connection_type == StorageConnectionType::Sequencer {
663 if let Some(pool) = config.pool {
665 return Ok(Self {
666 metrics,
667 pool_metrics,
668 pool,
669 pruner_cfg,
670 serializable_retry_config,
671 });
672 }
673 } else if config.pool.is_some() {
674 tracing::info!("not reusing existing pool for query connection");
675 }
676
677 #[cfg(not(feature = "embedded-db"))]
678 let schema = config.schema.clone();
679 #[cfg(not(feature = "embedded-db"))]
680 let pool = pool.after_connect(move |conn, _| {
681 let schema = config.schema.clone();
682 async move {
683 query(&format!("SET search_path TO {schema}"))
684 .execute(conn)
685 .await?;
686 Ok(())
687 }
688 .boxed()
689 });
690
691 #[cfg(feature = "embedded-db")]
692 if config.reset {
693 std::fs::remove_file(config.db_opt.get_filename())?;
694 }
695
696 let pool = pool.connect_with(config.db_opt).await?;
697
698 let mut conn = pool.acquire().await?;
700
701 #[cfg(not(feature = "embedded-db"))]
703 query("SET statement_timeout = 0")
704 .execute(conn.as_mut())
705 .await?;
706
707 #[cfg(not(feature = "embedded-db"))]
708 if config.reset {
709 query(&format!("DROP SCHEMA IF EXISTS {schema} CASCADE"))
710 .execute(conn.as_mut())
711 .await?;
712 }
713
714 #[cfg(not(feature = "embedded-db"))]
715 query(&format!("CREATE SCHEMA IF NOT EXISTS {schema}"))
716 .execute(conn.as_mut())
717 .await?;
718
719 validate_migrations(&mut config.migrations)?;
721 let migrations =
722 add_custom_migrations(default_migrations(), config.migrations).collect::<Vec<_>>();
723
724 let runner = refinery::Runner::new(&migrations).set_grouped(true);
727
728 if config.no_migrations {
729 let last_applied = runner
732 .get_last_applied_migration_async(&mut Migrator::from(&mut conn))
733 .await?;
734 let last_expected = migrations.last();
735 if last_applied.as_ref() != last_expected {
736 return Err(Error::msg(format!(
737 "DB is out of date: last applied migration is {last_applied:?}, but expected \
738 {last_expected:?}"
739 )));
740 }
741 } else {
742 match runner.run_async(&mut Migrator::from(&mut conn)).await {
744 Ok(report) => {
745 tracing::info!("ran DB migrations: {report:?}");
746 },
747 Err(err) => {
748 tracing::error!("DB migrations failed: {:?}", err.report());
749 Err(err)?;
750 },
751 }
752 }
753
754 if config.archive {
755 query("DELETE FROM pruned_height")
758 .execute(conn.as_mut())
759 .await?;
760 }
761
762 conn.close().await?;
763
764 Ok(Self {
765 pool,
766 pool_metrics,
767 metrics,
768 pruner_cfg,
769 serializable_retry_config,
770 })
771 }
772}
773
774#[derive(Clone, Copy, Debug)]
776pub struct SerializableRetryConfig {
777 base: Duration,
779 max: Duration,
781 factor: u32,
783 jitter: (u64, u64),
785 retry_max: u32,
787 pg_stat_diag: bool,
789}
790
791impl Default for SerializableRetryConfig {
792 fn default() -> Self {
793 Self {
794 base: Duration::from_millis(10),
795 max: Duration::from_millis(500),
796 factor: 2,
797 jitter: (5, 10),
798 retry_max: 100,
799 pg_stat_diag: false,
800 }
801 }
802}
803
804impl SerializableRetryConfig {
805 pub const fn new(
807 base: Duration,
808 max: Duration,
809 factor: u32,
810 jitter: (u64, u64),
811 retry_max: u32,
812 pg_stat_diag: bool,
813 ) -> Self {
814 Self {
815 base,
816 max,
817 factor,
818 jitter,
819 retry_max,
820 pg_stat_diag,
821 }
822 }
823
824 async fn retry_if<F, Fut, T, E>(
827 &self,
828 op: &'static str,
829 mut should_retry: impl FnMut(&E) -> bool,
830 f: F,
831 ) -> Result<T, E>
832 where
833 F: Fn() -> Fut,
834 Fut: Future<Output = Result<T, E>>,
835 {
836 let mut delay = self.base;
837 for i in 0..=self.retry_max {
838 match f().await {
839 Ok(res) => return Ok(res),
840 Err(err) if i < self.retry_max && should_retry(&err) => {
841 tracing::warn!(
842 op,
843 attempt = i + 1,
844 max_retries = self.retry_max,
845 delay_ms = delay.as_millis(),
846 "serialization conflict, retrying transaction after {delay:?}"
847 );
848 sleep(delay).await;
849 delay = self.backoff(delay);
850 },
851 Err(err) => return Err(err),
852 }
853 }
854 unreachable!()
855 }
856
857 fn backoff(&self, delay: Duration) -> Duration {
859 if delay >= self.max {
860 return self.max;
861 }
862 let ms = delay
863 .saturating_mul(self.factor)
864 .as_millis()
865 .min(u64::MAX as u128) as u64;
866 let (jitter_num, jitter_den) = self.jitter;
867 let jitter = if jitter_num == 0 || jitter_den == 0 {
868 0
869 } else {
870 let mut rng = rand::thread_rng();
871 ms * rng.gen_range(0..jitter_num) / jitter_den
872 };
873 min(Duration::from_millis(ms + jitter), self.max)
874 }
875}
876
877fn is_serialization_conflict_err<E: std::fmt::Display>(err: &E) -> bool {
880 format!("{err:#}").contains("could not serialize access")
881}
882
883fn serialization_conflict_with_diag<E: std::fmt::Display>(
885 pool: Pool<Db>,
886 op: &'static str,
887) -> impl FnMut(&E) -> bool {
888 let mut first = true;
889 move |err| {
890 if is_serialization_conflict_err(err) {
891 #[cfg(not(feature = "embedded-db"))]
892 if first {
893 first = false;
894 spawn_pg_stat_activity_log(pool.clone(), op);
895 }
896 #[cfg(feature = "embedded-db")]
897 let _ = (&pool, op, &mut first);
898 true
899 } else {
900 false
901 }
902 }
903}
904
905#[cfg(not(feature = "embedded-db"))]
908fn spawn_pg_stat_activity_log(pool: Pool<Db>, op: &'static str) {
909 use sqlx::Row as _;
910 tokio::spawn(async move {
911 match sqlx::query(
912 "SELECT pid, COALESCE(state, 'unknown') AS state, left(COALESCE(query, ''), 200) AS \
913 query FROM pg_stat_activity WHERE pid != pg_backend_pid() AND state IS DISTINCT FROM \
914 'idle' AND usename = current_user",
915 )
916 .fetch_all(&pool)
917 .await
918 {
919 Ok(rows) if rows.is_empty() => {
920 tracing::warn!(op, "serialization conflict: no other non-idle DB sessions");
921 },
922 Ok(rows) => {
923 for row in &rows {
924 let pid: i32 = row.try_get("pid").unwrap_or(-1);
925 let state: String = row.try_get("state").unwrap_or_default();
926 let query_text: String = row.try_get("query").unwrap_or_default();
927 tracing::warn!(
928 op,
929 pid,
930 state,
931 "serialization conflict: concurrent session: {query_text}",
932 );
933 }
934 },
935 Err(e) => {
936 tracing::error!(op, "failed to query pg_stat_activity: {e:#}");
937 },
938 }
939
940 match sqlx::query(
942 "SELECT l.pid, l.locktype, CASE WHEN l.relation IS NOT NULL THEN c.relname ELSE NULL \
943 END AS relation, l.page, l.tuple, left(COALESCE(a.query, ''), 100) AS query FROM \
944 pg_locks l JOIN pg_stat_activity a ON a.pid = l.pid LEFT JOIN pg_class c ON c.oid = \
945 l.relation WHERE l.mode = 'SIReadLock' AND a.state IS DISTINCT FROM 'idle' AND l.pid \
946 != pg_backend_pid() ORDER BY l.pid, c.relname",
947 )
948 .fetch_all(&pool)
949 .await
950 {
951 Ok(rows) if rows.is_empty() => {
952 tracing::warn!(op, "serialization conflict: no SIReadLocks held");
953 },
954 Ok(rows) => {
955 for row in &rows {
956 let pid: i32 = row.try_get("pid").unwrap_or(-1);
957 let locktype: String = row.try_get("locktype").unwrap_or_default();
958 let relation: Option<String> = row.try_get("relation").unwrap_or(None);
959 let page: Option<i32> = row.try_get("page").unwrap_or(None);
960 let tuple: Option<i16> = row.try_get("tuple").unwrap_or(None);
961 let query_text: String = row.try_get("query").unwrap_or_default();
962 tracing::warn!(
963 op,
964 pid,
965 locktype,
966 relation,
967 page,
968 tuple,
969 "serialization conflict: SIReadLock: {query_text}",
970 );
971 }
972 },
973 Err(e) => {
974 tracing::warn!(op, "failed to query pg_locks: {e:#}");
975 },
976 }
977 });
978}
979
980#[async_trait]
981impl SerializableRetry for SqlStorage {
982 async fn serializable_retry<T, E, F, Fut>(&self, op: &'static str, f: F) -> Result<T, E>
983 where
984 T: Send,
985 E: std::fmt::Display + Send,
986 F: Fn() -> Fut + Send + Sync,
987 Fut: Future<Output = Result<T, E>> + Send,
988 {
989 if self.serializable_retry_config.pg_stat_diag {
990 self.serializable_retry_config
991 .retry_if(op, serialization_conflict_with_diag(self.pool(), op), f)
992 .await
993 } else {
994 self.serializable_retry_config
995 .retry_if(op, is_serialization_conflict_err, f)
996 .await
997 }
998 }
999}
1000
1001#[cfg(test)]
1002mod serializable_retry_tests {
1003 use std::sync::{
1004 Arc,
1005 atomic::{AtomicU32, Ordering},
1006 };
1007
1008 use super::{Duration, SerializableRetryConfig, is_serialization_conflict_err};
1009
1010 const TEST_RETRY: SerializableRetryConfig = SerializableRetryConfig::new(
1012 Duration::from_millis(1),
1013 Duration::from_millis(5),
1014 2,
1015 (5, 10),
1016 5,
1017 false,
1018 );
1019
1020 fn mock_serialization_error() -> anyhow::Error {
1022 anyhow::anyhow!(
1023 "could not serialize access due to read/write dependencies among transactions"
1024 )
1025 }
1026
1027 #[test]
1028 fn test_is_serialization_conflict_err() {
1029 assert!(is_serialization_conflict_err(&mock_serialization_error()));
1031 assert!(!is_serialization_conflict_err(&anyhow::anyhow!(
1033 "duplicate key value violates unique constraint"
1034 )));
1035 assert!(!is_serialization_conflict_err(&anyhow::anyhow!(
1037 "plain error"
1038 )));
1039 }
1040
1041 #[test_log::test(tokio::test(flavor = "multi_thread"))]
1042 async fn test_retry_if_succeeds_immediately() {
1043 let calls = Arc::new(AtomicU32::new(0));
1044 let calls_clone = calls.clone();
1045
1046 let result: anyhow::Result<()> = TEST_RETRY
1047 .retry_if("test", is_serialization_conflict_err, || {
1048 let calls = calls_clone.clone();
1049 async move {
1050 calls.fetch_add(1, Ordering::SeqCst);
1051 Ok(())
1052 }
1053 })
1054 .await;
1055
1056 assert!(result.is_ok());
1057 assert_eq!(calls.load(Ordering::SeqCst), 1);
1058 }
1059
1060 #[test_log::test(tokio::test(flavor = "multi_thread"))]
1061 async fn test_retry_if_retries_on_serialization_error() {
1062 let calls = Arc::new(AtomicU32::new(0));
1063 let calls_clone = calls.clone();
1064
1065 let result: anyhow::Result<()> = TEST_RETRY
1067 .retry_if("test", is_serialization_conflict_err, || {
1068 let calls = calls_clone.clone();
1069 async move {
1070 let n = calls.fetch_add(1, Ordering::SeqCst);
1071 if n < 2 {
1072 Err(mock_serialization_error())
1073 } else {
1074 Ok(())
1075 }
1076 }
1077 })
1078 .await;
1079
1080 assert!(result.is_ok());
1081 assert_eq!(calls.load(Ordering::SeqCst), 3);
1082 }
1083
1084 #[test_log::test(tokio::test(flavor = "multi_thread"))]
1085 async fn test_retry_if_exhausts_retries() {
1086 let calls = Arc::new(AtomicU32::new(0));
1087 let calls_clone = calls.clone();
1088
1089 let result: anyhow::Result<()> = TEST_RETRY
1091 .retry_if("test", is_serialization_conflict_err, || {
1092 let calls = calls_clone.clone();
1093 async move {
1094 calls.fetch_add(1, Ordering::SeqCst);
1095 Err(mock_serialization_error())
1096 }
1097 })
1098 .await;
1099
1100 assert!(result.is_err());
1101 assert_eq!(calls.load(Ordering::SeqCst), 6);
1103 }
1104
1105 #[test_log::test(tokio::test(flavor = "multi_thread"))]
1106 async fn test_retry_if_no_retry_on_other_errors() {
1107 let calls = Arc::new(AtomicU32::new(0));
1108 let calls_clone = calls.clone();
1109
1110 let result: anyhow::Result<()> = TEST_RETRY
1112 .retry_if("test", is_serialization_conflict_err, || {
1113 let calls = calls_clone.clone();
1114 async move {
1115 calls.fetch_add(1, Ordering::SeqCst);
1116 Err(anyhow::anyhow!("unrelated error"))
1117 }
1118 })
1119 .await;
1120
1121 assert!(result.is_err());
1122 assert_eq!(calls.load(Ordering::SeqCst), 1);
1123 }
1124
1125 #[test]
1128 fn test_function_name_macro() {
1129 fn outer_test_fn() -> &'static str {
1130 crate::function_name!()
1131 }
1132 assert_eq!(outer_test_fn(), "outer_test_fn");
1133 assert_eq!(crate::function_name!(), "test_function_name_macro");
1134
1135 fn télécharger() -> &'static str {
1139 crate::function_name!()
1140 }
1141 assert_eq!(télécharger(), "télécharger");
1142 }
1143
1144 #[test_log::test(tokio::test)]
1148 async fn test_function_name_macro_async() {
1149 async fn plain_async_fn() -> &'static str {
1151 crate::function_name!()
1152 }
1153 assert_eq!(plain_async_fn().await, "plain_async_fn");
1154
1155 async fn nested_async_blocks() -> &'static str {
1159 let f = || async { crate::function_name!() };
1160 f().await
1161 }
1162 assert_eq!(nested_async_blocks().await, "nested_async_blocks");
1163
1164 struct S;
1167 #[async_trait::async_trait]
1168 trait T {
1169 async fn async_trait_method(&self) -> &'static str;
1170 }
1171 #[async_trait::async_trait]
1172 impl T for S {
1173 async fn async_trait_method(&self) -> &'static str {
1174 crate::function_name!()
1175 }
1176 }
1177 assert_eq!(S.async_trait_method().await, "async_trait_method");
1178 }
1179}
1180
1181impl PrunerConfig for SqlStorage {
1182 fn set_pruning_config(&mut self, cfg: PrunerCfg) {
1183 self.pruner_cfg = Some(cfg);
1184 }
1185
1186 fn get_pruning_config(&self) -> Option<PrunerCfg> {
1187 self.pruner_cfg.clone()
1188 }
1189}
1190
1191impl HasMetrics for SqlStorage {
1192 fn metrics(&self) -> &PrometheusMetrics {
1193 &self.metrics
1194 }
1195}
1196
1197impl SqlStorage {
1198 async fn prune_write(&self) -> anyhow::Result<Transaction<Prune>> {
1199 Transaction::new(&self.pool, self.pool_metrics.clone()).await
1200 }
1201
1202 pub async fn backfill(&self) -> anyhow::Result<Transaction<Backfill>> {
1207 Transaction::new(&self.pool, self.pool_metrics.clone()).await
1208 }
1209
1210 async fn new_pruner<'a>(&'a self) -> anyhow::Result<Pruner<'a>> {
1211 let cfg = self
1212 .pruner_cfg
1213 .as_ref()
1214 .context("pruning config not found")?;
1215 let now = Utc::now().timestamp();
1216
1217 let (min_height, state_min_height) = {
1218 let mut tx = self
1219 .read()
1220 .await
1221 .context("opening transaction to load pruned heights")?;
1222 (
1223 tx.load_pruned_height()
1224 .await?
1225 .map_or(0, |pruned| pruned + 1),
1226 tx.load_state_pruned_height()
1227 .await?
1228 .map_or(0, |pruned| pruned + 1),
1229 )
1230 };
1231 Ok(Pruner {
1232 data: PruneState {
1233 min_height,
1234 target_height: self
1235 .get_height_by_timestamp(now - (cfg.target_retention().as_secs()) as i64)
1236 .await
1237 .context("getting height for target retention")?
1238 .map_or(min_height, |to_prune| to_prune + 1),
1239 minimum_retention_height: self
1240 .get_height_by_timestamp(now - (cfg.minimum_retention().as_secs()) as i64)
1241 .await
1242 .context("getting height for minimum retention")?
1243 .map_or(min_height, |to_prune| to_prune + 1),
1244 },
1245 state: PruneState {
1246 min_height: state_min_height,
1247 target_height: self
1248 .get_height_by_timestamp(now - (cfg.state_target_retention().as_secs()) as i64)
1249 .await
1250 .context("getting height for state target retention")?
1251 .map_or(state_min_height, |to_prune| to_prune + 1),
1252 minimum_retention_height: self
1253 .get_height_by_timestamp(now - (cfg.state_minimum_retention().as_secs()) as i64)
1254 .await
1255 .context("getting height for state minimum retention")?
1256 .map_or(state_min_height, |to_prune| to_prune + 1),
1257 },
1258 cfg,
1259 extra_pruning: false,
1260 })
1261 }
1262
1263 #[instrument(skip(self, pruner))]
1264 async fn prune_batch(
1265 &self,
1266 pruner: &mut Pruner<'_>,
1267 category: PruneCategory,
1268 to: u64,
1269 ) -> anyhow::Result<()> {
1270 tracing::info!("pruning batch");
1271
1272 let mut tx = self
1275 .write()
1276 .await
1277 .context("opening transaction for pruned height")?;
1278 match category {
1279 PruneCategory::Data => tx.save_pruned_height(to).await?,
1280 PruneCategory::State => tx.save_state_pruned_height(to).await?,
1281 }
1282 tx.commit().await.context("committing pruned height")?;
1283
1284 let mut tx = self
1285 .prune_write()
1286 .await
1287 .context("opening pruning transaction")?;
1288 match category {
1289 PruneCategory::Data => tx.delete_batch(to).await?,
1290 PruneCategory::State => tx.delete_state_batch(pruner.cfg.state_tables(), to).await?,
1291 }
1292 tx.commit().await.context("committing deleted batch")?;
1293
1294 pruner.set_pruned_height(category, to);
1295 Ok(())
1296 }
1297
1298 async fn get_disk_usage(&self) -> anyhow::Result<u64> {
1299 let mut tx = self.read().await?;
1300
1301 #[cfg(not(feature = "embedded-db"))]
1302 let query = "SELECT pg_database_size(current_database())";
1303
1304 #[cfg(feature = "embedded-db")]
1305 let query = "
1306 SELECT( (SELECT page_count FROM pragma_page_count) * (SELECT * FROM pragma_page_size)) \
1307 AS total_bytes";
1308
1309 let row = tx.fetch_one(query).await.context("getting disk usage")?;
1310 let size: i64 = row.get(0);
1311
1312 Ok(size as u64)
1313 }
1314
1315 async fn vacuum(&self) -> anyhow::Result<()> {
1317 if cfg!(feature = "embedded-db") {
1320 let config = self.get_pruning_config().ok_or(QueryError::Error {
1321 message: "Pruning config not found".to_string(),
1322 })?;
1323 let mut conn = self.pool().acquire().await?;
1324 query(&format!(
1325 "PRAGMA incremental_vacuum({})",
1326 config.incremental_vacuum_pages()
1327 ))
1328 .execute(conn.as_mut())
1329 .await
1330 .context("triggering vacuum")?;
1331 conn.close().await?;
1332 }
1333 Ok(())
1334 }
1335
1336 async fn get_height_by_timestamp(&self, timestamp: i64) -> QueryResult<Option<u64>> {
1337 let mut tx = self.read().await.map_err(|err| QueryError::Error {
1338 message: err.to_string(),
1339 })?;
1340
1341 let Some((height,)) = query_as::<(i64,)>(
1348 "SELECT height FROM header
1349 WHERE timestamp <= $1
1350 ORDER BY timestamp DESC, height DESC
1351 LIMIT 1",
1352 )
1353 .bind(timestamp)
1354 .fetch_optional(tx.as_mut())
1355 .await?
1356 else {
1357 return Ok(None);
1358 };
1359 Ok(Some(height as u64))
1360 }
1361
1362 pub async fn get_vid_share<Types>(&self, block_id: BlockId<Types>) -> QueryResult<VidShare>
1364 where
1365 Types: NodeType,
1366 Header<Types>: QueryableHeader<Types>,
1367 {
1368 let mut tx = self.read().await.map_err(|err| QueryError::Error {
1369 message: err.to_string(),
1370 })?;
1371 let share = tx.vid_share(block_id).await?;
1372 Ok(share)
1373 }
1374
1375 pub async fn get_vid_common<Types: NodeType>(
1377 &self,
1378 block_id: BlockId<Types>,
1379 ) -> QueryResult<VidCommonQueryData<Types>>
1380 where
1381 <Types as NodeType>::BlockPayload: QueryablePayload<Types>,
1382 <Types as NodeType>::BlockHeader: QueryableHeader<Types>,
1383 {
1384 let mut tx = self.read().await.map_err(|err| QueryError::Error {
1385 message: err.to_string(),
1386 })?;
1387 let common = tx.get_vid_common(block_id).await?;
1388 Ok(common)
1389 }
1390
1391 pub async fn get_vid_common_metadata<Types: NodeType>(
1393 &self,
1394 block_id: BlockId<Types>,
1395 ) -> QueryResult<VidCommonMetadata<Types>>
1396 where
1397 <Types as NodeType>::BlockPayload: QueryablePayload<Types>,
1398 <Types as NodeType>::BlockHeader: QueryableHeader<Types>,
1399 {
1400 let mut tx = self.read().await.map_err(|err| QueryError::Error {
1401 message: err.to_string(),
1402 })?;
1403 let common = tx.get_vid_common_metadata(block_id).await?;
1404 Ok(common)
1405 }
1406}
1407
1408#[async_trait]
1409impl PruneStorage for SqlStorage {
1410 type Pruner<'a> = Option<Pruner<'a>>;
1411
1412 #[instrument(skip(self))]
1417 async fn prune<'a>(&'a self, pruner: &mut Option<Pruner<'a>>) -> anyhow::Result<Option<u64>> {
1418 let pruner = match pruner {
1419 Some(pruner) => pruner,
1420 None => pruner.get_or_insert(self.new_pruner().await?),
1421 };
1422
1423 if let Some((category, to)) = pruner.next_target_batch() {
1425 tracing::info!("pruning to target retention");
1426 self.prune_batch(pruner, category, to).await?;
1427 return Ok(Some(to));
1428 }
1429
1430 let Some(threshold) = pruner.cfg.pruning_threshold() else {
1433 return Ok(None);
1434 };
1435 let usage = self.get_disk_usage().await?;
1436
1437 if usage > threshold {
1439 tracing::warn!(usage, threshold, "Disk usage exceeds pruning threshold");
1440 pruner.extra_pruning = true;
1441 }
1442 if !pruner.extra_pruning {
1443 return Ok(None);
1444 }
1445
1446 if (usage as f64 / threshold as f64) <= (f64::from(pruner.cfg.max_usage()) / 10000.0) {
1449 tracing::info!(
1450 usage,
1451 threshold,
1452 "space reclaimed makes usage less than threshold"
1453 );
1454 return Ok(None);
1455 }
1456
1457 let Some((category, to)) = pruner.next_extra_batch() else {
1459 return Ok(None);
1460 };
1461
1462 tracing::info!("pruning beyond target retention");
1463 self.prune_batch(pruner, category, to).await?;
1464 self.vacuum().await?;
1465 Ok(Some(to))
1466 }
1467}
1468
1469impl VersionedDataSource for SqlStorage {
1470 type Transaction<'a>
1471 = Transaction<Write>
1472 where
1473 Self: 'a;
1474 type ReadOnly<'a>
1475 = Transaction<Read>
1476 where
1477 Self: 'a;
1478
1479 async fn write(&self) -> anyhow::Result<Transaction<Write>> {
1480 Transaction::new(&self.pool, self.pool_metrics.clone()).await
1481 }
1482
1483 async fn read(&self) -> anyhow::Result<Transaction<Read>> {
1484 Transaction::new(&self.pool, self.pool_metrics.clone()).await
1485 }
1486}
1487
1488#[cfg(all(any(test, feature = "testing"), not(target_os = "windows")))]
1490pub mod testing {
1491 #![allow(unused_imports)]
1492 use std::{
1493 env,
1494 process::{Child, Command, Stdio},
1495 time::Duration,
1496 };
1497
1498 use refinery::Migration;
1499 use test_utils::reserve_tcp_port;
1500 use tokio::time::timeout;
1501
1502 use super::Config;
1503 use crate::testing::sleep;
1504 #[derive(Debug)]
1505 pub struct TmpDb {
1506 #[cfg(not(feature = "embedded-db"))]
1507 host: String,
1508 #[cfg(not(feature = "embedded-db"))]
1509 port: u16,
1510 #[cfg(not(feature = "embedded-db"))]
1511 data_dir: std::path::PathBuf,
1512 #[cfg(not(feature = "embedded-db"))]
1513 postgres: Option<Child>,
1514 #[cfg(feature = "embedded-db")]
1515 db_path: std::path::PathBuf,
1516 #[allow(dead_code)]
1517 persistent: bool,
1518 }
1519 impl TmpDb {
1520 #[cfg(feature = "embedded-db")]
1521 fn init_sqlite_db(persistent: bool) -> Self {
1522 let file = tempfile::Builder::new()
1523 .prefix("sqlite-")
1524 .suffix(".db")
1525 .tempfile()
1526 .unwrap();
1527
1528 let (_, db_path) = file.keep().unwrap();
1529
1530 Self {
1531 db_path,
1532 persistent,
1533 }
1534 }
1535 pub async fn init() -> Self {
1536 #[cfg(feature = "embedded-db")]
1537 return Self::init_sqlite_db(false);
1538
1539 #[cfg(not(feature = "embedded-db"))]
1540 Self::init_postgres(false).await
1541 }
1542
1543 pub async fn persistent() -> Self {
1544 #[cfg(feature = "embedded-db")]
1545 return Self::init_sqlite_db(true);
1546
1547 #[cfg(not(feature = "embedded-db"))]
1548 Self::init_postgres(true).await
1549 }
1550
1551 #[cfg(not(feature = "embedded-db"))]
1552 async fn init_postgres(persistent: bool) -> Self {
1553 let port = reserve_tcp_port().unwrap();
1554 let host = "127.0.0.1".to_string();
1555
1556 let data_dir = env::temp_dir().join(format!("espresso-tmpdb-{port}"));
1559 let _ = std::fs::remove_dir_all(&data_dir);
1560
1561 let output = Command::new("initdb")
1562 .arg("-D")
1563 .arg(&data_dir)
1564 .args(["-U", "postgres", "--auth=trust"])
1565 .output()
1566 .expect("initdb failed to run; is postgres installed and on PATH?");
1567 assert!(
1568 output.status.success(),
1569 "initdb failed for {data_dir:?}: {}",
1570 String::from_utf8_lossy(&output.stderr)
1571 );
1572
1573 let mut db = Self {
1574 host,
1575 port,
1576 data_dir,
1577 postgres: None,
1578 persistent,
1579 };
1580
1581 db.start_postgres().await;
1582 db
1583 }
1584
1585 #[cfg(not(feature = "embedded-db"))]
1586 pub fn host(&self) -> String {
1587 self.host.clone()
1588 }
1589
1590 #[cfg(not(feature = "embedded-db"))]
1591 pub fn port(&self) -> u16 {
1592 self.port
1593 }
1594
1595 #[cfg(feature = "embedded-db")]
1596 pub fn path(&self) -> std::path::PathBuf {
1597 self.db_path.clone()
1598 }
1599
1600 pub fn config(&self) -> Config {
1601 #[cfg(feature = "embedded-db")]
1602 let mut cfg = Config::default().db_path(self.db_path.clone());
1603
1604 #[cfg(not(feature = "embedded-db"))]
1605 let mut cfg = Config::default()
1606 .user("postgres")
1607 .password("password")
1608 .host(self.host())
1609 .port(self.port());
1610
1611 cfg = cfg.migrations(vec![
1612 Migration::unapplied(
1613 "V101__create_test_merkle_tree_table.sql",
1614 &TestMerkleTreeMigration::create("test_tree"),
1615 )
1616 .unwrap(),
1617 ]);
1618
1619 cfg
1620 }
1621
1622 #[cfg(not(feature = "embedded-db"))]
1623 pub fn stop_postgres(&mut self) {
1624 let Some(mut postgres) = self.postgres.take() else {
1625 return;
1626 };
1627 tracing::info!(port = self.port, "stopping postgres");
1628 let stopped = Command::new("pg_ctl")
1631 .arg("-D")
1632 .arg(&self.data_dir)
1633 .args(["stop", "-m", "fast", "-w"])
1634 .stdout(Stdio::null())
1635 .stderr(Stdio::null())
1636 .status()
1637 .map(|status| status.success())
1638 .unwrap_or(false);
1639 if !stopped {
1640 let _ = postgres.kill();
1641 }
1642 let _ = postgres.wait();
1643 }
1644
1645 #[cfg(not(feature = "embedded-db"))]
1646 pub async fn start_postgres(&mut self) {
1647 self.stop_postgres();
1648 tracing::info!(port = self.port, "starting postgres");
1649 let postgres = Command::new("postgres")
1650 .arg("-D")
1651 .arg(&self.data_dir)
1652 .args(["-p", &self.port.to_string()])
1653 .args(["-h", &self.host])
1654 .arg("-k")
1656 .arg(&self.data_dir)
1657 .stdout(Stdio::null())
1658 .stderr(Stdio::null())
1659 .spawn()
1660 .expect("failed to start postgres; is it installed and on PATH?");
1661 self.postgres = Some(postgres);
1662
1663 self.wait_for_ready().await;
1664 }
1665
1666 #[cfg(not(feature = "embedded-db"))]
1667 async fn wait_for_ready(&self) {
1668 let timeout_duration = Duration::from_secs(
1669 env::var("SQL_TMP_DB_CONNECT_TIMEOUT")
1670 .unwrap_or("60".to_string())
1671 .parse()
1672 .expect("SQL_TMP_DB_CONNECT_TIMEOUT must be an integer number of seconds"),
1673 );
1674
1675 if let Err(err) = timeout(timeout_duration, async {
1676 while !Command::new("pg_isready")
1677 .args(["-h", &self.host])
1678 .args(["-p", &self.port.to_string()])
1679 .args(["-U", "postgres"])
1680 .stdout(Stdio::null())
1681 .stderr(Stdio::null())
1682 .status()
1683 .map(|status| status.success())
1684 .unwrap_or(false)
1685 {
1686 tracing::warn!("database is not ready");
1687 sleep(Duration::from_secs(1)).await;
1688 }
1689 })
1690 .await
1691 {
1692 panic!(
1693 "failed to connect to TmpDb within configured timeout {timeout_duration:?}: \
1694 {err:#}\n{}",
1695 "Consider increasing the timeout by setting SQL_TMP_DB_CONNECT_TIMEOUT"
1696 );
1697 }
1698 }
1699 }
1700
1701 #[cfg(not(feature = "embedded-db"))]
1702 impl Drop for TmpDb {
1703 fn drop(&mut self) {
1704 self.stop_postgres();
1705 if !self.persistent {
1706 let _ = std::fs::remove_dir_all(&self.data_dir);
1707 }
1708 }
1709 }
1710
1711 #[cfg(feature = "embedded-db")]
1712 impl Drop for TmpDb {
1713 fn drop(&mut self) {
1714 if !self.persistent {
1715 std::fs::remove_file(self.db_path.clone()).unwrap();
1716 }
1717 }
1718 }
1719
1720 pub struct TestMerkleTreeMigration;
1721
1722 impl TestMerkleTreeMigration {
1723 fn create(name: &str) -> String {
1724 let (bit_vec, binary, hash_pk, root_stored_column) = if cfg!(feature = "embedded-db") {
1725 (
1726 "TEXT",
1727 "BLOB",
1728 "INTEGER PRIMARY KEY AUTOINCREMENT",
1729 " (json_extract(data, '$.test_merkle_tree_root'))",
1730 )
1731 } else {
1732 (
1733 "BIT(8)",
1734 "BYTEA",
1735 "BIGSERIAL PRIMARY KEY",
1736 "(data->>'test_merkle_tree_root')",
1737 )
1738 };
1739
1740 format!(
1741 "CREATE TABLE IF NOT EXISTS hash_bigint
1742 (
1743 id {hash_pk},
1744 value {binary} NOT NULL UNIQUE
1745 );
1746
1747 ALTER TABLE header
1748 ADD column test_merkle_tree_root text
1749 GENERATED ALWAYS as {root_stored_column} STORED;
1750
1751 CREATE TABLE {name}
1752 (
1753 path JSONB NOT NULL,
1754 created BIGINT NOT NULL,
1755 hash_id BIGINT NOT NULL,
1756 children JSONB,
1757 children_bitvec {bit_vec},
1758 idx JSONB,
1759 entry JSONB,
1760 PRIMARY KEY (path, created)
1761 );
1762 CREATE INDEX {name}_created ON {name} (created);"
1763 )
1764 }
1765 }
1766}
1767
1768#[cfg(all(test, not(target_os = "windows")))]
1770mod test {
1771 use std::time::Duration;
1772
1773 use hotshot_example_types::{
1774 node_types::TEST_VERSIONS,
1775 state_types::{TestInstanceState, TestValidatedState},
1776 };
1777 use jf_merkle_tree_compat::{
1778 MerkleTreeScheme, ToTraversalPath, UniversalMerkleTreeScheme, prelude::UniversalMerkleTree,
1779 };
1780 use tokio::time::sleep;
1781
1782 use super::{testing::TmpDb, *};
1783 use crate::{
1784 availability::{BlockQueryData, LeafQueryData},
1785 data_source::storage::{
1786 MerklizedStateStorage, UpdateAvailabilityStorage, pruning::PrunedHeightStorage,
1787 },
1788 merklized_state::{MerklizedState, Snapshot, UpdateStateData},
1789 testing::mocks::{MockMerkleTree, MockTypes},
1790 };
1791
1792 impl SqlStorage {
1793 async fn get_minimum_height(&self) -> QueryResult<Option<u64>> {
1794 let mut tx = self.read().await.map_err(|err| QueryError::Error {
1795 message: err.to_string(),
1796 })?;
1797 let (Some(height),) =
1798 query_as::<(Option<i64>,)>("SELECT MIN(height) as height FROM header")
1799 .fetch_one(tx.as_mut())
1800 .await?
1801 else {
1802 return Ok(None);
1803 };
1804 Ok(Some(height as u64))
1805 }
1806 }
1807
1808 #[test_log::test(tokio::test(flavor = "multi_thread"))]
1809 async fn test_migrations() {
1810 let db = TmpDb::init().await;
1811 let cfg = db.config();
1812
1813 let connect = |migrations: bool, custom_migrations| {
1814 let cfg = cfg.clone();
1815 async move {
1816 let mut cfg = cfg.migrations(custom_migrations);
1817 if !migrations {
1818 cfg = cfg.no_migrations();
1819 }
1820 let client = SqlStorage::connect(cfg, StorageConnectionType::Query).await?;
1821 Ok::<_, Error>(client)
1822 }
1823 };
1824
1825 let err = connect(false, vec![]).await.unwrap_err();
1828 tracing::info!("connecting without running migrations failed as expected: {err}");
1829
1830 connect(true, vec![]).await.unwrap();
1832 connect(false, vec![]).await.unwrap();
1834
1835 let migrations = vec![
1839 Migration::unapplied(
1840 "V9999__create_test_table.sql",
1841 "ALTER TABLE test ADD COLUMN data INTEGER;",
1842 )
1843 .unwrap(),
1844 Migration::unapplied(
1845 "V9998__create_test_table.sql",
1846 "CREATE TABLE test (x bigint);",
1847 )
1848 .unwrap(),
1849 ];
1850 connect(true, migrations.clone()).await.unwrap();
1851
1852 let err = connect(false, vec![]).await.unwrap_err();
1855 tracing::info!("connecting without running migrations failed as expected: {err}");
1856
1857 connect(true, migrations).await.unwrap();
1859 }
1860
1861 #[test]
1862 #[cfg(not(feature = "embedded-db"))]
1863 fn test_config_from_str() {
1864 let cfg = Config::from_str("postgresql://user:password@host:8080").unwrap();
1865 assert_eq!(cfg.db_opt.get_username(), "user");
1866 assert_eq!(cfg.db_opt.get_host(), "host");
1867 assert_eq!(cfg.db_opt.get_port(), 8080);
1868 }
1869
1870 #[test]
1871 #[cfg(feature = "embedded-db")]
1872 fn test_config_from_str() {
1873 let cfg = Config::from_str("sqlite://data.db").unwrap();
1874 assert_eq!(cfg.db_opt.get_filename().to_string_lossy(), "data.db");
1875 }
1876
1877 async fn vacuum(storage: &SqlStorage) {
1878 #[cfg(feature = "embedded-db")]
1879 let query = "PRAGMA incremental_vacuum(16000)";
1880 #[cfg(not(feature = "embedded-db"))]
1881 let query = "VACUUM";
1882 storage
1883 .pool
1884 .acquire()
1885 .await
1886 .unwrap()
1887 .execute(query)
1888 .await
1889 .unwrap();
1890 }
1891
1892 #[test_log::test(tokio::test(flavor = "multi_thread"))]
1893 async fn test_target_period_pruning() {
1894 let db = TmpDb::init().await;
1895 let cfg = db.config();
1896
1897 let mut storage = SqlStorage::connect(cfg, StorageConnectionType::Query)
1898 .await
1899 .unwrap();
1900 let mut leaf = LeafQueryData::<MockTypes>::genesis(
1901 &TestValidatedState::default(),
1902 &TestInstanceState::default(),
1903 TEST_VERSIONS.test,
1904 )
1905 .await;
1906 for i in 0..20 {
1908 leaf.leaf.block_header_mut().block_number = i;
1909 leaf.leaf.block_header_mut().timestamp = Utc::now().timestamp() as u64;
1910 let mut tx = storage.write().await.unwrap();
1911 tx.insert_leaf(&leaf).await.unwrap();
1912 tx.commit().await.unwrap();
1913 }
1914
1915 let height_before_pruning = storage.get_minimum_height().await.unwrap().unwrap();
1916
1917 storage.set_pruning_config(PrunerCfg::new());
1919 let pruned_height = storage.prune(&mut Default::default()).await.unwrap();
1921
1922 vacuum(&storage).await;
1926 assert!(pruned_height.is_none());
1928
1929 let height_after_pruning = storage.get_minimum_height().await.unwrap().unwrap();
1930
1931 assert_eq!(
1932 height_after_pruning, height_before_pruning,
1933 "some data has been pruned"
1934 );
1935
1936 storage.set_pruning_config(PrunerCfg::new().with_target_retention(Duration::from_secs(1)));
1938 sleep(Duration::from_secs(2)).await;
1939 let usage_before_pruning = storage.get_disk_usage().await.unwrap();
1940 let pruned_height = storage.prune(&mut Default::default()).await.unwrap();
1943 vacuum(&storage).await;
1947
1948 assert!(pruned_height.is_some());
1950 let usage_after_pruning = storage.get_disk_usage().await.unwrap();
1951 let header_rows = storage
1954 .read()
1955 .await
1956 .unwrap()
1957 .fetch_one("select count(*) as count from header")
1958 .await
1959 .unwrap()
1960 .get::<i64, _>("count");
1961 assert_eq!(header_rows, 0);
1963
1964 let leaf_rows = storage
1968 .read()
1969 .await
1970 .unwrap()
1971 .fetch_one("select count(*) as count from leaf")
1972 .await
1973 .unwrap()
1974 .get::<i64, _>("count");
1975 assert_eq!(leaf_rows, 0);
1977
1978 assert!(
1979 usage_before_pruning > usage_after_pruning,
1980 " disk usage should decrease after pruning"
1981 )
1982 }
1983
1984 #[test_log::test(tokio::test(flavor = "multi_thread"))]
1985 async fn test_merklized_state_pruning() {
1986 let db = TmpDb::init().await;
1987 let storage = SqlStorage::connect(db.config(), StorageConnectionType::Query)
1988 .await
1989 .unwrap();
1990
1991 let num_blocks = 10_000u64;
1992 let mut test_tree: UniversalMerkleTree<_, _, _, 8, _> =
1993 MockMerkleTree::new(MockMerkleTree::tree_height());
1994
1995 let mut tx = storage.write().await.unwrap();
1997 for height in 0..num_blocks {
1998 test_tree.update(height as usize, height as usize).unwrap();
1999
2000 let test_data = serde_json::json!({
2001 MockMerkleTree::header_state_commitment_field():
2002 serde_json::to_value(test_tree.commitment()).unwrap()
2003 });
2004 tx.upsert(
2005 "header",
2006 [
2007 "height",
2008 "hash",
2009 "payload_hash",
2010 "timestamp",
2011 "data",
2012 "ns_table",
2013 ],
2014 ["height"],
2015 [(
2016 height as i64,
2017 format!("hash{height}"),
2018 "ph".to_string(),
2019 0,
2020 test_data,
2021 "ns".to_string(),
2022 )],
2023 )
2024 .await
2025 .unwrap();
2026
2027 let (_, proof) = test_tree.lookup(height as usize).expect_ok().unwrap();
2028 let traversal_path = <usize as ToTraversalPath<8>>::to_traversal_path(
2029 &(height as usize),
2030 test_tree.height(),
2031 );
2032 UpdateStateData::<_, MockMerkleTree, 8>::insert_merkle_nodes(
2033 &mut tx,
2034 proof.clone(),
2035 traversal_path,
2036 height,
2037 )
2038 .await
2039 .unwrap();
2040 }
2041 UpdateStateData::<_, MockMerkleTree, 8>::set_last_state_height(
2042 &mut tx,
2043 num_blocks as usize,
2044 )
2045 .await
2046 .unwrap();
2047 tx.commit().await.unwrap();
2048
2049 let prune_height = 5678u64;
2051 let mut tx = storage.prune_write().await.unwrap();
2052 tx.delete_state_batch(vec!["test_tree".to_string()], prune_height)
2053 .await
2054 .unwrap();
2055 tx.commit().await.unwrap();
2056
2057 let mut tx = storage.read().await.unwrap();
2059 let (duplicates,) = query_as::<(i64,)>(
2060 "SELECT count(*) FROM (SELECT count(*) FROM test_tree WHERE created <= $1 GROUP BY \
2061 path HAVING count(*) > 1) AS s",
2062 )
2063 .bind(prune_height as i64)
2064 .fetch_one(tx.as_mut())
2065 .await
2066 .unwrap();
2067 assert_eq!(
2068 duplicates, 0,
2069 "found {duplicates} paths with duplicate versions at or below prune height"
2070 );
2071
2072 let commitment = test_tree.commitment();
2074 let mut tx = storage.read().await.unwrap();
2075 for key in 0..num_blocks as usize {
2076 let proof = MerklizedStateStorage::<MockTypes, MockMerkleTree, 8>::get_path(
2077 &mut tx,
2078 Snapshot::Index(num_blocks - 1),
2079 key,
2080 )
2081 .await
2082 .unwrap_or_else(|e| panic!("get_path failed for key {key} after pruning: {e:#}"));
2083 assert_eq!(
2084 proof.elem(),
2085 Some(&key),
2086 "proof for key {key} has wrong element: {:?}",
2087 proof.elem()
2088 );
2089 MockMerkleTree::verify(commitment, key, &proof)
2090 .unwrap()
2091 .unwrap();
2092 }
2093 }
2094
2095 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2096 async fn test_minimum_retention_pruning() {
2097 let db = TmpDb::init().await;
2098
2099 let mut storage = SqlStorage::connect(db.config(), StorageConnectionType::Query)
2100 .await
2101 .unwrap();
2102 let mut leaf = LeafQueryData::<MockTypes>::genesis(
2103 &TestValidatedState::default(),
2104 &TestInstanceState::default(),
2105 TEST_VERSIONS.test,
2106 )
2107 .await;
2108 for i in 0..20 {
2110 leaf.leaf.block_header_mut().block_number = i;
2111 leaf.leaf.block_header_mut().timestamp = Utc::now().timestamp() as u64;
2112 let mut tx = storage.write().await.unwrap();
2113 tx.insert_leaf(&leaf).await.unwrap();
2114 tx.commit().await.unwrap();
2115 }
2116
2117 let height_before_pruning = storage.get_minimum_height().await.unwrap().unwrap();
2118 let cfg = PrunerCfg::new();
2119 storage.set_pruning_config(cfg.clone().with_pruning_threshold(1));
2124 println!("{:?}", storage.get_pruning_config().unwrap());
2125 let pruned_height = storage.prune(&mut Default::default()).await.unwrap();
2128 vacuum(&storage).await;
2132
2133 assert!(pruned_height.is_none());
2135
2136 let height_after_pruning = storage.get_minimum_height().await.unwrap().unwrap();
2137
2138 assert_eq!(
2139 height_after_pruning, height_before_pruning,
2140 "some data has been pruned"
2141 );
2142
2143 storage.set_pruning_config(
2145 cfg.with_minimum_retention(Duration::from_secs(1))
2146 .with_pruning_threshold(1),
2147 );
2148 sleep(Duration::from_secs(2)).await;
2150 let pruned_height = storage.prune(&mut Default::default()).await.unwrap();
2152 vacuum(&storage).await;
2156
2157 assert!(pruned_height.is_some());
2159 let header_rows = storage
2162 .read()
2163 .await
2164 .unwrap()
2165 .fetch_one("select count(*) as count from header")
2166 .await
2167 .unwrap()
2168 .get::<i64, _>("count");
2169 assert_eq!(header_rows, 0);
2171 }
2172
2173 #[tokio::test]
2174 #[test_log::test]
2175 async fn test_payload_pruning() {
2176 let db = TmpDb::init().await;
2177 let mut storage = SqlStorage::connect(db.config(), StorageConnectionType::Query)
2178 .await
2179 .unwrap();
2180 storage.set_pruning_config(Default::default());
2181
2182 let mut leaf = LeafQueryData::<MockTypes>::genesis(
2184 &TestValidatedState::default(),
2185 &TestInstanceState::default(),
2186 TEST_VERSIONS.test,
2187 )
2188 .await;
2189 let block = BlockQueryData::<MockTypes>::genesis(
2190 &Default::default(),
2191 &Default::default(),
2192 TEST_VERSIONS.test.base,
2193 )
2194 .await;
2195 let vid = VidCommonQueryData::<MockTypes>::genesis(
2196 &Default::default(),
2197 &Default::default(),
2198 TEST_VERSIONS.test.base,
2199 )
2200 .await;
2201 {
2202 let mut tx = storage.write().await.unwrap();
2203 tx.insert_leaf(&leaf).await.unwrap();
2204 tx.insert_block(&block).await.unwrap();
2205 tx.insert_vid(&vid, None).await.unwrap();
2206 tx.commit().await.unwrap();
2207 }
2208
2209 leaf.leaf.block_header_mut().block_number += 1;
2211 {
2212 let mut tx = storage.write().await.unwrap();
2213 tx.insert_leaf(&leaf).await.unwrap();
2214 tx.commit().await.unwrap();
2215 }
2216 {
2217 let mut tx = storage.read().await.unwrap();
2218 let (num_payloads,): (i64,) = query_as("SELECT count(*) FROM payload")
2219 .fetch_one(tx.as_mut())
2220 .await
2221 .unwrap();
2222 assert_eq!(num_payloads, 1);
2223 let (num_vid,): (i64,) = query_as("SELECT count(*) FROM vid_common")
2224 .fetch_one(tx.as_mut())
2225 .await
2226 .unwrap();
2227 assert_eq!(num_vid, 1);
2228 }
2229
2230 let mut pruner = Some(Pruner {
2232 data: PruneState {
2233 min_height: 0,
2234 target_height: 1,
2235 minimum_retention_height: 1,
2236 },
2237 state: PruneState {
2238 min_height: 0,
2239 target_height: 0,
2240 minimum_retention_height: 0,
2241 },
2242 cfg: &Default::default(),
2243 extra_pruning: false,
2244 });
2245 let pruned_height = storage.prune(&mut pruner).await.unwrap();
2246 tracing::info!(?pruned_height, "first pruning run complete");
2247 {
2248 let mut tx = storage.read().await.unwrap();
2249
2250 let err = tx
2252 .get_block(BlockId::<MockTypes>::Number(0))
2253 .await
2254 .unwrap_err();
2255 assert!(matches!(err, QueryError::NotFound), "{err:#}");
2256 let err = tx
2257 .get_vid_common(BlockId::<MockTypes>::Number(0))
2258 .await
2259 .unwrap_err();
2260 assert!(matches!(err, QueryError::NotFound), "{err:#}");
2261
2262 assert_eq!(
2264 tx.get_block(BlockId::<MockTypes>::Number(1)).await.unwrap(),
2265 BlockQueryData::new(leaf.header().clone(), block.payload)
2266 );
2267 assert_eq!(
2268 tx.get_vid_common(BlockId::<MockTypes>::Number(1))
2269 .await
2270 .unwrap(),
2271 VidCommonQueryData::new(leaf.header().clone(), vid.common)
2272 );
2273
2274 let (num_payloads,): (i64,) = query_as("SELECT count(*) FROM payload")
2275 .fetch_one(tx.as_mut())
2276 .await
2277 .unwrap();
2278 assert_eq!(num_payloads, 1);
2279
2280 let (num_vid,): (i64,) = query_as("SELECT count(*) FROM vid_common")
2281 .fetch_one(tx.as_mut())
2282 .await
2283 .unwrap();
2284 assert_eq!(num_vid, 1);
2285 }
2286
2287 pruner.as_mut().unwrap().data.target_height = 2;
2289 let pruned_height = storage.prune(&mut pruner).await.unwrap();
2290 tracing::info!(?pruned_height, "second pruning run complete");
2291
2292 let mut tx = storage.read().await.unwrap();
2293 for i in 0..2 {
2294 let err = tx
2295 .get_block(BlockId::<MockTypes>::Number(i))
2296 .await
2297 .unwrap_err();
2298 assert!(matches!(err, QueryError::NotFound), "{err:#}");
2299
2300 let err = tx
2301 .get_vid_common(BlockId::<MockTypes>::Number(i))
2302 .await
2303 .unwrap_err();
2304 assert!(matches!(err, QueryError::NotFound), "{err:#}");
2305 }
2306 let (num_payloads,): (i64,) = query_as("SELECT count(*) FROM payload")
2307 .fetch_one(tx.as_mut())
2308 .await
2309 .unwrap();
2310 assert_eq!(num_payloads, 0);
2311
2312 let (num_vid,): (i64,) = query_as("SELECT count(*) FROM vid_common")
2313 .fetch_one(tx.as_mut())
2314 .await
2315 .unwrap();
2316 assert_eq!(num_vid, 0);
2317 }
2318
2319 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2320 async fn test_pruned_height_storage() {
2321 let db = TmpDb::init().await;
2322 let cfg = db.config();
2323
2324 let storage = SqlStorage::connect(cfg, StorageConnectionType::Query)
2325 .await
2326 .unwrap();
2327 assert!(
2328 storage
2329 .read()
2330 .await
2331 .unwrap()
2332 .load_pruned_height()
2333 .await
2334 .unwrap()
2335 .is_none()
2336 );
2337 for height in [10, 20, 30] {
2338 let mut tx = storage.write().await.unwrap();
2339 tx.save_pruned_height(height).await.unwrap();
2340 tx.commit().await.unwrap();
2341 assert_eq!(
2342 storage
2343 .read()
2344 .await
2345 .unwrap()
2346 .load_pruned_height()
2347 .await
2348 .unwrap(),
2349 Some(height)
2350 );
2351 }
2352 }
2353
2354 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2355 async fn test_separate_state_data_pruning() {
2356 let db = TmpDb::init().await;
2357 let mut storage = SqlStorage::connect(db.config(), StorageConnectionType::Query)
2358 .await
2359 .unwrap();
2360
2361 let num_blocks = 10u64;
2362 let mut test_tree: UniversalMerkleTree<_, _, _, 8, _> =
2363 MockMerkleTree::new(MockMerkleTree::tree_height());
2364
2365 let mut tx = storage.write().await.unwrap();
2367 for height in 0..num_blocks {
2368 test_tree.update(height as usize, height as usize).unwrap();
2369
2370 let test_data = serde_json::json!({
2371 MockMerkleTree::header_state_commitment_field():
2372 serde_json::to_value(test_tree.commitment()).unwrap()
2373 });
2374 tx.upsert(
2375 "header",
2376 [
2377 "height",
2378 "hash",
2379 "payload_hash",
2380 "timestamp",
2381 "data",
2382 "ns_table",
2383 ],
2384 ["height"],
2385 [(
2386 height as i64,
2387 format!("hash{height}"),
2388 "ph".to_string(),
2389 0,
2390 test_data,
2391 "ns".to_string(),
2392 )],
2393 )
2394 .await
2395 .unwrap();
2396
2397 let (_, proof) = test_tree.lookup(height as usize).expect_ok().unwrap();
2398 let traversal_path = <usize as ToTraversalPath<8>>::to_traversal_path(
2399 &(height as usize),
2400 test_tree.height(),
2401 );
2402 UpdateStateData::<_, MockMerkleTree, 8>::insert_merkle_nodes(
2403 &mut tx,
2404 proof.clone(),
2405 traversal_path,
2406 height,
2407 )
2408 .await
2409 .unwrap();
2410 }
2411 UpdateStateData::<_, MockMerkleTree, 8>::set_last_state_height(
2412 &mut tx,
2413 num_blocks as usize,
2414 )
2415 .await
2416 .unwrap();
2417 tx.commit().await.unwrap();
2418
2419 {
2421 let mut tx = storage.read().await.unwrap();
2422 assert_eq!(tx.load_pruned_height().await.unwrap(), None);
2423 assert_eq!(tx.load_state_pruned_height().await.unwrap(), None);
2424
2425 for height in 0..num_blocks {
2426 assert_eq!(
2427 query_as::<(i64,)>("SELECT count(*) FROM header WHERE height = $1")
2428 .bind(height as i64)
2429 .fetch_one(tx.as_mut())
2430 .await
2431 .unwrap(),
2432 (1,)
2433 );
2434 for i in 0..=height {
2435 tx.get_path(
2436 Snapshot::<_, MockMerkleTree, { MockMerkleTree::ARITY }>::Index(height),
2437 i as usize,
2438 )
2439 .await
2440 .unwrap();
2441 }
2442 }
2443 }
2444
2445 storage.set_pruning_config(
2447 PrunerCfg::default()
2448 .with_state_target_retention(Duration::ZERO)
2449 .with_state_tables(vec![MockMerkleTree::state_type().into()]),
2450 );
2451 storage.prune(&mut Default::default()).await.unwrap();
2452
2453 {
2455 let mut tx = storage.read().await.unwrap();
2456 assert_eq!(tx.load_pruned_height().await.unwrap(), None);
2457 assert_eq!(
2458 tx.load_state_pruned_height().await.unwrap(),
2459 Some(num_blocks - 1)
2460 );
2461
2462 for height in 0..num_blocks {
2463 assert_eq!(
2464 query_as::<(i64,)>("SELECT count(*) FROM header WHERE height = $1")
2465 .bind(height as i64)
2466 .fetch_one(tx.as_mut())
2467 .await
2468 .unwrap(),
2469 (1,)
2470 );
2471
2472 for i in 0..=height {
2473 let err = tx
2474 .get_path(
2475 Snapshot::<_, MockMerkleTree, { MockMerkleTree::ARITY }>::Index(height),
2476 i as usize,
2477 )
2478 .await
2479 .unwrap_err();
2480 assert!(matches!(err, QueryError::NotFound), "{err:?}");
2481 }
2482 }
2483 }
2484 }
2485
2486 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2487 async fn test_transaction_upsert_retries() {
2488 let db = TmpDb::init().await;
2489 let config = db.config();
2490
2491 let storage = SqlStorage::connect(config, StorageConnectionType::Query)
2492 .await
2493 .unwrap();
2494
2495 let mut tx = storage.write().await.unwrap();
2496
2497 tx.upsert("does_not_exist", ["test"], ["test"], [(1_i64,)])
2508 .await
2509 .unwrap_err();
2510 }
2511}