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 104 : pub fn get(tenants_dir: &Utf8Path, mocked: Option<&mock::Behavior>) -> nix::Result<Self> {
16 104 : if let Some(mocked) = mocked {
17 0 : Ok(Statvfs::Mock(mock::get(tenants_dir, mocked)?))
18 : } else {
19 104 : Ok(Statvfs::Real(nix::sys::statvfs::statvfs(
20 104 : tenants_dir.as_std_path(),
21 104 : )?))
22 : }
23 104 : }
24 :
25 : // NB: allow() because the block count type is u32 on macOS.
26 : #[allow(clippy::useless_conversion, clippy::unnecessary_fallible_conversions)]
27 104 : pub fn blocks(&self) -> u64 {
28 104 : match self {
29 104 : Statvfs::Real(stat) => u64::try_from(stat.blocks()).unwrap(),
30 0 : Statvfs::Mock(stat) => stat.blocks,
31 : }
32 104 : }
33 :
34 : // NB: allow() because the block count type is u32 on macOS.
35 : #[allow(clippy::useless_conversion, clippy::unnecessary_fallible_conversions)]
36 104 : pub fn blocks_available(&self) -> u64 {
37 104 : match self {
38 104 : Statvfs::Real(stat) => u64::try_from(stat.blocks_available()).unwrap(),
39 0 : Statvfs::Mock(stat) => stat.blocks_available,
40 : }
41 104 : }
42 :
43 208 : pub fn fragment_size(&self) -> u64 {
44 208 : match self {
45 208 : Statvfs::Real(stat) => stat.fragment_size(),
46 0 : Statvfs::Mock(stat) => stat.fragment_size,
47 : }
48 208 : }
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 104 : pub fn get_avail_total_bytes(&self) -> (u64, u64) {
59 : // https://unix.stackexchange.com/a/703650
60 104 : let blocksize = if self.fragment_size() > 0 {
61 104 : 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 104 : let avail_bytes = self.blocks_available() * blocksize;
68 104 : let total_bytes = self.blocks() * blocksize;
69 104 :
70 104 : (avail_bytes, total_bytes)
71 104 : }
72 : }
73 :
74 : pub mod mock {
75 : use camino::Utf8Path;
76 : pub use pageserver_api::config::statvfs::mock::Behavior;
77 : use regex::Regex;
78 : use tracing::log::info;
79 :
80 0 : pub fn get(tenants_dir: &Utf8Path, behavior: &Behavior) -> nix::Result<Statvfs> {
81 0 : info!("running mocked statvfs");
82 :
83 0 : match behavior {
84 : Behavior::Success {
85 0 : blocksize,
86 0 : total_blocks,
87 0 : name_filter,
88 0 : } => {
89 0 : let used_bytes = walk_dir_disk_usage(tenants_dir, name_filter.as_deref()).unwrap();
90 0 :
91 0 : // round it up to the nearest block multiple
92 0 : let used_blocks = used_bytes.div_ceil(*blocksize);
93 0 :
94 0 : if used_blocks > *total_blocks {
95 0 : panic!(
96 0 : "mocking error: used_blocks > total_blocks: {used_blocks} > {total_blocks}"
97 0 : );
98 0 : }
99 0 :
100 0 : let avail_blocks = total_blocks - used_blocks;
101 0 :
102 0 : Ok(Statvfs {
103 0 : blocks: *total_blocks,
104 0 : blocks_available: avail_blocks,
105 0 : fragment_size: *blocksize,
106 0 : block_size: *blocksize,
107 0 : })
108 : }
109 : #[cfg(feature = "testing")]
110 0 : Behavior::Failure { mocked_error } => Err((*mocked_error).into()),
111 : }
112 0 : }
113 :
114 0 : fn walk_dir_disk_usage(path: &Utf8Path, name_filter: Option<&Regex>) -> anyhow::Result<u64> {
115 0 : let mut total = 0;
116 0 : for entry in walkdir::WalkDir::new(path) {
117 0 : let entry = entry?;
118 0 : if !entry.file_type().is_file() {
119 0 : continue;
120 0 : }
121 0 : if !name_filter
122 0 : .as_ref()
123 0 : .map(|filter| filter.is_match(entry.file_name().to_str().unwrap()))
124 0 : .unwrap_or(true)
125 : {
126 0 : continue;
127 0 : }
128 0 : let m = match entry.metadata() {
129 0 : Ok(m) => m,
130 0 : Err(e) if is_not_found(&e) => {
131 0 : // some temp file which got removed right as we are walking
132 0 : continue;
133 : }
134 0 : Err(e) => {
135 0 : return Err(anyhow::Error::new(e)
136 0 : .context(format!("get metadata of {:?}", entry.path())));
137 : }
138 : };
139 0 : total += m.len();
140 : }
141 0 : Ok(total)
142 0 : }
143 :
144 0 : fn is_not_found(e: &walkdir::Error) -> bool {
145 0 : let Some(io_error) = e.io_error() else {
146 0 : return false;
147 : };
148 0 : let kind = io_error.kind();
149 0 : matches!(kind, std::io::ErrorKind::NotFound)
150 0 : }
151 :
152 : pub struct Statvfs {
153 : pub blocks: u64,
154 : pub blocks_available: u64,
155 : pub fragment_size: u64,
156 : pub block_size: u64,
157 : }
158 : }
|