This is easier to work with in terms of Serde, but I quickly hit another issue: std::sync::RwLockReadGuard is not Send, so I can’t hold the guard across .await. For example:
let qb_config: RwLockReadGuard<QbConfig> = qb_config(); // from OnceLock<Config>
async_fn(&qb_config.host).await; // error: non-Send across await
Workaround: use Arc<str>
I changed the field type to Arc<str> so I can cheaply clone it out of the lock before calling async code:
There might be something in serde_with for this, but writing your own deserialisation function for the Config::qb field is not too complex or much boilerplate:
use serde::Deserialize;
use serde::de::Deserializer;
use tokio::sync::RwLock;
#[derive(Deserialize, Debug)]
pub struct Config {
#[serde(deserialize_with = "qb_config")]
pub qb: RwLock<QbConfig>,
// ...
}
#[derive(Deserialize, Debug)]
pub struct QbConfig {
pub host: String,
// ...
}
fn qb_config<'de, D>(d: D) -> Result<RwLock<QbConfig>, D::Error>
where
D: Deserializer<'de>,
{
let qb = QbConfig::deserialize(d)?;
Ok(RwLock::new(qb))
}
fn main() {
let json = r#"{ "qb": { "host": "localhost" } }"#;
let config: Config = serde_json::from_str(json).unwrap();
println!("{config:?}");
}
This is indeed a feasible solution, but my lock contention isn't very high, and I'm not very keen on introducing spawn_blocking, as it would add unnecessary overhead. Therefore, I think this issue is worth further discussion.
I'm relatively new to Rust, so I'm curious about the idiomatic rust solution to the configuration structure design. In other words, this post focus on how to design a nice configuration structure in async context.
You could use two different structs for your Config, one for runtime and one for serialization, and make the runtime Config give you a copy you can serialise (if you want to make it a view type, you're going to need to lock first and make a view in a second step, because self-referential limitation prevents returning a lock guard and inner references at the same time).
You could serialise the inner part of config separately after locking it.
You could write a manual implementation of Serialize for the Config that locks the lock (in a blocking way) and serializes.
You could use ArcSwap to update the config. It supports serde
The only downside is that you can't know for sure when all threads and tasks will use an updated configuration. But the world is inconsistent anyway, and perhaps it's better to be prepared for that.
I'd argue you shouldn't be holding the config lock over .await anyway since often those can block practically indefinitely waiting on a network client, which sounds like a good way to have a denial of service vulnerability.