Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-greenlet for openSUSE:Factory checked in at 2021-10-20 20:23:26 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-greenlet (Old) and /work/SRC/openSUSE:Factory/.python-greenlet.new.1890 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-greenlet" Wed Oct 20 20:23:26 2021 rev:38 rq:925731 version:1.1.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-greenlet/python-greenlet.changes 2021-09-03 21:26:48.274216348 +0200 +++ /work/SRC/openSUSE:Factory/.python-greenlet.new.1890/python-greenlet.changes 2021-10-20 20:24:11.245374487 +0200 @@ -1,0 +2,19 @@ +Sat Oct 16 19:07:41 UTC 2021 - Dirk M??ller <dmuel...@suse.com> + +- update to 1.1.2: + - Fix a potential crash due to a reference counting error when Python + subclasses of ``greenlet.greenlet`` were deallocated. The crash + became more common on Python 3.10; on earlier versions, silent + memory corruption could result. + - Fix a leak of a list object when the last reference to a greenlet + was deleted from some other thread than the one to which it + belonged. For this to work correctly, you must call a greenlet API + like ``getcurrent()`` before the thread owning the greenlet exits: + this is a long-standing limitation that can also lead to the leak of + a thread's main greenlet if not called; we hope to lift this + limitation. Note that in some cases this may also fix leaks of + greenlet objects themselves. See `issue 251 + - Python 3.10: Tracing or profiling into a spawned greenlet didn't + work as expected. See `issue 256 + +------------------------------------------------------------------- Old: ---- greenlet-1.1.0.tar.gz New: ---- greenlet-1.1.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-greenlet.spec ++++++ --- /var/tmp/diff_new_pack.hnmjPy/_old 2021-10-20 20:24:11.709374774 +0200 +++ /var/tmp/diff_new_pack.hnmjPy/_new 2021-10-20 20:24:11.713374776 +0200 @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-greenlet -Version: 1.1.0 +Version: 1.1.2 Release: 0 Summary: Lightweight in-process concurrent programming License: MIT ++++++ greenlet-1.1.0.tar.gz -> greenlet-1.1.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/greenlet-1.1.0/.github/workflows/tests.yml new/greenlet-1.1.2/.github/workflows/tests.yml --- old/greenlet-1.1.0/.github/workflows/tests.yml 2021-05-06 16:53:06.000000000 +0200 +++ new/greenlet-1.1.2/.github/workflows/tests.yml 2021-09-29 12:35:47.000000000 +0200 @@ -7,6 +7,7 @@ ZOPE_INTERFACE_STRICT_IRO: 1 PYTHONUNBUFFERED: 1 PYTHONDONTWRITEBYTECODE: 1 + PYTHONDEVMODE: 1 PIP_UPGRADE_STRATEGY: eager # Don't get warnings about Python 2 support being deprecated. We # know. The env var works for pip 20. @@ -23,7 +24,7 @@ runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10.0-beta.1] + python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10.0-rc.1] os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@v2 @@ -58,6 +59,11 @@ - name: Test run: | python -m unittest discover -v greenlet.tests + - name: Doctest + # FIXME: This conditional can go away when a Sphinx greater than 4.1.2 + # is released. See https://github.com/sphinx-doc/sphinx/issues/9512 + if: matrix.python-version != '3.10.0-rc.1' + run: | sphinx-build -b doctest -d docs/_build/doctrees2 docs docs/_build/doctest2 - name: Publish package to PyPI (mac) # We cannot 'uses: pypa/gh-action-pypi-publish@v1.4.1' because diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/greenlet-1.1.0/CHANGES.rst new/greenlet-1.1.2/CHANGES.rst --- old/greenlet-1.1.0/CHANGES.rst 2021-05-06 16:53:06.000000000 +0200 +++ new/greenlet-1.1.2/CHANGES.rst 2021-09-29 12:35:47.000000000 +0200 @@ -2,6 +2,38 @@ Changes ========= +1.1.2 (2021-09-29) +================== + +- Fix a potential crash due to a reference counting error when Python + subclasses of ``greenlet.greenlet`` were deallocated. The crash + became more common on Python 3.10; on earlier versions, silent + memory corruption could result. See `issue 245 + <https://github.com/python-greenlet/greenlet/issues/245>`_. Patch by + fygao-wish. +- Fix a leak of a list object when the last reference to a greenlet + was deleted from some other thread than the one to which it + belonged. For this to work correctly, you must call a greenlet API + like ``getcurrent()`` before the thread owning the greenlet exits: + this is a long-standing limitation that can also lead to the leak of + a thread's main greenlet if not called; we hope to lift this + limitation. Note that in some cases this may also fix leaks of + greenlet objects themselves. See `issue 251 + <https://github.com/python-greenlet/greenlet/issues/251>`_. +- Python 3.10: Tracing or profiling into a spawned greenlet didn't + work as expected. See `issue 256 + <https://github.com/python-greenlet/greenlet/issues/256>`_, reported + by Joe Rickerby. + +1.1.1 (2021-08-06) +================== + +- Provide Windows binary wheels for Python 3.10 (64-bit only). + +- Update Python 3.10 wheels to be built against 3.10rc1, where + applicable. + + 1.1.0 (2021-05-06) ================== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/greenlet-1.1.0/PKG-INFO new/greenlet-1.1.2/PKG-INFO --- old/greenlet-1.1.0/PKG-INFO 2021-05-06 16:53:07.788602400 +0200 +++ new/greenlet-1.1.2/PKG-INFO 2021-09-29 12:35:47.783776500 +0200 @@ -1,8 +1,12 @@ Metadata-Version: 2.1 Name: greenlet -Version: 1.1.0 +Version: 1.1.2 Summary: Lightweight in-process concurrent programming Home-page: https://greenlet.readthedocs.io/ +Author: Alexey Borzenkov +Author-email: sna...@gmail.com +Maintainer: Jason Madden +Maintainer-email: ja...@nextthought.com License: MIT License Project-URL: Bug Tracker, https://github.com/python-greenlet/greenlet/issues Project-URL: Source Code, https://github.com/python-greenlet/greenlet/ @@ -69,7 +73,9 @@ Documentation is available on readthedocs.org: https://greenlet.readthedocs.io +Keywords: greenlet coroutine concurrency threads cooperative Platform: any +Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/greenlet-1.1.0/appveyor.yml new/greenlet-1.1.2/appveyor.yml --- old/greenlet-1.1.0/appveyor.yml 2021-05-06 16:53:06.000000000 +0200 +++ new/greenlet-1.1.2/appveyor.yml 2021-09-29 12:35:47.000000000 +0200 @@ -19,6 +19,7 @@ CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_env.cmd" # Use a fixed hash seed for reproducability PYTHONHASHSEED: 8675309 + PYTHONDEVMODE: 1 # Don't get warnings about Python 2 support being deprecated. We # know. PIP_NO_PYTHON_VERSION_WARNING: 1 @@ -33,6 +34,11 @@ matrix: # http://www.appveyor.com/docs/installed-software#python + - PYTHON: "C:\\Python310-x64" + PYTHON_VERSION: "3.10.0rc2" + PYTHON_ARCH: "64" + PYTHON_EXE: python + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - PYTHON: "C:\\Python39-x64" PYTHON_ARCH: "64" @@ -161,7 +167,8 @@ test_script: - "%CMD_IN_ENV% python -m unittest discover -v greenlet.tests" - - "%CMD_IN_ENV% python -m sphinx -b doctest -d docs/_build/doctrees docs docs/_build/doctest" +# XXX: Doctest disabled pending sphinx release for 3.10; see tests.yml. +# - "%CMD_IN_ENV% python -m sphinx -b doctest -d docs/_build/doctrees docs docs/_build/doctest" after_test: - "%CMD_IN_ENV% python setup.py bdist_wheel" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/greenlet-1.1.0/setup.py new/greenlet-1.1.2/setup.py --- old/greenlet-1.1.0/setup.py 2021-05-06 16:53:06.000000000 +0200 +++ new/greenlet-1.1.2/setup.py 2021-09-29 12:35:47.000000000 +0200 @@ -112,6 +112,11 @@ long_description=readfile("README.rst"), long_description_content_type="text/x-rst", url="https://greenlet.readthedocs.io/", + keywords="greenlet coroutine concurrency threads cooperative", + author="Alexey Borzenkov", + author_email="sna...@gmail.com", + maintainer='Jason Madden', + maintainer_email='ja...@nextthought.com', project_urls={ 'Bug Tracker': 'https://github.com/python-greenlet/greenlet/issues', 'Source Code': 'https://github.com/python-greenlet/greenlet/', @@ -125,6 +130,7 @@ headers=headers, ext_modules=ext_modules, classifiers=[ + "Development Status :: 5 - Production/Stable", 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/greenlet-1.1.0/src/greenlet/__init__.py new/greenlet-1.1.2/src/greenlet/__init__.py --- old/greenlet-1.1.0/src/greenlet/__init__.py 2021-05-06 16:53:06.000000000 +0200 +++ new/greenlet-1.1.2/src/greenlet/__init__.py 2021-09-29 12:35:47.000000000 +0200 @@ -25,7 +25,7 @@ ### # Metadata ### -__version__ = '1.1.0' +__version__ = '1.1.2' from ._greenlet import _C_API # pylint:disable=no-name-in-module ### @@ -48,7 +48,8 @@ from ._greenlet import settrace except ImportError: # Tracing wasn't supported. - # TODO: Remove the option to disable it. + # XXX: The option to disable it was removed in 1.0, + # so this branch should be dead code. pass ### diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/greenlet-1.1.0/src/greenlet/greenlet.c new/greenlet-1.1.2/src/greenlet/greenlet.c --- old/greenlet-1.1.0/src/greenlet/greenlet.c 2021-05-06 16:53:06.000000000 +0200 +++ new/greenlet-1.1.2/src/greenlet/greenlet.c 2021-09-29 12:35:47.000000000 +0200 @@ -136,6 +136,11 @@ static PyObject* volatile ts_passaround_args = NULL; static PyObject* volatile ts_passaround_kwargs = NULL; +/* Used internally in ``g_switchstack()`` */ +#if GREENLET_USE_CFRAME +static int volatile ts__g_switchstack_use_tracing = 0; +#endif + /***********************************************************/ /* Thread-aware routines, switching global variables when needed */ @@ -195,6 +200,7 @@ } gmain->stack_start = (char*)1; gmain->stack_stop = (char*)-1; + /* GetDict() returns a borrowed reference. Make it strong. */ gmain->run_info = dict; Py_INCREF(dict); return gmain; @@ -254,6 +260,11 @@ it stores them in the thread dict; delete them now. */ deleteme = PyDict_GetItem(tstate->dict, ts_delkey); if (deleteme != NULL) { + /* The only reference to these greenlets should be in this list, so + clearing the list should let them be deleted again, triggering + calls to green_dealloc() in the correct thread. This may run + arbitrary Python code? + */ PyList_SetSlice(deleteme, 0, INT_MAX, NULL); } @@ -267,7 +278,6 @@ /* release an extra reference */ Py_DECREF(current); - /* restore current exception */ PyErr_Restore(exc, val, tb); @@ -276,7 +286,6 @@ if (ts_current->run_info != tstate->dict) { goto green_updatecurrent_restart; } - return 0; } @@ -481,26 +490,37 @@ return 0; } +/** + Perform a stack switch according to some global variables + that must be set before calling this function. Those variables + are: + + - ts_current: current greenlet (holds a reference) + - ts_target: greenlet to switch to (weak reference) + - ts_passaround_args: NULL if PyErr_Occurred(), + else a tuple of args sent to ts_target (holds a reference) + - ts_passaround_kwargs: switch kwargs (holds a reference) + + Because the stack switch happens in this function, this function can't use + its own stack (local) variables, set before the switch, and then accessed after the + switch. Global variables beginning with ``ts__g_switchstack`` are used + internally instead. + + On return results are passed via global variables as well: + + - ts_origin: originating greenlet (holds a reference) + - ts_current: current greenlet (holds a reference) + - ts_passaround_args: NULL if PyErr_Occurred(), + else a tuple of args sent to ts_current (holds a reference) + - ts_passaround_kwargs: switch kwargs (holds a reference) + + It is very important that stack switch is 'atomic', i.e. no + calls into other Python code allowed (except very few that + are safe), because global variables are very fragile. +*/ static int g_switchstack(void) { - /* Perform a stack switch according to some global variables - that must be set before: - - ts_current: current greenlet (holds a reference) - - ts_target: greenlet to switch to (weak reference) - - ts_passaround_args: NULL if PyErr_Occurred(), - else a tuple of args sent to ts_target (holds a reference) - - ts_passaround_kwargs: switch kwargs (holds a reference) - On return results are passed via global variables as well: - - ts_origin: originating greenlet (holds a reference) - - ts_current: current greenlet (holds a reference) - - ts_passaround_args: NULL if PyErr_Occurred(), - else a tuple of args sent to ts_current (holds a reference) - - ts_passaround_kwargs: switch kwargs (holds a reference) - It is very important that stack switch is 'atomic', i.e. no - calls into other Python code allowed (except very few that - are safe), because global variables are very fragile. - */ int err; { /* save state */ PyGreenlet* current = ts_current; @@ -519,10 +539,23 @@ current->exc_traceback = tstate->exc_traceback; #endif #if GREENLET_USE_CFRAME + /* + IMPORTANT: ``cframe`` is a pointer into the STACK. + Thus, because the call to ``slp_switch()`` + changes the contents of the stack, you cannot read from + ``ts_current->cframe`` after that call and necessarily + get the same values you get from reading it here. Anything + you need to restore from now to then must be saved + in a global variable (because we can't use stack variables + here either). + */ current->cframe = tstate->cframe; + ts__g_switchstack_use_tracing = tstate->cframe->use_tracing; #endif } + err = slp_switch(); + if (err < 0) { /* error */ PyGreenlet* current = ts_current; current->top_frame = NULL; @@ -566,6 +599,13 @@ #if GREENLET_USE_CFRAME tstate->cframe = target->cframe; + /* + If we were tracing, we need to keep tracing. + There should never be the possibility of hitting the + root_cframe here. See note above about why we can't + just copy this from ``origin->cframe->use_tracing``. + */ + tstate->cframe->use_tracing = ts__g_switchstack_use_tracing; #endif assert(ts_origin == NULL); @@ -678,10 +718,8 @@ PyObject* tracefunc; origin = ts_origin; ts_origin = NULL; - current = ts_current; - if ((tracefunc = PyDict_GetItem(current->run_info, ts_tracekey)) != - NULL) { + if ((tracefunc = PyDict_GetItem(current->run_info, ts_tracekey)) != NULL) { Py_INCREF(tracefunc); if (g_calltrace(tracefunc, args ? ts_event_switch : ts_event_throw, @@ -765,7 +803,15 @@ PyGreenlet* self = ts_target; PyObject* args = ts_passaround_args; PyObject* kwargs = ts_passaround_kwargs; - +#if GREENLET_USE_CFRAME + /* + See green_new(). This is a stack-allocated variable used + while *self* is in PyObject_Call(). + We want to defer copying the state info until we're sure + we need it and are in a stable place to do so. + */ + CFrame trace_info; +#endif /* save exception in case getattr clears it */ PyErr_Fetch(&exc, &val, &tb); /* self.run is the object to call in the new greenlet */ @@ -806,6 +852,17 @@ return 1; } +#if GREENLET_USE_CFRAME + /* OK, we need it, we're about to switch greenlets, save the state. */ + trace_info = *PyThreadState_GET()->cframe; + /* Make the target greenlet refer to the stack value. */ + self->cframe = &trace_info; + /* + And restore the link to the previous frame so this one gets + unliked appropriately. + */ + self->cframe->previous = &PyThreadState_GET()->root_cframe; +#endif /* start the greenlet */ self->stack_start = NULL; self->stack_stop = (char*)mark; @@ -829,8 +886,8 @@ err = g_switchstack(); /* returns twice! - The 1st time with err=1: we are in the new greenlet - The 2nd time with err=0: back in the caller's greenlet + The 1st time with ``err == 1``: we are in the new greenlet + The 2nd time with ``err <= 0``: back in the caller's greenlet */ if (err == 1) { /* in the new greenlet */ @@ -850,8 +907,7 @@ Py_INCREF(self->run_info); Py_XDECREF(o); - if ((tracefunc = PyDict_GetItem(self->run_info, ts_tracekey)) != - NULL) { + if ((tracefunc = PyDict_GetItem(self->run_info, ts_tracekey)) != NULL) { Py_INCREF(tracefunc); if (g_calltrace(tracefunc, args ? ts_event_switch : ts_event_throw, @@ -918,6 +974,47 @@ Py_INCREF(ts_current); ((PyGreenlet*)o)->parent = ts_current; #if GREENLET_USE_CFRAME + /* + The PyThreadState->cframe pointer usually points to memory on the + stack, alloceted in a call into PyEval_EvalFrameDefault. + + Initially, before any evaluation begins, it points to the initial + PyThreadState object's ``root_cframe`` object, which is statically + allocated for the lifetime of the thread. + + A greenlet can last for longer than a call to + PyEval_EvalFrameDefault, so we can't set its ``cframe`` pointer to + be the current ``PyThreadState->cframe``; nor could we use one from + the greenlet parent for the same reason. Yet a further no: we can't + allocate one scoped to the greenlet and then destroy it when the + greenlet is deallocated, because inside the interpreter the CFrame + objects form a linked list, and that too can result in accessing + memory beyond its dynamic lifetime (if the greenlet doesn't actually + finish before it dies, its entry could still be in the list). + + Using the ``root_cframe`` is problematic, though, because its + members are never modified by the interpreter and are set to 0, + meaning that its ``use_tracing`` flag is never updated. We don't + want to modify that value in the ``root_cframe`` ourself: it + *shouldn't* matter much because we should probably never get back to + the point where that's the only cframe on the stack; even if it did + matter, the major consequence of an incorrect value for + ``use_tracing`` is that if its true the interpreter does some extra + work --- however, it's just good code hygiene. + + Our solution: before a greenlet runs, after its initial creation, + it uses the ``root_cframe`` just to have something to put there. + However, once the greenlet is actually switched to for the first + time, ``g_initialstub`` (which doesn't actually "return" while the + greenlet is running) stores a new CFrame on its local stack, and + copies the appropriate values from the currently running CFrame; + this is then made the CFrame for the newly-minted greenlet. + ``g_initialstub`` then proceeds to call ``glet.run()``, which + results in ``PyEval_...`` adding the CFrame to the list. Switches + continue as normal. Finally, when the greenlet finishes, the call to + ``glet.run()`` returns and the CFrame is taken out of the linked + list and the stack value is now unused and free to expire. + */ ((PyGreenlet*)o)->cframe = &PyThreadState_GET()->root_cframe; #endif } @@ -988,10 +1085,16 @@ lst = PyDict_GetItem(self->run_info, ts_delkey); if (lst == NULL) { lst = PyList_New(0); - if (lst == NULL || - PyDict_SetItem(self->run_info, ts_delkey, lst) < 0) { + if (lst == NULL + || PyDict_SetItem(self->run_info, ts_delkey, lst) < 0) { return -1; } + /* PyDict_SetItem now holds a strong reference. PyList_New also + returned a fresh reference. We need to DECREF it now and let + the dictionary keep sole ownership. Frow now on, we're working + with a borrowed reference that will go away when the thread + dies. */ + Py_DECREF(lst); } if (PyList_Append(lst, (PyObject*)self) < 0) { return -1; @@ -1116,6 +1219,20 @@ /* Resurrected! */ _Py_NewReference((PyObject*)self); Py_SET_REFCNT(self, refcnt); + /* Better to use tp_finalizer slot (PEP 442) + * and call ``PyObject_CallFinalizerFromDealloc``, + * but that's only supported in Python 3.4+; see + * Modules/_io/iobase.c for an example. + * + * The following approach is copied from iobase.c in CPython 2.7. + * (along with much of this function in general). Here's their + * comment: + * + * When called from a heap type's dealloc, the type will be + * decref'ed on return (see e.g. subtype_dealloc in typeobject.c). */ + if (PyType_HasFeature(Py_TYPE(self), Py_TPFLAGS_HEAPTYPE)) { + Py_INCREF(Py_TYPE(self)); + } PyObject_GC_Track((PyObject*)self); @@ -1567,6 +1684,13 @@ #endif if (_green_not_dead(self)) { + /* XXX: The otid= is almost useless becasue you can't correlate it to + any thread identifier exposed to Python. We could use + PyThreadState_GET()->thread_id, but we'd need to save that in the + greenlet, or save the whole PyThreadState object itself. + + As it stands, its only useful for identifying greenlets from the same thread. + */ result = GNative_FromFormat( "<%s object at %p (otid=%p)%s%s%s%s>", Py_TYPE(self)->tp_name, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/greenlet-1.1.0/src/greenlet/tests/test_greenlet.py new/greenlet-1.1.2/src/greenlet/tests/test_greenlet.py --- old/greenlet-1.1.0/src/greenlet/tests/test_greenlet.py 2021-05-06 16:53:06.000000000 +0200 +++ new/greenlet-1.1.2/src/greenlet/tests/test_greenlet.py 2021-09-29 12:35:47.000000000 +0200 @@ -7,6 +7,8 @@ from greenlet import greenlet +# We manually manage locks in many tests +# pylint:disable=consider-using-with class SomeError(Exception): pass @@ -30,7 +32,7 @@ g1.switch(exc) -class GreenletTests(unittest.TestCase): +class TestGreenlet(unittest.TestCase): def test_simple(self): lst = [] @@ -549,6 +551,105 @@ if attempt(): break + def test_issue_245_reference_counting_subclass_no_threads(self): + # https://github.com/python-greenlet/greenlet/issues/245 + # Before the fix, this crashed pretty reliably on + # Python 3.10, at least on macOS; but much less reliably on other + # interpreters (memory layout must have changed). + # The threaded test crashed more reliably on more interpreters. + from greenlet import getcurrent + from greenlet import GreenletExit + + class Greenlet(greenlet): + pass + + initial_refs = sys.getrefcount(Greenlet) + # This has to be an instance variable because + # Python 2 raises a SyntaxError if we delete a local + # variable referenced in an inner scope. + self.glets = [] # pylint:disable=attribute-defined-outside-init + + def greenlet_main(): + try: + getcurrent().parent.switch() + except GreenletExit: + self.glets.append(getcurrent()) + + # Before the + for _ in range(10): + Greenlet(greenlet_main).switch() + + del self.glets + self.assertEqual(sys.getrefcount(Greenlet), initial_refs) + + def test_issue_245_reference_counting_subclass_threads(self): + # https://github.com/python-greenlet/greenlet/issues/245 + from threading import Thread + from threading import Event + + from greenlet import getcurrent + + class MyGreenlet(greenlet): + pass + + glets = [] + ref_cleared = Event() + + def greenlet_main(): + getcurrent().parent.switch() + + def thread_main(greenlet_running_event): + mine = MyGreenlet(greenlet_main) + glets.append(mine) + # The greenlets being deleted must be active + mine.switch() + # Don't keep any reference to it in this thread + del mine + # Let main know we published our greenlet. + greenlet_running_event.set() + # Wait for main to let us know the references are + # gone and the greenlet objects no longer reachable + ref_cleared.wait() + # The creating thread must call getcurrent() (or a few other + # greenlet APIs) because that's when the thread-local list of dead + # greenlets gets cleared. + getcurrent() + + # We start with 3 references to the subclass: + # - This module + # - Its __mro__ + # - The __subclassess__ attribute of greenlet + # - (If we call gc.get_referents(), we find four entries, including + # some other tuple ``(greenlet)`` that I'm not sure about but must be part + # of the machinery.) + # + # On Python 3.10 it's often enough to just run 3 threads; on Python 2.7, + # more threads are needed, and the results are still + # non-deterministic. Presumably the memory layouts are different + initial_refs = sys.getrefcount(MyGreenlet) + thread_ready_events = [] + for _ in range( + initial_refs + 45 + ): + event = Event() + thread = Thread(target=thread_main, args=(event,)) + thread_ready_events.append(event) + thread.start() + + + for done_event in thread_ready_events: + done_event.wait() + + + del glets[:] + ref_cleared.set() + # Let any other thread run; it will crash the interpreter + # if not fixed (or silently corrupt memory and we possibly crash + # later). + time.sleep(1) + self.assertEqual(sys.getrefcount(MyGreenlet), initial_refs) + + class TestRepr(unittest.TestCase): def assertEndsWith(self, got, suffix): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/greenlet-1.1.0/src/greenlet/tests/test_leaks.py new/greenlet-1.1.2/src/greenlet/tests/test_leaks.py --- old/greenlet-1.1.0/src/greenlet/tests/test_leaks.py 2021-05-06 16:53:06.000000000 +0200 +++ new/greenlet-1.1.2/src/greenlet/tests/test_leaks.py 2021-09-29 12:35:47.000000000 +0200 @@ -4,82 +4,175 @@ import time import weakref -import greenlet import threading +import greenlet + +class TestLeaks(unittest.TestCase): -class ArgRefcountTests(unittest.TestCase): def test_arg_refs(self): args = ('a', 'b', 'c') refcount_before = sys.getrefcount(args) + # pylint:disable=unnecessary-lambda g = greenlet.greenlet( lambda *args: greenlet.getcurrent().parent.switch(*args)) - for i in range(100): + for _ in range(100): g.switch(*args) self.assertEqual(sys.getrefcount(args), refcount_before) def test_kwarg_refs(self): kwargs = {} + # pylint:disable=unnecessary-lambda g = greenlet.greenlet( lambda **kwargs: greenlet.getcurrent().parent.switch(**kwargs)) - for i in range(100): + for _ in range(100): g.switch(**kwargs) self.assertEqual(sys.getrefcount(kwargs), 2) - if greenlet.GREENLET_USE_GC: - # These only work with greenlet gc support + assert greenlet.GREENLET_USE_GC # Option to disable this was removed in 1.0 - def recycle_threads(self): - # By introducing a thread that does sleep we allow other threads, - # that have triggered their __block condition, but did not have a - # chance to deallocate their thread state yet, to finally do so. - # The way it works is by requiring a GIL switch (different thread), - # which does a GIL release (sleep), which might do a GIL switch - # to finished threads and allow them to clean up. - def worker(): - time.sleep(0.001) + def recycle_threads(self): + # By introducing a thread that does sleep we allow other threads, + # that have triggered their __block condition, but did not have a + # chance to deallocate their thread state yet, to finally do so. + # The way it works is by requiring a GIL switch (different thread), + # which does a GIL release (sleep), which might do a GIL switch + # to finished threads and allow them to clean up. + def worker(): + time.sleep(0.001) + t = threading.Thread(target=worker) + t.start() + time.sleep(0.001) + t.join() + + def test_threaded_leak(self): + gg = [] + def worker(): + # only main greenlet present + gg.append(weakref.ref(greenlet.getcurrent())) + for _ in range(2): t = threading.Thread(target=worker) t.start() - time.sleep(0.001) t.join() - - def test_threaded_leak(self): - gg = [] - def worker(): - # only main greenlet present - gg.append(weakref.ref(greenlet.getcurrent())) - for i in range(2): - t = threading.Thread(target=worker) - t.start() - t.join() - del t - greenlet.getcurrent() # update ts_current - self.recycle_threads() - greenlet.getcurrent() # update ts_current - gc.collect() - greenlet.getcurrent() # update ts_current - for g in gg: - self.assertTrue(g() is None) - - def test_threaded_adv_leak(self): - gg = [] - def worker(): - # main and additional *finished* greenlets - ll = greenlet.getcurrent().ll = [] - def additional(): - ll.append(greenlet.getcurrent()) - for i in range(2): - greenlet.greenlet(additional).switch() - gg.append(weakref.ref(greenlet.getcurrent())) - for i in range(2): - t = threading.Thread(target=worker) - t.start() - t.join() - del t - greenlet.getcurrent() # update ts_current - self.recycle_threads() - greenlet.getcurrent() # update ts_current + del t + greenlet.getcurrent() # update ts_current + self.recycle_threads() + greenlet.getcurrent() # update ts_current + gc.collect() + greenlet.getcurrent() # update ts_current + for g in gg: + self.assertIsNone(g()) + + def test_threaded_adv_leak(self): + gg = [] + def worker(): + # main and additional *finished* greenlets + ll = greenlet.getcurrent().ll = [] + def additional(): + ll.append(greenlet.getcurrent()) + for _ in range(2): + greenlet.greenlet(additional).switch() + gg.append(weakref.ref(greenlet.getcurrent())) + for _ in range(2): + t = threading.Thread(target=worker) + t.start() + t.join() + del t + greenlet.getcurrent() # update ts_current + self.recycle_threads() + greenlet.getcurrent() # update ts_current + gc.collect() + greenlet.getcurrent() # update ts_current + for g in gg: + self.assertIsNone(g()) + + def test_issue251_killing_cross_thread_leaks_list(self, manually_collect_background=True): + # See https://github.com/python-greenlet/greenlet/issues/251 + # Killing a greenlet (probably not the main one) + # in one thread from another thread would + # result in leaking a list (the ts_delkey list). + + # For the test to be valid, even empty lists have to be tracked by the + # GC + assert gc.is_tracked([]) + + def count_objects(kind=list): + # pylint:disable=unidiomatic-typecheck + # Collect the garbage. + for _ in range(3): + gc.collect() gc.collect() - greenlet.getcurrent() # update ts_current - for g in gg: - self.assertTrue(g() is None) + return sum( + 1 + for x in gc.get_objects() + if type(x) is kind + ) + + # XXX: The main greenlet of a dead thread is only released + # when one of the proper greenlet APIs is used from a different + # running thread. See #252 (https://github.com/python-greenlet/greenlet/issues/252) + greenlet.getcurrent() + greenlets_before = count_objects(greenlet.greenlet) + + background_glet_running = threading.Event() + background_glet_killed = threading.Event() + background_greenlets = [] + def background_greenlet(): + # Throw control back to the main greenlet. + greenlet.getcurrent().parent.switch() + + def background_thread(): + glet = greenlet.greenlet(background_greenlet) + background_greenlets.append(glet) + glet.switch() # Be sure it's active. + # Control is ours again. + del glet # Delete one reference from the thread it runs in. + background_glet_running.set() + background_glet_killed.wait() + # To trigger the background collection of the dead + # greenlet, thus clearing out the contents of the list, we + # need to run some APIs. See issue 252. + if manually_collect_background: + greenlet.getcurrent() + + + t = threading.Thread(target=background_thread) + t.start() + background_glet_running.wait() + + lists_before = count_objects() + + assert len(background_greenlets) == 1 + self.assertFalse(background_greenlets[0].dead) + # Delete the last reference to the background greenlet + # from a different thread. This puts it in the background thread's + # ts_delkey list. + del background_greenlets[:] + background_glet_killed.set() + + # Now wait for the background thread to die. + t.join(10) + del t + + # Free the background main greenlet by forcing greenlet to notice a difference. + greenlet.getcurrent() + greenlets_after = count_objects(greenlet.greenlet) + + lists_after = count_objects() + # On 2.7, we observe that lists_after is smaller than + # lists_before. No idea what lists got cleaned up. All the + # Python 3 versions match exactly. + self.assertLessEqual(lists_after, lists_before) + + self.assertEqual(greenlets_before, greenlets_after) + + @unittest.expectedFailure + def test_issue251_issue252_need_to_collect_in_background(self): + # This still fails because the leak of the list + # still exists when we don't call a greenlet API before exiting the + # thread. The proximate cause is that neither of the two greenlets + # from the background thread are actually being destroyed, even though + # the GC is in fact visiting both objects. + # It's not clear where that leak is? For some reason the thread-local dict + # holding it isn't being cleaned up. + self.test_issue251_killing_cross_thread_leaks_list(manually_collect_background=False) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/greenlet-1.1.0/src/greenlet/tests/test_tracing.py new/greenlet-1.1.2/src/greenlet/tests/test_tracing.py --- old/greenlet-1.1.0/src/greenlet/tests/test_tracing.py 2021-05-06 16:53:06.000000000 +0200 +++ new/greenlet-1.1.2/src/greenlet/tests/test_tracing.py 2021-09-29 12:35:47.000000000 +0200 @@ -1,52 +1,267 @@ +import sys import unittest -import threading import greenlet class SomeError(Exception): pass -class TracingTests(unittest.TestCase): - if greenlet.GREENLET_USE_TRACING: - def test_greenlet_tracing(self): - main = greenlet.getcurrent() - actions = [] - def trace(*args): - actions.append(args) - def dummy(): - pass - def dummyexc(): - raise SomeError() - oldtrace = greenlet.settrace(trace) - try: - g1 = greenlet.greenlet(dummy) - g1.switch() - g2 = greenlet.greenlet(dummyexc) - self.assertRaises(SomeError, g2.switch) - finally: - greenlet.settrace(oldtrace) - self.assertEqual(actions, [ - ('switch', (main, g1)), - ('switch', (g1, main)), - ('switch', (main, g2)), - ('throw', (g2, main)), - ]) - - def test_exception_disables_tracing(self): - main = greenlet.getcurrent() - actions = [] - def trace(*args): - actions.append(args) - raise SomeError() - def dummy(): - main.switch() - g = greenlet.greenlet(dummy) - g.switch() - oldtrace = greenlet.settrace(trace) - try: - self.assertRaises(SomeError, g.switch) - self.assertEqual(greenlet.gettrace(), None) - finally: - greenlet.settrace(oldtrace) - self.assertEqual(actions, [ - ('switch', (main, g)), - ]) +class GreenletTracer(object): + oldtrace = None + + def __init__(self, error_on_trace=False): + self.actions = [] + self.error_on_trace = error_on_trace + + def __call__(self, *args): + self.actions.append(args) + if self.error_on_trace: + raise SomeError + + def __enter__(self): + self.oldtrace = greenlet.settrace(self) + return self.actions + + def __exit__(self, *args): + greenlet.settrace(self.oldtrace) + + +class TestGreenletTracing(unittest.TestCase): + """ + Tests of ``greenlet.settrace()`` + """ + + def test_greenlet_tracing(self): + main = greenlet.getcurrent() + def dummy(): + pass + def dummyexc(): + raise SomeError() + + with GreenletTracer() as actions: + g1 = greenlet.greenlet(dummy) + g1.switch() + g2 = greenlet.greenlet(dummyexc) + self.assertRaises(SomeError, g2.switch) + + self.assertEqual(actions, [ + ('switch', (main, g1)), + ('switch', (g1, main)), + ('switch', (main, g2)), + ('throw', (g2, main)), + ]) + + def test_exception_disables_tracing(self): + main = greenlet.getcurrent() + def dummy(): + main.switch() + g = greenlet.greenlet(dummy) + g.switch() + with GreenletTracer(error_on_trace=True) as actions: + self.assertRaises(SomeError, g.switch) + self.assertEqual(greenlet.gettrace(), None) + + self.assertEqual(actions, [ + ('switch', (main, g)), + ]) + + +class PythonTracer(object): + oldtrace = None + + def __init__(self): + self.actions = [] + + def __call__(self, frame, event, arg): + # Record the co_name so we have an idea what function we're in. + self.actions.append((event, frame.f_code.co_name)) + + def __enter__(self): + self.oldtrace = sys.setprofile(self) + return self.actions + + def __exit__(self, *args): + sys.setprofile(self.oldtrace) + +def tpt_callback(): + return 42 + +class TestPythonTracing(unittest.TestCase): + """ + Tests of the interaction of ``sys.settrace()`` + with greenlet facilities. + + NOTE: Most of this is probably CPython specific. + """ + + maxDiff = None + + def test_trace_events_trivial(self): + with PythonTracer() as actions: + tpt_callback() + # If we use the sys.settrace instead of setprofile, we get + # this: + + # self.assertEqual(actions, [ + # ('call', 'tpt_callback'), + # ('call', '__exit__'), + # ]) + + self.assertEqual(actions, [ + ('return', '__enter__'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + def _trace_switch(self, glet): + with PythonTracer() as actions: + glet.switch() + return actions + + def _check_trace_events_func_already_set(self, glet): + actions = self._trace_switch(glet) + self.assertEqual(actions, [ + ('return', '__enter__'), + ('c_call', '_trace_switch'), + ('call', 'run'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('return', 'run'), + ('c_return', '_trace_switch'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + def test_trace_events_into_greenlet_func_already_set(self): + def run(): + return tpt_callback() + + self._check_trace_events_func_already_set(greenlet.greenlet(run)) + + def test_trace_events_into_greenlet_subclass_already_set(self): + class X(greenlet.greenlet): + def run(self): + return tpt_callback() + self._check_trace_events_func_already_set(X()) + + def _check_trace_events_from_greenlet_sets_profiler(self, g, tracer): + g.switch() + tpt_callback() + tracer.__exit__() + self.assertEqual(tracer.actions, [ + ('return', '__enter__'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('return', 'run'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + + def test_trace_events_from_greenlet_func_sets_profiler(self): + tracer = PythonTracer() + def run(): + tracer.__enter__() + return tpt_callback() + + self._check_trace_events_from_greenlet_sets_profiler(greenlet.greenlet(run), + tracer) + + def test_trace_events_from_greenlet_subclass_sets_profiler(self): + tracer = PythonTracer() + class X(greenlet.greenlet): + def run(self): + tracer.__enter__() + return tpt_callback() + + self._check_trace_events_from_greenlet_sets_profiler(X(), tracer) + + + def test_trace_events_multiple_greenlets_switching(self): + tracer = PythonTracer() + + g1 = None + g2 = None + + def g1_run(): + tracer.__enter__() + tpt_callback() + g2.switch() + tpt_callback() + return 42 + + def g2_run(): + tpt_callback() + tracer.__exit__() + tpt_callback() + g1.switch() + + g1 = greenlet.greenlet(g1_run) + g2 = greenlet.greenlet(g2_run) + + x = g1.switch() + self.assertEqual(x, 42) + tpt_callback() # ensure not in the trace + self.assertEqual(tracer.actions, [ + ('return', '__enter__'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('c_call', 'g1_run'), + ('call', 'g2_run'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + def test_trace_events_multiple_greenlets_switching_siblings(self): + # Like the first version, but get both greenlets running first + # as "siblings" and then establish the tracing. + tracer = PythonTracer() + + g1 = None + g2 = None + + def g1_run(): + greenlet.getcurrent().parent.switch() + tracer.__enter__() + tpt_callback() + g2.switch() + tpt_callback() + return 42 + + def g2_run(): + greenlet.getcurrent().parent.switch() + + tpt_callback() + tracer.__exit__() + tpt_callback() + g1.switch() + + g1 = greenlet.greenlet(g1_run) + g2 = greenlet.greenlet(g2_run) + + # Start g1 + g1.switch() + # And it immediately returns control to us. + # Start g2 + g2.switch() + # Which also returns. Now kick of the real part of the + # test. + x = g1.switch() + self.assertEqual(x, 42) + + tpt_callback() # ensure not in the trace + self.assertEqual(tracer.actions, [ + ('return', '__enter__'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('c_call', 'g1_run'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/greenlet-1.1.0/src/greenlet.egg-info/PKG-INFO new/greenlet-1.1.2/src/greenlet.egg-info/PKG-INFO --- old/greenlet-1.1.0/src/greenlet.egg-info/PKG-INFO 2021-05-06 16:53:07.000000000 +0200 +++ new/greenlet-1.1.2/src/greenlet.egg-info/PKG-INFO 2021-09-29 12:35:47.000000000 +0200 @@ -1,8 +1,12 @@ Metadata-Version: 2.1 Name: greenlet -Version: 1.1.0 +Version: 1.1.2 Summary: Lightweight in-process concurrent programming Home-page: https://greenlet.readthedocs.io/ +Author: Alexey Borzenkov +Author-email: sna...@gmail.com +Maintainer: Jason Madden +Maintainer-email: ja...@nextthought.com License: MIT License Project-URL: Bug Tracker, https://github.com/python-greenlet/greenlet/issues Project-URL: Source Code, https://github.com/python-greenlet/greenlet/ @@ -69,7 +73,9 @@ Documentation is available on readthedocs.org: https://greenlet.readthedocs.io +Keywords: greenlet coroutine concurrency threads cooperative Platform: any +Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/greenlet-1.1.0/tox.ini new/greenlet-1.1.2/tox.ini --- old/greenlet-1.1.0/tox.ini 2021-05-06 16:53:06.000000000 +0200 +++ new/greenlet-1.1.2/tox.ini 2021-09-29 12:35:47.000000000 +0200 @@ -11,12 +11,18 @@ test docs +[testenv:py310] +# See tests.yml +commands = + python -m unittest discover -v greenlet.tests + [testenv:docs] # usedevelop to save rebuilding the extension usedevelop = true +# We can't use Python 3.10 yet, so specify fully what we need. basepython = - python3 + python3.9 commands = sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest