Coverage Report

Created: 2026-10-29 17:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-store/src/filesystem_store.rs
Line
Count
Source
1
// Copyright 2024 The NativeLink Authors. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (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
//    See LICENSE file for details
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::cmp;
16
use core::fmt::{Debug, Display, Formatter};
17
use core::pin::Pin;
18
use core::sync::atomic::{AtomicU64, Ordering};
19
use core::time::Duration;
20
use std::borrow::Cow;
21
#[cfg(unix)]
22
use std::collections::HashMap;
23
use std::ffi::{OsStr, OsString};
24
use std::sync::{Arc, Weak};
25
use std::time::SystemTime;
26
27
#[cfg(unix)]
28
use async_lock::Mutex;
29
use async_lock::RwLock;
30
use async_trait::async_trait;
31
use bytes::{Bytes, BytesMut};
32
use futures::stream::{StreamExt, TryStreamExt};
33
use futures::{Future, TryFutureExt};
34
use nativelink_config::stores::FilesystemSpec;
35
use nativelink_error::{Code, Error, ResultExt, make_err};
36
use nativelink_metric::MetricsComponent;
37
use nativelink_util::background_spawn;
38
use nativelink_util::buf_channel::{
39
    DropCloserReadHalf, DropCloserWriteHalf, make_buf_channel_pair,
40
};
41
use nativelink_util::common::{DigestInfo, fs};
42
use nativelink_util::evicting_map::{EvictingMap, EvictionSnapshot, LenEntry};
43
use nativelink_util::fs::FileSlot;
44
use nativelink_util::health_utils::{HealthRegistryBuilder, HealthStatus, HealthStatusIndicator};
45
#[cfg(unix)]
46
use nativelink_util::spawn_blocking;
47
#[cfg(unix)]
48
use nativelink_util::store_trait::RemoveItemCallback;
49
use nativelink_util::store_trait::{
50
    RemoveCallback, StoreDriver, StoreKey, StoreKeyBorrow, StoreOptimizations, UploadSizeInfo,
51
};
52
use tokio::io::{AsyncReadExt, AsyncWriteExt, Take};
53
use tokio::sync::Semaphore;
54
use tokio::time::timeout;
55
use tokio_stream::wrappers::ReadDirStream;
56
use tracing::{debug, error, info, trace, warn};
57
58
use crate::callback_utils::RemoveCallbackHolder;
59
use crate::cas_utils::is_zero_digest;
60
61
// Default size to allocate memory of the buffer when reading files.
62
const DEFAULT_BUFF_SIZE: usize = 32 * 1024;
63
// Default block size of all major filesystems is 4KB
64
const DEFAULT_BLOCK_SIZE: u64 = 4 * 1024;
65
66
pub const STR_FOLDER_V1: &str = "s";
67
pub const DIGEST_FOLDER_V1: &str = "d";
68
69
pub const STR_FOLDER_V2: &str = "s2";
70
pub const DIGEST_FOLDER_V2: &str = "d2";
71
72
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73
enum Version {
74
    Flat,
75
    V1,
76
    V2,
77
}
78
79
/// Suffix for the sibling directory that holds per-digest read-only
80
/// **executable** (0o555) variants of CAS blobs (see
81
/// [`FilesystemStore::get_executable_hardlink_source`]). It is a sibling of
82
/// `content_path` rather than a child so the normal content/temp scan and prune
83
/// logic never touches it. Cleared on writable startup; entries are
84
/// regenerable. If the wipe is blocked by a read-only filesystem, executable
85
/// variants are disabled so surviving files are never trusted.
86
#[cfg(unix)]
87
const EXECUTABLE_DIR_SUFFIX: &str = ".exec";
88
89
const MAX_CONCURRENT_VARIANT_LOOKUPS: usize = 32;
90
91
#[derive(Clone, Copy, Debug)]
92
pub enum FileType {
93
    Digest,
94
    String,
95
}
96
97
#[derive(Debug, MetricsComponent)]
98
pub struct SharedContext {
99
    // Used in testing to know how many active drop() spawns are running.
100
    // TODO(palfrey) It is probably a good idea to use a spin lock during
101
    // destruction of the store to ensure that all files are actually
102
    // deleted (similar to how it is done in tests).
103
    #[metric(help = "Number of active drop spawns")]
104
    pub active_drop_spawns: AtomicU64,
105
    #[metric(help = "Path to the configured temp path")]
106
    temp_path: String,
107
    #[metric(help = "Path to the configured content path")]
108
    content_path: String,
109
}
110
111
#[derive(Eq, PartialEq, Debug)]
112
enum PathType {
113
    Content,
114
    Temp,
115
    Custom(OsString),
116
}
117
118
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
119
pub struct Generation(u64);
120
121
impl Generation {
122
4.29k
    pub const fn new(generation: u64) -> Self {
123
4.29k
        Self(generation)
124
4.29k
    }
125
126
1.49k
    pub const fn inner(&self) -> u64 {
127
1.49k
        self.0
128
1.49k
    }
129
}
130
131
impl Display for Generation {
132
10.9k
    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
133
10.9k
        write!(f, "{}", self.0)
134
10.9k
    }
135
}
136
137
/// [`EncodedFilePath`] stores the path to the file
138
/// including the context, path type and key to the file.
139
/// The whole [`StoreKey`] is stored as opposed to solely
140
/// the [`DigestInfo`] so that it is more usable for things
141
/// such as BEP -see Issue #1108
142
#[derive(Debug)]
143
pub struct EncodedFilePath {
144
    shared_context: Arc<SharedContext>,
145
    path_type: PathType,
146
    key: StoreKey<'static>,
147
    generation: Generation,
148
    version: Version,
149
}
150
151
impl EncodedFilePath {
152
    #[inline]
153
9.10k
    fn get_file_path(&self) -> Cow<'_, OsStr> {
154
9.10k
        get_file_path_raw(
155
9.10k
            &self.path_type,
156
9.10k
            self.shared_context.as_ref(),
157
9.10k
            &self.key,
158
9.10k
            self.generation,
159
9.10k
            self.version,
160
        )
161
9.10k
    }
162
}
163
164
#[inline]
165
11.4k
fn get_file_path_raw<'a>(
166
11.4k
    path_type: &'a PathType,
167
11.4k
    shared_context: &SharedContext,
168
11.4k
    key: &StoreKey<'a>,
169
11.4k
    generation: Generation,
170
11.4k
    version: Version,
171
11.4k
) -> Cow<'a, OsStr> {
172
11.4k
    let 
folder10.8k
= match path_type {
173
4.61k
        PathType::Content => &shared_context.content_path,
174
6.27k
        PathType::Temp => &shared_context.temp_path,
175
550
        PathType::Custom(path) => return Cow::Borrowed(path),
176
    };
177
10.8k
    Cow::Owned(to_full_path_from_key(folder, key, generation, version))
178
11.4k
}
179
180
impl Drop for EncodedFilePath {
181
2.58k
    fn drop(&mut self) {
182
        // `drop()` can be called during shutdown, so we use `path_type` flag to know if the
183
        // file actually needs to be deleted.
184
2.58k
        if self.path_type == PathType::Content {
185
2.51k
            return;
186
74
        }
187
188
74
        let file_path = self.get_file_path().to_os_string();
189
74
        let shared_context = self.shared_context.clone();
190
        // .fetch_add returns previous value, so we add one to get approximate current value
191
74
        let current_active_drop_spawns = shared_context
192
74
            .active_drop_spawns
193
74
            .fetch_add(1, Ordering::Relaxed)
194
74
            + 1;
195
74
        debug!(
196
            %current_active_drop_spawns,
197
            ?file_path,
198
            "Spawned a filesystem_delete_file"
199
        );
200
74
        background_spawn!("filesystem_delete_file", async move 
{66
201
66
            match fs::remove_file(&file_path).await {
202
48
                Ok(()) => debug!(?file_path, "File deleted"),
203
                // The file already being gone is the desired end state of a
204
                // delete, not a failure — e.g. an entry marked Temp after an
205
                // already-gone unref points at a path that was never created.
206
6
                Err(err) if err.code == Code::NotFound => {
207
6
                    debug!(?file_path, "File already gone, nothing to delete");
208
                }
209
0
                Err(err) => error!(?file_path, ?err, "Failed to delete file"),
210
            }
211
            // .fetch_sub returns previous value, so we subtract one to get approximate current value
212
54
            let current_active_drop_spawns = shared_context
213
54
                .active_drop_spawns
214
54
                .fetch_sub(1, Ordering::Relaxed)
215
54
                - 1;
216
54
            debug!(
217
                ?current_active_drop_spawns,
218
                ?file_path,
219
                "Dropped a filesystem_delete_file"
220
            );
221
54
        });
222
2.58k
    }
223
}
224
225
/// This creates the file path from the [`StoreKey`]. If
226
/// it is a string, the string, prefixed with [`STR_PREFIX`]
227
/// for backwards compatibility, is stored.
228
///
229
/// If it is a [`DigestInfo`], it is prefixed by [`DIGEST_PREFIX`]
230
/// followed by the string representation of a digest - the hash in hex,
231
/// a hyphen then the size in bytes
232
///
233
/// Previously, only the string representation of the [`DigestInfo`] was
234
/// used with no prefix
235
#[inline]
236
10.9k
fn to_full_path_from_key(
237
10.9k
    folder: &str,
238
10.9k
    key: &StoreKey<'_>,
239
10.9k
    generation: Generation,
240
10.9k
    version: Version,
241
10.9k
) -> OsString {
242
10.9k
    match (key, version) {
243
1
        (StoreKey::Digest(digest_info), Version::Flat) => format!("{folder}/{digest_info}"),
244
0
        (StoreKey::Str(str), Version::Flat) => format!("{folder}/{str}"),
245
73
        (StoreKey::Str(str), Version::V2) => {
246
73
            format!("{folder}/{STR_FOLDER_V2}/{str}-{generation}")
247
        }
248
3
        (StoreKey::Str(str), Version::V1) => format!("{folder}/{STR_FOLDER_V1}/{str}"),
249
10.8k
        (StoreKey::Digest(digest_info), Version::V2) => {
250
10.8k
            format!("{folder}/{DIGEST_FOLDER_V2}/{digest_info}-{generation}")
251
        }
252
0
        (StoreKey::Digest(digest_info), Version::V1) => {
253
0
            format!("{folder}/{DIGEST_FOLDER_V1}/{digest_info}")
254
        }
255
    }
256
10.9k
    .into()
257
10.9k
}
258
259
pub trait FileEntry: LenEntry + Send + Sync + Debug + 'static {
260
    /// Responsible for creating the underlying `FileEntry`.
261
    fn create(
262
        data_size: u64,
263
        block_size: u64,
264
        generation: Generation,
265
        encoded_file_path: RwLock<EncodedFilePath>,
266
    ) -> Self;
267
268
    /// Creates a (usually) temp file, opens it and returns the path to the temp file.
269
    fn make_and_open_file(
270
        block_size: u64,
271
        generation: Generation,
272
        encoded_file_path: EncodedFilePath,
273
    ) -> impl Future<Output = Result<(Self, FileSlot, OsString), Error>> + Send
274
    where
275
        Self: Sized;
276
277
    /// Returns the file generation.
278
    fn generation(&self) -> Generation;
279
280
    /// Returns the underlying size of the data in bytes
281
    fn data_size(&self) -> u64;
282
283
    /// Returns the underlying reference to the size of the data in bytes
284
    fn data_size_mut(&mut self) -> &mut u64;
285
286
    /// Returns the actual size of the underlying file on the disk after accounting for filesystem block size.
287
    fn size_on_disk(&self) -> u64;
288
289
    /// Gets the underlying `EncodedfilePath`.
290
    fn get_encoded_file_path(&self) -> &RwLock<EncodedFilePath>;
291
292
    /// Returns a reader that will read part of the underlying file.
293
    fn read_file_part(
294
        &self,
295
        offset: u64,
296
        length: u64,
297
    ) -> impl Future<Output = Result<Take<FileSlot>, Error>> + Send;
298
299
    /// This function is a safe way to extract the file name of the underlying file. To protect users from
300
    /// accidentally creating undefined behavior we encourage users to do the logic they need to do with
301
    /// the filename inside this function instead of extracting the filename and doing the logic outside.
302
    /// This is because the filename is not guaranteed to exist after this function returns, however inside
303
    /// the callback the file is always guaranteed to exist and immutable.
304
    /// DO NOT USE THIS FUNCTION TO EXTRACT THE FILENAME AND STORE IT FOR LATER USE.
305
    fn get_file_path_locked<
306
        T,
307
        Fut: Future<Output = Result<T, Error>> + Send,
308
        F: FnOnce(OsString) -> Fut + Send,
309
    >(
310
        &self,
311
        handler: F,
312
    ) -> impl Future<Output = Result<T, Error>> + Send;
313
}
314
315
pub struct FileEntryImpl {
316
    data_size: u64,
317
    block_size: u64,
318
    // We lock around this as it gets rewritten when we move between temp and content types
319
    encoded_file_path: RwLock<EncodedFilePath>,
320
    generation: Generation,
321
}
322
323
impl FileEntryImpl {
324
0
    pub fn get_shared_context_for_test(&mut self) -> Arc<SharedContext> {
325
0
        self.encoded_file_path.get_mut().shared_context.clone()
326
0
    }
327
}
328
329
impl FileEntry for FileEntryImpl {
330
2.58k
    fn create(
331
2.58k
        data_size: u64,
332
2.58k
        block_size: u64,
333
2.58k
        generation: Generation,
334
2.58k
        encoded_file_path: RwLock<EncodedFilePath>,
335
2.58k
    ) -> Self {
336
2.58k
        Self {
337
2.58k
            data_size,
338
2.58k
            block_size,
339
2.58k
            encoded_file_path,
340
2.58k
            generation,
341
2.58k
        }
342
2.58k
    }
343
344
    /// This encapsulates the logic for the edge case of if the file fails to create
345
    /// the cleanup of the file is handled without creating a `FileEntry`, which would
346
    /// try to cleanup the file as well during `drop()`.
347
2.07k
    async fn make_and_open_file(
348
2.07k
        block_size: u64,
349
2.07k
        generation: Generation,
350
2.07k
        encoded_file_path: EncodedFilePath,
351
2.07k
    ) -> Result<(Self, FileSlot, OsString), Error> {
352
2.07k
        let temp_full_path = encoded_file_path.get_file_path().to_os_string();
353
2.07k
        let 
temp_file_result2.07k
= fs::create_file(temp_full_path.clone())
354
2.07k
            .or_else(|mut err| async 
{0
355
0
                let remove_result = fs::remove_file(&temp_full_path).await.err_tip(|| {
356
0
                    format!(
357
                        "Failed to remove file {} in filesystem store",
358
0
                        temp_full_path.display()
359
                    )
360
0
                });
361
0
                if let Err(remove_err) = remove_result {
362
0
                    err = err.merge(remove_err);
363
0
                }
364
0
                warn!(?err, ?block_size, ?temp_full_path, "Failed to create file",);
365
0
                Err(err).err_tip(|| {
366
0
                    format!(
367
                        "Failed to create {} in filesystem store",
368
0
                        temp_full_path.display()
369
                    )
370
0
                })
371
0
            })
372
2.07k
            .await
?0
;
373
374
2.07k
        Ok((
375
2.07k
            <Self as FileEntry>::create(
376
2.07k
                0, /* Unknown yet, we will fill it in later */
377
2.07k
                block_size,
378
2.07k
                generation,
379
2.07k
                RwLock::new(encoded_file_path),
380
2.07k
            ),
381
2.07k
            temp_file_result,
382
2.07k
            temp_full_path,
383
2.07k
        ))
384
2.07k
    }
385
386
94
    fn data_size(&self) -> u64 {
387
94
        self.data_size
388
94
    }
389
390
26
    fn generation(&self) -> Generation {
391
26
        self.generation
392
26
    }
393
394
2.06k
    fn data_size_mut(&mut self) -> &mut u64 {
395
2.06k
        &mut self.data_size
396
2.06k
    }
397
398
5.34k
    fn size_on_disk(&self) -> u64 {
399
5.34k
        self.data_size.div_ceil(self.block_size) * self.block_size
400
5.34k
    }
401
402
6.90k
    fn get_encoded_file_path(&self) -> &RwLock<EncodedFilePath> {
403
6.90k
        &self.encoded_file_path
404
6.90k
    }
405
406
799
    fn read_file_part(
407
799
        &self,
408
799
        offset: u64,
409
799
        length: u64,
410
799
    ) -> impl Future<Output = Result<Take<FileSlot>, Error>> + Send {
411
799
        self.get_file_path_locked(move |full_content_path| async move {
412
799
            let 
file794
= fs::open_file(&full_content_path, offset, length)
413
799
                .await
414
799
                .err_tip(|| 
{5
415
5
                    format!(
416
                        "Failed to open file in filesystem store {}",
417
5
                        full_content_path.display()
418
                    )
419
5
                })?;
420
794
            Ok(file)
421
1.59k
        })
422
799
    }
423
424
2.20k
    async fn get_file_path_locked<
425
2.20k
        T,
426
2.20k
        Fut: Future<Output = Result<T, Error>> + Send,
427
2.20k
        F: FnOnce(OsString) -> Fut + Send,
428
2.20k
    >(
429
2.20k
        &self,
430
2.20k
        handler: F,
431
2.20k
    ) -> Result<T, Error> {
432
2.20k
        let encoded_file_path = self.get_encoded_file_path().read().await;
433
2.20k
        handler(encoded_file_path.get_file_path().to_os_string()).await
434
2.20k
    }
435
}
436
437
impl Debug for FileEntryImpl {
438
0
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), core::fmt::Error> {
439
0
        f.debug_struct("FileEntryImpl")
440
0
            .field("data_size", &self.data_size)
441
0
            .field("encoded_file_path", &"<behind mutex>")
442
0
            .finish()
443
0
    }
444
}
445
446
2.15k
fn make_temp_digest(mut digest: DigestInfo) -> DigestInfo {
447
    static DELETE_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
448
2.15k
    let mut hash = *digest.packed_hash();
449
2.15k
    hash[24..].clone_from_slice(
450
2.15k
        &DELETE_FILE_COUNTER
451
2.15k
            .fetch_add(1, Ordering::Relaxed)
452
2.15k
            .to_le_bytes(),
453
    );
454
2.15k
    digest.set_packed_hash(*hash);
455
2.15k
    digest
456
2.15k
}
457
458
2.15k
pub fn make_temp_key(key: &StoreKey) -> StoreKey<'static> {
459
2.15k
    StoreKey::Digest(make_temp_digest(key.borrow().into_digest()))
460
2.15k
}
461
462
/// Group-commit flush coalescer (macOS only).
463
///
464
/// On macOS, `File::sync_all` issues `fcntl(F_FULLFSYNC)` — a full
465
/// device-cache flush — once per call. The flush is serialized at the
466
/// device, costs multiple milliseconds, and dominates uploads of many
467
/// small blobs (a 4,400-tiny-file action input tree spends ~12s of its
468
/// ~14.5s materialization in these flushes). Linux does not have this
469
/// problem because concurrent `fsync` calls coalesce inside the
470
/// filesystem journal's group commit.
471
///
472
/// This applies the same group-commit idea in userspace, exploiting an
473
/// asymmetry in `F_FULLFSYNC`: writing a file's pages to the storage
474
/// device is per-file work (plain `fsync(2)`, cheap), while the expensive
475
/// device-cache drain is device-wide by nature. Each writer first pushes
476
/// its own data to the device with `fsync(2)`, then joins the current
477
/// commit round; a single long-lived flusher task per DEVICE issues one
478
/// `F_FULLFSYNC` (on a dedicated sentinel file) per round, covering every
479
/// previously-`fsync`ed blob at once. Rounds self-batch exactly like a
480
/// journal: while one flush is running, later writers accumulate into
481
/// the next round.
482
///
483
/// The coalescer (and its flusher task) is per store instance. Sharing
484
/// one coalescer across all stores on a device would amortize further —
485
/// the flush is device-wide — but a process-global flusher task cannot
486
/// safely outlive the tokio runtime that spawned it (multiple runtimes
487
/// coexist in one process, e.g. one per test); doing this correctly
488
/// needs a runtime-agnostic flusher (a dedicated OS thread) and is left
489
/// as a follow-up.
490
///
491
/// Durability is identical to per-file `F_FULLFSYNC`: a writer only
492
/// proceeds (and only renames its blob into the content directory) after
493
/// a device-cache flush that started after its own `fsync(2)` completed.
494
#[cfg(target_os = "macos")]
495
#[derive(Debug)]
496
struct FlushCoalescer {
497
    /// The round currently accepting waiters. Swapped out by the flusher
498
    /// right before it flushes, so writers whose `fsync(2)` finished
499
    /// after the flush began land in the next round.
500
    current_round: parking_lot::Mutex<Arc<FlushRound>>,
501
    /// Wakes the flusher task. Writers notify after subscribing to their
502
    /// round, so a wakeup can never be observed before its waiter.
503
    wake: Arc<tokio::sync::Notify>,
504
    /// Dedicated file the `F_FULLFSYNC` is issued on. A SIBLING of the
505
    /// store's content path (like the `.exec` variant directory), so
506
    /// neither the startup scan nor `move_old_cache`'s legacy root sweep
507
    /// ever sees it.
508
    sentinel: Arc<std::fs::File>,
509
    /// Number of device-wide barriers issued. Kept separately from the
510
    /// per-file `fsync(2)` count so tests can pin the coalescing invariant.
511
    full_flush_count: Arc<AtomicU64>,
512
}
513
514
#[cfg(target_os = "macos")]
515
#[derive(Debug)]
516
struct FlushRound {
517
    /// Broadcasts the flush outcome to every writer in the round.
518
    result_tx: tokio::sync::watch::Sender<Option<Result<(), Error>>>,
519
}
520
521
#[cfg(target_os = "macos")]
522
impl FlushRound {
523
    fn new() -> Arc<Self> {
524
        let (result_tx, _) = tokio::sync::watch::channel(None);
525
        Arc::new(Self { result_tx })
526
    }
527
}
528
529
/// Guarantees a [`FlushRound`]'s waiters always receive a result: if the
530
/// flusher task is cancelled at an await point (runtime shutdown), waiters
531
/// must get an error rather than pend forever — their own `Arc<FlushRound>`
532
/// keeps the channel alive, so a dropped-sender wakeup can never happen.
533
#[cfg(target_os = "macos")]
534
struct SendOnDrop(Option<Arc<FlushRound>>);
535
536
#[cfg(target_os = "macos")]
537
impl SendOnDrop {
538
    fn finish(mut self, result: Result<(), Error>) {
539
        if let Some(round) = self.0.take() {
540
            // Ignore send errors: every waiter may have been cancelled.
541
            drop(round.result_tx.send(Some(result)));
542
        }
543
    }
544
}
545
546
#[cfg(target_os = "macos")]
547
impl Drop for SendOnDrop {
548
    fn drop(&mut self) {
549
        if let Some(round) = self.0.take() {
550
            drop(round.result_tx.send(Some(Err(make_err!(
551
                Code::Internal,
552
                "Flush coalescer round task cancelled before completing"
553
            )))));
554
        }
555
    }
556
}
557
558
#[cfg(target_os = "macos")]
559
impl Drop for FlushCoalescer {
560
    fn drop(&mut self) {
561
        // Wake the flusher so it observes the dead Weak and exits instead
562
        // of parking forever.
563
        self.wake.notify_one();
564
    }
565
}
566
567
#[cfg(target_os = "macos")]
568
impl FlushCoalescer {
569
    /// Creates the coalescer for a store and spawns its flusher task on
570
    /// the current runtime (the same runtime the store's uploads run on).
571
    async fn for_content_path(content_path: &str) -> Result<Arc<Self>, Error> {
572
        // Sibling of `content_path` (never inside it): the startup scan
573
        // and `move_old_cache` sweep everything under the content root.
574
        let sentinel_path = format!("{content_path}.flush_sentinel");
575
        let sentinel = spawn_blocking!("filesystem_store_flush_sentinel_open", move || {
576
            std::fs::OpenOptions::new()
577
                .write(true)
578
                .create(true)
579
                .truncate(false)
580
                .open(&sentinel_path)
581
                .map_err(|e| {
582
                    make_err!(
583
                        Code::Internal,
584
                        "Failed to create flush sentinel {sentinel_path}: {e:?}"
585
                    )
586
                })
587
        })
588
        .await
589
        .err_tip(|| "Failed to join flush sentinel open task")??;
590
591
        let coalescer = Arc::new(Self {
592
            current_round: parking_lot::Mutex::new(FlushRound::new()),
593
            wake: Arc::new(tokio::sync::Notify::new()),
594
            sentinel: Arc::new(sentinel),
595
            full_flush_count: Arc::new(AtomicU64::new(0)),
596
        });
597
598
        let weak = Arc::downgrade(&coalescer);
599
        let wake = coalescer.wake.clone();
600
        background_spawn!("filesystem_store_flush_coalescer", async move {
601
            loop {
602
                wake.notified().await;
603
                let Some(coalescer) = weak.upgrade() else {
604
                    return;
605
                };
606
                coalescer.flush_one_round().await;
607
                // Drop the strong ref before parking so the store can be
608
                // torn down while the flusher is idle.
609
            }
610
        });
611
        Ok(coalescer)
612
    }
613
614
    /// Waits until a device-cache flush that started after this call has
615
    /// completed. Callers must have already `fsync(2)`ed their own file.
616
    async fn commit(&self) -> Result<(), Error> {
617
        let round = self.current_round.lock().clone();
618
        let mut result_rx = round.result_tx.subscribe();
619
        // Notify strictly after subscribing: the flusher skips rounds
620
        // with no receivers, so this ordering makes lost wakeups
621
        // impossible (a permit is stored even while the flusher is busy).
622
        self.wake.notify_one();
623
        let result_ref = result_rx
624
            .wait_for(Option::is_some)
625
            .await
626
            .map_err(|_| make_err!(Code::Internal, "Flush coalescer round dropped"))?;
627
        result_ref
628
            .clone()
629
            .unwrap_or_else(|| Err(make_err!(Code::Internal, "Flush round result missing")))
630
    }
631
632
    async fn flush_one_round(&self) {
633
        let round = self.current_round.lock().clone();
634
        if round.result_tx.receiver_count() == 0 {
635
            // Spurious wakeup (e.g. a permit left over from a round that
636
            // a previous iteration already served).
637
            return;
638
        }
639
        let send_guard = SendOnDrop(Some(round.clone()));
640
641
        // Brief accumulation window, only when this round actually has
642
        // multiple waiters: lets a burst pack more writers into the round
643
        // before it closes, trading ~3ms of publish latency (about the
644
        // cost of one device flush) for fewer device flushes. A solo
645
        // writer on an idle store skips it and pays only its own flush.
646
        if round.result_tx.receiver_count() > 1 {
647
            tokio::time::sleep(Duration::from_millis(3)).await;
648
        }
649
        // Close the round: writers arriving from here on cannot assume
650
        // this flush covers them, so they must get a fresh round.
651
        {
652
            let mut current = self.current_round.lock();
653
            if Arc::ptr_eq(&*current, &round) {
654
                *current = FlushRound::new();
655
            }
656
        }
657
        let sentinel = self.sentinel.clone();
658
        let full_flush_count = self.full_flush_count.clone();
659
        let flush_result = spawn_blocking!("filesystem_store_full_flush", move || {
660
            use std::os::fd::AsRawFd;
661
            use std::os::unix::fs::FileExt;
662
            // Keep the sentinel dirty so the flush is never a no-op.
663
            // Best-effort: a failed write (e.g. ENOSPC on a full CoW
664
            // volume) must not fail the whole round — the fcntl below is
665
            // what provides the device-cache drain.
666
            if let Err(err) = sentinel.write_at(b"f", 0) {
667
                warn!(?err, "Flush sentinel write failed; issuing flush anyway");
668
            }
669
            // Count the actual blocking operation, rather than merely counting
670
            // rounds scheduled by the async task.
671
            full_flush_count.fetch_add(1, Ordering::Relaxed);
672
            loop {
673
                if unsafe { libc::fcntl(sentinel.as_raw_fd(), libc::F_FULLFSYNC) } != -1 {
674
                    return Ok(());
675
                }
676
                let err = std::io::Error::last_os_error();
677
                // std's sync_all retries EINTR (cvt_r); match it.
678
                if err.kind() != std::io::ErrorKind::Interrupted {
679
                    return Err(Error::from(err).append("F_FULLFSYNC failed in flush coalescer"));
680
                }
681
            }
682
        })
683
        .await
684
        .unwrap_or_else(|e| {
685
            Err(Error::from_std_err(Code::Internal, &e).append("Flush coalescer task failed"))
686
        });
687
        send_guard.finish(flush_result);
688
    }
689
}
690
691
#[cfg(unix)]
692
210
async fn prepare_executable_dir(content_path: &str) -> Result<bool, Error> {
693
1
    fn is_non_writable_error(err: &std::io::Error) -> bool {
694
0
        matches!(
695
1
            err.kind(),
696
            std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::ReadOnlyFilesystem
697
        )
698
1
    }
699
700
210
    let executable_dir = format!("{content_path}{EXECUTABLE_DIR_SUFFIX}");
701
210
    let executable_digest_dir = format!("{executable_dir}/{DIGEST_FOLDER_V2}");
702
210
    spawn_blocking!("filesystem_store_prepare_executable_dir", move || {
703
210
        match std::fs::remove_dir_all(&executable_dir) {
704
7
            Ok(()) => {}
705
203
            Err(
err202
) if err.kind() == std::io::ErrorKind::NotFoun
d202
=>
{}202
706
            // A read-only cache must remain available for ordinary reads. Do
707
            // not reuse the directory, though: a surviving executable variant
708
            // may be stale or torn after an unclean shutdown.
709
1
            Err(err) if is_non_writable_error(&err) => return Ok(false),
710
0
            Err(err) => {
711
0
                return Err(Error::from(err)
712
0
                    .append(format!("Failed to clear executable dir {executable_dir}")));
713
            }
714
        }
715
716
209
        match std::fs::create_dir_all(&executable_digest_dir) {
717
209
            Ok(()) => Ok(true),
718
0
            Err(err) if is_non_writable_error(&err) => Ok(false),
719
0
            Err(err) => Err(Error::from(err).append(format!(
720
0
                "Failed to create executable dir {executable_digest_dir}"
721
0
            ))),
722
        }
723
210
    })
724
210
    .await
725
210
    .err_tip(|| "Failed to join executable-dir preparation task")
?0
726
210
}
727
728
impl LenEntry for FileEntryImpl {
729
    #[inline]
730
5.34k
    fn len(&self) -> u64 {
731
5.34k
        self.size_on_disk()
732
5.34k
    }
733
734
    /// The file's actual length; `len()` is rounded up to whole blocks.
735
    #[inline]
736
1.03k
    fn content_len(&self) -> u64 {
737
1.03k
        self.data_size
738
1.03k
    }
739
740
0
    fn is_empty(&self) -> bool {
741
0
        self.data_size == 0
742
0
    }
743
744
    // unref() only triggers when an item is removed from the eviction_map. It is possible
745
    // that another place in code has a reference to `FileEntryImpl` and may later read the
746
    // file. To support this edge case, we first move the file to a temp file and point
747
    // target file location to the new temp file. `unref()` should only ever be called once.
748
    #[inline]
749
54
    async fn unref(&self) {
750
54
        let mut encoded_file_path = self.encoded_file_path.write().await;
751
54
        if encoded_file_path.path_type == PathType::Temp {
752
            // We are already a temp file that is now marked for deletion on drop.
753
            // This is very rare, but most likely the rename into the content path failed.
754
1
            warn!(
755
1
                key = ?encoded_file_path.key,
756
                "File is already a temp file",
757
            );
758
1
            return;
759
53
        }
760
53
        let from_path = encoded_file_path.get_file_path();
761
53
        let new_key = make_temp_key(&encoded_file_path.key);
762
763
53
        let to_path = to_full_path_from_key(
764
53
            &encoded_file_path.shared_context.temp_path,
765
53
            &new_key,
766
53
            encoded_file_path.generation,
767
            // Legacy files may survive a failed migration, but only the new
768
            // temporary directories are guaranteed to exist on this startup.
769
53
            Version::V2,
770
        );
771
772
53
        if let Err(
err10
) = fs::rename(&from_path, &to_path).await {
773
            // ENOENT from rename is ambiguous: the source may be gone, or
774
            // a directory component of the destination (the temp dir) may
775
            // be missing. Confirm the source is genuinely gone before
776
            // treating it as benign — otherwise a removed temp dir would
777
            // flip an intact content file to Temp and orphan it on disk.
778
10
            let source_gone = err.code == Code::NotFound
779
9
                && matches!(
780
10
                    fs::metadata(&from_path).await,
781
9
                    Err(meta_err) if meta_err.code == Code::NotFound
782
                );
783
10
            if source_gone {
784
                // The file is already gone — typically another thread's
785
                // eviction beat us, or the entry never got its file on
786
                // disk. Benign here, and it dominates log volume under
787
                // heavy write+evict concurrency, so keep it at `debug`.
788
                // Mark the entry Temp (as a successful rename would) so a
789
                // repeat unref is a no-op and drop stops claiming the
790
                // content path.
791
9
                debug!(
792
9
                    key = ?encoded_file_path.key,
793
                    "Failed to rename file (already gone, treating as benign)",
794
                );
795
9
                encoded_file_path.path_type = PathType::Temp;
796
9
                encoded_file_path.key = new_key;
797
9
                encoded_file_path.version = Version::V2;
798
            } else {
799
                // Either a non-ENOENT failure (EACCES, EXDEV, EBUSY, …) or
800
                // ENOENT with the source still present (missing temp dir).
801
                // The content file is intact; leave the entry as Content.
802
1
                warn!(
803
1
                    key = ?encoded_file_path.key,
804
                    ?from_path,
805
                    ?to_path,
806
                    ?err,
807
                    "Failed to rename file",
808
                );
809
            }
810
        } else {
811
43
            debug!(
812
43
                key = ?encoded_file_path.key,
813
                ?from_path,
814
                ?to_path,
815
                "Renamed file (unref)",
816
            );
817
43
            encoded_file_path.path_type = PathType::Temp;
818
43
            encoded_file_path.key = new_key;
819
43
            encoded_file_path.version = Version::V2;
820
        }
821
54
    }
822
}
823
824
#[inline]
825
412
fn digest_from_filename(file_name: &str) -> Result<DigestInfo, Error> {
826
412
    let (hash, size) = file_name.split_once('-').err_tip(|| "")
?0
;
827
412
    let size = size.parse::<i64>()
?0
;
828
412
    DigestInfo::try_new(hash, size)
829
412
}
830
831
3
fn key_from_file_v1(file_name: &str, file_type: FileType) -> Result<StoreKey<'_>, Error> {
832
3
    match file_type {
833
2
        FileType::String => Ok(StoreKey::new_str(file_name)),
834
1
        FileType::Digest => digest_from_filename(file_name).map(StoreKey::Digest),
835
    }
836
3
}
837
838
459
pub fn key_and_generation_from_file_v2(
839
459
    file_name: &str,
840
459
    file_type: FileType,
841
459
) -> Result<(StoreKey<'_>, Generation), Error> {
842
459
    let (key, generation) = file_name.rsplit_once('-').err_tip(|| "")
?0
;
843
459
    let generation = Generation::new(generation.parse::<u64>()
?0
);
844
845
459
    let key = match file_type {
846
48
        FileType::String => StoreKey::new_str(key),
847
411
        FileType::Digest => digest_from_filename(key).map(StoreKey::Digest)
?0
,
848
    };
849
850
459
    Ok((key, generation))
851
459
}
852
853
/// The number of files to read the metadata for at the same time when running
854
/// `add_files_to_cache`.
855
const SIMULTANEOUS_METADATA_READS: usize = 200;
856
857
type FsEvictingMap<'a, Fe> =
858
    EvictingMap<StoreKeyBorrow, StoreKey<'a>, Arc<Fe>, SystemTime, RemoveCallbackHolder>;
859
860
210
async fn add_files_to_cache<Fe: FileEntry>(
861
210
    evicting_map: &FsEvictingMap<'_, Fe>,
862
210
    anchor_time: &SystemTime,
863
210
    shared_context: &Arc<SharedContext>,
864
210
    block_size: u64,
865
210
    rename_fn: fn(&OsStr, &OsStr) -> Result<(), std::io::Error>,
866
210
    migrate: bool,
867
210
) -> Result<Generation, Error> {
868
    #[expect(clippy::too_many_arguments)]
869
237
    async fn process_entry<Fe: FileEntry>(
870
237
        evicting_map: &FsEvictingMap<'_, Fe>,
871
237
        file_name: &str,
872
237
        file_type: FileType,
873
237
        version: Version,
874
237
        atime: SystemTime,
875
237
        data_size: u64,
876
237
        block_size: u64,
877
237
        anchor_time: &SystemTime,
878
237
        shared_context: &Arc<SharedContext>,
879
237
    ) -> Result<Generation, Error> {
880
237
        let (key, generation) = match version {
881
            Version::Flat | Version::V1 => {
882
3
                (key_from_file_v1(file_name, file_type)
?0
, Generation::new(0))
883
            }
884
234
            Version::V2 => key_and_generation_from_file_v2(file_name, file_type)
?0
,
885
        };
886
887
237
        let file_entry = Arc::new(Fe::create(
888
237
            data_size,
889
237
            block_size,
890
237
            generation,
891
237
            RwLock::new(EncodedFilePath {
892
237
                shared_context: shared_context.clone(),
893
237
                path_type: PathType::Content,
894
237
                key: key.borrow().into_owned(),
895
237
                generation,
896
237
                version,
897
237
            }),
898
        ));
899
237
        let time_since_anchor = if let Ok(
d236
) = anchor_time.duration_since(atime) {
900
236
            d
901
        } else {
902
1
            warn!(
903
                %file_name,
904
1
                atime = %humantime::format_rfc3339(atime),
905
1
                anchor_time = %humantime::format_rfc3339(*anchor_time),
906
                "File access time newer than FilesystemStore start time",
907
            );
908
1
            Duration::ZERO
909
        };
910
237
        let (inserted, _) = evicting_map
911
237
            .insert_with_time_if(
912
237
                key.into_owned().into(),
913
237
                file_entry.clone(),
914
1
                |present_entry, new_entry| present_entry.generation() < new_entry.generation(),
915
237
                i32::try_from(time_since_anchor.as_secs()).unwrap_or(i32::MAX),
916
            )
917
237
            .await;
918
237
        if !inserted {
919
            // Older generations can survive a crash before deferred cleanup.
920
            // Retire rejected files too so the next restart cannot revive them.
921
0
            file_entry.unref().await;
922
237
        }
923
237
        Ok(generation)
924
237
    }
925
926
1.68k
    async fn read_files(
927
1.68k
        folder: Option<&str>,
928
1.68k
        shared_context: &SharedContext,
929
1.68k
    ) -> Result<Vec<(String, SystemTime, u64, bool)>, Error> {
930
        // Note: In Dec 2024 this is for backwards compatibility with the old
931
        // way files were stored on disk. Previously all files were in a single
932
        // folder regardless of the StoreKey type. This allows old versions of
933
        // nativelink file version to be upgraded at startup time.
934
        // This logic can be removed once more time has passed.
935
1.68k
        let read_dir = folder.map_or_else(
936
210
            || format!("{}/", shared_context.content_path),
937
1.47k
            |folder| format!("{}/{folder}/", shared_context.content_path),
938
        );
939
940
1.68k
        let (
_permit866
,
dir_handle866
) = match fs::read_dir(read_dir).await {
941
866
            Ok(dir_handle) => dir_handle.into_inner(),
942
814
            Err(err) if err.code == Code::NotFound => return Ok(Vec::new()),
943
0
            Err(err) => {
944
0
                return Err(err).err_tip(
945
                    || "Failed opening content directory for iterating in filesystem store",
946
                );
947
            }
948
        };
949
950
866
        let read_dir_stream = ReadDirStream::new(dir_handle);
951
866
        read_dir_stream
952
1.12k
            .
map866
(|dir_entry| async move {
953
1.12k
                let dir_entry = dir_entry.unwrap();
954
1.12k
                let file_name = dir_entry.file_name().into_string().unwrap();
955
1.12k
                let metadata = dir_entry
956
1.12k
                    .metadata()
957
1.12k
                    .await
958
1.12k
                    .err_tip(|| "Failed to get metadata in filesystem store")
?0
;
959
                // We need to filter out folders - we do not want to try to cache
960
                // the per-version key folders.
961
1.12k
                let is_file = metadata.is_file()
962
866
                    || ![
963
866
                        STR_FOLDER_V2,
964
866
                        DIGEST_FOLDER_V2,
965
866
                        STR_FOLDER_V1,
966
866
                        DIGEST_FOLDER_V1,
967
866
                    ]
968
866
                    .contains(&file_name.as_str());
969
                // Using access time is not perfect, but better than random. We do not update the
970
                // atime when a file is actually "touched", we rely on whatever the filesystem does
971
                // when we read the file (usually update on read).
972
1.12k
                let atime = metadata
973
1.12k
                    .accessed()
974
1.12k
                    .or_else(|_| 
metadata0
.
modified0
())
975
1.12k
                    .unwrap_or(SystemTime::UNIX_EPOCH);
976
1.12k
                Result::<(String, SystemTime, u64, bool), Error>::Ok((
977
1.12k
                    file_name,
978
1.12k
                    atime,
979
1.12k
                    metadata.len(),
980
1.12k
                    is_file,
981
1.12k
                ))
982
2.24k
            })
983
866
            .buffer_unordered(SIMULTANEOUS_METADATA_READS)
984
866
            .try_collect()
985
866
            .await
986
1.68k
    }
987
988
    /// Best effort: a failure leaves the source where it is, so the loader
989
    /// still picks it up and a later startup retries.
990
18
    fn migrate_file(
991
18
        from_file: &OsStr,
992
18
        to_file: &OsStr,
993
18
        rename_fn: fn(&OsStr, &OsStr) -> Result<(), std::io::Error>,
994
18
    ) {
995
18
        if let Err(
err3
) = rename_fn(from_file, to_file) {
996
3
            warn!(?from_file, ?to_file, ?err, "Failed to migrate file");
997
        } else {
998
15
            debug!(?from_file, ?to_file, "Migrated file");
999
        }
1000
18
    }
1001
1002
    /// Note: In Dec 2024 this is for backwards compatibility with the old
1003
    /// way files were stored on disk. Previously all files were in a single
1004
    /// folder regardless of the [`StoreKey`] type.
1005
210
    async fn migrate_old_cache_1(
1006
210
        shared_context: &Arc<SharedContext>,
1007
210
        rename_fn: fn(&OsStr, &OsStr) -> Result<(), std::io::Error>,
1008
210
    ) -> Result<(), Error> {
1009
210
        let file_infos = read_files(None, shared_context).await
?0
;
1010
210
        let from_path = &shared_context.content_path;
1011
210
        let to_path = format!("{}/{DIGEST_FOLDER_V2}", shared_context.content_path);
1012
1013
210
        for (
file_name3
, _, _, _) in file_infos.into_iter().filter(|x| x.3) {
1014
3
            migrate_file(
1015
3
                &OsString::from(format!("{from_path}/{file_name}")),
1016
3
                &OsString::from(format!("{to_path}/{file_name}-0")),
1017
3
                rename_fn,
1018
3
            );
1019
3
        }
1020
210
        Ok(())
1021
210
    }
1022
1023
    /// Moves [`Version::V1`] folders into [`Version::V2`].
1024
210
    async fn migrate_old_cache_2(
1025
210
        shared_context: &Arc<SharedContext>,
1026
210
        rename_fn: fn(&OsStr, &OsStr) -> Result<(), std::io::Error>,
1027
210
    ) -> Result<(), Error> {
1028
420
        for (legacy_folder, folder) in [
1029
210
            (DIGEST_FOLDER_V1, DIGEST_FOLDER_V2),
1030
210
            (STR_FOLDER_V1, STR_FOLDER_V2),
1031
210
        ] {
1032
420
            let file_infos = read_files(Some(legacy_folder), shared_context).await
?0
;
1033
420
            let from_path = format!("{}/{legacy_folder}", shared_context.content_path);
1034
420
            let to_path = format!("{}/{folder}", shared_context.content_path);
1035
1036
420
            for (
file_name15
, _, _, _) in file_infos.into_iter().filter(|x| x.3) {
1037
15
                migrate_file(
1038
15
                    &OsString::from(format!("{from_path}/{file_name}")),
1039
15
                    &OsString::from(format!("{to_path}/{file_name}-0")),
1040
15
                    rename_fn,
1041
15
                );
1042
15
            }
1043
        }
1044
210
        Ok(())
1045
210
    }
1046
1047
1.05k
    async fn add_folder_to_cache<Fe: FileEntry>(
1048
1.05k
        evicting_map: &FsEvictingMap<'_, Fe>,
1049
1.05k
        anchor_time: &SystemTime,
1050
1.05k
        shared_context: &Arc<SharedContext>,
1051
1.05k
        block_size: u64,
1052
1.05k
        folder: &str,
1053
1.05k
        file_type: FileType,
1054
1.05k
        version: Version,
1055
1.05k
    ) -> Result<Generation, Error> {
1056
1.05k
        let mut file_infos = read_files(Some(folder), shared_context).await
?0
;
1057
        // Load older generations first. Otherwise cache pressure could evict a
1058
        // newer generation before an older file for that key is encountered.
1059
1.05k
        if version == Version::V2 {
1060
420
            file_infos.sort_by_cached_key(|(file_name, ..)| 
{224
1061
224
                key_and_generation_from_file_v2(file_name, file_type)
1062
224
                    .map_or(Generation::new(0), |(_, generation)| generation)
1063
224
            });
1064
630
        }
1065
1066
1.05k
        let path_root = format!("{}/{folder}", shared_context.content_path);
1067
1068
1.05k
        let mut max_generation = 0;
1069
1070
1.05k
        for (
file_name237
,
atime237
,
data_size237
, _) in file_infos.into_iter().filter(|x| x.3) {
1071
237
            let result = process_entry(
1072
237
                evicting_map,
1073
237
                &file_name,
1074
237
                file_type,
1075
237
                version,
1076
237
                atime,
1077
237
                data_size,
1078
237
                block_size,
1079
237
                anchor_time,
1080
237
                shared_context,
1081
237
            )
1082
237
            .await;
1083
1084
237
            match result {
1085
237
                Ok(generation) => {
1086
237
                    max_generation = cmp::max(max_generation, generation.inner());
1087
237
                }
1088
0
                Err(err) => {
1089
0
                    warn!(?file_name, ?err, "Failed to add file to eviction cache",);
1090
                    // Ignore result.
1091
0
                    drop(fs::remove_file(format!("{path_root}/{file_name}")).await);
1092
                }
1093
            }
1094
        }
1095
1.05k
        Ok(Generation::new(max_generation))
1096
1.05k
    }
1097
1098
210
    if migrate {
1099
210
        migrate_old_cache_1(shared_context, rename_fn).await
?0
;
1100
210
        migrate_old_cache_2(shared_context, rename_fn).await
?0
;
1101
0
    }
1102
1103
210
    let mut max_generation = 0;
1104
1.05k
    for (folder, file_type, version) in [
1105
210
        ("", FileType::Digest, Version::Flat),
1106
210
        (DIGEST_FOLDER_V1, FileType::Digest, Version::V1),
1107
210
        (STR_FOLDER_V1, FileType::String, Version::V1),
1108
210
        (DIGEST_FOLDER_V2, FileType::Digest, Version::V2),
1109
210
        (STR_FOLDER_V2, FileType::String, Version::V2),
1110
210
    ] {
1111
1.05k
        let generation = add_folder_to_cache(
1112
1.05k
            evicting_map,
1113
1.05k
            anchor_time,
1114
1.05k
            shared_context,
1115
1.05k
            block_size,
1116
1.05k
            folder,
1117
1.05k
            file_type,
1118
1.05k
            version,
1119
1.05k
        )
1120
1.05k
        .await
?0
;
1121
1.05k
        max_generation = cmp::max(max_generation, generation.inner());
1122
    }
1123
1124
210
    Ok(Generation::new(max_generation.checked_add(1).ok_or_else(
1125
0
        || {
1126
0
            make_err!(
1127
0
                Code::ResourceExhausted,
1128
                "Filesystem generation counter exhausted"
1129
            )
1130
0
        },
1131
0
    )?))
1132
210
}
1133
1134
210
async fn prune_temp_path(temp_path: &str) -> Result<(), Error> {
1135
840
    async fn prune_temp_inner(temp_path: &str, subpath: &str) -> Result<(), Error> {
1136
840
        let (
_permit420
,
dir_handle420
) = match fs::read_dir(format!("{temp_path}/{subpath}")).await {
1137
420
            Ok(dir_handle) => dir_handle.into_inner(),
1138
420
            Err(err) if err.code == Code::NotFound => return Ok(()),
1139
0
            Err(err) => {
1140
0
                return Err(err).err_tip(|| {
1141
0
                    "Failed opening temp directory to prune partial downloads in filesystem store"
1142
0
                });
1143
            }
1144
        };
1145
1146
420
        let mut read_dir_stream = ReadDirStream::new(dir_handle);
1147
420
        while let Some(
dir_entry0
) = read_dir_stream.next().await {
1148
0
            let path = dir_entry?.path();
1149
0
            if let Err(err) = fs::remove_file(&path).await {
1150
0
                warn!(?path, ?err, "Failed to delete file",);
1151
0
            }
1152
        }
1153
420
        Ok(())
1154
840
    }
1155
1156
840
    for folder in [
1157
210
        STR_FOLDER_V2,
1158
210
        DIGEST_FOLDER_V2,
1159
210
        STR_FOLDER_V1,
1160
210
        DIGEST_FOLDER_V1,
1161
210
    ] {
1162
840
        prune_temp_inner(temp_path, folder).await
?0
;
1163
    }
1164
210
    Ok(())
1165
210
}
1166
1167
// Sometimes we get files to emplace that are identical to the existing files
1168
// Due to the evict/remove/replace cycle taking some amount of time, we actually
1169
// want to drop these
1170
// Return value is "is duplicate"
1171
2.34k
pub async fn check_duplicate_files<Fe>(
1172
2.34k
    evicting_map: &Arc<FsEvictingMap<'_, Fe>>,
1173
2.34k
    key: &StoreKey<'static>,
1174
2.34k
    entry: &Arc<Fe>,
1175
2.34k
) -> Result<bool, Error>
1176
2.34k
where
1177
2.34k
    Fe: FileEntry,
1178
2.34k
{
1179
2.34k
    let temp_file_encoded_file_path = entry.get_encoded_file_path().write().await;
1180
2.34k
    let maybe_existing_item = evicting_map.get(&key.borrow().into_owned()).await;
1181
2.34k
    if let Some(
existing_item31
) = maybe_existing_item {
1182
31
        if Arc::ptr_eq(entry, &existing_item) {
1183
1
            warn!("Tried to check duplicate of an entry we already have!");
1184
1
            return Ok(true);
1185
30
        }
1186
30
        let existing_item_encoded_file_path = existing_item.get_encoded_file_path().write().await;
1187
30
        if entry.data_size() == existing_item.data_size() {
1188
            const CHUNK_SIZE: usize = 16 * 1024; // 16kb chunks, kinda picked out of the air
1189
26
            let file_length = entry.data_size();
1190
26
            let existing_path = existing_item_encoded_file_path.get_file_path();
1191
26
            let temp_path = temp_file_encoded_file_path.get_file_path();
1192
26
            trace!(?existing_path, ?temp_path, "Checking duplicate files");
1193
26
            let mut temp_file = fs::open_file(&temp_path, 0, file_length).await
?0
;
1194
26
            let 
mut existing_file21
= match fs::open_file(&existing_path, 0, file_length).await {
1195
21
                Ok(file) => file,
1196
5
                Err(
err4
) if err.code == Code::NotFoun
d4
=> {
1197
4
                    warn!(
1198
                        ?key,
1199
                        ?err,
1200
                        "Map/disk divergence in check_duplicate_files: dropping stale entry",
1201
                    );
1202
                    // Removal calls unref(), which needs the path lock. Release
1203
                    // both locks and the open file before running callbacks.
1204
4
                    drop(temp_file);
1205
4
                    drop(existing_item_encoded_file_path);
1206
4
                    drop(temp_file_encoded_file_path);
1207
4
                    evicting_map
1208
4
                        .remove_if(key, |map_entry| Arc::ptr_eq(map_entry, &existing_item))
1209
4
                        .await;
1210
4
                    return Ok(false);
1211
                }
1212
1
                Err(err) => return Err(err),
1213
            };
1214
1215
21
            let mut temp_buffer: [u8; CHUNK_SIZE] = [0; CHUNK_SIZE];
1216
21
            let mut existing_buffer: [u8; CHUNK_SIZE] = [0; CHUNK_SIZE];
1217
21
            for offset in (0..file_length).step_by(CHUNK_SIZE) {
1218
21
                let buffer_size = if offset + (CHUNK_SIZE as u64) <= file_length {
1219
2
                    CHUNK_SIZE
1220
19
                } else if file_length < CHUNK_SIZE as u64 {
1221
17
                    usize::try_from(file_length)
1222
17
                        .expect("Always succeeds because file_length < 16384")
1223
                } else {
1224
2
                    usize::try_from(file_length - offset).expect("Always succeeds because offset < file_length, and offset-file_length must be < 16384")
1225
                };
1226
21
                if let Err(
err0
) = temp_file.read_exact(&mut temp_buffer[0..buffer_size]).await {
1227
0
                    warn!(
1228
                        ?err,
1229
                        ?temp_path,
1230
                        file_length,
1231
                        offset,
1232
                        buffer_size,
1233
                        "Failed to read temp file, skipping duplicate check"
1234
                    );
1235
0
                    return Ok(false);
1236
21
                }
1237
21
                if let Err(
err0
) = existing_file
1238
21
                    .read_exact(&mut existing_buffer[0..buffer_size])
1239
21
                    .await
1240
                {
1241
0
                    warn!(
1242
                        ?err,
1243
                        ?existing_path,
1244
                        file_length,
1245
                        offset,
1246
                        buffer_size,
1247
                        "Failed to read existing, skipping duplicate check"
1248
                    );
1249
0
                    return Ok(false);
1250
21
                }
1251
21
                if temp_buffer.ne(&existing_buffer) {
1252
11
                    trace!(
1253
                        ?existing_path,
1254
                        ?temp_path,
1255
                        "Files are different, so non-duplicate"
1256
                    );
1257
11
                    return Ok(false);
1258
10
                }
1259
            }
1260
10
            trace!(
1261
                ?existing_path,
1262
                ?temp_path,
1263
                "Identical files, so don't need to edit, skipping emplace"
1264
            );
1265
10
            drop(existing_file);
1266
10
            drop(temp_file);
1267
10
            drop(existing_item_encoded_file_path);
1268
10
            drop(temp_file_encoded_file_path);
1269
            // Comparison can overlap eviction. An identical retired file no
1270
            // longer satisfies this upload unless it is still the mapped entry.
1271
10
            return Ok(evicting_map
1272
10
                .get(key)
1273
10
                .await
1274
10
                .is_some_and(|current| Arc::ptr_eq(&current, &existing_item)));
1275
4
        }
1276
4
        trace!(
1277
4
            entry_data_size = entry.data_size(),
1278
4
            existing_data_size = existing_item.data_size(),
1279
4
            existing_path = ?existing_item_encoded_file_path.get_file_path(),
1280
            "Different data sizes, so non-duplicate"
1281
        );
1282
    } else {
1283
2.31k
        trace!(
1284
2.31k
            temp_file = ?temp_file_encoded_file_path.get_file_path(),
1285
            "No existing entry, so not duplicate"
1286
        );
1287
    }
1288
2.31k
    Ok(false)
1289
2.34k
}
1290
1291
/// Deletes a digest's `.exec` variant (see
1292
/// [`FilesystemStore::get_executable_hardlink_source`]) when that digest is
1293
/// evicted or replaced in the primary CAS `evicting_map`. Without this, the
1294
/// `.exec` directory is invisible to `max_bytes` and is only ever cleared by
1295
/// the startup `remove_dir_all`, so it grows without bound at runtime (#2474).
1296
/// Tying its lifetime to the primary entry instead bounds total disk use to
1297
/// roughly `2 * max_bytes` in the worst case (every blob also executable).
1298
#[cfg(unix)]
1299
#[derive(Debug)]
1300
struct ExecutableVariantRemover {
1301
    content_path: String,
1302
}
1303
1304
#[cfg(unix)]
1305
impl RemoveItemCallback for ExecutableVariantRemover {
1306
50
    fn callback<'a>(
1307
50
        &'a self,
1308
50
        store_key: StoreKey<'a>,
1309
50
    ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
1310
50
        Box::pin(async move {
1311
50
            let StoreKey::Digest(
digest40
) = store_key else {
1312
10
                return;
1313
            };
1314
40
            let variant_path = format!(
1315
                "{}{EXECUTABLE_DIR_SUFFIX}/{DIGEST_FOLDER_V2}/{digest}",
1316
                self.content_path
1317
            );
1318
40
            match fs::remove_file(&variant_path).await {
1319
3
                Ok(()) => debug!(
1320
                    ?variant_path,
1321
                    "Deleted executable variant for evicted digest"
1322
                ),
1323
                // Common case: no variant was ever materialized for this digest.
1324
37
                Err(err) if err.code == Code::NotFound => {}
1325
0
                Err(err) => warn!(
1326
                    ?variant_path,
1327
                    ?err,
1328
                    "Failed to delete executable variant for evicted digest"
1329
                ),
1330
            }
1331
50
        })
1332
50
    }
1333
}
1334
1335
#[derive(Debug, MetricsComponent)]
1336
pub struct FilesystemStore<Fe: FileEntry = FileEntryImpl> {
1337
    #[metric]
1338
    shared_context: Arc<SharedContext>,
1339
    #[metric(group = "evicting_map")]
1340
    evicting_map: Arc<FsEvictingMap<'static, Fe>>,
1341
    #[metric(help = "Block size of the configured filesystem")]
1342
    block_size: u64,
1343
    #[metric(help = "Size of the configured read buffer size")]
1344
    read_buffer_size: usize,
1345
    /// See [`FilesystemSpec::evict_page_cache`]. When false (the default) the
1346
    /// per-blob `posix_fadvise(DONTNEED)` calls are skipped.
1347
    evict_page_cache: bool,
1348
    weak_self: Weak<Self>,
1349
    rename_fn: fn(&OsStr, &OsStr) -> Result<(), std::io::Error>,
1350
    /// Limits concurrent write operations to prevent disk I/O saturation.
1351
    write_semaphore: Option<Semaphore>,
1352
    /// A monotonic counter by which we stamp newly added files.
1353
    next_generation: AtomicU64,
1354
    /// See [`FlushCoalescer`]: amortizes the per-blob `F_FULLFSYNC` cost
1355
    /// across concurrent uploads without weakening durability. `None` when
1356
    /// the sentinel could not be created (e.g. read-only content volume);
1357
    /// uploads then fall back to per-file `sync_all`, matching the old
1358
    /// behavior (and such stores fail at write time anyway).
1359
    #[cfg(target_os = "macos")]
1360
    flush_coalescer: Option<Arc<FlushCoalescer>>,
1361
    /// Per-digest single-flight locks guarding creation of the executable
1362
    /// variant in `{content_path}.exec`, so each variant's writable fd is
1363
    /// opened exactly once. The outer lock is sync and only ever held to
1364
    /// get/insert/remove the per-digest async lock — never across I/O.
1365
    #[cfg(unix)]
1366
    executable_locks: std::sync::Mutex<HashMap<DigestInfo, Arc<Mutex<()>>>>,
1367
    /// False when the startup wipe could not safely remove old executable
1368
    /// variants from a read-only filesystem. Ordinary CAS reads remain
1369
    /// available, but executable hardlink sources must not reuse stale files.
1370
    #[cfg(unix)]
1371
    executable_variants_enabled: bool,
1372
}
1373
1374
impl<Fe: FileEntry> FilesystemStore<Fe> {
1375
193
    pub async fn new(spec: &FilesystemSpec) -> Result<Arc<Self>, Error> {
1376
2.34k
        
Self::new_with_timeout_and_rename_fn193
(
spec193
, |from, to| std::fs::rename(from, to)).
await193
1377
193
    }
1378
1379
210
    pub async fn new_with_timeout_and_rename_fn(
1380
210
        spec: &FilesystemSpec,
1381
210
        rename_fn: fn(&OsStr, &OsStr) -> Result<(), std::io::Error>,
1382
210
    ) -> Result<Arc<Self>, Error> {
1383
420
        async fn create_subdirs(path: &str) -> Result<bool, Error> {
1384
420
            let mut writable = true;
1385
840
            for folder in 
[STR_FOLDER_V2, DIGEST_FOLDER_V2]420
{
1386
840
                let dir = format!("{path}/{folder}");
1387
840
                writable &= fs::call_with_permit(move |_| match std::fs::create_dir_all(&dir) {
1388
840
                    Ok(()) => Ok(true),
1389
0
                    Err(err) if matches!(err.kind(), std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::ReadOnlyFilesystem) => {
1390
0
                        warn!(%dir, ?err, "Cannot create directory on read-only volume; retaining legacy files");
1391
0
                        Ok(false)
1392
                    }
1393
0
                    Err(err) => Err(Error::from(err).append(format!("Failed to create directory {dir}"))),
1394
840
                }).await
?0
;
1395
            }
1396
420
            Ok(writable)
1397
420
        }
1398
1399
210
        let now = SystemTime::now();
1400
1401
210
        let empty_policy = nativelink_config::stores::EvictionPolicy::default();
1402
210
        let eviction_policy = spec.eviction_policy.as_ref().unwrap_or(&empty_policy);
1403
210
        let evicting_map = Arc::new(EvictingMap::new(eviction_policy, now));
1404
1405
        // Create the generation-aware temp and content directories.
1406
1407
210
        let temp_dirs_writable = create_subdirs(&spec.temp_path).await
?0
;
1408
210
        let content_dirs_writable = create_subdirs(&spec.content_path).await
?0
;
1409
        // Nothing to migrate into; skip the scan rather than warn per file.
1410
210
        let migrate = temp_dirs_writable && content_dirs_writable;
1411
1412
        // Executable-variant directory: a sibling of `content_path` holding
1413
        // per-digest 0o555 copies used as hardlink sources for executable
1414
        // inputs (see `get_executable_hardlink_source`). Cleared on writable
1415
        // startup — the variants are regenerable and we never want a stale one
1416
        // to leak across runs. If a read-only filesystem prevents the wipe,
1417
        // ordinary CAS reads remain enabled but executable variants do not.
1418
        // Unix-only: the executable bit (and the ETXTBSY race it guards
1419
        // against) does not apply on Windows.
1420
        #[cfg(unix)]
1421
210
        let executable_variants_enabled = {
1422
210
            let enabled = prepare_executable_dir(&spec.content_path).await
?0
;
1423
210
            if !enabled {
1424
1
                warn!(
1425
1
                    executable_dir = %format!("{}{EXECUTABLE_DIR_SUFFIX}", spec.content_path),
1426
                    "Executable directory is not writable; serving CAS reads with executable variants disabled"
1427
                );
1428
209
            }
1429
210
            if enabled {
1430
209
                // Only register cleanup when executable variants are enabled.
1431
209
                // Otherwise surviving variants are deliberately quarantined,
1432
209
                // and repeated deletion warnings would obscure the fallback.
1433
209
                evicting_map.add_remove_callback(RemoveCallbackHolder::new(Arc::new(
1434
209
                    ExecutableVariantRemover {
1435
209
                        content_path: spec.content_path.clone(),
1436
209
                    },
1437
209
                )));
1438
209
            
}1
1439
210
            enabled
1440
        };
1441
1442
210
        let shared_context = Arc::new(SharedContext {
1443
210
            active_drop_spawns: AtomicU64::new(0),
1444
210
            temp_path: spec.temp_path.clone(),
1445
210
            content_path: spec.content_path.clone(),
1446
210
        });
1447
1448
210
        let block_size = if spec.block_size == 0 {
1449
168
            DEFAULT_BLOCK_SIZE
1450
        } else {
1451
42
            spec.block_size
1452
        };
1453
210
        let next_generation = add_files_to_cache(
1454
210
            evicting_map.as_ref(),
1455
210
            &now,
1456
210
            &shared_context,
1457
210
            block_size,
1458
210
            rename_fn,
1459
210
            migrate,
1460
210
        )
1461
210
        .await
?0
;
1462
210
        prune_temp_path(&shared_context.temp_path).await
?0
;
1463
1464
210
        let read_buffer_size = if spec.read_buffer_size == 0 {
1465
195
            DEFAULT_BUFF_SIZE
1466
        } else {
1467
15
            spec.read_buffer_size as usize
1468
        };
1469
210
        let write_semaphore = if spec.max_concurrent_writes > 0 {
1470
0
            Some(Semaphore::new(spec.max_concurrent_writes))
1471
        } else {
1472
210
            None
1473
        };
1474
        // Never make construction fail over the durability optimization:
1475
        // a read-only content volume must still serve reads, exactly as
1476
        // it did before the coalescer existed.
1477
        #[cfg(target_os = "macos")]
1478
        let flush_coalescer = FlushCoalescer::for_content_path(&shared_context.content_path)
1479
            .await
1480
            .inspect_err(|err| {
1481
                warn!(
1482
                    ?err,
1483
                    "Failed to create flush coalescer; falling back to per-file sync_all"
1484
                );
1485
            })
1486
            .ok();
1487
1488
210
        Ok(Arc::new_cyclic(|weak_self| Self {
1489
210
            shared_context,
1490
210
            evicting_map,
1491
210
            block_size,
1492
210
            read_buffer_size,
1493
210
            evict_page_cache: spec.evict_page_cache,
1494
210
            weak_self: weak_self.clone(),
1495
210
            rename_fn,
1496
210
            next_generation: AtomicU64::new(next_generation.inner()),
1497
210
            write_semaphore,
1498
            #[cfg(target_os = "macos")]
1499
            flush_coalescer,
1500
            #[cfg(unix)]
1501
210
            executable_locks: std::sync::Mutex::new(HashMap::new()),
1502
            #[cfg(unix)]
1503
210
            executable_variants_enabled,
1504
210
        }))
1505
210
    }
1506
1507
126
    pub fn get_arc(&self) -> Option<Arc<Self>> {
1508
126
        self.weak_self.upgrade()
1509
126
    }
1510
1511
    /// Reserve a digest before it is populated so active action inputs cannot
1512
    /// be evicted while they are being materialized.
1513
38
    pub fn lease_digest(&self, digest: &DigestInfo) {
1514
38
        let key: StoreKey<'static> = (*digest).into();
1515
38
        self.evicting_map.lease_key(StoreKeyBorrow::from(key));
1516
38
    }
1517
1518
    /// Release a batch of action-input leases and trim retained entries once.
1519
9
    pub async fn release_digests(&self, digests: &[DigestInfo]) {
1520
9
        self.evicting_map
1521
38
            .
release_keys9
(
digests9
.
iter9
().
map9
(|digest| StoreKey::Digest(*digest)))
1522
9
            .await;
1523
9
    }
1524
1525
2.35k
    fn get_and_update_generation(&self) -> Result<Generation, Error> {
1526
2.35k
        self.next_generation
1527
2.35k
            .
try_update2.35k
(
Ordering::Relaxed2.35k
,
Ordering::Relaxed2.35k
, |generation| {
1528
2.35k
                generation.checked_add(1)
1529
2.35k
            })
1530
2.35k
            .map(Generation::new)
1531
2.35k
            .map_err(|_| 
{0
1532
0
                make_err!(
1533
0
                    Code::ResourceExhausted,
1534
                    "Filesystem generation counter exhausted"
1535
                )
1536
0
            })
1537
2.35k
    }
1538
1539
    /// Path of the read-only executable (0o555) variant for `digest`.
1540
    #[cfg(unix)]
1541
28
    fn executable_variant_path(&self, digest: &DigestInfo) -> OsString {
1542
28
        format!(
1543
            "{}{EXECUTABLE_DIR_SUFFIX}/{DIGEST_FOLDER_V2}/{digest}",
1544
28
            self.shared_context.content_path
1545
        )
1546
28
        .into()
1547
28
    }
1548
1549
    /// Resolves the executable variant for many digests at once, batching the
1550
    /// warm path's existence checks into a single dispatch.
1551
    ///
1552
    /// Every input file carrying the executable bit resolves through here, and
1553
    /// in a toolchain-heavy input tree that is nearly all of them. The `stat(2)`
1554
    /// this needs costs single-digit microseconds on tmpfs, while the permit
1555
    /// acquisition and thread hop to dispatch one cost tens: asking per path
1556
    /// spent ~4.6s per action on a 15k-file tree, and asking once for the tree
1557
    /// spends ~37ms.
1558
    ///
1559
    /// A hit also counts as a use of the blob. The variant lives outside the
1560
    /// eviction index, so a toolchain binary that every action hardlinks from
1561
    /// its variant would otherwise never move in the LRU and be evicted as the
1562
    /// coldest entry of a full store, taking its variant with it under the
1563
    /// next action's batch. One batched lookup refreshes every hit's position.
1564
    /// A variant whose blob is no longer indexed is reported `NotFound`, not
1565
    /// handed out: the caller then fills the fast store again, which puts the
1566
    /// blob back in the index so its next eviction deletes the variant too.
1567
74
    pub async fn get_executable_hardlink_sources(
1568
74
        &self,
1569
74
        digests: &[DigestInfo],
1570
74
    ) -> Vec<Result<OsString, Error>> {
1571
74
        let hits = self.materialized_variants(digests).await;
1572
74
        let hit_keys: Vec<StoreKey<'static>> = digests
1573
74
            .iter()
1574
74
            .zip(&hits)
1575
74
            .filter(|(_, hit)| 
hit11
.
is_some11
())
1576
74
            .map(|(digest, _)| StoreKey::Digest(
*digest5
))
1577
74
            .collect();
1578
74
        let mut indexed = vec![None; hit_keys.len()];
1579
74
        if !hit_keys.is_empty() {
1580
5
            self.evicting_map
1581
5
                .sizes_for_keys(hit_keys.iter(), &mut indexed, false /* peek */)
1582
5
                .await;
1583
69
        }
1584
74
        let mut indexed = indexed.into_iter();
1585
1586
74
        let mut tasks = Vec::with_capacity(digests.len());
1587
74
        for (
digest11
,
hit11
) in digests.iter().copied().zip(hits) {
1588
11
            let orphaned = hit.is_some() && 
indexed5
.next().flatten().
is_none5
();
1589
11
            tasks.push(async move {
1590
5
                match hit {
1591
2
                    Some(_) if orphaned => Err(make_err!(
1592
2
                        Code::NotFound,
1593
2
                        "{digest} has an executable variant on disk but no indexed blob; fill the fast store again"
1594
2
                    )),
1595
3
                    Some(path) => Ok(path),
1596
6
                    None => self.get_executable_hardlink_source(&digest).await,
1597
                }
1598
11
            });
1599
        }
1600
74
        futures::stream::iter(tasks)
1601
74
            .buffered(MAX_CONCURRENT_VARIANT_LOOKUPS)
1602
74
            .collect()
1603
74
            .await
1604
74
    }
1605
1606
    /// Reports which digests already have an executable variant on disk, in one
1607
    /// batched existence check.
1608
    #[cfg(unix)]
1609
74
    async fn materialized_variants(&self, digests: &[DigestInfo]) -> Vec<Option<OsString>> {
1610
74
        let paths: Vec<OsString> = digests
1611
74
            .iter()
1612
74
            .map(|digest| 
self11
.
executable_variant_path11
(
digest11
))
1613
74
            .collect();
1614
1615
        // Variants disabled and a failed batch are the same situation: nothing
1616
        // is known to exist, so every digest takes the per-digest path, which
1617
        // reports real errors per file.
1618
74
        let exists = if self.executable_variants_enabled {
1619
74
            fs::exists_many(paths.iter().map(Into::into).collect())
1620
74
                .await
1621
74
                .ok()
1622
        } else {
1623
0
            None
1624
        }
1625
74
        .unwrap_or_else(|| 
vec!0
[false;
paths0
.
len0
()]);
1626
1627
74
        paths
1628
74
            .into_iter()
1629
74
            .zip(exists)
1630
74
            .map(|(path, hit)| 
hit11
.
then_some11
(
path11
))
1631
74
            .collect()
1632
74
    }
1633
1634
    /// Non-unix never builds executable variants, so there is never one on disk
1635
    /// to find and nothing to batch.
1636
    #[cfg(not(unix))]
1637
    async fn materialized_variants(&self, digests: &[DigestInfo]) -> Vec<Option<OsString>> {
1638
        vec![None; digests.len()]
1639
    }
1640
1641
    /// Returns the path to a private, read-only **executable** (0o555) copy of
1642
    /// the blob for `digest`, creating it at most once. Callers **hardlink**
1643
    /// the returned path into action input trees instead of copying the
1644
    /// executable per action.
1645
    ///
1646
    /// Why this exists: a CAS blob is stored read-only **0o444** and shared
1647
    /// across actions by hardlink, so it cannot carry the executable bit and
1648
    /// must never be `chmod`'d (that mutates the shared inode — the #2347
1649
    /// corruption class). Materializing an executable input therefore needs a
1650
    /// separate 0o555 inode. Doing that copy *per action* opens a writable fd
1651
    /// in the worker's hot path; under fork-heavy concurrency a child can
1652
    /// inherit that fd and a concurrent `execve` of the executable then fails
1653
    /// with `ETXTBSY` ("Text file busy", os error 26). Creating the 0o555 inode
1654
    /// **once** — writer fd fsync'd and closed, then atomically renamed into
1655
    /// place before the inode is ever hardlinked or executed — and hardlinking
1656
    /// it thereafter keeps the per-action path hardlink-only.
1657
    #[cfg(unix)]
1658
18
    pub async fn get_executable_hardlink_source(
1659
18
        &self,
1660
18
        digest: &DigestInfo,
1661
18
    ) -> Result<OsString, Error> {
1662
18
        if !self.executable_variants_enabled {
1663
1
            return Err(make_err!(
1664
1
                Code::FailedPrecondition,
1665
1
                "Executable hardlink sources are disabled because the startup wipe could not safely clear the read-only executable directory"
1666
1
            ));
1667
17
        }
1668
17
        let variant_path = self.executable_variant_path(digest);
1669
1670
        // Fast path: the variant already exists, so the caller can hardlink it
1671
        // with no writable fd anywhere in sight.
1672
17
        if fs::metadata(&variant_path).await.is_ok() {
1673
2
            return Ok(variant_path);
1674
15
        }
1675
1676
        // Single-flight: exactly one task ever opens a writable fd for this
1677
        // variant. Without this, a cold-cache burst of concurrent actions
1678
        // would each open a writer for the same executable — the very window
1679
        // ETXTBSY exploits.
1680
15
        let lock = {
1681
15
            let mut locks = self
1682
15
                .executable_locks
1683
15
                .lock()
1684
15
                .expect("executable_locks poisoned");
1685
15
            locks
1686
15
                .entry(*digest)
1687
15
                .or_insert_with(|| Arc::new(Mutex::new(())))
1688
15
                .clone()
1689
        };
1690
15
        let _guard = lock.lock().await;
1691
1692
        // Re-check: another task may have constructed it while we waited.
1693
15
        if fs::metadata(&variant_path).await.is_ok() {
1694
0
            self.forget_executable_lock(digest);
1695
0
            return Ok(variant_path);
1696
15
        }
1697
1698
15
        let result = self.create_executable_variant(digest, &variant_path).await;
1699
1700
        // The digest may have been evicted mid-copy: its eviction callback ran
1701
        // before the rename published the variant, so nothing owns the file
1702
        // anymore. This orphans the variant from eviction accounting, but the
1703
        // race is rare enough (needs an eviction to land in the narrow window
1704
        // between rename and this check, on a digest's first-ever variant
1705
        // materialization) that it's a self-limiting leak, not a systemic one
1706
        // — cheaper to log and let this action succeed with the still-valid
1707
        // file than to fail an otherwise-successful action over it.
1708
15
        if result.is_ok() && 
self.evicting_map11
.
get11
(&digest.into()).
await11
.
is_none11
() {
1709
0
            warn!(
1710
                %digest,
1711
                ?variant_path,
1712
                "Digest evicted while materializing its executable variant; \
1713
                 variant is now untracked by eviction accounting"
1714
            );
1715
15
        }
1716
1717
        // Drop the per-digest lock entry regardless of outcome so the map
1718
        // cannot grow unbounded; a concurrent waiter already cloned the Arc.
1719
15
        self.forget_executable_lock(digest);
1720
15
        result.map(|()| variant_path)
1721
18
    }
1722
1723
    /// Non-unix has no executable bit and no `ETXTBSY`, so just hardlink the
1724
    /// CAS blob directly.
1725
    #[cfg(not(unix))]
1726
    pub async fn get_executable_hardlink_source(
1727
        &self,
1728
        digest: &DigestInfo,
1729
    ) -> Result<OsString, Error> {
1730
        let file_entry = self.get_file_entry_for_digest(digest).await?;
1731
        file_entry
1732
            .get_file_path_locked(|p| async move { Ok(p) })
1733
            .await
1734
    }
1735
1736
    #[cfg(unix)]
1737
15
    fn forget_executable_lock(&self, digest: &DigestInfo) {
1738
15
        self.executable_locks
1739
15
            .lock()
1740
15
            .expect("executable_locks poisoned")
1741
15
            .remove(digest);
1742
15
    }
1743
1744
    /// Materializes the 0o555 executable variant for `digest`. Must be called
1745
    /// under the per-digest single-flight guard.
1746
    #[cfg(unix)]
1747
15
    async fn create_executable_variant(
1748
15
        &self,
1749
15
        digest: &DigestInfo,
1750
15
        variant_path: &OsStr,
1751
15
    ) -> Result<(), Error> {
1752
        // Resolve the on-disk CAS blob (0o444) to copy from. Must be present in
1753
        // this tier; callers populate the fast store first.
1754
15
        let 
file_entry11
= self
1755
15
            .get_file_entry_for_digest(digest)
1756
15
            .await
1757
15
            .err_tip(|| "Resolving CAS blob for executable variant")
?4
;
1758
11
        let src_path = file_entry
1759
22
            .
get_file_path_locked11
(|p| async move
{11
Ok(p)11
})
1760
11
            .await
?0
;
1761
1762
11
        let variant_owned = variant_path.to_os_string();
1763
11
        let mut temp_owned = variant_path.to_os_string();
1764
11
        temp_owned.push(".tmp");
1765
11
        let rename_fn = self.rename_fn;
1766
1767
        // All of this is blocking std::fs; run it off the async runtime. The
1768
        // writable fd opened by `copy` is fully closed before the `rename`
1769
        // publishes the inode, so no reachable hardlink of the variant ever has
1770
        // an open writer.
1771
11
        spawn_blocking!(
1772
            "filesystem_store_executable_variant",
1773
11
            move || -> Result<(), Error> {
1774
                use std::os::unix::fs::PermissionsExt;
1775
11
                std::fs::copy(&src_path, &temp_owned).map_err(|e| 
{0
1776
0
                    make_err!(Code::Internal, "executable-variant copy failed: {e:?}")
1777
0
                })?;
1778
11
                std::fs::set_permissions(&temp_owned, std::fs::Permissions::from_mode(0o555))
1779
11
                    .map_err(|e| 
{0
1780
0
                        make_err!(
1781
0
                            Code::Internal,
1782
                            "executable-variant chmod 0o555 failed: {e:?}"
1783
                        )
1784
0
                    })?;
1785
                // Belt-and-suspenders flush before publish. The `.exec`
1786
                // directory is cleared before executable variants are enabled,
1787
                // so durability is not needed across process restarts. The
1788
                // flush still ensures a published variant is complete for the
1789
                // current process. Non-macOS keeps the prior `sync_all`
1790
                // behavior; macOS uses plain `fsync(2)` (data handed to the
1791
                // device, no multi-ms `F_FULLFSYNC` device-cache drain), which
1792
                // covers kernel panics — the next writable startup wipe covers
1793
                // power loss.
1794
11
                let f = std::fs::File::open(&temp_owned)
1795
11
                    .map_err(|e| 
make_err!0
(
Code::Internal0
, "executable-variant reopen: {e:?}"))
?0
;
1796
                #[cfg(target_os = "macos")]
1797
                let flush_result = {
1798
                    use std::os::fd::AsRawFd;
1799
                    loop {
1800
                        if unsafe { libc::fsync(f.as_raw_fd()) } == 0 {
1801
                            break Ok(());
1802
                        }
1803
                        let err = std::io::Error::last_os_error();
1804
                        if err.kind() != std::io::ErrorKind::Interrupted {
1805
                            break Err(err);
1806
                        }
1807
                    }
1808
                };
1809
                #[cfg(not(target_os = "macos"))]
1810
11
                let flush_result = f.sync_all();
1811
11
                flush_result
1812
11
                    .map_err(|e| 
make_err!0
(
Code::Internal0
, "executable-variant fsync: {e:?}"))
?0
;
1813
11
                drop(f);
1814
11
                rename_fn(temp_owned.as_os_str(), variant_owned.as_os_str()).map_err(|e| 
{0
1815
0
                    make_err!(Code::Internal, "executable-variant rename failed: {e:?}")
1816
0
                })?;
1817
11
                Ok(())
1818
11
            }
1819
        )
1820
11
        .await
1821
11
        .err_tip(|| "executable-variant spawn_blocking join failed")
?0
1822
15
    }
1823
1824
3.82k
    pub async fn get_file_entry_for_digest(&self, digest: &DigestInfo) -> Result<Arc<Fe>, Error> {
1825
        // Zero-digest blobs have no backing file on disk (FilesystemStore
1826
        // never persists zero-byte content). The previous implementation
1827
        // returned a synthetic FileEntry whose content_path did not exist,
1828
        // which downstream callers would then try to hard_link from,
1829
        // silently producing missing or empty output files in worker
1830
        // execution directories. Return NotFound so callers are forced to
1831
        // take the explicit zero-digest path (e.g. fs::create_file).
1832
3.82k
        if is_zero_digest(digest) {
1833
1
            return Err(make_err!(
1834
1
                Code::NotFound,
1835
1
                "{digest} is a zero-digest; FilesystemStore does not persist zero-byte files. \
1836
1
                 Callers must materialise empty files directly rather than going through get_file_entry_for_digest."
1837
1
            ));
1838
3.82k
        }
1839
3.82k
        self.evicting_map
1840
3.82k
            .get(&digest.into())
1841
3.82k
            .await
1842
3.82k
            .ok_or_else(|| 
make_err!165
(
Code::NotFound165
, "{digest} not found in filesystem store. This may indicate the file was evicted due to cache pressure. Consider increasing 'max_bytes' in your filesystem store's eviction_policy configuration."))
1843
3.82k
    }
1844
1845
1.77k
    async fn update_file(
1846
1.77k
        self: Pin<&Self>,
1847
1.77k
        mut entry: Fe,
1848
1.77k
        mut temp_file: FileSlot,
1849
1.77k
        final_key: StoreKey<'static>,
1850
1.77k
        mut reader: DropCloserReadHalf,
1851
1.77k
        upload_size: UploadSizeInfo,
1852
1.77k
    ) -> Result<u64, Error> {
1853
1.77k
        let mut data_size = 0;
1854
        loop {
1855
3.53k
            let 
mut data3.53k
= reader
1856
3.53k
                .recv()
1857
3.53k
                .await
1858
3.53k
                .err_tip(|| "Failed to receive data in filesystem store")
?1
;
1859
3.53k
            let data_len = data.len();
1860
3.53k
            if data_len == 0 {
1861
1.77k
                break; // EOF.
1862
1.76k
            }
1863
1.76k
            temp_file
1864
1.76k
                .write_all_buf(&mut data)
1865
1.76k
                .await
1866
1.76k
                .err_tip(|| "Failed to write data into filesystem store")
?0
;
1867
1.76k
            data_size += data_len as u64;
1868
        }
1869
1870
1.77k
        upload_size.validate_size(data_size)
?4
;
1871
1.76k
        let permit = if let Some(
sem0
) = &self.write_semaphore {
1872
0
            Some(sem.acquire().await.map_err(|err| {
1873
0
                Error::from_std_err(Code::Internal, &err).append("Write semaphore closed")
1874
0
            })?)
1875
        } else {
1876
1.76k
            None
1877
        };
1878
1879
        // tokio defers write errors to the next write or flush call, so without an explicit flush
1880
        // the final write's failure is silently swallowed by `sync_all`, and a truncated file would
1881
        // be renamed into the content path.
1882
1.76k
        temp_file
1883
1.76k
            .flush()
1884
1.76k
            .await
1885
1.76k
            .err_tip(|| "Failed to flush in filesystem store")
?0
;
1886
1.76k
        self.flush_durably(&temp_file)
1887
1.76k
            .await
1888
1.76k
            .err_tip(|| "Failed to sync in filesystem store")
?0
;
1889
1890
1.76k
        drop(permit);
1891
1892
1.76k
        if self.evict_page_cache {
1893
0
            temp_file.advise_dontneed();
1894
1.76k
        }
1895
1.76k
        trace!(?temp_file, "Dropping file to update_file");
1896
1.76k
        drop(temp_file);
1897
1898
1.76k
        *entry.data_size_mut() = data_size;
1899
1.76k
        self.emplace_file(final_key, Arc::new(entry)).await
?1
;
1900
1.76k
        Ok(data_size)
1901
1.77k
    }
1902
1903
2.34k
    async fn emplace_file(&self, key: StoreKey<'static>, entry: Arc<Fe>) -> Result<(), Error> {
1904
        // Each generation owns a distinct content path. Publish the file before
1905
        // inserting it into the map, so readers only observe completed renames.
1906
        // A delayed unref of another generation cannot touch this file.
1907
2.34k
        let evicting_map = self.evicting_map.clone();
1908
2.34k
        let rename_fn = self.rename_fn;
1909
1910
        // We need to guarantee that this will get to the end even if the parent future is dropped.
1911
        // See: https://github.com/TraceMachina/nativelink/issues/495
1912
2.34k
        background_spawn!("filesystem_store_emplace_file", async move {
1913
            // Sometimes we get files to emplace that are identical to the existing files
1914
            // Due to the evict/remove/replace cycle taking some amount of time, we actually
1915
            // want to drop these
1916
2.34k
            if check_duplicate_files(&evicting_map, &key, &entry).await
?1
{
1917
9
                return Ok(());
1918
2.33k
            }
1919
1920
2.33k
            let mut encoded_file_path = entry.get_encoded_file_path().write().await;
1921
1922
2.33k
            let final_path = get_file_path_raw(
1923
2.33k
                &PathType::Content,
1924
2.33k
                encoded_file_path.shared_context.as_ref(),
1925
2.33k
                &key,
1926
2.33k
                encoded_file_path.generation,
1927
2.33k
                encoded_file_path.version,
1928
            );
1929
1930
2.33k
            let from_path = encoded_file_path.get_file_path();
1931
1932
            // Lock the blob down as read-only *before* it lands at its final
1933
            // content path, so every hardlink of it (the worker directory cache
1934
            // and `download_to_directory`) inherits an immutable, read-only
1935
            // inode. This is what preserves the input-tree hermeticity contract
1936
            // (actions cannot mutate their inputs) without a per-materialization
1937
            // chmod walk, and it means callers must never `chmod` a hardlinked
1938
            // blob — anything needing a different mode (e.g. an executable's +x
1939
            // bit) must take a private copy. Content-addressed blobs are
1940
            // write-once and replaced by `rename` rather than in-place writes,
1941
            // and unlink only needs the *parent directory* to be writable, so a
1942
            // read-only file mode is safe for both overwrite-by-rename and
1943
            // eviction. Best-effort: a failure here (e.g. the temp file was
1944
            // already evicted out from under us) must not abort the emplace.
1945
            #[cfg(unix)]
1946
            {
1947
                use std::os::unix::fs::PermissionsExt;
1948
0
                if let Err(err) =
1949
2.33k
                    fs::set_permissions(&from_path, std::fs::Permissions::from_mode(0o444)).await
1950
                {
1951
0
                    warn!(?err, ?from_path, "Failed to set CAS blob read-only");
1952
2.33k
                }
1953
            }
1954
2.33k
            let result = (rename_fn)(&from_path, &final_path).err_tip(|| 
{2
1955
2
                format!(
1956
                    "Failed to rename temp file to final path {}",
1957
2
                    final_path.display()
1958
                )
1959
2
            });
1960
1961
2.33k
            if let Err(
err2
) = result {
1962
2
                error!(?err, ?from_path, ?final_path, "Failed to rename file",);
1963
                // This entry was never visible in the map. Drop cleans up its
1964
                // temporary file and the previous generation remains usable.
1965
2
                return Err(err);
1966
2.32k
            }
1967
2.32k
            trace!(?key, "Finished emplace file");
1968
2.32k
            encoded_file_path.path_type = PathType::Content;
1969
2.32k
            encoded_file_path.key = key.clone();
1970
            // Insertion can unref this entry immediately under cache pressure.
1971
2.32k
            drop(encoded_file_path);
1972
2.32k
            let (inserted, _) = evicting_map
1973
2.32k
                .insert_if(key.clone().into(), entry.clone(), |present, new| 
{12
1974
12
                    present.generation() < new.generation()
1975
12
                })
1976
2.32k
                .await;
1977
2.32k
            if !inserted {
1978
                // A newer upload completed first. Retire our unindexed content
1979
                // file too, so it cannot leak or reappear after a restart.
1980
1
                info!(%key, "Newer generation already emplaced, dropping");
1981
1
                entry.unref().await;
1982
2.32k
            }
1983
2.32k
            Ok(())
1984
2.34k
        })
1985
2.34k
        .await
1986
2.34k
        .err_tip(|| "Failed to create spawn in filesystem store update_file")
?0
1987
2.34k
    }
1988
1989
    /// Makes `file`'s contents durable with `sync_all` semantics.
1990
    ///
1991
    /// On macOS the naive equivalent (`fcntl(F_FULLFSYNC)` per file) is a
1992
    /// full device-cache flush that serializes at the device and dominates
1993
    /// many-small-blob uploads, so the flush is split into the cheap
1994
    /// per-file part (`fsync(2)`: push this file's pages to the device)
1995
    /// and a device-wide cache drain shared with every concurrent upload
1996
    /// via [`FlushCoalescer`]. Other platforms get their journal's group
1997
    /// commit for free and keep plain `sync_all`.
1998
2.06k
    async fn flush_durably(&self, file: &FileSlot) -> Result<(), Error> {
1999
        #[cfg(target_os = "macos")]
2000
        {
2001
            use std::os::fd::AsRawFd;
2002
            let Some(flush_coalescer) = &self.flush_coalescer else {
2003
                return file.as_ref().sync_all().await.map_err(Into::into);
2004
            };
2005
            // Dup the handle so the blocking task owns its own fd: if
2006
            // this future is cancelled mid-await, the temp file's fd can
2007
            // close and be reused, and an already-started blocking task
2008
            // must never fsync an unrelated descriptor.
2009
            let file_dup = file
2010
                .as_ref()
2011
                .try_clone()
2012
                .await
2013
                .err_tip(|| "Failed to dup fd for fsync in filesystem store")?
2014
                .into_std()
2015
                .await;
2016
            // Deliberately NOT via `fs::call_with_permit`: the caller's
2017
            // `FileSlot` already holds an open-file permit, so requiring a
2018
            // second one here deadlocks when the semaphore is drained (the
2019
            // exact scenario the #2051 regression test pins at one
2020
            // permit). Concurrency is still bounded: every in-flight fsync
2021
            // belongs to an upload holding a `FileSlot` permit.
2022
            spawn_blocking!("filesystem_store_fsync", move || {
2023
                loop {
2024
                    if unsafe { libc::fsync(file_dup.as_raw_fd()) } == 0 {
2025
                        return Ok(());
2026
                    }
2027
                    let err = std::io::Error::last_os_error();
2028
                    // std's sync_all retries EINTR (cvt_r); match it.
2029
                    if err.kind() != std::io::ErrorKind::Interrupted {
2030
                        return Err(Error::from(err).append("fsync failed in filesystem store"));
2031
                    }
2032
                }
2033
            })
2034
            .await
2035
            .err_tip(|| "Failed to join fsync task in filesystem store")??;
2036
            flush_coalescer.commit().await
2037
        }
2038
        #[cfg(not(target_os = "macos"))]
2039
        {
2040
2.06k
            file.as_ref().sync_all().await.map_err(Into::into)
2041
        }
2042
2.06k
    }
2043
2044
0
    pub fn get_eviction_snapshot(&self) -> EvictionSnapshot {
2045
0
        self.evicting_map.get_snapshot()
2046
0
    }
2047
2048
    // Only for tests, so we can run check_duplicate_files
2049
8
    pub fn get_evicting_map(&self) -> Arc<FsEvictingMap<'static, Fe>> {
2050
8
        self.evicting_map.clone()
2051
8
    }
2052
2053
    /// Returns the number of device-wide durability barriers issued by this
2054
    /// store's macOS flush coalescer. Exposed so the integration test can pin
2055
    /// the batching behavior rather than only checking data correctness.
2056
    #[cfg(target_os = "macos")]
2057
    pub fn full_flush_count_for_test(&self) -> Option<u64> {
2058
        self.flush_coalescer
2059
            .as_ref()
2060
            .map(|coalescer| coalescer.full_flush_count.load(Ordering::Relaxed))
2061
    }
2062
2063
    // Separated out so tests can use this
2064
2.07k
    pub async fn make_temp_file(
2065
2.07k
        &self,
2066
2.07k
        temp_key: StoreKey<'static>,
2067
2.07k
    ) -> Result<(Fe, FileSlot, OsString), Error> {
2068
2.07k
        let generation = self.get_and_update_generation()
?0
;
2069
2070
2.07k
        Fe::make_and_open_file(
2071
2.07k
            self.block_size,
2072
2.07k
            generation,
2073
2.07k
            EncodedFilePath {
2074
2.07k
                shared_context: self.shared_context.clone(),
2075
2.07k
                path_type: PathType::Temp,
2076
2.07k
                key: temp_key,
2077
2.07k
                generation,
2078
2.07k
                version: Version::V2,
2079
2.07k
            },
2080
2.07k
        )
2081
2.07k
        .await
2082
2.07k
    }
2083
}
2084
2085
#[async_trait]
2086
impl<Fe: FileEntry> StoreDriver for FilesystemStore<Fe> {
2087
0
    async fn post_init(self: Arc<Self>) -> Result<(), Error> {
2088
        Ok(())
2089
0
    }
2090
2091
9
    async fn remove(self: Pin<&Self>, key: StoreKey<'_>) -> Result<bool, Error> {
2092
        Ok(self.evicting_map.remove(&key.into_owned()).await)
2093
9
    }
2094
2095
    async fn has_with_results(
2096
        self: Pin<&Self>,
2097
        keys: &[StoreKey<'_>],
2098
        results: &mut [Option<u64>],
2099
2.82k
    ) -> Result<(), Error> {
2100
        let own_keys = keys
2101
            .iter()
2102
2.82k
            .map(|sk| sk.borrow().into_owned())
2103
            .collect::<Vec<_>>();
2104
        self.evicting_map
2105
            .sizes_for_keys(own_keys.iter(), results, false /* peek */)
2106
            .await;
2107
        // We need to do a special pass to ensure our zero files exist.
2108
        // If our results failed and the result was a zero file, we need to
2109
        // create the file by spec.
2110
        for (key, result) in keys.iter().zip(results.iter_mut()) {
2111
            if result.is_some() || !is_zero_digest(key.borrow()) {
2112
                continue;
2113
            }
2114
            let (mut tx, rx) = make_buf_channel_pair();
2115
            let send_eof_result = tx.send_eof();
2116
            self.update(key.borrow(), rx, UploadSizeInfo::ExactSize(0))
2117
                .await
2118
0
                .err_tip(|| format!("Failed to create zero file for key {}", key.as_str()))
2119
                .merge(
2120
                    send_eof_result
2121
                        .err_tip(|| "Failed to send zero file EOF in filesystem store has"),
2122
                )?;
2123
2124
            *result = Some(0);
2125
        }
2126
        Ok(())
2127
2.82k
    }
2128
2129
    async fn update(
2130
        self: Pin<&Self>,
2131
        key: StoreKey<'_>,
2132
        mut reader: DropCloserReadHalf,
2133
        upload_size: UploadSizeInfo,
2134
2.03k
    ) -> Result<u64, Error> {
2135
        if is_zero_digest(key.borrow()) {
2136
            let size = reader.drain().await?;
2137
            UploadSizeInfo::ExactSize(0).validate_size(size)?;
2138
            upload_size.validate_size(size)?;
2139
            return Ok(0);
2140
        }
2141
2142
        let temp_key = make_temp_key(&key);
2143
2144
        // There's a possibility of deadlock here where we take all of the
2145
        // file semaphores with make_and_open_file and the semaphores for
2146
        // whatever is populating reader is exhasted on the threads that
2147
        // have the FileSlots and not on those which can't.  To work around
2148
        // this we don't take the FileSlot until there's something on the
2149
        // reader available to know that the populator is active.
2150
        reader.peek().await?;
2151
2152
        let (entry, temp_file, temp_full_path) = self.make_temp_file(temp_key).await?;
2153
2154
        self.update_file(entry, temp_file, key.into_owned(), reader, upload_size)
2155
            .await
2156
6
            .err_tip(|| {
2157
6
                format!(
2158
                    "While processing with temp file {}",
2159
6
                    temp_full_path.display()
2160
                )
2161
6
            })
2162
2.03k
    }
2163
2164
2.22k
    fn optimized_for(&self, optimization: StoreOptimizations) -> bool {
2165
1.95k
        matches!(
2166
2.22k
            optimization,
2167
            StoreOptimizations::FileUpdates | StoreOptimizations::SubscribesToUpdateOneshot
2168
        )
2169
2.22k
    }
2170
2171
302
    async fn update_oneshot(self: Pin<&Self>, key: StoreKey<'_>, data: Bytes) -> Result<(), Error> {
2172
        if is_zero_digest(key.borrow()) {
2173
            UploadSizeInfo::ExactSize(0).validate_size(data.len() as u64)?;
2174
            return Ok(());
2175
        }
2176
2177
        let temp_key = make_temp_key(&key);
2178
        let (mut entry, mut temp_file, temp_full_path) = self
2179
            .make_temp_file(temp_key)
2180
            .await
2181
            .err_tip(|| "Failed to create temp file in filesystem store update_oneshot")?;
2182
2183
        // Write directly without channel overhead
2184
        if !data.is_empty() {
2185
            temp_file
2186
                .write_all(&data)
2187
                .await
2188
0
                .err_tip(|| format!("Failed to write data to {}", temp_full_path.display()))?;
2189
        }
2190
2191
        let _permit = if let Some(sem) = &self.write_semaphore {
2192
0
            Some(sem.acquire().await.map_err(|err| {
2193
0
                Error::from_std_err(Code::Internal, &err).append("Write semaphore closed")
2194
0
            })?)
2195
        } else {
2196
            None
2197
        };
2198
2199
        // See comment in `update_file` above.
2200
        temp_file
2201
            .flush()
2202
            .await
2203
            .err_tip(|| "Failed to flush in filesystem store update_oneshot")?;
2204
        self.flush_durably(&temp_file)
2205
            .await
2206
            .err_tip(|| "Failed to sync in filesystem store update_oneshot")?;
2207
2208
        drop(_permit);
2209
2210
        if self.evict_page_cache {
2211
            temp_file.advise_dontneed();
2212
        }
2213
        drop(temp_file);
2214
2215
        *entry.data_size_mut() = data.len() as u64;
2216
        self.emplace_file(key.into_owned(), Arc::new(entry)).await
2217
302
    }
2218
2219
    async fn update_with_whole_file(
2220
        self: Pin<&Self>,
2221
        key: StoreKey<'_>,
2222
        path: OsString,
2223
        mut file: FileSlot,
2224
        upload_size: UploadSizeInfo,
2225
277
    ) -> Result<(u64, Option<FileSlot>), Error> {
2226
        file.flush()
2227
            .await
2228
            .err_tip(|| "Flushing whole-file upload before checking its size")?;
2229
        let file_size = file
2230
            .as_ref()
2231
            .metadata()
2232
            .await
2233
0
            .err_tip(|| format!("While reading metadata for {}", path.display()))?
2234
            .len();
2235
        upload_size.validate_size(file_size)?;
2236
        if is_zero_digest(key.borrow()) {
2237
            UploadSizeInfo::ExactSize(0).validate_size(file_size)?;
2238
            // Empty CAS blobs are implicit; ordinary empty values still need a file.
2239
            return Ok((0, None));
2240
        }
2241
        let generation = self.get_and_update_generation()?;
2242
        let entry = Fe::create(
2243
            file_size,
2244
            self.block_size,
2245
            generation,
2246
            RwLock::new(EncodedFilePath {
2247
                shared_context: self.shared_context.clone(),
2248
                path_type: PathType::Custom(path),
2249
                key: key.borrow().into_owned(),
2250
                generation,
2251
                version: Version::V2,
2252
            }),
2253
        );
2254
        // We are done with the file, if we hold a reference to the file here, it could
2255
        // result in a deadlock if `emplace_file()` also needs file descriptors.
2256
        trace!(?file, "Dropping file to to update_with_whole_file");
2257
        if self.evict_page_cache {
2258
            file.advise_dontneed();
2259
        }
2260
        drop(file);
2261
        self.emplace_file(key.into_owned(), Arc::new(entry))
2262
            .await
2263
            .err_tip(|| "Could not move file into store in upload_file_to_store, maybe dest is on different volume?")?;
2264
        return Ok((file_size, None));
2265
277
    }
2266
2267
    async fn get_part(
2268
        self: Pin<&Self>,
2269
        key: StoreKey<'_>,
2270
        writer: &mut DropCloserWriteHalf,
2271
        offset: u64,
2272
        length: Option<u64>,
2273
856
    ) -> Result<(), Error> {
2274
        if is_zero_digest(key.borrow()) {
2275
            self.has(key.borrow())
2276
                .await
2277
                .err_tip(|| "Failed to check if zero digest exists in filesystem store")?;
2278
            writer
2279
                .send_eof()
2280
                .err_tip(|| "Failed to send zero EOF in filesystem store get_part")?;
2281
            return Ok(());
2282
        }
2283
        let owned_key = key.into_owned();
2284
2
        let entry = self.evicting_map.get(&owned_key).await.ok_or_else(|| {
2285
2
            make_err!(
2286
2
                Code::NotFound,
2287
                "{} not found in filesystem store here",
2288
2
                owned_key.as_str()
2289
            )
2290
2
        })?;
2291
        let read_limit = length.unwrap_or(u64::MAX);
2292
        let mut temp_file = match entry.read_file_part(offset, read_limit).await {
2293
            Ok(file) => file,
2294
            Err(err) => {
2295
                // If the file is not found, we need to remove it from the eviction map.
2296
                if err.code == Code::NotFound {
2297
                    // Map said the file was present but `open()` hit ENOENT.
2298
                    // Self-heals: we remove the stale entry below and a
2299
                    // fast/slow caller re-populates from the slow store, so
2300
                    // this is a recoverable warn, not a fatal error.
2301
                    warn!(
2302
                        ?err,
2303
                        key = ?owned_key,
2304
                        "Filesystem store map/disk divergence: removing entry; reader will fall through to slow store",
2305
                    );
2306
                    self.evicting_map
2307
5
                        .remove_if(&owned_key, |map_entry| Arc::ptr_eq(map_entry, &entry))
2308
                        .await;
2309
                }
2310
                return Err(err);
2311
            }
2312
        };
2313
2314
        loop {
2315
            let mut buf = BytesMut::with_capacity(self.read_buffer_size);
2316
            temp_file
2317
                .read_buf(&mut buf)
2318
                .await
2319
                .err_tip(|| "Failed to read data in filesystem store")?;
2320
            if buf.is_empty() {
2321
                break; // EOF.
2322
            }
2323
            writer
2324
                .send(buf.freeze())
2325
                .await
2326
                .err_tip(|| "Failed to send chunk in filesystem store get_part")?;
2327
        }
2328
        if self.evict_page_cache {
2329
            temp_file.get_ref().advise_dontneed();
2330
        }
2331
        writer
2332
            .send_eof()
2333
            .err_tip(|| "Filed to send EOF in filesystem store get_part")?;
2334
2335
        Ok(())
2336
856
    }
2337
2338
2.08k
    fn inner_store(&self, _digest: Option<StoreKey>) -> &dyn StoreDriver {
2339
2.08k
        self
2340
2.08k
    }
2341
2342
126
    fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) {
2343
126
        self
2344
126
    }
2345
2346
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> {
2347
0
        self
2348
0
    }
2349
2350
0
    fn register_health(self: Arc<Self>, registry: &mut HealthRegistryBuilder) {
2351
0
        registry.register_indicator(self);
2352
0
    }
2353
2354
0
    fn enable_cache_size_metrics(&self, attrs: &[opentelemetry::KeyValue]) -> bool {
2355
0
        self.evicting_map.enable_cache_size_metrics(attrs.to_vec());
2356
0
        true
2357
0
    }
2358
2359
0
    fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> {
2360
0
        self.evicting_map
2361
0
            .add_remove_callback(RemoveCallbackHolder::new(callback));
2362
0
        Ok(())
2363
0
    }
2364
}
2365
2366
#[async_trait]
2367
impl<Fe: FileEntry> HealthStatusIndicator for FilesystemStore<Fe> {
2368
0
    fn get_name(&self) -> &'static str {
2369
0
        "FilesystemStore"
2370
0
    }
2371
2372
    /// Lightweight probe: `stat()` the `content_path` directory. No
2373
    /// write-semaphore / eviction-map contention with production
2374
    /// traffic, and bounded so a hung NFS / EBS mount can't wedge the
2375
    /// indicator.
2376
2
    async fn check_health(&self, _namespace: Cow<'static, str>) -> HealthStatus {
2377
        const HEALTH_PROBE_TIMEOUT: Duration = Duration::from_secs(2);
2378
2379
        let content_path = &self.shared_context.content_path;
2380
        let stat = tokio::fs::metadata(&content_path);
2381
        match timeout(HEALTH_PROBE_TIMEOUT, stat).await {
2382
            Ok(Ok(meta)) if meta.is_dir() => {
2383
                HealthStatus::new_ok(self, "FilesystemStore::check_health: ok".into())
2384
            }
2385
            Ok(Ok(_)) => HealthStatus::new_failed(
2386
                self,
2387
                format!(
2388
                    "FilesystemStore::check_health: content_path {content_path} is not a directory"
2389
                )
2390
                .into(),
2391
            ),
2392
            Ok(Err(e)) => {
2393
                warn!(
2394
                    ?e,
2395
                    %content_path,
2396
                    "FilesystemStore::check_health: stat errored",
2397
                );
2398
                HealthStatus::new_failed(
2399
                    self,
2400
                    format!("FilesystemStore::check_health: stat errored: {e}").into(),
2401
                )
2402
            }
2403
            Err(_) => {
2404
                warn!(
2405
                    %content_path,
2406
                    timeout_secs = HEALTH_PROBE_TIMEOUT.as_secs(),
2407
                    "FilesystemStore::check_health: stat timed out",
2408
                );
2409
                HealthStatus::Timeout {
2410
                    struct_name: self.struct_name(),
2411
                }
2412
            }
2413
        }
2414
2
    }
2415
}