taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

lib.rs (4571B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2024, 2025, 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::{path::PathBuf, time::Duration};
     18 
     19 use config::{Config, parser::ConfigSource};
     20 use mimalloc::MiMalloc;
     21 use tracing::error;
     22 use tracing_subscriber::util::SubscriberInitExt;
     23 
     24 use crate::log::taler_logger;
     25 
     26 pub mod api;
     27 pub mod bench;
     28 pub mod cli;
     29 pub mod config;
     30 pub mod db;
     31 pub mod encoding;
     32 pub mod error;
     33 pub mod error_code;
     34 pub mod json_file;
     35 pub mod log;
     36 pub mod signature;
     37 pub mod types;
     38 
     39 #[global_allocator]
     40 static GLOBAL: MiMalloc = MiMalloc;
     41 
     42 #[derive(clap::Parser, Debug, Clone)]
     43 pub struct CommonArgs {
     44     /// Specifies the configuration file
     45     #[arg(short, long, global = true)]
     46     config: Option<PathBuf>,
     47 
     48     /// Configure logging to use LOGLEVEL
     49     #[arg(short('L'), long, global = true)]
     50     log: Option<tracing::Level>,
     51 
     52     /// Show logs from all sources
     53     #[arg(short, long, global = true, hide = true)]
     54     verbose: bool,
     55 }
     56 
     57 pub fn taler_main(
     58     src: ConfigSource,
     59     args: CommonArgs,
     60     app: impl AsyncFnOnce(&Config) -> Result<(), anyhow::Error>,
     61 ) {
     62     taler_logger(args.log, args.verbose).init();
     63     let cfg = match Config::load(src, args.config) {
     64         Ok(cfg) => cfg,
     65         Err(err) => {
     66             error!(target: "config", "{}", err);
     67             std::process::exit(6);
     68         }
     69     };
     70 
     71     // Setup async runtime
     72     let runtime = tokio::runtime::Builder::new_multi_thread()
     73         .enable_all()
     74         .build()
     75         .unwrap();
     76 
     77     // Run app
     78     let result = runtime.block_on(app(&cfg));
     79     if let Err(err) = result {
     80         error!(target: "cli", "{}", err);
     81         std::process::exit(error_exit_status(&err));
     82     }
     83 }
     84 
     85 /// DD102: only diagnosed configuration errors suppress service recovery.
     86 fn error_exit_status(err: &anyhow::Error) -> i32 {
     87     if err.chain().any(|cause| {
     88         cause.is::<config::parser::ParserErr>()
     89             || cause.is::<config::ValueErr>()
     90             || cause.is::<config::PathsubErr>()
     91     }) {
     92         6
     93     } else {
     94         1
     95     }
     96 }
     97 
     98 #[cfg(test)]
     99 mod exit_status_tests {
    100     use super::*;
    101 
    102     #[test]
    103     fn configuration_errors_keep_their_status_through_context() {
    104         let cfg = Config::from_mem("[test]\nport = invalid\n").unwrap();
    105         let err = cfg
    106             .section("test")
    107             .number::<u16>("port")
    108             .require()
    109             .unwrap_err();
    110         assert_eq!(
    111             6,
    112             error_exit_status(&anyhow::Error::new(err).context("starting server"))
    113         );
    114         let err = cfg.section("test").str("missing").require().unwrap_err();
    115         assert_eq!(6, error_exit_status(&anyhow::Error::new(err)));
    116         let err = Config::from_mem("not a configuration entry").unwrap_err();
    117         assert_eq!(6, error_exit_status(&anyhow::Error::new(err)));
    118     }
    119 
    120     #[test]
    121     fn unavailable_dependencies_remain_restartable() {
    122         let err = std::io::Error::from(std::io::ErrorKind::ConnectionRefused);
    123         assert_eq!(
    124             1,
    125             error_exit_status(&anyhow::Error::new(err).context("database"))
    126         );
    127     }
    128 }
    129 
    130 /// Infinite exponential backoff with decorrelated jitter
    131 pub struct ExpoBackoffDecorr {
    132     base: u32,
    133     max: u32,
    134     factor: f32,
    135     sleep: u32,
    136 }
    137 
    138 impl ExpoBackoffDecorr {
    139     pub fn new(base: Duration, max: Duration, factor: f32) -> Self {
    140         Self {
    141             base: base.as_millis() as u32,
    142             max: max.as_millis() as u32,
    143             factor,
    144             sleep: base.as_millis() as u32,
    145         }
    146     }
    147 
    148     pub fn backoff(&mut self) -> Duration {
    149         self.sleep =
    150             rand::random_range(self.base..(self.sleep as f32 * self.factor) as u32).min(self.max);
    151         Duration::from_millis(self.sleep as u64)
    152     }
    153 
    154     pub fn reset(&mut self) {
    155         self.sleep = self.base
    156     }
    157 }
    158 
    159 impl Default for ExpoBackoffDecorr {
    160     fn default() -> Self {
    161         Self::new(Duration::from_millis(400), Duration::from_secs(30), 2.5)
    162     }
    163 }