syd 3.52.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/retry.rs: Utilities to handle restarting syscalls
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

//! Utilities to handle restarting syscalls

// SAFETY: This module has been liberated from unsafe code!
#![forbid(unsafe_code)]

use std::time::Duration;

use nix::errno::Errno;
use retry::{delay::Exponential, retry, OperationResult};

use crate::config::{
    EAGAIN_BACKOFF_FACTOR, EAGAIN_INITIAL_DELAY, EAGAIN_MAX_DELAY, EAGAIN_MAX_RETRY,
};

/// Retries a closure on `EAGAIN` and `EINTR` errors.
///
/// This function will call the provided closure, and if the closure
/// returns `EAGAIN` or `EINTR` error, it will retry the operation until it
/// succeeds or fails with a different error.
pub fn retry_on_intr<F, T>(mut f: F) -> Result<T, Errno>
where
    F: FnMut() -> Result<T, Errno>,
{
    let strategy =
        Exponential::from_millis_with_factor(EAGAIN_INITIAL_DELAY, EAGAIN_BACKOFF_FACTOR)
            .map(|d| Duration::from_millis(EAGAIN_MAX_DELAY).min(d))
            .take(EAGAIN_MAX_RETRY);

    retry(strategy, || match retry_on_eintr(&mut f) {
        Ok(v) => OperationResult::Ok(v),
        Err(Errno::EAGAIN) => OperationResult::Retry(Errno::EAGAIN),
        Err(errno) => OperationResult::Err(errno),
    })
    .map_err(|e| e.error)
}

/// Retries a closure on `EINTR` errors.
///
/// This function will call the provided closure, and if the closure
/// returns `EINTR` error, it will retry the operation until it
/// succeeds or fails with a different error.
pub fn retry_on_eintr<F, T>(mut f: F) -> Result<T, Errno>
where
    F: FnMut() -> Result<T, Errno>,
{
    loop {
        match f() {
            Err(Errno::EINTR) => continue,
            result => return result,
        }
    }
}

/// Retries a closure on `EAGAIN` errors.
///
/// This function will call the provided closure, and if the closure
/// returns `EAGAIN` error, it will retry the operation until it
/// succeeds or fails with a different error.
pub fn retry_on_eagain<F, T>(mut f: F) -> Result<T, Errno>
where
    F: FnMut() -> Result<T, Errno>,
{
    let strategy =
        Exponential::from_millis_with_factor(EAGAIN_INITIAL_DELAY, EAGAIN_BACKOFF_FACTOR)
            .map(|d| Duration::from_millis(EAGAIN_MAX_DELAY).min(d))
            .take(EAGAIN_MAX_RETRY);

    retry(strategy, || match f() {
        Ok(v) => OperationResult::Ok(v),
        Err(Errno::EAGAIN) => OperationResult::Retry(Errno::EAGAIN),
        Err(errno) => OperationResult::Err(errno),
    })
    .map_err(|e| e.error)
}

/// write! which retries on EINTR and EAGAIN.
#[macro_export]
macro_rules! rwrite {
    ($dst:expr, $($arg:tt)*) => {{
        $crate::retry::retry_on_intr(|| {
            $dst.write_fmt(format_args!($($arg)*))
                .map_err(|err| $crate::err::err2no(&err))
        })
    }};
}

/// writeln! which retries on EINTR and EAGAIN.
#[macro_export]
macro_rules! rwriteln {
    ($dst:expr $(, $($arg:tt)*)?) => {{
        $crate::retry::retry_on_intr(|| {
            let () = $dst
                .write_fmt(format_args!($($($arg)*)?))
                .map_err(|err| $crate::err::err2no(&err))?;
            $dst
                .write_all(b"\n")
                .map_err(|err| $crate::err::err2no(&err))
        })
    }};
}

#[cfg(test)]
mod tests {
    use std::time::Instant;

    use super::*;

    #[test]
    fn test_retry_1() {
        let start = Instant::now();
        let mut attempts = 3;
        let result = retry_on_intr(move || {
            if attempts > 0 {
                attempts -= 1;
                Err(Errno::EAGAIN) // Simulate EAGAIN.
            } else {
                Ok(42) // Simulate success after retries.
            }
        });

        assert_eq!(result, Ok(42));

        let elapsed = start.elapsed();
        let expected_duration = Duration::from_millis(EAGAIN_INITIAL_DELAY as u64 * 7);
        assert!(
            elapsed >= expected_duration,
            "Expected delay due to exponential backoff"
        );
    }

    #[test]
    fn test_retry_2() {
        let start = Instant::now();
        let mut attempts = EAGAIN_MAX_RETRY;
        let result = retry_on_intr(move || {
            if attempts > 0 {
                attempts -= 1;
                Err(Errno::EAGAIN) // Simulate EAGAIN.
            } else {
                Ok(42) // Simulate success after retries.
            }
        });

        assert_eq!(result, Ok(42));

        let elapsed = start.elapsed();
        assert!(
            elapsed >= Duration::from_millis(EAGAIN_MAX_DELAY as u64),
            "Expected delay to exceed max backoff duration"
        );
    }

    #[test]
    fn test_retry_3() {
        let start = Instant::now();
        let result: Result<(), Errno> = retry_on_intr(|| Err(Errno::EINVAL));

        let elapsed = start.elapsed();
        assert!(
            elapsed < Duration::from_millis(10),
            "Expected immediate termination without delay"
        );
        assert_eq!(result, Err(Errno::EINVAL));
    }

    #[test]
    fn test_retry_4() {
        let result = retry_on_eintr(|| Ok(42));
        assert_eq!(result, Ok(42));
    }

    #[test]
    fn test_retry_5() {
        let mut attempts = 0;
        let result = retry_on_eintr(|| {
            attempts += 1;
            if attempts < 5 {
                Err(Errno::EINTR)
            } else {
                Ok(99)
            }
        });
        assert_eq!(result, Ok(99));
        assert_eq!(attempts, 5);
    }

    #[test]
    fn test_retry_6() {
        let mut attempts = 0;
        let result: Result<(), Errno> = retry_on_eintr(|| {
            attempts += 1;
            Err(Errno::EPERM)
        });
        assert_eq!(result, Err(Errno::EPERM));
        assert_eq!(attempts, 1);
    }

    #[test]
    fn test_retry_7() {
        let mut attempts = 0;
        let result: Result<(), Errno> = retry_on_eintr(|| {
            attempts += 1;
            if attempts < 3 {
                Err(Errno::EINTR)
            } else {
                Err(Errno::ENOENT)
            }
        });
        assert_eq!(result, Err(Errno::ENOENT));
        assert_eq!(attempts, 3);
    }

    #[test]
    fn test_retry_8() {
        let result = retry_on_eagain(|| Ok(42));
        assert_eq!(result, Ok(42));
    }

    #[test]
    fn test_retry_9() {
        let mut attempts = 0;
        let result = retry_on_eagain(move || {
            attempts += 1;
            if attempts < 3 {
                Err(Errno::EAGAIN)
            } else {
                Ok(77)
            }
        });
        assert_eq!(result, Ok(77));
    }

    #[test]
    fn test_retry_10() {
        let mut attempts = 0;
        let result: Result<(), Errno> = retry_on_eagain(|| {
            attempts += 1;
            Err(Errno::EACCES)
        });
        assert_eq!(result, Err(Errno::EACCES));
        assert_eq!(attempts, 1);
    }

    #[test]
    fn test_retry_11() {
        let result: Result<(), Errno> = retry_on_eagain(|| Err(Errno::EAGAIN));
        assert_eq!(result, Err(Errno::EAGAIN));
    }

    #[test]
    fn test_retry_12() {
        let result = retry_on_intr(|| Ok(42));
        assert_eq!(result, Ok(42));
    }

    #[test]
    fn test_retry_13() {
        let mut attempts = 0;
        let result = retry_on_intr(move || {
            attempts += 1;
            if attempts < 3 {
                Err(Errno::EINTR)
            } else {
                Ok(55)
            }
        });
        assert_eq!(result, Ok(55));
    }

    #[test]
    fn test_retry_14() {
        let result: Result<(), Errno> = retry_on_intr(|| Err(Errno::ENOENT));
        assert_eq!(result, Err(Errno::ENOENT));
    }
}