use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
pub endpoint: String,
pub timeout: Duration,
pub max_retries: u32,
pub feature_flags: Vec<String>,
}
impl Config {
pub fn new(endpoint: impl Into<String>) -> Self {
Config {
endpoint: endpoint.into(),
timeout: Duration::from_secs(30),
max_retries: 3,
feature_flags: Vec::new(),
}
}
pub fn with_timeout(&self, timeout: Duration) -> Self {
let mut next = self.clone();
next.timeout = timeout;
next
}
pub fn with_flag(&self, flag: impl Into<String>) -> Self {
let mut next = self.clone();
let flag = flag.into();
if !next.feature_flags.contains(&flag) {
next.feature_flags.push(flag);
}
next
}
pub fn has_flag(&self, flag: &str) -> bool {
self.feature_flags.iter().any(|f| f == flag)
}
}
use std::sync::Arc;
use arc_swap::ArcSwap;
use crate::config::Config;
pub struct ConfigStore {
inner: ArcSwap<Config>,
}
impl ConfigStore {
pub fn new(initial: Config) -> Self {
ConfigStore {
inner: ArcSwap::from_pointee(initial),
}
}
// Lock-free read: returns an immutable snapshot readers can hold as long as they like.
pub fn current(&self) -> Arc<Config> {
self.inner.load_full()
}
// Unconditional replace with a freshly built snapshot.
pub fn store(&self, next: Config) {
self.inner.store(Arc::new(next));
}
// Read-copy-update: derive the new config from the current one, retrying on races.
pub fn update<F>(&self, f: F)
where
F: Fn(&Config) -> Config,
{
self.inner.rcu(|current| Arc::new(f(current)));
}
pub fn reload(&self, endpoint: impl Into<String>) {
let endpoint = endpoint.into();
self.update(|current| {
let mut next = current.clone();
next.endpoint = endpoint.clone();
next
});
}
}
mod config;
mod store;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use config::Config;
use store::ConfigStore;
fn main() {
let store = Arc::new(ConfigStore::new(Config::new("https://api.local")));
let mut readers = Vec::new();
for id in 0..4 {
let store = Arc::clone(&store);
readers.push(thread::spawn(move || {
for _ in 0..5 {
let snapshot = store.current();
println!(
"reader {id}: endpoint={} timeout={:?} beta={}",
snapshot.endpoint,
snapshot.timeout,
snapshot.has_flag("beta")
);
thread::sleep(Duration::from_millis(10));
}
}));
}
let writer = {
let store = Arc::clone(&store);
thread::spawn(move || {
thread::sleep(Duration::from_millis(15));
store.reload("https://api.prod");
store.update(|c| c.with_timeout(Duration::from_secs(5)).with_flag("beta"));
})
};
writer.join().unwrap();
for r in readers {
r.join().unwrap();
}
let final_config = store.current();
println!("final: {:?}", final_config);
}
This snippet shows a common pattern for serving hot-reloadable configuration to many reader threads without any lock contention on the read path. The core idea is copy-on-write: readers hold an immutable Arc<Config> snapshot, and updates never mutate a shared struct in place. Instead a writer builds a brand new Config, wraps it in a fresh Arc, and atomically swaps the pointer. Readers that grabbed the old Arc keep observing a consistent snapshot until they drop it, so there is no torn read and no reader-side blocking.
In config.rs, Config is a plain immutable struct plus a with_timeout helper that returns a modified clone rather than mutating self. This is the copy-on-write step: cloning a config is cheap relative to how often it is read, and it guarantees each published snapshot is frozen forever. Because Config is never mutated after publication, it needs no interior locking and is trivially Send + Sync.
In store.rs, ConfigStore wraps an arc_swap::ArcSwap<Config>. The read path current() calls load_full(), which is a lock-free atomic load returning a cheaply-cloned Arc; hot code can call this on every request. The write path reload() and update() use rcu, a read-copy-update loop that reads the current value, derives a new one via a closure, and compare-and-swaps it in, retrying if another writer won the race. This makes concurrent updates safe without a mutex, and store() handles the simpler unconditional replace.
The trade-off is memory and clone cost: each update allocates a new Config, and readers holding old snapshots delay reclamation. That is acceptable when reads vastly outnumber writes, which is exactly the config-reload case. A subtle pitfall is expecting a long-running request to see a mid-flight update — it will not, and that consistency is usually a feature. main.rs demonstrates spawning reader threads that each pin a snapshot while a writer thread swaps in new values, showing readers never block and always see a coherent Config.
Related snips
class Money
include Comparable
attr_reader :amount, :currency
def initialize(amount, currency = 'USD')
Value objects for domain modeling
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
Share this code
Here's the card — post it anywhere.