Skip to content
This repository has been archived by the owner on Sep 14, 2021. It is now read-only.

Resolve versions using aether, and uncouple existing Resolver from code base. #45

Merged
merged 16 commits into from
Jul 28, 2017

Conversation

petroseskinder
Copy link
Member

@petroseskinder petroseskinder commented Jul 19, 2017

Context

This is a part of an ongoing effort to isolate and then, replace the existing Resolver with Aether.

Alongside actually resolving artifacts, the existing Resolver also:
(1) builds Artifacts from coordinates,
(2) parses versions from version specifications and decides on a version to use.

Both are orthogonal to the role of the Resolver, and make the Resolver unnecessarily coupled to various classes. This PR isolates (and also corrects) this functionality. Note: as this PR is not intended to lead to regressions, it should be merged to master.

Detailed Design

Building Artifacts
Currently, artifacts are built using Resolver.getArtifact(). The Resolver also defines a class of exceptions for when this fails. These are both orthogonal to the role of the Resolver. However, since the function/exception is called from the DefaultModelResolver and the GenerateWorkspaceOptions, it unnecessarily couples the Resolver to these two classes. I have removed these and placed them in the ArtifactBuilder.

Version Resolution
Version resolution selects a version given a maven coordinate and a version specification. This is also called from the DefaultModelResolver and GenerateWorkspaceOptions.

I have also elected to separate that and also provide a corrected implementation. The current implementation of resolving a version from a version spec is adhoc and idiosyncratic. This CL isolates this functionality, and provides a canonical/correct version resolution mechanism.

Maven's version resolution has two components:
(1) it selects the pinned version from both hard and soft pins, e.g. "[3.0]" and "3.0" should evaluate to "3.0"
(2) it selects the highest available version for version ranges e.g. "[3.0, )" should select 3.9 if its highest available

This functionality is easily implemented through aether using a version range request.

Unit Tests
Tests are implemented for when there is invalid behavior, as well as for pinned version ranges, i.e. "3.2". However, due to limitations of Mockito and Aether (mockito cannot mock final classes), tests
are not included for correct behavior of version ranges. In particular, there are no tests for ensuring that the highest version is selected.

Reviewer: @kchodorow
Related Issues #16

@bazel-io
Copy link
Member

Can one of the admins verify this patch?

@petroseskinder petroseskinder changed the title [Refactor Resolver] Resolve versions using aether, and uncouple existing Resolver from code base. [Refactor] Resolve versions using aether, and uncouple existing Resolver from code base. Jul 19, 2017
@petroseskinder petroseskinder changed the title [Refactor] Resolve versions using aether, and uncouple existing Resolver from code base. Resolve versions using aether, and uncouple existing Resolver from code base. Jul 19, 2017
@petroseskinder petroseskinder force-pushed the decouple branch 3 times, most recently from 331fafd to a835e37 Compare July 19, 2017 13:00
if (isInvalidRangeResult(rangeResult)) {
throw new InvalidArtifactCoordinateException(messageForInvalidArtifact(groupId, artifactId, versionSpec));
}
return rangeResult.getHighestVersion().toString();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be getRecommendedVersion() (if not null) or getSelectedVersion(artifact).

Copy link
Member Author

Choose a reason for hiding this comment

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

VersionRangeResult actually does not support either method. [source]

Those methods are associated with org.apache.maven.artifact.versioning.VersionRange. They are independent of aether.

Let me play around with this.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is still unresolved.

try {
rangeResult = repositorySystem.resolveVersionRange(repositorySystemSession, rangeRequest);
} catch (VersionRangeResolutionException e) {
throw new InvalidArtifactCoordinateException(messageForInvalidArtifact(groupId, artifactId, versionSpec));
Copy link
Contributor

Choose a reason for hiding this comment

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

Add e.getMessage() to the error.

Copy link
Member Author

Choose a reason for hiding this comment

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

done.


VersionRangeResult rangeResult;
try {
rangeResult = repositorySystem.resolveVersionRange(repositorySystemSession, rangeRequest);
Copy link
Contributor

Choose a reason for hiding this comment

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

If I say 3.0, no one conflicts with 3.0, 3.0 exists... we should just use 3.0, not make a request to Maven Central. How about using some of the existing "find a version" code and, if that can't find anything, we do a network request?

Copy link
Member Author

Choose a reason for hiding this comment

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

If I say 3.0, no one conflicts with 3.0, 3.0 exists...

How do we determine that 3.0 exists without checking the Maven Central?

Suppose there is some artifact a:b:3.12 we want. Now, suppose we create a mirror of maven central. This a:b:3.12 exists within Maven Central. However, there is no a:b:3.12 in the mirror, but there is a a:b:3.13. Using the the above heuristic in lieu of a network request, we would select a:b:3.12. Later, when we try to fetch the artifact, we would find that there is no a:b:3.12, and then skip resolving its transitive dependencies.

This is incorrect behavior. "3.10" is a soft pin, rather than a hard pin. In the situation, there is no 3.10, it should still be able to use 3.11, 3.12, 3.13, etc.

How about using some of the existing "find a version" code and, if that can't find anything, we do a network request?

This is very much a tradeoff between correctness and performance. IMO, until we can match maven/aether one to one, I think we should err on the side of over correctness. We can freely tune things afterwards.

@@ -14,13 +14,25 @@

package com.google.devtools.build.workspace.maven;

import static com.google.devtools.build.workspace.maven.ArtifactBuilder.InvalidArtifactCoordinateException;
import static com.google.devtools.build.workspace.maven.ArtifactBuilder.fromMavenDependency;
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't use static imports for methods unless it's a test.

Copy link
Member Author

Choose a reason for hiding this comment

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

done.

private final RepositorySystem repositorySystem;
private final RepositorySystemSession repositorySystemSession;

private VersionResolver(RepositorySystem repositorySystem,
Copy link
Contributor

Choose a reason for hiding this comment

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

Make AetherUtils non-static, pass it in here, and add helper methods when you need to contact aether (aetherUtils.requestVersionRange()). Then you can customize repos & test.

Copy link
Member Author

@petroseskinder petroseskinder Jul 19, 2017

Choose a reason for hiding this comment

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

I like this. It's basically a facade around aether. I made the necessary changes. I have also made a few hacks to ensure it works with the Resolver and DefaultMoldelResolver, i.e. defaultResolver

@@ -0,0 +1,47 @@
package com.google.devtools.build.workspace.maven;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: add licenses at the tops of new files.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

@@ -1,7 +1,16 @@
package com.google.devtools.build.workspace.maven;


import static com.google.devtools.build.workspace.maven.AetherUtils.mavenCentralRepository;
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove.

Copy link
Member Author

Choose a reason for hiding this comment

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

done.

Copy link
Contributor

@kchodorow kchodorow left a comment

Choose a reason for hiding this comment

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

Bah, I always mix up the Aether and Maven versions of classes.

try {
Mockito.when(system.resolveVersionRange(anySession(), anyRangeRequest()))
.thenThrow(new VersionRangeResolutionException(any()));
Aether aether = Aether.builder().systemSession(anySession(), system).build();
Copy link
Contributor

Choose a reason for hiding this comment

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

The point is to do mock(Aether.class) here.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

public void softPinnedVersions() {
try {
String version =
defaultResolver().resolveVersion("something", "something", "3.4");
Copy link
Contributor

Choose a reason for hiding this comment

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

Unit tests should not contact Maven.

Copy link
Member Author

@petroseskinder petroseskinder Jul 24, 2017

Choose a reason for hiding this comment

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

I figured as such. This was more a desperate attempt to at least document the intended behavior.

I am finding testing the actual version resolution, not the failure, to be somewhat prohibitory. Two reasons why. (1) VersionRangeResult can't be mocked since it's a final class. (2) VersionRangeResult is very tightly coupled with other things within Aether, so creating using a constructor is also meh.

What are your thoughts on the following options:

  1. Using PowerMock. This lets us mock final classes. It is a bit of a pain to import this into the code base, but very doable.
  2. Modifying the method for our Aether class to return the highest version. I'm not the biggest fan of this.
  3. Just not test the behavior. "You can't fail unit tests, if you don't have any" - some authoritative source

Copy link
Contributor

Choose a reason for hiding this comment

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

I am finding testing the actual version resolution, not the failure, to be somewhat prohibitory.

Keep in mind that we don't want to test the actual version resolution. That's Aether's business. We just want to be able to test how your code behaves.

Two reasons why. (1) VersionRangeResult can't be mocked since it's a final class. (2) VersionRangeResult is very tightly coupled with other things within Aether, so creating using a constructor is also meh.

Have it return a Version. Or a list of versions. You don't need a response, here, just the version.

Just not test the behavior. "You can't fail unit tests, if you don't have any" - some authoritative source

ಠ_ಠ

Copy link
Member Author

Choose a reason for hiding this comment

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

Keep in mind that we don't want to test the actual version resolution. That's Aether's business. We just want to be able to test how your code behaves.

I am not we need to add any further tests or whether we can need the soft pin tests. For full code coverage, I believe we simply need to cover the following cases:

  • given a null result, it throws an exception
  • given a versionrangeresolution exception, it throws an exception
  • given neither, it doesn't throw an exception and returns something.

Have it return a Version. Or a list of versions. You don't need a response, here, just the version.

Again this is a tradeoff. Finer grained tests will mean more responsibility to mimic aether/maven's behavior.

I assume we are trying to match with how aether or maven resolves versions. If so, returning a list of versions means we will need to include code to decide which version to pick. Right now, we can simply write versionRangeResult.getHighestVersion() and call it a day. However, if we return the list, then we will need to account for any weird behavior aether/maven may have.

For example, Aether internally uses VersionRangeResult.getHighestVersion() for resolving all versions. For a soft pin, I have found that this returns the soft pinned value provided it exists. This is regardless of whether the soft pinned value is the highest version or not. The behavior when that soft pin does not exist is undocumented. I am guessing it will be the next closest version, but I am not sure on that front.

Copy link
Contributor

Choose a reason for hiding this comment

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

I assume we are trying to match with how aether or maven resolves versions. If so, returning a list of versions means we will need to include code to decide which version to pick.

I was basing this on VersionRangeResult's API. It doesn't have any "get the right version" method, so your code will have to handle it regardless.

The behavior when that soft pin does not exist is undocumented.

You can try this out pretty easily and see what Aether does. I wrote up a quick sample program to do version requests for different version constraints on Guava and I have bad news for you: it seem that Aether returns the soft pin version, even if it doesn't exist. I'm not sure what you're supposed to do to check if it's valid.

Copy link
Member Author

Choose a reason for hiding this comment

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

i'll look into it and determine a solution.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you for pointing this out. It's quite unfortunate aether is not as complete as hoped for. That said, I have been playing around with a few ideas. For any given artifact a:b:version-spec, we can make a version range request for a:b:[,) and aether will return a list of all potential versions of that artifact.

Here is one approach we could take. Whenever resolveVersion is called, we can simply make the request for [,), i.e. the unbounded request. With all potential versions, we could then manually select the version from the available versions. This would push the brunt of selecting versions on us. However, it is much more reliable than what I have right now.

Copy link
Member Author

@petroseskinder petroseskinder Jul 27, 2017

Choose a reason for hiding this comment

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

Thank you for pointing that out. Disappointing news, indeed.

I'm not sure what you're supposed to do to check if it's valid.

I was playing around with it, and found that if you request an unbounded version [,), it returns all possible versions for an artifact as a list (ascending from oldest to latest).

For this soft pin issue, here is a potential algorithm.

def resolve_version(group, artifact, version):
    if is_soft_pin(version): 
        allVersions = fetch_all_versions(group, artifact, version)
        return select_version(version, all_versions)
    # what we have implemented
    return resolve_version_range(group, artifact, version) 

def select_version(version, all_versions):
    if version in all_versions:
        return version
    # choose one from {fail, find nearest alternative, select highest available}

This issue only comes up with soft pins, so for version ranges, we can default to the implementation in this CL.

As for how we approach it if it's not there, I have a few ideas but let's address that in another pull request. The best approach imo would be to select the nearest valid version.

Copy link
Member Author

Choose a reason for hiding this comment

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

Here is a simple implementation of what I commented on above. I'll make a separate pull request after this is merged.

@googlebot
Copy link

So there's good news and bad news.

👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there.

😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that they're okay with their commits being contributed to this project. Please have them confirm that here in the pull request.

Note to project maintainer: This is a terminal state, meaning the cla/google commit status will not change from this state. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

petroseskinder added 7 commits July 27, 2017 09:58
This is a part of my effort to replace the Resolver, with Aether.
Currently, artifacts are built using Resolver.getArtifact().
In addition, the Resolver defines a class of exceptions that is
associated with creating a new DefaultArtifact.
This functionality can and should be removed from the Resolver.

After this change, the resolver is only called from within
(1) itself, (2) its tests and (3) the defaultmodelresolver.
This is a part of an ongoing effort to isolate and then replace
the existing Resolver with Aether.

Version resolution is the only remaining part of the Resolver
that is used by any other class, DefaultModelResolver. With this
removed the Resolver is currently only used by itself, its tests and
GenerateWorkspace.

The current implementation of resolving version given a version spec
is adhoc and idiosyncratic. This CL isolates this functionality, and
provides a canonical/correct version resolution mechanism.

Correct version resolution has two components. (1) Pins both hard and
soft, e.g. "[3.0]" and "3.0", should select the pinned version, e.g.
"3.0", and (2) version ranges should select the highest available
version.

This functionality is easily accomplished through aether.
It involves the construction of a version range request,
followed by a call to aether.

Tests are implemented for when there is invalid behavior, as well
as for pinned version ranges, i.e. "3.2". However, due to limitations
of Mockito and Aether (mockito annot mock final classes), tests
are not included for correct behavior of version ranges.
All classes make create and make requests through the
Aether facade. This convenience class allows us to not have
to pass the RepositorySystemSession, RepositorySystem, and
RemoteRepository's in multiple classes.

There is one major TODO. The older classes are not constructing
this object. They are just using default settings.
@petroseskinder petroseskinder force-pushed the decouple branch 2 times, most recently from 287f7ad to 71ab932 Compare July 27, 2017 14:00
@googlebot
Copy link

CLAs look good, thanks!

@petroseskinder petroseskinder force-pushed the decouple branch 2 times, most recently from 3563fca to 92fc31c Compare July 27, 2017 14:18
@@ -74,33 +73,44 @@ public void failsOnInvalidVersionRange() {
* Asserts that given a soft pinned version specification, it selects that version,
* and does not get the highest version. "3.4" is an example of a soft pinned version specification.
*/
//TODO(petros): implicitly using Maven central for resolving version. This is sketchy.
@Test
public void softPinnedVersions() {
Copy link
Member Author

Choose a reason for hiding this comment

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

Here are the changed unit tests. Tried rebasing on your last pull request and that wiped out history.

I simply assert that given a list of versions from aether, we select the highest version (i.e last element in list). I included a test for soft pins, where aether simply returns a single version.

Copy link
Contributor

@kchodorow kchodorow left a comment

Choose a reason for hiding this comment

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

Gotta love GitHub code review. Sigh.

If this ends up being too slow, I think a good optimization would be to only start looking up valid versions if getting the artifact fails. However, I'm willing to let that go, for now.

resolver.resolveVersion("something", "something", "1.0");
assertThat(version).isEqualTo("1.0");

} catch (
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove the try/catch.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

Aether aether = Mockito.mock(Aether.class);

try {
// Using `anyRangeResult()` will ensure that rangeResult.highestVersion() == null.
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

that's quite neat, actually.

@kchodorow
Copy link
Contributor

Jenkins, test this please.

@kchodorow
Copy link
Contributor

kchodorow commented Jul 27, 2017 via email

petroseskinder added 2 commits July 27, 2017 16:15
Previously version resolution was determined locally, without
consideration of the maven central repository. This change removes
any hacky calls to maven central.
All version resolution is now conducted by the VersionResolver
using aether.
@petroseskinder
Copy link
Member Author

Jenkins, test this please.

@petroseskinder
Copy link
Member Author

petroseskinder commented Jul 27, 2017

I resolved the issue. I removed the Resolver.resolveVersion function from the codebase and made sure all version resolution was conducted by the VersionResolver I introduce here. Also used it to regenerate the generate_workspace.bzl file and things build with no issue.

I also resolved the issue with the soft pins. If you want I can add the commit to this PR or another one. The relevant changes can be found here. It was a simple enough solution. Transform "3.0" to "[3.0, )" and return the first element in the list of potential versions.

@petroseskinder
Copy link
Member Author

petroseskinder commented Jul 28, 2017

@kchodorow can you retract the approval, and rereview? I decided on simply including the soft pin fix in this pull request. Since the change was simple and very closely related to this, I felt it was best to keep it in the same PR. Here is the relevant commit message:

Resolve soft pinned version to nearest version if it does not exist.

Currently, even if a soft pinned version, e.g. "3.0", does not
exist aether returns that soft pinned version. Instead, it should
provide another valid version. This change solves this issue by
transforming the soft pin into a version range, and making the
aether request using that version range.

For example, instead of requesting for "3.0", we transform it to "[3.0,)"
providing us all versions between 3.0 and the latest version.
If the version exists, we simply take it. If it doesn't, then
we select the next closest valid version.

If you prefer I do this in a separate pull request, I can also do that.

@petroseskinder petroseskinder force-pushed the decouple branch 3 times, most recently from dbf5bdd to a1bf4c0 Compare July 28, 2017 14:30
petroseskinder added 2 commits July 28, 2017 10:48
Currently, even if a soft pinned version, e.g. "3.0", does not
exist aether returns that soft pinned version. Instead, it should
provide another valid version. This change solves this issue by
transforming the soft pin into a version range, and making the
aether request using that version range.

For example, instead of requesting for "3.0", we transform it to "[3.0,)"
providing us all versions between 3.0 and the latest version.
If the version exists, we simply take it. If it doesn't, then
we select the next closest valid version.
@petroseskinder petroseskinder force-pushed the decouple branch 2 times, most recently from 78f1920 to 55289fc Compare July 28, 2017 14:51
@googlebot
Copy link

So there's good news and bad news.

👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there.

😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that they're okay with their commits being contributed to this project. Please have them confirm that here in the pull request.

Note to project maintainer: This is a terminal state, meaning the cla/google commit status will not change from this state. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

@googlebot
Copy link

CLAs look good, thanks!

Copy link
Contributor

@kchodorow kchodorow left a comment

Choose a reason for hiding this comment

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

Jenkins, test this please.

@kchodorow
Copy link
Contributor

Interesting, I guess Jenkins doesn't see the code review comment?

Test this please.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants