test_leaks.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import unittest
  2. import sys
  3. import gc
  4. import time
  5. import weakref
  6. import threading
  7. import greenlet
  8. class TestLeaks(unittest.TestCase):
  9. def test_arg_refs(self):
  10. args = ('a', 'b', 'c')
  11. refcount_before = sys.getrefcount(args)
  12. # pylint:disable=unnecessary-lambda
  13. g = greenlet.greenlet(
  14. lambda *args: greenlet.getcurrent().parent.switch(*args))
  15. for _ in range(100):
  16. g.switch(*args)
  17. self.assertEqual(sys.getrefcount(args), refcount_before)
  18. def test_kwarg_refs(self):
  19. kwargs = {}
  20. # pylint:disable=unnecessary-lambda
  21. g = greenlet.greenlet(
  22. lambda **kwargs: greenlet.getcurrent().parent.switch(**kwargs))
  23. for _ in range(100):
  24. g.switch(**kwargs)
  25. self.assertEqual(sys.getrefcount(kwargs), 2)
  26. assert greenlet.GREENLET_USE_GC # Option to disable this was removed in 1.0
  27. def recycle_threads(self):
  28. # By introducing a thread that does sleep we allow other threads,
  29. # that have triggered their __block condition, but did not have a
  30. # chance to deallocate their thread state yet, to finally do so.
  31. # The way it works is by requiring a GIL switch (different thread),
  32. # which does a GIL release (sleep), which might do a GIL switch
  33. # to finished threads and allow them to clean up.
  34. def worker():
  35. time.sleep(0.001)
  36. t = threading.Thread(target=worker)
  37. t.start()
  38. time.sleep(0.001)
  39. t.join()
  40. def test_threaded_leak(self):
  41. gg = []
  42. def worker():
  43. # only main greenlet present
  44. gg.append(weakref.ref(greenlet.getcurrent()))
  45. for _ in range(2):
  46. t = threading.Thread(target=worker)
  47. t.start()
  48. t.join()
  49. del t
  50. greenlet.getcurrent() # update ts_current
  51. self.recycle_threads()
  52. greenlet.getcurrent() # update ts_current
  53. gc.collect()
  54. greenlet.getcurrent() # update ts_current
  55. for g in gg:
  56. self.assertIsNone(g())
  57. def test_threaded_adv_leak(self):
  58. gg = []
  59. def worker():
  60. # main and additional *finished* greenlets
  61. ll = greenlet.getcurrent().ll = []
  62. def additional():
  63. ll.append(greenlet.getcurrent())
  64. for _ in range(2):
  65. greenlet.greenlet(additional).switch()
  66. gg.append(weakref.ref(greenlet.getcurrent()))
  67. for _ in range(2):
  68. t = threading.Thread(target=worker)
  69. t.start()
  70. t.join()
  71. del t
  72. greenlet.getcurrent() # update ts_current
  73. self.recycle_threads()
  74. greenlet.getcurrent() # update ts_current
  75. gc.collect()
  76. greenlet.getcurrent() # update ts_current
  77. for g in gg:
  78. self.assertIsNone(g())
  79. def test_issue251_killing_cross_thread_leaks_list(self, manually_collect_background=True):
  80. # See https://github.com/python-greenlet/greenlet/issues/251
  81. # Killing a greenlet (probably not the main one)
  82. # in one thread from another thread would
  83. # result in leaking a list (the ts_delkey list).
  84. # For the test to be valid, even empty lists have to be tracked by the
  85. # GC
  86. assert gc.is_tracked([])
  87. def count_objects(kind=list):
  88. # pylint:disable=unidiomatic-typecheck
  89. # Collect the garbage.
  90. for _ in range(3):
  91. gc.collect()
  92. gc.collect()
  93. return sum(
  94. 1
  95. for x in gc.get_objects()
  96. if type(x) is kind
  97. )
  98. # XXX: The main greenlet of a dead thread is only released
  99. # when one of the proper greenlet APIs is used from a different
  100. # running thread. See #252 (https://github.com/python-greenlet/greenlet/issues/252)
  101. greenlet.getcurrent()
  102. greenlets_before = count_objects(greenlet.greenlet)
  103. background_glet_running = threading.Event()
  104. background_glet_killed = threading.Event()
  105. background_greenlets = []
  106. def background_greenlet():
  107. # Throw control back to the main greenlet.
  108. greenlet.getcurrent().parent.switch()
  109. def background_thread():
  110. glet = greenlet.greenlet(background_greenlet)
  111. background_greenlets.append(glet)
  112. glet.switch() # Be sure it's active.
  113. # Control is ours again.
  114. del glet # Delete one reference from the thread it runs in.
  115. background_glet_running.set()
  116. background_glet_killed.wait()
  117. # To trigger the background collection of the dead
  118. # greenlet, thus clearing out the contents of the list, we
  119. # need to run some APIs. See issue 252.
  120. if manually_collect_background:
  121. greenlet.getcurrent()
  122. t = threading.Thread(target=background_thread)
  123. t.start()
  124. background_glet_running.wait()
  125. lists_before = count_objects()
  126. assert len(background_greenlets) == 1
  127. self.assertFalse(background_greenlets[0].dead)
  128. # Delete the last reference to the background greenlet
  129. # from a different thread. This puts it in the background thread's
  130. # ts_delkey list.
  131. del background_greenlets[:]
  132. background_glet_killed.set()
  133. # Now wait for the background thread to die.
  134. t.join(10)
  135. del t
  136. # Free the background main greenlet by forcing greenlet to notice a difference.
  137. greenlet.getcurrent()
  138. greenlets_after = count_objects(greenlet.greenlet)
  139. lists_after = count_objects()
  140. # On 2.7, we observe that lists_after is smaller than
  141. # lists_before. No idea what lists got cleaned up. All the
  142. # Python 3 versions match exactly.
  143. self.assertLessEqual(lists_after, lists_before)
  144. self.assertEqual(greenlets_before, greenlets_after)
  145. @unittest.expectedFailure
  146. def test_issue251_issue252_need_to_collect_in_background(self):
  147. # This still fails because the leak of the list
  148. # still exists when we don't call a greenlet API before exiting the
  149. # thread. The proximate cause is that neither of the two greenlets
  150. # from the background thread are actually being destroyed, even though
  151. # the GC is in fact visiting both objects.
  152. # It's not clear where that leak is? For some reason the thread-local dict
  153. # holding it isn't being cleaned up.
  154. self.test_issue251_killing_cross_thread_leaks_list(manually_collect_background=False)