Skip to content

fix dependency resolution edge cases issues #3985

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

Merged
merged 2 commits into from
Jan 9, 2022

Conversation

tomerd
Copy link
Contributor

@tomerd tomerd commented Jan 7, 2022

motivation: dependency resolution could be incorrect in some edge cases when a pre-release requirement is specified, and not report the issue to the user

changes:

  • depedency resolustion should not assume 0.0.0 is the correct minimal requirment, instead it should use the specified version
  • fix dependency term computation which used incorrect version requirment for the the parent node
  • add validation at the end of package udpate API to make sure no missing packages exist after resolution, and fail the operation if such exists. this mirrors the behavior of package resolve API

@tomerd
Copy link
Contributor Author

tomerd commented Jan 7, 2022

@swift-ci please smoke test

@tomerd tomerd changed the title fix dependency resolution edge cases issues [wip] fix dependency resolution edge cases issues Jan 7, 2022
motivation: dependency resolution could be incorrect in some edge cases when a pre-release requirement is specified, and not report the issue to the user

changes:
* depedency resolustion should not assume 0.0.0 is the correct minimal requirment, instead it should use the specified version
* fix dependency term computation which used incorrect version requirment for the the parent node
* add validation at the end of package udpate API to make sure no missing packages exist after resolution, and fail the operation if such exists. this mirrors the behavior of package resolve API
@tomerd tomerd force-pushed the fix/dependency-resolution-edge-cases branch from 04ccc12 to 6c445b5 Compare January 7, 2022 06:58
// the package version requirement
terms.append(Term(node, .exact(version)))
// the dependency's version requirement
terms.append(Term(not: constraintNode, constraintRequirement))
Copy link
Contributor Author

@tomerd tomerd Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the heart of the change, there a couple of things going on here that seem wrong:

  1. requirement was used for two different purposes: to record the dependency version ( self.emittedIncompatibilities[constraintNode] = requirement.union(emittedIncompatibilities[constraintNode] ?? .empty)) but also as the resolved version for node (terms.append(Term(node, requirement))), which iiuc does not really make sense as it mixed the dependency (constraintNode) version with the package (node) version.

  2. requirement is looked up on the bounds computed for the dependencies using computeBounds with constraintNode as the key which is correct, however if not found, defaults to 0.0.0 < version(major+1, 0, 0). from what I can see, the defaulting is wrong in two ways: it does not work for pre-release versions since it strips the pre-release extensions (e.g. .exact("1.0.0-alpha.10") becomes .range(0.0.0 ..< 2.0.0) which is causing downstream issue as we are using range checks later in the graph resolution and "1.0.0-alpha.10" is not included in 0.0.0 ..< 2.0.0 based on our version inclusion rules (which is the bug I was chasing and got me here). maybe slightly worse, the default is based on version which is the version of node and has nothing to do with constraintNode so we end up recording (in self.emittedIncompatibilities) version ranges that are potentially wrong

the change suggested here is to always set the node term to the exact version we are computing for which works for pre-release and non-pre-release. it does however break a bunch of tests that recorded the 0.0.0 < version(major+1, 0, 0) behavior, but maybe that is okay since they are testing for wrong behavior?

@SDGGiesbrecht very interested in your 👀 on this, as you are probably more familiar with this code / logic than I am. I actually hope I am wrong and this is not as problematic as I think it is :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have much knowledge here, but I would not be surprised if some of the tests are conceptually wrong and just verifying the implemented behavior. So I wouldn't be concerned about having to change the tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking a closer look now. I will get back to you.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe both your observations are correct, @tomerd, and same goes for your solutions.

Whatever situation used to hit the defaults will now result in nothing recorded into emittedIncompatibilities. At first that concerned me, but after tracking where that leads, it feels safe, even if a little odd. The only two times emittedIncompatibilities is read from are:

  • To skip processing a node that has already been handled. Such nodes will be re‐processed now, but I don’t think that would be a problem.
  • To not trust a pin if we have already encountered anything odd. This too only prevents a shortcut, as the whole graph will be traversed either way, just on the one hand not necessarily trying the most probable solution first when obvious, or on the other hand not preloading alternatives when overestimating the probability that a solution will pan out.

I would be curious to know how the defaults were reachable in the first place, and if it could be done from the wild or only by mocked tests that bypass earlier checks.

I actually hope I am wrong and this is not as problematic as I think it is :D

Yes, that code appears to have been embarrassingly invalid. I think it snuck by everyone for so long because without the proper incompatibility here, it would just fail to record a short‐circuit opportunity and merely be slower, and because the nonsensical extra incompatibility would usually end up so far afield that it would be completely disjoint from the actual problem space.

guard !observabilityScope.errorsReported else { return nil }
guard !observabilityScope.errorsReported else {
return nil
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting only

return observabilityScope.trap { return try computePackageStateChanges(root: graphRoot, resolvedDependencies: updateResults, updateBranches: true, observabilityScope: observabilityScope) }
return observabilityScope.trap {
return try self.computePackageStateChanges(root: graphRoot, resolvedDependencies: updateResults, updateBranches: true, observabilityScope: observabilityScope)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting only

let missing = stillMissingPackages.map{ $0.description }
observabilityScope.emit(error: "exhausted attempts to resolve the dependencies graph, with '\(missing.joined(separator: "', '"))' unresolved.")
return nil
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added validation as package update could fail silently without this check

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great improvement too — the silent failures cause so much confusion.

@@ -1715,7 +1728,7 @@ extension Workspace {
let dependenciesToLoad = dependenciesRequired.map{ $0.createPackageRef() }.filter { !loadedManifests.keys.contains($0.identity) }
let dependenciesManifests = try temp_await { self.loadManagedManifests(for: dependenciesToLoad, observabilityScope: observabilityScope, completion: $0) }
dependenciesManifests.forEach { loadedManifests[$0.key] = $0.value }
return pair.item.dependenciesRequired(for: pair.key.productFilter).compactMap { dependency in
return dependenciesRequired.compactMap { dependency in
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small cleanup as dependenciesRequired is already defined above as pair.item.dependenciesRequired(for: pair.key.productFilter)

@@ -496,7 +496,7 @@ final class PubgrubTests: XCTestCase {
])
}

func testCycle1() {
func _testCycle1() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test crashes now on a precondition and will be fixed together with the other failing tests if this change is good

return try Incompatibility(terms, root: root, cause: .dependency(node: node))
}
}.joined())
}
Copy link
Contributor Author

@tomerd tomerd Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bit of style cleanup, uses flatMap instead of creating an array and then joining it. no functional change.

@tomerd
Copy link
Contributor Author

tomerd commented Jan 7, 2022

@swift-ci please smoke test

@tomerd
Copy link
Contributor Author

tomerd commented Jan 7, 2022

@swift-ci please test package compatibility

@tomerd tomerd changed the title [wip] fix dependency resolution edge cases issues fix dependency resolution edge cases issues Jan 7, 2022
@tomerd
Copy link
Contributor Author

tomerd commented Jan 8, 2022

@swift-ci please smoke test macOS

@tomerd tomerd added the ready Author believes the PR is ready to be merged & any feedback has been addressed label Jan 8, 2022
@tomerd tomerd merged commit 12b8e88 into swiftlang:main Jan 9, 2022
tomerd added a commit that referenced this pull request Jan 9, 2022
motivation: dependency resolution could be incorrect in some edge cases when a pre-release requirement is specified, and not report the issue to the user

changes:
* depedency resolustion should not assume 0.0.0 is the correct minimal requirment, instead it should use the specified version
* fix dependency term computation which used incorrect version requirement for the the parent node
* add validation at the end of package udpate API to make sure no missing packages exist after resolution, and fail the operation if such exists. this mirrors the behavior of package resolve API
* adjust tests
tomerd added a commit that referenced this pull request Jan 9, 2022
motivation: dependency resolution could be incorrect in some edge cases when a pre-release requirement is specified, and not report the issue to the user

changes:
* depedency resolustion should not assume 0.0.0 is the correct minimal requirment, instead it should use the specified version
* fix dependency term computation which used incorrect version requirement for the the parent node
* add validation at the end of package udpate API to make sure no missing packages exist after resolution, and fail the operation if such exists. this mirrors the behavior of package resolve API
* adjust tests
tomerd added a commit to tomerd/swift-package-manager that referenced this pull request Jan 12, 2022
motivation: improve dependency resolution performance

changes:
* add informational diagnostics on manifest cache hit/miss
* disable expensive optimization in pubgrub resolver that has no functional value given changes from  swiftlang#3985
* adjust resolver workspace delgate to be less noisy given the difference in resolution callbacks
tomerd added a commit to tomerd/swift-package-manager that referenced this pull request Jan 12, 2022
motivation: improve dependency resolution performance

changes:
* add informational diagnostics on manifest cache hit/miss
* disable expensive optimization in pubgrub resolver that has no functional value given changes from  swiftlang#3985
* adjust resolver workspace delgate to be less noisy given the difference in resolution callbacks
tomerd added a commit to tomerd/swift-package-manager that referenced this pull request Jan 12, 2022
motivation: improve dependency resolution performance

changes:
* add informational diagnostics on manifest cache hit/miss
* disable expensive optimization in pubgrub resolver that has no functional value given changes from  swiftlang#3985
* adjust resolver workspace delgate to be less noisy given the difference in resolution callbacks
tomerd added a commit that referenced this pull request Jan 13, 2022
motivation: improve dependency resolution performance

changes:
* add informational diagnostics on manifest cache hit/miss
* disable expensive optimization in pubgrub resolver that has no functional value given changes from  #3985
* adjust resolver workspace delgate to be less noisy given the difference in resolution callbacks
tomerd added a commit to tomerd/swift-package-manager that referenced this pull request Jan 13, 2022
motivation: improve dependency resolution performance

changes:
* add informational diagnostics on manifest cache hit/miss
* disable expensive optimization in pubgrub resolver that has no functional value given changes from  swiftlang#3985
* adjust resolver workspace delgate to be less noisy given the difference in resolution callbacks
tomerd added a commit that referenced this pull request Jan 13, 2022
motivation: improve dependency resolution performance

changes:
* add informational diagnostics on manifest cache hit/miss
* disable expensive optimization in pubgrub resolver that has no functional value given changes from  #3985
* adjust resolver workspace delgate to be less noisy given the difference in resolution callbacks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ready Author believes the PR is ready to be merged & any feedback has been addressed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants