Skip to content

Commit a33bf05

Browse files
konraddysputKonrad Dysput
andauthored
Prevent multiple faulting threads (#26)
# Why During debugging AiQ bugs in Python we discovered, the fingerprint is always 0s. After a deeper investigation, I noted our algorithm can set two faulting threads. This pull request fixes the problem and makes sure we only set one faulting thread --------- Co-authored-by: Konrad Dysput <[email protected]>
1 parent e59151a commit a33bf05

File tree

2 files changed

+100
-56
lines changed

2 files changed

+100
-56
lines changed

backtracepython/report.py

Lines changed: 67 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
class BacktraceReport:
1414
def __init__(self):
1515
self.fault_thread = threading.current_thread()
16+
self.faulting_thread_id = str(self.fault_thread.ident)
1617
self.source_code = {}
1718
self.source_path_dict = {}
1819
self.attachments = []
1920

21+
stack_trace = self.__generate_stack_trace()
22+
2023
attributes, annotations = attribute_manager.get()
2124
attributes.update({"error.type": "Exception"})
2225

@@ -29,16 +32,19 @@ def __init__(self):
2932
"langVersion": python_version,
3033
"agent": "backtrace-python",
3134
"agentVersion": version_string,
32-
"mainThread": str(self.fault_thread.ident),
35+
"mainThread": self.faulting_thread_id,
3336
"attributes": attributes,
3437
"annotations": annotations,
35-
"threads": self.generate_stack_trace(),
38+
"threads": stack_trace,
3639
}
3740

3841
def set_exception(self, garbage, ex_value, ex_traceback):
3942
self.report["classifiers"] = [ex_value.__class__.__name__]
4043
self.report["attributes"]["error.message"] = str(ex_value)
4144

45+
# reset faulting thread id and make sure the faulting thread is not listed twice
46+
self.report["threads"][self.faulting_thread_id]["fault"] = False
47+
4248
# update faulting thread with information from the error
4349
fault_thread_id = str(self.fault_thread.ident)
4450
if not fault_thread_id in self.report["threads"]:
@@ -50,42 +56,92 @@ def set_exception(self, garbage, ex_value, ex_traceback):
5056

5157
faulting_thread = self.report["threads"][fault_thread_id]
5258

53-
faulting_thread["stack"] = self.convert_stack_trace(
54-
self.traverse_exception_stack(ex_traceback), False
59+
faulting_thread["stack"] = self.__convert_stack_trace(
60+
self.__traverse_exception_stack(ex_traceback), False
5561
)
5662
faulting_thread["fault"] = True
63+
self.faulting_thread_id = fault_thread_id
64+
self.report["mainThread"] = self.faulting_thread_id
65+
66+
def capture_last_exception(self):
67+
self.set_exception(*sys.exc_info())
68+
69+
def set_attribute(self, key, value):
70+
self.report["attributes"][key] = value
71+
72+
def set_dict_attributes(self, target_dict):
73+
self.report["attributes"].update(target_dict)
74+
75+
def set_annotation(self, key, value):
76+
self.report["annotations"][key] = value
77+
78+
def get_annotations(self):
79+
return self.report["annotations"]
80+
81+
def get_attributes(self):
82+
return self.report["attributes"]
83+
84+
def set_dict_annotations(self, target_dict):
85+
self.report["annotations"].update(target_dict)
86+
87+
def log(self, line):
88+
self.log_lines.append(
89+
{
90+
"ts": time.time(),
91+
"msg": line,
92+
}
93+
)
94+
95+
def add_attachment(self, attachment_path):
96+
self.attachments.append(attachment_path)
97+
98+
def get_attachments(self):
99+
return self.attachments
100+
101+
def get_data(self):
102+
return self.report
103+
104+
def send(self):
105+
if len(self.log_lines) != 0 and "Log" not in self.report["annotations"]:
106+
self.report["annotations"]["Log"] = self.log_lines
107+
from backtracepython.client import send
108+
109+
send(self)
57110

58-
def generate_stack_trace(self):
111+
def __generate_stack_trace(self):
59112
current_frames = sys._current_frames()
60113
threads = {}
61114
for thread in threading.enumerate():
62115
thread_frame = current_frames.get(thread.ident)
63116
is_main_thread = thread.name == "MainThread"
64-
threads[str(thread.ident)] = {
117+
thread_id = str(thread.ident)
118+
threads[thread_id] = {
65119
"name": thread.name,
66-
"stack": self.convert_stack_trace(
67-
self.traverse_process_thread_stack(thread_frame), is_main_thread
120+
"stack": self.__convert_stack_trace(
121+
self.__traverse_process_thread_stack(thread_frame), is_main_thread
68122
),
69123
"fault": is_main_thread,
70124
}
125+
if is_main_thread:
126+
self.faulting_thread_id = thread_id
71127

72128
return threads
73129

74-
def traverse_exception_stack(self, traceback):
130+
def __traverse_exception_stack(self, traceback):
75131
stack = []
76132
while traceback:
77133
stack.append({"frame": traceback.tb_frame, "line": traceback.tb_lineno})
78134
traceback = traceback.tb_next
79135
return reversed(stack)
80136

81-
def traverse_process_thread_stack(self, thread_frame):
137+
def __traverse_process_thread_stack(self, thread_frame):
82138
stack = []
83139
while thread_frame:
84140
stack.append({"frame": thread_frame, "line": thread_frame.f_lineno})
85141
thread_frame = thread_frame.f_back
86142
return stack
87143

88-
def convert_stack_trace(self, thread_stack_trace, skip_backtrace_module):
144+
def __convert_stack_trace(self, thread_stack_trace, skip_backtrace_module):
89145
stack_trace = []
90146

91147
for thread_stack_frame in thread_stack_trace:
@@ -109,48 +165,3 @@ def convert_stack_trace(self, thread_stack_trace, skip_backtrace_module):
109165
)
110166

111167
return stack_trace
112-
113-
def capture_last_exception(self):
114-
self.set_exception(*sys.exc_info())
115-
116-
def set_attribute(self, key, value):
117-
self.report["attributes"][key] = value
118-
119-
def set_dict_attributes(self, target_dict):
120-
self.report["attributes"].update(target_dict)
121-
122-
def set_annotation(self, key, value):
123-
self.report["annotations"][key] = value
124-
125-
def get_annotations(self):
126-
return self.report["annotations"]
127-
128-
def get_attributes(self):
129-
return self.report["attributes"]
130-
131-
def set_dict_annotations(self, target_dict):
132-
self.report["annotations"].update(target_dict)
133-
134-
def log(self, line):
135-
self.log_lines.append(
136-
{
137-
"ts": time.time(),
138-
"msg": line,
139-
}
140-
)
141-
142-
def add_attachment(self, attachment_path):
143-
self.attachments.append(attachment_path)
144-
145-
def get_attachments(self):
146-
return self.attachments
147-
148-
def get_data(self):
149-
return self.report
150-
151-
def send(self):
152-
if len(self.log_lines) != 0 and "Log" not in self.report["annotations"]:
153-
self.report["annotations"]["Log"] = self.log_lines
154-
from backtracepython.client import send
155-
156-
send(self)

tests/test_stack_trace_parser.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,39 @@ def test_main_thread_generation_with_exception():
4444
assert len(stack_trace["stack"]) == expected_number_of_frames
4545

4646

47+
def test_stack_trace_generation_from_background_thread():
48+
background_thread_name = "test_background"
49+
data_container = []
50+
51+
def throw_in_background():
52+
try:
53+
failing_function()
54+
except:
55+
report = BacktraceReport()
56+
report.capture_last_exception()
57+
data = report.get_data()
58+
data_container.append(data)
59+
60+
thread = threading.Thread(target=throw_in_background, name=background_thread_name)
61+
thread.start()
62+
thread.join()
63+
if data_container:
64+
data = data_container[0]
65+
faulting_thread = data["threads"][data["mainThread"]]
66+
assert faulting_thread["name"] != "MainThread"
67+
assert faulting_thread["name"] == background_thread_name
68+
assert faulting_thread["fault"] == True
69+
# make sure other threads are not marked as faulting threads
70+
for thread_id in data["threads"]:
71+
thread = data["threads"][thread_id]
72+
if thread["name"] == background_thread_name:
73+
continue
74+
assert thread["fault"] == False
75+
76+
else:
77+
assert False
78+
79+
4780
def test_background_thread_stack_trace_generation():
4881
if_stop = False
4982

0 commit comments

Comments
 (0)