use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
pub struct MultiRunnedThread {
pub thread: Mutex<Option<JoinHandle<()>>>,
pub shutdown_event: Arc<Mutex<bool>>,
pub daemon: bool,
}
impl Default for MultiRunnedThread {
fn default() -> Self {
Self::new()
}
}
impl MultiRunnedThread {
pub fn new() -> Self {
Self {
thread: Mutex::new(None),
shutdown_event: Arc::new(Mutex::new(false)),
daemon: true,
}
}
pub fn is_alive(&self) -> bool {
let t = self.thread.lock().unwrap();
match t.as_ref() {
None => false,
Some(h) => !h.is_finished(),
}
}
pub fn start<F>(&self, run: F)
where
F: FnOnce(Arc<Mutex<bool>>) + Send + 'static,
{
self.start_with(run);
}
pub fn start_with<F>(&self, run: F)
where
F: FnOnce(Arc<Mutex<bool>>) + Send + 'static,
{
*self.shutdown_event.lock().unwrap() = false;
let event = self.shutdown_event.clone();
let handle = std::thread::spawn(move || run(event));
*self.thread.lock().unwrap() = Some(handle);
}
pub fn join(&self) -> Option<()> {
let mut t = self.thread.lock().unwrap();
let handle = t.take()?;
let _ = handle.join();
Some(())
}
pub fn set_shutdown(&self) {
*self.shutdown_event.lock().unwrap() = true;
}
}
pub struct ThreadedSegment {
pub base: MultiRunnedThread,
pub min_sleep_time: f64,
pub update_first: bool,
pub interval: f64,
pub run_once: bool,
pub crashed: bool,
pub crashed_value: Option<String>,
pub updated: bool,
pub do_update_first: bool,
}
impl Default for ThreadedSegment {
fn default() -> Self {
Self::new()
}
}
impl ThreadedSegment {
pub fn new() -> Self {
let mut base = MultiRunnedThread::new();
base.daemon = false;
Self {
base,
min_sleep_time: 0.1,
update_first: true,
interval: 1.0,
run_once: true,
crashed: false,
crashed_value: None,
updated: false,
do_update_first: true,
}
}
pub fn set_interval(&mut self, interval: Option<f64>) {
if let Some(i) = interval {
self.interval = i;
}
}
pub fn set_state(&mut self, interval: Option<f64>, update_first: bool) {
self.set_interval(interval);
self.do_update_first = update_first && self.update_first;
self.updated = self.updated || !self.do_update_first;
}
pub fn startup<F>(&mut self, use_daemon_threads: bool, run: F)
where
F: FnOnce(Arc<Mutex<bool>>) + Send + 'static,
{
self.run_once = false;
self.base.daemon = use_daemon_threads;
if !self.base.is_alive() {
self.base.start_with(run);
}
}
pub fn run<F>(
&self,
shutdown_event: &Arc<Mutex<bool>>,
min_sleep_time: f64,
mut set_update_value: F,
) where
F: FnMut(),
{
use crate::ported::lib::monotonic::monotonic;
if self.do_update_first {
let mut start_time = monotonic();
loop {
let sleep = (self.interval - (monotonic() - start_time)).max(min_sleep_time);
std::thread::sleep(std::time::Duration::from_secs_f64(sleep.max(0.0)));
if *shutdown_event.lock().unwrap() {
break;
}
start_time = monotonic();
set_update_value();
}
} else {
while !*shutdown_event.lock().unwrap() {
let start_time = monotonic();
set_update_value();
let sleep = (self.interval - (monotonic() - start_time)).max(min_sleep_time);
std::thread::sleep(std::time::Duration::from_secs_f64(sleep.max(0.0)));
}
}
}
pub fn shutdown(&self) {
self.base.set_shutdown();
}
pub fn get_update_value<F>(&mut self, update: bool, refresh: F) -> Option<String>
where
F: FnMut() -> Result<String, String>,
{
if update {
return self.set_update_value(refresh);
}
None
}
pub fn argspecobjs(argmethods: &[&'static str]) -> Vec<(String, String)> {
argmethods
.iter()
.map(|name| (name.to_string(), name.to_string()))
.collect()
}
pub fn additional_args(interval: Option<f64>) -> Vec<(String, Option<f64>)> {
vec![("interval".to_string(), interval)]
}
pub fn omitted_args_table(name: &str) -> Vec<&'static str> {
match name {
"render" => vec!["0"],
"set_state" => vec!["shutdown_event"],
_ => Vec::new(),
}
}
pub fn omitted_args(name: &str, is_method: bool) -> Vec<String> {
let raw = Self::omitted_args_table(name);
raw.iter()
.map(|arg| {
if is_method {
if let Ok(idx) = arg.parse::<i32>() {
return (idx + 1).to_string();
}
}
arg.to_string()
})
.collect()
}
pub fn critical(class_name: &str, message: &str) -> (String, String) {
(class_name.to_string(), message.to_string())
}
pub fn exception(class_name: &str, message: &str) -> (String, String) {
(class_name.to_string(), message.to_string())
}
pub fn info(class_name: &str, message: &str) -> (String, String) {
(class_name.to_string(), message.to_string())
}
pub fn error(class_name: &str, message: &str) -> (String, String) {
(class_name.to_string(), message.to_string())
}
pub fn warn(class_name: &str, message: &str) -> (String, String) {
(class_name.to_string(), message.to_string())
}
pub fn debug(class_name: &str, message: &str) -> (String, String) {
(class_name.to_string(), message.to_string())
}
pub fn set_update_value<F>(&mut self, mut update: F) -> Option<String>
where
F: FnMut() -> Result<String, String>,
{
match update() {
Ok(v) => {
self.crashed = false;
self.updated = true;
Some(v)
}
Err(_) => {
self.crashed = true;
None
}
}
}
}
pub struct KwThreadedSegment {
pub base: ThreadedSegment,
pub updates: std::collections::HashMap<String, KwUpdateState>,
}
#[derive(Debug, Clone)]
pub struct KwUpdateState {
pub crashed: bool,
pub value: Option<String>,
}
impl Default for KwThreadedSegment {
fn default() -> Self {
Self::new()
}
}
impl KwThreadedSegment {
pub fn new() -> Self {
Self {
base: ThreadedSegment::new(),
updates: std::collections::HashMap::new(),
}
}
pub fn update_one<F>(&mut self, key: &str, mut update: F)
where
F: FnMut() -> Result<String, String>,
{
let state = self
.updates
.entry(key.to_string())
.or_insert(KwUpdateState {
crashed: false,
value: None,
});
match update() {
Ok(v) => {
state.crashed = false;
state.value = Some(v);
}
Err(_) => {
state.crashed = true;
}
}
}
pub fn key(kwargs: &[(String, String)]) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = kwargs.to_vec();
out.sort();
out
}
pub fn render<R>(
queries: &std::collections::HashMap<String, (f64, Option<String>)>,
key: &str,
do_update_first: bool,
run_once: bool,
after_update: bool,
mut render_with_update: R,
) -> Option<String>
where
R: FnMut(bool, &str) -> Option<String>,
{
let update_state = match queries.get(key) {
Some((_, state)) => state.clone(),
None => {
if do_update_first || run_once {
if after_update {
None
} else {
return render_with_update(true, key);
}
} else {
None
}
}
};
Self::render_one(update_state)
}
pub fn update<F>(
old_queries: &std::collections::HashMap<String, (f64, Option<String>)>,
new_queries: &[String],
interval: f64,
mut update_one: F,
) -> (
std::collections::HashMap<String, (f64, Option<String>)>,
std::collections::HashSet<String>,
)
where
F: FnMut(
&mut std::collections::HashSet<String>,
&mut std::collections::HashMap<String, (f64, Option<String>)>,
&str,
),
{
use crate::ported::lib::monotonic::monotonic;
let mut updates: std::collections::HashMap<String, (f64, Option<String>)> =
std::collections::HashMap::new();
let mut crashed: std::collections::HashSet<String> = std::collections::HashSet::new();
let now = monotonic();
for (key, (last_query_time, state)) in old_queries {
if *last_query_time < now && now < last_query_time + interval {
updates.insert(key.clone(), (*last_query_time, state.clone()));
} else {
update_one(&mut crashed, &mut updates, key);
}
}
for key in new_queries {
update_one(&mut crashed, &mut updates, key);
}
(updates, crashed)
}
pub fn render_one(update_state: Option<String>) -> Option<String> {
update_state
}
pub fn omitted_args_table(name: &str) -> Vec<&'static str> {
match name {
"render" => vec!["update_value", "key", "after_update"],
"set_state" => vec!["shutdown_event"],
"render_one" => vec!["0"],
_ => Vec::new(),
}
}
pub fn set_state(&mut self, interval: Option<f64>, update_first: bool) {
self.base.set_interval(interval);
self.base.do_update_first = update_first;
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CallDecision {
Crashed,
Render {
update_first: bool,
do_update_first: bool,
},
}
impl ThreadedSegment {
pub fn call_dispatch(
run_once: bool,
is_alive: bool,
updated: bool,
do_update_first: bool,
crashed: bool,
update_first: bool,
) -> CallDecision {
if crashed {
return CallDecision::Crashed;
}
if run_once {
return CallDecision::Render {
update_first,
do_update_first: true,
};
}
if !is_alive {
return CallDecision::Render {
update_first,
do_update_first,
};
}
CallDecision::Render {
update_first,
do_update_first: !updated,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
#[test]
fn new_thread_is_not_alive() {
let t = MultiRunnedThread::new();
assert!(!t.is_alive());
}
#[test]
fn start_then_join_runs_closure() {
let t = MultiRunnedThread::new();
let ran = Arc::new(AtomicU32::new(0));
let ran_clone = ran.clone();
t.start_with(move |_event| {
ran_clone.fetch_add(1, Ordering::SeqCst);
});
t.join();
assert_eq!(ran.load(Ordering::SeqCst), 1);
}
#[test]
fn set_shutdown_signals_the_thread() {
let t = MultiRunnedThread::new();
let observed = Arc::new(Mutex::new(false));
let observed_clone = observed.clone();
t.start_with(move |event| {
loop {
if *event.lock().unwrap() {
*observed_clone.lock().unwrap() = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(2));
}
});
std::thread::sleep(std::time::Duration::from_millis(10));
t.set_shutdown();
t.join();
assert!(*observed.lock().unwrap());
}
#[test]
fn threaded_segment_defaults_match_upstream() {
let s = ThreadedSegment::new();
assert!((s.min_sleep_time - 0.1).abs() < 1e-9);
assert!(s.update_first);
assert_eq!(s.interval, 1.0);
assert!(s.run_once);
assert!(!s.crashed);
assert!(!s.updated);
}
#[test]
fn threaded_segment_set_interval_overrides_default() {
let mut s = ThreadedSegment::new();
s.set_interval(Some(5.0));
assert_eq!(s.interval, 5.0);
}
#[test]
fn threaded_segment_set_interval_none_preserves_default() {
let mut s = ThreadedSegment::new();
s.set_interval(None);
assert_eq!(s.interval, 1.0);
}
#[test]
fn threaded_segment_set_state_sets_do_update_first() {
let mut s = ThreadedSegment::new();
s.set_state(Some(2.0), true);
assert!(s.do_update_first);
assert_eq!(s.interval, 2.0);
}
#[test]
fn threaded_segment_set_state_false_clears_do_update_first() {
let mut s = ThreadedSegment::new();
s.set_state(None, false);
assert!(!s.do_update_first);
assert!(s.updated);
}
#[test]
fn threaded_segment_set_update_value_ok_clears_crashed() {
let mut s = ThreadedSegment::new();
s.crashed = true;
let v = s.set_update_value(|| Ok("data".to_string()));
assert_eq!(v, Some("data".to_string()));
assert!(!s.crashed);
assert!(s.updated);
}
#[test]
fn threaded_segment_set_update_value_err_sets_crashed() {
let mut s = ThreadedSegment::new();
let v = s.set_update_value(|| Err::<String, String>("boom".to_string()));
assert!(v.is_none());
assert!(s.crashed);
}
#[test]
fn threaded_segment_shutdown_signals_thread() {
let s = ThreadedSegment::new();
s.shutdown();
assert!(*s.base.shutdown_event.lock().unwrap());
}
#[test]
fn kw_threaded_segment_new_empty() {
let s = KwThreadedSegment::new();
assert!(s.updates.is_empty());
}
#[test]
fn kw_threaded_segment_update_one_inserts_key() {
let mut s = KwThreadedSegment::new();
s.update_one("foo", || Ok("v".to_string()));
let state = s.updates.get("foo").unwrap();
assert!(!state.crashed);
assert_eq!(state.value.as_deref(), Some("v"));
}
#[test]
fn kw_threaded_segment_update_one_crash_preserves_value() {
let mut s = KwThreadedSegment::new();
s.update_one("foo", || Ok("good".to_string()));
s.update_one("foo", || Err::<String, String>("boom".to_string()));
let state = s.updates.get("foo").unwrap();
assert!(state.crashed);
assert_eq!(state.value.as_deref(), Some("good"));
}
#[test]
fn kw_threaded_segment_update_one_recovery_clears_crashed() {
let mut s = KwThreadedSegment::new();
s.update_one("foo", || Err::<String, String>("boom".to_string()));
s.update_one("foo", || Ok("recovered".to_string()));
let state = s.updates.get("foo").unwrap();
assert!(!state.crashed);
assert_eq!(state.value.as_deref(), Some("recovered"));
}
#[test]
fn kw_threaded_segment_multiple_keys_isolated() {
let mut s = KwThreadedSegment::new();
s.update_one("a", || Ok("av".to_string()));
s.update_one("b", || Err::<String, String>("err".to_string()));
let a = s.updates.get("a").unwrap();
let b = s.updates.get("b").unwrap();
assert!(!a.crashed);
assert!(b.crashed);
}
#[test]
fn get_update_value_runs_refresh_when_update_true() {
let mut s = ThreadedSegment::default();
let r = s.get_update_value(true, || Ok("fresh".to_string()));
assert_eq!(r, Some("fresh".to_string()));
assert!(s.updated);
}
#[test]
fn get_update_value_skips_refresh_when_update_false() {
let mut s = ThreadedSegment::default();
let r = s.get_update_value(false, || Ok("should_not_run".to_string()));
assert!(r.is_none());
assert!(!s.updated);
}
#[test]
fn argspecobjs_yields_argmethod_name_pairs() {
let r = ThreadedSegment::argspecobjs(&["render", "set_state"]);
assert_eq!(r.len(), 2);
assert_eq!(r[0].0, "render");
assert_eq!(r[1].0, "set_state");
}
#[test]
fn additional_args_returns_interval_pair() {
let r = ThreadedSegment::additional_args(Some(5.0));
assert_eq!(r.len(), 1);
assert_eq!(r[0].0, "interval");
assert_eq!(r[0].1, Some(5.0));
}
#[test]
fn omitted_args_table_render_omits_first() {
assert_eq!(ThreadedSegment::omitted_args_table("render"), vec!["0"]);
}
#[test]
fn omitted_args_table_set_state_omits_shutdown_event() {
assert_eq!(
ThreadedSegment::omitted_args_table("set_state"),
vec!["shutdown_event"]
);
}
#[test]
fn omitted_args_table_unknown_name_returns_empty() {
assert!(ThreadedSegment::omitted_args_table("other").is_empty());
}
#[test]
fn omitted_args_unbound_passes_indices_through() {
let r = ThreadedSegment::omitted_args("render", false);
assert_eq!(r, vec!["0".to_string()]);
}
#[test]
fn omitted_args_bound_shifts_integer_indices_by_one() {
let r = ThreadedSegment::omitted_args("render", true);
assert_eq!(r, vec!["1".to_string()]);
}
#[test]
fn omitted_args_bound_leaves_string_indices_unchanged() {
let r = ThreadedSegment::omitted_args("set_state", true);
assert_eq!(r, vec!["shutdown_event".to_string()]);
}
#[test]
fn critical_returns_prefix_message_pair() {
let (prefix, msg) = ThreadedSegment::critical("MyClass", "boom");
assert_eq!(prefix, "MyClass");
assert_eq!(msg, "boom");
}
#[test]
fn exception_returns_prefix_message_pair() {
let (prefix, msg) = ThreadedSegment::exception("MyClass", "exc");
assert_eq!(prefix, "MyClass");
assert_eq!(msg, "exc");
}
#[test]
fn info_warn_error_debug_all_return_prefix_message_pair() {
assert_eq!(ThreadedSegment::info("X", "i"), ("X".into(), "i".into()));
assert_eq!(ThreadedSegment::error("X", "e"), ("X".into(), "e".into()));
assert_eq!(ThreadedSegment::warn("X", "w"), ("X".into(), "w".into()));
assert_eq!(ThreadedSegment::debug("X", "d"), ("X".into(), "d".into()));
}
#[test]
fn kw_key_sorts_kwargs() {
let r = KwThreadedSegment::key(&[
("b".to_string(), "2".to_string()),
("a".to_string(), "1".to_string()),
]);
assert_eq!(
r,
vec![
("a".to_string(), "1".to_string()),
("b".to_string(), "2".to_string()),
]
);
}
#[test]
fn kw_key_empty_returns_empty() {
let r = KwThreadedSegment::key(&[]);
assert!(r.is_empty());
}
#[test]
fn kw_render_one_returns_update_state_unchanged() {
assert_eq!(
KwThreadedSegment::render_one(Some("hi".to_string())),
Some("hi".to_string())
);
assert_eq!(KwThreadedSegment::render_one(None), None);
}
#[test]
fn kw_omitted_args_table_render_includes_three_args() {
let r = KwThreadedSegment::omitted_args_table("render");
assert_eq!(r, vec!["update_value", "key", "after_update"]);
}
#[test]
fn kw_omitted_args_table_render_one_omits_first() {
let r = KwThreadedSegment::omitted_args_table("render_one");
assert_eq!(r, vec!["0"]);
}
#[test]
fn kw_omitted_args_table_set_state_omits_shutdown_event() {
let r = KwThreadedSegment::omitted_args_table("set_state");
assert_eq!(r, vec!["shutdown_event"]);
}
#[test]
fn kw_omitted_args_table_unknown_returns_empty() {
let r = KwThreadedSegment::omitted_args_table("other");
assert!(r.is_empty());
}
#[test]
fn kw_set_state_pins_do_update_first() {
let mut s = KwThreadedSegment::new();
s.set_state(Some(5.0), false);
assert!(!s.base.do_update_first);
s.set_state(Some(5.0), true);
assert!(s.base.do_update_first);
}
#[test]
fn kw_set_state_calls_set_interval() {
let mut s = KwThreadedSegment::new();
s.set_state(Some(2.0), true);
assert!((s.base.interval - 2.0).abs() < 1e-9);
}
#[test]
fn call_dispatch_crashed_returns_crashed() {
let d = ThreadedSegment::call_dispatch(false, true, true, false, true, true);
assert_eq!(d, CallDecision::Crashed);
}
#[test]
fn call_dispatch_run_once_forces_update() {
let d = ThreadedSegment::call_dispatch(true, false, false, false, false, true);
assert_eq!(
d,
CallDecision::Render {
update_first: true,
do_update_first: true,
}
);
}
#[test]
fn call_dispatch_not_alive_uses_do_update_first() {
let d = ThreadedSegment::call_dispatch(false, false, false, true, false, true);
assert_eq!(
d,
CallDecision::Render {
update_first: true,
do_update_first: true,
}
);
}
#[test]
fn call_dispatch_default_updates_when_stale() {
let d = ThreadedSegment::call_dispatch(false, true, false, true, false, true);
assert_eq!(
d,
CallDecision::Render {
update_first: true,
do_update_first: true,
}
);
}
#[test]
fn call_dispatch_default_skips_update_when_fresh() {
let d = ThreadedSegment::call_dispatch(false, true, true, true, false, true);
assert_eq!(
d,
CallDecision::Render {
update_first: true,
do_update_first: false,
}
);
}
#[test]
fn multi_runned_thread_start_dispatches_to_start_with() {
let t = MultiRunnedThread::new();
let counter = std::sync::Arc::new(AtomicU32::new(0));
let counter_c = counter.clone();
t.start(move |_event| {
counter_c.fetch_add(7, Ordering::SeqCst);
});
t.join();
assert_eq!(counter.load(Ordering::SeqCst), 7);
}
#[test]
fn kw_threaded_render_falls_back_to_render_with_update_on_cache_miss() {
let queries = std::collections::HashMap::new();
let mut called_with: Option<(bool, String)> = None;
let result = KwThreadedSegment::render(
&queries,
"missing_key",
true, false,
false,
|upd, key| {
called_with = Some((upd, key.to_string()));
Some("recursive_result".to_string())
},
);
assert_eq!(result, Some("recursive_result".to_string()));
assert_eq!(called_with, Some((true, "missing_key".to_string())));
}
#[test]
fn kw_threaded_render_returns_cached_state_when_present() {
let mut queries = std::collections::HashMap::new();
queries.insert("k1".to_string(), (0.0_f64, Some("cached".to_string())));
let result = KwThreadedSegment::render(&queries, "k1", true, false, false, |_, _| {
panic!("should not be called when cached")
});
assert_eq!(result, Some("cached".to_string()));
}
#[test]
fn kw_threaded_render_after_update_internal_error_returns_none() {
let queries = std::collections::HashMap::new();
let result = KwThreadedSegment::render(
&queries,
"missing",
true,
false,
true, |_, _| panic!("should not recurse on after_update"),
);
assert_eq!(result, None);
}
#[test]
fn kw_threaded_update_refreshes_stale_entries() {
let mut old = std::collections::HashMap::new();
old.insert("k".to_string(), (0.000001_f64, Some("old".to_string())));
let new_queries: Vec<String> = vec![];
let calls = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
let calls_c = calls.clone();
let (updates, crashed) = KwThreadedSegment::update(
&old,
&new_queries,
0.000001, move |_crashed, updates, key| {
calls_c.lock().unwrap().push(key.to_string());
updates.insert(key.to_string(), (1.0, Some("new".to_string())));
},
);
assert_eq!(*calls.lock().unwrap(), vec!["k".to_string()]);
assert_eq!(updates.get("k").unwrap().1, Some("new".to_string()));
assert!(crashed.is_empty());
}
#[test]
fn kw_threaded_update_keeps_fresh_entries_as_is() {
let mut old = std::collections::HashMap::new();
let now = crate::ported::lib::monotonic::monotonic();
old.insert("k".to_string(), (now, Some("kept".to_string())));
let new_queries: Vec<String> = vec![];
let calls = std::sync::Arc::new(std::sync::Mutex::new(0u32));
let calls_c = calls.clone();
let (updates, _) = KwThreadedSegment::update(
&old,
&new_queries,
3600.0, move |_, _, _| {
*calls_c.lock().unwrap() += 1;
},
);
assert_eq!(*calls.lock().unwrap(), 0, "no refresh should happen");
assert_eq!(updates.get("k").unwrap().1, Some("kept".to_string()));
}
#[test]
fn kw_threaded_update_processes_new_queries() {
let old = std::collections::HashMap::new();
let new_queries = vec!["k1".to_string(), "k2".to_string()];
let calls = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
let calls_c = calls.clone();
let (updates, _crashed) =
KwThreadedSegment::update(&old, &new_queries, 60.0, move |_crashed, updates, key| {
calls_c.lock().unwrap().push(key.to_string());
updates.insert(key.to_string(), (1.0, Some(format!("v_{key}"))));
});
let mut got = calls.lock().unwrap().clone();
got.sort();
assert_eq!(got, vec!["k1".to_string(), "k2".to_string()]);
assert_eq!(updates.get("k1").unwrap().1, Some("v_k1".to_string()));
assert_eq!(updates.get("k2").unwrap().1, Some("v_k2".to_string()));
}
#[test]
fn threaded_segment_run_calls_update_once_then_stops_when_shutdown_set() {
let mut seg = ThreadedSegment::new();
seg.interval = 0.001;
seg.do_update_first = false;
let event = Arc::new(Mutex::new(false));
let event_c = event.clone();
let counter = std::sync::Arc::new(AtomicU32::new(0));
let counter_c = counter.clone();
seg.run(&event, 0.001, move || {
counter_c.fetch_add(1, Ordering::SeqCst);
*event_c.lock().unwrap() = true;
});
let n = counter.load(Ordering::SeqCst);
assert!(n >= 1, "expected at least 1 call, got {n}");
}
}