buildman: Handle exceptions in threads gracefully

There have been at least a few cases where an exception has occurred in a
thread and resulted in buildman hanging: running out of disk space and
getting a unicode error.

Handle these by collecting a list of exceptions, printing them out and
reporting failure if any are found. Add a test for this.

Signed-off-by: Simon Glass <sjg@chromium.org>
This commit is contained in:
Simon Glass 2021-04-11 16:27:27 +12:00
parent ab9b4f35e3
commit 8116c78ffd
4 changed files with 56 additions and 11 deletions

View File

@ -182,6 +182,7 @@ class Builder:
only useful for testing in-tree builds. only useful for testing in-tree builds.
work_in_output: Use the output directory as the work directory and work_in_output: Use the output directory as the work directory and
don't write to a separate output directory. don't write to a separate output directory.
thread_exceptions: List of exceptions raised by thread jobs
Private members: Private members:
_base_board_dict: Last-summarised Dict of boards _base_board_dict: Last-summarised Dict of boards
@ -235,7 +236,8 @@ class Builder:
no_subdirs=False, full_path=False, verbose_build=False, no_subdirs=False, full_path=False, verbose_build=False,
mrproper=False, per_board_out_dir=False, mrproper=False, per_board_out_dir=False,
config_only=False, squash_config_y=False, config_only=False, squash_config_y=False,
warnings_as_errors=False, work_in_output=False): warnings_as_errors=False, work_in_output=False,
test_thread_exceptions=False):
"""Create a new Builder object """Create a new Builder object
Args: Args:
@ -262,6 +264,9 @@ class Builder:
warnings_as_errors: Treat all compiler warnings as errors warnings_as_errors: Treat all compiler warnings as errors
work_in_output: Use the output directory as the work directory and work_in_output: Use the output directory as the work directory and
don't write to a separate output directory. don't write to a separate output directory.
test_thread_exceptions: Uses for tests only, True to make the
threads raise an exception instead of reporting their result.
This simulates a failure in the code somewhere
""" """
self.toolchains = toolchains self.toolchains = toolchains
self.base_dir = base_dir self.base_dir = base_dir
@ -311,13 +316,16 @@ class Builder:
self._re_migration_warning = re.compile(r'^={21} WARNING ={22}\n.*\n=+\n', self._re_migration_warning = re.compile(r'^={21} WARNING ={22}\n.*\n=+\n',
re.MULTILINE | re.DOTALL) re.MULTILINE | re.DOTALL)
self.thread_exceptions = []
self.test_thread_exceptions = test_thread_exceptions
if self.num_threads: if self.num_threads:
self._single_builder = None self._single_builder = None
self.queue = queue.Queue() self.queue = queue.Queue()
self.out_queue = queue.Queue() self.out_queue = queue.Queue()
for i in range(self.num_threads): for i in range(self.num_threads):
t = builderthread.BuilderThread(self, i, mrproper, t = builderthread.BuilderThread(
per_board_out_dir) self, i, mrproper, per_board_out_dir,
test_exception=test_thread_exceptions)
t.setDaemon(True) t.setDaemon(True)
t.start() t.start()
self.threads.append(t) self.threads.append(t)
@ -1676,6 +1684,7 @@ class Builder:
Tuple containing: Tuple containing:
- number of boards that failed to build - number of boards that failed to build
- number of boards that issued warnings - number of boards that issued warnings
- list of thread exceptions raised
""" """
self.commit_count = len(commits) if commits else 1 self.commit_count = len(commits) if commits else 1
self.commits = commits self.commits = commits
@ -1689,7 +1698,7 @@ class Builder:
Print('\rStarting build...', newline=False) Print('\rStarting build...', newline=False)
self.SetupBuild(board_selected, commits) self.SetupBuild(board_selected, commits)
self.ProcessResult(None) self.ProcessResult(None)
self.thread_exceptions = []
# Create jobs to build all commits for each board # Create jobs to build all commits for each board
for brd in board_selected.values(): for brd in board_selected.values():
job = builderthread.BuilderJob() job = builderthread.BuilderJob()
@ -1728,5 +1737,8 @@ class Builder:
rate = float(self.count) / duration.total_seconds() rate = float(self.count) / duration.total_seconds()
msg += ', duration %s, rate %1.2f' % (duration, rate) msg += ', duration %s, rate %1.2f' % (duration, rate)
Print(msg) Print(msg)
if self.thread_exceptions:
Print('Failed: %d thread exceptions' % len(self.thread_exceptions),
colour=self.col.RED)
return (self.fail, self.warned) return (self.fail, self.warned, self.thread_exceptions)

View File

@ -97,12 +97,15 @@ class BuilderThread(threading.Thread):
test_exception: Used for testing; True to raise an exception instead of test_exception: Used for testing; True to raise an exception instead of
reporting the build result reporting the build result
""" """
def __init__(self, builder, thread_num, mrproper, per_board_out_dir,
test_exception=False):
"""Set up a new builder thread""" """Set up a new builder thread"""
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.builder = builder self.builder = builder
self.thread_num = thread_num self.thread_num = thread_num
self.mrproper = mrproper self.mrproper = mrproper
self.per_board_out_dir = per_board_out_dir self.per_board_out_dir = per_board_out_dir
self.test_exception = test_exception
def Make(self, commit, brd, stage, cwd, *args, **kwargs): def Make(self, commit, brd, stage, cwd, *args, **kwargs):
"""Run 'make' on a particular commit and board. """Run 'make' on a particular commit and board.
@ -449,7 +452,12 @@ class BuilderThread(threading.Thread):
Args: Args:
result: CommandResult object containing the results of the build result: CommandResult object containing the results of the build
Raises:
ValueError if self.test_exception is true (for testing)
""" """
if self.test_exception:
raise ValueError('test exception')
if self.thread_num != -1: if self.thread_num != -1:
self.builder.out_queue.put(result) self.builder.out_queue.put(result)
else: else:
@ -547,5 +555,9 @@ class BuilderThread(threading.Thread):
""" """
while True: while True:
job = self.builder.queue.get() job = self.builder.queue.get()
self.RunJob(job) try:
self.RunJob(job)
except Exception as e:
print('Thread exception:', e)
self.builder.thread_exceptions.append(e)
self.builder.queue.task_done() self.builder.queue.task_done()

View File

@ -110,7 +110,7 @@ def ShowToolchainPrefix(boards, toolchains):
return None return None
def DoBuildman(options, args, toolchains=None, make_func=None, boards=None, def DoBuildman(options, args, toolchains=None, make_func=None, boards=None,
clean_dir=False): clean_dir=False, test_thread_exceptions=False):
"""The main control code for buildman """The main control code for buildman
Args: Args:
@ -126,6 +126,9 @@ def DoBuildman(options, args, toolchains=None, make_func=None, boards=None,
boards. If this is None it will be created and scanned. boards. If this is None it will be created and scanned.
clean_dir: Used for tests only, indicates that the existing output_dir clean_dir: Used for tests only, indicates that the existing output_dir
should be removed before starting the build should be removed before starting the build
test_thread_exceptions: Uses for tests only, True to make the threads
raise an exception instead of reporting their result. This simulates
a failure in the code somewhere
""" """
global builder global builder
@ -330,7 +333,8 @@ def DoBuildman(options, args, toolchains=None, make_func=None, boards=None,
config_only=options.config_only, config_only=options.config_only,
squash_config_y=not options.preserve_config_y, squash_config_y=not options.preserve_config_y,
warnings_as_errors=options.warnings_as_errors, warnings_as_errors=options.warnings_as_errors,
work_in_output=options.work_in_output) work_in_output=options.work_in_output,
test_thread_exceptions=test_thread_exceptions)
builder.force_config_on_failure = not options.quick builder.force_config_on_failure = not options.quick
if make_func: if make_func:
builder.do_make = make_func builder.do_make = make_func
@ -370,9 +374,11 @@ def DoBuildman(options, args, toolchains=None, make_func=None, boards=None,
if options.summary: if options.summary:
builder.ShowSummary(commits, board_selected) builder.ShowSummary(commits, board_selected)
else: else:
fail, warned = builder.BuildBoards(commits, board_selected, fail, warned, excs = builder.BuildBoards(
options.keep_outputs, options.verbose) commits, board_selected, options.keep_outputs, options.verbose)
if fail: if excs:
return 102
elif fail:
return 100 return 100
elif warned and not options.ignore_warnings: elif warned and not options.ignore_warnings:
return 101 return 101

View File

@ -16,6 +16,7 @@ from buildman import toolchain
from patman import command from patman import command
from patman import gitutil from patman import gitutil
from patman import terminal from patman import terminal
from patman import test_util
from patman import tools from patman import tools
settings_data = ''' settings_data = '''
@ -219,6 +220,8 @@ class TestFunctional(unittest.TestCase):
return command.RunPipe([[self._buildman_pathname] + list(args)], return command.RunPipe([[self._buildman_pathname] + list(args)],
capture=True, capture_stderr=True) capture=True, capture_stderr=True)
def _RunControl(self, *args, boards=None, clean_dir=False,
test_thread_exceptions=False):
"""Run buildman """Run buildman
Args: Args:
@ -226,6 +229,9 @@ class TestFunctional(unittest.TestCase):
boards: boards:
clean_dir: Used for tests only, indicates that the existing output_dir clean_dir: Used for tests only, indicates that the existing output_dir
should be removed before starting the build should be removed before starting the build
test_thread_exceptions: Uses for tests only, True to make the threads
raise an exception instead of reporting their result. This simulates
a failure in the code somewhere
Returns: Returns:
result code from buildman result code from buildman
@ -234,6 +240,8 @@ class TestFunctional(unittest.TestCase):
options, args = cmdline.ParseArgs() options, args = cmdline.ParseArgs()
result = control.DoBuildman(options, args, toolchains=self._toolchains, result = control.DoBuildman(options, args, toolchains=self._toolchains,
make_func=self._HandleMake, boards=boards or self._boards, make_func=self._HandleMake, boards=boards or self._boards,
clean_dir=clean_dir,
test_thread_exceptions=test_thread_exceptions)
self._builder = control.builder self._builder = control.builder
return result return result
@ -597,3 +605,10 @@ class TestFunctional(unittest.TestCase):
with self.assertRaises(SystemExit) as e: with self.assertRaises(SystemExit) as e:
self._RunControl('-w', clean_dir=False) self._RunControl('-w', clean_dir=False)
self.assertIn("specify -o", str(e.exception)) self.assertIn("specify -o", str(e.exception))
def testThreadExceptions(self):
"""Test that exceptions in threads are reported"""
with test_util.capture_sys_output() as (stdout, stderr):
self.assertEqual(102, self._RunControl('-o', self._output_dir,
test_thread_exceptions=True))
self.assertIn('Thread exception: test exception', stdout.getvalue())