use super::config::{BarkConfig, rules_for};
use super::firing_delivery::{Delivery, now_ms, spawn_firings};
use super::rules::Rules;
use core::future::Future;
use shep_client::RequestError;
use shep_core::protocol::ProcessInfo;
pub trait FlockSource: Send + Sync {
fn flock(&self) -> impl Future<Output = Result<Vec<ProcessInfo>, RequestError>> + Send;
}
pub trait ConfigSource: Send + Sync {
fn section(&self) -> impl Future<Output = Result<String, RequestError>> + Send;
}
pub(super) async fn reloaded_config<C: ConfigSource>(source: &C) -> Option<(BarkConfig, Rules)> {
let section = match source.section().await {
Ok(section) => section,
Err(err) => {
eprintln!("shep dog bark: could not re-read [bark] from the shepherd: {err}");
return None;
}
};
let config = if section.is_empty() {
BarkConfig::default()
} else {
match toml::from_str::<BarkConfig>(§ion) {
Ok(config) => config,
Err(_err) => {
eprintln!("shep dog bark: [bark] in dogs.toml does not parse; see `shep dogs`");
return None;
}
}
};
match rules_for(&config) {
Ok(rules) => Some((config, rules)),
Err(err) => {
eprintln!("shep dog bark: keeping the running rules; the new ones are refused: {err}");
None
}
}
}
pub(super) async fn reconcile<F: FlockSource>(flock: &F, rules: &mut Rules, delivery: &Delivery) {
match flock.flock().await {
Ok(snapshot) => {
let firings = rules.on_poll(&snapshot, now_ms());
spawn_firings(firings, delivery);
}
Err(err) => eprintln!("shep dog bark: reconciliation poll failed: {err}"),
}
}
#[cfg(test)]
mod tests {
use super::super::config::BarkConfig;
use super::super::dog_lifecycle::run_loop;
use std::collections::BTreeMap;
use std::time::Duration;
use shep_client::LinkLost;
use shep_core::barks::{self};
use shep_core::values::UpDuration;
use super::super::rules::Rules;
use shep_core::protocol::ProcessEventKind;
use tokio::sync::broadcast;
use super::super::testing::*;
#[tokio::test]
async fn a_dropped_frame_makes_bark_poll_and_catch_up() {
let (tx, mut rx) = tokio::sync::broadcast::channel(4);
for i in 0..64 {
tx.send(log_event(i)).unwrap();
}
tx.send(errored_event("web")).unwrap();
assert!(
matches!(rx.recv().await, Err(broadcast::error::RecvError::Lagged(n)) if n > 0),
"the fixture must actually overflow the channel"
);
let (tx2, rx2) = tokio::sync::broadcast::channel(4);
for i in 0..64 {
tx2.send(log_event(i)).unwrap();
}
tx2.send(errored_event("web")).unwrap();
let (addr, captured) = one_shot_sink(200, "").await;
let dir = tempfile::tempdir().unwrap();
let barks_path = dir.path().join("barks.jsonl");
let flock = ScriptedFlock::answering(vec![errored_info("web", 16)]);
let loop_handle = tokio::spawn(run_loop(
rx2,
flock.clone(),
gave_up_rules(),
&config_with_sink(addr, &barks_path),
&barks_path,
ScriptedConfig::answering(String::new()),
));
let req = tokio::time::timeout(Duration::from_secs(5), captured)
.await
.expect("a dropped frame must produce a delivered bark")
.unwrap();
assert!(String::from_utf8_lossy(&req.body).contains("web"));
let recorded = tokio::time::timeout(Duration::from_secs(5), async {
loop {
let records = shep_core::barks::read(&barks_path).unwrap();
if !records.is_empty() {
break records;
}
tokio::task::yield_now().await;
}
})
.await
.expect("the delivered bark must be recorded promptly after delivery");
assert_eq!(recorded.len(), 1);
assert_eq!(recorded[0].subject, "web");
assert_eq!(recorded[0].sinks[0].error, None);
assert_eq!(
flock.calls(),
1,
"the poll ran because of the lag, not because an interval elapsed: \
the interval is 60s and this test is milliseconds old"
);
loop_handle.abort();
}
#[tokio::test]
async fn a_handover_re_subscribes_instead_of_ending_the_dog() {
let (first_tx, first_rx) = tokio::sync::broadcast::channel(8);
let (second_tx, second_rx) = tokio::sync::broadcast::channel(8);
let (source, resubscribes) = HandoverSource::across(
vec![first_rx, second_rx],
LinkLost::Budget {
waited: Duration::ZERO,
},
);
let (addr, captured) = one_shot_sink(200, "").await;
let dir = tempfile::tempdir().unwrap();
let barks_path = dir.path().join("barks.jsonl");
let flock = ScriptedFlock::answering(Vec::new());
let loop_handle = tokio::spawn(run_loop(
source,
flock.clone(),
gave_up_rules(),
&config_with_sink(addr, &barks_path),
&barks_path,
ScriptedConfig::answering(String::new()),
));
drop(first_tx);
let delivered = tokio::time::timeout(Duration::from_secs(5), async {
loop {
let _ = second_tx.send(errored_event("web"));
tokio::time::sleep(Duration::from_millis(20)).await;
if loop_handle.is_finished() {
panic!("the loop ended on a handover instead of re-subscribing");
}
}
});
let captured = tokio::time::timeout(Duration::from_secs(5), captured);
let request = tokio::select! {
() = async { delivered.await.ok(); } => panic!(
"no bark was delivered within 5s of the handover, so the \
second generation never reached the loop"
),
request = captured => request.expect("a bark must be delivered after the handover"),
};
assert!(String::from_utf8_lossy(&request.unwrap().body).contains("web"));
assert_eq!(
resubscribes.load(std::sync::atomic::Ordering::SeqCst),
1,
"the handover must have been answered by exactly one re-subscribe"
);
assert!(
flock.calls() >= 1,
"a re-subscribe must reconcile: frames sent while there was no \
subscription are gone, and only the shepherd knows the flock now"
);
assert!(
!loop_handle.is_finished(),
"the dog must still be running after a handover"
);
loop_handle.abort();
}
#[tokio::test]
async fn a_slow_sink_never_stalls_the_loop() {
let (slow_addr, slow_connected) = slow_sink().await;
let (fast_addr, fast_captured) = one_shot_sink(200, "").await;
let dir = tempfile::tempdir().unwrap();
let barks_path = dir.path().join("barks.jsonl");
let mut sinks = BTreeMap::new();
sinks.insert(
"slow".to_owned(),
json_sink(format!("http://{slow_addr}/hook")),
);
sinks.insert(
"fast".to_owned(),
json_sink(format!("http://{fast_addr}/hook")),
);
let rules = Rules::new(
vec![
super::super::rules::Rule {
when: super::super::rules::Trigger::GaveUp {},
sinks: vec!["slow".to_owned()],
debounce: UpDuration::from_millis(0),
},
super::super::rules::Rule {
when: super::super::rules::Trigger::Event {
kinds: vec!["online".to_owned()],
},
sinks: vec!["fast".to_owned()],
debounce: UpDuration::from_millis(0),
},
],
&sinks,
)
.unwrap();
let config = BarkConfig {
sinks,
rules: Vec::new(),
poll: UpDuration::from_millis(60_000),
history_bytes: barks::DEFAULT_MAX_BYTES,
sink_timeout: UpDuration::from_millis(600_000),
};
let (tx, rx) = tokio::sync::broadcast::channel(8);
tx.send(errored_event("web")).unwrap();
tx.send(process_event("api", ProcessEventKind::Online))
.unwrap();
let flock = ScriptedFlock::answering(Vec::new());
let loop_handle = tokio::spawn(run_loop(
rx,
flock,
rules,
&config,
&barks_path,
ScriptedConfig::answering(String::new()),
));
tokio::time::timeout(Duration::from_secs(30), slow_connected)
.await
.expect("the slow sink must be reached at all, or this test proves nothing")
.unwrap();
let req = tokio::time::timeout(Duration::from_secs(30), fast_captured)
.await
.expect(
"the fast sink must be reached while a slow sink is still in \
flight; a slow sink must not stall the loop",
)
.unwrap();
assert_eq!(req.method, "POST");
loop_handle.abort();
}
}