108f9eb17SAlex Crichton //! Background worker that watches over the cache.
208f9eb17SAlex Crichton //!
308f9eb17SAlex Crichton //! It cleans up old cache, updates statistics and optimizes the cache.
408f9eb17SAlex Crichton //! We allow losing some messages (it doesn't hurt) and some races,
508f9eb17SAlex Crichton //! but we guarantee eventual consistency and fault tolerancy.
608f9eb17SAlex Crichton //! Background tasks can be CPU intensive, but the worker thread has low priority.
708f9eb17SAlex Crichton
8703871a2SAlex Crichton #![cfg_attr(
9703871a2SAlex Crichton not(test),
10703871a2SAlex Crichton expect(
11703871a2SAlex Crichton clippy::useless_conversion,
12703871a2SAlex Crichton reason = "cfg(test) and cfg(not(test)) have a different definition \
13703871a2SAlex Crichton of `SystemTime`, so conversions below are needed in \
14703871a2SAlex Crichton one mode but not the other, just ignore the lint in this \
15703871a2SAlex Crichton module in not(test) mode where the conversion isn't required",
16703871a2SAlex Crichton )
17703871a2SAlex Crichton )]
18703871a2SAlex Crichton
1990ac295eSAlex Crichton use super::{CacheConfig, fs_write_atomic};
2008f9eb17SAlex Crichton use log::{debug, info, trace, warn};
219ec02f9dSChristopher Serr use serde_derive::{Deserialize, Serialize};
2208f9eb17SAlex Crichton use std::cmp;
2308f9eb17SAlex Crichton use std::collections::HashMap;
2408f9eb17SAlex Crichton use std::ffi::OsStr;
2508f9eb17SAlex Crichton use std::fmt;
2608f9eb17SAlex Crichton use std::fs;
2708f9eb17SAlex Crichton use std::path::{Path, PathBuf};
2890ac295eSAlex Crichton use std::sync::mpsc::{Receiver, SyncSender, sync_channel};
2908f9eb17SAlex Crichton #[cfg(test)]
3008f9eb17SAlex Crichton use std::sync::{Arc, Condvar, Mutex};
3108f9eb17SAlex Crichton use std::thread;
3208f9eb17SAlex Crichton use std::time::Duration;
3308f9eb17SAlex Crichton #[cfg(not(test))]
3408f9eb17SAlex Crichton use std::time::SystemTime;
3508f9eb17SAlex Crichton #[cfg(test)]
3608f9eb17SAlex Crichton use tests::system_time_stub::SystemTimeStub as SystemTime;
3708f9eb17SAlex Crichton
3808f9eb17SAlex Crichton #[derive(Clone)]
3908f9eb17SAlex Crichton pub(super) struct Worker {
4008f9eb17SAlex Crichton sender: SyncSender<CacheEvent>,
4108f9eb17SAlex Crichton #[cfg(test)]
4208f9eb17SAlex Crichton stats: Arc<(Mutex<WorkerStats>, Condvar)>,
4308f9eb17SAlex Crichton }
4408f9eb17SAlex Crichton
4508f9eb17SAlex Crichton struct WorkerThread {
4608f9eb17SAlex Crichton receiver: Receiver<CacheEvent>,
4708f9eb17SAlex Crichton cache_config: CacheConfig,
4808f9eb17SAlex Crichton #[cfg(test)]
4908f9eb17SAlex Crichton stats: Arc<(Mutex<WorkerStats>, Condvar)>,
5008f9eb17SAlex Crichton }
5108f9eb17SAlex Crichton
5208f9eb17SAlex Crichton #[cfg(test)]
5308f9eb17SAlex Crichton #[derive(Default)]
5408f9eb17SAlex Crichton struct WorkerStats {
5508f9eb17SAlex Crichton dropped: u32,
5608f9eb17SAlex Crichton sent: u32,
5708f9eb17SAlex Crichton handled: u32,
5808f9eb17SAlex Crichton }
5908f9eb17SAlex Crichton
6008f9eb17SAlex Crichton #[derive(Debug, Clone)]
6108f9eb17SAlex Crichton enum CacheEvent {
6208f9eb17SAlex Crichton OnCacheGet(PathBuf),
6308f9eb17SAlex Crichton OnCacheUpdate(PathBuf),
6408f9eb17SAlex Crichton }
6508f9eb17SAlex Crichton
6608f9eb17SAlex Crichton impl Worker {
start_new(cache_config: &CacheConfig) -> Self671d4766f3SQuentin Gliech pub(super) fn start_new(cache_config: &CacheConfig) -> Self {
6808f9eb17SAlex Crichton let queue_size = match cache_config.worker_event_queue_size() {
6908f9eb17SAlex Crichton num if num <= usize::max_value() as u64 => num as usize,
7008f9eb17SAlex Crichton _ => usize::max_value(),
7108f9eb17SAlex Crichton };
7208f9eb17SAlex Crichton let (tx, rx) = sync_channel(queue_size);
7308f9eb17SAlex Crichton
7408f9eb17SAlex Crichton #[cfg(test)]
7508f9eb17SAlex Crichton let stats = Arc::new((Mutex::new(WorkerStats::default()), Condvar::new()));
7608f9eb17SAlex Crichton
7708f9eb17SAlex Crichton let worker_thread = WorkerThread {
7808f9eb17SAlex Crichton receiver: rx,
7908f9eb17SAlex Crichton cache_config: cache_config.clone(),
8008f9eb17SAlex Crichton #[cfg(test)]
8108f9eb17SAlex Crichton stats: stats.clone(),
8208f9eb17SAlex Crichton };
8308f9eb17SAlex Crichton
8408f9eb17SAlex Crichton // when self is dropped, sender will be dropped, what will cause the channel
8508f9eb17SAlex Crichton // to hang, and the worker thread to exit -- it happens in the tests
8608f9eb17SAlex Crichton // non-tests binary has only a static worker, so Rust doesn't drop it
871d4766f3SQuentin Gliech thread::spawn(move || worker_thread.run());
8808f9eb17SAlex Crichton
8908f9eb17SAlex Crichton Self {
9008f9eb17SAlex Crichton sender: tx,
9108f9eb17SAlex Crichton #[cfg(test)]
9208f9eb17SAlex Crichton stats,
9308f9eb17SAlex Crichton }
9408f9eb17SAlex Crichton }
9508f9eb17SAlex Crichton
on_cache_get_async(&self, path: impl AsRef<Path>)9608f9eb17SAlex Crichton pub(super) fn on_cache_get_async(&self, path: impl AsRef<Path>) {
9708f9eb17SAlex Crichton let event = CacheEvent::OnCacheGet(path.as_ref().to_path_buf());
9808f9eb17SAlex Crichton self.send_cache_event(event);
9908f9eb17SAlex Crichton }
10008f9eb17SAlex Crichton
on_cache_update_async(&self, path: impl AsRef<Path>)10108f9eb17SAlex Crichton pub(super) fn on_cache_update_async(&self, path: impl AsRef<Path>) {
10208f9eb17SAlex Crichton let event = CacheEvent::OnCacheUpdate(path.as_ref().to_path_buf());
10308f9eb17SAlex Crichton self.send_cache_event(event);
10408f9eb17SAlex Crichton }
10508f9eb17SAlex Crichton
10608f9eb17SAlex Crichton #[inline]
send_cache_event(&self, event: CacheEvent)10708f9eb17SAlex Crichton fn send_cache_event(&self, event: CacheEvent) {
10808f9eb17SAlex Crichton let sent_event = self.sender.try_send(event.clone());
10908f9eb17SAlex Crichton
11008f9eb17SAlex Crichton if let Err(ref err) = sent_event {
11108f9eb17SAlex Crichton info!(
11208f9eb17SAlex Crichton "Failed to send asynchronously message to worker thread, \
1132bac6574SAlex Crichton event: {event:?}, error: {err}"
11408f9eb17SAlex Crichton );
11508f9eb17SAlex Crichton }
11608f9eb17SAlex Crichton
11708f9eb17SAlex Crichton #[cfg(test)]
11808f9eb17SAlex Crichton {
11908f9eb17SAlex Crichton let mut stats = self
12008f9eb17SAlex Crichton .stats
12108f9eb17SAlex Crichton .0
12208f9eb17SAlex Crichton .lock()
12308f9eb17SAlex Crichton .expect("Failed to acquire worker stats lock");
12408f9eb17SAlex Crichton
12508f9eb17SAlex Crichton if sent_event.is_ok() {
12608f9eb17SAlex Crichton stats.sent += 1;
12708f9eb17SAlex Crichton } else {
12808f9eb17SAlex Crichton stats.dropped += 1;
12908f9eb17SAlex Crichton }
13008f9eb17SAlex Crichton }
13108f9eb17SAlex Crichton }
13208f9eb17SAlex Crichton
13308f9eb17SAlex Crichton #[cfg(test)]
events_dropped(&self) -> u3213408f9eb17SAlex Crichton pub(super) fn events_dropped(&self) -> u32 {
13508f9eb17SAlex Crichton let stats = self
13608f9eb17SAlex Crichton .stats
13708f9eb17SAlex Crichton .0
13808f9eb17SAlex Crichton .lock()
13908f9eb17SAlex Crichton .expect("Failed to acquire worker stats lock");
14008f9eb17SAlex Crichton stats.dropped
14108f9eb17SAlex Crichton }
14208f9eb17SAlex Crichton
14308f9eb17SAlex Crichton #[cfg(test)]
wait_for_all_events_handled(&self)14408f9eb17SAlex Crichton pub(super) fn wait_for_all_events_handled(&self) {
14508f9eb17SAlex Crichton let (stats, condvar) = &*self.stats;
14608f9eb17SAlex Crichton let mut stats = stats.lock().expect("Failed to acquire worker stats lock");
14708f9eb17SAlex Crichton while stats.handled != stats.sent {
14808f9eb17SAlex Crichton stats = condvar
14908f9eb17SAlex Crichton .wait(stats)
15008f9eb17SAlex Crichton .expect("Failed to reacquire worker stats lock");
15108f9eb17SAlex Crichton }
15208f9eb17SAlex Crichton }
15308f9eb17SAlex Crichton }
15408f9eb17SAlex Crichton
15508f9eb17SAlex Crichton impl fmt::Debug for Worker {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result15608f9eb17SAlex Crichton fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15708f9eb17SAlex Crichton f.debug_struct("Worker").finish()
15808f9eb17SAlex Crichton }
15908f9eb17SAlex Crichton }
16008f9eb17SAlex Crichton
16108f9eb17SAlex Crichton #[derive(Serialize, Deserialize)]
16208f9eb17SAlex Crichton struct ModuleCacheStatistics {
16308f9eb17SAlex Crichton pub usages: u64,
16408f9eb17SAlex Crichton #[serde(rename = "optimized-compression")]
16508f9eb17SAlex Crichton pub compression_level: i32,
16608f9eb17SAlex Crichton }
16708f9eb17SAlex Crichton
16808f9eb17SAlex Crichton impl ModuleCacheStatistics {
default(cache_config: &CacheConfig) -> Self16908f9eb17SAlex Crichton fn default(cache_config: &CacheConfig) -> Self {
17008f9eb17SAlex Crichton Self {
17108f9eb17SAlex Crichton usages: 0,
17208f9eb17SAlex Crichton compression_level: cache_config.baseline_compression_level(),
17308f9eb17SAlex Crichton }
17408f9eb17SAlex Crichton }
17508f9eb17SAlex Crichton }
17608f9eb17SAlex Crichton
17708f9eb17SAlex Crichton enum CacheEntry {
17808f9eb17SAlex Crichton Recognized {
17908f9eb17SAlex Crichton path: PathBuf,
18008f9eb17SAlex Crichton mtime: SystemTime,
18108f9eb17SAlex Crichton size: u64,
18208f9eb17SAlex Crichton },
18308f9eb17SAlex Crichton Unrecognized {
18408f9eb17SAlex Crichton path: PathBuf,
18508f9eb17SAlex Crichton is_dir: bool,
18608f9eb17SAlex Crichton },
18708f9eb17SAlex Crichton }
18808f9eb17SAlex Crichton
18908f9eb17SAlex Crichton macro_rules! unwrap_or_warn {
19008f9eb17SAlex Crichton ($result:expr, $cont:stmt, $err_msg:expr, $path:expr) => {
19108f9eb17SAlex Crichton match $result {
19208f9eb17SAlex Crichton Ok(val) => val,
19308f9eb17SAlex Crichton Err(err) => {
19408f9eb17SAlex Crichton warn!("{}, path: {}, msg: {}", $err_msg, $path.display(), err);
19508f9eb17SAlex Crichton $cont
19608f9eb17SAlex Crichton }
19708f9eb17SAlex Crichton }
19808f9eb17SAlex Crichton };
19908f9eb17SAlex Crichton }
20008f9eb17SAlex Crichton
20108f9eb17SAlex Crichton impl WorkerThread {
run(self)2021d4766f3SQuentin Gliech fn run(self) {
20308f9eb17SAlex Crichton debug!("Cache worker thread started.");
20408f9eb17SAlex Crichton
20508f9eb17SAlex Crichton Self::lower_thread_priority();
20608f9eb17SAlex Crichton
20708f9eb17SAlex Crichton #[cfg(test)]
20808f9eb17SAlex Crichton let (stats, condvar) = &*self.stats;
20908f9eb17SAlex Crichton
21008f9eb17SAlex Crichton for event in self.receiver.iter() {
21108f9eb17SAlex Crichton match event {
21208f9eb17SAlex Crichton CacheEvent::OnCacheGet(path) => self.handle_on_cache_get(path),
21308f9eb17SAlex Crichton CacheEvent::OnCacheUpdate(path) => self.handle_on_cache_update(path),
21408f9eb17SAlex Crichton }
21508f9eb17SAlex Crichton
21608f9eb17SAlex Crichton #[cfg(test)]
21708f9eb17SAlex Crichton {
21808f9eb17SAlex Crichton let mut stats = stats.lock().expect("Failed to acquire worker stats lock");
21908f9eb17SAlex Crichton stats.handled += 1;
22008f9eb17SAlex Crichton condvar.notify_all();
22108f9eb17SAlex Crichton }
22208f9eb17SAlex Crichton }
22308f9eb17SAlex Crichton }
22408f9eb17SAlex Crichton
22508f9eb17SAlex Crichton #[cfg(target_os = "fuchsia")]
lower_thread_priority()22608f9eb17SAlex Crichton fn lower_thread_priority() {
22708f9eb17SAlex Crichton // TODO This needs to use Fuchsia thread profiles
22808f9eb17SAlex Crichton // https://fuchsia.dev/fuchsia-src/reference/kernel_objects/profile
22908f9eb17SAlex Crichton warn!(
23008f9eb17SAlex Crichton "Lowering thread priority on Fuchsia is currently a noop. It might affect application performance."
23108f9eb17SAlex Crichton );
23208f9eb17SAlex Crichton }
23308f9eb17SAlex Crichton
23408f9eb17SAlex Crichton #[cfg(target_os = "windows")]
lower_thread_priority()23508f9eb17SAlex Crichton fn lower_thread_priority() {
236df150253SAlex Crichton use windows_sys::Win32::System::Threading::*;
23708f9eb17SAlex Crichton
23808f9eb17SAlex Crichton // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadpriority
23908f9eb17SAlex Crichton // https://docs.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities
24008f9eb17SAlex Crichton
2412d95076dSAlex Crichton if unsafe { SetThreadPriority(GetCurrentThread(), THREAD_MODE_BACKGROUND_BEGIN) } == 0 {
24208f9eb17SAlex Crichton warn!(
24308f9eb17SAlex Crichton "Failed to lower worker thread priority. It might affect application performance."
24408f9eb17SAlex Crichton );
24508f9eb17SAlex Crichton }
24608f9eb17SAlex Crichton }
24708f9eb17SAlex Crichton
24808f9eb17SAlex Crichton #[cfg(not(any(target_os = "windows", target_os = "fuchsia")))]
lower_thread_priority()24908f9eb17SAlex Crichton fn lower_thread_priority() {
25008f9eb17SAlex Crichton // http://man7.org/linux/man-pages/man7/sched.7.html
25108f9eb17SAlex Crichton
25208f9eb17SAlex Crichton const NICE_DELTA_FOR_BACKGROUND_TASKS: i32 = 3;
25308f9eb17SAlex Crichton
254ea0cb971SDan Gohman match rustix::process::nice(NICE_DELTA_FOR_BACKGROUND_TASKS) {
25547490b43SDan Gohman Ok(current_nice) => {
2562bac6574SAlex Crichton debug!("New nice value of worker thread: {current_nice}");
25708f9eb17SAlex Crichton }
25847490b43SDan Gohman Err(err) => {
25947490b43SDan Gohman warn!(
2602bac6574SAlex Crichton "Failed to lower worker thread priority ({err:?}). It might affect application performance."
26190ac295eSAlex Crichton );
26247490b43SDan Gohman }
26347490b43SDan Gohman };
26408f9eb17SAlex Crichton }
26508f9eb17SAlex Crichton
26608f9eb17SAlex Crichton /// Increases the usage counter and recompresses the file
2670e9121daSFrankReh /// if the usage counter reached configurable threshold.
handle_on_cache_get(&self, path: PathBuf)26808f9eb17SAlex Crichton fn handle_on_cache_get(&self, path: PathBuf) {
26908f9eb17SAlex Crichton trace!("handle_on_cache_get() for path: {}", path.display());
27008f9eb17SAlex Crichton
27108f9eb17SAlex Crichton // construct .stats file path
27208f9eb17SAlex Crichton let filename = path.file_name().unwrap().to_str().unwrap();
273a0442ea0SHamir Mahal let stats_path = path.with_file_name(format!("{filename}.stats"));
27408f9eb17SAlex Crichton
27508f9eb17SAlex Crichton // load .stats file (default if none or error)
27608f9eb17SAlex Crichton let mut stats = read_stats_file(stats_path.as_ref())
27708f9eb17SAlex Crichton .unwrap_or_else(|| ModuleCacheStatistics::default(&self.cache_config));
27808f9eb17SAlex Crichton
27908f9eb17SAlex Crichton // step 1: update the usage counter & write to the disk
28008f9eb17SAlex Crichton // it's racy, but it's fine (the counter will be just smaller,
28108f9eb17SAlex Crichton // sometimes will retrigger recompression)
28208f9eb17SAlex Crichton stats.usages += 1;
28308f9eb17SAlex Crichton if !write_stats_file(stats_path.as_ref(), &stats) {
28408f9eb17SAlex Crichton return;
28508f9eb17SAlex Crichton }
28608f9eb17SAlex Crichton
28708f9eb17SAlex Crichton // step 2: recompress if there's a need
28808f9eb17SAlex Crichton let opt_compr_lvl = self.cache_config.optimized_compression_level();
28908f9eb17SAlex Crichton if stats.compression_level >= opt_compr_lvl
29008f9eb17SAlex Crichton || stats.usages
29108f9eb17SAlex Crichton < self
29208f9eb17SAlex Crichton .cache_config
29308f9eb17SAlex Crichton .optimized_compression_usage_counter_threshold()
29408f9eb17SAlex Crichton {
29508f9eb17SAlex Crichton return;
29608f9eb17SAlex Crichton }
29708f9eb17SAlex Crichton
29808f9eb17SAlex Crichton let lock_path = if let Some(p) = acquire_task_fs_lock(
29908f9eb17SAlex Crichton path.as_ref(),
30008f9eb17SAlex Crichton self.cache_config.optimizing_compression_task_timeout(),
30108f9eb17SAlex Crichton self.cache_config
30208f9eb17SAlex Crichton .allowed_clock_drift_for_files_from_future(),
30308f9eb17SAlex Crichton ) {
30408f9eb17SAlex Crichton p
30508f9eb17SAlex Crichton } else {
30608f9eb17SAlex Crichton return;
30708f9eb17SAlex Crichton };
30808f9eb17SAlex Crichton
30908f9eb17SAlex Crichton trace!("Trying to recompress file: {}", path.display());
31008f9eb17SAlex Crichton
31108f9eb17SAlex Crichton // recompress, write to other file, rename (it's atomic file content exchange)
31208f9eb17SAlex Crichton // and update the stats file
31308f9eb17SAlex Crichton let compressed_cache_bytes = unwrap_or_warn!(
31408f9eb17SAlex Crichton fs::read(&path),
31508f9eb17SAlex Crichton return,
31608f9eb17SAlex Crichton "Failed to read old cache file",
31708f9eb17SAlex Crichton path
31808f9eb17SAlex Crichton );
31908f9eb17SAlex Crichton
32008f9eb17SAlex Crichton let cache_bytes = unwrap_or_warn!(
32108f9eb17SAlex Crichton zstd::decode_all(&compressed_cache_bytes[..]),
32208f9eb17SAlex Crichton return,
32308f9eb17SAlex Crichton "Failed to decompress cached code",
32408f9eb17SAlex Crichton path
32508f9eb17SAlex Crichton );
32608f9eb17SAlex Crichton
32708f9eb17SAlex Crichton let recompressed_cache_bytes = unwrap_or_warn!(
32808f9eb17SAlex Crichton zstd::encode_all(&cache_bytes[..], opt_compr_lvl),
32908f9eb17SAlex Crichton return,
33008f9eb17SAlex Crichton "Failed to compress cached code",
33108f9eb17SAlex Crichton path
33208f9eb17SAlex Crichton );
33308f9eb17SAlex Crichton
33408f9eb17SAlex Crichton unwrap_or_warn!(
33508f9eb17SAlex Crichton fs::write(&lock_path, &recompressed_cache_bytes),
33608f9eb17SAlex Crichton return,
33708f9eb17SAlex Crichton "Failed to write recompressed cache",
33808f9eb17SAlex Crichton lock_path
33908f9eb17SAlex Crichton );
34008f9eb17SAlex Crichton
34108f9eb17SAlex Crichton unwrap_or_warn!(
34208f9eb17SAlex Crichton fs::rename(&lock_path, &path),
34308f9eb17SAlex Crichton {
34408f9eb17SAlex Crichton if let Err(error) = fs::remove_file(&lock_path) {
34508f9eb17SAlex Crichton warn!(
34608f9eb17SAlex Crichton "Failed to clean up (remove) recompressed cache, path {}, err: {}",
34708f9eb17SAlex Crichton lock_path.display(),
34808f9eb17SAlex Crichton error
34908f9eb17SAlex Crichton );
35008f9eb17SAlex Crichton }
35108f9eb17SAlex Crichton
35208f9eb17SAlex Crichton return;
35308f9eb17SAlex Crichton },
35408f9eb17SAlex Crichton "Failed to rename recompressed cache",
35508f9eb17SAlex Crichton lock_path
35608f9eb17SAlex Crichton );
35708f9eb17SAlex Crichton
35808f9eb17SAlex Crichton // update stats file (reload it! recompression can take some time)
35908f9eb17SAlex Crichton if let Some(mut new_stats) = read_stats_file(stats_path.as_ref()) {
36008f9eb17SAlex Crichton if new_stats.compression_level >= opt_compr_lvl {
36108f9eb17SAlex Crichton // Rare race:
36208f9eb17SAlex Crichton // two instances with different opt_compr_lvl: we don't know in which order they updated
36308f9eb17SAlex Crichton // the cache file and the stats file (they are not updated together atomically)
36408f9eb17SAlex Crichton // Possible solution is to use directories per cache entry, but it complicates the system
36508f9eb17SAlex Crichton // and is not worth it.
36608f9eb17SAlex Crichton debug!(
36708f9eb17SAlex Crichton "DETECTED task did more than once (or race with new file): \
36808f9eb17SAlex Crichton recompression of {}. Note: if optimized compression level setting \
36908f9eb17SAlex Crichton has changed in the meantine, the stats file might contain \
37008f9eb17SAlex Crichton inconsistent compression level due to race.",
37108f9eb17SAlex Crichton path.display()
37208f9eb17SAlex Crichton );
37308f9eb17SAlex Crichton } else {
37408f9eb17SAlex Crichton new_stats.compression_level = opt_compr_lvl;
37508f9eb17SAlex Crichton let _ = write_stats_file(stats_path.as_ref(), &new_stats);
37608f9eb17SAlex Crichton }
37708f9eb17SAlex Crichton
37808f9eb17SAlex Crichton if new_stats.usages < stats.usages {
37908f9eb17SAlex Crichton debug!(
38008f9eb17SAlex Crichton "DETECTED lower usage count (new file or race with counter \
38108f9eb17SAlex Crichton increasing): file {}",
38208f9eb17SAlex Crichton path.display()
38308f9eb17SAlex Crichton );
38408f9eb17SAlex Crichton }
38508f9eb17SAlex Crichton } else {
38608f9eb17SAlex Crichton debug!(
38708f9eb17SAlex Crichton "Can't read stats file again to update compression level (it might got \
38808f9eb17SAlex Crichton cleaned up): file {}",
38908f9eb17SAlex Crichton stats_path.display()
39008f9eb17SAlex Crichton );
39108f9eb17SAlex Crichton }
39208f9eb17SAlex Crichton
39308f9eb17SAlex Crichton trace!("Task finished: recompress file: {}", path.display());
39408f9eb17SAlex Crichton }
39508f9eb17SAlex Crichton
directory(&self) -> &PathBuf396*61a371acSJesse Rusak fn directory(&self) -> &PathBuf {
397*61a371acSJesse Rusak self.cache_config
398*61a371acSJesse Rusak .directory()
399*61a371acSJesse Rusak .expect("CacheConfig should be validated before being passed to a WorkerThread")
400*61a371acSJesse Rusak }
401*61a371acSJesse Rusak
handle_on_cache_update(&self, path: PathBuf)40208f9eb17SAlex Crichton fn handle_on_cache_update(&self, path: PathBuf) {
40308f9eb17SAlex Crichton trace!("handle_on_cache_update() for path: {}", path.display());
40408f9eb17SAlex Crichton
40508f9eb17SAlex Crichton // ---------------------- step 1: create .stats file
40608f9eb17SAlex Crichton
40708f9eb17SAlex Crichton // construct .stats file path
40808f9eb17SAlex Crichton let filename = path
40908f9eb17SAlex Crichton .file_name()
41008f9eb17SAlex Crichton .expect("Expected valid cache file name")
41108f9eb17SAlex Crichton .to_str()
41208f9eb17SAlex Crichton .expect("Expected valid cache file name");
413a0442ea0SHamir Mahal let stats_path = path.with_file_name(format!("{filename}.stats"));
41408f9eb17SAlex Crichton
41508f9eb17SAlex Crichton // create and write stats file
41608f9eb17SAlex Crichton let mut stats = ModuleCacheStatistics::default(&self.cache_config);
41708f9eb17SAlex Crichton stats.usages += 1;
41808f9eb17SAlex Crichton write_stats_file(&stats_path, &stats);
41908f9eb17SAlex Crichton
42008f9eb17SAlex Crichton // ---------------------- step 2: perform cleanup task if needed
42108f9eb17SAlex Crichton
42208f9eb17SAlex Crichton // acquire lock for cleanup task
42308f9eb17SAlex Crichton // Lock is a proof of recent cleanup task, so we don't want to delete them.
42408f9eb17SAlex Crichton // Expired locks will be deleted by the cleanup task.
425*61a371acSJesse Rusak let cleanup_file = self.directory().join(".cleanup"); // some non existing marker file
42608f9eb17SAlex Crichton if acquire_task_fs_lock(
42708f9eb17SAlex Crichton &cleanup_file,
42808f9eb17SAlex Crichton self.cache_config.cleanup_interval(),
42908f9eb17SAlex Crichton self.cache_config
43008f9eb17SAlex Crichton .allowed_clock_drift_for_files_from_future(),
43108f9eb17SAlex Crichton )
43208f9eb17SAlex Crichton .is_none()
43308f9eb17SAlex Crichton {
43408f9eb17SAlex Crichton return;
43508f9eb17SAlex Crichton }
43608f9eb17SAlex Crichton
43708f9eb17SAlex Crichton trace!("Trying to clean up cache");
43808f9eb17SAlex Crichton
43908f9eb17SAlex Crichton let mut cache_index = self.list_cache_contents();
44008f9eb17SAlex Crichton let future_tolerance = SystemTime::now()
44108f9eb17SAlex Crichton .checked_add(
44208f9eb17SAlex Crichton self.cache_config
44308f9eb17SAlex Crichton .allowed_clock_drift_for_files_from_future(),
44408f9eb17SAlex Crichton )
44508f9eb17SAlex Crichton .expect("Brace your cache, the next Big Bang is coming (time overflow)");
44608f9eb17SAlex Crichton cache_index.sort_unstable_by(|lhs, rhs| {
44708f9eb17SAlex Crichton // sort by age
44808f9eb17SAlex Crichton use CacheEntry::*;
44908f9eb17SAlex Crichton match (lhs, rhs) {
45008f9eb17SAlex Crichton (Recognized { mtime: lhs_mt, .. }, Recognized { mtime: rhs_mt, .. }) => {
45108f9eb17SAlex Crichton match (*lhs_mt > future_tolerance, *rhs_mt > future_tolerance) {
45208f9eb17SAlex Crichton // later == younger
45308f9eb17SAlex Crichton (false, false) => rhs_mt.cmp(lhs_mt),
45408f9eb17SAlex Crichton // files from far future are treated as oldest recognized files
45508f9eb17SAlex Crichton // we want to delete them, so the cache keeps track of recent files
45608f9eb17SAlex Crichton // however, we don't delete them uncodintionally,
45708f9eb17SAlex Crichton // because .stats file can be overwritten with a meaningful mtime
45808f9eb17SAlex Crichton (true, false) => cmp::Ordering::Greater,
45908f9eb17SAlex Crichton (false, true) => cmp::Ordering::Less,
46008f9eb17SAlex Crichton (true, true) => cmp::Ordering::Equal,
46108f9eb17SAlex Crichton }
46208f9eb17SAlex Crichton }
46308f9eb17SAlex Crichton // unrecognized is kind of infinity
46408f9eb17SAlex Crichton (Recognized { .. }, Unrecognized { .. }) => cmp::Ordering::Less,
46508f9eb17SAlex Crichton (Unrecognized { .. }, Recognized { .. }) => cmp::Ordering::Greater,
46608f9eb17SAlex Crichton (Unrecognized { .. }, Unrecognized { .. }) => cmp::Ordering::Equal,
46708f9eb17SAlex Crichton }
46808f9eb17SAlex Crichton });
46908f9eb17SAlex Crichton
47008f9eb17SAlex Crichton // find "cut" boundary:
47108f9eb17SAlex Crichton // - remove unrecognized files anyway,
47208f9eb17SAlex Crichton // - remove some cache files if some quota has been exceeded
47308f9eb17SAlex Crichton let mut total_size = 0u64;
47408f9eb17SAlex Crichton let mut start_delete_idx = None;
47508f9eb17SAlex Crichton let mut start_delete_idx_if_deleting_recognized_items: Option<usize> = None;
47608f9eb17SAlex Crichton
47708f9eb17SAlex Crichton let total_size_limit = self.cache_config.files_total_size_soft_limit();
47808f9eb17SAlex Crichton let file_count_limit = self.cache_config.file_count_soft_limit();
47908f9eb17SAlex Crichton let tsl_if_deleting = total_size_limit
48008f9eb17SAlex Crichton .checked_mul(
48108f9eb17SAlex Crichton self.cache_config
48208f9eb17SAlex Crichton .files_total_size_limit_percent_if_deleting() as u64,
48308f9eb17SAlex Crichton )
48408f9eb17SAlex Crichton .unwrap()
48508f9eb17SAlex Crichton / 100;
48608f9eb17SAlex Crichton let fcl_if_deleting = file_count_limit
48708f9eb17SAlex Crichton .checked_mul(self.cache_config.file_count_limit_percent_if_deleting() as u64)
48808f9eb17SAlex Crichton .unwrap()
48908f9eb17SAlex Crichton / 100;
49008f9eb17SAlex Crichton
49108f9eb17SAlex Crichton for (idx, item) in cache_index.iter().enumerate() {
49208f9eb17SAlex Crichton let size = if let CacheEntry::Recognized { size, .. } = item {
49308f9eb17SAlex Crichton size
49408f9eb17SAlex Crichton } else {
49508f9eb17SAlex Crichton start_delete_idx = Some(idx);
49608f9eb17SAlex Crichton break;
49708f9eb17SAlex Crichton };
49808f9eb17SAlex Crichton
49908f9eb17SAlex Crichton total_size += size;
50008f9eb17SAlex Crichton if start_delete_idx_if_deleting_recognized_items.is_none()
50108f9eb17SAlex Crichton && (total_size > tsl_if_deleting || (idx + 1) as u64 > fcl_if_deleting)
50208f9eb17SAlex Crichton {
50308f9eb17SAlex Crichton start_delete_idx_if_deleting_recognized_items = Some(idx);
50408f9eb17SAlex Crichton }
50508f9eb17SAlex Crichton
50608f9eb17SAlex Crichton if total_size > total_size_limit || (idx + 1) as u64 > file_count_limit {
50708f9eb17SAlex Crichton start_delete_idx = start_delete_idx_if_deleting_recognized_items;
50808f9eb17SAlex Crichton break;
50908f9eb17SAlex Crichton }
51008f9eb17SAlex Crichton }
51108f9eb17SAlex Crichton
51208f9eb17SAlex Crichton if let Some(idx) = start_delete_idx {
51308f9eb17SAlex Crichton for item in &cache_index[idx..] {
51408f9eb17SAlex Crichton let (result, path, entity) = match item {
51508f9eb17SAlex Crichton CacheEntry::Recognized { path, .. }
51608f9eb17SAlex Crichton | CacheEntry::Unrecognized {
51708f9eb17SAlex Crichton path,
51808f9eb17SAlex Crichton is_dir: false,
51908f9eb17SAlex Crichton } => (fs::remove_file(path), path, "file"),
52008f9eb17SAlex Crichton CacheEntry::Unrecognized { path, is_dir: true } => {
52108f9eb17SAlex Crichton (fs::remove_dir_all(path), path, "directory")
52208f9eb17SAlex Crichton }
52308f9eb17SAlex Crichton };
52408f9eb17SAlex Crichton if let Err(err) = result {
52508f9eb17SAlex Crichton warn!(
52608f9eb17SAlex Crichton "Failed to remove {} during cleanup, path: {}, err: {}",
52708f9eb17SAlex Crichton entity,
52808f9eb17SAlex Crichton path.display(),
52908f9eb17SAlex Crichton err
53008f9eb17SAlex Crichton );
53108f9eb17SAlex Crichton }
53208f9eb17SAlex Crichton }
53308f9eb17SAlex Crichton }
53408f9eb17SAlex Crichton
53508f9eb17SAlex Crichton trace!("Task finished: clean up cache");
53608f9eb17SAlex Crichton }
53708f9eb17SAlex Crichton
53808f9eb17SAlex Crichton // Be fault tolerant: list as much as you can, and ignore the rest
list_cache_contents(&self) -> Vec<CacheEntry>53908f9eb17SAlex Crichton fn list_cache_contents(&self) -> Vec<CacheEntry> {
54008f9eb17SAlex Crichton fn enter_dir(
54108f9eb17SAlex Crichton vec: &mut Vec<CacheEntry>,
54208f9eb17SAlex Crichton dir_path: &Path,
54308f9eb17SAlex Crichton level: u8,
54408f9eb17SAlex Crichton cache_config: &CacheConfig,
54508f9eb17SAlex Crichton ) {
54608f9eb17SAlex Crichton macro_rules! add_unrecognized {
54708f9eb17SAlex Crichton (file: $path:expr) => {
54808f9eb17SAlex Crichton add_unrecognized!(false, $path)
54908f9eb17SAlex Crichton };
55008f9eb17SAlex Crichton (dir: $path:expr) => {
55108f9eb17SAlex Crichton add_unrecognized!(true, $path)
55208f9eb17SAlex Crichton };
55308f9eb17SAlex Crichton ($is_dir:expr, $path:expr) => {
55408f9eb17SAlex Crichton vec.push(CacheEntry::Unrecognized {
55508f9eb17SAlex Crichton path: $path.to_path_buf(),
55608f9eb17SAlex Crichton is_dir: $is_dir,
5579e142f87SAlex Crichton })
55808f9eb17SAlex Crichton };
55908f9eb17SAlex Crichton }
56008f9eb17SAlex Crichton macro_rules! add_unrecognized_and {
56108f9eb17SAlex Crichton ([ $( $ty:ident: $path:expr ),* ], $cont:stmt) => {{
56208f9eb17SAlex Crichton $( add_unrecognized!($ty: $path); )*
56308f9eb17SAlex Crichton $cont
56408f9eb17SAlex Crichton }};
56508f9eb17SAlex Crichton }
56608f9eb17SAlex Crichton
56708f9eb17SAlex Crichton macro_rules! unwrap_or {
56808f9eb17SAlex Crichton ($result:expr, $cont:stmt, $err_msg:expr) => {
56908f9eb17SAlex Crichton unwrap_or!($result, $cont, $err_msg, dir_path)
57008f9eb17SAlex Crichton };
57108f9eb17SAlex Crichton ($result:expr, $cont:stmt, $err_msg:expr, $path:expr) => {
57208f9eb17SAlex Crichton unwrap_or_warn!(
57308f9eb17SAlex Crichton $result,
57408f9eb17SAlex Crichton $cont,
57508f9eb17SAlex Crichton format!("{}, level: {}", $err_msg, level),
57608f9eb17SAlex Crichton $path
57708f9eb17SAlex Crichton )
57808f9eb17SAlex Crichton };
57908f9eb17SAlex Crichton }
58008f9eb17SAlex Crichton
58108f9eb17SAlex Crichton // If we fail to list a directory, something bad is happening anyway
58208f9eb17SAlex Crichton // (something touches our cache or we have disk failure)
58308f9eb17SAlex Crichton // Try to delete it, so we can stay within soft limits of the cache size.
58408f9eb17SAlex Crichton // This comment applies later in this function, too.
58508f9eb17SAlex Crichton let it = unwrap_or!(
58608f9eb17SAlex Crichton fs::read_dir(dir_path),
58708f9eb17SAlex Crichton add_unrecognized_and!([dir: dir_path], return),
58808f9eb17SAlex Crichton "Failed to list cache directory, deleting it"
58908f9eb17SAlex Crichton );
59008f9eb17SAlex Crichton
59108f9eb17SAlex Crichton let mut cache_files = HashMap::new();
59208f9eb17SAlex Crichton for entry in it {
59308f9eb17SAlex Crichton // read_dir() returns an iterator over results - in case some of them are errors
59408f9eb17SAlex Crichton // we don't know their names, so we can't delete them. We don't want to delete
59508f9eb17SAlex Crichton // the whole directory with good entries too, so we just ignore the erroneous entries.
59608f9eb17SAlex Crichton let entry = unwrap_or!(
59708f9eb17SAlex Crichton entry,
59808f9eb17SAlex Crichton continue,
59908f9eb17SAlex Crichton "Failed to read a cache dir entry (NOT deleting it, it still occupies space)"
60008f9eb17SAlex Crichton );
60108f9eb17SAlex Crichton let path = entry.path();
60208f9eb17SAlex Crichton match (level, path.is_dir()) {
60308f9eb17SAlex Crichton (0..=1, true) => enter_dir(vec, &path, level + 1, cache_config),
60408f9eb17SAlex Crichton (0..=1, false) => {
60508f9eb17SAlex Crichton if level == 0
60608f9eb17SAlex Crichton && path.file_stem() == Some(OsStr::new(".cleanup"))
60708f9eb17SAlex Crichton && path.extension().is_some()
60808f9eb17SAlex Crichton // assume it's cleanup lock
60908f9eb17SAlex Crichton && !is_fs_lock_expired(
61008f9eb17SAlex Crichton Some(&entry),
61108f9eb17SAlex Crichton &path,
61208f9eb17SAlex Crichton cache_config.cleanup_interval(),
61308f9eb17SAlex Crichton cache_config.allowed_clock_drift_for_files_from_future(),
61408f9eb17SAlex Crichton )
61508f9eb17SAlex Crichton {
61608f9eb17SAlex Crichton continue; // skip active lock
61708f9eb17SAlex Crichton }
61808f9eb17SAlex Crichton add_unrecognized!(file: path);
61908f9eb17SAlex Crichton }
62008f9eb17SAlex Crichton (2, false) => {
62108f9eb17SAlex Crichton match path.extension().and_then(OsStr::to_str) {
62208f9eb17SAlex Crichton // mod or stats file
62308f9eb17SAlex Crichton None | Some("stats") => {
62408f9eb17SAlex Crichton cache_files.insert(path, entry);
62508f9eb17SAlex Crichton }
62608f9eb17SAlex Crichton
62708f9eb17SAlex Crichton Some(ext) => {
62808f9eb17SAlex Crichton // check if valid lock
62908f9eb17SAlex Crichton let recognized = ext.starts_with("wip-")
63008f9eb17SAlex Crichton && !is_fs_lock_expired(
63108f9eb17SAlex Crichton Some(&entry),
63208f9eb17SAlex Crichton &path,
63308f9eb17SAlex Crichton cache_config.optimizing_compression_task_timeout(),
63408f9eb17SAlex Crichton cache_config.allowed_clock_drift_for_files_from_future(),
63508f9eb17SAlex Crichton );
63608f9eb17SAlex Crichton
63708f9eb17SAlex Crichton if !recognized {
63808f9eb17SAlex Crichton add_unrecognized!(file: path);
63908f9eb17SAlex Crichton }
64008f9eb17SAlex Crichton }
64108f9eb17SAlex Crichton }
64208f9eb17SAlex Crichton }
64308f9eb17SAlex Crichton (_, is_dir) => add_unrecognized!(is_dir, path),
64408f9eb17SAlex Crichton }
64508f9eb17SAlex Crichton }
64608f9eb17SAlex Crichton
64708f9eb17SAlex Crichton // associate module with its stats & handle them
64808f9eb17SAlex Crichton // assumption: just mods and stats
64908f9eb17SAlex Crichton for (path, entry) in cache_files.iter() {
65008f9eb17SAlex Crichton let path_buf: PathBuf;
65108f9eb17SAlex Crichton let (mod_, stats_, is_mod) = match path.extension() {
65208f9eb17SAlex Crichton Some(_) => {
65308f9eb17SAlex Crichton path_buf = path.with_extension("");
65408f9eb17SAlex Crichton (
65508f9eb17SAlex Crichton cache_files.get(&path_buf).map(|v| (&path_buf, v)),
65608f9eb17SAlex Crichton Some((path, entry)),
65708f9eb17SAlex Crichton false,
65808f9eb17SAlex Crichton )
65908f9eb17SAlex Crichton }
66008f9eb17SAlex Crichton None => {
66108f9eb17SAlex Crichton path_buf = path.with_extension("stats");
66208f9eb17SAlex Crichton (
66308f9eb17SAlex Crichton Some((path, entry)),
66408f9eb17SAlex Crichton cache_files.get(&path_buf).map(|v| (&path_buf, v)),
66508f9eb17SAlex Crichton true,
66608f9eb17SAlex Crichton )
66708f9eb17SAlex Crichton }
66808f9eb17SAlex Crichton };
66908f9eb17SAlex Crichton
67008f9eb17SAlex Crichton // construct a cache entry
67108f9eb17SAlex Crichton match (mod_, stats_, is_mod) {
67208f9eb17SAlex Crichton (Some((mod_path, mod_entry)), Some((stats_path, stats_entry)), true) => {
67308f9eb17SAlex Crichton let mod_metadata = unwrap_or!(
67408f9eb17SAlex Crichton mod_entry.metadata(),
67508f9eb17SAlex Crichton add_unrecognized_and!([file: stats_path, file: mod_path], continue),
67608f9eb17SAlex Crichton "Failed to get metadata, deleting BOTH module cache and stats files",
67708f9eb17SAlex Crichton mod_path
67808f9eb17SAlex Crichton );
67908f9eb17SAlex Crichton let stats_mtime = unwrap_or!(
68008f9eb17SAlex Crichton stats_entry.metadata().and_then(|m| m.modified()),
68108f9eb17SAlex Crichton add_unrecognized_and!(
68208f9eb17SAlex Crichton [file: stats_path],
68308f9eb17SAlex Crichton unwrap_or!(
68408f9eb17SAlex Crichton mod_metadata.modified(),
68508f9eb17SAlex Crichton add_unrecognized_and!(
68608f9eb17SAlex Crichton [file: stats_path, file: mod_path],
68708f9eb17SAlex Crichton continue
68808f9eb17SAlex Crichton ),
68908f9eb17SAlex Crichton "Failed to get mtime, deleting BOTH module cache and stats \
69008f9eb17SAlex Crichton files",
69108f9eb17SAlex Crichton mod_path
69208f9eb17SAlex Crichton )
69308f9eb17SAlex Crichton ),
69408f9eb17SAlex Crichton "Failed to get metadata/mtime, deleting the file",
69508f9eb17SAlex Crichton stats_path
69608f9eb17SAlex Crichton );
69708f9eb17SAlex Crichton // .into() called for the SystemTimeStub if cfg(test)
69808f9eb17SAlex Crichton vec.push(CacheEntry::Recognized {
69908f9eb17SAlex Crichton path: mod_path.to_path_buf(),
70008f9eb17SAlex Crichton mtime: stats_mtime.into(),
70108f9eb17SAlex Crichton size: mod_metadata.len(),
70208f9eb17SAlex Crichton })
70308f9eb17SAlex Crichton }
70408f9eb17SAlex Crichton (Some(_), Some(_), false) => (), // was or will be handled by previous branch
70508f9eb17SAlex Crichton (Some((mod_path, mod_entry)), None, _) => {
70608f9eb17SAlex Crichton let (mod_metadata, mod_mtime) = unwrap_or!(
70708f9eb17SAlex Crichton mod_entry
70808f9eb17SAlex Crichton .metadata()
70908f9eb17SAlex Crichton .and_then(|md| md.modified().map(|mt| (md, mt))),
71008f9eb17SAlex Crichton add_unrecognized_and!([file: mod_path], continue),
71108f9eb17SAlex Crichton "Failed to get metadata/mtime, deleting the file",
71208f9eb17SAlex Crichton mod_path
71308f9eb17SAlex Crichton );
71408f9eb17SAlex Crichton // .into() called for the SystemTimeStub if cfg(test)
71508f9eb17SAlex Crichton vec.push(CacheEntry::Recognized {
71608f9eb17SAlex Crichton path: mod_path.to_path_buf(),
71708f9eb17SAlex Crichton mtime: mod_mtime.into(),
71808f9eb17SAlex Crichton size: mod_metadata.len(),
71908f9eb17SAlex Crichton })
72008f9eb17SAlex Crichton }
72108f9eb17SAlex Crichton (None, Some((stats_path, _stats_entry)), _) => {
72208f9eb17SAlex Crichton debug!("Found orphaned stats file: {}", stats_path.display());
72308f9eb17SAlex Crichton add_unrecognized!(file: stats_path);
72408f9eb17SAlex Crichton }
72508f9eb17SAlex Crichton _ => unreachable!(),
72608f9eb17SAlex Crichton }
72708f9eb17SAlex Crichton }
72808f9eb17SAlex Crichton }
72908f9eb17SAlex Crichton
73008f9eb17SAlex Crichton let mut vec = Vec::new();
731*61a371acSJesse Rusak enter_dir(&mut vec, self.directory(), 0, &self.cache_config);
73208f9eb17SAlex Crichton vec
73308f9eb17SAlex Crichton }
73408f9eb17SAlex Crichton }
73508f9eb17SAlex Crichton
read_stats_file(path: &Path) -> Option<ModuleCacheStatistics>73608f9eb17SAlex Crichton fn read_stats_file(path: &Path) -> Option<ModuleCacheStatistics> {
73708c7359fSAlex Crichton fs::read_to_string(path)
73808f9eb17SAlex Crichton .map_err(|err| {
73908f9eb17SAlex Crichton trace!(
74008f9eb17SAlex Crichton "Failed to read stats file, path: {}, err: {}",
74108f9eb17SAlex Crichton path.display(),
74208f9eb17SAlex Crichton err
74308f9eb17SAlex Crichton )
74408f9eb17SAlex Crichton })
74508c7359fSAlex Crichton .and_then(|contents| {
74608c7359fSAlex Crichton toml::from_str::<ModuleCacheStatistics>(&contents).map_err(|err| {
74708f9eb17SAlex Crichton trace!(
74808f9eb17SAlex Crichton "Failed to parse stats file, path: {}, err: {}",
74908f9eb17SAlex Crichton path.display(),
75008f9eb17SAlex Crichton err,
75108f9eb17SAlex Crichton )
75208f9eb17SAlex Crichton })
75308f9eb17SAlex Crichton })
75408f9eb17SAlex Crichton .ok()
75508f9eb17SAlex Crichton }
75608f9eb17SAlex Crichton
write_stats_file(path: &Path, stats: &ModuleCacheStatistics) -> bool75708f9eb17SAlex Crichton fn write_stats_file(path: &Path, stats: &ModuleCacheStatistics) -> bool {
75808f9eb17SAlex Crichton toml::to_string_pretty(&stats)
75908f9eb17SAlex Crichton .map_err(|err| {
76008f9eb17SAlex Crichton warn!(
76108f9eb17SAlex Crichton "Failed to serialize stats file, path: {}, err: {}",
76208f9eb17SAlex Crichton path.display(),
76308f9eb17SAlex Crichton err
76408f9eb17SAlex Crichton )
76508f9eb17SAlex Crichton })
76608f9eb17SAlex Crichton .and_then(|serialized| {
7671ced2ef4SXinzhao Xu fs_write_atomic(path, "stats", serialized.as_bytes()).map_err(|_| ())
76808f9eb17SAlex Crichton })
76908f9eb17SAlex Crichton .is_ok()
77008f9eb17SAlex Crichton }
77108f9eb17SAlex Crichton
77208f9eb17SAlex Crichton /// Tries to acquire a lock for specific task.
77308f9eb17SAlex Crichton ///
77408f9eb17SAlex Crichton /// Returns Some(path) to the lock if succeeds. The task path must not
77508f9eb17SAlex Crichton /// contain any extension and have file stem.
77608f9eb17SAlex Crichton ///
77708f9eb17SAlex Crichton /// To release a lock you need either manually rename or remove it,
77808f9eb17SAlex Crichton /// or wait until it expires and cleanup task removes it.
77908f9eb17SAlex Crichton ///
78008f9eb17SAlex Crichton /// Note: this function is racy. Main idea is: be fault tolerant and
78108f9eb17SAlex Crichton /// never block some task. The price is that we rarely do some task
78208f9eb17SAlex Crichton /// more than once.
acquire_task_fs_lock( task_path: &Path, timeout: Duration, allowed_future_drift: Duration, ) -> Option<PathBuf>78308f9eb17SAlex Crichton fn acquire_task_fs_lock(
78408f9eb17SAlex Crichton task_path: &Path,
78508f9eb17SAlex Crichton timeout: Duration,
78608f9eb17SAlex Crichton allowed_future_drift: Duration,
78708f9eb17SAlex Crichton ) -> Option<PathBuf> {
78808f9eb17SAlex Crichton assert!(task_path.extension().is_none());
78908f9eb17SAlex Crichton assert!(task_path.file_stem().is_some());
79008f9eb17SAlex Crichton
79108f9eb17SAlex Crichton // list directory
79208f9eb17SAlex Crichton let dir_path = task_path.parent()?;
79308f9eb17SAlex Crichton let it = fs::read_dir(dir_path)
79408f9eb17SAlex Crichton .map_err(|err| {
79508f9eb17SAlex Crichton warn!(
79608f9eb17SAlex Crichton "Failed to list cache directory, path: {}, err: {}",
79708f9eb17SAlex Crichton dir_path.display(),
79808f9eb17SAlex Crichton err
79908f9eb17SAlex Crichton )
80008f9eb17SAlex Crichton })
80108f9eb17SAlex Crichton .ok()?;
80208f9eb17SAlex Crichton
80308f9eb17SAlex Crichton // look for existing locks
80408f9eb17SAlex Crichton for entry in it {
80508f9eb17SAlex Crichton let entry = entry
80608f9eb17SAlex Crichton .map_err(|err| {
80708f9eb17SAlex Crichton warn!(
80808f9eb17SAlex Crichton "Failed to list cache directory, path: {}, err: {}",
80908f9eb17SAlex Crichton dir_path.display(),
81008f9eb17SAlex Crichton err
81108f9eb17SAlex Crichton )
81208f9eb17SAlex Crichton })
81308f9eb17SAlex Crichton .ok()?;
81408f9eb17SAlex Crichton
81508f9eb17SAlex Crichton let path = entry.path();
81608f9eb17SAlex Crichton if path.is_dir() || path.file_stem() != task_path.file_stem() {
81708f9eb17SAlex Crichton continue;
81808f9eb17SAlex Crichton }
81908f9eb17SAlex Crichton
82008f9eb17SAlex Crichton // check extension and mtime
82108f9eb17SAlex Crichton match path.extension() {
82208f9eb17SAlex Crichton None => continue,
82308f9eb17SAlex Crichton Some(ext) => {
82408f9eb17SAlex Crichton if let Some(ext_str) = ext.to_str() {
82508f9eb17SAlex Crichton // if it's None, i.e. not valid UTF-8 string, then that's not our lock for sure
82608f9eb17SAlex Crichton if ext_str.starts_with("wip-")
82708f9eb17SAlex Crichton && !is_fs_lock_expired(Some(&entry), &path, timeout, allowed_future_drift)
82808f9eb17SAlex Crichton {
82908f9eb17SAlex Crichton return None;
83008f9eb17SAlex Crichton }
83108f9eb17SAlex Crichton }
83208f9eb17SAlex Crichton }
83308f9eb17SAlex Crichton }
83408f9eb17SAlex Crichton }
83508f9eb17SAlex Crichton
83608f9eb17SAlex Crichton // create the lock
83708f9eb17SAlex Crichton let lock_path = task_path.with_extension(format!("wip-{}", std::process::id()));
83808f9eb17SAlex Crichton let _file = fs::OpenOptions::new()
83908f9eb17SAlex Crichton .create_new(true)
84008f9eb17SAlex Crichton .write(true)
84108f9eb17SAlex Crichton .open(&lock_path)
84208f9eb17SAlex Crichton .map_err(|err| {
84308f9eb17SAlex Crichton warn!(
84408f9eb17SAlex Crichton "Failed to create lock file (note: it shouldn't exists): path: {}, err: {}",
84508f9eb17SAlex Crichton lock_path.display(),
84608f9eb17SAlex Crichton err
84708f9eb17SAlex Crichton )
84808f9eb17SAlex Crichton })
84908f9eb17SAlex Crichton .ok()?;
85008f9eb17SAlex Crichton
85108f9eb17SAlex Crichton Some(lock_path)
85208f9eb17SAlex Crichton }
85308f9eb17SAlex Crichton
85408f9eb17SAlex Crichton // we have either both, or just path; dir entry is desirable since on some platforms we can get
85508f9eb17SAlex Crichton // metadata without extra syscalls
8560e9121daSFrankReh // furthermore: it's better to get a path if we have it instead of allocating a new one from the dir entry
is_fs_lock_expired( entry: Option<&fs::DirEntry>, path: &PathBuf, threshold: Duration, allowed_future_drift: Duration, ) -> bool85708f9eb17SAlex Crichton fn is_fs_lock_expired(
85808f9eb17SAlex Crichton entry: Option<&fs::DirEntry>,
85908f9eb17SAlex Crichton path: &PathBuf,
86008f9eb17SAlex Crichton threshold: Duration,
86108f9eb17SAlex Crichton allowed_future_drift: Duration,
86208f9eb17SAlex Crichton ) -> bool {
86308f9eb17SAlex Crichton let mtime = match entry
86408f9eb17SAlex Crichton .map_or_else(|| path.metadata(), |e| e.metadata())
86508f9eb17SAlex Crichton .and_then(|metadata| metadata.modified())
86608f9eb17SAlex Crichton {
86708f9eb17SAlex Crichton Ok(mt) => mt,
86808f9eb17SAlex Crichton Err(err) => {
86908f9eb17SAlex Crichton warn!(
87008f9eb17SAlex Crichton "Failed to get metadata/mtime, treating as an expired lock, path: {}, err: {}",
87108f9eb17SAlex Crichton path.display(),
87208f9eb17SAlex Crichton err
87308f9eb17SAlex Crichton );
87408f9eb17SAlex Crichton return true; // can't read mtime, treat as expired, so this task will not be starved
87508f9eb17SAlex Crichton }
87608f9eb17SAlex Crichton };
87708f9eb17SAlex Crichton
87808f9eb17SAlex Crichton // DON'T use: mtime.elapsed() -- we must call SystemTime directly for the tests to be deterministic
87908f9eb17SAlex Crichton match SystemTime::now().duration_since(mtime) {
88008f9eb17SAlex Crichton Ok(elapsed) => elapsed >= threshold,
88108f9eb17SAlex Crichton Err(err) => {
88208f9eb17SAlex Crichton trace!(
88308f9eb17SAlex Crichton "Found mtime in the future, treating as a not expired lock, path: {}, err: {}",
88408f9eb17SAlex Crichton path.display(),
88508f9eb17SAlex Crichton err
88608f9eb17SAlex Crichton );
88708f9eb17SAlex Crichton // the lock is expired if the time is too far in the future
88808f9eb17SAlex Crichton // it is fine to have network share and not synchronized clocks,
88908f9eb17SAlex Crichton // but it's not good when user changes time in their system clock
89008f9eb17SAlex Crichton err.duration() > allowed_future_drift
89108f9eb17SAlex Crichton }
89208f9eb17SAlex Crichton }
89308f9eb17SAlex Crichton }
89408f9eb17SAlex Crichton
89508f9eb17SAlex Crichton #[cfg(test)]
89608f9eb17SAlex Crichton mod tests;
897