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

patcher diverts subprocess exceptions instead of keeping class identity #413

Open
Linbing opened this issue May 13, 2017 · 26 comments
Open

Comments

@Linbing
Copy link

Linbing commented May 13, 2017

when using eventlet.monkey_patch(), it's will call patch module raise Error change, linke subprocess.check_output or check_call, when somtthing error happened, it's will raise CalledProcessError(retcode, cmd), and this CalledProcessError maybe change in eventlet.green.subprocess, so, we can not except this error like subprocess.CalledProcessError, instead of, we must except this error eventlet.green.subprocess.CalledProcessError.

example:

#!/usr/bin/python

import eventlet
import subprocess

eventlet.monkey_patch()

try:
        subprocess.check_output(["xxx"],shell=True)
except eventlet.green.subprocess.CalledProcessError as e:
    print ">> except in eventlet.green.subprocess.CalledProcessError"
except subprocess.CalledProcessError as e:
    print ">> except in subprocess.CalledProcessError "
@temoto temoto changed the title patch module calls raise Error changes patcher diverts subprocess exceptions instead of keeping class identity May 13, 2017
@temoto temoto self-assigned this May 13, 2017
@temoto temoto added this to the v0.22 milestone May 13, 2017
@temoto
Copy link
Member

temoto commented May 13, 2017

Thank you, the problem is clear.

@temoto
Copy link
Member

temoto commented May 13, 2017

This revealed the ugly side of monkey patching, when original stdlib modules are saved for eventlet.patcher.original() function, they are re-imported from scratch, which creates another copy of exception classes in memory.

I'm not sure where things start to get wrong, don't have a good idea how to fix this right now. Stay tuned.

Linbing pushed a commit to Linbing/eventlet that referenced this issue May 14, 2017
…lass identity

reset new reimport module in sys.modules.

eventlet#413
@Linbing
Copy link
Author

Linbing commented May 14, 2017

@temoto see pull/414, inject will return new reimport module. so the exception classes will take the same.

@Linbing
Copy link
Author

Linbing commented May 14, 2017

CalledProcessError has been set in globals(), and he is from the new import subprocess module, so when after inject. sys.modules.subprocess keep older, that's why we can't using subprocess.CalledProcessError(sys.modules) to except the error.

@temoto
Copy link
Member

temoto commented May 14, 2017

Yeah I understand what's going on, so far the best solution I can do is to rebind certain names from stdlib to green module. Please, try this patch eac49d8

pip install https://github.com/eventlet/eventlet/archive/eac49d80bbdb2245173f46d4cf3e1f19b33f5462.zip

@Linbing
Copy link
Author

Linbing commented May 14, 2017

@temoto i have modify like your patch before, but it's not resolved. we can't do that like

CalledProcessError = subprocess_orig.CalledProcessError
or
CalledProcessError = sys.modules['subprocess'].CalledProcessError

both or that will change CalledProcessError in eventlet.green.subprocess.

so we can't define CalledProcessError in eventlet.green.subprocess

@Linbing
Copy link
Author

Linbing commented May 14, 2017

@temoto CalledProcessError = sys.modules['subprocess'].CalledProcessError can compatible with subprocess.CalledProcessError and eventlet.green.subprocess.CalledProcessError.

@temoto
Copy link
Member

temoto commented May 14, 2017

@Linbing I don't understand, can you show what's the problem with code example?

@temoto
Copy link
Member

temoto commented May 15, 2017

Never mind, I just had to try again with your original example code.
Turns out I've written bad test for previous patch.

Please, try again with this patch.

pip install https://github.com/eventlet/eventlet/archive/b9cc871b4005c4068a30f2342e66604ba4af623c.zip

@Linbing
Copy link
Author

Linbing commented May 15, 2017

@temoto my code example


#!/usr/bin/python
import eventlet
import subprocess

eventlet.monkey_patch()
try:
        subprocess.check_output(["xxx"],shell=True)
except eventlet.green.subprocess.CalledProcessError as e:
        print ">> except in eventlet.green.subprocess.CalledProcessError"
except subprocess.CalledProcessError as e:
    print ">> except in subprocess.CalledProcessError"

1、when set eventlet.green.subprocess

CalledProcessError = subprocess_orig.CalledProcessError

output is:

/bin/sh: xxx: 未找到命令
>> except in eventlet.green.subprocess.CalledProcessError

2、when set eventlet.green.subprocess

# CalledProcessError = subprocess_orig.CalledProcessError
CalledProcessError = sys.modules['subprocess'].CalledProcessError

the output is the same with before

/bin/sh: xxx: 未找到命令
>> except in eventlet.green.subprocess.CalledProcessError

But when we change the example code with delete except eventlet.green.subprocess like

#!/usr/bin/python

import eventlet
import subprocess

eventlet.monkey_patch()

try:
        subprocess.check_output(["xxx"],shell=True)
# except eventlet.green.subprocess.CalledProcessError as e:
        # print ">> except in eventlet.green.subprocess.CalledProcessError"
except subprocess.CalledProcessError as e:
    print ">> except in subprocess.CalledProcessError"

1、using subprocess_orig.CalledProcessError

 CalledProcessError = subprocess_orig.CalledProcessError
# CalledProcessError = sys.modules['subprocess'].CalledProcessError

output is

/bin/sh: xxx: 未找到命令
Traceback (most recent call last):
  File "./gg.py", line 9, in <module>
    subprocess.check_output(["xxx"],shell=True)
  File "/usr/lib64/python2.7/subprocess.py", line 578, in check_output
    raise CalledProcessError(retcode, cmd, output=output)
subprocess.CalledProcessError: Command '['xxx']' returned non-zero exit status 127

2、 using sys.modules['subprocess'].CalledProcessError

# CalledProcessError = subprocess_orig.CalledProcessError
CalledProcessError = sys.modules['subprocess'].CalledProcessError

output is

/bin/sh: xxx: 未找到命令
>> except in subprocess.CalledProcessError

the real problem is that when we reimport subprocess module, no matter inject or original, the globals CalledProcessError (eventlet.green.CalledProcessError) is new, that's raise CalledProcessError from, so, it's never be instance of sys.modules['subprocess'].CalledProcessError. the only resolution is take globals CalledProcessError (eventlet.green.CalledProcessError) from sys.modules['subprocess'].CalledProcessError, this make globals CalledProcessError equal with sys.modules['subprocess'].CalledProcessError. that's why we can except sys.modules['subprocess'].CalledProcessError or eventlet.green.subprocess.CalledProcessError , because they the some class in memory.

You can see my another code example to see class id in memory

#!/usr/bin/python
import eventlet
import subprocess
import sys

print ">> start test subprocess.CalledProcessError id is : " + str(id(sys.modules['subprocess'].CalledProcessError))

# import ipdb;ipdb.set_trace()
eventlet.monkey_patch()

print ">> after monkey_patch subprocess.CalledProcessError id is : " + str(id(sys.modules['subprocess'].CalledProcessError))
print ">> after monkey_patch eventlet.green.subprocess.CalledProcessError id is : " + str(id(sys.modules['eventlet.green.subprocess'].CalledProcessError))
# import ipdb;ipdb.set_trace()
try:
        subprocess.check_output(["xxx"],shell=True)
except eventlet.green.subprocess.CalledProcessError as e:
        print ">> except in eventlet: eventlet.green.subprocess.CalledProcessError id is : " + str(id(sys.modules['eventlet.green.subprocess'].CalledProcessError))
        print ">> except in eventlet: subprocess.CalledProcessErrorid is : " + str(id(sys.modules['subprocess'].CalledProcessError))
except subprocess.CalledProcessError as e:
    print ">> except in subprocess: subprocess.CalledProcessError id is : " + str(id(sys.modules['subprocess'].CalledProcessError))
    print ">> except in subprocess: eventlet...CalledProcessError id is : " + str(id(sys.modules['eventlet.green.subprocess'].CalledProcessError))

modify /usr/lib64/python2.7/subprocess.py check_outpu, before raise CalledProcessError.

        print ">> in lib64 subprcess CalledProcessError id is " + str(id(CalledProcessError))
        print ">> in lib64 subprcess subprocess.CalledProcessError id is " + str(id(sys.modules['subprocess'].CalledProcessError))
        print ">> in lib64 subprcess eventlet.green.subprocess.CalledProcessError id is " + str(id(sys.modules['eventlet.green.subprocess'].CalledProcessError))
        raise CalledProcessError(retcode, cmd, output=output)

@Linbing
Copy link
Author

Linbing commented May 15, 2017

@temoto
pip install https://github.com/eventlet/eventlet/archive/b9cc871b4005c4068a30f2342e66604ba4af623c.zip

that is what i am suggest,but this is only for CalledProcessError resolution,and it's not only the problem of CalledProcessError. all Class used in patched method may casue the same problem.

@temoto
Copy link
Member

temoto commented May 15, 2017

Yes it's exactly your patch, I'll add authorship. Please test whole package though.

And yes it matters for all names. I don't know a good solution without making eventlet contaminate everything with patched modules.

@temoto
Copy link
Member

temoto commented May 15, 2017

If you encounter same problem for other classes, please write me.

@YorikSar
Copy link

YorikSar commented Jul 2, 2017

@temoto We're running into the same issue with SubprocessError in openstack/oslo.concurrency [0].
Latest commit [1] doesn't fix that issue.

[0] https://bugs.launchpad.net/oslo.concurrency/+bug/1688201
[1] https://github.com/eventlet/eventlet/commit/79cda12ba73c2fdbf7ebc01fed3fa15c6cb4c058.zip

@temoto
Copy link
Member

temoto commented Jul 2, 2017

@YorikSar please try another version

pip install https://github.com/eventlet/eventlet/archive/f27ca63e817f35576eef021d179bdd1d9937f25d.zip

@YorikSar
Copy link

YorikSar commented Jul 2, 2017

@temoto Thanks for quick response! Sadly it still doesn't work.

You can reproduce the issue like following:

# clone oslo.concurrency repo
% git clone git://git.openstack.org/openstack/oslo.concurrency.git
% cd oslo.concurrency
# prepare environment (assuming you have tox installed)
% tox -e py35 --notest
# install current version of patch
% .tox/py35/bin/pip install -U https://github.com/eventlet/eventlet/archive/f27ca63e817f35576eef021d179bdd1d9937f25d.zip
# run that one test
% env TEST_EVENTLET=1 .tox/py35/bin/python -m testtools.run oslo_concurrency.tests.unit.test_processutils.UtilsTest.test_execute_with_preexec_fn

Note that with eventlet==0.19.0 (before extra import in patcher.original("subprocess")) it works fine and it fails with subprocess.SubprocessError: Exception occurred in preexec_fn. (that should've been caught in the test if it were same exception class) statring with 0.20.0.

@temoto
Copy link
Member

temoto commented Jul 2, 2017

@YorikSar sorry, but SubprocessError is the same now. Maybe the root cause is something else.

@YorikSar
Copy link

YorikSar commented Jul 3, 2017

@temoto But they are still different. I wrote a simple script to check that:

% cat check.py                 
import eventlet
eventlet.monkey_patch()

import subprocess
print(id(subprocess.SubprocessError), id(eventlet.green.subprocess.SubprocessError))
try:
    subprocess.check_call('false')
except subprocess.SubprocessError:
    print('subprocess.SubprocessError')
except eventlet.green.subprocess.SubprocessError:
    print('eventlet.green.subprocess.SubprocessError')
except Exception as ex:
    print('Neither', type(ex))
% .tox/py35/bin/python check.py
46457160 46395480
eventlet.green.subprocess.SubprocessError

As you can see, subprocess.SubprocessError is not the one that is being thrown...

@temoto
Copy link
Member

temoto commented Jul 3, 2017

I was checking on Linux, CPython 3.6
Maybe 3.5 is different.

Meanwhile, please try to recreate tox environment to exclude cache or something similar.

This test must be pretty convincing, right?
https://github.com/eventlet/eventlet/blob/f27ca63e817f35576eef021d179bdd1d9937f25d/tests/isolated/subprocess_exception_identity.py

@temoto
Copy link
Member

temoto commented Jul 3, 2017

@YorikSar also please include this code in your check.py:

print(open(eventlet.green.subprocess.__file__).readlines()[-7:])

@YorikSar
Copy link

YorikSar commented Jul 4, 2017

@temoto I did recreate environment that time. Yes, I saw the test and it's strange that it passes although simple check doesn't work. I'll try to run this test again later today with clean venv and different versions of CPython.

@YorikSar
Copy link

YorikSar commented Jul 5, 2017

@temoto sorry for long waiting. I did this in clean virtualenvs in clean Ubuntu, still no luck, both on CPython 3.5 and 3.6:

ubuntu@ubuntu test-eventlet % virtualenv -p python3.6 venv36
Already using interpreter /usr/bin/python3.6
Using base prefix '/usr'
New python executable in /home/ubuntu/test-eventlet/venv36/bin/python3.6
Also creating executable in /home/ubuntu/test-eventlet/venv36/bin/python
Installing setuptools, pip, wheel...done.
ubuntu@ubuntu test-eventlet % virtualenv -p python3.5 venv35
Running virtualenv with interpreter /usr/bin/python3.5
Using base prefix '/usr'
New python executable in /home/ubuntu/test-eventlet/venv35/bin/python3.5
Also creating executable in /home/ubuntu/test-eventlet/venv35/bin/python
Installing setuptools, pip, wheel...done.
ubuntu@ubuntu test-eventlet % venv35/bin/pip install https://github.com/eventlet/eventlet/archive/f27ca63e817f35576eef021d179bdd1d9937f25d.zip
Collecting https://github.com/eventlet/eventlet/archive/f27ca63e817f35576eef021d179bdd1d9937f25d.zip
  Downloading https://github.com/eventlet/eventlet/archive/f27ca63e817f35576eef021d179bdd1d9937f25d.zip
     \ 839kB 716kB/s
Collecting enum-compat (from eventlet==0.21.0)
  Downloading enum-compat-0.0.2.tar.gz
Collecting greenlet>=0.3 (from eventlet==0.21.0)
  Downloading greenlet-0.4.12-cp35-cp35m-manylinux1_x86_64.whl (42kB)
    100% |████████████████████████████████| 51kB 969kB/s 
Building wheels for collected packages: enum-compat
  Running setup.py bdist_wheel for enum-compat ... done
  Stored in directory: /home/ubuntu/.cache/pip/wheels/cb/f2/00/7616514d23c84bf58b7e9d034cdb7ff92f1b93c08b6a21ca8b
Successfully built enum-compat
Installing collected packages: enum-compat, greenlet, eventlet
  Running setup.py install for eventlet ... done
Successfully installed enum-compat-0.0.2 eventlet-0.21.0 greenlet-0.4.12
ubuntu@ubuntu test-eventlet % venv36/bin/pip install https://github.com/eventlet/eventlet/archive/f27ca63e817f35576eef021d179bdd1d9937f25d.zip
Collecting https://github.com/eventlet/eventlet/archive/f27ca63e817f35576eef021d179bdd1d9937f25d.zip
  Downloading https://github.com/eventlet/eventlet/archive/f27ca63e817f35576eef021d179bdd1d9937f25d.zip (835kB)
    100% |████████████████████████████████| 839kB 478kB/s 
Collecting enum-compat (from eventlet==0.21.0)
  Using cached enum-compat-0.0.2.tar.gz
Collecting greenlet>=0.3 (from eventlet==0.21.0)
  Downloading greenlet-0.4.12-cp36-cp36m-manylinux1_x86_64.whl (42kB)
    100% |████████████████████████████████| 51kB 473kB/s 
Building wheels for collected packages: enum-compat
  Running setup.py bdist_wheel for enum-compat ... done
  Stored in directory: /home/ubuntu/.cache/pip/wheels/cb/f2/00/7616514d23c84bf58b7e9d034cdb7ff92f1b93c08b6a21ca8b
Successfully built enum-compat
Installing collected packages: enum-compat, greenlet, eventlet
  Running setup.py install for eventlet ... done
Successfully installed enum-compat-0.0.2 eventlet-0.21.0 greenlet-0.4.12
ubuntu@ubuntu test-eventlet % cat > check.py
import eventlet
eventlet.monkey_patch()

import subprocess
print(id(subprocess.SubprocessError), id(eventlet.green.subprocess.SubprocessError))
try:
    subprocess.check_call('false')
except subprocess.SubprocessError:
    print('subprocess.SubprocessError')
except eventlet.green.subprocess.SubprocessError:
    print('eventlet.green.subprocess.SubprocessError')
except Exception as ex:
    print('Neither', type(ex))
print(open(eventlet.green.subprocess.__file__).readlines()[-7:])
ubuntu@ubuntu test-eventlet % venv35/bin/python check.py 
33958904 33898280
eventlet.green.subprocess.SubprocessError
['_current_module = sys.modules[__name__]\n', "_keep_names = ('CalledProcessError', 'SubprocessError')\n", 'for k in _keep_names:\n', '    v = getattr(subprocess_imported, k, _MISSING)\n', '    if v is not _MISSING:\n', '        setattr(_current_module, k, v)\n', 'del _current_module, _keep_names, k, v, subprocess_imported\n']
ubuntu@ubuntu test-eventlet % venv36/bin/python check.py
41033688 41013400
eventlet.green.subprocess.SubprocessError
['_current_module = sys.modules[__name__]\n', "_keep_names = ('CalledProcessError', 'SubprocessError')\n", 'for k in _keep_names:\n', '    v = getattr(subprocess_imported, k, _MISSING)\n', '    if v is not _MISSING:\n', '        setattr(_current_module, k, v)\n', 'del _current_module, _keep_names, k, v, subprocess_imported\n']

My guess is that tests run in some dirty context that is different from my check.py.

@temoto
Copy link
Member

temoto commented Jul 6, 2017

Thanks you for helping. We have special isolated test mechanism that runs separate Python process with a minimal script.

I found that monkey_patch/import order different from existing test is causing this problem. So it's testable now, wait for fix.

@nat-goodspeed
Copy link
Contributor

fwiw this seems like the same as #357

@temoto
Copy link
Member

temoto commented Apr 17, 2018

Yes, it's a duplicate issue, thanks Nat.

nat-goodspeed added a commit to nat-goodspeed/eventlet that referenced this issue Apr 25, 2018
The default Windows hosts file starts something like this:

    # Copyright (c) 1993-2009 Microsoft Corp.
    #
    # This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
    ...

But `greendns.HostsResolver.LINES_RE` doesn't admit the possibility of a
comment starting at the beginning of a line. It produces a sequence consisting
of data from the comment lines as well as (eventually) the real content of the
hosts file.

This has been ignored since `is_ipv4_addr()` and `is_ipv6_addr()` catch
`dns.exception.SyntaxError` and return `False`. But in a runtime environment
that encounters eventlet#413, `dns.exception.SyntaxError` is not caught, and the
import fails.

Changing '+' to '*' allows `HostsResolver._readlines()` to recognize and skip
comment lines. However, that produces empty strings in the sequence. Filter
out the empty strings by passing the sequence through
`itertools.ifilter(None)`.
nat-goodspeed added a commit to nat-goodspeed/eventlet that referenced this issue Apr 25, 2018
In a frozen environment, the runtime bootstrap loader may not properly handle
absolute imports of (e.g.) `dns.exception` when there is no top-level `dns`
package. Relative imports work, e.g.:

    from . import exception

But you can't say:

    import ..dns.exception

You can't even say:

    from .. import dns

because that doesn't work when `dns` is found via `sys.path`.

Since the referencing code consistently uses fully qualified names such as
`dns.exception.SyntaxError`, we could write:

    from . import exception as dns_exception

and change every reference to (e.g.) `dns_exception.SyntaxError`.

It was deemed less intrusive to introduce a dummy class as a namespace, e.g.:

    class dns(object):
        from . import exception

which allows references of the form `dns.exception.SyntaxError` to work as is.

Using relative imports means that `eventlet.support.greendns` need not tinker
with `sys.path`.

Also encapsulate some of the redundancy in `greendns.py`'s `import_patched()`
stanza in a transient `patch_imports()` helper function. Use `try`/`finally`
to ensure `patch_imports()` and the local `import_patched()` are transient.

Also attempt to circumvent eventlet#413 by catching (e.g.)
`eventlet.support.dns.exception.SyntaxError`. It's not clear that this
addresses the problem, however.
nat-goodspeed added a commit to nat-goodspeed/eventlet that referenced this issue May 1, 2018
The default Windows hosts file starts something like this:

    # Copyright (c) 1993-2009 Microsoft Corp.
    #
    # This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
    ...

But `greendns.HostsResolver.LINES_RE` doesn't admit the possibility of a
comment starting at the beginning of a line. It produces a sequence consisting
of data from the comment lines as well as (eventually) the real content of the
hosts file.

This has been ignored since `is_ipv4_addr()` and `is_ipv6_addr()` catch
`dns.exception.SyntaxError` and return `False`. But in a runtime environment
that encounters eventlet#413, `dns.exception.SyntaxError` is not caught, and the
import fails.

Changing '+' to '*' allows `HostsResolver._readlines()` to recognize and skip
comment lines. However, that produces empty strings in the sequence. Filter
out the empty strings by passing the sequence through
`itertools.ifilter(None)`.
temoto pushed a commit that referenced this issue May 7, 2018
`greendns.HostsResolver.LINES_RE` doesn't admit the possibility of a
comment starting at the beginning of a line. This has been ignored since `is_ipv4_addr()` and `is_ipv6_addr()` catch `dns.exception.SyntaxError` and return `False`. But in a runtime environment that encounters #413, `dns.exception.SyntaxError` is not caught, and the
import fails.

Changing '+' to '*' allows `HostsResolver._readlines()` to recognize and skip
comment lines.

greendns.HostsResolver._readlines(), a purely internal method, now returns an
itertools generator rather than a list. Change relevant asserts to build a
list before comparing.
@temoto temoto removed this from the 0.23 milestone May 8, 2018
@temoto temoto removed their assignment Jul 13, 2019
@temoto
Copy link
Member

temoto commented Jul 13, 2019

Sorry for leaving this issue open. There is work-in-progress for anyone who wants to continue: f27ca63 0c41697

starlingx-github pushed a commit to starlingx/distcloud that referenced this issue Feb 5, 2020
The python kubernetes client requires a newer version of eventlet.
However, newer versions of eventlet have an issue with the subprocess
module, which requires subprocess to be imported from eventlet.green
instead of being imported directly. See:
eventlet/eventlet#413

The eventlet has been upversioned to 0.24.1, therefore, the
subprocess import is changed. This update also fixes the issue
that raise e triggers 'CalledProcessError' object is not
callable error.

Change-Id: If3fd8506ececf062ee1b390dc8a87771cb01dec9
Story: 2006980
Task: 37715
Signed-off-by: Tao Liu <tao.liu@windriver.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants