Skip to content

Commit 1752c4a

Browse files
committed
register signal handlers at start of main to clean up locks on receiving SIGTERM & co (fixes #3280)
1 parent e7f2226 commit 1752c4a

File tree

3 files changed

+76
-5
lines changed

3 files changed

+76
-5
lines changed

easybuild/main.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
from easybuild.tools.containers.common import containerize
5858
from easybuild.tools.docs import list_software
5959
from easybuild.tools.filetools import adjust_permissions, cleanup, copy_file, copy_files, dump_index, load_index
60-
from easybuild.tools.filetools import read_file, write_file
60+
from easybuild.tools.filetools import read_file, register_lock_cleanup_signal_handlers, write_file
6161
from easybuild.tools.github import check_github, close_pr, new_branch_github, find_easybuild_easyconfig
6262
from easybuild.tools.github import install_github_token, list_prs, new_pr, new_pr_from_branch, merge_pr
6363
from easybuild.tools.github import sync_branch_with_develop, sync_pr_with_develop, update_branch, update_pr
@@ -189,6 +189,9 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
189189
:param do_build: whether or not to actually perform the build
190190
:param testing: enable testing mode
191191
"""
192+
193+
register_lock_cleanup_signal_handlers()
194+
192195
# if $CDPATH is set, unset it, it'll only cause trouble...
193196
# see https://github.com/easybuilders/easybuild-framework/issues/2944
194197
if 'CDPATH' in os.environ:
@@ -518,5 +521,5 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
518521
main()
519522
except EasyBuildError as err:
520523
print_error(err.msg)
521-
except KeyboardInterrupt:
522-
print_error("Cancelled by user (keyboard interrupt)")
524+
except KeyboardInterrupt as err:
525+
print_error("Cancelled by user: %s" % err)

easybuild/tools/filetools.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,6 +1562,21 @@ def clean_up_locks_signal_handler(signum, frame):
15621562
sys.exit(signum)
15631563

15641564

1565+
def register_lock_cleanup_signal_handlers():
1566+
"""
1567+
Register signal handler for signals that cancel the current EasyBuild session,
1568+
so we can clean up the locks that were created first.
1569+
"""
1570+
signums = [
1571+
signal.SIGABRT,
1572+
signal.SIGINT, # Ctrl-C
1573+
signal.SIGTERM, # signal 15, soft kill (like when Slurm job is cancelled or received timeout)
1574+
signal.SIGQUIT, # kinda like Ctrl-C
1575+
]
1576+
for signum in signums:
1577+
signal.signal(signum, clean_up_locks_signal_handler)
1578+
1579+
15651580
def expand_glob_paths(glob_paths):
15661581
"""Expand specified glob paths to a list of unique non-glob paths to only files."""
15671582
paths = []

test/framework/toy_build.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import stat
4040
import sys
4141
import tempfile
42+
import time
4243
from distutils.version import LooseVersion
4344
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered
4445
from test.framework.package import mock_fpm
@@ -118,7 +119,8 @@ def check_toy(self, installpath, outtxt, version='0.0', versionprefix='', versio
118119
self.assertTrue(os.path.exists(devel_module_path))
119120

120121
def test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, verify=True, fails=False, verbose=True,
121-
raise_error=False, test_report=None, versionsuffix='', testing=True):
122+
raise_error=False, test_report=None, versionsuffix='', testing=True,
123+
raise_systemexit=False):
122124
"""Perform a toy build."""
123125
if extra_args is None:
124126
extra_args = []
@@ -145,7 +147,7 @@ def test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, verify=True
145147
myerr = None
146148
try:
147149
outtxt = self.eb_main(args, logfile=self.dummylogfn, do_build=True, verbose=verbose,
148-
raise_error=raise_error, testing=testing)
150+
raise_error=raise_error, testing=testing, raise_systemexit=raise_systemexit)
149151
except Exception as err:
150152
myerr = err
151153
if raise_error:
@@ -2607,6 +2609,57 @@ def __exit__(self, type, value, traceback):
26072609
self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build,
26082610
extra_args=extra_args, raise_error=True, verbose=False)
26092611

2612+
def test_toy_lock_cleanup_signals(self):
2613+
"""Test cleanup of locks after EasyBuild session gets a cancellation signal."""
2614+
2615+
locks_dir = os.path.join(self.test_installpath, 'software', '.locks')
2616+
self.assertFalse(os.path.exists(locks_dir))
2617+
2618+
# context manager which stops the function being called with the specified signal
2619+
class wait_and_signal:
2620+
def __init__(self, seconds, signum):
2621+
self.seconds = seconds
2622+
self.signum = signum
2623+
2624+
def send_signal(self, *args):
2625+
os.kill(os.getpid(), self.signum)
2626+
2627+
def __enter__(self):
2628+
signal.signal(signal.SIGALRM, self.send_signal)
2629+
signal.alarm(self.seconds)
2630+
2631+
def __exit__(self, type, value, traceback):
2632+
pass
2633+
2634+
# add extra sleep command to ensure session takes long enough
2635+
test_ecs_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')
2636+
toy_ec_txt = read_file(os.path.join(test_ecs_dir, 't', 'toy', 'toy-0.0.eb'))
2637+
2638+
test_ec = os.path.join(self.test_prefix, 'test.eb')
2639+
write_file(test_ec, toy_ec_txt + '\npostinstallcmds = ["sleep 5"]')
2640+
2641+
signums = [
2642+
(signal.SIGABRT, SystemExit),
2643+
(signal.SIGINT, KeyboardInterrupt),
2644+
(signal.SIGTERM, SystemExit),
2645+
(signal.SIGQUIT, SystemExit),
2646+
]
2647+
for (signum, exc) in signums:
2648+
with wait_and_signal(1, signum):
2649+
self.mock_stderr(True)
2650+
self.mock_stdout(True)
2651+
self.assertErrorRegex(exc, '.*', self.test_toy_build, ec_file=test_ec, verify=False,
2652+
raise_error=True, testing=False, raise_systemexit=True)
2653+
2654+
stderr = self.get_stderr().strip()
2655+
self.mock_stderr(False)
2656+
self.mock_stdout(False)
2657+
2658+
pattern = r"^WARNING: signal received \(%s\), " % int(signum)
2659+
pattern += r"cleaning up locks \(.*software_toy_0.0\)\.\.\."
2660+
regex = re.compile(pattern)
2661+
self.assertTrue(regex.search(stderr), "Pattern '%s' found in: %s" % (regex.pattern, stderr))
2662+
26102663
def test_toy_build_unicode_description(self):
26112664
"""Test installation of easyconfig file that has non-ASCII characters in description."""
26122665
# cfr. https://github.com/easybuilders/easybuild-framework/issues/3284

0 commit comments

Comments
 (0)