about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet_trash.py
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet_trash.py
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet_trash.py')
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet_trash.py187
1 files changed, 187 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet_trash.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet_trash.py
new file mode 100644
index 00000000..c1fc1374
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet_trash.py
@@ -0,0 +1,187 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for greenlets interacting with the CPython trash can API.
+
+The CPython trash can API is not designed to be re-entered from a
+single thread. But this can happen using greenlets, if something
+during the object deallocation process switches greenlets, and this second
+greenlet then causes the trash can to get entered again. Here, we do this
+very explicitly, but in other cases (like gevent) it could be arbitrarily more
+complicated: for example, a weakref callback might try to acquire a lock that's
+already held by another greenlet; that would allow a greenlet switch to occur.
+
+See https://github.com/gevent/gevent/issues/1909
+
+This test is fragile and relies on details of the CPython
+implementation (like most of the rest of this package):
+
+    - We enter the trashcan and deferred deallocation after
+      ``_PyTrash_UNWIND_LEVEL`` calls. This constant, defined in
+      CPython's object.c, is generally 50. That's basically how many objects are required to
+      get us into the deferred deallocation situation.
+
+    - The test fails by hitting an ``assert()`` in object.c; if the
+      build didn't enable assert, then we don't catch this.
+
+    - If the test fails in that way, the interpreter crashes.
+"""
+from __future__ import print_function, absolute_import, division
+
+import unittest
+
+
+class TestTrashCanReEnter(unittest.TestCase):
+
+    def test_it(self):
+        try:
+            # pylint:disable-next=no-name-in-module
+            from greenlet._greenlet import get_tstate_trash_delete_nesting # pylint:disable=unused-import
+        except ImportError:
+            import sys
+            # Python 3.13 has not "trash delete nesting" anymore (but "delete later")
+            assert sys.version_info[:2] >= (3, 13)
+            self.skipTest("get_tstate_trash_delete_nesting is not available.")
+
+        # Try several times to trigger it, because it isn't 100%
+        # reliable.
+        for _ in range(10):
+            self.check_it()
+
+    def check_it(self): # pylint:disable=too-many-statements
+        import greenlet
+        from greenlet._greenlet import get_tstate_trash_delete_nesting # pylint:disable=no-name-in-module
+        main = greenlet.getcurrent()
+
+        assert get_tstate_trash_delete_nesting() == 0
+
+        # We expect to be in deferred deallocation after this many
+        # deallocations have occurred. TODO: I wish we had a better way to do
+        # this --- that was before get_tstate_trash_delete_nesting; perhaps
+        # we can use that API to do better?
+        TRASH_UNWIND_LEVEL = 50
+        # How many objects to put in a container; it's the container that
+        # queues objects for deferred deallocation.
+        OBJECTS_PER_CONTAINER = 500
+
+        class Dealloc: # define the class here because we alter class variables each time we run.
+            """
+            An object with a ``__del__`` method. When it starts getting deallocated
+            from a deferred trash can run, it switches greenlets, allocates more objects
+            which then also go in the trash can. If we don't save state appropriately,
+            nesting gets out of order and we can crash the interpreter.
+            """
+
+            #: Has our deallocation actually run and switched greenlets?
+            #: When it does, this will be set to the current greenlet. This should
+            #: be happening in the main greenlet, so we check that down below.
+            SPAWNED = False
+
+            #: Has the background greenlet run?
+            BG_RAN = False
+
+            BG_GLET = None
+
+            #: How many of these things have ever been allocated.
+            CREATED = 0
+
+            #: How many of these things have ever been deallocated.
+            DESTROYED = 0
+
+            #: How many were destroyed not in the main greenlet. There should always
+            #: be some.
+            #: If the test is broken or things change in the trashcan implementation,
+            #: this may not be correct.
+            DESTROYED_BG = 0
+
+            def __init__(self, sequence_number):
+                """
+                :param sequence_number: The ordinal of this object during
+                   one particular creation run. This is used to detect (guess, really)
+                   when we have entered the trash can's deferred deallocation.
+                """
+                self.i = sequence_number
+                Dealloc.CREATED += 1
+
+            def __del__(self):
+                if self.i == TRASH_UNWIND_LEVEL and not self.SPAWNED:
+                    Dealloc.SPAWNED = greenlet.getcurrent()
+                    other = Dealloc.BG_GLET = greenlet.greenlet(background_greenlet)
+                    x = other.switch()
+                    assert x == 42
+                    # It's important that we don't switch back to the greenlet,
+                    # we leave it hanging there in an incomplete state. But we don't let it
+                    # get collected, either. If we complete it now, while we're still
+                    # in the scope of the initial trash can, things work out and we
+                    # don't see the problem. We need this greenlet to complete
+                    # at some point in the future, after we've exited this trash can invocation.
+                    del other
+                elif self.i == 40 and greenlet.getcurrent() is not main:
+                    Dealloc.BG_RAN = True
+                    try:
+                        main.switch(42)
+                    except greenlet.GreenletExit as ex:
+                        # We expect this; all references to us go away
+                        # while we're still running, and we need to finish deleting
+                        # ourself.
+                        Dealloc.BG_RAN = type(ex)
+                        del ex
+
+                # Record the fact that we're dead last of all. This ensures that
+                # we actually get returned too.
+                Dealloc.DESTROYED += 1
+                if greenlet.getcurrent() is not main:
+                    Dealloc.DESTROYED_BG += 1
+
+
+        def background_greenlet():
+            # We direct through a second function, instead of
+            # directly calling ``make_some()``, so that we have complete
+            # control over when these objects are destroyed: we need them
+            # to be destroyed in the context of the background greenlet
+            t = make_some()
+            del t # Triggere deletion.
+
+        def make_some():
+            t = ()
+            i = OBJECTS_PER_CONTAINER
+            while i:
+                # Nest the tuples; it's the recursion that gets us
+                # into trash.
+                t = (Dealloc(i), t)
+                i -= 1
+            return t
+
+
+        some = make_some()
+        self.assertEqual(Dealloc.CREATED, OBJECTS_PER_CONTAINER)
+        self.assertEqual(Dealloc.DESTROYED, 0)
+
+        # If we're going to crash, it should be on the following line.
+        # We only crash if ``assert()`` is enabled, of course.
+        del some
+
+        # For non-debug builds of CPython, we won't crash. The best we can do is check
+        # the nesting level explicitly.
+        self.assertEqual(0, get_tstate_trash_delete_nesting())
+
+        # Discard this, raising GreenletExit into where it is waiting.
+        Dealloc.BG_GLET = None
+        # The same nesting level maintains.
+        self.assertEqual(0, get_tstate_trash_delete_nesting())
+
+        # We definitely cleaned some up in the background
+        self.assertGreater(Dealloc.DESTROYED_BG, 0)
+
+        # Make sure all the cleanups happened.
+        self.assertIs(Dealloc.SPAWNED, main)
+        self.assertTrue(Dealloc.BG_RAN)
+        self.assertEqual(Dealloc.BG_RAN, greenlet.GreenletExit)
+        self.assertEqual(Dealloc.CREATED, Dealloc.DESTROYED )
+        self.assertEqual(Dealloc.CREATED, OBJECTS_PER_CONTAINER * 2)
+
+        import gc
+        gc.collect()
+
+
+if __name__ == '__main__':
+    unittest.main()