Skip to content

Commit 4c04afc

Browse files
baszalmstraaochagaviatdejager
authored
Improved Concurrent metadata fetching (#29)
* chore: update rust-toolchain to latest stable release * feat: concurrent metadata fetching * more concurrency * feat: sort_candidates is now async * fix: fmt and clippy * refactor: no more channels * refactor: everything concurrent * fix: tests and clippy * feat: runtime agnostic impl * fix: forgot public docs * fix: remove outdated comment --------- Co-authored-by: Adolfo Ochagavía <github@adolfo.ochagavia.nl> Co-authored-by: Tim de Jager <tim@prefix.dev>
1 parent 357a64d commit 4c04afc

10 files changed

+785
-400
lines changed

Cargo.toml

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "resolvo"
33
version = "0.3.0"
4-
authors = ["Adolfo Ochagavía <github@adolfo.ochagavia.nl>", "Bas Zalmstra <zalmstra.bas@gmail.com>", "Tim de Jager <tdejager89@gmail.com>" ]
4+
authors = ["Adolfo Ochagavía <github@adolfo.ochagavia.nl>", "Bas Zalmstra <zalmstra.bas@gmail.com>", "Tim de Jager <tdejager89@gmail.com>"]
55
description = "Fast package resolver written in Rust (CDCL based SAT solving)"
66
keywords = ["dependency", "solver", "version"]
77
categories = ["algorithms"]
@@ -10,17 +10,25 @@ repository = "https://github.com/mamba-org/resolvo"
1010
license = "BSD-3-Clause"
1111
edition = "2021"
1212
readme = "README.md"
13+
resolver = "2"
1314

1415
[dependencies]
15-
itertools = "0.11.0"
16+
itertools = "0.12.1"
1617
petgraph = "0.6.4"
1718
tracing = "0.1.37"
1819
elsa = "1.9.0"
1920
bitvec = "1.0.1"
2021
serde = { version = "1.0", features = ["derive"], optional = true }
22+
futures = { version = "0.3.30", default-features = false, features = ["alloc"] }
23+
event-listener = "5.0.0"
24+
25+
tokio = { version = "1.35.1", features = ["rt"], optional = true }
26+
async-std = { version = "1.12.0", default-features = false, features = ["alloc", "default"], optional = true }
2127

2228
[dev-dependencies]
2329
insta = "1.31.0"
2430
indexmap = "2.0.0"
2531
proptest = "1.2.0"
2632
tracing-test = { version = "0.2.4", features = ["no-env-filter"] }
33+
tokio = { version = "1.35.1", features = ["time", "rt"] }
34+
resolvo = { path = ".", features = ["tokio"] }

rust-toolchain

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.72.0
1+
1.75.0

src/lib.rs

+17-9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub(crate) mod internal;
1414
mod pool;
1515
pub mod problem;
1616
pub mod range;
17+
pub mod runtime;
1718
mod solvable;
1819
mod solver;
1920

@@ -30,9 +31,10 @@ use std::{
3031
any::Any,
3132
fmt::{Debug, Display},
3233
hash::Hash,
34+
rc::Rc,
3335
};
3436

35-
/// The solver is based around the fact that for for every package name we are trying to find a
37+
/// The solver is based around the fact that for every package name we are trying to find a
3638
/// single variant. Variants are grouped by their respective package name. A package name is
3739
/// anything that we can compare and hash for uniqueness checks.
3840
///
@@ -44,7 +46,7 @@ pub trait PackageName: Eq + Hash {}
4446

4547
impl<N: Eq + Hash> PackageName for N {}
4648

47-
/// A [`VersionSet`] is describes a set of "versions". The trait defines whether a given version
49+
/// A [`VersionSet`] describes a set of "versions". The trait defines whether a given version
4850
/// is part of the set or not.
4951
///
5052
/// One could implement [`VersionSet`] for [`std::ops::Range<u32>`] where the implementation
@@ -61,21 +63,26 @@ pub trait VersionSet: Debug + Display + Clone + Eq + Hash {
6163
/// packages that are available in the system.
6264
pub trait DependencyProvider<VS: VersionSet, N: PackageName = String>: Sized {
6365
/// Returns the [`Pool`] that is used to allocate the Ids returned from this instance
64-
fn pool(&self) -> &Pool<VS, N>;
66+
fn pool(&self) -> Rc<Pool<VS, N>>;
6567

6668
/// Sort the specified solvables based on which solvable to try first. The solver will
6769
/// iteratively try to select the highest version. If a conflict is found with the highest
6870
/// version the next version is tried. This continues until a solution is found.
69-
fn sort_candidates(&self, solver: &SolverCache<VS, N, Self>, solvables: &mut [SolvableId]);
71+
#[allow(async_fn_in_trait)]
72+
async fn sort_candidates(
73+
&self,
74+
solver: &SolverCache<VS, N, Self>,
75+
solvables: &mut [SolvableId],
76+
);
7077

71-
/// Returns a list of solvables that should be considered when a package with the given name is
78+
/// Obtains a list of solvables that should be considered when a package with the given name is
7279
/// requested.
73-
///
74-
/// Returns `None` if no such package exist.
75-
fn get_candidates(&self, name: NameId) -> Option<Candidates>;
80+
#[allow(async_fn_in_trait)]
81+
async fn get_candidates(&self, name: NameId) -> Option<Candidates>;
7682

7783
/// Returns the dependencies for the specified solvable.
78-
fn get_dependencies(&self, solvable: SolvableId) -> Dependencies;
84+
#[allow(async_fn_in_trait)]
85+
async fn get_dependencies(&self, solvable: SolvableId) -> Dependencies;
7986

8087
/// Whether the solver should stop the dependency resolution algorithm.
8188
///
@@ -126,6 +133,7 @@ pub struct Candidates {
126133
}
127134

128135
/// Holds information about the dependencies of a package.
136+
#[derive(Debug, Clone)]
129137
pub enum Dependencies {
130138
/// The dependencies are known.
131139
Known(KnownDependencies),

src/problem.rs

+17-16
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,17 @@ use std::collections::{HashMap, HashSet};
44
use std::fmt;
55
use std::fmt::{Display, Formatter};
66
use std::hash::Hash;
7-
87
use std::rc::Rc;
98

109
use itertools::Itertools;
1110
use petgraph::graph::{DiGraph, EdgeIndex, EdgeReference, NodeIndex};
1211
use petgraph::visit::{Bfs, DfsPostOrder, EdgeRef};
1312
use petgraph::Direction;
1413

15-
use crate::internal::id::StringId;
1614
use crate::{
17-
internal::id::{ClauseId, SolvableId, VersionSetId},
15+
internal::id::{ClauseId, SolvableId, StringId, VersionSetId},
1816
pool::Pool,
17+
runtime::AsyncRuntime,
1918
solver::{clause::Clause, Solver},
2019
DependencyProvider, PackageName, SolvableDisplay, VersionSet,
2120
};
@@ -41,9 +40,9 @@ impl Problem {
4140
}
4241

4342
/// Generates a graph representation of the problem (see [`ProblemGraph`] for details)
44-
pub fn graph<VS: VersionSet, N: PackageName, D: DependencyProvider<VS, N>>(
43+
pub fn graph<VS: VersionSet, N: PackageName, D: DependencyProvider<VS, N>, RT: AsyncRuntime>(
4544
&self,
46-
solver: &Solver<VS, N, D>,
45+
solver: &Solver<VS, N, D, RT>,
4746
) -> ProblemGraph {
4847
let mut graph = DiGraph::<ProblemNode, ProblemEdge>::default();
4948
let mut nodes: HashMap<SolvableId, NodeIndex> = HashMap::default();
@@ -53,7 +52,7 @@ impl Problem {
5352
let unresolved_node = graph.add_node(ProblemNode::UnresolvedDependency);
5453

5554
for clause_id in &self.clauses {
56-
let clause = &solver.clauses[*clause_id].kind;
55+
let clause = &solver.clauses.borrow()[*clause_id].kind;
5756
match clause {
5857
Clause::InstallRoot => (),
5958
Clause::Excluded(solvable, reason) => {
@@ -73,7 +72,7 @@ impl Problem {
7372
&Clause::Requires(package_id, version_set_id) => {
7473
let package_node = Self::add_node(&mut graph, &mut nodes, package_id);
7574

76-
let candidates = solver.cache.get_or_cache_sorted_candidates(version_set_id).unwrap_or_else(|_| {
75+
let candidates = solver.async_runtime.block_on(solver.cache.get_or_cache_sorted_candidates(version_set_id)).unwrap_or_else(|_| {
7776
unreachable!("The version set was used in the solver, so it must have been cached. Therefore cancellation is impossible here and we cannot get an `Err(...)`")
7877
});
7978
if candidates.is_empty() {
@@ -167,13 +166,15 @@ impl Problem {
167166
N: PackageName + Display,
168167
D: DependencyProvider<VS, N>,
169168
M: SolvableDisplay<VS, N>,
169+
RT: AsyncRuntime,
170170
>(
171171
&self,
172-
solver: &'a Solver<VS, N, D>,
172+
solver: &'a Solver<VS, N, D, RT>,
173+
pool: Rc<Pool<VS, N>>,
173174
merged_solvable_display: &'a M,
174175
) -> DisplayUnsat<'a, VS, N, M> {
175176
let graph = self.graph(solver);
176-
DisplayUnsat::new(graph, solver.pool(), merged_solvable_display)
177+
DisplayUnsat::new(graph, pool, merged_solvable_display)
177178
}
178179
}
179180

@@ -515,7 +516,7 @@ pub struct DisplayUnsat<'pool, VS: VersionSet, N: PackageName + Display, M: Solv
515516
merged_candidates: HashMap<SolvableId, Rc<MergedProblemNode>>,
516517
installable_set: HashSet<NodeIndex>,
517518
missing_set: HashSet<NodeIndex>,
518-
pool: &'pool Pool<VS, N>,
519+
pool: Rc<Pool<VS, N>>,
519520
merged_solvable_display: &'pool M,
520521
}
521522

@@ -524,10 +525,10 @@ impl<'pool, VS: VersionSet, N: PackageName + Display, M: SolvableDisplay<VS, N>>
524525
{
525526
pub(crate) fn new(
526527
graph: ProblemGraph,
527-
pool: &'pool Pool<VS, N>,
528+
pool: Rc<Pool<VS, N>>,
528529
merged_solvable_display: &'pool M,
529530
) -> Self {
530-
let merged_candidates = graph.simplify(pool);
531+
let merged_candidates = graph.simplify(&pool);
531532
let installable_set = graph.get_installable_set();
532533
let missing_set = graph.get_missing_set();
533534

@@ -669,10 +670,10 @@ impl<'pool, VS: VersionSet, N: PackageName + Display, M: SolvableDisplay<VS, N>>
669670
let version = if let Some(merged) = self.merged_candidates.get(&solvable_id) {
670671
reported.extend(merged.ids.iter().cloned());
671672
self.merged_solvable_display
672-
.display_candidates(self.pool, &merged.ids)
673+
.display_candidates(&self.pool, &merged.ids)
673674
} else {
674675
self.merged_solvable_display
675-
.display_candidates(self.pool, &[solvable_id])
676+
.display_candidates(&self.pool, &[solvable_id])
676677
};
677678

678679
let excluded = graph
@@ -796,9 +797,9 @@ impl<VS: VersionSet, N: PackageName + Display, M: SolvableDisplay<VS, N>> fmt::D
796797
writeln!(
797798
f,
798799
"{indent}{} {} is locked, but another version is required as reported above",
799-
locked.name.display(self.pool),
800+
locked.name.display(&self.pool),
800801
self.merged_solvable_display
801-
.display_candidates(self.pool, &[solvable_id])
802+
.display_candidates(&self.pool, &[solvable_id])
802803
)?;
803804
}
804805
ConflictCause::Excluded => continue,

src/range.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ pub mod tests {
409409
segments.push((start_bound, Unbounded));
410410
}
411411

412-
return Range { segments }.check_invariants();
412+
Range { segments }.check_invariants()
413413
})
414414
}
415415

src/runtime.rs

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//! Solving in resolvo is a compute heavy operation. However, while computing the solver will
2+
//! request additional information from the [`crate::DependencyProvider`] and a dependency provider
3+
//! might want to perform multiple requests concurrently. To that end the
4+
//! [`crate::DependencyProvider`]s methods are async. The implementer can implement the async
5+
//! operations in any way they choose including with any runtime they choose.
6+
//! However, the solver itself is completely single threaded, but it still has to await the calls to
7+
//! the dependency provider. Using the [`AsyncRuntime`] allows the caller of the solver to choose
8+
//! how to await the futures.
9+
//!
10+
//! By default, the solver uses the [`NowOrNeverRuntime`] runtime which polls any future once. If
11+
//! the future yields (thus requiring an additional poll) the runtime panics. If the methods of
12+
//! [`crate::DependencyProvider`] do not yield (e.g. do not `.await`) this will suffice.
13+
//!
14+
//! Only if the [`crate::DependencyProvider`] implementation yields you will need to provide a
15+
//! [`AsyncRuntime`] to the solver.
16+
//!
17+
//! ## `tokio`
18+
//!
19+
//! The [`AsyncRuntime`] trait is implemented both for [`tokio::runtime::Handle`] and for
20+
//! [`tokio::runtime::Runtime`].
21+
//!
22+
//! ## `async-std`
23+
//!
24+
//! Use the [`AsyncStdRuntime`] struct to block on async methods from the
25+
//! [`crate::DependencyProvider`] using the `async-std` executor.
26+
27+
use futures::FutureExt;
28+
use std::future::Future;
29+
30+
/// A trait to wrap an async runtime.
31+
pub trait AsyncRuntime {
32+
/// Runs the given future on the current thread, blocking until it is complete, and yielding its
33+
/// resolved result.
34+
fn block_on<F: Future>(&self, f: F) -> F::Output;
35+
}
36+
37+
/// The simplest runtime possible evaluates and consumes the future, returning the resulting
38+
/// output if the future is ready after the first call to [`Future::poll`]. If the future does
39+
/// yield the runtime panics.
40+
///
41+
/// This assumes that the passed in future never yields. For purely blocking computations this
42+
/// is the preferred method since it also incurs very little overhead and doesn't require the
43+
/// inclusion of a heavy-weight runtime.
44+
#[derive(Default, Copy, Clone)]
45+
pub struct NowOrNeverRuntime;
46+
47+
impl AsyncRuntime for NowOrNeverRuntime {
48+
fn block_on<F: Future>(&self, f: F) -> F::Output {
49+
f.now_or_never()
50+
.expect("can only use non-yielding futures with the NowOrNeverRuntime")
51+
}
52+
}
53+
54+
#[cfg(feature = "tokio")]
55+
impl AsyncRuntime for tokio::runtime::Handle {
56+
fn block_on<F: Future>(&self, f: F) -> F::Output {
57+
self.block_on(f)
58+
}
59+
}
60+
61+
#[cfg(feature = "tokio")]
62+
impl AsyncRuntime for tokio::runtime::Runtime {
63+
fn block_on<F: Future>(&self, f: F) -> F::Output {
64+
self.block_on(f)
65+
}
66+
}
67+
68+
/// An implementation of [`AsyncRuntime`] that spawns and awaits any passed future on the current
69+
/// thread.
70+
#[cfg(feature = "async-std")]
71+
pub struct AsyncStdRuntime;
72+
73+
#[cfg(feature = "async-std")]
74+
impl AsyncRuntime for AsyncStdRuntime {
75+
fn block_on<F: Future>(&self, f: F) -> F::Output {
76+
async_std::task::block_on(f)
77+
}
78+
}

0 commit comments

Comments
 (0)