From 15df8a820e83084506982ffc9cde8297243cb8a1 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Wed, 6 Mar 2024 23:56:24 +0100 Subject: [PATCH 01/57] wip --- crates/rattler_conda_types/src/channel/mod.rs | 15 + crates/rattler_repodata_gateway/Cargo.toml | 3 + crates/rattler_repodata_gateway/src/error.rs | 10 + .../rattler_repodata_gateway/src/gateway.rs | 452 ++++++++++++++++++ crates/rattler_repodata_gateway/src/lib.rs | 2 + .../src/sparse/mod.rs | 12 + .../src/utils/barrier_cell.rs | 83 ++++ .../rattler_repodata_gateway/src/utils/mod.rs | 2 + 8 files changed, 579 insertions(+) create mode 100644 crates/rattler_repodata_gateway/src/error.rs create mode 100644 crates/rattler_repodata_gateway/src/gateway.rs create mode 100644 crates/rattler_repodata_gateway/src/utils/barrier_cell.rs diff --git a/crates/rattler_conda_types/src/channel/mod.rs b/crates/rattler_conda_types/src/channel/mod.rs index 03b2197cf..6ef6302ab 100644 --- a/crates/rattler_conda_types/src/channel/mod.rs +++ b/crates/rattler_conda_types/src/channel/mod.rs @@ -160,6 +160,21 @@ impl Channel { } } + /// Constructs a channel from a directory path. + /// + /// # Panics + /// + /// Panics if the path is not a valid url. + pub fn from_directory(path: &Path) -> Self { + let path = absolute_path(path); + let url = Url::from_directory_path(&path).expect("path is a valid url"); + Self { + platforms: None, + base_url: url, + name: None, + } + } + /// Returns the name of the channel pub fn name(&self) -> &str { match self.base_url().scheme() { diff --git a/crates/rattler_repodata_gateway/Cargo.toml b/crates/rattler_repodata_gateway/Cargo.toml index 7ecd824a6..70b4ebeb3 100644 --- a/crates/rattler_repodata_gateway/Cargo.toml +++ b/crates/rattler_repodata_gateway/Cargo.toml @@ -11,10 +11,13 @@ license.workspace = true readme.workspace = true [dependencies] +async-trait = "0.1.77" async-compression = { workspace = true, features = ["gzip", "tokio", "bzip2", "zstd"] } blake2 = { workspace = true } cache_control = { workspace = true } chrono = { workspace = true, features = ["std", "serde", "alloc", "clock"] } +dashmap = "5.5.3" +elsa = "1.10.0" humansize = { workspace = true } humantime = { workspace = true } futures = { workspace = true } diff --git a/crates/rattler_repodata_gateway/src/error.rs b/crates/rattler_repodata_gateway/src/error.rs new file mode 100644 index 000000000..cc8fc3595 --- /dev/null +++ b/crates/rattler_repodata_gateway/src/error.rs @@ -0,0 +1,10 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum GatewayError { + #[error("{0}")] + IoError(String, #[source] std::io::Error), + + #[error("'{0}' is not a supported URI scheme")] + UnsupportedScheme(String), +} \ No newline at end of file diff --git a/crates/rattler_repodata_gateway/src/gateway.rs b/crates/rattler_repodata_gateway/src/gateway.rs new file mode 100644 index 000000000..fca28f31d --- /dev/null +++ b/crates/rattler_repodata_gateway/src/gateway.rs @@ -0,0 +1,452 @@ +use crate::{error::GatewayError, sparse::SparseRepoData, utils::BarrierCell}; +use dashmap::{mapref::entry::Entry, DashMap}; +use futures::stream::FuturesUnordered; +use futures::{select_biased, StreamExt}; +use itertools::Itertools; +use rattler_conda_types::{Channel, PackageName, Platform, RepoDataRecord}; +use std::borrow::Borrow; +use std::collections::HashSet; +use std::{ + path::Path, + sync::{Arc, Weak}, +}; +use tokio::{sync::broadcast, task::JoinError}; + +#[derive(Clone)] +struct Gateway { + inner: Arc, +} + +impl Gateway { + pub fn new() -> Self { + Self { + inner: Arc::default(), + } + } + + pub async fn load_records_recursive( + &self, + channels: CI, + platforms: PI, + names: N, + ) -> Result, GatewayError> + where + C: Borrow + Clone, + CI: IntoIterator, + PI: IntoIterator, + ::IntoIter: Clone, + N: IntoIterator, + PN: Into, + { + // Collect all the channels and platforms together + let channels_and_platforms = channels + .into_iter() + .cartesian_product(platforms.into_iter()) + .collect_vec(); + + // Create barrier cells for each subdirectory. This can be used to wait until the subdir + // becomes available. + let mut subdirs = Vec::with_capacity(channels_and_platforms.len()); + let mut pending_subdirs = FuturesUnordered::new(); + for (channel, platform) in channels_and_platforms.into_iter() { + // Create a barrier so work that need this subdir can await it. + let barrier = Arc::new(BarrierCell::new()); + subdirs.push(barrier.clone()); + + let inner = self.inner.clone(); + pending_subdirs.push(async move { + let subdir = inner + .get_or_create_subdir(channel.borrow(), platform) + .await?; + barrier.set(subdir).expect("subdir was set twice"); + Ok(()) + }); + } + + let mut pending_package_names = names.into_iter().map(Into::into).collect_vec(); + let mut seen = pending_package_names + .iter() + .cloned() + .collect::>(); + let mut pending_records = FuturesUnordered::new(); + let mut result = Vec::new(); + loop { + // Iterate over all pending package names and create futures to fetch them from all + // subdirs. + for pending_package_name in pending_package_names.drain(..) { + for subdir in subdirs.iter().cloned() { + let pending_package_name = pending_package_name.clone(); + pending_records.push(async move { + let barrier_cell = subdir.clone(); + let subdir = barrier_cell.wait().await; + subdir + .get_or_fetch_package_records(&pending_package_name) + .await + }); + } + } + + // Wait for the subdir to become available. + select_biased! { + // Handle any error that was emitted by the pending subdirs. + subdir_result = pending_subdirs.select_next_some() => { + if let Err(subdir_result) = subdir_result { + return Err(subdir_result); + } + } + // Handle any records that were fetched + records = pending_records.select_next_some() => { + let records = records?; + + // Extract the dependencies from the records + for record in records.iter() { + for dependency in &record.package_record.depends { + let dependency_name = PackageName::new_unchecked( + dependency.split_once(' ').unwrap_or((dependency, "")).0, + ); + if seen.insert(dependency_name.clone()) { + pending_package_names.push(dependency_name.clone()); + } + } + } + + // Add the records to the result + result.extend_from_slice(&records); + } + + // All futures have been handled + complete => { + break; + } + } + } + + Ok(result) + } +} + +#[derive(Default)] +struct GatewayInner { + /// A map of subdirectories for each channel and platform. + subdirs: DashMap<(Channel, Platform), PendingOrFetched>>, +} + +impl GatewayInner { + async fn get_or_create_subdir( + &self, + channel: &Channel, + platform: Platform, + ) -> Result, GatewayError> { + let sender = match self.subdirs.entry((channel.clone(), platform)) { + Entry::Vacant(entry) => { + // Construct a sender so other tasks can subscribe + let (sender, _) = broadcast::channel(1); + let sender = Arc::new(sender); + + // Modify the current entry to the pending entry, this is an atomic operation + // because who holds the entry holds mutable access. + entry.insert(PendingOrFetched::Pending(Arc::downgrade(&sender))); + + sender + } + Entry::Occupied(mut entry) => { + let subdir = entry.get(); + match subdir { + PendingOrFetched::Pending(sender) => { + let sender = sender.upgrade(); + + if let Some(sender) = sender { + // Explicitly drop the entry, so we don't block any other tasks. + drop(entry); + + // The sender is still active, so we can wait for the subdir to be + // created. + return match sender.subscribe().recv().await { + Ok(subdir) => Ok(subdir), + Err(_) => { + // If this happens the sender was dropped. + Err(GatewayError::IoError( + "a coalesced request failed".to_string(), + std::io::ErrorKind::Other.into(), + )) + } + }; + } else { + // Construct a sender so other tasks can subscribe + let (sender, _) = broadcast::channel(1); + let sender = Arc::new(sender); + + // Modify the current entry to the pending entry, this is an atomic + // operation because who holds the entry holds mutable access. + entry.insert(PendingOrFetched::Pending(Arc::downgrade(&sender))); + + sender + } + } + PendingOrFetched::Fetched(records) => return Ok(records.clone()), + } + } + }; + + // At this point we have exclusive write access to this specific entry. All other tasks + // will find a pending entry and will wait for the records to become available. + // + // Let's start by creating the subdir. If an error occurs we immediately return the error. + // This will drop the sender and all other waiting tasks will receive an error. + let subdir = Arc::new(self.create_subdir(channel, platform).await?); + + // Store the fetched files in the entry. + self.subdirs.insert( + (channel.clone(), platform), + PendingOrFetched::Fetched(subdir.clone()), + ); + + // Send the records to all waiting tasks. We don't care if there are no receivers, so we + // drop the error. + let _ = sender.send(subdir.clone()); + + Ok(subdir) + } + + async fn create_subdir( + &self, + channel: &Channel, + platform: Platform, + ) -> Result { + let url = channel.platform_url(platform); + if url.scheme() == "file" { + if let Ok(path) = url.to_file_path() { + return Ok(Subdir::from_client( + LocalSubdirClient::from_directory(&path).await?, + )); + } + } + + Err(GatewayError::UnsupportedScheme(url.scheme().to_string())) + } +} + +/// Represents a subdirectory of a repodata directory. +struct Subdir { + /// The client to use to fetch repodata. + client: Arc, + + /// Previously fetched or currently pending records. + records: DashMap>>, +} + +impl Subdir { + pub fn from_client(client: C) -> Self { + Self { + client: Arc::new(client), + records: Default::default(), + } + } + + pub async fn get_or_fetch_package_records( + &self, + name: &PackageName, + ) -> Result, GatewayError> { + let sender = match self.records.entry(name.clone()) { + Entry::Vacant(entry) => { + // Construct a sender so other tasks can subscribe + let (sender, _) = broadcast::channel(1); + let sender = Arc::new(sender); + + // Modify the current entry to the pending entry, this is an atomic operation + // because who holds the entry holds mutable access. + entry.insert(PendingOrFetched::Pending(Arc::downgrade(&sender))); + + sender + } + Entry::Occupied(mut entry) => { + let records = entry.get(); + match records { + PendingOrFetched::Pending(sender) => { + let sender = sender.upgrade(); + + if let Some(sender) = sender { + // Explicitly drop the entry, so we don't block any other tasks. + drop(entry); + + // The sender is still active, so we can wait for the records to be + // fetched. + return match sender.subscribe().recv().await { + Ok(records) => Ok(records), + Err(_) => { + // If this happens the sender was dropped. We simply have to + // retry. + Err(GatewayError::IoError( + "a coalesced request failed".to_string(), + std::io::ErrorKind::Other.into(), + )) + } + }; + } else { + // Construct a sender so other tasks can subscribe + let (sender, _) = broadcast::channel(1); + let sender = Arc::new(sender); + + // Modify the current entry to the pending entry, this is an atomic + // operation because who holds the entry holds mutable access. + entry.insert(PendingOrFetched::Pending(Arc::downgrade(&sender))); + + sender + } + } + PendingOrFetched::Fetched(records) => return Ok(records.clone()), + } + } + }; + + // At this point we have exclusive write access to this specific entry. All other tasks + // will find a pending entry and will wait for the records to become available. + // + // Let's start by fetching the records. If an error occurs we immediately return the error. + // This will drop the sender and all other waiting tasks will receive an error. + let records = match tokio::spawn({ + let client = self.client.clone(); + let name = name.clone(); + async move { client.fetch_package_records(&name).await } + }) + .await + .map_err(JoinError::try_into_panic) + { + Ok(Ok(records)) => records, + Ok(Err(err)) => return Err(GatewayError::from(err)), + Err(Ok(panic)) => std::panic::resume_unwind(panic), + Err(Err(_)) => { + return Err(GatewayError::IoError( + "fetching records was cancelled".to_string(), + std::io::ErrorKind::Interrupted.into(), + )); + } + }; + + // Store the fetched files in the entry. + self.records + .insert(name.clone(), PendingOrFetched::Fetched(records.clone())); + + // Send the records to all waiting tasks. We don't care if there are no receivers so we + // drop the error. + let _ = sender.send(records.clone()); + + Ok(records) + } +} + +/// A record that is either pending or has been fetched. +#[derive(Clone)] +enum PendingOrFetched { + Pending(Weak>), + Fetched(T), +} + +/// A client that can be used to fetch repodata for a specific subdirectory. +#[async_trait::async_trait] +trait SubdirClient: Send + Sync { + /// Fetches all repodata records for the package with the given name in a channel subdirectory. + async fn fetch_package_records( + &self, + name: &PackageName, + ) -> Result, GatewayError>; +} + +/// A client that can be used to fetch repodata for a specific subdirectory from a local directory. +struct LocalSubdirClient { + sparse: Arc, +} + +impl LocalSubdirClient { + pub async fn from_directory(subdir: &Path) -> Result { + let subdir_name = subdir + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + // Determine the channel from the directory path + let channel_dir = subdir.parent().unwrap_or(subdir); + let channel = Channel::from_directory(channel_dir); + + // Load the sparse repodata + let repodata_path = subdir.join("repodata.json"); + let sparse = match tokio::task::spawn_blocking(move || { + SparseRepoData::new(channel, subdir_name, &repodata_path, None).map_err(|err| { + GatewayError::IoError("failed to parse repodata.json".to_string(), err) + }) + }) + .await + .map_err(JoinError::try_into_panic) + { + Ok(result) => result?, + Err(Ok(panic)) => std::panic::resume_unwind(panic), + Err(_) => { + return Err(GatewayError::IoError( + "loading of the repodata was cancelled".to_string(), + std::io::ErrorKind::Interrupted.into(), + )); + } + }; + + Ok(Self { + sparse: Arc::new(sparse), + }) + } +} + +#[async_trait::async_trait] +impl SubdirClient for LocalSubdirClient { + async fn fetch_package_records( + &self, + name: &PackageName, + ) -> Result, GatewayError> { + let sparse_repodata = self.sparse.clone(); + let name = name.clone(); + match tokio::task::spawn_blocking(move || sparse_repodata.load_records(&name)) + .await + .map_err(JoinError::try_into_panic) + { + Ok(Ok(records)) => Ok(records.into()), + Ok(Err(err)) => Err(GatewayError::IoError( + "failed to extract repodata records from sparse repodata".to_string(), + err, + )), + Err(Ok(panic)) => std::panic::resume_unwind(panic), + Err(Err(_)) => Err(GatewayError::IoError( + "loading of the records was cancelled".to_string(), + std::io::ErrorKind::Interrupted.into(), + )), + } + } +} + +#[cfg(test)] +mod test { + use crate::gateway::Gateway; + use rattler_conda_types::{Channel, PackageName, Platform}; + use std::path::Path; + use std::str::FromStr; + + fn local_conda_forge() -> Channel { + Channel::from_directory( + &Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/channels/conda-forge"), + ) + } + + #[tokio::test] + async fn test_gateway() { + let gateway = Gateway::new(); + + let records = gateway + .load_records_recursive( + vec![local_conda_forge()], + vec![Platform::Linux64, Platform::NoArch], + vec![PackageName::from_str("rubin-env").unwrap()].into_iter(), + ) + .await + .unwrap(); + + assert_eq!(records.len(), 45060); + } +} diff --git a/crates/rattler_repodata_gateway/src/lib.rs b/crates/rattler_repodata_gateway/src/lib.rs index 9fabf6575..b712ae9fb 100644 --- a/crates/rattler_repodata_gateway/src/lib.rs +++ b/crates/rattler_repodata_gateway/src/lib.rs @@ -65,3 +65,5 @@ pub mod fetch; pub mod sparse; mod utils; +mod gateway; +mod error; diff --git a/crates/rattler_repodata_gateway/src/sparse/mod.rs b/crates/rattler_repodata_gateway/src/sparse/mod.rs index f6c79939a..e42901148 100644 --- a/crates/rattler_repodata_gateway/src/sparse/mod.rs +++ b/crates/rattler_repodata_gateway/src/sparse/mod.rs @@ -461,6 +461,18 @@ mod test { assert_eq!(total_records, 21732); } + #[tokio::test] + async fn test_sparse_rubin_env() { + let sparse_empty_data = load_sparse(["rubin-env"]).await; + + let total_records = sparse_empty_data + .iter() + .map(std::vec::Vec::len) + .sum::(); + + assert_eq!(total_records, 45060); + } + #[tokio::test] async fn test_sparse_numpy_dev() { let sparse_empty_data = load_sparse([ diff --git a/crates/rattler_repodata_gateway/src/utils/barrier_cell.rs b/crates/rattler_repodata_gateway/src/utils/barrier_cell.rs new file mode 100644 index 000000000..b6cf08f8e --- /dev/null +++ b/crates/rattler_repodata_gateway/src/utils/barrier_cell.rs @@ -0,0 +1,83 @@ +use std::{ + cell::UnsafeCell, + mem::MaybeUninit, + sync::atomic::{AtomicU8, Ordering}, +}; +use thiserror::Error; +use tokio::sync::{Notify}; + +/// A synchronization primitive that can be used to wait for a value to become available. +/// +/// The [`BarrierCell`] is initially empty, requesters can wait for a value to become available +/// using the `wait` method. Once a value is available, the `set` method can be used to set the +/// value in the cell. The `set` method can only be called once. If the `set` method is called +/// multiple times, it will return an error. When `set` is called all waiters will be notified. +pub struct BarrierCell { + state: AtomicU8, + value: UnsafeCell>, + notify: Notify, +} + +unsafe impl Sync for BarrierCell {} + +unsafe impl Send for BarrierCell {} + +#[repr(u8)] +enum BarrierCellState { + Uninitialized, + Initializing, + Initialized, +} + +impl Default for BarrierCell { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, Error)] +pub enum SetError { + #[error("cannot assign a BarrierCell twice")] + AlreadySet, +} + +impl BarrierCell { + /// Constructs a new instance. + pub fn new() -> Self { + Self { + state: AtomicU8::new(BarrierCellState::Uninitialized as u8), + value: UnsafeCell::new(MaybeUninit::uninit()), + notify: Notify::new(), + } + } + + /// Wait for a value to become available in the cell + pub async fn wait(&self) -> &T { + let notified = self.notify.notified(); + if self.state.load(Ordering::Acquire) != BarrierCellState::Initialized as u8 { + notified.await; + } + unsafe { (*self.value.get()).assume_init_ref() } + } + + /// Set the value in the cell, if the cell was already initialized this will return an error. + pub fn set(&self, value: T) -> Result<(), SetError> { + let state = self + .state + .fetch_max(BarrierCellState::Initializing as u8, Ordering::SeqCst); + + // If the state is larger than started writing, then either there is an active writer or + // the cell has already been initialized. + if state == BarrierCellState::Initialized as u8 { + return Err(SetError::AlreadySet); + } else { + unsafe { *self.value.get() = MaybeUninit::new(value) }; + self.state + .store(BarrierCellState::Initialized as u8, Ordering::Release); + + self.notify.notify_waiters(); + } + + Ok(()) + } +} diff --git a/crates/rattler_repodata_gateway/src/utils/mod.rs b/crates/rattler_repodata_gateway/src/utils/mod.rs index c0647dd91..e741eba82 100644 --- a/crates/rattler_repodata_gateway/src/utils/mod.rs +++ b/crates/rattler_repodata_gateway/src/utils/mod.rs @@ -2,6 +2,7 @@ pub use encoding::{AsyncEncoding, Encoding}; pub use flock::LockedFile; use std::fmt::Write; use url::Url; +pub use barrier_cell::BarrierCell; mod encoding; @@ -9,6 +10,7 @@ mod encoding; pub(crate) mod simple_channel_server; mod flock; +mod barrier_cell; /// Convert a URL to a cache filename pub(crate) fn url_to_cache_filename(url: &Url) -> String { From 7b2c25f516dc54c522bde4a8e3dccc5b095d790c Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Thu, 7 Mar 2024 10:37:04 +0100 Subject: [PATCH 02/57] docs: more docs --- .../rattler_repodata_gateway/src/gateway.rs | 60 +++++++++++++++---- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/crates/rattler_repodata_gateway/src/gateway.rs b/crates/rattler_repodata_gateway/src/gateway.rs index fca28f31d..7edb779e6 100644 --- a/crates/rattler_repodata_gateway/src/gateway.rs +++ b/crates/rattler_repodata_gateway/src/gateway.rs @@ -24,19 +24,36 @@ impl Gateway { } } - pub async fn load_records_recursive( + /// Recursively loads all repodata records for the given channels, platforms and package names. + /// + /// This function will asynchronously load the repodata from all subdirectories (combination of + /// channels and platforms) and recursively load all repodata records and the dependencies of + /// the those records. + /// + /// Most processing will happen on the background so downloading and parsing can happen + /// simultaneously. + /// + /// Repodata is cached by the [`Gateway`] so calling this function twice with the same channels + /// will not result in the repodata being fetched twice. + pub async fn load_records_recursive< + AsChannel, + ChannelIter, + PlatformIter, + PackageNameIter, + IntoPackageName, + >( &self, - channels: CI, - platforms: PI, - names: N, + channels: ChannelIter, + platforms: PlatformIter, + names: PackageNameIter, ) -> Result, GatewayError> where - C: Borrow + Clone, - CI: IntoIterator, - PI: IntoIterator, - ::IntoIter: Clone, - N: IntoIterator, - PN: Into, + AsChannel: Borrow + Clone, + ChannelIter: IntoIterator, + PlatformIter: IntoIterator, + ::IntoIter: Clone, + PackageNameIter: IntoIterator, + IntoPackageName: Into, { // Collect all the channels and platforms together let channels_and_platforms = channels @@ -63,13 +80,23 @@ impl Gateway { }); } + // Package names that we still need to fetch. let mut pending_package_names = names.into_iter().map(Into::into).collect_vec(); + + // Package names that we have or will issue requests for. let mut seen = pending_package_names .iter() .cloned() .collect::>(); + + // A list of futures to fetch the records for the pending package names. The main task + // awaits these futures. let mut pending_records = FuturesUnordered::new(); + + // The resulting list of repodata records. let mut result = Vec::new(); + + // Loop until all pending package names have been fetched. loop { // Iterate over all pending package names and create futures to fetch them from all // subdirs. @@ -94,11 +121,13 @@ impl Gateway { return Err(subdir_result); } } + // Handle any records that were fetched records = pending_records.select_next_some() => { let records = records?; - // Extract the dependencies from the records + // Extract the dependencies from the records and recursively add them to the + // list of package names that we need to fetch. for record in records.iter() { for dependency in &record.package_record.depends { let dependency_name = PackageName::new_unchecked( @@ -114,7 +143,8 @@ impl Gateway { result.extend_from_slice(&records); } - // All futures have been handled + // All futures have been handled, all subdirectories have been loaded and all + // repodata records have been fetched. complete => { break; } @@ -132,6 +162,12 @@ struct GatewayInner { } impl GatewayInner { + /// Returns the [`Subdir`] for the given channel and platform. This function will create the + /// [`Subdir`] if it does not exist yet, otherwise it will return the previously created subdir. + /// + /// If multiple threads request the same subdir their requests will be coalesced, and they will + /// all receive the same subdir. If an error occurs while creating the subdir all waiting tasks + /// will also return an error. async fn get_or_create_subdir( &self, channel: &Channel, From 38423496c74bb431378f9b227db57a134b7e2b30 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Thu, 7 Mar 2024 16:01:46 +0100 Subject: [PATCH 03/57] fix: refactor into files --- .../src/{ => gateway}/error.rs | 0 .../src/gateway/local_subdir.rs | 77 +++++++ .../src/{gateway.rs => gateway/mod.rs} | 203 ++---------------- .../src/gateway/subdir.rs | 116 ++++++++++ crates/rattler_repodata_gateway/src/lib.rs | 1 - 5 files changed, 210 insertions(+), 187 deletions(-) rename crates/rattler_repodata_gateway/src/{ => gateway}/error.rs (100%) create mode 100644 crates/rattler_repodata_gateway/src/gateway/local_subdir.rs rename crates/rattler_repodata_gateway/src/{gateway.rs => gateway/mod.rs} (61%) create mode 100644 crates/rattler_repodata_gateway/src/gateway/subdir.rs diff --git a/crates/rattler_repodata_gateway/src/error.rs b/crates/rattler_repodata_gateway/src/gateway/error.rs similarity index 100% rename from crates/rattler_repodata_gateway/src/error.rs rename to crates/rattler_repodata_gateway/src/gateway/error.rs diff --git a/crates/rattler_repodata_gateway/src/gateway/local_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/local_subdir.rs new file mode 100644 index 000000000..3a3be4c79 --- /dev/null +++ b/crates/rattler_repodata_gateway/src/gateway/local_subdir.rs @@ -0,0 +1,77 @@ +use std::sync::Arc; +use std::path::Path; +use rattler_conda_types::{Channel, PackageName, RepoDataRecord}; +use tokio::task::JoinError; +use crate::gateway::{GatewayError, SubdirClient}; +use crate::sparse::SparseRepoData; + +/// A client that can be used to fetch repodata for a specific subdirectory from a local directory. +/// +/// Use the [`LocalSubdirClient::from_directory`] function to create a new instance of this client. +pub struct LocalSubdirClient { + sparse: Arc, +} + +impl LocalSubdirClient { + pub async fn from_directory(subdir: &Path) -> Result { + let subdir_name = subdir + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + // Determine the channel from the directory path + let channel_dir = subdir.parent().unwrap_or(subdir); + let channel = Channel::from_directory(channel_dir); + + // Load the sparse repodata + let repodata_path = subdir.join("repodata.json"); + let sparse = match tokio::task::spawn_blocking(move || { + SparseRepoData::new(channel, subdir_name, &repodata_path, None).map_err(|err| { + GatewayError::IoError("failed to parse repodata.json".to_string(), err) + }) + }) + .await + .map_err(JoinError::try_into_panic) + { + Ok(result) => result?, + Err(Ok(panic)) => std::panic::resume_unwind(panic), + Err(_) => { + return Err(GatewayError::IoError( + "loading of the repodata was cancelled".to_string(), + std::io::ErrorKind::Interrupted.into(), + )); + } + }; + + Ok(Self { + sparse: Arc::new(sparse), + }) + } +} + +#[async_trait::async_trait] +impl SubdirClient for LocalSubdirClient { + async fn fetch_package_records( + &self, + name: &PackageName, + ) -> Result, GatewayError> { + let sparse_repodata = self.sparse.clone(); + let name = name.clone(); + match tokio::task::spawn_blocking(move || sparse_repodata.load_records(&name)) + .await + .map_err(JoinError::try_into_panic) + { + Ok(Ok(records)) => Ok(records.into()), + Ok(Err(err)) => Err(GatewayError::IoError( + "failed to extract repodata records from sparse repodata".to_string(), + err, + )), + Err(Ok(panic)) => std::panic::resume_unwind(panic), + Err(Err(_)) => Err(GatewayError::IoError( + "loading of the records was cancelled".to_string(), + std::io::ErrorKind::Interrupted.into(), + )), + } + } +} diff --git a/crates/rattler_repodata_gateway/src/gateway.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs similarity index 61% rename from crates/rattler_repodata_gateway/src/gateway.rs rename to crates/rattler_repodata_gateway/src/gateway/mod.rs index 7edb779e6..0bc6333d1 100644 --- a/crates/rattler_repodata_gateway/src/gateway.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -1,19 +1,28 @@ -use crate::{error::GatewayError, sparse::SparseRepoData, utils::BarrierCell}; +mod error; +mod local_subdir; +mod subdir; + +pub use error::GatewayError; + +use crate::utils::BarrierCell; use dashmap::{mapref::entry::Entry, DashMap}; -use futures::stream::FuturesUnordered; -use futures::{select_biased, StreamExt}; +use futures::{select_biased, stream::FuturesUnordered, StreamExt}; use itertools::Itertools; +use local_subdir::LocalSubdirClient; use rattler_conda_types::{Channel, PackageName, Platform, RepoDataRecord}; -use std::borrow::Borrow; -use std::collections::HashSet; use std::{ - path::Path, + borrow::Borrow, + collections::HashSet, sync::{Arc, Weak}, }; -use tokio::{sync::broadcast, task::JoinError}; +use subdir::Subdir; +use tokio::sync::broadcast; + +// TODO: Instead of using `Channel` it would be better if we could use just the base url. Maybe we +// can wrap that in a type. Mamba has the CondaUrl class. #[derive(Clone)] -struct Gateway { +pub struct Gateway { inner: Arc, } @@ -262,115 +271,6 @@ impl GatewayInner { } } -/// Represents a subdirectory of a repodata directory. -struct Subdir { - /// The client to use to fetch repodata. - client: Arc, - - /// Previously fetched or currently pending records. - records: DashMap>>, -} - -impl Subdir { - pub fn from_client(client: C) -> Self { - Self { - client: Arc::new(client), - records: Default::default(), - } - } - - pub async fn get_or_fetch_package_records( - &self, - name: &PackageName, - ) -> Result, GatewayError> { - let sender = match self.records.entry(name.clone()) { - Entry::Vacant(entry) => { - // Construct a sender so other tasks can subscribe - let (sender, _) = broadcast::channel(1); - let sender = Arc::new(sender); - - // Modify the current entry to the pending entry, this is an atomic operation - // because who holds the entry holds mutable access. - entry.insert(PendingOrFetched::Pending(Arc::downgrade(&sender))); - - sender - } - Entry::Occupied(mut entry) => { - let records = entry.get(); - match records { - PendingOrFetched::Pending(sender) => { - let sender = sender.upgrade(); - - if let Some(sender) = sender { - // Explicitly drop the entry, so we don't block any other tasks. - drop(entry); - - // The sender is still active, so we can wait for the records to be - // fetched. - return match sender.subscribe().recv().await { - Ok(records) => Ok(records), - Err(_) => { - // If this happens the sender was dropped. We simply have to - // retry. - Err(GatewayError::IoError( - "a coalesced request failed".to_string(), - std::io::ErrorKind::Other.into(), - )) - } - }; - } else { - // Construct a sender so other tasks can subscribe - let (sender, _) = broadcast::channel(1); - let sender = Arc::new(sender); - - // Modify the current entry to the pending entry, this is an atomic - // operation because who holds the entry holds mutable access. - entry.insert(PendingOrFetched::Pending(Arc::downgrade(&sender))); - - sender - } - } - PendingOrFetched::Fetched(records) => return Ok(records.clone()), - } - } - }; - - // At this point we have exclusive write access to this specific entry. All other tasks - // will find a pending entry and will wait for the records to become available. - // - // Let's start by fetching the records. If an error occurs we immediately return the error. - // This will drop the sender and all other waiting tasks will receive an error. - let records = match tokio::spawn({ - let client = self.client.clone(); - let name = name.clone(); - async move { client.fetch_package_records(&name).await } - }) - .await - .map_err(JoinError::try_into_panic) - { - Ok(Ok(records)) => records, - Ok(Err(err)) => return Err(GatewayError::from(err)), - Err(Ok(panic)) => std::panic::resume_unwind(panic), - Err(Err(_)) => { - return Err(GatewayError::IoError( - "fetching records was cancelled".to_string(), - std::io::ErrorKind::Interrupted.into(), - )); - } - }; - - // Store the fetched files in the entry. - self.records - .insert(name.clone(), PendingOrFetched::Fetched(records.clone())); - - // Send the records to all waiting tasks. We don't care if there are no receivers so we - // drop the error. - let _ = sender.send(records.clone()); - - Ok(records) - } -} - /// A record that is either pending or has been fetched. #[derive(Clone)] enum PendingOrFetched { @@ -388,75 +288,6 @@ trait SubdirClient: Send + Sync { ) -> Result, GatewayError>; } -/// A client that can be used to fetch repodata for a specific subdirectory from a local directory. -struct LocalSubdirClient { - sparse: Arc, -} - -impl LocalSubdirClient { - pub async fn from_directory(subdir: &Path) -> Result { - let subdir_name = subdir - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - // Determine the channel from the directory path - let channel_dir = subdir.parent().unwrap_or(subdir); - let channel = Channel::from_directory(channel_dir); - - // Load the sparse repodata - let repodata_path = subdir.join("repodata.json"); - let sparse = match tokio::task::spawn_blocking(move || { - SparseRepoData::new(channel, subdir_name, &repodata_path, None).map_err(|err| { - GatewayError::IoError("failed to parse repodata.json".to_string(), err) - }) - }) - .await - .map_err(JoinError::try_into_panic) - { - Ok(result) => result?, - Err(Ok(panic)) => std::panic::resume_unwind(panic), - Err(_) => { - return Err(GatewayError::IoError( - "loading of the repodata was cancelled".to_string(), - std::io::ErrorKind::Interrupted.into(), - )); - } - }; - - Ok(Self { - sparse: Arc::new(sparse), - }) - } -} - -#[async_trait::async_trait] -impl SubdirClient for LocalSubdirClient { - async fn fetch_package_records( - &self, - name: &PackageName, - ) -> Result, GatewayError> { - let sparse_repodata = self.sparse.clone(); - let name = name.clone(); - match tokio::task::spawn_blocking(move || sparse_repodata.load_records(&name)) - .await - .map_err(JoinError::try_into_panic) - { - Ok(Ok(records)) => Ok(records.into()), - Ok(Err(err)) => Err(GatewayError::IoError( - "failed to extract repodata records from sparse repodata".to_string(), - err, - )), - Err(Ok(panic)) => std::panic::resume_unwind(panic), - Err(Err(_)) => Err(GatewayError::IoError( - "loading of the records was cancelled".to_string(), - std::io::ErrorKind::Interrupted.into(), - )), - } - } -} - #[cfg(test)] mod test { use crate::gateway::Gateway; diff --git a/crates/rattler_repodata_gateway/src/gateway/subdir.rs b/crates/rattler_repodata_gateway/src/gateway/subdir.rs new file mode 100644 index 000000000..46500b1dd --- /dev/null +++ b/crates/rattler_repodata_gateway/src/gateway/subdir.rs @@ -0,0 +1,116 @@ +use super::GatewayError; +use crate::gateway::{PendingOrFetched, SubdirClient}; +use dashmap::mapref::entry::Entry; +use dashmap::DashMap; +use rattler_conda_types::{PackageName, RepoDataRecord}; +use std::sync::Arc; +use tokio::{sync::broadcast, task::JoinError}; + +/// Represents a subdirectory of a repodata directory. +pub struct Subdir { + /// The client to use to fetch repodata. + client: Arc, + + /// Previously fetched or currently pending records. + records: DashMap>>, +} + +impl Subdir { + pub fn from_client(client: C) -> Self { + Self { + client: Arc::new(client), + records: Default::default(), + } + } + + pub async fn get_or_fetch_package_records( + &self, + name: &PackageName, + ) -> Result, GatewayError> { + let sender = match self.records.entry(name.clone()) { + Entry::Vacant(entry) => { + // Construct a sender so other tasks can subscribe + let (sender, _) = broadcast::channel(1); + let sender = Arc::new(sender); + + // Modify the current entry to the pending entry, this is an atomic operation + // because who holds the entry holds mutable access. + entry.insert(PendingOrFetched::Pending(Arc::downgrade(&sender))); + + sender + } + Entry::Occupied(mut entry) => { + let records = entry.get(); + match records { + PendingOrFetched::Pending(sender) => { + let sender = sender.upgrade(); + + if let Some(sender) = sender { + // Explicitly drop the entry, so we don't block any other tasks. + drop(entry); + + // The sender is still active, so we can wait for the records to be + // fetched. + return match sender.subscribe().recv().await { + Ok(records) => Ok(records), + Err(_) => { + // If this happens the sender was dropped. We simply have to + // retry. + Err(GatewayError::IoError( + "a coalesced request failed".to_string(), + std::io::ErrorKind::Other.into(), + )) + } + }; + } else { + // Construct a sender so other tasks can subscribe + let (sender, _) = broadcast::channel(1); + let sender = Arc::new(sender); + + // Modify the current entry to the pending entry, this is an atomic + // operation because who holds the entry holds mutable access. + entry.insert(PendingOrFetched::Pending(Arc::downgrade(&sender))); + + sender + } + } + PendingOrFetched::Fetched(records) => return Ok(records.clone()), + } + } + }; + + // At this point we have exclusive write access to this specific entry. All other tasks + // will find a pending entry and will wait for the records to become available. + // + // Let's start by fetching the records. If an error occurs we immediately return the error. + // This will drop the sender and all other waiting tasks will receive an error. + let records = match tokio::spawn({ + let client = self.client.clone(); + let name = name.clone(); + async move { client.fetch_package_records(&name).await } + }) + .await + .map_err(JoinError::try_into_panic) + { + Ok(Ok(records)) => records, + Ok(Err(err)) => return Err(GatewayError::from(err)), + Err(Ok(panic)) => std::panic::resume_unwind(panic), + Err(Err(_)) => { + return Err(GatewayError::IoError( + "fetching records was cancelled".to_string(), + std::io::ErrorKind::Interrupted.into(), + )); + } + }; + + // Store the fetched files in the entry. + self.records + .insert(name.clone(), PendingOrFetched::Fetched(records.clone())); + + // Send the records to all waiting tasks. We don't care if there are no receivers so we + // drop the error. + let _ = sender.send(records.clone()); + + Ok(records) + } +} diff --git a/crates/rattler_repodata_gateway/src/lib.rs b/crates/rattler_repodata_gateway/src/lib.rs index b712ae9fb..ad96cd3f8 100644 --- a/crates/rattler_repodata_gateway/src/lib.rs +++ b/crates/rattler_repodata_gateway/src/lib.rs @@ -66,4 +66,3 @@ pub mod sparse; mod utils; mod gateway; -mod error; From f8f2de2421d5ca0df63bf6e86aa88bd410031d08 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Thu, 7 Mar 2024 22:28:41 +0100 Subject: [PATCH 04/57] fix: works with missing subdirs --- crates/rattler_conda_types/src/channel/mod.rs | 21 ++- crates/rattler_repodata_gateway/Cargo.toml | 3 + .../rattler_repodata_gateway/src/fetch/mod.rs | 2 +- .../src/{utils => gateway}/barrier_cell.rs | 2 +- .../src/gateway/channel_config.rs | 51 +++++ .../src/gateway/error.rs | 10 +- .../src/gateway/local_subdir.rs | 54 ++++-- .../src/gateway/mod.rs | 176 +++++++++++++++--- .../src/gateway/remote_subdir.rs | 58 ++++++ .../src/gateway/subdir.rs | 26 ++- crates/rattler_repodata_gateway/src/lib.rs | 8 +- .../rattler_repodata_gateway/src/utils/mod.rs | 2 - .../src/utils/simple_channel_server.rs | 6 + 13 files changed, 352 insertions(+), 67 deletions(-) rename crates/rattler_repodata_gateway/src/{utils => gateway}/barrier_cell.rs (98%) create mode 100644 crates/rattler_repodata_gateway/src/gateway/channel_config.rs create mode 100644 crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs diff --git a/crates/rattler_conda_types/src/channel/mod.rs b/crates/rattler_conda_types/src/channel/mod.rs index 6ef6302ab..5762f28bb 100644 --- a/crates/rattler_conda_types/src/channel/mod.rs +++ b/crates/rattler_conda_types/src/channel/mod.rs @@ -64,7 +64,7 @@ impl Channel { let channel = if parse_scheme(channel).is_some() { let url = Url::parse(channel)?; - Channel::from_url(url, platforms, config) + Channel::from_url(url, platforms.into_iter().flatten()) } else if is_path(channel) { let path = PathBuf::from(channel); @@ -90,11 +90,7 @@ impl Channel { } /// Constructs a new [`Channel`] from a `Url` and associated platforms. - pub fn from_url( - url: Url, - platforms: Option>>, - _config: &ChannelConfig, - ) -> Self { + pub fn from_url(url: Url, platforms: impl IntoIterator) -> Self { // Get the path part of the URL but trim the directory suffix let path = url.path().trim_end_matches('/'); @@ -114,11 +110,18 @@ impl Channel { // Case 4: custom_channels matches // Case 5: channel_alias match + let mut platforms = platforms.into_iter().peekable(); + let platforms = if platforms.peek().is_none() { + None + } else { + Some(platforms.collect()) + }; + if base_url.has_host() { // Case 7: Fallback let name = path.trim_start_matches('/'); Self { - platforms: platforms.map(Into::into), + platforms, name: (!name.is_empty()).then_some(name).map(str::to_owned), base_url, } @@ -128,7 +131,7 @@ impl Channel { .rsplit_once('/') .map_or_else(|| base_url.path(), |(_, path_part)| path_part); Self { - platforms: platforms.map(Into::into), + platforms, name: (!name.is_empty()).then_some(name).map(str::to_owned), base_url, } @@ -167,7 +170,7 @@ impl Channel { /// Panics if the path is not a valid url. pub fn from_directory(path: &Path) -> Self { let path = absolute_path(path); - let url = Url::from_directory_path(&path).expect("path is a valid url"); + let url = Url::from_directory_path(path).expect("path is a valid url"); Self { platforms: None, base_url: url, diff --git a/crates/rattler_repodata_gateway/Cargo.toml b/crates/rattler_repodata_gateway/Cargo.toml index 5d9255e96..373169dcb 100644 --- a/crates/rattler_repodata_gateway/Cargo.toml +++ b/crates/rattler_repodata_gateway/Cargo.toml @@ -34,6 +34,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } pin-project-lite = { workspace = true } md-5 = { workspace = true } +dirs = { workspace = true } rattler_digest = { path="../rattler_digest", version = "0.19.1", default-features = false, features = ["tokio", "serde"] } rattler_conda_types = { path="../rattler_conda_types", version = "0.20.0", default-features = false, optional = true } fxhash = { workspace = true, optional = true } @@ -61,9 +62,11 @@ rstest = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tower-http = { workspace = true, features = ["fs", "compression-gzip", "trace"] } tracing-test = { workspace = true } +rattler_conda_types = { path="../rattler_conda_types", version = "0.20.0", default-features = false } [features] default = ['native-tls'] native-tls = ['reqwest/native-tls'] rustls-tls = ['reqwest/rustls-tls'] sparse = ["rattler_conda_types", "memmap2", "ouroboros", "superslice", "itertools", "serde_json/raw_value"] +gateway = ["sparse"] \ No newline at end of file diff --git a/crates/rattler_repodata_gateway/src/fetch/mod.rs b/crates/rattler_repodata_gateway/src/fetch/mod.rs index b7b371691..f25911b7d 100644 --- a/crates/rattler_repodata_gateway/src/fetch/mod.rs +++ b/crates/rattler_repodata_gateway/src/fetch/mod.rs @@ -36,7 +36,7 @@ pub enum RepoDataNotFoundError { /// There was a file system error #[error(transparent)] - FileSystemError(std::io::Error), + FileSystemError(#[from] std::io::Error), } #[allow(missing_docs)] diff --git a/crates/rattler_repodata_gateway/src/utils/barrier_cell.rs b/crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs similarity index 98% rename from crates/rattler_repodata_gateway/src/utils/barrier_cell.rs rename to crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs index b6cf08f8e..a01140ec0 100644 --- a/crates/rattler_repodata_gateway/src/utils/barrier_cell.rs +++ b/crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs @@ -4,7 +4,7 @@ use std::{ sync::atomic::{AtomicU8, Ordering}, }; use thiserror::Error; -use tokio::sync::{Notify}; +use tokio::sync::Notify; /// A synchronization primitive that can be used to wait for a value to become available. /// diff --git a/crates/rattler_repodata_gateway/src/gateway/channel_config.rs b/crates/rattler_repodata_gateway/src/gateway/channel_config.rs new file mode 100644 index 000000000..e9affbb42 --- /dev/null +++ b/crates/rattler_repodata_gateway/src/gateway/channel_config.rs @@ -0,0 +1,51 @@ +use crate::fetch::CacheAction; +use rattler_conda_types::Channel; +use std::collections::HashMap; + +/// Describes additional properties that influence how the gateway fetches repodata for a specific +/// channel. +#[derive(Debug, Clone)] +pub struct SourceConfig { + /// When enabled repodata can be fetched incrementally using JLAP (defaults to true) + pub jlap_enabled: bool, + + /// When enabled, the zstd variant will be used if available (defaults to true) + pub zstd_enabled: bool, + + /// When enabled, the bz2 variant will be used if available (defaults to true) + pub bz2_enabled: bool, + + /// Describes fetching repodata from a channel should interact with any + /// caches. + pub cache_action: CacheAction, +} + +impl Default for SourceConfig { + fn default() -> Self { + Self { + jlap_enabled: true, + zstd_enabled: true, + bz2_enabled: true, + cache_action: CacheAction::default(), + } + } +} + +/// Describes additional information for fetching channels. +#[derive(Debug, Default)] +pub struct ChannelConfig { + /// The default source configuration. If a channel does not have a specific source configuration + /// this configuration will be used. + pub default: SourceConfig, + + /// Describes per channel properties that influence how the gateway fetches repodata. + pub per_channel: HashMap, +} + +impl ChannelConfig { + /// Returns the source configuration for the given channel. If the channel does not have a + /// specific source configuration the default source configuration will be returned. + pub fn get(&self, channel: &Channel) -> &SourceConfig { + self.per_channel.get(channel).unwrap_or(&self.default) + } +} diff --git a/crates/rattler_repodata_gateway/src/gateway/error.rs b/crates/rattler_repodata_gateway/src/gateway/error.rs index cc8fc3595..a99c87eb7 100644 --- a/crates/rattler_repodata_gateway/src/gateway/error.rs +++ b/crates/rattler_repodata_gateway/src/gateway/error.rs @@ -1,3 +1,4 @@ +use crate::fetch::FetchRepoDataError; use thiserror::Error; #[derive(Debug, Error)] @@ -5,6 +6,9 @@ pub enum GatewayError { #[error("{0}")] IoError(String, #[source] std::io::Error), - #[error("'{0}' is not a supported URI scheme")] - UnsupportedScheme(String), -} \ No newline at end of file + #[error(transparent)] + FetchRepoDataError(#[from] FetchRepoDataError), + + #[error("{0}")] + UnsupportedUrl(String), +} diff --git a/crates/rattler_repodata_gateway/src/gateway/local_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/local_subdir.rs index 3a3be4c79..017327084 100644 --- a/crates/rattler_repodata_gateway/src/gateway/local_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/local_subdir.rs @@ -1,9 +1,11 @@ -use std::sync::Arc; -use std::path::Path; +use crate::fetch::FetchRepoDataError; +use crate::gateway::subdir::SubdirClient; +use crate::gateway::GatewayError; +use crate::sparse::SparseRepoData; use rattler_conda_types::{Channel, PackageName, RepoDataRecord}; +use std::path::Path; +use std::sync::Arc; use tokio::task::JoinError; -use crate::gateway::{GatewayError, SubdirClient}; -use crate::sparse::SparseRepoData; /// A client that can be used to fetch repodata for a specific subdirectory from a local directory. /// @@ -13,22 +15,20 @@ pub struct LocalSubdirClient { } impl LocalSubdirClient { - pub async fn from_directory(subdir: &Path) -> Result { - let subdir_name = subdir - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - // Determine the channel from the directory path - let channel_dir = subdir.parent().unwrap_or(subdir); - let channel = Channel::from_directory(channel_dir); - - // Load the sparse repodata - let repodata_path = subdir.join("repodata.json"); + pub async fn from_channel_subdir( + repodata_path: &Path, + channel: Channel, + subdir: &str, + ) -> Result { + let repodata_path = repodata_path.to_path_buf(); + let subdir = subdir.to_string(); let sparse = match tokio::task::spawn_blocking(move || { - SparseRepoData::new(channel, subdir_name, &repodata_path, None).map_err(|err| { - GatewayError::IoError("failed to parse repodata.json".to_string(), err) + SparseRepoData::new(channel, subdir, &repodata_path, None).map_err(|err| { + if err.kind() == std::io::ErrorKind::NotFound { + GatewayError::FetchRepoDataError(FetchRepoDataError::NotFound(err.into())) + } else { + GatewayError::IoError("failed to parse repodata.json".to_string(), err) + } }) }) .await @@ -48,6 +48,22 @@ impl LocalSubdirClient { sparse: Arc::new(sparse), }) } + + pub async fn from_directory(subdir: &Path) -> Result { + let subdir_name = subdir + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + // Determine the channel from the directory path + let channel_dir = subdir.parent().unwrap_or(subdir); + let channel = Channel::from_directory(channel_dir); + + // Load the sparse repodata + let repodata_path = subdir.join("repodata.json"); + Self::from_channel_subdir(&repodata_path, channel, &subdir_name).await + } } #[async_trait::async_trait] diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index 0bc6333d1..ab466ec13 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -1,36 +1,102 @@ +mod barrier_cell; +mod channel_config; mod error; mod local_subdir; +mod remote_subdir; mod subdir; +pub use barrier_cell::BarrierCell; +pub use channel_config::{ChannelConfig, SourceConfig}; pub use error::GatewayError; -use crate::utils::BarrierCell; +use crate::fetch::FetchRepoDataError; use dashmap::{mapref::entry::Entry, DashMap}; use futures::{select_biased, stream::FuturesUnordered, StreamExt}; use itertools::Itertools; use local_subdir::LocalSubdirClient; use rattler_conda_types::{Channel, PackageName, Platform, RepoDataRecord}; +use reqwest::Client; +use reqwest_middleware::ClientWithMiddleware; use std::{ borrow::Borrow, collections::HashSet, + path::PathBuf, sync::{Arc, Weak}, }; -use subdir::Subdir; +use subdir::{Subdir, SubdirData}; use tokio::sync::broadcast; // TODO: Instead of using `Channel` it would be better if we could use just the base url. Maybe we // can wrap that in a type. Mamba has the CondaUrl class. +#[derive(Default)] +pub struct GatewayBuilder { + channel_config: ChannelConfig, + client: Option, + cache: Option, +} + +impl GatewayBuilder { + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn with_client(mut self, client: ClientWithMiddleware) -> Self { + self.client = Some(client); + self + } + + #[must_use] + pub fn with_channel_config(mut self, channel_config: ChannelConfig) -> Self { + self.channel_config = channel_config; + self + } + + #[must_use] + pub fn with_cache_dir(mut self, cache: impl Into) -> Self { + self.cache = Some(cache.into()); + self + } + + /// Finish the construction of the gateway returning a constructed gateway. + pub fn finish(self) -> Gateway { + let client = self + .client + .unwrap_or_else(|| ClientWithMiddleware::from(Client::new())); + + let cache = self.cache.unwrap_or_else(|| { + dirs::cache_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("rattler/cache") + }); + + Gateway { + inner: Arc::new(GatewayInner { + subdirs: Default::default(), + client, + channel_config: self.channel_config, + cache, + }), + } + } +} + #[derive(Clone)] pub struct Gateway { inner: Arc, } impl Gateway { + /// Constructs a simple gateway with the default configuration. Use [`Gateway::builder`] if you + /// want more control over how the gateway is constructed. pub fn new() -> Self { - Self { - inner: Arc::default(), - } + Gateway::builder().finish() + } + + /// Constructs a new gateway with the given client and channel configuration. + pub fn builder() -> GatewayBuilder { + GatewayBuilder::default() } /// Recursively loads all repodata records for the given channels, platforms and package names. @@ -115,9 +181,14 @@ impl Gateway { pending_records.push(async move { let barrier_cell = subdir.clone(); let subdir = barrier_cell.wait().await; - subdir - .get_or_fetch_package_records(&pending_package_name) - .await + match subdir.as_ref() { + Subdir::Found(subdir) => { + subdir + .get_or_fetch_package_records(&pending_package_name) + .await + } + Subdir::NotFound => Ok(Arc::from(vec![])), + } }); } } @@ -164,10 +235,18 @@ impl Gateway { } } -#[derive(Default)] struct GatewayInner { /// A map of subdirectories for each channel and platform. subdirs: DashMap<(Channel, Platform), PendingOrFetched>>, + + /// The client to use to fetch repodata. + client: ClientWithMiddleware, + + /// The channel configuration + channel_config: ChannelConfig, + + /// The directory to store any cache + cache: PathBuf, } impl GatewayInner { @@ -259,15 +338,44 @@ impl GatewayInner { platform: Platform, ) -> Result { let url = channel.platform_url(platform); - if url.scheme() == "file" { + let subdir_data = if url.scheme() == "file" { if let Ok(path) = url.to_file_path() { - return Ok(Subdir::from_client( - LocalSubdirClient::from_directory(&path).await?, + LocalSubdirClient::from_directory(&path) + .await + .map(SubdirData::from_client) + } else { + return Err(GatewayError::UnsupportedUrl( + "unsupported file based url".to_string(), )); } - } + } else if url.scheme() == "http" || url.scheme() == "https" { + remote_subdir::RemoteSubdirClient::new( + channel.clone(), + platform, + self.client.clone(), + self.cache.clone(), + self.channel_config.get(channel).clone(), + ) + .await + .map(SubdirData::from_client) + } else { + return Err(GatewayError::UnsupportedUrl(format!( + "'{}' is not a supported scheme", + url.scheme() + ))); + }; - Err(GatewayError::UnsupportedScheme(url.scheme().to_string())) + match subdir_data { + Ok(client) => Ok(Subdir::Found(client)), + Err(GatewayError::FetchRepoDataError(FetchRepoDataError::NotFound(_))) + if platform != Platform::NoArch => + { + // If the subdir was not found and the platform is not `noarch` we assume its just + // empty. + Ok(Subdir::NotFound) + } + Err(err) => Err(err), + } } } @@ -278,19 +386,10 @@ enum PendingOrFetched { Fetched(T), } -/// A client that can be used to fetch repodata for a specific subdirectory. -#[async_trait::async_trait] -trait SubdirClient: Send + Sync { - /// Fetches all repodata records for the package with the given name in a channel subdirectory. - async fn fetch_package_records( - &self, - name: &PackageName, - ) -> Result, GatewayError>; -} - #[cfg(test)] mod test { use crate::gateway::Gateway; + use crate::utils::simple_channel_server::SimpleChannelServer; use rattler_conda_types::{Channel, PackageName, Platform}; use std::path::Path; use std::str::FromStr; @@ -301,14 +400,39 @@ mod test { ) } + async fn remote_conda_forge() -> SimpleChannelServer { + SimpleChannelServer::new( + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/channels/conda-forge"), + ) + .await + } + #[tokio::test] - async fn test_gateway() { + async fn test_local_gateway() { let gateway = Gateway::new(); let records = gateway .load_records_recursive( vec![local_conda_forge()], - vec![Platform::Linux64, Platform::NoArch], + vec![Platform::Linux64, Platform::Win32, Platform::NoArch], + vec![PackageName::from_str("rubin-env").unwrap()].into_iter(), + ) + .await + .unwrap(); + + assert_eq!(records.len(), 45060); + } + + #[tokio::test] + async fn test_remote_gateway() { + let gateway = Gateway::new(); + + let index = remote_conda_forge().await; + + let records = gateway + .load_records_recursive( + vec![index.channel()], + vec![Platform::Linux64, Platform::Win32, Platform::NoArch], vec![PackageName::from_str("rubin-env").unwrap()].into_iter(), ) .await diff --git a/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs new file mode 100644 index 000000000..d4aef2488 --- /dev/null +++ b/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs @@ -0,0 +1,58 @@ +use super::{local_subdir::LocalSubdirClient, GatewayError, SourceConfig}; +use crate::fetch::{fetch_repo_data, FetchRepoDataOptions}; +use crate::gateway::subdir::SubdirClient; +use rattler_conda_types::{Channel, PackageName, Platform, RepoDataRecord}; +use reqwest_middleware::ClientWithMiddleware; +use std::{path::PathBuf, sync::Arc}; + +pub struct RemoteSubdirClient { + sparse: LocalSubdirClient, +} + +impl RemoteSubdirClient { + pub async fn new( + channel: Channel, + platform: Platform, + client: ClientWithMiddleware, + cache_dir: PathBuf, + source_config: SourceConfig, + ) -> Result { + let subdir_url = channel.platform_url(platform); + + // Fetch the repodata from the remote server + let repodata = fetch_repo_data( + subdir_url, + client, + cache_dir, + FetchRepoDataOptions { + cache_action: source_config.cache_action, + variant: Default::default(), + jlap_enabled: source_config.jlap_enabled, + zstd_enabled: source_config.zstd_enabled, + bz2_enabled: source_config.bz2_enabled, + }, + None, + ) + .await?; + + // Create a new sparse repodata client that can be used to read records from the repodata. + let sparse = LocalSubdirClient::from_channel_subdir( + &repodata.repo_data_json_path, + channel.clone(), + platform.as_str(), + ) + .await?; + + Ok(Self { sparse }) + } +} + +#[async_trait::async_trait] +impl SubdirClient for RemoteSubdirClient { + async fn fetch_package_records( + &self, + name: &PackageName, + ) -> Result, GatewayError> { + self.sparse.fetch_package_records(name).await + } +} diff --git a/crates/rattler_repodata_gateway/src/gateway/subdir.rs b/crates/rattler_repodata_gateway/src/gateway/subdir.rs index 46500b1dd..a8dc1e4f5 100644 --- a/crates/rattler_repodata_gateway/src/gateway/subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/subdir.rs @@ -1,13 +1,21 @@ use super::GatewayError; -use crate::gateway::{PendingOrFetched, SubdirClient}; +use crate::gateway::PendingOrFetched; use dashmap::mapref::entry::Entry; use dashmap::DashMap; use rattler_conda_types::{PackageName, RepoDataRecord}; use std::sync::Arc; use tokio::{sync::broadcast, task::JoinError}; -/// Represents a subdirectory of a repodata directory. -pub struct Subdir { +pub enum Subdir { + /// The subdirectory is missing from the channel, it is considered empty. + NotFound, + + /// A subdirectory and the data associated with it. + Found(SubdirData), +} + +/// Fetches and caches repodata records by package name for a specific subdirectory of a channel. +pub struct SubdirData { /// The client to use to fetch repodata. client: Arc, @@ -15,7 +23,7 @@ pub struct Subdir { records: DashMap>>, } -impl Subdir { +impl SubdirData { pub fn from_client(client: C) -> Self { Self { client: Arc::new(client), @@ -114,3 +122,13 @@ impl Subdir { Ok(records) } } + +/// A client that can be used to fetch repodata for a specific subdirectory. +#[async_trait::async_trait] +pub trait SubdirClient: Send + Sync { + /// Fetches all repodata records for the package with the given name in a channel subdirectory. + async fn fetch_package_records( + &self, + name: &PackageName, + ) -> Result, GatewayError>; +} diff --git a/crates/rattler_repodata_gateway/src/lib.rs b/crates/rattler_repodata_gateway/src/lib.rs index ad96cd3f8..b12c8941b 100644 --- a/crates/rattler_repodata_gateway/src/lib.rs +++ b/crates/rattler_repodata_gateway/src/lib.rs @@ -49,7 +49,7 @@ //! let result = match result { //! Err(err) => { //! panic!("{:?}", err); -//! }, +//! } //! Ok(result) => result //! }; //! @@ -63,6 +63,10 @@ pub mod fetch; #[cfg(feature = "sparse")] pub mod sparse; - mod utils; + +#[cfg(feature = "gateway")] mod gateway; + +#[cfg(feature = "gateway")] +pub use gateway::{ChannelConfig, Gateway, GatewayBuilder, GatewayError, SourceConfig}; diff --git a/crates/rattler_repodata_gateway/src/utils/mod.rs b/crates/rattler_repodata_gateway/src/utils/mod.rs index e741eba82..c0647dd91 100644 --- a/crates/rattler_repodata_gateway/src/utils/mod.rs +++ b/crates/rattler_repodata_gateway/src/utils/mod.rs @@ -2,7 +2,6 @@ pub use encoding::{AsyncEncoding, Encoding}; pub use flock::LockedFile; use std::fmt::Write; use url::Url; -pub use barrier_cell::BarrierCell; mod encoding; @@ -10,7 +9,6 @@ mod encoding; pub(crate) mod simple_channel_server; mod flock; -mod barrier_cell; /// Convert a URL to a cache filename pub(crate) fn url_to_cache_filename(url: &Url) -> String { diff --git a/crates/rattler_repodata_gateway/src/utils/simple_channel_server.rs b/crates/rattler_repodata_gateway/src/utils/simple_channel_server.rs index 8b4e51fe8..7463586c3 100644 --- a/crates/rattler_repodata_gateway/src/utils/simple_channel_server.rs +++ b/crates/rattler_repodata_gateway/src/utils/simple_channel_server.rs @@ -1,5 +1,7 @@ use axum::routing::get_service; +use rattler_conda_types::Channel; use std::future::IntoFuture; +use std::iter; use std::net::SocketAddr; use std::path::Path; use tokio::sync::oneshot; @@ -16,6 +18,10 @@ impl SimpleChannelServer { pub fn url(&self) -> Url { Url::parse(&format!("http://localhost:{}", self.local_addr.port())).unwrap() } + + pub fn channel(&self) -> Channel { + Channel::from_url(self.url(), iter::empty()) + } } impl SimpleChannelServer { From cd041f37864348650a6a299474bddc6606aeeddb Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Sat, 9 Mar 2024 10:35:57 +0100 Subject: [PATCH 05/57] feat: remove unused file --- .../src/gateway/barrier_cell.rs | 83 ------------------- 1 file changed, 83 deletions(-) delete mode 100644 crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs diff --git a/crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs b/crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs deleted file mode 100644 index a01140ec0..000000000 --- a/crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::{ - cell::UnsafeCell, - mem::MaybeUninit, - sync::atomic::{AtomicU8, Ordering}, -}; -use thiserror::Error; -use tokio::sync::Notify; - -/// A synchronization primitive that can be used to wait for a value to become available. -/// -/// The [`BarrierCell`] is initially empty, requesters can wait for a value to become available -/// using the `wait` method. Once a value is available, the `set` method can be used to set the -/// value in the cell. The `set` method can only be called once. If the `set` method is called -/// multiple times, it will return an error. When `set` is called all waiters will be notified. -pub struct BarrierCell { - state: AtomicU8, - value: UnsafeCell>, - notify: Notify, -} - -unsafe impl Sync for BarrierCell {} - -unsafe impl Send for BarrierCell {} - -#[repr(u8)] -enum BarrierCellState { - Uninitialized, - Initializing, - Initialized, -} - -impl Default for BarrierCell { - fn default() -> Self { - Self::new() - } -} - -#[derive(Debug, Clone, Error)] -pub enum SetError { - #[error("cannot assign a BarrierCell twice")] - AlreadySet, -} - -impl BarrierCell { - /// Constructs a new instance. - pub fn new() -> Self { - Self { - state: AtomicU8::new(BarrierCellState::Uninitialized as u8), - value: UnsafeCell::new(MaybeUninit::uninit()), - notify: Notify::new(), - } - } - - /// Wait for a value to become available in the cell - pub async fn wait(&self) -> &T { - let notified = self.notify.notified(); - if self.state.load(Ordering::Acquire) != BarrierCellState::Initialized as u8 { - notified.await; - } - unsafe { (*self.value.get()).assume_init_ref() } - } - - /// Set the value in the cell, if the cell was already initialized this will return an error. - pub fn set(&self, value: T) -> Result<(), SetError> { - let state = self - .state - .fetch_max(BarrierCellState::Initializing as u8, Ordering::SeqCst); - - // If the state is larger than started writing, then either there is an active writer or - // the cell has already been initialized. - if state == BarrierCellState::Initialized as u8 { - return Err(SetError::AlreadySet); - } else { - unsafe { *self.value.get() = MaybeUninit::new(value) }; - self.state - .store(BarrierCellState::Initialized as u8, Ordering::Release); - - self.notify.notify_waiters(); - } - - Ok(()) - } -} From b2fcdc8e805ebec3a28502c8a9c9c5840cf38ac8 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Sat, 9 Mar 2024 11:52:04 +0100 Subject: [PATCH 06/57] Revert "feat: remove unused file" This reverts commit cd041f37864348650a6a299474bddc6606aeeddb. --- .../src/gateway/barrier_cell.rs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs diff --git a/crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs b/crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs new file mode 100644 index 000000000..a01140ec0 --- /dev/null +++ b/crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs @@ -0,0 +1,83 @@ +use std::{ + cell::UnsafeCell, + mem::MaybeUninit, + sync::atomic::{AtomicU8, Ordering}, +}; +use thiserror::Error; +use tokio::sync::Notify; + +/// A synchronization primitive that can be used to wait for a value to become available. +/// +/// The [`BarrierCell`] is initially empty, requesters can wait for a value to become available +/// using the `wait` method. Once a value is available, the `set` method can be used to set the +/// value in the cell. The `set` method can only be called once. If the `set` method is called +/// multiple times, it will return an error. When `set` is called all waiters will be notified. +pub struct BarrierCell { + state: AtomicU8, + value: UnsafeCell>, + notify: Notify, +} + +unsafe impl Sync for BarrierCell {} + +unsafe impl Send for BarrierCell {} + +#[repr(u8)] +enum BarrierCellState { + Uninitialized, + Initializing, + Initialized, +} + +impl Default for BarrierCell { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, Error)] +pub enum SetError { + #[error("cannot assign a BarrierCell twice")] + AlreadySet, +} + +impl BarrierCell { + /// Constructs a new instance. + pub fn new() -> Self { + Self { + state: AtomicU8::new(BarrierCellState::Uninitialized as u8), + value: UnsafeCell::new(MaybeUninit::uninit()), + notify: Notify::new(), + } + } + + /// Wait for a value to become available in the cell + pub async fn wait(&self) -> &T { + let notified = self.notify.notified(); + if self.state.load(Ordering::Acquire) != BarrierCellState::Initialized as u8 { + notified.await; + } + unsafe { (*self.value.get()).assume_init_ref() } + } + + /// Set the value in the cell, if the cell was already initialized this will return an error. + pub fn set(&self, value: T) -> Result<(), SetError> { + let state = self + .state + .fetch_max(BarrierCellState::Initializing as u8, Ordering::SeqCst); + + // If the state is larger than started writing, then either there is an active writer or + // the cell has already been initialized. + if state == BarrierCellState::Initialized as u8 { + return Err(SetError::AlreadySet); + } else { + unsafe { *self.value.get() = MaybeUninit::new(value) }; + self.state + .store(BarrierCellState::Initialized as u8, Ordering::Release); + + self.notify.notify_waiters(); + } + + Ok(()) + } +} From f1981c49dad4299f8d0fd6672b1bdb6ac2de6b45 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Sun, 10 Mar 2024 11:06:17 +0100 Subject: [PATCH 07/57] test: test for barrier cell --- .../src/gateway/barrier_cell.rs | 30 +++++++++++++++++++ .../src/gateway/error.rs | 1 + .../src/gateway/mod.rs | 6 ++++ 3 files changed, 37 insertions(+) diff --git a/crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs b/crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs index a01140ec0..32248f991 100644 --- a/crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs +++ b/crates/rattler_repodata_gateway/src/gateway/barrier_cell.rs @@ -81,3 +81,33 @@ impl BarrierCell { Ok(()) } } + +#[cfg(test)] +mod test { + use super::BarrierCell; + use std::sync::Arc; + + /// Test that setting the barrier cell works, and we can wait on the value + #[tokio::test] + pub async fn test_barrier_cell() { + let barrier = Arc::new(BarrierCell::new()); + let barrier_clone = barrier.clone(); + + let handle = tokio::spawn(async move { + let value = barrier_clone.wait().await; + assert_eq!(*value, 42); + }); + + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + barrier.set(42).unwrap(); + handle.await.unwrap(); + } + + /// Test that we cannot set the barrier cell twice + #[tokio::test] + pub async fn test_barrier_cell_set_twice() { + let barrier = Arc::new(BarrierCell::new()); + barrier.set(42).unwrap(); + assert!(barrier.set(42).is_err()); + } +} diff --git a/crates/rattler_repodata_gateway/src/gateway/error.rs b/crates/rattler_repodata_gateway/src/gateway/error.rs index a99c87eb7..66cba90c3 100644 --- a/crates/rattler_repodata_gateway/src/gateway/error.rs +++ b/crates/rattler_repodata_gateway/src/gateway/error.rs @@ -2,6 +2,7 @@ use crate::fetch::FetchRepoDataError; use thiserror::Error; #[derive(Debug, Error)] +#[allow(missing_docs)] pub enum GatewayError { #[error("{0}")] IoError(String, #[source] std::io::Error), diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index ab466ec13..8ca041523 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -29,6 +29,7 @@ use tokio::sync::broadcast; // TODO: Instead of using `Channel` it would be better if we could use just the base url. Maybe we // can wrap that in a type. Mamba has the CondaUrl class. +/// A builder for constructing a [`Gateway`]. #[derive(Default)] pub struct GatewayBuilder { channel_config: ChannelConfig, @@ -37,22 +38,26 @@ pub struct GatewayBuilder { } impl GatewayBuilder { + /// New instance of the builder. pub fn new() -> Self { Self::default() } + /// Set the client to use for fetching repodata. #[must_use] pub fn with_client(mut self, client: ClientWithMiddleware) -> Self { self.client = Some(client); self } + /// Set the channel configuration to use for fetching repodata. #[must_use] pub fn with_channel_config(mut self, channel_config: ChannelConfig) -> Self { self.channel_config = channel_config; self } + /// Set the directory to use for caching repodata. #[must_use] pub fn with_cache_dir(mut self, cache: impl Into) -> Self { self.cache = Some(cache.into()); @@ -82,6 +87,7 @@ impl GatewayBuilder { } } +/// Central access point for fetching repodata records. #[derive(Clone)] pub struct Gateway { inner: Arc, From 56f995220057567b39dd19715fbfa6e07ba2b875 Mon Sep 17 00:00:00 2001 From: Tim de Jager Date: Mon, 11 Mar 2024 13:06:26 +0100 Subject: [PATCH 08/57] fix: allow unused code --- .../rattler_repodata_gateway/src/utils/simple_channel_server.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/rattler_repodata_gateway/src/utils/simple_channel_server.rs b/crates/rattler_repodata_gateway/src/utils/simple_channel_server.rs index 7463586c3..54fb32ad4 100644 --- a/crates/rattler_repodata_gateway/src/utils/simple_channel_server.rs +++ b/crates/rattler_repodata_gateway/src/utils/simple_channel_server.rs @@ -19,6 +19,7 @@ impl SimpleChannelServer { Url::parse(&format!("http://localhost:{}", self.local_addr.port())).unwrap() } + #[allow(dead_code)] pub fn channel(&self) -> Channel { Channel::from_url(self.url(), iter::empty()) } From e67c8c5b09500419df596ad0b2c613dbee2a73a7 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Sat, 27 Apr 2024 10:54:08 +0200 Subject: [PATCH 09/57] feat: sharded repodata --- Cargo.toml | 3 + crates/rattler_conda_types/src/channel/mod.rs | 50 +++--- crates/rattler_repodata_gateway/Cargo.toml | 13 +- .../src/gateway/mod.rs | 55 +++++- .../src/gateway/sharded_subdir.rs | 169 ++++++++++++++++++ .../src/utils/simple_channel_server.rs | 7 +- 6 files changed, 252 insertions(+), 45 deletions(-) create mode 100644 crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs diff --git a/Cargo.toml b/Cargo.toml index 15f7ff430..78c513f71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,3 +148,6 @@ zip = { version = "0.6.6", default-features = false } zstd = { version = "0.13.1", default-features = false } [patch.crates-io] + +[profile.release] +debug = true \ No newline at end of file diff --git a/crates/rattler_conda_types/src/channel/mod.rs b/crates/rattler_conda_types/src/channel/mod.rs index 2b34d542a..0011848fd 100644 --- a/crates/rattler_conda_types/src/channel/mod.rs +++ b/crates/rattler_conda_types/src/channel/mod.rs @@ -5,7 +5,6 @@ use std::path::{Component, Path, PathBuf}; use std::str::FromStr; use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; use thiserror::Error; use url::Url; @@ -121,7 +120,7 @@ pub struct Channel { /// The platforms supported by this channel, or None if no explicit platforms have been /// specified. #[serde(skip_serializing_if = "Option::is_none")] - pub platforms: Option>, + pub platforms: Option>, /// Base URL of the channel, everything is relative to this url. pub base_url: Url, @@ -141,7 +140,10 @@ impl Channel { let channel = if parse_scheme(channel).is_some() { let url = Url::parse(channel)?; - Channel::from_url(url, platforms.into_iter().flatten()) + Channel { + platforms, + ..Channel::from_url(url) + } } else if is_path(channel) { let path = PathBuf::from(channel); @@ -160,14 +162,25 @@ impl Channel { } } } else { - Channel::from_name(channel, platforms, config) + Channel { + platforms, + ..Channel::from_name(channel, config) + } }; Ok(channel) } + /// Set the explicit platforms of the channel. + pub fn with_explicit_platforms(self, platforms: impl IntoIterator) -> Self { + Self { + platforms: Some(platforms.into_iter().collect()), + ..self + } + } + /// Constructs a new [`Channel`] from a `Url` and associated platforms. - pub fn from_url(url: Url, platforms: impl IntoIterator) -> Self { + pub fn from_url(url: Url) -> Self { // Get the path part of the URL but trim the directory suffix let path = url.path().trim_end_matches('/'); @@ -187,18 +200,11 @@ impl Channel { // Case 4: custom_channels matches // Case 5: channel_alias match - let mut platforms = platforms.into_iter().peekable(); - let platforms = if platforms.peek().is_none() { - None - } else { - Some(platforms.collect()) - }; - if base_url.has_host() { // Case 7: Fallback let name = path.trim_start_matches('/'); Self { - platforms, + platforms: None, name: (!name.is_empty()).then_some(name).map(str::to_owned), base_url, } @@ -208,7 +214,7 @@ impl Channel { .rsplit_once('/') .map_or_else(|| base_url.path(), |(_, path_part)| path_part); Self { - platforms, + platforms: None, name: (!name.is_empty()).then_some(name).map(str::to_owned), base_url, } @@ -216,11 +222,7 @@ impl Channel { } /// Construct a channel from a name, platform and configuration. - pub fn from_name( - name: &str, - platforms: Option>, - config: &ChannelConfig, - ) -> Self { + pub fn from_name(name: &str, config: &ChannelConfig) -> Self { // TODO: custom channels let dir_name = if name.ends_with('/') { @@ -231,7 +233,7 @@ impl Channel { let name = name.trim_end_matches('/'); Self { - platforms, + platforms: None, base_url: config .channel_alias .join(dir_name.as_ref()) @@ -342,18 +344,16 @@ impl From for ParseChannelError { /// Extract the platforms from the given human readable channel. #[allow(clippy::type_complexity)] -fn parse_platforms( - channel: &str, -) -> Result<(Option>, &str), ParsePlatformError> { +fn parse_platforms(channel: &str) -> Result<(Option>, &str), ParsePlatformError> { if channel.rfind(']').is_some() { if let Some(start_platform_idx) = channel.find('[') { let platform_part = &channel[start_platform_idx + 1..channel.len() - 1]; - let platforms: SmallVec<_> = platform_part + let platforms = platform_part .split(',') .map(str::trim) .filter(|s| !s.is_empty()) .map(FromStr::from_str) - .collect::>()?; + .collect::, _>>()?; let platforms = if platforms.is_empty() { None } else { diff --git a/crates/rattler_repodata_gateway/Cargo.toml b/crates/rattler_repodata_gateway/Cargo.toml index f7394a5b3..9c87b2eb1 100644 --- a/crates/rattler_repodata_gateway/Cargo.toml +++ b/crates/rattler_repodata_gateway/Cargo.toml @@ -21,7 +21,7 @@ elsa = "1.10.0" humansize = { workspace = true } humantime = { workspace = true } futures = { workspace = true } -reqwest = { workspace = true, features = ["stream"] } +reqwest = { workspace = true, features = ["stream", "http2"] } reqwest-middleware = { workspace = true } tokio-util = { workspace = true, features = ["codec", "io"] } tempfile = { workspace = true } @@ -36,8 +36,8 @@ pin-project-lite = { workspace = true } md-5 = { workspace = true } dirs = { workspace = true } fxhash = { workspace = true, optional = true } -rattler_digest = { path="../rattler_digest", version = "0.19.3", default-features = false, features = ["tokio", "serde"] } -rattler_conda_types = { path="../rattler_conda_types", version = "0.22.0", default-features = false, optional = true } +rattler_digest = { path = "../rattler_digest", version = "0.19.3", default-features = false, features = ["tokio", "serde"] } +rattler_conda_types = { path = "../rattler_conda_types", version = "0.22.0", default-features = false, optional = true } memmap2 = { workspace = true, optional = true } ouroboros = { workspace = true, optional = true } serde_with = { workspace = true } @@ -45,7 +45,8 @@ superslice = { workspace = true, optional = true } itertools = { workspace = true, optional = true } json-patch = { workspace = true } hex = { workspace = true, features = ["serde"] } -rattler_networking = { path="../rattler_networking", version = "0.20.3", default-features = false } +rattler_networking = { path = "../rattler_networking", version = "0.20.3", default-features = false } +zstd = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } @@ -62,11 +63,11 @@ rstest = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tower-http = { workspace = true, features = ["fs", "compression-gzip", "trace"] } tracing-test = { workspace = true } -rattler_conda_types = { path="../rattler_conda_types", version = "0.22.0", default-features = false } +rattler_conda_types = { path = "../rattler_conda_types", version = "0.22.0", default-features = false } [features] default = ['native-tls'] -native-tls = ['reqwest/native-tls'] +native-tls = ['reqwest/native-tls', 'reqwest/native-tls-alpn'] rustls-tls = ['reqwest/rustls-tls'] sparse = ["rattler_conda_types", "memmap2", "ouroboros", "superslice", "itertools", "serde_json/raw_value"] gateway = ["sparse"] \ No newline at end of file diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index 8ca041523..1b2514302 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -3,6 +3,7 @@ mod channel_config; mod error; mod local_subdir; mod remote_subdir; +mod sharded_subdir; mod subdir; pub use barrier_cell::BarrierCell; @@ -355,15 +356,25 @@ impl GatewayInner { )); } } else if url.scheme() == "http" || url.scheme() == "https" { - remote_subdir::RemoteSubdirClient::new( - channel.clone(), - platform, - self.client.clone(), - self.cache.clone(), - self.channel_config.get(channel).clone(), - ) - .await - .map(SubdirData::from_client) + if url.as_str().starts_with("https://conda.anaconda.org/conda-forge/") { + sharded_subdir::ShardedSubdir::new( + channel.clone(), + platform.to_string(), + self.client.clone(), + ) + .await + .map(SubdirData::from_client) + } else { + remote_subdir::RemoteSubdirClient::new( + channel.clone(), + platform, + self.client.clone(), + self.cache.clone(), + self.channel_config.get(channel).clone(), + ) + .await + .map(SubdirData::from_client) + } } else { return Err(GatewayError::UnsupportedUrl(format!( "'{}' is not a supported scheme", @@ -399,6 +410,8 @@ mod test { use rattler_conda_types::{Channel, PackageName, Platform}; use std::path::Path; use std::str::FromStr; + use std::time::Instant; + use url::Url; fn local_conda_forge() -> Channel { Channel::from_directory( @@ -446,4 +459,28 @@ mod test { assert_eq!(records.len(), 45060); } + + #[tokio::test(flavor="multi_thread")] + async fn test_sharded_gateway() { + let gateway = Gateway::new(); + + let start = Instant::now(); + let records = gateway + .load_records_recursive( + vec![Channel::from_url( + Url::parse("https://conda.anaconda.org/conda-forge").unwrap(), + )], + vec![Platform::OsxArm64, Platform::NoArch], + vec![ + PackageName::from_str("rubin-env").unwrap(), + PackageName::from_str("rubin-env").unwrap() + ].into_iter(), + ) + .await + .unwrap(); + let end = Instant::now(); + println!("{} records in {:?}", records.len(), end - start); + + assert_eq!(records.len(), 84242); + } } diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs new file mode 100644 index 000000000..d57ebdfb7 --- /dev/null +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs @@ -0,0 +1,169 @@ +use crate::fetch::{FetchRepoDataError, RepoDataNotFoundError}; +use crate::gateway::subdir::SubdirClient; +use crate::GatewayError; +use rattler_conda_types::{ + compute_package_url, Channel, PackageName, PackageRecord, RepoDataRecord, +}; +use reqwest::{Response, StatusCode, Version}; +use reqwest_middleware::ClientWithMiddleware; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::task::JoinError; +use url::Url; + +pub struct ShardedSubdir { + channel: Channel, + client: ClientWithMiddleware, + shard_base_url: Url, + sharded_repodata: ShardedRepodata, +} + +impl ShardedSubdir { + pub async fn new( + channel: Channel, + subdir: String, + client: ClientWithMiddleware, + ) -> Result { + // TODO: our sharded index only serves conda-forge so we simply override it. + let channel = + Channel::from_url(Url::parse("https://conda.anaconda.org/conda-forge").unwrap()); + + let shard_base_url = Url::parse(&format!("https://fast.prefiks.dev/{subdir}/")).unwrap(); + + // Fetch the sharded repodata from the remote server + let repodata_shards_url = shard_base_url + .join("repodata_shards.json") + .expect("invalid shard base url"); + let response = client + .get(repodata_shards_url.clone()) + .send() + .await + .map_err(FetchRepoDataError::from)?; + + // Check if the response was succesfull. + if response.status() == StatusCode::NOT_FOUND { + return Err(GatewayError::FetchRepoDataError( + FetchRepoDataError::NotFound(RepoDataNotFoundError::from( + response.error_for_status().unwrap_err(), + )), + )); + }; + + let response = response + .error_for_status() + .map_err(FetchRepoDataError::from)?; + + // Parse the sharded repodata from the response + let sharded_repodata: ShardedRepodata = + response.json().await.map_err(FetchRepoDataError::from)?; + + Ok(Self { + channel, + client, + sharded_repodata, + shard_base_url, + }) + } +} + +#[async_trait::async_trait] +impl SubdirClient for ShardedSubdir { + async fn fetch_package_records( + &self, + name: &PackageName, + ) -> Result, GatewayError> { + // Find the shard that contains the package + let Some(shard) = self.sharded_repodata.shards.get(name.as_normalized()) else { + return Ok(vec![].into()); + }; + + // Download the shard + let shard_url = self + .shard_base_url + .join(&format!("shards/{}.json.zst", shard.sha256)) + .expect("invalid shard url"); + + let shard_response = self + .client + .get(shard_url.clone()) + .send() + .await + .and_then(|r| r.error_for_status().map_err(Into::into)) + .map_err(FetchRepoDataError::from)?; + + let shard_bytes = shard_response + .bytes() + .await + .map_err(FetchRepoDataError::from)?; + + let base_url = self.sharded_repodata.info.base_url.clone(); + let channel_name = self.channel.canonical_name(); + match tokio::task::spawn_blocking(move || { + // Decompress the shard and read the data as package records. + let packages = zstd::decode_all(shard_bytes.as_ref()) + .and_then(|shard| { + serde_json::from_slice::(&shard).map_err(std::io::Error::from) + }) + .map(|shard| shard.packages)?; + + // Convert to repodata records + let repodata_records: Vec<_> = packages + .into_iter() + .map(|(file_name, package_record)| RepoDataRecord { + url: base_url.join(&file_name).unwrap(), + channel: channel_name.clone(), + package_record, + file_name, + }) + .collect(); + + Ok(repodata_records) + }) + .await + .map_err(JoinError::try_into_panic) + { + Ok(Ok(records)) => Ok(records.into()), + Ok(Err(err)) => Err(GatewayError::IoError( + "failed to extract repodata records from sparse repodata".to_string(), + err, + )), + Err(Ok(panic)) => std::panic::resume_unwind(panic), + Err(Err(_)) => Err(GatewayError::IoError( + "loading of the records was cancelled".to_string(), + std::io::ErrorKind::Interrupted.into(), + )), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShardedRepodata { + pub info: ShardedSubdirInfo, + /// The individual shards indexed by package name. + pub shards: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShardRef { + // The sha256 hash of the shard + pub sha256: String, + + // The size of the shard. + pub size: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Shard { + pub packages: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShardedSubdirInfo { + /// The name of the subdirectory + pub subdir: String, + + /// The base url of the subdirectory. This is the location where the actual + /// packages are stored. + pub base_url: Url, +} diff --git a/crates/rattler_repodata_gateway/src/utils/simple_channel_server.rs b/crates/rattler_repodata_gateway/src/utils/simple_channel_server.rs index 54fb32ad4..c98376fcb 100644 --- a/crates/rattler_repodata_gateway/src/utils/simple_channel_server.rs +++ b/crates/rattler_repodata_gateway/src/utils/simple_channel_server.rs @@ -1,9 +1,6 @@ use axum::routing::get_service; use rattler_conda_types::Channel; -use std::future::IntoFuture; -use std::iter; -use std::net::SocketAddr; -use std::path::Path; +use std::{future::IntoFuture, net::SocketAddr, path::Path}; use tokio::sync::oneshot; use tower_http::services::ServeDir; use url::Url; @@ -21,7 +18,7 @@ impl SimpleChannelServer { #[allow(dead_code)] pub fn channel(&self) -> Channel { - Channel::from_url(self.url(), iter::empty()) + Channel::from_url(self.url()) } } From ec64f2f85678f603662f930fdb5462e85557aabb Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Sat, 27 Apr 2024 22:59:42 +0200 Subject: [PATCH 10/57] feat: msgpack --- Cargo.toml | 6 +- crates/rattler_lock/Cargo.toml | 1 + crates/rattler_repodata_gateway/Cargo.toml | 4 +- .../src/gateway/mod.rs | 34 ++-- .../src/gateway/sharded_subdir.rs | 186 ++++++++++++------ 5 files changed, 157 insertions(+), 74 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 78c513f71..cee6b7900 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,7 +65,7 @@ futures-util = "0.3.30" fxhash = "0.2.1" getrandom = { version = "0.2.14", default-features = false } glob = "0.3.1" -google-cloud-auth = { version = "0.13.2", default-features = false} +google-cloud-auth = { version = "0.13.2", default-features = false } hex = "0.4.3" hex-literal = "0.4.1" http = "1.1" @@ -106,8 +106,10 @@ reqwest-middleware = "0.3.0" reqwest-retry = "0.5.0" resolvo = { version = "0.4.0" } retry-policies = { version = "0.3.0", default-features = false } +rmp-serde = { version = "1.2.0" } rstest = { version = "0.19.0" } rstest_reuse = "0.6.0" +rayon = { version = "1.10.0", default-features = false } serde = { version = "1.0.198" } serde_json = { version = "1.0.116" } serde_repr = "0.1" @@ -150,4 +152,4 @@ zstd = { version = "0.13.1", default-features = false } [patch.crates-io] [profile.release] -debug = true \ No newline at end of file +debug = true diff --git a/crates/rattler_lock/Cargo.toml b/crates/rattler_lock/Cargo.toml index b9e3b39e5..749b2c857 100644 --- a/crates/rattler_lock/Cargo.toml +++ b/crates/rattler_lock/Cargo.toml @@ -27,6 +27,7 @@ thiserror = { workspace = true } url = { workspace = true, features = ["serde"] } purl = { workspace = true, features = ["serde"] } percent-encoding = { workspace = true } +rayon = { workspace = true } [dev-dependencies] insta = { workspace = true, features = ["yaml"] } diff --git a/crates/rattler_repodata_gateway/Cargo.toml b/crates/rattler_repodata_gateway/Cargo.toml index 9c87b2eb1..ac7a40bad 100644 --- a/crates/rattler_repodata_gateway/Cargo.toml +++ b/crates/rattler_repodata_gateway/Cargo.toml @@ -32,6 +32,7 @@ tokio = { workspace = true, features = ["rt", "io-util"] } anyhow = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +rmp-serde = { workspace = true } pin-project-lite = { workspace = true } md-5 = { workspace = true } dirs = { workspace = true } @@ -47,6 +48,7 @@ json-patch = { workspace = true } hex = { workspace = true, features = ["serde"] } rattler_networking = { path = "../rattler_networking", version = "0.20.3", default-features = false } zstd = { workspace = true } +bytes = "1.6.0" [target.'cfg(unix)'.dependencies] libc = { workspace = true } @@ -70,4 +72,4 @@ default = ['native-tls'] native-tls = ['reqwest/native-tls', 'reqwest/native-tls-alpn'] rustls-tls = ['reqwest/rustls-tls'] sparse = ["rattler_conda_types", "memmap2", "ouroboros", "superslice", "itertools", "serde_json/raw_value"] -gateway = ["sparse"] \ No newline at end of file +gateway = ["sparse"] diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index 1b2514302..e3c30c4a9 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -162,14 +162,11 @@ impl Gateway { }); } - // Package names that we still need to fetch. - let mut pending_package_names = names.into_iter().map(Into::into).collect_vec(); - // Package names that we have or will issue requests for. - let mut seen = pending_package_names - .iter() - .cloned() - .collect::>(); + let mut seen = names.into_iter().map(Into::into).collect::>(); + + // Package names that we still need to fetch. + let mut pending_package_names = seen.iter().cloned().collect::>(); // A list of futures to fetch the records for the pending package names. The main task // awaits these futures. @@ -356,11 +353,15 @@ impl GatewayInner { )); } } else if url.scheme() == "http" || url.scheme() == "https" { - if url.as_str().starts_with("https://conda.anaconda.org/conda-forge/") { + if url + .as_str() + .starts_with("https://conda.anaconda.org/conda-forge/") + { sharded_subdir::ShardedSubdir::new( channel.clone(), platform.to_string(), self.client.clone(), + self.cache.clone(), ) .await .map(SubdirData::from_client) @@ -460,7 +461,7 @@ mod test { assert_eq!(records.len(), 45060); } - #[tokio::test(flavor="multi_thread")] + #[tokio::test(flavor = "multi_thread")] async fn test_sharded_gateway() { let gateway = Gateway::new(); @@ -470,11 +471,18 @@ mod test { vec![Channel::from_url( Url::parse("https://conda.anaconda.org/conda-forge").unwrap(), )], - vec![Platform::OsxArm64, Platform::NoArch], + vec![Platform::Linux64, Platform::NoArch], vec![ - PackageName::from_str("rubin-env").unwrap(), - PackageName::from_str("rubin-env").unwrap() - ].into_iter(), + // PackageName::from_str("rubin-env").unwrap(), + + // PackageName::from_str("jupyterlab").unwrap(), + // PackageName::from_str("detectron2").unwrap(), + + PackageName::from_str("python").unwrap(), + PackageName::from_str("boto3").unwrap(), + PackageName::from_str("requests").unwrap(), + ] + .into_iter(), ) .await .unwrap(); diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs index d57ebdfb7..c5bf916c8 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs @@ -1,13 +1,13 @@ use crate::fetch::{FetchRepoDataError, RepoDataNotFoundError}; use crate::gateway::subdir::SubdirClient; use crate::GatewayError; -use rattler_conda_types::{ - compute_package_url, Channel, PackageName, PackageRecord, RepoDataRecord, -}; -use reqwest::{Response, StatusCode, Version}; +use futures::TryFutureExt; +use rattler_conda_types::{Channel, PackageName, PackageRecord, RepoDataRecord}; +use reqwest::StatusCode; use reqwest_middleware::ClientWithMiddleware; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::path::PathBuf; use std::sync::Arc; use tokio::task::JoinError; use url::Url; @@ -17,13 +17,15 @@ pub struct ShardedSubdir { client: ClientWithMiddleware, shard_base_url: Url, sharded_repodata: ShardedRepodata, + cache_dir: PathBuf, } impl ShardedSubdir { pub async fn new( - channel: Channel, + _channel: Channel, subdir: String, client: ClientWithMiddleware, + cache_dir: PathBuf, ) -> Result { // TODO: our sharded index only serves conda-forge so we simply override it. let channel = @@ -58,11 +60,18 @@ impl ShardedSubdir { let sharded_repodata: ShardedRepodata = response.json().await.map_err(FetchRepoDataError::from)?; + // Determine the cache directory and make sure it exists. + let cache_dir = cache_dir.join("shards_v1"); + tokio::fs::create_dir_all(&cache_dir) + .await + .map_err(FetchRepoDataError::IoError)?; + Ok(Self { channel, client, sharded_repodata, shard_base_url, + cache_dir, }) } } @@ -78,62 +87,120 @@ impl SubdirClient for ShardedSubdir { return Ok(vec![].into()); }; - // Download the shard - let shard_url = self - .shard_base_url - .join(&format!("shards/{}.json.zst", shard.sha256)) - .expect("invalid shard url"); - - let shard_response = self - .client - .get(shard_url.clone()) - .send() + // Check if we already have the shard in the cache. + let shard_cache_path = self.cache_dir.join(&format!("{}.msgpack", shard.sha256)); + if shard_cache_path.is_file() { + // Read the cached shard + let cached_bytes = tokio::fs::read(&shard_cache_path) + .await + .map_err(FetchRepoDataError::IoError)?; + + // Decode the cached shard + parse_records( + cached_bytes, + self.channel.canonical_name(), + self.sharded_repodata.info.base_url.clone(), + ) .await - .and_then(|r| r.error_for_status().map_err(Into::into)) - .map_err(FetchRepoDataError::from)?; + .map(Arc::from) + } else { + // Download the shard + let shard_url = self + .shard_base_url + .join(&format!("shards/{}.msgpack.zst", shard.sha256)) + .expect("invalid shard url"); + + let shard_response = self + .client + .get(shard_url.clone()) + .send() + .await + .and_then(|r| r.error_for_status().map_err(Into::into)) + .map_err(FetchRepoDataError::from)?; + + let shard_bytes = shard_response + .bytes() + .await + .map_err(FetchRepoDataError::from)?; + + let shard_bytes = decode_zst_bytes_async(shard_bytes).await?; + + // Create a future to write the cached bytes to disk + let write_to_cache_fut = tokio::fs::write(&shard_cache_path, shard_bytes.clone()) + .map_err(FetchRepoDataError::IoError) + .map_err(GatewayError::from); + + // Create a future to parse the records from the shard + let parse_records_fut = parse_records( + shard_bytes, + self.channel.canonical_name(), + self.sharded_repodata.info.base_url.clone(), + ); + + // Await both futures concurrently. + let (_, records) = tokio::try_join!(write_to_cache_fut, parse_records_fut)?; + + Ok(records.into()) + } + } +} - let shard_bytes = shard_response - .bytes() - .await - .map_err(FetchRepoDataError::from)?; +async fn decode_zst_bytes_async + Send + 'static>( + bytes: R, +) -> Result, GatewayError> { + match tokio::task::spawn_blocking(move || match zstd::decode_all(bytes.as_ref()) { + Ok(decoded) => Ok(decoded), + Err(err) => Err(GatewayError::IoError( + "failed to decode zstd shard".to_string(), + err, + )), + }) + .await + .map_err(JoinError::try_into_panic) + { + Ok(Ok(bytes)) => Ok(bytes), + Ok(Err(err)) => Err(err), + Err(Ok(panic)) => std::panic::resume_unwind(panic), + Err(Err(_)) => Err(GatewayError::IoError( + "loading of the records was cancelled".to_string(), + std::io::ErrorKind::Interrupted.into(), + )), + } +} - let base_url = self.sharded_repodata.info.base_url.clone(); - let channel_name = self.channel.canonical_name(); - match tokio::task::spawn_blocking(move || { - // Decompress the shard and read the data as package records. - let packages = zstd::decode_all(shard_bytes.as_ref()) - .and_then(|shard| { - serde_json::from_slice::(&shard).map_err(std::io::Error::from) - }) - .map(|shard| shard.packages)?; - - // Convert to repodata records - let repodata_records: Vec<_> = packages - .into_iter() - .map(|(file_name, package_record)| RepoDataRecord { - url: base_url.join(&file_name).unwrap(), - channel: channel_name.clone(), - package_record, - file_name, - }) - .collect(); - - Ok(repodata_records) - }) - .await - .map_err(JoinError::try_into_panic) - { - Ok(Ok(records)) => Ok(records.into()), - Ok(Err(err)) => Err(GatewayError::IoError( - "failed to extract repodata records from sparse repodata".to_string(), - err, - )), - Err(Ok(panic)) => std::panic::resume_unwind(panic), - Err(Err(_)) => Err(GatewayError::IoError( - "loading of the records was cancelled".to_string(), - std::io::ErrorKind::Interrupted.into(), - )), - } +async fn parse_records + Send + 'static>( + bytes: R, + channel_name: String, + base_url: Url, +) -> Result, GatewayError> { + match tokio::task::spawn_blocking(move || { + // let shard = serde_json::from_slice::(bytes.as_ref()).map_err(std::io::Error::from)?; + let shard = rmp_serde::from_slice::(bytes.as_ref()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; + let packages = + itertools::chain(shard.packages.into_iter(), shard.packages_conda.into_iter()); + Ok(packages + .map(|(file_name, package_record)| RepoDataRecord { + url: base_url.join(&file_name).unwrap(), + channel: channel_name.clone(), + package_record, + file_name, + }) + .collect()) + }) + .await + .map_err(JoinError::try_into_panic) + { + Ok(Ok(records)) => Ok(records), + Ok(Err(err)) => Err(GatewayError::IoError( + "failed to parse repodata records from repodata shard".to_string(), + err, + )), + Err(Ok(panic)) => std::panic::resume_unwind(panic), + Err(Err(_)) => Err(GatewayError::IoError( + "loading of the records was cancelled".to_string(), + std::io::ErrorKind::Interrupted.into(), + )), } } @@ -156,6 +223,9 @@ pub struct ShardRef { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Shard { pub packages: HashMap, + + #[serde(rename = "packages.conda", default)] + pub packages_conda: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] From 52eff36d86dd13de8e716469924cbfafcc870fe9 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Sat, 27 Apr 2024 23:55:21 +0200 Subject: [PATCH 11/57] chore: format --- Cargo.toml | 1 + crates/rattler_repodata_gateway/Cargo.toml | 4 +- .../src/gateway/mod.rs | 9 +- .../src/gateway/sharded_subdir.rs | 124 ++++++++---------- 4 files changed, 60 insertions(+), 78 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cee6b7900..4f5b931be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,7 @@ tokio-stream = "0.1.15" tokio-util = "0.7.10" tower = { version = "0.4.13", default-features = false } tower-http = { version = "0.5.2", default-features = false } +tokio-rayon = { version = "2.1.0" } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", default-features = false } tracing-test = { version = "0.2.4" } diff --git a/crates/rattler_repodata_gateway/Cargo.toml b/crates/rattler_repodata_gateway/Cargo.toml index ac7a40bad..1fee531c1 100644 --- a/crates/rattler_repodata_gateway/Cargo.toml +++ b/crates/rattler_repodata_gateway/Cargo.toml @@ -32,7 +32,7 @@ tokio = { workspace = true, features = ["rt", "io-util"] } anyhow = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -rmp-serde = { workspace = true } +rmp-serde = { workspace = true } pin-project-lite = { workspace = true } md-5 = { workspace = true } dirs = { workspace = true } @@ -49,6 +49,8 @@ hex = { workspace = true, features = ["serde"] } rattler_networking = { path = "../rattler_networking", version = "0.20.3", default-features = false } zstd = { workspace = true } bytes = "1.6.0" +rayon = { workspace = true } +tokio-rayon = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index e3c30c4a9..eee72ccc5 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -473,14 +473,13 @@ mod test { )], vec![Platform::Linux64, Platform::NoArch], vec![ - // PackageName::from_str("rubin-env").unwrap(), - + PackageName::from_str("rubin-env").unwrap(), // PackageName::from_str("jupyterlab").unwrap(), // PackageName::from_str("detectron2").unwrap(), - PackageName::from_str("python").unwrap(), - PackageName::from_str("boto3").unwrap(), - PackageName::from_str("requests").unwrap(), + // PackageName::from_str("python").unwrap(), + // PackageName::from_str("boto3").unwrap(), + // PackageName::from_str("requests").unwrap(), ] .into_iter(), ) diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs index c5bf916c8..0802c6541 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs @@ -9,7 +9,6 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; -use tokio::task::JoinError; use url::Url; pub struct ShardedSubdir { @@ -89,66 +88,69 @@ impl SubdirClient for ShardedSubdir { // Check if we already have the shard in the cache. let shard_cache_path = self.cache_dir.join(&format!("{}.msgpack", shard.sha256)); - if shard_cache_path.is_file() { - // Read the cached shard - let cached_bytes = tokio::fs::read(&shard_cache_path) + + // Read the cached shard + match tokio::fs::read(&shard_cache_path).await { + Ok(cached_bytes) => { + // Decode the cached shard + return parse_records( + cached_bytes, + self.channel.canonical_name(), + self.sharded_repodata.info.base_url.clone(), + ) .await - .map_err(FetchRepoDataError::IoError)?; - - // Decode the cached shard - parse_records( - cached_bytes, - self.channel.canonical_name(), - self.sharded_repodata.info.base_url.clone(), - ) + .map(Arc::from); + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + // The file is missing from the cache, we need to download it. + } + Err(err) => return Err(FetchRepoDataError::IoError(err).into()), + } + + // Download the shard + let shard_url = self + .shard_base_url + .join(&format!("shards/{}.msgpack.zst", shard.sha256)) + .expect("invalid shard url"); + + let shard_response = self + .client + .get(shard_url.clone()) + .send() .await - .map(Arc::from) - } else { - // Download the shard - let shard_url = self - .shard_base_url - .join(&format!("shards/{}.msgpack.zst", shard.sha256)) - .expect("invalid shard url"); - - let shard_response = self - .client - .get(shard_url.clone()) - .send() - .await - .and_then(|r| r.error_for_status().map_err(Into::into)) - .map_err(FetchRepoDataError::from)?; + .and_then(|r| r.error_for_status().map_err(Into::into)) + .map_err(FetchRepoDataError::from)?; - let shard_bytes = shard_response - .bytes() - .await - .map_err(FetchRepoDataError::from)?; + let shard_bytes = shard_response + .bytes() + .await + .map_err(FetchRepoDataError::from)?; - let shard_bytes = decode_zst_bytes_async(shard_bytes).await?; + let shard_bytes = decode_zst_bytes_async(shard_bytes).await?; - // Create a future to write the cached bytes to disk - let write_to_cache_fut = tokio::fs::write(&shard_cache_path, shard_bytes.clone()) - .map_err(FetchRepoDataError::IoError) - .map_err(GatewayError::from); + // Create a future to write the cached bytes to disk + let write_to_cache_fut = tokio::fs::write(&shard_cache_path, shard_bytes.clone()) + .map_err(FetchRepoDataError::IoError) + .map_err(GatewayError::from); - // Create a future to parse the records from the shard - let parse_records_fut = parse_records( - shard_bytes, - self.channel.canonical_name(), - self.sharded_repodata.info.base_url.clone(), - ); + // Create a future to parse the records from the shard + let parse_records_fut = parse_records( + shard_bytes, + self.channel.canonical_name(), + self.sharded_repodata.info.base_url.clone(), + ); - // Await both futures concurrently. - let (_, records) = tokio::try_join!(write_to_cache_fut, parse_records_fut)?; + // Await both futures concurrently. + let (_, records) = tokio::try_join!(write_to_cache_fut, parse_records_fut)?; - Ok(records.into()) - } + Ok(records.into()) } } async fn decode_zst_bytes_async + Send + 'static>( bytes: R, ) -> Result, GatewayError> { - match tokio::task::spawn_blocking(move || match zstd::decode_all(bytes.as_ref()) { + tokio_rayon::spawn(move || match zstd::decode_all(bytes.as_ref()) { Ok(decoded) => Ok(decoded), Err(err) => Err(GatewayError::IoError( "failed to decode zstd shard".to_string(), @@ -156,16 +158,6 @@ async fn decode_zst_bytes_async + Send + 'static>( )), }) .await - .map_err(JoinError::try_into_panic) - { - Ok(Ok(bytes)) => Ok(bytes), - Ok(Err(err)) => Err(err), - Err(Ok(panic)) => std::panic::resume_unwind(panic), - Err(Err(_)) => Err(GatewayError::IoError( - "loading of the records was cancelled".to_string(), - std::io::ErrorKind::Interrupted.into(), - )), - } } async fn parse_records + Send + 'static>( @@ -173,10 +165,11 @@ async fn parse_records + Send + 'static>( channel_name: String, base_url: Url, ) -> Result, GatewayError> { - match tokio::task::spawn_blocking(move || { + tokio_rayon::spawn(move || { // let shard = serde_json::from_slice::(bytes.as_ref()).map_err(std::io::Error::from)?; let shard = rmp_serde::from_slice::(bytes.as_ref()) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) + .map_err(FetchRepoDataError::IoError)?; let packages = itertools::chain(shard.packages.into_iter(), shard.packages_conda.into_iter()); Ok(packages @@ -189,19 +182,6 @@ async fn parse_records + Send + 'static>( .collect()) }) .await - .map_err(JoinError::try_into_panic) - { - Ok(Ok(records)) => Ok(records), - Ok(Err(err)) => Err(GatewayError::IoError( - "failed to parse repodata records from repodata shard".to_string(), - err, - )), - Err(Ok(panic)) => std::panic::resume_unwind(panic), - Err(Err(_)) => Err(GatewayError::IoError( - "loading of the records was cancelled".to_string(), - std::io::ErrorKind::Interrupted.into(), - )), - } } #[derive(Debug, Clone, Serialize, Deserialize)] From 4960b2e279b685e2bef512628dc3314464030075 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 29 Apr 2024 14:30:25 +0200 Subject: [PATCH 12/57] feat: update to latest --- Cargo.toml | 1 + crates/rattler-bin/Cargo.toml | 3 +- crates/rattler-bin/src/commands/create.rs | 416 +++++++++--------- crates/rattler_digest/Cargo.toml | 3 +- crates/rattler_digest/src/serde.rs | 11 + .../src/gateway/mod.rs | 45 +- .../src/gateway/repo_data.rs | 80 ++++ .../src/gateway/sharded_subdir.rs | 33 +- crates/rattler_solve/src/lib.rs | 12 + 9 files changed, 359 insertions(+), 245 deletions(-) create mode 100644 crates/rattler_repodata_gateway/src/gateway/repo_data.rs diff --git a/Cargo.toml b/Cargo.toml index 4f5b931be..4fa80d824 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ fslock = "0.2.1" futures = "0.3.30" futures-util = "0.3.30" fxhash = "0.2.1" +generic-array = "0.14.4" getrandom = { version = "0.2.14", default-features = false } glob = "0.3.1" google-cloud-auth = { version = "0.13.2", default-features = false } diff --git a/crates/rattler-bin/Cargo.toml b/crates/rattler-bin/Cargo.toml index ca4975355..d911ed2a1 100644 --- a/crates/rattler-bin/Cargo.toml +++ b/crates/rattler-bin/Cargo.toml @@ -31,13 +31,14 @@ once_cell = { workspace = true } rattler = { path="../rattler", version = "0.23.1", default-features = false } rattler_conda_types = { path="../rattler_conda_types", version = "0.22.0", default-features = false } rattler_networking = { path="../rattler_networking", version = "0.20.3", default-features = false } -rattler_repodata_gateway = { path="../rattler_repodata_gateway", version = "0.19.9", default-features = false, features = ["sparse"] } +rattler_repodata_gateway = { path="../rattler_repodata_gateway", version = "0.19.9", default-features = false, features = ["gateway"] } rattler_solve = { path="../rattler_solve", version = "0.21.0", default-features = false, features = ["resolvo", "libsolv_c"] } rattler_virtual_packages = { path="../rattler_virtual_packages", version = "0.19.8", default-features = false } reqwest = { workspace = true } reqwest-middleware = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } +itertools = { workspace = true } [package.metadata.release] # Dont publish the binary diff --git a/crates/rattler-bin/src/commands/create.rs b/crates/rattler-bin/src/commands/create.rs index 600a9f21c..0e0fefb2c 100644 --- a/crates/rattler-bin/src/commands/create.rs +++ b/crates/rattler-bin/src/commands/create.rs @@ -1,7 +1,8 @@ use crate::global_multi_progress; use anyhow::Context; use futures::{stream, stream::FuturesUnordered, FutureExt, StreamExt, TryFutureExt, TryStreamExt}; -use indicatif::{HumanBytes, ProgressBar, ProgressState, ProgressStyle}; +use indicatif::{ProgressBar, ProgressStyle}; +use itertools::Itertools; use rattler::{ default_cache_dir, install::{ @@ -18,20 +19,18 @@ use rattler_conda_types::{ use rattler_networking::{ retry_policies::default_retry_policy, AuthenticationMiddleware, AuthenticationStorage, }; -use rattler_repodata_gateway::fetch::{ - CacheResult, DownloadProgress, FetchRepoDataError, FetchRepoDataOptions, -}; -use rattler_repodata_gateway::sparse::SparseRepoData; +use rattler_repodata_gateway::Gateway; use rattler_solve::{ libsolv_c::{self}, - resolvo, ChannelPriority, SolverImpl, SolverTask, + resolvo, ChannelPriority, RepoDataIter, SolverImpl, SolverTask, }; use reqwest::Client; +use std::future::Future; use std::sync::Arc; +use std::time::Instant; use std::{ borrow::Cow, env, - fmt::Write, future::ready, path::{Path, PathBuf}, str::FromStr, @@ -72,7 +71,7 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { let target_prefix = opt .target_prefix .unwrap_or_else(|| current_dir.join(".prefix")); - println!("target prefix: {target_prefix:?}"); + println!("Target prefix: {}", target_prefix.display()); // Determine the platform we're going to install for let install_platform = if let Some(platform) = opt.platform { @@ -81,7 +80,7 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { Platform::current() }; - println!("installing for platform: {install_platform:?}"); + println!("Installing for platform: {install_platform:?}"); // Parse the specs from the command line. We do this explicitly instead of allow clap to deal // with this because we need to parse the `channel_config` when parsing matchspecs. @@ -105,19 +104,6 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { .map(|channel_str| Channel::from_str(channel_str, &channel_config)) .collect::, _>>()?; - // Each channel contains multiple subdirectories. Users can specify the subdirectories they want - // to use when specifying their channels. If the user didn't specify the default subdirectories - // we use defaults based on the current platform. - let channel_urls = channels - .iter() - .flat_map(|channel| { - vec![ - (channel.clone(), install_platform), - (channel.clone(), Platform::NoArch), - ] - }) - .collect::>(); - // Determine the packages that are currently installed in the environment. let installed_packages = find_installed_packages(&target_prefix, 100) .await @@ -138,54 +124,31 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { ))) .build(); - let multi_progress = global_multi_progress(); - - let repodata_cache_path = cache_dir.join("repodata"); - let channel_and_platform_len = channel_urls.len(); - let repodata_download_client = download_client.clone(); - let sparse_repo_datas = futures::stream::iter(channel_urls) - .map(move |(channel, platform)| { - let repodata_cache = repodata_cache_path.clone(); - let download_client = repodata_download_client.clone(); - let multi_progress = multi_progress.clone(); - async move { - fetch_repo_data_records_with_progress( - channel, - platform, - &repodata_cache, - download_client.clone(), - multi_progress, - ) - .await - } - }) - .buffer_unordered(channel_and_platform_len) - .filter_map(|result| async move { - match result { - Err(e) => Some(Err(e)), - Ok(Some(data)) => Some(Ok(data)), - Ok(None) => None, - } - }) - .collect::>() - .await - // Collect into another iterator where we extract the first erroneous result - .into_iter() - .collect::, _>>()?; - // Get the package names from the matchspecs so we can only load the package records that we need. + let gateway = Gateway::builder() + .with_cache_dir(cache_dir.join("repodata")) + .with_client(download_client.clone()) + .finish(); + + let start_load_repo_data = Instant::now(); let package_names = specs.iter().filter_map(|spec| spec.name.as_ref().cloned()); - let repodatas = wrap_in_progress("parsing repodata", move || { - SparseRepoData::load_records_recursive( - &sparse_repo_datas, + let repo_data = wrap_in_async_progress( + "loading repodata", + gateway.load_records_recursive( + channels, + [install_platform, Platform::NoArch], package_names, - Some(|record| { - if record.name.as_normalized() == "python" { - record.depends.push("pip".to_string()); - } - }), - ) - })?; + ), + ) + .await?; + + // Determine the number of recors + let total_records: usize = repo_data.iter().map(|r| r.len()).sum(); + println!( + "Loaded {} records in {:?}", + total_records, + start_load_repo_data.elapsed() + ); // Determine virtual packages of the system. These packages define the capabilities of the // system. Some packages depend on these virtual packages to indiciate compability with the @@ -218,7 +181,12 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { } })?; - println!("virtual packages: {virtual_packages:?}"); + println!( + "Virtual packages:\n{}\n", + virtual_packages + .iter() + .format_with("\n", |i, f| f(&format_args!(" - {i}",)),) + ); // Now that we parsed and downloaded all information, construct the packaging problem that we // need to solve. We do this by constructing a `SolverProblem`. This encapsulates all the @@ -229,7 +197,10 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { .collect(); let solver_task = SolverTask { - available_packages: &repodatas, + available_packages: repo_data + .iter() + .map(|repo_data| RepoDataIter(repo_data)) + .collect::>(), locked_packages, virtual_packages, specs, @@ -274,19 +245,30 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { for operation in &transaction.operations { match operation { - TransactionOperation::Install(r) => println!("* Install: {}", format_record(r)), + TransactionOperation::Install(r) => { + println!("{} {}", console::style("+").green(), format_record(r)) + } TransactionOperation::Change { old, new } => { println!( - "* Change: {} -> {}", + "{} {} -> {}", + console::style("~").yellow(), format_record(&old.repodata_record), format_record(new) ); } TransactionOperation::Reinstall(r) => { - println!("* Reinstall: {}", format_record(&r.repodata_record)); + println!( + "{} {}", + console::style("~").yellow(), + format_record(&r.repodata_record) + ); } TransactionOperation::Remove(r) => { - println!("* Remove: {}", format_record(&r.repodata_record)); + println!( + "{} {}", + console::style("-").red(), + format_record(&r.repodata_record) + ); } } } @@ -571,127 +553,141 @@ fn wrap_in_progress T>(msg: impl Into>, func result } -/// Given a channel and platform, download and cache the `repodata.json` for it. This function -/// reports its progress via a CLI progressbar. -async fn fetch_repo_data_records_with_progress( - channel: Channel, - platform: Platform, - repodata_cache: &Path, - client: reqwest_middleware::ClientWithMiddleware, - multi_progress: indicatif::MultiProgress, -) -> Result, anyhow::Error> { - // Create a progress bar - let progress_bar = multi_progress.add( - indicatif::ProgressBar::new(1) - .with_finish(indicatif::ProgressFinish::AndLeave) - .with_prefix(format!("{}/{platform}", friendly_channel_name(&channel))) - .with_style(default_bytes_style()), - ); - progress_bar.enable_steady_tick(Duration::from_millis(100)); - - // Download the repodata.json - let download_progress_progress_bar = progress_bar.clone(); - let result = rattler_repodata_gateway::fetch::fetch_repo_data( - channel.platform_url(platform), - client, - repodata_cache.to_path_buf(), - FetchRepoDataOptions::default(), - Some(Box::new(move |DownloadProgress { total, bytes }| { - download_progress_progress_bar.set_length(total.unwrap_or(bytes)); - download_progress_progress_bar.set_position(bytes); - })), - ) - .await; - - // Error out if an error occurred, but also update the progress bar - let result = match result { - Err(e) => { - let not_found = matches!(&e, FetchRepoDataError::NotFound(_)); - if not_found && platform != Platform::NoArch { - progress_bar.set_style(finished_progress_style()); - progress_bar.finish_with_message("Not Found"); - return Ok(None); - } - - progress_bar.set_style(errored_progress_style()); - progress_bar.finish_with_message("Error"); - return Err(e.into()); - } - Ok(result) => result, - }; - - // Notify that we are deserializing - progress_bar.set_style(deserializing_progress_style()); - progress_bar.set_message("Deserializing.."); - - // Deserialize the data. This is a hefty blocking operation so we spawn it as a tokio blocking - // task. - let repo_data_json_path = result.repo_data_json_path.clone(); - match tokio::task::spawn_blocking(move || { - SparseRepoData::new( - channel, - platform.to_string(), - repo_data_json_path, - Some(|record: &mut PackageRecord| { - if record.name.as_normalized() == "python" { - record.depends.push("pip".to_string()); - } - }), - ) - }) - .await - { - Ok(Ok(repodata)) => { - progress_bar.set_style(finished_progress_style()); - let is_cache_hit = matches!( - result.cache_result, - CacheResult::CacheHit | CacheResult::CacheHitAfterFetch - ); - progress_bar.finish_with_message(if is_cache_hit { "Using cache" } else { "Done" }); - Ok(Some(repodata)) - } - Ok(Err(err)) => { - progress_bar.set_style(errored_progress_style()); - progress_bar.finish_with_message("Error"); - Err(err.into()) - } - Err(err) => { - if let Ok(panic) = err.try_into_panic() { - std::panic::resume_unwind(panic); - } else { - progress_bar.set_style(errored_progress_style()); - progress_bar.finish_with_message("Cancelled.."); - // Since the task was cancelled most likely the whole async stack is being cancelled. - Err(anyhow::anyhow!("cancelled")) - } - } - } -} - -/// Returns a friendly name for the specified channel. -fn friendly_channel_name(channel: &Channel) -> String { - channel - .name - .as_ref() - .map_or_else(|| channel.canonical_name(), String::from) -} - -/// Returns the style to use for a progressbar that is currently in progress. -fn default_bytes_style() -> indicatif::ProgressStyle { - indicatif::ProgressStyle::default_bar() - .template("{spinner:.green} {prefix:20!} [{elapsed_precise}] [{bar:40!.bright.yellow/dim.white}] {bytes:>8} @ {smoothed_bytes_per_sec:8}").unwrap() - .progress_chars("━━╾─") - .with_key( - "smoothed_bytes_per_sec", - |s: &ProgressState, w: &mut dyn Write| match (s.pos(), s.elapsed().as_millis()) { - (pos, elapsed_ms) if elapsed_ms > 0 => { - write!(w, "{}/s", HumanBytes((pos as f64 * 1000_f64 / elapsed_ms as f64) as u64)).unwrap(); - } - _ => write!(w, "-").unwrap(), - }, - ) +/// Displays a spinner with the given message while running the specified function to completion. +async fn wrap_in_async_progress>( + msg: impl Into>, + fut: F, +) -> T { + let pb = ProgressBar::new_spinner(); + pb.enable_steady_tick(Duration::from_millis(100)); + pb.set_style(long_running_progress_style()); + pb.set_message(msg); + let result = fut.await; + pb.finish_and_clear(); + result } - +// +// /// Given a channel and platform, download and cache the `repodata.json` for it. This function +// /// reports its progress via a CLI progressbar. +// async fn fetch_repo_data_records_with_progress( +// channel: Channel, +// platform: Platform, +// repodata_cache: &Path, +// client: reqwest_middleware::ClientWithMiddleware, +// multi_progress: indicatif::MultiProgress, +// ) -> Result, anyhow::Error> { +// // Create a progress bar +// let progress_bar = multi_progress.add( +// indicatif::ProgressBar::new(1) +// .with_finish(indicatif::ProgressFinish::AndLeave) +// .with_prefix(format!("{}/{platform}", friendly_channel_name(&channel))) +// .with_style(default_bytes_style()), +// ); +// progress_bar.enable_steady_tick(Duration::from_millis(100)); +// +// // Download the repodata.json +// let download_progress_progress_bar = progress_bar.clone(); +// let result = rattler_repodata_gateway::fetch::fetch_repo_data( +// channel.platform_url(platform), +// client, +// repodata_cache.to_path_buf(), +// FetchRepoDataOptions::default(), +// Some(Box::new(move |DownloadProgress { total, bytes }| { +// download_progress_progress_bar.set_length(total.unwrap_or(bytes)); +// download_progress_progress_bar.set_position(bytes); +// })), +// ) +// .await; +// +// // Error out if an error occurred, but also update the progress bar +// let result = match result { +// Err(e) => { +// let not_found = matches!(&e, FetchRepoDataError::NotFound(_)); +// if not_found && platform != Platform::NoArch { +// progress_bar.set_style(finished_progress_style()); +// progress_bar.finish_with_message("Not Found"); +// return Ok(None); +// } +// +// progress_bar.set_style(errored_progress_style()); +// progress_bar.finish_with_message("Error"); +// return Err(e.into()); +// } +// Ok(result) => result, +// }; +// +// // Notify that we are deserializing +// progress_bar.set_style(deserializing_progress_style()); +// progress_bar.set_message("Deserializing.."); +// +// // Deserialize the data. This is a hefty blocking operation so we spawn it as a tokio blocking +// // task. +// let repo_data_json_path = result.repo_data_json_path.clone(); +// match tokio::task::spawn_blocking(move || { +// SparseRepoData::new( +// channel, +// platform.to_string(), +// repo_data_json_path, +// Some(|record: &mut PackageRecord| { +// if record.name.as_normalized() == "python" { +// record.depends.push("pip".to_string()); +// } +// }), +// ) +// }) +// .await +// { +// Ok(Ok(repodata)) => { +// progress_bar.set_style(finished_progress_style()); +// let is_cache_hit = matches!( +// result.cache_result, +// CacheResult::CacheHit | CacheResult::CacheHitAfterFetch +// ); +// progress_bar.finish_with_message(if is_cache_hit { "Using cache" } else { "Done" }); +// Ok(Some(repodata)) +// } +// Ok(Err(err)) => { +// progress_bar.set_style(errored_progress_style()); +// progress_bar.finish_with_message("Error"); +// Err(err.into()) +// } +// Err(err) => { +// if let Ok(panic) = err.try_into_panic() { +// std::panic::resume_unwind(panic); +// } else { +// progress_bar.set_style(errored_progress_style()); +// progress_bar.finish_with_message("Cancelled.."); +// // Since the task was cancelled most likely the whole async stack is being cancelled. +// Err(anyhow::anyhow!("cancelled")) +// } +// } +// } +// } + +// /// Returns a friendly name for the specified channel. +// fn friendly_channel_name(channel: &Channel) -> String { +// channel +// .name +// .as_ref() +// .map_or_else(|| channel.canonical_name(), String::from) +// } +// +// /// Returns the style to use for a progressbar that is currently in progress. +// fn default_bytes_style() -> indicatif::ProgressStyle { +// indicatif::ProgressStyle::default_bar() +// .template("{spinner:.green} {prefix:20!} [{elapsed_precise}] [{bar:40!.bright.yellow/dim.white}] {bytes:>8} @ {smoothed_bytes_per_sec:8}").unwrap() +// .progress_chars("━━╾─") +// .with_key( +// "smoothed_bytes_per_sec", +// |s: &ProgressState, w: &mut dyn Write| match (s.pos(), s.elapsed().as_millis()) { +// (pos, elapsed_ms) if elapsed_ms > 0 => { +// write!(w, "{}/s", HumanBytes((pos as f64 * 1000_f64 / elapsed_ms as f64) as u64)).unwrap(); +// } +// _ => write!(w, "-").unwrap(), +// }, +// ) +// } +// /// Returns the style to use for a progressbar that is currently in progress. fn default_progress_style() -> indicatif::ProgressStyle { indicatif::ProgressStyle::default_bar() @@ -699,13 +695,13 @@ fn default_progress_style() -> indicatif::ProgressStyle { .progress_chars("━━╾─") } -/// Returns the style to use for a progressbar that is in Deserializing state. -fn deserializing_progress_style() -> indicatif::ProgressStyle { - indicatif::ProgressStyle::default_bar() - .template("{spinner:.green} {prefix:20!} [{elapsed_precise}] {wide_msg}") - .unwrap() - .progress_chars("━━╾─") -} +// /// Returns the style to use for a progressbar that is in Deserializing state. +// fn deserializing_progress_style() -> indicatif::ProgressStyle { +// indicatif::ProgressStyle::default_bar() +// .template("{spinner:.green} {prefix:20!} [{elapsed_precise}] {wide_msg}") +// .unwrap() +// .progress_chars("━━╾─") +// } /// Returns the style to use for a progressbar that is finished. fn finished_progress_style() -> indicatif::ProgressStyle { @@ -718,16 +714,16 @@ fn finished_progress_style() -> indicatif::ProgressStyle { .progress_chars("━━╾─") } -/// Returns the style to use for a progressbar that is in error state. -fn errored_progress_style() -> indicatif::ProgressStyle { - indicatif::ProgressStyle::default_bar() - .template(&format!( - "{} {{prefix:20!}} [{{elapsed_precise}}] {{msg:.bold.red}}", - console::style(console::Emoji("❌", " ")).red() - )) - .unwrap() - .progress_chars("━━╾─") -} +// /// Returns the style to use for a progressbar that is in error state. +// fn errored_progress_style() -> indicatif::ProgressStyle { +// indicatif::ProgressStyle::default_bar() +// .template(&format!( +// "{} {{prefix:20!}} [{{elapsed_precise}}] {{msg:.bold.red}}", +// console::style(console::Emoji("❌", " ")).red() +// )) +// .unwrap() +// .progress_chars("━━╾─") +// } /// Returns the style to use for a progressbar that is indeterminate and simply shows a spinner. fn long_running_progress_style() -> indicatif::ProgressStyle { diff --git a/crates/rattler_digest/Cargo.toml b/crates/rattler_digest/Cargo.toml index 1a1b9763a..427f11fd1 100644 --- a/crates/rattler_digest/Cargo.toml +++ b/crates/rattler_digest/Cargo.toml @@ -19,10 +19,11 @@ serde = { workspace = true, features = ["derive"], optional = true } serde_with = { workspace = true } sha2 = { workspace = true } tokio = { workspace = true, features = ["io-util"], optional = true } +generic-array = { workspace = true } [features] tokio = ["dep:tokio"] -serde = ["dep:serde"] +serde = ["dep:serde", "generic-array/serde"] [dev-dependencies] rstest = { workspace = true } diff --git a/crates/rattler_digest/src/serde.rs b/crates/rattler_digest/src/serde.rs index 779451122..b37a0d098 100644 --- a/crates/rattler_digest/src/serde.rs +++ b/crates/rattler_digest/src/serde.rs @@ -109,6 +109,17 @@ impl<'de, T: Digest + Default> DeserializeAs<'de, Output> for SerializableHas } } +// pub mod bytes { +// use serde::Serializer; +// +// fn serialize(&self, serializer: S) -> Result +// where +// S: Serializer, +// { +// crate::serde::serialize::(&self.0, serializer) +// } +// } + #[cfg(test)] mod test { use crate::serde::SerializableHash; diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index eee72ccc5..777a2e9dd 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -3,6 +3,7 @@ mod channel_config; mod error; mod local_subdir; mod remote_subdir; +mod repo_data; mod sharded_subdir; mod subdir; @@ -11,11 +12,12 @@ pub use channel_config::{ChannelConfig, SourceConfig}; pub use error::GatewayError; use crate::fetch::FetchRepoDataError; +use crate::gateway::repo_data::RepoData; use dashmap::{mapref::entry::Entry, DashMap}; use futures::{select_biased, stream::FuturesUnordered, StreamExt}; use itertools::Itertools; use local_subdir::LocalSubdirClient; -use rattler_conda_types::{Channel, PackageName, Platform, RepoDataRecord}; +use rattler_conda_types::{Channel, PackageName, Platform}; use reqwest::Client; use reqwest_middleware::ClientWithMiddleware; use std::{ @@ -128,7 +130,7 @@ impl Gateway { channels: ChannelIter, platforms: PlatformIter, names: PackageNameIter, - ) -> Result, GatewayError> + ) -> Result, GatewayError> where AsChannel: Borrow + Clone, ChannelIter: IntoIterator, @@ -138,8 +140,11 @@ impl Gateway { IntoPackageName: Into, { // Collect all the channels and platforms together + let channels = channels.into_iter().collect_vec(); + let channel_count = channels.len(); let channels_and_platforms = channels .into_iter() + .enumerate() .cartesian_product(platforms.into_iter()) .collect_vec(); @@ -147,10 +152,10 @@ impl Gateway { // becomes available. let mut subdirs = Vec::with_capacity(channels_and_platforms.len()); let mut pending_subdirs = FuturesUnordered::new(); - for (channel, platform) in channels_and_platforms.into_iter() { + for ((channel_idx, channel), platform) in channels_and_platforms.into_iter() { // Create a barrier so work that need this subdir can await it. let barrier = Arc::new(BarrierCell::new()); - subdirs.push(barrier.clone()); + subdirs.push((channel_idx, barrier.clone())); let inner = self.inner.clone(); pending_subdirs.push(async move { @@ -173,25 +178,24 @@ impl Gateway { let mut pending_records = FuturesUnordered::new(); // The resulting list of repodata records. - let mut result = Vec::new(); + let mut result = vec![RepoData::default(); channel_count]; // Loop until all pending package names have been fetched. loop { // Iterate over all pending package names and create futures to fetch them from all // subdirs. for pending_package_name in pending_package_names.drain(..) { - for subdir in subdirs.iter().cloned() { + for (channel_idx, subdir) in subdirs.iter().cloned() { let pending_package_name = pending_package_name.clone(); pending_records.push(async move { let barrier_cell = subdir.clone(); let subdir = barrier_cell.wait().await; match subdir.as_ref() { - Subdir::Found(subdir) => { - subdir - .get_or_fetch_package_records(&pending_package_name) - .await - } - Subdir::NotFound => Ok(Arc::from(vec![])), + Subdir::Found(subdir) => subdir + .get_or_fetch_package_records(&pending_package_name) + .await + .map(|records| (channel_idx, records)), + Subdir::NotFound => Ok((channel_idx, Arc::from(vec![]))), } }); } @@ -208,7 +212,7 @@ impl Gateway { // Handle any records that were fetched records = pending_records.select_next_some() => { - let records = records?; + let (channel_idx, records) = records?; // Extract the dependencies from the records and recursively add them to the // list of package names that we need to fetch. @@ -224,7 +228,11 @@ impl Gateway { } // Add the records to the result - result.extend_from_slice(&records); + if records.len() > 0 { + let result = &mut result[channel_idx]; + result.len += records.len(); + result.shards.push(records); + } } // All futures have been handled, all subdirectories have been loaded and all @@ -440,7 +448,8 @@ mod test { .await .unwrap(); - assert_eq!(records.len(), 45060); + let total_records: usize = records.iter().map(|r| r.len()).sum(); + assert_eq!(total_records, 45060); } #[tokio::test] @@ -458,7 +467,8 @@ mod test { .await .unwrap(); - assert_eq!(records.len(), 45060); + let total_records: usize = records.iter().map(|r| r.len()).sum(); + assert_eq!(total_records, 45060); } #[tokio::test(flavor = "multi_thread")] @@ -488,6 +498,7 @@ mod test { let end = Instant::now(); println!("{} records in {:?}", records.len(), end - start); - assert_eq!(records.len(), 84242); + let total_records: usize = records.iter().map(|r| r.len()).sum(); + assert_eq!(total_records, 84242); } } diff --git a/crates/rattler_repodata_gateway/src/gateway/repo_data.rs b/crates/rattler_repodata_gateway/src/gateway/repo_data.rs new file mode 100644 index 000000000..354c9eed8 --- /dev/null +++ b/crates/rattler_repodata_gateway/src/gateway/repo_data.rs @@ -0,0 +1,80 @@ +use rattler_conda_types::RepoDataRecord; +use std::iter::FusedIterator; +use std::sync::Arc; + +/// A container for RepoDataRecords that are returned from the [`Gateway`]. +/// +/// This struct references the same memory as the gateway therefor not +/// duplicating the records. +#[derive(Default, Clone)] +pub struct RepoData { + pub(super) shards: Vec>, + pub(super) len: usize, +} + +impl RepoData { + /// Returns an iterator over all the records in this instance. + pub fn iter(&self) -> RepoDataIterator<'_> { + RepoDataIterator { + records: self, + shard_idx: 0, + record_idx: 0, + total: 0, + } + } + + /// Returns the total number of records stored in this instance. + pub fn len(&self) -> usize { + self.len + } +} + +impl<'r> IntoIterator for &'r RepoData { + type Item = &'r RepoDataRecord; + type IntoIter = RepoDataIterator<'r>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +pub struct RepoDataIterator<'r> { + records: &'r RepoData, + shard_idx: usize, + record_idx: usize, + total: usize, +} + +impl<'r> Iterator for RepoDataIterator<'r> { + type Item = &'r RepoDataRecord; + + fn next(&mut self) -> Option { + while self.shard_idx < self.records.shards.len() { + let shard = &self.records.shards[self.shard_idx]; + if self.record_idx < shard.len() { + let record = &shard[self.record_idx]; + self.record_idx += 1; + self.total += 1; + return Some(record); + } else { + self.shard_idx += 1; + self.record_idx = 0; + } + } + + None + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.records.len - self.total; + (remaining, Some(remaining)) + } +} + +impl FusedIterator for RepoDataIterator<'_> {} + +impl ExactSizeIterator for RepoDataIterator<'_> { + fn len(&self) -> usize { + self.records.len - self.total + } +} diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs index 0802c6541..fe896a899 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs @@ -3,6 +3,7 @@ use crate::gateway::subdir::SubdirClient; use crate::GatewayError; use futures::TryFutureExt; use rattler_conda_types::{Channel, PackageName, PackageRecord, RepoDataRecord}; +use rattler_digest::Sha256Hash; use reqwest::StatusCode; use reqwest_middleware::ClientWithMiddleware; use serde::{Deserialize, Serialize}; @@ -30,11 +31,12 @@ impl ShardedSubdir { let channel = Channel::from_url(Url::parse("https://conda.anaconda.org/conda-forge").unwrap()); - let shard_base_url = Url::parse(&format!("https://fast.prefiks.dev/{subdir}/")).unwrap(); + let shard_base_url = + Url::parse(&format!("https://fast.prefiks.dev/conda-forge/{subdir}/")).unwrap(); // Fetch the sharded repodata from the remote server let repodata_shards_url = shard_base_url - .join("repodata_shards.json") + .join("repodata_shards.msgpack.zst") .expect("invalid shard base url"); let response = client .get(repodata_shards_url.clone()) @@ -56,8 +58,16 @@ impl ShardedSubdir { .map_err(FetchRepoDataError::from)?; // Parse the sharded repodata from the response - let sharded_repodata: ShardedRepodata = - response.json().await.map_err(FetchRepoDataError::from)?; + let sharded_repodata_compressed_bytes = + response.bytes().await.map_err(FetchRepoDataError::from)?; + let sharded_repodata_bytes = + decode_zst_bytes_async(sharded_repodata_compressed_bytes).await?; + let sharded_repodata = tokio_rayon::spawn(move || { + rmp_serde::from_slice::(&sharded_repodata_bytes) + }) + .await + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) + .map_err(FetchRepoDataError::IoError)?; // Determine the cache directory and make sure it exists. let cache_dir = cache_dir.join("shards_v1"); @@ -87,7 +97,7 @@ impl SubdirClient for ShardedSubdir { }; // Check if we already have the shard in the cache. - let shard_cache_path = self.cache_dir.join(&format!("{}.msgpack", shard.sha256)); + let shard_cache_path = self.cache_dir.join(&format!("{:x}.msgpack", shard)); // Read the cached shard match tokio::fs::read(&shard_cache_path).await { @@ -110,7 +120,7 @@ impl SubdirClient for ShardedSubdir { // Download the shard let shard_url = self .shard_base_url - .join(&format!("shards/{}.msgpack.zst", shard.sha256)) + .join(&format!("shards/{:x}.msgpack.zst", shard)) .expect("invalid shard url"); let shard_response = self @@ -188,16 +198,7 @@ async fn parse_records + Send + 'static>( pub struct ShardedRepodata { pub info: ShardedSubdirInfo, /// The individual shards indexed by package name. - pub shards: HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ShardRef { - // The sha256 hash of the shard - pub sha256: String, - - // The size of the shard. - pub size: u64, + pub shards: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/rattler_solve/src/lib.rs b/crates/rattler_solve/src/lib.rs index 8f2160661..e656807b8 100644 --- a/crates/rattler_solve/src/lib.rs +++ b/crates/rattler_solve/src/lib.rs @@ -159,3 +159,15 @@ impl<'a, S: SolverRepoData<'a>> IntoRepoData<'a, S> for S { self } } + +/// A helper struct that implements `IntoRepoData` for anything that can +/// iterate over `RepoDataRecord`s. +pub struct RepoDataIter(pub T); + +impl<'a, T: IntoIterator, S: SolverRepoData<'a>> IntoRepoData<'a, S> + for RepoDataIter +{ + fn into(self) -> S { + self.0.into_iter().collect() + } +} From ecd9099b1c3e7a3d6c5152885a18f0ebc9b2f7ea Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Mon, 29 Apr 2024 14:58:58 +0200 Subject: [PATCH 13/57] fix computing the URL for sharded repodata --- .../src/gateway/sharded_subdir.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs index fe896a899..63ac3b15e 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs @@ -183,11 +183,16 @@ async fn parse_records + Send + 'static>( let packages = itertools::chain(shard.packages.into_iter(), shard.packages_conda.into_iter()); Ok(packages - .map(|(file_name, package_record)| RepoDataRecord { - url: base_url.join(&file_name).unwrap(), - channel: channel_name.clone(), - package_record, - file_name, + .map(|(file_name, package_record)| { + // TODO: use compute_package_url ? + let subdir = &package_record.subdir; + let url = base_url.join(&format!("{subdir}/{file_name}")).unwrap(); + RepoDataRecord { + url, + channel: channel_name.clone(), + package_record, + file_name, + } }) .collect()) }) From 5d66e1f89965fd4804252c0b35cc10c97bcf3c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Mon, 29 Apr 2024 12:14:36 +0200 Subject: [PATCH 14/57] feat: create SparseRepoData from byte slices (#624) Fixes #619 --------- Co-authored-by: Bas Zalmstra --- crates/rattler_repodata_gateway/Cargo.toml | 4 +- .../src/sparse/mod.rs | 157 ++++++++++++++---- 2 files changed, 128 insertions(+), 33 deletions(-) diff --git a/crates/rattler_repodata_gateway/Cargo.toml b/crates/rattler_repodata_gateway/Cargo.toml index 1fee531c1..99162a6ee 100644 --- a/crates/rattler_repodata_gateway/Cargo.toml +++ b/crates/rattler_repodata_gateway/Cargo.toml @@ -14,6 +14,7 @@ readme.workspace = true async-trait = "0.1.77" async-compression = { workspace = true, features = ["gzip", "tokio", "bzip2", "zstd"] } blake2 = { workspace = true } +bytes = { workspace = true, optional = true } cache_control = { workspace = true } chrono = { workspace = true, features = ["std", "serde", "alloc", "clock"] } dashmap = "5.5.3" @@ -48,7 +49,6 @@ json-patch = { workspace = true } hex = { workspace = true, features = ["serde"] } rattler_networking = { path = "../rattler_networking", version = "0.20.3", default-features = false } zstd = { workspace = true } -bytes = "1.6.0" rayon = { workspace = true } tokio-rayon = { workspace = true } @@ -73,5 +73,5 @@ rattler_conda_types = { path = "../rattler_conda_types", version = "0.22.0", def default = ['native-tls'] native-tls = ['reqwest/native-tls', 'reqwest/native-tls-alpn'] rustls-tls = ['reqwest/rustls-tls'] -sparse = ["rattler_conda_types", "memmap2", "ouroboros", "superslice", "itertools", "serde_json/raw_value"] +sparse = ["rattler_conda_types", "memmap2", "ouroboros", "superslice", "itertools", "serde_json/raw_value", "bytes"] gateway = ["sparse"] diff --git a/crates/rattler_repodata_gateway/src/sparse/mod.rs b/crates/rattler_repodata_gateway/src/sparse/mod.rs index 7fb1094f4..e7b5571af 100644 --- a/crates/rattler_repodata_gateway/src/sparse/mod.rs +++ b/crates/rattler_repodata_gateway/src/sparse/mod.rs @@ -3,6 +3,7 @@ #![allow(clippy::mem_forget)] +use bytes::Bytes; use futures::{stream, StreamExt, TryFutureExt, TryStreamExt}; use itertools::Itertools; use rattler_conda_types::{ @@ -24,8 +25,7 @@ use superslice::Ext; /// A struct to enable loading records from a `repodata.json` file on demand. Since most of the time you /// don't need all the records from the `repodata.json` this can help provide some significant speedups. pub struct SparseRepoData { - /// Data structure that holds a memory mapped repodata.json file and an index into the the records - /// store in that data. + /// Data structure that holds an index into the the records stored in a repo data. inner: SparseRepoDataInner, /// The channel from which this data was downloaded. @@ -39,11 +39,26 @@ pub struct SparseRepoData { patch_record_fn: Option, } +enum SparseRepoDataInner { + /// The repo data is stored as a memory mapped file + Memmapped(MemmappedSparseRepoDataInner), + /// The repo data is stored as `Bytes` + Bytes(BytesSparseRepoDataInner), +} + +impl SparseRepoDataInner { + fn borrow_repo_data(&self) -> &LazyRepoData<'_> { + match self { + SparseRepoDataInner::Memmapped(inner) => inner.borrow_repo_data(), + SparseRepoDataInner::Bytes(inner) => inner.borrow_repo_data(), + } + } +} + /// A struct that holds a memory map of a `repodata.json` file and also a self-referential field which /// indexes the data in the memory map with a sparsely parsed json struct. See [`LazyRepoData`]. - #[ouroboros::self_referencing] -struct SparseRepoDataInner { +struct MemmappedSparseRepoDataInner { /// Memory map of the `repodata.json` file memory_map: memmap2::Mmap, @@ -54,8 +69,23 @@ struct SparseRepoDataInner { repo_data: LazyRepoData<'this>, } +/// A struct that holds a reference to the bytes of a `repodata.json` file and also a self-referential +/// field which indexes the data in the `bytes` with a sparsely parsed json struct. See [`LazyRepoData`]. +#[ouroboros::self_referencing] +struct BytesSparseRepoDataInner { + /// Bytes of the `repodata.json` file + bytes: Bytes, + + /// Sparsely parsed json content of the file's bytes. This data struct holds references into the + /// bytes so we have to use ouroboros to make this legal. + #[borrows(bytes)] + #[covariant] + repo_data: LazyRepoData<'this>, +} + impl SparseRepoData { /// Construct an instance of self from a file on disk and a [`Channel`]. + /// /// The `patch_function` can be used to patch the package record after it has been parsed /// (e.g. to add `pip` to `python`). pub fn new( @@ -67,17 +97,43 @@ impl SparseRepoData { let file = std::fs::File::open(path)?; let memory_map = unsafe { memmap2::Mmap::map(&file) }?; Ok(SparseRepoData { - inner: SparseRepoDataInnerTryBuilder { - memory_map, - repo_data_builder: |memory_map| serde_json::from_slice(memory_map.as_ref()), - } - .try_build()?, + inner: SparseRepoDataInner::Memmapped( + MemmappedSparseRepoDataInnerTryBuilder { + memory_map, + repo_data_builder: |memory_map| serde_json::from_slice(memory_map.as_ref()), + } + .try_build()?, + ), subdir: subdir.into(), channel, patch_record_fn: patch_function, }) } + /// Construct an instance of self from a bytes and a [`Channel`]. + /// + /// The `patch_function` can be used to patch the package record after it has been parsed + /// (e.g. to add `pip` to `python`). + pub fn from_bytes( + channel: Channel, + subdir: impl Into, + bytes: Bytes, + patch_function: Option, + ) -> Result { + Ok(Self { + inner: SparseRepoDataInner::Bytes( + BytesSparseRepoDataInnerTryBuilder { + bytes, + repo_data_builder: |bytes| serde_json::from_slice(bytes), + } + .try_build()?, + ), + channel, + subdir: subdir.into(), + patch_record_fn: patch_function, + }) + } + /// Returns an iterator over all package names in this repodata file. /// /// This works by iterating over all elements in the `packages` and `conda_packages` fields of @@ -350,7 +406,7 @@ fn deserialize_filename_and_raw_record<'d, D: Deserializer<'d>>( // // Since (in most cases) the repodata is already ordered by filename which does closely resemble // ordering by package name this sort operation will most likely be very fast. - entries.sort_by(|(a, _), (b, _)| a.package.cmp(b.package)); + entries.sort_unstable_by(|(a, _), (b, _)| a.package.cmp(b.package)); Ok(entries) } @@ -386,7 +442,8 @@ impl<'de> TryFrom<&'de str> for PackageFilename<'de> { #[cfg(test)] mod test { - use super::{load_repo_data_recursively, PackageFilename}; + use super::{load_repo_data_recursively, PackageFilename, SparseRepoData}; + use bytes::Bytes; use rattler_conda_types::{Channel, ChannelConfig, PackageName, RepoData, RepoDataRecord}; use rstest::rstest; use std::path::{Path, PathBuf}; @@ -395,23 +452,54 @@ mod test { Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data") } + fn default_repo_datas() -> Vec<(Channel, &'static str, PathBuf)> { + let channel_config = ChannelConfig::default_with_root_dir(std::env::current_dir().unwrap()); + vec![ + ( + Channel::from_str("conda-forge", &channel_config).unwrap(), + "noarch", + test_dir().join("channels/conda-forge/noarch/repodata.json"), + ), + ( + Channel::from_str("conda-forge", &channel_config).unwrap(), + "linux-64", + test_dir().join("channels/conda-forge/linux-64/repodata.json"), + ), + ] + } + + fn default_repo_data_bytes() -> Vec<(Channel, &'static str, Bytes)> { + default_repo_datas() + .into_iter() + .map(|(channel, subdir, path)| { + let bytes = std::fs::read(path).unwrap(); + (channel, subdir, bytes.into()) + }) + .collect() + } + + fn load_sparse_from_bytes( + repo_datas: &[(Channel, &'static str, Bytes)], + package_names: impl IntoIterator>, + ) -> Vec> { + let sparse: Vec<_> = repo_datas + .iter() + .map(|(channel, subdir, bytes)| { + SparseRepoData::from_bytes(channel.clone(), *subdir, bytes.clone(), None).unwrap() + }) + .collect(); + + let package_names = package_names + .into_iter() + .map(|name| PackageName::try_from(name.as_ref()).unwrap()); + SparseRepoData::load_records_recursive(&sparse, package_names, None).unwrap() + } + async fn load_sparse( package_names: impl IntoIterator>, ) -> Vec> { - let channel_config = ChannelConfig::default_with_root_dir(std::env::current_dir().unwrap()); load_repo_data_recursively( - [ - ( - Channel::from_str("conda-forge", &channel_config).unwrap(), - "noarch", - test_dir().join("channels/conda-forge/noarch/repodata.json"), - ), - ( - Channel::from_str("conda-forge", &channel_config).unwrap(), - "linux-64", - test_dir().join("channels/conda-forge/linux-64/repodata.json"), - ), - ], + default_repo_datas(), package_names .into_iter() .map(|name| PackageName::try_from(name.as_ref()).unwrap()), @@ -476,7 +564,7 @@ mod test { #[tokio::test] async fn test_sparse_numpy_dev() { - let sparse_empty_data = load_sparse([ + let package_names = vec![ "python", "cython", "compilers", @@ -499,13 +587,20 @@ mod test { "gitpython", "cffi", "pytz", - ]) - .await; + ]; - let total_records = sparse_empty_data - .iter() - .map(std::vec::Vec::len) - .sum::(); + // Memmapped + let sparse_empty_data = load_sparse(package_names.clone()).await; + + let total_records = sparse_empty_data.iter().map(Vec::len).sum::(); + + assert_eq!(total_records, 16065); + + // Bytes + let repo_datas = default_repo_data_bytes(); + let sparse_empty_data = load_sparse_from_bytes(&repo_datas, package_names); + + let total_records = sparse_empty_data.iter().map(Vec::len).sum::(); assert_eq!(total_records, 16065); } From d7cf370e563d3d02aec3268b0ad49e635c66e653 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 29 Apr 2024 15:28:47 +0200 Subject: [PATCH 15/57] fix: expose RepoData --- crates/rattler_repodata_gateway/src/gateway/mod.rs | 2 +- crates/rattler_repodata_gateway/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index 777a2e9dd..006051a02 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -10,9 +10,9 @@ mod subdir; pub use barrier_cell::BarrierCell; pub use channel_config::{ChannelConfig, SourceConfig}; pub use error::GatewayError; +pub use repo_data::RepoData; use crate::fetch::FetchRepoDataError; -use crate::gateway::repo_data::RepoData; use dashmap::{mapref::entry::Entry, DashMap}; use futures::{select_biased, stream::FuturesUnordered, StreamExt}; use itertools::Itertools; diff --git a/crates/rattler_repodata_gateway/src/lib.rs b/crates/rattler_repodata_gateway/src/lib.rs index b12c8941b..de880c389 100644 --- a/crates/rattler_repodata_gateway/src/lib.rs +++ b/crates/rattler_repodata_gateway/src/lib.rs @@ -69,4 +69,4 @@ mod utils; mod gateway; #[cfg(feature = "gateway")] -pub use gateway::{ChannelConfig, Gateway, GatewayBuilder, GatewayError, SourceConfig}; +pub use gateway::{ChannelConfig, Gateway, GatewayBuilder, GatewayError, SourceConfig, RepoData}; From 8672cfde289fad5e2edb5a908f889b751b73c5c8 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 29 Apr 2024 15:45:02 +0200 Subject: [PATCH 16/57] fix: base_url has a trailing slash --- .../src/gateway/sharded_subdir.rs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs index 63ac3b15e..766c2f864 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs @@ -7,6 +7,7 @@ use rattler_digest::Sha256Hash; use reqwest::StatusCode; use reqwest_middleware::ClientWithMiddleware; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; @@ -182,23 +183,33 @@ async fn parse_records + Send + 'static>( .map_err(FetchRepoDataError::IoError)?; let packages = itertools::chain(shard.packages.into_iter(), shard.packages_conda.into_iter()); + let base_url = add_trailing_slash(&base_url); Ok(packages - .map(|(file_name, package_record)| { - // TODO: use compute_package_url ? - let subdir = &package_record.subdir; - let url = base_url.join(&format!("{subdir}/{file_name}")).unwrap(); - RepoDataRecord { - url, - channel: channel_name.clone(), - package_record, - file_name, - } + .map(|(file_name, package_record)| RepoDataRecord { + url: base_url + .join(&file_name) + .expect("filename is not a valid url"), + channel: channel_name.clone(), + package_record, + file_name, }) .collect()) }) .await } +/// Returns the URL with a trailing slash if it doesn't already have one. +fn add_trailing_slash(url: &Url) -> Cow<'_, Url> { + let path = url.path(); + if path.ends_with('/') { + Cow::Borrowed(url) + } else { + let mut url = url.clone(); + url.set_path(&format!("{path}/")); + Cow::Owned(url) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ShardedRepodata { pub info: ShardedSubdirInfo, From b68965e7c78b1178b26f9ed041fb1d64f928f585 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 29 Apr 2024 16:05:56 +0200 Subject: [PATCH 17/57] feat: also adds non-recursive option --- Cargo.toml | 1 + crates/rattler_repodata_gateway/Cargo.toml | 3 +- .../src/gateway/mod.rs | 81 ++++++++++++++++--- .../src/gateway/repo_data.rs | 5 ++ 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4fa80d824..52d4f33f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ clap = { version = "4.5.4", features = ["derive"] } cmake = "0.1.50" console = { version = "0.15.8", features = ["windows-console-colors"] } criterion = "0.5" +dashmap = "5.5.3" difference = "2.0.0" digest = "0.10.7" dirs = "5.0.1" diff --git a/crates/rattler_repodata_gateway/Cargo.toml b/crates/rattler_repodata_gateway/Cargo.toml index 99162a6ee..4360897b7 100644 --- a/crates/rattler_repodata_gateway/Cargo.toml +++ b/crates/rattler_repodata_gateway/Cargo.toml @@ -17,8 +17,7 @@ blake2 = { workspace = true } bytes = { workspace = true, optional = true } cache_control = { workspace = true } chrono = { workspace = true, features = ["std", "serde", "alloc", "clock"] } -dashmap = "5.5.3" -elsa = "1.10.0" +dashmap = { workspace = true } humansize = { workspace = true } humantime = { workspace = true } futures = { workspace = true } diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index 006051a02..a3bdd3205 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -131,6 +131,67 @@ impl Gateway { platforms: PlatformIter, names: PackageNameIter, ) -> Result, GatewayError> + where + AsChannel: Borrow + Clone, + ChannelIter: IntoIterator, + PlatformIter: IntoIterator, + ::IntoIter: Clone, + PackageNameIter: IntoIterator, + IntoPackageName: Into, + { + self.load_records_inner(channels, platforms, names, true) + .await + } + + /// Loads all repodata records for the given channels, platforms and package names. + /// + /// This function will asynchronously load the repodata from all subdirectories (combination of + /// channels and platforms). + /// + /// Most processing will happen on the background so downloading and parsing can happen + /// simultaneously. + /// + /// Repodata is cached by the [`Gateway`] so calling this function twice with the same channels + /// will not result in the repodata being fetched twice. + /// + /// To also fetch the dependencies of the packages use [`Gateway::load_records_recursive`]. + pub async fn load_records< + AsChannel, + ChannelIter, + PlatformIter, + PackageNameIter, + IntoPackageName, + >( + &self, + channels: ChannelIter, + platforms: PlatformIter, + names: PackageNameIter, + ) -> Result, GatewayError> + where + AsChannel: Borrow + Clone, + ChannelIter: IntoIterator, + PlatformIter: IntoIterator, + ::IntoIter: Clone, + PackageNameIter: IntoIterator, + IntoPackageName: Into, + { + self.load_records_inner(channels, platforms, names, false) + .await + } + + async fn load_records_inner< + AsChannel, + ChannelIter, + PlatformIter, + PackageNameIter, + IntoPackageName, + >( + &self, + channels: ChannelIter, + platforms: PlatformIter, + names: PackageNameIter, + recursive: bool, + ) -> Result, GatewayError> where AsChannel: Borrow + Clone, ChannelIter: IntoIterator, @@ -214,15 +275,17 @@ impl Gateway { records = pending_records.select_next_some() => { let (channel_idx, records) = records?; - // Extract the dependencies from the records and recursively add them to the - // list of package names that we need to fetch. - for record in records.iter() { - for dependency in &record.package_record.depends { - let dependency_name = PackageName::new_unchecked( - dependency.split_once(' ').unwrap_or((dependency, "")).0, - ); - if seen.insert(dependency_name.clone()) { - pending_package_names.push(dependency_name.clone()); + if recursive { + // Extract the dependencies from the records and recursively add them to the + // list of package names that we need to fetch. + for record in records.iter() { + for dependency in &record.package_record.depends { + let dependency_name = PackageName::new_unchecked( + dependency.split_once(' ').unwrap_or((dependency, "")).0, + ); + if seen.insert(dependency_name.clone()) { + pending_package_names.push(dependency_name.clone()); + } } } } diff --git a/crates/rattler_repodata_gateway/src/gateway/repo_data.rs b/crates/rattler_repodata_gateway/src/gateway/repo_data.rs index 354c9eed8..6910a0569 100644 --- a/crates/rattler_repodata_gateway/src/gateway/repo_data.rs +++ b/crates/rattler_repodata_gateway/src/gateway/repo_data.rs @@ -27,6 +27,11 @@ impl RepoData { pub fn len(&self) -> usize { self.len } + + /// Returns true if there are no records stored in this instance. + pub fn is_empty(&self) -> bool { + self.len == 0 + } } impl<'r> IntoIterator for &'r RepoData { From 69e714fdb34ee28508260a2bf0d9b760c911f0a8 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Mon, 29 Apr 2024 17:00:38 +0200 Subject: [PATCH 18/57] cache the shard index file --- .../src/gateway/sharded_subdir.rs | 152 +++++++++++++++++- crates/rattler_repodata_gateway/src/lib.rs | 2 +- 2 files changed, 150 insertions(+), 4 deletions(-) diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs index 766c2f864..d8a501e99 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs @@ -1,6 +1,7 @@ use crate::fetch::{FetchRepoDataError, RepoDataNotFoundError}; use crate::gateway::subdir::SubdirClient; use crate::GatewayError; +use chrono::{DateTime, Utc}; use futures::TryFutureExt; use rattler_conda_types::{Channel, PackageName, PackageRecord, RepoDataRecord}; use rattler_digest::Sha256Hash; @@ -9,8 +10,10 @@ use reqwest_middleware::ClientWithMiddleware; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::collections::HashMap; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; +use std::time::SystemTime; +use tokio::io::AsyncReadExt; use url::Url; pub struct ShardedSubdir { @@ -21,6 +24,57 @@ pub struct ShardedSubdir { cache_dir: PathBuf, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CacheHeader { + pub etag: Option, + pub last_modified: Option>, +} + +/// Magic number that identifies the cache file format. +const MAGIC_NUMBER: &[u8] = b"SHARD-CACHE-V1"; + +async fn write_cache( + cache_file: &Path, + cache_header: CacheHeader, + cache_data: &[u8], +) -> Result<(), std::io::Error> { + let cache_header_bytes = rmp_serde::to_vec(&cache_header).unwrap(); + let header_length = cache_header_bytes.len() as usize; + // write it as 4 bytes + let content = [ + MAGIC_NUMBER, + &header_length.to_le_bytes(), + &cache_header_bytes, + cache_data, + ] + .concat(); + tokio::fs::write(&cache_file, content).await +} + +async fn read_cache_header( + cache_file: &Path, +) -> Result<(CacheHeader, tokio::io::BufReader), std::io::Error> { + let cache_data = tokio::fs::File::open(&cache_file).await?; + let mut reader = tokio::io::BufReader::new(cache_data); + let mut magic_number = [0; MAGIC_NUMBER.len()]; + reader.read_exact(&mut magic_number).await?; + if magic_number != MAGIC_NUMBER { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "invalid magic number", + )); + } + + let mut header_length_bytes = [0; 8]; + reader.read_exact(&mut header_length_bytes).await?; + let header_length = usize::from_le_bytes(header_length_bytes); + let mut header_bytes = vec![0; header_length]; + reader.read_exact(&mut header_bytes).await?; + let cache_header = rmp_serde::from_slice::(&header_bytes) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; + Ok((cache_header, reader)) +} + impl ShardedSubdir { pub async fn new( _channel: Channel, @@ -39,13 +93,60 @@ impl ShardedSubdir { let repodata_shards_url = shard_base_url .join("repodata_shards.msgpack.zst") .expect("invalid shard base url"); + + let cache_key = crate::utils::url_to_cache_filename(&repodata_shards_url); + let sharded_repodata_path = cache_dir.join(format!("{cache_key}.shard-cache-v1")); + + let mut cache_data = None; + if sharded_repodata_path.exists() { + // split the header from the sharded repodata + let mut result = None; + match read_cache_header(&sharded_repodata_path).await { + Ok((cache_header, file)) => { + result = Some((cache_header, file)); + } + Err(e) => { + tracing::info!("failed to read cache header: {:?}", e); + // remove the file and try to fetch it again, ignore any error here + tokio::fs::remove_file(&sharded_repodata_path).await.ok(); + } + } + + if let Some((cache_header, mut file)) = result { + // Cache times out after 1 hour + let mut rest = Vec::new(); + // parse the last_modified header + if let Some(last_modified) = &cache_header.last_modified { + let now: DateTime = SystemTime::now().into(); + let elapsed = now - last_modified; + if elapsed > chrono::Duration::hours(1) { + // insert the etag + cache_data = Some((cache_header, file)); + } else { + tracing::info!("Using cached sharded repodata - cache still valid"); + // read the rest of the file + file.read_to_end(&mut rest) + .await + .map_err(FetchRepoDataError::IoError)?; + return Ok(Self { + channel, + client, + sharded_repodata: rmp_serde::from_slice(&rest).unwrap(), + shard_base_url, + cache_dir, + }); + } + } + } + } + let response = client .get(repodata_shards_url.clone()) .send() .await .map_err(FetchRepoDataError::from)?; - // Check if the response was succesfull. + // Check if the response was successful. if response.status() == StatusCode::NOT_FOUND { return Err(GatewayError::FetchRepoDataError( FetchRepoDataError::NotFound(RepoDataNotFoundError::from( @@ -54,15 +155,60 @@ impl ShardedSubdir { )); }; + if let Some((cache_header, mut file)) = cache_data { + let found_etag = response.headers().get("etag").and_then(|v| v.to_str().ok()); + + if found_etag == cache_header.etag.as_deref() { + // The cached file is up to date + tracing::info!("Using cached sharded repodata - etag match"); + let mut data = Vec::new(); + file.read_to_end(&mut data) + .await + .map_err(FetchRepoDataError::IoError)?; + return Ok(Self { + channel, + client, + sharded_repodata: rmp_serde::from_slice(&data).unwrap(), + shard_base_url, + cache_dir, + }); + } + } + let response = response .error_for_status() .map_err(FetchRepoDataError::from)?; + let cache_header = CacheHeader { + etag: response + .headers() + .get("etag") + .map(|v| v.to_str().unwrap().to_string()), + last_modified: response + .headers() + .get("last-modified") + .and_then(|v| v.to_str().ok()) + .and_then(|v| DateTime::parse_from_rfc2822(v).ok()) + .map(|v| v.with_timezone(&Utc)), + }; + // Parse the sharded repodata from the response let sharded_repodata_compressed_bytes = response.bytes().await.map_err(FetchRepoDataError::from)?; let sharded_repodata_bytes = decode_zst_bytes_async(sharded_repodata_compressed_bytes).await?; + + // write the sharded repodata to disk + write_cache( + &sharded_repodata_path, + cache_header, + &sharded_repodata_bytes, + ) + .await + .map_err(|e| { + FetchRepoDataError::IoError(std::io::Error::new(std::io::ErrorKind::Other, e)) + })?; + let sharded_repodata = tokio_rayon::spawn(move || { rmp_serde::from_slice::(&sharded_repodata_bytes) }) @@ -71,7 +217,7 @@ impl ShardedSubdir { .map_err(FetchRepoDataError::IoError)?; // Determine the cache directory and make sure it exists. - let cache_dir = cache_dir.join("shards_v1"); + let cache_dir = cache_dir.join("shards-v1"); tokio::fs::create_dir_all(&cache_dir) .await .map_err(FetchRepoDataError::IoError)?; diff --git a/crates/rattler_repodata_gateway/src/lib.rs b/crates/rattler_repodata_gateway/src/lib.rs index de880c389..84d76290f 100644 --- a/crates/rattler_repodata_gateway/src/lib.rs +++ b/crates/rattler_repodata_gateway/src/lib.rs @@ -69,4 +69,4 @@ mod utils; mod gateway; #[cfg(feature = "gateway")] -pub use gateway::{ChannelConfig, Gateway, GatewayBuilder, GatewayError, SourceConfig, RepoData}; +pub use gateway::{ChannelConfig, Gateway, GatewayBuilder, GatewayError, RepoData, SourceConfig}; From 60ecf29883fcaefda7e738e8c34cd2c3a0300e35 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Mon, 29 Apr 2024 17:13:50 +0200 Subject: [PATCH 19/57] remove cache file if parsing fails --- .../src/gateway/sharded_subdir.rs | 60 ++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs index d8a501e99..66e90197c 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs @@ -33,6 +33,7 @@ pub struct CacheHeader { /// Magic number that identifies the cache file format. const MAGIC_NUMBER: &[u8] = b"SHARD-CACHE-V1"; +/// Write the shard index cache to disk. async fn write_cache( cache_file: &Path, cache_header: CacheHeader, @@ -51,6 +52,8 @@ async fn write_cache( tokio::fs::write(&cache_file, content).await } +/// Read the cache header - returns the cache header and the reader that can be +/// used to read the rest of the file. async fn read_cache_header( cache_file: &Path, ) -> Result<(CacheHeader, tokio::io::BufReader), std::io::Error> { @@ -124,17 +127,23 @@ impl ShardedSubdir { cache_data = Some((cache_header, file)); } else { tracing::info!("Using cached sharded repodata - cache still valid"); - // read the rest of the file - file.read_to_end(&mut rest) - .await - .map_err(FetchRepoDataError::IoError)?; - return Ok(Self { - channel, - client, - sharded_repodata: rmp_serde::from_slice(&rest).unwrap(), - shard_base_url, - cache_dir, - }); + match file.read_to_end(&mut rest).await { + Ok(_) => { + let sharded_repodata = rmp_serde::from_slice(&rest).unwrap(); + return Ok(Self { + channel, + client, + sharded_repodata, + shard_base_url, + cache_dir, + }); + } + Err(e) => { + tracing::info!("failed to read cache data: {:?}", e); + // remove the file and try to fetch it again, ignore any error here + tokio::fs::remove_file(&sharded_repodata_path).await.ok(); + } + } } } } @@ -161,17 +170,24 @@ impl ShardedSubdir { if found_etag == cache_header.etag.as_deref() { // The cached file is up to date tracing::info!("Using cached sharded repodata - etag match"); - let mut data = Vec::new(); - file.read_to_end(&mut data) - .await - .map_err(FetchRepoDataError::IoError)?; - return Ok(Self { - channel, - client, - sharded_repodata: rmp_serde::from_slice(&data).unwrap(), - shard_base_url, - cache_dir, - }); + let mut rest = Vec::new(); + match file.read_to_end(&mut rest).await { + Ok(_) => { + let sharded_repodata = rmp_serde::from_slice(&rest).unwrap(); + return Ok(Self { + channel, + client, + sharded_repodata, + shard_base_url, + cache_dir, + }); + } + Err(e) => { + tracing::info!("failed to read cache data: {:?}", e); + // remove the file and try to fetch it again, ignore any error here + tokio::fs::remove_file(&sharded_repodata_path).await.ok(); + } + } } } From c9421667a3803b2811af803469bd5988a6966da2 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 29 Apr 2024 23:23:24 +0200 Subject: [PATCH 20/57] feat: http cache semantics --- .cargo/config | 2 +- Cargo.toml | 1 + crates/rattler-bin/src/commands/create.rs | 17 +- .../rattler_conda_types/src/match_spec/mod.rs | 10 + crates/rattler_repodata_gateway/Cargo.toml | 8 +- .../src/gateway/mod.rs | 132 +++--- .../src/gateway/remote_subdir.rs | 4 +- .../src/gateway/repo_data.rs | 2 +- .../src/gateway/sharded_subdir.rs | 423 ++++++++++-------- .../src/gateway/subdir.rs | 4 +- 10 files changed, 354 insertions(+), 249 deletions(-) diff --git a/.cargo/config b/.cargo/config index fec10deea..29d762df1 100644 --- a/.cargo/config +++ b/.cargo/config @@ -75,4 +75,4 @@ rustflags = [ "-Wfuture_incompatible", "-Wnonstandard_style", "-Wrust_2018_idioms", -] +] \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 52d4f33f0..fd8b0acde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ google-cloud-auth = { version = "0.13.2", default-features = false } hex = "0.4.3" hex-literal = "0.4.1" http = "1.1" +http-cache-semantics = "2.1.0" humansize = "2.1.3" humantime = "2.1.0" indexmap = "2.2.6" diff --git a/crates/rattler-bin/src/commands/create.rs b/crates/rattler-bin/src/commands/create.rs index 0e0fefb2c..b8d1f2936 100644 --- a/crates/rattler-bin/src/commands/create.rs +++ b/crates/rattler-bin/src/commands/create.rs @@ -19,7 +19,7 @@ use rattler_conda_types::{ use rattler_networking::{ retry_policies::default_retry_policy, AuthenticationMiddleware, AuthenticationStorage, }; -use rattler_repodata_gateway::Gateway; +use rattler_repodata_gateway::{Gateway, RepoData}; use rattler_solve::{ libsolv_c::{self}, resolvo, ChannelPriority, RepoDataIter, SolverImpl, SolverTask, @@ -131,19 +131,19 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { .finish(); let start_load_repo_data = Instant::now(); - let package_names = specs.iter().filter_map(|spec| spec.name.as_ref().cloned()); let repo_data = wrap_in_async_progress( "loading repodata", gateway.load_records_recursive( channels, [install_platform, Platform::NoArch], - package_names, + specs.clone(), ), ) - .await?; + .await + .context("failed to load repodata")?; // Determine the number of recors - let total_records: usize = repo_data.iter().map(|r| r.len()).sum(); + let total_records: usize = repo_data.iter().map(RepoData::len).sum(); println!( "Loaded {} records in {:?}", total_records, @@ -197,10 +197,7 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { .collect(); let solver_task = SolverTask { - available_packages: repo_data - .iter() - .map(|repo_data| RepoDataIter(repo_data)) - .collect::>(), + available_packages: repo_data.iter().map(RepoDataIter).collect::>(), locked_packages, virtual_packages, specs, @@ -246,7 +243,7 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { for operation in &transaction.operations { match operation { TransactionOperation::Install(r) => { - println!("{} {}", console::style("+").green(), format_record(r)) + println!("{} {}", console::style("+").green(), format_record(r)); } TransactionOperation::Change { old, new } => { println!( diff --git a/crates/rattler_conda_types/src/match_spec/mod.rs b/crates/rattler_conda_types/src/match_spec/mod.rs index 1b3dc8e44..774a6dcd2 100644 --- a/crates/rattler_conda_types/src/match_spec/mod.rs +++ b/crates/rattler_conda_types/src/match_spec/mod.rs @@ -253,6 +253,16 @@ impl MatchSpec { } } +// Enable constructing a match spec from a package name. +impl From for MatchSpec { + fn from(value: PackageName) -> Self { + Self { + name: Some(value), + ..Default::default() + } + } +} + /// Similar to a [`MatchSpec`] but does not include the package name. This is useful in places /// where the package name is already known (e.g. `foo = "3.4.1 *cuda"`) #[serde_as] diff --git a/crates/rattler_repodata_gateway/Cargo.toml b/crates/rattler_repodata_gateway/Cargo.toml index 4360897b7..1dda57cf3 100644 --- a/crates/rattler_repodata_gateway/Cargo.toml +++ b/crates/rattler_repodata_gateway/Cargo.toml @@ -20,6 +20,7 @@ chrono = { workspace = true, features = ["std", "serde", "alloc", "clock"] } dashmap = { workspace = true } humansize = { workspace = true } humantime = { workspace = true } +http = { workspace = true, optional = true} futures = { workspace = true } reqwest = { workspace = true, features = ["stream", "http2"] } reqwest-middleware = { workspace = true } @@ -48,8 +49,9 @@ json-patch = { workspace = true } hex = { workspace = true, features = ["serde"] } rattler_networking = { path = "../rattler_networking", version = "0.20.3", default-features = false } zstd = { workspace = true } -rayon = { workspace = true } -tokio-rayon = { workspace = true } +rayon = { workspace = true, optional = true } +tokio-rayon = { workspace = true, optional = true } +http-cache-semantics = { workspace = true, optional = true, features = ["reqwest", "serde"] } [target.'cfg(unix)'.dependencies] libc = { workspace = true } @@ -73,4 +75,4 @@ default = ['native-tls'] native-tls = ['reqwest/native-tls', 'reqwest/native-tls-alpn'] rustls-tls = ['reqwest/rustls-tls'] sparse = ["rattler_conda_types", "memmap2", "ouroboros", "superslice", "itertools", "serde_json/raw_value", "bytes"] -gateway = ["sparse"] +gateway = ["sparse", "http", "http-cache-semantics", "rayon", "tokio-rayon"] diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index a3bdd3205..e87fa9a42 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -17,7 +17,7 @@ use dashmap::{mapref::entry::Entry, DashMap}; use futures::{select_biased, stream::FuturesUnordered, StreamExt}; use itertools::Itertools; use local_subdir::LocalSubdirClient; -use rattler_conda_types::{Channel, PackageName, Platform}; +use rattler_conda_types::{Channel, MatchSpec, PackageName, Platform}; use reqwest::Client; use reqwest_middleware::ClientWithMiddleware; use std::{ @@ -81,7 +81,7 @@ impl GatewayBuilder { Gateway { inner: Arc::new(GatewayInner { - subdirs: Default::default(), + subdirs: DashMap::default(), client, channel_config: self.channel_config, cache, @@ -96,6 +96,12 @@ pub struct Gateway { inner: Arc, } +impl Default for Gateway { + fn default() -> Self { + Gateway::new() + } +} + impl Gateway { /// Constructs a simple gateway with the default configuration. Use [`Gateway::builder`] if you /// want more control over how the gateway is constructed. @@ -108,53 +114,62 @@ impl Gateway { GatewayBuilder::default() } - /// Recursively loads all repodata records for the given channels, platforms and package names. + /// Recursively loads all repodata records for the given channels, platforms + /// and specs. /// - /// This function will asynchronously load the repodata from all subdirectories (combination of - /// channels and platforms) and recursively load all repodata records and the dependencies of - /// the those records. + /// The `specs` passed to this are the root specs. The function will also + /// recursively fetch the dependencies of the packages that match the root + /// specs. Only the dependencies of the records that match the root specs + /// will be fetched. /// - /// Most processing will happen on the background so downloading and parsing can happen - /// simultaneously. + /// This function will asynchronously load the repodata from all + /// subdirectories (combination of channels and platforms). /// - /// Repodata is cached by the [`Gateway`] so calling this function twice with the same channels - /// will not result in the repodata being fetched twice. + /// Most processing will happen on the background so downloading and + /// parsing can happen simultaneously. + /// + /// Repodata is cached by the [`Gateway`] so calling this function twice + /// with the same channels will not result in the repodata being fetched + /// twice. pub async fn load_records_recursive< AsChannel, ChannelIter, PlatformIter, PackageNameIter, - IntoPackageName, + IntoMatchSpec, >( &self, channels: ChannelIter, platforms: PlatformIter, - names: PackageNameIter, + specs: PackageNameIter, ) -> Result, GatewayError> where AsChannel: Borrow + Clone, ChannelIter: IntoIterator, PlatformIter: IntoIterator, ::IntoIter: Clone, - PackageNameIter: IntoIterator, - IntoPackageName: Into, + PackageNameIter: IntoIterator, + IntoMatchSpec: Into, { - self.load_records_inner(channels, platforms, names, true) + self.load_records_inner(channels, platforms, specs, true) .await } - /// Loads all repodata records for the given channels, platforms and package names. + /// Recursively loads all repodata records for the given channels, platforms + /// and specs. /// - /// This function will asynchronously load the repodata from all subdirectories (combination of - /// channels and platforms). + /// This function will asynchronously load the repodata from all + /// subdirectories (combination of channels and platforms). /// - /// Most processing will happen on the background so downloading and parsing can happen - /// simultaneously. + /// Most processing will happen on the background so downloading and parsing + /// can happen simultaneously. /// - /// Repodata is cached by the [`Gateway`] so calling this function twice with the same channels - /// will not result in the repodata being fetched twice. + /// Repodata is cached by the [`Gateway`] so calling this function twice + /// with the same channels will not result in the repodata being fetched + /// twice. /// - /// To also fetch the dependencies of the packages use [`Gateway::load_records_recursive`]. + /// To also fetch the dependencies of the packages use + /// [`Gateway::load_records_recursive`]. pub async fn load_records< AsChannel, ChannelIter, @@ -175,21 +190,26 @@ impl Gateway { PackageNameIter: IntoIterator, IntoPackageName: Into, { - self.load_records_inner(channels, platforms, names, false) - .await + self.load_records_inner( + channels, + platforms, + names.into_iter().map(|name| MatchSpec::from(name.into())), + false, + ) + .await } async fn load_records_inner< AsChannel, ChannelIter, PlatformIter, - PackageNameIter, - IntoPackageName, + MatchSpecIter, + IntoMatchSpec, >( &self, channels: ChannelIter, platforms: PlatformIter, - names: PackageNameIter, + specs: MatchSpecIter, recursive: bool, ) -> Result, GatewayError> where @@ -197,8 +217,8 @@ impl Gateway { ChannelIter: IntoIterator, PlatformIter: IntoIterator, ::IntoIter: Clone, - PackageNameIter: IntoIterator, - IntoPackageName: Into, + MatchSpecIter: IntoIterator, + IntoMatchSpec: Into, { // Collect all the channels and platforms together let channels = channels.into_iter().collect_vec(); @@ -213,26 +233,33 @@ impl Gateway { // becomes available. let mut subdirs = Vec::with_capacity(channels_and_platforms.len()); let mut pending_subdirs = FuturesUnordered::new(); - for ((channel_idx, channel), platform) in channels_and_platforms.into_iter() { + for ((channel_idx, channel), platform) in channels_and_platforms { // Create a barrier so work that need this subdir can await it. let barrier = Arc::new(BarrierCell::new()); subdirs.push((channel_idx, barrier.clone())); let inner = self.inner.clone(); pending_subdirs.push(async move { - let subdir = inner - .get_or_create_subdir(channel.borrow(), platform) - .await?; - barrier.set(subdir).expect("subdir was set twice"); - Ok(()) + match inner.get_or_create_subdir(channel.borrow(), platform).await { + Ok(subdir) => { + barrier.set(subdir).expect("subdir was set twice"); + Ok(()) + } + Err(e) => Err(e), + } }); } // Package names that we have or will issue requests for. - let mut seen = names.into_iter().map(Into::into).collect::>(); - - // Package names that we still need to fetch. - let mut pending_package_names = seen.iter().cloned().collect::>(); + let mut seen = HashSet::new(); + let mut pending_package_specs = Vec::new(); + for spec in specs { + let spec = spec.into(); + if let Some(name) = &spec.name { + seen.insert(name.clone()); + pending_package_specs.push(spec); + } + } // A list of futures to fetch the records for the pending package names. The main task // awaits these futures. @@ -245,18 +272,21 @@ impl Gateway { loop { // Iterate over all pending package names and create futures to fetch them from all // subdirs. - for pending_package_name in pending_package_names.drain(..) { + for spec in pending_package_specs.drain(..) { for (channel_idx, subdir) in subdirs.iter().cloned() { - let pending_package_name = pending_package_name.clone(); + let spec = spec.clone(); + let Some(package_name) = spec.name.clone() else { + continue; + }; pending_records.push(async move { let barrier_cell = subdir.clone(); let subdir = barrier_cell.wait().await; match subdir.as_ref() { Subdir::Found(subdir) => subdir - .get_or_fetch_package_records(&pending_package_name) + .get_or_fetch_package_records(&package_name) .await - .map(|records| (channel_idx, records)), - Subdir::NotFound => Ok((channel_idx, Arc::from(vec![]))), + .map(|records| (channel_idx, spec, records)), + Subdir::NotFound => Ok((channel_idx, spec, Arc::from(vec![]))), } }); } @@ -266,25 +296,27 @@ impl Gateway { select_biased! { // Handle any error that was emitted by the pending subdirs. subdir_result = pending_subdirs.select_next_some() => { - if let Err(subdir_result) = subdir_result { - return Err(subdir_result); - } + subdir_result?; } // Handle any records that were fetched records = pending_records.select_next_some() => { - let (channel_idx, records) = records?; + let (channel_idx, request_spec, records) = records?; if recursive { // Extract the dependencies from the records and recursively add them to the // list of package names that we need to fetch. for record in records.iter() { + if !request_spec.matches(&record.package_record) { + // Do not recurse into records that do not match to root spec. + continue; + } for dependency in &record.package_record.depends { let dependency_name = PackageName::new_unchecked( dependency.split_once(' ').unwrap_or((dependency, "")).0, ); if seen.insert(dependency_name.clone()) { - pending_package_names.push(dependency_name.clone()); + pending_package_specs.push(dependency_name.into()); } } } diff --git a/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs index d4aef2488..fd2cc35c4 100644 --- a/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs @@ -1,5 +1,5 @@ use super::{local_subdir::LocalSubdirClient, GatewayError, SourceConfig}; -use crate::fetch::{fetch_repo_data, FetchRepoDataOptions}; +use crate::fetch::{fetch_repo_data, FetchRepoDataOptions, Variant}; use crate::gateway::subdir::SubdirClient; use rattler_conda_types::{Channel, PackageName, Platform, RepoDataRecord}; use reqwest_middleware::ClientWithMiddleware; @@ -26,7 +26,7 @@ impl RemoteSubdirClient { cache_dir, FetchRepoDataOptions { cache_action: source_config.cache_action, - variant: Default::default(), + variant: Variant::default(), jlap_enabled: source_config.jlap_enabled, zstd_enabled: source_config.zstd_enabled, bz2_enabled: source_config.bz2_enabled, diff --git a/crates/rattler_repodata_gateway/src/gateway/repo_data.rs b/crates/rattler_repodata_gateway/src/gateway/repo_data.rs index 6910a0569..47579dc84 100644 --- a/crates/rattler_repodata_gateway/src/gateway/repo_data.rs +++ b/crates/rattler_repodata_gateway/src/gateway/repo_data.rs @@ -2,7 +2,7 @@ use rattler_conda_types::RepoDataRecord; use std::iter::FusedIterator; use std::sync::Arc; -/// A container for RepoDataRecords that are returned from the [`Gateway`]. +/// A container for `RepoDataRecord`s that are returned from the [`Gateway`]. /// /// This struct references the same memory as the gateway therefor not /// duplicating the records. diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs index 66e90197c..db01b0b10 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs @@ -1,19 +1,30 @@ -use crate::fetch::{FetchRepoDataError, RepoDataNotFoundError}; -use crate::gateway::subdir::SubdirClient; -use crate::GatewayError; -use chrono::{DateTime, Utc}; -use futures::TryFutureExt; +use crate::{ + fetch::{FetchRepoDataError, RepoDataNotFoundError}, + gateway::subdir::SubdirClient, + GatewayError, +}; +use bytes::Bytes; +use futures::{FutureExt, TryFutureExt}; +use http_cache_semantics::{AfterResponse, BeforeRequest, CachePolicy}; use rattler_conda_types::{Channel, PackageName, PackageRecord, RepoDataRecord}; use rattler_digest::Sha256Hash; -use reqwest::StatusCode; +use reqwest::{Response, StatusCode}; use reqwest_middleware::ClientWithMiddleware; use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::SystemTime; -use tokio::io::AsyncReadExt; +use std::{ + borrow::Cow, + collections::HashMap, + io::Write, + path::{Path, PathBuf}, + str::FromStr, + sync::Arc, + time::SystemTime, +}; +use tempfile::NamedTempFile; +use tokio::{ + fs::File, + io::{AsyncReadExt, BufReader}, +}; use url::Url; pub struct ShardedSubdir { @@ -24,41 +35,219 @@ pub struct ShardedSubdir { cache_dir: PathBuf, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CacheHeader { - pub etag: Option, - pub last_modified: Option>, -} - /// Magic number that identifies the cache file format. const MAGIC_NUMBER: &[u8] = b"SHARD-CACHE-V1"; -/// Write the shard index cache to disk. -async fn write_cache( - cache_file: &Path, - cache_header: CacheHeader, - cache_data: &[u8], -) -> Result<(), std::io::Error> { - let cache_header_bytes = rmp_serde::to_vec(&cache_header).unwrap(); - let header_length = cache_header_bytes.len() as usize; - // write it as 4 bytes - let content = [ - MAGIC_NUMBER, - &header_length.to_le_bytes(), - &cache_header_bytes, - cache_data, - ] - .concat(); - tokio::fs::write(&cache_file, content).await +// Fetches the shard index from the url or read it from the cache. +async fn fetch_index( + client: &ClientWithMiddleware, + shard_index_url: &Url, + cache_path: &Path, +) -> Result { + async fn from_response( + cache_path: &Path, + policy: CachePolicy, + response: Response, + ) -> Result { + // Read the bytes of the response + let bytes = response.bytes().await.map_err(FetchRepoDataError::from)?; + + // Decompress the bytes + let decoded_bytes = Bytes::from(decode_zst_bytes_async(bytes).await?); + + // Write the cache to disk if the policy allows it. + let cache_fut = if policy.is_storable() { + write_shard_index_cache(cache_path, policy, decoded_bytes.clone()) + .map_err(FetchRepoDataError::IoError) + .map_ok(Some) + .left_future() + } else { + // Otherwise delete the file + tokio::fs::remove_file(cache_path) + .map_ok_or_else( + |e| { + if e.kind() == std::io::ErrorKind::NotFound { + Ok(None) + } else { + Err(FetchRepoDataError::IoError(e)) + } + }, + |_| Ok(None), + ) + .right_future() + }; + + // Parse the bytes + let parse_fut = tokio_rayon::spawn(move || rmp_serde::from_slice(&decoded_bytes)) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) + .map_err(FetchRepoDataError::IoError); + + // Parse and write the file to disk concurrently + let (temp_file, sharded_index) = tokio::try_join!(cache_fut, parse_fut)?; + + // Persist the cache if succesfully updated the cache. + if let Some(temp_file) = temp_file { + temp_file + .persist(cache_path) + .map_err(FetchRepoDataError::from)?; + } + + Ok(sharded_index) + } + + // Construct the request to fetch the shard index. + let request = client + .get(shard_index_url.clone()) + .build() + .expect("invalid shard_index request"); + + // Try reading the cached file + if let Ok((cache_header, file)) = read_cached_index(cache_path).await { + match cache_header + .policy + .before_request(&request, SystemTime::now()) + { + BeforeRequest::Fresh(_) => { + if let Ok(shard_index) = read_shard_index_from_reader(file).await { + tracing::debug!("shard index cache hit"); + return Ok(shard_index); + } + } + BeforeRequest::Stale { + request: state_request, + .. + } => { + let request = convert_request(client.clone(), state_request.clone()) + .expect("failed to create request to check staleness"); + let response = client + .execute(request) + .await + .map_err(FetchRepoDataError::from)?; + + match cache_header.policy.after_response( + &state_request, + &response, + SystemTime::now(), + ) { + AfterResponse::NotModified(_policy, _) => { + // The cached file is still valid + if let Ok(shard_index) = read_shard_index_from_reader(file).await { + tracing::debug!("shard index cache was not modified"); + // If reading the file failed for some reason we'll just fetch it again. + return Ok(shard_index); + } + } + AfterResponse::Modified(policy, _) => { + // Close the old file so we can create a new one. + drop(file); + + tracing::debug!("shard index cache has become stale"); + return from_response(cache_path, policy, response).await; + } + } + } + } + }; + + tracing::debug!("fetching fresh shard index"); + + // Do a fresh requests + let response = client + .execute( + request + .try_clone() + .expect("failed to clone initial request"), + ) + .await + .map_err(FetchRepoDataError::from)?; + + // Check if the response was successful. + if response.status() == StatusCode::NOT_FOUND { + return Err(GatewayError::FetchRepoDataError( + FetchRepoDataError::NotFound(RepoDataNotFoundError::from( + response.error_for_status().unwrap_err(), + )), + )); + }; + + let policy = CachePolicy::new(&request, &response); + from_response(cache_path, policy, response).await +} + +/// Writes the shard index cache to disk. +async fn write_shard_index_cache( + cache_path: &Path, + policy: CachePolicy, + decoded_bytes: Bytes, +) -> std::io::Result { + let cache_path = cache_path.to_path_buf(); + tokio::task::spawn_blocking(move || { + // Write the header + let cache_header = rmp_serde::encode::to_vec(&CacheHeader { policy }) + .expect("failed to encode cache header"); + let cache_dir = cache_path + .parent() + .expect("the cache path must have a parent"); + std::fs::create_dir_all(cache_dir)?; + let mut temp_file = tempfile::Builder::new() + .tempfile_in(cache_dir) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + temp_file.write_all(MAGIC_NUMBER)?; + temp_file.write_all(&(cache_header.len() as u32).to_le_bytes())?; + temp_file.write_all(&cache_header)?; + temp_file.write_all(decoded_bytes.as_ref())?; + + Ok(temp_file) + }) + .map_err(|e| match e.try_into_panic() { + Ok(payload) => std::panic::resume_unwind(payload), + Err(e) => std::io::Error::new(std::io::ErrorKind::Other, e), + }) + .await? +} + +/// Converts from a `http::request::Parts` into a `reqwest::Request`. +fn convert_request( + client: ClientWithMiddleware, + parts: http::request::Parts, +) -> Result { + client + .request( + parts.method, + Url::from_str(&parts.uri.to_string()).expect("uris should be the same"), + ) + .headers(parts.headers) + .version(parts.version) + .build() +} + +/// Read the shard index from a reader and deserialize it. +async fn read_shard_index_from_reader( + mut reader: BufReader, +) -> std::io::Result { + // Read the file to memory + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + + // Deserialize the bytes + tokio_rayon::spawn(move || rmp_serde::from_slice(&bytes)) + .await + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) } -/// Read the cache header - returns the cache header and the reader that can be -/// used to read the rest of the file. -async fn read_cache_header( - cache_file: &Path, -) -> Result<(CacheHeader, tokio::io::BufReader), std::io::Error> { - let cache_data = tokio::fs::File::open(&cache_file).await?; - let mut reader = tokio::io::BufReader::new(cache_data); +/// Cache information stored at the start of the cache file. +#[derive(Clone, Debug, Serialize, Deserialize)] +struct CacheHeader { + pub policy: CachePolicy, +} + +/// Try reading the cache file from disk. +async fn read_cached_index(cache_path: &Path) -> std::io::Result<(CacheHeader, BufReader)> { + // Open the file for reading + let file = File::open(cache_path).await?; + let mut reader = BufReader::new(file); + + // Read the magic from the file let mut magic_number = [0; MAGIC_NUMBER.len()]; reader.read_exact(&mut magic_number).await?; if magic_number != MAGIC_NUMBER { @@ -68,13 +257,17 @@ async fn read_cache_header( )); } - let mut header_length_bytes = [0; 8]; - reader.read_exact(&mut header_length_bytes).await?; - let header_length = usize::from_le_bytes(header_length_bytes); + // Read the length of the header + let header_length = reader.read_u32_le().await? as usize; + + // Read the header from the file let mut header_bytes = vec![0; header_length]; reader.read_exact(&mut header_bytes).await?; + + // Deserialize the header let cache_header = rmp_serde::from_slice::(&header_bytes) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; + Ok((cache_header, reader)) } @@ -99,138 +292,8 @@ impl ShardedSubdir { let cache_key = crate::utils::url_to_cache_filename(&repodata_shards_url); let sharded_repodata_path = cache_dir.join(format!("{cache_key}.shard-cache-v1")); - - let mut cache_data = None; - if sharded_repodata_path.exists() { - // split the header from the sharded repodata - let mut result = None; - match read_cache_header(&sharded_repodata_path).await { - Ok((cache_header, file)) => { - result = Some((cache_header, file)); - } - Err(e) => { - tracing::info!("failed to read cache header: {:?}", e); - // remove the file and try to fetch it again, ignore any error here - tokio::fs::remove_file(&sharded_repodata_path).await.ok(); - } - } - - if let Some((cache_header, mut file)) = result { - // Cache times out after 1 hour - let mut rest = Vec::new(); - // parse the last_modified header - if let Some(last_modified) = &cache_header.last_modified { - let now: DateTime = SystemTime::now().into(); - let elapsed = now - last_modified; - if elapsed > chrono::Duration::hours(1) { - // insert the etag - cache_data = Some((cache_header, file)); - } else { - tracing::info!("Using cached sharded repodata - cache still valid"); - match file.read_to_end(&mut rest).await { - Ok(_) => { - let sharded_repodata = rmp_serde::from_slice(&rest).unwrap(); - return Ok(Self { - channel, - client, - sharded_repodata, - shard_base_url, - cache_dir, - }); - } - Err(e) => { - tracing::info!("failed to read cache data: {:?}", e); - // remove the file and try to fetch it again, ignore any error here - tokio::fs::remove_file(&sharded_repodata_path).await.ok(); - } - } - } - } - } - } - - let response = client - .get(repodata_shards_url.clone()) - .send() - .await - .map_err(FetchRepoDataError::from)?; - - // Check if the response was successful. - if response.status() == StatusCode::NOT_FOUND { - return Err(GatewayError::FetchRepoDataError( - FetchRepoDataError::NotFound(RepoDataNotFoundError::from( - response.error_for_status().unwrap_err(), - )), - )); - }; - - if let Some((cache_header, mut file)) = cache_data { - let found_etag = response.headers().get("etag").and_then(|v| v.to_str().ok()); - - if found_etag == cache_header.etag.as_deref() { - // The cached file is up to date - tracing::info!("Using cached sharded repodata - etag match"); - let mut rest = Vec::new(); - match file.read_to_end(&mut rest).await { - Ok(_) => { - let sharded_repodata = rmp_serde::from_slice(&rest).unwrap(); - return Ok(Self { - channel, - client, - sharded_repodata, - shard_base_url, - cache_dir, - }); - } - Err(e) => { - tracing::info!("failed to read cache data: {:?}", e); - // remove the file and try to fetch it again, ignore any error here - tokio::fs::remove_file(&sharded_repodata_path).await.ok(); - } - } - } - } - - let response = response - .error_for_status() - .map_err(FetchRepoDataError::from)?; - - let cache_header = CacheHeader { - etag: response - .headers() - .get("etag") - .map(|v| v.to_str().unwrap().to_string()), - last_modified: response - .headers() - .get("last-modified") - .and_then(|v| v.to_str().ok()) - .and_then(|v| DateTime::parse_from_rfc2822(v).ok()) - .map(|v| v.with_timezone(&Utc)), - }; - - // Parse the sharded repodata from the response - let sharded_repodata_compressed_bytes = - response.bytes().await.map_err(FetchRepoDataError::from)?; - let sharded_repodata_bytes = - decode_zst_bytes_async(sharded_repodata_compressed_bytes).await?; - - // write the sharded repodata to disk - write_cache( - &sharded_repodata_path, - cache_header, - &sharded_repodata_bytes, - ) - .await - .map_err(|e| { - FetchRepoDataError::IoError(std::io::Error::new(std::io::ErrorKind::Other, e)) - })?; - - let sharded_repodata = tokio_rayon::spawn(move || { - rmp_serde::from_slice::(&sharded_repodata_bytes) - }) - .await - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) - .map_err(FetchRepoDataError::IoError)?; + let sharded_repodata = + fetch_index(&client, &repodata_shards_url, &sharded_repodata_path).await?; // Determine the cache directory and make sure it exists. let cache_dir = cache_dir.join("shards-v1"); @@ -241,8 +304,8 @@ impl ShardedSubdir { Ok(Self { channel, client, - sharded_repodata, shard_base_url, + sharded_repodata, cache_dir, }) } @@ -260,7 +323,7 @@ impl SubdirClient for ShardedSubdir { }; // Check if we already have the shard in the cache. - let shard_cache_path = self.cache_dir.join(&format!("{:x}.msgpack", shard)); + let shard_cache_path = self.cache_dir.join(format!("{shard:x}.msgpack")); // Read the cached shard match tokio::fs::read(&shard_cache_path).await { @@ -283,7 +346,7 @@ impl SubdirClient for ShardedSubdir { // Download the shard let shard_url = self .shard_base_url - .join(&format!("shards/{:x}.msgpack.zst", shard)) + .join(&format!("shards/{shard:x}.msgpack.zst")) .expect("invalid shard url"); let shard_response = self diff --git a/crates/rattler_repodata_gateway/src/gateway/subdir.rs b/crates/rattler_repodata_gateway/src/gateway/subdir.rs index a8dc1e4f5..c83dda100 100644 --- a/crates/rattler_repodata_gateway/src/gateway/subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/subdir.rs @@ -27,7 +27,7 @@ impl SubdirData { pub fn from_client(client: C) -> Self { Self { client: Arc::new(client), - records: Default::default(), + records: DashMap::default(), } } @@ -101,7 +101,7 @@ impl SubdirData { .map_err(JoinError::try_into_panic) { Ok(Ok(records)) => records, - Ok(Err(err)) => return Err(GatewayError::from(err)), + Ok(Err(err)) => return Err(err), Err(Ok(panic)) => std::panic::resume_unwind(panic), Err(Err(_)) => { return Err(GatewayError::IoError( From 4dc3c96542158f058f6a9cbff23cba14648366bf Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Tue, 30 Apr 2024 13:34:30 +0200 Subject: [PATCH 21/57] feat: add token request --- Cargo.toml | 1 + .../src/authentication_middleware.rs | 7 +- crates/rattler_repodata_gateway/Cargo.toml | 5 +- .../src/gateway/builder.rs | 65 ++++ .../src/gateway/error.rs | 3 + .../src/gateway/mod.rs | 89 ++---- .../src/gateway/repo_data.rs | 10 +- .../src/gateway/sharded_subdir.rs | 298 +++++++++++++++--- 8 files changed, 359 insertions(+), 119 deletions(-) create mode 100644 crates/rattler_repodata_gateway/src/gateway/builder.rs diff --git a/Cargo.toml b/Cargo.toml index fd8b0acde..6b1ca1799 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,7 @@ nom = "7.1.3" num_cpus = "1.16.0" once_cell = "1.19.0" ouroboros = "0.18.3" +parking_lot = "0.12.2" pathdiff = "0.2.1" pep440_rs = { version = "0.5.0" } pep508_rs = { version = "0.4.2" } diff --git a/crates/rattler_networking/src/authentication_middleware.rs b/crates/rattler_networking/src/authentication_middleware.rs index 6c9870ad0..3b601b979 100644 --- a/crates/rattler_networking/src/authentication_middleware.rs +++ b/crates/rattler_networking/src/authentication_middleware.rs @@ -8,6 +8,7 @@ use reqwest_middleware::{Middleware, Next}; use std::path::{Path, PathBuf}; use std::sync::OnceLock; use url::Url; + /// `reqwest` middleware to authenticate requests #[derive(Clone, Default)] pub struct AuthenticationMiddleware { @@ -22,8 +23,12 @@ impl Middleware for AuthenticationMiddleware { extensions: &mut http::Extensions, next: Next<'_>, ) -> reqwest_middleware::Result { - let url = req.url().clone(); + // If an `Authorization` header is already present, don't authenticate + if req.headers().get(reqwest::header::AUTHORIZATION).is_some() { + return next.run(req, extensions).await; + } + let url = req.url().clone(); match self.auth_storage.get_by_url(url) { Err(_) => { // Forward error to caller (invalid URL) diff --git a/crates/rattler_repodata_gateway/Cargo.toml b/crates/rattler_repodata_gateway/Cargo.toml index 1dda57cf3..beb22fb7a 100644 --- a/crates/rattler_repodata_gateway/Cargo.toml +++ b/crates/rattler_repodata_gateway/Cargo.toml @@ -20,7 +20,7 @@ chrono = { workspace = true, features = ["std", "serde", "alloc", "clock"] } dashmap = { workspace = true } humansize = { workspace = true } humantime = { workspace = true } -http = { workspace = true, optional = true} +http = { workspace = true, optional = true } futures = { workspace = true } reqwest = { workspace = true, features = ["stream", "http2"] } reqwest-middleware = { workspace = true } @@ -52,6 +52,7 @@ zstd = { workspace = true } rayon = { workspace = true, optional = true } tokio-rayon = { workspace = true, optional = true } http-cache-semantics = { workspace = true, optional = true, features = ["reqwest", "serde"] } +parking_lot = { workspace = true, optional = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } @@ -75,4 +76,4 @@ default = ['native-tls'] native-tls = ['reqwest/native-tls', 'reqwest/native-tls-alpn'] rustls-tls = ['reqwest/rustls-tls'] sparse = ["rattler_conda_types", "memmap2", "ouroboros", "superslice", "itertools", "serde_json/raw_value", "bytes"] -gateway = ["sparse", "http", "http-cache-semantics", "rayon", "tokio-rayon"] +gateway = ["sparse", "http", "http-cache-semantics", "rayon", "tokio-rayon", "parking_lot"] diff --git a/crates/rattler_repodata_gateway/src/gateway/builder.rs b/crates/rattler_repodata_gateway/src/gateway/builder.rs new file mode 100644 index 000000000..438a2fdfc --- /dev/null +++ b/crates/rattler_repodata_gateway/src/gateway/builder.rs @@ -0,0 +1,65 @@ +use crate::gateway::GatewayInner; +use crate::{ChannelConfig, Gateway}; +use dashmap::DashMap; +use reqwest::Client; +use reqwest_middleware::ClientWithMiddleware; +use std::path::PathBuf; +use std::sync::Arc; + +/// A builder for constructing a [`Gateway`]. +#[derive(Default)] +pub struct GatewayBuilder { + channel_config: ChannelConfig, + client: Option, + cache: Option, +} + +impl GatewayBuilder { + /// New instance of the builder. + pub fn new() -> Self { + Self::default() + } + + /// Set the client to use for fetching repodata. + #[must_use] + pub fn with_client(mut self, client: ClientWithMiddleware) -> Self { + self.client = Some(client); + self + } + + /// Set the channel configuration to use for fetching repodata. + #[must_use] + pub fn with_channel_config(mut self, channel_config: ChannelConfig) -> Self { + self.channel_config = channel_config; + self + } + + /// Set the directory to use for caching repodata. + #[must_use] + pub fn with_cache_dir(mut self, cache: impl Into) -> Self { + self.cache = Some(cache.into()); + self + } + + /// Finish the construction of the gateway returning a constructed gateway. + pub fn finish(self) -> Gateway { + let client = self + .client + .unwrap_or_else(|| ClientWithMiddleware::from(Client::new())); + + let cache = self.cache.unwrap_or_else(|| { + dirs::cache_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("rattler/cache") + }); + + Gateway { + inner: Arc::new(GatewayInner { + subdirs: DashMap::default(), + client, + channel_config: self.channel_config, + cache, + }), + } + } +} diff --git a/crates/rattler_repodata_gateway/src/gateway/error.rs b/crates/rattler_repodata_gateway/src/gateway/error.rs index 66cba90c3..a132716b0 100644 --- a/crates/rattler_repodata_gateway/src/gateway/error.rs +++ b/crates/rattler_repodata_gateway/src/gateway/error.rs @@ -12,4 +12,7 @@ pub enum GatewayError { #[error("{0}")] UnsupportedUrl(String), + + #[error("{0}")] + Generic(String), } diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index e87fa9a42..fdc992b6e 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -1,4 +1,5 @@ mod barrier_cell; +mod builder; mod channel_config; mod error; mod local_subdir; @@ -8,6 +9,7 @@ mod sharded_subdir; mod subdir; pub use barrier_cell::BarrierCell; +pub use builder::GatewayBuilder; pub use channel_config::{ChannelConfig, SourceConfig}; pub use error::GatewayError; pub use repo_data::RepoData; @@ -18,7 +20,6 @@ use futures::{select_biased, stream::FuturesUnordered, StreamExt}; use itertools::Itertools; use local_subdir::LocalSubdirClient; use rattler_conda_types::{Channel, MatchSpec, PackageName, Platform}; -use reqwest::Client; use reqwest_middleware::ClientWithMiddleware; use std::{ borrow::Borrow, @@ -29,68 +30,18 @@ use std::{ use subdir::{Subdir, SubdirData}; use tokio::sync::broadcast; -// TODO: Instead of using `Channel` it would be better if we could use just the base url. Maybe we -// can wrap that in a type. Mamba has the CondaUrl class. - -/// A builder for constructing a [`Gateway`]. -#[derive(Default)] -pub struct GatewayBuilder { - channel_config: ChannelConfig, - client: Option, - cache: Option, -} - -impl GatewayBuilder { - /// New instance of the builder. - pub fn new() -> Self { - Self::default() - } - - /// Set the client to use for fetching repodata. - #[must_use] - pub fn with_client(mut self, client: ClientWithMiddleware) -> Self { - self.client = Some(client); - self - } - - /// Set the channel configuration to use for fetching repodata. - #[must_use] - pub fn with_channel_config(mut self, channel_config: ChannelConfig) -> Self { - self.channel_config = channel_config; - self - } - - /// Set the directory to use for caching repodata. - #[must_use] - pub fn with_cache_dir(mut self, cache: impl Into) -> Self { - self.cache = Some(cache.into()); - self - } - - /// Finish the construction of the gateway returning a constructed gateway. - pub fn finish(self) -> Gateway { - let client = self - .client - .unwrap_or_else(|| ClientWithMiddleware::from(Client::new())); - - let cache = self.cache.unwrap_or_else(|| { - dirs::cache_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("rattler/cache") - }); - - Gateway { - inner: Arc::new(GatewayInner { - subdirs: DashMap::default(), - client, - channel_config: self.channel_config, - cache, - }), - } - } -} - -/// Central access point for fetching repodata records. +/// Central access point for high level queries about [`RepoDataRecord`]s from +/// different channels. +/// +/// The gateway is responsible for fetching and caching repodata. Requests are +/// deduplicated which means that if multiple requests are made for the same +/// repodata only the first request will actually fetch the data. All other +/// requests will wait for the first request to complete and then return the +/// same data. +/// +/// The gateway is thread-safe and can be shared between multiple threads. The +/// gateway struct itself uses internal reference counting and is cheaply +/// clonable. There is no need to wrap the gateway in an `Arc`. #[derive(Clone)] pub struct Gateway { inner: Arc, @@ -357,12 +308,14 @@ struct GatewayInner { } impl GatewayInner { - /// Returns the [`Subdir`] for the given channel and platform. This function will create the - /// [`Subdir`] if it does not exist yet, otherwise it will return the previously created subdir. + /// Returns the [`Subdir`] for the given channel and platform. This + /// function will create the [`Subdir`] if it does not exist yet, otherwise + /// it will return the previously created subdir. /// - /// If multiple threads request the same subdir their requests will be coalesced, and they will - /// all receive the same subdir. If an error occurs while creating the subdir all waiting tasks - /// will also return an error. + /// If multiple threads request the same subdir their requests will be + /// coalesced, and they will all receive the same subdir. If an error + /// occurs while creating the subdir all waiting tasks will also return an + /// error. async fn get_or_create_subdir( &self, channel: &Channel, diff --git a/crates/rattler_repodata_gateway/src/gateway/repo_data.rs b/crates/rattler_repodata_gateway/src/gateway/repo_data.rs index 47579dc84..517def1af 100644 --- a/crates/rattler_repodata_gateway/src/gateway/repo_data.rs +++ b/crates/rattler_repodata_gateway/src/gateway/repo_data.rs @@ -2,10 +2,13 @@ use rattler_conda_types::RepoDataRecord; use std::iter::FusedIterator; use std::sync::Arc; -/// A container for `RepoDataRecord`s that are returned from the [`Gateway`]. +/// A container for [`RepoDataRecord`]s that are returned from the [`Gateway`]. /// -/// This struct references the same memory as the gateway therefor not -/// duplicating the records. +/// This struct references the same memory as the `Gateway` therefor not +/// duplicating the records in memory. +/// +/// `RepoData` uses internal reference counting, therefor it is relatively +/// cheap to clone. #[derive(Default, Clone)] pub struct RepoData { pub(super) shards: Vec>, @@ -43,6 +46,7 @@ impl<'r> IntoIterator for &'r RepoData { } } +/// An iterator over the records in a [`RepoData`] instance. pub struct RepoDataIterator<'r> { records: &'r RepoData, shard_idx: usize, diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs index db01b0b10..d3d4516d4 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs @@ -1,16 +1,24 @@ +use crate::gateway::PendingOrFetched; +use crate::utils::url_to_cache_filename; use crate::{ fetch::{FetchRepoDataError, RepoDataNotFoundError}, gateway::subdir::SubdirClient, GatewayError, }; use bytes::Bytes; +use chrono::{DateTime, TimeDelta, Utc}; use futures::{FutureExt, TryFutureExt}; -use http_cache_semantics::{AfterResponse, BeforeRequest, CachePolicy}; +use http::header::CACHE_CONTROL; +use http::{HeaderMap, HeaderValue, Method, Uri}; +use http_cache_semantics::{AfterResponse, BeforeRequest, CachePolicy, RequestLike}; +use itertools::Either; +use parking_lot::Mutex; use rattler_conda_types::{Channel, PackageName, PackageRecord, RepoDataRecord}; use rattler_digest::Sha256Hash; use reqwest::{Response, StatusCode}; use reqwest_middleware::ClientWithMiddleware; use serde::{Deserialize, Serialize}; +use std::ops::Add; use std::{ borrow::Cow, collections::HashMap, @@ -30,19 +38,56 @@ use url::Url; pub struct ShardedSubdir { channel: Channel, client: ClientWithMiddleware, - shard_base_url: Url, + channel_base_url: Url, + token_client: TokenClient, sharded_repodata: ShardedRepodata, cache_dir: PathBuf, } /// Magic number that identifies the cache file format. const MAGIC_NUMBER: &[u8] = b"SHARD-CACHE-V1"; +const REPODATA_SHARDS_FILENAME: &str = "repodata_shards.msgpack.zst"; + +struct SimpleRequest { + uri: Uri, + method: Method, + headers: HeaderMap, +} + +impl SimpleRequest { + pub fn get(url: &Url) -> Self { + Self { + uri: Uri::from_str(url.as_str()).expect("failed to convert Url to Uri"), + method: Method::GET, + headers: HeaderMap::default(), + } + } +} + +impl RequestLike for SimpleRequest { + fn method(&self) -> &Method { + &self.method + } + + fn uri(&self) -> Uri { + self.uri.clone() + } + + fn headers(&self) -> &HeaderMap { + &self.headers + } + + fn is_same_uri(&self, other: &Uri) -> bool { + &self.uri() == other + } +} // Fetches the shard index from the url or read it from the cache. async fn fetch_index( - client: &ClientWithMiddleware, - shard_index_url: &Url, - cache_path: &Path, + client: ClientWithMiddleware, + channel_base_url: &Url, + token_client: &TokenClient, + cache_dir: &Path, ) -> Result { async fn from_response( cache_path: &Path, @@ -95,17 +140,24 @@ async fn fetch_index( Ok(sharded_index) } - // Construct the request to fetch the shard index. - let request = client - .get(shard_index_url.clone()) - .build() - .expect("invalid shard_index request"); + // Fetch the sharded repodata from the remote server + let canonical_shards_url = channel_base_url + .join(REPODATA_SHARDS_FILENAME) + .expect("invalid shard base url"); + + let cache_file_name = format!( + "{}.shards-cache-v1", + url_to_cache_filename(&canonical_shards_url) + ); + let cache_path = cache_dir.join(cache_file_name); + + let canonical_request = SimpleRequest::get(&canonical_shards_url); // Try reading the cached file - if let Ok((cache_header, file)) = read_cached_index(cache_path).await { + if let Ok((cache_header, file)) = read_cached_index(&cache_path).await { match cache_header .policy - .before_request(&request, SystemTime::now()) + .before_request(&canonical_request, SystemTime::now()) { BeforeRequest::Fresh(_) => { if let Ok(shard_index) = read_shard_index_from_reader(file).await { @@ -117,8 +169,26 @@ async fn fetch_index( request: state_request, .. } => { - let request = convert_request(client.clone(), state_request.clone()) - .expect("failed to create request to check staleness"); + // Get the token from the token client + let token = token_client.get_token().await?; + + // Determine the actual URL to use for the request + let shards_url = token + .shard_base_url + .as_ref() + .unwrap_or(channel_base_url) + .join(REPODATA_SHARDS_FILENAME) + .expect("invalid shard base url"); + + // Construct the actual request that we will send + let mut request = client + .get(shards_url) + .headers(state_request.headers().clone()) + .build() + .expect("failed to build request for shard index"); + token.add_to_headers(request.headers_mut()); + + // Send the request let response = client .execute(request) .await @@ -142,7 +212,7 @@ async fn fetch_index( drop(file); tracing::debug!("shard index cache has become stale"); - return from_response(cache_path, policy, response).await; + return from_response(&cache_path, policy, response).await; } } } @@ -151,6 +221,24 @@ async fn fetch_index( tracing::debug!("fetching fresh shard index"); + // Get the token from the token client + let token = token_client.get_token().await?; + + // Determine the actual URL to use for the request + let shards_url = token + .shard_base_url + .as_ref() + .unwrap_or(channel_base_url) + .join(REPODATA_SHARDS_FILENAME) + .expect("invalid shard base url"); + + // Construct the actual request that we will send + let mut request = client + .get(shards_url) + .build() + .expect("failed to build request for shard index"); + token.add_to_headers(request.headers_mut()); + // Do a fresh requests let response = client .execute( @@ -170,8 +258,8 @@ async fn fetch_index( )); }; - let policy = CachePolicy::new(&request, &response); - from_response(cache_path, policy, response).await + let policy = CachePolicy::new(&canonical_request, &response); + from_response(&cache_path, policy, response).await } /// Writes the shard index cache to disk. @@ -206,21 +294,6 @@ async fn write_shard_index_cache( .await? } -/// Converts from a `http::request::Parts` into a `reqwest::Request`. -fn convert_request( - client: ClientWithMiddleware, - parts: http::request::Parts, -) -> Result { - client - .request( - parts.method, - Url::from_str(&parts.uri.to_string()).expect("uris should be the same"), - ) - .headers(parts.headers) - .version(parts.version) - .build() -} - /// Read the shard index from a reader and deserialize it. async fn read_shard_index_from_reader( mut reader: BufReader, @@ -271,6 +344,101 @@ async fn read_cached_index(cache_path: &Path) -> std::io::Result<(CacheHeader, B Ok((cache_header, reader)) } +struct TokenClient { + client: ClientWithMiddleware, + token_base_url: Url, + token: Arc>>>>, +} + +impl TokenClient { + pub fn new(client: ClientWithMiddleware, token_base_url: Url) -> Self { + Self { + client, + token_base_url, + token: Arc::new(Mutex::new(PendingOrFetched::Fetched(None))), + } + } + + pub async fn get_token(&self) -> Result, GatewayError> { + let sender_or_receiver = { + let mut token = self.token.lock(); + match &*token { + PendingOrFetched::Fetched(Some(token)) if token.is_fresh() => { + // The token is still fresh. + return Ok(token.clone()); + } + PendingOrFetched::Fetched(_) => { + let (sender, _) = tokio::sync::broadcast::channel(1); + let sender = Arc::new(sender); + *token = PendingOrFetched::Pending(Arc::downgrade(&sender)); + + Either::Left(sender) + } + PendingOrFetched::Pending(sender) => { + let sender = sender.upgrade(); + if let Some(sender) = sender { + Either::Right(sender.subscribe()) + } else { + let (sender, _) = tokio::sync::broadcast::channel(1); + let sender = Arc::new(sender); + *token = PendingOrFetched::Pending(Arc::downgrade(&sender)); + Either::Left(sender) + } + } + } + }; + + let sender = match sender_or_receiver { + Either::Left(sender) => sender, + Either::Right(mut receiver) => { + return match receiver.recv().await { + Ok(Some(token)) => Ok(token), + _ => { + // If this happens the sender was dropped. + Err(GatewayError::IoError( + "a coalesced request for a token failed".to_string(), + std::io::ErrorKind::Other.into(), + )) + } + }; + } + }; + + let token_url = self + .token_base_url + .join("token") + .expect("invalid token url"); + tracing::debug!("fetching token from {}", &token_url); + + // Fetch the token + let response = self + .client + .get(token_url) + .header(CACHE_CONTROL, HeaderValue::from_static("max-age=0")) + .send() + .await + .and_then(|r| r.error_for_status().map_err(Into::into)) + .map_err(FetchRepoDataError::from) + .map_err(GatewayError::from)?; + + let token = response + .json::() + .await + .map_err(FetchRepoDataError::from) + .map_err(GatewayError::from) + .map(Arc::new)?; + + // Reacquire the token + let mut token_lock = self.token.lock(); + *token_lock = PendingOrFetched::Fetched(Some(token.clone())); + + // Publish the change + let _ = sender.send(Some(token.clone())); + + Ok(token) + } +} + impl ShardedSubdir { pub async fn new( _channel: Channel, @@ -282,18 +450,12 @@ impl ShardedSubdir { let channel = Channel::from_url(Url::parse("https://conda.anaconda.org/conda-forge").unwrap()); - let shard_base_url = + let channel_base_url = Url::parse(&format!("https://fast.prefiks.dev/conda-forge/{subdir}/")).unwrap(); + let token_client = TokenClient::new(client.clone(), channel_base_url.clone()); - // Fetch the sharded repodata from the remote server - let repodata_shards_url = shard_base_url - .join("repodata_shards.msgpack.zst") - .expect("invalid shard base url"); - - let cache_key = crate::utils::url_to_cache_filename(&repodata_shards_url); - let sharded_repodata_path = cache_dir.join(format!("{cache_key}.shard-cache-v1")); let sharded_repodata = - fetch_index(&client, &repodata_shards_url, &sharded_repodata_path).await?; + fetch_index(client.clone(), &channel_base_url, &token_client, &cache_dir).await?; // Determine the cache directory and make sure it exists. let cache_dir = cache_dir.join("shards-v1"); @@ -304,8 +466,10 @@ impl ShardedSubdir { Ok(Self { channel, client, - shard_base_url, + channel_base_url, + token_client, sharded_repodata, + cache_dir, }) } @@ -343,16 +507,28 @@ impl SubdirClient for ShardedSubdir { Err(err) => return Err(FetchRepoDataError::IoError(err).into()), } + // Get the token + let token = self.token_client.get_token().await?; + // Download the shard - let shard_url = self + let shard_url = token .shard_base_url + .as_ref() + .unwrap_or(&self.channel_base_url) .join(&format!("shards/{shard:x}.msgpack.zst")) .expect("invalid shard url"); - let shard_response = self + let mut shard_request = self .client .get(shard_url.clone()) - .send() + .header(CACHE_CONTROL, HeaderValue::from_static("no-store")) + .build() + .expect("failed to build shard request"); + token.add_to_headers(shard_request.headers_mut()); + + let shard_response = self + .client + .execute(shard_request) .await .and_then(|r| r.error_for_status().map_err(Into::into)) .map_err(FetchRepoDataError::from)?; @@ -423,6 +599,38 @@ async fn parse_records + Send + 'static>( .await } +/// The token endpoint response. +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Token { + token: Option, + issued_at: Option>, + expires_in: Option, + shard_base_url: Option, +} + +impl Token { + /// Returns true if the token is still considered to be valid. + pub fn is_fresh(&self) -> bool { + if let (Some(issued_at), Some(expires_in)) = (&self.issued_at, self.expires_in) { + let now = Utc::now(); + if issued_at.add(TimeDelta::seconds(expires_in as i64)) > now { + return false; + } + } + true + } + + /// Add the token to the headers if its available + pub fn add_to_headers(&self, headers: &mut http::header::HeaderMap) { + if let Some(token) = &self.token { + headers.insert( + http::header::AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {token}")).unwrap(), + ); + } + } +} + /// Returns the URL with a trailing slash if it doesn't already have one. fn add_trailing_slash(url: &Url) -> Cow<'_, Url> { let path = url.path(); From d4bf52cefbd160ddbc346c901893e66665b218d6 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Tue, 30 Apr 2024 13:45:07 +0200 Subject: [PATCH 22/57] fix create command --- crates/rattler-bin/src/commands/create.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rattler-bin/src/commands/create.rs b/crates/rattler-bin/src/commands/create.rs index b8d1f2936..1cad36d28 100644 --- a/crates/rattler-bin/src/commands/create.rs +++ b/crates/rattler-bin/src/commands/create.rs @@ -742,7 +742,7 @@ async fn find_installed_packages( { let entry = entry?; let path = entry.path(); - if path.ends_with(".json") { + if !path.ends_with(".json") { continue; } From 9b16da869923e9369b06f9a56973437ad0e1e490 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Tue, 30 Apr 2024 16:15:11 +0200 Subject: [PATCH 23/57] fix: enable multiple matchspecs for the same package --- .../src/gateway/mod.rs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index fdc992b6e..f371d13c4 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -21,6 +21,7 @@ use itertools::Itertools; use local_subdir::LocalSubdirClient; use rattler_conda_types::{Channel, MatchSpec, PackageName, Platform}; use reqwest_middleware::ClientWithMiddleware; +use std::collections::HashMap; use std::{ borrow::Borrow, collections::HashSet, @@ -203,12 +204,15 @@ impl Gateway { // Package names that we have or will issue requests for. let mut seen = HashSet::new(); - let mut pending_package_specs = Vec::new(); + let mut pending_package_specs = HashMap::new(); for spec in specs { let spec = spec.into(); if let Some(name) = &spec.name { seen.insert(name.clone()); - pending_package_specs.push(spec); + pending_package_specs + .entry(name.clone()) + .or_insert_with(Vec::new) + .push(spec); } } @@ -223,12 +227,10 @@ impl Gateway { loop { // Iterate over all pending package names and create futures to fetch them from all // subdirs. - for spec in pending_package_specs.drain(..) { + for (package_name, specs) in pending_package_specs.drain() { for (channel_idx, subdir) in subdirs.iter().cloned() { - let spec = spec.clone(); - let Some(package_name) = spec.name.clone() else { - continue; - }; + let specs = specs.clone(); + let package_name = package_name.clone(); pending_records.push(async move { let barrier_cell = subdir.clone(); let subdir = barrier_cell.wait().await; @@ -236,8 +238,8 @@ impl Gateway { Subdir::Found(subdir) => subdir .get_or_fetch_package_records(&package_name) .await - .map(|records| (channel_idx, spec, records)), - Subdir::NotFound => Ok((channel_idx, spec, Arc::from(vec![]))), + .map(|records| (channel_idx, specs, records)), + Subdir::NotFound => Ok((channel_idx, specs, Arc::from(vec![]))), } }); } @@ -252,13 +254,13 @@ impl Gateway { // Handle any records that were fetched records = pending_records.select_next_some() => { - let (channel_idx, request_spec, records) = records?; + let (channel_idx, request_specs, records) = records?; if recursive { // Extract the dependencies from the records and recursively add them to the // list of package names that we need to fetch. for record in records.iter() { - if !request_spec.matches(&record.package_record) { + if !request_specs.iter().any(|spec| spec.matches(&record.package_record)) { // Do not recurse into records that do not match to root spec. continue; } @@ -267,7 +269,7 @@ impl Gateway { dependency.split_once(' ').unwrap_or((dependency, "")).0, ); if seen.insert(dependency_name.clone()) { - pending_package_specs.push(dependency_name.into()); + pending_package_specs.insert(dependency_name.clone(), vec![dependency_name.into()]); } } } @@ -531,13 +533,12 @@ mod test { )], vec![Platform::Linux64, Platform::NoArch], vec![ - PackageName::from_str("rubin-env").unwrap(), + // PackageName::from_str("rubin-env").unwrap(), // PackageName::from_str("jupyterlab").unwrap(), // PackageName::from_str("detectron2").unwrap(), - - // PackageName::from_str("python").unwrap(), - // PackageName::from_str("boto3").unwrap(), - // PackageName::from_str("requests").unwrap(), + PackageName::from_str("python").unwrap(), + PackageName::from_str("boto3").unwrap(), + PackageName::from_str("requests").unwrap(), ] .into_iter(), ) From fdad58a28101d2f42a85dfdd14eee9aac4acb949 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Tue, 30 Apr 2024 17:37:17 +0200 Subject: [PATCH 24/57] feat: limit the number of concurrent requests --- .gitignore | 3 + .../src/gateway/builder.rs | 13 + .../src/gateway/mod.rs | 9 +- .../src/gateway/sharded_subdir.rs | 669 ------------------ .../src/gateway/sharded_subdir/index.rs | 333 +++++++++ .../src/gateway/sharded_subdir/mod.rs | 236 ++++++ .../src/gateway/sharded_subdir/token.rs | 149 ++++ 7 files changed, 739 insertions(+), 673 deletions(-) delete mode 100644 crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs create mode 100644 crates/rattler_repodata_gateway/src/gateway/sharded_subdir/index.rs create mode 100644 crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs create mode 100644 crates/rattler_repodata_gateway/src/gateway/sharded_subdir/token.rs diff --git a/.gitignore b/.gitignore index ae387afa6..1e6de59ee 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ Cargo.lock # pixi .pixi/ pixi.lock + +# Visual studio files +.vs/ diff --git a/crates/rattler_repodata_gateway/src/gateway/builder.rs b/crates/rattler_repodata_gateway/src/gateway/builder.rs index 438a2fdfc..0a5c2b579 100644 --- a/crates/rattler_repodata_gateway/src/gateway/builder.rs +++ b/crates/rattler_repodata_gateway/src/gateway/builder.rs @@ -12,6 +12,7 @@ pub struct GatewayBuilder { channel_config: ChannelConfig, client: Option, cache: Option, + max_concurrent_requests: Option, } impl GatewayBuilder { @@ -41,6 +42,13 @@ impl GatewayBuilder { self } + /// Sets the maximum number of concurrent HTTP requests to make. + #[must_use] + pub fn with_max_concurrent_requests(mut self, max_concurrent_requests: usize) -> Self { + self.max_concurrent_requests = Some(max_concurrent_requests); + self + } + /// Finish the construction of the gateway returning a constructed gateway. pub fn finish(self) -> Gateway { let client = self @@ -53,12 +61,17 @@ impl GatewayBuilder { .join("rattler/cache") }); + let max_concurrent_requests = self.max_concurrent_requests.unwrap_or(100); + Gateway { inner: Arc::new(GatewayInner { subdirs: DashMap::default(), client, channel_config: self.channel_config, cache, + concurrent_requests_semaphore: Arc::new(tokio::sync::Semaphore::new( + max_concurrent_requests, + )), }), } } diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index f371d13c4..9e428cce1 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -307,6 +307,9 @@ struct GatewayInner { /// The directory to store any cache cache: PathBuf, + + /// A semaphore to limit the number of concurrent requests. + concurrent_requests_semaphore: Arc, } impl GatewayInner { @@ -411,15 +414,13 @@ impl GatewayInner { )); } } else if url.scheme() == "http" || url.scheme() == "https" { - if url - .as_str() - .starts_with("https://conda.anaconda.org/conda-forge/") - { + if url.as_str().starts_with("https://fast.prefiks.dev/") { sharded_subdir::ShardedSubdir::new( channel.clone(), platform.to_string(), self.client.clone(), self.cache.clone(), + self.concurrent_requests_semaphore.clone(), ) .await .map(SubdirData::from_client) diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs deleted file mode 100644 index d3d4516d4..000000000 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir.rs +++ /dev/null @@ -1,669 +0,0 @@ -use crate::gateway::PendingOrFetched; -use crate::utils::url_to_cache_filename; -use crate::{ - fetch::{FetchRepoDataError, RepoDataNotFoundError}, - gateway::subdir::SubdirClient, - GatewayError, -}; -use bytes::Bytes; -use chrono::{DateTime, TimeDelta, Utc}; -use futures::{FutureExt, TryFutureExt}; -use http::header::CACHE_CONTROL; -use http::{HeaderMap, HeaderValue, Method, Uri}; -use http_cache_semantics::{AfterResponse, BeforeRequest, CachePolicy, RequestLike}; -use itertools::Either; -use parking_lot::Mutex; -use rattler_conda_types::{Channel, PackageName, PackageRecord, RepoDataRecord}; -use rattler_digest::Sha256Hash; -use reqwest::{Response, StatusCode}; -use reqwest_middleware::ClientWithMiddleware; -use serde::{Deserialize, Serialize}; -use std::ops::Add; -use std::{ - borrow::Cow, - collections::HashMap, - io::Write, - path::{Path, PathBuf}, - str::FromStr, - sync::Arc, - time::SystemTime, -}; -use tempfile::NamedTempFile; -use tokio::{ - fs::File, - io::{AsyncReadExt, BufReader}, -}; -use url::Url; - -pub struct ShardedSubdir { - channel: Channel, - client: ClientWithMiddleware, - channel_base_url: Url, - token_client: TokenClient, - sharded_repodata: ShardedRepodata, - cache_dir: PathBuf, -} - -/// Magic number that identifies the cache file format. -const MAGIC_NUMBER: &[u8] = b"SHARD-CACHE-V1"; -const REPODATA_SHARDS_FILENAME: &str = "repodata_shards.msgpack.zst"; - -struct SimpleRequest { - uri: Uri, - method: Method, - headers: HeaderMap, -} - -impl SimpleRequest { - pub fn get(url: &Url) -> Self { - Self { - uri: Uri::from_str(url.as_str()).expect("failed to convert Url to Uri"), - method: Method::GET, - headers: HeaderMap::default(), - } - } -} - -impl RequestLike for SimpleRequest { - fn method(&self) -> &Method { - &self.method - } - - fn uri(&self) -> Uri { - self.uri.clone() - } - - fn headers(&self) -> &HeaderMap { - &self.headers - } - - fn is_same_uri(&self, other: &Uri) -> bool { - &self.uri() == other - } -} - -// Fetches the shard index from the url or read it from the cache. -async fn fetch_index( - client: ClientWithMiddleware, - channel_base_url: &Url, - token_client: &TokenClient, - cache_dir: &Path, -) -> Result { - async fn from_response( - cache_path: &Path, - policy: CachePolicy, - response: Response, - ) -> Result { - // Read the bytes of the response - let bytes = response.bytes().await.map_err(FetchRepoDataError::from)?; - - // Decompress the bytes - let decoded_bytes = Bytes::from(decode_zst_bytes_async(bytes).await?); - - // Write the cache to disk if the policy allows it. - let cache_fut = if policy.is_storable() { - write_shard_index_cache(cache_path, policy, decoded_bytes.clone()) - .map_err(FetchRepoDataError::IoError) - .map_ok(Some) - .left_future() - } else { - // Otherwise delete the file - tokio::fs::remove_file(cache_path) - .map_ok_or_else( - |e| { - if e.kind() == std::io::ErrorKind::NotFound { - Ok(None) - } else { - Err(FetchRepoDataError::IoError(e)) - } - }, - |_| Ok(None), - ) - .right_future() - }; - - // Parse the bytes - let parse_fut = tokio_rayon::spawn(move || rmp_serde::from_slice(&decoded_bytes)) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) - .map_err(FetchRepoDataError::IoError); - - // Parse and write the file to disk concurrently - let (temp_file, sharded_index) = tokio::try_join!(cache_fut, parse_fut)?; - - // Persist the cache if succesfully updated the cache. - if let Some(temp_file) = temp_file { - temp_file - .persist(cache_path) - .map_err(FetchRepoDataError::from)?; - } - - Ok(sharded_index) - } - - // Fetch the sharded repodata from the remote server - let canonical_shards_url = channel_base_url - .join(REPODATA_SHARDS_FILENAME) - .expect("invalid shard base url"); - - let cache_file_name = format!( - "{}.shards-cache-v1", - url_to_cache_filename(&canonical_shards_url) - ); - let cache_path = cache_dir.join(cache_file_name); - - let canonical_request = SimpleRequest::get(&canonical_shards_url); - - // Try reading the cached file - if let Ok((cache_header, file)) = read_cached_index(&cache_path).await { - match cache_header - .policy - .before_request(&canonical_request, SystemTime::now()) - { - BeforeRequest::Fresh(_) => { - if let Ok(shard_index) = read_shard_index_from_reader(file).await { - tracing::debug!("shard index cache hit"); - return Ok(shard_index); - } - } - BeforeRequest::Stale { - request: state_request, - .. - } => { - // Get the token from the token client - let token = token_client.get_token().await?; - - // Determine the actual URL to use for the request - let shards_url = token - .shard_base_url - .as_ref() - .unwrap_or(channel_base_url) - .join(REPODATA_SHARDS_FILENAME) - .expect("invalid shard base url"); - - // Construct the actual request that we will send - let mut request = client - .get(shards_url) - .headers(state_request.headers().clone()) - .build() - .expect("failed to build request for shard index"); - token.add_to_headers(request.headers_mut()); - - // Send the request - let response = client - .execute(request) - .await - .map_err(FetchRepoDataError::from)?; - - match cache_header.policy.after_response( - &state_request, - &response, - SystemTime::now(), - ) { - AfterResponse::NotModified(_policy, _) => { - // The cached file is still valid - if let Ok(shard_index) = read_shard_index_from_reader(file).await { - tracing::debug!("shard index cache was not modified"); - // If reading the file failed for some reason we'll just fetch it again. - return Ok(shard_index); - } - } - AfterResponse::Modified(policy, _) => { - // Close the old file so we can create a new one. - drop(file); - - tracing::debug!("shard index cache has become stale"); - return from_response(&cache_path, policy, response).await; - } - } - } - } - }; - - tracing::debug!("fetching fresh shard index"); - - // Get the token from the token client - let token = token_client.get_token().await?; - - // Determine the actual URL to use for the request - let shards_url = token - .shard_base_url - .as_ref() - .unwrap_or(channel_base_url) - .join(REPODATA_SHARDS_FILENAME) - .expect("invalid shard base url"); - - // Construct the actual request that we will send - let mut request = client - .get(shards_url) - .build() - .expect("failed to build request for shard index"); - token.add_to_headers(request.headers_mut()); - - // Do a fresh requests - let response = client - .execute( - request - .try_clone() - .expect("failed to clone initial request"), - ) - .await - .map_err(FetchRepoDataError::from)?; - - // Check if the response was successful. - if response.status() == StatusCode::NOT_FOUND { - return Err(GatewayError::FetchRepoDataError( - FetchRepoDataError::NotFound(RepoDataNotFoundError::from( - response.error_for_status().unwrap_err(), - )), - )); - }; - - let policy = CachePolicy::new(&canonical_request, &response); - from_response(&cache_path, policy, response).await -} - -/// Writes the shard index cache to disk. -async fn write_shard_index_cache( - cache_path: &Path, - policy: CachePolicy, - decoded_bytes: Bytes, -) -> std::io::Result { - let cache_path = cache_path.to_path_buf(); - tokio::task::spawn_blocking(move || { - // Write the header - let cache_header = rmp_serde::encode::to_vec(&CacheHeader { policy }) - .expect("failed to encode cache header"); - let cache_dir = cache_path - .parent() - .expect("the cache path must have a parent"); - std::fs::create_dir_all(cache_dir)?; - let mut temp_file = tempfile::Builder::new() - .tempfile_in(cache_dir) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; - temp_file.write_all(MAGIC_NUMBER)?; - temp_file.write_all(&(cache_header.len() as u32).to_le_bytes())?; - temp_file.write_all(&cache_header)?; - temp_file.write_all(decoded_bytes.as_ref())?; - - Ok(temp_file) - }) - .map_err(|e| match e.try_into_panic() { - Ok(payload) => std::panic::resume_unwind(payload), - Err(e) => std::io::Error::new(std::io::ErrorKind::Other, e), - }) - .await? -} - -/// Read the shard index from a reader and deserialize it. -async fn read_shard_index_from_reader( - mut reader: BufReader, -) -> std::io::Result { - // Read the file to memory - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - - // Deserialize the bytes - tokio_rayon::spawn(move || rmp_serde::from_slice(&bytes)) - .await - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) -} - -/// Cache information stored at the start of the cache file. -#[derive(Clone, Debug, Serialize, Deserialize)] -struct CacheHeader { - pub policy: CachePolicy, -} - -/// Try reading the cache file from disk. -async fn read_cached_index(cache_path: &Path) -> std::io::Result<(CacheHeader, BufReader)> { - // Open the file for reading - let file = File::open(cache_path).await?; - let mut reader = BufReader::new(file); - - // Read the magic from the file - let mut magic_number = [0; MAGIC_NUMBER.len()]; - reader.read_exact(&mut magic_number).await?; - if magic_number != MAGIC_NUMBER { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "invalid magic number", - )); - } - - // Read the length of the header - let header_length = reader.read_u32_le().await? as usize; - - // Read the header from the file - let mut header_bytes = vec![0; header_length]; - reader.read_exact(&mut header_bytes).await?; - - // Deserialize the header - let cache_header = rmp_serde::from_slice::(&header_bytes) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; - - Ok((cache_header, reader)) -} - -struct TokenClient { - client: ClientWithMiddleware, - token_base_url: Url, - token: Arc>>>>, -} - -impl TokenClient { - pub fn new(client: ClientWithMiddleware, token_base_url: Url) -> Self { - Self { - client, - token_base_url, - token: Arc::new(Mutex::new(PendingOrFetched::Fetched(None))), - } - } - - pub async fn get_token(&self) -> Result, GatewayError> { - let sender_or_receiver = { - let mut token = self.token.lock(); - match &*token { - PendingOrFetched::Fetched(Some(token)) if token.is_fresh() => { - // The token is still fresh. - return Ok(token.clone()); - } - PendingOrFetched::Fetched(_) => { - let (sender, _) = tokio::sync::broadcast::channel(1); - let sender = Arc::new(sender); - *token = PendingOrFetched::Pending(Arc::downgrade(&sender)); - - Either::Left(sender) - } - PendingOrFetched::Pending(sender) => { - let sender = sender.upgrade(); - if let Some(sender) = sender { - Either::Right(sender.subscribe()) - } else { - let (sender, _) = tokio::sync::broadcast::channel(1); - let sender = Arc::new(sender); - *token = PendingOrFetched::Pending(Arc::downgrade(&sender)); - Either::Left(sender) - } - } - } - }; - - let sender = match sender_or_receiver { - Either::Left(sender) => sender, - Either::Right(mut receiver) => { - return match receiver.recv().await { - Ok(Some(token)) => Ok(token), - _ => { - // If this happens the sender was dropped. - Err(GatewayError::IoError( - "a coalesced request for a token failed".to_string(), - std::io::ErrorKind::Other.into(), - )) - } - }; - } - }; - - let token_url = self - .token_base_url - .join("token") - .expect("invalid token url"); - tracing::debug!("fetching token from {}", &token_url); - - // Fetch the token - let response = self - .client - .get(token_url) - .header(CACHE_CONTROL, HeaderValue::from_static("max-age=0")) - .send() - .await - .and_then(|r| r.error_for_status().map_err(Into::into)) - .map_err(FetchRepoDataError::from) - .map_err(GatewayError::from)?; - - let token = response - .json::() - .await - .map_err(FetchRepoDataError::from) - .map_err(GatewayError::from) - .map(Arc::new)?; - - // Reacquire the token - let mut token_lock = self.token.lock(); - *token_lock = PendingOrFetched::Fetched(Some(token.clone())); - - // Publish the change - let _ = sender.send(Some(token.clone())); - - Ok(token) - } -} - -impl ShardedSubdir { - pub async fn new( - _channel: Channel, - subdir: String, - client: ClientWithMiddleware, - cache_dir: PathBuf, - ) -> Result { - // TODO: our sharded index only serves conda-forge so we simply override it. - let channel = - Channel::from_url(Url::parse("https://conda.anaconda.org/conda-forge").unwrap()); - - let channel_base_url = - Url::parse(&format!("https://fast.prefiks.dev/conda-forge/{subdir}/")).unwrap(); - let token_client = TokenClient::new(client.clone(), channel_base_url.clone()); - - let sharded_repodata = - fetch_index(client.clone(), &channel_base_url, &token_client, &cache_dir).await?; - - // Determine the cache directory and make sure it exists. - let cache_dir = cache_dir.join("shards-v1"); - tokio::fs::create_dir_all(&cache_dir) - .await - .map_err(FetchRepoDataError::IoError)?; - - Ok(Self { - channel, - client, - channel_base_url, - token_client, - sharded_repodata, - - cache_dir, - }) - } -} - -#[async_trait::async_trait] -impl SubdirClient for ShardedSubdir { - async fn fetch_package_records( - &self, - name: &PackageName, - ) -> Result, GatewayError> { - // Find the shard that contains the package - let Some(shard) = self.sharded_repodata.shards.get(name.as_normalized()) else { - return Ok(vec![].into()); - }; - - // Check if we already have the shard in the cache. - let shard_cache_path = self.cache_dir.join(format!("{shard:x}.msgpack")); - - // Read the cached shard - match tokio::fs::read(&shard_cache_path).await { - Ok(cached_bytes) => { - // Decode the cached shard - return parse_records( - cached_bytes, - self.channel.canonical_name(), - self.sharded_repodata.info.base_url.clone(), - ) - .await - .map(Arc::from); - } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - // The file is missing from the cache, we need to download it. - } - Err(err) => return Err(FetchRepoDataError::IoError(err).into()), - } - - // Get the token - let token = self.token_client.get_token().await?; - - // Download the shard - let shard_url = token - .shard_base_url - .as_ref() - .unwrap_or(&self.channel_base_url) - .join(&format!("shards/{shard:x}.msgpack.zst")) - .expect("invalid shard url"); - - let mut shard_request = self - .client - .get(shard_url.clone()) - .header(CACHE_CONTROL, HeaderValue::from_static("no-store")) - .build() - .expect("failed to build shard request"); - token.add_to_headers(shard_request.headers_mut()); - - let shard_response = self - .client - .execute(shard_request) - .await - .and_then(|r| r.error_for_status().map_err(Into::into)) - .map_err(FetchRepoDataError::from)?; - - let shard_bytes = shard_response - .bytes() - .await - .map_err(FetchRepoDataError::from)?; - - let shard_bytes = decode_zst_bytes_async(shard_bytes).await?; - - // Create a future to write the cached bytes to disk - let write_to_cache_fut = tokio::fs::write(&shard_cache_path, shard_bytes.clone()) - .map_err(FetchRepoDataError::IoError) - .map_err(GatewayError::from); - - // Create a future to parse the records from the shard - let parse_records_fut = parse_records( - shard_bytes, - self.channel.canonical_name(), - self.sharded_repodata.info.base_url.clone(), - ); - - // Await both futures concurrently. - let (_, records) = tokio::try_join!(write_to_cache_fut, parse_records_fut)?; - - Ok(records.into()) - } -} - -async fn decode_zst_bytes_async + Send + 'static>( - bytes: R, -) -> Result, GatewayError> { - tokio_rayon::spawn(move || match zstd::decode_all(bytes.as_ref()) { - Ok(decoded) => Ok(decoded), - Err(err) => Err(GatewayError::IoError( - "failed to decode zstd shard".to_string(), - err, - )), - }) - .await -} - -async fn parse_records + Send + 'static>( - bytes: R, - channel_name: String, - base_url: Url, -) -> Result, GatewayError> { - tokio_rayon::spawn(move || { - // let shard = serde_json::from_slice::(bytes.as_ref()).map_err(std::io::Error::from)?; - let shard = rmp_serde::from_slice::(bytes.as_ref()) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) - .map_err(FetchRepoDataError::IoError)?; - let packages = - itertools::chain(shard.packages.into_iter(), shard.packages_conda.into_iter()); - let base_url = add_trailing_slash(&base_url); - Ok(packages - .map(|(file_name, package_record)| RepoDataRecord { - url: base_url - .join(&file_name) - .expect("filename is not a valid url"), - channel: channel_name.clone(), - package_record, - file_name, - }) - .collect()) - }) - .await -} - -/// The token endpoint response. -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Token { - token: Option, - issued_at: Option>, - expires_in: Option, - shard_base_url: Option, -} - -impl Token { - /// Returns true if the token is still considered to be valid. - pub fn is_fresh(&self) -> bool { - if let (Some(issued_at), Some(expires_in)) = (&self.issued_at, self.expires_in) { - let now = Utc::now(); - if issued_at.add(TimeDelta::seconds(expires_in as i64)) > now { - return false; - } - } - true - } - - /// Add the token to the headers if its available - pub fn add_to_headers(&self, headers: &mut http::header::HeaderMap) { - if let Some(token) = &self.token { - headers.insert( - http::header::AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {token}")).unwrap(), - ); - } - } -} - -/// Returns the URL with a trailing slash if it doesn't already have one. -fn add_trailing_slash(url: &Url) -> Cow<'_, Url> { - let path = url.path(); - if path.ends_with('/') { - Cow::Borrowed(url) - } else { - let mut url = url.clone(); - url.set_path(&format!("{path}/")); - Cow::Owned(url) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ShardedRepodata { - pub info: ShardedSubdirInfo, - /// The individual shards indexed by package name. - pub shards: HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Shard { - pub packages: HashMap, - - #[serde(rename = "packages.conda", default)] - pub packages_conda: HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ShardedSubdirInfo { - /// The name of the subdirectory - pub subdir: String, - - /// The base url of the subdirectory. This is the location where the actual - /// packages are stored. - pub base_url: Url, -} diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/index.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/index.rs new file mode 100644 index 000000000..6ca8b7a00 --- /dev/null +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/index.rs @@ -0,0 +1,333 @@ +use super::{token::TokenClient, ShardedRepodata}; +use crate::{ + fetch::{FetchRepoDataError, RepoDataNotFoundError}, + utils::url_to_cache_filename, + GatewayError, +}; +use bytes::Bytes; +use futures::{FutureExt, TryFutureExt}; +use http::{HeaderMap, Method, StatusCode, Uri}; +use http_cache_semantics::{AfterResponse, BeforeRequest, CachePolicy, RequestLike}; +use reqwest::Response; +use reqwest_middleware::ClientWithMiddleware; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use std::{io::Write, path::Path, str::FromStr, time::SystemTime}; +use tempfile::NamedTempFile; +use tokio::fs::File; +use tokio::io::{AsyncReadExt, BufReader}; +use url::Url; + +/// Magic number that identifies the cache file format. +const MAGIC_NUMBER: &[u8] = b"SHARD-CACHE-V1"; + +const REPODATA_SHARDS_FILENAME: &str = "repodata_shards.msgpack.zst"; + +// Fetches the shard index from the url or read it from the cache. +pub async fn fetch_index( + client: ClientWithMiddleware, + channel_base_url: &Url, + token_client: &TokenClient, + cache_dir: &Path, + concurrent_requests_semaphore: Arc, +) -> Result { + async fn from_response( + cache_path: &Path, + policy: CachePolicy, + response: Response, + ) -> Result { + // Read the bytes of the response + let bytes = response.bytes().await.map_err(FetchRepoDataError::from)?; + + // Decompress the bytes + let decoded_bytes = Bytes::from(super::decode_zst_bytes_async(bytes).await?); + + // Write the cache to disk if the policy allows it. + let cache_fut = if policy.is_storable() { + write_shard_index_cache(cache_path, policy, decoded_bytes.clone()) + .map_err(FetchRepoDataError::IoError) + .map_ok(Some) + .left_future() + } else { + // Otherwise delete the file + tokio::fs::remove_file(cache_path) + .map_ok_or_else( + |e| { + if e.kind() == std::io::ErrorKind::NotFound { + Ok(None) + } else { + Err(FetchRepoDataError::IoError(e)) + } + }, + |_| Ok(None), + ) + .right_future() + }; + + // Parse the bytes + let parse_fut = tokio_rayon::spawn(move || rmp_serde::from_slice(&decoded_bytes)) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) + .map_err(FetchRepoDataError::IoError); + + // Parse and write the file to disk concurrently + let (temp_file, sharded_index) = tokio::try_join!(cache_fut, parse_fut)?; + + // Persist the cache if succesfully updated the cache. + if let Some(temp_file) = temp_file { + temp_file + .persist(cache_path) + .map_err(FetchRepoDataError::from)?; + } + + Ok(sharded_index) + } + + // Fetch the sharded repodata from the remote server + let canonical_shards_url = channel_base_url + .join(REPODATA_SHARDS_FILENAME) + .expect("invalid shard base url"); + + let cache_file_name = format!( + "{}.shards-cache-v1", + url_to_cache_filename(&canonical_shards_url) + ); + let cache_path = cache_dir.join(cache_file_name); + + let canonical_request = SimpleRequest::get(&canonical_shards_url); + + // Try reading the cached file + if let Ok((cache_header, file)) = read_cached_index(&cache_path).await { + match cache_header + .policy + .before_request(&canonical_request, SystemTime::now()) + { + BeforeRequest::Fresh(_) => { + if let Ok(shard_index) = read_shard_index_from_reader(file).await { + tracing::debug!("shard index cache hit"); + return Ok(shard_index); + } + } + BeforeRequest::Stale { + request: state_request, + .. + } => { + // Get the token from the token client + let token = token_client.get_token().await?; + + // Determine the actual URL to use for the request + let shards_url = token + .shard_base_url + .as_ref() + .unwrap_or(channel_base_url) + .join(REPODATA_SHARDS_FILENAME) + .expect("invalid shard base url"); + + // Construct the actual request that we will send + let mut request = client + .get(shards_url) + .headers(state_request.headers().clone()) + .build() + .expect("failed to build request for shard index"); + token.add_to_headers(request.headers_mut()); + + // Acquire a permit to do a request + let _request_permit = concurrent_requests_semaphore.acquire().await; + + // Send the request + let response = client + .execute(request) + .await + .map_err(FetchRepoDataError::from)?; + + match cache_header.policy.after_response( + &state_request, + &response, + SystemTime::now(), + ) { + AfterResponse::NotModified(_policy, _) => { + // The cached file is still valid + match read_shard_index_from_reader(file).await { + Ok(shard_index) => { + tracing::debug!("shard index cache was not modified"); + // If reading the file failed for some reason we'll just fetch it again. + return Ok(shard_index); + } + Err(e) => { + tracing::warn!("the cached shard index has been corrupted: {e}"); + } + } + } + AfterResponse::Modified(policy, _) => { + // Close the old file so we can create a new one. + drop(file); + + tracing::debug!("shard index cache has become stale"); + return from_response(&cache_path, policy, response).await; + } + } + } + } + }; + + tracing::debug!("fetching fresh shard index"); + + // Get the token from the token client + let token = token_client.get_token().await?; + + // Determine the actual URL to use for the request + let shards_url = token + .shard_base_url + .as_ref() + .unwrap_or(channel_base_url) + .join(REPODATA_SHARDS_FILENAME) + .expect("invalid shard base url"); + + // Construct the actual request that we will send + let mut request = client + .get(shards_url) + .build() + .expect("failed to build request for shard index"); + token.add_to_headers(request.headers_mut()); + + // Acquire a permit to do a request + let _request_permit = concurrent_requests_semaphore.acquire().await; + + // Do a fresh requests + let response = client + .execute( + request + .try_clone() + .expect("failed to clone initial request"), + ) + .await + .map_err(FetchRepoDataError::from)?; + + // Check if the response was successful. + if response.status() == StatusCode::NOT_FOUND { + return Err(GatewayError::FetchRepoDataError( + FetchRepoDataError::NotFound(RepoDataNotFoundError::from( + response.error_for_status().unwrap_err(), + )), + )); + }; + + let policy = CachePolicy::new(&canonical_request, &response); + from_response(&cache_path, policy, response).await +} + +/// Writes the shard index cache to disk. +async fn write_shard_index_cache( + cache_path: &Path, + policy: CachePolicy, + decoded_bytes: Bytes, +) -> std::io::Result { + let cache_path = cache_path.to_path_buf(); + tokio::task::spawn_blocking(move || { + // Write the header + let cache_header = rmp_serde::encode::to_vec(&CacheHeader { policy }) + .expect("failed to encode cache header"); + let cache_dir = cache_path + .parent() + .expect("the cache path must have a parent"); + std::fs::create_dir_all(cache_dir)?; + let mut temp_file = tempfile::Builder::new() + .tempfile_in(cache_dir) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + temp_file.write_all(MAGIC_NUMBER)?; + temp_file.write_all(&(cache_header.len() as u32).to_le_bytes())?; + temp_file.write_all(&cache_header)?; + temp_file.write_all(decoded_bytes.as_ref())?; + + Ok(temp_file) + }) + .map_err(|e| match e.try_into_panic() { + Ok(payload) => std::panic::resume_unwind(payload), + Err(e) => std::io::Error::new(std::io::ErrorKind::Other, e), + }) + .await? +} + +/// Read the shard index from a reader and deserialize it. +async fn read_shard_index_from_reader( + mut reader: BufReader, +) -> std::io::Result { + // Read the file to memory + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + + // Deserialize the bytes + tokio_rayon::spawn(move || rmp_serde::from_slice(&bytes)) + .await + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) +} + +/// Cache information stored at the start of the cache file. +#[derive(Clone, Debug, Serialize, Deserialize)] +struct CacheHeader { + pub policy: CachePolicy, +} + +/// Try reading the cache file from disk. +async fn read_cached_index(cache_path: &Path) -> std::io::Result<(CacheHeader, BufReader)> { + // Open the file for reading + let file = File::open(cache_path).await?; + let mut reader = BufReader::new(file); + + // Read the magic from the file + let mut magic_number = [0; MAGIC_NUMBER.len()]; + reader.read_exact(&mut magic_number).await?; + if magic_number != MAGIC_NUMBER { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "invalid magic number", + )); + } + + // Read the length of the header + let header_length = reader.read_u32_le().await? as usize; + + // Read the header from the file + let mut header_bytes = vec![0; header_length]; + reader.read_exact(&mut header_bytes).await?; + + // Deserialize the header + let cache_header = rmp_serde::from_slice::(&header_bytes) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; + + Ok((cache_header, reader)) +} + +/// A helper struct to make it easier to construct something that implements [`RequestLike`]. +struct SimpleRequest { + uri: Uri, + method: Method, + headers: HeaderMap, +} + +impl SimpleRequest { + pub fn get(url: &Url) -> Self { + Self { + uri: Uri::from_str(url.as_str()).expect("failed to convert Url to Uri"), + method: Method::GET, + headers: HeaderMap::default(), + } + } +} + +impl RequestLike for SimpleRequest { + fn method(&self) -> &Method { + &self.method + } + + fn uri(&self) -> Uri { + self.uri.clone() + } + + fn headers(&self) -> &HeaderMap { + &self.headers + } + + fn is_same_uri(&self, other: &Uri) -> bool { + &self.uri() == other + } +} diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs new file mode 100644 index 000000000..d6a66d5a4 --- /dev/null +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs @@ -0,0 +1,236 @@ +use crate::{fetch::FetchRepoDataError, gateway::subdir::SubdirClient, GatewayError}; +use futures::TryFutureExt; +use http::header::CACHE_CONTROL; +use http::HeaderValue; +use rattler_conda_types::{Channel, PackageName, PackageRecord, RepoDataRecord}; +use rattler_digest::Sha256Hash; +use reqwest_middleware::ClientWithMiddleware; +use serde::{Deserialize, Serialize}; +use std::{borrow::Cow, collections::HashMap, path::PathBuf, sync::Arc}; +use token::TokenClient; +use url::Url; + +mod index; +mod token; + +pub struct ShardedSubdir { + channel: Channel, + client: ClientWithMiddleware, + shard_base_url: Url, + token_client: TokenClient, + sharded_repodata: ShardedRepodata, + cache_dir: PathBuf, + concurrent_requests_semaphore: Arc, +} + +impl ShardedSubdir { + pub async fn new( + channel: Channel, + subdir: String, + client: ClientWithMiddleware, + cache_dir: PathBuf, + concurrent_requests_semaphore: Arc, + ) -> Result { + // Construct the base url for the shards (e.g. `/`). + let shard_base_url = add_trailing_slash(channel.base_url()) + .join(&format!("{subdir}/")) + .expect("invalid subdir url"); + + // Construct a token client to fetch the token when we need it. + let token_client = TokenClient::new( + client.clone(), + shard_base_url.clone(), + concurrent_requests_semaphore.clone(), + ); + + // Fetch the shard index + let sharded_repodata = index::fetch_index( + client.clone(), + &shard_base_url, + &token_client, + &cache_dir, + concurrent_requests_semaphore.clone(), + ) + .await?; + + // Determine the cache directory and make sure it exists. + let cache_dir = cache_dir.join("shards-v1"); + tokio::fs::create_dir_all(&cache_dir) + .await + .map_err(FetchRepoDataError::IoError)?; + + Ok(Self { + channel, + client, + shard_base_url, + token_client, + sharded_repodata, + cache_dir, + concurrent_requests_semaphore, + }) + } +} + +#[async_trait::async_trait] +impl SubdirClient for ShardedSubdir { + async fn fetch_package_records( + &self, + name: &PackageName, + ) -> Result, GatewayError> { + // Find the shard that contains the package + let Some(shard) = self.sharded_repodata.shards.get(name.as_normalized()) else { + return Ok(vec![].into()); + }; + + // Check if we already have the shard in the cache. + let shard_cache_path = self.cache_dir.join(format!("{shard:x}.msgpack")); + + // Read the cached shard + match tokio::fs::read(&shard_cache_path).await { + Ok(cached_bytes) => { + // Decode the cached shard + return parse_records( + cached_bytes, + self.channel.canonical_name(), + self.sharded_repodata.info.base_url.clone(), + ) + .await + .map(Arc::from); + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + // The file is missing from the cache, we need to download it. + } + Err(err) => return Err(FetchRepoDataError::IoError(err).into()), + } + + // Get the token + let token = self.token_client.get_token().await?; + + // Download the shard + let shard_url = token + .shard_base_url + .as_ref() + .unwrap_or(&self.shard_base_url) + .join(&format!("shards/{shard:x}.msgpack.zst")) + .expect("invalid shard url"); + + let mut shard_request = self + .client + .get(shard_url.clone()) + .header(CACHE_CONTROL, HeaderValue::from_static("no-store")) + .build() + .expect("failed to build shard request"); + token.add_to_headers(shard_request.headers_mut()); + + let shard_bytes = { + let _permit = self.concurrent_requests_semaphore.acquire(); + let shard_response = self + .client + .execute(shard_request) + .await + .and_then(|r| r.error_for_status().map_err(Into::into)) + .map_err(FetchRepoDataError::from)?; + + shard_response + .bytes() + .await + .map_err(FetchRepoDataError::from)? + }; + + let shard_bytes = decode_zst_bytes_async(shard_bytes).await?; + + // Create a future to write the cached bytes to disk + let write_to_cache_fut = tokio::fs::write(&shard_cache_path, shard_bytes.clone()) + .map_err(FetchRepoDataError::IoError) + .map_err(GatewayError::from); + + // Create a future to parse the records from the shard + let parse_records_fut = parse_records( + shard_bytes, + self.channel.canonical_name(), + self.sharded_repodata.info.base_url.clone(), + ); + + // Await both futures concurrently. + let (_, records) = tokio::try_join!(write_to_cache_fut, parse_records_fut)?; + + Ok(records.into()) + } +} + +async fn decode_zst_bytes_async + Send + 'static>( + bytes: R, +) -> Result, GatewayError> { + tokio_rayon::spawn(move || match zstd::decode_all(bytes.as_ref()) { + Ok(decoded) => Ok(decoded), + Err(err) => Err(GatewayError::IoError( + "failed to decode zstd shard".to_string(), + err, + )), + }) + .await +} + +async fn parse_records + Send + 'static>( + bytes: R, + channel_name: String, + base_url: Url, +) -> Result, GatewayError> { + tokio_rayon::spawn(move || { + // let shard = serde_json::from_slice::(bytes.as_ref()).map_err(std::io::Error::from)?; + let shard = rmp_serde::from_slice::(bytes.as_ref()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) + .map_err(FetchRepoDataError::IoError)?; + let packages = + itertools::chain(shard.packages.into_iter(), shard.packages_conda.into_iter()); + let base_url = add_trailing_slash(&base_url); + Ok(packages + .map(|(file_name, package_record)| RepoDataRecord { + url: base_url + .join(&file_name) + .expect("filename is not a valid url"), + channel: channel_name.clone(), + package_record, + file_name, + }) + .collect()) + }) + .await +} + +/// Returns the URL with a trailing slash if it doesn't already have one. +fn add_trailing_slash(url: &Url) -> Cow<'_, Url> { + let path = url.path(); + if path.ends_with('/') { + Cow::Borrowed(url) + } else { + let mut url = url.clone(); + url.set_path(&format!("{path}/")); + Cow::Owned(url) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShardedRepodata { + pub info: ShardedSubdirInfo, + /// The individual shards indexed by package name. + pub shards: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Shard { + pub packages: HashMap, + + #[serde(rename = "packages.conda", default)] + pub packages_conda: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShardedSubdirInfo { + /// The name of the subdirectory + pub subdir: String, + + /// The base url of the subdirectory. This is the location where the actual + /// packages are stored. + pub base_url: Url, +} diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/token.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/token.rs new file mode 100644 index 000000000..9f12de279 --- /dev/null +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/token.rs @@ -0,0 +1,149 @@ +use crate::{fetch::FetchRepoDataError, gateway::PendingOrFetched, GatewayError}; +use chrono::{DateTime, TimeDelta, Utc}; +use http::header::CACHE_CONTROL; +use http::HeaderValue; +use itertools::Either; +use parking_lot::Mutex; +use reqwest_middleware::ClientWithMiddleware; +use serde::{Deserialize, Serialize}; +use std::ops::Add; +use std::sync::Arc; +use url::Url; + +/// A simple client that makes it simple to fetch a token from the token endpoint. +pub struct TokenClient { + client: ClientWithMiddleware, + token_base_url: Url, + token: Arc>>>>, + concurrent_request_semaphore: Arc, +} + +impl TokenClient { + pub fn new( + client: ClientWithMiddleware, + token_base_url: Url, + concurrent_request_semaphore: Arc, + ) -> Self { + Self { + client, + token_base_url, + token: Arc::new(Mutex::new(PendingOrFetched::Fetched(None))), + concurrent_request_semaphore, + } + } + + /// Returns the current token or fetches a new one if the current one is expired. + pub async fn get_token(&self) -> Result, GatewayError> { + let sender_or_receiver = { + let mut token = self.token.lock(); + match &*token { + PendingOrFetched::Fetched(Some(token)) if token.is_fresh() => { + // The token is still fresh. + return Ok(token.clone()); + } + PendingOrFetched::Fetched(_) => { + let (sender, _) = tokio::sync::broadcast::channel(1); + let sender = Arc::new(sender); + *token = PendingOrFetched::Pending(Arc::downgrade(&sender)); + + Either::Left(sender) + } + PendingOrFetched::Pending(sender) => { + let sender = sender.upgrade(); + if let Some(sender) = sender { + Either::Right(sender.subscribe()) + } else { + let (sender, _) = tokio::sync::broadcast::channel(1); + let sender = Arc::new(sender); + *token = PendingOrFetched::Pending(Arc::downgrade(&sender)); + Either::Left(sender) + } + } + } + }; + + let sender = match sender_or_receiver { + Either::Left(sender) => sender, + Either::Right(mut receiver) => { + return match receiver.recv().await { + Ok(Some(token)) => Ok(token), + _ => { + // If this happens the sender was dropped. + Err(GatewayError::IoError( + "a coalesced request for a token failed".to_string(), + std::io::ErrorKind::Other.into(), + )) + } + }; + } + }; + + let token_url = self + .token_base_url + .join("token") + .expect("invalid token url"); + tracing::debug!("fetching token from {}", &token_url); + + // Fetch the token + let token = { + let _permit = self.concurrent_request_semaphore.acquire().await; + let response = self + .client + .get(token_url) + .header(CACHE_CONTROL, HeaderValue::from_static("max-age=0")) + .send() + .await + .and_then(|r| r.error_for_status().map_err(Into::into)) + .map_err(FetchRepoDataError::from) + .map_err(GatewayError::from)?; + + response + .json::() + .await + .map_err(FetchRepoDataError::from) + .map_err(GatewayError::from) + .map(Arc::new)? + }; + + // Reacquire the token + let mut token_lock = self.token.lock(); + *token_lock = PendingOrFetched::Fetched(Some(token.clone())); + + // Publish the change + let _ = sender.send(Some(token.clone())); + + Ok(token) + } +} + +/// The token endpoint response. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Token { + pub token: Option, + issued_at: Option>, + expires_in: Option, + pub shard_base_url: Option, +} + +impl Token { + /// Returns true if the token is still considered to be valid. + pub fn is_fresh(&self) -> bool { + if let (Some(issued_at), Some(expires_in)) = (&self.issued_at, self.expires_in) { + let now = Utc::now(); + if issued_at.add(TimeDelta::seconds(expires_in as i64)) > now { + return false; + } + } + true + } + + /// Add the token to the headers if its available + pub fn add_to_headers(&self, headers: &mut http::header::HeaderMap) { + if let Some(token) = &self.token { + headers.insert( + http::header::AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {token}")).unwrap(), + ); + } + } +} From 8e869cdbbb92239a90b1d650d2d2f7a06fa0aaf3 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Tue, 30 Apr 2024 22:04:54 +0200 Subject: [PATCH 25/57] feat: serialize binary hashes as bytes --- crates/rattler_digest/src/serde.rs | 37 ++++++++++++++++-------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/crates/rattler_digest/src/serde.rs b/crates/rattler_digest/src/serde.rs index b37a0d098..37dfb92db 100644 --- a/crates/rattler_digest/src/serde.rs +++ b/crates/rattler_digest/src/serde.rs @@ -22,17 +22,27 @@ use std::borrow::Cow; use std::fmt::LowerHex; use std::ops::Deref; -/// Deserialize into [`Output`] of a [`Digest`] +/// Deserialize the [`Output`] of a [`Digest`]. +/// +/// If the deserializer is human-readable, it will parse the digest from a hex +/// string. Otherwise, it will deserialize raw bytes. pub fn deserialize<'de, D, Dig: Digest>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { - let str = Cow::<'de, str>::deserialize(deserializer)?; - super::parse_digest_from_hex::(str.as_ref()) - .ok_or_else(|| Error::custom("failed to parse digest")) + if deserializer.is_human_readable() { + let str = Cow::<'de, str>::deserialize(deserializer)?; + super::parse_digest_from_hex::(str.as_ref()) + .ok_or_else(|| Error::custom("failed to parse digest")) + } else { + Output::::deserialize(deserializer) + } } -/// Serialize into a string +/// Serializes the [`Output`] of a [`Digest`]. +/// +/// If the serializer is human-readable, it will write the digest as a hex +/// string. Otherwise, it will deserialize raw bytes. pub fn serialize<'a, S: Serializer, Dig: Digest>( digest: &'a Output, s: S, @@ -40,7 +50,11 @@ pub fn serialize<'a, S: Serializer, Dig: Digest>( where &'a Output: LowerHex, { - format!("{digest:x}").serialize(s) + if s.is_human_readable() { + format!("{digest:x}").serialize(s) + } else { + digest.serialize(s) + } } /// Wrapper type for easily serializing a Hash @@ -109,17 +123,6 @@ impl<'de, T: Digest + Default> DeserializeAs<'de, Output> for SerializableHas } } -// pub mod bytes { -// use serde::Serializer; -// -// fn serialize(&self, serializer: S) -> Result -// where -// S: Serializer, -// { -// crate::serde::serialize::(&self.0, serializer) -// } -// } - #[cfg(test)] mod test { use crate::serde::SerializableHash; From eeb519e0abcc30f6619c20f8311d9c2320eff45c Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Thu, 2 May 2024 08:07:55 +0200 Subject: [PATCH 26/57] allow both fast.prefix and fast.prefiks --- crates/rattler_repodata_gateway/src/gateway/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index 9e428cce1..8ad74d251 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -414,7 +414,9 @@ impl GatewayInner { )); } } else if url.scheme() == "http" || url.scheme() == "https" { - if url.as_str().starts_with("https://fast.prefiks.dev/") { + if url.host_str() == Some("fast.prefiks.dev") + || url.host_str() == Some("fast.prefix.dev") + { sharded_subdir::ShardedSubdir::new( channel.clone(), platform.to_string(), From b707b3631eed6be1acda66b7999dec694dd00324 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Thu, 2 May 2024 08:25:58 +0200 Subject: [PATCH 27/57] fix some tests --- crates/rattler_conda_types/src/channel/mod.rs | 13 ++++++------- crates/rattler_repodata_gateway/Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/rattler_conda_types/src/channel/mod.rs b/crates/rattler_conda_types/src/channel/mod.rs index 0011848fd..9cf125b45 100644 --- a/crates/rattler_conda_types/src/channel/mod.rs +++ b/crates/rattler_conda_types/src/channel/mod.rs @@ -461,7 +461,6 @@ fn absolute_path<'a>(path: &'a Path, root_dir: &Path) -> Cow<'a, Path> { #[cfg(test)] mod tests { use super::*; - use smallvec::smallvec; use std::{ path::{Path, PathBuf}, str::FromStr, @@ -472,15 +471,15 @@ mod tests { fn test_parse_platforms() { assert_eq!( parse_platforms("[noarch, linux-64]"), - Ok((Some(smallvec![Platform::NoArch, Platform::Linux64]), "")) + Ok((Some(vec![Platform::NoArch, Platform::Linux64]), "")) ); assert_eq!( parse_platforms("sometext[noarch]"), - Ok((Some(smallvec![Platform::NoArch]), "sometext")) + Ok((Some(vec![Platform::NoArch]), "sometext")) ); assert_eq!( parse_platforms("sometext[noarch,]"), - Ok((Some(smallvec![Platform::NoArch]), "sometext")) + Ok((Some(vec![Platform::NoArch]), "sometext")) ); assert_eq!(parse_platforms("sometext[]"), Ok((None, "sometext"))); assert!(matches!( @@ -565,7 +564,7 @@ mod tests { assert_eq!(channel.name(), "conda-forge"); assert_eq!(channel.platforms, None); - assert_eq!(channel, Channel::from_name("conda-forge/", None, &config)); + assert_eq!(channel, Channel::from_name("conda-forge/", &config)); } #[test] @@ -660,7 +659,7 @@ mod tests { Url::from_str("https://conda.anaconda.org/conda-forge/").unwrap() ); assert_eq!(channel.name.as_deref(), Some("conda-forge")); - assert_eq!(channel.platforms, Some(smallvec![platform])); + assert_eq!(channel.platforms, Some(vec![platform])); let channel = Channel::from_str( format!("https://conda.anaconda.org/pkgs/main[{platform}]"), @@ -672,7 +671,7 @@ mod tests { Url::from_str("https://conda.anaconda.org/pkgs/main/").unwrap() ); assert_eq!(channel.name.as_deref(), Some("pkgs/main")); - assert_eq!(channel.platforms, Some(smallvec![platform])); + assert_eq!(channel.platforms, Some(vec![platform])); let channel = Channel::from_str("conda-forge/label/rust_dev", &config).unwrap(); assert_eq!( diff --git a/crates/rattler_repodata_gateway/Cargo.toml b/crates/rattler_repodata_gateway/Cargo.toml index e2d82ef0a..8bfe159bc 100644 --- a/crates/rattler_repodata_gateway/Cargo.toml +++ b/crates/rattler_repodata_gateway/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true readme.workspace = true [dependencies] -async-trait = "0.1.77" +async-trait = { workspace = true } async-compression = { workspace = true, features = ["gzip", "tokio", "bzip2", "zstd"] } blake2 = { workspace = true } bytes = { workspace = true, optional = true } From 93a56991035c5da835088561d196c1687000fba7 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Thu, 2 May 2024 08:31:43 +0200 Subject: [PATCH 28/57] fix missing tokio macros dependency --- crates/rattler_repodata_gateway/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/rattler_repodata_gateway/Cargo.toml b/crates/rattler_repodata_gateway/Cargo.toml index 8bfe159bc..d8f1895af 100644 --- a/crates/rattler_repodata_gateway/Cargo.toml +++ b/crates/rattler_repodata_gateway/Cargo.toml @@ -29,7 +29,7 @@ tempfile = { workspace = true } tracing = { workspace = true } thiserror = { workspace = true } url = { workspace = true, features = ["serde"] } -tokio = { workspace = true, features = ["rt", "io-util"] } +tokio = { workspace = true, features = ["rt", "io-util", "macros"] } anyhow = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } @@ -66,7 +66,7 @@ axum = { workspace = true, features = ["tokio"] } hex-literal = { workspace = true } insta = { workspace = true, features = ["yaml"] } rstest = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tokio = { workspace = true, features = ["rt-multi-thread"] } tower-http = { workspace = true, features = ["fs", "compression-gzip", "trace"] } tracing-test = { workspace = true } rattler_conda_types = { path = "../rattler_conda_types", version = "0.22.0", default-features = false } From 70156c383fd1e908c878cb1911b20a8a11b6a71f Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Thu, 2 May 2024 15:11:44 +0200 Subject: [PATCH 29/57] move definitions of sharded repodata to `rattler_conda_types` --- crates/rattler_conda_types/src/lib.rs | 1 + .../rattler_conda_types/src/repo_data/mod.rs | 1 + .../src/repo_data/sharded.rs | 41 +++++++++++++++++++ .../src/gateway/sharded_subdir/mod.rs | 31 +------------- 4 files changed, 45 insertions(+), 29 deletions(-) create mode 100644 crates/rattler_conda_types/src/repo_data/sharded.rs diff --git a/crates/rattler_conda_types/src/lib.rs b/crates/rattler_conda_types/src/lib.rs index 1c9b74cf7..4d1a83911 100644 --- a/crates/rattler_conda_types/src/lib.rs +++ b/crates/rattler_conda_types/src/lib.rs @@ -39,6 +39,7 @@ pub use parse_mode::ParseStrictness; pub use platform::{Arch, ParseArchError, ParsePlatformError, Platform}; pub use prefix_record::PrefixRecord; pub use repo_data::patches::{PackageRecordPatch, PatchInstructions, RepoDataPatch}; +pub use repo_data::sharded::{Shard, ShardedRepodata, ShardedSubdirInfo}; pub use repo_data::{ compute_package_url, ChannelInfo, ConvertSubdirError, PackageRecord, RepoData, }; diff --git a/crates/rattler_conda_types/src/repo_data/mod.rs b/crates/rattler_conda_types/src/repo_data/mod.rs index 46959f451..58fef5021 100644 --- a/crates/rattler_conda_types/src/repo_data/mod.rs +++ b/crates/rattler_conda_types/src/repo_data/mod.rs @@ -2,6 +2,7 @@ //! of a channel. It provides indexing functionality. pub mod patches; +pub mod sharded; mod topological_sort; use std::borrow::Cow; diff --git a/crates/rattler_conda_types/src/repo_data/sharded.rs b/crates/rattler_conda_types/src/repo_data/sharded.rs new file mode 100644 index 000000000..d1a79ac9d --- /dev/null +++ b/crates/rattler_conda_types/src/repo_data/sharded.rs @@ -0,0 +1,41 @@ +//! Structs to deal with repodata "shards" which are per-package repodata files. + +use std::collections::HashMap; + +use rattler_digest::Sha256Hash; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::PackageRecord; + +/// The sharded repodata holds a hashmap of package name -> shard (hash). +/// This index file is stored under `//repodata_shards.msgpack.zst` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShardedRepodata { + /// Additional information about the sharded subdirectory such as the base url. + pub info: ShardedSubdirInfo, + /// The individual shards indexed by package name. + pub shards: HashMap, +} + +/// Information about a sharded subdirectory that is stored inside the index file. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShardedSubdirInfo { + /// The name of the subdirectory + pub subdir: String, + + /// The base url of the subdirectory. This is the location where the actual + /// packages are stored. + pub base_url: Url, +} + +/// An individual shard that contains repodata for a single package name. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Shard { + /// The records for all `.tar.bz2` packages + pub packages: HashMap, + + /// The records for all `.conda` packages + #[serde(rename = "packages.conda", default)] + pub packages_conda: HashMap, +} diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs index d6a66d5a4..cb2c4e934 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs @@ -2,11 +2,9 @@ use crate::{fetch::FetchRepoDataError, gateway::subdir::SubdirClient, GatewayErr use futures::TryFutureExt; use http::header::CACHE_CONTROL; use http::HeaderValue; -use rattler_conda_types::{Channel, PackageName, PackageRecord, RepoDataRecord}; -use rattler_digest::Sha256Hash; +use rattler_conda_types::{Channel, PackageName, RepoDataRecord, Shard, ShardedRepodata}; use reqwest_middleware::ClientWithMiddleware; -use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, collections::HashMap, path::PathBuf, sync::Arc}; +use std::{borrow::Cow, path::PathBuf, sync::Arc}; use token::TokenClient; use url::Url; @@ -209,28 +207,3 @@ fn add_trailing_slash(url: &Url) -> Cow<'_, Url> { Cow::Owned(url) } } - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ShardedRepodata { - pub info: ShardedSubdirInfo, - /// The individual shards indexed by package name. - pub shards: HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Shard { - pub packages: HashMap, - - #[serde(rename = "packages.conda", default)] - pub packages_conda: HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ShardedSubdirInfo { - /// The name of the subdirectory - pub subdir: String, - - /// The base url of the subdirectory. This is the location where the actual - /// packages are stored. - pub base_url: Url, -} From fa3193c9f5fb3e97091fe48396946fdd1da573eb Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Thu, 2 May 2024 15:38:03 +0200 Subject: [PATCH 30/57] use fxhashmap --- crates/rattler_conda_types/src/repo_data/sharded.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/rattler_conda_types/src/repo_data/sharded.rs b/crates/rattler_conda_types/src/repo_data/sharded.rs index d1a79ac9d..455f66993 100644 --- a/crates/rattler_conda_types/src/repo_data/sharded.rs +++ b/crates/rattler_conda_types/src/repo_data/sharded.rs @@ -1,7 +1,5 @@ //! Structs to deal with repodata "shards" which are per-package repodata files. - -use std::collections::HashMap; - +use fxhash::FxHashMap; use rattler_digest::Sha256Hash; use serde::{Deserialize, Serialize}; use url::Url; @@ -15,7 +13,7 @@ pub struct ShardedRepodata { /// Additional information about the sharded subdirectory such as the base url. pub info: ShardedSubdirInfo, /// The individual shards indexed by package name. - pub shards: HashMap, + pub shards: FxHashMap, } /// Information about a sharded subdirectory that is stored inside the index file. @@ -33,9 +31,9 @@ pub struct ShardedSubdirInfo { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Shard { /// The records for all `.tar.bz2` packages - pub packages: HashMap, + pub packages: FxHashMap, /// The records for all `.conda` packages #[serde(rename = "packages.conda", default)] - pub packages_conda: HashMap, + pub packages_conda: FxHashMap, } From a529261a2314ca8ddfe972324e7ede987955fcb6 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Thu, 2 May 2024 15:43:00 +0200 Subject: [PATCH 31/57] feat: progress reporting --- crates/rattler-bin/src/commands/create.rs | 18 +- crates/rattler_repodata_gateway/Cargo.toml | 4 +- .../src/fetch/jlap/mod.rs | 130 +++++---- .../rattler_repodata_gateway/src/fetch/mod.rs | 101 ++++--- .../src/gateway/error.rs | 10 + .../src/gateway/local_subdir.rs | 36 +-- .../src/gateway/mod.rs | 252 ++---------------- .../src/gateway/query.rs | 221 +++++++++++++++ .../src/gateway/remote_subdir.rs | 4 +- .../src/gateway/repo_data.rs | 6 +- .../src/gateway/sharded_subdir/index.rs | 33 ++- .../src/gateway/sharded_subdir/mod.rs | 20 +- .../src/gateway/sharded_subdir/token.rs | 27 +- .../src/gateway/subdir.rs | 9 +- crates/rattler_repodata_gateway/src/lib.rs | 2 + .../rattler_repodata_gateway/src/reporter.rs | 114 ++++++++ .../src/utils/body.rs | 71 +++++ .../rattler_repodata_gateway/src/utils/mod.rs | 24 ++ 18 files changed, 676 insertions(+), 406 deletions(-) create mode 100644 crates/rattler_repodata_gateway/src/gateway/query.rs create mode 100644 crates/rattler_repodata_gateway/src/reporter.rs create mode 100644 crates/rattler_repodata_gateway/src/utils/body.rs diff --git a/crates/rattler-bin/src/commands/create.rs b/crates/rattler-bin/src/commands/create.rs index 1cad36d28..4a757538f 100644 --- a/crates/rattler-bin/src/commands/create.rs +++ b/crates/rattler-bin/src/commands/create.rs @@ -25,7 +25,7 @@ use rattler_solve::{ resolvo, ChannelPriority, RepoDataIter, SolverImpl, SolverTask, }; use reqwest::Client; -use std::future::Future; +use std::future::IntoFuture; use std::sync::Arc; use std::time::Instant; use std::{ @@ -133,11 +133,13 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { let start_load_repo_data = Instant::now(); let repo_data = wrap_in_async_progress( "loading repodata", - gateway.load_records_recursive( - channels, - [install_platform, Platform::NoArch], - specs.clone(), - ), + gateway + .query( + channels, + [install_platform, Platform::NoArch], + specs.clone(), + ) + .recursive(true), ) .await .context("failed to load repodata")?; @@ -551,7 +553,7 @@ fn wrap_in_progress T>(msg: impl Into>, func } /// Displays a spinner with the given message while running the specified function to completion. -async fn wrap_in_async_progress>( +async fn wrap_in_async_progress>( msg: impl Into>, fut: F, ) -> T { @@ -559,7 +561,7 @@ async fn wrap_in_async_progress>( pb.enable_steady_tick(Duration::from_millis(100)); pb.set_style(long_running_progress_style()); pb.set_message(msg); - let result = fut.await; + let result = fut.into_future().await; pb.finish_and_clear(); result } diff --git a/crates/rattler_repodata_gateway/Cargo.toml b/crates/rattler_repodata_gateway/Cargo.toml index d8f1895af..4173253c2 100644 --- a/crates/rattler_repodata_gateway/Cargo.toml +++ b/crates/rattler_repodata_gateway/Cargo.toml @@ -14,7 +14,7 @@ readme.workspace = true async-trait = { workspace = true } async-compression = { workspace = true, features = ["gzip", "tokio", "bzip2", "zstd"] } blake2 = { workspace = true } -bytes = { workspace = true, optional = true } +bytes = { workspace = true } cache_control = { workspace = true } chrono = { workspace = true, features = ["std", "serde", "alloc", "clock"] } dashmap = { workspace = true } @@ -75,5 +75,5 @@ rattler_conda_types = { path = "../rattler_conda_types", version = "0.22.0", def default = ['native-tls'] native-tls = ['reqwest/native-tls', 'reqwest/native-tls-alpn'] rustls-tls = ['reqwest/rustls-tls'] -sparse = ["rattler_conda_types", "memmap2", "ouroboros", "superslice", "itertools", "serde_json/raw_value", "bytes"] +sparse = ["rattler_conda_types", "memmap2", "ouroboros", "superslice", "itertools", "serde_json/raw_value"] gateway = ["sparse", "http", "http-cache-semantics", "rayon", "tokio-rayon", "parking_lot"] diff --git a/crates/rattler_repodata_gateway/src/fetch/jlap/mod.rs b/crates/rattler_repodata_gateway/src/fetch/jlap/mod.rs index d003f5c6d..8e7cc3620 100644 --- a/crates/rattler_repodata_gateway/src/fetch/jlap/mod.rs +++ b/crates/rattler_repodata_gateway/src/fetch/jlap/mod.rs @@ -70,7 +70,8 @@ //! &client, //! subdir_url, //! repo_data_state, -//! ¤t_repo_data +//! ¤t_repo_data, +//! None //! ).await.unwrap(); //! //! // Now we can use the `updated_jlap_state` object to update our `.info.json` file @@ -90,8 +91,8 @@ use reqwest::{ }; use reqwest_middleware::ClientWithMiddleware; use serde::{Deserialize, Serialize}; +use serde_json::Value; use serde_with::serde_as; -use std::collections::{BTreeMap, HashMap}; use std::io::Write; use std::iter::Iterator; use std::path::Path; @@ -99,10 +100,12 @@ use std::str; use std::str::FromStr; use std::sync::Arc; use tempfile::NamedTempFile; -use tokio::task::JoinError; use url::Url; pub use crate::fetch::cache::{JLAPFooter, JLAPState, RepoDataState}; +use crate::reporter::ResponseReporterExt; +use crate::utils::{run_blocking_task, Cancelled}; +use crate::Reporter; /// File suffix for JLAP file pub const JLAP_FILE_SUFFIX: &str = "jlap"; @@ -164,6 +167,12 @@ pub enum JLAPError { Cancelled, } +impl From for JLAPError { + fn from(_: Cancelled) -> Self { + JLAPError::Cancelled + } +} + impl From for JLAPError { fn from(value: reqwest_middleware::Error) -> Self { Self::HTTP(value.redact()) @@ -304,6 +313,7 @@ impl<'a> JLAPResponse<'a> { &self, repo_data_json_path: &Path, hash: Output, + reporter: Option>, ) -> Result { // We use the current hash to find which patches we need to apply let current_idx = self.patches.iter().position(|patch| patch.from == hash); @@ -313,18 +323,12 @@ impl<'a> JLAPResponse<'a> { // Apply the patches on a blocking thread. Applying the patches is a relatively CPU intense // operation and we don't want to block the tokio runtime. - let patches = self.patches.clone(); + let repo_data_path = self.patches.clone(); let repo_data_json_path = repo_data_json_path.to_path_buf(); - match tokio::task::spawn_blocking(move || { - apply_jlap_patches(patches, idx, &repo_data_json_path) + run_blocking_task(move || { + apply_jlap_patches(repo_data_path, idx, &repo_data_json_path, reporter) }) .await - .map_err(JoinError::try_into_panic) - { - Ok(hash) => hash, - Err(Ok(reason)) => std::panic::resume_unwind(reason), - Err(_) => Err(JLAPError::Cancelled), - } } /// Returns a new [`JLAPState`] based on values in [`JLAPResponse`] struct @@ -410,6 +414,7 @@ pub async fn patch_repo_data( subdir_url: Url, repo_data_state: RepoDataState, repo_data_json_path: &Path, + reporter: Option>, ) -> Result<(JLAPState, Blake2b256Hash), JLAPError> { // Determine what we should use as our starting state let mut jlap_state = get_jlap_state(repo_data_state.jlap); @@ -418,12 +423,19 @@ pub async fn patch_repo_data( .join(JLAP_FILE_NAME) .expect("Valid URLs should always be join-able with this constant value"); + let download_report = reporter + .as_deref() + .map(|reporter| (reporter, reporter.on_download_start(&jlap_url))); let (response, position) = - fetch_jlap_with_retry(jlap_url.as_str(), client, jlap_state.position).await?; - let response_text = match response.text().await { + fetch_jlap_with_retry(&jlap_url, client, jlap_state.position).await?; + let jlap_response_url = response.url().clone(); + let response_text = match response.text_with_progress(download_report).await { Ok(value) => value, Err(error) => return Err(error.into()), }; + if let Some((reporter, index)) = download_report { + reporter.on_download_complete(&jlap_response_url, index); + } // Update position as it may have changed jlap_state.position = position; @@ -445,7 +457,7 @@ pub async fn patch_repo_data( } // Applies patches and returns early if an error is encountered - let hash = jlap.apply(repo_data_json_path, hash).await?; + let hash = jlap.apply(repo_data_json_path, hash, reporter).await?; // Patches were applied successfully, so we need to update the position Ok((jlap.get_state(jlap.new_position, new_iv), hash)) @@ -453,11 +465,11 @@ pub async fn patch_repo_data( /// Fetches a JLAP response from server async fn fetch_jlap( - url: &str, + url: &Url, client: &ClientWithMiddleware, range: &str, ) -> reqwest_middleware::Result { - let request_builder = client.get(url); + let request_builder = client.get(url.clone()); let mut headers = HeaderMap::default(); headers.insert( @@ -477,7 +489,7 @@ async fn fetch_jlap( /// We return a new value for position if this was triggered so that we can update the /// `JLAPState` accordingly. async fn fetch_jlap_with_retry( - url: &str, + url: &Url, client: &ClientWithMiddleware, position: u64, ) -> Result<(Response, u64), JLAPError> { @@ -502,40 +514,6 @@ async fn fetch_jlap_with_retry( } } -#[derive(Serialize, Deserialize, Default)] -struct OrderedRepoData { - info: Option>, - - #[serde(serialize_with = "ordered_map")] - packages: Option>>, - - #[serde(serialize_with = "ordered_map", rename = "packages.conda")] - packages_conda: Option>>, - - removed: Option>, - - repodata_version: Option, -} - -fn ordered_map( - value: &Option>>, - serializer: S, -) -> Result -where - S: serde::Serializer, -{ - match value { - Some(value) => { - let ordered: BTreeMap<_, _> = value - .iter() - .map(|(key, packages)| (key, packages.iter().collect::>())) - .collect(); - ordered.serialize(serializer) - } - None => serializer.serialize_none(), - } -} - /// Applies JLAP patches to a `repodata.json` file /// /// This is a multi-step process that involves: @@ -548,14 +526,28 @@ fn apply_jlap_patches( patches: Arc<[Patch]>, start_index: usize, repo_data_path: &Path, + reporter: Option>, ) -> Result { + let report = reporter + .as_deref() + .map(|reporter| (reporter, reporter.on_jlap_start())); + + if let Some((reporter, index)) = report { + reporter.on_jlap_decode_start(index); + } + // Read the contents of the current repodata to a string let repo_data_contents = std::fs::read_to_string(repo_data_path).map_err(JLAPError::FileSystem)?; // Parse the JSON so we can manipulate it tracing::info!("parsing cached repodata.json as JSON"); - let mut doc = serde_json::from_str(&repo_data_contents).map_err(JLAPError::JSONParse)?; + let mut repo_data = + serde_json::from_str::(&repo_data_contents).map_err(JLAPError::JSONParse)?; + + if let Some((reporter, index)) = report { + reporter.on_jlap_decode_completed(index); + } // Apply any patches that we have not already applied tracing::info!( @@ -563,21 +555,22 @@ fn apply_jlap_patches( start_index + 1, patches.len() ); - for patch in patches[start_index..].iter() { - if let Err(error) = json_patch::patch_unsafe(&mut doc, &patch.patch) { + for (patch_index, patch) in patches[start_index..].iter().enumerate() { + if let Some((reporter, index)) = report { + reporter.on_jlap_apply_patch(index, patch_index, patches.len()); + } + if let Err(error) = json_patch::patch_unsafe(&mut repo_data, &patch.patch) { return Err(JLAPError::JSONPatch(error)); } } - // Order the json - tracing::info!("converting patched JSON back to repodata"); - let ordered_doc: OrderedRepoData = serde_json::from_value(doc).map_err(JLAPError::JSONParse)?; + if let Some((reporter, index)) = report { + reporter.on_jlap_apply_patches_completed(index); + reporter.on_jlap_encode_start(index); + } // Convert the json to bytes, but we don't really care about formatting. - let mut updated_json = serde_json::to_string(&ordered_doc).map_err(JLAPError::JSONParse)?; - - // We need to add an extra newline character to the end of our string so the hashes match - updated_json.insert(updated_json.len(), '\n'); + let updated_json = serde_json::to_string(&repo_data).map_err(JLAPError::JSONParse)?; // Write the content to disk and immediately compute the hash of the file contents. tracing::info!("writing patched repodata to disk"); @@ -595,6 +588,11 @@ fn apply_jlap_patches( file.persist(repo_data_path) .map_err(|e| JLAPError::FileSystem(e.error))?; + if let Some((reporter, index)) = report { + reporter.on_jlap_encode_completed(index); + reporter.on_jlap_completed(index); + } + Ok(hash) } @@ -779,12 +777,11 @@ mod test { }, "removed": [], "repodata_version": 1 -} -"#; +}"#; - const FAKE_REPO_DATA_UPDATE_ONE: &str = "{\"info\":{\"subdir\":\"osx-64\"},\"packages\":{},\"packages.conda\":{\"zstd-1.5.4-hc035e20_0.conda\":{\"build\":\"hc035e20_0\",\"build_number\":0,\"depends\":[\"libcxx >=14.0.6\",\"lz4-c >=1.9.4,<1.10.0a0\",\"xz >=5.2.10,<6.0a0\",\"zlib >=1.2.13,<1.3.0a0\"],\"license\":\"BSD-3-Clause AND GPL-2.0-or-later\",\"license_family\":\"BSD\",\"md5\":\"f284fea068c51b1a0eaea3ac58c300c0\",\"name\":\"zstd\",\"sha256\":\"0af4513ef7ad7fa8854fa714130c25079f3744471fc106f47df80eb10c34429d\",\"size\":605550,\"subdir\":\"osx-64\",\"timestamp\":1680034665911,\"version\":\"1.5.4\"},\"zstd-1.5.5-hc035e20_0.conda\":{\"build\":\"hc035e20_0\",\"build_number\":0,\"depends\":[\"libcxx >=14.0.6\",\"lz4-c >=1.9.4,<1.10.0a0\",\"xz >=5.2.10,<6.0a0\",\"zlib >=1.2.13,<1.3.0a0\"],\"license\":\"BSD-3-Clause AND GPL-2.0-or-later\",\"license_family\":\"BSD\",\"md5\":\"5e0b7ddb1b7dc6b630e1f9a03499c19c\",\"name\":\"zstd\",\"sha256\":\"5b192501744907b841de036bb89f5a2776b4cac5795ccc25dcaebeac784db038\",\"size\":622467,\"subdir\":\"osx-64\",\"timestamp\":1681304595869,\"version\":\"1.5.5\"}},\"removed\":[],\"repodata_version\":1}\n"; + const FAKE_REPO_DATA_UPDATE_ONE: &str = "{\"info\":{\"subdir\":\"osx-64\"},\"packages\":{},\"packages.conda\":{\"zstd-1.5.4-hc035e20_0.conda\":{\"build\":\"hc035e20_0\",\"build_number\":0,\"depends\":[\"libcxx >=14.0.6\",\"lz4-c >=1.9.4,<1.10.0a0\",\"xz >=5.2.10,<6.0a0\",\"zlib >=1.2.13,<1.3.0a0\"],\"license\":\"BSD-3-Clause AND GPL-2.0-or-later\",\"license_family\":\"BSD\",\"md5\":\"f284fea068c51b1a0eaea3ac58c300c0\",\"name\":\"zstd\",\"sha256\":\"0af4513ef7ad7fa8854fa714130c25079f3744471fc106f47df80eb10c34429d\",\"size\":605550,\"subdir\":\"osx-64\",\"timestamp\":1680034665911,\"version\":\"1.5.4\"},\"zstd-1.5.5-hc035e20_0.conda\":{\"build\":\"hc035e20_0\",\"build_number\":0,\"depends\":[\"libcxx >=14.0.6\",\"lz4-c >=1.9.4,<1.10.0a0\",\"xz >=5.2.10,<6.0a0\",\"zlib >=1.2.13,<1.3.0a0\"],\"license\":\"BSD-3-Clause AND GPL-2.0-or-later\",\"license_family\":\"BSD\",\"md5\":\"5e0b7ddb1b7dc6b630e1f9a03499c19c\",\"name\":\"zstd\",\"sha256\":\"5b192501744907b841de036bb89f5a2776b4cac5795ccc25dcaebeac784db038\",\"size\":622467,\"subdir\":\"osx-64\",\"timestamp\":1681304595869,\"version\":\"1.5.5\"}},\"removed\":[],\"repodata_version\":1}"; - const FAKE_REPO_DATA_UPDATE_TWO: &str = "{\"info\":{\"subdir\":\"osx-64\"},\"packages\":{},\"packages.conda\":{\"zstd-1.5.4-hc035e20_0.conda\":{\"build\":\"hc035e20_0\",\"build_number\":0,\"depends\":[\"libcxx >=14.0.6\",\"lz4-c >=1.9.4,<1.10.0a0\",\"xz >=5.2.10,<6.0a0\",\"zlib >=1.2.13,<1.3.0a0\"],\"license\":\"BSD-3-Clause AND GPL-2.0-or-later\",\"license_family\":\"BSD\",\"md5\":\"f284fea068c51b1a0eaea3ac58c300c0\",\"name\":\"zstd\",\"sha256\":\"0af4513ef7ad7fa8854fa714130c25079f3744471fc106f47df80eb10c34429d\",\"size\":605550,\"subdir\":\"osx-64\",\"timestamp\":1680034665911,\"version\":\"1.5.4\"},\"zstd-1.5.5-hc035e20_0.conda\":{\"build\":\"hc035e20_0\",\"build_number\":0,\"depends\":[\"libcxx >=14.0.6\",\"lz4-c >=1.9.4,<1.10.0a0\",\"xz >=5.2.10,<6.0a0\",\"zlib >=1.2.13,<1.3.0a0\"],\"license\":\"BSD-3-Clause AND GPL-2.0-or-later\",\"license_family\":\"BSD\",\"md5\":\"5e0b7ddb1b7dc6b630e1f9a03499c19c\",\"name\":\"zstd\",\"sha256\":\"5b192501744907b841de036bb89f5a2776b4cac5795ccc25dcaebeac784db038\",\"size\":622467,\"subdir\":\"osx-64\",\"timestamp\":1681304595869,\"version\":\"1.5.5\"},\"zstd-static-1.4.5-hb1e8313_0.conda\":{\"build\":\"hb1e8313_0\",\"build_number\":0,\"depends\":[\"libcxx >=10.0.0\",\"zstd 1.4.5 h41d2c2f_0\"],\"license\":\"BSD 3-Clause\",\"md5\":\"5447986040e0b73d6c681a4d8f615d6c\",\"name\":\"zstd-static\",\"sha256\":\"3759ab53ff8320d35c6db00d34059ba99058eeec1cbdd0da968c5e12f73f7658\",\"size\":13930,\"subdir\":\"osx-64\",\"timestamp\":1595965109852,\"version\":\"1.4.5\"}},\"removed\":[],\"repodata_version\":1}\n"; + const FAKE_REPO_DATA_UPDATE_TWO: &str = "{\"info\":{\"subdir\":\"osx-64\"},\"packages\":{},\"packages.conda\":{\"zstd-1.5.4-hc035e20_0.conda\":{\"build\":\"hc035e20_0\",\"build_number\":0,\"depends\":[\"libcxx >=14.0.6\",\"lz4-c >=1.9.4,<1.10.0a0\",\"xz >=5.2.10,<6.0a0\",\"zlib >=1.2.13,<1.3.0a0\"],\"license\":\"BSD-3-Clause AND GPL-2.0-or-later\",\"license_family\":\"BSD\",\"md5\":\"f284fea068c51b1a0eaea3ac58c300c0\",\"name\":\"zstd\",\"sha256\":\"0af4513ef7ad7fa8854fa714130c25079f3744471fc106f47df80eb10c34429d\",\"size\":605550,\"subdir\":\"osx-64\",\"timestamp\":1680034665911,\"version\":\"1.5.4\"},\"zstd-1.5.5-hc035e20_0.conda\":{\"build\":\"hc035e20_0\",\"build_number\":0,\"depends\":[\"libcxx >=14.0.6\",\"lz4-c >=1.9.4,<1.10.0a0\",\"xz >=5.2.10,<6.0a0\",\"zlib >=1.2.13,<1.3.0a0\"],\"license\":\"BSD-3-Clause AND GPL-2.0-or-later\",\"license_family\":\"BSD\",\"md5\":\"5e0b7ddb1b7dc6b630e1f9a03499c19c\",\"name\":\"zstd\",\"sha256\":\"5b192501744907b841de036bb89f5a2776b4cac5795ccc25dcaebeac784db038\",\"size\":622467,\"subdir\":\"osx-64\",\"timestamp\":1681304595869,\"version\":\"1.5.5\"},\"zstd-static-1.4.5-hb1e8313_0.conda\":{\"build\":\"hb1e8313_0\",\"build_number\":0,\"depends\":[\"libcxx >=10.0.0\",\"zstd 1.4.5 h41d2c2f_0\"],\"license\":\"BSD 3-Clause\",\"md5\":\"5447986040e0b73d6c681a4d8f615d6c\",\"name\":\"zstd-static\",\"sha256\":\"3759ab53ff8320d35c6db00d34059ba99058eeec1cbdd0da968c5e12f73f7658\",\"size\":13930,\"subdir\":\"osx-64\",\"timestamp\":1595965109852,\"version\":\"1.4.5\"}},\"removed\":[],\"repodata_version\":1}"; const FAKE_REPO_DATA_UPDATE_ONE_HASH: &str = "9b76165ba998f77b2f50342006192bf28817dad474d78d760ab12cc0260e3ed9"; @@ -951,6 +948,7 @@ mod test { test_env.server_url, test_env.repo_data_state, &test_env.cache_repo_data, + None, ) .await .unwrap(); diff --git a/crates/rattler_repodata_gateway/src/fetch/mod.rs b/crates/rattler_repodata_gateway/src/fetch/mod.rs index f25911b7d..ed8977646 100644 --- a/crates/rattler_repodata_gateway/src/fetch/mod.rs +++ b/crates/rattler_repodata_gateway/src/fetch/mod.rs @@ -1,6 +1,8 @@ //! This module provides functionality to download and cache `repodata.json` from a remote location. +use crate::reporter::ResponseReporterExt; use crate::utils::{AsyncEncoding, Encoding, LockedFile}; +use crate::Reporter; use cache::{CacheHeaders, Expiring, RepoDataState}; use cache_control::{Cachability, CacheControl}; use futures::{future::ready, FutureExt, TryStreamExt}; @@ -11,6 +13,7 @@ use reqwest::{ header::{HeaderMap, HeaderValue}, Response, StatusCode, }; +use std::sync::Arc; use std::{ io::ErrorKind, path::{Path, PathBuf}, @@ -24,9 +27,6 @@ use url::Url; mod cache; pub mod jlap; -/// Type alias for function to report progress while downloading repodata -pub type ProgressFunc = Box; - /// `RepoData` could not be found for given channel and platform #[derive(Debug, thiserror::Error)] pub enum RepoDataNotFoundError { @@ -197,17 +197,6 @@ impl Default for FetchRepoDataOptions { } } -/// A struct that provides information about download progress. -#[derive(Debug, Clone)] -pub struct DownloadProgress { - /// The number of bytes already downloaded - pub bytes: u64, - - /// The total number of bytes to download. Or `None` if this is not known. This can happen - /// if the server does not supply a `Content-Length` header. - pub total: Option, -} - /// The result of [`fetch_repo_data`]. #[derive(Debug)] pub struct CachedRepoData { @@ -321,7 +310,7 @@ pub async fn fetch_repo_data( client: reqwest_middleware::ClientWithMiddleware, cache_path: PathBuf, options: FetchRepoDataOptions, - progress: Option, + reporter: Option>, ) -> Result { let subdir_url = normalize_subdir_url(subdir_url); @@ -429,6 +418,7 @@ pub async fn fetch_repo_data( subdir_url.clone(), repo_data_state.clone(), &repo_data_json_path, + reporter.clone(), ) .await { @@ -505,6 +495,9 @@ pub async fn fetch_repo_data( cache_headers.add_to_request(&mut headers); } // Send the request and wait for a reply + let download_reporter = reporter + .as_deref() + .map(|r| (r, r.on_download_start(&repo_data_url))); let response = match request_builder.headers(headers).send().await { Ok(response) if response.status() == StatusCode::NOT_FOUND => { return Err(FetchRepoDataError::NotFound(RepoDataNotFoundError::from( @@ -551,6 +544,7 @@ pub async fn fetch_repo_data( let cache_headers = CacheHeaders::from(&response); // Stream the content to a temporary file + let response_url = response.url().clone(); let (temp_file, blake2_hash) = stream_and_decode_to_file( repo_data_url.clone(), response, @@ -562,10 +556,14 @@ pub async fn fetch_repo_data( Encoding::Passthrough }, &cache_path, - progress, + download_reporter, ) .await?; + if let Some((reporter, index)) = download_reporter { + reporter.on_download_complete(&response_url, index); + } + // Persist the file to its final destination let repo_data_destination_path = repo_data_json_path.clone(); let repo_data_json_metadata = tokio::task::spawn_blocking(move || { @@ -625,41 +623,20 @@ async fn stream_and_decode_to_file( response: Response, content_encoding: Encoding, temp_dir: &Path, - mut progress_func: Option, + reporter: Option<(&dyn Reporter, usize)>, ) -> Result<(NamedTempFile, blake2::digest::Output), FetchRepoDataError> { - // Determine the length of the response in bytes and notify the listener that a download is - // starting. The response may be compressed. Decompression happens below. - let content_size = response.content_length(); - if let Some(progress_func) = progress_func.as_mut() { - progress_func(DownloadProgress { - bytes: 0, - total: content_size, - }); - } - // Determine the encoding of the response let transfer_encoding = Encoding::from(&response); // Convert the response into a byte stream + let mut total_bytes = 0; let bytes_stream = response - .bytes_stream() + .byte_stream_with_progress(reporter) + .inspect_ok(|bytes| { + total_bytes += bytes.len(); + }) .map_err(|e| std::io::Error::new(ErrorKind::Other, e)); - // Listen in on the bytes as they come from the response. Progress is tracked here instead of - // after decoding because that doesnt properly represent the number of bytes that are being - // transferred over the network. - let mut total_bytes = 0; - let total_bytes_mut = &mut total_bytes; - let bytes_stream = bytes_stream.inspect_ok(move |bytes| { - *total_bytes_mut += bytes.len() as u64; - if let Some(progress_func) = progress_func.as_mut() { - progress_func(DownloadProgress { - bytes: *total_bytes_mut, - total: content_size, - }); - } - }); - // Create a new stream from the byte stream that decodes the bytes using the transfer encoding // on the fly. let decoded_byte_stream = StreamReader::new(bytes_stream).decode(transfer_encoding); @@ -1038,19 +1015,18 @@ fn validate_cached_state( #[cfg(test)] mod test { - use super::{ - fetch_repo_data, CacheResult, CachedRepoData, DownloadProgress, FetchRepoDataOptions, - }; + use super::{fetch_repo_data, CacheResult, CachedRepoData, FetchRepoDataOptions}; use crate::fetch::{FetchRepoDataError, RepoDataNotFoundError}; use crate::utils::simple_channel_server::SimpleChannelServer; use crate::utils::Encoding; + use crate::Reporter; use assert_matches::assert_matches; use hex_literal::hex; use rattler_networking::AuthenticationMiddleware; use reqwest::Client; use reqwest_middleware::ClientWithMiddleware; use std::path::Path; - use std::sync::atomic::{AtomicU64, Ordering}; + use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use tempfile::TempDir; use tokio::io::AsyncWriteExt; @@ -1405,12 +1381,27 @@ mod test { std::fs::write(subdir_path.path().join("repodata.json"), FAKE_REPO_DATA).unwrap(); let server = SimpleChannelServer::new(subdir_path.path()).await; - let last_download_progress = Arc::new(AtomicU64::new(0)); - let last_download_progress_captured = last_download_progress.clone(); - let download_progress = move |progress: DownloadProgress| { - last_download_progress_captured.store(progress.bytes, Ordering::SeqCst); - assert_eq!(progress.total, Some(1110)); - }; + struct BasicReporter { + last_download_progress: AtomicUsize, + } + + impl Reporter for BasicReporter { + fn on_download_progress( + &self, + _url: &Url, + _index: usize, + bytes_downloaded: usize, + total_bytes: Option, + ) { + self.last_download_progress + .store(bytes_downloaded, Ordering::SeqCst); + assert_eq!(total_bytes, Some(1110)); + } + } + + let reporter = Arc::new(BasicReporter { + last_download_progress: AtomicUsize::new(0), + }); // Download the data from the channel with an empty cache. let cache_dir = TempDir::new().unwrap(); @@ -1419,12 +1410,12 @@ mod test { ClientWithMiddleware::from(Client::new()), cache_dir.into_path(), FetchRepoDataOptions::default(), - Some(Box::new(download_progress)), + Some(reporter.clone()), ) .await .unwrap(); - assert_eq!(last_download_progress.load(Ordering::SeqCst), 1110); + assert_eq!(reporter.last_download_progress.load(Ordering::SeqCst), 1110); } #[tracing_test::traced_test] diff --git a/crates/rattler_repodata_gateway/src/gateway/error.rs b/crates/rattler_repodata_gateway/src/gateway/error.rs index a132716b0..97bb9543d 100644 --- a/crates/rattler_repodata_gateway/src/gateway/error.rs +++ b/crates/rattler_repodata_gateway/src/gateway/error.rs @@ -1,4 +1,5 @@ use crate::fetch::FetchRepoDataError; +use crate::utils::Cancelled; use thiserror::Error; #[derive(Debug, Error)] @@ -15,4 +16,13 @@ pub enum GatewayError { #[error("{0}")] Generic(String), + + #[error("the operation was cancelled")] + Cancelled, +} + +impl From for GatewayError { + fn from(_: Cancelled) -> Self { + GatewayError::Cancelled + } } diff --git a/crates/rattler_repodata_gateway/src/gateway/local_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/local_subdir.rs index 017327084..48897e383 100644 --- a/crates/rattler_repodata_gateway/src/gateway/local_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/local_subdir.rs @@ -2,10 +2,11 @@ use crate::fetch::FetchRepoDataError; use crate::gateway::subdir::SubdirClient; use crate::gateway::GatewayError; use crate::sparse::SparseRepoData; +use crate::utils::run_blocking_task; +use crate::Reporter; use rattler_conda_types::{Channel, PackageName, RepoDataRecord}; use std::path::Path; use std::sync::Arc; -use tokio::task::JoinError; /// A client that can be used to fetch repodata for a specific subdirectory from a local directory. /// @@ -22,7 +23,7 @@ impl LocalSubdirClient { ) -> Result { let repodata_path = repodata_path.to_path_buf(); let subdir = subdir.to_string(); - let sparse = match tokio::task::spawn_blocking(move || { + let sparse = run_blocking_task(move || { SparseRepoData::new(channel, subdir, &repodata_path, None).map_err(|err| { if err.kind() == std::io::ErrorKind::NotFound { GatewayError::FetchRepoDataError(FetchRepoDataError::NotFound(err.into())) @@ -31,18 +32,7 @@ impl LocalSubdirClient { } }) }) - .await - .map_err(JoinError::try_into_panic) - { - Ok(result) => result?, - Err(Ok(panic)) => std::panic::resume_unwind(panic), - Err(_) => { - return Err(GatewayError::IoError( - "loading of the repodata was cancelled".to_string(), - std::io::ErrorKind::Interrupted.into(), - )); - } - }; + .await?; Ok(Self { sparse: Arc::new(sparse), @@ -71,23 +61,17 @@ impl SubdirClient for LocalSubdirClient { async fn fetch_package_records( &self, name: &PackageName, + _reporter: Option<&dyn Reporter>, ) -> Result, GatewayError> { let sparse_repodata = self.sparse.clone(); let name = name.clone(); - match tokio::task::spawn_blocking(move || sparse_repodata.load_records(&name)) - .await - .map_err(JoinError::try_into_panic) - { - Ok(Ok(records)) => Ok(records.into()), - Ok(Err(err)) => Err(GatewayError::IoError( + run_blocking_task(move || match sparse_repodata.load_records(&name) { + Ok(records) => Ok(records.into()), + Err(err) => Err(GatewayError::IoError( "failed to extract repodata records from sparse repodata".to_string(), err, )), - Err(Ok(panic)) => std::panic::resume_unwind(panic), - Err(Err(_)) => Err(GatewayError::IoError( - "loading of the records was cancelled".to_string(), - std::io::ErrorKind::Interrupted.into(), - )), - } + }) + .await } } diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index 8ad74d251..f046c2fec 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -3,6 +3,7 @@ mod builder; mod channel_config; mod error; mod local_subdir; +mod query; mod remote_subdir; mod repo_data; mod sharded_subdir; @@ -12,27 +13,24 @@ pub use barrier_cell::BarrierCell; pub use builder::GatewayBuilder; pub use channel_config::{ChannelConfig, SourceConfig}; pub use error::GatewayError; +pub use query::GatewayQuery; pub use repo_data::RepoData; use crate::fetch::FetchRepoDataError; +use crate::Reporter; use dashmap::{mapref::entry::Entry, DashMap}; -use futures::{select_biased, stream::FuturesUnordered, StreamExt}; -use itertools::Itertools; use local_subdir::LocalSubdirClient; -use rattler_conda_types::{Channel, MatchSpec, PackageName, Platform}; +use rattler_conda_types::{Channel, MatchSpec, Platform}; use reqwest_middleware::ClientWithMiddleware; -use std::collections::HashMap; use std::{ - borrow::Borrow, - collections::HashSet, path::PathBuf, sync::{Arc, Weak}, }; use subdir::{Subdir, SubdirData}; use tokio::sync::broadcast; -/// Central access point for high level queries about [`RepoDataRecord`]s from -/// different channels. +/// Central access point for high level queries about +/// [`rattler_conda_types::RepoDataRecord`]s from different channels. /// /// The gateway is responsible for fetching and caching repodata. Requests are /// deduplicated which means that if multiple requests are made for the same @@ -66,232 +64,27 @@ impl Gateway { GatewayBuilder::default() } - /// Recursively loads all repodata records for the given channels, platforms - /// and specs. - /// - /// The `specs` passed to this are the root specs. The function will also - /// recursively fetch the dependencies of the packages that match the root - /// specs. Only the dependencies of the records that match the root specs - /// will be fetched. - /// - /// This function will asynchronously load the repodata from all - /// subdirectories (combination of channels and platforms). - /// - /// Most processing will happen on the background so downloading and - /// parsing can happen simultaneously. - /// - /// Repodata is cached by the [`Gateway`] so calling this function twice - /// with the same channels will not result in the repodata being fetched - /// twice. - pub async fn load_records_recursive< - AsChannel, - ChannelIter, - PlatformIter, - PackageNameIter, - IntoMatchSpec, - >( + /// Constructs a new [`GatewayQuery`] which can be used to query repodata records. + pub fn query( &self, channels: ChannelIter, platforms: PlatformIter, specs: PackageNameIter, - ) -> Result, GatewayError> + ) -> GatewayQuery where - AsChannel: Borrow + Clone, + AsChannel: Into, ChannelIter: IntoIterator, PlatformIter: IntoIterator, ::IntoIter: Clone, PackageNameIter: IntoIterator, IntoMatchSpec: Into, { - self.load_records_inner(channels, platforms, specs, true) - .await - } - - /// Recursively loads all repodata records for the given channels, platforms - /// and specs. - /// - /// This function will asynchronously load the repodata from all - /// subdirectories (combination of channels and platforms). - /// - /// Most processing will happen on the background so downloading and parsing - /// can happen simultaneously. - /// - /// Repodata is cached by the [`Gateway`] so calling this function twice - /// with the same channels will not result in the repodata being fetched - /// twice. - /// - /// To also fetch the dependencies of the packages use - /// [`Gateway::load_records_recursive`]. - pub async fn load_records< - AsChannel, - ChannelIter, - PlatformIter, - PackageNameIter, - IntoPackageName, - >( - &self, - channels: ChannelIter, - platforms: PlatformIter, - names: PackageNameIter, - ) -> Result, GatewayError> - where - AsChannel: Borrow + Clone, - ChannelIter: IntoIterator, - PlatformIter: IntoIterator, - ::IntoIter: Clone, - PackageNameIter: IntoIterator, - IntoPackageName: Into, - { - self.load_records_inner( - channels, - platforms, - names.into_iter().map(|name| MatchSpec::from(name.into())), - false, + GatewayQuery::new( + self.clone(), + channels.into_iter().map(Into::into).collect(), + platforms.into_iter().collect(), + specs.into_iter().map(Into::into).collect(), ) - .await - } - - async fn load_records_inner< - AsChannel, - ChannelIter, - PlatformIter, - MatchSpecIter, - IntoMatchSpec, - >( - &self, - channels: ChannelIter, - platforms: PlatformIter, - specs: MatchSpecIter, - recursive: bool, - ) -> Result, GatewayError> - where - AsChannel: Borrow + Clone, - ChannelIter: IntoIterator, - PlatformIter: IntoIterator, - ::IntoIter: Clone, - MatchSpecIter: IntoIterator, - IntoMatchSpec: Into, - { - // Collect all the channels and platforms together - let channels = channels.into_iter().collect_vec(); - let channel_count = channels.len(); - let channels_and_platforms = channels - .into_iter() - .enumerate() - .cartesian_product(platforms.into_iter()) - .collect_vec(); - - // Create barrier cells for each subdirectory. This can be used to wait until the subdir - // becomes available. - let mut subdirs = Vec::with_capacity(channels_and_platforms.len()); - let mut pending_subdirs = FuturesUnordered::new(); - for ((channel_idx, channel), platform) in channels_and_platforms { - // Create a barrier so work that need this subdir can await it. - let barrier = Arc::new(BarrierCell::new()); - subdirs.push((channel_idx, barrier.clone())); - - let inner = self.inner.clone(); - pending_subdirs.push(async move { - match inner.get_or_create_subdir(channel.borrow(), platform).await { - Ok(subdir) => { - barrier.set(subdir).expect("subdir was set twice"); - Ok(()) - } - Err(e) => Err(e), - } - }); - } - - // Package names that we have or will issue requests for. - let mut seen = HashSet::new(); - let mut pending_package_specs = HashMap::new(); - for spec in specs { - let spec = spec.into(); - if let Some(name) = &spec.name { - seen.insert(name.clone()); - pending_package_specs - .entry(name.clone()) - .or_insert_with(Vec::new) - .push(spec); - } - } - - // A list of futures to fetch the records for the pending package names. The main task - // awaits these futures. - let mut pending_records = FuturesUnordered::new(); - - // The resulting list of repodata records. - let mut result = vec![RepoData::default(); channel_count]; - - // Loop until all pending package names have been fetched. - loop { - // Iterate over all pending package names and create futures to fetch them from all - // subdirs. - for (package_name, specs) in pending_package_specs.drain() { - for (channel_idx, subdir) in subdirs.iter().cloned() { - let specs = specs.clone(); - let package_name = package_name.clone(); - pending_records.push(async move { - let barrier_cell = subdir.clone(); - let subdir = barrier_cell.wait().await; - match subdir.as_ref() { - Subdir::Found(subdir) => subdir - .get_or_fetch_package_records(&package_name) - .await - .map(|records| (channel_idx, specs, records)), - Subdir::NotFound => Ok((channel_idx, specs, Arc::from(vec![]))), - } - }); - } - } - - // Wait for the subdir to become available. - select_biased! { - // Handle any error that was emitted by the pending subdirs. - subdir_result = pending_subdirs.select_next_some() => { - subdir_result?; - } - - // Handle any records that were fetched - records = pending_records.select_next_some() => { - let (channel_idx, request_specs, records) = records?; - - if recursive { - // Extract the dependencies from the records and recursively add them to the - // list of package names that we need to fetch. - for record in records.iter() { - if !request_specs.iter().any(|spec| spec.matches(&record.package_record)) { - // Do not recurse into records that do not match to root spec. - continue; - } - for dependency in &record.package_record.depends { - let dependency_name = PackageName::new_unchecked( - dependency.split_once(' ').unwrap_or((dependency, "")).0, - ); - if seen.insert(dependency_name.clone()) { - pending_package_specs.insert(dependency_name.clone(), vec![dependency_name.into()]); - } - } - } - } - - // Add the records to the result - if records.len() > 0 { - let result = &mut result[channel_idx]; - result.len += records.len(); - result.shards.push(records); - } - } - - // All futures have been handled, all subdirectories have been loaded and all - // repodata records have been fetched. - complete => { - break; - } - } - } - - Ok(result) } } @@ -325,6 +118,7 @@ impl GatewayInner { &self, channel: &Channel, platform: Platform, + reporter: Option<&dyn Reporter>, ) -> Result, GatewayError> { let sender = match self.subdirs.entry((channel.clone(), platform)) { Entry::Vacant(entry) => { @@ -382,7 +176,7 @@ impl GatewayInner { // // Let's start by creating the subdir. If an error occurs we immediately return the error. // This will drop the sender and all other waiting tasks will receive an error. - let subdir = Arc::new(self.create_subdir(channel, platform).await?); + let subdir = Arc::new(self.create_subdir(channel, platform, reporter).await?); // Store the fetched files in the entry. self.subdirs.insert( @@ -401,6 +195,7 @@ impl GatewayInner { &self, channel: &Channel, platform: Platform, + reporter: Option<&dyn Reporter>, ) -> Result { let url = channel.platform_url(platform); let subdir_data = if url.scheme() == "file" { @@ -423,6 +218,7 @@ impl GatewayInner { self.client.clone(), self.cache.clone(), self.concurrent_requests_semaphore.clone(), + reporter, ) .await .map(SubdirData::from_client) @@ -493,11 +289,12 @@ mod test { let gateway = Gateway::new(); let records = gateway - .load_records_recursive( + .query( vec![local_conda_forge()], vec![Platform::Linux64, Platform::Win32, Platform::NoArch], vec![PackageName::from_str("rubin-env").unwrap()].into_iter(), ) + .recursive(true) .await .unwrap(); @@ -512,11 +309,12 @@ mod test { let index = remote_conda_forge().await; let records = gateway - .load_records_recursive( + .query( vec![index.channel()], vec![Platform::Linux64, Platform::Win32, Platform::NoArch], vec![PackageName::from_str("rubin-env").unwrap()].into_iter(), ) + .recursive(true) .await .unwrap(); @@ -524,13 +322,14 @@ mod test { assert_eq!(total_records, 45060); } + #[ignore] #[tokio::test(flavor = "multi_thread")] async fn test_sharded_gateway() { let gateway = Gateway::new(); let start = Instant::now(); let records = gateway - .load_records_recursive( + .query( vec![Channel::from_url( Url::parse("https://conda.anaconda.org/conda-forge").unwrap(), )], @@ -545,6 +344,7 @@ mod test { ] .into_iter(), ) + .recursive(true) .await .unwrap(); let end = Instant::now(); diff --git a/crates/rattler_repodata_gateway/src/gateway/query.rs b/crates/rattler_repodata_gateway/src/gateway/query.rs new file mode 100644 index 000000000..0733aaf1b --- /dev/null +++ b/crates/rattler_repodata_gateway/src/gateway/query.rs @@ -0,0 +1,221 @@ +use super::{subdir::Subdir, BarrierCell, Gateway, GatewayError, RepoData}; +use crate::Reporter; +use futures::{select_biased, stream::FuturesUnordered, FutureExt, StreamExt}; +use itertools::Itertools; +use rattler_conda_types::{Channel, MatchSpec, PackageName, Platform}; +use std::{ + collections::{HashMap, HashSet}, + future::IntoFuture, + sync::Arc, +}; + +/// Represents a query to execute with a [`Gateway`]. +/// +/// When executed the query will asynchronously load the repodata from all +/// subdirectories (combination of channels and platforms). +/// +/// Most processing will happen on the background so downloading and parsing +/// can happen simultaneously. +/// +/// Repodata is cached by the [`Gateway`] so executing the same query twice +/// with the same channels will not result in the repodata being fetched +/// twice. +#[derive(Clone)] +pub struct GatewayQuery { + /// The gateway that manages all resources + gateway: Gateway, + + /// The channels to fetch from + channels: Vec, + + /// The platforms the fetch from + platforms: Vec, + + /// The specs to fetch records for + specs: Vec, + + /// Whether to recursively fetch dependencies + recursive: bool, + + /// The reporter to use by the query. + reporter: Option>, +} + +impl GatewayQuery { + /// Constructs a new instance. This should not be called directly, use + /// [`Gateway::query`] instead. + pub(super) fn new( + gateway: Gateway, + channels: Vec, + platforms: Vec, + specs: Vec, + ) -> Self { + Self { + gateway, + channels, + platforms, + specs, + + recursive: false, + reporter: None, + } + } + + /// Sets whether the query should be recursive. If recursive is set to true + /// the query will also recursively fetch the dependencies of the packages + /// that match the root specs. + /// + /// Only the dependencies of the records that match the root specs will be + /// fetched. + #[must_use] + pub fn recursive(self, recursive: bool) -> Self { + Self { recursive, ..self } + } + + /// Sets the reporter to use for this query. + /// + /// The reporter is notified of important evens during the execution of the + /// query. This allows reporting progress back to a user. + pub fn with_reporter(self, reporter: impl Reporter + 'static) -> Self { + Self { + reporter: Some(Arc::new(reporter)), + ..self + } + } + + /// Execute the query and return the resulting repodata records. + pub async fn execute(self) -> Result, GatewayError> { + // Collect all the channels and platforms together + let channels_and_platforms = self + .channels + .iter() + .enumerate() + .cartesian_product(self.platforms.into_iter()) + .collect_vec(); + + // Create barrier cells for each subdirectory. This can be used to wait until the subdir + // becomes available. + let mut subdirs = Vec::with_capacity(channels_and_platforms.len()); + let mut pending_subdirs = FuturesUnordered::new(); + for ((channel_idx, channel), platform) in channels_and_platforms { + // Create a barrier so work that need this subdir can await it. + let barrier = Arc::new(BarrierCell::new()); + subdirs.push((channel_idx, barrier.clone())); + + let inner = self.gateway.inner.clone(); + let reporter = self.reporter.clone(); + pending_subdirs.push(async move { + match inner + .get_or_create_subdir(channel, platform, reporter.as_deref()) + .await + { + Ok(subdir) => { + barrier.set(subdir).expect("subdir was set twice"); + Ok(()) + } + Err(e) => Err(e), + } + }); + } + + // Package names that we have or will issue requests for. + let mut seen = HashSet::new(); + let mut pending_package_specs = HashMap::new(); + for spec in self.specs { + if let Some(name) = &spec.name { + seen.insert(name.clone()); + pending_package_specs + .entry(name.clone()) + .or_insert_with(Vec::new) + .push(spec); + } + } + + // A list of futures to fetch the records for the pending package names. The main task + // awaits these futures. + let mut pending_records = FuturesUnordered::new(); + + // The resulting list of repodata records. + let mut result = vec![RepoData::default(); self.channels.len()]; + + // Loop until all pending package names have been fetched. + loop { + // Iterate over all pending package names and create futures to fetch them from all + // subdirs. + for (package_name, specs) in pending_package_specs.drain() { + for (channel_idx, subdir) in subdirs.iter().cloned() { + let specs = specs.clone(); + let package_name = package_name.clone(); + let reporter = self.reporter.clone(); + pending_records.push(async move { + let barrier_cell = subdir.clone(); + let subdir = barrier_cell.wait().await; + match subdir.as_ref() { + Subdir::Found(subdir) => subdir + .get_or_fetch_package_records(&package_name, reporter) + .await + .map(|records| (channel_idx, specs, records)), + Subdir::NotFound => Ok((channel_idx, specs, Arc::from(vec![]))), + } + }); + } + } + + // Wait for the subdir to become available. + select_biased! { + // Handle any error that was emitted by the pending subdirs. + subdir_result = pending_subdirs.select_next_some() => { + subdir_result?; + } + + // Handle any records that were fetched + records = pending_records.select_next_some() => { + let (channel_idx, request_specs, records) = records?; + + if self.recursive { + // Extract the dependencies from the records and recursively add them to the + // list of package names that we need to fetch. + for record in records.iter() { + if !request_specs.iter().any(|spec| spec.matches(&record.package_record)) { + // Do not recurse into records that do not match to root spec. + continue; + } + for dependency in &record.package_record.depends { + let dependency_name = PackageName::new_unchecked( + dependency.split_once(' ').unwrap_or((dependency, "")).0, + ); + if seen.insert(dependency_name.clone()) { + pending_package_specs.insert(dependency_name.clone(), vec![dependency_name.into()]); + } + } + } + } + + // Add the records to the result + if records.len() > 0 { + let result = &mut result[channel_idx]; + result.len += records.len(); + result.shards.push(records); + } + } + + // All futures have been handled, all subdirectories have been loaded and all + // repodata records have been fetched. + complete => { + break; + } + } + } + + Ok(result) + } +} + +impl IntoFuture for GatewayQuery { + type Output = Result, GatewayError>; + type IntoFuture = futures::future::BoxFuture<'static, Self::Output>; + + fn into_future(self) -> Self::IntoFuture { + self.execute().boxed() + } +} diff --git a/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs index fd2cc35c4..71dd135c5 100644 --- a/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs @@ -1,6 +1,7 @@ use super::{local_subdir::LocalSubdirClient, GatewayError, SourceConfig}; use crate::fetch::{fetch_repo_data, FetchRepoDataOptions, Variant}; use crate::gateway::subdir::SubdirClient; +use crate::Reporter; use rattler_conda_types::{Channel, PackageName, Platform, RepoDataRecord}; use reqwest_middleware::ClientWithMiddleware; use std::{path::PathBuf, sync::Arc}; @@ -52,7 +53,8 @@ impl SubdirClient for RemoteSubdirClient { async fn fetch_package_records( &self, name: &PackageName, + reporter: Option<&dyn Reporter>, ) -> Result, GatewayError> { - self.sparse.fetch_package_records(name).await + self.sparse.fetch_package_records(name, reporter).await } } diff --git a/crates/rattler_repodata_gateway/src/gateway/repo_data.rs b/crates/rattler_repodata_gateway/src/gateway/repo_data.rs index 517def1af..6bf78b40c 100644 --- a/crates/rattler_repodata_gateway/src/gateway/repo_data.rs +++ b/crates/rattler_repodata_gateway/src/gateway/repo_data.rs @@ -2,7 +2,7 @@ use rattler_conda_types::RepoDataRecord; use std::iter::FusedIterator; use std::sync::Arc; -/// A container for [`RepoDataRecord`]s that are returned from the [`Gateway`]. +/// A container for [`RepoDataRecord`]s that are returned from the [`super::Gateway`]. /// /// This struct references the same memory as the `Gateway` therefor not /// duplicating the records in memory. @@ -11,8 +11,8 @@ use std::sync::Arc; /// cheap to clone. #[derive(Default, Clone)] pub struct RepoData { - pub(super) shards: Vec>, - pub(super) len: usize, + pub(crate) shards: Vec>, + pub(crate) len: usize, } impl RepoData { diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/index.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/index.rs index 6ca8b7a00..610da9657 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/index.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/index.rs @@ -1,8 +1,9 @@ use super::{token::TokenClient, ShardedRepodata}; +use crate::reporter::ResponseReporterExt; use crate::{ fetch::{FetchRepoDataError, RepoDataNotFoundError}, utils::url_to_cache_filename, - GatewayError, + GatewayError, Reporter, }; use bytes::Bytes; use futures::{FutureExt, TryFutureExt}; @@ -30,14 +31,24 @@ pub async fn fetch_index( token_client: &TokenClient, cache_dir: &Path, concurrent_requests_semaphore: Arc, + reporter: Option<&dyn Reporter>, ) -> Result { async fn from_response( cache_path: &Path, policy: CachePolicy, response: Response, + reporter: Option<(&dyn Reporter, usize)>, ) -> Result { // Read the bytes of the response - let bytes = response.bytes().await.map_err(FetchRepoDataError::from)?; + let response_url = response.url().clone(); + let bytes = response + .bytes_with_progress(reporter) + .await + .map_err(FetchRepoDataError::from)?; + + if let Some((reporter, index)) = reporter { + reporter.on_download_complete(&response_url, index); + } // Decompress the bytes let decoded_bytes = Bytes::from(super::decode_zst_bytes_async(bytes).await?); @@ -112,7 +123,7 @@ pub async fn fetch_index( .. } => { // Get the token from the token client - let token = token_client.get_token().await?; + let token = token_client.get_token(reporter).await?; // Determine the actual URL to use for the request let shards_url = token @@ -124,7 +135,7 @@ pub async fn fetch_index( // Construct the actual request that we will send let mut request = client - .get(shards_url) + .get(shards_url.clone()) .headers(state_request.headers().clone()) .build() .expect("failed to build request for shard index"); @@ -134,6 +145,7 @@ pub async fn fetch_index( let _request_permit = concurrent_requests_semaphore.acquire().await; // Send the request + let download_reporter = reporter.map(|r| (r, r.on_download_start(&shards_url))); let response = client .execute(request) .await @@ -154,6 +166,9 @@ pub async fn fetch_index( } Err(e) => { tracing::warn!("the cached shard index has been corrupted: {e}"); + if let Some((reporter, index)) = download_reporter { + reporter.on_download_complete(response.url(), index); + } } } } @@ -162,7 +177,8 @@ pub async fn fetch_index( drop(file); tracing::debug!("shard index cache has become stale"); - return from_response(&cache_path, policy, response).await; + return from_response(&cache_path, policy, response, download_reporter) + .await; } } } @@ -172,7 +188,7 @@ pub async fn fetch_index( tracing::debug!("fetching fresh shard index"); // Get the token from the token client - let token = token_client.get_token().await?; + let token = token_client.get_token(reporter).await?; // Determine the actual URL to use for the request let shards_url = token @@ -184,7 +200,7 @@ pub async fn fetch_index( // Construct the actual request that we will send let mut request = client - .get(shards_url) + .get(shards_url.clone()) .build() .expect("failed to build request for shard index"); token.add_to_headers(request.headers_mut()); @@ -193,6 +209,7 @@ pub async fn fetch_index( let _request_permit = concurrent_requests_semaphore.acquire().await; // Do a fresh requests + let reporter = reporter.map(|r| (r, r.on_download_start(&shards_url))); let response = client .execute( request @@ -212,7 +229,7 @@ pub async fn fetch_index( }; let policy = CachePolicy::new(&canonical_request, &response); - from_response(&cache_path, policy, response).await + from_response(&cache_path, policy, response, reporter).await } /// Writes the shard index cache to disk. diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs index cb2c4e934..9590b42ba 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs @@ -1,3 +1,5 @@ +use crate::reporter::ResponseReporterExt; +use crate::Reporter; use crate::{fetch::FetchRepoDataError, gateway::subdir::SubdirClient, GatewayError}; use futures::TryFutureExt; use http::header::CACHE_CONTROL; @@ -28,6 +30,7 @@ impl ShardedSubdir { client: ClientWithMiddleware, cache_dir: PathBuf, concurrent_requests_semaphore: Arc, + reporter: Option<&dyn Reporter>, ) -> Result { // Construct the base url for the shards (e.g. `/`). let shard_base_url = add_trailing_slash(channel.base_url()) @@ -48,6 +51,7 @@ impl ShardedSubdir { &token_client, &cache_dir, concurrent_requests_semaphore.clone(), + reporter, ) .await?; @@ -74,6 +78,7 @@ impl SubdirClient for ShardedSubdir { async fn fetch_package_records( &self, name: &PackageName, + reporter: Option<&dyn Reporter>, ) -> Result, GatewayError> { // Find the shard that contains the package let Some(shard) = self.sharded_repodata.shards.get(name.as_normalized()) else { @@ -102,7 +107,7 @@ impl SubdirClient for ShardedSubdir { } // Get the token - let token = self.token_client.get_token().await?; + let token = self.token_client.get_token(reporter).await?; // Download the shard let shard_url = token @@ -122,6 +127,7 @@ impl SubdirClient for ShardedSubdir { let shard_bytes = { let _permit = self.concurrent_requests_semaphore.acquire(); + let reporter = reporter.map(|r| (r, r.on_download_start(&shard_url))); let shard_response = self .client .execute(shard_request) @@ -129,10 +135,16 @@ impl SubdirClient for ShardedSubdir { .and_then(|r| r.error_for_status().map_err(Into::into)) .map_err(FetchRepoDataError::from)?; - shard_response - .bytes() + let bytes = shard_response + .bytes_with_progress(reporter) .await - .map_err(FetchRepoDataError::from)? + .map_err(FetchRepoDataError::from)?; + + if let Some((reporter, index)) = reporter { + reporter.on_download_complete(&shard_url, index); + } + + bytes }; let shard_bytes = decode_zst_bytes_async(shard_bytes).await?; diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/token.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/token.rs index 9f12de279..aa20c1ce1 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/token.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/token.rs @@ -1,3 +1,5 @@ +use crate::reporter::ResponseReporterExt; +use crate::Reporter; use crate::{fetch::FetchRepoDataError, gateway::PendingOrFetched, GatewayError}; use chrono::{DateTime, TimeDelta, Utc}; use http::header::CACHE_CONTROL; @@ -33,7 +35,10 @@ impl TokenClient { } /// Returns the current token or fetches a new one if the current one is expired. - pub async fn get_token(&self) -> Result, GatewayError> { + pub async fn get_token( + &self, + reporter: Option<&dyn Reporter>, + ) -> Result, GatewayError> { let sender_or_receiver = { let mut token = self.token.lock(); match &*token { @@ -87,9 +92,10 @@ impl TokenClient { // Fetch the token let token = { let _permit = self.concurrent_request_semaphore.acquire().await; + let reporter = reporter.map(|r| (r, r.on_download_start(&token_url))); let response = self .client - .get(token_url) + .get(token_url.clone()) .header(CACHE_CONTROL, HeaderValue::from_static("max-age=0")) .send() .await @@ -97,12 +103,21 @@ impl TokenClient { .map_err(FetchRepoDataError::from) .map_err(GatewayError::from)?; - response - .json::() + let bytes = response + .bytes_with_progress(reporter) .await .map_err(FetchRepoDataError::from) - .map_err(GatewayError::from) - .map(Arc::new)? + .map_err(GatewayError::from)?; + + if let Some((reporter, index)) = reporter { + reporter.on_download_complete(&token_url, index); + } + + let token: Token = serde_json::from_slice(&bytes).map_err(|e| { + GatewayError::IoError("failed to parse sharded index token".to_string(), e.into()) + })?; + + Arc::new(token) }; // Reacquire the token diff --git a/crates/rattler_repodata_gateway/src/gateway/subdir.rs b/crates/rattler_repodata_gateway/src/gateway/subdir.rs index c83dda100..971233c58 100644 --- a/crates/rattler_repodata_gateway/src/gateway/subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/subdir.rs @@ -1,5 +1,6 @@ use super::GatewayError; use crate::gateway::PendingOrFetched; +use crate::Reporter; use dashmap::mapref::entry::Entry; use dashmap::DashMap; use rattler_conda_types::{PackageName, RepoDataRecord}; @@ -34,6 +35,7 @@ impl SubdirData { pub async fn get_or_fetch_package_records( &self, name: &PackageName, + reporter: Option>, ) -> Result, GatewayError> { let sender = match self.records.entry(name.clone()) { Entry::Vacant(entry) => { @@ -95,7 +97,11 @@ impl SubdirData { let records = match tokio::spawn({ let client = self.client.clone(); let name = name.clone(); - async move { client.fetch_package_records(&name).await } + async move { + client + .fetch_package_records(&name, reporter.as_deref()) + .await + } }) .await .map_err(JoinError::try_into_panic) @@ -130,5 +136,6 @@ pub trait SubdirClient: Send + Sync { async fn fetch_package_records( &self, name: &PackageName, + reporter: Option<&dyn Reporter>, ) -> Result, GatewayError>; } diff --git a/crates/rattler_repodata_gateway/src/lib.rs b/crates/rattler_repodata_gateway/src/lib.rs index 84d76290f..e86eaf556 100644 --- a/crates/rattler_repodata_gateway/src/lib.rs +++ b/crates/rattler_repodata_gateway/src/lib.rs @@ -61,9 +61,11 @@ //! ``` pub mod fetch; +mod reporter; #[cfg(feature = "sparse")] pub mod sparse; mod utils; +pub use reporter::Reporter; #[cfg(feature = "gateway")] mod gateway; diff --git a/crates/rattler_repodata_gateway/src/reporter.rs b/crates/rattler_repodata_gateway/src/reporter.rs new file mode 100644 index 000000000..6a8c8ccdd --- /dev/null +++ b/crates/rattler_repodata_gateway/src/reporter.rs @@ -0,0 +1,114 @@ +use crate::utils::BodyStreamExt; +use bytes::Bytes; +use futures::{Stream, TryStreamExt}; +use std::future::Future; +use url::Url; + +/// A trait that enables being notified of download progress. +pub trait Reporter: Send + Sync { + /// Called when a download of a file started. + /// + /// Returns an index that can be used to identify the download in subsequent calls. + fn on_download_start(&self, _url: &Url) -> usize { + 0 + } + + /// Called when the download of a file makes any progress. + /// + /// The `total_bytes` parameter is `None` if the total size of the file is unknown. + /// + /// The `index` parameter is the index returned by `on_download_start`. + fn on_download_progress( + &self, + _url: &Url, + _index: usize, + _bytes_downloaded: usize, + _total_bytes: Option, + ) { + } + + /// Called when the download of a file finished. + /// + /// The `index` parameter is the index returned by `on_download_start`. + fn on_download_complete(&self, _url: &Url, _index: usize) {} + + /// Called when starting to apply JLAP to existing repodata. + /// + /// This function should return a unique index that can be used to + /// identify the subsequent JLAP operation. + fn on_jlap_start(&self) -> usize { + 0 + } + + /// Called when reading and decoding the repodata started. + fn on_jlap_decode_start(&self, _index: usize) {} + + /// Called when reading and decoding the repodata completed. + fn on_jlap_decode_completed(&self, _index: usize) {} + + /// Called when starting to apply a JLAP patch. + fn on_jlap_apply_patch(&self, _index: usize, _patch_index: usize, _total: usize) {} + + /// Called when all JLAP patches have been applied. + fn on_jlap_apply_patches_completed(&self, _index: usize) {} + + /// Called when reading and decoding the repodata started. + fn on_jlap_encode_start(&self, _index: usize) {} + + /// Called when reading and decoding the repodata completed. + fn on_jlap_encode_completed(&self, _index: usize) {} + + /// Called when finished applying JLAP to existing repodata. + fn on_jlap_completed(&self, _index: usize) {} +} + +pub(crate) trait ResponseReporterExt { + /// Converts a response into a stream of bytes, notifying a reporter of the progress. + fn byte_stream_with_progress( + self, + reporter: Option<(&dyn Reporter, usize)>, + ) -> impl Stream>; + + /// Reads all the bytes from a stream and notifies a reporter of the progress. + fn bytes_with_progress( + self, + reporter: Option<(&dyn Reporter, usize)>, + ) -> impl Future>>; + + /// Reads all the bytes from a stream and convert it to text and notifies a reporter of the progress. + fn text_with_progress( + self, + reporter: Option<(&dyn Reporter, usize)>, + ) -> impl Future>; +} + +impl ResponseReporterExt for reqwest::Response { + fn byte_stream_with_progress( + self, + reporter: Option<(&dyn Reporter, usize)>, + ) -> impl Stream> { + let total_size = self.content_length().map(|len| len as usize); + let url = self.url().clone(); + let mut bytes_read = 0; + self.bytes_stream().inspect_ok(move |bytes| { + if let Some((reporter, index)) = reporter { + bytes_read += bytes.len(); + reporter.on_download_progress(&url, index, bytes_read, total_size); + } + }) + } + + fn bytes_with_progress( + self, + reporter: Option<(&dyn Reporter, usize)>, + ) -> impl Future>> { + self.byte_stream_with_progress(reporter).bytes() + } + + fn text_with_progress( + self, + reporter: Option<(&dyn Reporter, usize)>, + ) -> impl Future> { + self.byte_stream_with_progress(reporter).text() + } +} diff --git a/crates/rattler_repodata_gateway/src/utils/body.rs b/crates/rattler_repodata_gateway/src/utils/body.rs new file mode 100644 index 000000000..4f3cab627 --- /dev/null +++ b/crates/rattler_repodata_gateway/src/utils/body.rs @@ -0,0 +1,71 @@ +use bytes::Bytes; +use futures::Stream; +use pin_project_lite::pin_project; +use std::collections::VecDeque; +use std::future::Future; +use std::marker::PhantomData; +use std::pin::Pin; +use std::task::{Context, Poll}; + +pub trait BodyStreamExt: Sized { + fn bytes(self) -> BytesCollect; + + /// Read the contents of a body stream as text. + async fn text(self) -> Result; +} + +impl>> BodyStreamExt for S { + fn bytes(self) -> BytesCollect { + BytesCollect::new(self) + } + + async fn text(self) -> Result { + let full = self.bytes().await?; + let text = String::from_utf8_lossy(&full); + Ok(text.into_owned()) + } +} + +pin_project! { + #[project = BytesCollectProj] + pub struct BytesCollect { + #[pin] + stream: S, + bytes: VecDeque, + _err: PhantomData, + } +} + +impl BytesCollect { + pub fn new(stream: S) -> Self { + Self { + stream, + bytes: VecDeque::new(), + _err: PhantomData, + } + } +} + +impl>> Future for BytesCollect { + type Output = Result, E>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + loop { + match this.stream.as_mut().poll_next(cx) { + Poll::Ready(Some(Ok(chunk))) => { + this.bytes.push_back(chunk); + } + Poll::Ready(Some(Err(e))) => return Poll::Ready(Err(e)), + Poll::Ready(None) => { + let mut result = Vec::with_capacity(this.bytes.iter().map(Bytes::len).sum()); + for chunk in this.bytes.iter() { + result.extend_from_slice(chunk); + } + return Poll::Ready(Ok(result)); + } + Poll::Pending => return Poll::Pending, + } + } + } +} diff --git a/crates/rattler_repodata_gateway/src/utils/mod.rs b/crates/rattler_repodata_gateway/src/utils/mod.rs index 66901ad00..c5353b857 100644 --- a/crates/rattler_repodata_gateway/src/utils/mod.rs +++ b/crates/rattler_repodata_gateway/src/utils/mod.rs @@ -1,6 +1,8 @@ +pub use body::BodyStreamExt; pub use encoding::{AsyncEncoding, Encoding}; pub use flock::LockedFile; use std::fmt::Write; +use tokio::task::JoinError; use url::Url; mod encoding; @@ -8,6 +10,7 @@ mod encoding; #[cfg(test)] pub(crate) mod simple_channel_server; +mod body; mod flock; /// Convert a URL to a cache filename @@ -39,6 +42,27 @@ pub(crate) fn url_to_cache_filename(url: &Url) -> String { result } +/// A marker type that is used to signal that a task was cancelled. +pub(crate) struct Cancelled; + +/// Run a blocking task to complettion. If the task is cancelled, the function +/// will return an error converted from `Error`. +pub async fn run_blocking_task(f: F) -> Result +where + F: FnOnce() -> Result + Send + 'static, + T: Send + 'static, + E: From + Send + 'static, +{ + match tokio::task::spawn_blocking(f) + .await + .map_err(JoinError::try_into_panic) + { + Ok(result) => result, + Err(Err(_err)) => Err(E::from(Cancelled)), + Err(Ok(payload)) => std::panic::resume_unwind(payload), + } +} + #[cfg(test)] mod test { use super::url_to_cache_filename; From b1b2c74e6830e5cf94fc1fa47565b202fd26e67d Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Thu, 2 May 2024 16:25:52 +0200 Subject: [PATCH 32/57] slight change to naming, add removed to shard, and make apply_patches work --- .../src/repo_data/patches.rs | 104 +++++++++++------- .../src/repo_data/sharded.rs | 7 +- .../src/gateway/sharded_subdir/mod.rs | 2 +- 3 files changed, 71 insertions(+), 42 deletions(-) diff --git a/crates/rattler_conda_types/src/repo_data/patches.rs b/crates/rattler_conda_types/src/repo_data/patches.rs index 0839924da..4eb6f4e76 100644 --- a/crates/rattler_conda_types/src/repo_data/patches.rs +++ b/crates/rattler_conda_types/src/repo_data/patches.rs @@ -6,7 +6,7 @@ use serde_with::{serde_as, skip_serializing_none, OneOrMany}; use std::io; use std::path::Path; -use crate::{package::ArchiveType, PackageRecord, PackageUrl, RepoData}; +use crate::{package::ArchiveType, PackageRecord, PackageUrl, RepoData, Shard}; /// Represents a Conda repodata patch. /// @@ -148,56 +148,82 @@ impl PackageRecord { } } -impl RepoData { - /// Apply a patch to a repodata file - /// Note that we currently do not handle `revoked` instructions - pub fn apply_patches(&mut self, instructions: &PatchInstructions) { - for (pkg, patch) in instructions.packages.iter() { - if let Some(record) = self.packages.get_mut(pkg) { - record.apply_patch(patch); - } - - // also apply the patch to the conda packages - if let Some((pkg_name, archive_type)) = ArchiveType::split_str(pkg) { - assert!(archive_type == ArchiveType::TarBz2); - if let Some(record) = self.conda_packages.get_mut(&format!("{pkg_name}.conda")) { - record.apply_patch(patch); - } - } +/// Apply a patch to a repodata file +/// Note that we currently do not handle `revoked` instructions +pub fn apply_patches_impl( + packages: &mut FxHashMap, + conda_packages: &mut FxHashMap, + removed: &mut FxHashSet, + instructions: &PatchInstructions, +) { + for (pkg, patch) in instructions.packages.iter() { + if let Some(record) = packages.get_mut(pkg) { + record.apply_patch(patch); } - for (pkg, patch) in instructions.conda_packages.iter() { - if let Some(record) = self.conda_packages.get_mut(pkg) { + // also apply the patch to the conda packages + if let Some((pkg_name, archive_type)) = ArchiveType::split_str(pkg) { + assert!(archive_type == ArchiveType::TarBz2); + if let Some(record) = conda_packages.get_mut(&format!("{pkg_name}.conda")) { record.apply_patch(patch); } } + } - let mut removed = FxHashSet::::default(); - // remove packages that have been removed - for pkg in instructions.remove.iter() { - if let Some((pkg_name, archive_type)) = ArchiveType::split_str(pkg) { - match archive_type { - ArchiveType::TarBz2 => { - if self.packages.remove_entry(pkg).is_some() { - removed.insert(pkg.clone()); - } - - // also remove equivalent .conda package if it exists - let conda_pkg_name = format!("{pkg_name}.conda"); - if self.conda_packages.remove_entry(&conda_pkg_name).is_some() { - removed.insert(conda_pkg_name); - } + for (pkg, patch) in instructions.conda_packages.iter() { + if let Some(record) = conda_packages.get_mut(pkg) { + record.apply_patch(patch); + } + } + + // remove packages that have been removed + for pkg in instructions.remove.iter() { + if let Some((pkg_name, archive_type)) = ArchiveType::split_str(pkg) { + match archive_type { + ArchiveType::TarBz2 => { + if packages.remove_entry(pkg).is_some() { + removed.insert(pkg.clone()); } - ArchiveType::Conda => { - if self.conda_packages.remove_entry(pkg).is_some() { - removed.insert(pkg.clone()); - } + + // also remove equivalent .conda package if it exists + let conda_pkg_name = format!("{pkg_name}.conda"); + if conda_packages.remove_entry(&conda_pkg_name).is_some() { + removed.insert(conda_pkg_name); + } + } + ArchiveType::Conda => { + if conda_packages.remove_entry(pkg).is_some() { + removed.insert(pkg.clone()); } } } } + } +} - self.removed.extend(removed); +impl RepoData { + /// Apply a patch to a repodata file + /// Note that we currently do not handle `revoked` instructions + pub fn apply_patches(&mut self, instructions: &PatchInstructions) { + apply_patches_impl( + &mut self.packages, + &mut self.conda_packages, + &mut self.removed, + instructions, + ); + } +} + +impl Shard { + /// Apply a patch to a shard + /// Note that we currently do not handle `revoked` instructions + pub fn apply_patches(&mut self, instructions: &PatchInstructions) { + apply_patches_impl( + &mut self.packages, + &mut self.conda_packages, + &mut self.removed, + instructions, + ); } } diff --git a/crates/rattler_conda_types/src/repo_data/sharded.rs b/crates/rattler_conda_types/src/repo_data/sharded.rs index 455f66993..26cc64e8e 100644 --- a/crates/rattler_conda_types/src/repo_data/sharded.rs +++ b/crates/rattler_conda_types/src/repo_data/sharded.rs @@ -1,5 +1,5 @@ //! Structs to deal with repodata "shards" which are per-package repodata files. -use fxhash::FxHashMap; +use fxhash::{FxHashMap, FxHashSet}; use rattler_digest::Sha256Hash; use serde::{Deserialize, Serialize}; use url::Url; @@ -35,5 +35,8 @@ pub struct Shard { /// The records for all `.conda` packages #[serde(rename = "packages.conda", default)] - pub packages_conda: FxHashMap, + pub conda_packages: FxHashMap, + + /// The file names of all removed for this shard + pub removed: FxHashSet, } diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs index 9590b42ba..a45662d64 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs @@ -192,7 +192,7 @@ async fn parse_records + Send + 'static>( .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) .map_err(FetchRepoDataError::IoError)?; let packages = - itertools::chain(shard.packages.into_iter(), shard.packages_conda.into_iter()); + itertools::chain(shard.packages.into_iter(), shard.conda_packages.into_iter()); let base_url = add_trailing_slash(&base_url); Ok(packages .map(|(file_name, package_record)| RepoDataRecord { From 5afcdac2a67d9c3abb39555b7c052e6bf4870d87 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Thu, 2 May 2024 16:46:02 +0200 Subject: [PATCH 33/57] fix: add reporter to normal subdir as well --- crates/rattler_repodata_gateway/src/gateway/mod.rs | 7 ++++--- crates/rattler_repodata_gateway/src/gateway/query.rs | 2 +- .../rattler_repodata_gateway/src/gateway/remote_subdir.rs | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index f046c2fec..cf5d3a33a 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -118,7 +118,7 @@ impl GatewayInner { &self, channel: &Channel, platform: Platform, - reporter: Option<&dyn Reporter>, + reporter: Option>, ) -> Result, GatewayError> { let sender = match self.subdirs.entry((channel.clone(), platform)) { Entry::Vacant(entry) => { @@ -195,7 +195,7 @@ impl GatewayInner { &self, channel: &Channel, platform: Platform, - reporter: Option<&dyn Reporter>, + reporter: Option>, ) -> Result { let url = channel.platform_url(platform); let subdir_data = if url.scheme() == "file" { @@ -218,7 +218,7 @@ impl GatewayInner { self.client.clone(), self.cache.clone(), self.concurrent_requests_semaphore.clone(), - reporter, + reporter.as_deref(), ) .await .map(SubdirData::from_client) @@ -229,6 +229,7 @@ impl GatewayInner { self.client.clone(), self.cache.clone(), self.channel_config.get(channel).clone(), + reporter, ) .await .map(SubdirData::from_client) diff --git a/crates/rattler_repodata_gateway/src/gateway/query.rs b/crates/rattler_repodata_gateway/src/gateway/query.rs index 0733aaf1b..353d6705a 100644 --- a/crates/rattler_repodata_gateway/src/gateway/query.rs +++ b/crates/rattler_repodata_gateway/src/gateway/query.rs @@ -106,7 +106,7 @@ impl GatewayQuery { let reporter = self.reporter.clone(); pending_subdirs.push(async move { match inner - .get_or_create_subdir(channel, platform, reporter.as_deref()) + .get_or_create_subdir(channel, platform, reporter) .await { Ok(subdir) => { diff --git a/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs b/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs index 71dd135c5..859b98b15 100644 --- a/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/remote_subdir.rs @@ -17,6 +17,7 @@ impl RemoteSubdirClient { client: ClientWithMiddleware, cache_dir: PathBuf, source_config: SourceConfig, + reporter: Option>, ) -> Result { let subdir_url = channel.platform_url(platform); @@ -32,7 +33,7 @@ impl RemoteSubdirClient { zstd_enabled: source_config.zstd_enabled, bz2_enabled: source_config.bz2_enabled, }, - None, + reporter, ) .await?; From 3d02db2cf569b38da0d93377ba2a2495fcf26a77 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Thu, 2 May 2024 17:06:40 +0200 Subject: [PATCH 34/57] fix: respect removed --- crates/rattler_conda_types/src/repo_data/sharded.rs | 1 + .../rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/rattler_conda_types/src/repo_data/sharded.rs b/crates/rattler_conda_types/src/repo_data/sharded.rs index 26cc64e8e..8b0f77b2c 100644 --- a/crates/rattler_conda_types/src/repo_data/sharded.rs +++ b/crates/rattler_conda_types/src/repo_data/sharded.rs @@ -38,5 +38,6 @@ pub struct Shard { pub conda_packages: FxHashMap, /// The file names of all removed for this shard + #[serde(default)] pub removed: FxHashSet, } diff --git a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs index a45662d64..02f08dcf1 100644 --- a/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/sharded_subdir/mod.rs @@ -192,7 +192,8 @@ async fn parse_records + Send + 'static>( .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) .map_err(FetchRepoDataError::IoError)?; let packages = - itertools::chain(shard.packages.into_iter(), shard.conda_packages.into_iter()); + itertools::chain(shard.packages.into_iter(), shard.conda_packages.into_iter()) + .filter(|(name, _record)| !shard.removed.contains(name)); let base_url = add_trailing_slash(&base_url); Ok(packages .map(|(file_name, package_record)| RepoDataRecord { From 62a385d7b92872824476664a5fc9858560dec746 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 6 May 2024 09:47:09 +0200 Subject: [PATCH 35/57] fix: deadlock --- crates/rattler_repodata_gateway/src/gateway/mod.rs | 12 +++++++++++- .../rattler_repodata_gateway/src/gateway/subdir.rs | 10 +++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index cf5d3a33a..a35209c59 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -28,6 +28,7 @@ use std::{ }; use subdir::{Subdir, SubdirData}; use tokio::sync::broadcast; +use tracing::instrument; /// Central access point for high level queries about /// [`rattler_conda_types::RepoDataRecord`]s from different channels. @@ -114,6 +115,7 @@ impl GatewayInner { /// coalesced, and they will all receive the same subdir. If an error /// occurs while creating the subdir all waiting tasks will also return an /// error. + #[instrument(skip(self, reporter), err)] async fn get_or_create_subdir( &self, channel: &Channel, @@ -139,12 +141,20 @@ impl GatewayInner { let sender = sender.upgrade(); if let Some(sender) = sender { + // Create a receiver before we drop the entry. While we hold on to + // the entry we have exclusive access to it, this means the task + // currently fetching the subdir will not be able to store a value + // until we drop the entry. + // By creating the receiver here we ensure that we are subscribed + // before the other tasks sends a value over the channel. + let mut receiver = sender.subscribe(); + // Explicitly drop the entry, so we don't block any other tasks. drop(entry); // The sender is still active, so we can wait for the subdir to be // created. - return match sender.subscribe().recv().await { + return match receiver.recv().await { Ok(subdir) => Ok(subdir), Err(_) => { // If this happens the sender was dropped. diff --git a/crates/rattler_repodata_gateway/src/gateway/subdir.rs b/crates/rattler_repodata_gateway/src/gateway/subdir.rs index 971233c58..df57352e5 100644 --- a/crates/rattler_repodata_gateway/src/gateway/subdir.rs +++ b/crates/rattler_repodata_gateway/src/gateway/subdir.rs @@ -56,12 +56,20 @@ impl SubdirData { let sender = sender.upgrade(); if let Some(sender) = sender { + // Create a receiver before we drop the entry. While we hold on to + // the entry we have exclusive access to it, this means the task + // currently fetching the package will not be able to store a value + // until we drop the entry. + // By creating the receiver here we ensure that we are subscribed + // before the other tasks sends a value over the channel. + let mut receiver = sender.subscribe(); + // Explicitly drop the entry, so we don't block any other tasks. drop(entry); // The sender is still active, so we can wait for the records to be // fetched. - return match sender.subscribe().recv().await { + return match receiver.recv().await { Ok(records) => Ok(records), Err(_) => { // If this happens the sender was dropped. We simply have to From 0b233043e95c3b6770b321cbcef3b2b4eb1889be Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Fri, 3 May 2024 15:27:34 +0200 Subject: [PATCH 36/57] feat: py-rattler bindings --- Cargo.toml | 2 +- .../src/gateway/builder.rs | 24 ++ py-rattler/Cargo.lock | 168 ++++++++++- py-rattler/Cargo.toml | 2 + py-rattler/pixi.lock | 274 +++++++++++++++++- py-rattler/pixi.toml | 7 +- py-rattler/rattler/repo_data/gateway.py | 64 ++++ py-rattler/src/channel/mod.rs | 2 +- py-rattler/src/lib.rs | 22 +- py-rattler/src/networking/mod.rs | 39 ++- py-rattler/src/repo_data/gateway.rs | 112 +++++++ py-rattler/src/repo_data/mod.rs | 1 + 12 files changed, 686 insertions(+), 31 deletions(-) create mode 100644 py-rattler/rattler/repo_data/gateway.py create mode 100644 py-rattler/src/repo_data/gateway.rs diff --git a/Cargo.toml b/Cargo.toml index 6b1ca1799..45fe2a9ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ nom = "7.1.3" num_cpus = "1.16.0" once_cell = "1.19.0" ouroboros = "0.18.3" -parking_lot = "0.12.2" +parking_lot = "0.12.1" pathdiff = "0.2.1" pep440_rs = { version = "0.5.0" } pep508_rs = { version = "0.4.2" } diff --git a/crates/rattler_repodata_gateway/src/gateway/builder.rs b/crates/rattler_repodata_gateway/src/gateway/builder.rs index 0a5c2b579..0d0686427 100644 --- a/crates/rattler_repodata_gateway/src/gateway/builder.rs +++ b/crates/rattler_repodata_gateway/src/gateway/builder.rs @@ -24,6 +24,12 @@ impl GatewayBuilder { /// Set the client to use for fetching repodata. #[must_use] pub fn with_client(mut self, client: ClientWithMiddleware) -> Self { + self.set_client(client); + self + } + + /// Set the client to use for fetching repodata. + pub fn set_client(&mut self, client: ClientWithMiddleware) -> &mut Self { self.client = Some(client); self } @@ -31,6 +37,12 @@ impl GatewayBuilder { /// Set the channel configuration to use for fetching repodata. #[must_use] pub fn with_channel_config(mut self, channel_config: ChannelConfig) -> Self { + self.set_channel_config(channel_config); + self + } + + /// Sets the channel configuration to use for fetching repodata. + pub fn set_channel_config(&mut self, channel_config: ChannelConfig) -> &mut Self { self.channel_config = channel_config; self } @@ -38,6 +50,12 @@ impl GatewayBuilder { /// Set the directory to use for caching repodata. #[must_use] pub fn with_cache_dir(mut self, cache: impl Into) -> Self { + self.set_cache_dir(cache); + self + } + + /// Set the directory to use for caching repodata. + pub fn set_cache_dir(&mut self, cache: impl Into) -> &mut Self { self.cache = Some(cache.into()); self } @@ -45,6 +63,12 @@ impl GatewayBuilder { /// Sets the maximum number of concurrent HTTP requests to make. #[must_use] pub fn with_max_concurrent_requests(mut self, max_concurrent_requests: usize) -> Self { + self.set_max_concurrent_requests(max_concurrent_requests); + self + } + + /// Sets the maximum number of concurrent HTTP requests to make. + pub fn set_max_concurrent_requests(&mut self, max_concurrent_requests: usize) -> &mut Self { self.max_concurrent_requests = Some(max_concurrent_requests); self } diff --git a/py-rattler/Cargo.lock b/py-rattler/Cargo.lock index b76a585a3..009c4b8ce 100644 --- a/py-rattler/Cargo.lock +++ b/py-rattler/Cargo.lock @@ -505,6 +505,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" @@ -556,6 +575,19 @@ dependencies = [ "syn 2.0.59", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "deranged" version = "0.3.11" @@ -983,6 +1015,7 @@ version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ + "serde", "typenum", "version_check", ] @@ -1073,6 +1106,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.1.0", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1189,6 +1241,29 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-cache-semantics" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92baf25cf0b8c9246baecf3a444546360a97b569168fdf92563ee6a47829920c" +dependencies = [ + "http 1.1.0", + "http-serde", + "reqwest 0.12.3", + "serde", + "time", +] + +[[package]] +name = "http-serde" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1133cafcce27ea69d35e56b3a8772e265633e04de73c5f4e1afdffc1d19b5419" +dependencies = [ + "http 1.1.0", + "serde", +] + [[package]] name = "httparse" version = "1.8.0" @@ -1226,14 +1301,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.6", "tokio", "tower-service", "tracing", @@ -1249,6 +1324,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.4", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -1978,6 +2054,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pem" version = "3.0.4" @@ -2407,7 +2489,7 @@ dependencies = [ [[package]] name = "rattler" -version = "0.23.1" +version = "0.23.2" dependencies = [ "anyhow", "bytes", @@ -2473,6 +2555,7 @@ version = "0.19.3" dependencies = [ "blake2", "digest", + "generic-array", "hex", "md-5", "serde", @@ -2483,7 +2566,7 @@ dependencies = [ [[package]] name = "rattler_index" -version = "0.19.8" +version = "0.19.9" dependencies = [ "fs-err", "rattler_conda_types", @@ -2496,7 +2579,7 @@ dependencies = [ [[package]] name = "rattler_lock" -version = "0.22.3" +version = "0.22.4" dependencies = [ "chrono", "fxhash", @@ -2508,8 +2591,10 @@ dependencies = [ "purl", "rattler_conda_types", "rattler_digest", + "rayon", "serde", "serde_json", + "serde_repr", "serde_with", "serde_yaml", "thiserror", @@ -2526,7 +2611,7 @@ dependencies = [ [[package]] name = "rattler_networking" -version = "0.20.3" +version = "0.20.4" dependencies = [ "anyhow", "async-trait", @@ -2552,7 +2637,7 @@ dependencies = [ [[package]] name = "rattler_package_streaming" -version = "0.20.6" +version = "0.20.7" dependencies = [ "bzip2", "chrono", @@ -2576,28 +2661,38 @@ dependencies = [ [[package]] name = "rattler_repodata_gateway" -version = "0.19.9" +version = "0.19.10" dependencies = [ "anyhow", "async-compression", + "async-trait", "blake2", + "bytes", "cache_control", "chrono", + "dashmap", + "dirs", "futures", "hex", + "http 1.1.0", + "http-cache-semantics", "humansize", "humantime", "itertools", "json-patch", "libc", + "md-5", "memmap2", "ouroboros", + "parking_lot", "pin-project-lite", "rattler_conda_types", "rattler_digest", "rattler_networking", + "rayon", "reqwest 0.12.3", "reqwest-middleware", + "rmp-serde", "serde", "serde_json", "serde_with", @@ -2605,10 +2700,12 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tokio-rayon", "tokio-util", "tracing", "url", "windows-sys 0.52.0", + "zstd", ] [[package]] @@ -2658,6 +2755,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2729,7 +2846,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", @@ -2773,6 +2890,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", + "h2 0.4.4", "http 1.1.0", "http-body 1.0.0", "http-body-util", @@ -2865,6 +2983,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -3527,6 +3667,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rayon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf33a76e0b1dd03b778f83244137bd59887abf25c0e87bc3e7071105f457693" +dependencies = [ + "rayon", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" diff --git a/py-rattler/Cargo.toml b/py-rattler/Cargo.toml index 42f11845d..1f9854b55 100644 --- a/py-rattler/Cargo.toml +++ b/py-rattler/Cargo.toml @@ -22,6 +22,7 @@ futures = "0.3.30" rattler = { path = "../crates/rattler", default-features = false } rattler_repodata_gateway = { path = "../crates/rattler_repodata_gateway", default-features = false, features = [ "sparse", + "gateway", ] } rattler_conda_types = { path = "../crates/rattler_conda_types", default-features = false } rattler_digest = { path = "../crates/rattler_digest" } @@ -39,6 +40,7 @@ pyo3 = { version = "0.20", features = [ "abi3-py38", "extension-module", "multiple-pymethods", + ] } pyo3-asyncio = { version = "0.20", features = ["tokio-runtime"] } tokio = { version = "1.37" } diff --git a/py-rattler/pixi.lock b/py-rattler/pixi.lock index 98f80edc3..b8c9d2f8a 100644 --- a/py-rattler/pixi.lock +++ b/py-rattler/pixi.lock @@ -1,4 +1,4 @@ -version: 4 +version: 5 environments: build: channels: @@ -7,13 +7,18 @@ environments: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-ha885e6a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-13.2.0-h9eb54c0_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-2.6.32-he073ed8_17.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-13.2.0-hceb6213_106.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-hc881cc4_6.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-hc881cc4_6.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-13.2.0-h6ddb7a1_6.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h95c4c6d_6.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda @@ -27,7 +32,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.8.19-hd12c33a_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.8-4_cp38.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rust-1.77.2-h70c747d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-x86_64-unknown-linux-gnu-1.77.2-h2c6d0dc_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.12-he073ed8_17.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.43.0-pyhd8ed1ab_1.conda @@ -45,6 +53,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.8.19-h5ba8234_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.8-4_cp38.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/rust-1.77.2-h7e1429e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-x86_64-apple-darwin-1.77.2-h38e4360_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 @@ -63,6 +73,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.8.19-h2469fbe_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.8-4_cp38.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rust-1.77.2-h4ff7c5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-aarch64-apple-darwin-1.77.2-hf6ec828_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 @@ -85,6 +97,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pip-23.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.8.19-h4de0772_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.8-4_cp38.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rust-1.77.2-hf8d6059_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-x86_64-pc-windows-msvc-1.77.2-h17fc481_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 @@ -517,26 +531,34 @@ environments: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-ha885e6a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-13.2.0-h9eb54c0_6.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-2.6.32-he073ed8_17.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h55db66e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-13.2.0-hceb6213_106.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-hc881cc4_6.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-hc881cc4_6.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-13.2.0-h6ddb7a1_6.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.45.3-h2797004_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h95c4c6d_6.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/maturin-1.2.3-py38hcdda232_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.5.1-py38h01eb140_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4.20240210-h59595ed_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/patchelf-0.17.2-h58526e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-23.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-5.9.8-py38h01eb140_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda @@ -546,9 +568,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.8-4_cp38.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.3.7-py38h18b4745_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rust-1.77.2-h70c747d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-x86_64-unknown-linux-gnu-1.77.2-h2c6d0dc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.12-he073ed8_17.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.11.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.43.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 osx-64: - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda @@ -560,11 +587,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.45.3-h92b6c6a_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/maturin-1.2.3-py38h196e9ca_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.5.1-py38hcafd530_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.4.20240210-h73e2aa4_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.2.1-hd75f5a5_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-23.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-5.9.8-py38hae2e43d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda @@ -574,9 +603,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.8-4_cp38.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ruff-0.3.7-py38h1916ca8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/rust-1.77.2-h7e1429e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-x86_64-apple-darwin-1.77.2-h38e4360_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.11.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.43.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2 osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h93a5062_5.conda @@ -588,11 +621,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.45.3-h091b4b1_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.2.13-h53f4e23_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/maturin-1.2.3-py38h92a0862_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.5.1-py38hb192615_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.4.20240210-h078ce10_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.2.1-h0d3ecfb_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-23.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-5.9.8-py38h336bac9_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda @@ -602,9 +637,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.8-4_cp38.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.3.7-py38h5477e86_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rust-1.77.2-h4ff7c5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-aarch64-apple-darwin-1.77.2-hf6ec828_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.11.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.43.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-hcfcfb64_5.conda @@ -615,10 +654,18 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.2-h8ffe710_5.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.45.3-hcfcfb64_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.2.13-hcfcfb64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libgfortran-5.3.0-6.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-5.3.0-7.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gcc-libs-core-5.3.0-7.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gmp-6.1.0-2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-libwinpthread-git-5.0.0.4634.697f757-2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/maturin-1.2.3-py38hf90c7e5_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.5.1-py38h91455d4_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.2.1-hcfcfb64_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pip-23.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-5.9.8-py38h91455d4_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-7.4.4-pyhd8ed1ab_0.conda @@ -627,6 +674,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.8.19-h4de0772_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.8-4_cp38.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.3.7-py38h5e48be7_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rust-1.77.2-hf8d6059_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rust-std-x86_64-pc-windows-msvc-1.77.2-h17fc481_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.11.0-pyha770c72_0.conda @@ -634,6 +684,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hcf57466_18.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.38.33130-h82b7239_18.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.38.33130-hcb4865c_18.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.43.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 packages: - kind: conda @@ -697,6 +748,21 @@ packages: license_family: BSD size: 7609750 timestamp: 1702422720584 +- kind: conda + name: binutils_impl_linux-64 + version: '2.40' + build: ha885e6a_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-ha885e6a_0.conda + sha256: 180b268f207d1481beb9de5c173751d14c429a7226fa9a85941e4a54cf6be1b4 + md5: 800a4c872b5bc06fa83888d112fe6c4f + depends: + - ld_impl_linux-64 2.40 h55db66e_0 + - sysroot_linux-64 + license: GPL-3.0-only + license_family: GPL + size: 5797310 + timestamp: 1713651250214 - kind: conda name: brotli-python version: 1.1.0 @@ -1488,6 +1554,26 @@ packages: license: GPL-2.0-only OR FTL size: 510306 timestamp: 1694616398888 +- kind: conda + name: gcc_impl_linux-64 + version: 13.2.0 + build: h9eb54c0_6 + build_number: 6 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-13.2.0-h9eb54c0_6.conda + sha256: 67d16151d316f04ea2779ff3a4f5fcf4a5454e89bc21dabc1a4f7c08cf5ea821 + md5: 36ca2a36806ab26c2daf20d5b62280d7 + depends: + - binutils_impl_linux-64 >=2.40 + - libgcc-devel_linux-64 13.2.0 hceb6213_106 + - libgcc-ng >=13.2.0 + - libgomp >=13.2.0 + - libsanitizer 13.2.0 h6ddb7a1_6 + - libstdcxx-ng >=13.2.0 + - sysroot_linux-64 + license: GPL-3.0-only WITH GCC-exception-3.1 + size: 53360656 + timestamp: 1714581875812 - kind: conda name: ghp-import version: 2.1.0 @@ -1637,6 +1723,22 @@ packages: license_family: BSD size: 111589 timestamp: 1704967140287 +- kind: conda + name: kernel-headers_linux-64 + version: 2.6.32 + build: he073ed8_17 + build_number: 17 + subdir: noarch + noarch: generic + url: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-2.6.32-he073ed8_17.conda + sha256: fb39d64b48f3d9d1acc3df208911a41f25b6a00bd54935d5973b4739a9edd5b6 + md5: d731b543793afc0433c4fd593e693fce + constrains: + - sysroot_linux-64 ==2.12 + license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later AND MPL-2.0 + license_family: GPL + size: 710627 + timestamp: 1708000830116 - kind: conda name: lcms2 version: '2.16' @@ -1966,6 +2068,19 @@ packages: license_family: MIT size: 42063 timestamp: 1636489106777 +- kind: conda + name: libgcc-devel_linux-64 + version: 13.2.0 + build: hceb6213_106 + build_number: 106 + subdir: noarch + noarch: generic + url: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-13.2.0-hceb6213_106.conda + sha256: f5af7a346ba0a2c322028a7fa8ba99f5094911439d5aab2c6bc42a4e9022bc68 + md5: b85d6b583f498b4ddc9150aefb492f7f + license: GPL-3.0-only WITH GCC-exception-3.1 + size: 2575829 + timestamp: 1714581666472 - kind: conda name: libgcc-ng version: 13.2.0 @@ -2310,6 +2425,20 @@ packages: license: zlib-acknowledgement size: 268524 timestamp: 1708780496420 +- kind: conda + name: libsanitizer + version: 13.2.0 + build: h6ddb7a1_6 + build_number: 6 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-13.2.0-h6ddb7a1_6.conda + sha256: 06f3695963ee86badbfe006f13fa9fe600539acb77f19c5c972d498a14e9b53d + md5: 95b48df99634d9e706a0bf7e30ae91c8 + depends: + - libgcc-ng >=13.2.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + size: 4188343 + timestamp: 1714581787957 - kind: conda name: libsqlite version: 3.45.3 @@ -4435,6 +4564,133 @@ packages: license_family: MIT size: 6343187 timestamp: 1712963346969 +- kind: conda + name: rust + version: 1.77.2 + build: h4ff7c5d_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/rust-1.77.2-h4ff7c5d_0.conda + sha256: 048ffabbbbd1b5109d59ec15610cf0e489c39b4f6f380953816bcb26dad8da17 + md5: 4083c1a9d7f5c9591273f578530d6388 + depends: + - rust-std-aarch64-apple-darwin 1.77.2 hf6ec828_0 + license: MIT + license_family: MIT + size: 145759919 + timestamp: 1712743398771 +- kind: conda + name: rust + version: 1.77.2 + build: h70c747d_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/rust-1.77.2-h70c747d_0.conda + sha256: 3b8cf09335d23c52d6e7150e4cc6d999ed4e2b3dc2307652f20e1a4669ff0846 + md5: ba764892e80fe0380bb7fa99751b186d + depends: + - gcc_impl_linux-64 + - libgcc-ng >=12 + - libzlib >=1.2.13,<1.3.0a0 + - rust-std-x86_64-unknown-linux-gnu 1.77.2 h2c6d0dc_0 + license: MIT + license_family: MIT + size: 186765686 + timestamp: 1712741423714 +- kind: conda + name: rust + version: 1.77.2 + build: h7e1429e_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/rust-1.77.2-h7e1429e_0.conda + sha256: d12cde3691eb50148b49460ac2bff0c0716204099a38d36132762ffb0c6c79fd + md5: 13c8a97dd157999cdd23adaac7919047 + depends: + - rust-std-x86_64-apple-darwin 1.77.2 h38e4360_0 + license: MIT + license_family: MIT + size: 192493395 + timestamp: 1712743664947 +- kind: conda + name: rust + version: 1.77.2 + build: hf8d6059_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/rust-1.77.2-hf8d6059_0.conda + sha256: 978228c14a3d2af2d9d52230443f232d7a22cbbe98d359a306b1a761773d4589 + md5: ba05fee8761e5bd25ae642a4b77d2ed7 + depends: + - rust-std-x86_64-pc-windows-msvc 1.77.2 h17fc481_0 + license: MIT + license_family: MIT + size: 187565499 + timestamp: 1712743189902 +- kind: conda + name: rust-std-aarch64-apple-darwin + version: 1.77.2 + build: hf6ec828_0 + subdir: noarch + noarch: generic + url: https://conda.anaconda.org/conda-forge/noarch/rust-std-aarch64-apple-darwin-1.77.2-hf6ec828_0.conda + sha256: 19b17ddca3896f12a640858b45a7ba5e8495ca07286b622535ca5a4bf8217906 + md5: 729f181cdeb249ff2da37f434b548633 + depends: + - __unix + constrains: + - rust >=1.77.2,<1.77.3.0a0 + license: MIT + license_family: MIT + size: 30933811 + timestamp: 1712740743456 +- kind: conda + name: rust-std-x86_64-apple-darwin + version: 1.77.2 + build: h38e4360_0 + subdir: noarch + noarch: generic + url: https://conda.anaconda.org/conda-forge/noarch/rust-std-x86_64-apple-darwin-1.77.2-h38e4360_0.conda + sha256: 1d0a99136ab0a2b05d9df4d5a7a8d665595c2e72ee1d19fcad0c6f1b402f37d1 + md5: 67db6d59468a8145fb076d75d156b69c + depends: + - __unix + constrains: + - rust >=1.77.2,<1.77.3.0a0 + license: MIT + license_family: MIT + size: 31857486 + timestamp: 1712740749097 +- kind: conda + name: rust-std-x86_64-pc-windows-msvc + version: 1.77.2 + build: h17fc481_0 + subdir: noarch + noarch: generic + url: https://conda.anaconda.org/conda-forge/noarch/rust-std-x86_64-pc-windows-msvc-1.77.2-h17fc481_0.conda + sha256: 0c290c52a3cf1ac43a316d6caf0e073614351ccae31c681d6953dec7a2ff21e3 + md5: 2149767f1c882154246a9a569991e3c3 + depends: + - __win + constrains: + - rust >=1.77.2,<1.77.3.0a0 + license: MIT + license_family: MIT + size: 25276039 + timestamp: 1712742986757 +- kind: conda + name: rust-std-x86_64-unknown-linux-gnu + version: 1.77.2 + build: h2c6d0dc_0 + subdir: noarch + noarch: generic + url: https://conda.anaconda.org/conda-forge/noarch/rust-std-x86_64-unknown-linux-gnu-1.77.2-h2c6d0dc_0.conda + sha256: 73f7537db6bc0471135a85a261798abe77e7e83794f945a0355c4068973f31f6 + md5: db8b81b3806faafe2f6f7bd431f72e37 + depends: + - __unix + constrains: + - rust >=1.77.2,<1.77.3.0a0 + license: MIT + license_family: MIT + size: 33827015 + timestamp: 1712741238767 - kind: conda name: setuptools version: 69.5.1 @@ -4465,6 +4721,22 @@ packages: license_family: MIT size: 14259 timestamp: 1620240338595 +- kind: conda + name: sysroot_linux-64 + version: '2.12' + build: he073ed8_17 + build_number: 17 + subdir: noarch + noarch: generic + url: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.12-he073ed8_17.conda + sha256: b4e4d685e41cb36cfb16f0cb15d2c61f8f94f56fab38987a44eff95d8a673fb5 + md5: 595db67e32b276298ff3d94d07d47fbf + depends: + - kernel-headers_linux-64 2.6.32 he073ed8_17 + license: LGPL-2.0-or-later AND LGPL-2.0-or-later WITH exceptions AND GPL-2.0-or-later AND MPL-2.0 + license_family: GPL + size: 15127123 + timestamp: 1708000843849 - kind: conda name: tinycss2 version: 1.3.0 diff --git a/py-rattler/pixi.toml b/py-rattler/pixi.toml index b6945b1a0..733e5b9ca 100644 --- a/py-rattler/pixi.toml +++ b/py-rattler/pixi.toml @@ -16,6 +16,7 @@ license = "BSD-3-Clause" [feature.build.dependencies] maturin = "~=1.2.2" pip = "~=23.2.1" +rust = "~=1.77" [feature.build.tasks] build = "PIP_REQUIRE_VIRTUALENV=false maturin develop" @@ -63,6 +64,6 @@ docs = { cmd = "mkdocs serve" } build-docs = { cmd = "mkdocs build" } [environments] -build = [ "build" ] -test = [ "test" ] -docs = [ "docs" ] +build = { features = ["build"], solve-group = "default" } +test = { features = ["build", "test"], solve-group = "default" } +docs = ["docs"] diff --git a/py-rattler/rattler/repo_data/gateway.py b/py-rattler/rattler/repo_data/gateway.py new file mode 100644 index 000000000..49052b89f --- /dev/null +++ b/py-rattler/rattler/repo_data/gateway.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import typing +from dataclasses import dataclass + +from rattler.rattler import PyGateway, PySourceConfig + +from rattler import Channel + +if typing.TYPE_CHECKING: + import os + from typing import Optional + +CacheAction = typing.Literal["cache-or-fetch", "use-cache-only", "force-cache-only", "no-cache"] + + +@dataclass +class SourceConfig: + """ + Describes properties about a channel. + + This can be used to configure the Gateway to handle channels in a certain + way. + """ + + jlap_enabled: bool = True + zstd_enabled: bool = True + bz2_enabled: bool = True + cache_action: CacheAction = "cache-or-fetch" + + def _into_py(self) -> PySourceConfig: + return PySourceConfig( + jlap_enabled=self.jlap_enabled, + zstd_enabled=self.zstd_enabled, + bz2_enabled=self.bz2_enabled, + cache_action=self.cache_action, + ) + + +class Gateway: + """ + An object that manages repodata and allows efficiently querying different + channels for it. + """ + + def __init__( + self, + cache_dir: Optional[os.PathLike[str]] = None, + default_config: Optional[SourceConfig] = None, + per_channel_config: Optional[dict[Channel | str, SourceConfig]] = None, + max_concurrent_requests: int = 100 + ) -> None: + default_config = default_config or SourceConfig() + + self._gateway = PyGateway( + cache_dir=cache_dir, + default_config=default_config._into_py(), + per_channel_config={ + channel._channel if isinstance(channel, Channel) else Channel( + channel)._channel: config._into_py() + for channel, config in (per_channel_config or {}).items() + }, + max_concurrent_requests=max_concurrent_requests + ) diff --git a/py-rattler/src/channel/mod.rs b/py-rattler/src/channel/mod.rs index 27bf578a3..358927454 100644 --- a/py-rattler/src/channel/mod.rs +++ b/py-rattler/src/channel/mod.rs @@ -39,7 +39,7 @@ impl PyChannelConfig { #[pyclass] #[repr(transparent)] -#[derive(Clone)] +#[derive(Clone, Hash, Eq, PartialEq)] pub struct PyChannel { pub(crate) inner: Channel, } diff --git a/py-rattler/src/lib.rs b/py-rattler/src/lib.rs index 466301692..2da5a077d 100644 --- a/py-rattler/src/lib.rs +++ b/py-rattler/src/lib.rs @@ -22,6 +22,7 @@ mod virtual_package; mod index_json; mod run_exports_json; + use about_json::PyAboutJson; use channel::{PyChannel, PyChannelConfig, PyChannelPriority}; use error::{ @@ -44,8 +45,14 @@ use networking::{authenticated_client::PyAuthenticatedClient, py_fetch_repo_data use package_name::PyPackageName; use paths_json::{PyFileMode, PyPathType, PyPathsEntry, PyPathsJson, PyPrefixPlaceholder}; use prefix_paths::{PyPrefixPathType, PyPrefixPaths, PyPrefixPathsEntry}; -use repo_data::{patch_instructions::PyPatchInstructions, sparse::PySparseRepoData, PyRepoData}; +use repo_data::{ + gateway::{PyGateway, PySourceConfig}, + patch_instructions::PyPatchInstructions, + sparse::PySparseRepoData, + PyRepoData, +}; use run_exports_json::PyRunExportsJson; +use std::ops::Deref; use version::PyVersion; use pyo3::prelude::*; @@ -59,6 +66,17 @@ use shell::{PyActivationResult, PyActivationVariables, PyActivator, PyShellEnum} use solver::py_solve; use virtual_package::PyVirtualPackage; +/// A struct to make it easy to wrap a type as a python type. +#[repr(transparent)] +pub struct Wrap(pub T); + +impl Deref for Wrap { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[pymodule] fn rattler(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::().unwrap(); @@ -85,6 +103,8 @@ fn rattler(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::().unwrap(); m.add_class::().unwrap(); m.add_class::().unwrap(); + m.add_class::().unwrap(); + m.add_class::().unwrap(); m.add_class::().unwrap(); diff --git a/py-rattler/src/networking/mod.rs b/py-rattler/src/networking/mod.rs index 703444331..87b5ea1e7 100644 --- a/py-rattler/src/networking/mod.rs +++ b/py-rattler/src/networking/mod.rs @@ -3,17 +3,18 @@ use pyo3::{pyfunction, types::PyTuple, Py, PyAny, PyResult, Python, ToPyObject}; use pyo3_asyncio::tokio::future_into_py; use rattler_repodata_gateway::fetch::{ - fetch_repo_data, CachedRepoData, DownloadProgress, FetchRepoDataError, FetchRepoDataOptions, + fetch_repo_data, CachedRepoData, FetchRepoDataError, FetchRepoDataOptions, }; use url::Url; -use std::{path::PathBuf, str::FromStr}; +use std::{path::PathBuf, str::FromStr, sync::Arc}; use crate::{ channel::PyChannel, error::PyRattlerError, platform::PyPlatform, repo_data::sparse::PySparseRepoData, }; use authenticated_client::PyAuthenticatedClient; +use rattler_repodata_gateway::Reporter; pub mod authenticated_client; pub mod cached_repo_data; @@ -32,12 +33,11 @@ pub fn py_fetch_repo_data<'a>( let client = PyAuthenticatedClient::new(); for (subdir, chan) in get_subdir_urls(channels, platforms)? { - let progress = if let Some(callback) = callback { - let callback = callback.to_object(py); - Some(get_progress_func(callback)) - } else { - None - }; + let callback = callback.map(|callback| { + Arc::new(ProgressReporter { + callback: callback.to_object(py), + }) as _ + }); let cache_path = cache_path.clone(); let client = client.clone(); @@ -49,7 +49,7 @@ pub fn py_fetch_repo_data<'a>( client.into(), cache_path, FetchRepoDataOptions::default(), - progress, + callback, ) .await?, chan, @@ -72,14 +72,23 @@ pub fn py_fetch_repo_data<'a>( }) } -/// Creates a closure to show progress of Download -fn get_progress_func(callback: Py) -> Box { - Box::new(move |progress: DownloadProgress| { +struct ProgressReporter { + callback: Py, +} + +impl Reporter for ProgressReporter { + fn on_download_progress( + &self, + _url: &Url, + _index: usize, + bytes_downloaded: usize, + total_bytes: Option, + ) { Python::with_gil(|py| { - let args = PyTuple::new(py, [Some(progress.bytes), progress.total]); - callback.call1(py, args).expect("Callback failed!"); + let args = PyTuple::new(py, [Some(bytes_downloaded), total_bytes]); + self.callback.call1(py, args).expect("Callback failed!"); }); - }) + } } /// Creates a subdir urls out of channels and channels. diff --git a/py-rattler/src/repo_data/gateway.rs b/py-rattler/src/repo_data/gateway.rs new file mode 100644 index 000000000..8df04b65f --- /dev/null +++ b/py-rattler/src/repo_data/gateway.rs @@ -0,0 +1,112 @@ +use crate::{PyChannel, Wrap}; +use pyo3::exceptions::PyValueError; +use pyo3::{pyclass, pymethods, FromPyObject, PyAny, PyResult}; +use rattler_repodata_gateway::fetch::CacheAction; +use rattler_repodata_gateway::{ChannelConfig, Gateway, SourceConfig}; +use std::collections::HashMap; +use std::path::PathBuf; + +#[pyclass] +#[repr(transparent)] +#[derive(Clone)] +pub struct PyGateway { + pub(crate) inner: Gateway, +} + +impl From for Gateway { + fn from(value: PyGateway) -> Self { + value.inner + } +} + +impl From for PyGateway { + fn from(value: Gateway) -> Self { + Self { inner: value } + } +} + +#[pymethods] +impl PyGateway { + #[new] + pub fn new( + max_concurrent_requests: usize, + default_config: PySourceConfig, + per_channel_config: HashMap, + cache_dir: Option, + ) -> PyResult { + let mut channel_config = ChannelConfig::default(); + channel_config.default = default_config.into(); + channel_config.per_channel = per_channel_config + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(); + + let mut gateway = Gateway::builder() + .with_max_concurrent_requests(max_concurrent_requests) + .with_channel_config(channel_config); + + if let Some(cache_dir) = cache_dir { + gateway.set_cache_dir(cache_dir); + } + + Ok(Self { + inner: gateway.finish(), + }) + } +} + +#[pyclass] +#[repr(transparent)] +#[derive(Clone)] +pub struct PySourceConfig { + pub(crate) inner: SourceConfig, +} + +impl From for SourceConfig { + fn from(value: PySourceConfig) -> Self { + value.inner + } +} + +impl From for PySourceConfig { + fn from(value: SourceConfig) -> Self { + Self { inner: value } + } +} + +impl FromPyObject<'_> for Wrap { + fn extract(ob: &'_ PyAny) -> PyResult { + let parsed = match &*ob.extract::()? { + "cache-or-fetch" => CacheAction::CacheOrFetch, + "use-cache-only" => CacheAction::UseCacheOnly, + "force-cache-only" => CacheAction::ForceCacheOnly, + "no-cache" => CacheAction::NoCache, + v => { + return Err(PyValueError::new_err(format!( + "cache action must be one of {{'cache-or-fetch', 'use-cache-only', 'force-cache-only', 'no-cache'}}, got {v}", + ))) + }, + }; + Ok(Wrap(parsed)) + } +} + +#[pymethods] +impl PySourceConfig { + #[new] + pub fn new( + jlap_enabled: bool, + zstd_enabled: bool, + bz2_enabled: bool, + cache_action: Wrap, + ) -> Self { + Self { + inner: SourceConfig { + jlap_enabled, + zstd_enabled, + bz2_enabled, + cache_action: cache_action.0, + }, + } + } +} diff --git a/py-rattler/src/repo_data/mod.rs b/py-rattler/src/repo_data/mod.rs index 7d567bcc7..56e64821b 100644 --- a/py-rattler/src/repo_data/mod.rs +++ b/py-rattler/src/repo_data/mod.rs @@ -7,6 +7,7 @@ use crate::{channel::PyChannel, error::PyRattlerError, record::PyRecord}; use patch_instructions::PyPatchInstructions; +pub mod gateway; pub mod patch_instructions; pub mod sparse; From 847685289bf0f7808885bd021bdab11a28083015 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 6 May 2024 15:50:45 +0200 Subject: [PATCH 37/57] fix: py-rattler --- py-rattler/pixi.lock | 827 +++++++++++++++--------- py-rattler/rattler/exceptions.py | 5 + py-rattler/rattler/repo_data/gateway.py | 116 +++- py-rattler/src/channel/mod.rs | 2 +- py-rattler/src/error.rs | 5 + py-rattler/src/lib.rs | 5 + py-rattler/src/linker.rs | 6 +- py-rattler/src/platform.rs | 15 +- py-rattler/src/repo_data/gateway.rs | 50 +- 9 files changed, 678 insertions(+), 353 deletions(-) diff --git a/py-rattler/pixi.lock b/py-rattler/pixi.lock index b8c9d2f8a..945b713f2 100644 --- a/py-rattler/pixi.lock +++ b/py-rattler/pixi.lock @@ -121,14 +121,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/astunparse-1.6.3-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py38h17151c0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h30efb56_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cairocffi-1.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cairosvg-2.7.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.16.0-py38h6d47a40_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.16.0-py312hf06ca03_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 @@ -171,7 +171,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py38h01eb140_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py312h98912ed_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mergedeep-1.3.4-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-1.5.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-autorefs-1.0.1-pyhd8ed1ab_0.conda @@ -186,7 +186,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/paginate-0.5.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.43-hcad00b1_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py38h9e66945_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py312hdcec9eb_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2 @@ -194,14 +194,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.8-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.8.19-hd12c33a_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.3-hab00c5b_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.8-4_cp38.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-4_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py38h01eb140_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py312h98912ed_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-env-tag-0.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/regex-2024.4.16-py38h01eb140_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/regex-2024.4.28-py312h9a8786e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 @@ -209,8 +209,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.11.0-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.11.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/watchdog-4.0.0-py38h578d9bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/watchdog-4.0.0-py312h7900ff3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-kbproto-1.0.7-h7f98852_1002.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.1-hd590300_0.conda @@ -231,14 +232,14 @@ environments: osx-64: - conda: https://conda.anaconda.org/conda-forge/noarch/astunparse-1.6.3-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py38h940360d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py312heafc425_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.2.2-h8857fd0_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/cairo-1.18.0-h99e66fa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cairocffi-1.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cairosvg-2.7.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.16.0-py38h082e395_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.16.0-py312h38bf5a0_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 @@ -276,7 +277,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/libxcb-1.15-hb7f2c08_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-2.1.5-py38hae2e43d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-2.1.5-py312h41838bb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mergedeep-1.3.4-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-1.5.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-autorefs-1.0.1-pyhd8ed1ab_0.conda @@ -291,7 +292,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/paginate-0.5.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/pcre2-10.43-h0ad2156_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pillow-10.3.0-py38h85abd47_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pillow-10.3.0-py312h0c923fa_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/pixman-0.43.4-h73e2aa4_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/pthread-stubs-0.4-hc929b4f_1001.tar.bz2 @@ -299,14 +300,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.8-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.8.19-h5ba8234_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.3-h1411813_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.8-4_cp38.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-4_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.1-py38hcafd530_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.1-py312h104f124_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-env-tag-0.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/regex-2024.4.16-py38hae2e43d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/regex-2024.4.28-py312h5fa3f64_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 @@ -314,8 +315,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.11.0-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.11.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-64/watchdog-4.0.0-py38h2bea1e5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/watchdog-4.0.0-py312hc2c2f20_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxau-1.0.11-h0dc2134_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/xorg-libxdmcp-1.1.3-h35c211d_0.tar.bz2 @@ -327,14 +329,14 @@ environments: osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/astunparse-1.6.3-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py38he333c0f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py312h9f69965_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h93a5062_5.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.2.2-hf0a4a13_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.0-hd1e100b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cairocffi-1.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cairosvg-2.7.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.16.0-py38h73f40f7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.16.0-py312h8e38eb3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 @@ -372,7 +374,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.15-hf346824_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.2.13-h53f4e23_5.conda - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-2.1.5-py38h336bac9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-2.1.5-py312he37b823_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mergedeep-1.3.4-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-1.5.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-autorefs-1.0.1-pyhd8ed1ab_0.conda @@ -387,7 +389,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/paginate-0.5.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.43-h26f9a81_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-10.3.0-py38h9ef4633_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-10.3.0-py312h8a801b1_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.43.4-hebf3989_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-h27ca646_1001.tar.bz2 @@ -395,14 +397,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.8-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.8.19-h2469fbe_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.3-h4a7b5fc_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.8-4_cp38.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-4_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.1-py38hb192615_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.1-py312h02f2b3b_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-env-tag-0.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/regex-2024.4.16-py38h336bac9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/regex-2024.4.28-py312h4a164c9_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 @@ -410,8 +412,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.11.0-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.11.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/watchdog-4.0.0-py38h336bac9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/watchdog-4.0.0-py312he37b823_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.11-hb547adb_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.3-h27ca646_0.tar.bz2 @@ -423,14 +426,14 @@ environments: win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/astunparse-1.6.3-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py38hd3f51b4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py312h53d5487_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-hcfcfb64_5.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2024.2.2-h56e8100_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.0-h1fef639_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cairocffi-1.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cairosvg-2.7.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.16.0-py38h91455d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.16.0-py312he70551f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-win_pyh7428d3b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 @@ -472,7 +475,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-gmp-6.1.0-2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-libwinpthread-git-5.0.0.4634.697f757-2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-2.1.5-py38h91455d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-2.1.5-py312he70551f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mergedeep-1.3.4-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-1.5.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-autorefs-1.0.1-pyhd8ed1ab_0.conda @@ -487,7 +490,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/paginate-0.5.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.43-h17e33f8_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pillow-10.3.0-py38h894f861_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pillow-10.3.0-py312h6f6a607_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.43.4-h63175ca_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-hcd874cb_1001.tar.bz2 @@ -495,13 +498,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.8-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh0701188_6.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.8.19-h4de0772_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.3-h2628c8c_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.8-4_cp38.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.12-4_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.1-py38h91455d4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.1-py312he70551f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-env-tag-0.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/regex-2024.4.16-py38h91455d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/regex-2024.4.28-py312h4389bb4_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 @@ -509,12 +512,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.11.0-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.11.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hcf57466_18.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.38.33130-h82b7239_18.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.38.33130-hcb4865c_18.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/watchdog-4.0.0-py38haa244fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/watchdog-4.0.0-py312h2e8e312_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyhd8ed1ab_6.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.11-hcd874cb_0.conda @@ -766,83 +770,83 @@ packages: - kind: conda name: brotli-python version: 1.1.0 - build: py38h17151c0_1 + build: py312h30efb56_1 build_number: 1 subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py38h17151c0_1.conda - sha256: f932ae77f10885dd991b0e1f56f6effea9f19b169e8606dab0bdafd0e44db3c9 - md5: 7a5a699c8992fc51ef25e980f4502c2a + url: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h30efb56_1.conda + sha256: b68706698b6ac0d31196a8bcb061f0d1f35264bcd967ea45e03e108149a74c6f + md5: 45801a89533d3336a365284d93298e36 depends: - libgcc-ng >=12 - libstdcxx-ng >=12 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12.0rc3,<3.13.0a0 + - python_abi 3.12.* *_cp312 constrains: - libbrotlicommon 1.1.0 hd590300_1 license: MIT license_family: MIT - size: 350830 - timestamp: 1695990250755 + size: 350604 + timestamp: 1695990206327 - kind: conda name: brotli-python version: 1.1.0 - build: py38h940360d_1 + build: py312h53d5487_1 build_number: 1 - subdir: osx-64 - url: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py38h940360d_1.conda - sha256: 0a088bff62ddd2e505bdc80cc16da009c134b9ccfa6352b0cfe9d4eeed27d8c2 - md5: ad8d4ae4e8351a2fc0fe92f13bd266d8 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py312h53d5487_1.conda + sha256: 769e276ecdebf86f097786cbde1ebd11e018cd6cd838800995954fe6360e0797 + md5: d01a6667b99f0e8ad4097af66c938e62 depends: - - libcxx >=15.0.7 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12.0rc3,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 constrains: - - libbrotlicommon 1.1.0 h0dc2134_1 + - libbrotlicommon 1.1.0 hcfcfb64_1 license: MIT license_family: MIT - size: 366343 - timestamp: 1695990788245 + size: 322514 + timestamp: 1695991054894 - kind: conda name: brotli-python version: 1.1.0 - build: py38hd3f51b4_1 + build: py312h9f69965_1 build_number: 1 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py38hd3f51b4_1.conda - sha256: a292d6b3118ef284cc03a99a6efe5e08ca3a6d0e37eff78eb8d87cfca3830d7b - md5: 72708ea626a2530148ea49eb743576f4 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py312h9f69965_1.conda + sha256: 3418b1738243abba99e931c017b952771eeaa1f353c07f7d45b55e83bb74fcb3 + md5: 1bc01b9ffdf42beb1a9fe4e9222e0567 depends: - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - libcxx >=15.0.7 + - python >=3.12.0rc3,<3.13.0a0 + - python >=3.12.0rc3,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 constrains: - - libbrotlicommon 1.1.0 hcfcfb64_1 + - libbrotlicommon 1.1.0 hb547adb_1 license: MIT license_family: MIT - size: 321650 - timestamp: 1695990817828 + size: 343435 + timestamp: 1695990731924 - kind: conda name: brotli-python version: 1.1.0 - build: py38he333c0f_1 + build: py312heafc425_1 build_number: 1 - subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py38he333c0f_1.conda - sha256: 3fd1e0a4b7ea1b20f69bbc2d74c798f3eebd775ccbcdee170f68b1871f8bbb74 - md5: 29160c74d5977b1c5ecd654b00d576f0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/brotli-python-1.1.0-py312heafc425_1.conda + sha256: fc55988f9bc05a938ea4b8c20d6545bed6e9c6c10aa5147695f981136ca894c1 + md5: a288b88f06b8bfe0dedaf5c4b6ac6b7a depends: - libcxx >=15.0.7 - - python >=3.8,<3.9.0a0 - - python >=3.8,<3.9.0a0 *_cpython - - python_abi 3.8.* *_cp38 + - python >=3.12.0rc3,<3.13.0a0 + - python_abi 3.12.* *_cp312 constrains: - - libbrotlicommon 1.1.0 hb547adb_1 + - libbrotlicommon 1.1.0 h0dc2134_1 license: MIT license_family: MIT - size: 343036 - timestamp: 1695990970956 + size: 366883 + timestamp: 1695990710194 - kind: conda name: bzip2 version: 1.0.8 @@ -1098,75 +1102,75 @@ packages: - kind: conda name: cffi version: 1.16.0 - build: py38h082e395_0 + build: py312h38bf5a0_0 subdir: osx-64 - url: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.16.0-py38h082e395_0.conda - sha256: c79e5074c663670f75258f6fce8ebd0e65042bd22ecbb4979294c57ff4fa8fc5 - md5: 046fe2a8edb11f1b8a7d3bd8e2fd1de7 + url: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.16.0-py312h38bf5a0_0.conda + sha256: 8b856583b56fc30f064a7cb286f85e4b5725f2bd4fda8ba0c4e94bffe258741e + md5: a45759c013ab20b9017ef9539d234dd7 depends: - libffi >=3.4,<4.0a0 - pycparser - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12.0rc3,<3.13.0a0 + - python_abi 3.12.* *_cp312 license: MIT license_family: MIT - size: 228367 - timestamp: 1696002058694 + size: 282370 + timestamp: 1696002004433 - kind: conda name: cffi version: 1.16.0 - build: py38h6d47a40_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.16.0-py38h6d47a40_0.conda - sha256: ec0a62d4836d3ec2321d07cffa5aeef37c6818c6cce6383dc6be7205d09551b3 - md5: fc010dfb8ce6540d289436fbba499ee7 + build: py312h8e38eb3_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.16.0-py312h8e38eb3_0.conda + sha256: 1544403cb1a5ca2aeabf0dac86d9ce6066d6fb4363493643b33ffd1b78038d18 + md5: 960ecbd65860d3b1de5e30373e1bffb1 depends: - libffi >=3.4,<4.0a0 - - libgcc-ng >=12 - pycparser - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12.0rc3,<3.13.0a0 + - python >=3.12.0rc3,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 license: MIT license_family: MIT - size: 239127 - timestamp: 1696001978654 + size: 284245 + timestamp: 1696002181644 - kind: conda name: cffi version: 1.16.0 - build: py38h73f40f7_0 - subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.16.0-py38h73f40f7_0.conda - sha256: 375e0be4068f4b00facfa569aa26c92ed87858f45be875f2c4bf90f33733f4de - md5: 02911ce6163d7a3e8fe9d9398fb9986d + build: py312he70551f_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/cffi-1.16.0-py312he70551f_0.conda + sha256: dd39e594f5c6bca52dfed343de2af9326a99700ce2ba3404bd89706926fc0137 + md5: 5a51096925d52332c62bfd8904899055 depends: - - libffi >=3.4,<4.0a0 - pycparser - - python >=3.8,<3.9.0a0 - - python >=3.8,<3.9.0a0 *_cpython - - python_abi 3.8.* *_cp38 + - python >=3.12.0rc3,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 license: MIT license_family: MIT - size: 230759 - timestamp: 1696002169830 + size: 287805 + timestamp: 1696002408940 - kind: conda name: cffi version: 1.16.0 - build: py38h91455d4_0 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/cffi-1.16.0-py38h91455d4_0.conda - sha256: 0704377274cfe0b3a5c308facecdeaaf2207303ee847842a4bbd3f70b7331ddc - md5: e9b2ac14b9c3d3eaeb2f69745e021e49 + build: py312hf06ca03_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.16.0-py312hf06ca03_0.conda + sha256: 5a36e2c254603c367d26378fa3a205bd92263e30acf195f488749562b4c44251 + md5: 56b0ca764ce23cc54f3f7e2a7b970f6d depends: + - libffi >=3.4,<4.0a0 + - libgcc-ng >=12 - pycparser - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - python >=3.12.0rc3,<3.13.0a0 + - python_abi 3.12.* *_cp312 license: MIT license_family: MIT - size: 234905 - timestamp: 1696002150251 + size: 294523 + timestamp: 1696001868949 - kind: conda name: charset-normalizer version: 3.3.2 @@ -1572,6 +1576,7 @@ packages: - libstdcxx-ng >=13.2.0 - sysroot_linux-64 license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL size: 53360656 timestamp: 1714581875812 - kind: conda @@ -2079,6 +2084,7 @@ packages: sha256: f5af7a346ba0a2c322028a7fa8ba99f5094911439d5aab2c6bc42a4e9022bc68 md5: b85d6b583f498b4ddc9150aefb492f7f license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL size: 2575829 timestamp: 1714581666472 - kind: conda @@ -2437,6 +2443,7 @@ packages: depends: - libgcc-ng >=13.2.0 license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL size: 4188343 timestamp: 1714581787957 - kind: conda @@ -2912,76 +2919,76 @@ packages: - kind: conda name: markupsafe version: 2.1.5 - build: py38h01eb140_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py38h01eb140_0.conda - sha256: 384a193d11c89463533e6fc5d94a6c67c16c598b32747a8f86f9ad227f0aed17 - md5: aeeb09febb02542e020c3ba7084ead01 + build: py312h41838bb_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-2.1.5-py312h41838bb_0.conda + sha256: 8dc8f31f78d00713300da000b6ebaa1943a17c112f267de310d5c3d82950079c + md5: c4a9c25c09cef3901789ca818d9beb10 depends: - - libgcc-ng >=12 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 constrains: - jinja2 >=3.0.0 license: BSD-3-Clause license_family: BSD - size: 24274 - timestamp: 1706900087252 + size: 25742 + timestamp: 1706900456837 - kind: conda name: markupsafe version: 2.1.5 - build: py38h336bac9_0 - subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-2.1.5-py38h336bac9_0.conda - sha256: f1b1b405c5246c499d66658e754e920529866826b247111cd481e15d0571f702 - md5: 76e1802508a91e5970f42f6558f5064e + build: py312h98912ed_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py312h98912ed_0.conda + sha256: 273d8efd6c089c534ccbede566394c0ac1e265bfe5d89fe76e80332f3d75a636 + md5: 6ff0b9582da2d4a74a1f9ae1f9ce2af6 depends: - - python >=3.8,<3.9.0a0 - - python >=3.8,<3.9.0a0 *_cpython - - python_abi 3.8.* *_cp38 + - libgcc-ng >=12 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 constrains: - jinja2 >=3.0.0 license: BSD-3-Clause license_family: BSD - size: 23719 - timestamp: 1706900313162 + size: 26685 + timestamp: 1706900070330 - kind: conda name: markupsafe version: 2.1.5 - build: py38h91455d4_0 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/markupsafe-2.1.5-py38h91455d4_0.conda - sha256: a0753407d33dbeebf3ee3118cc4bd3559af81e3de497b15f01a52b2702314c73 - md5: 0b3eb104f5c37ba2e7ec675b6a8ea453 + build: py312he37b823_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-2.1.5-py312he37b823_0.conda + sha256: 61480b725490f68856dd14e646f51ffc34f77f2c985bd33e3b77c04b2856d97d + md5: ba3a8f8cf8bbdb81394275b1e1d271da depends: - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 constrains: - jinja2 >=3.0.0 license: BSD-3-Clause license_family: BSD - size: 26598 - timestamp: 1706900643364 + size: 26382 + timestamp: 1706900495057 - kind: conda name: markupsafe version: 2.1.5 - build: py38hae2e43d_0 - subdir: osx-64 - url: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-2.1.5-py38hae2e43d_0.conda - sha256: ef6eaa455d99e40df64131d23f4b52bc3601f95a48f255cb9917f2d4eb760a36 - md5: 5107dae4aa6cbcb0cb73718cdd951c29 + build: py312he70551f_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/markupsafe-2.1.5-py312he70551f_0.conda + sha256: f8690a3c87e2e96cebd434a829bb95cac43afe6c439530b336dc3452fe4ce4af + md5: 4950a739b19edaac1ed29ca9474e49ac depends: - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 constrains: - jinja2 >=3.0.0 license: BSD-3-Clause license_family: BSD - size: 23167 - timestamp: 1706900242727 + size: 29060 + timestamp: 1706900374745 - kind: conda name: maturin version: 1.2.3 @@ -3623,11 +3630,11 @@ packages: - kind: conda name: pillow version: 10.3.0 - build: py38h85abd47_0 + build: py312h0c923fa_0 subdir: osx-64 - url: https://conda.anaconda.org/conda-forge/osx-64/pillow-10.3.0-py38h85abd47_0.conda - sha256: b3f85b5c20ab9d2614e81816adb0a88769ae5b3392e02b802d470b3e4805e0bf - md5: 785f9ef8a330dcf8d6619c127b13f21d + url: https://conda.anaconda.org/conda-forge/osx-64/pillow-10.3.0-py312h0c923fa_0.conda + sha256: 3e33ce8ba364948eeeeb06da435059b1ed0e6cfb2b1195931b76e190ee671310 + md5: 6f0591ae972e9b815739da3392fbb3c3 depends: - freetype >=2.12.1,<3.0a0 - lcms2 >=2.16,<3.0a0 @@ -3637,20 +3644,20 @@ packages: - libxcb >=1.15,<1.16.0a0 - libzlib >=1.2.13,<1.3.0a0 - openjpeg >=2.5.2,<3.0a0 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 - tk >=8.6.13,<8.7.0a0 license: HPND - size: 41189766 - timestamp: 1712154782357 + size: 42531277 + timestamp: 1712154782302 - kind: conda name: pillow version: 10.3.0 - build: py38h894f861_0 + build: py312h6f6a607_0 subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/pillow-10.3.0-py38h894f861_0.conda - sha256: d661083709fd0e2262388adeae52a0c8591c762a54582a6fa0941158990ecd68 - md5: 9b2eb85eed298007db9c714981ab87fe + url: https://conda.anaconda.org/conda-forge/win-64/pillow-10.3.0-py312h6f6a607_0.conda + sha256: f1621c28346609886ccce14b6ae0069b5cb34925ace73e05a8c06770d2ad7a19 + md5: 8d5f5f1fa36200f1ef987299a47de403 depends: - freetype >=2.12.1,<3.0a0 - lcms2 >=2.16,<3.0a0 @@ -3660,63 +3667,63 @@ packages: - libxcb >=1.15,<1.16.0a0 - libzlib >=1.2.13,<1.3.0a0 - openjpeg >=2.5.2,<3.0a0 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 - tk >=8.6.13,<8.7.0a0 - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 license: HPND - size: 40924506 - timestamp: 1712155038917 + size: 42439434 + timestamp: 1712155248737 - kind: conda name: pillow version: 10.3.0 - build: py38h9e66945_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py38h9e66945_0.conda - sha256: b44195d022fa378808397b29406447f0a9e0e4486d03a8a97f014e8f78b091a5 - md5: 06a7c758cf349a5bf24989f179bb504e + build: py312h8a801b1_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-10.3.0-py312h8a801b1_0.conda + sha256: 26bc04e81ae5fce70e4b72478dadea29d32b693eed17640be7721108a3c9af0d + md5: 1d42544faaed27dce36268912b8dfedf depends: - freetype >=2.12.1,<3.0a0 - lcms2 >=2.16,<3.0a0 - - libgcc-ng >=12 - libjpeg-turbo >=3.0.0,<4.0a0 - libtiff >=4.6.0,<4.7.0a0 - libwebp-base >=1.3.2,<2.0a0 - libxcb >=1.15,<1.16.0a0 - libzlib >=1.2.13,<1.3.0a0 - openjpeg >=2.5.2,<3.0a0 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 - tk >=8.6.13,<8.7.0a0 license: HPND - size: 42314111 - timestamp: 1712154579877 + size: 42729895 + timestamp: 1712155044162 - kind: conda name: pillow version: 10.3.0 - build: py38h9ef4633_0 - subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-10.3.0-py38h9ef4633_0.conda - sha256: 1d32b60ffb4b851e839e7d0ee5930b9d2a4c6158ee149b400ab9cf55f41c7a3c - md5: c43a7c5715ba3697fb8e9fc5da1a2d96 + build: py312hdcec9eb_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/pillow-10.3.0-py312hdcec9eb_0.conda + sha256: a7fdcc1e56b66d95622bad073cc8d347cc180988040419754abb2a4ed7b29471 + md5: 425bb325f970e57a047ac57c4586489d depends: - freetype >=2.12.1,<3.0a0 - lcms2 >=2.16,<3.0a0 + - libgcc-ng >=12 - libjpeg-turbo >=3.0.0,<4.0a0 - libtiff >=4.6.0,<4.7.0a0 - libwebp-base >=1.3.2,<2.0a0 - libxcb >=1.15,<1.16.0a0 - libzlib >=1.2.13,<1.3.0a0 - openjpeg >=2.5.2,<3.0a0 - - python >=3.8,<3.9.0a0 - - python >=3.8,<3.9.0a0 *_cpython - - python_abi 3.8.* *_cp38 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 - tk >=8.6.13,<8.7.0a0 license: HPND - size: 41460984 - timestamp: 1712155043935 + size: 41991755 + timestamp: 1712154634705 - kind: conda name: pip version: 23.2.1 @@ -4180,6 +4187,114 @@ packages: license: Python-2.0 size: 22357104 timestamp: 1710939954552 +- kind: conda + name: python + version: 3.12.3 + build: h1411813_0_cpython + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.3-h1411813_0_cpython.conda + sha256: 3b327ffc152a245011011d1d730781577a8274fde1cf6243f073749ead8f1c2a + md5: df1448ec6cbf8eceb03d29003cf72ae6 + depends: + - __osx >=10.9 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.6.2,<3.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.45.2,<4.0a0 + - libzlib >=1.2.13,<1.3.0a0 + - ncurses >=6.4.20240210,<7.0a0 + - openssl >=3.2.1,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + size: 14557341 + timestamp: 1713208068012 +- kind: conda + name: python + version: 3.12.3 + build: h2628c8c_0_cpython + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/python-3.12.3-h2628c8c_0_cpython.conda + sha256: 1a95494abe572a8819c933f978df89f00bde72ea9432d46a70632599e8029ea4 + md5: f07c8c5dd98767f9a652de5d039b284e + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.6.2,<3.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.45.2,<4.0a0 + - libzlib >=1.2.13,<1.3.0a0 + - openssl >=3.2.1,<4.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + size: 16179248 + timestamp: 1713205644673 +- kind: conda + name: python + version: 3.12.3 + build: h4a7b5fc_0_cpython + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.3-h4a7b5fc_0_cpython.conda + sha256: c761fb3713ea66bce3889b33b6f400afb2dd192d1fc2686446e9d8166cfcec6b + md5: 8643ab37bece6ae8f112464068d9df9c + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.6.2,<3.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.45.2,<4.0a0 + - libzlib >=1.2.13,<1.3.0a0 + - ncurses >=6.4.20240210,<7.0a0 + - openssl >=3.2.1,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + size: 13207557 + timestamp: 1713206576646 +- kind: conda + name: python + version: 3.12.3 + build: hab00c5b_0_cpython + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.3-hab00c5b_0_cpython.conda + sha256: f9865bcbff69f15fd89a33a2da12ad616e98d65ce7c83c644b92e66e5016b227 + md5: 2540b74d304f71d3e89c81209db4db84 + depends: + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.6.2,<3.0a0 + - libffi >=3.4,<4.0a0 + - libgcc-ng >=12 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.45.2,<4.0a0 + - libuuid >=2.38.1,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.2.13,<1.3.0a0 + - ncurses >=6.4.20240210,<7.0a0 + - openssl >=3.2.1,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + size: 31991381 + timestamp: 1713208036041 - kind: conda name: python-dateutil version: 2.9.0 @@ -4256,6 +4371,66 @@ packages: license_family: BSD size: 6751 timestamp: 1695147671006 +- kind: conda + name: python_abi + version: '3.12' + build: 4_cp312 + build_number: 4 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-4_cp312.conda + sha256: 182a329de10a4165f6e8a3804caf751f918f6ea6176dd4e5abcdae1ed3095bf6 + md5: dccc2d142812964fcc6abdc97b672dff + constrains: + - python 3.12.* *_cpython + license: BSD-3-Clause + license_family: BSD + size: 6385 + timestamp: 1695147396604 +- kind: conda + name: python_abi + version: '3.12' + build: 4_cp312 + build_number: 4 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-4_cp312.conda + sha256: 82c154d95c1637604671a02a89e72f1382e89a4269265a03506496bd928f6f14 + md5: 87201ac4314b911b74197e588cca3639 + constrains: + - python 3.12.* *_cpython + license: BSD-3-Clause + license_family: BSD + size: 6496 + timestamp: 1695147498447 +- kind: conda + name: python_abi + version: '3.12' + build: 4_cp312 + build_number: 4 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-4_cp312.conda + sha256: db25428e4f24f8693ffa39f3ff6dfbb8fd53bc298764b775b57edab1c697560f + md5: bbb3a02c78b2d8219d7213f76d644a2a + constrains: + - python 3.12.* *_cpython + license: BSD-3-Clause + license_family: BSD + size: 6508 + timestamp: 1695147497048 +- kind: conda + name: python_abi + version: '3.12' + build: 4_cp312 + build_number: 4 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.12-4_cp312.conda + sha256: 488f8519d04b48f59bd6fde21ebe2d7a527718ff28aac86a8b53aa63658bdef6 + md5: 17f4ccf6be9ded08bd0a376f489ac1a6 + constrains: + - python 3.12.* *_cpython + license: BSD-3-Clause + license_family: BSD + size: 6785 + timestamp: 1695147430513 - kind: conda name: pytz version: '2024.1' @@ -4274,76 +4449,76 @@ packages: - kind: conda name: pyyaml version: 6.0.1 - build: py38h01eb140_1 + build: py312h02f2b3b_1 build_number: 1 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py38h01eb140_1.conda - sha256: 7741529957e3b3428af73f003f043c9983ed672c69dc4aafef848b2583c4571b - md5: 5f05353ae9a6c37e1b4aebc9f7834d23 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.1-py312h02f2b3b_1.conda + sha256: b6b4027b89c17b9bbd8089aec3e44bc29f802a7d5668d5a75b5358d7ed9705ca + md5: a0c843e52a1c4422d8657dd76e9eb994 depends: - - libgcc-ng >=12 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12.0rc3,<3.13.0a0 + - python >=3.12.0rc3,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 - yaml >=0.2.5,<0.3.0a0 license: MIT license_family: MIT - size: 182153 - timestamp: 1695373618370 + size: 182705 + timestamp: 1695373895409 - kind: conda name: pyyaml version: 6.0.1 - build: py38h91455d4_1 + build: py312h104f124_1 build_number: 1 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.1-py38h91455d4_1.conda - sha256: 1cd8fe0f885c7e491b41e55611f546d011db8ac45941202eb2ef1549f6df0507 - md5: 4d9ea280b4f91fa5b0c0d34f2fce99cb + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.1-py312h104f124_1.conda + sha256: 04aa180782cb675b960c0bf4aad439b4a7a08553c6af74d0b8e5df9a0c7cc4f4 + md5: 260ed90aaf06061edabd7209638cf03b depends: - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - python >=3.12.0rc3,<3.13.0a0 + - python_abi 3.12.* *_cp312 - yaml >=0.2.5,<0.3.0a0 license: MIT license_family: MIT - size: 151945 - timestamp: 1695373981322 + size: 185636 + timestamp: 1695373742454 - kind: conda name: pyyaml version: 6.0.1 - build: py38hb192615_1 + build: py312h98912ed_1 build_number: 1 - subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.1-py38hb192615_1.conda - sha256: a4bcd94eda8611ade946a52cb52cf60ca6aa4d69915a9c68a9d9b7cbf02e4ac0 - md5: 72ee6bc5ee0182fb7c5f26461504cbf5 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py312h98912ed_1.conda + sha256: 7f347a10a7121b08d79d21cd4f438c07c23479ea0c74dfb89d6dc416f791bb7f + md5: e3fd78d8d490af1d84763b9fe3f2e552 depends: - - python >=3.8,<3.9.0a0 - - python >=3.8,<3.9.0a0 *_cpython - - python_abi 3.8.* *_cp38 + - libgcc-ng >=12 + - python >=3.12.0rc3,<3.13.0a0 + - python_abi 3.12.* *_cp312 - yaml >=0.2.5,<0.3.0a0 license: MIT license_family: MIT - size: 158422 - timestamp: 1695373866893 + size: 196583 + timestamp: 1695373632212 - kind: conda name: pyyaml version: 6.0.1 - build: py38hcafd530_1 + build: py312he70551f_1 build_number: 1 - subdir: osx-64 - url: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.1-py38hcafd530_1.conda - sha256: cd1dceaa9bb8296ddea04cfb5e933bf5ab2b189c566bb55e1a3c9a38efffa82d - md5: 17cfcfdd18fa2fe701ff68c9bbcea9a5 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.1-py312he70551f_1.conda + sha256: a72fa8152791b4738432f270e70b3a9a4d583ef059a78aa1c62f4b4ab7b15494 + md5: f91e0baa89ba21166916624ba7bfb422 depends: - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12.0rc3,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 - yaml >=0.2.5,<0.3.0a0 license: MIT license_family: MIT - size: 161848 - timestamp: 1695373748011 + size: 167932 + timestamp: 1695374097139 - kind: conda name: pyyaml-env-tag version: '0.1' @@ -4408,69 +4583,71 @@ packages: timestamp: 1679532707590 - kind: conda name: regex - version: 2024.4.16 - build: py38h01eb140_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/regex-2024.4.16-py38h01eb140_0.conda - sha256: 6be2e931308e4245aefaab5a4658de2353da895f3f4a860f9c672ce021063f7d - md5: ab3c16328ee4d9702eb90c56c8228450 + version: 2024.4.28 + build: py312h4389bb4_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/regex-2024.4.28-py312h4389bb4_0.conda + sha256: b16227dbc411267d9b23ea9bfc9778bbae4a8593b1e84fac71fb1cdf829d1d61 + md5: fc45482d8e83a3c85acb107385550166 depends: - - libgcc-ng >=12 - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 license: Python-2.0 license_family: PSF - size: 346393 - timestamp: 1713305283145 + size: 358313 + timestamp: 1714348290002 - kind: conda name: regex - version: 2024.4.16 - build: py38h336bac9_0 + version: 2024.4.28 + build: py312h4a164c9_0 subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/regex-2024.4.16-py38h336bac9_0.conda - sha256: c7cd0f88c7ccfa53d85f47881f4d4b100c64301a3e216e1d854739a00f5b52d4 - md5: 4c1ff46c475f150365b38560c0126928 + url: https://conda.anaconda.org/conda-forge/osx-arm64/regex-2024.4.28-py312h4a164c9_0.conda + sha256: a9cbb9201f987a1449634f70877939795c17fa58c50ff466191194e7ea955af1 + md5: bb22ba0467f1fd98413d2c1a3df76231 depends: - - python >=3.8,<3.9.0a0 - - python >=3.8,<3.9.0a0 *_cpython - - python_abi 3.8.* *_cp38 + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 license: Python-2.0 license_family: PSF - size: 307898 - timestamp: 1713305521381 + size: 361720 + timestamp: 1714348060826 - kind: conda name: regex - version: 2024.4.16 - build: py38h91455d4_0 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/regex-2024.4.16-py38h91455d4_0.conda - sha256: 8d23a0e3f5c6dd727f62b7f9c47b28df6b305a562bee0dce42053afbe7c0b39a - md5: ff05c0626ba6b41b284639d6f23b192a + version: 2024.4.28 + build: py312h5fa3f64_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/regex-2024.4.28-py312h5fa3f64_0.conda + sha256: 9b4386d84b0511ad48ea1d208f177a388d2b10c2c8062850ee35f123738ba78e + md5: 025920a03909118fc0f208c1dcc62b94 depends: - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - __osx >=10.9 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 license: Python-2.0 license_family: PSF - size: 306076 - timestamp: 1713305834816 + size: 366832 + timestamp: 1714347981911 - kind: conda name: regex - version: 2024.4.16 - build: py38hae2e43d_0 - subdir: osx-64 - url: https://conda.anaconda.org/conda-forge/osx-64/regex-2024.4.16-py38hae2e43d_0.conda - sha256: f32549036335e1056746c1f501ffe9208de094be6efdf2656c09e3c24bd7517c - md5: 7a5e00f13c2862b7e0a53ad92e7dea9b + version: 2024.4.28 + build: py312h9a8786e_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/regex-2024.4.28-py312h9a8786e_0.conda + sha256: 3ee80b9a7bc73fe1a68feeb3eebedf19d6cc57f4181e5e7f75a772afb269f221 + md5: 39fbec9483427256beaec8b6104e52c0 depends: - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - libgcc-ng >=12 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 license: Python-2.0 license_family: PSF - size: 312856 - timestamp: 1713305547739 + size: 399284 + timestamp: 1714347894590 - kind: conda name: requests version: 2.31.0 @@ -4861,6 +5038,18 @@ packages: license_family: PSF size: 37583 timestamp: 1712330089194 +- kind: conda + name: tzdata + version: 2024a + build: h0c530f3_0 + subdir: noarch + noarch: generic + url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + sha256: 7b2b69c54ec62a243eb6fba2391b5e443421608c3ae5dbff938ad33ca8db5122 + md5: 161081fc7cec0bfda0d86d7cb595f8d8 + license: LicenseRef-Public-Domain + size: 119815 + timestamp: 1706886945727 - kind: conda name: ucrt version: 10.0.22621.0 @@ -4944,68 +5133,68 @@ packages: - kind: conda name: watchdog version: 4.0.0 - build: py38h2bea1e5_0 - subdir: osx-64 - url: https://conda.anaconda.org/conda-forge/osx-64/watchdog-4.0.0-py38h2bea1e5_0.conda - sha256: e8505e3d0453cc290f11c96ba5d56330be2a021b6848164811f37fe49828d7d7 - md5: 0d0a92d705e8a42ea70651eb6f3e2e8c + build: py312h2e8e312_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/watchdog-4.0.0-py312h2e8e312_0.conda + sha256: 4b1eeaecccadf55a5c322e25290d75c8bed7b0d5e25fa6dfa03fc16fc9919fc4 + md5: 186ec4486a2c5d738c002067665b50be depends: - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 - pyyaml >=3.10 license: Apache-2.0 license_family: APACHE - size: 118056 - timestamp: 1707295680540 + size: 152911 + timestamp: 1707295573907 - kind: conda name: watchdog version: 4.0.0 - build: py38h336bac9_0 - subdir: osx-arm64 - url: https://conda.anaconda.org/conda-forge/osx-arm64/watchdog-4.0.0-py38h336bac9_0.conda - sha256: 8acd6290c7f09b5b791eb9eeb43508eaed5aa22b75f81aad01e7f8287f98a77d - md5: 6987169f47c17148af8b2073726810b8 + build: py312h7900ff3_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/watchdog-4.0.0-py312h7900ff3_0.conda + sha256: db3ef9753934826c008216b198f04a6637150e1d91d72733148c0822e4a042a2 + md5: 1b87b82dd803565550e6358c0790f3d2 depends: - - python >=3.8,<3.9.0a0 - - python >=3.8,<3.9.0a0 *_cpython - - python_abi 3.8.* *_cp38 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 - pyyaml >=3.10 license: Apache-2.0 license_family: APACHE - size: 118848 - timestamp: 1707295730630 + size: 136845 + timestamp: 1707295261797 - kind: conda name: watchdog version: 4.0.0 - build: py38h578d9bd_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/watchdog-4.0.0-py38h578d9bd_0.conda - sha256: f6a07da2d18ed7366e2b07832de4626cdd7a82d2c42d64866c20decbc10996b0 - md5: fd6f9afe747e1ec3744158e83728fc0b + build: py312hc2c2f20_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/watchdog-4.0.0-py312hc2c2f20_0.conda + sha256: f333e1f11d60e096d8b0f2b7dbe313fc9ee22d6c09f0a0cc7d3c9fed56ee48dd + md5: ebd7ea0d23052393f0a62efe8a508e99 depends: - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 - pyyaml >=3.10 license: Apache-2.0 license_family: APACHE - size: 110414 - timestamp: 1707295340662 + size: 144711 + timestamp: 1707295580304 - kind: conda name: watchdog version: 4.0.0 - build: py38haa244fe_0 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/watchdog-4.0.0-py38haa244fe_0.conda - sha256: 53248a9bc2f0cead364d18fde7f3e169bdd8ea1284dd71e04684bd45601dcad2 - md5: 6c76373804c8f4b37dec9b23522d9624 + build: py312he37b823_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/watchdog-4.0.0-py312he37b823_0.conda + sha256: 3e7486e161e4478a1bb63cb124a446b21b0af113458522d215ba76eebb1a473a + md5: c483c04540c229b50564201c5432667c depends: - - python >=3.8,<3.9.0a0 - - python_abi 3.8.* *_cp38 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 - pyyaml >=3.10 license: Apache-2.0 license_family: APACHE - size: 127122 - timestamp: 1707295617098 + size: 145347 + timestamp: 1707295575866 - kind: conda name: webencodings version: 0.5.1 diff --git a/py-rattler/rattler/exceptions.py b/py-rattler/rattler/exceptions.py index dd9b1599a..954789ca4 100644 --- a/py-rattler/rattler/exceptions.py +++ b/py-rattler/rattler/exceptions.py @@ -19,6 +19,7 @@ VersionBumpError, EnvironmentCreationError, ExtractError, + GatewayError, ) except ImportError: # They are only redefined for documentation purposes @@ -81,6 +82,9 @@ class EnvironmentCreationError(Exception): # type: ignore[no-redef] class ExtractError(Exception): # type: ignore[no-redef] """An error that can occur when extracting an archive.""" + class GatewayError(Exception): # type: ignore[no-redef] + """An error that can occur when querying the repodata gateway.""" + __all__ = [ "ActivationError", @@ -102,4 +106,5 @@ class ExtractError(Exception): # type: ignore[no-redef] "VersionBumpError", "EnvironmentCreationError", "ExtractError", + "GatewayError", ] diff --git a/py-rattler/rattler/repo_data/gateway.py b/py-rattler/rattler/repo_data/gateway.py index 49052b89f..5d3f76771 100644 --- a/py-rattler/rattler/repo_data/gateway.py +++ b/py-rattler/rattler/repo_data/gateway.py @@ -1,17 +1,15 @@ from __future__ import annotations -import typing +import os +from typing import Optional, List, Literal from dataclasses import dataclass -from rattler.rattler import PyGateway, PySourceConfig +from rattler.rattler import PyGateway, PySourceConfig, PyMatchSpec -from rattler import Channel +from rattler import Channel, MatchSpec, RepoDataRecord, PackageName, Platform +from rattler.platform.platform import PlatformLiteral -if typing.TYPE_CHECKING: - import os - from typing import Optional - -CacheAction = typing.Literal["cache-or-fetch", "use-cache-only", "force-cache-only", "no-cache"] +CacheAction = Literal["cache-or-fetch", "use-cache-only", "force-cache-only", "no-cache"] @dataclass @@ -29,6 +27,17 @@ class SourceConfig: cache_action: CacheAction = "cache-or-fetch" def _into_py(self) -> PySourceConfig: + """ + Converts this object into a type that can be used by the Rust code. + + Examples + -------- + ```python + >>> SourceConfig()._into_py() # doctest: +ELLIPSIS + + >>> + ``` + """ return PySourceConfig( jlap_enabled=self.jlap_enabled, zstd_enabled=self.zstd_enabled, @@ -48,8 +57,24 @@ def __init__( cache_dir: Optional[os.PathLike[str]] = None, default_config: Optional[SourceConfig] = None, per_channel_config: Optional[dict[Channel | str, SourceConfig]] = None, - max_concurrent_requests: int = 100 + max_concurrent_requests: int = 100, ) -> None: + """ + Arguments: + cache_dir: The directory where the repodata should be cached. If not specified the + default cache directory is used. + default_config: The default configuration for channels. + per_channel_config: Per channel configuration. + max_concurrent_requests: The maximum number of concurrent requests that can be made. + + Examples + -------- + ```python + >>> Gateway() + Gateway() + >>> + ``` + """ default_config = default_config or SourceConfig() self._gateway = PyGateway( @@ -60,5 +85,76 @@ def __init__( channel)._channel: config._into_py() for channel, config in (per_channel_config or {}).items() }, - max_concurrent_requests=max_concurrent_requests + max_concurrent_requests=max_concurrent_requests, ) + + async def query( + self, + channels: List[Channel | str], + platforms: List[Platform | PlatformLiteral], + specs: List[MatchSpec | PackageName | str], + recursive: bool = True, + ) -> List[List[RepoDataRecord]]: + """Queries the gateway for repodata. + + If `recursive` is `True` the gateway will recursively fetch the dependencies of the + encountered records. If `recursive` is `False` only the records with the package names + specified in `specs` are returned. + + The `specs` can either be a `MatchSpec`, `PackageName` or a string. If a string or a + `PackageName` is provided it will be converted into a MatchSpec that matches any record + with the given name. If a `MatchSpec` is provided all records that match the name + specified in the spec will be returned, but only the dependencies of the records + that match the entire spec are recursively fetched. + + The gateway caches the records internally, so if the same channel is queried multiple + times the records will only be fetched once. However, the conversion of the records to + a python object is done every time the query method is called. + + Arguments: + channels: The channels to query. + platforms: The platforms to query. + specs: The specs to query. + recursive: Whether recursively fetch dependencies or not. + + Returns: + A list of lists of `RepoDataRecord`s. The outer list contains the results for each + channel in the same order they are provided in the `channels` argument. + + Examples + -------- + ```python + >>> import asyncio + >>> gateway = Gateway() + >>> records = asyncio.run(gateway.query(["conda-forge"], ["linux-aarch64"], ["python"])) + >>> assert len(records) == 1 + >>> + """ + py_records = await self._gateway.query( + channels=[ + channel._channel if isinstance(channel, Channel) else Channel(channel)._channel for + channel in channels + ], + platforms=[platform._inner if isinstance(platform, Platform) else Platform(platform)._inner for platform in platforms], + specs=[spec._match_spec if isinstance(spec, MatchSpec) else PyMatchSpec(str(spec), True) + for spec in specs], + recursive=recursive, + ) + + # Convert the records into python objects + return [[RepoDataRecord._from_py_record(record) for record in records] for records + in py_records] + + def __repr__(self) -> str: + """ + Returns a representation of the Gateway. + + Examples + -------- + ```python + >>> Gateway() + Gateway() + >>> + ``` + """ + return f"{type(self).__name__}()" diff --git a/py-rattler/src/channel/mod.rs b/py-rattler/src/channel/mod.rs index 358927454..2b9c012ea 100644 --- a/py-rattler/src/channel/mod.rs +++ b/py-rattler/src/channel/mod.rs @@ -79,7 +79,7 @@ impl PyChannel { /// Returns the Urls for the given platform. pub fn platform_url(&self, platform: &PyPlatform) -> String { - self.inner.platform_url(platform.clone().into()).into() + self.inner.platform_url((*platform).into()).into() } } diff --git a/py-rattler/src/error.rs b/py-rattler/src/error.rs index e57d08659..2d048c0c9 100644 --- a/py-rattler/src/error.rs +++ b/py-rattler/src/error.rs @@ -10,6 +10,7 @@ use rattler_conda_types::{ use rattler_lock::{ConversionError, ParseCondaLockError}; use rattler_package_streaming::ExtractError; use rattler_repodata_gateway::fetch::FetchRepoDataError; +use rattler_repodata_gateway::GatewayError; use rattler_shell::activation::ActivationError; use rattler_solve::SolveError; use rattler_virtual_packages::DetectVirtualPackageError; @@ -64,6 +65,8 @@ pub enum PyRattlerError { ExtractError(#[from] ExtractError), #[error(transparent)] ActivationScriptFormatError(std::fmt::Error), + #[error(transparent)] + GatewayError(#[from] GatewayError), } impl From for PyErr { @@ -114,6 +117,7 @@ impl From for PyErr { PyRattlerError::ActivationScriptFormatError(err) => { ActivationScriptFormatException::new_err(err.to_string()) } + PyRattlerError::GatewayError(err) => GatewayException::new_err(err.to_string()), } } } @@ -141,3 +145,4 @@ create_exception!(exceptions, RequirementException, PyException); create_exception!(exceptions, EnvironmentCreationException, PyException); create_exception!(exceptions, ExtractException, PyException); create_exception!(exceptions, ActivationScriptFormatException, PyException); +create_exception!(exceptions, GatewayException, PyException); diff --git a/py-rattler/src/lib.rs b/py-rattler/src/lib.rs index 2da5a077d..03876b3af 100644 --- a/py-rattler/src/lib.rs +++ b/py-rattler/src/lib.rs @@ -57,6 +57,7 @@ use version::PyVersion; use pyo3::prelude::*; +use crate::error::GatewayException; use index::py_index; use linker::py_link; use meta::get_rattler_version; @@ -68,6 +69,7 @@ use virtual_package::PyVirtualPackage; /// A struct to make it easy to wrap a type as a python type. #[repr(transparent)] +#[derive(Clone)] pub struct Wrap(pub T); impl Deref for Wrap { @@ -210,5 +212,8 @@ fn rattler(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add("ExtractError", py.get_type::()) .unwrap(); + m.add("GatewayError", py.get_type::()) + .unwrap(); + Ok(()) } diff --git a/py-rattler/src/linker.rs b/py-rattler/src/linker.rs index fc5e90a39..67ca338b6 100644 --- a/py-rattler/src/linker.rs +++ b/py-rattler/src/linker.rs @@ -199,7 +199,7 @@ pub async fn install_package_to_environment( }; let target_prefix = target_prefix.clone(); - match tokio::task::spawn_blocking(move || { + let write_prefix_fut = tokio::task::spawn_blocking(move || { let conda_meta_path = target_prefix.join("conda-meta"); std::fs::create_dir_all(&conda_meta_path)?; @@ -215,8 +215,8 @@ pub async fn install_package_to_environment( )); prefix_record.write_to_path(pkg_meta_path, true) }) - .await - { + .await; + match write_prefix_fut { Ok(result) => Ok(result?), Err(err) => { if let Ok(panic) = err.try_into_panic() { diff --git a/py-rattler/src/platform.rs b/py-rattler/src/platform.rs index f1b024b9c..06527d7ae 100644 --- a/py-rattler/src/platform.rs +++ b/py-rattler/src/platform.rs @@ -53,7 +53,8 @@ impl PyArch { /////////////////////////// #[pyclass] -#[derive(Clone, Eq, PartialEq, Hash)] +#[repr(transparent)] +#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd)] pub struct PyPlatform { pub inner: Platform, } @@ -70,18 +71,6 @@ impl From for Platform { } } -impl PartialOrd for PyPlatform { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for PyPlatform { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.inner.cmp(&other.inner) - } -} - impl FromStr for PyPlatform { type Err = PyRattlerError; diff --git a/py-rattler/src/repo_data/gateway.rs b/py-rattler/src/repo_data/gateway.rs index 8df04b65f..9cf8fbf1e 100644 --- a/py-rattler/src/repo_data/gateway.rs +++ b/py-rattler/src/repo_data/gateway.rs @@ -1,6 +1,11 @@ +use crate::error::PyRattlerError; +use crate::match_spec::PyMatchSpec; +use crate::platform::PyPlatform; +use crate::record::PyRecord; use crate::{PyChannel, Wrap}; use pyo3::exceptions::PyValueError; -use pyo3::{pyclass, pymethods, FromPyObject, PyAny, PyResult}; +use pyo3::{pyclass, pymethods, FromPyObject, PyAny, PyResult, Python}; +use pyo3_asyncio::tokio::future_into_py; use rattler_repodata_gateway::fetch::CacheAction; use rattler_repodata_gateway::{ChannelConfig, Gateway, SourceConfig}; use std::collections::HashMap; @@ -34,12 +39,13 @@ impl PyGateway { per_channel_config: HashMap, cache_dir: Option, ) -> PyResult { - let mut channel_config = ChannelConfig::default(); - channel_config.default = default_config.into(); - channel_config.per_channel = per_channel_config - .into_iter() - .map(|(k, v)| (k.into(), v.into())) - .collect(); + let channel_config = ChannelConfig { + default: default_config.into(), + per_channel: per_channel_config + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(), + }; let mut gateway = Gateway::builder() .with_max_concurrent_requests(max_concurrent_requests) @@ -53,6 +59,36 @@ impl PyGateway { inner: gateway.finish(), }) } + + pub fn query<'a>( + &self, + py: Python<'a>, + channels: Vec, + platforms: Vec, + specs: Vec, + recursive: bool, + ) -> PyResult<&'a PyAny> { + let gateway = self.inner.clone(); + future_into_py(py, async move { + let repodatas = gateway + .query(channels, platforms.into_iter().map(|p| p.inner), specs) + .recursive(recursive) + .execute() + .await + .map_err(PyRattlerError::from)?; + + // Convert the records into a list of lists + Ok(repodatas + .into_iter() + .map(|r| { + r.into_iter() + .cloned() + .map(PyRecord::from) + .collect::>() + }) + .collect::>()) + }) + } } #[pyclass] From 5c8c741b43901e4963ad708ac57346a9799659c0 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 6 May 2024 17:23:14 +0200 Subject: [PATCH 38/57] feat: solve takes a gateway instead --- py-rattler/docs/gateway.md | 3 + py-rattler/docs/stylesheets/extra.css | 48 ++++++++++++- py-rattler/mkdocs.yml | 67 +++++++++++++++-- py-rattler/pixi.lock | 40 ++++++++--- py-rattler/pixi.toml | 3 +- py-rattler/rattler/__init__.py | 7 +- py-rattler/rattler/platform/__init__.py | 4 +- py-rattler/rattler/repo_data/__init__.py | 3 + py-rattler/rattler/repo_data/gateway.py | 74 +++++++++++++------ py-rattler/rattler/solver/solver.py | 44 ++++++++---- py-rattler/src/solver.rs | 92 +++++++++++++++--------- py-rattler/tests/conftest.py | 20 ++++++ py-rattler/tests/unit/test_link.py | 22 ++---- py-rattler/tests/unit/test_solver.py | 57 +++++---------- 14 files changed, 335 insertions(+), 149 deletions(-) create mode 100644 py-rattler/docs/gateway.md create mode 100644 py-rattler/tests/conftest.py diff --git a/py-rattler/docs/gateway.md b/py-rattler/docs/gateway.md new file mode 100644 index 000000000..2af00aa81 --- /dev/null +++ b/py-rattler/docs/gateway.md @@ -0,0 +1,3 @@ +# Gateway + +::: rattler.repo_data.gateway diff --git a/py-rattler/docs/stylesheets/extra.css b/py-rattler/docs/stylesheets/extra.css index 6678fd5ad..66be165cc 100644 --- a/py-rattler/docs/stylesheets/extra.css +++ b/py-rattler/docs/stylesheets/extra.css @@ -2,18 +2,60 @@ font-family: 'Dosis', sans-serif; } +[data-md-color-primary=prefix] { + --md-primary-fg-color: #F9C405; + --md-primary-fg-color--light: #ffee57; + --md-primary-fg-color--dark: #F9C405; + --md-primary-bg-color: #000000de; + --md-primary-bg-color--light: #0000008a +} + +[data-md-color-accent=prefix] { + --md-accent-fg-color: #fa0; + --md-accent2-fg-color: #eab308; + --md-accent-fg-color--transparent: #ffaa001a; + --md-accent-bg-color: #000000de; + --md-accent-bg-color--light: #0000008a +} + + +[data-md-color-primary=prefix-light] { + --md-primary-fg-color: #000000de; + --md-primary-fg-color--light: #ffee57; + --md-primary-fg-color--dark: #F9C405; + --md-primary-bg-color: #F9C405; + --md-primary-bg-color--light: #F9C405; + --md-code-bg-color: rgba(0, 0, 0, 0.04); +} + +[data-md-color-accent=prefix-light] { + --md-accent-fg-color: #2e2400; + --md-accent2-fg-color: #19116f; + --md-accent-fg-color--transparent: #ffaa001a; + --md-accent-bg-color: #000000de; + --md-accent-bg-color--light: #0000008a +} + +.md-typeset a { + color: var(--md-accent2-fg-color); +} + +.md-nav__item .md-nav__link--active, .md-nav__item .md-nav__link--active code { + color: var(--md-accent-fg-color); + font-weight: bold; +} .md-header__topic:first-child { font-weight: normal; } .md-typeset h1 { - color: #ffec3d; + color: var(--md-accent-fg-color); } .md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4, .md-typeset h5, .md-typeset h6 { font-family: 'Dosis', sans-serif; font-weight: 500; - color: #ffec3d; + color: var(--md-accent-fg-color); } .md-typeset p { @@ -22,7 +64,7 @@ } :root > * { - --md-code-hl-string-color: #ffec3d; + --md-code-hl-string-color: var(--md-accent-fg-color); } .md-header__button.md-logo { diff --git a/py-rattler/mkdocs.yml b/py-rattler/mkdocs.yml index f480a9889..719c5fbff 100644 --- a/py-rattler/mkdocs.yml +++ b/py-rattler/mkdocs.yml @@ -6,28 +6,84 @@ theme: primary: yellow accent: yellow scheme: slate - + site_url: https://prefix.dev font: text: Red Hat Text code: JetBrains Mono + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: prefix-light + accent: prefix-light + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: prefix + accent: prefix + toggle: + icon: material/brightness-4 + name: Switch to system preference + icon: + edit: material/pencil + view: material/eye + + features: + - content.tabs.link + - content.code.copy + - content.action.edit + - content.code.annotate + # - content.code.select Sponsor only + - navigation.instant + - navigation.instant.progress + - navigation.tracking + - navigation.sections + - navigation.top + - navigation.footer extra_css: - stylesheets/extra.css repo_url: https://github.com/mamba-org/rattler/ +edit_uri: edit/main/py-rattler/docs/ markdown_extensions: + - admonition + - def_list + - footnotes + - admonition + - def_list + - footnotes + - pymdownx.tasklist: + custom_checkbox: true - pymdownx.highlight: anchor_linenums: true line_spans: __span pygments_lang_class: true - pymdownx.inlinehilite - pymdownx.snippets - - pymdownx.superfences - - admonition - - def_list - - footnotes + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + - pymdownx.tabbed: + alternate_style: true + - toc: + toc_depth: 3 + permalink: "#" + - mdx_truly_sane_lists nav: - First Steps: index.md @@ -69,6 +125,7 @@ nav: - PrefixPaths: prefix_paths.md - PrefixRecord: prefix_record.md - repo_data: + - Gateway: gateway.md - PackageRecord: package_record.md - PatchInstructions: patch_instructions.md - RepoDataRecord: repo_data_record.md diff --git a/py-rattler/pixi.lock b/py-rattler/pixi.lock index 945b713f2..feeadd341 100644 --- a/py-rattler/pixi.lock +++ b/py-rattler/pixi.lock @@ -172,10 +172,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py312h98912ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdx_truly_sane_lists-1.3-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/mergedeep-1.3.4-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-1.5.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-autorefs-1.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.18-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.20-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-0.24.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.9.2-pyhd8ed1ab_0.conda @@ -278,10 +279,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/markupsafe-2.1.5-py312h41838bb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdx_truly_sane_lists-1.3-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/mergedeep-1.3.4-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-1.5.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-autorefs-1.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.18-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.20-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-0.24.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.9.2-pyhd8ed1ab_0.conda @@ -375,10 +377,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.2.13-h53f4e23_5.conda - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-2.1.5-py312he37b823_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdx_truly_sane_lists-1.3-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/mergedeep-1.3.4-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-1.5.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-autorefs-1.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.18-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.20-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-0.24.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.9.2-pyhd8ed1ab_0.conda @@ -476,10 +479,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-libwinpthread-git-5.0.0.4634.697f757-2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-2.1.5-py312he70551f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdx_truly_sane_lists-1.3-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/mergedeep-1.3.4-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-1.5.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-autorefs-1.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.18-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.20-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-0.24.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.9.2-pyhd8ed1ab_0.conda @@ -3064,6 +3068,22 @@ packages: license_family: MIT size: 4675089 timestamp: 1695301899264 +- kind: conda + name: mdx_truly_sane_lists + version: '1.3' + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/mdx_truly_sane_lists-1.3-pyhd8ed1ab_0.tar.bz2 + sha256: 2a00cd521d63ae8a20b52de590ff2f1f63ea4ba569f7e66ae629330f0e69cf43 + md5: 3c4c4f9b8ae968cb20823351d81d12b5 + depends: + - markdown >=2.6 + - python >=3.6 + license: MIT + license_family: MIT + size: 10480 + timestamp: 1658251565870 - kind: conda name: mergedeep version: 1.3.4 @@ -3131,13 +3151,13 @@ packages: timestamp: 1709500020733 - kind: conda name: mkdocs-material - version: 9.5.18 + version: 9.5.20 build: pyhd8ed1ab_0 subdir: noarch noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.18-pyhd8ed1ab_0.conda - sha256: 47f3d939ade648d8288705f1dd95c9a1f80b10772b1fdbd8a81a6fe92689c395 - md5: d5d4bb5f8a501e9ce4bc73a5baa3553b + url: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.5.20-pyhd8ed1ab_0.conda + sha256: 38f61b17fa334d20a60c5a37eefa836e05c4e4b0a3cff763591c941be90de348 + md5: 5f09758905bfaf7d5c748196f63aba35 depends: - babel ~=2.10 - colorama ~=0.4 @@ -3153,8 +3173,8 @@ packages: - requests ~=2.26 license: MIT license_family: MIT - size: 5002352 - timestamp: 1713278417020 + size: 5007228 + timestamp: 1714393800216 - kind: conda name: mkdocs-material-extensions version: 1.3.1 diff --git a/py-rattler/pixi.toml b/py-rattler/pixi.toml index 733e5b9ca..7b7cccece 100644 --- a/py-rattler/pixi.toml +++ b/py-rattler/pixi.toml @@ -55,7 +55,8 @@ fmt-check = { depends_on = ["fmt-python-check", "fmt-rust-check"] } mkdocs = "1.5.3.*" mkdocstrings-python = ">=1.9.0,<1.10" mkdocstrings = ">=0.24.1,<0.25" -mkdocs-material = ">=9.5.17" +mkdocs-material = ">=9.5.20" +mdx_truly_sane_lists = ">=1.3,<2" cairosvg = "2.7.1.*" pillow = ">=9.4.0" diff --git a/py-rattler/rattler/__init__.py b/py-rattler/rattler/__init__.py index d3bbe3cf9..2f317e34c 100644 --- a/py-rattler/rattler/__init__.py +++ b/py-rattler/rattler/__init__.py @@ -6,6 +6,8 @@ RepoDataRecord, PatchInstructions, SparseRepoData, + Gateway, + SourceConfig, ) from rattler.channel import Channel, ChannelConfig, ChannelPriority from rattler.networking import AuthenticatedClient, fetch_repo_data @@ -19,9 +21,9 @@ PathType, PrefixPlaceholder, FileMode, + IndexJson, ) from rattler.prefix import PrefixRecord, PrefixPaths, PrefixPathsEntry, PrefixPathType -from rattler.solver import solve from rattler.platform import Platform from rattler.utils.rattler_version import get_rattler_version as _get_rattler_version from rattler.linker import link @@ -35,6 +37,7 @@ PypiPackageData, PypiPackageEnvironmentData, ) +from rattler.solver import solve __version__ = _get_rattler_version() del _get_rattler_version @@ -80,4 +83,6 @@ "PrefixPlaceholder", "FileMode", "IndexJson", + "Gateway", + "SourceConfig", ] diff --git a/py-rattler/rattler/platform/__init__.py b/py-rattler/rattler/platform/__init__.py index 06cf2e856..4a56a4f6e 100644 --- a/py-rattler/rattler/platform/__init__.py +++ b/py-rattler/rattler/platform/__init__.py @@ -1,4 +1,4 @@ -from rattler.platform.platform import Platform +from rattler.platform.platform import Platform, PlatformLiteral from rattler.platform.arch import Arch -__all__ = ["Platform", "Arch"] +__all__ = ["Platform", "PlatformLiteral", "Arch"] diff --git a/py-rattler/rattler/repo_data/__init__.py b/py-rattler/rattler/repo_data/__init__.py index 9512b7097..2cf581994 100644 --- a/py-rattler/rattler/repo_data/__init__.py +++ b/py-rattler/rattler/repo_data/__init__.py @@ -3,6 +3,7 @@ from rattler.repo_data.patch_instructions import PatchInstructions from rattler.repo_data.record import RepoDataRecord from rattler.repo_data.sparse import SparseRepoData +from rattler.repo_data.gateway import Gateway, SourceConfig __all__ = [ "PackageRecord", @@ -10,4 +11,6 @@ "PatchInstructions", "RepoDataRecord", "SparseRepoData", + "Gateway", + "SourceConfig", ] diff --git a/py-rattler/rattler/repo_data/gateway.py b/py-rattler/rattler/repo_data/gateway.py index 5d3f76771..b1b01f9aa 100644 --- a/py-rattler/rattler/repo_data/gateway.py +++ b/py-rattler/rattler/repo_data/gateway.py @@ -6,8 +6,11 @@ from rattler.rattler import PyGateway, PySourceConfig, PyMatchSpec -from rattler import Channel, MatchSpec, RepoDataRecord, PackageName, Platform -from rattler.platform.platform import PlatformLiteral +from rattler.channel import Channel +from rattler.match_spec import MatchSpec +from rattler.repo_data.record import RepoDataRecord +from rattler.platform import Platform, PlatformLiteral +from rattler.package.package_name import PackageName CacheAction = Literal["cache-or-fetch", "use-cache-only", "force-cache-only", "no-cache"] @@ -22,9 +25,22 @@ class SourceConfig: """ jlap_enabled: bool = True + """Whether the JLAP compression is enabled or not.""" + zstd_enabled: bool = True + """Whether the ZSTD compression is enabled or not.""" + bz2_enabled: bool = True + """Whether the BZ2 compression is enabled or not.""" + cache_action: CacheAction = "cache-or-fetch" + """How to interact with the cache. + + * `'cache-or-fetch'` (default): Use the cache if its up to date or fetch from the URL if there is no valid cached value. + * `'use-cache-only'`: Only use the cache, but error out if the cache is not up to date + * `'force-cache-only'`: Only use the cache, ignore whether or not it is up to date. + * `'no-cache'`: Do not use the cache even if there is an up to date entry + """ def _into_py(self) -> PySourceConfig: """ @@ -48,16 +64,28 @@ def _into_py(self) -> PySourceConfig: class Gateway: """ - An object that manages repodata and allows efficiently querying different - channels for it. + The gateway manages all the quircks and complex bits of efficiently acquiring + repodata. It implements all the necessary logic to fetch the repodata from a + remote server, cache it locally and convert it into python objects. + + The gateway can also easily be used concurrently, as it is designed to be + thread-safe. When two threads are querying the same channel at the same time, + their requests are coallesced into a single request. This is done to reduce the + number of requests made to the remote server and reduce the overal memory usage. + + The gateway caches the repodata internally, so if the same channel is queried + multiple times the records will only be fetched once. However, the conversion + of the records to a python object is done every time the query method is called. + Therefor, instead of requesting records directly, its more efficient to pass the + gateway itself to methods that accepts it. """ def __init__( - self, - cache_dir: Optional[os.PathLike[str]] = None, - default_config: Optional[SourceConfig] = None, - per_channel_config: Optional[dict[Channel | str, SourceConfig]] = None, - max_concurrent_requests: int = 100, + self, + cache_dir: Optional[os.PathLike[str]] = None, + default_config: Optional[SourceConfig] = None, + per_channel_config: Optional[dict[Channel | str, SourceConfig]] = None, + max_concurrent_requests: int = 100, ) -> None: """ Arguments: @@ -81,19 +109,18 @@ def __init__( cache_dir=cache_dir, default_config=default_config._into_py(), per_channel_config={ - channel._channel if isinstance(channel, Channel) else Channel( - channel)._channel: config._into_py() + channel._channel if isinstance(channel, Channel) else Channel(channel)._channel: config._into_py() for channel, config in (per_channel_config or {}).items() }, max_concurrent_requests=max_concurrent_requests, ) async def query( - self, - channels: List[Channel | str], - platforms: List[Platform | PlatformLiteral], - specs: List[MatchSpec | PackageName | str], - recursive: bool = True, + self, + channels: List[Channel | str], + platforms: List[Platform | PlatformLiteral], + specs: List[MatchSpec | PackageName | str], + recursive: bool = True, ) -> List[List[RepoDataRecord]]: """Queries the gateway for repodata. @@ -129,21 +156,22 @@ async def query( >>> records = asyncio.run(gateway.query(["conda-forge"], ["linux-aarch64"], ["python"])) >>> assert len(records) == 1 >>> + ``` """ py_records = await self._gateway.query( channels=[ - channel._channel if isinstance(channel, Channel) else Channel(channel)._channel for - channel in channels + channel._channel if isinstance(channel, Channel) else Channel(channel)._channel for channel in channels + ], + platforms=[ + platform._inner if isinstance(platform, Platform) else Platform(platform)._inner + for platform in platforms ], - platforms=[platform._inner if isinstance(platform, Platform) else Platform(platform)._inner for platform in platforms], - specs=[spec._match_spec if isinstance(spec, MatchSpec) else PyMatchSpec(str(spec), True) - for spec in specs], + specs=[spec._match_spec if isinstance(spec, MatchSpec) else PyMatchSpec(str(spec), True) for spec in specs], recursive=recursive, ) # Convert the records into python objects - return [[RepoDataRecord._from_py_record(record) for record in records] for records - in py_records] + return [[RepoDataRecord._from_py_record(record) for record in records] for records in py_records] def __repr__(self) -> str: """ diff --git a/py-rattler/rattler/solver/solver.py b/py-rattler/rattler/solver/solver.py index bf25b5c69..21629ea7b 100644 --- a/py-rattler/rattler/solver/solver.py +++ b/py-rattler/rattler/solver/solver.py @@ -1,18 +1,24 @@ from __future__ import annotations import datetime from typing import List, Optional + +from rattler import Channel, Platform from rattler.match_spec.match_spec import MatchSpec from rattler.channel import ChannelPriority -from rattler.rattler import py_solve +from rattler.rattler import py_solve, PyMatchSpec + +from rattler.platform.platform import PlatformLiteral +from rattler.repo_data.gateway import Gateway from rattler.repo_data.record import RepoDataRecord -from rattler.repo_data.sparse import SparseRepoData from rattler.virtual_package.generic import GenericVirtualPackage -def solve( - specs: List[MatchSpec], - available_packages: List[SparseRepoData], +async def solve( + channels: List[Channel | str], + platforms: List[Platform | PlatformLiteral], + specs: List[MatchSpec | str], + gateway: Gateway, locked_packages: Optional[List[RepoDataRecord]] = None, pinned_packages: Optional[List[RepoDataRecord]] = None, virtual_packages: Optional[List[GenericVirtualPackage]] = None, @@ -25,7 +31,9 @@ def solve( Arguments: specs: A list of matchspec to solve. - available_packages: A list of RepoData to use for solving the `specs`. + channels: The channels to query for the packages. + platforms: The platforms to query for the packages. + gateway: The gateway to use for acquiring repodata. locked_packages: Records of packages that are previously selected. If the solver encounters multiple variants of a single package (identified by its name), it will sort the records @@ -46,6 +54,7 @@ def solve( the channel that the package is first found in will be used as the only channel for that package. When `ChannelPriority.Disabled` it will search for every package in every channel. + timeout: The maximum time the solver is allowed to run. Returns: Resolved list of `RepoDataRecord`s. @@ -53,13 +62,20 @@ def solve( return [ RepoDataRecord._from_py_record(solved_package) - for solved_package in py_solve( - [spec._match_spec for spec in specs], - [package._sparse for package in available_packages], - [package._record for package in locked_packages or []], - [package._record for package in pinned_packages or []], - [v_package._generic_virtual_package for v_package in virtual_packages or []], - channel_priority.value, - timeout.microseconds if timeout else None, + for solved_package in await py_solve( + channels=[ + channel._channel if isinstance(channel, Channel) else Channel(channel)._channel for channel in channels + ], + platforms=[ + platform._inner if isinstance(platform, Platform) else Platform(platform)._inner + for platform in platforms + ], + specs=[spec._match_spec if isinstance(spec, MatchSpec) else PyMatchSpec(str(spec), True) for spec in specs], + gateway=gateway._gateway, + locked_packages=[package._record for package in locked_packages or []], + pinned_packages=[package._record for package in pinned_packages or []], + virtual_packages=[v_package._generic_virtual_package for v_package in virtual_packages or []], + channel_priority=channel_priority.value, + timeout=timeout.microseconds if timeout else None, ) ] diff --git a/py-rattler/src/solver.rs b/py-rattler/src/solver.rs index d21603923..fa2aae5a7 100644 --- a/py-rattler/src/solver.rs +++ b/py-rattler/src/solver.rs @@ -1,55 +1,79 @@ -use pyo3::{pyfunction, PyResult, Python}; -use rattler_repodata_gateway::sparse::SparseRepoData; -use rattler_solve::{resolvo::Solver, SolverImpl, SolverTask}; +use pyo3::{pyfunction, PyAny, PyErr, PyResult, Python}; +use pyo3_asyncio::tokio::future_into_py; +use rattler_solve::{resolvo::Solver, RepoDataIter, SolverImpl, SolverTask}; +use tokio::task::JoinError; +use crate::channel::PyChannel; +use crate::platform::PyPlatform; +use crate::repo_data::gateway::PyGateway; use crate::{ channel::PyChannelPriority, error::PyRattlerError, generic_virtual_package::PyGenericVirtualPackage, match_spec::PyMatchSpec, record::PyRecord, - repo_data::sparse::PySparseRepoData, }; #[allow(clippy::too_many_arguments)] #[pyfunction] pub fn py_solve( py: Python<'_>, + channels: Vec, + platforms: Vec, specs: Vec, - available_packages: Vec, + gateway: PyGateway, locked_packages: Vec, pinned_packages: Vec, virtual_packages: Vec, channel_priority: PyChannelPriority, timeout: Option, -) -> PyResult> { - py.allow_threads(move || { - let package_names = specs - .iter() - .filter_map(|match_spec| match_spec.inner.name.clone()); +) -> PyResult<&'_ PyAny> { + future_into_py(py, async move { + let available_packages = gateway + .inner + .query( + channels.into_iter(), + platforms.into_iter().map(Into::into), + specs.clone().into_iter(), + ) + .recursive(true) + .execute() + .await + .map_err(PyRattlerError::from)?; - let available_packages = SparseRepoData::load_records_recursive( - available_packages.iter().map(Into::into), - package_names, - None, - )?; + let solve_result = tokio::task::spawn_blocking(move || { + let task = SolverTask { + available_packages: available_packages + .iter() + .map(RepoDataIter) + .collect::>(), + locked_packages: locked_packages + .into_iter() + .map(TryInto::try_into) + .collect::>>()?, + pinned_packages: pinned_packages + .into_iter() + .map(TryInto::try_into) + .collect::>>()?, + virtual_packages: virtual_packages.into_iter().map(Into::into).collect(), + specs: specs.into_iter().map(Into::into).collect(), + timeout: timeout.map(std::time::Duration::from_micros), + channel_priority: channel_priority.into(), + }; - let task = SolverTask { - available_packages: &available_packages, - locked_packages: locked_packages - .into_iter() - .map(TryInto::try_into) - .collect::>>()?, - pinned_packages: pinned_packages - .into_iter() - .map(TryInto::try_into) - .collect::>>()?, - virtual_packages: virtual_packages.into_iter().map(Into::into).collect(), - specs: specs.into_iter().map(Into::into).collect(), - timeout: timeout.map(std::time::Duration::from_micros), - channel_priority: channel_priority.into(), - }; + Ok::<_, PyErr>( + Solver + .solve(task) + .map(|res| res.into_iter().map(Into::into).collect::>()) + .map_err(PyRattlerError::from)?, + ) + }) + .await; - Ok(Solver - .solve(task) - .map(|res| res.into_iter().map(Into::into).collect::>()) - .map_err(PyRattlerError::from)?) + match solve_result.map_err(JoinError::try_into_panic) { + Ok(solve_result) => Ok(solve_result?), + Err(Ok(payload)) => std::panic::resume_unwind(payload), + Err(Err(_err)) => Err(PyRattlerError::IoError(std::io::Error::new( + std::io::ErrorKind::Interrupted, + "solver task was cancelled", + )))?, + } }) } diff --git a/py-rattler/tests/conftest.py b/py-rattler/tests/conftest.py new file mode 100644 index 000000000..f078e5e3a --- /dev/null +++ b/py-rattler/tests/conftest.py @@ -0,0 +1,20 @@ +import os + +import pytest + +from rattler import Gateway, Channel + + +@pytest.fixture(scope="session") +def gateway(): + return Gateway() + +@pytest.fixture +def conda_forge_channel(): + data_dir = os.path.join(os.path.dirname(__file__), "../../test-data/channels/conda-forge") + return Channel(data_dir) + +@pytest.fixture +def pytorch_channel(): + data_dir = os.path.join(os.path.dirname(__file__), "../../test-data/channels/pytorch") + return Channel(data_dir) diff --git a/py-rattler/tests/unit/test_link.py b/py-rattler/tests/unit/test_link.py index e235bfc72..0fc68976c 100644 --- a/py-rattler/tests/unit/test_link.py +++ b/py-rattler/tests/unit/test_link.py @@ -1,27 +1,19 @@ -# type: ignore import os import pytest -from rattler import Channel, SparseRepoData, MatchSpec, solve, link +from rattler import solve, link @pytest.mark.asyncio -async def test_link(tmp_path): +async def test_link(gateway, conda_forge_channel, tmp_path): cache_dir = tmp_path / "cache" env_dir = tmp_path / "env" - linux64_chan = Channel("conda-forge") - data_dir = os.path.join(os.path.dirname(__file__), "../../../test-data/") - linux64_path = os.path.join(data_dir, "channels/conda-forge/linux-64/repodata.json") - linux64_data = SparseRepoData( - channel=linux64_chan, - subdir="linux-64", - path=linux64_path, - ) - - solved_data = solve( - [MatchSpec("xtensor")], - [linux64_data], + solved_data = await solve( + [conda_forge_channel], + ["linux-64"], + ["xtensor"], + gateway, ) await link(solved_data, env_dir, cache_dir) diff --git a/py-rattler/tests/unit/test_solver.py b/py-rattler/tests/unit/test_solver.py index f20b13e73..04d0af0a4 100644 --- a/py-rattler/tests/unit/test_solver.py +++ b/py-rattler/tests/unit/test_solver.py @@ -1,29 +1,18 @@ -# type: ignore -import os.path +import pytest from rattler import ( solve, - Channel, ChannelPriority, - MatchSpec, RepoDataRecord, - SparseRepoData, ) - -def test_solve(): - linux64_chan = Channel("conda-forge") - data_dir = os.path.join(os.path.dirname(__file__), "../../../test-data/") - linux64_path = os.path.join(data_dir, "channels/conda-forge/linux-64/repodata.json") - linux64_data = SparseRepoData( - channel=linux64_chan, - subdir="linux-64", - path=linux64_path, - ) - - solved_data = solve( - [MatchSpec("python"), MatchSpec("sqlite")], - [linux64_data], +@pytest.mark.asyncio +async def test_solve(gateway, conda_forge_channel): + solved_data = await solve( + [conda_forge_channel], + ["linux-64"], + ["python", "sqlite"], + gateway, ) assert isinstance(solved_data, list) @@ -31,31 +20,17 @@ def test_solve(): assert len(solved_data) == 19 -def test_solve_channel_priority_disabled(): - cf_chan = Channel("conda-forge") - data_dir = os.path.join(os.path.dirname(__file__), "../../../test-data/") - cf_path = os.path.join(data_dir, "channels/conda-forge/linux-64/repodata.json") - cf_data = SparseRepoData( - channel=cf_chan, - subdir="linux-64", - path=cf_path, - ) - - pytorch_chan = Channel("pytorch") - pytorch_path = os.path.join(data_dir, "channels/pytorch/linux-64/repodata.json") - pytorch_data = SparseRepoData( - channel=pytorch_chan, - subdir="linux-64", - path=pytorch_path, - ) - - solved_data = solve( - [MatchSpec("pytorch-cpu=0.4.1=py36_cpu_1")], - [cf_data, pytorch_data], +@pytest.mark.asyncio +async def test_solve_channel_priority_disabled(gateway, pytorch_channel, conda_forge_channel): + solved_data = await solve( + [conda_forge_channel, pytorch_channel], + ["linux-64"], + ["pytorch-cpu=0.4.1=py36_cpu_1"], + gateway, channel_priority=ChannelPriority.Disabled, ) assert isinstance(solved_data, list) assert isinstance(solved_data[0], RepoDataRecord) - assert list(filter(lambda r: r.file_name.startswith("pytorch-cpu-0.4.1-py36_cpu_1"), solved_data))[0].channel == "https://conda.anaconda.org/pytorch/" + assert list(filter(lambda r: r.file_name.startswith("pytorch-cpu-0.4.1-py36_cpu_1"), solved_data))[0].channel == pytorch_channel.base_url assert len(solved_data) == 32 From 28d44e2a8670d66259d26d5dcbe73d02929004a5 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 6 May 2024 17:45:19 +0200 Subject: [PATCH 39/57] bump: update pixi version in ci --- .github/workflows/python-bindings.yml | 2 +- crates/rattler_repodata_gateway/src/gateway/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml index 84dcd939f..9d949ca73 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows/python-bindings.yml @@ -35,7 +35,7 @@ jobs: lfs: true - uses: prefix-dev/setup-pixi@v0.6.0 with: - pixi-version: v0.13.0 + pixi-version: v0.20.1 cache: true manifest-path: py-rattler/pixi.toml - uses: actions-rust-lang/setup-rust-toolchain@v1 diff --git a/crates/rattler_repodata_gateway/src/gateway/mod.rs b/crates/rattler_repodata_gateway/src/gateway/mod.rs index a35209c59..1cd562775 100644 --- a/crates/rattler_repodata_gateway/src/gateway/mod.rs +++ b/crates/rattler_repodata_gateway/src/gateway/mod.rs @@ -65,7 +65,7 @@ impl Gateway { GatewayBuilder::default() } - /// Constructs a new [`GatewayQuery`] which can be used to query repodata records. + /// Constructs a new `GatewayQuery` which can be used to query repodata records. pub fn query( &self, channels: ChannelIter, From a11670f1461e3f0d2c49d2c617519b643a9ba505 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 6 May 2024 17:59:14 +0200 Subject: [PATCH 40/57] fix: lower pixi lock-file version --- py-rattler/pixi.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py-rattler/pixi.lock b/py-rattler/pixi.lock index feeadd341..f42976b16 100644 --- a/py-rattler/pixi.lock +++ b/py-rattler/pixi.lock @@ -1,4 +1,4 @@ -version: 5 +version: 4 environments: build: channels: From c4ed85dde254935878314fbfbdb4ccb412d41513 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 6 May 2024 18:02:10 +0200 Subject: [PATCH 41/57] fix: use correct env --- .github/workflows/python-bindings.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml index 9d949ca73..38dc1be0f 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows/python-bindings.yml @@ -44,9 +44,9 @@ jobs: - name: Format and Lint run: | cd py-rattler - pixi run lint - pixi run fmt-check + pixi run -e test lint + pixi run -e test fmt-check - name: Run tests run: | cd py-rattler - pixi run test + pixi run -e test test From 1262a35c2d36185565280c0407b60984f18e3657 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 6 May 2024 18:09:32 +0200 Subject: [PATCH 42/57] fix: test types --- py-rattler/tests/conftest.py | 6 +++--- py-rattler/tests/unit/test_link.py | 6 ++++-- py-rattler/tests/unit/test_solver.py | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/py-rattler/tests/conftest.py b/py-rattler/tests/conftest.py index f078e5e3a..5676e5019 100644 --- a/py-rattler/tests/conftest.py +++ b/py-rattler/tests/conftest.py @@ -6,15 +6,15 @@ @pytest.fixture(scope="session") -def gateway(): +def gateway() -> Gateway: return Gateway() @pytest.fixture -def conda_forge_channel(): +def conda_forge_channel() -> Channel: data_dir = os.path.join(os.path.dirname(__file__), "../../test-data/channels/conda-forge") return Channel(data_dir) @pytest.fixture -def pytorch_channel(): +def pytorch_channel() ->Channel: data_dir = os.path.join(os.path.dirname(__file__), "../../test-data/channels/pytorch") return Channel(data_dir) diff --git a/py-rattler/tests/unit/test_link.py b/py-rattler/tests/unit/test_link.py index 0fc68976c..ada58792b 100644 --- a/py-rattler/tests/unit/test_link.py +++ b/py-rattler/tests/unit/test_link.py @@ -1,11 +1,13 @@ import os +from pathlib import Path + import pytest -from rattler import solve, link +from rattler import solve, link, Gateway, Channel @pytest.mark.asyncio -async def test_link(gateway, conda_forge_channel, tmp_path): +async def test_link(gateway: Gateway, conda_forge_channel: Channel, tmp_path: Path) -> None: cache_dir = tmp_path / "cache" env_dir = tmp_path / "env" diff --git a/py-rattler/tests/unit/test_solver.py b/py-rattler/tests/unit/test_solver.py index 04d0af0a4..aec3c859b 100644 --- a/py-rattler/tests/unit/test_solver.py +++ b/py-rattler/tests/unit/test_solver.py @@ -3,11 +3,11 @@ from rattler import ( solve, ChannelPriority, - RepoDataRecord, + RepoDataRecord, Channel, Gateway, ) @pytest.mark.asyncio -async def test_solve(gateway, conda_forge_channel): +async def test_solve(gateway: Gateway, conda_forge_channel: Channel) -> None: solved_data = await solve( [conda_forge_channel], ["linux-64"], @@ -21,7 +21,7 @@ async def test_solve(gateway, conda_forge_channel): @pytest.mark.asyncio -async def test_solve_channel_priority_disabled(gateway, pytorch_channel, conda_forge_channel): +async def test_solve_channel_priority_disabled(gateway: Gateway, pytorch_channel: Channel, conda_forge_channel: Channel) -> None: solved_data = await solve( [conda_forge_channel, pytorch_channel], ["linux-64"], From 9f484e0370a708f96606dd69541640d0199fcf49 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Tue, 7 May 2024 10:12:28 +0200 Subject: [PATCH 43/57] fix: link test without symlinks and base_url --- py-rattler/tests/conftest.py | 15 +++++++++------ py-rattler/tests/unit/test_link.py | 10 +++++----- .../conda-forge/linux-64/repodata.json | 4 ++-- .../channels/conda-forge/noarch/repodata.json | 4 ++-- .../conda-forge/noarch/repodata.json.gz | Bin 7049342 -> 6818212 bytes 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/py-rattler/tests/conftest.py b/py-rattler/tests/conftest.py index 5676e5019..b8fb593a4 100644 --- a/py-rattler/tests/conftest.py +++ b/py-rattler/tests/conftest.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import pytest @@ -10,11 +11,13 @@ def gateway() -> Gateway: return Gateway() @pytest.fixture -def conda_forge_channel() -> Channel: - data_dir = os.path.join(os.path.dirname(__file__), "../../test-data/channels/conda-forge") - return Channel(data_dir) +def test_data_dir() -> str: + return os.path.normpath(os.path.join(os.path.dirname(__file__), "../../test-data")) @pytest.fixture -def pytorch_channel() ->Channel: - data_dir = os.path.join(os.path.dirname(__file__), "../../test-data/channels/pytorch") - return Channel(data_dir) +def conda_forge_channel(test_data_dir: str) -> Channel: + return Channel(os.path.join(test_data_dir, "channels/conda-forge")) + +@pytest.fixture +def pytorch_channel(test_data_dir: str) -> Channel: + return Channel(os.path.join(test_data_dir, "channels/conda-pytorch")) diff --git a/py-rattler/tests/unit/test_link.py b/py-rattler/tests/unit/test_link.py index ada58792b..b633209ce 100644 --- a/py-rattler/tests/unit/test_link.py +++ b/py-rattler/tests/unit/test_link.py @@ -13,13 +13,13 @@ async def test_link(gateway: Gateway, conda_forge_channel: Channel, tmp_path: Pa solved_data = await solve( [conda_forge_channel], - ["linux-64"], - ["xtensor"], + ["noarch"], + ["conda-forge-pinning"], gateway, ) await link(solved_data, env_dir, cache_dir) - assert os.path.exists(env_dir / "include/xtensor.hpp") - assert os.path.exists(env_dir / "include/xtensor") - assert os.path.exists(env_dir / "include/xtl") + assert os.path.exists(env_dir / "conda_build_config.yaml") + assert os.path.exists(env_dir / "share/conda-forge/migrations/pypy37.yaml") + assert os.path.exists(env_dir / "share/conda-forge/migrations/pypy37-windows.yaml") diff --git a/test-data/channels/conda-forge/linux-64/repodata.json b/test-data/channels/conda-forge/linux-64/repodata.json index 966e55547..15baf460b 100644 --- a/test-data/channels/conda-forge/linux-64/repodata.json +++ b/test-data/channels/conda-forge/linux-64/repodata.json @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cc0553f5f68b30bbb831a03cb0a76cb5d40cd129e073e551e27c8373ee83f94 -size 205364821 +oid sha256:69822802dd157f97607ce07870f5645489fdcf29857c968020c57674b7b0e87e +size 205364889 diff --git a/test-data/channels/conda-forge/noarch/repodata.json b/test-data/channels/conda-forge/noarch/repodata.json index 051ae233b..1aa81b4f7 100644 --- a/test-data/channels/conda-forge/noarch/repodata.json +++ b/test-data/channels/conda-forge/noarch/repodata.json @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05e0c4ce7be29f36949c33cce782f21aecfbdd41f9e3423839670fb38fc5d691 -size 51649914 +oid sha256:58c9b7ebea25cdbc1e2c67804f14c605a868adaef816e33717db98e328c5847e +size 51649980 diff --git a/test-data/channels/conda-forge/noarch/repodata.json.gz b/test-data/channels/conda-forge/noarch/repodata.json.gz index 9586164568f04a4d02d867c4d4b29e6d2dc3ca0f..d6c2fad8f9d83f83b50116c9d0fb2e92866e0474 100644 GIT binary patch delta 6782639 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�LiahezY&SXQNdc#c5U<5B+P@1{S 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
6oO@Y7g5s`XFgzKeO0MmzyPqaKVvP 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
VXixNmUW>ft8t>@DFJroVoYPkU8)h&fB?+34|l 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
-|>vRQLAax8-Fhn^2rLI` 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