Skip to content

Commit 3337ff1

Browse files
committed
fix: serialize configure locator updates (Fixes #385)
1 parent 172d4b1 commit 3337ff1

File tree

1 file changed

+140
-42
lines changed

1 file changed

+140
-42
lines changed

crates/pet/src/jsonrpc.rs

Lines changed: 140 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -539,21 +539,24 @@ pub fn handle_configure(context: Arc<Context>, id: u32, params: Value) {
539539

540540
// Expand glob patterns before acquiring the write lock so we
541541
// don't block readers/writers while traversing the filesystem.
542-
let workspace_directories = configure_options.workspace_directories.map(|dirs| {
543-
let start = Instant::now();
544-
let result: Vec<PathBuf> = expand_glob_patterns(&dirs)
545-
.into_iter()
546-
.filter(|p| p.is_dir())
547-
.collect();
548-
trace!(
549-
"Expanded workspace directory patterns ({:?}) in {:?}",
550-
dirs,
551-
start.elapsed()
552-
);
553-
result
554-
});
555-
let environment_directories =
556-
configure_options.environment_directories.map(|dirs| {
542+
let workspace_directories =
543+
configure_options.workspace_directories.clone().map(|dirs| {
544+
let start = Instant::now();
545+
let result: Vec<PathBuf> = expand_glob_patterns(&dirs)
546+
.into_iter()
547+
.filter(|p| p.is_dir())
548+
.collect();
549+
trace!(
550+
"Expanded workspace directory patterns ({:?}) in {:?}",
551+
dirs,
552+
start.elapsed()
553+
);
554+
result
555+
});
556+
let environment_directories = configure_options
557+
.environment_directories
558+
.clone()
559+
.map(|dirs| {
557560
let start = Instant::now();
558561
let result: Vec<PathBuf> = expand_glob_patterns(&dirs)
559562
.into_iter()
@@ -575,33 +578,13 @@ pub fn handle_configure(context: Arc<Context>, id: u32, params: Value) {
575578
);
576579
}
577580

578-
let config = {
579-
let mut state = context.configuration.write().unwrap();
580-
state.config.workspace_directories = workspace_directories;
581-
state.config.conda_executable = configure_options.conda_executable;
582-
state.config.environment_directories = environment_directories;
583-
state.config.pipenv_executable = configure_options.pipenv_executable;
584-
state.config.poetry_executable = configure_options.poetry_executable;
585-
// We will not support changing the cache directories once set.
586-
// No point, supporting such a use case.
587-
if let Some(cache_directory) = configure_options.cache_directory {
588-
set_cache_directory(cache_directory.clone());
589-
state.config.cache_directory = Some(cache_directory);
590-
}
591-
state.generation += 1;
592-
// Reset missing-env reporting so that the next refresh
593-
// after reconfiguration can trigger it again (Fixes #395).
594-
// Done inside the write lock to avoid a TOCTOU window with
595-
// concurrent refresh threads reading the generation.
596-
MISSING_ENVS_REPORTING_STATE.store(MISSING_ENVS_AVAILABLE, Ordering::Release);
597-
trace!(
598-
"Configuring locators with generation {}: {:?}",
599-
state.generation,
600-
state.config
601-
);
602-
state.config.clone()
603-
};
604-
configure_locators(&context.locators, &config);
581+
apply_configure_options(
582+
context.configuration.as_ref(),
583+
&context.locators,
584+
configure_options,
585+
workspace_directories,
586+
environment_directories,
587+
);
605588
info!("Configure completed in {:?}", now.elapsed());
606589
send_reply(id, None::<()>);
607590
});
@@ -665,6 +648,39 @@ fn parse_refresh_options(params: Value) -> Result<RefreshOptions, serde_json::Er
665648
.map(|options| canonicalize_refresh_options(options.unwrap_or_default()))
666649
}
667650

651+
fn apply_configure_options(
652+
configuration: &RwLock<ConfigurationState>,
653+
locators: &Arc<Vec<Arc<dyn Locator>>>,
654+
configure_options: ConfigureOptions,
655+
workspace_directories: Option<Vec<PathBuf>>,
656+
environment_directories: Option<Vec<PathBuf>>,
657+
) {
658+
let mut state = configuration.write().unwrap();
659+
state.config.workspace_directories = workspace_directories;
660+
state.config.conda_executable = configure_options.conda_executable;
661+
state.config.environment_directories = environment_directories;
662+
state.config.pipenv_executable = configure_options.pipenv_executable;
663+
state.config.poetry_executable = configure_options.poetry_executable;
664+
// We will not support changing the cache directories once set.
665+
// No point, supporting such a use case.
666+
if let Some(cache_directory) = configure_options.cache_directory {
667+
set_cache_directory(cache_directory.clone());
668+
state.config.cache_directory = Some(cache_directory);
669+
}
670+
state.generation += 1;
671+
// Reset missing-env reporting so that the next refresh after
672+
// reconfiguration can trigger it again (Fixes #395). Done inside the write
673+
// lock to avoid a TOCTOU window with concurrent refresh threads reading the
674+
// generation.
675+
MISSING_ENVS_REPORTING_STATE.store(MISSING_ENVS_AVAILABLE, Ordering::Release);
676+
trace!(
677+
"Configuring locators with generation {}: {:?}",
678+
state.generation,
679+
state.config
680+
);
681+
configure_locators(locators, &state.config);
682+
}
683+
668684
fn configure_locators(locators: &Arc<Vec<Arc<dyn Locator>>>, config: &Configuration) {
669685
for locator in locators.iter() {
670686
locator.configure(config);
@@ -1244,6 +1260,12 @@ mod tests {
12441260
reported: Mutex<bool>,
12451261
}
12461262

1263+
struct BlockingConfigureLocator {
1264+
started: mpsc::Sender<()>,
1265+
release: Mutex<mpsc::Receiver<()>>,
1266+
configured_workspace_directories: Mutex<Option<Vec<PathBuf>>>,
1267+
}
1268+
12471269
impl Reporter for RecordingReporter {
12481270
fn report_manager(&self, manager: &EnvManager) {
12491271
self.managers.lock().unwrap().push(manager.clone());
@@ -1275,6 +1297,29 @@ mod tests {
12751297
}
12761298
}
12771299

1300+
impl Locator for BlockingConfigureLocator {
1301+
fn get_kind(&self) -> LocatorKind {
1302+
LocatorKind::Venv
1303+
}
1304+
1305+
fn configure(&self, config: &Configuration) {
1306+
self.started.send(()).unwrap();
1307+
self.release.lock().unwrap().recv().unwrap();
1308+
*self.configured_workspace_directories.lock().unwrap() =
1309+
config.workspace_directories.clone();
1310+
}
1311+
1312+
fn supported_categories(&self) -> Vec<PythonEnvironmentKind> {
1313+
vec![PythonEnvironmentKind::Venv]
1314+
}
1315+
1316+
fn try_from(&self, _env: &pet_core::env::PythonEnv) -> Option<PythonEnvironment> {
1317+
None
1318+
}
1319+
1320+
fn find(&self, _reporter: &dyn Reporter) {}
1321+
}
1322+
12781323
fn make_refresh_key(generation: u64, options: RefreshOptions) -> RefreshKey {
12791324
RefreshKey::new(&options, generation)
12801325
}
@@ -1402,6 +1447,59 @@ mod tests {
14021447
assert!(*inner.reported.lock().unwrap());
14031448
}
14041449

1450+
#[test]
1451+
fn test_configure_publishes_state_after_shared_locators_are_configured() {
1452+
let configuration = Arc::new(RwLock::new(ConfigurationState::default()));
1453+
let (started_tx, started_rx) = mpsc::channel();
1454+
let (release_tx, release_rx) = mpsc::channel();
1455+
let locator = Arc::new(BlockingConfigureLocator {
1456+
started: started_tx,
1457+
release: Mutex::new(release_rx),
1458+
configured_workspace_directories: Mutex::new(None),
1459+
});
1460+
let locators = Arc::new(vec![locator.clone() as Arc<dyn Locator>]);
1461+
let workspace_directories = vec![PathBuf::from("/workspace")];
1462+
1463+
let worker = {
1464+
let configuration = configuration.clone();
1465+
let locators = locators.clone();
1466+
let workspace_directories = workspace_directories.clone();
1467+
thread::spawn(move || {
1468+
apply_configure_options(
1469+
configuration.as_ref(),
1470+
&locators,
1471+
ConfigureOptions {
1472+
workspace_directories: None,
1473+
conda_executable: None,
1474+
pipenv_executable: None,
1475+
poetry_executable: None,
1476+
environment_directories: None,
1477+
cache_directory: None,
1478+
},
1479+
Some(workspace_directories),
1480+
None,
1481+
);
1482+
})
1483+
};
1484+
1485+
started_rx.recv().unwrap();
1486+
assert!(configuration.try_read().is_err());
1487+
1488+
release_tx.send(()).unwrap();
1489+
worker.join().unwrap();
1490+
1491+
let state = configuration.read().unwrap();
1492+
assert_eq!(state.generation, 1);
1493+
assert_eq!(
1494+
state.config.workspace_directories,
1495+
Some(workspace_directories.clone())
1496+
);
1497+
assert_eq!(
1498+
*locator.configured_workspace_directories.lock().unwrap(),
1499+
Some(workspace_directories)
1500+
);
1501+
}
1502+
14051503
#[test]
14061504
fn test_stale_generation_does_not_begin_missing_env_reporting() {
14071505
let _guard = MISSING_ENVS_TEST_LOCK.lock().unwrap();

0 commit comments

Comments
 (0)