use std::io::{BufRead, Write};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, OnceLock, RwLock};
use std::time::Duration;
use crate::dispatch::{Dispatch, Outcome, run};
use crate::metric::MetricOptions;
use crate::outbox::{DEFAULT_CAPACITY, Drain, Outbox};
use crate::{
CHANNEL_VERSION, Channel, ChannelError, ChildMessage, LambLabel, 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 flush(&self, timeout: Duration) -> Result<(), ChannelError> {
let Some(outbox) = &self.0.outbox else {
return Ok(());
};
match outbox.drain(timeout) {
Drain::Empty => Ok(()),
Drain::Stopped => Err(ChannelError::Closed),
Drain::TimedOut => Err(ChannelError::TimedOut),
}
}
#[must_use]
pub fn pending(&self) -> usize {
self.0.outbox.as_ref().map_or(0, |outbox| outbox.pending())
}
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,
metric_type: None,
labels: std::collections::BTreeMap::new(),
help: None,
});
}
}
pub fn metric_with(&self, name: impl Into<String>, value: f64, options: MetricOptions) {
if let Some(outbox) = &self.0.outbox {
outbox.push_lossy(ChildMessage::Metric {
name: name.into(),
value,
metric_type: options.kind,
labels: options.labels,
help: options.help,
});
}
}
pub fn label_lamb(&self, pid: u32, label: LambLabel) -> Result<(), ChannelError> {
match &self.0.outbox {
Some(outbox) => outbox.push_blocking(ChildMessage::LambLabel { pid, label }),
None => Ok(()),
}
}
}
#[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) {
let mut batch = Vec::new();
let mut line = Vec::new();
while outbox.take_batch(&mut batch) {
if session::write_messages(writer, &batch, &mut line).is_err() {
break;
}
outbox.wrote(batch.len());
}
outbox.stop();
}
pub(crate) fn reader_loop<R: BufRead>(
reader: &mut R,
outbox: &Outbox,
dispatch: &RwLock<Dispatch>,
warn: &dyn Fn(&str),
) {
let mut warned_malformed = false;
let mut line = Vec::new();
loop {
match session::read_message(reader, &mut line) {
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.stop();
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::{PoisonError, mpsc};
use std::time::Duration;
use super::*;
use crate::MetricType;
const DEADLINE: Duration = Duration::from_secs(5);
fn shepherd_over(outbox: &Arc<Outbox>) -> Shepherd {
Shepherd(Arc::new(Inner {
outbox: Some(Arc::clone(outbox)),
dispatch: Arc::new(RwLock::new(Dispatch::default())),
version: None,
}))
}
#[track_caller]
fn flush_bounded(shepherd: &Shepherd, timeout: Duration) -> Result<(), ChannelError> {
let (tx, rx) = mpsc::channel();
let flushing = shepherd.clone();
std::thread::spawn(move || {
let _ = tx.send(flushing.flush(timeout));
});
rx.recv_timeout(DEADLINE).expect("flush never returned")
}
#[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.metric_with("rps", 42.0, MetricOptions::new());
shepherd.ready().expect("an inert ready is not an error");
shepherd
.flush(Duration::ZERO)
.expect("an inert flush is not an error");
assert_eq!(shepherd.pending(), 0);
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_over(&outbox);
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 metric_with_puts_labels_type_and_help_on_the_wire() {
let outbox = Arc::new(Outbox::new(4));
let shepherd = shepherd_over(&outbox);
shepherd.metric_with(
"queue_depth",
3.0,
MetricOptions::new()
.kind(MetricType::Counter)
.label("model", "qwen")
.help("Requests waiting for the lease."),
);
let taken = outbox.take();
let [ChildMessage::Metric { .. }] = taken.as_slice() else {
panic!("expected one metric sample in the outbox, got {taken:?}");
};
assert_eq!(
serde_json::to_string(&taken[0]).expect("encode"),
concat!(
r#"{"kind":"metric","name":"queue_depth","value":3.0,"type":"counter","#,
r#""labels":{"model":"qwen"},"help":"Requests waiting for the lease."}"#,
)
);
}
#[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,
metric_type: None,
labels: std::collections::BTreeMap::new(),
help: None,
});
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.take(),
vec![],
"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.take().as_slice() {
[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 a_lamb_label_the_shepherd_cannot_take_is_an_error_not_a_drop() {
let outbox = Arc::new(Outbox::new(1));
let shepherd = shepherd_over(&outbox);
let label = LambLabel::new("worker 1").expect("a valid label");
shepherd
.label_lamb(4312, label.clone())
.expect("room for the label");
assert_eq!(
outbox.take(),
vec![ChildMessage::LambLabel {
pid: 4312,
label: label.clone()
}]
);
outbox.close();
assert!(matches!(
shepherd.label_lamb(4312, label),
Err(ChannelError::Closed)
));
}
#[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,
metric_type: None,
labels: std::collections::BTreeMap::new(),
help: None,
})
.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 the_writer_writes_a_queued_burst_in_one_write() {
struct CountingSink {
wire: Vec<u8>,
writes: usize,
}
impl Write for CountingSink {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.writes += 1;
self.wire.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let outbox = Outbox::new(8);
for value in [1.0, 2.0, 3.0] {
outbox.push_lossy(ChildMessage::Metric {
name: "rps".to_string(),
value,
metric_type: None,
labels: std::collections::BTreeMap::new(),
help: None,
});
}
outbox.close();
let mut sink = CountingSink {
wire: Vec::new(),
writes: 0,
};
writer_loop(&mut sink, &outbox);
assert_eq!(sink.writes, 1, "the burst was written message by message");
assert_eq!(
String::from_utf8(sink.wire)
.expect("valid utf8")
.lines()
.count(),
3
);
assert_eq!(outbox.pending(), 0, "a written burst is still pending");
}
#[test]
fn a_shutdown_handler_can_flush_a_reply_that_was_still_queued() {
let outbox = Arc::new(Outbox::new(4));
let shepherd = shepherd_over(&outbox);
outbox
.push_blocking(ChildMessage::ActionReply {
action: "gc".to_string(),
body: "collected".to_string(),
id: Some(7),
})
.expect("room for the reply");
let wire = Arc::new(std::sync::Mutex::new(Vec::new()));
let (open, gate) = mpsc::channel();
let mut sink = GatedSink {
wire: Arc::clone(&wire),
gate: Some(gate),
};
let writing = Arc::clone(&outbox);
let writer = std::thread::spawn(move || writer_loop(&mut sink, &writing));
let (report, handled) = mpsc::channel();
let flushing = shepherd.clone();
let seen = Arc::clone(&wire);
shepherd.on_shutdown(move || {
let flushed = flushing.flush(DEADLINE).is_ok();
let bytes = seen.lock().unwrap_or_else(PoisonError::into_inner).clone();
report
.send((flushed, String::from_utf8(bytes).expect("valid utf8")))
.expect("report");
});
let dispatch = Arc::clone(&shepherd.0.dispatch);
let reader = std::thread::spawn(move || {
let mut input = Cursor::new(b"{\"kind\":\"shutdown\"}\n".to_vec());
reader_loop(&mut input, &outbox, &dispatch, &|_: &str| {});
});
assert!(
handled.recv_timeout(Duration::from_millis(200)).is_err(),
"the shutdown handler returned before the queued reply was written"
);
open.send(()).expect("open the sink");
let (flushed, written) = handled
.recv_timeout(DEADLINE)
.expect("the shutdown handler never returned");
assert!(flushed, "flush did not report the reply delivered");
assert!(
written.contains("\"body\":\"collected\""),
"the handler resumed before its queued reply reached the wire: {written:?}"
);
reader.join().expect("reader panicked");
writer.join().expect("writer panicked");
}
#[test]
fn a_flush_reports_closed_when_the_writer_failed_mid_message() {
let outbox = Arc::new(Outbox::new(4));
let shepherd = shepherd_over(&outbox);
outbox
.push_blocking(ChildMessage::Ready)
.expect("room for readiness");
writer_loop(&mut FailingSink, &outbox);
assert!(matches!(
flush_bounded(&shepherd, DEADLINE),
Err(ChannelError::Closed)
));
assert_eq!(shepherd.pending(), 1, "the lost message is still counted");
}
#[test]
fn a_flush_reports_a_timeout_rather_than_a_closed_channel() {
let outbox = Arc::new(Outbox::new(4));
let shepherd = shepherd_over(&outbox);
outbox
.push_blocking(ChildMessage::Ready)
.expect("room for readiness");
assert!(matches!(
flush_bounded(&shepherd, Duration::from_millis(50)),
Err(ChannelError::TimedOut)
));
assert_eq!(shepherd.pending(), 1);
}
#[derive(Debug)]
struct GatedSink {
wire: Arc<std::sync::Mutex<Vec<u8>>>,
gate: Option<mpsc::Receiver<()>>,
}
impl Write for GatedSink {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if let Some(gate) = self.gate.take() {
gate.recv().expect("the test never opened the sink");
}
self.wire
.lock()
.unwrap_or_else(PoisonError::into_inner)
.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[derive(Debug)]
struct FailingSink;
impl Write for FailingSink {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("the shepherd went away"))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[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");
match outbox.take().as_slice() {
[
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:?}"),
}
}
}