Skip to content

Commit c9e3eb3

Browse files
authored
Merge pull request #3667 from boegel/install_extensions
add initial/experimental support for installing extensions in parallel
2 parents a136f57 + 0dd9061 commit c9e3eb3

File tree

9 files changed

+423
-77
lines changed

9 files changed

+423
-77
lines changed

easybuild/framework/easyblock.py

Lines changed: 222 additions & 57 deletions
Large diffs are not rendered by default.

easybuild/framework/extension.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
from easybuild.framework.easyconfig.templates import TEMPLATE_NAMES_EASYBLOCK_RUN_STEP, template_constant_dict
4141
from easybuild.tools.build_log import EasyBuildError, raise_nosupport
4242
from easybuild.tools.filetools import change_dir
43-
from easybuild.tools.run import run_cmd
43+
from easybuild.tools.run import check_async_cmd, run_cmd
4444
from easybuild.tools.py2vs3 import string_type
4545

4646

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

141141
self.sanity_check_fail_msgs = []
142+
self.async_cmd_info = None
143+
self.async_cmd_output = None
144+
self.async_cmd_check_cnt = None
145+
# initial read size should be relatively small,
146+
# to avoid hanging for a long time until desired output is available in async_cmd_check
147+
self.async_cmd_read_size = 1024
142148

143149
@property
144150
def name(self):
@@ -160,18 +166,67 @@ def prerun(self):
160166
"""
161167
pass
162168

163-
def run(self):
169+
def run(self, *args, **kwargs):
164170
"""
165-
Actual installation of a extension.
171+
Actual installation of an extension.
166172
"""
167173
pass
168174

175+
def run_async(self, *args, **kwargs):
176+
"""
177+
Asynchronous installation of an extension.
178+
"""
179+
raise NotImplementedError
180+
169181
def postrun(self):
170182
"""
171183
Stuff to do after installing a extension.
172184
"""
173185
self.master.run_post_install_commands(commands=self.cfg.get('postinstallcmds', []))
174186

187+
def async_cmd_start(self, cmd, inp=None):
188+
"""
189+
Start installation asynchronously using specified command.
190+
"""
191+
self.async_cmd_output = ''
192+
self.async_cmd_check_cnt = 0
193+
self.async_cmd_info = run_cmd(cmd, log_all=True, simple=False, inp=inp, regexp=False, asynchronous=True)
194+
195+
def async_cmd_check(self):
196+
"""
197+
Check progress of installation command that was started asynchronously.
198+
199+
:return: True if command completed, False otherwise
200+
"""
201+
if self.async_cmd_info is None:
202+
raise EasyBuildError("No installation command running asynchronously for %s", self.name)
203+
elif self.async_cmd_info is False:
204+
self.log.info("No asynchronous command was started for extension %s", self.name)
205+
return True
206+
else:
207+
self.log.debug("Checking on installation of extension %s...", self.name)
208+
# use small read size, to avoid waiting for a long time until sufficient output is produced
209+
res = check_async_cmd(*self.async_cmd_info, output_read_size=self.async_cmd_read_size)
210+
self.async_cmd_output += res['output']
211+
if res['done']:
212+
self.log.info("Installation of extension %s completed!", self.name)
213+
self.async_cmd_info = None
214+
else:
215+
self.async_cmd_check_cnt += 1
216+
self.log.debug("Installation of extension %s still running (checked %d times)",
217+
self.name, self.async_cmd_check_cnt)
218+
# increase read size after sufficient checks,
219+
# to avoid that installation hangs due to output buffer filling up...
220+
if self.async_cmd_check_cnt % 10 == 0 and self.async_cmd_read_size < (1024 ** 2):
221+
self.async_cmd_read_size *= 2
222+
223+
return res['done']
224+
225+
@property
226+
def required_deps(self):
227+
"""Return list of required dependencies for this extension."""
228+
raise NotImplementedError("Don't know how to determine required dependencies for %s" % self.name)
229+
175230
@property
176231
def toolchain(self):
177232
"""

easybuild/tools/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
270270
'module_extensions',
271271
'module_only',
272272
'package',
273+
'parallel_extensions_install',
273274
'read_only_installdir',
274275
'remove_ghost_install_dirs',
275276
'rebuild',

easybuild/tools/options.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,8 @@ def override_options(self):
456456
'choice', 'store', OUTPUT_STYLE_AUTO, OUTPUT_STYLES),
457457
'parallel': ("Specify (maximum) level of parallellism used during build procedure",
458458
'int', 'store', None),
459+
'parallel-extensions-install': ("Install list of extensions in parallel (if supported)",
460+
None, 'store_true', False),
459461
'pre-create-installdir': ("Create installation directory before submitting build jobs",
460462
None, 'store_true', True),
461463
'pretend': (("Does the build/installation in a test directory located in $HOME/easybuildinstall"),
@@ -890,6 +892,10 @@ def postprocess(self):
890892
# set tmpdir
891893
self.tmpdir = set_tmpdir(self.options.tmpdir)
892894

895+
# early check for opt-in to installing extensions in parallel (experimental feature)
896+
if self.options.parallel_extensions_install:
897+
self.log.experimental("installing extensions in parallel")
898+
893899
# take --include options into account (unless instructed otherwise)
894900
if self.with_include:
895901
self._postprocess_include()

easybuild/tools/output.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ def extensions_progress_bar():
242242
Get progress bar to show progress for installing extensions.
243243
"""
244244
progress_bar = Progress(
245-
TextColumn("[bold blue]{task.description} ({task.completed}/{task.total})"),
245+
TextColumn("[bold blue]{task.description}"),
246246
BarColumn(),
247247
TimeElapsedColumn(),
248248
)

test/framework/run.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,11 +611,15 @@ def test_run_cmd_async(self):
611611
# check asynchronous running of failing command
612612
error_test_cmd = "echo 'FAIL!' >&2; exit 123"
613613
cmd_info = run_cmd(error_test_cmd, asynchronous=True)
614+
time.sleep(1)
614615
error_pattern = 'cmd ".*" exited with exit code 123'
615616
self.assertErrorRegex(EasyBuildError, error_pattern, check_async_cmd, *cmd_info)
616617

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

621625
# also test with a command that produces a lot of output,

test/framework/sandbox/easybuild/easyblocks/generic/toy_extension.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030

3131
from easybuild.framework.easyconfig import CUSTOM
3232
from easybuild.framework.extensioneasyblock import ExtensionEasyBlock
33-
from easybuild.easyblocks.toy import EB_toy
33+
from easybuild.easyblocks.toy import EB_toy, compose_toy_build_cmd
34+
from easybuild.tools.build_log import EasyBuildError
3435
from easybuild.tools.run import run_cmd
3536

3637

@@ -45,20 +46,59 @@ def extra_options():
4546
}
4647
return ExtensionEasyBlock.extra_options(extra_vars=extra_vars)
4748

48-
def run(self):
49-
"""Build toy extension."""
49+
@property
50+
def required_deps(self):
51+
"""Return list of required dependencies for this extension."""
52+
deps = {
53+
'bar': [],
54+
'barbar': ['bar'],
55+
'ls': [],
56+
}
57+
if self.name in deps:
58+
return deps[self.name]
59+
else:
60+
raise EasyBuildError("Dependencies for %s are unknown!", self.name)
61+
62+
def run(self, *args, **kwargs):
63+
"""
64+
Install toy extension.
65+
"""
5066
if self.src:
51-
super(Toy_Extension, self).run(unpack_src=True)
52-
EB_toy.configure_step(self.master, name=self.name)
5367
EB_toy.build_step(self.master, name=self.name, buildopts=self.cfg['buildopts'])
5468

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

58-
EB_toy.install_step(self.master, name=self.name)
59-
6072
return self.module_generator.set_environment('TOY_EXT_%s' % self.name.upper(), self.name)
6173

74+
def prerun(self):
75+
"""
76+
Prepare installation of toy extension.
77+
"""
78+
super(Toy_Extension, self).prerun()
79+
80+
if self.src:
81+
super(Toy_Extension, self).run(unpack_src=True)
82+
EB_toy.configure_step(self.master, name=self.name)
83+
84+
def run_async(self):
85+
"""
86+
Install toy extension asynchronously.
87+
"""
88+
if self.src:
89+
cmd = compose_toy_build_cmd(self.cfg, self.name, self.cfg['prebuildopts'], self.cfg['buildopts'])
90+
self.async_cmd_start(cmd)
91+
else:
92+
self.async_cmd_info = False
93+
94+
def postrun(self):
95+
"""
96+
Wrap up installation of toy extension.
97+
"""
98+
super(Toy_Extension, self).postrun()
99+
100+
EB_toy.install_step(self.master, name=self.name)
101+
62102
def sanity_check_step(self, *args, **kwargs):
63103
"""Custom sanity check for toy extensions."""
64104
self.log.info("Loaded modules: %s", self.modules_tool.list())

test/framework/sandbox/easybuild/easyblocks/t/toy.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@
4141
from easybuild.tools.run import run_cmd
4242

4343

44+
def compose_toy_build_cmd(cfg, name, prebuildopts, buildopts):
45+
"""
46+
Compose command to build toy.
47+
"""
48+
49+
cmd = "%(prebuildopts)s gcc %(name)s.c -o %(name)s %(buildopts)s" % {
50+
'name': name,
51+
'prebuildopts': prebuildopts,
52+
'buildopts': buildopts,
53+
}
54+
return cmd
55+
56+
4457
class EB_toy(ExtensionEasyBlock):
4558
"""Support for building/installing toy."""
4659

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

93106
def build_step(self, name=None, buildopts=None):
94107
"""Build toy."""
95-
96108
if buildopts is None:
97109
buildopts = self.cfg['buildopts']
98-
99110
if name is None:
100111
name = self.name
101-
run_cmd('%(prebuildopts)s gcc %(name)s.c -o %(name)s %(buildopts)s' % {
102-
'name': name,
103-
'prebuildopts': self.cfg['prebuildopts'],
104-
'buildopts': buildopts,
105-
})
112+
113+
cmd = compose_toy_build_cmd(self.cfg, name, self.cfg['prebuildopts'], buildopts)
114+
run_cmd(cmd)
106115

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

121-
def run(self):
122-
"""Install toy as extension."""
130+
@property
131+
def required_deps(self):
132+
"""Return list of required dependencies for this extension."""
133+
if self.name == 'toy':
134+
return ['bar', 'barbar']
135+
else:
136+
raise EasyBuildError("Dependencies for %s are unknown!", self.name)
137+
138+
def prerun(self):
139+
"""
140+
Prepare installation of toy as extension.
141+
"""
123142
super(EB_toy, self).run(unpack_src=True)
124143
self.configure_step()
144+
145+
def run(self):
146+
"""
147+
Install toy as extension.
148+
"""
125149
self.build_step()
150+
151+
def run_async(self):
152+
"""
153+
Asynchronous installation of toy as extension.
154+
"""
155+
cmd = compose_toy_build_cmd(self.cfg, self.name, self.cfg['prebuildopts'], self.cfg['buildopts'])
156+
self.async_cmd_start(cmd)
157+
158+
def postrun(self):
159+
"""
160+
Wrap up installation of toy as extension.
161+
"""
126162
self.install_step()
127163

128164
def make_module_step(self, fake=False):

test/framework/toy_build.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1777,6 +1777,45 @@ def test_module_only_extensions(self):
17771777
self.eb_main([test_ec, '--module-only', '--force'], do_build=True, raise_error=True)
17781778
self.assertTrue(os.path.exists(toy_mod))
17791779

1780+
def test_toy_exts_parallel(self):
1781+
"""
1782+
Test parallel installation of extensions (--parallel-extensions-install)
1783+
"""
1784+
topdir = os.path.abspath(os.path.dirname(__file__))
1785+
toy_ec = os.path.join(topdir, 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
1786+
1787+
toy_mod = os.path.join(self.test_installpath, 'modules', 'all', 'toy', '0.0')
1788+
if get_module_syntax() == 'Lua':
1789+
toy_mod += '.lua'
1790+
1791+
test_ec = os.path.join(self.test_prefix, 'test.eb')
1792+
test_ec_txt = read_file(toy_ec)
1793+
test_ec_txt += '\n' + '\n'.join([
1794+
"exts_list = [",
1795+
" ('ls'),",
1796+
" ('bar', '0.0'),",
1797+
" ('barbar', '0.0', {",
1798+
" 'start_dir': 'src',",
1799+
" }),",
1800+
" ('toy', '0.0'),",
1801+
"]",
1802+
"sanity_check_commands = ['barbar', 'toy']",
1803+
"sanity_check_paths = {'files': ['bin/barbar', 'bin/toy'], 'dirs': ['bin']}",
1804+
])
1805+
write_file(test_ec, test_ec_txt)
1806+
1807+
args = ['--parallel-extensions-install', '--experimental', '--force']
1808+
stdout, stderr = self.run_test_toy_build_with_output(ec_file=test_ec, extra_args=args)
1809+
self.assertEqual(stderr, '')
1810+
expected_stdout = '\n'.join([
1811+
"== 0 out of 4 extensions installed (2 queued, 2 running: ls, bar)",
1812+
"== 2 out of 4 extensions installed (1 queued, 1 running: barbar)",
1813+
"== 3 out of 4 extensions installed (0 queued, 1 running: toy)",
1814+
"== 4 out of 4 extensions installed (0 queued, 0 running: )",
1815+
'',
1816+
])
1817+
self.assertEqual(stdout, expected_stdout)
1818+
17801819
def test_backup_modules(self):
17811820
"""Test use of backing up of modules with --module-only."""
17821821

0 commit comments

Comments
 (0)