Skip to content

Commit 9bf31b5

Browse files
committed
fix: replay conda find_and_report on cache hit; fix racy cache miss (PR #458)
1 parent e7b0d0c commit 9bf31b5

2 files changed

Lines changed: 162 additions & 83 deletions

File tree

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

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,45 @@ fn empty_result() -> LocatorResult {
2626
}
2727
}
2828

29+
/// What a single company-level worker thread produces: the registry-derived
30+
/// `PythonEnvironment`s plus the install dirs that turned out to be conda
31+
/// roots. We capture conda dirs (instead of just calling
32+
/// `conda_locator.find_and_report` and forgetting) so that subsequent
33+
/// `find()` calls on a cache hit can replay them — otherwise registry-only
34+
/// conda installs would silently disappear after the first refresh.
35+
#[cfg(windows)]
36+
struct CompanyWalkOutcome {
37+
result: LocatorResult,
38+
conda_install_dirs: Vec<PathBuf>,
39+
}
40+
41+
#[cfg(windows)]
42+
impl CompanyWalkOutcome {
43+
fn empty() -> Self {
44+
Self {
45+
result: empty_result(),
46+
conda_install_dirs: vec![],
47+
}
48+
}
49+
}
50+
2951
/// Logs a warning if a spawned registry-walk thread panicked, then
30-
/// substitutes an empty result so the surviving hive/companies still
52+
/// substitutes an empty outcome so the surviving hive/companies still
3153
/// surface their environments. Without this we'd silently lose the entire
3254
/// hive when one company's walk panics — exactly the kind of regression
3355
/// that's hardest to debug after the fact.
3456
///
35-
/// Returns `(result, had_panic)` so callers can decide not to cache a
57+
/// Returns `(outcome, had_panic)` so callers can decide not to cache a
3658
/// partial result (otherwise a single transient panic would persist as a
3759
/// stale empty/partial cache across refreshes — see issue #454).
3860
#[cfg(windows)]
3961
fn join_or_warn(
40-
join_result: std::thread::Result<LocatorResult>,
62+
join_result: std::thread::Result<CompanyWalkOutcome>,
4163
label: &str,
42-
) -> (LocatorResult, bool) {
64+
) -> (CompanyWalkOutcome, bool) {
4365
use log::warn;
4466
match join_result {
45-
Ok(result) => (result, false),
67+
Ok(outcome) => (outcome, false),
4668
Err(panic_payload) => {
4769
// Try to render the payload for the log; payloads are commonly
4870
// either a `&'static str` or a `String`.
@@ -52,18 +74,21 @@ fn join_or_warn(
5274
.or_else(|| panic_payload.downcast_ref::<String>().cloned())
5375
.unwrap_or_else(|| "<non-string panic payload>".to_string());
5476
warn!("Registry walk thread for {} panicked: {}", label, message);
55-
(empty_result(), true)
77+
(CompanyWalkOutcome::empty(), true)
5678
}
5779
}
5880
}
5981

60-
/// Outcome of a registry walk: the discovered environments/managers and a
61-
/// flag indicating whether any worker thread panicked. Callers should
62-
/// avoid caching a result with `had_panic = true` so a transient failure
63-
/// can be retried on the next refresh instead of becoming sticky.
82+
/// Outcome of a registry walk: the discovered environments/managers, the
83+
/// conda install dirs found via the registry (so cache-hit replays can
84+
/// re-invoke `conda_locator.find_and_report`), and a flag indicating
85+
/// whether any worker thread panicked. Callers should avoid caching a
86+
/// result with `had_panic = true` so a transient failure can be retried
87+
/// on the next refresh instead of becoming sticky.
6488
#[cfg(windows)]
6589
pub struct RegistryWalkOutcome {
6690
pub result: LocatorResult,
91+
pub conda_install_dirs: Vec<PathBuf>,
6792
pub had_panic: bool,
6893
}
6994

@@ -79,7 +104,7 @@ pub fn get_registry_pythons(
79104
// independent registry trees and Defender intercepts every read, so the
80105
// serial baseline was paying for both round-trips back to back; the
81106
// scope-spawn pattern matches `pet-pyenv` / `pet-homebrew` / `pet-conda`.
82-
let ((hklm_result, hklm_panic), (hkcu_result, hkcu_panic)) = thread::scope(|s| {
107+
let (hklm_outcome, hkcu_outcome) = thread::scope(|s| {
83108
let hklm = s.spawn(|| {
84109
get_registry_pythons_for_hive(
85110
"HKLM",
@@ -102,39 +127,46 @@ pub fn get_registry_pythons(
102127
)
103128
});
104129

105-
let mut environments = hklm_result.environments;
106-
environments.extend(hkcu_result.environments);
107-
let mut managers = hklm_result.managers;
108-
managers.extend(hkcu_result.managers);
130+
let mut environments = hklm_outcome.result.environments;
131+
environments.extend(hkcu_outcome.result.environments);
132+
let mut managers = hklm_outcome.result.managers;
133+
managers.extend(hkcu_outcome.result.managers);
134+
let mut conda_install_dirs = hklm_outcome.conda_install_dirs;
135+
conda_install_dirs.extend(hkcu_outcome.conda_install_dirs);
109136

110137
RegistryWalkOutcome {
111138
result: LocatorResult {
112139
environments,
113140
managers,
114141
},
115-
had_panic: hklm_panic || hkcu_panic,
142+
conda_install_dirs,
143+
had_panic: hklm_outcome.had_panic || hkcu_outcome.had_panic,
116144
}
117145
}
118146

119147
/// Sibling of `join_or_warn` for hive-level threads. Returns the recovered
120-
/// `LocatorResult` plus a `had_panic` flag that already accounts for any
121-
/// company-level panics propagated through `RegistryWalkOutcome`.
148+
/// outcome (already aggregating any company-level panics) so the top
149+
/// level can OR the panic flags and concatenate the conda install dirs.
122150
#[cfg(windows)]
123151
fn join_hive_outcome(
124152
join_result: std::thread::Result<RegistryWalkOutcome>,
125153
label: &str,
126-
) -> (LocatorResult, bool) {
154+
) -> RegistryWalkOutcome {
127155
use log::warn;
128156
match join_result {
129-
Ok(outcome) => (outcome.result, outcome.had_panic),
157+
Ok(outcome) => outcome,
130158
Err(panic_payload) => {
131159
let message = panic_payload
132160
.downcast_ref::<&'static str>()
133161
.map(|s| (*s).to_string())
134162
.or_else(|| panic_payload.downcast_ref::<String>().cloned())
135163
.unwrap_or_else(|| "<non-string panic payload>".to_string());
136164
warn!("Registry walk thread for {} panicked: {}", label, message);
137-
(empty_result(), true)
165+
RegistryWalkOutcome {
166+
result: empty_result(),
167+
conda_install_dirs: vec![],
168+
had_panic: true,
169+
}
138170
}
139171
}
140172
}
@@ -158,6 +190,7 @@ fn get_registry_pythons_for_hive(
158190
warn!("Failed to open {}\\Software\\Python, {:?}", name, err);
159191
return RegistryWalkOutcome {
160192
result: empty_result(),
193+
conda_install_dirs: vec![],
161194
had_panic: false,
162195
};
163196
}
@@ -183,7 +216,7 @@ fn get_registry_pythons_for_hive(
183216
})
184217
.collect();
185218

186-
let results: Vec<(LocatorResult, bool)> = thread::scope(|s| {
219+
let results: Vec<(CompanyWalkOutcome, bool)> = thread::scope(|s| {
187220
let handles: Vec<_> = companies
188221
.into_iter()
189222
.map(|(company, company_key)| {
@@ -214,17 +247,20 @@ fn get_registry_pythons_for_hive(
214247

215248
let mut environments = vec![];
216249
let mut managers = vec![];
250+
let mut conda_install_dirs = vec![];
217251
let mut had_panic = false;
218-
for (r, panicked) in results {
219-
environments.extend(r.environments);
220-
managers.extend(r.managers);
252+
for (outcome, panicked) in results {
253+
environments.extend(outcome.result.environments);
254+
managers.extend(outcome.result.managers);
255+
conda_install_dirs.extend(outcome.conda_install_dirs);
221256
had_panic |= panicked;
222257
}
223258
RegistryWalkOutcome {
224259
result: LocatorResult {
225260
environments,
226261
managers,
227262
},
263+
conda_install_dirs,
228264
had_panic,
229265
}
230266
}
@@ -236,12 +272,13 @@ fn get_registry_pythons_from_key_for_company(
236272
company: &str,
237273
conda_locator: &Arc<dyn CondaLocator>,
238274
reporter: &Option<&dyn Reporter>,
239-
) -> LocatorResult {
275+
) -> CompanyWalkOutcome {
240276
use log::{trace, warn};
241277
use pet_conda::utils::is_conda_env;
242278
use pet_fs::path::norm_case;
243279

244280
let mut environments = vec![];
281+
let mut conda_install_dirs = vec![];
245282
// let company_display_name: Option<String> = company_key.get_value("DisplayName").ok();
246283
for installed_python in company_key.enum_keys().filter_map(Result::ok) {
247284
match company_key.open_subkey(installed_python.clone()) {
@@ -281,6 +318,15 @@ fn get_registry_pythons_from_key_for_company(
281318
if let Some(reporter) = reporter {
282319
conda_locator.find_and_report(*reporter, &env_path);
283320
}
321+
// Capture the dir even when no reporter is
322+
// attached (e.g. cache-warming via
323+
// `find_with_cache(None)`) so a later cache
324+
// hit in `find()` can replay
325+
// `conda_locator.find_and_report` against
326+
// each dir; without this, registry-only conda
327+
// installs would silently disappear from
328+
// every refresh after the first (#454).
329+
conda_install_dirs.push(env_path);
284330
continue;
285331
}
286332

@@ -366,8 +412,11 @@ fn get_registry_pythons_from_key_for_company(
366412
}
367413
}
368414

369-
LocatorResult {
370-
environments,
371-
managers: vec![],
415+
CompanyWalkOutcome {
416+
result: LocatorResult {
417+
environments,
418+
managers: vec![],
419+
},
420+
conda_install_dirs,
372421
}
373422
}

0 commit comments

Comments
 (0)