|
| 1 | +import resource |
1 | 2 | import sys |
2 | 3 | import os |
3 | 4 | import threading |
4 | 5 | import time |
5 | 6 | import traceback |
6 | 7 | import gc |
7 | 8 |
|
| 9 | + |
| 10 | + |
| 11 | +# To Compile Python |
| 12 | +""" |
| 13 | +mkdir -p /home/odoo/Documents/Pythons/py12 && |
| 14 | +./configure --prefix=/home/odoo/Documents/Pythons/py12 --enable-optimizations && |
| 15 | +make --quiet clean && |
| 16 | +make --quiet -j 10 && |
| 17 | +make --quiet altinstall |
| 18 | +""" |
| 19 | + |
| 20 | +""" |
| 21 | +./configure --enable-optimizations && |
| 22 | +make --quiet clean && |
| 23 | +make --quiet -j 10 |
| 24 | +""" |
| 25 | + |
| 26 | + |
| 27 | + |
8 | 28 | """ |
9 | 29 | Current explaination: |
10 | 30 |
|
11 | | -When one `handler` Thread is hard kill between after `_start_new_thread` and before `_started.set()` (inside `_bootstrap_inner`): |
12 | | -Two issues happens |
13 | | -- The first issue, is that `threading.enumerate` returns it, but it shouldn't since it is not alive: |
14 | | -That's because `_limbo` is not clean if hard kill (finally clause of `_bootstrap_inner` is bypass). Weird but not blocking |
| 31 | +When one `handler` Thread is killed (not enough memory) between after starting the new thread (thread_run C code) |
| 32 | +and before `_started.set()` Python (inside `_bootstrap_inner` or just before in the C threaded code): |
| 33 | +
|
| 34 | +Leading into deadlock situation the serving Thread wait the `_started` to be set, but |
| 35 | +it will never be since the specific Thread is dead... Also if this dead Thread remains in |
| 36 | +the _limbo dictionary (small inconsistency since it will be return in case of threading.enumarate()). |
| 37 | +
|
| 38 | +When the thread is killed inside before `_started.set()`, we always get this weird error message: |
| 39 | +"Exception ignored in thread started by: <A method>" |
| 40 | +This comes from the `thread_run` (C code) calling `_PyErr_WriteUnraisableMsg` |
| 41 | +At first glance, it seems that the ignored Exception is a MemoryError that skip part of the `_bootstrap_inner` method |
| 42 | +
|
| 43 | +What we tried: |
| 44 | +- Increasing the stack_size to force Python to only accept create new Thread when more |
| 45 | +memory is available on the stack. That's doesn't seems to work even with a hug number... |
| 46 | +Does this method even work ?? Signature is odd and the doc is incomplete ?? |
| 47 | +
|
| 48 | +- Catch the MemoryError, to sleep + gc after in order to force free memory => doesn't work better |
| 49 | +
|
| 50 | +- Biscept on stable version, but is is reproductible: |
| 51 | + - Python 3.8.0b1: Reproductible but seems more rare? |
| 52 | + - Our version 3.12.3: Reproductible |
| 53 | + - Python 3.12.11+: Reproductible |
| 54 | + - Python 3.13.7+: Reproductible |
| 55 | + - Python 3.14.0rc2+: Reproductible |
| 56 | + - Python main/3.15.0a0: Reproductible |
15 | 57 |
|
16 | | -- The Thread of `serving` is stuck waiting that self._started of the killed Thread becomes free, but it will never be ... |
| 58 | +- Find a magic solution to bypass `self._started.wait()`. The only 'working' solution is to add |
| 59 | +a timeout (1 sec) on it that recheck the status of the Thread. But is fragile, because a timeout |
| 60 | +is always a trade-off (after 1 sec is long and still not ensure that the Thread has been effectively |
| 61 | +killed). |
17 | 62 |
|
| 63 | +
|
| 64 | +Deadlock traceback |
| 65 | +------------------ |
18 | 66 | File "/home/odoo/Documents/Pythons/py12_11/lib/python3.12/threading.py", line 1033, in _bootstrap |
19 | 67 | self._bootstrap_inner() |
20 | 68 | File "/home/odoo/Documents/Pythons/py12_11/lib/python3.12/threading.py", line 1078, in _bootstrap_inner |
|
30 | 78 | File "/home/odoo/Documents/Pythons/py12_11/lib/python3.12/threading.py", line 355, in wait |
31 | 79 | waiter.acquire() |
32 | 80 |
|
33 | | -Because the Thread is hard kill by the OS, should it release his lock and then setting _started ??? |
34 | | -
|
| 81 | +TODO: |
| 82 | +- Check inside the ignored Exception with a debugger C |
| 83 | +- Check works stack_size |
| 84 | +- |
35 | 85 |
|
36 | 86 | """ |
| 87 | +threading.stack_size(33000) |
37 | 88 |
|
| 89 | +print("PID: ", os.getpid(), " Stack Size ", threading.stack_size()) |
38 | 90 |
|
| 91 | +# print("Sleep 10 sec") |
| 92 | +# time.sleep(10) |
39 | 93 |
|
40 | | - |
41 | | - |
42 | | - |
43 | | -print("PID: ", os.getpid()) |
44 | | -print("Sleep 10 sec") |
45 | | -time.sleep(1) |
46 | | - |
47 | | - |
48 | | - |
49 | | -# |
50 | | - |
51 | | - |
52 | | - |
| 94 | +resource.setrlimit(resource.RLIMIT_AS, (1_000_000_000, 1_500_000_000)) |
53 | 95 |
|
54 | 96 | def memory_error(): |
55 | 97 | memory_issue_list = [] |
56 | 98 | while True: |
57 | | - memory_issue_list.append("a" * 10_000) |
| 99 | + memory_issue_list.append("a" * 150_000) |
58 | 100 |
|
59 | 101 | def handler(): |
| 102 | + time.sleep(0.001) |
60 | 103 | try: |
61 | 104 | memory_error() |
62 | 105 | except MemoryError: |
63 | | - print("MemoryError - stop") |
| 106 | + print(f"MemoryError in {threading.current_thread()}") |
64 | 107 | # gc.collect() |
65 | | - |
| 108 | + # time.sleep(0.1) |
66 | 109 |
|
67 | 110 | def serving(): |
68 | 111 | for _ in range(100): |
69 | | - t = threading.Thread(target=handler) |
70 | | - print('Start Sub-Thread') |
71 | | - t.start() |
| 112 | + try: |
| 113 | + t = threading.Thread(target=handler) |
| 114 | + print(f'Start Thread: {t}') |
| 115 | + t.start() |
| 116 | + except RuntimeError: |
| 117 | + print('RuntimeError', {t}) |
72 | 118 |
|
73 | 119 | print('Start Serving') |
74 | 120 | serving_thread = threading.Thread(target=serving, daemon=True) |
75 | 121 | serving_thread.start() |
76 | 122 |
|
| 123 | + |
| 124 | +time.sleep(10) |
77 | 125 | while serving_thread.is_alive(): |
78 | | - time.sleep(2) |
79 | 126 | for alive_thread in threading.enumerate(): |
80 | 127 | print(alive_thread, alive_thread.is_alive(), alive_thread.ident, alive_thread.native_id) |
81 | 128 | if not alive_thread.is_alive() and serving_thread.is_alive(): |
82 | 129 | print(f"Serving thread info: {serving_thread._started=} | {serving_thread._started._flag=}") |
83 | 130 | print(f"Alive but dead thread: {alive_thread._started=} | {alive_thread._started._flag=}") |
84 | 131 |
|
85 | | - frames_serving_thread = sys._current_frames()[serving_thread.ident] |
86 | | - traceback.print_stack(frames_serving_thread) |
87 | | - |
88 | | -# File "/home/odoo/Documents/Pythons/py14/lib/python3.14/threading.py", line 1043, in _bootstrap |
89 | | -# self._bootstrap_inner() |
90 | | -# File "/home/odoo/Documents/Pythons/py14/lib/python3.14/threading.py", line 1081, in _bootstrap_inner |
91 | | -# self._context.run(self.run) |
92 | | -# File "/home/odoo/Documents/Pythons/py14/lib/python3.14/threading.py", line 1023, in run |
93 | | -# self._target(*self._args, **self._kwargs) |
94 | | -# File "/home/odoo/Documents/dev/./odoo_scripts/python_issue.py", line 17, in serving |
95 | | -# t.start() |
96 | | -# File "/home/odoo/Documents/Pythons/py14/lib/python3.14/threading.py", line 1010, in start |
97 | | -# self._started.wait() # Will set ident and native_id |
98 | | -# File "/home/odoo/Documents/Pythons/py14/lib/python3.14/threading.py", line 669, in wait |
99 | | -# signaled = self._cond.wait(timeout) |
100 | | -# File "/home/odoo/Documents/Pythons/py14/lib/python3.14/threading.py", line 369, in wait |
101 | | -# waiter.acquire() |
102 | | - |
103 | | - |
104 | | -""" |
105 | | -git checkout main && |
106 | | -mkdir -p /home/odoo/Documents/Pythons/pymain && |
107 | | -make --quiet clean && |
108 | | -./configure --prefix=/home/odoo/Documents/Pythons/pymain --enable-optimizations && |
109 | | -make --quiet -j 10 && |
110 | | -make --quiet altinstall |
111 | | -""" |
112 | | - |
113 | | -""" |
114 | | -make --quiet clean && |
115 | | -./configure --enable-optimizations && |
116 | | -make --quiet -j 10 |
117 | | -""" |
118 | | - |
119 | | - |
120 | | - |
121 | | -# ./configure --prefix=/home/odoo/Documents/Pythons/py14 --enable-optimizations |
122 | | - |
123 | | -# Python 3.8.0b1: Reproductible but seems more rare? |
124 | | - |
125 | | -# Our version 3.12.3: Reproductible |
126 | | - |
127 | | -# Python 3.12.11+: Reproductible |
128 | | -# Python 3.13.7+: Reproductible |
129 | | -# Python 3.14.0rc2+: Reproductible |
130 | | - |
131 | | -# Python main/3.15.0a0: Reproductible |
| 132 | + if serving_thread.ident in sys._current_frames(): |
| 133 | + frames_serving_thread = sys._current_frames()[serving_thread.ident] |
| 134 | + traceback.print_stack(frames_serving_thread) |
132 | 135 |
|
| 136 | + time.sleep(10) |
0 commit comments