Skip to content

Remove the atexit callback.#508

Open
jamadden wants to merge 1 commit intomasterfrom
issue507-remove-atexit
Open

Remove the atexit callback.#508
jamadden wants to merge 1 commit intomasterfrom
issue507-remove-atexit

Conversation

@jamadden
Copy link
Copy Markdown
Contributor

@jamadden jamadden commented Apr 24, 2026

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) are gone. 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.

Fixes #506. Fixes #507.

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.
@godlygeek
Copy link
Copy Markdown

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.

@jamadden
Copy link
Copy Markdown
Contributor Author

would you consider yanking 3.4.0?

I am considering that.

@nbouvrette
Copy link
Copy Markdown
Contributor

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 (004e1e9, reporting 3.4.1.dev0) and ran:

  • Python 3.11 ARM64 production-style profile, max-requests=500, 3,000 requests
  • Python 3.11 ARM64 direct greenlet-use profile, 3,000 requests
  • Python 3.10 direct greenlet-use profile, 3,000 requests

All passed with zero segfaults, zero Accessing state after destruction errors, and successful worker recycling.

So from my side, this looks correct: removing the early atexit callback does not appear to regress the crash scenarios I was trying to fix in #499, while it fixes the broader issue that atexit callbacks should still be able to use greenlet while the interpreter is still fully operational.

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 atexit phase.

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.

Segfault with greenlet 3.4.0 and gevent monkeypatches 3.4.0 having trouble with filelock

3 participants