You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

179 lines
6.5 KiB

  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)