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

generator bug with exception: tstate->exc_value is not cleared after an except block #67542

Closed
vstinner opened this issue Jan 30, 2015 · 23 comments

Comments

@vstinner
Copy link
Member

BPO 23353
Nosy @pitrou, @vstinner, @serhiy-storchaka
Files
  • excinfo_bug2.py
  • excinfo_bug6.py
  • gen_exc_value.patch
  • gen_exc_value_py27.patch
  • gen_exc_state_restore.patch
  • gen_exc_value_py27.patch
  • exctests.patch
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = <Date 2015-03-18.21:25:33.671>
    created_at = <Date 2015-01-30.14:08:56.293>
    labels = []
    title = 'generator bug with exception: tstate->exc_value is not cleared after an except block'
    updated_at = <Date 2015-03-18.21:29:02.728>
    user = 'https://github.com/vstinner'

    bugs.python.org fields:

    activity = <Date 2015-03-18.21:29:02.728>
    actor = 'vstinner'
    assignee = 'none'
    closed = True
    closed_date = <Date 2015-03-18.21:25:33.671>
    closer = 'pitrou'
    components = []
    creation = <Date 2015-01-30.14:08:56.293>
    creator = 'vstinner'
    dependencies = []
    files = ['37922', '37923', '37931', '37932', '37933', '37937', '37943']
    hgrepos = []
    issue_num = 23353
    keywords = ['patch']
    message_count = 23.0
    messages = ['235033', '235037', '235067', '235069', '235070', '235072', '235073', '235074', '235076', '235077', '235078', '235097', '235099', '235104', '235110', '235112', '235115', '235116', '235117', '235277', '238403', '238470', '238472']
    nosy_count = 5.0
    nosy_names = ['pitrou', 'vstinner', 'Arfrever', 'python-dev', 'serhiy.storchaka']
    pr_nums = []
    priority = 'normal'
    resolution = 'fixed'
    stage = 'resolved'
    status = 'closed'
    superseder = None
    type = None
    url = 'https://bugs.python.org/issue23353'
    versions = ['Python 2.7', 'Python 3.4', 'Python 3.5']

    @vstinner
    Copy link
    Member Author

    Since my changeset a5efd5021ca1, the Python test suite starts to fail randomly. Running test_asyncio modifies sys.exc_info(): it is not (None, None, None) after the execution of test_asyncio. The problem comes from test_cancel_make_subprocess_transport_exec() of Lib/test/test_asyncio/test_subprocess.py.

    I tried to write a simple script which does not depend on Python to reproduce the issue. See attached excinfo_bug2.py script.

    Output:
    ---
    exc_info after except (<class '__main__.MyException'>, MyException(), <traceback object at 0x7f09201ad7c8>)
    exc_info at exit (<class '__main__.MyException'>, MyException(), <traceback object at 0x7f09201abd08>)
    ---

    exc_info is supposed to be (None, None, None), at least at exit.

    I will try to write an even simpler script to identify the bug.

    @vstinner
    Copy link
    Member Author

    I simplified the script even more: 287 lines (6 functions/generators, 7 classes/exceptions) => 28 lines (1 generator)!

    @vstinner
    Copy link
    Member Author

    Last major change related to generators in Python/ceval.c:
    ---
    changeset: 47594:212a1fee6bf9
    parent: 47585:b0ef00187a7e
    user: Benjamin Peterson <benjamin@python.org>
    date: Wed Jun 11 15:59:43 2008 +0000
    files: Doc/library/dis.rst Doc/library/inspect.rst Doc/library/sys.rst Doc/reference/datamodel.rst Include/frameobject.h Include/opcode.h Lib/doctest.py Li
    description:
    bpo-3021: Antoine Pitrou's Lexical exception handlers
    ---

    This change introduced SWAP_EXC_STATE() and SAVE_EXC_STATE().

    @vstinner vstinner changed the title gnerator bug with exception: tstate->exc_value is not cleared after an except block generator bug with exception: tstate->exc_value is not cleared after an except block Jan 31, 2015
    @vstinner
    Copy link
    Member Author

    Attached gen_exc_value.patch changes how generators handle the currently handled exception (tstate->exc_value). The patch probably lacks tests to test the exact behaviour of sys.exc_info(). The 3 examples below can be used to write such tests. But before investing time on an implemenation, I would like to ensure that I correctly understood the bug and discuss how it should be fixed.

    Currently, a generator inherits the currently handled exception from the "caller" (function calling next(), gen.send() or gen.throw()). With the patch, the generator and its caller don't share the exception anymore. The generator still remembers the current exception when it is interrupted by yield.

    It's still possible to pass the exception from the caller to the generator using the throw() method of the generator. My patch doesn't change the behaviour of throw(): see the example 3 below.

    The patch also fixes the initial issue: "./python -m test test_asyncio test_functools" doesn't fail anymore.

    Python 2.7 is also affected by the bug. I don't know yet if it's safe to change the behaviour of currently handled exception in Python 2 generators. It may break backward compatibility. We should probably check applications which heavily depends on generators. For example, the Trollius and Twisted projects use generators for coroutines in asynchronous programming.

    The backward compatibility also matters in Python 3.4, so same question: should we change the behaviour of Python 3.4? Can it break applications?

    In Python 3, the currently handled exception is more important than in Python 2, because it is used to automatically fill the __context__ attribute of raised exceptions.

    I didn't test the behaviour of yield-from yet, I don't know how my patch changes its behaviour.

    Example 1:
    ---

    import sys
    
    def gen():
        print(sys.exc_info())
        yield
    
    g = gen()
    try:
        raise ValueError
    except Exception:
        next(g)

    Original output:
    ---
    (<class 'ValueError'>, ValueError(), <traceback object at 0x7f22a1ab52c8>)
    ---

    With the patch:
    ---
    (None, None, None)
    ---

    Example 2:
    ---

    import sys
    
    def gen():
        try:
            yield
            raise TypeError()
        except:
            print(sys.exc_info())
        print(sys.exc_info())
        yield
    
    g = gen()
    next(g)
    try:
        raise ValueError
    except Exception:
        next(g)

    Original output:
    ---
    (<class 'TypeError'>, TypeError(), <traceback object at 0x7fad239a22c8>)
    (<class 'ValueError'>, ValueError(), <traceback object at 0x7fad239a2288>)
    ---

    With the patch:
    ---
    (<class 'TypeError'>, TypeError(), <traceback object at 0x7f278b174988>)
    (None, None, None)
    ---

    Example 3:
    ---

    import sys
    
    def gen():
        try:
            try:
                yield
            except:
                print(sys.exc_info())
                raise TypeError()
        except Exception as exc:
            print("TypeError context:", repr(exc.__context__))
        yield
    
    g = gen()
    next(g)
    try:
        raise ValueError
    except Exception as exc:
        g.throw(exc)

    Original output:
    ---
    (<class 'ValueError'>, ValueError(), <traceback object at 0x7f233f05e388>)
    TypeError context: ValueError()
    (<class 'ValueError'>, ValueError(), <traceback object at 0x7f233f05e348>)
    ---

    With the patch (unchanged):
    ---
    (<class 'ValueError'>, ValueError(), <traceback object at 0x7fdf356fead8>)
    TypeError context: ValueError()
    (None, None, None)
    ---

    @vstinner
    Copy link
    Member Author

    See also:

    @vstinner
    Copy link
    Member Author

    Oh, by the way: keeping the exception after the except block is also a tricky reference leak. In Python 3, since exceptions store their traceback, this issue may keep a lot of objects alive too long, whereas they are expected to be destroyed much earlier.

    When I started to investigate this issue, it took me 2 hours to begin to understand why so many objects were kept alive. It looks like a reference cycle, a reference leak, or other kind of complex memory leak. Clearing manually local variables (ex: "self = None" in methods) is not enough.

    Python 2 has a sys.exc_clear() method which can be used to workaround this issue. It cannot be used in Python 3 since the function was removed in Python 3.

    @pitrou
    Copy link
    Member

    pitrou commented Jan 31, 2015

    Currently, a generator inherits the currently handled exception from
    the "caller"

    This is expected, since this is how normal functions behave.

    @vstinner
    Copy link
    Member Author

    Attached gen_exc_value_py27.patch: Patch for Python 2.7. No unit test yet.

    The full test suite of trollius pass on the patched Python 2.7 and on the patched Python 3.5. The full test suite of asyncio also pass on the patched Python 3.5.

    @vstinner
    Copy link
    Member Author

    > Currently, a generator inherits the currently handled exception from
    > the "caller"

    This is expected, since this is how normal functions behave.

    Do you see how to fix the issue without changing the behaviour?

    @pitrou
    Copy link
    Member

    pitrou commented Jan 31, 2015

    Attached patch fixes the test script and doesn't break any test.

    @pitrou
    Copy link
    Member

    pitrou commented Jan 31, 2015

    Note the patch also fixes the reference leak in test_asyncio.

    @python-dev
    Copy link
    Mannequin

    python-dev mannequin commented Jan 31, 2015

    New changeset 4555da8c3091 by Victor Stinner in branch '3.4':
    Issue bpo-23353: Fix the exception handling of generators in PyEval_EvalFrameEx().
    https://hg.python.org/cpython/rev/4555da8c3091

    @vstinner
    Copy link
    Member Author

    I agree that it's better to not change the behaviour of generators, backward compatibility matters :-)

    I wrote tests using my examples and I combined them with gen_exc_state_restore.patch. I commited the changeset in Python 3.4 and 3.5.

    Backporting the fix to Python 2.7 looks more complex because the EXCEPT_HANDLE try block type and the POP_EXCEPT instruction are new in Python 3.0: introduced by 212a1fee6bf9 from the issue bpo-3021.

    What do you think? Is it worth to fix this issue in Python 2.7?

    I plan to workaround this bug in Tulip to support Python 3.3. I will also workaround it in Trollius to support Python 2.6 and newer. So for me, it's ok to live with this known bug.

    It's just yet another generator bug. asyncio/trollius already work around a yield-from bug (issue bpo-21209) ;-)

    Note the patch also fixes the reference leak in test_asyncio.

    Yes, as I explained in msg235072, this bug caused strange "memory leaks".

    @pitrou
    Copy link
    Member

    pitrou commented Jan 31, 2015

    I think your patch for 2.7 is wrong as was your patch for 3.x. You shouldn't change the behaviour of sys.exc_info() in the nominal case.

    @pitrou
    Copy link
    Member

    pitrou commented Jan 31, 2015

    It would have been nice to wait for a review. Generator tests are already in test_exceptions.py.

    @pitrou
    Copy link
    Member

    pitrou commented Jan 31, 2015

    Additional simple tests for test_exceptions.py.

    @serhiy-storchaka
    Copy link
    Member

    If you need non-normalized exception for testing, a NameError generated by interpreter is not normalized.

    @vstinner
    Copy link
    Member Author

    It would have been nice to wait for a review. Generator tests are already in test_exceptions.py.

    Sorry, I wanted to quickly push your fix to fix buildbots. I dislike being the responsible of turning all buildbots to red...

    Before working on this issue, I didn't know test_generators. Well, I didn't know test_exceptions neither :-) test_exceptions.py sounds like a better name for checks on the currently handled exception :-)

    I saw that test_generators.py is mostly written with doctests. At the beginning, doctests were shiny and fun. Now I consider that it's worse than classic unit tests and I plan to rewrite doctests to unittest.TestCase classes. I will open a new issue for that.

    I think your patch for 2.7 is wrong as was your patch for 3.x. You shouldn't change the behaviour of sys.exc_info() in the nominal case.

    I now agree that gen_exc_value.patch was wrong. gen_exc_value_py27.patch was just a backport of my patch to Python 2.7.

    (Oh I see that I uploaded gen_exc_value_py27.patch twice, it's a mistake.)

    In my previous message, I asked myself if it would be possible to backport your patch (gen_exc_state_restore.patch) to Python 2.7.

    @vstinner
    Copy link
    Member Author

    By the way: buildbots are green again, cool.

    @python-dev
    Copy link
    Mannequin

    python-dev mannequin commented Feb 2, 2015

    New changeset 2cd6621a9fbc by Victor Stinner in branch '3.4':
    Issue bpo-23353, asyncio: Workaround CPython bug bpo-23353
    https://hg.python.org/cpython/rev/2cd6621a9fbc

    @vstinner
    Copy link
    Member Author

    @antoine: exctests.patch looks good to me, can you commit it?

    @python-dev
    Copy link
    Mannequin

    python-dev mannequin commented Mar 18, 2015

    New changeset ac43268da908 by Antoine Pitrou in branch '3.4':
    Issue bpo-23353: improve exceptions tests for generators
    https://hg.python.org/cpython/rev/ac43268da908

    New changeset b29342f53174 by Antoine Pitrou in branch 'default':
    Issue bpo-23353: improve exceptions tests for generators
    https://hg.python.org/cpython/rev/b29342f53174

    @pitrou pitrou closed this as completed Mar 18, 2015
    @vstinner
    Copy link
    Member Author

    Thanks Antoine!

    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    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

    No branches or pull requests

    3 participants