Rust: How to read a config file at runtime and store in a global struct, accessible across threads?
Asked Answered
A

1

7

This is my first project in Rust, and I think I'm missing something simple.

I'm attempting to create a simple Web API daemon that will receive POSTs of JSON, parse the JSON, and send an email using credentials provided in a config file. 90% of that problem has been easy. I am struggling with "parse the config file at runtime".

I'm successfully use hyper and letter to receive JSON and send emails. But I'd like this daemon to be configurable on the server, not at build-time (like most Linux / Unix daemons). I've diligently followed through here.

I've created a config module, declared a struct, and used lazy_static!{} to store an initial version of the configuration struct.

I think I've boiled my problem down to one core question: How do I read and parse a config file, then clone the values into my struct? Especially considering the fact that the size of those values can't be known at runtime...

e.g. src/config.rs

use std::sync::RwLock;
use serde::Deserialize;
use std::fs;
use std::io::prelude::*;

#[derive(Debug, Deserialize, Clone, Copy)]
pub struct RimfireCfg {
    pub verbose: u8,

    /* web api server config */
    pub listen_address: &'static str,

    /* mail server config */
    pub mailserver: &'static str,
    pub port:                u16,
    pub user:       &'static str,
    pub password:   &'static str,
}

lazy_static! {
    pub static ref CONFIG: RwLock<RimfireCfg> = RwLock::new(
        RimfireCfg {
            verbose: 0,
            listen_address: "127.0.0.1:3000",
            mailserver: "smtp-mail.outlook.com",
            port: 587,
            user: "",
            password: "",
        }
    );
}

impl RimfireCfg {
    pub fn init() -> Result<(), i32> {
        let mut w = CONFIG.write().unwrap();

        /* read the config file */
        let _lcfg: RimfireCfg =
            toml::from_slice(&fs::read("rimfire.toml").unwrap()).unwrap();

        // this is clearly wrong ...
        *w.listen_address = _lcfg.listen_address.clone();
        dbg!(*w);

        Ok(())
    }

    pub fn clone_config() -> RimfireCfg {
        let m = CONFIG.read().unwrap();

        *m
    }
}

and src/main.rs:

#[macro_use]
extern crate lazy_static;

mod config;

use config::RimfireCfg;

fn main() {
    let a = RimfireCfg::clone_config();

    dbg!(a);

    RimfireCfg::init().unwrap();

    let a = RimfireCfg::clone_config();

    dbg!(a);
}

Any thoughts? suggestions?

Allpowerful answered 24/11, 2019 at 1:36 Comment(2)
If you're going to clone the config anyway, why not just construct it normally and share it between threads? As for the config parsing itself, that depends on what format you use, you could either roll your own, or use something like toml crate to do it for you. I see you already use serde, and toml depends on it :) Since you seem to write to it only once, there's no reason not to construct Arc<Config> in main and share it immutably without any locks by cloning just the Arc.Horned
@Sahsahae could you form this into an answer?Shoa
M
1

Your struct should store Strings. Strings are mutable and growable -- you can read data from a file into them, unlike &'static strs. &'static strs are usually only for constant string literals. I think using Strings should get your example working with minimal tweaks.

Also, you can use std::mem::swap() to write the whole config at once. This is also more efficient because it doesn't require cloning strings.

I think the global static RwLock is reasonable if you want the config to be a global variable. Alternatively, as in Sahsahae's comment, you could avoid global state and pass an Arc<RimfireCfg> into anything that uses it. This is often cleaner.

Misshape answered 11/12, 2021 at 18:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.