Skip to content

Commit

Permalink
feat: runtime agnostic impl
Browse files Browse the repository at this point in the history
  • Loading branch information
baszalmstra committed Feb 8, 2024
1 parent 774a205 commit 05ce4f9
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 184 deletions.
13 changes: 9 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "resolvo"
version = "0.3.0"
authors = ["Adolfo Ochagavía <[email protected]>", "Bas Zalmstra <[email protected]>", "Tim de Jager <[email protected]>" ]
authors = ["Adolfo Ochagavía <[email protected]>", "Bas Zalmstra <[email protected]>", "Tim de Jager <[email protected]>"]
description = "Fast package resolver written in Rust (CDCL based SAT solving)"
keywords = ["dependency", "solver", "version"]
categories = ["algorithms"]
Expand All @@ -10,20 +10,25 @@ repository = "https://github.com/mamba-org/resolvo"
license = "BSD-3-Clause"
edition = "2021"
readme = "README.md"
resolver = "2"

[dependencies]
itertools = "0.11.0"
itertools = "0.12.1"
petgraph = "0.6.4"
tracing = "0.1.37"
elsa = "1.9.0"
bitvec = "1.0.1"
serde = { version = "1.0", features = ["derive"], optional = true }
futures = { version = "0.3.30", default-features = false, features = ["alloc"] }
tokio = { version = "1.35.1", features = ["rt", "sync"] }
event-listener = "5.0.0"

tokio = { version = "1.35.1", features = ["rt"], optional = true }
async-std = { version = "1.12.0", default-features = false, features = ["alloc", "default"], optional = true }

[dev-dependencies]
insta = "1.31.0"
indexmap = "2.0.0"
proptest = "1.2.0"
tracing-test = { version = "0.2.4", features = ["no-env-filter"] }
tokio = { version = "1.35.1", features = ["time"] }
tokio = { version = "1.35.1", features = ["time", "rt"] }
resolvo = { path = ".", features = ["tokio"] }
30 changes: 3 additions & 27 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub(crate) mod internal;
mod pool;
pub mod problem;
pub mod range;
pub mod runtime;
mod solvable;
mod solver;

Expand All @@ -33,7 +34,7 @@ use std::{
rc::Rc,
};

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

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

/// A [`VersionSet`] is describes a set of "versions". The trait defines whether a given version
/// A [`VersionSet`] describes a set of "versions". The trait defines whether a given version
/// is part of the set or not.
///
/// One could implement [`VersionSet`] for [`std::ops::Range<u32>`] where the implementation
Expand All @@ -67,15 +68,6 @@ pub trait DependencyProvider<VS: VersionSet, N: PackageName = String>: Sized {
/// Sort the specified solvables based on which solvable to try first. The solver will
/// iteratively try to select the highest version. If a conflict is found with the highest
/// version the next version is tried. This continues until a solution is found.
///
/// # Async
///
/// The returned future will be awaited by a tokio runtime blocking the main thread. You are
/// free to use other runtimes in your implementation, as long as the runtime-specific code runs
/// in threads controlled by that runtime (and _not_ in the main thread). For instance, you can
/// use `async_std::task::spawn` to spawn a new task, use `async_std::io` inside the task to
/// retrieve necessary information from the network, and `await` the returned task handle.
#[allow(async_fn_in_trait)]
async fn sort_candidates(
&self,
Expand All @@ -85,26 +77,10 @@ pub trait DependencyProvider<VS: VersionSet, N: PackageName = String>: Sized {

/// Obtains a list of solvables that should be considered when a package with the given name is
/// requested.
///
/// # Async
///
/// The returned future will be awaited by a tokio runtime blocking the main thread. You are
/// free to use other runtimes in your implementation, as long as the runtime-specific code runs
/// in threads controlled by that runtime (and _not_ in the main thread). For instance, you can
/// use `async_std::task::spawn` to spawn a new task, use `async_std::io` inside the task to
/// retrieve necessary information from the network, and `await` the returned task handle.
#[allow(async_fn_in_trait)]
async fn get_candidates(&self, name: NameId) -> Option<Candidates>;

/// Returns the dependencies for the specified solvable.
///
/// # Async
///
/// The returned future will be awaited by a tokio runtime blocking the main thread. You are
/// free to use other runtimes in your implementation, as long as the runtime-specific code runs
/// in threads controlled by that runtime (and _not_ in the main thread). For instance, you can
/// use `async_std::task::spawn` to spawn a new task, use `async_std::io` inside the task to
/// retrieve necessary information from the network, and `await` the returned task handle.
#[allow(async_fn_in_trait)]
async fn get_dependencies(&self, solvable: SolvableId) -> Dependencies;

Expand Down
11 changes: 6 additions & 5 deletions src/problem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ use petgraph::graph::{DiGraph, EdgeIndex, EdgeReference, NodeIndex};
use petgraph::visit::{Bfs, DfsPostOrder, EdgeRef};
use petgraph::Direction;

use crate::internal::id::StringId;
use crate::{
internal::id::{ClauseId, SolvableId, VersionSetId},
internal::id::{ClauseId, SolvableId, StringId, VersionSetId},
pool::Pool,
runtime::AsyncRuntime,
solver::{clause::Clause, Solver},
DependencyProvider, PackageName, SolvableDisplay, VersionSet,
};
Expand All @@ -40,9 +40,9 @@ impl Problem {
}

/// Generates a graph representation of the problem (see [`ProblemGraph`] for details)
pub fn graph<VS: VersionSet, N: PackageName, D: DependencyProvider<VS, N>>(
pub fn graph<VS: VersionSet, N: PackageName, D: DependencyProvider<VS, N>, RT: AsyncRuntime>(
&self,
solver: &Solver<VS, N, D>,
solver: &Solver<VS, N, D, RT>,
) -> ProblemGraph {
let mut graph = DiGraph::<ProblemNode, ProblemEdge>::default();
let mut nodes: HashMap<SolvableId, NodeIndex> = HashMap::default();
Expand Down Expand Up @@ -158,9 +158,10 @@ impl Problem {
N: PackageName + Display,
D: DependencyProvider<VS, N>,
M: SolvableDisplay<VS, N>,
RT: AsyncRuntime,
>(
&self,
solver: &'a Solver<VS, N, D>,
solver: &'a Solver<VS, N, D, RT>,
pool: Rc<Pool<VS, N>>,
merged_solvable_display: &'a M,
) -> DisplayUnsat<'a, VS, N, M> {
Expand Down
81 changes: 81 additions & 0 deletions src/runtime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//! Solving in resolvo is a compute heavy operation. However, while computing the solver will
//! request additional information from the [`crate::DependencyProvider`] and a dependency provider
//! might want to perform multiple requests concurrently. To that end the
//! [`crate::DependencyProvider`]s methods are async. The implementer can implement the async
//! operations in any way they choose including with any runtime they choose.
//! However, the solver itself is completely single threaded, but it still has to await the calls to
//! the dependency provider. Using the [`AsyncRuntime`] allows the caller of the solver to choose
//! how to await the futures.
//!
//! By default, the solver uses the [`NowOrNeverRuntime`] runtime which polls any future once. If
//! the future yields (thus requiring an additional poll) the runtime panics. If the methods of
//! [`crate::DependencyProvider`] do not yield (e.g. do not `.await`) this will suffice.
//!
//! Only if the [`crate::DependencyProvider`] implementation yields you will need to provide a
//! [`AsyncRuntime`] to the solver.
//!
//! ## `tokio`
//!
//! The solver uses tokio to await the results of async methods in [`crate::DependencyProvider`]. It
//! will run them concurrently, but blocking the main thread. That means that a single-threaded
//! tokio runtime is usually enough. It is also possible to use a different runtime, as long as you
//! avoid mixing incompatible futures.
//!
//! The [`AsyncRuntime`] trait is implemented both for [`tokio::runtime::Handle`] and for
//! [`tokio::runtime::Runtime`].
//!
//! ## `async-std`
//!
//! Use the [`AsyncStdRuntime`] struct to block on async methods from the
//! [`crate::DependencyProvider`] using the `async-std` executor.
use futures::FutureExt;
use std::future::Future;

/// A trait to wrap an async runtime.
pub trait AsyncRuntime {
/// Runs the given future on the current thread, blocking until it is complete, and yielding its
/// resolved result.
fn block_on<F: Future>(&self, f: F) -> F::Output;
}

/// The simplest runtime possible evaluates and consumes the future, returning the resulting
/// output if the future is ready after the first call to [`Future::poll`]. If the future does
/// yield the runtime panics.
///
/// This assumes that the passed in future never yields. For purely blocking computations this
/// is the preferred method since it also incurs very little overhead and doesn't require the
/// inclusion of a heavy-weight runtime.
#[derive(Default, Copy, Clone)]
pub struct NowOrNeverRuntime;

impl AsyncRuntime for NowOrNeverRuntime {
fn block_on<F: Future>(&self, f: F) -> F::Output {
f.now_or_never()
.expect("can only use non-yielding futures with the NowOrNeverRuntime")
}
}

#[cfg(feature = "tokio")]
impl AsyncRuntime for tokio::runtime::Handle {
fn block_on<F: Future>(&self, f: F) -> F::Output {
self.block_on(f)
}
}

#[cfg(feature = "tokio")]
impl AsyncRuntime for tokio::runtime::Runtime {
fn block_on<F: Future>(&self, f: F) -> F::Output {
self.block_on(f)
}
}

#[cfg(feature = "async-std")]
pub struct AsyncStdRuntime;

Check failure on line 74 in src/runtime.rs

View workflow job for this annotation

GitHub Actions / Check intra-doc links

missing documentation for a struct

#[cfg(feature = "async-std")]
impl AsyncRuntime for AsyncStdRuntime {
fn block_on<F: Future>(&self, f: F) -> F::Output {
async_std::task::block_on(f)
}
}
10 changes: 5 additions & 5 deletions src/solver/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use crate::{
};
use bitvec::vec::BitVec;
use elsa::FrozenMap;
use event_listener::Event;
use std::{any::Any, cell::RefCell, collections::HashMap, marker::PhantomData, rc::Rc};
use tokio::sync::Notify;

/// Keeps a cache of previously computed and/or requested information about solvables and version
/// sets.
Expand All @@ -21,7 +21,7 @@ pub struct SolverCache<VS: VersionSet, N: PackageName, D: DependencyProvider<VS,
/// A mapping from package name to a list of candidates.
candidates: Arena<CandidatesId, Candidates>,
package_name_to_candidates: FrozenCopyMap<NameId, CandidatesId>,
package_name_to_candidates_in_flight: RefCell<HashMap<NameId, Rc<Notify>>>,
package_name_to_candidates_in_flight: RefCell<HashMap<NameId, Rc<Event>>>,

/// A mapping of `VersionSetId` to the candidates that match that set.
version_set_candidates: FrozenMap<VersionSetId, Vec<SolvableId>>,
Expand Down Expand Up @@ -99,7 +99,7 @@ impl<VS: VersionSet, N: PackageName, D: DependencyProvider<VS, N>> SolverCache<V
match in_flight_request {
Some(in_flight) => {
// Found an in-flight request, wait for that request to finish and return the computed result.
in_flight.notified().await;
in_flight.listen().await;
self.package_name_to_candidates
.get_copy(&package_name)
.expect("after waiting for a request the result should be available")
Expand All @@ -108,7 +108,7 @@ impl<VS: VersionSet, N: PackageName, D: DependencyProvider<VS, N>> SolverCache<V
// Prepare an in-flight notifier for other requests coming in.
self.package_name_to_candidates_in_flight
.borrow_mut()
.insert(package_name, Rc::new(Notify::new()));
.insert(package_name, Rc::new(Event::new()));

// Otherwise we have to get them from the DependencyProvider
let candidates = self
Expand Down Expand Up @@ -142,7 +142,7 @@ impl<VS: VersionSet, N: PackageName, D: DependencyProvider<VS, N>> SolverCache<V
.borrow_mut()
.remove(&package_name)
.expect("notifier should be there");
notifier.notify_waiters();
notifier.notify(usize::MAX);

candidates_id
}
Expand Down
Loading

0 comments on commit 05ce4f9

Please sign in to comment.