Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add initial/experimental support for installing extensions in parallel #3667

Merged
merged 25 commits into from
Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6b77925
add support for installing extensions in parallel
boegel Apr 21, 2021
c811d9c
Merge branch 'develop' into install_extensions
boegel Oct 13, 2021
5f86294
Merge branch 'develop' into install_extensions
boegel Oct 20, 2021
a1cf735
Merge branch 'develop' into install_extensions
boegel Oct 21, 2021
6053c0c
also update extensions progress bar when installing extensions in par…
boegel Oct 21, 2021
3fe1f68
mark support for installing extensions in parallel as experimental + …
boegel Oct 21, 2021
2e5c6ab
start extensions progress bar a bit earlier, also mention preparatory…
boegel Oct 21, 2021
3748d9c
add and use update_exts_progress_bar method to EasyBlock
boegel Oct 21, 2021
45a2627
fix formatting for extension progress bar when installing extensions …
boegel Oct 21, 2021
78faeab
update extensions progress bar with more detailed info when creating …
boegel Oct 21, 2021
fdc8a1a
only update extensions progress bar in init_ext_instances if there ac…
boegel Oct 22, 2021
a4502f1
Merge branch 'develop' into install_extensions
boegel Oct 22, 2021
137ac32
Merge branch 'develop' into install_extensions
boegel Oct 22, 2021
85273d0
use check_async_cmd in Extension.async_cmd_check
boegel Oct 22, 2021
b2938dd
remove import for unused complete_cmd from framework/extension.py
boegel Oct 22, 2021
805a461
Merge branch 'develop' into install_extensions
boegel Oct 25, 2021
447c1da
fix occasional failure in test_run_cmd_async
boegel Oct 25, 2021
d82ade9
check early for opt-in to using experimental feature when --parallel-…
boegel Oct 25, 2021
42c0bb3
tweak extensions progress bar label to also show 'X/Y done' when inst…
boegel Oct 25, 2021
c5d598f
inject short sleep before checking status of failing asynchronous com…
boegel Oct 26, 2021
73af425
drop 'parallel' argument for install_extensions, to avoid having to o…
boegel Oct 26, 2021
16e02ea
add run_async method to install extension asynchronously
boegel Oct 26, 2021
60c5d15
move printing of progress info on installing extensions in parallel a…
boegel Oct 26, 2021
3e186fb
return True in Extension.async_cmd_check if async_cmd_info is set to …
boegel Oct 26, 2021
0dd9061
add test for installing extensions in parallel
boegel Oct 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 222 additions & 57 deletions easybuild/framework/easyblock.py

Large diffs are not rendered by default.

61 changes: 58 additions & 3 deletions easybuild/framework/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from easybuild.framework.easyconfig.templates import TEMPLATE_NAMES_EASYBLOCK_RUN_STEP, template_constant_dict
from easybuild.tools.build_log import EasyBuildError, raise_nosupport
from easybuild.tools.filetools import change_dir
from easybuild.tools.run import run_cmd
from easybuild.tools.run import check_async_cmd, run_cmd
from easybuild.tools.py2vs3 import string_type


Expand Down Expand Up @@ -139,6 +139,12 @@ def __init__(self, mself, ext, extra_params=None):
key, name, version, value)

self.sanity_check_fail_msgs = []
self.async_cmd_info = None
self.async_cmd_output = None
self.async_cmd_check_cnt = None
# initial read size should be relatively small,
# to avoid hanging for a long time until desired output is available in async_cmd_check
self.async_cmd_read_size = 1024

@property
def name(self):
Expand All @@ -160,18 +166,67 @@ def prerun(self):
"""
pass

def run(self):
def run(self, *args, **kwargs):
"""
Actual installation of a extension.
Actual installation of an extension.
"""
pass

def run_async(self, *args, **kwargs):
"""
Asynchronous installation of an extension.
"""
raise NotImplementedError

def postrun(self):
"""
Stuff to do after installing a extension.
"""
self.master.run_post_install_commands(commands=self.cfg.get('postinstallcmds', []))

def async_cmd_start(self, cmd, inp=None):
"""
Start installation asynchronously using specified command.
"""
self.async_cmd_output = ''
self.async_cmd_check_cnt = 0
self.async_cmd_info = run_cmd(cmd, log_all=True, simple=False, inp=inp, regexp=False, asynchronous=True)

def async_cmd_check(self):
"""
Check progress of installation command that was started asynchronously.

:return: True if command completed, False otherwise
"""
if self.async_cmd_info is None:
raise EasyBuildError("No installation command running asynchronously for %s", self.name)
elif self.async_cmd_info is False:
self.log.info("No asynchronous command was started for extension %s", self.name)
return True
else:
self.log.debug("Checking on installation of extension %s...", self.name)
# use small read size, to avoid waiting for a long time until sufficient output is produced
res = check_async_cmd(*self.async_cmd_info, output_read_size=self.async_cmd_read_size)
self.async_cmd_output += res['output']
if res['done']:
self.log.info("Installation of extension %s completed!", self.name)
self.async_cmd_info = None
else:
self.async_cmd_check_cnt += 1
self.log.debug("Installation of extension %s still running (checked %d times)",
self.name, self.async_cmd_check_cnt)
# increase read size after sufficient checks,
# to avoid that installation hangs due to output buffer filling up...
if self.async_cmd_check_cnt % 10 == 0 and self.async_cmd_read_size < (1024 ** 2):
self.async_cmd_read_size *= 2

return res['done']

@property
def required_deps(self):
"""Return list of required dependencies for this extension."""
raise NotImplementedError("Don't know how to determine required dependencies for %s" % self.name)

@property
def toolchain(self):
"""
Expand Down
1 change: 1 addition & 0 deletions easybuild/tools/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'module_extensions',
'module_only',
'package',
'parallel_extensions_install',
'read_only_installdir',
'remove_ghost_install_dirs',
'rebuild',
Expand Down
6 changes: 6 additions & 0 deletions easybuild/tools/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,8 @@ def override_options(self):
'choice', 'store', OUTPUT_STYLE_AUTO, OUTPUT_STYLES),
'parallel': ("Specify (maximum) level of parallellism used during build procedure",
'int', 'store', None),
'parallel-extensions-install': ("Install list of extensions in parallel (if supported)",
None, 'store_true', False),
'pre-create-installdir': ("Create installation directory before submitting build jobs",
None, 'store_true', True),
'pretend': (("Does the build/installation in a test directory located in $HOME/easybuildinstall"),
Expand Down Expand Up @@ -890,6 +892,10 @@ def postprocess(self):
# set tmpdir
self.tmpdir = set_tmpdir(self.options.tmpdir)

# early check for opt-in to installing extensions in parallel (experimental feature)
if self.options.parallel_extensions_install:
self.log.experimental("installing extensions in parallel")

# take --include options into account (unless instructed otherwise)
if self.with_include:
self._postprocess_include()
Expand Down
2 changes: 1 addition & 1 deletion easybuild/tools/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ def extensions_progress_bar():
Get progress bar to show progress for installing extensions.
"""
progress_bar = Progress(
TextColumn("[bold blue]{task.description} ({task.completed}/{task.total})"),
TextColumn("[bold blue]{task.description}"),
boegel marked this conversation as resolved.
Show resolved Hide resolved
BarColumn(),
TimeElapsedColumn(),
)
Expand Down
4 changes: 4 additions & 0 deletions test/framework/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,11 +611,15 @@ def test_run_cmd_async(self):
# check asynchronous running of failing command
error_test_cmd = "echo 'FAIL!' >&2; exit 123"
cmd_info = run_cmd(error_test_cmd, asynchronous=True)
time.sleep(1)
error_pattern = 'cmd ".*" exited with exit code 123'
self.assertErrorRegex(EasyBuildError, error_pattern, check_async_cmd, *cmd_info)

cmd_info = run_cmd(error_test_cmd, asynchronous=True)
res = check_async_cmd(*cmd_info, fail_on_error=False)
# keep checking until command is fully done
while not res['done']:
res = check_async_cmd(*cmd_info, fail_on_error=False)
self.assertEqual(res, {'done': True, 'exit_code': 123, 'output': "FAIL!\n"})

# also test with a command that produces a lot of output,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@

from easybuild.framework.easyconfig import CUSTOM
from easybuild.framework.extensioneasyblock import ExtensionEasyBlock
from easybuild.easyblocks.toy import EB_toy
from easybuild.easyblocks.toy import EB_toy, compose_toy_build_cmd
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.run import run_cmd


Expand All @@ -45,20 +46,59 @@ def extra_options():
}
return ExtensionEasyBlock.extra_options(extra_vars=extra_vars)

def run(self):
"""Build toy extension."""
@property
def required_deps(self):
"""Return list of required dependencies for this extension."""
deps = {
'bar': [],
'barbar': ['bar'],
'ls': [],
}
if self.name in deps:
return deps[self.name]
else:
raise EasyBuildError("Dependencies for %s are unknown!", self.name)

def run(self, *args, **kwargs):
"""
Install toy extension.
"""
if self.src:
super(Toy_Extension, self).run(unpack_src=True)
EB_toy.configure_step(self.master, name=self.name)
EB_toy.build_step(self.master, name=self.name, buildopts=self.cfg['buildopts'])

if self.cfg['toy_ext_param']:
run_cmd(self.cfg['toy_ext_param'])

EB_toy.install_step(self.master, name=self.name)

return self.module_generator.set_environment('TOY_EXT_%s' % self.name.upper(), self.name)

def prerun(self):
"""
Prepare installation of toy extension.
"""
super(Toy_Extension, self).prerun()

if self.src:
super(Toy_Extension, self).run(unpack_src=True)
EB_toy.configure_step(self.master, name=self.name)

def run_async(self):
"""
Install toy extension asynchronously.
"""
if self.src:
cmd = compose_toy_build_cmd(self.cfg, self.name, self.cfg['prebuildopts'], self.cfg['buildopts'])
self.async_cmd_start(cmd)
else:
self.async_cmd_info = False

def postrun(self):
"""
Wrap up installation of toy extension.
"""
super(Toy_Extension, self).postrun()

EB_toy.install_step(self.master, name=self.name)

def sanity_check_step(self, *args, **kwargs):
"""Custom sanity check for toy extensions."""
self.log.info("Loaded modules: %s", self.modules_tool.list())
Expand Down
54 changes: 45 additions & 9 deletions test/framework/sandbox/easybuild/easyblocks/t/toy.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@
from easybuild.tools.run import run_cmd


def compose_toy_build_cmd(cfg, name, prebuildopts, buildopts):
"""
Compose command to build toy.
"""

cmd = "%(prebuildopts)s gcc %(name)s.c -o %(name)s %(buildopts)s" % {
'name': name,
'prebuildopts': prebuildopts,
'buildopts': buildopts,
}
return cmd


class EB_toy(ExtensionEasyBlock):
"""Support for building/installing toy."""

Expand Down Expand Up @@ -92,17 +105,13 @@ def configure_step(self, name=None):

def build_step(self, name=None, buildopts=None):
"""Build toy."""

if buildopts is None:
buildopts = self.cfg['buildopts']

if name is None:
name = self.name
run_cmd('%(prebuildopts)s gcc %(name)s.c -o %(name)s %(buildopts)s' % {
'name': name,
'prebuildopts': self.cfg['prebuildopts'],
'buildopts': buildopts,
})

cmd = compose_toy_build_cmd(self.cfg, name, self.cfg['prebuildopts'], buildopts)
run_cmd(cmd)

def install_step(self, name=None):
"""Install toy."""
Expand All @@ -118,11 +127,38 @@ def install_step(self, name=None):
mkdir(libdir, parents=True)
write_file(os.path.join(libdir, 'lib%s.a' % name), name.upper())

def run(self):
"""Install toy as extension."""
@property
def required_deps(self):
"""Return list of required dependencies for this extension."""
if self.name == 'toy':
return ['bar', 'barbar']
else:
raise EasyBuildError("Dependencies for %s are unknown!", self.name)

def prerun(self):
"""
Prepare installation of toy as extension.
"""
super(EB_toy, self).run(unpack_src=True)
self.configure_step()

def run(self):
"""
Install toy as extension.
"""
self.build_step()

def run_async(self):
"""
Asynchronous installation of toy as extension.
"""
cmd = compose_toy_build_cmd(self.cfg, self.name, self.cfg['prebuildopts'], self.cfg['buildopts'])
self.async_cmd_start(cmd)

def postrun(self):
"""
Wrap up installation of toy as extension.
"""
self.install_step()

def make_module_step(self, fake=False):
Expand Down
39 changes: 39 additions & 0 deletions test/framework/toy_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -1777,6 +1777,45 @@ def test_module_only_extensions(self):
self.eb_main([test_ec, '--module-only', '--force'], do_build=True, raise_error=True)
self.assertTrue(os.path.exists(toy_mod))

def test_toy_exts_parallel(self):
"""
Test parallel installation of extensions (--parallel-extensions-install)
"""
topdir = os.path.abspath(os.path.dirname(__file__))
toy_ec = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')

toy_mod = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0')
if get_module_syntax() == 'Lua':
toy_mod += '.lua'

test_ec = os.path.join(self.test_prefix, 'test.eb')
test_ec_txt = read_file(toy_ec)
test_ec_txt += '\n' + '\n'.join([
"exts_list = [",
" ('ls'),",
" ('bar', '0.0'),",
" ('barbar', '0.0', {",
" 'start_dir': 'src',",
" }),",
" ('toy', '0.0'),",
"]",
"sanity_check_commands = ['barbar', 'toy']",
"sanity_check_paths = {'files': ['bin/barbar', 'bin/toy'], 'dirs': ['bin']}",
])
write_file(test_ec, test_ec_txt)

args = ['--parallel-extensions-install', '--experimental', '--force']
stdout, stderr = self.run_test_toy_build_with_output(ec_file=test_ec, extra_args=args)
self.assertEqual(stderr, '')
expected_stdout = '\n'.join([
"== 0 out of 4 extensions installed (2 queued, 2 running: ls, bar)",
"== 2 out of 4 extensions installed (1 queued, 1 running: barbar)",
"== 3 out of 4 extensions installed (0 queued, 1 running: toy)",
"== 4 out of 4 extensions installed (0 queued, 0 running: )",
'',
])
self.assertEqual(stdout, expected_stdout)

def test_backup_modules(self):
"""Test use of backing up of modules with --module-only."""

Expand Down