Conversation
This callback caused greenlet APIs to become unavailable far too soon during interpreter shutdown. Now they remain available while all ``atexit`` callbacks run; using greenlet APIs from atexit callbacks registered in any order is a valid thing to do and happens naturally with gevent monkey-patching. A careful, thorough reading of the CPython documentation and the CPython source code for all supported versions of Python (3.10+) indicates that any gating needing to be done is correctly handled with ``Py_IsFinalizing``. The comments in PR #499 that added the callback were wrong: it's not possible to access partially torn-down state during ``atexit``. And the tests provided in that PR did not demonstrate any crashes (they pass with or without the ``atexit`` callback). CPython shuts things down in the following order: 1. Attempt to wait for all non-daemon threads to finish. 2. Invoke any pending calls. 3. Invoke ``atexit`` callbacks. At this point, it is guaranteed that the interpreter is still fully operational, the import machinery still works, etc. All such callbacks can successfully use greenlet APIs. 4. Finalize any sub-intepreters in newer versions. greenlet doesn't support sub-interpreters, so this is inconsequential. 5. Detach any remaining threads in newer versions. 6. Set ``Py_IsFinalizing`` to true. Any other threads still remaining will no longer be able to run Python code. All cleanup operations continue in this thread. At this point, ``getcurrent`` will start returning ``None`` or raising an exception (C API). 7. Garbage collect threads (active objects on the call stack). 8. Run cyclic garbage collection. 9. Only now does the interpreter begin to tear down module state, beginning by clearing out module dictionaries and allowing finalizers/weakref to be cleared. Up to and through the beginning of this process, greenlet APIs are safe to call, and greenlet objects can be used (switched/thrown). At some point, this may become untrue, but all unreachable greenlet objects (which should be anything not stashed away in a C extension). greenlet can't do anything about extant objects that may still have methods called on them, but it can prevent getting access to implicit objects that may be getting torn down: that's why getcurrent behaves the way it does (any exceptions generated during at least steps 7, 8, 9 are "unraisable" and just get printed). Note that the CPython documentation specifically calls out the fact that modules may be finalized in any order, so modules that rely on other modules MUST be coded defensively. The long and short is that greenlet can't do anything reasonable to protect other modules from accessing state that may be torn down (``atexit`` is too soon; a PyCapsule destructor may be too late or never get fired; module ``m_clear`` and ``m_free`` functions may never get called). It's up to other C modules to check for interpreter finalization and be aware that any other C modules they use may no longer be valid at that point.
|
If the conclusion is that 3.4.0's approach is simply buggy, then would you consider yanking 3.4.0? Either before or after 3.5.0 is available on PyPI, that makes no difference to me. |
I am considering that. |
|
Thanks for the detailed explanation @jamadden, and sorry for the oversight in PR #499. I re-tested the code from this PR locally against the same uWSGI worker-recycling crash reproducer I used to validate #499. I built greenlet from this PR's branch (
All passed with zero segfaults, zero So from my side, this looks correct: removing the early Again, apologies for encoding the wrong shutdown assumption into the #499 tests. The tests did go red/green, but they were asserting the wrong contract for the |
This callback caused greenlet APIs to become unavailable far too soon during interpreter shutdown. Now they remain available while all
atexitcallbacks run; using greenlet APIs from atexit callbacks registered in any order is a valid thing to do and happens naturally with gevent monkey-patching.A careful, thorough reading of the CPython documentation and the CPython source code for all supported versions of Python (3.10+) indicates that any gating needing to be done is correctly handled with
Py_IsFinalizing.The comments in PR #499 that added the callback were wrong: it's not possible to access partially torn-down state during
atexit. And the tests provided in that PR did not demonstrate any crashes (they pass with or without theatexitcallback).CPython shuts things down in the following order:
atexitcallbacks. At this point, it is guaranteed that the interpreter is still fully operational, the import machinery still works, etc. All such callbacks can successfully use greenlet APIs.Py_IsFinalizingto true. Any other threads still remaining will no longer be able to run Python code. All cleanup operations continue in this thread. At this point,getcurrentwill start returningNoneor raising an exception (C API).The long and short is that greenlet can't do anything reasonable to protect other modules from accessing state that may be torn down (
atexitis too soon; a PyCapsule destructor may be too late or never get fired; modulem_clearandm_freefunctions may never get called). It's up to other C modules to check for interpreter finalization and be aware that any other C modules they use may no longer be valid at that point.Fixes #506. Fixes #507.