summaryrefslogtreecommitdiffstats
path: root/lib/greenlet/tests/test_leaks.py
diff options
context:
space:
mode:
authorxiubuzhe <xiubuzhe@sina.com>2023-10-08 20:59:00 +0800
committerxiubuzhe <xiubuzhe@sina.com>2023-10-08 20:59:00 +0800
commit1dac2263372df2b85db5d029a45721fa158a5c9d (patch)
tree0365f9c57df04178a726d7584ca6a6b955a7ce6a /lib/greenlet/tests/test_leaks.py
parentb494be364bb39e1de128ada7dc576a729d99907e (diff)
downloadsunhpc-1dac2263372df2b85db5d029a45721fa158a5c9d.tar.gz
sunhpc-1dac2263372df2b85db5d029a45721fa158a5c9d.tar.bz2
sunhpc-1dac2263372df2b85db5d029a45721fa158a5c9d.zip
first add files
Diffstat (limited to 'lib/greenlet/tests/test_leaks.py')
-rw-r--r--lib/greenlet/tests/test_leaks.py178
1 files changed, 178 insertions, 0 deletions
diff --git a/lib/greenlet/tests/test_leaks.py b/lib/greenlet/tests/test_leaks.py
new file mode 100644
index 0000000..2b02bfd
--- /dev/null
+++ b/lib/greenlet/tests/test_leaks.py
@@ -0,0 +1,178 @@
+import unittest
+import sys
+import gc
+
+import time
+import weakref
+import threading
+
+import greenlet
+
+class TestLeaks(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 _ 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 _ in range(100):
+ g.switch(**kwargs)
+ self.assertEqual(sys.getrefcount(kwargs), 2)
+
+ 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)
+ 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()
+ 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_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()
+ 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)