diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-22 11:07:42 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-22 11:15:29 +0900 |
| commit | 9f0d210021c54f63c9984446862b6ec68834bc63 (patch) | |
| tree | d1522b8047c60bc7ee7a9d832178dd24e1b07636 /crates/mozart-registry | |
| parent | 2c243a3cb814939bbe40fda1608781825ab0d77d (diff) | |
| download | php-mozart-9f0d210021c54f63c9984446862b6ec68834bc63.tar.gz php-mozart-9f0d210021c54f63c9984446862b6ec68834bc63.tar.zst php-mozart-9f0d210021c54f63c9984446862b6ec68834bc63.zip | |
refactor(async): migrate from blocking HTTP to async/await with tokio
Replace reqwest::blocking with async reqwest across the entire codebase.
All command execute functions, registry API calls (packagist, downloader,
resolver, lockfile), and the main entry point now use async/await with
the tokio runtime. The pubgrub resolver runs on spawn_blocking since its
DependencyProvider trait is synchronous, using Handle::block_on for
async I/O within that context.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-registry')
| -rw-r--r-- | crates/mozart-registry/src/downloader.rs | 23 | ||||
| -rw-r--r-- | crates/mozart-registry/src/lockfile.rs | 23 | ||||
| -rw-r--r-- | crates/mozart-registry/src/packagist.rs | 27 | ||||
| -rw-r--r-- | crates/mozart-registry/src/resolver.rs | 189 |
4 files changed, 149 insertions, 113 deletions
diff --git a/crates/mozart-registry/src/downloader.rs b/crates/mozart-registry/src/downloader.rs index cfed951..9a5ed24 100644 --- a/crates/mozart-registry/src/downloader.rs +++ b/crates/mozart-registry/src/downloader.rs @@ -79,7 +79,7 @@ impl DownloadProgress { /// the `Content-Length` response header. /// If `files_cache` is provided, the downloaded bytes are cached by URL; cache hits skip /// the network request entirely. -pub fn download_dist( +pub async fn download_dist( url: &str, expected_shasum: Option<&str>, progress: Option<&mut DownloadProgress>, @@ -108,7 +108,7 @@ pub fn download_dist( } } - let response = reqwest::blocking::get(url)?; + let response = reqwest::get(url).await?; if !response.status().is_success() { anyhow::bail!( @@ -123,20 +123,15 @@ pub fn download_dist( if let Some(content_length) = response.content_length() { pb.set_total(content_length); } - let mut reader = response; let mut buf = Vec::new(); - let mut chunk = [0u8; 8192]; - loop { - let n = reader.read(&mut chunk)?; - if n == 0 { - break; - } - buf.extend_from_slice(&chunk[..n]); - pb.inc(n as u64); + let mut stream = response; + while let Some(chunk) = stream.chunk().await? { + buf.extend_from_slice(&chunk); + pb.inc(chunk.len() as u64); } buf } else { - response.bytes()?.to_vec() + response.bytes().await?.to_vec() }; // Verify SHA-1 checksum if provided @@ -325,7 +320,7 @@ pub fn extract_tar_gz(data: &[u8], target_dir: &Path) -> anyhow::Result<()> { /// - `package_name`: e.g. `"monolog/monolog"` /// - `progress`: optional mutable progress tracker to update during download /// - `files_cache`: optional files cache; if provided, the archive bytes are cached by URL -pub fn install_package( +pub async fn install_package( dist_url: &str, dist_type: &str, dist_shasum: Option<&str>, @@ -342,7 +337,7 @@ pub fn install_package( } fs::create_dir_all(&target)?; - let bytes = download_dist(dist_url, dist_shasum, progress, files_cache)?; + let bytes = download_dist(dist_url, dist_shasum, progress, files_cache).await?; match dist_type { "zip" => extract_zip(&bytes, &target)?, diff --git a/crates/mozart-registry/src/lockfile.rs b/crates/mozart-registry/src/lockfile.rs index 16337c4..9064109 100644 --- a/crates/mozart-registry/src/lockfile.rs +++ b/crates/mozart-registry/src/lockfile.rs @@ -392,11 +392,12 @@ fn extract_platform_requirements(requirements: &BTreeMap<String, String>) -> ser /// 2. Separates packages into production vs dev-only /// 3. Computes the content-hash /// 4. Assembles the complete `LockFile` struct -pub fn generate_lock_file(request: &LockFileGenerationRequest) -> anyhow::Result<LockFile> { +pub async fn generate_lock_file(request: &LockFileGenerationRequest) -> anyhow::Result<LockFile> { // 1. Fetch full metadata for all resolved packages let mut package_metadata: HashMap<String, PackagistVersion> = HashMap::new(); for pkg in &request.resolved_packages { - let versions = packagist::fetch_package_versions(&pkg.name, request.repo_cache.as_ref())?; + let versions = + packagist::fetch_package_versions(&pkg.name, request.repo_cache.as_ref()).await?; // Find the exact version matching pkg.version_normalized let matching = versions .into_iter() @@ -913,8 +914,8 @@ mod tests { assert_eq!(platform, serde_json::json!({})); } - #[test] - fn test_generate_lock_file_minimal() { + #[tokio::test] + async fn test_generate_lock_file_minimal() { let composer_json_content = r#"{"name": "test/project", "require": {"php": ">=8.1"}}"#.to_string(); let composer_json: RawPackageData = serde_json::from_str(&composer_json_content).unwrap(); @@ -927,7 +928,7 @@ mod tests { repo_cache: None, }; - let lock = generate_lock_file(&request).unwrap(); + let lock = generate_lock_file(&request).await.unwrap(); assert_eq!(lock.packages.len(), 0); assert_eq!(lock.packages_dev.as_ref().unwrap().len(), 0); @@ -1009,9 +1010,9 @@ mod tests { assert_eq!(packages[1].name, "vendor/zebra"); } - #[test] + #[tokio::test] #[ignore] - fn test_generate_lock_file_monolog() { + async fn test_generate_lock_file_monolog() { use crate::resolver::PlatformConfig; use crate::resolver::{ResolveRequest, resolve}; use mozart_core::package::Stability; @@ -1031,7 +1032,9 @@ mod tests { repo_cache: None, }; - let resolved = resolve(&resolve_request).expect("Resolution should succeed"); + let resolved = resolve(&resolve_request) + .await + .expect("Resolution should succeed"); assert!(!resolved.is_empty()); let composer_json_content = @@ -1046,7 +1049,9 @@ mod tests { repo_cache: None, }; - let lock = generate_lock_file(&gen_request).expect("Lock file generation should succeed"); + let lock = generate_lock_file(&gen_request) + .await + .expect("Lock file generation should succeed"); // Verify monolog is in packages assert!( diff --git a/crates/mozart-registry/src/packagist.rs b/crates/mozart-registry/src/packagist.rs index ba80e7e..95d02ad 100644 --- a/crates/mozart-registry/src/packagist.rs +++ b/crates/mozart-registry/src/packagist.rs @@ -128,7 +128,7 @@ pub fn parse_p2_response(json: &str, package_name: &str) -> anyhow::Result<Vec<P /// If `repo_cache` is provided, the JSON response is cached on disk under the /// key `"provider-{vendor}~{package}.json"`. Subsequent calls for the same /// package are served from cache without a network request. -pub fn fetch_package_versions( +pub async fn fetch_package_versions( package_name: &str, repo_cache: Option<&Cache>, ) -> anyhow::Result<Vec<PackagistVersion>> { @@ -144,7 +144,7 @@ pub fn fetch_package_versions( // Cache miss — fetch from Packagist let url = format!("https://repo.packagist.org/p2/{package_name}.json"); - let response = reqwest::blocking::get(&url)?; + let response = reqwest::get(&url).await?; if !response.status().is_success() { anyhow::bail!( @@ -153,7 +153,7 @@ pub fn fetch_package_versions( ); } - let body = response.text()?; + let body = response.text().await?; // Write to cache if let Some(cache) = repo_cache { @@ -209,11 +209,11 @@ fn url_encode(s: &str) -> String { /// /// Fetches up to `SEARCH_MAX_PAGES` pages of results and returns the full list. /// An optional `package_type` filter can narrow results (e.g. `"library"`). -pub fn search_packages( +pub async fn search_packages( query: &str, package_type: Option<&str>, ) -> anyhow::Result<(Vec<SearchResult>, u64)> { - let client = reqwest::blocking::Client::builder() + let client = reqwest::Client::builder() .user_agent("mozart/0.1.0") .build()?; @@ -224,11 +224,11 @@ pub fn search_packages( loop { let response: SearchResponse = if let Some(ref url) = next_url { - let resp = client.get(url).send()?; + let resp = client.get(url).send().await?; if !resp.status().is_success() { anyhow::bail!("Packagist search request failed (HTTP {})", resp.status()); } - resp.json()? + resp.json().await? } else { let encoded_query = url_encode(query); let mut url = format!("https://packagist.org/search.json?q={encoded_query}"); @@ -237,11 +237,11 @@ pub fn search_packages( url.push_str(&url_encode(t)); } - let resp = client.get(&url).send()?; + let resp = client.get(&url).send().await?; if !resp.status().is_success() { anyhow::bail!("Packagist search request failed (HTTP {})", resp.status()); } - resp.json()? + resp.json().await? }; if page == 1 { @@ -321,10 +321,10 @@ pub struct SecurityAdvisoriesResponse { /// /// If the package list is very large (500+), requests are batched in chunks of /// 500 names per request and the results are merged. -pub fn fetch_security_advisories( +pub async fn fetch_security_advisories( package_names: &[&str], ) -> anyhow::Result<BTreeMap<String, Vec<SecurityAdvisory>>> { - let client = reqwest::blocking::Client::builder() + let client = reqwest::Client::builder() .user_agent("mozart/0.1.0") .build()?; @@ -343,7 +343,8 @@ pub fn fetch_security_advisories( .post("https://packagist.org/api/security-advisories/") .header("Content-Type", "application/x-www-form-urlencoded") .body(body) - .send()?; + .send() + .await?; if !response.status().is_success() { anyhow::bail!( @@ -352,7 +353,7 @@ pub fn fetch_security_advisories( ); } - let parsed: SecurityAdvisoriesResponse = response.json()?; + let parsed: SecurityAdvisoriesResponse = response.json().await?; for (pkg_name, advisories) in parsed.advisories { if !advisories.is_empty() { diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs index eb4f6e5..84e6a96 100644 --- a/crates/mozart-registry/src/resolver.rs +++ b/crates/mozart-registry/src/resolver.rs @@ -586,6 +586,9 @@ struct VersionDependencies { /// pubgrub `DependencyProvider` that fetches package metadata from Packagist. pub struct MozartProvider { + /// Tokio runtime handle for calling async functions from sync trait methods. + handle: tokio::runtime::Handle, + /// Cache of fetched package metadata. Populated lazily from Packagist. package_cache: RefCell<HashMap<String, PackageVersions>>, @@ -632,13 +635,16 @@ impl MozartProvider { } // Fetch from Packagist (with optional on-disk repo cache) - let packagist_versions = packagist::fetch_package_versions( - package_name, - self.repo_cache.as_ref(), - ) - .map_err(|e| { - ResolverError::PackagistError(format!("Failed to fetch {}: {}", package_name, e)) - })?; + // Uses block_on because pubgrub's DependencyProvider trait is synchronous. + let packagist_versions = self + .handle + .block_on(packagist::fetch_package_versions( + package_name, + self.repo_cache.as_ref(), + )) + .map_err(|e| { + ResolverError::PackagistError(format!("Failed to fetch {}: {}", package_name, e)) + })?; // Convert and filter let mut versions = BTreeMap::new(); @@ -975,8 +981,8 @@ pub struct ResolvedPackage { /// /// Returns a list of resolved packages (excluding root and platform packages), /// or a human-readable error. -pub fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, ResolveError> { - // 1. Build root dependencies +pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, ResolveError> { + // 1. Build root dependencies (parsing is CPU-only, no async needed) let mut root_deps: Vec<(PackageName, ComposerVS)> = Vec::new(); let root_conflicts: Vec<(PackageName, ComposerVS)> = Vec::new(); @@ -1012,79 +1018,94 @@ pub fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, Resolve } } - // 2. Build the provider - let provider = MozartProvider { - package_cache: RefCell::new(HashMap::new()), - repo_cache: request.repo_cache.clone(), - platform_packages: request.platform.to_versions(), - minimum_stability: request.minimum_stability, - stability_flags: request.stability_flags.clone(), - prefer_stable: request.prefer_stable, - prefer_lowest: request.prefer_lowest, - root_dependencies: root_deps, - root_conflicts, - ignore_platform_reqs: request.ignore_platform_reqs, - ignore_platform_req_list: request.ignore_platform_req_list.clone(), - }; + // Capture the current tokio Handle so the provider can call async functions + // from within pubgrub's synchronous DependencyProvider trait methods. + let handle = tokio::runtime::Handle::current(); - // 3. Run pubgrub - let root = PackageName::root(); - let root_version = ComposerVersion::stable(0, 0, 0, 0); + // Clone data needed by spawn_blocking (which requires 'static) + let repo_cache = request.repo_cache.clone(); + let platform_packages = request.platform.to_versions(); + let minimum_stability = request.minimum_stability; + let stability_flags = request.stability_flags.clone(); + let prefer_stable = request.prefer_stable; + let prefer_lowest = request.prefer_lowest; + let ignore_platform_reqs = request.ignore_platform_reqs; + let ignore_platform_req_list = request.ignore_platform_req_list.clone(); - match pubgrub::resolve(&provider, root, root_version) { - Ok(solution) => { - // 4. Convert solution to ResolvedPackage list - let mut result = Vec::new(); - for (pkg, version) in solution { - // Skip root and platform packages - if pkg.is_root() || pkg.is_platform() { - continue; - } + // 2. Run pubgrub on a blocking thread (it is CPU-bound + uses block_on for I/O) + tokio::task::spawn_blocking(move || { + let provider = MozartProvider { + handle, + package_cache: RefCell::new(HashMap::new()), + repo_cache, + platform_packages, + minimum_stability, + stability_flags, + prefer_stable, + prefer_lowest, + root_dependencies: root_deps, + root_conflicts, + ignore_platform_reqs, + ignore_platform_req_list, + }; - // Look up the original version string from the cache - let cache = provider.package_cache.borrow(); - let (version_str, version_normalized) = if let Some(pvs) = cache.get(&pkg.0) { - if let Some(vd) = pvs.versions.get(&version) { - (vd.version_string.clone(), vd.version_normalized.clone()) + let root = PackageName::root(); + let root_version = ComposerVersion::stable(0, 0, 0, 0); + + match pubgrub::resolve(&provider, root, root_version) { + Ok(solution) => { + let mut result = Vec::new(); + for (pkg, version) in solution { + if pkg.is_root() || pkg.is_platform() { + continue; + } + + let cache = provider.package_cache.borrow(); + let (version_str, version_normalized) = if let Some(pvs) = cache.get(&pkg.0) { + if let Some(vd) = pvs.versions.get(&version) { + (vd.version_string.clone(), vd.version_normalized.clone()) + } else { + (version.to_string(), version.to_string()) + } } else { (version.to_string(), version.to_string()) - } - } else { - (version.to_string(), version.to_string()) - }; + }; - result.push(ResolvedPackage { - name: pkg.0.clone(), - version: version_str, - version_normalized, - is_dev: version.stability < STABILITY_ALPHA_BASE, - }); + result.push(ResolvedPackage { + name: pkg.0.clone(), + version: version_str, + version_normalized, + is_dev: version.stability < STABILITY_ALPHA_BASE, + }); + } + Ok(result) + } + Err(PubGrubError::NoSolution(mut derivation_tree)) => { + derivation_tree.collapse_no_versions(); + let report = DefaultStringReporter::report(&derivation_tree); + Err(ResolveError::NoSolution(report)) + } + Err(PubGrubError::ErrorRetrievingDependencies { + package, + version, + source, + }) => Err(ResolveError::DependencyFetchError(format!( + "Error retrieving dependencies for {} {}: {}", + package, version, source + ))), + Err(PubGrubError::ErrorChoosingVersion { package, source }) => { + Err(ResolveError::DependencyFetchError(format!( + "Error choosing version for {}: {}", + package, source + ))) + } + Err(PubGrubError::ErrorInShouldCancel(e)) => { + Err(ResolveError::Internal(format!("Resolver cancelled: {}", e))) } - Ok(result) - } - Err(PubGrubError::NoSolution(mut derivation_tree)) => { - derivation_tree.collapse_no_versions(); - let report = DefaultStringReporter::report(&derivation_tree); - Err(ResolveError::NoSolution(report)) - } - Err(PubGrubError::ErrorRetrievingDependencies { - package, - version, - source, - }) => Err(ResolveError::DependencyFetchError(format!( - "Error retrieving dependencies for {} {}: {}", - package, version, source - ))), - Err(PubGrubError::ErrorChoosingVersion { package, source }) => { - Err(ResolveError::DependencyFetchError(format!( - "Error choosing version for {}: {}", - package, source - ))) - } - Err(PubGrubError::ErrorInShouldCancel(e)) => { - Err(ResolveError::Internal(format!("Resolver cancelled: {}", e))) } - } + }) + .await + .unwrap() } // ───────────────────────────────────────────────────────────────────────────── @@ -1096,6 +1117,13 @@ mod tests { use super::*; use pubgrub::{OfflineDependencyProvider, Ranges}; + fn test_handle() -> tokio::runtime::Handle { + static RT: std::sync::OnceLock<tokio::runtime::Runtime> = std::sync::OnceLock::new(); + RT.get_or_init(|| tokio::runtime::Runtime::new().unwrap()) + .handle() + .clone() + } + // ──────────── ComposerVersion parsing ──────────── #[test] @@ -1532,6 +1560,7 @@ mod tests { #[test] fn test_stability_filter() { let provider = MozartProvider { + handle: test_handle(), package_cache: RefCell::new(HashMap::new()), platform_packages: HashMap::new(), minimum_stability: Stability::Stable, @@ -1585,6 +1614,7 @@ mod tests { #[test] fn test_stability_filter_beta() { let provider = MozartProvider { + handle: test_handle(), package_cache: RefCell::new(HashMap::new()), platform_packages: HashMap::new(), minimum_stability: Stability::Beta, @@ -1630,6 +1660,7 @@ mod tests { #[test] fn test_stability_filter_dev() { let provider = MozartProvider { + handle: test_handle(), package_cache: RefCell::new(HashMap::new()), platform_packages: HashMap::new(), minimum_stability: Stability::Dev, @@ -1656,6 +1687,7 @@ mod tests { #[test] fn test_skip_platform_dep() { let provider = MozartProvider { + handle: test_handle(), package_cache: RefCell::new(HashMap::new()), platform_packages: HashMap::new(), minimum_stability: Stability::Stable, @@ -1677,6 +1709,7 @@ mod tests { #[test] fn test_skip_specific_platform_dep() { let provider = MozartProvider { + handle: test_handle(), package_cache: RefCell::new(HashMap::new()), platform_packages: HashMap::new(), minimum_stability: Stability::Stable, @@ -1699,6 +1732,7 @@ mod tests { #[test] fn test_root_package_choose_version() { let provider = MozartProvider { + handle: test_handle(), package_cache: RefCell::new(HashMap::new()), platform_packages: HashMap::new(), minimum_stability: Stability::Stable, @@ -1726,6 +1760,7 @@ mod tests { platform.insert("php".to_string(), php_v); let provider = MozartProvider { + handle: test_handle(), package_cache: RefCell::new(HashMap::new()), platform_packages: platform, minimum_stability: Stability::Stable, @@ -1884,9 +1919,9 @@ mod tests { // ──────────── End-to-end tests (require network, marked #[ignore]) ──────────── - #[test] + #[tokio::test] #[ignore] - fn test_resolve_monolog_e2e() { + async fn test_resolve_monolog_e2e() { let request = ResolveRequest { require: vec![("monolog/monolog".to_string(), "^3.0".to_string())], require_dev: vec![], @@ -1901,7 +1936,7 @@ mod tests { repo_cache: None, }; - let result = resolve(&request); + let result = resolve(&request).await; match result { Ok(packages) => { println!("Resolved {} packages:", packages.len()); |
