Skip to content
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

pex throws a resolution error when using cache, succeeds otherwise #178

Closed
kwlzn opened this issue Nov 13, 2015 · 14 comments · Fixed by #365
Closed

pex throws a resolution error when using cache, succeeds otherwise #178

kwlzn opened this issue Nov 13, 2015 · 14 comments · Fixed by #365
Labels

Comments

@kwlzn
Copy link
Contributor

kwlzn commented Nov 13, 2015

when passing two requirements of the form (unpinned, pinned) for the same package (e.g. 'thrift thrift==0.9.1'), pex has different behavior in resolution depending on whether or not the cache is used. this also manifests in consumers of pex (i.e. pants) when dependencies land in a similar order.

repro:

[illuminati build]$ pex --version
pex 1.0.3
[illuminati build]$ rm -rf ~/.pex
[illuminati build]$ pex thrift thrift==0.9.1 -o test.pex
[illuminati build]$ pex thrift thrift==0.9.1 -o test.pex
Could not satisfy all requirements for thrift:
    thrift, thrift==0.9.1

this also repros under 1.1.0 and for different packages:

[illuminati build]$ pex --version
pex 1.1.0
[illuminati build]$ rm -rf ~/.pex
[illuminati build]$ pex psutil psutil==3.2.1 -o test.pex
[illuminati build]$ pex psutil psutil==3.2.1 -o test.pex
Could not satisfy all requirements for psutil:
    psutil, psutil==3.2.1

and is also affected by ordering:

[illuminati build]$ rm -rf ~/.pex
[illuminati build]$ pex psutil==3.2.1 psutil -o test.pex
[illuminati build]$ pex psutil==3.2.1 psutil -o test.pex
[illuminati build]$ pex psutil==3.2.1 psutil -o test.pex
[illuminati build]$ pex psutil psutil==3.2.1 -o test.pex
Could not satisfy all requirements for psutil:
    psutil, psutil==3.2.1
@jsirois
Copy link
Member

jsirois commented Nov 13, 2015

Some of the research here is relevant: pantsbuild/pants#2533

@arnib
Copy link

arnib commented Mar 22, 2016

I've been hitting this with our pants setup as well.
We have tests depending on moto, which in turn has a dependency on boto with an unpinned version.
The application has a dependency on boto with a pinned version.

This results in a error from pants/pex, which looks like it could be solved:
Exception message: Could not satisfy all requirements for boto==2.38.0: boto==2.38.0, boto>=2.20.0(from: moto==0.4.10)

@kwlzn
Copy link
Contributor Author

kwlzn commented Mar 22, 2016

@arnib do you have any data or a repro you can share that indicates these issues are related? afaict, #178 only happens when one of the deps is completely unpinned - however in your case it appears boto is pinned somewhere (boto==2.38.0).

fwiw:

$ pex boto==2.38.0 moto==0.4.10
Python 2.7.10 (default, Dec 16 2015, 14:09:45) 
[GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> import boto
>>> import moto
>>> boto.__file__, moto.__file__
('/private/var/folders/3t/xkwqrkld4xxgklk2s4n41jb80000gn/T/tmpul9NGo/.deps/boto-2.38.0-py2.py3-none-any.whl/boto/__init__.py', '/private/var/folders/3t/xkwqrkld4xxgklk2s4n41jb80000gn/T/tmpul9NGo/.deps/moto-0.4.10-py2.py3-none-any.whl/moto/__init__.py')

$ pex boto moto==0.4.10
Python 2.7.10 (default, Dec 16 2015, 14:09:45) 
[GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 

@arnib
Copy link

arnib commented Mar 23, 2016

Hey @kwlzn, thanks for checking this out.
I'm not able to reproduce the issue using the pex command line, so my error might not be related to this particular issue.

However the exception message still looks like something that should be resolvable.

@kwlzn
Copy link
Contributor Author

kwlzn commented May 30, 2016

related to pantsbuild/pants#3527

@butlern
Copy link
Contributor

butlern commented Jul 12, 2016

+1 Would like to see this fixed as well. We're currently seeing random pants failures due to this. 1.1.11 fails as well.

$ rm -rf ~/.pex/
$ pex --version
pex 1.1.11
$ pex thrift thrift==0.9.1 -o test.pex
$ pex thrift thrift==0.9.1 -o test.pex
Could not satisfy all requirements for thrift:
    thrift, thrift==0.9.1

@kwlzn kwlzn added the bug label Jul 13, 2016
@butlern
Copy link
Contributor

butlern commented Feb 16, 2017

So I dove into ze code and this is a multilayered problem. The trivial issue that causes this test to fail on the second go-around is because we're not passing --cache-ttl in the test. This works:

$ rm -rf ~/.pex
$ pex --version
pex 1.2.2
$ pex thrift thrift==0.9.1 -o test.pex
$ pex thrift thrift==0.9.1 -o test.pex
Could not satisfy all requirements for thrift:
    thrift, thrift==0.9.1
$ pex --cache-ttl=120 thrift thrift==0.9.1 -o test.pex
$

The reason it works with cache-ttl is because in https://github.com/pantsbuild/pex/blob/master/pex/resolver.py#L249 it resolves the packages found in the cache for the unbounded thrift resolvable if cache-ttl is set.

Note that in https://github.com/pantsbuild/pex/blob/master/pex/resolver.py#L246 if the resolvable is exact, i.e. thrift==0.9.1 and it finds it in the cache, it always uses the cache.

So, if cache-ttl is unset, for the unbounded thrift resolvable, it finds a bunch of SourcePackages on pypi, for the thrift==0.9.0 resolvable, if it finds 0.9.0 in cache, it ALWAYS returns the cached versions and then in https://github.com/pantsbuild/pex/blob/master/pex/resolver.py#L58 it does a set intersection and finds no common packages and returns the empty set.

So, in the case of --cache-ttl being unset, you can either:

  1. Set --cache-tll, and this will work until --cache-ttl expires, and then it won't work until you remove the cached 0.9.0 version
  2. Remove the behavior of always returning the cached artifact if the resolvable is exact. I would favor this, because if --cache-ttl is set and the ttl of the artifact is within the ttl, it will still return the cached artifacts for both the unbounded and the exact resolvables.

However, I still think we have an identity crisis. Ultimately, I think it's a problem that this:

SourcePackage(u'https://pypi.python.org/packages/e7/9e/a1e13b5c3cb24f160fc34f589baf874fcb53951ec54895a7aa101cc39056/thrift-0.9.0.tar.gz#md5=600ad4d76bc87d3e0c8570557d60ae65')

Is not considered the same as this:

SourcePackage('file:///Users/nate/.pex/build/thrift-0.9.0.tar.gz')

Potential Solutions:

  1. Hash on the name and version in the Package class
diff --git a/pex/package.py b/pex/package.py
index f786729..3520725 100644
--- a/pex/package.py
+++ b/pex/package.py
@@ -55,6 +55,12 @@ class Package(Link):
       cls._HREF_TO_PACKAGE_CACHE.store(href, package)
     return package

+  def __hash__(self):
+      return hash((self.name, self.version))
+
+  def __eq__(self, other):
+      return hash(self) == hash(other)
+
   @property
   def name(self):
     return NotImplementedError

The problem with this approach is that it's now order dependent, the set intersection can return the remote SourcePackage or the local SourcePackage depending upon which side of the intersection you're on.

  1. Don't use intersection, create a comparitor based on name, value and maybe package type (prefer Wheels/Eggs over SourcePackages and Local over Remote) and loop through the two sets applying it.

Ultimately this bites us in pants land, we have no control over transitive 3rdparty dependencies and are often bit at build time by this which creates non-deterministic builds. We could disable cache but that would slow our build times unacceptably.

In the meantime I'm going to probably go for option 1, patch our pex with it in our cheeseshop and have it run on our build environment for a time to test it out.

@butlern
Copy link
Contributor

butlern commented Feb 16, 2017

Actually, I ran into another edge case, this happens when an unbounded or inexact requirement in a resolvable is passed and --cache-ttl is used. In this case, if the cached version fits within the bounds of the resolvable requirement, and is less than ttl, only the cached version is returned. But if another resolvable has a requirement that doesn't match that cached version, then it fails.

For example:

$ pex --cache-ttl=86400 'thrift' 'thrift<0.9.1' -o test.pex
$ pex --cache-ttl=86400 'thrift' 'thrift==0.9.1 -o test.pex
Could not satisfy all requirements for thrift:
    thrift, thrift==0.9.1

In this case, the first pex run pulls down thrift==0.9.0 into the cache. On the second run, it finds the unbounded dependency and pulls the thrift==0.9.0 from cache. It then finds the exact dependency for 0.9.1, grabs that from pypi, but since the first resolvable only pulls from cache, the intersection results in an empty set and Unsatisfiable is raised.

This patch addresses that case by returning both the cached version found and the pypi results. In fact, this patch should mean we don't need to deal with package identity at all.

diff --git i/pex/resolver.py w/pex/resolver.py
index e7669c4..bc512ed 100644
--- i/pex/resolver.py
+++ w/pex/resolver.py
@@ -241,8 +241,11 @@ class CachingResolver(Resolver):

       if self.__cache_ttl:
         packages = self.filter_packages_by_ttl(packages, self.__cache_ttl)
-        if packages:
-          return packages
+        # Return both cached packages and the pypi packages. The edge case is if a inexact
+        # resolvable finds a cached version, it needs to return that cached package AND all pypi
+        # packages because the cached version might not match another resolvable which has a tighter
+        # bound or an exact version.
+        return packages + super(CachingResolver, self).package_iterator(resolvable, existing=existing)

     return super(CachingResolver, self).package_iterator(resolvable, existing=existing)

This doesn't solve the case where --cache-ttl is not used, but I think as far as pants goes, it passes a cache-ttl by default so this shouldn't affect it.

I'll add some unit tests after further testing in our build environment and submit a PR soon.

butlern added a commit to butlern/pex that referenced this issue Feb 17, 2017
  Ref: pex-tool#178

  The error is as follows:

  1. Two resolvables, one pinned, the other not.
  2. First pex run with clean cache works.
  3. Change the pinned version.
  4. Second pex run fails with Unsatisfiable.

  The problem is that the non-pinned resolvable locates the cached dep
  and only returns it. Since that version doesn't match the pinned
  version, we fail. This fixes that behavior by returning both the
  cached version and all uncached pypi packages for unpinned
  resolvables.

  We change the pinned version to test the use case where you have a
  cached version of a dep and then you transitively pull in another
  requirement that either has a tighter bound or a pin on a version not
  found in the cache.
butlern added a commit to butlern/pex that referenced this issue Feb 18, 2017
  Ref: pex-tool#178

  The error is as follows:

  1. Two resolvables, one pinned, the other not.
  2. First pex run with clean cache works.
  3. Change the pinned version.
  4. Second pex run fails with Unsatisfiable.

  The problem is that the non-pinned resolvable locates the cached dep
  and only returns it. Since that version doesn't match the pinned
  version, we fail. This fixes that behavior by returning both the
  cached version and all uncached pypi packages for unpinned
  resolvables.

  We change the pinned version to test the use case where you have a
  cached version of a dep and then you transitively pull in another
  requirement that either has a tighter bound or a pin on a version not
  found in the cache.
@kwlzn kwlzn closed this as completed in 731d124 Feb 22, 2017
@kwlzn
Copy link
Contributor Author

kwlzn commented Feb 22, 2017

hmm, I still seem to be hitting this after the fix in #362:

$ pex --version
pex 1.2.3
$ pex psutil psutil==3.2.1 -o test.pex
$ pex psutil psutil==3.2.1 -o test.pex
Could not satisfy all requirements for psutil:
    psutil, psutil==3.2.1
$ pex psutil psutil==3.2.1 -o test.pex --disable-cache
$

@kwlzn kwlzn reopened this Feb 22, 2017
@butlern
Copy link
Contributor

butlern commented Feb 22, 2017

Yah, 731d124 fixes a related problem where --cache-ttl is set and still raises Unsatisfiable. If there's no --cache-ttl then you can still hit this because it returns 3.2.1 from cache because it's exact and then searches pypi for the unbounded and takes the latest and those don't match in the resolvable_set.merge(). Since pants always sets --cache-ttl I fixed it for that case.

@butlern
Copy link
Contributor

butlern commented Feb 22, 2017

Actually, now that I think about it, there is still the problem where you pass --cache-ttl, have an exact resolvable, an inexact resolvable and an expired cache. Probably exact resolvables shouldn't return from cache either if the cache has expired. But by default cache-ttl is set to None.

@kwlzn
Copy link
Contributor Author

kwlzn commented Feb 22, 2017

makes sense - was mainly just highlighting my bad assumption that the core repro in this issue was repaired by #362.

and fwiw, I have pantsbuild/pants#4277 out to consume this change in pants.

@butlern
Copy link
Contributor

butlern commented Feb 22, 2017

Cool, I'll work on a fix and submit a new PR w/unit tests for this specific issue.

butlern added a commit to butlern/pex that referenced this issue Feb 27, 2017
  * Add unit test and patch code to pass unit test

  Bug description:
  1. Exact resolvable and inexact resolvable
  2. First run completes, pushes exact resolvable into cache
  3. Second run fails with Unsatisfiable

  Reproduce:
  $ pex psutil psutil==3.2.1 -o test.pex
  $ pex psutil psutil==3.2.1 -o test.pex
  Could not satisfy all requirements for psutil:
      psutil, psutil==3.2.1

  The bug manifest itself in pex-tool#178 because by default --cache-ttl is
  set to None. This doesn't change those semantics. The default is still
  None, so we simply don't return an exact resolvable found in cache
  if the --cache-ttl isn't set. Doing so also fixes another bug where
  --cache-ttl is set, there is an exact resolvable found in cache, but
  it is expired.

  It should be noted that this patch could affect the performance of
  default pex runs which specify an exact resolvable but don't set
  --cache-ttl.
@kwlzn kwlzn closed this as completed in #365 Mar 7, 2017
kwlzn pushed a commit that referenced this issue Mar 7, 2017
* Fix bug in cached dependency resolution with exact resolvable

  * Add unit test and patch code to pass unit test

  Bug description:
  1. Exact resolvable and inexact resolvable
  2. First run completes, pushes exact resolvable into cache
  3. Second run fails with Unsatisfiable

  Reproduce:
  $ pex psutil psutil==3.2.1 -o test.pex
  $ pex psutil psutil==3.2.1 -o test.pex
  Could not satisfy all requirements for psutil:
      psutil, psutil==3.2.1

  The bug manifest itself in #178 because by default --cache-ttl is
  set to None. This doesn't change those semantics. The default is still
  None, so we simply don't return an exact resolvable found in cache
  if the --cache-ttl isn't set. Doing so also fixes another bug where
  --cache-ttl is set, there is an exact resolvable found in cache, but
  it is expired.

  It should be noted that this patch could affect the performance of
  default pex runs which specify an exact resolvable but don't set
  --cache-ttl.

* fixup! Fix bug in cached dependency resolution with exact resolvable

* fixup! Fix bug in cached dependency resolution with exact resolvable
@kwlzn
Copy link
Contributor Author

kwlzn commented Mar 7, 2017

verified this is fixed in master. huge shoutout to @butlern for the bugfix(es)!

butlern added a commit to butlern/pex that referenced this issue Jun 30, 2017
Fixes pex-tool#178

The error is as follows:

Two resolvables, one pinned, the other not.
First pex run with clean cache works.
Change the pinned version.
Second pex run fails with Unsatisfiable.
The problem is that the non-pinned resolvable locates the cached dep
and only returns it. Since that version doesn't match the pinned
version, we fail. This fixes that behavior by returning both the
cached version and all uncached pypi packages for unpinned
resolvables.

We change the pinned version to test the use case where you have a
cached version of a dep and then you transitively pull in another
requirement that either has a tighter bound or a pin on a version not
found in the cache.
butlern added a commit to butlern/pex that referenced this issue Jun 30, 2017
…ol#365)

* Fix bug in cached dependency resolution with exact resolvable

  * Add unit test and patch code to pass unit test

  Bug description:
  1. Exact resolvable and inexact resolvable
  2. First run completes, pushes exact resolvable into cache
  3. Second run fails with Unsatisfiable

  Reproduce:
  $ pex psutil psutil==3.2.1 -o test.pex
  $ pex psutil psutil==3.2.1 -o test.pex
  Could not satisfy all requirements for psutil:
      psutil, psutil==3.2.1

  The bug manifest itself in pex-tool#178 because by default --cache-ttl is
  set to None. This doesn't change those semantics. The default is still
  None, so we simply don't return an exact resolvable found in cache
  if the --cache-ttl isn't set. Doing so also fixes another bug where
  --cache-ttl is set, there is an exact resolvable found in cache, but
  it is expired.

  It should be noted that this patch could affect the performance of
  default pex runs which specify an exact resolvable but don't set
  --cache-ttl.

* fixup! Fix bug in cached dependency resolution with exact resolvable

* fixup! Fix bug in cached dependency resolution with exact resolvable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants