Skip to content
Merged
39 changes: 11 additions & 28 deletions easybuild/framework/easyblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@
from easybuild.tools.config import install_path, log_path, package_path, source_paths
from easybuild.tools.environment import restore_env, sanitize_env
from easybuild.tools.filetools import CHECKSUM_TYPE_MD5, CHECKSUM_TYPE_SHA256
from easybuild.tools.filetools import adjust_permissions, apply_patch, back_up_file
from easybuild.tools.filetools import change_dir, convert_name, compute_checksum, copy_file, derive_alt_pypi_url
from easybuild.tools.filetools import diff_files, download_file, encode_class_name, extract_file
from easybuild.tools.filetools import adjust_permissions, apply_patch, back_up_file, change_dir, convert_name
from easybuild.tools.filetools import compute_checksum, copy_file, check_lock, create_lock, derive_alt_pypi_url
from easybuild.tools.filetools import diff_files, dir_contains_files, download_file, encode_class_name, extract_file
from easybuild.tools.filetools import find_backup_name_candidate, get_source_tarball_from_git, is_alt_pypi_url
from easybuild.tools.filetools import is_binary, is_sha256_checksum, mkdir, move_file, move_logs, read_file, remove_dir
from easybuild.tools.filetools import remove_file, verify_checksum, weld_paths, write_file, dir_contains_files
from easybuild.tools.filetools import remove_file, remove_lock, verify_checksum, weld_paths, write_file
from easybuild.tools.hooks import BUILD_STEP, CLEANUP_STEP, CONFIGURE_STEP, EXTENSIONS_STEP, FETCH_STEP, INSTALL_STEP
from easybuild.tools.hooks import MODULE_STEP, PACKAGE_STEP, PATCH_STEP, PERMISSIONS_STEP, POSTITER_STEP, POSTPROC_STEP
from easybuild.tools.hooks import PREPARE_STEP, READY_STEP, SANITYCHECK_STEP, SOURCE_STEP, TEST_STEP, TESTCASES_STEP
Expand Down Expand Up @@ -3094,30 +3094,14 @@ def run_all_steps(self, run_test_cases):
if ignore_locks:
self.log.info("Ignoring locks...")
else:
locks_dir = build_option('locks_dir') or os.path.join(install_path('software'), '.locks')
lock_path = os.path.join(locks_dir, '%s.lock' % self.installdir.replace('/', '_'))

# if lock already exists, either abort or wait until it disappears
if os.path.exists(lock_path):
wait_on_lock = build_option('wait_on_lock')
if wait_on_lock:
while os.path.exists(lock_path):
print_msg("lock %s exists, waiting %d seconds..." % (lock_path, wait_on_lock),
silent=self.silent)
time.sleep(wait_on_lock)
else:
raise EasyBuildError("Lock %s already exists, aborting!", lock_path)
lock_name = self.installdir.replace('/', '_')

# create lock to avoid that another installation running in parallel messes things up;
# we use a directory as a lock, since that's atomically created
try:
mkdir(lock_path, parents=True)
except EasyBuildError as err:
# clean up the error message a bit, get rid of the "Failed to create directory" part + quotes
stripped_err = str(err).split(':', 1)[1].strip().replace("'", '').replace('"', '')
raise EasyBuildError("Failed to create lock %s: %s", lock_path, stripped_err)
# check if lock already exists;
# either aborts with an error or waits until it disappears (depends on --wait-on-lock)
check_lock(lock_name)

self.log.info("Lock created: %s", lock_path)
# create lock to avoid that another installation running in parallel messes things up
create_lock(lock_name)

try:
for (step_name, descr, step_methods, skippable) in steps:
Expand All @@ -3135,8 +3119,7 @@ def run_all_steps(self, run_test_cases):
pass
finally:
if not ignore_locks:
remove_dir(lock_path)
self.log.info("Lock removed: %s", lock_path)
remove_lock(lock_name)

# return True for successfull build (or stopped build)
return True
Expand Down
9 changes: 6 additions & 3 deletions easybuild/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
from easybuild.tools.containers.common import containerize
from easybuild.tools.docs import list_software
from easybuild.tools.filetools import adjust_permissions, cleanup, copy_file, copy_files, dump_index, load_index
from easybuild.tools.filetools import read_file, write_file
from easybuild.tools.filetools import read_file, register_lock_cleanup_signal_handlers, write_file
from easybuild.tools.github import check_github, close_pr, new_branch_github, find_easybuild_easyconfig
from easybuild.tools.github import install_github_token, list_prs, new_pr, new_pr_from_branch, merge_pr
from easybuild.tools.github import sync_branch_with_develop, sync_pr_with_develop, update_branch, update_pr
Expand Down Expand Up @@ -189,6 +189,9 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
:param do_build: whether or not to actually perform the build
:param testing: enable testing mode
"""

register_lock_cleanup_signal_handlers()

# if $CDPATH is set, unset it, it'll only cause trouble...
# see https://github.com/easybuilders/easybuild-framework/issues/2944
if 'CDPATH' in os.environ:
Expand Down Expand Up @@ -518,5 +521,5 @@ def main(args=None, logfile=None, do_build=None, testing=False, modtool=None):
main()
except EasyBuildError as err:
print_error(err.msg)
except KeyboardInterrupt:
print_error("Cancelled by user (keyboard interrupt)")
except KeyboardInterrupt as err:
print_error("Cancelled by user: %s" % err)
8 changes: 7 additions & 1 deletion easybuild/tools/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@
DEFAULT_PNS = 'EasyBuildPNS'
DEFAULT_PREFIX = os.path.join(os.path.expanduser('~'), ".local", "easybuild")
DEFAULT_REPOSITORY = 'FileRepository'
DEFAULT_WAIT_ON_LOCK_INTERVAL = 60
DEFAULT_WAIT_ON_LOCK_LIMIT = 0

EBROOT_ENV_VAR_ACTIONS = [ERROR, IGNORE, UNSET, WARN]
LOADED_MODULES_ACTIONS = [ERROR, IGNORE, PURGE, UNLOAD, WARN]
Expand Down Expand Up @@ -211,6 +213,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'subdir_user_modules',
'test_report_env_filter',
'testoutput',
'wait_on_lock',
'umask',
'zip_logs',
],
Expand Down Expand Up @@ -256,7 +259,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'use_f90cache',
'use_existing_modules',
'set_default_module',
'wait_on_lock',
'wait_on_lock_limit',
],
True: [
'cleanup_builddir',
Expand Down Expand Up @@ -305,6 +308,9 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
DEFAULT_ALLOW_LOADED_MODULES: [
'allow_loaded_modules',
],
DEFAULT_WAIT_ON_LOCK_INTERVAL: [
'wait_on_lock_interval',
],
}
# build option that do not have a perfectly matching command line option
BUILD_OPTIONS_OTHER = {
Expand Down
131 changes: 130 additions & 1 deletion easybuild/tools/filetools.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import os
import re
import shutil
import signal
import stat
import sys
import tempfile
Expand All @@ -59,7 +60,7 @@
from easybuild.tools import run
# import build_log must stay, to use of EasyBuildLog
from easybuild.tools.build_log import EasyBuildError, dry_run_msg, print_msg, print_warning
from easybuild.tools.config import GENERIC_EASYBLOCK_PKG, build_option
from easybuild.tools.config import DEFAULT_WAIT_ON_LOCK_INTERVAL, GENERIC_EASYBLOCK_PKG, build_option, install_path
from easybuild.tools.py2vs3 import std_urllib, string_type
from easybuild.tools.utilities import nub, remove_unwanted_chars

Expand Down Expand Up @@ -155,6 +156,9 @@
'.tar.z': "tar xzf %(filepath)s",
}

# global set of names of locks that were created in this session
global_lock_names = set()


class ZlibChecksum(object):
"""
Expand Down Expand Up @@ -1493,6 +1497,131 @@ def mkdir(path, parents=False, set_gid=None, sticky=None):
_log.debug("Not creating existing path %s" % path)


def det_lock_path(lock_name):
"""
Determine full path for lock with specifed name.
"""
locks_dir = build_option('locks_dir') or os.path.join(install_path('software'), '.locks')
return os.path.join(locks_dir, lock_name + '.lock')


def create_lock(lock_name):
"""Create lock with specified name."""

lock_path = det_lock_path(lock_name)
_log.info("Creating lock at %s...", lock_path)
try:
# we use a directory as a lock, since that's atomically created
mkdir(lock_path, parents=True)
global_lock_names.add(lock_name)
except EasyBuildError as err:
# clean up the error message a bit, get rid of the "Failed to create directory" part + quotes
stripped_err = str(err).split(':', 1)[1].strip().replace("'", '').replace('"', '')
raise EasyBuildError("Failed to create lock %s: %s", lock_path, stripped_err)
_log.info("Lock created: %s", lock_path)


def check_lock(lock_name):
"""
Check whether a lock with specified name already exists.

If it exists, either wait until it's released, or raise an error
(depending on --wait-on-lock configuration option).
"""
lock_path = det_lock_path(lock_name)
if os.path.exists(lock_path):
_log.info("Lock %s exists!", lock_path)

wait_interval = build_option('wait_on_lock_interval')
wait_limit = build_option('wait_on_lock_limit')

# --wait-on-lock is deprecated, should use --wait-on-lock-limit and --wait-on-lock-interval instead
wait_on_lock = build_option('wait_on_lock')
if wait_on_lock is not None:
depr_msg = "Use of --wait-on-lock is deprecated, use --wait-on-lock-limit and --wait-on-lock-interval"
_log.deprecated(depr_msg, '5.0')

# if --wait-on-lock-interval has default value and --wait-on-lock is specified too, the latter wins
# (required for backwards compatibility)
if wait_interval == DEFAULT_WAIT_ON_LOCK_INTERVAL and wait_on_lock > 0:
wait_interval = wait_on_lock

# if --wait-on-lock-limit is not specified we need to wait indefinitely if --wait-on-lock is specified,
# since the original semantics of --wait-on-lock was that it specified the waiting time interval (no limit)
if not wait_limit:
wait_limit = -1

# wait limit could be zero (no waiting), -1 (no waiting limit) or non-zero value (waiting limit in seconds)
if wait_limit != 0:
wait_time = 0
while os.path.exists(lock_path) and (wait_limit == -1 or wait_time < wait_limit):
print_msg("lock %s exists, waiting %d seconds..." % (lock_path, wait_interval),
silent=build_option('silent'))
time.sleep(wait_interval)
wait_time += wait_interval

if os.path.exists(lock_path) and wait_limit != -1 and wait_time >= wait_limit:
error_msg = "Maximum wait time for lock %s to be released reached: %s sec >= %s sec"
raise EasyBuildError(error_msg, lock_path, wait_time, wait_limit)
else:
_log.info("Lock %s was released!", lock_path)
else:
raise EasyBuildError("Lock %s already exists, aborting!", lock_path)
else:
_log.info("Lock %s does not exist", lock_path)


def remove_lock(lock_name):
"""
Remove lock with specified name.
"""
lock_path = det_lock_path(lock_name)
_log.info("Removing lock %s...", lock_path)
remove_dir(lock_path)
if lock_name in global_lock_names:
global_lock_names.remove(lock_name)
_log.info("Lock removed: %s", lock_path)


def clean_up_locks():
"""
Clean up all still existing locks that were created in this session.
"""
for lock_name in list(global_lock_names):
remove_lock(lock_name)


def clean_up_locks_signal_handler(signum, frame):
"""
Signal handler, cleans up locks & exits with received signal number.
"""

if not build_option('silent'):
print_warning("signal received (%s), cleaning up locks (%s)..." % (signum, ', '.join(global_lock_names)))
clean_up_locks()

# by default, a KeyboardInterrupt is raised with SIGINT, so keep doing so
if signum == signal.SIGINT:
raise KeyboardInterrupt("keyboard interrupt")
else:
sys.exit(signum)


def register_lock_cleanup_signal_handlers():
"""
Register signal handler for signals that cancel the current EasyBuild session,
so we can clean up the locks that were created first.
"""
signums = [
signal.SIGABRT,
signal.SIGINT, # Ctrl-C
signal.SIGTERM, # signal 15, soft kill (like when Slurm job is cancelled or received timeout)
signal.SIGQUIT, # kinda like Ctrl-C
]
for signum in signums:
signal.signal(signum, clean_up_locks_signal_handler)


def expand_glob_paths(glob_paths):
"""Expand specified glob paths to a list of unique non-glob paths to only files."""
paths = []
Expand Down
24 changes: 15 additions & 9 deletions easybuild/tools/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@
from easybuild.tools.config import DEFAULT_JOB_BACKEND, DEFAULT_LOGFILE_FORMAT, DEFAULT_MAX_FAIL_RATIO_PERMS
from easybuild.tools.config import DEFAULT_MNS, DEFAULT_MODULE_SYNTAX, DEFAULT_MODULES_TOOL, DEFAULT_MODULECLASSES
from easybuild.tools.config import DEFAULT_PATH_SUBDIRS, DEFAULT_PKG_RELEASE, DEFAULT_PKG_TOOL, DEFAULT_PKG_TYPE
from easybuild.tools.config import DEFAULT_PNS, DEFAULT_PREFIX, DEFAULT_REPOSITORY, EBROOT_ENV_VAR_ACTIONS, ERROR
from easybuild.tools.config import FORCE_DOWNLOAD_CHOICES, GENERAL_CLASS, IGNORE, JOB_DEPS_TYPE_ABORT_ON_ERROR
from easybuild.tools.config import JOB_DEPS_TYPE_ALWAYS_RUN, LOADED_MODULES_ACTIONS, WARN
from easybuild.tools.config import LOCAL_VAR_NAMING_CHECK_WARN, LOCAL_VAR_NAMING_CHECKS
from easybuild.tools.config import DEFAULT_PNS, DEFAULT_PREFIX, DEFAULT_REPOSITORY, DEFAULT_WAIT_ON_LOCK_INTERVAL
from easybuild.tools.config import DEFAULT_WAIT_ON_LOCK_LIMIT, EBROOT_ENV_VAR_ACTIONS, ERROR, FORCE_DOWNLOAD_CHOICES
from easybuild.tools.config import GENERAL_CLASS, IGNORE, JOB_DEPS_TYPE_ABORT_ON_ERROR, JOB_DEPS_TYPE_ALWAYS_RUN
from easybuild.tools.config import LOADED_MODULES_ACTIONS, LOCAL_VAR_NAMING_CHECK_WARN, LOCAL_VAR_NAMING_CHECKS, WARN
from easybuild.tools.config import get_pretend_installpath, init, init_build_options, mk_full_default_path
from easybuild.tools.configobj import ConfigObj, ConfigObjError
from easybuild.tools.docs import FORMAT_TXT, FORMAT_RST
Expand All @@ -76,9 +76,8 @@
from easybuild.tools.docs import list_easyblocks, list_toolchains
from easybuild.tools.environment import restore_env, unset_env_vars
from easybuild.tools.filetools import CHECKSUM_TYPE_SHA256, CHECKSUM_TYPES, install_fake_vsc, move_file, which
from easybuild.tools.github import GITHUB_EB_MAIN, GITHUB_EASYCONFIGS_REPO
from easybuild.tools.github import GITHUB_PR_DIRECTION_DESC, GITHUB_PR_ORDER_CREATED, GITHUB_PR_STATE_OPEN
from easybuild.tools.github import GITHUB_PR_STATES, GITHUB_PR_ORDERS, GITHUB_PR_DIRECTIONS
from easybuild.tools.github import GITHUB_EB_MAIN, GITHUB_PR_DIRECTION_DESC, GITHUB_PR_ORDER_CREATED
from easybuild.tools.github import GITHUB_PR_STATE_OPEN, GITHUB_PR_STATES, GITHUB_PR_ORDERS, GITHUB_PR_DIRECTIONS
from easybuild.tools.github import HAVE_GITHUB_API, HAVE_KEYRING, VALID_CLOSE_PR_REASONS
from easybuild.tools.github import fetch_easyblocks_from_pr, fetch_github_token
from easybuild.tools.hooks import KNOWN_HOOKS
Expand Down Expand Up @@ -442,8 +441,15 @@ def override_options(self):
None, 'store_true', False),
'verify-easyconfig-filenames': ("Verify whether filename of specified easyconfigs matches with contents",
None, 'store_true', False),
'wait-on-lock': ("Wait interval (in seconds) to use when waiting for existing lock to be removed "
"(0: implies no waiting, but exiting with an error)", int, 'store', 0),
'wait-on-lock': ("Wait for lock to be released; 0 implies no waiting (exit with an error if the lock "
"already exists), non-zero value specified waiting interval [DEPRECATED: "
"use --wait-on-lock-interval and --wait-on-lock-limit instead]",
int, 'store_or_None', None),
'wait-on-lock-interval': ("Wait interval (in seconds) to use when waiting for existing lock to be removed",
int, 'store', DEFAULT_WAIT_ON_LOCK_INTERVAL),
'wait-on-lock-limit': ("Maximum amount of time (in seconds) to wait until lock is released (0 means no "
"waiting at all, exit with error; -1 means no waiting limit, keep waiting)",
int, 'store', DEFAULT_WAIT_ON_LOCK_LIMIT),
'zip-logs': ("Zip logs that are copied to install directory, using specified command",
None, 'store_or_None', 'gzip'),

Expand Down
Loading