use std::io::{BufRead, Write};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, OnceLock, RwLock};
use crate::dispatch::{Dispatch, Outcome, run};
use crate::outbox::{DEFAULT_CAPACITY, Outbox};
use crate::{CHANNEL_VERSION, Channel, ChannelError, ChildMessage, VERSION_VAR, session};
const NO_CHANNEL_ADVICE: &str = "no channel on this process. Set `channel = true` \
(or `wait_ready` / `shutdown_with_message`) on this app in the Flockfile to open one.";
const UNHANDLED_SHUTDOWN_ADVICE: &str = "the shepherd sent shutdown and no on_shutdown handler is registered. This \
process will be killed when kill_timeout expires. Register one to stop gracefully.";
fn warn(message: &str) {
eprintln!("shep-channel: {message}");
}
#[derive(Clone)]
pub struct Shepherd(Arc<Inner>);
impl core::fmt::Debug for Shepherd {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Shepherd")
.field("active", &self.is_active())
.field("dropped_metrics", &self.dropped_metrics())
.field("stamped", &self.0.version.is_some())
.finish()
}
}
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Shepherd>();
};
#[derive(Debug)]
struct Inner {
outbox: Option<Arc<Outbox>>,
dispatch: Arc<RwLock<Dispatch>>,
version: Option<String>,
}
impl Shepherd {
fn inert(version: Option<String>) -> Self {
Self(Arc::new(Inner {
outbox: None,
dispatch: Arc::new(RwLock::new(Dispatch::default())),
version,
}))
}
#[must_use]
pub fn is_active(&self) -> bool {
self.0
.outbox
.as_ref()
.is_some_and(|outbox| !outbox.is_closed())
}
#[must_use]
pub fn version(&self) -> Option<&str> {
self.0.version.as_deref()
}
#[must_use]
pub fn dropped_metrics(&self) -> u64 {
self.0.outbox.as_ref().map_or(0, |outbox| outbox.dropped())
}
pub fn on_action<H>(&self, name: impl AsRef<str>, handler: H) -> &Self
where
H: Fn(Option<&str>, &str) -> String + Send + Sync + 'static,
{
self.0
.dispatch
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.register_action(name.as_ref().to_string(), Box::new(handler));
self
}
pub fn on_shutdown<H>(&self, handler: H) -> &Self
where
H: Fn() + Send + Sync + 'static,
{
self.0
.dispatch
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.register_shutdown(Box::new(handler));
self
}
pub fn ready(&self) -> Result<(), ChannelError> {
match &self.0.outbox {
Some(outbox) => outbox.push_blocking(ChildMessage::Ready),
None => Ok(()),
}
}
pub fn metric(&self, name: impl Into<String>, value: f64) {
if let Some(outbox) = &self.0.outbox {
outbox.push_lossy(ChildMessage::Metric {
name: name.into(),
value,
});
}
}
}
#[must_use]
pub fn serve() -> Shepherd {
static SHEPHERD: OnceLock<Shepherd> = OnceLock::new();
static CALLS: AtomicU32 = AtomicU32::new(0);
let shepherd = SHEPHERD.get_or_init(start);
if CALLS.fetch_add(1, Ordering::Relaxed) == 1 {
warn(
"serve() called more than once; returning the first handle. \
The channel is one descriptor and cannot be opened twice.",
);
}
shepherd.clone()
}
pub(crate) fn writer_loop<W: Write>(writer: &mut W, outbox: &Outbox) {
while let Some(message) = outbox.pop() {
if session::write_message(writer, &message).is_err() {
break;
}
}
outbox.close();
}
pub(crate) fn reader_loop<R: BufRead>(
reader: &mut R,
outbox: &Outbox,
dispatch: &RwLock<Dispatch>,
warn: &dyn Fn(&str),
) {
let mut warned_malformed = false;
loop {
match session::read_message(reader) {
Ok(Some(message)) => {
let resolved = dispatch
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.resolve(message);
let outcome = run(resolved);
match outcome {
Outcome::Reply(reply) => {
if outbox.push_blocking(reply).is_err() {
break;
}
}
Outcome::Handled => {}
Outcome::UnhandledShutdown => warn(UNHANDLED_SHUTDOWN_ADVICE),
Outcome::ShutdownFailed(message) => {
warn(&format!("shutdown handler panicked: {message}"));
}
}
}
Err(ChannelError::Malformed(message)) => {
if !warned_malformed {
warned_malformed = true;
warn(&format!("malformed frame from the shepherd: {message}"));
}
}
Ok(None) | Err(_) => break,
}
}
outbox.close();
}
fn start() -> Shepherd {
let channel = match Channel::open() {
Ok(Some(channel)) => channel,
Ok(None) => {
if std::env::var_os("SHEP_NAME").is_some() {
warn(NO_CHANNEL_ADVICE);
}
return Shepherd::inert(None);
}
Err(error) => {
warn(&format!("{error}; continuing without a channel"));
return Shepherd::inert(None);
}
};
let (reader, mut writer, version) = channel.into_halves();
if let Some(stamp) = &version
&& stamp != CHANNEL_VERSION
{
warn(&format!(
"the shepherd stamps {VERSION_VAR}={stamp} and this crate implements \
{CHANNEL_VERSION}; continuing, since a newer wire has so far only added \
fields an older reader ignores"
));
}
let outbox = Arc::new(Outbox::new(DEFAULT_CAPACITY));
let dispatch = Arc::new(RwLock::new(Dispatch::default()));
let writing = Arc::clone(&outbox);
let writer_spawn = std::thread::Builder::new()
.name("shep-channel-writer".to_string())
.spawn(move || writer_loop(&mut writer, &writing));
if let Err(error) = writer_spawn {
warn(&format!(
"failed to spawn the shep-channel writer thread: {error}; continuing without a channel"
));
outbox.close();
return Shepherd::inert(version);
}
let reading = Arc::clone(&outbox);
let handlers = Arc::clone(&dispatch);
let reader_spawn = std::thread::Builder::new()
.name("shep-channel-reader".to_string())
.spawn(move || {
let mut reader = reader;
reader_loop(&mut reader, &reading, &handlers, &warn);
});
if let Err(error) = reader_spawn {
warn(&format!(
"failed to spawn the shep-channel reader thread: {error}; readiness and metrics still work, but no action sent to this process will ever be answered"
));
}
Shepherd(Arc::new(Inner {
outbox: Some(outbox),
dispatch,
version,
}))
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use std::sync::mpsc;
use std::time::Duration;
use super::*;
const DEADLINE: Duration = Duration::from_secs(5);
#[test]
fn an_inert_handle_accepts_everything_and_does_nothing() {
let shepherd = Shepherd::inert(None);
assert!(!shepherd.is_active());
shepherd.on_action("gc", |_, _| "ok".to_string());
shepherd.on_shutdown(|| {});
shepherd.metric("rps", 42.0);
shepherd.ready().expect("an inert ready is not an error");
assert_eq!(shepherd.dropped_metrics(), 0);
assert_eq!(shepherd.version(), None);
}
#[test]
fn a_handle_stops_being_active_once_the_channel_closes() {
let outbox = Arc::new(Outbox::new(4));
let shepherd = Shepherd(Arc::new(Inner {
outbox: Some(Arc::clone(&outbox)),
dispatch: Arc::new(RwLock::new(Dispatch::default())),
version: None,
}));
assert!(shepherd.is_active(), "a fresh channel should read as live");
outbox.close();
assert!(
!shepherd.is_active(),
"a handle whose shepherd went away still reads as live"
);
}
#[test]
fn a_shepherds_debug_names_state_and_never_a_queued_payload() {
let outbox = Arc::new(Outbox::new(4));
outbox.push_lossy(ChildMessage::ActionReply {
action: "gc".into(),
body: "SECRET-REPLY-BODY".into(),
id: Some(7),
});
outbox.push_lossy(ChildMessage::Metric {
name: "SECRET-METRIC-NAME".into(),
value: 1.0,
});
let shepherd = Shepherd(Arc::new(Inner {
outbox: Some(Arc::clone(&outbox)),
dispatch: Arc::new(RwLock::new(Dispatch::default())),
version: Some("1".into()),
}));
let rendered = format!("{shepherd:?}");
assert_eq!(
rendered,
"Shepherd { active: true, dropped_metrics: 0, stamped: true }"
);
assert!(
!rendered.contains("SECRET-REPLY-BODY") && !rendered.contains("SECRET-METRIC-NAME"),
"a queued payload reached the Debug output: {rendered}"
);
}
#[test]
fn the_no_channel_advice_names_every_field_that_opens_one() {
for field in ["channel = true", "wait_ready", "shutdown_with_message"] {
assert!(
NO_CHANNEL_ADVICE.contains(field),
"advice does not mention {field}"
);
}
}
#[test]
fn the_unhandled_shutdown_warning_names_the_method_to_call() {
assert!(UNHANDLED_SHUTDOWN_ADVICE.contains("on_shutdown"));
assert!(UNHANDLED_SHUTDOWN_ADVICE.contains("kill_timeout"));
}
#[test]
fn two_malformed_lines_warn_once_and_the_loop_keeps_going() {
let mut reader =
Cursor::new(b"not json\nalso not json\n{\"kind\":\"shutdown\"}\n".to_vec());
let outbox = Outbox::new(4);
let dispatch = RwLock::new(Dispatch::default());
let shutdown_hits = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let counter = Arc::clone(&shutdown_hits);
dispatch
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.register_shutdown(Box::new(move || {
counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}));
let warnings: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
let warn = |message: &str| warnings.lock().unwrap().push(message.to_string());
reader_loop(&mut reader, &outbox, &dispatch, &warn);
let collected = warnings.into_inner().unwrap();
assert_eq!(
collected.len(),
1,
"expected exactly one warning for two malformed lines, got {collected:?}"
);
assert_eq!(
shutdown_hits.load(std::sync::atomic::Ordering::SeqCst),
1,
"the well-formed shutdown after the two bad lines was never reached"
);
}
#[test]
fn end_of_stream_breaks_the_loop_and_closes_the_outbox() {
let mut reader = Cursor::new(Vec::new());
let outbox = Outbox::new(4);
let dispatch = RwLock::new(Dispatch::default());
let warn = |_: &str| {};
reader_loop(&mut reader, &outbox, &dispatch, &warn);
assert!(
outbox.push_blocking(ChildMessage::Ready).is_err(),
"reader_loop returned without closing the outbox"
);
assert_eq!(
outbox.pop(),
None,
"outbox should read as closed-and-empty after EOF, not park a waiter"
);
}
#[test]
fn an_actions_reply_reaches_the_outbox_carrying_its_id() {
let mut reader = Cursor::new(b"{\"kind\":\"action\",\"name\":\"gc\",\"id\":7}\n".to_vec());
let outbox = Outbox::new(4);
let dispatch = RwLock::new(Dispatch::default());
dispatch
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.register_action("gc".to_string(), Box::new(|_, _| "ok".to_string()));
let warn = |_: &str| {};
reader_loop(&mut reader, &outbox, &dispatch, &warn);
match outbox.pop() {
Some(ChildMessage::ActionReply { action, body, id }) => {
assert_eq!(action, "gc");
assert_eq!(body, "ok");
assert_eq!(id, Some(7));
}
other => panic!("expected an action reply, got {other:?}"),
}
}
#[test]
fn the_writer_drains_what_is_already_queued_after_close() {
let outbox = Outbox::new(4);
outbox
.push_blocking(ChildMessage::Ready)
.expect("room for the first message");
outbox
.push_blocking(ChildMessage::Metric {
name: "rps".to_string(),
value: 1.0,
})
.expect("room for the second message");
outbox.close();
let mut written = Vec::new();
writer_loop(&mut written, &outbox);
let text = String::from_utf8(written).expect("valid utf8");
let lines: Vec<&str> = text.lines().collect();
assert_eq!(
lines.len(),
2,
"expected both already-queued messages to be written, got {lines:?}"
);
assert!(lines[0].contains("\"kind\":\"ready\""));
assert!(lines[1].contains("\"kind\":\"metric\""));
}
#[test]
fn a_handler_that_registers_another_handler_does_not_deadlock_the_reader() {
let shepherd = Shepherd::inert(None);
let inner_shepherd = shepherd.clone();
shepherd.on_action("reload", move |_, _| {
inner_shepherd.on_action("late", |_, _| "late ok".to_string());
"reloaded".to_string()
});
let outbox = Arc::new(Outbox::new(4));
let dispatch = Arc::clone(&shepherd.0.dispatch);
let outbox_thread = Arc::clone(&outbox);
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let mut reader = Cursor::new(
b"{\"kind\":\"action\",\"name\":\"reload\",\"id\":1}\n\
{\"kind\":\"action\",\"name\":\"late\",\"id\":2}\n"
.to_vec(),
);
let warn = |_: &str| {};
reader_loop(&mut reader, &outbox_thread, &dispatch, &warn);
let _ = tx.send(());
});
rx.recv_timeout(DEADLINE)
.expect("reader_loop deadlocked: a handler that registers a handler hung the reader");
let first = outbox.pop().expect("the reload reply");
let second = outbox
.pop()
.expect("the late reply, registered inside the reload handler");
match (first, second) {
(
ChildMessage::ActionReply {
action: action1,
body: body1,
id: id1,
},
ChildMessage::ActionReply {
action: action2,
body: body2,
id: id2,
},
) => {
assert_eq!(action1, "reload");
assert_eq!(body1, "reloaded");
assert_eq!(id1, Some(1));
assert_eq!(action2, "late");
assert_eq!(body2, "late ok");
assert_eq!(id2, Some(2));
}
other => panic!("expected two action replies, got {other:?}"),
}
}
}