Skip to main content

espresso_node/persistence/sql/
options.rs

1use std::{num::NonZeroUsize, path::PathBuf, str::FromStr, time::Duration};
2
3use clap::Parser;
4use derivative::Derivative;
5use derive_more::derive::From;
6use espresso_types::{BlockMerkleTree, FeeMerkleTree, Ratio, parse_duration, parse_size};
7use hotshot_query_service::{
8    data_source::storage::pruning::PrunerCfg, merklized_state::MerklizedState,
9};
10
11/// A few days of heights at mainnet rates, and what binds whenever the light client's history is
12/// shorter than that.
13pub const DEFAULT_ARCHIVE_STATE_MIN_RETENTION: u64 = 500_000;
14
15/// Options for Postgres-backed persistence.
16#[derive(Parser, Clone, Derivative)]
17#[derivative(Debug)]
18pub struct PostgresOptions {
19    /// Hostname for the remote Postgres database server.
20    #[clap(long, env = "ESPRESSO_NODE_POSTGRES_HOST")]
21    pub(crate) host: Option<String>,
22
23    /// Port for the remote Postgres database server.
24    #[clap(long, env = "ESPRESSO_NODE_POSTGRES_PORT")]
25    pub(crate) port: Option<u16>,
26
27    /// Name of database to connect to.
28    #[clap(long, env = "ESPRESSO_NODE_POSTGRES_DATABASE")]
29    pub(crate) database: Option<String>,
30
31    /// Postgres user to connect as.
32    #[clap(long, env = "ESPRESSO_NODE_POSTGRES_USER")]
33    pub(crate) user: Option<String>,
34
35    /// Password for Postgres user.
36    #[clap(long, env = "ESPRESSO_NODE_POSTGRES_PASSWORD")]
37    // Hide from debug output since may contain sensitive data.
38    #[derivative(Debug = "ignore")]
39    pub(crate) password: Option<String>,
40
41    /// Use TLS for an encrypted connection to the database.
42    #[clap(long, env = "ESPRESSO_NODE_POSTGRES_USE_TLS")]
43    pub(crate) use_tls: bool,
44
45    /// Disable `DEFERRABLE` on read transactions for the query service.
46    ///
47    /// When true (the default), read transactions on Postgres start with `SERIALIZABLE READ ONLY`
48    /// (no `DEFERRABLE`), so they begin immediately rather than waiting for a safe serializable
49    /// snapshot. This trades start-up latency for the chance of a serialization-error retry.
50    /// Set to false to restore `DEFERRABLE`.
51    #[clap(
52        long,
53        env = "ESPRESSO_NODE_POSTGRES_NO_DEFERRABLE",
54        default_value_t = true
55    )]
56    pub(crate) no_deferrable: bool,
57}
58
59impl Default for PostgresOptions {
60    fn default() -> Self {
61        Self::parse_from(std::iter::empty::<String>())
62    }
63}
64
65#[derive(Parser, Clone, Derivative, Default)]
66#[derivative(Debug)]
67pub struct SqliteOptions {
68    /// Base directory for the SQLite database.
69    /// The SQLite file will be created in the `sqlite` subdirectory with filename as `database`.
70    ///
71    /// Required with `ESPRESSO_NODE_EMBEDDED_DB`.
72    #[clap(
73        long,
74        env = "ESPRESSO_NODE_STORAGE_PATH",
75        value_parser = build_sqlite_path
76    )]
77    pub(crate) path: Option<PathBuf>,
78}
79
80/// The path of the SQLite database under the base directory `path`.
81///
82/// The `sqlite` directory is created when the database is opened, not here: the same directory
83/// is also read by Postgres and file system nodes, which never open it.
84pub fn build_sqlite_path(path: &str) -> anyhow::Result<PathBuf> {
85    Ok(PathBuf::from_str(path)?.join("sqlite").join("database"))
86}
87
88/// A connection pool for whichever backend [`Options::embedded_db`] selects.
89#[derive(Clone, Debug, From)]
90pub(crate) enum SqlPool {
91    Postgres(sqlx::PgPool),
92    Sqlite(sqlx::SqlitePool),
93}
94
95impl TryFrom<SqlPool> for sqlx::PgPool {
96    type Error = SqlPool;
97
98    fn try_from(pool: SqlPool) -> Result<sqlx::PgPool, SqlPool> {
99        match pool {
100            SqlPool::Postgres(pool) => Ok(pool),
101            SqlPool::Sqlite(_) => Err(pool),
102        }
103    }
104}
105
106impl TryFrom<SqlPool> for sqlx::SqlitePool {
107    type Error = SqlPool;
108
109    fn try_from(pool: SqlPool) -> Result<sqlx::SqlitePool, SqlPool> {
110        match pool {
111            SqlPool::Sqlite(pool) => Ok(pool),
112            SqlPool::Postgres(_) => Err(pool),
113        }
114    }
115}
116
117/// [`Options`] wrapped for the backend [`Options::embedded_db`] selects.
118pub enum SqlBackendOptions {
119    Postgres(super::postgres::Options),
120    Sqlite(super::sqlite::Options),
121}
122
123/// Options for database-backed persistence, supporting both Postgres and SQLite.
124#[derive(Parser, Clone, Derivative)]
125#[derivative(Debug)]
126pub struct Options {
127    /// Store data in an embedded SQLite database instead of Postgres.
128    ///
129    /// The database lives under ESPRESSO_NODE_STORAGE_PATH, and the Postgres options are ignored.
130    #[clap(long, env = "ESPRESSO_NODE_EMBEDDED_DB", value_parser = clap::builder::BoolishValueParser::new())]
131    pub embedded_db: bool,
132
133    #[clap(flatten)]
134    pub(crate) postgres_options: PostgresOptions,
135
136    #[clap(flatten)]
137    pub(crate) sqlite_options: SqliteOptions,
138
139    /// Database URI for Postgres or SQLite.
140    ///
141    /// This is a shorthand for setting a number of other options all at once. The URI has the
142    /// following format ([brackets] indicate optional segments):
143    ///
144    /// - **Postgres:** `postgres[ql]://[username[:password]@][host[:port],]/database[?parameter_list]`
145    /// - **SQLite:** `sqlite://path/to/db.sqlite`
146    ///
147    /// Options set explicitly via other env vars or flags will take precedence, so you can use this
148    /// URI to set a baseline and then use other parameters to override or add configuration. In
149    /// addition, there are some parameters which cannot be set via the URI, such as TLS.
150    // Hide from debug output since may contain sensitive data.
151    #[derivative(Debug = "ignore")]
152    pub(crate) uri: Option<String>,
153
154    /// This will enable the pruner and set the default pruning parameters unless provided.
155    /// Default parameters:
156    /// - pruning_threshold: 3 TB
157    /// - minimum_retention: 1 day
158    /// - target_retention: 7 days
159    /// - batch_size: 100
160    /// - max_usage: 80%
161    /// - interval: 1 hour
162    ///
163    /// A pruned node with no merklized state starts it from a recent epoch boundary. Its trees then
164    /// lack block proofs below that boundary, and fee accounts that no block after it touched. They
165    /// stay incomplete after a switch to `--archive`: complete trees need a replay from genesis,
166    /// which runs only on a database with no merklized state.
167    ///
168    /// A pruned node keeps no transaction or payload totals, which count from genesis: it does not
169    /// run the aggregator, and the routes that serve the totals return 404.
170    #[clap(long, env = "ESPRESSO_NODE_DATABASE_PRUNE")]
171    pub(crate) prune: bool,
172
173    /// Pruning parameters.
174    #[clap(flatten)]
175    pub(crate) pruning: PruningOptions,
176
177    /// Pruning parameters for ephemeral consensus storage.
178    #[clap(flatten)]
179    pub(crate) consensus_pruning: ConsensusPruningOptions,
180
181    /// Retry/backoff parameters for PostgreSQL serialization conflicts.
182    #[clap(flatten)]
183    pub(crate) serializable_retry: SerializableRetryOptions,
184
185    /// Specifies the maximum number of concurrent fetch requests allowed from peers.
186    #[clap(long, env = "ESPRESSO_NODE_FETCH_RATE_LIMIT")]
187    pub(crate) fetch_rate_limit: Option<usize>,
188
189    /// The minimum delay between active fetches in a stream.
190    #[clap(long, env = "ESPRESSO_NODE_ACTIVE_FETCH_DELAY", value_parser = parse_duration)]
191    pub(crate) active_fetch_delay: Option<Duration>,
192
193    /// The minimum delay between loading chunks in a stream.
194    #[clap(long, env = "ESPRESSO_NODE_CHUNK_FETCH_DELAY", value_parser = parse_duration)]
195    pub(crate) chunk_fetch_delay: Option<Duration>,
196
197    /// How many of a request's height ranges to serve at once.
198    #[clap(long, env = "ESPRESSO_NODE_RANGES_CONCURRENCY")]
199    pub(crate) ranges_concurrency: Option<NonZeroUsize>,
200
201    /// The number of items to process in a single transaction when scanning the database for
202    /// missing objects.
203    #[clap(long, env = "ESPRESSO_NODE_SYNC_STATUS_CHUNK_SIZE")]
204    pub(crate) sync_status_chunk_size: Option<usize>,
205
206    /// Duration to cache sync status results for.
207    #[clap(long, env = "ESPRESSO_NODE_SYNC_STATUS_TTL", value_parser = parse_duration)]
208    pub(crate) sync_status_ttl: Option<Duration>,
209
210    /// The number of items to process at a time when scanning for proactive fetching.
211    #[clap(long, env = "ESPRESSO_NODE_PROACTIVE_SCAN_CHUNK_SIZE")]
212    pub(crate) proactive_scan_chunk_size: Option<usize>,
213
214    /// The time interval between proactive fetching scans.
215    #[clap(long, env = "ESPRESSO_NODE_PROACTIVE_SCAN_INTERVAL", value_parser = parse_duration)]
216    pub(crate) proactive_scan_interval: Option<Duration>,
217
218    /// How long a proactive scan waits for one request of missing ranges before fetching its
219    /// chunks one at a time instead.
220    #[clap(long, env = "ESPRESSO_NODE_PROACTIVE_FETCH_TIMEOUT", value_parser = parse_duration)]
221    pub(crate) proactive_fetch_timeout: Option<Duration>,
222
223    /// Disable the proactive scanner task.
224    #[clap(long, env = "ESPRESSO_NODE_DISABLE_PROACTIVE_FETCHING")]
225    pub(crate) disable_proactive_fetching: bool,
226
227    /// Disable pruning and reconstruct previously pruned data.
228    ///
229    /// While running without pruning is the default behavior, the default will not try to
230    /// reconstruct data that was pruned in a previous run where pruning was enabled. This option
231    /// instructs the service to run without pruning _and_ reconstruct all previously pruned data by
232    /// fetching from peers.
233    ///
234    /// Historical merklized state older than the light client contract history is garbage collected,
235    /// but never less than ESPRESSO_NODE_ARCHIVE_STATE_MIN_RETENTION heights behind the head. Use
236    /// ESPRESSO_NODE_ARCHIVE_FULL_STATE to retain all of it.
237    #[clap(long, env = "ESPRESSO_NODE_ARCHIVE", conflicts_with = "prune")]
238    pub(crate) archive: bool,
239
240    /// Block heights of merklized state an archive node retains regardless of the light client.
241    ///
242    /// Collection stops at whichever reaches further back, this floor or the light client
243    /// contract's history. Set it to zero to follow the contract alone.
244    #[clap(
245        long,
246        env = "ESPRESSO_NODE_ARCHIVE_STATE_MIN_RETENTION",
247        default_value_t = DEFAULT_ARCHIVE_STATE_MIN_RETENTION
248    )]
249    pub(crate) archive_state_min_retention: u64,
250
251    /// Retain all historical merklized state on an archive node.
252    ///
253    /// Disables the state garbage collector that archive nodes run by default.
254    #[clap(long, env = "ESPRESSO_NODE_ARCHIVE_FULL_STATE")]
255    pub(crate) archive_full_state: bool,
256
257    /// Turns on leaf only data storage
258    ///
259    /// A lightweight node keeps no transaction or payload totals: it does not run the aggregator,
260    /// and the routes that serve the totals return 404.
261    #[clap(
262        long,
263        env = "ESPRESSO_NODE_LIGHTWEIGHT",
264        default_value_t = false,
265        conflicts_with = "archive"
266    )]
267    pub(crate) lightweight: bool,
268
269    /// The maximum idle time of a database connection.
270    ///
271    /// Any connection which has been open and unused longer than this duration will be
272    /// automatically closed to reduce load on the server.
273    #[clap(long, env = "ESPRESSO_NODE_DATABASE_IDLE_CONNECTION_TIMEOUT", value_parser = parse_duration, default_value = "10m")]
274    pub(crate) idle_connection_timeout: Duration,
275
276    /// The maximum lifetime of a database connection.
277    ///
278    /// Any connection which has been open longer than this duration will be automatically closed
279    /// (and, if needed, replaced), even if it is otherwise healthy. It is good practice to refresh
280    /// even healthy connections once in a while (e.g. daily) in case of resource leaks in the
281    /// server implementation.
282    #[clap(long, env = "ESPRESSO_NODE_DATABASE_CONNECTION_TIMEOUT", value_parser = parse_duration, default_value = "30m")]
283    pub(crate) connection_timeout: Duration,
284
285    #[clap(long, env = "ESPRESSO_NODE_DATABASE_SLOW_STATEMENT_THRESHOLD", value_parser = parse_duration, default_value = "1s")]
286    pub(crate) slow_statement_threshold: Duration,
287
288    /// The maximum time a single SQL statement is allowed to run before being canceled.
289    ///
290    /// This helps prevent queries from running indefinitely and consuming resources.
291    /// Set to 10 minutes by default
292    #[clap(long, env = "ESPRESSO_NODE_DATABASE_STATEMENT_TIMEOUT", value_parser = parse_duration, default_value = "10m")]
293    pub(crate) statement_timeout: Duration,
294
295    /// The minimum number of database connections to maintain at any time.
296    ///
297    /// The database client will, to the best of its ability, maintain at least `min` open
298    /// connections at all times. This can be used to reduce the latency hit of opening new
299    /// connections when at least this many simultaneous connections are frequently needed.
300    #[clap(
301        long,
302        env = "ESPRESSO_NODE_DATABASE_MIN_CONNECTIONS",
303        default_value = "0"
304    )]
305    pub(crate) min_connections: u32,
306
307    /// Allows setting a different maximum number of connections for query operations.
308    /// Default value of None implies using the min_connections value. Postgres only.
309    #[clap(long, env = "ESPRESSO_NODE_DATABASE_QUERY_MIN_CONNECTIONS", default_value = None)]
310    pub(crate) query_min_connections: Option<u32>,
311
312    /// The maximum number of database connections to maintain at any time.
313    ///
314    /// Once `max` connections are in use simultaneously, further attempts to acquire a connection
315    /// (or begin a transaction) will block until one of the existing connections is released.
316    #[clap(
317        long,
318        env = "ESPRESSO_NODE_DATABASE_MAX_CONNECTIONS",
319        default_value = "25"
320    )]
321    pub(crate) max_connections: u32,
322
323    /// Allows setting a different maximum number of connections for query operations.
324    /// Default value of None implies using the max_connections value. Postgres only.
325    #[clap(long, env = "ESPRESSO_NODE_DATABASE_QUERY_MAX_CONNECTIONS", default_value = None)]
326    pub(crate) query_max_connections: Option<u32>,
327
328    // Keep the database connection pool when persistence is created,
329    // allowing it to be reused across multiple instances instead of creating
330    // a new pool each time such as for API, consensus storage etc
331    // This also ensures all storage instances adhere to the MAX_CONNECTIONS limit if set
332    //
333    // Note: Cloning the `Pool` is lightweight and efficient because it simply
334    // creates a new reference-counted handle to the underlying pool state.
335    #[clap(skip)]
336    pub(crate) pool: Option<SqlPool>,
337
338    #[clap(skip)]
339    pub(crate) consensus_only: bool,
340}
341
342impl Default for Options {
343    fn default() -> Self {
344        Self::parse_from(std::iter::empty::<String>())
345    }
346}
347
348impl Options {
349    /// These options for the backend [`embedded_db`](Self::embedded_db) selects.
350    pub fn into_backend(self) -> SqlBackendOptions {
351        if self.embedded_db {
352            SqlBackendOptions::Sqlite(super::sqlite::Options(self))
353        } else {
354            SqlBackendOptions::Postgres(super::postgres::Options(self))
355        }
356    }
357}
358
359impl From<PostgresOptions> for Options {
360    fn from(opt: PostgresOptions) -> Self {
361        Options {
362            embedded_db: false,
363            postgres_options: opt,
364            max_connections: 20,
365            idle_connection_timeout: Duration::from_secs(120),
366            connection_timeout: Duration::from_secs(10240),
367            slow_statement_threshold: Duration::from_secs(1),
368            statement_timeout: Duration::from_secs(600),
369            ..Default::default()
370        }
371    }
372}
373
374impl From<SqliteOptions> for Options {
375    fn from(opt: SqliteOptions) -> Self {
376        Options {
377            embedded_db: true,
378            postgres_options: PostgresOptions::default(),
379            sqlite_options: opt,
380            max_connections: 5,
381            idle_connection_timeout: Duration::from_secs(120),
382            connection_timeout: Duration::from_secs(10240),
383            slow_statement_threshold: Duration::from_secs(1),
384            uri: None,
385            statement_timeout: Duration::from_secs(600),
386            prune: false,
387            pruning: Default::default(),
388            consensus_pruning: Default::default(),
389            fetch_rate_limit: None,
390            active_fetch_delay: None,
391            chunk_fetch_delay: None,
392            ranges_concurrency: None,
393            sync_status_chunk_size: None,
394            sync_status_ttl: None,
395            proactive_scan_chunk_size: None,
396            proactive_scan_interval: None,
397            proactive_fetch_timeout: None,
398            disable_proactive_fetching: false,
399            archive: false,
400            archive_state_min_retention: DEFAULT_ARCHIVE_STATE_MIN_RETENTION,
401            archive_full_state: false,
402            lightweight: false,
403            min_connections: 0,
404            query_min_connections: None,
405            query_max_connections: None,
406            pool: None,
407            consensus_only: false,
408            serializable_retry: SerializableRetryOptions::default(),
409        }
410    }
411}
412
413/// Pruning parameters.
414#[derive(Parser, Clone, Copy, Debug)]
415pub struct PruningOptions {
416    /// Threshold for pruning, specified in bytes.
417    /// If the disk usage surpasses this threshold, pruning is initiated for data older than the specified minimum retention period.
418    /// Pruning continues until the disk usage drops below the MAX USAGE.
419    #[clap(long, env = "ESPRESSO_NODE_PRUNER_PRUNING_THRESHOLD", value_parser = parse_size)]
420    pub(crate) pruning_threshold: Option<u64>,
421
422    /// Minimum retention period.
423    /// Data is retained for at least this duration, even if there's no free disk space.
424    #[clap(
425        long,
426        env = "ESPRESSO_NODE_PRUNER_MINIMUM_RETENTION",
427        value_parser = parse_duration,
428    )]
429    pub(crate) minimum_retention: Option<Duration>,
430
431    /// Minimum retention period for Merklized state.
432    /// State is retained for at least this duration, even if there's no free disk space.
433    #[clap(
434        long,
435        env = "ESPRESSO_NODE_PRUNER_STATE_MINIMUM_RETENTION",
436        value_parser = parse_duration,
437    )]
438    state_minimum_retention: Option<Duration>,
439
440    /// Target retention period.
441    /// Data older than this is pruned to free up space.
442    #[clap(
443        long,
444        env = "ESPRESSO_NODE_PRUNER_TARGET_RETENTION",
445        value_parser = parse_duration,
446    )]
447    pub(crate) target_retention: Option<Duration>,
448
449    /// Target retention period for Merklized state.
450    /// State older than this is pruned to free up space, but never at or past the newest
451    /// merklized state, which the state writer resumes from.
452    #[clap(
453        long,
454        env = "ESPRESSO_NODE_PRUNER_STATE_TARGET_RETENTION",
455        value_parser = parse_duration,
456    )]
457    state_target_retention: Option<Duration>,
458
459    /// Batch size for pruning.
460    /// This is the number of blocks worth of data to delete in a single transaction. Heights that
461    /// hold no data are skipped without counting, so a batch, and the `Pruned to height` log line,
462    /// can advance by more than this many heights at once.
463    #[clap(long, env = "ESPRESSO_NODE_PRUNER_BATCH_SIZE")]
464    pub(crate) batch_size: Option<u64>,
465
466    /// Maximum disk usage (in basis points).
467    ///
468    /// Pruning stops once the disk usage falls below this value, even if
469    /// some data older than the `MINIMUM_RETENTION` remains. Values range
470    /// from 0 (0%) to 10000 (100%).
471    #[clap(long, env = "ESPRESSO_NODE_PRUNER_MAX_USAGE")]
472    pub(crate) max_usage: Option<u16>,
473
474    /// Interval for running the pruner.
475    #[clap(
476        long,
477        env = "ESPRESSO_NODE_PRUNER_INTERVAL",
478        value_parser = parse_duration,
479    )]
480    pub(crate) interval: Option<Duration>,
481
482    /// Number of SQLite pages to vacuum from the freelist
483    /// during each pruner cycle.
484    /// This value corresponds to `N` in the SQLite PRAGMA `incremental_vacuum(N)`,
485    #[clap(long, env = "ESPRESSO_NODE_PRUNER_INCREMENTAL_VACUUM_PAGES")]
486    pub(crate) pages: Option<u64>,
487}
488
489impl Default for PruningOptions {
490    fn default() -> Self {
491        Self::parse_from(std::iter::empty::<String>())
492    }
493}
494
495impl From<PruningOptions> for PrunerCfg {
496    fn from(opt: PruningOptions) -> Self {
497        let mut cfg = PrunerCfg::new();
498        if let Some(threshold) = opt.pruning_threshold {
499            cfg = cfg.with_pruning_threshold(threshold);
500        }
501        if let Some(min) = opt.minimum_retention {
502            cfg = cfg.with_minimum_retention(min);
503        }
504        if let Some(min) = opt.state_minimum_retention {
505            cfg = cfg.with_state_minimum_retention(min);
506        }
507        if let Some(target) = opt.target_retention {
508            cfg = cfg.with_target_retention(target);
509        }
510        if let Some(target) = opt.state_target_retention {
511            cfg = cfg.with_state_target_retention(target);
512        }
513        if let Some(batch) = opt.batch_size {
514            cfg = cfg.with_batch_size(batch);
515        }
516        if let Some(max) = opt.max_usage {
517            cfg = cfg.with_max_usage(max);
518        }
519        if let Some(interval) = opt.interval {
520            cfg = cfg.with_interval(interval);
521        }
522
523        if let Some(pages) = opt.pages {
524            cfg = cfg.with_incremental_vacuum_pages(pages)
525        }
526
527        cfg = cfg.with_state_tables(vec![
528            BlockMerkleTree::state_type().to_string(),
529            FeeMerkleTree::state_type().to_string(),
530        ]);
531
532        cfg
533    }
534}
535
536/// Pruning parameters for ephemeral consensus storage.
537#[derive(Parser, Clone, Copy, Debug)]
538pub struct ConsensusPruningOptions {
539    /// Number of views to try to retain in consensus storage before data that hasn't been archived
540    /// is garbage collected.
541    ///
542    /// The longer this is, the more certain that all data will eventually be archived, even if
543    /// there are temporary problems with archive storage or partially missing data. This can be set
544    /// very large, as most data is garbage collected as soon as it is finalized by consensus. This
545    /// setting only applies to views which never get decided (ie forks in consensus) and views for
546    /// which this node is partially offline. These should be exceptionally rare.
547    ///
548    /// Note that in extreme scenarios, data may be garbage collected even before TARGET_RETENTION
549    /// views, if consensus storage exceeds TARGET_USAGE. For a hard lower bound on how long
550    /// consensus data will be retained, see MINIMUM_RETENTION.
551    ///
552    /// The default of 302000 views equates to approximately 1 week (604800 seconds) at an average
553    /// view time of 2s.
554    #[clap(
555        name = "TARGET_RETENTION",
556        long = "consensus-storage-target-retention",
557        env = "ESPRESSO_NODE_CONSENSUS_STORAGE_TARGET_RETENTION",
558        default_value = "302000"
559    )]
560    pub(crate) target_retention: u64,
561
562    /// Minimum number of views to try to retain in consensus storage before data that hasn't been
563    /// archived is garbage collected.
564    ///
565    /// This bound allows data to be retained even if consensus storage occupies more than
566    /// TARGET_USAGE. This can be used to ensure sufficient time to move consensus data to archival
567    /// storage as necessary, even under extreme circumstances where otherwise garbage collection
568    /// would kick in based on TARGET_RETENTION.
569    ///
570    /// The default of 130000 views equates to approximately 3 days (259200 seconds) at an average
571    /// view time of 2s.
572    #[clap(
573        name = "MINIMUM_RETENTION",
574        long = "consensus-storage-minimum-retention",
575        env = "ESPRESSO_NODE_CONSENSUS_STORAGE_MINIMUM_RETENTION",
576        default_value = "130000"
577    )]
578    pub(crate) minimum_retention: u64,
579
580    /// Amount (in bytes) of data to retain in consensus storage before garbage collecting more
581    /// aggressively.
582    ///
583    /// See also TARGET_RETENTION and MINIMUM_RETENTION.
584    #[clap(
585        name = "TARGET_USAGE",
586        long = "consensus-storage-target-usage",
587        env = "ESPRESSO_NODE_CONSENSUS_STORAGE_TARGET_USAGE",
588        default_value = "1000000000"
589    )]
590    pub(crate) target_usage: u64,
591
592    /// How often to measure consensus storage against TARGET_USAGE.
593    ///
594    /// On SQLite a measurement scans every page of the consensus tables. Between measurements,
595    /// each decide prunes only to TARGET_RETENTION.
596    #[clap(
597        name = "USAGE_CHECK_INTERVAL",
598        long = "consensus-storage-usage-check-interval",
599        env = "ESPRESSO_NODE_CONSENSUS_STORAGE_USAGE_CHECK_INTERVAL",
600        default_value = "5m",
601        value_parser = parse_duration
602    )]
603    pub(crate) usage_check_interval: Duration,
604}
605
606impl Default for ConsensusPruningOptions {
607    fn default() -> Self {
608        Self::parse_from(std::iter::empty::<String>())
609    }
610}
611
612/// Configuration for retrying transactions aborted by PostgreSQL serialization conflicts (error
613/// 40001).  Under SERIALIZABLE isolation these aborts are expected and safe to retry from scratch.
614#[derive(Parser, Clone, Copy, Debug)]
615pub struct SerializableRetryOptions {
616    /// Maximum number of retries on PostgreSQL serialization conflicts.
617    #[clap(
618        long = "serializable-retry-max",
619        env = "ESPRESSO_NODE_SERIALIZABLE_RETRY_MAX",
620        default_value = "100"
621    )]
622    pub(super) retry_max: u32,
623
624    /// Initial delay before retrying after a PostgreSQL serialization conflict.
625    #[clap(
626        long = "serializable-backoff-base",
627        env = "ESPRESSO_NODE_SERIALIZABLE_BACKOFF_BASE",
628        default_value = "10ms",
629        value_parser = parse_duration
630    )]
631    pub(super) backoff_base: Duration,
632
633    /// Maximum delay between retries after a PostgreSQL serialization conflict.
634    #[clap(
635        long = "serializable-backoff-max",
636        env = "ESPRESSO_NODE_SERIALIZABLE_BACKOFF_MAX",
637        default_value = "500ms",
638        value_parser = parse_duration
639    )]
640    pub(super) backoff_max: Duration,
641
642    /// Multiplier applied to the backoff delay between successive retries.
643    #[clap(
644        long = "serializable-backoff-factor",
645        env = "ESPRESSO_NODE_SERIALIZABLE_BACKOFF_FACTOR",
646        default_value = "2"
647    )]
648    pub(super) backoff_factor: u32,
649
650    /// Backoff jitter as a ratio of the backoff delay, numerator:denominator.
651    #[clap(
652        long = "serializable-backoff-jitter",
653        env = "ESPRESSO_NODE_SERIALIZABLE_BACKOFF_JITTER",
654        default_value = "5:10"
655    )]
656    pub(super) backoff_jitter: Ratio,
657
658    /// When set, enable verbose diagnostic logging on PostgreSQL serialization conflicts.  Each
659    /// retried conflict spawns a background query against `pg_stat_activity` and `pg_locks` to
660    /// log concurrent sessions and predicate locks. Off by default.
661    #[clap(
662        long = "serializable-pg-stat-diag",
663        env = "ESPRESSO_NODE_SERIALIZABLE_PG_STAT_DIAG"
664    )]
665    pub(super) pg_stat_diag: bool,
666}
667
668impl Default for SerializableRetryOptions {
669    fn default() -> Self {
670        Self::parse_from(std::iter::empty::<String>())
671    }
672}
673
674/// How far behind the newest persisted decide an out-of-order ("gap-fill") decide can still
675/// arrive. Mirrors `DECIDE_BUFFER` in `hotshot-new-protocol`; a leaf missing further behind the
676/// watermark than this will never be filled in by consensus.
677pub(crate) const DECIDE_GAP_FILL_HORIZON: u64 = 20;
678
679/// Whether the height gap directly below the leaf at `view` can still be filled by a late
680/// decide: the missing view (at most `view - 1`) must be within [`DECIDE_GAP_FILL_HORIZON`] of
681/// `watermark`, the newest persisted decide.
682pub(crate) fn within_gap_fill_horizon(view: u64, watermark: u64) -> bool {
683    view.saturating_sub(1) + DECIDE_GAP_FILL_HORIZON > watermark
684}
685
686#[cfg(test)]
687mod test {
688    use super::*;
689
690    #[test]
691    fn embedded_db_is_read_from_the_env_as_a_strict_boolean() {
692        use clap::{CommandFactory as _, FromArgMatches as _};
693
694        fn parse(command: &clap::Command, value: &str) -> Option<bool> {
695            let matches = command
696                .clone()
697                .try_get_matches_from(["espresso-node", "--embedded-db", value])
698                .ok()?;
699            Some(Options::from_arg_matches(&matches).unwrap().embedded_db)
700        }
701
702        let command = Options::command();
703        let arg = command
704            .get_arguments()
705            .find(|arg| arg.get_id() == "embedded_db")
706            .unwrap();
707        assert_eq!(
708            arg.get_env(),
709            Some(std::ffi::OsStr::new("ESPRESSO_NODE_EMBEDDED_DB"))
710        );
711
712        // Feeds the value on the command line so the test runs the variable's parser without
713        // mutating the process environment that concurrent tests read.
714        let command = command.mut_arg("embedded_db", |arg| arg.action(clap::ArgAction::Set));
715        for value in ["true", "TRUE", "t", "1", "yes", "Y", "on"] {
716            assert_eq!(parse(&command, value), Some(true), "{value:?}");
717        }
718        for value in ["false", "False", "f", "0", "no", "N", "off"] {
719            assert_eq!(parse(&command, value), Some(false), "{value:?}");
720        }
721        for value in ["maybe", "enabled", "2"] {
722            assert_eq!(parse(&command, value), None, "{value:?}");
723        }
724    }
725
726    #[test]
727    fn embedded_db_selects_sqlite_under_the_storage_path() {
728        let opt =
729            Options::try_parse_from(["storage-sql", "--embedded-db", "--path", "/data"]).unwrap();
730        assert!(opt.embedded_db);
731        assert_eq!(
732            opt.sqlite_options.path,
733            Some(PathBuf::from("/data/sqlite/database"))
734        );
735    }
736
737    #[test]
738    fn parsing_the_sqlite_path_creates_no_directory() {
739        let dir = tempfile::tempdir().unwrap();
740        let base = dir.path().join("store");
741        Options::try_parse_from(["storage-sql", "--path", base.to_str().unwrap()]).unwrap();
742        assert!(!base.exists());
743    }
744}