rmux-server 0.9.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use std::io;
use std::time::Duration;

const TRANSIENT_CONSOLE_INPUT_RETRIES: usize = 8;
const TRANSIENT_CONSOLE_INPUT_RETRY_DELAY: Duration = Duration::from_millis(50);

pub(crate) fn write_with_transient_retry(
    mut write: impl FnMut() -> io::Result<()>,
) -> io::Result<()> {
    for attempt in 0..=TRANSIENT_CONSOLE_INPUT_RETRIES {
        match write() {
            Ok(()) => return Ok(()),
            Err(error)
                if attempt < TRANSIENT_CONSOLE_INPUT_RETRIES
                    && is_transient_console_input_error(&error) =>
            {
                std::thread::sleep(TRANSIENT_CONSOLE_INPUT_RETRY_DELAY);
            }
            Err(error) => return Err(error),
        }
    }
    Ok(())
}

fn is_transient_console_input_error(error: &io::Error) -> bool {
    const ERROR_GEN_FAILURE: i32 = 31;
    error.raw_os_error() == Some(ERROR_GEN_FAILURE)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicUsize, Ordering};

    fn transient_error() -> io::Error {
        io::Error::from_raw_os_error(31)
    }

    #[test]
    fn retries_transient_console_input_failure() {
        let attempts = AtomicUsize::new(0);

        write_with_transient_retry(|| {
            let attempt = attempts.fetch_add(1, Ordering::SeqCst);
            if attempt < 2 {
                return Err(transient_error());
            }
            Ok(())
        })
        .expect("transient failure should be retried");

        assert_eq!(attempts.load(Ordering::SeqCst), 3);
    }

    #[test]
    fn does_not_retry_non_transient_console_input_failure() {
        let attempts = AtomicUsize::new(0);

        let error = write_with_transient_retry(|| {
            attempts.fetch_add(1, Ordering::SeqCst);
            Err(io::Error::from_raw_os_error(5))
        })
        .expect_err("non-transient failure should not be retried");

        assert_eq!(error.raw_os_error(), Some(5));
        assert_eq!(attempts.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn stops_after_transient_retry_budget() {
        let attempts = AtomicUsize::new(0);

        let error = write_with_transient_retry(|| {
            attempts.fetch_add(1, Ordering::SeqCst);
            Err(transient_error())
        })
        .expect_err("retry budget should be bounded");

        assert_eq!(error.raw_os_error(), Some(31));
        assert_eq!(
            attempts.load(Ordering::SeqCst),
            TRANSIENT_CONSOLE_INPUT_RETRIES + 1
        );
    }
}