Skip to content

Commit 3989865

Browse files
committed
use lock files to avoid concurrent cache collisions
1 parent 3af1812 commit 3989865

3 files changed

Lines changed: 40 additions & 5 deletions

File tree

clang-installer/src/downloader/pypi.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{DownloadError, caching::Cacher, download, hashing::HashAlgorithm};
2-
use crate::{ClangTool, progress_bar::ProgressBar};
2+
use crate::{ClangTool, progress_bar::ProgressBar, utils::lock_path};
33

44
use semver::{Version, VersionReq};
55
use serde::{Deserialize, de::Visitor};
@@ -452,7 +452,7 @@ impl PyPiDownloader {
452452
let cache_file = Self::get_cache_dir()
453453
.join("pypi")
454454
.join(format!("{clang_tool}_pypi.json"));
455-
455+
let file_lock = lock_path(&cache_file)?;
456456
// PyPI package info cache should not be refreshed unless it is more than 10 minutes old.
457457
// This is behavior recommended by PyPI response header `Cache-Control: max-age=600`.
458458
// Instead of caching the `Cache-Control` header, we'll just check the cached file's "last modified" time.
@@ -462,14 +462,15 @@ impl PyPiDownloader {
462462
"Using cached PyPI info for {clang_tool} from {}",
463463
cache_file.to_string_lossy()
464464
);
465-
std::fs::read_to_string(cache_file)?
465+
std::fs::read_to_string(&cache_file)?
466466
} else {
467467
let api_url = format!("{PYPI_JSON_API_URL}/pypi/{clang_tool}/");
468468
let endpoint = Url::parse(&api_url)?.join("json")?;
469469
log::info!("Fetching PyPI info for {clang_tool} from {endpoint}");
470470
download(&endpoint, &cache_file, 10).await?;
471-
std::fs::read_to_string(cache_file)?
471+
std::fs::read_to_string(&cache_file)?
472472
};
473+
file_lock.unlock()?;
473474
Ok(serde_json::from_str(body.as_str())?)
474475
}
475476

@@ -487,6 +488,7 @@ impl PyPiDownloader {
487488
let cached_filename = format!("{clang_tool}_{ver}.whl");
488489
let cached_dir = Self::get_cache_dir();
489490
let cached_wheel = cached_dir.join("pypi").join(&cached_filename);
491+
let file_lock = lock_path(&cached_wheel)?;
490492
if Self::is_cache_valid(&cached_wheel, None) {
491493
log::info!(
492494
"Using cached wheel for {clang_tool} version {ver} from {}",
@@ -510,6 +512,7 @@ impl PyPiDownloader {
510512
Some(dir) => dir.join(&bin_name),
511513
};
512514
Self::extract_bin(clang_tool, &cached_wheel, &extracted_bin)?;
515+
file_lock.unlock()?;
513516
Ok(extracted_bin)
514517
}
515518

clang-installer/src/downloader/static_dist.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use url::Url;
1313
use crate::{
1414
Cacher, ClangTool, DownloadError,
1515
downloader::{download, hashing::HashAlgorithm},
16+
utils::lock_path,
1617
};
1718

1819
/// An error that can occur while downloading a static binary.
@@ -155,6 +156,7 @@ impl StaticDistDownloader {
155156
None => cache_path.join("bin").join(&bin_name),
156157
Some(dir) => dir.join(&bin_name),
157158
};
159+
let file_lock = lock_path(&download_path)?;
158160
if download_path.exists() {
159161
log::info!(
160162
"Using cached static binary for {tool} version {ver_str} from {:?}",
@@ -182,6 +184,7 @@ impl StaticDistDownloader {
182184
download(&sha512_url, &sha512_cache_path, 10).await?;
183185
}
184186
Self::verify_sha512(&download_path, &sha512_cache_path)?;
187+
file_lock.unlock()?;
185188
Ok(download_path)
186189
}
187190
}

clang-installer/src/utils.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
//! A utility module for path normalization.
2-
use std::path::{Component, Path, PathBuf};
2+
use std::{
3+
fs::{self, File, OpenOptions},
4+
path::{Component, Path, PathBuf},
5+
};
36

47
/// This was copied from [cargo source code](https://github.com/rust-lang/cargo/blob/8cc0cb136772b8f54eafe0d163fcb7226a06af0c/crates/cargo-util/src/paths.rs#L84).
58
///
@@ -31,6 +34,32 @@ pub fn normalize_path(path: &Path) -> PathBuf {
3134
ret
3235
}
3336

37+
/// Creates (and returns) a lock [`File`] for the given `path` and locks it.
38+
///
39+
/// This function will create a lock file with `.lock` appended to the
40+
/// given `path`'s extension. It will then acquire an exclusive lock on
41+
/// the file to prevent concurrent access.
42+
///
43+
/// Note, This will block until a lock is obtained.
44+
///
45+
/// The caller is responsible for cleanup, which includes
46+
///
47+
/// 1. unlocking the returned file and
48+
/// 2. deleting the file when done (if desired).
49+
pub fn lock_path(path: &Path) -> Result<File, std::io::Error> {
50+
let lock_path = path.with_added_extension("lock");
51+
if let Some(parent) = lock_path.parent() {
52+
fs::create_dir_all(parent)?;
53+
}
54+
let file_lock = OpenOptions::new()
55+
.write(true)
56+
.create(true)
57+
.truncate(true)
58+
.open(&lock_path)?;
59+
file_lock.lock()?;
60+
Ok(file_lock)
61+
}
62+
3463
#[cfg(test)]
3564
mod tests {
3665
use std::{env::current_dir, path::PathBuf};

0 commit comments

Comments
 (0)