Skip to content

Commit d3a060f

Browse files
authored
fix: deduplicate concurrent refresh requests (#386)
This changes refresh handling so concurrent compatible refresh requests join a single physical refresh instead of rerunning the same work, while keeping locator state safe across configure and refresh boundaries. - replace the coarse global refresh lock with a generation-aware single-flight coordinator - run physical refreshes on transient locator graphs and sync back the shared Conda and Poetry state that later requests rely on - canonicalize refresh parameters so compatible `searchPaths` requests dedupe correctly and malformed non-empty array params still fail - add coordinator unit coverage for joining, waiting, generation boundaries, completion-window reuse, panic safety, and cache sync helpers - add a reusable JSONRPC integration test client plus real server tests for configure/refresh behavior and concurrent refresh deduplication Fixes #383
1 parent a99cf64 commit d3a060f

File tree

13 files changed

+2247
-167
lines changed

13 files changed

+2247
-167
lines changed

crates/pet-conda/src/lib.rs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use pet_core::{
1616
os_environment::Environment,
1717
python_environment::{PythonEnvironment, PythonEnvironmentKind},
1818
reporter::Reporter,
19-
Locator, LocatorKind,
19+
Locator, LocatorKind, RefreshStatePersistence, RefreshStateSyncScope,
2020
};
2121
use pet_fs::path::norm_case;
2222
use rayon::prelude::*;
@@ -216,11 +216,39 @@ impl Locator for Conda {
216216
fn get_kind(&self) -> LocatorKind {
217217
LocatorKind::Conda
218218
}
219-
fn configure(&self, config: &pet_core::Configuration) {
220-
if let Some(ref conda_exe) = config.conda_executable {
221-
let mut conda_executable = self.conda_executable.write().unwrap();
222-
conda_executable.replace(conda_exe.clone());
219+
fn refresh_state(&self) -> RefreshStatePersistence {
220+
RefreshStatePersistence::SyncedDiscoveryState
221+
}
222+
fn sync_refresh_state_from(&self, source: &dyn Locator, scope: &RefreshStateSyncScope) {
223+
let source = source.as_any().downcast_ref::<Conda>().unwrap_or_else(|| {
224+
panic!("attempted to sync Conda state from {:?}", source.get_kind())
225+
});
226+
227+
match scope {
228+
RefreshStateSyncScope::Full => {}
229+
RefreshStateSyncScope::GlobalFiltered(kind)
230+
if self.supported_categories().contains(kind) => {}
231+
RefreshStateSyncScope::GlobalFiltered(_) | RefreshStateSyncScope::Workspace => {
232+
return;
233+
}
223234
}
235+
236+
self.environments.clear();
237+
self.environments
238+
.insert_many(source.environments.clone_map());
239+
240+
self.managers.clear();
241+
self.managers.insert_many(source.managers.clone_map());
242+
243+
self.mamba_managers.clear();
244+
self.mamba_managers
245+
.insert_many(source.mamba_managers.clone_map());
246+
}
247+
fn configure(&self, config: &pet_core::Configuration) {
248+
self.conda_executable
249+
.write()
250+
.unwrap()
251+
.clone_from(&config.conda_executable);
224252
}
225253
fn supported_categories(&self) -> Vec<PythonEnvironmentKind> {
226254
vec![PythonEnvironmentKind::Conda]

crates/pet-core/src/lib.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use std::path::PathBuf;
4+
use std::{any::Any, path::PathBuf};
55

66
use env::PythonEnv;
77
use manager::EnvManager;
@@ -61,7 +61,26 @@ pub enum LocatorKind {
6161
WindowsStore,
6262
}
6363

64-
pub trait Locator: Send + Sync {
64+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65+
pub enum RefreshStatePersistence {
66+
/// The locator keeps no mutable state across requests.
67+
Stateless,
68+
/// The locator keeps configured inputs only; refresh must not copy them back.
69+
ConfiguredOnly,
70+
/// The locator keeps cache-like state, but later requests can repopulate it on demand.
71+
SelfHydratingCache,
72+
/// The locator keeps refresh-discovered state that later requests depend on for correctness.
73+
SyncedDiscoveryState,
74+
}
75+
76+
#[derive(Debug, Clone, PartialEq, Eq)]
77+
pub enum RefreshStateSyncScope {
78+
Full,
79+
GlobalFiltered(PythonEnvironmentKind),
80+
Workspace,
81+
}
82+
83+
pub trait Locator: Any + Send + Sync {
6584
/// Returns the name of the locator.
6685
fn get_kind(&self) -> LocatorKind;
6786
/// Configures the locator with the given configuration.
@@ -100,6 +119,21 @@ pub trait Locator: Send + Sync {
100119
fn configure(&self, _config: &Configuration) {
101120
//
102121
}
122+
/// Describes what mutable state, if any, must survive a refresh boundary.
123+
///
124+
/// Refresh runs execute against transient locator graphs and then invoke
125+
/// `sync_refresh_state_from()` on the long-lived shared locators.
126+
fn refresh_state(&self) -> RefreshStatePersistence {
127+
RefreshStatePersistence::Stateless
128+
}
129+
/// Copies correctness-critical post-refresh state from a transient locator into this
130+
/// long-lived shared locator.
131+
///
132+
/// Override this only when `refresh_state()` returns
133+
/// `RefreshStatePersistence::SyncedDiscoveryState`.
134+
fn sync_refresh_state_from(&self, _source: &dyn Locator, _scope: &RefreshStateSyncScope) {
135+
//
136+
}
103137
/// Returns a list of supported categories for this locator.
104138
fn supported_categories(&self) -> Vec<PythonEnvironmentKind>;
105139
/// Given a Python executable, and some optional data like prefix,
@@ -112,3 +146,9 @@ pub trait Locator: Send + Sync {
112146
/// Finds all environments specific to this locator.
113147
fn find(&self, reporter: &dyn Reporter);
114148
}
149+
150+
impl dyn Locator {
151+
pub fn as_any(&self) -> &dyn Any {
152+
self
153+
}
154+
}

crates/pet-linux-global-python/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use pet_core::{
1515
env::PythonEnv,
1616
python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentKind},
1717
reporter::Reporter,
18-
Locator, LocatorKind,
18+
Locator, LocatorKind, RefreshStatePersistence,
1919
};
2020
use pet_fs::path::resolve_symlink;
2121
use pet_python_utils::{env::ResolvedPythonEnv, executable::find_executables};
@@ -62,6 +62,9 @@ impl Locator for LinuxGlobalPython {
6262
fn get_kind(&self) -> LocatorKind {
6363
LocatorKind::LinuxGlobal
6464
}
65+
fn refresh_state(&self) -> RefreshStatePersistence {
66+
RefreshStatePersistence::SelfHydratingCache
67+
}
6568
fn supported_categories(&self) -> Vec<PythonEnvironmentKind> {
6669
vec![PythonEnvironmentKind::LinuxGlobal]
6770
}

crates/pet-pipenv/src/lib.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use pet_core::LocatorKind;
1111
use pet_core::{
1212
python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentKind},
1313
reporter::Reporter,
14-
Configuration, Locator,
14+
Configuration, Locator, RefreshStatePersistence,
1515
};
1616
use pet_fs::path::norm_case;
1717
use pet_python_utils::executable::find_executables;
@@ -418,10 +418,15 @@ impl Locator for PipEnv {
418418
LocatorKind::PipEnv
419419
}
420420

421+
fn refresh_state(&self) -> RefreshStatePersistence {
422+
RefreshStatePersistence::ConfiguredOnly
423+
}
424+
421425
fn configure(&self, config: &Configuration) {
422-
if let Some(exe) = &config.pipenv_executable {
423-
self.pipenv_executable.write().unwrap().replace(exe.clone());
424-
}
426+
self.pipenv_executable
427+
.write()
428+
.unwrap()
429+
.clone_from(&config.pipenv_executable);
425430
}
426431

427432
fn supported_categories(&self) -> Vec<PythonEnvironmentKind> {

0 commit comments

Comments
 (0)