test_leaks.py 18 KB

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