use crate::error::Error;
pub type Commit = Box<dyn FnOnce() + Send>;
pub trait Reloadable: 'static {
fn prepare() -> Result<Commit, Error>;
fn name() -> &'static str;
}
#[derive(Default)]
pub struct ReloadGroup {
members: Vec<Member>,
reloading: std::sync::Mutex<()>,
}
struct Member {
name: &'static str,
prepare: fn() -> Result<Commit, Error>,
}
impl ReloadGroup {
#[must_use]
pub const fn new() -> Self {
Self {
members: Vec::new(),
reloading: std::sync::Mutex::new(()),
}
}
#[must_use]
pub fn with<T: Reloadable>(mut self) -> Self {
self.members.push(Member {
name: T::name(),
prepare: T::prepare,
});
self
}
pub fn members(&self) -> impl Iterator<Item = &'static str> + '_ {
self.members.iter().map(|member| member.name)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.members.is_empty()
}
pub fn reload(&self) -> Result<(), Error> {
let _guard = self
.reloading
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut commits = Vec::with_capacity(self.members.len());
for member in &self.members {
let commit = (member.prepare)().map_err(|error| error.prepend_key(member.name))?;
commits.push(commit);
}
for commit in commits {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(commit));
if outcome.is_err() {
crate::log::warning!(
"a commit's reload hook panicked; the remaining members \
were still committed"
);
}
}
Ok(())
}
}
impl std::fmt::Debug for ReloadGroup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list().entries(self.members()).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
macro_rules! members {
($counter:ident, $good:ident, $bad:ident) => {
static $counter: AtomicUsize = AtomicUsize::new(0);
struct $good;
// Not every test needs the failing half; the macro declares both so
#[allow(dead_code)]
struct $bad;
impl Reloadable for $good {
fn prepare() -> Result<Commit, Error> {
Ok(Box::new(|| {
$counter.fetch_add(1, Ordering::SeqCst);
}))
}
fn name() -> &'static str {
stringify!($good)
}
}
impl Reloadable for $bad {
fn prepare() -> Result<Commit, Error> {
Err(Error::new(crate::ErrorKind::Missing, "nothing supplies it"))
}
fn name() -> &'static str {
stringify!($bad)
}
}
};
}
members!(ALL_COMMITTED, AllGood, AllBad);
members!(NONE_COMMITTED, NoneGood, NoneBad);
members!(ORDER_COMMITTED, OrderGood, OrderBad);
#[test]
fn every_member_commits_when_every_member_prepares() {
ReloadGroup::new()
.with::<AllGood>()
.with::<AllGood>()
.reload()
.expect("both prepare cleanly");
assert_eq!(ALL_COMMITTED.load(Ordering::SeqCst), 2);
}
#[test]
fn one_failure_stops_every_commit_including_the_ones_that_would_have_worked() {
let error = ReloadGroup::new()
.with::<NoneGood>()
.with::<NoneBad>()
.with::<NoneGood>()
.reload()
.expect_err("the middle member fails");
assert_eq!(
NONE_COMMITTED.load(Ordering::SeqCst),
0,
"the member that prepared before the failure must not have committed"
);
assert!(error.path().starts_with("NoneBad"), "{error}");
}
#[test]
fn an_empty_group_is_a_no_op() {
let group = ReloadGroup::new();
assert!(group.is_empty());
assert!(group.reload().is_ok());
}
#[test]
fn a_group_reports_its_members_in_order() {
let group = ReloadGroup::new().with::<OrderGood>().with::<OrderBad>();
assert_eq!(
group.members().collect::<Vec<_>>(),
["OrderGood", "OrderBad"]
);
}
}