use std::io;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::thread::{self, JoinHandle};
use std::time::Duration;
use crate::engine::RawFrame;
use super::{DeviceHandle, FrameSender, WriterCmd};
pub trait DeviceFactory: Send + 'static {
fn open(&mut self) -> io::Result<DeviceHandle>;
fn name(&self) -> &str {
"device"
}
}
impl<F> DeviceFactory for F
where
F: FnMut() -> io::Result<DeviceHandle> + Send + 'static,
{
fn open(&mut self) -> io::Result<DeviceHandle> {
(self)()
}
}
pub const INITIAL_BACKOFF: Duration = Duration::from_millis(500);
pub const MAX_BACKOFF: Duration = Duration::from_secs(30);
pub struct Supervisor {
pub frames_rx: mpsc::Receiver<RawFrame>,
sender: FrameSender,
stop: Arc<AtomicBool>,
join: Option<JoinHandle<()>>,
}
impl Supervisor {
pub fn new<F: DeviceFactory>(factory: F) -> Self {
let (frames_tx, frames_rx) = mpsc::channel::<RawFrame>();
let (cmd_tx, cmd_rx) = mpsc::channel::<WriterCmd>();
let stop = Arc::new(AtomicBool::new(false));
let stop_for_thread = stop.clone();
let join = thread::Builder::new()
.name("device-supervisor".into())
.spawn(move || supervisor_loop(factory, frames_tx, cmd_rx, stop_for_thread))
.expect("spawn device supervisor");
Self {
frames_rx,
sender: FrameSender::from_cmd_tx(cmd_tx),
stop,
join: Some(join),
}
}
pub fn frame_sender(&self) -> FrameSender {
self.sender.clone()
}
pub fn shutdown(&self) {
self.stop.store(true, Ordering::Relaxed);
}
}
impl Drop for Supervisor {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(j) = self.join.take() {
let _ = j.join();
}
}
}
fn supervisor_loop<F: DeviceFactory>(
mut factory: F,
frames_tx: mpsc::Sender<RawFrame>,
cmd_rx: mpsc::Receiver<WriterCmd>,
stop: Arc<AtomicBool>,
) {
let mut backoff = INITIAL_BACKOFF;
let mut held_cmd_rx = Some(cmd_rx);
while !stop.load(Ordering::Relaxed) {
let handle = match factory.open() {
Ok(h) => {
log::info!("{}: device session opened", factory.name());
backoff = INITIAL_BACKOFF;
h
}
Err(e) => {
log::warn!(
"{}: open failed: {e}; retrying in {:?}",
factory.name(),
backoff
);
sleep_interruptible(backoff, &stop);
backoff = (backoff * 2).min(MAX_BACKOFF);
continue;
}
};
let cmd_rx = held_cmd_rx
.take()
.expect("held_cmd_rx is always Some at session start");
held_cmd_rx = Some(run_session(
factory.name(),
handle,
&frames_tx,
cmd_rx,
&stop,
));
if stop.load(Ordering::Relaxed) {
break;
}
log::warn!("{}: session ended; will reconnect", factory.name());
sleep_interruptible(Duration::from_millis(500), &stop);
}
log::info!("{}: supervisor stopped", factory.name());
}
fn sleep_interruptible(duration: Duration, stop: &Arc<AtomicBool>) {
let step = Duration::from_millis(50);
let mut remaining = duration;
while remaining > Duration::ZERO {
if stop.load(Ordering::Relaxed) {
return;
}
let s = step.min(remaining);
thread::sleep(s);
remaining = remaining.saturating_sub(s);
}
}
fn run_session(
name: &str,
handle: DeviceHandle,
frames_tx: &mpsc::Sender<RawFrame>,
cmd_rx: mpsc::Receiver<WriterCmd>,
stop: &Arc<AtomicBool>,
) -> mpsc::Receiver<WriterCmd> {
let device_sender = handle.frame_sender();
let stop_for_fwd = stop.clone();
let session_done = Arc::new(AtomicBool::new(false));
let session_done_for_fwd = session_done.clone();
let (return_tx, return_rx) = mpsc::channel::<mpsc::Receiver<WriterCmd>>();
let fwd_name = format!("{name}-cmd-fwd");
let cmd_join = thread::Builder::new()
.name(fwd_name)
.spawn(move || {
loop {
if stop_for_fwd.load(Ordering::Relaxed)
|| session_done_for_fwd.load(Ordering::Relaxed)
{
break;
}
match cmd_rx.recv_timeout(Duration::from_millis(250)) {
Ok(WriterCmd::Frame(f)) => {
if device_sender.send_frame(f).is_err() {
break;
}
}
Ok(WriterCmd::Bytes(_) | WriterCmd::Shutdown(_)) => {
}
Err(mpsc::RecvTimeoutError::Timeout) => continue,
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
let _ = return_tx.send(cmd_rx);
})
.expect("spawn supervisor cmd forwarder");
'pump: loop {
let frame = match handle.frames_rx.recv_timeout(Duration::from_millis(250)) {
Ok(frame) => frame,
Err(mpsc::RecvTimeoutError::Timeout) if stop.load(Ordering::Relaxed) => break,
Err(mpsc::RecvTimeoutError::Timeout) => continue,
Err(mpsc::RecvTimeoutError::Disconnected) => break,
};
if stop.load(Ordering::Relaxed) {
break;
}
if frames_tx.send(frame).is_err() {
break;
}
loop {
match handle.frames_rx.try_recv() {
Ok(frame) => {
if frames_tx.send(frame).is_err() {
break 'pump;
}
}
Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => break 'pump,
}
}
}
session_done.store(true, Ordering::Relaxed);
if stop.load(Ordering::Relaxed) {
log::info!("{name}: closing the device");
let _ = handle.close();
} else {
drop(handle);
}
let _ = cmd_join.join();
return_rx
.recv()
.expect("cmd forwarder always returns cmd_rx")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::device::{Closed, DeviceDecoder, DeviceEncoder, DeviceEvent, run as run_device};
use std::io::{self, Read, Write};
use std::sync::Mutex;
use std::sync::atomic::AtomicU32;
struct TestDecoder;
impl DeviceDecoder for TestDecoder {
fn decode(&mut self, bytes: &[u8], events: &mut Vec<DeviceEvent>) {
for &b in bytes {
events.push(DeviceEvent::Frame(RawFrame::new(
None,
0,
b as u32,
0,
0,
std::iter::empty(),
)));
}
}
}
struct TestEncoder;
impl DeviceEncoder for TestEncoder {
fn encode_frame(&self, _frame: &RawFrame) -> Option<Vec<u8>> {
None
}
}
struct OneShotReader(Option<Vec<u8>>);
impl Read for OneShotReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self.0.take() {
Some(data) => {
let n = data.len().min(buf.len());
buf[..n].copy_from_slice(&data[..n]);
Ok(n)
}
None => Ok(0),
}
}
}
struct SinkWriter;
impl Write for SinkWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
struct ReconnectFactory {
sessions: Arc<AtomicU32>,
bytes_per_session: Arc<Mutex<Vec<Vec<u8>>>>,
}
impl DeviceFactory for ReconnectFactory {
fn open(&mut self) -> io::Result<DeviceHandle> {
let n = self.sessions.fetch_add(1, Ordering::Relaxed) as usize;
let payload = {
let table = self.bytes_per_session.lock().unwrap();
table.get(n).cloned().unwrap_or_default()
};
if payload.is_empty() {
return Err(io::Error::other("no payload for session"));
}
let reader: Box<dyn Read + Send> = Box::new(OneShotReader(Some(payload)));
let writer: Box<dyn Write + Send> = Box::new(SinkWriter);
Ok(run_device(TestDecoder, TestEncoder, reader, writer))
}
}
#[test]
fn supervisor_reconnects_across_eof() {
let sessions = Arc::new(AtomicU32::new(0));
let payloads = Arc::new(Mutex::new(vec![
vec![1u8, 2, 3],
vec![], vec![10u8, 20],
vec![100u8, 101],
]));
let factory = ReconnectFactory {
sessions: sessions.clone(),
bytes_per_session: payloads,
};
let sup = Supervisor::new(factory);
let mut got: Vec<u32> = Vec::new();
let deadline = std::time::Instant::now() + Duration::from_secs(60);
while got.len() < 7 && std::time::Instant::now() < deadline {
match sup.frames_rx.recv_timeout(Duration::from_millis(500)) {
Ok(f) => got.push(f.pgn),
Err(_) => continue,
}
}
sup.shutdown();
assert_eq!(
got,
vec![1, 2, 3, 10, 20, 100, 101],
"expected all 3 sessions' bytes in order"
);
assert!(
sessions.load(Ordering::Relaxed) >= 4,
"expected ≥ 4 open attempts (3 successful + 1 transient failure)"
);
}
struct QuietReader;
impl Read for QuietReader {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
thread::sleep(Duration::from_millis(20));
Err(io::ErrorKind::TimedOut.into())
}
}
#[derive(Clone, Default)]
struct RecordingWriter(Arc<Mutex<Vec<u8>>>);
impl Write for RecordingWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
struct ByeEncoder;
impl DeviceEncoder for ByeEncoder {
fn encode_frame(&self, _frame: &RawFrame) -> Option<Vec<u8>> {
None
}
fn shutdown_bytes(&self) -> Vec<u8> {
b"BYE".to_vec()
}
}
fn quiet_device(written: &RecordingWriter) -> DeviceHandle {
run_device(
TestDecoder,
ByeEncoder,
Box::new(QuietReader),
Box::new(written.clone()),
)
}
#[test]
fn closing_a_device_writes_its_goodbye() {
let written = RecordingWriter::default();
assert_eq!(quiet_device(&written).close(), Closed::Confirmed);
assert_eq!(*written.0.lock().unwrap(), b"BYE");
}
#[test]
fn a_closer_closes_from_another_thread() {
let written = RecordingWriter::default();
let handle = quiet_device(&written);
let closer = handle.closer();
assert_eq!(
thread::spawn(move || closer.close(Duration::from_secs(5)))
.join()
.unwrap(),
Closed::Confirmed
);
assert_eq!(*written.0.lock().unwrap(), b"BYE");
}
struct BrokenWriter;
impl Write for BrokenWriter {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::ErrorKind::BrokenPipe.into())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn a_failed_goodbye_is_not_confirmed() {
let handle = run_device(
TestDecoder,
ByeEncoder,
Box::new(QuietReader),
Box::new(BrokenWriter),
);
assert_eq!(handle.close(), Closed::Unconfirmed);
}
#[test]
fn stopping_the_supervisor_says_goodbye() {
let written = RecordingWriter::default();
let for_factory = written.clone();
let sup = Supervisor::new(move || Ok(quiet_device(&for_factory)));
thread::sleep(Duration::from_millis(300)); drop(sup); assert_eq!(*written.0.lock().unwrap(), b"BYE");
let written = RecordingWriter::default();
let for_factory = written.clone();
let sup = Supervisor::new(move || {
Ok(run_device(
TestDecoder,
ByeEncoder,
Box::new(OneShotReader(Some(vec![1]))),
Box::new(for_factory.clone()),
))
});
let _ = sup.frames_rx.recv_timeout(Duration::from_secs(5));
thread::sleep(Duration::from_millis(200)); assert!(written.0.lock().unwrap().is_empty(), "no goodbye on EOF");
drop(sup);
}
struct SlowWriter(Option<Duration>, Arc<Mutex<Vec<u8>>>);
impl Write for SlowWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.0 {
Some(d) => thread::sleep(d),
None => loop {
thread::sleep(Duration::from_secs(3600));
},
}
self.1.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
struct ByteEncoder;
impl DeviceEncoder for ByteEncoder {
fn encode_frame(&self, _frame: &RawFrame) -> Option<Vec<u8>> {
Some(vec![1])
}
fn shutdown_bytes(&self) -> Vec<u8> {
b"BYE".to_vec()
}
}
fn frame() -> RawFrame {
RawFrame::new(None, 0, 1, 0, 0, std::iter::empty())
}
#[test]
fn a_long_backlog_drains_before_the_goodbye() {
let written = Arc::new(Mutex::new(Vec::new()));
let handle = run_device(
TestDecoder,
ByteEncoder,
Box::new(QuietReader),
Box::new(SlowWriter(Some(Duration::from_millis(20)), written.clone())),
);
for _ in 0..30 {
handle.send_frame(frame()).unwrap();
}
let closer = handle.closer();
assert_eq!(closer.close(Duration::from_millis(500)), Closed::Confirmed);
assert!(written.lock().unwrap().ends_with(b"BYE"));
}
#[test]
fn a_stuck_writer_is_unconfirmed() {
let handle = run_device(
TestDecoder,
ByteEncoder,
Box::new(QuietReader),
Box::new(SlowWriter(None, Arc::default())),
);
handle.send_frame(frame()).unwrap();
assert_eq!(
handle.closer().close(Duration::from_millis(150)),
Closed::Unconfirmed
);
}
#[test]
fn a_device_that_went_away_is_gone() {
let handle = run_device(
TestDecoder,
ByteEncoder,
Box::new(QuietReader),
Box::new(BrokenWriter),
);
handle.send_frame(frame()).unwrap(); thread::sleep(Duration::from_millis(100));
assert_eq!(handle.close(), Closed::Gone);
}
}