Skip to content

Commit ab7de2a

Browse files
committed
Remove atime references to FilesystemStore
FilesystemStore will no longer keep atime of files up-to-date. This was an expensive operation, since every time any CAS/AC item was touched, it'd update the atime, which caused a lot of sys calls. While the system is running this PR has no effect, but when the program is restarted, items are now inserted in order of atime then mtime, but it relies on the filesystem to keep atime up-to-date. This means that .has() calls do not "refresh" the atime on disk.
1 parent 79c2357 commit ab7de2a

File tree

8 files changed

+37
-198
lines changed

8 files changed

+37
-198
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nativelink-config/src/stores.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,7 @@ pub enum StoreSpec {
318318
/// Stores the data on the filesystem. This store is designed for
319319
/// local persistent storage. Restarts of this program should restore
320320
/// the previous state, meaning anything uploaded will be persistent
321-
/// as long as the filesystem integrity holds. This store uses the
322-
/// filesystem's `atime` (access time) to hold the last touched time
323-
/// of the file(s).
321+
/// as long as the filesystem integrity holds.
324322
///
325323
/// **Example JSON Config:**
326324
/// ```json

nativelink-store/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ rust_library(
5252
"@crates//:bytes",
5353
"@crates//:bytes-utils",
5454
"@crates//:const_format",
55-
"@crates//:filetime",
5655
"@crates//:fred",
5756
"@crates//:futures",
5857
"@crates//:hex",

nativelink-store/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ byteorder = { version = "1.5.0", default-features = false }
2828
bytes = { version = "1.9.0", default-features = false }
2929
bytes-utils = { version = "0.1.4", default-features = false }
3030
const_format = { version = "0.2.34", default-features = false }
31-
filetime = "0.2.25"
3231
fred = { version = "10.0.3", default-features = false, features = [
3332
"i-std",
3433
"i-scripts",

nativelink-store/src/filesystem_store.rs

Lines changed: 21 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ use std::time::SystemTime;
2323
use async_lock::RwLock;
2424
use async_trait::async_trait;
2525
use bytes::BytesMut;
26-
use filetime::{set_file_atime, FileTime};
2726
use futures::stream::{StreamExt, TryStreamExt};
2827
use futures::{Future, TryFutureExt};
2928
use nativelink_config::stores::FilesystemSpec;
3029
use nativelink_error::{make_err, make_input_err, Code, Error, ResultExt};
3130
use nativelink_metric::MetricsComponent;
31+
use nativelink_util::background_spawn;
3232
use nativelink_util::buf_channel::{
3333
make_buf_channel_pair, DropCloserReadHalf, DropCloserWriteHalf,
3434
};
@@ -38,7 +38,6 @@ use nativelink_util::health_utils::{HealthRegistryBuilder, HealthStatus, HealthS
3838
use nativelink_util::store_trait::{
3939
StoreDriver, StoreKey, StoreKeyBorrow, StoreOptimizations, UploadSizeInfo,
4040
};
41-
use nativelink_util::{background_spawn, spawn_blocking};
4241
use tokio::io::{AsyncReadExt, AsyncWriteExt, Take};
4342
use tokio_stream::wrappers::ReadDirStream;
4443
use tracing::{event, Level};
@@ -338,33 +337,6 @@ impl LenEntry for FileEntryImpl {
338337
self.data_size == 0
339338
}
340339

341-
#[inline]
342-
async fn touch(&self) -> bool {
343-
let result = self
344-
.get_file_path_locked(move |full_content_path| async move {
345-
let full_content_path = full_content_path.clone();
346-
spawn_blocking!("filesystem_touch_set_mtime", move || {
347-
set_file_atime(&full_content_path, FileTime::now()).err_tip(|| {
348-
format!("Failed to touch file in filesystem store {full_content_path:?}")
349-
})
350-
})
351-
.await
352-
.map_err(|e| {
353-
make_err!(
354-
Code::Internal,
355-
"Failed to change atime of file due to spawn failing {:?}",
356-
e
357-
)
358-
})?
359-
})
360-
.await;
361-
if let Err(err) = result {
362-
event!(Level::ERROR, ?err, "Failed to touch file",);
363-
return false;
364-
}
365-
true
366-
}
367-
368340
// unref() only triggers when an item is removed from the eviction_map. It is possible
369341
// that another place in code has a reference to `FileEntryImpl` and may later read the
370342
// file. To support this edge case, we first move the file to a temp file and point
@@ -499,20 +471,13 @@ async fn add_files_to_cache<Fe: FileEntry>(
499471
// We need to filter out folders - we do not want to try to cache the s and d folders.
500472
let is_file =
501473
metadata.is_file() || !(file_name == STR_FOLDER || file_name == DIGEST_FOLDER);
502-
let atime = match metadata.accessed() {
503-
Ok(atime) => atime,
504-
Err(err) => {
505-
panic!(
506-
"{}{}{} : {} {:?}",
507-
"It appears this filesystem does not support access time. ",
508-
"Please configure this program to run on a drive that supports ",
509-
"atime",
510-
file_name,
511-
err
512-
);
513-
}
514-
};
515-
474+
// Using access time is not perfect, but better than random. We do not update the
475+
// atime when a file is actually "touched", we rely on whatever the filesystem does
476+
// when we read the file (usually update on read).
477+
let atime = metadata
478+
.accessed()
479+
.or_else(|_| metadata.modified())
480+
.unwrap_or(SystemTime::UNIX_EPOCH);
516481
Result::<(String, SystemTime, u64, bool), Error>::Ok((
517482
file_name,
518483
atime,
@@ -966,7 +931,19 @@ impl<Fe: FileEntry> StoreDriver for FilesystemStore<Fe> {
966931
)
967932
})?;
968933
let read_limit = length.unwrap_or(u64::MAX);
969-
let mut temp_file = entry.read_file_part(offset, read_limit).await?;
934+
let mut temp_file = entry.read_file_part(offset, read_limit).or_else(|err| async move {
935+
// If the file is not found, we need to remove it from the eviction map.
936+
if err.code == Code::NotFound {
937+
event!(
938+
Level::ERROR,
939+
?err,
940+
?key,
941+
"Entry was in our map, but not found on disk. Removing from map as a precaution, but process probably need restarted."
942+
);
943+
self.evicting_map.remove(&key).await;
944+
}
945+
Err(err)
946+
}).await?;
970947

971948
loop {
972949
let mut buf = BytesMut::with_capacity(self.read_buffer_size);

nativelink-store/tests/filesystem_store_test.rs

Lines changed: 5 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ use std::marker::PhantomData;
1919
use std::path::Path;
2020
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
2121
use std::sync::{Arc, LazyLock};
22-
use std::time::{Duration, SystemTime};
22+
use std::time::Duration;
2323

2424
use async_lock::RwLock;
2525
use bytes::Bytes;
26-
use filetime::{set_file_atime, FileTime};
2726
use futures::executor::block_on;
2827
use futures::task::Poll;
2928
use futures::{poll, Future, FutureExt};
@@ -142,10 +141,6 @@ impl<Hooks: FileEntryHooks + 'static + Sync + Send> LenEntry for TestFileEntry<H
142141
self.inner.as_ref().unwrap().is_empty()
143142
}
144143

145-
async fn touch(&self) -> bool {
146-
self.inner.as_ref().unwrap().touch().await
147-
}
148-
149144
async fn unref(&self) {
150145
Hooks::on_unref(self);
151146
self.inner.as_ref().unwrap().unref().await;
@@ -570,98 +565,6 @@ async fn file_gets_cleans_up_on_cache_eviction() -> Result<(), Error> {
570565
check_temp_empty(&temp_path).await
571566
}
572567

573-
#[nativelink_test]
574-
async fn atime_updates_on_get_part_test() -> Result<(), Error> {
575-
let digest1 = DigestInfo::try_new(HASH1, VALUE1.len())?;
576-
577-
let store = Box::pin(
578-
FilesystemStore::<FileEntryImpl>::new(&FilesystemSpec {
579-
content_path: make_temp_path("content_path"),
580-
temp_path: make_temp_path("temp_path"),
581-
eviction_policy: None,
582-
..Default::default()
583-
})
584-
.await?,
585-
);
586-
// Insert data into store.
587-
store.update_oneshot(digest1, VALUE1.into()).await?;
588-
589-
let file_entry = store.get_file_entry_for_digest(&digest1).await?;
590-
file_entry
591-
.get_file_path_locked(move |path| async move {
592-
// Set atime to along time ago.
593-
set_file_atime(&path, FileTime::from_system_time(SystemTime::UNIX_EPOCH))?;
594-
595-
// Check to ensure it was set to zero from previous command.
596-
assert_eq!(
597-
fs::metadata(&path).await?.accessed()?,
598-
SystemTime::UNIX_EPOCH
599-
);
600-
Ok(())
601-
})
602-
.await?;
603-
604-
// Now touch digest1.
605-
let data = store.get_part_unchunked(digest1, 0, None).await?;
606-
assert_eq!(data, VALUE1.as_bytes());
607-
608-
file_entry
609-
.get_file_path_locked(move |path| async move {
610-
// Ensure it was updated.
611-
assert!(fs::metadata(&path).await?.accessed()? > SystemTime::UNIX_EPOCH);
612-
Ok(())
613-
})
614-
.await?;
615-
616-
Ok(())
617-
}
618-
619-
#[nativelink_test]
620-
async fn eviction_drops_file_test() -> Result<(), Error> {
621-
let digest1 = DigestInfo::try_new(HASH1, VALUE1.len())?;
622-
623-
let store = Box::pin(
624-
FilesystemStore::<FileEntryImpl>::new(&FilesystemSpec {
625-
content_path: make_temp_path("content_path"),
626-
temp_path: make_temp_path("temp_path"),
627-
eviction_policy: None,
628-
..Default::default()
629-
})
630-
.await?,
631-
);
632-
// Insert data into store.
633-
store.update_oneshot(digest1, VALUE1.into()).await?;
634-
635-
let file_entry = store.get_file_entry_for_digest(&digest1).await?;
636-
file_entry
637-
.get_file_path_locked(move |path| async move {
638-
// Set atime to along time ago.
639-
set_file_atime(&path, FileTime::from_system_time(SystemTime::UNIX_EPOCH))?;
640-
641-
// Check to ensure it was set to zero from previous command.
642-
assert_eq!(
643-
fs::metadata(&path).await?.accessed()?,
644-
SystemTime::UNIX_EPOCH
645-
);
646-
Ok(())
647-
})
648-
.await?;
649-
650-
// Now touch digest1.
651-
let data = store.get_part_unchunked(digest1, 0, None).await?;
652-
assert_eq!(data, VALUE1.as_bytes());
653-
654-
file_entry
655-
.get_file_path_locked(move |path| async move {
656-
// Ensure it was updated.
657-
assert!(fs::metadata(&path).await?.accessed()? > SystemTime::UNIX_EPOCH);
658-
Ok(())
659-
})
660-
.await?;
661-
662-
Ok(())
663-
}
664-
665568
// Test to ensure that if we are holding a reference to `FileEntry` and the contents are
666569
// replaced, the `FileEntry` continues to use the old data.
667570
// `FileEntry` file contents should be immutable for the lifetime of the object.
@@ -1150,11 +1053,8 @@ async fn deleted_file_removed_from_store() -> Result<(), Error> {
11501053
let stored_file_path = OsString::from(format!("{content_path}/{DIGEST_FOLDER}/{digest}"));
11511054
std::fs::remove_file(stored_file_path)?;
11521055

1153-
let digest_result = store
1154-
.has(digest)
1155-
.await
1156-
.err_tip(|| "Failed to execute has")?;
1157-
assert!(digest_result.is_none());
1056+
let get_part_res = store.get_part_unchunked(digest, 0, None).await;
1057+
assert_eq!(get_part_res.unwrap_err().code, Code::NotFound);
11581058

11591059
// Repeat with a string typed key.
11601060

@@ -1168,11 +1068,8 @@ async fn deleted_file_removed_from_store() -> Result<(), Error> {
11681068
let stored_file_path = OsString::from(format!("{content_path}/{STR_FOLDER}/{STRING_NAME}"));
11691069
std::fs::remove_file(stored_file_path)?;
11701070

1171-
let string_result = store
1172-
.has(string_key)
1173-
.await
1174-
.err_tip(|| "Failed to execute has")?;
1175-
assert!(string_result.is_none());
1071+
let string_digest_get_part_res = store.get_part_unchunked(string_key, 0, None).await;
1072+
assert_eq!(string_digest_get_part_res.unwrap_err().code, Code::NotFound);
11761073

11771074
Ok(())
11781075
}

nativelink-util/src/evicting_map.rs

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,6 @@ pub trait LenEntry: 'static {
5050
/// Returns `true` if `self` has zero length.
5151
fn is_empty(&self) -> bool;
5252

53-
/// Called when an entry is touched. On failure, will remove the entry
54-
/// from the map.
55-
#[inline]
56-
fn touch(&self) -> impl Future<Output = bool> + Send {
57-
std::future::ready(true)
58-
}
59-
6053
/// This will be called when object is removed from map.
6154
/// Note: There may still be a reference to it held somewhere else, which
6255
/// is why it can't be mutable. This is a good place to mark the item
@@ -86,11 +79,6 @@ impl<T: LenEntry + Send + Sync> LenEntry for Arc<T> {
8679
T::is_empty(self.as_ref())
8780
}
8881

89-
#[inline]
90-
async fn touch(&self) -> bool {
91-
self.as_ref().touch().await
92-
}
93-
9482
#[inline]
9583
async fn unref(&self) {
9684
self.as_ref().unref().await;
@@ -347,27 +335,21 @@ where
347335
};
348336
match maybe_entry {
349337
Some(entry) => {
350-
// Since we are not inserting anythign we don't need to evict based
351-
// on the size of the store.
352338
// Note: We need to check eviction because the item might be expired
353339
// based on the current time. In such case, we remove the item while
354340
// we are here.
355-
let should_evict = self.should_evict(lru_len, entry, 0, u64::MAX);
356-
if !should_evict && peek {
357-
*result = Some(entry.data.len());
358-
} else if !should_evict && entry.data.touch().await {
359-
entry.seconds_since_anchor = self.anchor_time.elapsed().as_secs() as i32;
360-
*result = Some(entry.data.len());
361-
} else {
341+
if self.should_evict(lru_len, entry, 0, u64::MAX) {
362342
*result = None;
363343
if let Some((key, eviction_item)) = state.lru.pop_entry(key.borrow()) {
364-
if should_evict {
365-
event!(Level::INFO, ?key, "Item expired, evicting");
366-
} else {
367-
event!(Level::INFO, ?key, "Touch failed, evicting");
368-
}
344+
event!(Level::INFO, ?key, "Item expired, evicting");
369345
state.remove(key.borrow(), &eviction_item, false).await;
370346
}
347+
} else {
348+
if !peek {
349+
entry.seconds_since_anchor =
350+
self.anchor_time.elapsed().as_secs() as i32;
351+
}
352+
*result = Some(entry.data.len());
371353
}
372354
}
373355
None => *result = None,
@@ -385,15 +367,8 @@ where
385367

386368
let entry = state.lru.get_mut(key.borrow())?;
387369

388-
if entry.data.touch().await {
389-
entry.seconds_since_anchor = self.anchor_time.elapsed().as_secs() as i32;
390-
return Some(entry.data.clone());
391-
}
392-
393-
let (key, eviction_item) = state.lru.pop_entry(key.borrow())?;
394-
event!(Level::INFO, ?key, "Touch failed, evicting");
395-
state.remove(key.borrow(), &eviction_item, false).await;
396-
None
370+
entry.seconds_since_anchor = self.anchor_time.elapsed().as_secs() as i32;
371+
Some(entry.data.clone())
397372
}
398373

399374
/// Returns the replaced item if any.

nativelink-util/tests/evicting_map_test.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -356,11 +356,6 @@ async fn unref_called_on_replace() -> Result<(), Error> {
356356
unreachable!("We are not testing this functionality");
357357
}
358358

359-
async fn touch(&self) -> bool {
360-
// Do nothing. We are not testing this functionality.
361-
true
362-
}
363-
364359
async fn unref(&self) {
365360
self.unref_called.store(true, Ordering::Relaxed);
366361
}

0 commit comments

Comments
 (0)