use super::rules::Firing;
use super::sinks::Sink;
use shep_core::barks::{self, SinkOutcome};
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::MissedTickBehavior;
#[derive(Debug, Clone)]
pub(super) struct Delivery {
pub(super) sinks: Arc<BTreeMap<String, Sink>>,
pub(super) append_lock: Arc<Mutex<()>>,
pub(super) barks_path: Arc<PathBuf>,
pub(super) sink_timeout: Duration,
pub(super) max_bytes: u64,
}
pub(super) fn poll_timer(period: Duration) -> tokio::time::Interval {
let mut interval = tokio::time::interval_at(tokio::time::Instant::now() + period, period);
interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
interval
}
pub(super) fn spawn_firings(firings: Vec<Firing>, delivery: &Delivery) {
for firing in firings {
let delivery = delivery.clone();
tokio::spawn(async move {
deliver_and_record(firing, &delivery).await;
});
}
}
async fn deliver_and_record(firing: Firing, delivery: &Delivery) {
let mut bark = firing.bark;
let mut outcomes = Vec::with_capacity(firing.sinks.len());
for name in &firing.sinks {
let outcome = match delivery.sinks.get(name) {
Some(sink) => match super::sinks::deliver(sink, &bark, delivery.sink_timeout).await {
Ok(()) => SinkOutcome {
sink: name.clone(),
error: None,
},
Err(err) => SinkOutcome {
sink: name.clone(),
error: Some(err.to_string()),
},
},
None => SinkOutcome {
sink: name.clone(),
error: Some("sink not configured".to_owned()),
},
};
outcomes.push(outcome);
}
bark.sinks = outcomes;
let _guard = delivery.append_lock.lock().await;
if let Err(err) = barks::append(&delivery.barks_path, &bark, delivery.max_bytes) {
eprintln!("shep dog bark: could not record a fired bark: {err}");
}
}
pub(super) fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.map(|elapsed| elapsed.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Duration;
use shep_core::barks::{self};
use tokio::sync::Mutex;
use super::super::rules::Firing;
use shep_core::barks::Bark;
use super::*;
use super::super::testing::*;
#[tokio::test]
async fn a_bark_is_recorded_even_when_every_sink_refuses_it() {
let (addr, _captured) = one_shot_sink(500, "refused").await;
let dir = tempfile::tempdir().unwrap();
let barks_path = dir.path().join("barks.jsonl");
let mut sinks = BTreeMap::new();
sinks.insert("ops".to_owned(), json_sink(format!("http://{addr}/hook")));
let firing = Firing {
bark: Bark {
at_ms: 1_000,
rule: "gave_up".to_owned(),
subject: "web".to_owned(),
message: "web gave up: restart budget exhausted".to_owned(),
sinks: Vec::new(),
},
sinks: vec!["ops".to_owned()],
};
deliver_and_record(
firing,
&Delivery {
sinks: Arc::new(sinks),
append_lock: Arc::new(Mutex::new(())),
barks_path: Arc::new(barks_path.clone()),
sink_timeout: Duration::from_secs(5),
max_bytes: barks::DEFAULT_MAX_BYTES,
},
)
.await;
let recorded = shep_core::barks::read(&barks_path).unwrap();
assert_eq!(
recorded.len(),
1,
"a refused delivery must still be recorded"
);
assert_eq!(recorded[0].subject, "web");
assert!(
recorded[0].sinks[0].error.is_some(),
"the 500 must be recorded as a failed delivery, not silently dropped"
);
}
}