use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReadySignalDecl {
pub name: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReadyStatus {
Pending,
Ready,
}
#[derive(Debug, Clone, Default)]
pub struct ReadyTracker {
inner: Arc<RwLock<BTreeMap<(String, String), ReadyStatus>>>,
}
impl ReadyTracker {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn declare(&self, plugin_id: &str, signals: &[ReadySignalDecl]) {
if signals.is_empty() {
return;
}
if let Ok(mut map) = self.inner.write() {
for signal in signals {
map.entry((plugin_id.to_string(), signal.name.clone()))
.or_insert(ReadyStatus::Pending);
}
}
}
pub fn mark_ready(&self, plugin_id: &str, signal_name: &str) {
if let Ok(mut map) = self.inner.write() {
map.insert(
(plugin_id.to_string(), signal_name.to_string()),
ReadyStatus::Ready,
);
}
}
#[must_use]
pub fn is_ready(&self, plugin_id: &str, signal_name: &str) -> bool {
self.inner.read().ok().is_some_and(|map| {
map.get(&(plugin_id.to_string(), signal_name.to_string()))
.copied()
.is_some_and(|status| status == ReadyStatus::Ready)
})
}
#[must_use]
pub fn status(&self, plugin_id: &str, signal_name: &str) -> Option<ReadyStatus> {
self.inner.read().ok().and_then(|map| {
map.get(&(plugin_id.to_string(), signal_name.to_string()))
.copied()
})
}
#[must_use]
pub fn await_ready(
&self,
plugin_id: &str,
signal_name: &str,
timeout: std::time::Duration,
) -> bool {
let deadline = std::time::Instant::now() + timeout;
loop {
if self.is_ready(plugin_id, signal_name) {
return true;
}
if std::time::Instant::now() >= deadline {
return false;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
#[must_use]
pub fn snapshot(&self) -> BTreeMap<(String, String), ReadyStatus> {
self.inner.read().map(|map| map.clone()).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn declare_and_mark_ready_round_trip() {
let tracker = ReadyTracker::new();
let decls = vec![
ReadySignalDecl {
name: "first".into(),
description: None,
},
ReadySignalDecl {
name: "second".into(),
description: Some("second signal".into()),
},
];
tracker.declare("plug.a", &decls);
assert_eq!(
tracker.status("plug.a", "first"),
Some(ReadyStatus::Pending)
);
assert!(!tracker.is_ready("plug.a", "first"));
tracker.mark_ready("plug.a", "first");
assert!(tracker.is_ready("plug.a", "first"));
assert!(!tracker.is_ready("plug.a", "second"));
}
#[test]
fn await_ready_returns_true_when_signal_fires() {
let tracker = ReadyTracker::new();
tracker.declare(
"plug.a",
&[ReadySignalDecl {
name: "ready".into(),
description: None,
}],
);
let tracker_bg = tracker.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(25));
tracker_bg.mark_ready("plug.a", "ready");
});
assert!(tracker.await_ready("plug.a", "ready", std::time::Duration::from_millis(500)));
}
#[test]
fn await_ready_times_out_when_signal_never_fires() {
let tracker = ReadyTracker::new();
tracker.declare(
"plug.a",
&[ReadySignalDecl {
name: "stuck".into(),
description: None,
}],
);
assert!(!tracker.await_ready("plug.a", "stuck", std::time::Duration::from_millis(30)));
}
#[test]
fn status_returns_none_for_unknown_signal() {
let tracker = ReadyTracker::new();
assert_eq!(tracker.status("plug.a", "missing"), None);
}
}