Skip to content

Commit 36febd8

Browse files
committed
use run_shell_cmd to install extensions in parallel
1 parent a19776a commit 36febd8

File tree

5 files changed

+36
-63
lines changed

5 files changed

+36
-63
lines changed

easybuild/framework/easyblock.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
from easybuild.tools.hooks import MODULE_STEP, MODULE_WRITE, PACKAGE_STEP, PATCH_STEP, PERMISSIONS_STEP, POSTITER_STEP
8888
from easybuild.tools.hooks import POSTPROC_STEP, PREPARE_STEP, READY_STEP, SANITYCHECK_STEP, SOURCE_STEP
8989
from easybuild.tools.hooks import SINGLE_EXTENSION, TEST_STEP, TESTCASES_STEP, load_hooks, run_hook
90-
from easybuild.tools.run import RunShellCmdError, run_shell_cmd
90+
from easybuild.tools.run import RunShellCmdError, raise_run_shell_cmd_error, run_shell_cmd
9191
from easybuild.tools.jenkins import write_to_xml
9292
from easybuild.tools.module_generator import ModuleGeneratorLua, ModuleGeneratorTcl, module_generator, dependencies_for
9393
from easybuild.tools.module_naming_scheme.utilities import det_full_ec_version
@@ -1961,6 +1961,8 @@ def install_extensions_parallel(self, install=True):
19611961
"""
19621962
self.log.info("Installing extensions in parallel...")
19631963

1964+
thread_pool = ThreadPoolExecutor(max_workers=self.cfg['parallel'])
1965+
19641966
running_exts = []
19651967
installed_ext_names = []
19661968

@@ -1997,16 +1999,23 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
19971999

19982000
# check for extension installations that have completed
19992001
if running_exts:
2000-
self.log.info("Checking for completed extension installations (%d running)...", len(running_exts))
2002+
self.log.info(f"Checking for completed extension installations ({len(running_exts)} running)...")
20012003
for ext in running_exts[:]:
2002-
if self.dry_run or ext.async_cmd_check():
2003-
self.log.info("Installation of %s completed!", ext.name)
2004-
ext.postrun()
2005-
running_exts.remove(ext)
2006-
installed_ext_names.append(ext.name)
2007-
update_exts_progress_bar_helper(running_exts, 1)
2004+
if self.dry_run or ext.async_cmd_task.done():
2005+
res = ext.async_cmd_task.result()
2006+
if res.exit_code == 0:
2007+
self.log.info(f"Installation of extension {ext.name} completed!")
2008+
# run post-install method for extension from same working dir as installation of extension
2009+
cwd = change_dir(res.work_dir)
2010+
ext.postrun()
2011+
change_dir(cwd)
2012+
running_exts.remove(ext)
2013+
installed_ext_names.append(ext.name)
2014+
update_exts_progress_bar_helper(running_exts, 1)
2015+
else:
2016+
raise_run_shell_cmd_error(res)
20082017
else:
2009-
self.log.debug("Installation of %s is still running...", ext.name)
2018+
self.log.debug(f"Installation of extension {ext.name} is still running...")
20102019

20112020
# try to start as many extension installations as we can, taking into account number of available cores,
20122021
# but only consider first 100 extensions still in the queue
@@ -2073,9 +2082,9 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
20732082
rpath_filter_dirs=self.rpath_filter_dirs)
20742083
if install:
20752084
ext.prerun()
2076-
ext.run_async()
2085+
ext.async_cmd_task = ext.run_async(thread_pool)
20772086
running_exts.append(ext)
2078-
self.log.info("Started installation of extension %s in the background...", ext.name)
2087+
self.log.info(f"Started installation of extension {ext.name} in the background...")
20792088
update_exts_progress_bar_helper(running_exts, 0)
20802089

20812090
# print progress info after every iteration (unless that info is already shown via progress bar)
@@ -2088,6 +2097,8 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
20882097
running_ext_names = ', '.join(x.name for x in running_exts[:3]) + ", ..."
20892098
print_msg(msg % (installed_cnt, exts_cnt, queued_cnt, running_cnt, running_ext_names), log=self.log)
20902099

2100+
thread_pool.shutdown()
2101+
20912102
#
20922103
# MISCELLANEOUS UTILITY FUNCTIONS
20932104
#

easybuild/framework/extension.py

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from easybuild.framework.easyconfig.templates import TEMPLATE_NAMES_EASYBLOCK_RUN_STEP, template_constant_dict
4343
from easybuild.tools.build_log import EasyBuildError, raise_nosupport
4444
from easybuild.tools.filetools import change_dir
45-
from easybuild.tools.run import check_async_cmd, run_cmd, run_shell_cmd
45+
from easybuild.tools.run import run_shell_cmd
4646

4747

4848
def resolve_exts_filter_template(exts_filter, ext):
@@ -150,12 +150,7 @@ def __init__(self, mself, ext, extra_params=None):
150150
self.sanity_check_module_loaded = False
151151
self.fake_mod_data = None
152152

153-
self.async_cmd_info = None
154-
self.async_cmd_output = None
155-
self.async_cmd_check_cnt = None
156-
# initial read size should be relatively small,
157-
# to avoid hanging for a long time until desired output is available in async_cmd_check
158-
self.async_cmd_read_size = 1024
153+
self.async_cmd_task = None
159154

160155
@property
161156
def name(self):
@@ -195,44 +190,6 @@ def postrun(self):
195190
"""
196191
self.master.run_post_install_commands(commands=self.cfg.get('postinstallcmds', []))
197192

198-
def async_cmd_start(self, cmd, inp=None):
199-
"""
200-
Start installation asynchronously using specified command.
201-
"""
202-
self.async_cmd_output = ''
203-
self.async_cmd_check_cnt = 0
204-
self.async_cmd_info = run_cmd(cmd, log_all=True, simple=False, inp=inp, regexp=False, asynchronous=True)
205-
206-
def async_cmd_check(self):
207-
"""
208-
Check progress of installation command that was started asynchronously.
209-
210-
:return: True if command completed, False otherwise
211-
"""
212-
if self.async_cmd_info is None:
213-
raise EasyBuildError("No installation command running asynchronously for %s", self.name)
214-
elif self.async_cmd_info is False:
215-
self.log.info("No asynchronous command was started for extension %s", self.name)
216-
return True
217-
else:
218-
self.log.debug("Checking on installation of extension %s...", self.name)
219-
# use small read size, to avoid waiting for a long time until sufficient output is produced
220-
res = check_async_cmd(*self.async_cmd_info, output_read_size=self.async_cmd_read_size)
221-
self.async_cmd_output += res['output']
222-
if res['done']:
223-
self.log.info("Installation of extension %s completed!", self.name)
224-
self.async_cmd_info = None
225-
else:
226-
self.async_cmd_check_cnt += 1
227-
self.log.debug("Installation of extension %s still running (checked %d times)",
228-
self.name, self.async_cmd_check_cnt)
229-
# increase read size after sufficient checks,
230-
# to avoid that installation hangs due to output buffer filling up...
231-
if self.async_cmd_check_cnt % 10 == 0 and self.async_cmd_read_size < (1024 ** 2):
232-
self.async_cmd_read_size *= 2
233-
234-
return res['done']
235-
236193
@property
237194
def required_deps(self):
238195
"""Return list of required dependencies for this extension."""
@@ -273,7 +230,6 @@ def sanity_check_step(self):
273230
self.log.info("modulename set to False for '%s' extension, so skipping sanity check", self.name)
274231
elif exts_filter:
275232
cmd, stdin = resolve_exts_filter_template(exts_filter, self)
276-
# set log_ok to False so we can catch the error instead of run_cmd
277233
cmd_res = run_shell_cmd(cmd, fail_on_error=False, stdin=stdin)
278234

279235
if cmd_res.exit_code:

easybuild/tools/run.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ def run_shell_cmd(cmd, fail_on_error=True, split_stderr=False, stdin=None, env=N
207207
:param output_file: collect command output in temporary output file
208208
:param stream_output: stream command output to stdout (auto-enabled with --logtostdout if None)
209209
:param asynchronous: indicate that command is being run asynchronously
210+
:param task_id: task ID for specified shell command (included in return value)
210211
:param with_hooks: trigger pre/post run_shell_cmd hooks (if defined)
211212
:param qa_patterns: list of 2-tuples with patterns for questions + corresponding answers
212213
:param qa_wait_patterns: list of 2-tuples with patterns for non-questions

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
2828
@author: Kenneth Hoste (Ghent University)
2929
"""
30+
import os
3031

3132
from easybuild.framework.easyconfig import CUSTOM
3233
from easybuild.framework.extensioneasyblock import ExtensionEasyBlock
@@ -81,22 +82,24 @@ def prerun(self):
8182
super(Toy_Extension, self).run(unpack_src=True)
8283
EB_toy.configure_step(self.master, name=self.name, cfg=self.cfg)
8384

84-
def run_async(self):
85+
def run_async(self, thread_pool):
8586
"""
8687
Install toy extension asynchronously.
8788
"""
89+
task_id = f'ext_{self.name}_{self.version}'
8890
if self.src:
8991
cmd = compose_toy_build_cmd(self.cfg, self.name, self.cfg['prebuildopts'], self.cfg['buildopts'])
90-
self.async_cmd_start(cmd)
9192
else:
92-
self.async_cmd_info = False
93+
cmd = f"echo 'no sources for {self.name}'"
94+
95+
return thread_pool.submit(run_shell_cmd, cmd, asynchronous=True, env=os.environ.copy(),
96+
fail_on_error=False, task_id=task_id)
9397

9498
def postrun(self):
9599
"""
96100
Wrap up installation of toy extension.
97101
"""
98102
super(Toy_Extension, self).postrun()
99-
100103
EB_toy.install_step(self.master, name=self.name)
101104

102105
def sanity_check_step(self, *args, **kwargs):

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,14 @@ def run(self):
163163
"""
164164
self.build_step()
165165

166-
def run_async(self):
166+
def run_async(self, thread_pool):
167167
"""
168168
Asynchronous installation of toy as extension.
169169
"""
170170
cmd = compose_toy_build_cmd(self.cfg, self.name, self.cfg['prebuildopts'], self.cfg['buildopts'])
171-
self.async_cmd_start(cmd)
171+
task_id = f'ext_{self.name}_{self.version}'
172+
return thread_pool.submit(run_shell_cmd, cmd, asynchronous=True, env=os.environ.copy(),
173+
fail_on_error=False, task_id=task_id)
172174

173175
def postrun(self):
174176
"""

0 commit comments

Comments
 (0)