-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix(lockfile): add atomic writes and cache invalidation #7927
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ use crate::toolset::{ToolSource, ToolVersion, ToolVersionList, Toolset}; | |||||||||||||
| use eyre::{Report, Result, bail}; | ||||||||||||||
| use itertools::Itertools; | ||||||||||||||
| use serde_derive::{Deserialize, Serialize}; | ||||||||||||||
| use std::fs; | ||||||||||||||
| use std::path::{Path, PathBuf}; | ||||||||||||||
| use std::sync::LazyLock as Lazy; | ||||||||||||||
| use std::sync::Mutex; | ||||||||||||||
|
|
@@ -17,6 +18,22 @@ use std::{ | |||||||||||||
| use toml_edit::DocumentMut; | ||||||||||||||
| use xx::regex; | ||||||||||||||
|
|
||||||||||||||
| /// Global caches for lockfile data - declared here so invalidation can access them | ||||||||||||||
| static ALL_LOCKFILES_CACHE: Lazy<Mutex<HashMap<Vec<PathBuf>, Arc<Lockfile>>>> = | ||||||||||||||
| Lazy::new(Default::default); | ||||||||||||||
| static SINGLE_LOCKFILE_CACHE: Lazy<Mutex<HashMap<PathBuf, Arc<Lockfile>>>> = | ||||||||||||||
| Lazy::new(Default::default); | ||||||||||||||
|
|
||||||||||||||
| /// Invalidate all lockfile caches. Call this after modifying a lockfile. | ||||||||||||||
| pub fn invalidate_caches() { | ||||||||||||||
| if let Ok(mut cache) = ALL_LOCKFILES_CACHE.lock() { | ||||||||||||||
| cache.clear(); | ||||||||||||||
| } | ||||||||||||||
| if let Ok(mut cache) = SINGLE_LOCKFILE_CACHE.lock() { | ||||||||||||||
| cache.clear(); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| #[derive(Debug, Clone, Default, Serialize, Deserialize)] | ||||||||||||||
| #[serde(deny_unknown_fields)] | ||||||||||||||
| pub struct Lockfile { | ||||||||||||||
|
|
@@ -213,8 +230,10 @@ impl Lockfile { | |||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> { | ||||||||||||||
| let path = path.as_ref(); | ||||||||||||||
| if self.is_empty() { | ||||||||||||||
| let _ = file::remove_file(path); | ||||||||||||||
| invalidate_caches(); | ||||||||||||||
| } else { | ||||||||||||||
| let mut lockfile = toml::Table::new(); | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -252,7 +271,14 @@ impl Lockfile { | |||||||||||||
|
|
||||||||||||||
| let content = toml::to_string_pretty(&toml::Value::Table(lockfile))?; | ||||||||||||||
| let content = format(content.parse()?); | ||||||||||||||
| file::write(path, content)?; | ||||||||||||||
|
|
||||||||||||||
| // Use atomic write: write to temp file, then rename | ||||||||||||||
| // This prevents partial writes from corrupting the lockfile | ||||||||||||||
| let temp_path = path.with_extension("lock.tmp"); | ||||||||||||||
|
||||||||||||||
| let temp_path = path.with_extension("lock.tmp"); | |
| let temp_path = match path.file_name().and_then(|name| name.to_str()) { | |
| Some(name) => path.with_file_name(format!("{name}.tmp")), | |
| None => path.with_extension("lock.tmp"), | |
| }; |
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If fs::rename fails after successfully writing the temp file, the temporary file will be left behind without cleanup. Consider wrapping this operation in a guard or adding error handling to remove the temp file on failure.
| fs::rename(&temp_path, path)?; | |
| if let Err(err) = fs::rename(&temp_path, path) { | |
| // Best-effort cleanup of temporary file if rename fails | |
| let _ = fs::remove_file(&temp_path); | |
| return Err(err.into()); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation of
invalidate_cachessilently ignores poisoned mutexes. If a thread panics while holding a cache lock, the cache may not be invalidated for the lifetime of the process, leading to stale data. It's more robust to recover from the poison and clear the cache anyway, as theclear()operation is safe even with potentially inconsistent data.