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}