Coverage Report

Created: 2026-09-18 20:40

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
3.25k
    pub const fn new(generation: u64) -> Self {
123
3.25k
        Self(generation)
124
3.25k
    }
125
126
1.03k
    pub const fn inner(&self) -> u64 {
127
1.03k
        self.0
128
1.03k
    }
129
}
130
131
impl Display for Generation {
132
9.05k
    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
133
9.05k
        write!(f, "{}", self.0)
134
9.05k
    }
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
7.28k
    fn get_file_path(&self) -> Cow<'_, OsStr> {
154
7.28k
        get_file_path_raw(
155
7.28k
            &self.path_type,
156
7.28k
            self.shared_context.as_ref(),
157
7.28k
            &self.key,
158
7.28k
            self.generation,
159
7.28k
            self.version,
160
        )
161
7.28k
    }
162
}
163
164
#[inline]
165
9.04k
fn get_file_path_raw<'a>(
166
9.04k
    path_type: &'a PathType,
167
9.04k
    shared_context: &SharedContext,
168
9.04k
    key: &StoreKey<'a>,
169
9.04k
    generation: Generation,
170
9.04k
    version: Version,
171
9.04k
) -> Cow<'a, OsStr> {
172
9.04k
    let 
folder9.01k
= match path_type {
173
3.71k
        PathType::Content => &shared_context.content_path,
174
5.30k
        PathType::Temp => &shared_context.temp_path,
175
30
        PathType::Custom(path) => return Cow::Borrowed(path),
176
    };
177
9.01k
    Cow::Owned(to_full_path_from_key(folder, key, generation, version))
178
9.04k
}
179
180
impl Drop for EncodedFilePath {
181
2.00k
    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.00k
        if self.path_type == PathType::Content {
185
1.95k
            return;
186
50
        }
187
188
50
        let file_path = self.get_file_path().to_os_string();
189
50
        let shared_context = self.shared_context.clone();
190
        // .fetch_add returns previous value, so we add one to get approximate current value
191
50
        let current_active_drop_spawns = shared_context
192
50
            .active_drop_spawns
193
50
            .fetch_add(1, Ordering::Relaxed)
194
50
            + 1;
195
50
        debug!(
196
            %current_active_drop_spawns,
197
            ?file_path,
198
            "Spawned a filesystem_delete_file"
199
        );
200
50
        background_spawn!("filesystem_delete_file", async move 
{42
201
42
            match fs::remove_file(&file_path).await {
202
30
                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
5
                Err(err) if err.code == Code::NotFound => {
207
5
                    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
35
            let current_active_drop_spawns = shared_context
213
35
                .active_drop_spawns
214
35
                .fetch_sub(1, Ordering::Relaxed)
215
35
                - 1;
216
35
            debug!(
217
                ?current_active_drop_spawns,
218
                ?file_path,
219
                "Dropped a filesystem_delete_file"
220
            );
221
35
        });
222
2.00k
    }
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
9.05k
fn to_full_path_from_key(
237
9.05k
    folder: &str,
238
9.05k
    key: &StoreKey<'_>,
239
9.05k
    generation: Generation,
240
9.05k
    version: Version,
241
9.05k
) -> OsString {
242
9.05k
    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
70
        (StoreKey::Str(str), Version::V2) => {
246
70
            format!("{folder}/{STR_FOLDER_V2}/{str}-{generation}")
247
        }
248
3
        (StoreKey::Str(str), Version::V1) => format!("{folder}/{STR_FOLDER_V1}/{str}"),
249
8.98k
        (StoreKey::Digest(digest_info), Version::V2) => {
250
8.98k
            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
9.05k
    .into()
257
9.05k
}
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
21
    pub fn get_shared_context_for_test(&mut self) -> Arc<SharedContext> {
325
21
        self.encoded_file_path.get_mut().shared_context.clone()
326
21
    }
327
}
328
329
impl FileEntry for FileEntryImpl {
330
2.00k
    fn create(
331
2.00k
        data_size: u64,
332
2.00k
        block_size: u64,
333
2.00k
        generation: Generation,
334
2.00k
        encoded_file_path: RwLock<EncodedFilePath>,
335
2.00k
    ) -> Self {
336
2.00k
        Self {
337
2.00k
            data_size,
338
2.00k
            block_size,
339
2.00k
            encoded_file_path,
340
2.00k
            generation,
341
2.00k
        }
342
2.00k
    }
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
1.75k
    async fn make_and_open_file(
348
1.75k
        block_size: u64,
349
1.75k
        generation: Generation,
350
1.75k
        encoded_file_path: EncodedFilePath,
351
1.75k
    ) -> Result<(Self, FileSlot, OsString), Error> {
352
1.75k
        let temp_full_path = encoded_file_path.get_file_path().to_os_string();
353
1.75k
        let temp_file_result = fs::create_file(temp_full_path.clone())
354
1.75k
            .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
1.75k
            .await
?0
;
373
374
1.75k
        Ok((
375
1.75k
            <Self as FileEntry>::create(
376
1.75k
                0, /* Unknown yet, we will fill it in later */
377
1.75k
                block_size,
378
1.75k
                generation,
379
1.75k
                RwLock::new(encoded_file_path),
380
1.75k
            ),
381
1.75k
            temp_file_result,
382
1.75k
            temp_full_path,
383
1.75k
        ))
384
1.75k
    }
385
386
82
    fn data_size(&self) -> u64 {
387
82
        self.data_size
388
82
    }
389
390
26
    fn generation(&self) -> Generation {
391
26
        self.generation
392
26
    }
393
394
1.75k
    fn data_size_mut(&mut self) -> &mut u64 {
395
1.75k
        &mut self.data_size
396
1.75k
    }
397
398
4.99k
    fn size_on_disk(&self) -> u64 {
399
4.99k
        self.data_size.div_ceil(self.block_size) * self.block_size
400
4.99k
    }
401
402
5.44k
    fn get_encoded_file_path(&self) -> &RwLock<EncodedFilePath> {
403
5.44k
        &self.encoded_file_path
404
5.44k
    }
405
406
623
    fn read_file_part(
407
623
        &self,
408
623
        offset: u64,
409
623
        length: u64,
410
623
    ) -> impl Future<Output = Result<Take<FileSlot>, Error>> + Send {
411
623
        self.get_file_path_locked(move |full_content_path| async move {
412
623
            let 
file618
= fs::open_file(&full_content_path, offset, length)
413
623
                .await
414
623
                .err_tip(|| 
{5
415
5
                    format!(
416
                        "Failed to open file in filesystem store {}",
417
5
                        full_content_path.display()
418
                    )
419
5
                })?;
420
618
            Ok(file)
421
1.24k
        })
422
623
    }
423
424
1.88k
    async fn get_file_path_locked<
425
1.88k
        T,
426
1.88k
        Fut: Future<Output = Result<T, Error>> + Send,
427
1.88k
        F: FnOnce(OsString) -> Fut + Send,
428
1.88k
    >(
429
1.88k
        &self,
430
1.88k
        handler: F,
431
1.88k
    ) -> Result<T, Error> {
432
1.88k
        let encoded_file_path = self.get_encoded_file_path().read().await;
433
1.88k
        handler(encoded_file_path.get_file_path().to_os_string()).await
434
1.88k
    }
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
1.79k
fn make_temp_digest(mut digest: DigestInfo) -> DigestInfo {
447
    static DELETE_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
448
1.79k
    let mut hash = *digest.packed_hash();
449
1.79k
    hash[24..].clone_from_slice(
450
1.79k
        &DELETE_FILE_COUNTER
451
1.79k
            .fetch_add(1, Ordering::Relaxed)
452
1.79k
            .to_le_bytes(),
453
    );
454
1.79k
    digest.set_packed_hash(*hash);
455
1.79k
    digest
456
1.79k
}
457
458
1.79k
pub fn make_temp_key(key: &StoreKey) -> StoreKey<'static> {
459
1.79k
    StoreKey::Digest(make_temp_digest(key.borrow().into_digest()))
460
1.79k
}
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
133
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
133
    let executable_dir = format!("{content_path}{EXECUTABLE_DIR_SUFFIX}");
701
133
    let executable_digest_dir = format!("{executable_dir}/{DIGEST_FOLDER_V2}");
702
133
    spawn_blocking!("filesystem_store_prepare_executable_dir", move || {
703
133
        match std::fs::remove_dir_all(&executable_dir) {
704
7
            Ok(()) => {}
705
126
            Err(
err125
) if err.kind() == std::io::ErrorKind::NotFoun
d125
=>
{}125
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
132
        match std::fs::create_dir_all(&executable_digest_dir) {
717
132
            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
133
    })
724
133
    .await
725
133
    .err_tip(|| "Failed to join executable-dir preparation task")
?0
726
133
}
727
728
impl LenEntry for FileEntryImpl {
729
    #[inline]
730
4.99k
    fn len(&self) -> u64 {
731
4.99k
        self.size_on_disk()
732
4.99k
    }
733
734
0
    fn is_empty(&self) -> bool {
735
0
        self.data_size == 0
736
0
    }
737
738
    // unref() only triggers when an item is removed from the eviction_map. It is possible
739
    // that another place in code has a reference to `FileEntryImpl` and may later read the
740
    // file. To support this edge case, we first move the file to a temp file and point
741
    // target file location to the new temp file. `unref()` should only ever be called once.
742
    #[inline]
743
40
    async fn unref(&self) {
744
40
        let mut encoded_file_path = self.encoded_file_path.write().await;
745
40
        if encoded_file_path.path_type == PathType::Temp {
746
            // We are already a temp file that is now marked for deletion on drop.
747
            // This is very rare, but most likely the rename into the content path failed.
748
1
            warn!(
749
1
                key = ?encoded_file_path.key,
750
                "File is already a temp file",
751
            );
752
1
            return;
753
39
        }
754
39
        let from_path = encoded_file_path.get_file_path();
755
39
        let new_key = make_temp_key(&encoded_file_path.key);
756
757
39
        let to_path = to_full_path_from_key(
758
39
            &encoded_file_path.shared_context.temp_path,
759
39
            &new_key,
760
39
            encoded_file_path.generation,
761
            // Legacy files may survive a failed migration, but only the new
762
            // temporary directories are guaranteed to exist on this startup.
763
39
            Version::V2,
764
        );
765
766
39
        if let Err(
err10
) = fs::rename(&from_path, &to_path).await {
767
            // ENOENT from rename is ambiguous: the source may be gone, or
768
            // a directory component of the destination (the temp dir) may
769
            // be missing. Confirm the source is genuinely gone before
770
            // treating it as benign — otherwise a removed temp dir would
771
            // flip an intact content file to Temp and orphan it on disk.
772
10
            let source_gone = err.code == Code::NotFound
773
9
                && matches!(
774
10
                    fs::metadata(&from_path).await,
775
9
                    Err(meta_err) if meta_err.code == Code::NotFound
776
                );
777
10
            if source_gone {
778
                // The file is already gone — typically another thread's
779
                // eviction beat us, or the entry never got its file on
780
                // disk. Benign here, and it dominates log volume under
781
                // heavy write+evict concurrency, so keep it at `debug`.
782
                // Mark the entry Temp (as a successful rename would) so a
783
                // repeat unref is a no-op and drop stops claiming the
784
                // content path.
785
9
                debug!(
786
9
                    key = ?encoded_file_path.key,
787
                    "Failed to rename file (already gone, treating as benign)",
788
                );
789
9
                encoded_file_path.path_type = PathType::Temp;
790
9
                encoded_file_path.key = new_key;
791
9
                encoded_file_path.version = Version::V2;
792
            } else {
793
                // Either a non-ENOENT failure (EACCES, EXDEV, EBUSY, …) or
794
                // ENOENT with the source still present (missing temp dir).
795
                // The content file is intact; leave the entry as Content.
796
1
                warn!(
797
1
                    key = ?encoded_file_path.key,
798
                    ?from_path,
799
                    ?to_path,
800
                    ?err,
801
                    "Failed to rename file",
802
                );
803
            }
804
        } else {
805
29
            debug!(
806
29
                key = ?encoded_file_path.key,
807
                ?from_path,
808
                ?to_path,
809
                "Renamed file (unref)",
810
            );
811
29
            encoded_file_path.path_type = PathType::Temp;
812
29
            encoded_file_path.key = new_key;
813
29
            encoded_file_path.version = Version::V2;
814
        }
815
40
    }
816
}
817
818
#[inline]
819
412
fn digest_from_filename(file_name: &str) -> Result<DigestInfo, Error> {
820
412
    let (hash, size) = file_name.split_once('-').err_tip(|| "")
?0
;
821
412
    let size = size.parse::<i64>()
?0
;
822
412
    DigestInfo::try_new(hash, size)
823
412
}
824
825
3
fn key_from_file_v1(file_name: &str, file_type: FileType) -> Result<StoreKey<'_>, Error> {
826
3
    match file_type {
827
2
        FileType::String => Ok(StoreKey::new_str(file_name)),
828
1
        FileType::Digest => digest_from_filename(file_name).map(StoreKey::Digest),
829
    }
830
3
}
831
832
459
pub fn key_and_generation_from_file_v2(
833
459
    file_name: &str,
834
459
    file_type: FileType,
835
459
) -> Result<(StoreKey<'_>, Generation), Error> {
836
459
    let (key, generation) = file_name.rsplit_once('-').err_tip(|| "")
?0
;
837
459
    let generation = Generation::new(generation.parse::<u64>()
?0
);
838
839
459
    let key = match file_type {
840
48
        FileType::String => StoreKey::new_str(key),
841
411
        FileType::Digest => digest_from_filename(key).map(StoreKey::Digest)
?0
,
842
    };
843
844
459
    Ok((key, generation))
845
459
}
846
847
/// The number of files to read the metadata for at the same time when running
848
/// `add_files_to_cache`.
849
const SIMULTANEOUS_METADATA_READS: usize = 200;
850
851
type FsEvictingMap<'a, Fe> =
852
    EvictingMap<StoreKeyBorrow, StoreKey<'a>, Arc<Fe>, SystemTime, RemoveCallbackHolder>;
853
854
133
async fn add_files_to_cache<Fe: FileEntry>(
855
133
    evicting_map: &FsEvictingMap<'_, Fe>,
856
133
    anchor_time: &SystemTime,
857
133
    shared_context: &Arc<SharedContext>,
858
133
    block_size: u64,
859
133
    rename_fn: fn(&OsStr, &OsStr) -> Result<(), std::io::Error>,
860
133
    migrate: bool,
861
133
) -> Result<Generation, Error> {
862
    #[expect(clippy::too_many_arguments)]
863
237
    async fn process_entry<Fe: FileEntry>(
864
237
        evicting_map: &FsEvictingMap<'_, Fe>,
865
237
        file_name: &str,
866
237
        file_type: FileType,
867
237
        version: Version,
868
237
        atime: SystemTime,
869
237
        data_size: u64,
870
237
        block_size: u64,
871
237
        anchor_time: &SystemTime,
872
237
        shared_context: &Arc<SharedContext>,
873
237
    ) -> Result<Generation, Error> {
874
237
        let (key, generation) = match version {
875
            Version::Flat | Version::V1 => {
876
3
                (key_from_file_v1(file_name, file_type)
?0
, Generation::new(0))
877
            }
878
234
            Version::V2 => key_and_generation_from_file_v2(file_name, file_type)
?0
,
879
        };
880
881
237
        let file_entry = Arc::new(Fe::create(
882
237
            data_size,
883
237
            block_size,
884
237
            generation,
885
237
            RwLock::new(EncodedFilePath {
886
237
                shared_context: shared_context.clone(),
887
237
                path_type: PathType::Content,
888
237
                key: key.borrow().into_owned(),
889
237
                generation,
890
237
                version,
891
237
            }),
892
        ));
893
237
        let time_since_anchor = if let Ok(
d236
) = anchor_time.duration_since(atime) {
894
236
            d
895
        } else {
896
1
            warn!(
897
                %file_name,
898
1
                atime = %humantime::format_rfc3339(atime),
899
1
                anchor_time = %humantime::format_rfc3339(*anchor_time),
900
                "File access time newer than FilesystemStore start time",
901
            );
902
1
            Duration::ZERO
903
        };
904
237
        let (inserted, _) = evicting_map
905
237
            .insert_with_time_if(
906
237
                key.into_owned().into(),
907
237
                file_entry.clone(),
908
1
                |present_entry, new_entry| present_entry.generation() < new_entry.generation(),
909
237
                i32::try_from(time_since_anchor.as_secs()).unwrap_or(i32::MAX),
910
            )
911
237
            .await;
912
237
        if !inserted {
913
            // Older generations can survive a crash before deferred cleanup.
914
            // Retire rejected files too so the next restart cannot revive them.
915
0
            file_entry.unref().await;
916
237
        }
917
237
        Ok(generation)
918
237
    }
919
920
1.06k
    async fn read_files(
921
1.06k
        folder: Option<&str>,
922
1.06k
        shared_context: &SharedContext,
923
1.06k
    ) -> Result<Vec<(String, SystemTime, u64, bool)>, Error> {
924
        // Note: In Dec 2024 this is for backwards compatibility with the old
925
        // way files were stored on disk. Previously all files were in a single
926
        // folder regardless of the StoreKey type. This allows old versions of
927
        // nativelink file version to be upgraded at startup time.
928
        // This logic can be removed once more time has passed.
929
1.06k
        let read_dir = folder.map_or_else(
930
133
            || format!("{}/", shared_context.content_path),
931
931
            |folder| format!("{}/{folder}/", shared_context.content_path),
932
        );
933
934
1.06k
        let (
_permit558
,
dir_handle558
) = match fs::read_dir(read_dir).await {
935
558
            Ok(dir_handle) => dir_handle.into_inner(),
936
506
            Err(err) if err.code == Code::NotFound => return Ok(Vec::new()),
937
0
            Err(err) => {
938
0
                return Err(err).err_tip(
939
                    || "Failed opening content directory for iterating in filesystem store",
940
                );
941
            }
942
        };
943
944
558
        let read_dir_stream = ReadDirStream::new(dir_handle);
945
558
        read_dir_stream
946
813
            .
map558
(|dir_entry| async move {
947
813
                let dir_entry = dir_entry.unwrap();
948
813
                let file_name = dir_entry.file_name().into_string().unwrap();
949
813
                let metadata = dir_entry
950
813
                    .metadata()
951
813
                    .await
952
813
                    .err_tip(|| "Failed to get metadata in filesystem store")
?0
;
953
                // We need to filter out folders - we do not want to try to cache
954
                // the per-version key folders.
955
813
                let is_file = metadata.is_file()
956
558
                    || ![
957
558
                        STR_FOLDER_V2,
958
558
                        DIGEST_FOLDER_V2,
959
558
                        STR_FOLDER_V1,
960
558
                        DIGEST_FOLDER_V1,
961
558
                    ]
962
558
                    .contains(&file_name.as_str());
963
                // Using access time is not perfect, but better than random. We do not update the
964
                // atime when a file is actually "touched", we rely on whatever the filesystem does
965
                // when we read the file (usually update on read).
966
813
                let atime = metadata
967
813
                    .accessed()
968
813
                    .or_else(|_| 
metadata0
.
modified0
())
969
813
                    .unwrap_or(SystemTime::UNIX_EPOCH);
970
813
                Result::<(String, SystemTime, u64, bool), Error>::Ok((
971
813
                    file_name,
972
813
                    atime,
973
813
                    metadata.len(),
974
813
                    is_file,
975
813
                ))
976
1.62k
            })
977
558
            .buffer_unordered(SIMULTANEOUS_METADATA_READS)
978
558
            .try_collect()
979
558
            .await
980
1.06k
    }
981
982
    /// Best effort: a failure leaves the source where it is, so the loader
983
    /// still picks it up and a later startup retries.
984
18
    fn migrate_file(
985
18
        from_file: &OsStr,
986
18
        to_file: &OsStr,
987
18
        rename_fn: fn(&OsStr, &OsStr) -> Result<(), std::io::Error>,
988
18
    ) {
989
18
        if let Err(
err3
) = rename_fn(from_file, to_file) {
990
3
            warn!(?from_file, ?to_file, ?err, "Failed to migrate file");
991
        } else {
992
15
            debug!(?from_file, ?to_file, "Migrated file");
993
        }
994
18
    }
995
996
    /// Note: In Dec 2024 this is for backwards compatibility with the old
997
    /// way files were stored on disk. Previously all files were in a single
998
    /// folder regardless of the [`StoreKey`] type.
999
133
    async fn migrate_old_cache_1(
1000
133
        shared_context: &Arc<SharedContext>,
1001
133
        rename_fn: fn(&OsStr, &OsStr) -> Result<(), std::io::Error>,
1002
133
    ) -> Result<(), Error> {
1003
133
        let file_infos = read_files(None, shared_context).await
?0
;
1004
133
        let from_path = &shared_context.content_path;
1005
133
        let to_path = format!("{}/{DIGEST_FOLDER_V2}", shared_context.content_path);
1006
1007
133
        for (
file_name3
, _, _, _) in file_infos.into_iter().filter(|x| x.3) {
1008
3
            migrate_file(
1009
3
                &OsString::from(format!("{from_path}/{file_name}")),
1010
3
                &OsString::from(format!("{to_path}/{file_name}-0")),
1011
3
                rename_fn,
1012
3
            );
1013
3
        }
1014
133
        Ok(())
1015
133
    }
1016
1017
    /// Moves [`Version::V1`] folders into [`Version::V2`].
1018
133
    async fn migrate_old_cache_2(
1019
133
        shared_context: &Arc<SharedContext>,
1020
133
        rename_fn: fn(&OsStr, &OsStr) -> Result<(), std::io::Error>,
1021
133
    ) -> Result<(), Error> {
1022
266
        for (legacy_folder, folder) in [
1023
133
            (DIGEST_FOLDER_V1, DIGEST_FOLDER_V2),
1024
133
            (STR_FOLDER_V1, STR_FOLDER_V2),
1025
133
        ] {
1026
266
            let file_infos = read_files(Some(legacy_folder), shared_context).await
?0
;
1027
266
            let from_path = format!("{}/{legacy_folder}", shared_context.content_path);
1028
266
            let to_path = format!("{}/{folder}", shared_context.content_path);
1029
1030
266
            for (
file_name15
, _, _, _) in file_infos.into_iter().filter(|x| x.3) {
1031
15
                migrate_file(
1032
15
                    &OsString::from(format!("{from_path}/{file_name}")),
1033
15
                    &OsString::from(format!("{to_path}/{file_name}-0")),
1034
15
                    rename_fn,
1035
15
                );
1036
15
            }
1037
        }
1038
133
        Ok(())
1039
133
    }
1040
1041
665
    async fn add_folder_to_cache<Fe: FileEntry>(
1042
665
        evicting_map: &FsEvictingMap<'_, Fe>,
1043
665
        anchor_time: &SystemTime,
1044
665
        shared_context: &Arc<SharedContext>,
1045
665
        block_size: u64,
1046
665
        folder: &str,
1047
665
        file_type: FileType,
1048
665
        version: Version,
1049
665
    ) -> Result<Generation, Error> {
1050
665
        let mut file_infos = read_files(Some(folder), shared_context).await
?0
;
1051
        // Load older generations first. Otherwise cache pressure could evict a
1052
        // newer generation before an older file for that key is encountered.
1053
665
        if version == Version::V2 {
1054
266
            file_infos.sort_by_cached_key(|(file_name, ..)| 
{224
1055
224
                key_and_generation_from_file_v2(file_name, file_type)
1056
224
                    .map_or(Generation::new(0), |(_, generation)| generation)
1057
224
            });
1058
399
        }
1059
1060
665
        let path_root = format!("{}/{folder}", shared_context.content_path);
1061
1062
665
        let mut max_generation = 0;
1063
1064
665
        for (
file_name237
,
atime237
,
data_size237
, _) in file_infos.into_iter().filter(|x| x.3) {
1065
237
            let result = process_entry(
1066
237
                evicting_map,
1067
237
                &file_name,
1068
237
                file_type,
1069
237
                version,
1070
237
                atime,
1071
237
                data_size,
1072
237
                block_size,
1073
237
                anchor_time,
1074
237
                shared_context,
1075
237
            )
1076
237
            .await;
1077
1078
237
            match result {
1079
237
                Ok(generation) => {
1080
237
                    max_generation = cmp::max(max_generation, generation.inner());
1081
237
                }
1082
0
                Err(err) => {
1083
0
                    warn!(?file_name, ?err, "Failed to add file to eviction cache",);
1084
                    // Ignore result.
1085
0
                    drop(fs::remove_file(format!("{path_root}/{file_name}")).await);
1086
                }
1087
            }
1088
        }
1089
665
        Ok(Generation::new(max_generation))
1090
665
    }
1091
1092
133
    if migrate {
1093
133
        migrate_old_cache_1(shared_context, rename_fn).await
?0
;
1094
133
        migrate_old_cache_2(shared_context, rename_fn).await
?0
;
1095
0
    }
1096
1097
133
    let mut max_generation = 0;
1098
665
    for (folder, file_type, version) in [
1099
133
        ("", FileType::Digest, Version::Flat),
1100
133
        (DIGEST_FOLDER_V1, FileType::Digest, Version::V1),
1101
133
        (STR_FOLDER_V1, FileType::String, Version::V1),
1102
133
        (DIGEST_FOLDER_V2, FileType::Digest, Version::V2),
1103
133
        (STR_FOLDER_V2, FileType::String, Version::V2),
1104
133
    ] {
1105
665
        let generation = add_folder_to_cache(
1106
665
            evicting_map,
1107
665
            anchor_time,
1108
665
            shared_context,
1109
665
            block_size,
1110
665
            folder,
1111
665
            file_type,
1112
665
            version,
1113
665
        )
1114
665
        .await
?0
;
1115
665
        max_generation = cmp::max(max_generation, generation.inner());
1116
    }
1117
1118
133
    Ok(Generation::new(max_generation.checked_add(1).ok_or_else(
1119
0
        || {
1120
0
            make_err!(
1121
0
                Code::ResourceExhausted,
1122
                "Filesystem generation counter exhausted"
1123
            )
1124
0
        },
1125
0
    )?))
1126
133
}
1127
1128
133
async fn prune_temp_path(temp_path: &str) -> Result<(), Error> {
1129
532
    async fn prune_temp_inner(temp_path: &str, subpath: &str) -> Result<(), Error> {
1130
532
        let (
_permit266
,
dir_handle266
) = match fs::read_dir(format!("{temp_path}/{subpath}")).await {
1131
266
            Ok(dir_handle) => dir_handle.into_inner(),
1132
266
            Err(err) if err.code == Code::NotFound => return Ok(()),
1133
0
            Err(err) => {
1134
0
                return Err(err).err_tip(|| {
1135
0
                    "Failed opening temp directory to prune partial downloads in filesystem store"
1136
0
                });
1137
            }
1138
        };
1139
1140
266
        let mut read_dir_stream = ReadDirStream::new(dir_handle);
1141
266
        while let Some(
dir_entry0
) = read_dir_stream.next().await {
1142
0
            let path = dir_entry?.path();
1143
0
            if let Err(err) = fs::remove_file(&path).await {
1144
0
                warn!(?path, ?err, "Failed to delete file",);
1145
0
            }
1146
        }
1147
266
        Ok(())
1148
532
    }
1149
1150
532
    for folder in [
1151
133
        STR_FOLDER_V2,
1152
133
        DIGEST_FOLDER_V2,
1153
133
        STR_FOLDER_V1,
1154
133
        DIGEST_FOLDER_V1,
1155
133
    ] {
1156
532
        prune_temp_inner(temp_path, folder).await
?0
;
1157
    }
1158
133
    Ok(())
1159
133
}
1160
1161
// Sometimes we get files to emplace that are identical to the existing files
1162
// Due to the evict/remove/replace cycle taking some amount of time, we actually
1163
// want to drop these
1164
// Return value is "is duplicate"
1165
1.77k
pub async fn check_duplicate_files<Fe>(
1166
1.77k
    evicting_map: &Arc<FsEvictingMap<'_, Fe>>,
1167
1.77k
    key: &StoreKey<'static>,
1168
1.77k
    entry: &Arc<Fe>,
1169
1.77k
) -> Result<bool, Error>
1170
1.77k
where
1171
1.77k
    Fe: FileEntry,
1172
1.77k
{
1173
1.77k
    let temp_file_encoded_file_path = entry.get_encoded_file_path().write().await;
1174
1.77k
    let maybe_existing_item = evicting_map.get(&key.borrow().into_owned()).await;
1175
1.77k
    if let Some(
existing_item27
) = maybe_existing_item {
1176
27
        if Arc::ptr_eq(entry, &existing_item) {
1177
1
            warn!("Tried to check duplicate of an entry we already have!");
1178
1
            return Ok(true);
1179
26
        }
1180
26
        let existing_item_encoded_file_path = existing_item.get_encoded_file_path().write().await;
1181
26
        if entry.data_size() == existing_item.data_size() {
1182
            const CHUNK_SIZE: usize = 16 * 1024; // 16kb chunks, kinda picked out of the air
1183
22
            let file_length = entry.data_size();
1184
22
            let existing_path = existing_item_encoded_file_path.get_file_path();
1185
22
            let temp_path = temp_file_encoded_file_path.get_file_path();
1186
22
            trace!(?existing_path, ?temp_path, "Checking duplicate files");
1187
22
            let mut temp_file = fs::open_file(&temp_path, 0, file_length).await
?0
;
1188
22
            let 
mut existing_file17
= match fs::open_file(&existing_path, 0, file_length).await {
1189
17
                Ok(file) => file,
1190
5
                Err(
err4
) if err.code == Code::NotFoun
d4
=> {
1191
4
                    warn!(
1192
                        ?key,
1193
                        ?err,
1194
                        "Map/disk divergence in check_duplicate_files: dropping stale entry",
1195
                    );
1196
                    // Removal calls unref(), which needs the path lock. Release
1197
                    // both locks and the open file before running callbacks.
1198
4
                    drop(temp_file);
1199
4
                    drop(existing_item_encoded_file_path);
1200
4
                    drop(temp_file_encoded_file_path);
1201
4
                    evicting_map
1202
4
                        .remove_if(key, |map_entry| Arc::ptr_eq(map_entry, &existing_item))
1203
4
                        .await;
1204
4
                    return Ok(false);
1205
                }
1206
1
                Err(err) => return Err(err),
1207
            };
1208
1209
17
            let mut temp_buffer: [u8; CHUNK_SIZE] = [0; CHUNK_SIZE];
1210
17
            let mut existing_buffer: [u8; CHUNK_SIZE] = [0; CHUNK_SIZE];
1211
17
            for offset in (0..file_length).step_by(CHUNK_SIZE) {
1212
17
                let buffer_size = if offset + (CHUNK_SIZE as u64) <= file_length {
1213
2
                    CHUNK_SIZE
1214
15
                } else if file_length < CHUNK_SIZE as u64 {
1215
13
                    usize::try_from(file_length)
1216
13
                        .expect("Always succeeds because file_length < 16384")
1217
                } else {
1218
2
                    usize::try_from(file_length - offset).expect("Always succeeds because offset < file_length, and offset-file_length must be < 16384")
1219
                };
1220
17
                if let Err(
err0
) = temp_file.read_exact(&mut temp_buffer[0..buffer_size]).await {
1221
0
                    warn!(
1222
                        ?err,
1223
                        ?temp_path,
1224
                        file_length,
1225
                        offset,
1226
                        buffer_size,
1227
                        "Failed to read temp file, skipping duplicate check"
1228
                    );
1229
0
                    return Ok(false);
1230
17
                }
1231
17
                if let Err(
err0
) = existing_file
1232
17
                    .read_exact(&mut existing_buffer[0..buffer_size])
1233
17
                    .await
1234
                {
1235
0
                    warn!(
1236
                        ?err,
1237
                        ?existing_path,
1238
                        file_length,
1239
                        offset,
1240
                        buffer_size,
1241
                        "Failed to read existing, skipping duplicate check"
1242
                    );
1243
0
                    return Ok(false);
1244
17
                }
1245
17
                if temp_buffer.ne(&existing_buffer) {
1246
11
                    trace!(
1247
                        ?existing_path,
1248
                        ?temp_path,
1249
                        "Files are different, so non-duplicate"
1250
                    );
1251
11
                    return Ok(false);
1252
6
                }
1253
            }
1254
6
            trace!(
1255
                ?existing_path,
1256
                ?temp_path,
1257
                "Identical files, so don't need to edit, skipping emplace"
1258
            );
1259
6
            drop(existing_file);
1260
6
            drop(temp_file);
1261
6
            drop(existing_item_encoded_file_path);
1262
6
            drop(temp_file_encoded_file_path);
1263
            // Comparison can overlap eviction. An identical retired file no
1264
            // longer satisfies this upload unless it is still the mapped entry.
1265
6
            return Ok(evicting_map
1266
6
                .get(key)
1267
6
                .await
1268
6
                .is_some_and(|current| Arc::ptr_eq(&current, &existing_item)));
1269
4
        }
1270
4
        trace!(
1271
4
            entry_data_size = entry.data_size(),
1272
4
            existing_data_size = existing_item.data_size(),
1273
4
            existing_path = ?existing_item_encoded_file_path.get_file_path(),
1274
            "Different data sizes, so non-duplicate"
1275
        );
1276
    } else {
1277
1.74k
        trace!(
1278
1.74k
            temp_file = ?temp_file_encoded_file_path.get_file_path(),
1279
            "No existing entry, so not duplicate"
1280
        );
1281
    }
1282
1.74k
    Ok(false)
1283
1.77k
}
1284
1285
/// Deletes a digest's `.exec` variant (see
1286
/// [`FilesystemStore::get_executable_hardlink_source`]) when that digest is
1287
/// evicted or replaced in the primary CAS `evicting_map`. Without this, the
1288
/// `.exec` directory is invisible to `max_bytes` and is only ever cleared by
1289
/// the startup `remove_dir_all`, so it grows without bound at runtime (#2474).
1290
/// Tying its lifetime to the primary entry instead bounds total disk use to
1291
/// roughly `2 * max_bytes` in the worst case (every blob also executable).
1292
#[cfg(unix)]
1293
#[derive(Debug)]
1294
struct ExecutableVariantRemover {
1295
    content_path: String,
1296
}
1297
1298
#[cfg(unix)]
1299
impl RemoveItemCallback for ExecutableVariantRemover {
1300
36
    fn callback<'a>(
1301
36
        &'a self,
1302
36
        store_key: StoreKey<'a>,
1303
36
    ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
1304
36
        Box::pin(async move {
1305
36
            let StoreKey::Digest(
digest26
) = store_key else {
1306
10
                return;
1307
            };
1308
26
            let variant_path = format!(
1309
                "{}{EXECUTABLE_DIR_SUFFIX}/{DIGEST_FOLDER_V2}/{digest}",
1310
                self.content_path
1311
            );
1312
26
            match fs::remove_file(&variant_path).await {
1313
1
                Ok(()) => debug!(
1314
                    ?variant_path,
1315
                    "Deleted executable variant for evicted digest"
1316
                ),
1317
                // Common case: no variant was ever materialized for this digest.
1318
25
                Err(err) if err.code == Code::NotFound => {}
1319
0
                Err(err) => warn!(
1320
                    ?variant_path,
1321
                    ?err,
1322
                    "Failed to delete executable variant for evicted digest"
1323
                ),
1324
            }
1325
36
        })
1326
36
    }
1327
}
1328
1329
#[derive(Debug, MetricsComponent)]
1330
pub struct FilesystemStore<Fe: FileEntry = FileEntryImpl> {
1331
    #[metric]
1332
    shared_context: Arc<SharedContext>,
1333
    #[metric(group = "evicting_map")]
1334
    evicting_map: Arc<FsEvictingMap<'static, Fe>>,
1335
    #[metric(help = "Block size of the configured filesystem")]
1336
    block_size: u64,
1337
    #[metric(help = "Size of the configured read buffer size")]
1338
    read_buffer_size: usize,
1339
    /// See [`FilesystemSpec::evict_page_cache`]. When false (the default) the
1340
    /// per-blob `posix_fadvise(DONTNEED)` calls are skipped.
1341
    evict_page_cache: bool,
1342
    weak_self: Weak<Self>,
1343
    rename_fn: fn(&OsStr, &OsStr) -> Result<(), std::io::Error>,
1344
    /// Limits concurrent write operations to prevent disk I/O saturation.
1345
    write_semaphore: Option<Semaphore>,
1346
    /// A monotonic counter by which we stamp newly added files.
1347
    next_generation: AtomicU64,
1348
    /// See [`FlushCoalescer`]: amortizes the per-blob `F_FULLFSYNC` cost
1349
    /// across concurrent uploads without weakening durability. `None` when
1350
    /// the sentinel could not be created (e.g. read-only content volume);
1351
    /// uploads then fall back to per-file `sync_all`, matching the old
1352
    /// behavior (and such stores fail at write time anyway).
1353
    #[cfg(target_os = "macos")]
1354
    flush_coalescer: Option<Arc<FlushCoalescer>>,
1355
    /// Per-digest single-flight locks guarding creation of the executable
1356
    /// variant in `{content_path}.exec`, so each variant's writable fd is
1357
    /// opened exactly once. The outer lock is sync and only ever held to
1358
    /// get/insert/remove the per-digest async lock — never across I/O.
1359
    #[cfg(unix)]
1360
    executable_locks: std::sync::Mutex<HashMap<DigestInfo, Arc<Mutex<()>>>>,
1361
    /// False when the startup wipe could not safely remove old executable
1362
    /// variants from a read-only filesystem. Ordinary CAS reads remain
1363
    /// available, but executable hardlink sources must not reuse stale files.
1364
    #[cfg(unix)]
1365
    executable_variants_enabled: bool,
1366
}
1367
1368
impl<Fe: FileEntry> FilesystemStore<Fe> {
1369
116
    pub async fn new(spec: &FilesystemSpec) -> Result<Arc<Self>, Error> {
1370
1.76k
        
Self::new_with_timeout_and_rename_fn116
(
spec116
, |from, to| std::fs::rename(from, to)).
await116
1371
116
    }
1372
1373
133
    pub async fn new_with_timeout_and_rename_fn(
1374
133
        spec: &FilesystemSpec,
1375
133
        rename_fn: fn(&OsStr, &OsStr) -> Result<(), std::io::Error>,
1376
133
    ) -> Result<Arc<Self>, Error> {
1377
266
        async fn create_subdirs(path: &str) -> Result<bool, Error> {
1378
266
            let mut writable = true;
1379
532
            for folder in 
[STR_FOLDER_V2, DIGEST_FOLDER_V2]266
{
1380
532
                let dir = format!("{path}/{folder}");
1381
532
                writable &= fs::call_with_permit(move |_| match std::fs::create_dir_all(&dir) {
1382
532
                    Ok(()) => Ok(true),
1383
0
                    Err(err) if matches!(err.kind(), std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::ReadOnlyFilesystem) => {
1384
0
                        warn!(%dir, ?err, "Cannot create directory on read-only volume; retaining legacy files");
1385
0
                        Ok(false)
1386
                    }
1387
0
                    Err(err) => Err(Error::from(err).append(format!("Failed to create directory {dir}"))),
1388
532
                }).await
?0
;
1389
            }
1390
266
            Ok(writable)
1391
266
        }
1392
1393
133
        let now = SystemTime::now();
1394
1395
133
        let empty_policy = nativelink_config::stores::EvictionPolicy::default();
1396
133
        let eviction_policy = spec.eviction_policy.as_ref().unwrap_or(&empty_policy);
1397
133
        let evicting_map = Arc::new(EvictingMap::new(eviction_policy, now));
1398
1399
        // Create the generation-aware temp and content directories.
1400
1401
133
        let temp_dirs_writable = create_subdirs(&spec.temp_path).await
?0
;
1402
133
        let content_dirs_writable = create_subdirs(&spec.content_path).await
?0
;
1403
        // Nothing to migrate into; skip the scan rather than warn per file.
1404
133
        let migrate = temp_dirs_writable && content_dirs_writable;
1405
1406
        // Executable-variant directory: a sibling of `content_path` holding
1407
        // per-digest 0o555 copies used as hardlink sources for executable
1408
        // inputs (see `get_executable_hardlink_source`). Cleared on writable
1409
        // startup — the variants are regenerable and we never want a stale one
1410
        // to leak across runs. If a read-only filesystem prevents the wipe,
1411
        // ordinary CAS reads remain enabled but executable variants do not.
1412
        // Unix-only: the executable bit (and the ETXTBSY race it guards
1413
        // against) does not apply on Windows.
1414
        #[cfg(unix)]
1415
133
        let executable_variants_enabled = {
1416
133
            let enabled = prepare_executable_dir(&spec.content_path).await
?0
;
1417
133
            if !enabled {
1418
1
                warn!(
1419
1
                    executable_dir = %format!("{}{EXECUTABLE_DIR_SUFFIX}", spec.content_path),
1420
                    "Executable directory is not writable; serving CAS reads with executable variants disabled"
1421
                );
1422
132
            }
1423
133
            if enabled {
1424
132
                // Only register cleanup when executable variants are enabled.
1425
132
                // Otherwise surviving variants are deliberately quarantined,
1426
132
                // and repeated deletion warnings would obscure the fallback.
1427
132
                evicting_map.add_remove_callback(RemoveCallbackHolder::new(Arc::new(
1428
132
                    ExecutableVariantRemover {
1429
132
                        content_path: spec.content_path.clone(),
1430
132
                    },
1431
132
                )));
1432
132
            
}1
1433
133
            enabled
1434
        };
1435
1436
133
        let shared_context = Arc::new(SharedContext {
1437
133
            active_drop_spawns: AtomicU64::new(0),
1438
133
            temp_path: spec.temp_path.clone(),
1439
133
            content_path: spec.content_path.clone(),
1440
133
        });
1441
1442
133
        let block_size = if spec.block_size == 0 {
1443
111
            DEFAULT_BLOCK_SIZE
1444
        } else {
1445
22
            spec.block_size
1446
        };
1447
133
        let next_generation = add_files_to_cache(
1448
133
            evicting_map.as_ref(),
1449
133
            &now,
1450
133
            &shared_context,
1451
133
            block_size,
1452
133
            rename_fn,
1453
133
            migrate,
1454
133
        )
1455
133
        .await
?0
;
1456
133
        prune_temp_path(&shared_context.temp_path).await
?0
;
1457
1458
133
        let read_buffer_size = if spec.read_buffer_size == 0 {
1459
118
            DEFAULT_BUFF_SIZE
1460
        } else {
1461
15
            spec.read_buffer_size as usize
1462
        };
1463
133
        let write_semaphore = if spec.max_concurrent_writes > 0 {
1464
0
            Some(Semaphore::new(spec.max_concurrent_writes))
1465
        } else {
1466
133
            None
1467
        };
1468
        // Never make construction fail over the durability optimization:
1469
        // a read-only content volume must still serve reads, exactly as
1470
        // it did before the coalescer existed.
1471
        #[cfg(target_os = "macos")]
1472
        let flush_coalescer = FlushCoalescer::for_content_path(&shared_context.content_path)
1473
            .await
1474
            .inspect_err(|err| {
1475
                warn!(
1476
                    ?err,
1477
                    "Failed to create flush coalescer; falling back to per-file sync_all"
1478
                );
1479
            })
1480
            .ok();
1481
1482
133
        Ok(Arc::new_cyclic(|weak_self| Self {
1483
133
            shared_context,
1484
133
            evicting_map,
1485
133
            block_size,
1486
133
            read_buffer_size,
1487
133
            evict_page_cache: spec.evict_page_cache,
1488
133
            weak_self: weak_self.clone(),
1489
133
            rename_fn,
1490
133
            next_generation: AtomicU64::new(next_generation.inner()),
1491
133
            write_semaphore,
1492
            #[cfg(target_os = "macos")]
1493
            flush_coalescer,
1494
            #[cfg(unix)]
1495
133
            executable_locks: std::sync::Mutex::new(HashMap::new()),
1496
            #[cfg(unix)]
1497
133
            executable_variants_enabled,
1498
133
        }))
1499
133
    }
1500
1501
70
    pub fn get_arc(&self) -> Option<Arc<Self>> {
1502
70
        self.weak_self.upgrade()
1503
70
    }
1504
1505
    /// Reserve a digest before it is populated so active action inputs cannot
1506
    /// be evicted while they are being materialized.
1507
3
    pub fn lease_digest(&self, digest: &DigestInfo) {
1508
3
        let key: StoreKey<'static> = (*digest).into();
1509
3
        self.evicting_map.lease_key(StoreKeyBorrow::from(key));
1510
3
    }
1511
1512
    /// Release a batch of action-input leases and trim retained entries once.
1513
1
    pub async fn release_digests(&self, digests: &[DigestInfo]) {
1514
1
        self.evicting_map
1515
3
            .
release_keys1
(
digests1
.
iter1
().
map1
(|digest| StoreKey::Digest(*digest)))
1516
1
            .await;
1517
1
    }
1518
1519
1.77k
    fn get_and_update_generation(&self) -> Result<Generation, Error> {
1520
1.77k
        self.next_generation
1521
1.77k
            .
fetch_update1.77k
(
Ordering::Relaxed1.77k
,
Ordering::Relaxed1.77k
, |generation| {
1522
1.77k
                generation.checked_add(1)
1523
1.77k
            })
1524
1.77k
            .map(Generation::new)
1525
1.77k
            .map_err(|_| 
{0
1526
0
                make_err!(
1527
0
                    Code::ResourceExhausted,
1528
                    "Filesystem generation counter exhausted"
1529
                )
1530
0
            })
1531
1.77k
    }
1532
1533
    /// Path of the read-only executable (0o555) variant for `digest`.
1534
    #[cfg(unix)]
1535
10
    fn executable_variant_path(&self, digest: &DigestInfo) -> OsString {
1536
10
        format!(
1537
            "{}{EXECUTABLE_DIR_SUFFIX}/{DIGEST_FOLDER_V2}/{digest}",
1538
10
            self.shared_context.content_path
1539
        )
1540
10
        .into()
1541
10
    }
1542
1543
    /// Resolves the executable variant for many digests at once, batching the
1544
    /// warm path's existence checks into a single dispatch.
1545
    ///
1546
    /// Every input file carrying the executable bit resolves through here, and
1547
    /// in a toolchain-heavy input tree that is nearly all of them. The `stat(2)`
1548
    /// this needs costs single-digit microseconds on tmpfs, while the permit
1549
    /// acquisition and thread hop to dispatch one cost tens: asking per path
1550
    /// spent ~4.6s per action on a 15k-file tree, and asking once for the tree
1551
    /// spends ~37ms.
1552
28
    pub async fn get_executable_hardlink_sources(
1553
28
        &self,
1554
28
        digests: &[DigestInfo],
1555
28
    ) -> Vec<Result<OsString, Error>> {
1556
28
        let mut tasks = Vec::with_capacity(digests.len());
1557
28
        for (
digest3
,
hit3
) in digests
1558
28
            .iter()
1559
28
            .copied()
1560
28
            .zip(self.materialized_variants(digests).await)
1561
        {
1562
3
            tasks.push(async move {
1563
3
                match hit {
1564
1
                    Some(path) => Ok(path),
1565
2
                    None => self.get_executable_hardlink_source(&digest).await,
1566
                }
1567
3
            });
1568
        }
1569
28
        futures::stream::iter(tasks)
1570
28
            .buffered(MAX_CONCURRENT_VARIANT_LOOKUPS)
1571
28
            .collect()
1572
28
            .await
1573
28
    }
1574
1575
    /// Reports which digests already have an executable variant on disk, in one
1576
    /// batched existence check.
1577
    #[cfg(unix)]
1578
28
    async fn materialized_variants(&self, digests: &[DigestInfo]) -> Vec<Option<OsString>> {
1579
28
        let paths: Vec<OsString> = digests
1580
28
            .iter()
1581
28
            .map(|digest| 
self3
.
executable_variant_path3
(
digest3
))
1582
28
            .collect();
1583
1584
        // Variants disabled and a failed batch are the same situation: nothing
1585
        // is known to exist, so every digest takes the per-digest path, which
1586
        // reports real errors per file.
1587
28
        let exists = if self.executable_variants_enabled {
1588
28
            fs::exists_many(paths.iter().map(Into::into).collect())
1589
28
                .await
1590
28
                .ok()
1591
        } else {
1592
0
            None
1593
        }
1594
28
        .unwrap_or_else(|| 
vec!0
[false;
paths0
.
len0
()]);
1595
1596
28
        paths
1597
28
            .into_iter()
1598
28
            .zip(exists)
1599
28
            .map(|(path, hit)| 
hit3
.
then_some3
(
path3
))
1600
28
            .collect()
1601
28
    }
1602
1603
    /// Non-unix never builds executable variants, so there is never one on disk
1604
    /// to find and nothing to batch.
1605
    #[cfg(not(unix))]
1606
    async fn materialized_variants(&self, digests: &[DigestInfo]) -> Vec<Option<OsString>> {
1607
        vec![None; digests.len()]
1608
    }
1609
1610
    /// Returns the path to a private, read-only **executable** (0o555) copy of
1611
    /// the blob for `digest`, creating it at most once. Callers **hardlink**
1612
    /// the returned path into action input trees instead of copying the
1613
    /// executable per action.
1614
    ///
1615
    /// Why this exists: a CAS blob is stored read-only **0o444** and shared
1616
    /// across actions by hardlink, so it cannot carry the executable bit and
1617
    /// must never be `chmod`'d (that mutates the shared inode — the #2347
1618
    /// corruption class). Materializing an executable input therefore needs a
1619
    /// separate 0o555 inode. Doing that copy *per action* opens a writable fd
1620
    /// in the worker's hot path; under fork-heavy concurrency a child can
1621
    /// inherit that fd and a concurrent `execve` of the executable then fails
1622
    /// with `ETXTBSY` ("Text file busy", os error 26). Creating the 0o555 inode
1623
    /// **once** — writer fd fsync'd and closed, then atomically renamed into
1624
    /// place before the inode is ever hardlinked or executed — and hardlinking
1625
    /// it thereafter keeps the per-action path hardlink-only.
1626
    #[cfg(unix)]
1627
8
    pub async fn get_executable_hardlink_source(
1628
8
        &self,
1629
8
        digest: &DigestInfo,
1630
8
    ) -> Result<OsString, Error> {
1631
8
        if !self.executable_variants_enabled {
1632
1
            return Err(make_err!(
1633
1
                Code::FailedPrecondition,
1634
1
                "Executable hardlink sources are disabled because the startup wipe could not safely clear the read-only executable directory"
1635
1
            ));
1636
7
        }
1637
7
        let variant_path = self.executable_variant_path(digest);
1638
1639
        // Fast path: the variant already exists, so the caller can hardlink it
1640
        // with no writable fd anywhere in sight.
1641
7
        if fs::metadata(&variant_path).await.is_ok() {
1642
1
            return Ok(variant_path);
1643
6
        }
1644
1645
        // Single-flight: exactly one task ever opens a writable fd for this
1646
        // variant. Without this, a cold-cache burst of concurrent actions
1647
        // would each open a writer for the same executable — the very window
1648
        // ETXTBSY exploits.
1649
6
        let lock = {
1650
6
            let mut locks = self
1651
6
                .executable_locks
1652
6
                .lock()
1653
6
                .expect("executable_locks poisoned");
1654
6
            locks
1655
6
                .entry(*digest)
1656
6
                .or_insert_with(|| Arc::new(Mutex::new(())))
1657
6
                .clone()
1658
        };
1659
6
        let _guard = lock.lock().await;
1660
1661
        // Re-check: another task may have constructed it while we waited.
1662
6
        if fs::metadata(&variant_path).await.is_ok() {
1663
0
            self.forget_executable_lock(digest);
1664
0
            return Ok(variant_path);
1665
6
        }
1666
1667
6
        let result = self.create_executable_variant(digest, &variant_path).await;
1668
1669
        // The digest may have been evicted mid-copy: its eviction callback ran
1670
        // before the rename published the variant, so nothing owns the file
1671
        // anymore. This orphans the variant from eviction accounting, but the
1672
        // race is rare enough (needs an eviction to land in the narrow window
1673
        // between rename and this check, on a digest's first-ever variant
1674
        // materialization) that it's a self-limiting leak, not a systemic one
1675
        // — cheaper to log and let this action succeed with the still-valid
1676
        // file than to fail an otherwise-successful action over it.
1677
6
        if result.is_ok() && 
self.evicting_map5
.
get5
(&digest.into()).
await5
.
is_none5
() {
1678
0
            warn!(
1679
                %digest,
1680
                ?variant_path,
1681
                "Digest evicted while materializing its executable variant; \
1682
                 variant is now untracked by eviction accounting"
1683
            );
1684
6
        }
1685
1686
        // Drop the per-digest lock entry regardless of outcome so the map
1687
        // cannot grow unbounded; a concurrent waiter already cloned the Arc.
1688
6
        self.forget_executable_lock(digest);
1689
6
        result.map(|()| variant_path)
1690
8
    }
1691
1692
    /// Non-unix has no executable bit and no `ETXTBSY`, so just hardlink the
1693
    /// CAS blob directly.
1694
    #[cfg(not(unix))]
1695
    pub async fn get_executable_hardlink_source(
1696
        &self,
1697
        digest: &DigestInfo,
1698
    ) -> Result<OsString, Error> {
1699
        let file_entry = self.get_file_entry_for_digest(digest).await?;
1700
        file_entry
1701
            .get_file_path_locked(|p| async move { Ok(p) })
1702
            .await
1703
    }
1704
1705
    #[cfg(unix)]
1706
6
    fn forget_executable_lock(&self, digest: &DigestInfo) {
1707
6
        self.executable_locks
1708
6
            .lock()
1709
6
            .expect("executable_locks poisoned")
1710
6
            .remove(digest);
1711
6
    }
1712
1713
    /// Materializes the 0o555 executable variant for `digest`. Must be called
1714
    /// under the per-digest single-flight guard.
1715
    #[cfg(unix)]
1716
6
    async fn create_executable_variant(
1717
6
        &self,
1718
6
        digest: &DigestInfo,
1719
6
        variant_path: &OsStr,
1720
6
    ) -> Result<(), Error> {
1721
        // Resolve the on-disk CAS blob (0o444) to copy from. Must be present in
1722
        // this tier; callers populate the fast store first.
1723
6
        let 
file_entry5
= self
1724
6
            .get_file_entry_for_digest(digest)
1725
6
            .await
1726
6
            .err_tip(|| "Resolving CAS blob for executable variant")
?1
;
1727
5
        let src_path = file_entry
1728
10
            .
get_file_path_locked5
(|p| async move
{5
Ok(p)5
})
1729
5
            .await
?0
;
1730
1731
5
        let variant_owned = variant_path.to_os_string();
1732
5
        let mut temp_owned = variant_path.to_os_string();
1733
5
        temp_owned.push(".tmp");
1734
5
        let rename_fn = self.rename_fn;
1735
1736
        // All of this is blocking std::fs; run it off the async runtime. The
1737
        // writable fd opened by `copy` is fully closed before the `rename`
1738
        // publishes the inode, so no reachable hardlink of the variant ever has
1739
        // an open writer.
1740
5
        spawn_blocking!(
1741
            "filesystem_store_executable_variant",
1742
5
            move || -> Result<(), Error> {
1743
                use std::os::unix::fs::PermissionsExt;
1744
5
                std::fs::copy(&src_path, &temp_owned).map_err(|e| 
{0
1745
0
                    make_err!(Code::Internal, "executable-variant copy failed: {e:?}")
1746
0
                })?;
1747
5
                std::fs::set_permissions(&temp_owned, std::fs::Permissions::from_mode(0o555))
1748
5
                    .map_err(|e| 
{0
1749
0
                        make_err!(
1750
0
                            Code::Internal,
1751
                            "executable-variant chmod 0o555 failed: {e:?}"
1752
                        )
1753
0
                    })?;
1754
                // Belt-and-suspenders flush before publish. The `.exec`
1755
                // directory is cleared before executable variants are enabled,
1756
                // so durability is not needed across process restarts. The
1757
                // flush still ensures a published variant is complete for the
1758
                // current process. Non-macOS keeps the prior `sync_all`
1759
                // behavior; macOS uses plain `fsync(2)` (data handed to the
1760
                // device, no multi-ms `F_FULLFSYNC` device-cache drain), which
1761
                // covers kernel panics — the next writable startup wipe covers
1762
                // power loss.
1763
5
                let f = std::fs::File::open(&temp_owned)
1764
5
                    .map_err(|e| 
make_err!0
(
Code::Internal0
, "executable-variant reopen: {e:?}"))
?0
;
1765
                #[cfg(target_os = "macos")]
1766
                let flush_result = {
1767
                    use std::os::fd::AsRawFd;
1768
                    loop {
1769
                        if unsafe { libc::fsync(f.as_raw_fd()) } == 0 {
1770
                            break Ok(());
1771
                        }
1772
                        let err = std::io::Error::last_os_error();
1773
                        if err.kind() != std::io::ErrorKind::Interrupted {
1774
                            break Err(err);
1775
                        }
1776
                    }
1777
                };
1778
                #[cfg(not(target_os = "macos"))]
1779
5
                let flush_result = f.sync_all();
1780
5
                flush_result
1781
5
                    .map_err(|e| 
make_err!0
(
Code::Internal0
, "executable-variant fsync: {e:?}"))
?0
;
1782
5
                drop(f);
1783
5
                rename_fn(temp_owned.as_os_str(), variant_owned.as_os_str()).map_err(|e| 
{0
1784
0
                    make_err!(Code::Internal, "executable-variant rename failed: {e:?}")
1785
0
                })?;
1786
5
                Ok(())
1787
5
            }
1788
        )
1789
5
        .await
1790
5
        .err_tip(|| "executable-variant spawn_blocking join failed")
?0
1791
6
    }
1792
1793
3.90k
    pub async fn get_file_entry_for_digest(&self, digest: &DigestInfo) -> Result<Arc<Fe>, Error> {
1794
        // Zero-digest blobs have no backing file on disk (FilesystemStore
1795
        // never persists zero-byte content). The previous implementation
1796
        // returned a synthetic FileEntry whose content_path did not exist,
1797
        // which downstream callers would then try to hard_link from,
1798
        // silently producing missing or empty output files in worker
1799
        // execution directories. Return NotFound so callers are forced to
1800
        // take the explicit zero-digest path (e.g. fs::create_file).
1801
3.90k
        if is_zero_digest(digest) {
1802
1
            return Err(make_err!(
1803
1
                Code::NotFound,
1804
1
                "{digest} is a zero-digest; FilesystemStore does not persist zero-byte files. \
1805
1
                 Callers must materialise empty files directly rather than going through get_file_entry_for_digest."
1806
1
            ));
1807
3.90k
        }
1808
3.90k
        self.evicting_map
1809
3.90k
            .get(&digest.into())
1810
3.90k
            .await
1811
3.90k
            .ok_or_else(|| 
make_err!8
(
Code::NotFound8
, "{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."))
1812
3.90k
    }
1813
1814
1.47k
    async fn update_file(
1815
1.47k
        self: Pin<&Self>,
1816
1.47k
        mut entry: Fe,
1817
1.47k
        mut temp_file: FileSlot,
1818
1.47k
        final_key: StoreKey<'static>,
1819
1.47k
        mut reader: DropCloserReadHalf,
1820
1.47k
    ) -> Result<u64, Error> {
1821
1.47k
        let mut data_size = 0;
1822
        loop {
1823
2.95k
            let mut data = reader
1824
2.95k
                .recv()
1825
2.95k
                .await
1826
2.95k
                .err_tip(|| "Failed to receive data in filesystem store")
?0
;
1827
2.95k
            let data_len = data.len();
1828
2.95k
            if data_len == 0 {
1829
1.47k
                break; // EOF.
1830
1.47k
            }
1831
1.47k
            temp_file
1832
1.47k
                .write_all_buf(&mut data)
1833
1.47k
                .await
1834
1.47k
                .err_tip(|| "Failed to write data into filesystem store")
?0
;
1835
1.47k
            data_size += data_len as u64;
1836
        }
1837
1838
1.47k
        let permit = if let Some(
sem0
) = &self.write_semaphore {
1839
0
            Some(sem.acquire().await.map_err(|err| {
1840
0
                Error::from_std_err(Code::Internal, &err).append("Write semaphore closed")
1841
0
            })?)
1842
        } else {
1843
1.47k
            None
1844
        };
1845
1846
        // tokio defers write errors to the next write or flush call, so without an explicit flush
1847
        // the final write's failure is silently swallowed by `sync_all`, and a truncated file would
1848
        // be renamed into the content path.
1849
1.47k
        temp_file
1850
1.47k
            .flush()
1851
1.47k
            .await
1852
1.47k
            .err_tip(|| "Failed to flush in filesystem store")
?0
;
1853
1.47k
        self.flush_durably(&temp_file)
1854
1.47k
            .await
1855
1.47k
            .err_tip(|| "Failed to sync in filesystem store")
?0
;
1856
1857
1.47k
        drop(permit);
1858
1859
1.47k
        if self.evict_page_cache {
1860
0
            temp_file.advise_dontneed();
1861
1.47k
        }
1862
1.47k
        trace!(?temp_file, "Dropping file to update_file");
1863
1.47k
        drop(temp_file);
1864
1865
1.47k
        *entry.data_size_mut() = data_size;
1866
1.47k
        self.emplace_file(final_key, Arc::new(entry)).await
?1
;
1867
1.47k
        Ok(data_size)
1868
1.47k
    }
1869
1870
1.76k
    async fn emplace_file(&self, key: StoreKey<'static>, entry: Arc<Fe>) -> Result<(), Error> {
1871
        // Each generation owns a distinct content path. Publish the file before
1872
        // inserting it into the map, so readers only observe completed renames.
1873
        // A delayed unref of another generation cannot touch this file.
1874
1.76k
        let evicting_map = self.evicting_map.clone();
1875
1.76k
        let rename_fn = self.rename_fn;
1876
1877
        // We need to guarantee that this will get to the end even if the parent future is dropped.
1878
        // See: https://github.com/TraceMachina/nativelink/issues/495
1879
1.76k
        background_spawn!("filesystem_store_emplace_file", async move {
1880
            // Sometimes we get files to emplace that are identical to the existing files
1881
            // Due to the evict/remove/replace cycle taking some amount of time, we actually
1882
            // want to drop these
1883
1.76k
            if check_duplicate_files(&evicting_map, &key, &entry).await
?1
{
1884
5
                return Ok(());
1885
1.76k
            }
1886
1887
1.76k
            let mut encoded_file_path = entry.get_encoded_file_path().write().await;
1888
1889
1.76k
            let final_path = get_file_path_raw(
1890
1.76k
                &PathType::Content,
1891
1.76k
                encoded_file_path.shared_context.as_ref(),
1892
1.76k
                &key,
1893
1.76k
                encoded_file_path.generation,
1894
1.76k
                encoded_file_path.version,
1895
            );
1896
1897
1.76k
            let from_path = encoded_file_path.get_file_path();
1898
1899
            // Lock the blob down as read-only *before* it lands at its final
1900
            // content path, so every hardlink of it (the worker directory cache
1901
            // and `download_to_directory`) inherits an immutable, read-only
1902
            // inode. This is what preserves the input-tree hermeticity contract
1903
            // (actions cannot mutate their inputs) without a per-materialization
1904
            // chmod walk, and it means callers must never `chmod` a hardlinked
1905
            // blob — anything needing a different mode (e.g. an executable's +x
1906
            // bit) must take a private copy. Content-addressed blobs are
1907
            // write-once and replaced by `rename` rather than in-place writes,
1908
            // and unlink only needs the *parent directory* to be writable, so a
1909
            // read-only file mode is safe for both overwrite-by-rename and
1910
            // eviction. Best-effort: a failure here (e.g. the temp file was
1911
            // already evicted out from under us) must not abort the emplace.
1912
            #[cfg(unix)]
1913
            {
1914
                use std::os::unix::fs::PermissionsExt;
1915
0
                if let Err(err) =
1916
1.76k
                    fs::set_permissions(&from_path, std::fs::Permissions::from_mode(0o444)).await
1917
                {
1918
0
                    warn!(?err, ?from_path, "Failed to set CAS blob read-only");
1919
1.76k
                }
1920
            }
1921
1.76k
            let result = (rename_fn)(&from_path, &final_path).err_tip(|| 
{2
1922
2
                format!(
1923
                    "Failed to rename temp file to final path {}",
1924
2
                    final_path.display()
1925
                )
1926
2
            });
1927
1928
1.76k
            if let Err(
err2
) = result {
1929
2
                error!(?err, ?from_path, ?final_path, "Failed to rename file",);
1930
                // This entry was never visible in the map. Drop cleans up its
1931
                // temporary file and the previous generation remains usable.
1932
2
                return Err(err);
1933
1.76k
            }
1934
1.76k
            trace!(?key, "Finished emplace file");
1935
1.76k
            encoded_file_path.path_type = PathType::Content;
1936
1.76k
            encoded_file_path.key = key.clone();
1937
            // Insertion can unref this entry immediately under cache pressure.
1938
1.76k
            drop(encoded_file_path);
1939
1.76k
            let (inserted, _) = evicting_map
1940
1.76k
                .insert_if(key.clone().into(), entry.clone(), |present, new| 
{12
1941
12
                    present.generation() < new.generation()
1942
12
                })
1943
1.76k
                .await;
1944
1.76k
            if !inserted {
1945
                // A newer upload completed first. Retire our unindexed content
1946
                // file too, so it cannot leak or reappear after a restart.
1947
1
                info!(%key, "Newer generation already emplaced, dropping");
1948
1
                entry.unref().await;
1949
1.75k
            }
1950
1.76k
            Ok(())
1951
1.76k
        })
1952
1.76k
        .await
1953
1.76k
        .err_tip(|| "Failed to create spawn in filesystem store update_file")
?0
1954
1.76k
    }
1955
1956
    /// Makes `file`'s contents durable with `sync_all` semantics.
1957
    ///
1958
    /// On macOS the naive equivalent (`fcntl(F_FULLFSYNC)` per file) is a
1959
    /// full device-cache flush that serializes at the device and dominates
1960
    /// many-small-blob uploads, so the flush is split into the cheap
1961
    /// per-file part (`fsync(2)`: push this file's pages to the device)
1962
    /// and a device-wide cache drain shared with every concurrent upload
1963
    /// via [`FlushCoalescer`]. Other platforms get their journal's group
1964
    /// commit for free and keep plain `sync_all`.
1965
1.75k
    async fn flush_durably(&self, file: &FileSlot) -> Result<(), Error> {
1966
        #[cfg(target_os = "macos")]
1967
        {
1968
            use std::os::fd::AsRawFd;
1969
            let Some(flush_coalescer) = &self.flush_coalescer else {
1970
                return file.as_ref().sync_all().await.map_err(Into::into);
1971
            };
1972
            // Dup the handle so the blocking task owns its own fd: if
1973
            // this future is cancelled mid-await, the temp file's fd can
1974
            // close and be reused, and an already-started blocking task
1975
            // must never fsync an unrelated descriptor.
1976
            let file_dup = file
1977
                .as_ref()
1978
                .try_clone()
1979
                .await
1980
                .err_tip(|| "Failed to dup fd for fsync in filesystem store")?
1981
                .into_std()
1982
                .await;
1983
            // Deliberately NOT via `fs::call_with_permit`: the caller's
1984
            // `FileSlot` already holds an open-file permit, so requiring a
1985
            // second one here deadlocks when the semaphore is drained (the
1986
            // exact scenario the #2051 regression test pins at one
1987
            // permit). Concurrency is still bounded: every in-flight fsync
1988
            // belongs to an upload holding a `FileSlot` permit.
1989
            spawn_blocking!("filesystem_store_fsync", move || {
1990
                loop {
1991
                    if unsafe { libc::fsync(file_dup.as_raw_fd()) } == 0 {
1992
                        return Ok(());
1993
                    }
1994
                    let err = std::io::Error::last_os_error();
1995
                    // std's sync_all retries EINTR (cvt_r); match it.
1996
                    if err.kind() != std::io::ErrorKind::Interrupted {
1997
                        return Err(Error::from(err).append("fsync failed in filesystem store"));
1998
                    }
1999
                }
2000
            })
2001
            .await
2002
            .err_tip(|| "Failed to join fsync task in filesystem store")??;
2003
            flush_coalescer.commit().await
2004
        }
2005
        #[cfg(not(target_os = "macos"))]
2006
        {
2007
1.75k
            file.as_ref().sync_all().await.map_err(Into::into)
2008
        }
2009
1.75k
    }
2010
2011
0
    pub fn get_eviction_snapshot(&self) -> EvictionSnapshot {
2012
0
        self.evicting_map.get_snapshot()
2013
0
    }
2014
2015
    // Only for tests, so we can run check_duplicate_files
2016
8
    pub fn get_evicting_map(&self) -> Arc<FsEvictingMap<'static, Fe>> {
2017
8
        self.evicting_map.clone()
2018
8
    }
2019
2020
    /// Returns the number of device-wide durability barriers issued by this
2021
    /// store's macOS flush coalescer. Exposed so the integration test can pin
2022
    /// the batching behavior rather than only checking data correctness.
2023
    #[cfg(target_os = "macos")]
2024
    pub fn full_flush_count_for_test(&self) -> Option<u64> {
2025
        self.flush_coalescer
2026
            .as_ref()
2027
            .map(|coalescer| coalescer.full_flush_count.load(Ordering::Relaxed))
2028
    }
2029
2030
    // Separated out so tests can use this
2031
1.75k
    pub async fn make_temp_file(
2032
1.75k
        &self,
2033
1.75k
        temp_key: StoreKey<'static>,
2034
1.75k
    ) -> Result<(Fe, FileSlot, OsString), Error> {
2035
1.75k
        let generation = self.get_and_update_generation()
?0
;
2036
2037
1.75k
        Fe::make_and_open_file(
2038
1.75k
            self.block_size,
2039
1.75k
            generation,
2040
1.75k
            EncodedFilePath {
2041
1.75k
                shared_context: self.shared_context.clone(),
2042
1.75k
                path_type: PathType::Temp,
2043
1.75k
                key: temp_key,
2044
1.75k
                generation,
2045
1.75k
                version: Version::V2,
2046
1.75k
            },
2047
1.75k
        )
2048
1.75k
        .await
2049
1.75k
    }
2050
}
2051
2052
#[async_trait]
2053
impl<Fe: FileEntry> StoreDriver for FilesystemStore<Fe> {
2054
0
    async fn post_init(self: Arc<Self>) -> Result<(), Error> {
2055
        Ok(())
2056
0
    }
2057
2058
    async fn has_with_results(
2059
        self: Pin<&Self>,
2060
        keys: &[StoreKey<'_>],
2061
        results: &mut [Option<u64>],
2062
2.35k
    ) -> Result<(), Error> {
2063
        let own_keys = keys
2064
            .iter()
2065
2.35k
            .map(|sk| sk.borrow().into_owned())
2066
            .collect::<Vec<_>>();
2067
        self.evicting_map
2068
            .sizes_for_keys(own_keys.iter(), results, false /* peek */)
2069
            .await;
2070
        // We need to do a special pass to ensure our zero files exist.
2071
        // If our results failed and the result was a zero file, we need to
2072
        // create the file by spec.
2073
        for (key, result) in keys.iter().zip(results.iter_mut()) {
2074
            if result.is_some() || !is_zero_digest(key.borrow()) {
2075
                continue;
2076
            }
2077
            let (mut tx, rx) = make_buf_channel_pair();
2078
            let send_eof_result = tx.send_eof();
2079
            self.update(key.borrow(), rx, UploadSizeInfo::ExactSize(0))
2080
                .await
2081
0
                .err_tip(|| format!("Failed to create zero file for key {}", key.as_str()))
2082
                .merge(
2083
                    send_eof_result
2084
                        .err_tip(|| "Failed to send zero file EOF in filesystem store has"),
2085
                )?;
2086
2087
            *result = Some(0);
2088
        }
2089
        Ok(())
2090
2.35k
    }
2091
2092
    async fn update(
2093
        self: Pin<&Self>,
2094
        key: StoreKey<'_>,
2095
        mut reader: DropCloserReadHalf,
2096
        _upload_size: UploadSizeInfo,
2097
1.57k
    ) -> Result<u64, Error> {
2098
        if is_zero_digest(key.borrow()) {
2099
            // don't need to add, because zero length files are just assumed to exist.
2100
            return Ok(0);
2101
        }
2102
2103
        let temp_key = make_temp_key(&key);
2104
2105
        // There's a possibility of deadlock here where we take all of the
2106
        // file semaphores with make_and_open_file and the semaphores for
2107
        // whatever is populating reader is exhasted on the threads that
2108
        // have the FileSlots and not on those which can't.  To work around
2109
        // this we don't take the FileSlot until there's something on the
2110
        // reader available to know that the populator is active.
2111
        reader.peek().await?;
2112
2113
        let (entry, temp_file, temp_full_path) = self.make_temp_file(temp_key).await?;
2114
2115
        self.update_file(entry, temp_file, key.into_owned(), reader)
2116
            .await
2117
1
            .err_tip(|| {
2118
1
                format!(
2119
                    "While processing with temp file {}",
2120
1
                    temp_full_path.display()
2121
                )
2122
1
            })
2123
1.57k
    }
2124
2125
1.55k
    fn optimized_for(&self, optimization: StoreOptimizations) -> bool {
2126
1.54k
        matches!(
2127
1.55k
            optimization,
2128
            StoreOptimizations::FileUpdates | StoreOptimizations::SubscribesToUpdateOneshot
2129
        )
2130
1.55k
    }
2131
2132
278
    async fn update_oneshot(self: Pin<&Self>, key: StoreKey<'_>, data: Bytes) -> Result<(), Error> {
2133
        if is_zero_digest(key.borrow()) {
2134
            return Ok(());
2135
        }
2136
2137
        let temp_key = make_temp_key(&key);
2138
        let (mut entry, mut temp_file, temp_full_path) = self
2139
            .make_temp_file(temp_key)
2140
            .await
2141
            .err_tip(|| "Failed to create temp file in filesystem store update_oneshot")?;
2142
2143
        // Write directly without channel overhead
2144
        if !data.is_empty() {
2145
            temp_file
2146
                .write_all(&data)
2147
                .await
2148
0
                .err_tip(|| format!("Failed to write data to {}", temp_full_path.display()))?;
2149
        }
2150
2151
        let _permit = if let Some(sem) = &self.write_semaphore {
2152
0
            Some(sem.acquire().await.map_err(|err| {
2153
0
                Error::from_std_err(Code::Internal, &err).append("Write semaphore closed")
2154
0
            })?)
2155
        } else {
2156
            None
2157
        };
2158
2159
        // See comment in `update_file` above.
2160
        temp_file
2161
            .flush()
2162
            .await
2163
            .err_tip(|| "Failed to flush in filesystem store update_oneshot")?;
2164
        self.flush_durably(&temp_file)
2165
            .await
2166
            .err_tip(|| "Failed to sync in filesystem store update_oneshot")?;
2167
2168
        drop(_permit);
2169
2170
        if self.evict_page_cache {
2171
            temp_file.advise_dontneed();
2172
        }
2173
        drop(temp_file);
2174
2175
        *entry.data_size_mut() = data.len() as u64;
2176
        self.emplace_file(key.into_owned(), Arc::new(entry)).await
2177
278
    }
2178
2179
    async fn update_with_whole_file(
2180
        self: Pin<&Self>,
2181
        key: StoreKey<'_>,
2182
        path: OsString,
2183
        file: FileSlot,
2184
        upload_size: UploadSizeInfo,
2185
16
    ) -> Result<(u64, Option<FileSlot>), Error> {
2186
        let file_size = match upload_size {
2187
            UploadSizeInfo::ExactSize(size) => size,
2188
            UploadSizeInfo::MaxSize(_) => file
2189
                .as_ref()
2190
                .metadata()
2191
                .await
2192
0
                .err_tip(|| format!("While reading metadata for {}", path.display()))?
2193
                .len(),
2194
        };
2195
        if file_size == 0 {
2196
            // don't need to add, because zero length files are just assumed to exist
2197
            return Ok((0, None));
2198
        }
2199
        let generation = self.get_and_update_generation()?;
2200
        let entry = Fe::create(
2201
            file_size,
2202
            self.block_size,
2203
            generation,
2204
            RwLock::new(EncodedFilePath {
2205
                shared_context: self.shared_context.clone(),
2206
                path_type: PathType::Custom(path),
2207
                key: key.borrow().into_owned(),
2208
                generation,
2209
                version: Version::V2,
2210
            }),
2211
        );
2212
        // We are done with the file, if we hold a reference to the file here, it could
2213
        // result in a deadlock if `emplace_file()` also needs file descriptors.
2214
        trace!(?file, "Dropping file to to update_with_whole_file");
2215
        if self.evict_page_cache {
2216
            file.advise_dontneed();
2217
        }
2218
        drop(file);
2219
        self.emplace_file(key.into_owned(), Arc::new(entry))
2220
            .await
2221
            .err_tip(|| "Could not move file into store in upload_file_to_store, maybe dest is on different volume?")?;
2222
        return Ok((file_size, None));
2223
16
    }
2224
2225
    async fn get_part(
2226
        self: Pin<&Self>,
2227
        key: StoreKey<'_>,
2228
        writer: &mut DropCloserWriteHalf,
2229
        offset: u64,
2230
        length: Option<u64>,
2231
646
    ) -> Result<(), Error> {
2232
        if is_zero_digest(key.borrow()) {
2233
            self.has(key.borrow())
2234
                .await
2235
                .err_tip(|| "Failed to check if zero digest exists in filesystem store")?;
2236
            writer
2237
                .send_eof()
2238
                .err_tip(|| "Failed to send zero EOF in filesystem store get_part")?;
2239
            return Ok(());
2240
        }
2241
        let owned_key = key.into_owned();
2242
2
        let entry = self.evicting_map.get(&owned_key).await.ok_or_else(|| {
2243
2
            make_err!(
2244
2
                Code::NotFound,
2245
                "{} not found in filesystem store here",
2246
2
                owned_key.as_str()
2247
            )
2248
2
        })?;
2249
        let read_limit = length.unwrap_or(u64::MAX);
2250
        let mut temp_file = match entry.read_file_part(offset, read_limit).await {
2251
            Ok(file) => file,
2252
            Err(err) => {
2253
                // If the file is not found, we need to remove it from the eviction map.
2254
                if err.code == Code::NotFound {
2255
                    // Map said the file was present but `open()` hit ENOENT.
2256
                    // Self-heals: we remove the stale entry below and a
2257
                    // fast/slow caller re-populates from the slow store, so
2258
                    // this is a recoverable warn, not a fatal error.
2259
                    warn!(
2260
                        ?err,
2261
                        key = ?owned_key,
2262
                        "Filesystem store map/disk divergence: removing entry; reader will fall through to slow store",
2263
                    );
2264
                    self.evicting_map
2265
5
                        .remove_if(&owned_key, |map_entry| Arc::ptr_eq(map_entry, &entry))
2266
                        .await;
2267
                }
2268
                return Err(err);
2269
            }
2270
        };
2271
2272
        loop {
2273
            let mut buf = BytesMut::with_capacity(self.read_buffer_size);
2274
            temp_file
2275
                .read_buf(&mut buf)
2276
                .await
2277
                .err_tip(|| "Failed to read data in filesystem store")?;
2278
            if buf.is_empty() {
2279
                break; // EOF.
2280
            }
2281
            writer
2282
                .send(buf.freeze())
2283
                .await
2284
                .err_tip(|| "Failed to send chunk in filesystem store get_part")?;
2285
        }
2286
        if self.evict_page_cache {
2287
            temp_file.get_ref().advise_dontneed();
2288
        }
2289
        writer
2290
            .send_eof()
2291
            .err_tip(|| "Filed to send EOF in filesystem store get_part")?;
2292
2293
        Ok(())
2294
646
    }
2295
2296
1.61k
    fn inner_store(&self, _digest: Option<StoreKey>) -> &dyn StoreDriver {
2297
1.61k
        self
2298
1.61k
    }
2299
2300
70
    fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) {
2301
70
        self
2302
70
    }
2303
2304
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> {
2305
0
        self
2306
0
    }
2307
2308
0
    fn register_health(self: Arc<Self>, registry: &mut HealthRegistryBuilder) {
2309
0
        registry.register_indicator(self);
2310
0
    }
2311
2312
0
    fn enable_cache_size_metrics(&self, attrs: &[opentelemetry::KeyValue]) -> bool {
2313
0
        self.evicting_map.enable_cache_size_metrics(attrs.to_vec());
2314
0
        true
2315
0
    }
2316
2317
0
    fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> {
2318
0
        self.evicting_map
2319
0
            .add_remove_callback(RemoveCallbackHolder::new(callback));
2320
0
        Ok(())
2321
0
    }
2322
}
2323
2324
#[async_trait]
2325
impl<Fe: FileEntry> HealthStatusIndicator for FilesystemStore<Fe> {
2326
0
    fn get_name(&self) -> &'static str {
2327
0
        "FilesystemStore"
2328
0
    }
2329
2330
    /// Lightweight probe: `stat()` the `content_path` directory. No
2331
    /// write-semaphore / eviction-map contention with production
2332
    /// traffic, and bounded so a hung NFS / EBS mount can't wedge the
2333
    /// indicator.
2334
2
    async fn check_health(&self, _namespace: Cow<'static, str>) -> HealthStatus {
2335
        const HEALTH_PROBE_TIMEOUT: Duration = Duration::from_secs(2);
2336
2337
        let content_path = &self.shared_context.content_path;
2338
        let stat = tokio::fs::metadata(&content_path);
2339
        match timeout(HEALTH_PROBE_TIMEOUT, stat).await {
2340
            Ok(Ok(meta)) if meta.is_dir() => {
2341
                HealthStatus::new_ok(self, "FilesystemStore::check_health: ok".into())
2342
            }
2343
            Ok(Ok(_)) => HealthStatus::new_failed(
2344
                self,
2345
                format!(
2346
                    "FilesystemStore::check_health: content_path {content_path} is not a directory"
2347
                )
2348
                .into(),
2349
            ),
2350
            Ok(Err(e)) => {
2351
                warn!(
2352
                    ?e,
2353
                    %content_path,
2354
                    "FilesystemStore::check_health: stat errored",
2355
                );
2356
                HealthStatus::new_failed(
2357
                    self,
2358
                    format!("FilesystemStore::check_health: stat errored: {e}").into(),
2359
                )
2360
            }
2361
            Err(_) => {
2362
                warn!(
2363
                    %content_path,
2364
                    timeout_secs = HEALTH_PROBE_TIMEOUT.as_secs(),
2365
                    "FilesystemStore::check_health: stat timed out",
2366
                );
2367
                HealthStatus::Timeout {
2368
                    struct_name: self.struct_name(),
2369
                }
2370
            }
2371
        }
2372
2
    }
2373
}