LCOV - code coverage report
Current view: top level - pageserver/src - statvfs.rs (source / functions) Coverage Total Hit
Test: 7179b4db0d82ca8088cc95c44c4be4232078509c.info Lines: 27.7 % 94 26
Test Date: 2024-11-21 16:46:58 Functions: 50.0 % 10 5

            Line data    Source code
       1              : //! Wrapper around nix::sys::statvfs::Statvfs that allows for mocking.
       2              : 
       3              : use camino::Utf8Path;
       4              : 
       5              : pub enum Statvfs {
       6              :     Real(nix::sys::statvfs::Statvfs),
       7              :     Mock(mock::Statvfs),
       8              : }
       9              : 
      10              : // NB: on macOS, the block count type of struct statvfs is u32.
      11              : // The workaround seems to be to use the non-standard statfs64 call.
      12              : // Sincce it should only be a problem on > 2TiB disks, let's ignore
      13              : // the problem for now and upcast to u64.
      14              : impl Statvfs {
      15           40 :     pub fn get(tenants_dir: &Utf8Path, mocked: Option<&mock::Behavior>) -> nix::Result<Self> {
      16           40 :         if let Some(mocked) = mocked {
      17            0 :             Ok(Statvfs::Mock(mock::get(tenants_dir, mocked)?))
      18              :         } else {
      19           40 :             Ok(Statvfs::Real(nix::sys::statvfs::statvfs(
      20           40 :                 tenants_dir.as_std_path(),
      21           40 :             )?))
      22              :         }
      23           40 :     }
      24              : 
      25              :     // NB: allow() because the block count type is u32 on macOS.
      26              :     #[allow(clippy::useless_conversion, clippy::unnecessary_fallible_conversions)]
      27           40 :     pub fn blocks(&self) -> u64 {
      28           40 :         match self {
      29           40 :             Statvfs::Real(stat) => u64::try_from(stat.blocks()).unwrap(),
      30            0 :             Statvfs::Mock(stat) => stat.blocks,
      31              :         }
      32           40 :     }
      33              : 
      34              :     // NB: allow() because the block count type is u32 on macOS.
      35              :     #[allow(clippy::useless_conversion, clippy::unnecessary_fallible_conversions)]
      36           40 :     pub fn blocks_available(&self) -> u64 {
      37           40 :         match self {
      38           40 :             Statvfs::Real(stat) => u64::try_from(stat.blocks_available()).unwrap(),
      39            0 :             Statvfs::Mock(stat) => stat.blocks_available,
      40              :         }
      41           40 :     }
      42              : 
      43           80 :     pub fn fragment_size(&self) -> u64 {
      44           80 :         match self {
      45           80 :             Statvfs::Real(stat) => stat.fragment_size(),
      46            0 :             Statvfs::Mock(stat) => stat.fragment_size,
      47              :         }
      48           80 :     }
      49              : 
      50            0 :     pub fn block_size(&self) -> u64 {
      51            0 :         match self {
      52            0 :             Statvfs::Real(stat) => stat.block_size(),
      53            0 :             Statvfs::Mock(stat) => stat.block_size,
      54              :         }
      55            0 :     }
      56              : 
      57              :     /// Get the available and total bytes on the filesystem.
      58           40 :     pub fn get_avail_total_bytes(&self) -> (u64, u64) {
      59              :         // https://unix.stackexchange.com/a/703650
      60           40 :         let blocksize = if self.fragment_size() > 0 {
      61           40 :             self.fragment_size()
      62              :         } else {
      63            0 :             self.block_size()
      64              :         };
      65              : 
      66              :         // use blocks_available (b_avail) since, pageserver runs as unprivileged user
      67           40 :         let avail_bytes = self.blocks_available() * blocksize;
      68           40 :         let total_bytes = self.blocks() * blocksize;
      69           40 : 
      70           40 :         (avail_bytes, total_bytes)
      71           40 :     }
      72              : }
      73              : 
      74              : pub mod mock {
      75              :     use camino::Utf8Path;
      76              :     use regex::Regex;
      77              :     use tracing::log::info;
      78              : 
      79              :     pub use pageserver_api::config::statvfs::mock::Behavior;
      80              : 
      81            0 :     pub fn get(tenants_dir: &Utf8Path, behavior: &Behavior) -> nix::Result<Statvfs> {
      82            0 :         info!("running mocked statvfs");
      83              : 
      84            0 :         match behavior {
      85              :             Behavior::Success {
      86            0 :                 blocksize,
      87            0 :                 total_blocks,
      88            0 :                 ref name_filter,
      89            0 :             } => {
      90            0 :                 let used_bytes = walk_dir_disk_usage(tenants_dir, name_filter.as_deref()).unwrap();
      91            0 : 
      92            0 :                 // round it up to the nearest block multiple
      93            0 :                 let used_blocks = used_bytes.div_ceil(*blocksize);
      94            0 : 
      95            0 :                 if used_blocks > *total_blocks {
      96            0 :                     panic!(
      97            0 :                         "mocking error: used_blocks > total_blocks: {used_blocks} > {total_blocks}"
      98            0 :                     );
      99            0 :                 }
     100            0 : 
     101            0 :                 let avail_blocks = total_blocks - used_blocks;
     102            0 : 
     103            0 :                 Ok(Statvfs {
     104            0 :                     blocks: *total_blocks,
     105            0 :                     blocks_available: avail_blocks,
     106            0 :                     fragment_size: *blocksize,
     107            0 :                     block_size: *blocksize,
     108            0 :                 })
     109              :             }
     110              :             #[cfg(feature = "testing")]
     111            0 :             Behavior::Failure { mocked_error } => Err((*mocked_error).into()),
     112              :         }
     113            0 :     }
     114              : 
     115            0 :     fn walk_dir_disk_usage(path: &Utf8Path, name_filter: Option<&Regex>) -> anyhow::Result<u64> {
     116            0 :         let mut total = 0;
     117            0 :         for entry in walkdir::WalkDir::new(path) {
     118            0 :             let entry = entry?;
     119            0 :             if !entry.file_type().is_file() {
     120            0 :                 continue;
     121            0 :             }
     122            0 :             if !name_filter
     123            0 :                 .as_ref()
     124            0 :                 .map(|filter| filter.is_match(entry.file_name().to_str().unwrap()))
     125            0 :                 .unwrap_or(true)
     126              :             {
     127            0 :                 continue;
     128            0 :             }
     129            0 :             let m = match entry.metadata() {
     130            0 :                 Ok(m) => m,
     131            0 :                 Err(e) if is_not_found(&e) => {
     132            0 :                     // some temp file which got removed right as we are walking
     133            0 :                     continue;
     134              :                 }
     135            0 :                 Err(e) => {
     136            0 :                     return Err(anyhow::Error::new(e)
     137            0 :                         .context(format!("get metadata of {:?}", entry.path())))
     138              :                 }
     139              :             };
     140            0 :             total += m.len();
     141              :         }
     142            0 :         Ok(total)
     143            0 :     }
     144              : 
     145            0 :     fn is_not_found(e: &walkdir::Error) -> bool {
     146            0 :         let Some(io_error) = e.io_error() else {
     147            0 :             return false;
     148              :         };
     149            0 :         let kind = io_error.kind();
     150            0 :         matches!(kind, std::io::ErrorKind::NotFound)
     151            0 :     }
     152              : 
     153              :     pub struct Statvfs {
     154              :         pub blocks: u64,
     155              :         pub blocks_available: u64,
     156              :         pub fragment_size: u64,
     157              :         pub block_size: u64,
     158              :     }
     159              : }
        

Generated by: LCOV version 2.1-beta