test_leaks.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. # -*- coding: utf-8 -*-
  2. """
  3. Testing scenarios that may have leaked.
  4. """
  5. from __future__ import print_function, absolute_import, division
  6. import sys
  7. import gc
  8. import time
  9. import weakref
  10. import threading
  11. import greenlet
  12. from . import TestCase
  13. from . import PY314
  14. from . import RUNNING_ON_FREETHREAD_BUILD
  15. from .leakcheck import fails_leakcheck
  16. from .leakcheck import ignores_leakcheck
  17. from .leakcheck import RUNNING_ON_MANYLINUX
  18. # pylint:disable=protected-access
  19. assert greenlet.GREENLET_USE_GC # Option to disable this was removed in 1.0
  20. class HasFinalizerTracksInstances(object):
  21. EXTANT_INSTANCES = set()
  22. def __init__(self, msg):
  23. self.msg = sys.intern(msg)
  24. self.EXTANT_INSTANCES.add(id(self))
  25. def __del__(self):
  26. self.EXTANT_INSTANCES.remove(id(self))
  27. def __repr__(self):
  28. return "<HasFinalizerTracksInstances at 0x%x %r>" % (
  29. id(self), self.msg
  30. )
  31. @classmethod
  32. def reset(cls):
  33. cls.EXTANT_INSTANCES.clear()
  34. def fails_leakcheck_except_on_free_thraded(func):
  35. if RUNNING_ON_FREETHREAD_BUILD:
  36. # These all seem to pass on free threading because
  37. # of the changes to the garbage collector
  38. return func
  39. return fails_leakcheck(func)
  40. class TestLeaks(TestCase):
  41. def test_arg_refs(self):
  42. args = ('a', 'b', 'c')
  43. refcount_before = sys.getrefcount(args)
  44. # pylint:disable=unnecessary-lambda
  45. g = greenlet.greenlet(
  46. lambda *args: greenlet.getcurrent().parent.switch(*args))
  47. for _ in range(100):
  48. g.switch(*args)
  49. self.assertEqual(sys.getrefcount(args), refcount_before)
  50. def test_kwarg_refs(self):
  51. kwargs = {}
  52. self.assertEqual(sys.getrefcount(kwargs), 2 if not PY314 else 1)
  53. # pylint:disable=unnecessary-lambda
  54. g = greenlet.greenlet(
  55. lambda **gkwargs: greenlet.getcurrent().parent.switch(**gkwargs))
  56. for _ in range(100):
  57. g.switch(**kwargs)
  58. # Python 3.14 elides reference counting operations
  59. # in some cases. See https://github.com/python/cpython/pull/130708
  60. self.assertEqual(sys.getrefcount(kwargs), 2 if not PY314 else 1)
  61. @staticmethod
  62. def __recycle_threads():
  63. # By introducing a thread that does sleep we allow other threads,
  64. # that have triggered their __block condition, but did not have a
  65. # chance to deallocate their thread state yet, to finally do so.
  66. # The way it works is by requiring a GIL switch (different thread),
  67. # which does a GIL release (sleep), which might do a GIL switch
  68. # to finished threads and allow them to clean up.
  69. def worker():
  70. time.sleep(0.001)
  71. t = threading.Thread(target=worker)
  72. t.start()
  73. time.sleep(0.001)
  74. t.join(10)
  75. def test_threaded_leak(self):
  76. gg = []
  77. def worker():
  78. # only main greenlet present
  79. gg.append(weakref.ref(greenlet.getcurrent()))
  80. for _ in range(2):
  81. t = threading.Thread(target=worker)
  82. t.start()
  83. t.join(10)
  84. del t
  85. greenlet.getcurrent() # update ts_current
  86. self.__recycle_threads()
  87. greenlet.getcurrent() # update ts_current
  88. gc.collect()
  89. greenlet.getcurrent() # update ts_current
  90. for g in gg:
  91. self.assertIsNone(g())
  92. def test_threaded_adv_leak(self):
  93. gg = []
  94. def worker():
  95. # main and additional *finished* greenlets
  96. ll = greenlet.getcurrent().ll = []
  97. def additional():
  98. ll.append(greenlet.getcurrent())
  99. for _ in range(2):
  100. greenlet.greenlet(additional).switch()
  101. gg.append(weakref.ref(greenlet.getcurrent()))
  102. for _ in range(2):
  103. t = threading.Thread(target=worker)
  104. t.start()
  105. t.join(10)
  106. del t
  107. greenlet.getcurrent() # update ts_current
  108. self.__recycle_threads()
  109. greenlet.getcurrent() # update ts_current
  110. gc.collect()
  111. greenlet.getcurrent() # update ts_current
  112. for g in gg:
  113. self.assertIsNone(g())
  114. def assertClocksUsed(self):
  115. used = greenlet._greenlet.get_clocks_used_doing_optional_cleanup()
  116. self.assertGreaterEqual(used, 0)
  117. # we don't lose the value
  118. greenlet._greenlet.enable_optional_cleanup(True)
  119. used2 = greenlet._greenlet.get_clocks_used_doing_optional_cleanup()
  120. self.assertEqual(used, used2)
  121. self.assertGreater(greenlet._greenlet.CLOCKS_PER_SEC, 1)
  122. def _check_issue251(self,
  123. manually_collect_background=True,
  124. explicit_reference_to_switch=False):
  125. # See https://github.com/python-greenlet/greenlet/issues/251
  126. # Killing a greenlet (probably not the main one)
  127. # in one thread from another thread would
  128. # result in leaking a list (the ts_delkey list).
  129. # We no longer use lists to hold that stuff, though.
  130. # For the test to be valid, even empty lists have to be tracked by the
  131. # GC
  132. assert gc.is_tracked([])
  133. HasFinalizerTracksInstances.reset()
  134. greenlet.getcurrent()
  135. greenlets_before = self.count_objects(greenlet.greenlet, exact_kind=False)
  136. background_glet_running = threading.Event()
  137. background_glet_killed = threading.Event()
  138. background_greenlets = []
  139. # XXX: Switching this to a greenlet subclass that overrides
  140. # run results in all callers failing the leaktest; that
  141. # greenlet instance is leaked. There's a bound method for
  142. # run() living on the stack of the greenlet in g_initialstub,
  143. # and since we don't manually switch back to the background
  144. # greenlet to let it "fall off the end" and exit the
  145. # g_initialstub function, it never gets cleaned up. Making the
  146. # garbage collector aware of this bound method (making it an
  147. # attribute of the greenlet structure and traversing into it)
  148. # doesn't help, for some reason.
  149. def background_greenlet():
  150. # Throw control back to the main greenlet.
  151. jd = HasFinalizerTracksInstances("DELETING STACK OBJECT")
  152. greenlet._greenlet.set_thread_local(
  153. 'test_leaks_key',
  154. HasFinalizerTracksInstances("DELETING THREAD STATE"))
  155. # Explicitly keeping 'switch' in a local variable
  156. # breaks this test in all versions
  157. if explicit_reference_to_switch:
  158. s = greenlet.getcurrent().parent.switch
  159. s([jd])
  160. else:
  161. greenlet.getcurrent().parent.switch([jd])
  162. bg_main_wrefs = []
  163. def background_thread():
  164. glet = greenlet.greenlet(background_greenlet)
  165. bg_main_wrefs.append(weakref.ref(glet.parent))
  166. background_greenlets.append(glet)
  167. glet.switch() # Be sure it's active.
  168. # Control is ours again.
  169. del glet # Delete one reference from the thread it runs in.
  170. background_glet_running.set()
  171. background_glet_killed.wait(10)
  172. # To trigger the background collection of the dead
  173. # greenlet, thus clearing out the contents of the list, we
  174. # need to run some APIs. See issue 252.
  175. if manually_collect_background:
  176. greenlet.getcurrent()
  177. t = threading.Thread(target=background_thread)
  178. t.start()
  179. background_glet_running.wait(10)
  180. greenlet.getcurrent()
  181. lists_before = self.count_objects(list, exact_kind=True)
  182. assert len(background_greenlets) == 1
  183. self.assertFalse(background_greenlets[0].dead)
  184. # Delete the last reference to the background greenlet
  185. # from a different thread. This puts it in the background thread's
  186. # ts_delkey list.
  187. del background_greenlets[:]
  188. background_glet_killed.set()
  189. # Now wait for the background thread to die.
  190. t.join(10)
  191. del t
  192. # As part of the fix for 252, we need to cycle the ceval.c
  193. # interpreter loop to be sure it has had a chance to process
  194. # the pending call.
  195. self.wait_for_pending_cleanups()
  196. lists_after = self.count_objects(list, exact_kind=True)
  197. greenlets_after = self.count_objects(greenlet.greenlet, exact_kind=False)
  198. # On 2.7, we observe that lists_after is smaller than
  199. # lists_before. No idea what lists got cleaned up. All the
  200. # Python 3 versions match exactly.
  201. self.assertLessEqual(lists_after, lists_before)
  202. # On versions after 3.6, we've successfully cleaned up the
  203. # greenlet references thanks to the internal "vectorcall"
  204. # protocol; prior to that, there is a reference path through
  205. # the ``greenlet.switch`` method still on the stack that we
  206. # can't reach to clean up. The C code goes through terrific
  207. # lengths to clean that up.
  208. if not explicit_reference_to_switch \
  209. and greenlet._greenlet.get_clocks_used_doing_optional_cleanup() is not None:
  210. # If cleanup was disabled, though, we may not find it.
  211. self.assertEqual(greenlets_after, greenlets_before)
  212. if manually_collect_background:
  213. # TODO: Figure out how to make this work!
  214. # The one on the stack is still leaking somehow
  215. # in the non-manually-collect state.
  216. self.assertEqual(HasFinalizerTracksInstances.EXTANT_INSTANCES, set())
  217. else:
  218. # The explicit reference prevents us from collecting it
  219. # and it isn't always found by the GC either for some
  220. # reason. The entire frame is leaked somehow, on some
  221. # platforms (e.g., MacPorts builds of Python (all
  222. # versions!)), but not on other platforms (the linux and
  223. # windows builds on GitHub actions and Appveyor). So we'd
  224. # like to write a test that proves that the main greenlet
  225. # sticks around, and we can on my machine (macOS 11.6,
  226. # MacPorts builds of everything) but we can't write that
  227. # same test on other platforms. However, hopefully iteration
  228. # done by leakcheck will find it.
  229. pass
  230. if greenlet._greenlet.get_clocks_used_doing_optional_cleanup() is not None:
  231. self.assertClocksUsed()
  232. def test_issue251_killing_cross_thread_leaks_list(self):
  233. self._check_issue251()
  234. def test_issue251_with_cleanup_disabled(self):
  235. greenlet._greenlet.enable_optional_cleanup(False)
  236. try:
  237. self._check_issue251()
  238. finally:
  239. greenlet._greenlet.enable_optional_cleanup(True)
  240. @fails_leakcheck_except_on_free_thraded
  241. def test_issue251_issue252_need_to_collect_in_background(self):
  242. # Between greenlet 1.1.2 and the next version, this was still
  243. # failing because the leak of the list still exists when we
  244. # don't call a greenlet API before exiting the thread. The
  245. # proximate cause is that neither of the two greenlets from
  246. # the background thread are actually being destroyed, even
  247. # though the GC is in fact visiting both objects. It's not
  248. # clear where that leak is? For some reason the thread-local
  249. # dict holding it isn't being cleaned up.
  250. #
  251. # The leak, I think, is in the CPYthon internal function that
  252. # calls into green_switch(). The argument tuple is still on
  253. # the C stack somewhere and can't be reached? That doesn't
  254. # make sense, because the tuple should be collectable when
  255. # this object goes away.
  256. #
  257. # Note that this test sometimes spuriously passes on Linux,
  258. # for some reason, but I've never seen it pass on macOS.
  259. self._check_issue251(manually_collect_background=False)
  260. @fails_leakcheck_except_on_free_thraded
  261. def test_issue251_issue252_need_to_collect_in_background_cleanup_disabled(self):
  262. self.expect_greenlet_leak = True
  263. greenlet._greenlet.enable_optional_cleanup(False)
  264. try:
  265. self._check_issue251(manually_collect_background=False)
  266. finally:
  267. greenlet._greenlet.enable_optional_cleanup(True)
  268. @fails_leakcheck_except_on_free_thraded
  269. def test_issue251_issue252_explicit_reference_not_collectable(self):
  270. self._check_issue251(
  271. manually_collect_background=False,
  272. explicit_reference_to_switch=True)
  273. UNTRACK_ATTEMPTS = 100
  274. def _only_test_some_versions(self):
  275. # We're only looking for this problem specifically on 3.11,
  276. # and this set of tests is relatively fragile, depending on
  277. # OS and memory management details. So we want to run it on 3.11+
  278. # (obviously) but not every older 3.x version in order to reduce
  279. # false negatives. At the moment, those false results seem to have
  280. # resolved, so we are actually running this on 3.8+
  281. assert sys.version_info[0] >= 3
  282. if sys.version_info[:2] < (3, 8):
  283. self.skipTest('Only observed on 3.11')
  284. if RUNNING_ON_MANYLINUX:
  285. self.skipTest("Slow and not worth repeating here")
  286. @ignores_leakcheck
  287. # Because we're just trying to track raw memory, not objects, and running
  288. # the leakcheck makes an already slow test slower.
  289. def test_untracked_memory_doesnt_increase(self):
  290. # See https://github.com/gevent/gevent/issues/1924
  291. # and https://github.com/python-greenlet/greenlet/issues/328
  292. self._only_test_some_versions()
  293. def f():
  294. return 1
  295. ITER = 10000
  296. def run_it():
  297. for _ in range(ITER):
  298. greenlet.greenlet(f).switch()
  299. # Establish baseline
  300. for _ in range(3):
  301. run_it()
  302. # uss: (Linux, macOS, Windows): aka "Unique Set Size", this is
  303. # the memory which is unique to a process and which would be
  304. # freed if the process was terminated right now.
  305. uss_before = self.get_process_uss()
  306. for count in range(self.UNTRACK_ATTEMPTS):
  307. uss_before = max(uss_before, self.get_process_uss())
  308. run_it()
  309. uss_after = self.get_process_uss()
  310. if uss_after <= uss_before and count > 1:
  311. break
  312. self.assertLessEqual(uss_after, uss_before)
  313. def _check_untracked_memory_thread(self, deallocate_in_thread=True):
  314. self._only_test_some_versions()
  315. # Like the above test, but what if there are a bunch of
  316. # unfinished greenlets in a thread that dies?
  317. # Does it matter if we deallocate in the thread or not?
  318. EXIT_COUNT = [0]
  319. def f():
  320. try:
  321. greenlet.getcurrent().parent.switch()
  322. except greenlet.GreenletExit:
  323. EXIT_COUNT[0] += 1
  324. raise
  325. return 1
  326. ITER = 10000
  327. def run_it():
  328. glets = []
  329. for _ in range(ITER):
  330. # Greenlet starts, switches back to us.
  331. # We keep a strong reference to the greenlet though so it doesn't
  332. # get a GreenletExit exception.
  333. g = greenlet.greenlet(f)
  334. glets.append(g)
  335. g.switch()
  336. return glets
  337. test = self
  338. class ThreadFunc:
  339. uss_before = uss_after = 0
  340. glets = ()
  341. ITER = 2
  342. def __call__(self):
  343. self.uss_before = test.get_process_uss()
  344. for _ in range(self.ITER):
  345. self.glets += tuple(run_it())
  346. for g in self.glets:
  347. test.assertIn('suspended active', str(g))
  348. # Drop them.
  349. if deallocate_in_thread:
  350. self.glets = ()
  351. self.uss_after = test.get_process_uss()
  352. # Establish baseline
  353. uss_before = uss_after = None
  354. for count in range(self.UNTRACK_ATTEMPTS):
  355. EXIT_COUNT[0] = 0
  356. thread_func = ThreadFunc()
  357. t = threading.Thread(target=thread_func)
  358. t.start()
  359. t.join(30)
  360. self.assertFalse(t.is_alive())
  361. if uss_before is None:
  362. uss_before = thread_func.uss_before
  363. uss_before = max(uss_before, thread_func.uss_before)
  364. if deallocate_in_thread:
  365. self.assertEqual(thread_func.glets, ())
  366. self.assertEqual(EXIT_COUNT[0], ITER * thread_func.ITER)
  367. del thread_func # Deallocate the greenlets; but this won't raise into them
  368. del t
  369. if not deallocate_in_thread:
  370. self.assertEqual(EXIT_COUNT[0], 0)
  371. if deallocate_in_thread:
  372. self.wait_for_pending_cleanups()
  373. uss_after = self.get_process_uss()
  374. # See if we achieve a non-growth state at some point. Break when we do.
  375. if uss_after <= uss_before and count > 1:
  376. break
  377. self.wait_for_pending_cleanups()
  378. uss_after = self.get_process_uss()
  379. self.assertLessEqual(uss_after, uss_before, "after attempts %d" % (count,))
  380. @ignores_leakcheck
  381. # Because we're just trying to track raw memory, not objects, and running
  382. # the leakcheck makes an already slow test slower.
  383. def test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_thread(self):
  384. self._check_untracked_memory_thread(deallocate_in_thread=True)
  385. @ignores_leakcheck
  386. # Because the main greenlets from the background threads do not exit in a timely fashion,
  387. # we fail the object-based leakchecks.
  388. def test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_main(self):
  389. self._check_untracked_memory_thread(deallocate_in_thread=False)
  390. if __name__ == '__main__':
  391. __import__('unittest').main()