-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Concurrent metadata fetching #22
Concurrent metadata fetching #22
Conversation
Some remarks and questions:
|
I'll have a good look at this tomorrow! |
I think this currently slightly misses the point. The main goal that I would like to achieve is as follows: when solving incrementally, we know after each partial solve that there are 1 or more candidates that we are going to need more information from. Currently we fetch this information sequentially but especially when network requests are involved it makes sense to request this information concurrently. Another approach could be to add a function to the dependency provider that provides the result for multiple solvables. Implementers can choose to make this concurrent or not. Although this callback api is nice, the same could be achieved by passing in a closure that should be called. Internally the closure can do whatever. I also wonder if an async runtime is needed, couldnt you achieve the same result with a normal blocking channel? Since the solver loop is still blocking anyway. |
Yeah I agree a blocking channel can also be used in the asynchronous context because 'send' is non-blocking. Because you are using futures and smol here anyway I see no reason not to make the functions in the DependencyProvider asynchronous, you are bringing in a runtime anyways. However, avoiding this is also great but then I think a channel would just work :) |
For a bit of context, my proposed async / await approach is meant to keep the sequential structure of the code, while enabling concurrency. Await is used on the solver side whenever there is a chance of having to wait on the dependency provider. This, combined with At least from a functionality perspective, this seems to cover the use case you mention. Or am I missing something?
A normal blocking channel was my first prototype, but my impression was that it would require significant changes to the solver code (maybe I'm wrong, though). While the solver loop is indeed blocking, in some cases it has to pause execution of Regarding using an async runtime... It's kind of inevitable once you go down the async / await route. However, we could create our own barebones runtime with only the features we need (i.e. single-threaded and supporting async one-shot channels), instead of pulling a more heavyweight dependency. I had to create a custom runtime for my last project and found it surprisingly straightforward (once you understand that async / await is just syntactic sugar), so we should be able to pull it off if we want to go down that path.
I'm not totally sure about that. I'd say the solver shouldn't care about which runtime is used by the dependency provider. If we allow the dependency provider to return futures, that means they must be compatible with the solver's runtime. I consider the fact that the solver uses async / await under the hood to be an implementation detail and would rather avoid leaking that information into its API. Additionally, sharing a single async runtime between the solver and the dependency provider would force the solver to use a full-blown async runtime such as tokio to work well, which sounds like overkill in one of our main use cases (Conda packages) where there is no IO / waiting during solving. |
But if the solver defines the If you bring in a Runtime anyway I don't see a reason not to make the DependencyProvider async. Granted it would be best if the |
We had a long discussion offline. I didnt properly read the code. I think this does fix the goals. We decided to keep the (current) callback approach instead of making the @aochagavia will clean up the code and bring this in a mergable state! |
125cfdc
to
fe79c92
Compare
// This prevents concurrent requests to add clauses for a solvable from racing. Only the | ||
// first request for that solvable will go through, and others will wait till it | ||
// completes. | ||
let mut clauses_added = mutex.lock().await; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this await (and others below) is inside a loop, we could make concurrent metadata fetching even more aggressive by writing this loop in such a way that it processes its items concurrently... I didn't do it, just to keep things simple, but let me know if that's desirable after all!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do think that would be quite nice! But maybe we can do that in another PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems good to me!
src/solver/mod.rs
Outdated
&mut output.clauses_to_watch, | ||
dependency_name, | ||
) | ||
.await?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, it might pay off to handle the iterations of this loop concurrently.
src/solver/mod.rs
Outdated
&mut output.clauses_to_watch, | ||
dependency_name, | ||
) | ||
.await?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, it might pay off to handle the iterations of this loop concurrently.
tests/solver.rs
Outdated
.build() | ||
.unwrap(); | ||
|
||
provider.runtime = Some(executor); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code means that all tests using solve_snapshot
are now also using the async provider behind the scenes. If that's undesirable, let me know!
I have pushed a new wip commit. There are still a few open points, some in the form of comments, some written below:
|
Mm, adding tokio would add a TON of dependencies. I always thought that smol is also production ready?
Lets do it in another PR then. :) |
Tokio's dependencies can be kept reasonably low by only enabling the features you need, so I don't think there'd be much of a difference between it and smol. Regarding smol being production-ready, I have the feeling that tokio has much stronger backing and is more battle-tested, but I'm not opposed to giving smol a try. A nice benefit is that we get to keep the current code without Rcs, which is nice! I'll do a last polishing round directly using smol's underlying crates, to make sure we don't pull in more dependencies than strictly needed. |
I would also be happy to use tokio if you feel that doesnt add to much weight. |
fe79c92
to
ce12e32
Compare
If you end up adding Tokio you |
I ended up using smol's |
I tried updating To integrate the changes I implemented a two-stage approach. We have an object fn get_candidates(&self, name: NameId, sender: OneShotSender<Option<Candidates>>) {
let provider = self.inner.clone();
tokio::spawn(async move {
let candidates = provider.get_candidates(name).await;
let _ = sender.send(candidates);
});
} Here the inner I think this is what you intended with your design? The provider is an However, I ran into trouble when I came to the The return values of the fn pool(&self) -> &Pool<VS, N>; This limits me from turning the internal The only way around this that I can think of is to make the Another approach would be to make the WDYT @aochagavia ? |
ce12e32
to
fe6a7fa
Compare
We discussed this on Discord and came up with the following solution:
I just pushed the changes and will be checking if this update is more rip-friendly! |
fe6a7fa
to
6e26432
Compare
6e26432
to
0938551
Compare
96c8b60
to
a6d2138
Compare
So we just found out that for rattler (you can check-out this branch conda/rattler#504) we make use of functions in the |
Is that something you could solve by blocking on the futures, because you know they'll always return |
If thats the case (always ready) we can use .now_or_never() |
Right, that would work until someone changes the API but we could make that clear in the expect I suppose :) |
Im also not entirely sure that the assumption holds. This function may be called while the not all candidates for other packages have been loaded. But lets try to find out. |
Looking at the code it should be trivial to make sort_candidates async as well. I think this is the proper solution because that allows you to freely call the methods on the SolverCache and await them without issues. |
Okay let me give it a go later! |
Closing this in favor of #29 (from benchmarks that one is slightly faster) |
Closes #3