Coverage Report

Created: 2025-05-08 18:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-util/src/fs.rs
Line
Count
Source
1
// Copyright 2024 The NativeLink Authors. All rights reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use core::pin::Pin;
16
use core::sync::atomic::{AtomicUsize, Ordering};
17
use core::task::{Context, Poll};
18
use std::fs::Metadata;
19
use std::io::{IoSlice, Seek};
20
use std::path::{Path, PathBuf};
21
22
use nativelink_error::{Code, Error, ResultExt, make_err};
23
use rlimit::increase_nofile_limit;
24
/// We wrap all `tokio::fs` items in our own wrapper so we can limit the number of outstanding
25
/// open files at any given time. This will greatly reduce the chance we'll hit open file limit
26
/// issues.
27
pub use tokio::fs::DirEntry;
28
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncWrite, ReadBuf, SeekFrom, Take};
29
use tokio::sync::{Semaphore, SemaphorePermit};
30
use tracing::{error, info, warn};
31
32
use crate::spawn_blocking;
33
34
/// Default read buffer size when reading to/from disk.
35
pub const DEFAULT_READ_BUFF_SIZE: usize = 0x4000;
36
37
#[derive(Debug)]
38
pub struct FileSlot {
39
    // We hold the permit because once it is dropped it goes back into the queue.
40
    _permit: SemaphorePermit<'static>,
41
    inner: tokio::fs::File,
42
}
43
44
impl AsRef<tokio::fs::File> for FileSlot {
45
112
    fn as_ref(&self) -> &tokio::fs::File {
46
112
        &self.inner
47
112
    }
48
}
49
50
impl AsMut<tokio::fs::File> for FileSlot {
51
0
    fn as_mut(&mut self) -> &mut tokio::fs::File {
52
0
        &mut self.inner
53
0
    }
54
}
55
56
impl AsyncRead for FileSlot {
57
2.55k
    fn poll_read(
58
2.55k
        mut self: Pin<&mut Self>,
59
2.55k
        cx: &mut Context<'_>,
60
2.55k
        buf: &mut ReadBuf<'_>,
61
2.55k
    ) -> Poll<Result<(), tokio::io::Error>> {
62
2.55k
        Pin::new(&mut self.inner).poll_read(cx, buf)
63
2.55k
    }
64
}
65
66
impl AsyncSeek for FileSlot {
67
23
    fn start_seek(mut self: Pin<&mut Self>, position: SeekFrom) -> Result<(), tokio::io::Error> {
68
23
        Pin::new(&mut self.inner).start_seek(position)
69
23
    }
70
71
69
    fn poll_complete(
72
69
        mut self: Pin<&mut Self>,
73
69
        cx: &mut Context<'_>,
74
69
    ) -> Poll<Result<u64, tokio::io::Error>> {
75
69
        Pin::new(&mut self.inner).poll_complete(cx)
76
69
    }
77
}
78
79
impl AsyncWrite for FileSlot {
80
2
    fn poll_write(
81
2
        mut self: Pin<&mut Self>,
82
2
        cx: &mut Context<'_>,
83
2
        buf: &[u8],
84
2
    ) -> Poll<Result<usize, tokio::io::Error>> {
85
2
        Pin::new(&mut self.inner).poll_write(cx, buf)
86
2
    }
87
88
0
    fn poll_flush(
89
0
        mut self: Pin<&mut Self>,
90
0
        cx: &mut Context<'_>,
91
0
    ) -> Poll<Result<(), tokio::io::Error>> {
92
0
        Pin::new(&mut self.inner).poll_flush(cx)
93
0
    }
94
95
0
    fn poll_shutdown(
96
0
        mut self: Pin<&mut Self>,
97
0
        cx: &mut Context<'_>,
98
0
    ) -> Poll<Result<(), tokio::io::Error>> {
99
0
        Pin::new(&mut self.inner).poll_shutdown(cx)
100
0
    }
101
102
75
    fn poll_write_vectored(
103
75
        mut self: Pin<&mut Self>,
104
75
        cx: &mut Context<'_>,
105
75
        bufs: &[IoSlice<'_>],
106
75
    ) -> Poll<Result<usize, tokio::io::Error>> {
107
75
        Pin::new(&mut self.inner).poll_write_vectored(cx, bufs)
108
75
    }
109
110
75
    fn is_write_vectored(&self) -> bool {
111
75
        self.inner.is_write_vectored()
112
75
    }
113
}
114
115
// Note: If the default changes make sure you update the documentation in
116
// `config/cas_server.rs`.
117
pub const DEFAULT_OPEN_FILE_LIMIT: usize = 24 * 1024; // 24k.
118
static OPEN_FILE_LIMIT: AtomicUsize = AtomicUsize::new(DEFAULT_OPEN_FILE_LIMIT);
119
pub static OPEN_FILE_SEMAPHORE: Semaphore = Semaphore::const_new(DEFAULT_OPEN_FILE_LIMIT);
120
121
/// Try to acquire a permit from the open file semaphore.
122
#[inline]
123
25.3k
pub async fn get_permit() -> Result<SemaphorePermit<'static>, Error> {
124
25.3k
    OPEN_FILE_SEMAPHORE
125
25.3k
        .acquire()
126
25.3k
        .await
127
25.3k
        .map_err(|e| 
make_err!(Code::Internal, "Open file semaphore closed {:?}", e)0
)
128
25.3k
}
129
/// Acquire a permit from the open file semaphore and call a raw function.
130
#[inline]
131
736
pub async fn call_with_permit<F, T>(f: F) -> Result<T, Error>
132
736
where
133
736
    F: FnOnce(SemaphorePermit<'static>) -> Result<T, Error> + Send + 'static,
134
736
    T: Send + 'static,
135
736
{
136
736
    let permit = get_permit().await
?0
;
137
736
    spawn_blocking!("fs_call_with_permit", move || f(permit))
138
736
        .await
139
734
        .unwrap_or_else(|e| 
Err(make_err!(Code::Internal, "background task failed: {e:?}"))0
)
140
734
}
141
142
/// Sets the soft nofile limit to `desired_open_file_limit` and adjusts
143
/// `OPEN_FILE_SEMAPHORE` accordingly.
144
///
145
/// # Panics
146
///
147
/// If any type conversion fails. This can't happen if `usize` is smaller than
148
/// `u64`.
149
0
pub fn set_open_file_limit(desired_open_file_limit: usize) {
150
0
    let new_open_file_limit = {
151
0
        match increase_nofile_limit(
152
0
            u64::try_from(desired_open_file_limit)
153
0
                .expect("desired_open_file_limit is too large to convert to u64."),
154
0
        ) {
155
0
            Ok(open_file_limit) => {
156
0
                info!("set_open_file_limit() assigns new open file limit {open_file_limit}.",);
157
0
                usize::try_from(open_file_limit)
158
0
                    .expect("open_file_limit is too large to convert to usize.")
159
            }
160
0
            Err(e) => {
161
0
                error!(
162
0
                    "set_open_file_limit() failed to assign open file limit. Maybe system does not have ulimits, continuing anyway. - {e:?}",
163
                );
164
0
                DEFAULT_OPEN_FILE_LIMIT
165
            }
166
        }
167
    };
168
    // TODO(jaroeichler): Can we give a better estimate?
169
0
    if new_open_file_limit < DEFAULT_OPEN_FILE_LIMIT {
  Branch (169:8): [True: 0, False: 0]
  Branch (169:8): [Folded - Ignored]
170
0
        warn!(
171
0
            "The new open file limit ({new_open_file_limit}) is below the recommended value of {DEFAULT_OPEN_FILE_LIMIT}. Consider raising max_open_files.",
172
        );
173
0
    }
174
175
    // Use only 80% of the open file limit for permits from OPEN_FILE_SEMAPHORE
176
    // to give extra room for other file descriptors like sockets, pipes, and
177
    // other things.
178
0
    let reduced_open_file_limit = new_open_file_limit.saturating_sub(new_open_file_limit / 5);
179
0
    let previous_open_file_limit = OPEN_FILE_LIMIT.load(Ordering::Acquire);
180
0
    // No permit should be aquired yet, so this warning should not occur.
181
0
    if (OPEN_FILE_SEMAPHORE.available_permits() + reduced_open_file_limit)
  Branch (181:8): [True: 0, False: 0]
  Branch (181:8): [Folded - Ignored]
182
0
        < previous_open_file_limit
183
    {
184
0
        warn!(
185
0
            "There are not enough available permits to remove {previous_open_file_limit} - {reduced_open_file_limit} permits.",
186
        );
187
0
    }
188
0
    if previous_open_file_limit <= reduced_open_file_limit {
  Branch (188:8): [True: 0, False: 0]
  Branch (188:8): [Folded - Ignored]
189
0
        OPEN_FILE_LIMIT.fetch_add(
190
0
            reduced_open_file_limit - previous_open_file_limit,
191
0
            Ordering::Release,
192
0
        );
193
0
        OPEN_FILE_SEMAPHORE.add_permits(reduced_open_file_limit - previous_open_file_limit);
194
0
    } else {
195
0
        OPEN_FILE_LIMIT.fetch_sub(
196
0
            previous_open_file_limit - reduced_open_file_limit,
197
0
            Ordering::Release,
198
0
        );
199
0
        OPEN_FILE_SEMAPHORE.forget_permits(previous_open_file_limit - reduced_open_file_limit);
200
0
    }
201
0
}
202
203
21
pub fn get_open_files_for_test() -> usize {
204
21
    OPEN_FILE_LIMIT.load(Ordering::Acquire) - OPEN_FILE_SEMAPHORE.available_permits()
205
21
}
206
207
60
pub async fn open_file(
208
60
    path: impl AsRef<Path>,
209
60
    start: u64,
210
60
    limit: u64,
211
60
) -> Result<Take<FileSlot>, Error> {
212
60
    let path = path.as_ref().to_owned();
213
60
    let (
permit, os_file58
) = call_with_permit(move |permit| {
214
58
        let mut os_file =
215
60
            std::fs::File::open(&path).err_tip(|| 
format!("Could not open {path:?}")2
)
?2
;
216
58
        if start > 0 {
  Branch (216:12): [True: 0, False: 0]
  Branch (216:12): [Folded - Ignored]
  Branch (216:12): [True: 0, False: 1]
  Branch (216:12): [True: 0, False: 2]
  Branch (216:12): [True: 0, False: 4]
  Branch (216:12): [True: 0, False: 6]
  Branch (216:12): [True: 0, False: 0]
  Branch (216:12): [Folded - Ignored]
  Branch (216:12): [True: 0, False: 0]
  Branch (216:12): [True: 0, False: 38]
  Branch (216:12): [True: 0, False: 3]
  Branch (216:12): [True: 0, False: 4]
217
0
            os_file
218
0
                .seek(SeekFrom::Start(start))
219
0
                .err_tip(|| format!("Could not seek to {start} in {path:?}"))?;
220
58
        }
221
58
        Ok((permit, os_file))
222
60
    })
223
60
    .await
?2
;
224
58
    Ok(FileSlot {
225
58
        _permit: permit,
226
58
        inner: tokio::fs::File::from_std(os_file),
227
58
    }
228
58
    .take(limit))
229
60
}
230
231
112
pub async fn create_file(path: impl AsRef<Path>) -> Result<FileSlot, Error> {
232
112
    let path = path.as_ref().to_owned();
233
112
    let (permit, os_file) = call_with_permit(move |permit| {
234
112
        Ok((
235
112
            permit,
236
112
            std::fs::File::options()
237
112
                .read(true)
238
112
                .write(true)
239
112
                .create(true)
240
112
                .truncate(true)
241
112
                .open(&path)
242
112
                .err_tip(|| 
format!("Could not open {path:?}")0
)
?0
,
243
        ))
244
112
    })
245
112
    .await
?0
;
246
112
    Ok(FileSlot {
247
112
        _permit: permit,
248
112
        inner: tokio::fs::File::from_std(os_file),
249
112
    })
250
112
}
251
252
4
pub async fn hard_link(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), Error> {
253
4
    let src = src.as_ref().to_owned();
254
4
    let dst = dst.as_ref().to_owned();
255
4
    call_with_permit(move |_| std::fs::hard_link(src, dst).map_err(Into::<Error>::into)).await
256
4
}
257
258
1
pub async fn set_permissions(
259
1
    src: impl AsRef<Path>,
260
1
    perm: std::fs::Permissions,
261
1
) -> Result<(), Error> {
262
1
    let src = src.as_ref().to_owned();
263
1
    call_with_permit(move |_| std::fs::set_permissions(src, perm).map_err(Into::<Error>::into))
264
1
        .await
265
1
}
266
267
38
pub async fn create_dir(path: impl AsRef<Path>) -> Result<(), Error> {
268
38
    let path = path.as_ref().to_owned();
269
38
    call_with_permit(move |_| std::fs::create_dir(path).map_err(Into::<Error>::into)).await
270
38
}
271
272
197
pub async fn create_dir_all(path: impl AsRef<Path>) -> Result<(), Error> {
273
197
    let path = path.as_ref().to_owned();
274
197
    call_with_permit(move |_| std::fs::create_dir_all(path).map_err(Into::<Error>::into)).await
275
197
}
276
277
#[cfg(target_family = "unix")]
278
1
pub async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), Error> {
279
1
    let src = src.as_ref().to_owned();
280
1
    let dst = dst.as_ref().to_owned();
281
1
    call_with_permit(move |_| {
282
1
        tokio::runtime::Handle::current()
283
1
            .block_on(tokio::fs::symlink(src, dst))
284
1
            .map_err(Into::<Error>::into)
285
1
    })
286
1
    .await
287
1
}
288
289
2
pub async fn read_link(path: impl AsRef<Path>) -> Result<PathBuf, Error> {
290
2
    let path = path.as_ref().to_owned();
291
2
    call_with_permit(move |_| std::fs::read_link(path).map_err(Into::<Error>::into)).await
292
2
}
293
294
#[derive(Debug)]
295
pub struct ReadDir {
296
    // We hold the permit because once it is dropped it goes back into the queue.
297
    permit: SemaphorePermit<'static>,
298
    inner: tokio::fs::ReadDir,
299
}
300
301
impl ReadDir {
302
220
    pub fn into_inner(self) -> (SemaphorePermit<'static>, tokio::fs::ReadDir) {
303
220
        (self.permit, self.inner)
304
220
    }
305
}
306
307
impl AsRef<tokio::fs::ReadDir> for ReadDir {
308
0
    fn as_ref(&self) -> &tokio::fs::ReadDir {
309
0
        &self.inner
310
0
    }
311
}
312
313
impl AsMut<tokio::fs::ReadDir> for ReadDir {
314
2
    fn as_mut(&mut self) -> &mut tokio::fs::ReadDir {
315
2
        &mut self.inner
316
2
    }
317
}
318
319
222
pub async fn read_dir(path: impl AsRef<Path>) -> Result<ReadDir, Error> {
320
222
    let path = path.as_ref().to_owned();
321
222
    let (permit, inner) = call_with_permit(move |permit| {
322
222
        Ok((
323
222
            permit,
324
222
            tokio::runtime::Handle::current()
325
222
                .block_on(tokio::fs::read_dir(path))
326
222
                .map_err(Into::<Error>::into)
?0
,
327
        ))
328
222
    })
329
222
    .await
?0
;
330
222
    Ok(ReadDir { permit, inner })
331
222
}
332
333
27
pub async fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<(), Error> {
334
27
    let from = from.as_ref().to_owned();
335
27
    let to = to.as_ref().to_owned();
336
27
    call_with_permit(move |_| std::fs::rename(from, to).map_err(Into::<Error>::into)).await
337
27
}
338
339
25
pub async fn remove_file(path: impl AsRef<Path>) -> Result<(), Error> {
340
25
    let path = path.as_ref().to_owned();
341
25
    call_with_permit(move |_| std::fs::remove_file(path).map_err(Into::<Error>::into)).await
342
23
}
343
344
2
pub async fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, Error> {
345
2
    let path = path.as_ref().to_owned();
346
2
    call_with_permit(move |_| std::fs::canonicalize(path).map_err(Into::<Error>::into)).await
347
2
}
348
349
16
pub async fn metadata(path: impl AsRef<Path>) -> Result<Metadata, Error> {
350
16
    let path = path.as_ref().to_owned();
351
16
    call_with_permit(move |_| std::fs::metadata(path).map_err(Into::<Error>::into)).await
352
16
}
353
354
4
pub async fn read(path: impl AsRef<Path>) -> Result<Vec<u8>, Error> {
355
4
    let path = path.as_ref().to_owned();
356
4
    call_with_permit(move |_| std::fs::read(path).map_err(Into::<Error>::into)).await
357
4
}
358
359
8
pub async fn symlink_metadata(path: impl AsRef<Path>) -> Result<Metadata, Error> {
360
8
    let path = path.as_ref().to_owned();
361
8
    call_with_permit(move |_| std::fs::symlink_metadata(path).map_err(Into::<Error>::into)).await
362
8
}
363
364
17
pub async fn remove_dir_all(path: impl AsRef<Path>) -> Result<(), Error> {
365
17
    let path = path.as_ref().to_owned();
366
17
    call_with_permit(move |_| std::fs::remove_dir_all(path).map_err(Into::<Error>::into)).await
367
17
}