use std::sync::{Arc, OnceLock, RwLock};
use mercurio_server::plugin::{Access, BoxFuture, ClientInfo, Credentials, Decision, Plugin};
type Loader<T> = fn(&str) -> Result<T, String>;
pub struct Reloadable<T> {
path: String,
load: Loader<T>,
current: RwLock<Arc<T>>,
name: OnceLock<String>,
}
impl<T> Reloadable<T> {
pub fn load(path: &str, load: Loader<T>) -> Result<Self, String> {
Ok(Self {
path: path.to_string(),
load,
current: RwLock::new(Arc::new(load(path)?)),
name: OnceLock::new(),
})
}
pub fn reload(&self) -> Result<(), String> {
let value = (self.load)(&self.path)?;
*self.current.write().unwrap() = Arc::new(value);
Ok(())
}
pub fn path(&self) -> &str {
&self.path
}
fn get(&self) -> Arc<T> {
Arc::clone(&self.current.read().unwrap())
}
}
pub trait Reload: Send + Sync {
fn reload(&self) -> Result<(), String>;
fn path(&self) -> &str;
}
impl<T: Send + Sync> Reload for Reloadable<T> {
fn reload(&self) -> Result<(), String> {
Reloadable::reload(self)
}
fn path(&self) -> &str {
Reloadable::path(self)
}
}
impl<T: Plugin> Plugin for Reloadable<T> {
fn name(&self) -> &str {
self.name.get_or_init(|| self.get().name().to_string())
}
fn authenticate<'a>(
&'a self,
client: &'a ClientInfo,
credentials: &'a Credentials<'a>,
) -> BoxFuture<'a, Decision> {
let current = self.get();
Box::pin(async move { current.authenticate(client, credentials).await })
}
fn authorize<'a>(
&'a self,
client: &'a ClientInfo,
topic: &'a str,
access: Access,
) -> BoxFuture<'a, Decision> {
let current = self.get();
Box::pin(async move { current.authorize(client, topic, access).await })
}
}
#[cfg(test)]
mod tests {
use mercurio_server::{acl::AclFile, plugin::Plugin};
use super::Reloadable;
fn load(path: &str) -> Result<String, String> {
let contents = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
if contents.contains("bad") {
return Err("bad contents".to_string());
}
Ok(contents)
}
#[test]
fn test_reload_swaps_value_and_keeps_it_on_error() {
let path = std::env::temp_dir().join(format!("mercuriod-reload-{}", std::process::id()));
let path_str = path.to_str().unwrap();
std::fs::write(&path, "one").unwrap();
let value = Reloadable::load(path_str, load).unwrap();
assert_eq!(*value.get(), "one");
std::fs::write(&path, "two").unwrap();
value.reload().unwrap();
assert_eq!(*value.get(), "two");
std::fs::write(&path, "bad").unwrap();
assert!(value.reload().is_err());
assert_eq!(*value.get(), "two");
std::fs::remove_file(&path).unwrap();
assert!(value.reload().is_err());
assert_eq!(*value.get(), "two");
}
#[test]
fn test_plugins_keep_their_own_name() {
let path =
std::env::temp_dir().join(format!("mercuriod-reload-acl-{}", std::process::id()));
std::fs::write(&path, "topic read public/#").unwrap();
let acl = Reloadable::load(path.to_str().unwrap(), |path| {
AclFile::parse(&std::fs::read_to_string(path).unwrap())
})
.unwrap();
assert_eq!(Plugin::name(&acl), "acl-file");
std::fs::remove_file(&path).unwrap();
}
}