test_interpreter_shutdown.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests for greenlet behavior during interpreter shutdown (Py_FinalizeEx).
  4. Prior to the safe finalization fix, active greenlets being deallocated
  5. during interpreter shutdown could trigger SIGSEGV or SIGABRT on Python
  6. < 3.11, because green_dealloc attempted to throw GreenletExit via
  7. g_switch() into a partially-torn-down interpreter.
  8. The fix adds _Py_IsFinalizing() guards (on Python < 3.11 only) that
  9. call murder_in_place() instead of g_switch() when the interpreter is
  10. shutting down, avoiding the crash at the cost of not running cleanup
  11. code inside the greenlet.
  12. These tests verify:
  13. 1. No crashes on ANY Python version (the core safety guarantee).
  14. 2. GreenletExit cleanup code runs correctly during normal thread exit
  15. (the standard production path, e.g. uWSGI worker threads).
  16. """
  17. import sys
  18. import subprocess
  19. import unittest
  20. import textwrap
  21. from greenlet.tests import TestCase
  22. class TestInterpreterShutdown(TestCase):
  23. def _run_shutdown_script(self, script_body):
  24. """
  25. Run a Python script in a subprocess that exercises greenlet
  26. during interpreter shutdown. Returns (returncode, stdout, stderr).
  27. """
  28. full_script = textwrap.dedent(script_body)
  29. result = subprocess.run(
  30. [sys.executable, '-c', full_script],
  31. capture_output=True,
  32. text=True,
  33. timeout=30,
  34. check=False,
  35. )
  36. return result.returncode, result.stdout, result.stderr
  37. # -----------------------------------------------------------------
  38. # Core safety tests: no crashes on any Python version
  39. # -----------------------------------------------------------------
  40. def test_active_greenlet_at_shutdown_no_crash(self):
  41. """
  42. An active (suspended) greenlet that is deallocated during
  43. interpreter shutdown should not crash the process.
  44. Before the fix, this would SIGSEGV on Python < 3.11 because
  45. _green_dealloc_kill_started_non_main_greenlet tried to call
  46. g_switch() during Py_FinalizeEx.
  47. """
  48. rc, stdout, stderr = self._run_shutdown_script("""\
  49. import greenlet
  50. def worker():
  51. greenlet.getcurrent().parent.switch("from worker")
  52. return "done"
  53. g = greenlet.greenlet(worker)
  54. result = g.switch()
  55. assert result == "from worker", result
  56. print("OK: exiting with active greenlet")
  57. """)
  58. self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
  59. self.assertIn("OK: exiting with active greenlet", stdout)
  60. def test_multiple_active_greenlets_at_shutdown(self):
  61. """
  62. Multiple suspended greenlets at shutdown should all be cleaned
  63. up without crashing.
  64. """
  65. rc, stdout, stderr = self._run_shutdown_script("""\
  66. import greenlet
  67. def worker(name):
  68. greenlet.getcurrent().parent.switch(f"hello from {name}")
  69. return "done"
  70. greenlets = []
  71. for i in range(10):
  72. g = greenlet.greenlet(worker)
  73. result = g.switch(f"g{i}")
  74. greenlets.append(g)
  75. print(f"OK: {len(greenlets)} active greenlets at shutdown")
  76. """)
  77. self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
  78. self.assertIn("OK: 10 active greenlets at shutdown", stdout)
  79. def test_nested_greenlets_at_shutdown(self):
  80. """
  81. Nested (chained parent) greenlets at shutdown should not crash.
  82. """
  83. rc, stdout, stderr = self._run_shutdown_script("""\
  84. import greenlet
  85. def inner():
  86. greenlet.getcurrent().parent.switch("inner done")
  87. def outer():
  88. g_inner = greenlet.greenlet(inner)
  89. g_inner.switch()
  90. greenlet.getcurrent().parent.switch("outer done")
  91. g = greenlet.greenlet(outer)
  92. result = g.switch()
  93. assert result == "outer done", result
  94. print("OK: nested greenlets at shutdown")
  95. """)
  96. self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
  97. self.assertIn("OK: nested greenlets at shutdown", stdout)
  98. def test_threaded_greenlets_at_shutdown(self):
  99. """
  100. Greenlets in worker threads that are still referenced at
  101. shutdown should not crash.
  102. """
  103. rc, stdout, stderr = self._run_shutdown_script("""\
  104. import greenlet
  105. import threading
  106. results = []
  107. def thread_worker():
  108. def greenlet_func():
  109. greenlet.getcurrent().parent.switch("from thread greenlet")
  110. return "done"
  111. g = greenlet.greenlet(greenlet_func)
  112. val = g.switch()
  113. results.append((g, val))
  114. threads = []
  115. for _ in range(3):
  116. t = threading.Thread(target=thread_worker)
  117. t.start()
  118. threads.append(t)
  119. for t in threads:
  120. t.join()
  121. print(f"OK: {len(results)} threaded greenlets at shutdown")
  122. """)
  123. self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
  124. self.assertIn("OK: 3 threaded greenlets at shutdown", stdout)
  125. # -----------------------------------------------------------------
  126. # Cleanup semantics tests
  127. # -----------------------------------------------------------------
  128. #
  129. # Note on behavioral testing during interpreter shutdown:
  130. #
  131. # During Py_FinalizeEx, sys.stdout is set to None early, making
  132. # print() a no-op. More importantly, an active greenlet in the
  133. # module-level scope interferes with module dict clearing — the
  134. # greenlet's dealloc path (which temporarily resurrects the object
  135. # and performs a stack switch via g_switch) prevents reliable
  136. # observation of cleanup behavior.
  137. #
  138. # The production crash (SIGSEGV/SIGABRT) occurs during thread-state
  139. # cleanup in Py_FinalizeEx, not during module dict clearing. Our
  140. # _Py_IsFinalizing() guard in _green_dealloc_kill_started_non_main_
  141. # greenlet targets that path. The safety tests above verify that no
  142. # crashes occur; the tests below verify that greenlet cleanup works
  143. # correctly during normal thread exit (the most common code path).
  144. def test_greenlet_cleanup_during_thread_exit(self):
  145. """
  146. When a thread exits normally while holding active greenlets,
  147. GreenletExit IS thrown and cleanup code runs. This is the
  148. standard cleanup path used in production (e.g. uWSGI worker
  149. threads finishing a request).
  150. """
  151. rc, stdout, stderr = self._run_shutdown_script("""\
  152. import os
  153. import threading
  154. import greenlet
  155. _write = os.write
  156. def thread_func():
  157. def worker(_w=_write,
  158. _GreenletExit=greenlet.GreenletExit):
  159. try:
  160. greenlet.getcurrent().parent.switch("suspended")
  161. except _GreenletExit:
  162. _w(1, b"CLEANUP: GreenletExit caught\\n")
  163. raise
  164. g = greenlet.greenlet(worker)
  165. g.switch()
  166. # Thread exits with active greenlet -> thread-state
  167. # cleanup triggers GreenletExit
  168. t = threading.Thread(target=thread_func)
  169. t.start()
  170. t.join()
  171. print("OK: thread cleanup done")
  172. """)
  173. self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
  174. self.assertIn("OK: thread cleanup done", stdout)
  175. self.assertIn("CLEANUP: GreenletExit caught", stdout)
  176. def test_finally_block_during_thread_exit(self):
  177. """
  178. try/finally blocks in active greenlets run correctly when the
  179. owning thread exits.
  180. """
  181. rc, stdout, stderr = self._run_shutdown_script("""\
  182. import os
  183. import threading
  184. import greenlet
  185. _write = os.write
  186. def thread_func():
  187. def worker(_w=_write):
  188. try:
  189. greenlet.getcurrent().parent.switch("suspended")
  190. finally:
  191. _w(1, b"FINALLY: cleanup executed\\n")
  192. g = greenlet.greenlet(worker)
  193. g.switch()
  194. t = threading.Thread(target=thread_func)
  195. t.start()
  196. t.join()
  197. print("OK: thread cleanup done")
  198. """)
  199. self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
  200. self.assertIn("OK: thread cleanup done", stdout)
  201. self.assertIn("FINALLY: cleanup executed", stdout)
  202. def test_many_greenlets_with_cleanup_at_shutdown(self):
  203. """
  204. Stress test: many active greenlets with cleanup code at shutdown.
  205. Ensures no crashes regardless of deallocation order.
  206. """
  207. rc, stdout, stderr = self._run_shutdown_script("""\
  208. import sys
  209. import greenlet
  210. cleanup_count = 0
  211. def worker(idx):
  212. global cleanup_count
  213. try:
  214. greenlet.getcurrent().parent.switch(f"ready-{idx}")
  215. except greenlet.GreenletExit:
  216. cleanup_count += 1
  217. raise
  218. greenlets = []
  219. for i in range(50):
  220. g = greenlet.greenlet(worker)
  221. result = g.switch(i)
  222. greenlets.append(g)
  223. print(f"OK: {len(greenlets)} greenlets about to shut down")
  224. # Note: we can't easily print cleanup_count during shutdown
  225. # since it happens after the main module's code runs.
  226. """)
  227. self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
  228. self.assertIn("OK: 50 greenlets about to shut down", stdout)
  229. def test_deeply_nested_greenlets_at_shutdown(self):
  230. """
  231. Deeply nested greenlet parent chains at shutdown.
  232. Tests that the deallocation order doesn't cause issues.
  233. """
  234. rc, stdout, stderr = self._run_shutdown_script("""\
  235. import greenlet
  236. def level(depth, max_depth):
  237. if depth < max_depth:
  238. g = greenlet.greenlet(level)
  239. g.switch(depth + 1, max_depth)
  240. greenlet.getcurrent().parent.switch(f"depth-{depth}")
  241. g = greenlet.greenlet(level)
  242. result = g.switch(0, 10)
  243. print(f"OK: nested to depth 10, got {result}")
  244. """)
  245. self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
  246. self.assertIn("OK: nested to depth 10", stdout)
  247. def test_greenlet_with_traceback_at_shutdown(self):
  248. """
  249. A greenlet that has an active exception context when it's
  250. suspended should not crash during shutdown cleanup.
  251. """
  252. rc, stdout, stderr = self._run_shutdown_script("""\
  253. import greenlet
  254. def worker():
  255. try:
  256. raise ValueError("test error")
  257. except ValueError:
  258. # Suspend while an exception is active on the stack
  259. greenlet.getcurrent().parent.switch("suspended with exc")
  260. return "done"
  261. g = greenlet.greenlet(worker)
  262. result = g.switch()
  263. assert result == "suspended with exc"
  264. print("OK: greenlet with active exception at shutdown")
  265. """)
  266. self.assertEqual(rc, 0, f"Process crashed (rc={rc}):\n{stdout}{stderr}")
  267. self.assertIn("OK: greenlet with active exception at shutdown", stdout)
  268. if __name__ == '__main__':
  269. unittest.main()