Skip to content

Commit 251a1c4

Browse files
committed
fix: mount retry and logging
Signed-off-by: Kaita Nakamura <[email protected]>
1 parent 10b1c9b commit 251a1c4

2 files changed

Lines changed: 162 additions & 17 deletions

File tree

crates/libcontainer/src/rootfs/mount.rs

Lines changed: 134 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::fs::{canonicalize, create_dir_all, OpenOptions};
22
use std::mem;
33
use std::os::unix::io::AsRawFd;
44
use std::path::{Path, PathBuf};
5+
use std::time::Duration;
56
#[cfg(feature = "v1")]
67
use std::{borrow::Cow, collections::HashMap};
78

@@ -24,7 +25,16 @@ use super::symlink::SymlinkError;
2425
use super::utils::{parse_mount, MountOptionConfig};
2526
use crate::syscall::syscall::create_syscall;
2627
use crate::syscall::{linux, Syscall, SyscallError};
27-
use crate::utils::PathBufExt;
28+
use crate::utils::{retry, PathBufExt};
29+
30+
const MAX_EBUSY_MOUNT_ATTEMPTS: u32 = 3;
31+
// runc has a retry interval of 100ms. We are following this.
32+
// https://github.com/opencontainers/runc/blob/v1.3.0/libcontainer/rootfs_linux.go#L1235
33+
#[cfg(not(test))]
34+
const MOUNT_RETRY_DELAY_MS: u64 = 100;
35+
// In tests, there is no need to delay, so set it to 0ms.
36+
#[cfg(test)]
37+
const MOUNT_RETRY_DELAY_MS: u64 = 0;
2838

2939
#[derive(Debug, thiserror::Error)]
3040
pub enum MountError {
@@ -551,24 +561,35 @@ impl Mount {
551561
.mount(Some(&*src), dest, typ, mount_option_config.flags, Some(&*d))
552562
{
553563
if let SyscallError::Nix(errno) = err {
554-
if !matches!(errno, Errno::EINVAL) {
555-
tracing::error!("mount of {:?} failed. {}", m.destination(), errno);
564+
if matches!(errno, Errno::EINVAL) {
565+
self.syscall.mount(
566+
Some(&*src),
567+
dest,
568+
typ,
569+
mount_option_config.flags,
570+
Some(&mount_option_config.data),
571+
)?;
572+
} else if matches!(errno, Errno::EBUSY) {
573+
let mount_op = || -> std::result::Result<(), SyscallError> {
574+
self.syscall.mount(
575+
Some(&*src),
576+
dest,
577+
typ,
578+
mount_option_config.flags,
579+
Some(&*d),
580+
)
581+
};
582+
let delay = Duration::from_millis(MOUNT_RETRY_DELAY_MS);
583+
let retry_policy = |err: &SyscallError| -> bool {
584+
matches!(err, SyscallError::Nix(Errno::EBUSY))
585+
};
586+
retry(mount_op, MAX_EBUSY_MOUNT_ATTEMPTS - 1, delay, retry_policy)?;
587+
} else {
556588
return Err(err.into());
557589
}
590+
} else {
591+
return Err(err.into());
558592
}
559-
560-
self.syscall
561-
.mount(
562-
Some(&*src),
563-
dest,
564-
typ,
565-
mount_option_config.flags,
566-
Some(&mount_option_config.data),
567-
)
568-
.map_err(|err| {
569-
tracing::error!("failed to mount {src:?} to {dest:?}");
570-
err
571-
})?;
572593
}
573594

574595
if typ == Some("bind")
@@ -635,7 +656,7 @@ mod tests {
635656
use anyhow::{Context, Ok, Result};
636657

637658
use super::*;
638-
use crate::syscall::test::{MountArgs, TestHelperSyscall};
659+
use crate::syscall::test::{ArgName, MountArgs, TestHelperSyscall};
639660

640661
#[test]
641662
fn test_mount_into_container() -> Result<()> {
@@ -729,6 +750,102 @@ mod tests {
729750
assert_eq!(want, *got);
730751
assert_eq!(got.len(), 2);
731752
}
753+
{
754+
let m = Mount::new();
755+
let mount = &SpecMountBuilder::default()
756+
.destination(PathBuf::from("/tmp/retry"))
757+
.typ("tmpfs")
758+
.source(PathBuf::from("tmpfs"))
759+
.build()?;
760+
let mount_option_config = parse_mount(mount)?;
761+
762+
let syscall = m
763+
.syscall
764+
.as_any()
765+
.downcast_ref::<TestHelperSyscall>()
766+
.unwrap();
767+
syscall.set_ret_err(ArgName::Mount, || {
768+
Err(crate::syscall::SyscallError::Nix(nix::errno::Errno::EINVAL))
769+
});
770+
syscall.set_ret_err_times(ArgName::Mount, 1);
771+
772+
assert!(m
773+
.mount_into_container(mount, tmp_dir.path(), &mount_option_config, None)
774+
.is_ok());
775+
assert_eq!(syscall.get_mount_args().len(), 1);
776+
}
777+
{
778+
let m = Mount::new();
779+
let mount = &SpecMountBuilder::default()
780+
.destination(PathBuf::from("/tmp/retry"))
781+
.typ("tmpfs")
782+
.source(PathBuf::from("tmpfs"))
783+
.build()?;
784+
let mount_option_config = parse_mount(mount)?;
785+
786+
let syscall = m
787+
.syscall
788+
.as_any()
789+
.downcast_ref::<TestHelperSyscall>()
790+
.unwrap();
791+
syscall.set_ret_err(ArgName::Mount, || {
792+
Err(crate::syscall::SyscallError::Nix(nix::errno::Errno::EINVAL))
793+
});
794+
syscall.set_ret_err_times(ArgName::Mount, 2);
795+
796+
assert!(m
797+
.mount_into_container(mount, tmp_dir.path(), &mount_option_config, None)
798+
.is_err());
799+
assert_eq!(syscall.get_mount_args().len(), 0);
800+
}
801+
{
802+
let m = Mount::new();
803+
let mount = &SpecMountBuilder::default()
804+
.destination(PathBuf::from("/tmp/retry"))
805+
.typ("tmpfs")
806+
.source(PathBuf::from("tmpfs"))
807+
.build()?;
808+
let mount_option_config = parse_mount(mount)?;
809+
810+
let syscall = m
811+
.syscall
812+
.as_any()
813+
.downcast_ref::<TestHelperSyscall>()
814+
.unwrap();
815+
syscall.set_ret_err(ArgName::Mount, || {
816+
Err(crate::syscall::SyscallError::Nix(nix::errno::Errno::EBUSY))
817+
});
818+
syscall.set_ret_err_times(ArgName::Mount, MAX_EBUSY_MOUNT_ATTEMPTS as usize - 1);
819+
820+
assert!(m
821+
.mount_into_container(mount, tmp_dir.path(), &mount_option_config, None)
822+
.is_ok());
823+
assert_eq!(syscall.get_mount_args().len(), 1);
824+
}
825+
{
826+
let m = Mount::new();
827+
let mount = &SpecMountBuilder::default()
828+
.destination(PathBuf::from("/tmp/retry"))
829+
.typ("tmpfs")
830+
.source(PathBuf::from("tmpfs"))
831+
.build()?;
832+
let mount_option_config = parse_mount(mount)?;
833+
834+
let syscall = m
835+
.syscall
836+
.as_any()
837+
.downcast_ref::<TestHelperSyscall>()
838+
.unwrap();
839+
syscall.set_ret_err(ArgName::Mount, || {
840+
Err(crate::syscall::SyscallError::Nix(nix::errno::Errno::EBUSY))
841+
});
842+
syscall.set_ret_err_times(ArgName::Mount, MAX_EBUSY_MOUNT_ATTEMPTS as usize);
843+
844+
assert!(m
845+
.mount_into_container(mount, tmp_dir.path(), &mount_option_config, None)
846+
.is_err());
847+
assert_eq!(syscall.get_mount_args().len(), 0);
848+
}
732849

733850
Ok(())
734851
}

crates/libcontainer/src/utils.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::fs::{self, DirBuilder, File};
55
use std::os::linux::fs::MetadataExt;
66
use std::os::unix::fs::DirBuilderExt;
77
use std::path::{Component, Path, PathBuf};
8+
use std::time::Duration;
89

910
use nix::sys::stat::Mode;
1011
use nix::sys::statfs;
@@ -282,6 +283,33 @@ pub fn validate_spec_for_new_user_ns(spec: &Spec) -> Result<(), LibcontainerErro
282283
Ok(())
283284
}
284285

286+
// Generic retry function with delay and policy.
287+
// Retries the operation `op` up to `attempts` times if it fails.
288+
// Waits for `delay` duration between retries.
289+
// Only retries if the error satisfies the `policy` function.
290+
pub fn retry<F, T, E, P>(mut op: F, attempts: u32, delay: Duration, policy: P) -> Result<T, E>
291+
where
292+
F: FnMut() -> Result<T, E>,
293+
P: Fn(&E) -> bool,
294+
{
295+
if attempts == 0 {
296+
panic!("retry called with 0 attempts. Minimum attempts is 1.");
297+
}
298+
for attempt in 0..attempts {
299+
match op() {
300+
Ok(res) => return Ok(res),
301+
Err(err) => {
302+
if attempt + 1 < attempts && policy(&err) {
303+
std::thread::sleep(delay);
304+
} else {
305+
return Err(err);
306+
}
307+
}
308+
}
309+
}
310+
unreachable!("retry loop completed without returning a result.");
311+
}
312+
285313
#[cfg(test)]
286314
mod tests {
287315
use anyhow::{bail, Result};

0 commit comments

Comments
 (0)