Skip to content

Commit d01fe38

Browse files
Replace .lock().unwrap() with .expect() for better mutex poisoning diagnostics (#303)
The codebase had 39 occurrences of `.lock().unwrap()` in source files. These provide no context when mutex poisoning occurs, making debugging difficult. ## Changes - Replace all `.lock().unwrap()` with `.expect("<mutex_name> mutex poisoned")` in 12 source files - Update documentation example in `pet-core/src/lib.rs` to follow the new pattern - Test files intentionally unchanged (panicking on test setup is acceptable) ### Before ```rust let mut environments = self.environments.lock().unwrap(); ``` ### After ```rust let mut environments = self.environments.lock().expect("environments mutex poisoned"); ``` ## Files Modified - `pet-python-utils/src/cache.rs`, `env.rs` - `pet-reporter/src/collect.rs`, `stdio.rs` - `pet-core/src/os_environment.rs`, `lib.rs` - `pet-pyenv/src/lib.rs` - `pet-uv/src/lib.rs` - `pet-windows-registry/src/lib.rs` - `pet/src/jsonrpc.rs`, `find.rs`, `lib.rs` <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>Refactor: Replace .lock().unwrap() with proper error handling or expect()</issue_title> > <issue_description>## Summary > The codebase has 50+ occurrences of `.lock().unwrap()` which will panic if a thread holding the lock panics (causing mutex poisoning). While this may be acceptable for internal tools, better error handling would improve robustness. > > ## Current Pattern > ```rust > let mut environments = self.environments.lock().unwrap(); > ``` > > ## Proposed Improvements > > ### Option 1: Use `.expect()` with meaningful message > ```rust > let mut environments = self.environments > .lock() > .expect("environments mutex poisoned - previous thread panicked"); > ``` > > ### Option 2: Handle PoisonError gracefully > ```rust > let mut environments = self.environments > .lock() > .unwrap_or_else(|poisoned| { > log::warn!("Recovering from poisoned mutex"); > poisoned.into_inner() > }); > ``` > > ### Option 3: Use `parking_lot::Mutex` which doesn't poison > ```rust > // parking_lot::Mutex doesn't have PoisonError > use parking_lot::Mutex; > let mut environments = self.environments.lock(); > ``` > > ## Files with Most Occurrences > 1. `crates/pet-conda/src/lib.rs` - ~15 occurrences > 2. `crates/pet-python-utils/src/cache.rs` - ~10 occurrences > 3. `crates/pet-poetry/src/lib.rs` - ~8 occurrences > 4. `crates/pet-linux-global-python/src/lib.rs` - ~5 occurrences > 5. `crates/pet-reporter/src/cache.rs` - ~5 occurrences > > ## Recommendation > For a JSONRPC server that should be reliable: > 1. At minimum, replace `unwrap()` with `expect("meaningful message")` for better debugging > 2. Consider `parking_lot` crate which has better performance and no poisoning semantics > 3. For critical paths (like the reporter), consider graceful recovery > > ## Priority > Low - Current code works but could be more robust.</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #289 <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/microsoft/python-environment-tools/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: karthiknadig <3840081+karthiknadig@users.noreply.github.com>
1 parent e15723c commit d01fe38

File tree

12 files changed

+135
-45
lines changed

12 files changed

+135
-45
lines changed

crates/pet-core/src/lib.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,7 @@ pub trait Locator: Send + Sync {
9090
/// impl Locator for MyLocator {
9191
/// fn configure(&self, config: &Configuration) {
9292
/// if let Some(dirs) = &config.workspace_directories {
93-
/// // Using unwrap() is acceptable here as mutex poisoning indicates
94-
/// // a panic in another thread, which is unrecoverable in this context.
95-
/// *self.workspace_dirs.lock().unwrap() = dirs.clone();
93+
/// *self.workspace_dirs.lock().expect("workspace_dirs mutex poisoned") = dirs.clone();
9694
/// }
9795
/// }
9896
/// // ... other required methods

crates/pet-core/src/os_environment.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,26 @@ impl Environment for EnvironmentApi {
4747
get_env_var(key)
4848
}
4949
fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
50-
if self.global_search_locations.lock().unwrap().is_empty() {
50+
if self
51+
.global_search_locations
52+
.lock()
53+
.expect("global_search_locations mutex poisoned")
54+
.is_empty()
55+
{
5156
let mut paths =
5257
env::split_paths(&self.get_env_var("PATH".to_string()).unwrap_or_default())
5358
.filter(|p| p.exists())
5459
.collect::<Vec<PathBuf>>();
5560
trace!("Env PATH: {:?}", paths);
5661
self.global_search_locations
5762
.lock()
58-
.unwrap()
63+
.expect("global_search_locations mutex poisoned")
5964
.append(&mut paths);
6065
}
61-
self.global_search_locations.lock().unwrap().clone()
66+
self.global_search_locations
67+
.lock()
68+
.expect("global_search_locations mutex poisoned")
69+
.clone()
6270
}
6371
}
6472

@@ -74,7 +82,12 @@ impl Environment for EnvironmentApi {
7482
get_env_var(key)
7583
}
7684
fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
77-
if self.global_search_locations.lock().unwrap().is_empty() {
85+
if self
86+
.global_search_locations
87+
.lock()
88+
.expect("global_search_locations mutex poisoned")
89+
.is_empty()
90+
{
7891
let mut paths =
7992
env::split_paths(&self.get_env_var("PATH".to_string()).unwrap_or_default())
8093
.collect::<Vec<PathBuf>>();
@@ -126,10 +139,13 @@ impl Environment for EnvironmentApi {
126139

127140
self.global_search_locations
128141
.lock()
129-
.unwrap()
142+
.expect("global_search_locations mutex poisoned")
130143
.append(&mut paths);
131144
}
132-
self.global_search_locations.lock().unwrap().clone()
145+
self.global_search_locations
146+
.lock()
147+
.expect("global_search_locations mutex poisoned")
148+
.clone()
133149
}
134150
}
135151

crates/pet-pyenv/src/lib.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,18 @@ impl PyEnv {
4848
}
4949
}
5050
fn clear(&self) {
51-
self.manager.lock().unwrap().take();
52-
self.versions_dir.lock().unwrap().take();
51+
self.manager.lock().expect("manager mutex poisoned").take();
52+
self.versions_dir
53+
.lock()
54+
.expect("versions_dir mutex poisoned")
55+
.take();
5356
}
5457
fn get_manager_versions_dir(&self) -> (Option<EnvManager>, Option<PathBuf>) {
55-
let mut managers = self.manager.lock().unwrap();
56-
let mut versions = self.versions_dir.lock().unwrap();
58+
let mut managers = self.manager.lock().expect("manager mutex poisoned");
59+
let mut versions = self
60+
.versions_dir
61+
.lock()
62+
.expect("versions_dir mutex poisoned");
5763
if managers.is_none() || versions.is_none() {
5864
let pyenv_info = PyEnvInfo::from(&self.env_vars);
5965
trace!("PyEnv Info {:?}", pyenv_info);

crates/pet-python-utils/src/cache.rs

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,34 +60,59 @@ impl CacheImpl {
6060
}
6161

6262
fn get_cache_directory(&self) -> Option<PathBuf> {
63-
self.cache_dir.lock().unwrap().clone()
63+
self.cache_dir
64+
.lock()
65+
.expect("cache_dir mutex poisoned")
66+
.clone()
6467
}
6568

6669
/// Once a cache directory has been set, you cannot change it.
6770
/// No point supporting such a scenario.
6871
fn set_cache_directory(&self, cache_dir: PathBuf) {
69-
if let Some(cache_dir) = self.cache_dir.lock().unwrap().clone() {
72+
if let Some(cache_dir) = self
73+
.cache_dir
74+
.lock()
75+
.expect("cache_dir mutex poisoned")
76+
.clone()
77+
{
7078
warn!(
7179
"Cache directory has already been set to {:?}. Cannot change it now.",
7280
cache_dir
7381
);
7482
return;
7583
}
7684
trace!("Setting cache directory to {:?}", cache_dir);
77-
self.cache_dir.lock().unwrap().replace(cache_dir);
85+
self.cache_dir
86+
.lock()
87+
.expect("cache_dir mutex poisoned")
88+
.replace(cache_dir);
7889
}
7990
fn clear(&self) -> io::Result<()> {
8091
trace!("Clearing cache");
81-
self.locks.lock().unwrap().clear();
82-
if let Some(cache_directory) = self.cache_dir.lock().unwrap().clone() {
92+
self.locks.lock().expect("locks mutex poisoned").clear();
93+
if let Some(cache_directory) = self
94+
.cache_dir
95+
.lock()
96+
.expect("cache_dir mutex poisoned")
97+
.clone()
98+
{
8399
std::fs::remove_dir_all(cache_directory)
84100
} else {
85101
Ok(())
86102
}
87103
}
88104
fn create_cache(&self, executable: PathBuf) -> LockableCacheEntry {
89-
let cache_directory = self.cache_dir.lock().unwrap().clone();
90-
match self.locks.lock().unwrap().entry(executable.clone()) {
105+
let cache_directory = self
106+
.cache_dir
107+
.lock()
108+
.expect("cache_dir mutex poisoned")
109+
.clone();
110+
match self
111+
.locks
112+
.lock()
113+
.expect("locks mutex poisoned")
114+
.entry(executable.clone())
115+
{
91116
Entry::Occupied(lock) => lock.get().clone(),
92117
Entry::Vacant(lock) => {
93118
let cache = Box::new(CacheEntryImpl::create(cache_directory.clone(), executable))
@@ -122,7 +147,12 @@ impl CacheEntryImpl {
122147
}
123148
pub fn verify_in_memory_cache(&self) {
124149
// Check if any of the exes have changed since we last cached this.
125-
for symlink_info in self.symlinks.lock().unwrap().iter() {
150+
for symlink_info in self
151+
.symlinks
152+
.lock()
153+
.expect("symlinks mutex poisoned")
154+
.iter()
155+
{
126156
if let Ok(metadata) = symlink_info.0.metadata() {
127157
let mtime_changed = metadata.modified().ok() != Some(symlink_info.1);
128158
// Only check ctime if we have it stored (may be None on Linux)
@@ -139,7 +169,10 @@ impl CacheEntryImpl {
139169
metadata.modified().ok(),
140170
metadata.created().ok()
141171
);
142-
self.envoronment.lock().unwrap().take();
172+
self.envoronment
173+
.lock()
174+
.expect("envoronment mutex poisoned")
175+
.take();
143176
if let Some(cache_directory) = &self.cache_directory {
144177
delete_cache_file(cache_directory, &self.executable);
145178
}
@@ -155,15 +188,23 @@ impl CacheEntry for CacheEntryImpl {
155188

156189
// New scope to drop lock immediately after we have the value.
157190
{
158-
if let Some(env) = self.envoronment.lock().unwrap().clone() {
191+
if let Some(env) = self
192+
.envoronment
193+
.lock()
194+
.expect("envoronment mutex poisoned")
195+
.clone()
196+
{
159197
return Some(env);
160198
}
161199
}
162200

163201
if let Some(ref cache_directory) = self.cache_directory {
164202
let (env, mut symlinks) = get_cache_from_file(cache_directory, &self.executable)?;
165-
self.envoronment.lock().unwrap().replace(env.clone());
166-
let mut locked_symlinks = self.symlinks.lock().unwrap();
203+
self.envoronment
204+
.lock()
205+
.expect("envoronment mutex poisoned")
206+
.replace(env.clone());
207+
let mut locked_symlinks = self.symlinks.lock().expect("symlinks mutex poisoned");
167208
locked_symlinks.clear();
168209
locked_symlinks.append(&mut symlinks);
169210
Some(env)
@@ -190,13 +231,13 @@ impl CacheEntry for CacheEntryImpl {
190231
symlinks.dedup();
191232

192233
{
193-
let mut locked_symlinks = self.symlinks.lock().unwrap();
234+
let mut locked_symlinks = self.symlinks.lock().expect("symlinks mutex poisoned");
194235
locked_symlinks.clear();
195236
locked_symlinks.append(&mut symlinks.clone());
196237
}
197238
self.envoronment
198239
.lock()
199-
.unwrap()
240+
.expect("envoronment mutex poisoned")
200241
.replace(environment.clone());
201242

202243
trace!("Caching interpreter info for {:?}", self.executable);
@@ -213,7 +254,7 @@ impl CacheEntry for CacheEntryImpl {
213254
let known_symlinks: HashSet<PathBuf> = self
214255
.symlinks
215256
.lock()
216-
.unwrap()
257+
.expect("symlinks mutex poisoned")
217258
.clone()
218259
.iter()
219260
.map(|x| x.0.clone())

crates/pet-python-utils/src/env.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ impl ResolvedPythonEnv {
5656
&& environment.arch == arch
5757
{
5858
let cache = create_cache(self.executable.clone());
59-
let entry = cache.lock().unwrap();
59+
let entry = cache.lock().expect("cache mutex poisoned");
6060
entry.track_symlinks(symlinks)
6161
} else {
6262
error!(
@@ -75,7 +75,7 @@ impl ResolvedPythonEnv {
7575
// cache: &dyn Cache,
7676
) -> Option<Self> {
7777
let cache = create_cache(executable.to_path_buf());
78-
let entry = cache.lock().unwrap();
78+
let entry = cache.lock().expect("cache mutex poisoned");
7979
if let Some(env) = entry.get() {
8080
Some(env)
8181
} else if let Some(env) = get_interpreter_details(executable) {

crates/pet-reporter/src/collect.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,17 @@ impl Reporter for CollectReporter {
2929
//
3030
}
3131
fn report_manager(&self, manager: &EnvManager) {
32-
self.managers.lock().unwrap().push(manager.clone());
32+
self.managers
33+
.lock()
34+
.expect("managers mutex poisoned")
35+
.push(manager.clone());
3336
}
3437

3538
fn report_environment(&self, env: &PythonEnvironment) {
36-
self.environments.lock().unwrap().push(env.clone());
39+
self.environments
40+
.lock()
41+
.expect("environments mutex poisoned")
42+
.push(env.clone());
3743
}
3844
}
3945

crates/pet-reporter/src/stdio.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ pub struct Summary {
2828

2929
impl StdioReporter {
3030
pub fn get_summary(&self) -> Summary {
31-
let managers = self.managers.lock().unwrap();
32-
let environments = self.environments.lock().unwrap();
31+
let managers = self.managers.lock().expect("managers mutex poisoned");
32+
let environments = self
33+
.environments
34+
.lock()
35+
.expect("environments mutex poisoned");
3336
Summary {
3437
managers: managers.clone(),
3538
environments: environments.clone(),
@@ -41,7 +44,7 @@ impl Reporter for StdioReporter {
4144
//
4245
}
4346
fn report_manager(&self, manager: &EnvManager) {
44-
let mut managers = self.managers.lock().unwrap();
47+
let mut managers = self.managers.lock().expect("managers mutex poisoned");
4548
let count = managers.get(&manager.tool).unwrap_or(&0) + 1;
4649
managers.insert(manager.tool, count);
4750
if self.print_list {
@@ -53,7 +56,10 @@ impl Reporter for StdioReporter {
5356
if self.kind.is_some() && env.kind != self.kind {
5457
return;
5558
}
56-
let mut environments = self.environments.lock().unwrap();
59+
let mut environments = self
60+
.environments
61+
.lock()
62+
.expect("environments mutex poisoned");
5763
let count = environments.get(&env.kind).unwrap_or(&0) + 1;
5864
environments.insert(env.kind, count);
5965
if self.print_list {

crates/pet-uv/src/lib.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ impl Locator for Uv {
8282

8383
fn configure(&self, config: &Configuration) {
8484
if let Some(workspace_directories) = config.workspace_directories.as_ref() {
85-
let mut ws = self.workspace_directories.lock().unwrap();
85+
let mut ws = self
86+
.workspace_directories
87+
.lock()
88+
.expect("workspace_directories mutex poisoned");
8689
ws.clear();
8790
ws.extend(workspace_directories.iter().cloned());
8891
}
@@ -137,7 +140,11 @@ impl Locator for Uv {
137140

138141
fn find(&self, reporter: &dyn Reporter) {
139142
// look through workspace directories for uv-managed projects and any of their workspaces
140-
let workspaces = self.workspace_directories.lock().unwrap().clone();
143+
let workspaces = self
144+
.workspace_directories
145+
.lock()
146+
.expect("workspace_directories mutex poisoned")
147+
.clone();
141148
for workspace in workspaces {
142149
// TODO: maybe check for workspace in parent folders?
143150
for env in list_envs_in_directory(&workspace) {

crates/pet-windows-registry/src/lib.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ impl WindowsRegistry {
3131
}
3232
#[cfg(windows)]
3333
fn find_with_cache(&self, reporter: Option<&dyn Reporter>) -> Option<LocatorResult> {
34-
let mut result = self.search_result.lock().unwrap();
34+
let mut result = self
35+
.search_result
36+
.lock()
37+
.expect("search_result mutex poisoned");
3538
if let Some(result) = result.clone() {
3639
return Some(result);
3740
}
@@ -43,7 +46,10 @@ impl WindowsRegistry {
4346
}
4447
#[cfg(windows)]
4548
fn clear(&self) {
46-
let mut search_result = self.search_result.lock().unwrap();
49+
let mut search_result = self
50+
.search_result
51+
.lock()
52+
.expect("search_result mutex poisoned");
4753
search_result.take();
4854
}
4955
}

crates/pet/src/find.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ pub fn find_and_report_envs(
257257
.insert("Workspaces", start.elapsed());
258258
});
259259
});
260-
summary.lock().unwrap().total = start.elapsed();
260+
summary.lock().expect("summary mutex poisoned").total = start.elapsed();
261261

262262
summary
263263
}

0 commit comments

Comments
 (0)