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

avoid infinite regress in __getattr__ #41

Merged
merged 1 commit into from Sep 17, 2014

Conversation

bukzor
Copy link
Contributor

@bukzor bukzor commented Sep 16, 2014

After a large amount of debugging a deadlock in pip's test suite, I found an infinite recursion in the below __getattr__ method. I've fixed it and regression-tested it as best I could.

It's not clear to me how an attribute of this class is referenced after its __fp is deleted, but it did happen.

For historicity's sake, this is the stack that caused the inifinite recursion:

  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/workspace/venv/bin/pip", line 9, in <module>
    load_entry_point('pip==1.6.dev1', 'console_scripts', 'pip')()
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/__init__.py", line 198, in main
    return command.main(cmd_args)
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/basecommand.py", line 212, in main
    status = self.run(options, args)
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/commands/install.py", line 317, in run
    requirement_set.prepare_files(finder)
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/req/req_set.py", line 238, in prepare_files
    req_to_install, self.upgrade)
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/index.py", line 284, in find_requirement
    for page in self._get_pages(url_locations, req):
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/index.py", line 391, in _get_pages
    page = self._get_page(location, req)
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/index.py", line 611, in _get_page
    result = HTMLPage.get_page(link, req, session=self.session)
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/index.py", line 693, in get_page
    "Cache-Control": "max-age=600",
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/sessions.py", line 463, in get
    return self.request('GET', url, **kwargs)
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/download.py", line 286, in request
    result = super(PipSession, self).request(method, url, *args, **kwargs)
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/sessions.py", line 451, in request
    resp = self.send(prep, **send_kwargs)
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/sessions.py", line 557, in send
    r = adapter.send(request, **kwargs)
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/adapter.py", line 38, in send
    return self.build_response(request, cached_response, from_cache=True)
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/adapter.py", line 95, in build_response
    request, response
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/adapters.py", line 203, in build_response
    response.headers = CaseInsensitiveDict(getattr(resp, 'headers', {}))
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/structures.py", line 46, in __init__
    self.update(data, **kwargs)
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/workspace/venv/lib/python2.7/_abcoll.py", line 541, in update
    for key in other:
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/packages/urllib3/response.py", line 304, in closed
    elif hasattr(self._fp, 'isclosed'):  # Python 2
  File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 27, in __getattr__
    f.write(''.join(traceback.format_stack()))

I believe that gc is activating during Mapping.update, and jumping to response.closed. Why it would touch a property, I don't know. Further, this is happening after something (the gc I assume) has deleted __fp from the filewrapper. Because filewrapper references self.__fp in __getattr__, and __fp isn't present, it recurses there. Why I don't get a StackOverflowError rather than a busy deadlock, I also don't know.

But, this fixes it :D

@bukzor
Copy link
Contributor Author

bukzor commented Sep 16, 2014

Without the patch, the new test fails like so:

________________________________ test_getattr_during_gc ________________________________

    def test_getattr_during_gc():
        s = CallbackFileWrapper(None, None)
        # normal behavior:
        with pytest.raises(AttributeError):
            s.x

        # this previously had caused an infinite recursion
        del s._CallbackFileWrapper__fp
        with pytest.raises(AttributeError):
>           s.x

tests/test_regressions.py:31: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cachecontrol/filewrapper.py:22: in __getattr__
    return getattr(self.__fp, name)
cachecontrol/filewrapper.py:22: in __getattr__
    return getattr(self.__fp, name)
cachecontrol/filewrapper.py:22: in __getattr__
    return getattr(self.__fp, name)
!!! Recursion detected (same locals & position)
==================== 1 failed, 36 passed, 1 skipped in 0.57 seconds ====================

@dstufft
Copy link
Contributor

dstufft commented Sep 16, 2014

This is really confusing to me... I feel like maybe we're missing something here. My guess is it's somehow getting into a never ending loop when the responses close() method is being called by the responses __del__.

Is the munged name actually guaranteed to be stable across versions and Python implementations?

@dstufft
Copy link
Contributor

dstufft commented Sep 16, 2014

What happens if instead you do:

class CallbackFileWrapper(object):

    # ...

    def __del__(self):
        del self.__calback
        del self.__fp
        del self.__buf

does that work?

@sigmavirus24
Copy link
Contributor

Is the munged name actually guaranteed to be stable across versions and Python implementations?

Was my concern as well.

@bukzor
Copy link
Contributor Author

bukzor commented Sep 16, 2014

The exact name-mangling behavior is documented in the language reference: https://docs.python.org/2/reference/expressions.html#atom-identifiers

I would very much expect it to be the same cross-implementation.

We can also un-mangle this name if it makes everyone happier.

@bukzor
Copy link
Contributor Author

bukzor commented Sep 16, 2014

A printout from faulthandler of the same scenario:

Timeout (0:00:10)!
Current thread 0x00007f6b60150740:
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/filewrappe...", line 22 in __getattr__
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/filewrappe...", line 22 in __getattr__
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/filewrappe...", line 22 in __getattr__
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/filewrappe...", line 22 in __getattr__
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/filewrappe...", line 22 in __getattr__
<snip>
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/filewrappe...", line 22 in __getattr__
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/filewrappe...", line 22 in __getattr__
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/filewrappe...", line 22 in __getattr__
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/filewrappe...", line 22 in __getattr__
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/packages/urlli...", line 304 in closed
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/workspace/venv/lib/python2.7/_abcoll.py", line 541 in update
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/structures.py", line 46 in __init__
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/adapters.py", line 203 in build_response
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/adapter.py", line 95 in build_response
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/adapter.py", line 38 in send
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/sessions.py", line 557 in send
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/sessions.py", line 451 in request
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/download.py", line 286 in request
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/sessions.py", line 463 in get
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/index.py", line 693 in get_page
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/index.py", line 611 in _get_page
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/index.py", line 391 in _get_pages
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/index.py", line 284 in find_requirement
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/req/req_set.py", line 238 in prepare_files
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/commands/install.py", line 317 in run
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/basecommand.py", line 212 in main
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/__init__.py", line 198 in main
  File "/tmp/pytest-33/test_upgrade_user_conflict_in_globalsite0/workspace/venv/bin/pip", line 9 in <module>

@alex
Copy link
Member

alex commented Sep 16, 2014

This patch looks reasonable to me -- I don't know if the name manglign is guarnteed, but like 500000 things rely on it, so I wouldn't worry in the least.

# The vaguaries of garbage collection means that self.__fp is not always set.
# This strange style lets us throw AttributeError in that case, rather than
# recursing infinitely
fp = object.__getattribute__(self, '_CallbackFileWrapper__fp')
Copy link
Contributor

Choose a reason for hiding this comment

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

As said in IRC, I think checking explicitly for the attribute name in the __getattr__ is cleaner, although if this style is kept, does self.__getattribute__("_CallbackFileWrapper__fp") not work?

@dstufft
Copy link
Contributor

dstufft commented Sep 17, 2014

If @alex thinks relying on the name mangling is safe then it's probably safe.

@dstufft
Copy link
Contributor

dstufft commented Sep 17, 2014

This looks reasonable enough to me! :shipit:

@sigmavirus24
Copy link
Contributor

The name mangling still irks me but if it's safe, that's cool. LGTM

@bukzor
Copy link
Contributor Author

bukzor commented Sep 17, 2014

I believe we have consensus.

@sigmavirus24
Copy link
Contributor

Consensus does not a pull request merge.

@ionrock
Copy link
Contributor

ionrock commented Sep 17, 2014

@bukzor I'm happy to merge this, but would it be too much to ask to get a little more clarification in the comment? Specifically it would be helpful to get short summary of what happened, why this fixes it and why the _CallbackFileWrapper__fp is an appropriate bit of name munging. I'm making an assumption that is the namemunging @dstufft mentioned.

I hope this doesn't come across as "nit picky". My real reason is that I'm not terribly sure I totally understand the fix, which means if I look at it again in a few weeks my lack of understanding will undoubtedly multiplied a few times over! Feel free to leave a comment here if that is easier and I'll update the comment myself.

Sorry about difficult time debugging this and thanks for the PR!

ionrock added a commit that referenced this pull request Sep 17, 2014
avoid infinite regress in __getattr__
@ionrock ionrock merged commit 042ae8c into psf:master Sep 17, 2014
@ionrock
Copy link
Contributor

ionrock commented Sep 17, 2014

@bukzor Nevermind! I found where I was confused. Thanks again!

@sigmavirus24
Copy link
Contributor

@ionrock if I were you, I'd add comments in case future you becomes confused again.

@bukzor
Copy link
Contributor Author

bukzor commented Sep 17, 2014

@iorock 👍
Thanks for the merge!
I always make my comments too short =/

@bukzor bukzor deleted the buck-less-deadlock branch September 17, 2014 03:02
@ionrock
Copy link
Contributor

ionrock commented Sep 17, 2014

FYI, this has been released in 0.10.4.

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

Successfully merging this pull request may close these issues.

None yet

5 participants