better cli setup

This commit is contained in:
Aleksandr 2025-10-06 23:09:54 +03:00
parent 321ab18890
commit 8015bcc99f
8 changed files with 321 additions and 31 deletions

View file

@ -7,13 +7,16 @@ edition = "2024"
members = ["macros", "utils"] members = ["macros", "utils"]
[features] [features]
default = ["tokio"] default = []
cli = [ cli = [
"tokio/rt-multi-thread", "tokio/rt-multi-thread",
"dep:clap", "dep:clap",
"dep:figment",
"dep:color-eyre", "dep:color-eyre",
"dep:num_cpus", "dep:num_cpus",
"figment/yaml",
"figment/json",
"figment/toml",
"figment/env"
] ]
tokio = ["dep:tokio"] tokio = ["dep:tokio"]
# Very long running. # Very long running.
@ -53,7 +56,7 @@ blake3 = "1.8.2"
slotmap = { version = "1.0.7", features = ["serde"] } slotmap = { version = "1.0.7", features = ["serde"] }
clap = { version = "4.5.47", features = ["derive"], optional = true } clap = { version = "4.5.47", features = ["derive"], optional = true }
figment = { version = "0.10.19", features = ["json", "yaml", "env", "toml"], optional = true } figment = { version = "0.10.19" }
tokio = { version = "1.47.1", features = ["rt", "sync", "macros"], optional = true } tokio = { version = "1.47.1", features = ["rt", "sync", "macros"], optional = true }
color-eyre = { version = "0.6.5", optional = true } color-eyre = { version = "0.6.5", optional = true }
eyre = { version = "0.6.12" } eyre = { version = "0.6.12" }

View file

@ -1,4 +1,9 @@
use std::{num::NonZeroUsize, path::PathBuf}; use std::{
any::{TypeId, type_name},
num::NonZeroUsize,
path::PathBuf,
sync::{Arc, Mutex},
};
use clap::{Args, Parser}; use clap::{Args, Parser};
@ -7,24 +12,39 @@ use figment::{
Figment, Figment,
providers::{Env, Format, Json, Toml, YamlExtended}, providers::{Env, Format, Json, Toml, YamlExtended},
}; };
use hashbrown::hash_map;
use tokio::runtime as rt; use crate::{collections::HashMap, str::CompactString};
use crate::component_configs::{
ComponentConfigs, ConfigHandle, DeserializeCfg, ErasedValue, IsComponentConfig,
};
use tokio::runtime;
pub mod rt;
#[derive(Debug, Clone, Copy, clap::Args)]
pub struct NoArgs {}
#[derive(Parser)] #[derive(Parser)]
struct AppArgs<A: Args> { struct AppArgs<A: Args> {
#[clap(long, short)] #[clap(long, short)]
/// Config file(s) for the application. /// Config file(s) for the application.
config: Vec<PathBuf>, config: Vec<PathBuf>,
#[clap(long, short)]
/// Number of worker threads app will consume.
worker_threads: Option<NonZeroUsize>,
#[clap(flatten)] #[clap(flatten)]
args: A, args: A,
} }
struct ComponentConfigSetup {
section: CompactString,
deserialize: DeserializeCfg,
}
pub struct App { pub struct App {
figment: Figment, figment: Figment,
component_configs: HashMap<TypeId, ComponentConfigSetup>,
} }
impl App { impl App {
@ -34,19 +54,70 @@ impl App {
self self
} }
pub fn run<A, C>( pub fn optional<T: IsComponentConfig + Default>(mut self, section: &str) -> Self {
let hash_map::Entry::Vacant(vacant) = self.component_configs.entry(TypeId::of::<T>())
else {
panic!("{} already exists", type_name::<T>())
};
vacant.insert(ComponentConfigSetup {
section: section.into(),
deserialize: |path, figment| {
let res: figment::Result<T> = figment.extract_inner_lossy(path);
let res = match res {
Ok(o) => Ok(o),
Err(e) => match &e.kind {
figment::error::Kind::MissingField(field) if field == path => {
Ok(T::default())
}
_ => Err(e).wrap_err("failed to deserialize section"),
},
};
res.map(|x| {
let x: ErasedValue = Arc::new(Mutex::new(x));
x
})
},
});
self
}
pub fn require<T: IsComponentConfig>(mut self, section: &str) -> Self {
let hash_map::Entry::Vacant(vacant) = self.component_configs.entry(TypeId::of::<T>())
else {
panic!("{} already exists", type_name::<T>())
};
vacant.insert(ComponentConfigSetup {
section: section.into(),
deserialize: |path, figment| {
let res: T = figment
.extract_inner_lossy(path)
.wrap_err("failed to deserialize")?;
Ok(Arc::new(Mutex::new(res)))
},
});
self
}
pub fn run<A>(
self, self,
f: impl Send + 'static + AsyncFnOnce(A, C) -> eyre::Result<()>, f: impl Send + 'static + AsyncFnOnce(A, ComponentConfigs) -> eyre::Result<()>,
) -> eyre::Result<()> ) -> eyre::Result<()>
where where
A: Args, A: Args,
C: serde::de::DeserializeOwned,
{ {
let Self { mut figment } = self; const ONE: NonZeroUsize = NonZeroUsize::new(1).unwrap();
let Self {
mut figment,
component_configs,
} = self;
color_eyre::install().wrap_err("failed to install color-eyre")?; color_eyre::install().wrap_err("failed to install color-eyre")?;
let AppArgs { let AppArgs {
config: configs, config: configs,
worker_threads,
args, args,
} = AppArgs::<A>::parse(); } = AppArgs::<A>::parse();
@ -55,7 +126,10 @@ impl App {
let component = config.components().next_back().ok_or_else(|| { let component = config.components().next_back().ok_or_else(|| {
eyre::eyre!("{} must contain at least one component", config.display()) eyre::eyre!("{} must contain at least one component", config.display())
})?; })?;
let last = component.as_os_str().as_encoded_bytes(); let last = component
.as_os_str()
.as_encoded_bytes()
.to_ascii_lowercase();
if config.is_dir() { if config.is_dir() {
todo!("populate everything from the directory") todo!("populate everything from the directory")
@ -68,12 +142,27 @@ impl App {
} }
} }
let config: C = figment.extract_lossy().wrap_err("failed to load config")?; let mut map = HashMap::default();
let rt = match worker_threads.map_or(num_cpus::get(), NonZeroUsize::get) { for (type_id, setup) in component_configs {
0 | 1 => rt::Builder::new_current_thread(), let handle = ConfigHandle::initial(setup.section, setup.deserialize, &figment)?;
n => { map.insert(type_id, Arc::new(handle));
let mut b = rt::Builder::new_multi_thread(); }
b.worker_threads(n);
let component_configs = ComponentConfigs::new(map);
let rt_config = component_configs.get::<rt::RtConfig>();
let rt = match rt_config.flavor {
rt::Flavor::MultiThreaded {
threads: rt::ThreadsCount::Specific(ONE),
}
| rt::Flavor::SingleThreaded => runtime::Builder::new_current_thread(),
rt::Flavor::MultiThreaded { threads } => {
let mut b = runtime::Builder::new_current_thread();
b.worker_threads(match threads {
rt::ThreadsCount::Specific(s) => s.get(),
rt::ThreadsCount::ThreadPerCpu => num_cpus::get(),
});
b b
} }
} }
@ -81,14 +170,19 @@ impl App {
.build() .build()
.wrap_err("failed to create tokio runtime")?; .wrap_err("failed to create tokio runtime")?;
rt.block_on(f(args, config)) drop(rt_config);
rt.block_on(f(args, component_configs))
} }
} }
impl Default for App { impl Default for App {
fn default() -> Self { fn default() -> Self {
Self { let this = Self {
component_configs: HashMap::default(),
figment: Figment::new(), figment: Figment::new(),
} };
this.optional::<rt::RtConfig>("system.rt")
} }
} }

37
src/cli/rt.rs Normal file
View file

@ -0,0 +1,37 @@
use std::num::NonZeroUsize;
use crate::data;
#[data(crate = crate)]
#[derive(Default)]
#[serde(default)]
pub struct RtConfig {
pub flavor: Flavor,
}
#[data(crate = crate, copy)]
#[derive(Default)]
pub enum ThreadsCount {
#[default]
ThreadPerCpu,
#[serde(untagged)]
Specific(NonZeroUsize),
}
#[data(crate = crate, copy)]
pub enum Flavor {
SingleThreaded,
#[serde(rename = "worker_threads")]
MultiThreaded {
#[serde(default)]
threads: ThreadsCount,
},
}
impl Default for Flavor {
fn default() -> Self {
Self::MultiThreaded {
threads: ThreadsCount::default(),
}
}
}

124
src/component_configs.rs Normal file
View file

@ -0,0 +1,124 @@
use crate::{collections::HashMap, str::CompactString, trait_set};
use std::{
mem,
any::{Any, TypeId},
ops::Deref,
sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
},
};
use eyre::Context;
use figment::Figment;
pub(crate) type ErasedValue = Arc<Mutex<dyn Send + Sync + Any + 'static>>;
pub(crate) type DeserializeCfg = fn(&str, &Figment) -> eyre::Result<ErasedValue>;
trait_set! {
pub trait IsComponentConfig = Any + 'static + Clone + Send + Sync + serde::de::DeserializeOwned + serde::Serialize;
}
pub(crate) struct ConfigHandle {
section: CompactString,
recent: ErasedValue,
generation: AtomicUsize,
deserialize: DeserializeCfg,
}
impl ConfigHandle {
pub(crate) fn initial(
section: CompactString,
deserialize: DeserializeCfg,
figment: &Figment,
) -> eyre::Result<Self> {
let recent =
deserialize(&section, figment).wrap_err("failed to deserialize config at start")?;
Ok(Self {
section,
recent,
deserialize,
generation: AtomicUsize::new(0),
})
}
pub(crate) fn read<T: IsComponentConfig>(&self) -> Option<(T, usize)> {
let cfg = self.recent.lock().unwrap();
let generation = self.generation.load(Ordering::Relaxed);
cfg.downcast_ref::<T>().map(|r| (r.clone(), generation))
}
}
#[derive(Clone)]
pub struct ComponentConfigs {
map: Arc<HashMap<TypeId, Arc<ConfigHandle>>>,
}
impl ComponentConfigs {
pub(crate) fn new(map: HashMap<TypeId, Arc<ConfigHandle>>) -> Self {
Self { map: Arc::new(map) }
}
#[track_caller]
pub fn get<T: IsComponentConfig>(&self) -> ComponentConfig<T> {
self.try_get::<T>().unwrap_or_else(|| {
panic!(
"configuration for {} does not exist",
std::any::type_name::<T>()
)
})
}
pub fn try_get<T: IsComponentConfig>(&self) -> Option<ComponentConfig<T>> {
let type_id = TypeId::of::<T>();
self.map.get(&type_id).cloned().map(|handle| {
let (cfg, generation) = handle.read::<T>().unwrap();
ComponentConfig {
cfg,
generation,
handle,
}
})
}
}
pub struct ComponentConfig<T> {
cfg: T,
generation: usize,
handle: Arc<ConfigHandle>,
}
impl<T: IsComponentConfig> ComponentConfig<T> {
pub fn section(&self) -> &str {
&self.handle.section
}
/// Update component config. Returns previous config if something's changed.
pub fn try_update(&mut self) -> Option<T> {
let generation = self.handle.generation.load(Ordering::Relaxed);
if generation == self.generation {
None
} else {
Some(self.force_update())
}
}
/// Update component config forcibly.
pub fn force_update(&mut self) -> T {
let (cfg, generation) = self.handle.read::<T>().unwrap();
self.generation = generation;
mem::replace(&mut self.cfg, cfg)
}
}
impl<T> Deref for ComponentConfig<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.cfg
}
}

View file

@ -48,6 +48,9 @@ pub mod collections;
pub mod handling; pub mod handling;
pub mod hash; pub mod hash;
pub mod component_configs;
pub mod signal;
pub use either::{self, Either, Left, Right}; pub use either::{self, Either, Left, Right};
pub use paste::paste; pub use paste::paste;

7
src/signal.rs Normal file
View file

@ -0,0 +1,7 @@
use crate::data;
#[data(crate = crate, copy)]
pub enum Signal {
/// SIGHUP.
HangUp,
}

View file

@ -1,8 +1,10 @@
use compact_str::ToCompactString;
use eyre::eyre; use eyre::eyre;
use tokio::{sync::mpsc, task::JoinSet}; use tokio::{sync::mpsc, task::JoinSet};
use crate::{ use crate::{
collections::{HashMap, hash_map}, collections::{HashMap, hash_map},
component_configs::{ComponentConfig, ComponentConfigs, IsComponentConfig},
data, data,
fut::Fut, fut::Fut,
str::CompactString, str::CompactString,
@ -65,29 +67,49 @@ impl Handle {
} }
} }
#[derive(Default)] pub trait Supervised<T>:
Send + Sync + 'static + Fn(ComponentConfig<T>, SlaveRx) -> Self::Fut
{
type Fut: 'static + Fut<Output = eyre::Result<()>>;
}
impl<T, F, FFut> Supervised<T> for F
where
F: Send + Sync + 'static + Fn(ComponentConfig<T>, SlaveRx) -> FFut,
FFut: 'static + Fut<Output = eyre::Result<()>>,
{
type Fut = FFut;
}
pub struct Supervisor { pub struct Supervisor {
set: JoinSet<eyre::Result<()>>, set: JoinSet<eyre::Result<()>>,
handles: HashMap<CompactString, Handle>, handles: HashMap<CompactString, Handle>,
configs: ComponentConfigs,
} }
impl Supervisor { impl Supervisor {
pub fn empty() -> Self { pub fn new(configs: ComponentConfigs) -> Self {
Self::default() Self {
set: JoinSet::new(),
handles: HashMap::default(),
configs,
}
} }
pub fn add<F, FFut>(mut self, name: &str, f: F) -> Self pub fn add<T>(mut self, f: impl Supervised<T>) -> Self
where where
F: Send + 'static + FnOnce(SlaveRx) -> FFut, T: IsComponentConfig,
FFut: 'static + Fut<Output = eyre::Result<()>>,
{ {
let (tx, mut rx) = mpsc::channel(8); let (tx, mut rx) = mpsc::channel(8);
let (stx, srx) = mpsc::channel(8); let (stx, srx) = mpsc::channel(8);
let handle = Handle { tx }; let handle = Handle { tx };
let slave_rx = SlaveRx { rx: srx }; let slave_rx = SlaveRx { rx: srx };
let config = self.configs.get::<T>();
let name = config.section().to_compact_string();
self.set.spawn(async move { self.set.spawn(async move {
let fut = f(slave_rx); let fut = f(config, slave_rx);
tokio::pin!(fut); tokio::pin!(fut);
loop { loop {
tokio::select! { tokio::select! {
@ -116,7 +138,7 @@ impl Supervisor {
} }
}); });
let hash_map::Entry::Vacant(vac) = self.handles.entry(name.into()) else { let hash_map::Entry::Vacant(vac) = self.handles.entry(name.clone()) else {
panic!("{name:?} already exists") panic!("{name:?} already exists")
}; };

0
utils/src/sync/sema.rs Normal file
View file