-
-
Notifications
You must be signed in to change notification settings - Fork 238
Reimplement dependency resolution as a recursive process #1513
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
Conversation
|
Thanks for your pull request, @s-ludwig! |
|
Are there also changes in performance by this? |
| b depends on ~>1.0.2 | ||
| Conflicting dependencies to gitcompatibledubpackage: | ||
| b >=0.0.0 @/home/runner/dub/test/issue1037-better-dependency-messages/b depends on gitcompatibledubpackage ~>1.0.2 | ||
| issue1037-better-dependency-messages ~master depends on gitcompatibledubpackage 1.0.1 |
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.
love the new details in this message
source/dub/dependencyresolver.d
Outdated
| { | ||
| auto idx = indexOf(p, ':'); | ||
| if (idx < 0) return ""; | ||
| return p[idx+1 .. $]; |
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.
you could make this a one-liner using findSplit: return p.findSplit(":")[2];.
imo it's actually a lit bit more readable (and it would be even more readable if findSplit returned a tuple with start, match and end members) but it's not really required. Actually also think the performance might be a little bit worse.
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.
it's actually a lit bit more readable (and it would be even more readable if findSplit returned a tuple with start, match and end members)
Yeah I tried this once
tl;dr: it was reverted due to a regression in CTFE usage of tuples.
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.
That would cause a range error for package names without a sub package part, wouldn't it?
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.
no findSplit returns a tuple, it is always exactly 3 long (pre, match, post) no matter the input
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.
Ah, I actually missed the "split" part.
WebFreak001
left a comment
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'm not really a fan of the basePackage and subPackage names and using auto everywhere where they are used. Kind of makes me think it's some member function of a struct. I think if we wrote string as type and/or renamed them to basePackageName and subPackageName it would be a bit more clearer.
I didn't really look at the old code too much when reviewing so I don't know if any functionality is lost (I saw that some debug logs were removed though).
Do we have test cases that we can include that were previously breaking dependency resolution but don't anymore?
| bool[TreeNode] visited; | ||
| // Leave the possibility to opt-out from the loop limit | ||
| import std.process : environment; | ||
| bool no_loop_limit = environment.get("DUB_NO_RESOLVE_LIMIT") !is null; |
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 new flag should be documented somewhere
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.
It's not new, but yeah, it should still be documented.
source/dub/dependencyresolver.d
Outdated
| import std.bitmanip : BitArray; | ||
|
|
||
| configs[pidx] = config; | ||
| bool[string] visited; // key = sub package or empty string |
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.
while this is being rewritten, maybe we could consider making this a normal array (or some container like RedBlackTree) and add helper functions with readable names such as isVisited(string name) and visit(string name) that check if it is in the container and add it respectively.
source/dub/dependencyresolver.d
Outdated
|
|
||
| configs[pidx] = config; | ||
| bool[string] visited; // key = sub package or empty string | ||
| CONFIG[] configs; |
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.
Every time I look at this code again I get confused what CONFIG is supposed to be, being in PackageConfigs. Can we add a little bit of documentation here and to PackageConfigs describing what they are used for?
source/dub/dependencyresolver.d
Outdated
| package_names[pidx] = basepack; | ||
| // build up the dependency graph, eliminating as many configurations/ | ||
| // versions as possible | ||
| PackageConfigs[string] packs; |
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.
should we maybe call this cache or visited instead to make more clear what packs are stored in this variable?
source/dub/dependencyresolver.d
Outdated
|
|
||
| foreach (dep; dependencies) { | ||
| // lazily load all dependency configurations | ||
| auto depbase = basePackage(dep.pack); |
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.
why aren't we using UFCS here like in the other places?
source/dub/dependencyresolver.d
Outdated
| try constrainDependencies(n, dependencies, 0, packs, loop_counter); | ||
| catch (ResolveException e) { | ||
| // add any relevant information for this stack frame | ||
| e.chain(n, dependencies); |
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.
+1, I like this way of adding dependencies to the error
source/dub/dependencyresolver.d
Outdated
| if (first_err) throw first_err; | ||
|
|
||
| // should have thrown in constrainRec before reaching this | ||
| assert(false, "Got no configuration for dependency!?"); |
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.
just in case this actually gets triggered, including which package triggered this would probably be a good idea.
source/dub/dependencyresolver.d
Outdated
|
|
||
| foreach (v; config.allConfigs) | ||
| findConfigsRec(TreeNode(ch.pack, v), parent_unique && config.allConfigs.length == 1); | ||
| private void constrainRec(TreeNode n, ref PackageConfigs[string] packs, ref long loop_counter) |
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.
Why is there constrainRec and constrainDependencies? I would have guessed constrainRec stands for constrainRecursive by the name but that doesn't really make sense with constrainDependencies being there
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.
constrainDependencies calls back into constrainRec, but also calls itself recursively. Probably makes sense to just drop the Rec suffix.
source/dub/dependencyresolver.d
Outdated
|
|
||
| auto dep = &dependencies[depidx]; | ||
| auto depbase = dep.pack.basePackage; | ||
| auto dp = &packs[depbase]; |
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.
these variable names tho
source/dub/dependencyresolver.d
Outdated
| if (configs[i].allConfigs.length) | ||
| cs[i] = p~" "~configs[i].allConfigs[config_indices[i]].to!string~(i >= 0 && i >= conflict_index ? " (C)" : ""); | ||
| else cs[i] = p ~ " [no config]"; | ||
| private void constrainDependencies(TreeNode n, TreeNodes[] dependencies, size_t depidx, ref PackageConfigs[string] packs, ref long loop_counter) |
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.
is the n argument even used in this function?
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.
Now it's used in the assertion message at the bottom of the function ;-)
|
travis is failing because of the absolute paths in the test file, maybe we should use relative paths relative to the package root directory instead? |
|
By chance, does this pull request has any influence on optional dependencies? (#1148) |
source/dub/dependencyresolver.d
Outdated
| can be defined in terms of a version range. | ||
| */ | ||
| class DependencyResolver(CONFIGS, CONFIG) { | ||
| /** Encasulates a list of outgoing edges in the dependency graph. |
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.
s/Encasulates/Encapsulates/
It definitely improves performance for complex graphs (which is the whole point of this of course). For simple graphs it is probably a bit slower due to the heavy memory allocation and copying, but it doesn't matter because we are talking about milliseconds or less then. Most of the time is currently spent in superfluous queries to the registry anyway, since the cache mechanism stopped working at some point. |
It shouldn't affect that issue. A proper solution for that requires on a higher level approach, one that comes after the configurations have been determined and the project is about to be built. |
I just fixed the test for now, as outputting relative paths would be more complicated (relative to the CWD would be easier than relative to the root directory). |
The problem is that the known test cases depend on a rather large number of public packages. Capturing them in a local test case would be quite involved and just sourcing them from the registry is error prone and slows down the CI tests unnecessarily. |
|
but if we do have one public test case where it happens, can we at least try locally if the issue happened before and if it is fixed? |
eaed55e to
0af430c
Compare
#1300 does get fixed by this and is the only public test case that I found that still fails on master. |
|
I just noticed that not all sources end up in the conflict error message, because the stack doesn't generally encompass the whole dependency graph, so an additional map will be required to fix that. |
|
Refactored the code and fixed the log issue. |
WebFreak001
left a comment
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.
Looking good now. Maybe a unittest using conflicting subpackages would make sense too
|
You are right, those were definitely easy enough to add. I'll rebase/clean up once the CI is green again. |
bcd8838 to
6307538
Compare
|
Not sure what those CI failures are about. Travis looks fine and I can't spot the failure for Jenkins. |
We're currently migrating off from Jenkins to Buildkite due to these spurious failures. You can ignore them safely though we should probably see what Buildkite is telling us before merging this. |
98e733e to
fbfd5dc
Compare
|
I tried this PR. While it seems to solve an issue for me, I found another issue. Maybe this issue is completely unrelated to this pr or maybe it is somehow affected. I have a small app with dependency to vibe-d. I have to execute "dub" 4 times until all dependencies are resolved right. The first 3 times there are following errors: First try: Second try: Third try: My dub packages folder is completely empty while executing dub the first time. My assumption was, if a dependency 1 (vibe.d) depends on 2 (vibe-d:core) which has a dependency to 3 (libasync) then this will fail if dependency 2 and 3 aren't downloaded yet. |
No, we're migrating from Jenkins cause it's using too many server-side resources. |
|
Still need to look into the issue @andre2007 mentioned, but I'll try to do it this week. |
|
@s-ludwig I found the root cause for my issue. It is not related to your pr. |
|
It would be really nice to have this in the next release. I tested this branch on some of my projects and it worked good :) If it's needed, I can test it with more projects. |
|
Okay, good to hear, was just going to look into it myself. I guess someone can pull the trigger then. |
|
no changelog entry? |
|
Of course... forgot about that. |
|
Added now. |
Although this is rather expensive in terms of memory allocation and stack usage, it does a much better job at reducing the number of combinations that have to be tested.
57b01f4 to
98161aa
Compare
|
tests pass, reviews agree, feel like it's time to merge |
The new approach minimizes the dependency version graph while descending, making it much less likely to run into pathological cases. It also is a lot easier to understand and reason about than the previous iterative approach.
The downside is dynamic and stack memory use, where the latter could be optimized using a COW approach. If stack overflow should turn out to become an issue in large dependency graphs, the algorithm could be converted to use a heap based stack.
Fixes #1300.
Fixes #1508.
Looks related, but appears to be fixed already: #1484
#1001 appears to have been fixed already (gives a proper error message)
#1345 also appears to have been fixed, this time externally by fixing the faulty dependencies