We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
GC3Pie backend submits jobs even if dependency build failed
When robot-building a bunch of software via the GC3Pie job backend with SLURM, GC3Pie happily submits follow-up jobs even if building of one (or more) of the dependencies failed.  I am not entirely sure whether this is an issue in EasyBuild, in GC3Pie, or in the way the two are working together.  I also don't know whether this also happens with other job schedulers.

Steps to reproduce:
- Start with an empty installdir
- Edit `M4-1.4.17.eb` and add `preconfigopts='false && '`
- Run `eb GCC-5.1.0.eb -r --job` using GC3Pie with SLURM
- Both jobs will be submitted, both fail (the first for obvious reasons, the second because the M4 module can't be found)

GC3Pie backend submits jobs even if dependency build failed
When robot-building a bunch of software via the GC3Pie job backend with SLURM, GC3Pie happily submits follow-up jobs even if building of one (or more) of the dependencies failed.  I am not entirely sure whether this is an issue in EasyBuild, in GC3Pie, or in the way the two are working together.  I also don't know whether this also happens with other job schedulers.

Steps to reproduce:
- Start with an empty installdir
- Edit `M4-1.4.17.eb` and add `preconfigopts='false && '`
- Run `eb GCC-5.1.0.eb -r --job` using GC3Pie with SLURM
- Both jobs will be submitted, both fail (the first for obvious reasons, the second because the M4 module can't be found)


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `easybuild/tools/job/gc3pie.py`
Content:
```
1 ##
2 # Copyright 2015-2018 Ghent University
3 # Copyright 2015 S3IT, University of Zurich
4 #
5 # This file is part of EasyBuild,
6 # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
7 # with support of Ghent University (http://ugent.be/hpc),
8 # the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
9 # Flemish Research Foundation (FWO) (http://www.fwo.be/en)
10 # and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
11 #
12 # https://github.com/easybuilders/easybuild
13 #
14 # EasyBuild is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation v2.
17 #
18 # EasyBuild is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with EasyBuild.  If not, see <http://www.gnu.org/licenses/>.
25 ##
26 """
27 Interface for submitting jobs via GC3Pie.
28 
29 :author: Riccardo Murri (University of Zurich)
30 :author: Kenneth Hoste (Ghent University)
31 """
32 from distutils.version import LooseVersion
33 from time import gmtime, strftime
34 import re
35 import time
36 
37 from vsc.utils import fancylogger
38 
39 from easybuild.tools.build_log import EasyBuildError, print_msg
40 from easybuild.tools.config import build_option
41 from easybuild.tools.job.backend import JobBackend
42 from easybuild.tools.utilities import only_if_module_is_available
43 
44 
45 _log = fancylogger.getLogger('gc3pie', fname=False)
46 
47 
48 try:
49     import gc3libs
50     import gc3libs.exceptions
51     from gc3libs import Application, Run, create_engine
52     from gc3libs.core import Engine
53     from gc3libs.quantity import hours as hr
54     from gc3libs.workflow import DependentTaskCollection
55 
56     # inject EasyBuild logger into GC3Pie
57     gc3libs.log = fancylogger.getLogger('gc3pie', fname=False)
58     # make handling of log.error compatible with stdlib logging
59     gc3libs.log.raiseError = False
60 
61     # instruct GC3Pie to not ignore errors, but raise exceptions instead
62     gc3libs.UNIGNORE_ALL_ERRORS = True
63 
64 except ImportError as err:
65     _log.debug("Failed to import gc3libs from GC3Pie."
66                " Silently ignoring, this is a real issue only when GC3Pie is used as backend for --job")
67 
68 
69 # eb --job --job-backend=GC3Pie
70 class GC3Pie(JobBackend):
71     """
72     Use the GC3Pie framework to submit and monitor compilation jobs,
73     see http://gc3pie.readthedocs.org/.
74 
75     In contrast with accessing an external service, GC3Pie implements
76     its own workflow manager, which means ``eb --job
77     --job-backend=GC3Pie`` will keep running until all jobs have
78     terminated.
79     """
80 
81     REQ_VERSION = '2.4.0'
82     VERSION_REGEX = re.compile(r'^(?P<version>\S*) version')
83 
84     @only_if_module_is_available('gc3libs', pkgname='gc3pie')
85     def __init__(self, *args, **kwargs):
86         """GC3Pie JobBackend constructor."""
87         super(GC3Pie, self).__init__(*args, **kwargs)
88 
89     # _check_version is called by __init__, so guard it (too) with the decorator
90     @only_if_module_is_available('gc3libs', pkgname='gc3pie')
91     def _check_version(self):
92         """Check whether GC3Pie version complies with required version."""
93         # location of __version__ to use may change, depending on the minimal required SVN revision for development versions
94         version_str = gc3libs.core.__version__
95 
96         match = self.VERSION_REGEX.search(version_str)
97         if match:
98             version = match.group('version')
99             self.log.debug("Parsed GC3Pie version info: '%s'", version)
100 
101             if version == 'development':
102                 # presume it's OK -- there's no way to check since GC3Pie switched to git
103                 return True
104 
105             if LooseVersion(version) < LooseVersion(self.REQ_VERSION):
106                 raise EasyBuildError("Found GC3Pie version %s, but version %s or more recent is required",
107                                      version, self.REQ_VERSION)
108         else:
109             raise EasyBuildError("Failed to parse GC3Pie version string '%s' using pattern %s",
110                                  version_str, self.VERSION_REGEX.pattern)
111 
112     def init(self):
113         """
114         Initialise the GC3Pie job backend.
115         """
116         # List of config files for GC3Pie; non-existing ones will be
117         # silently ignored.  The list here copies GC3Pie's default,
118         # for the principle of minimal surprise, but there is no
119         # strict requirement that this be done and EB could actually
120         # choose to use a completely distinct set of conf. files.
121         self.config_files = gc3libs.Default.CONFIG_FILE_LOCATIONS[:]
122         cfgfile = build_option('job_backend_config')
123         if cfgfile:
124             self.config_files.append(cfgfile)
125 
126         self.output_dir = build_option('job_output_dir')
127         self.jobs = DependentTaskCollection(output_dir=self.output_dir)
128         self.job_cnt = 0
129 
130         # after polling for job status, sleep for this time duration
131         # before polling again (in seconds)
132         self.poll_interval = build_option('job_polling_interval')
133 
134     def make_job(self, script, name, env_vars=None, hours=None, cores=None):
135         """
136         Create and return a job object with the given parameters.
137 
138         First argument `server` is an instance of the corresponding
139         `JobBackend` class, i.e., a `GC3Pie`:class: instance in this case.
140 
141         Second argument `script` is the content of the job script
142         itself, i.e., the sequence of shell commands that will be
143         executed.
144 
145         Third argument `name` sets the job human-readable name.
146 
147         Fourth (optional) argument `env_vars` is a dictionary with
148         key-value pairs of environment variables that should be passed
149         on to the job.
150 
151         Fifth and sixth (optional) arguments `hours` and `cores` should be
152         integer values:
153         * hours must be in the range 1 .. MAX_WALLTIME;
154         * cores depends on which cluster the job is being run.
155         """
156         named_args = {
157             'jobname': name, # job name in GC3Pie
158             'name':    name, # job name in EasyBuild
159         }
160 
161         # environment
162         if env_vars:
163             named_args['environment'] = env_vars
164 
165         # input/output files for job (none)
166         named_args['inputs'] = []
167         named_args['outputs'] = []
168 
169         # job logs
170         named_args.update({
171             # join stdout/stderr in a single log
172             'join': True,
173             # location for log file
174             'output_dir': self.output_dir,
175             # log file name (including timestamp to try and ensure unique filename)
176             'stdout': 'eb-%s-gc3pie-job-%s.log' % (name, strftime("%Y%M%d-UTC-%H-%M-%S", gmtime()))
177         })
178 
179         # walltime
180         max_walltime = build_option('job_max_walltime')
181         if hours is None:
182             hours = max_walltime
183         if hours > max_walltime:
184             self.log.warn("Specified %s hours, but this is impossible. (resetting to %s hours)" % (hours, max_walltime))
185             hours = max_walltime
186         named_args['requested_walltime'] = hours * hr
187 
188         if cores:
189             named_args['requested_cores'] = cores
190         else:
191             self.log.warn("Number of cores to request not specified, falling back to whatever GC3Pie does by default")
192 
193         return Application(['/bin/sh', '-c', script], **named_args)
194 
195     def queue(self, job, dependencies=frozenset()):
196         """
197         Add a job to the queue, optionally specifying dependencies.
198 
199         :param dependencies: jobs on which this job depends.
200         """
201         self.jobs.add(job, dependencies)
202         # since it's not trivial to determine the correct job count from self.jobs, we keep track of a count ourselves
203         self.job_cnt += 1
204 
205     def complete(self):
206         """
207         Complete a bulk job submission.
208 
209         Create engine, and progress it until all jobs have terminated.
210         """
211         # create an instance of `Engine` using the list of configuration files
212         try:
213             self._engine = create_engine(*self.config_files, resource_errors_are_fatal=True)
214 
215         except gc3libs.exceptions.Error as err:
216             raise EasyBuildError("Failed to create GC3Pie engine: %s", err)
217 
218         # make sure that all job log files end up in the same directory, rather than renaming the output directory
219         # see https://gc3pie.readthedocs.org/en/latest/programmers/api/gc3libs/core.html#gc3libs.core.Engine
220         self._engine.retrieve_overwrites = True
221 
222         # some sites may not be happy with flooding the cluster with build jobs...
223         self._engine.max_in_flight = build_option('job_max_jobs')
224 
225         # `Engine.stats()` (which is used later on in `_print_status_report()`)
226         # changed between 2.4.2 and 2.5.0.dev -- make sure we stay compatible
227         # with both
228         try:
229             self._engine.init_stats_for(Application)
230         except AttributeError:
231             _log.debug("No `init_stats_for` method in the Engine class;"
232                        " assuming pre-2.5.0 GC3Pie and ignoring error.")
233 
234         # Add your application to the engine. This will NOT submit
235         # your application yet, but will make the engine *aware* of
236         # the application.
237         self._engine.add(self.jobs)
238 
239         # in case you want to select a specific resource, call
240         target_resource = build_option('job_target_resource')
241         if target_resource:
242             res = self._engine.select_resource(target_resource)
243             if res == 0:
244                 raise EasyBuildError("Failed to select target resource '%s' in GC3Pie", target_resource)
245 
246         # Periodically check the status of your application.
247         while self.jobs.execution.state != Run.State.TERMINATED:
248             # `Engine.progress()` will do the GC3Pie magic:
249             # submit new jobs, update status of submitted jobs, get
250             # results of terminating jobs etc...
251             self._engine.progress()
252 
253             # report progress
254             self._print_status_report()
255 
256             # Wait a few seconds...
257             time.sleep(self.poll_interval)
258 
259         # final status report
260         print_msg("Done processing jobs", log=self.log, silent=build_option('silent'))
261         self._print_status_report()
262 
263     def _print_status_report(self):
264         """
265         Print a job status report to STDOUT and the log file.
266 
267         The number of jobs in each state is reported; the
268         figures are extracted from the `stats()` method of the
269         currently-running GC3Pie engine.
270         """
271         stats = self._engine.stats(only=Application)
272         states = ', '.join(["%d %s" % (stats[s], s.lower()) for s in stats if s != 'total' and stats[s]])
273         print_msg("GC3Pie job overview: %s (total: %s)" % (states, self.job_cnt),
274                   log=self.log, silent=build_option('silent'))
275
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/easybuild/tools/job/gc3pie.py b/easybuild/tools/job/gc3pie.py
--- a/easybuild/tools/job/gc3pie.py
+++ b/easybuild/tools/job/gc3pie.py
@@ -51,7 +51,7 @@
     from gc3libs import Application, Run, create_engine
     from gc3libs.core import Engine
     from gc3libs.quantity import hours as hr
-    from gc3libs.workflow import DependentTaskCollection
+    from gc3libs.workflow import AbortOnError, DependentTaskCollection
 
     # inject EasyBuild logger into GC3Pie
     gc3libs.log = fancylogger.getLogger('gc3pie', fname=False)
@@ -61,6 +61,16 @@
     # instruct GC3Pie to not ignore errors, but raise exceptions instead
     gc3libs.UNIGNORE_ALL_ERRORS = True
 
+    # note: order of class inheritance is important!
+    class _BuildTaskCollection(AbortOnError, DependentTaskCollection):
+        """
+        A `DependentTaskCollection`:class: that aborts execution upon error.
+
+        This is used to stop the build process in case some dependency
+        fails.  See also `<https://github.com/easybuilders/easybuild-framework/issues/1441>`_
+        """
+        pass
+
 except ImportError as err:
     _log.debug("Failed to import gc3libs from GC3Pie."
                " Silently ignoring, this is a real issue only when GC3Pie is used as backend for --job")
@@ -124,7 +134,7 @@
             self.config_files.append(cfgfile)
 
         self.output_dir = build_option('job_output_dir')
-        self.jobs = DependentTaskCollection(output_dir=self.output_dir)
+        self.jobs = _BuildTaskCollection(output_dir=self.output_dir)
         self.job_cnt = 0
 
         # after polling for job status, sleep for this time duration
@@ -135,23 +145,20 @@
         """
         Create and return a job object with the given parameters.
 
-        First argument `server` is an instance of the corresponding
-        `JobBackend` class, i.e., a `GC3Pie`:class: instance in this case.
-
-        Second argument `script` is the content of the job script
+        Argument *script* is the content of the job script
         itself, i.e., the sequence of shell commands that will be
         executed.
 
-        Third argument `name` sets the job human-readable name.
+        Argument *name* sets the job's human-readable name.
 
-        Fourth (optional) argument `env_vars` is a dictionary with
+        Optional argument *env_vars* is a dictionary with
         key-value pairs of environment variables that should be passed
         on to the job.
 
-        Fifth and sixth (optional) arguments `hours` and `cores` should be
+        Optional arguments *hours* and *cores* should be
         integer values:
-        * hours must be in the range 1 .. MAX_WALLTIME;
-        * cores depends on which cluster the job is being run.
+        - *hours* must be in the range 1 .. ``MAX_WALLTIME``;
+        - *cores* depends on which cluster the job is being run.
         """
         named_args = {
             'jobname': name, # job name in GC3Pie
@@ -236,7 +243,7 @@
         # the application.
         self._engine.add(self.jobs)
 
-        # in case you want to select a specific resource, call
+        # select a specific execution resource?
         target_resource = build_option('job_target_resource')
         if target_resource:
             res = self._engine.select_resource(target_resource)

{"golden_diff": "diff --git a/easybuild/tools/job/gc3pie.py b/easybuild/tools/job/gc3pie.py\n--- a/easybuild/tools/job/gc3pie.py\n+++ b/easybuild/tools/job/gc3pie.py\n@@ -51,7 +51,7 @@\n     from gc3libs import Application, Run, create_engine\n     from gc3libs.core import Engine\n     from gc3libs.quantity import hours as hr\n-    from gc3libs.workflow import DependentTaskCollection\n+    from gc3libs.workflow import AbortOnError, DependentTaskCollection\n \n     # inject EasyBuild logger into GC3Pie\n     gc3libs.log = fancylogger.getLogger('gc3pie', fname=False)\n@@ -61,6 +61,16 @@\n     # instruct GC3Pie to not ignore errors, but raise exceptions instead\n     gc3libs.UNIGNORE_ALL_ERRORS = True\n \n+    # note: order of class inheritance is important!\n+    class _BuildTaskCollection(AbortOnError, DependentTaskCollection):\n+        \"\"\"\n+        A `DependentTaskCollection`:class: that aborts execution upon error.\n+\n+        This is used to stop the build process in case some dependency\n+        fails.  See also `<https://github.com/easybuilders/easybuild-framework/issues/1441>`_\n+        \"\"\"\n+        pass\n+\n except ImportError as err:\n     _log.debug(\"Failed to import gc3libs from GC3Pie.\"\n                \" Silently ignoring, this is a real issue only when GC3Pie is used as backend for --job\")\n@@ -124,7 +134,7 @@\n             self.config_files.append(cfgfile)\n \n         self.output_dir = build_option('job_output_dir')\n-        self.jobs = DependentTaskCollection(output_dir=self.output_dir)\n+        self.jobs = _BuildTaskCollection(output_dir=self.output_dir)\n         self.job_cnt = 0\n \n         # after polling for job status, sleep for this time duration\n@@ -135,23 +145,20 @@\n         \"\"\"\n         Create and return a job object with the given parameters.\n \n-        First argument `server` is an instance of the corresponding\n-        `JobBackend` class, i.e., a `GC3Pie`:class: instance in this case.\n-\n-        Second argument `script` is the content of the job script\n+        Argument *script* is the content of the job script\n         itself, i.e., the sequence of shell commands that will be\n         executed.\n \n-        Third argument `name` sets the job human-readable name.\n+        Argument *name* sets the job's human-readable name.\n \n-        Fourth (optional) argument `env_vars` is a dictionary with\n+        Optional argument *env_vars* is a dictionary with\n         key-value pairs of environment variables that should be passed\n         on to the job.\n \n-        Fifth and sixth (optional) arguments `hours` and `cores` should be\n+        Optional arguments *hours* and *cores* should be\n         integer values:\n-        * hours must be in the range 1 .. MAX_WALLTIME;\n-        * cores depends on which cluster the job is being run.\n+        - *hours* must be in the range 1 .. ``MAX_WALLTIME``;\n+        - *cores* depends on which cluster the job is being run.\n         \"\"\"\n         named_args = {\n             'jobname': name, # job name in GC3Pie\n@@ -236,7 +243,7 @@\n         # the application.\n         self._engine.add(self.jobs)\n \n-        # in case you want to select a specific resource, call\n+        # select a specific execution resource?\n         target_resource = build_option('job_target_resource')\n         if target_resource:\n             res = self._engine.select_resource(target_resource)\n", "issue": "GC3Pie backend submits jobs even if dependency build failed\nWhen robot-building a bunch of software via the GC3Pie job backend with SLURM, GC3Pie happily submits follow-up jobs even if building of one (or more) of the dependencies failed.  I am not entirely sure whether this is an issue in EasyBuild, in GC3Pie, or in the way the two are working together.  I also don't know whether this also happens with other job schedulers.\n\nSteps to reproduce:\n- Start with an empty installdir\n- Edit `M4-1.4.17.eb` and add `preconfigopts='false && '`\n- Run `eb GCC-5.1.0.eb -r --job` using GC3Pie with SLURM\n- Both jobs will be submitted, both fail (the first for obvious reasons, the second because the M4 module can't be found)\n\nGC3Pie backend submits jobs even if dependency build failed\nWhen robot-building a bunch of software via the GC3Pie job backend with SLURM, GC3Pie happily submits follow-up jobs even if building of one (or more) of the dependencies failed.  I am not entirely sure whether this is an issue in EasyBuild, in GC3Pie, or in the way the two are working together.  I also don't know whether this also happens with other job schedulers.\n\nSteps to reproduce:\n- Start with an empty installdir\n- Edit `M4-1.4.17.eb` and add `preconfigopts='false && '`\n- Run `eb GCC-5.1.0.eb -r --job` using GC3Pie with SLURM\n- Both jobs will be submitted, both fail (the first for obvious reasons, the second because the M4 module can't be found)\n\n", "before_files": [{"content": "##\n# Copyright 2015-2018 Ghent University\n# Copyright 2015 S3IT, University of Zurich\n#\n# This file is part of EasyBuild,\n# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),\n# with support of Ghent University (http://ugent.be/hpc),\n# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),\n# Flemish Research Foundation (FWO) (http://www.fwo.be/en)\n# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).\n#\n# https://github.com/easybuilders/easybuild\n#\n# EasyBuild is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation v2.\n#\n# EasyBuild is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with EasyBuild.  If not, see <http://www.gnu.org/licenses/>.\n##\n\"\"\"\nInterface for submitting jobs via GC3Pie.\n\n:author: Riccardo Murri (University of Zurich)\n:author: Kenneth Hoste (Ghent University)\n\"\"\"\nfrom distutils.version import LooseVersion\nfrom time import gmtime, strftime\nimport re\nimport time\n\nfrom vsc.utils import fancylogger\n\nfrom easybuild.tools.build_log import EasyBuildError, print_msg\nfrom easybuild.tools.config import build_option\nfrom easybuild.tools.job.backend import JobBackend\nfrom easybuild.tools.utilities import only_if_module_is_available\n\n\n_log = fancylogger.getLogger('gc3pie', fname=False)\n\n\ntry:\n    import gc3libs\n    import gc3libs.exceptions\n    from gc3libs import Application, Run, create_engine\n    from gc3libs.core import Engine\n    from gc3libs.quantity import hours as hr\n    from gc3libs.workflow import DependentTaskCollection\n\n    # inject EasyBuild logger into GC3Pie\n    gc3libs.log = fancylogger.getLogger('gc3pie', fname=False)\n    # make handling of log.error compatible with stdlib logging\n    gc3libs.log.raiseError = False\n\n    # instruct GC3Pie to not ignore errors, but raise exceptions instead\n    gc3libs.UNIGNORE_ALL_ERRORS = True\n\nexcept ImportError as err:\n    _log.debug(\"Failed to import gc3libs from GC3Pie.\"\n               \" Silently ignoring, this is a real issue only when GC3Pie is used as backend for --job\")\n\n\n# eb --job --job-backend=GC3Pie\nclass GC3Pie(JobBackend):\n    \"\"\"\n    Use the GC3Pie framework to submit and monitor compilation jobs,\n    see http://gc3pie.readthedocs.org/.\n\n    In contrast with accessing an external service, GC3Pie implements\n    its own workflow manager, which means ``eb --job\n    --job-backend=GC3Pie`` will keep running until all jobs have\n    terminated.\n    \"\"\"\n\n    REQ_VERSION = '2.4.0'\n    VERSION_REGEX = re.compile(r'^(?P<version>\\S*) version')\n\n    @only_if_module_is_available('gc3libs', pkgname='gc3pie')\n    def __init__(self, *args, **kwargs):\n        \"\"\"GC3Pie JobBackend constructor.\"\"\"\n        super(GC3Pie, self).__init__(*args, **kwargs)\n\n    # _check_version is called by __init__, so guard it (too) with the decorator\n    @only_if_module_is_available('gc3libs', pkgname='gc3pie')\n    def _check_version(self):\n        \"\"\"Check whether GC3Pie version complies with required version.\"\"\"\n        # location of __version__ to use may change, depending on the minimal required SVN revision for development versions\n        version_str = gc3libs.core.__version__\n\n        match = self.VERSION_REGEX.search(version_str)\n        if match:\n            version = match.group('version')\n            self.log.debug(\"Parsed GC3Pie version info: '%s'\", version)\n\n            if version == 'development':\n                # presume it's OK -- there's no way to check since GC3Pie switched to git\n                return True\n\n            if LooseVersion(version) < LooseVersion(self.REQ_VERSION):\n                raise EasyBuildError(\"Found GC3Pie version %s, but version %s or more recent is required\",\n                                     version, self.REQ_VERSION)\n        else:\n            raise EasyBuildError(\"Failed to parse GC3Pie version string '%s' using pattern %s\",\n                                 version_str, self.VERSION_REGEX.pattern)\n\n    def init(self):\n        \"\"\"\n        Initialise the GC3Pie job backend.\n        \"\"\"\n        # List of config files for GC3Pie; non-existing ones will be\n        # silently ignored.  The list here copies GC3Pie's default,\n        # for the principle of minimal surprise, but there is no\n        # strict requirement that this be done and EB could actually\n        # choose to use a completely distinct set of conf. files.\n        self.config_files = gc3libs.Default.CONFIG_FILE_LOCATIONS[:]\n        cfgfile = build_option('job_backend_config')\n        if cfgfile:\n            self.config_files.append(cfgfile)\n\n        self.output_dir = build_option('job_output_dir')\n        self.jobs = DependentTaskCollection(output_dir=self.output_dir)\n        self.job_cnt = 0\n\n        # after polling for job status, sleep for this time duration\n        # before polling again (in seconds)\n        self.poll_interval = build_option('job_polling_interval')\n\n    def make_job(self, script, name, env_vars=None, hours=None, cores=None):\n        \"\"\"\n        Create and return a job object with the given parameters.\n\n        First argument `server` is an instance of the corresponding\n        `JobBackend` class, i.e., a `GC3Pie`:class: instance in this case.\n\n        Second argument `script` is the content of the job script\n        itself, i.e., the sequence of shell commands that will be\n        executed.\n\n        Third argument `name` sets the job human-readable name.\n\n        Fourth (optional) argument `env_vars` is a dictionary with\n        key-value pairs of environment variables that should be passed\n        on to the job.\n\n        Fifth and sixth (optional) arguments `hours` and `cores` should be\n        integer values:\n        * hours must be in the range 1 .. MAX_WALLTIME;\n        * cores depends on which cluster the job is being run.\n        \"\"\"\n        named_args = {\n            'jobname': name, # job name in GC3Pie\n            'name':    name, # job name in EasyBuild\n        }\n\n        # environment\n        if env_vars:\n            named_args['environment'] = env_vars\n\n        # input/output files for job (none)\n        named_args['inputs'] = []\n        named_args['outputs'] = []\n\n        # job logs\n        named_args.update({\n            # join stdout/stderr in a single log\n            'join': True,\n            # location for log file\n            'output_dir': self.output_dir,\n            # log file name (including timestamp to try and ensure unique filename)\n            'stdout': 'eb-%s-gc3pie-job-%s.log' % (name, strftime(\"%Y%M%d-UTC-%H-%M-%S\", gmtime()))\n        })\n\n        # walltime\n        max_walltime = build_option('job_max_walltime')\n        if hours is None:\n            hours = max_walltime\n        if hours > max_walltime:\n            self.log.warn(\"Specified %s hours, but this is impossible. (resetting to %s hours)\" % (hours, max_walltime))\n            hours = max_walltime\n        named_args['requested_walltime'] = hours * hr\n\n        if cores:\n            named_args['requested_cores'] = cores\n        else:\n            self.log.warn(\"Number of cores to request not specified, falling back to whatever GC3Pie does by default\")\n\n        return Application(['/bin/sh', '-c', script], **named_args)\n\n    def queue(self, job, dependencies=frozenset()):\n        \"\"\"\n        Add a job to the queue, optionally specifying dependencies.\n\n        :param dependencies: jobs on which this job depends.\n        \"\"\"\n        self.jobs.add(job, dependencies)\n        # since it's not trivial to determine the correct job count from self.jobs, we keep track of a count ourselves\n        self.job_cnt += 1\n\n    def complete(self):\n        \"\"\"\n        Complete a bulk job submission.\n\n        Create engine, and progress it until all jobs have terminated.\n        \"\"\"\n        # create an instance of `Engine` using the list of configuration files\n        try:\n            self._engine = create_engine(*self.config_files, resource_errors_are_fatal=True)\n\n        except gc3libs.exceptions.Error as err:\n            raise EasyBuildError(\"Failed to create GC3Pie engine: %s\", err)\n\n        # make sure that all job log files end up in the same directory, rather than renaming the output directory\n        # see https://gc3pie.readthedocs.org/en/latest/programmers/api/gc3libs/core.html#gc3libs.core.Engine\n        self._engine.retrieve_overwrites = True\n\n        # some sites may not be happy with flooding the cluster with build jobs...\n        self._engine.max_in_flight = build_option('job_max_jobs')\n\n        # `Engine.stats()` (which is used later on in `_print_status_report()`)\n        # changed between 2.4.2 and 2.5.0.dev -- make sure we stay compatible\n        # with both\n        try:\n            self._engine.init_stats_for(Application)\n        except AttributeError:\n            _log.debug(\"No `init_stats_for` method in the Engine class;\"\n                       \" assuming pre-2.5.0 GC3Pie and ignoring error.\")\n\n        # Add your application to the engine. This will NOT submit\n        # your application yet, but will make the engine *aware* of\n        # the application.\n        self._engine.add(self.jobs)\n\n        # in case you want to select a specific resource, call\n        target_resource = build_option('job_target_resource')\n        if target_resource:\n            res = self._engine.select_resource(target_resource)\n            if res == 0:\n                raise EasyBuildError(\"Failed to select target resource '%s' in GC3Pie\", target_resource)\n\n        # Periodically check the status of your application.\n        while self.jobs.execution.state != Run.State.TERMINATED:\n            # `Engine.progress()` will do the GC3Pie magic:\n            # submit new jobs, update status of submitted jobs, get\n            # results of terminating jobs etc...\n            self._engine.progress()\n\n            # report progress\n            self._print_status_report()\n\n            # Wait a few seconds...\n            time.sleep(self.poll_interval)\n\n        # final status report\n        print_msg(\"Done processing jobs\", log=self.log, silent=build_option('silent'))\n        self._print_status_report()\n\n    def _print_status_report(self):\n        \"\"\"\n        Print a job status report to STDOUT and the log file.\n\n        The number of jobs in each state is reported; the\n        figures are extracted from the `stats()` method of the\n        currently-running GC3Pie engine.\n        \"\"\"\n        stats = self._engine.stats(only=Application)\n        states = ', '.join([\"%d %s\" % (stats[s], s.lower()) for s in stats if s != 'total' and stats[s]])\n        print_msg(\"GC3Pie job overview: %s (total: %s)\" % (states, self.job_cnt),\n                  log=self.log, silent=build_option('silent'))\n", "path": "easybuild/tools/job/gc3pie.py"}], "after_files": [{"content": "##\n# Copyright 2015-2018 Ghent University\n# Copyright 2015 S3IT, University of Zurich\n#\n# This file is part of EasyBuild,\n# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),\n# with support of Ghent University (http://ugent.be/hpc),\n# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),\n# Flemish Research Foundation (FWO) (http://www.fwo.be/en)\n# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).\n#\n# https://github.com/easybuilders/easybuild\n#\n# EasyBuild is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation v2.\n#\n# EasyBuild is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with EasyBuild.  If not, see <http://www.gnu.org/licenses/>.\n##\n\"\"\"\nInterface for submitting jobs via GC3Pie.\n\n:author: Riccardo Murri (University of Zurich)\n:author: Kenneth Hoste (Ghent University)\n\"\"\"\nfrom distutils.version import LooseVersion\nfrom time import gmtime, strftime\nimport re\nimport time\n\nfrom vsc.utils import fancylogger\n\nfrom easybuild.tools.build_log import EasyBuildError, print_msg\nfrom easybuild.tools.config import build_option\nfrom easybuild.tools.job.backend import JobBackend\nfrom easybuild.tools.utilities import only_if_module_is_available\n\n\n_log = fancylogger.getLogger('gc3pie', fname=False)\n\n\ntry:\n    import gc3libs\n    import gc3libs.exceptions\n    from gc3libs import Application, Run, create_engine\n    from gc3libs.core import Engine\n    from gc3libs.quantity import hours as hr\n    from gc3libs.workflow import AbortOnError, DependentTaskCollection\n\n    # inject EasyBuild logger into GC3Pie\n    gc3libs.log = fancylogger.getLogger('gc3pie', fname=False)\n    # make handling of log.error compatible with stdlib logging\n    gc3libs.log.raiseError = False\n\n    # instruct GC3Pie to not ignore errors, but raise exceptions instead\n    gc3libs.UNIGNORE_ALL_ERRORS = True\n\n    # note: order of class inheritance is important!\n    class _BuildTaskCollection(AbortOnError, DependentTaskCollection):\n        \"\"\"\n        A `DependentTaskCollection`:class: that aborts execution upon error.\n\n        This is used to stop the build process in case some dependency\n        fails.  See also `<https://github.com/easybuilders/easybuild-framework/issues/1441>`_\n        \"\"\"\n        pass\n\nexcept ImportError as err:\n    _log.debug(\"Failed to import gc3libs from GC3Pie.\"\n               \" Silently ignoring, this is a real issue only when GC3Pie is used as backend for --job\")\n\n\n# eb --job --job-backend=GC3Pie\nclass GC3Pie(JobBackend):\n    \"\"\"\n    Use the GC3Pie framework to submit and monitor compilation jobs,\n    see http://gc3pie.readthedocs.org/.\n\n    In contrast with accessing an external service, GC3Pie implements\n    its own workflow manager, which means ``eb --job\n    --job-backend=GC3Pie`` will keep running until all jobs have\n    terminated.\n    \"\"\"\n\n    REQ_VERSION = '2.4.0'\n    VERSION_REGEX = re.compile(r'^(?P<version>\\S*) version')\n\n    @only_if_module_is_available('gc3libs', pkgname='gc3pie')\n    def __init__(self, *args, **kwargs):\n        \"\"\"GC3Pie JobBackend constructor.\"\"\"\n        super(GC3Pie, self).__init__(*args, **kwargs)\n\n    # _check_version is called by __init__, so guard it (too) with the decorator\n    @only_if_module_is_available('gc3libs', pkgname='gc3pie')\n    def _check_version(self):\n        \"\"\"Check whether GC3Pie version complies with required version.\"\"\"\n        # location of __version__ to use may change, depending on the minimal required SVN revision for development versions\n        version_str = gc3libs.core.__version__\n\n        match = self.VERSION_REGEX.search(version_str)\n        if match:\n            version = match.group('version')\n            self.log.debug(\"Parsed GC3Pie version info: '%s'\", version)\n\n            if version == 'development':\n                # presume it's OK -- there's no way to check since GC3Pie switched to git\n                return True\n\n            if LooseVersion(version) < LooseVersion(self.REQ_VERSION):\n                raise EasyBuildError(\"Found GC3Pie version %s, but version %s or more recent is required\",\n                                     version, self.REQ_VERSION)\n        else:\n            raise EasyBuildError(\"Failed to parse GC3Pie version string '%s' using pattern %s\",\n                                 version_str, self.VERSION_REGEX.pattern)\n\n    def init(self):\n        \"\"\"\n        Initialise the GC3Pie job backend.\n        \"\"\"\n        # List of config files for GC3Pie; non-existing ones will be\n        # silently ignored.  The list here copies GC3Pie's default,\n        # for the principle of minimal surprise, but there is no\n        # strict requirement that this be done and EB could actually\n        # choose to use a completely distinct set of conf. files.\n        self.config_files = gc3libs.Default.CONFIG_FILE_LOCATIONS[:]\n        cfgfile = build_option('job_backend_config')\n        if cfgfile:\n            self.config_files.append(cfgfile)\n\n        self.output_dir = build_option('job_output_dir')\n        self.jobs = _BuildTaskCollection(output_dir=self.output_dir)\n        self.job_cnt = 0\n\n        # after polling for job status, sleep for this time duration\n        # before polling again (in seconds)\n        self.poll_interval = build_option('job_polling_interval')\n\n    def make_job(self, script, name, env_vars=None, hours=None, cores=None):\n        \"\"\"\n        Create and return a job object with the given parameters.\n\n        Argument *script* is the content of the job script\n        itself, i.e., the sequence of shell commands that will be\n        executed.\n\n        Argument *name* sets the job's human-readable name.\n\n        Optional argument *env_vars* is a dictionary with\n        key-value pairs of environment variables that should be passed\n        on to the job.\n\n        Optional arguments *hours* and *cores* should be\n        integer values:\n        - *hours* must be in the range 1 .. ``MAX_WALLTIME``;\n        - *cores* depends on which cluster the job is being run.\n        \"\"\"\n        named_args = {\n            'jobname': name, # job name in GC3Pie\n            'name':    name, # job name in EasyBuild\n        }\n\n        # environment\n        if env_vars:\n            named_args['environment'] = env_vars\n\n        # input/output files for job (none)\n        named_args['inputs'] = []\n        named_args['outputs'] = []\n\n        # job logs\n        named_args.update({\n            # join stdout/stderr in a single log\n            'join': True,\n            # location for log file\n            'output_dir': self.output_dir,\n            # log file name (including timestamp to try and ensure unique filename)\n            'stdout': 'eb-%s-gc3pie-job-%s.log' % (name, strftime(\"%Y%M%d-UTC-%H-%M-%S\", gmtime()))\n        })\n\n        # walltime\n        max_walltime = build_option('job_max_walltime')\n        if hours is None:\n            hours = max_walltime\n        if hours > max_walltime:\n            self.log.warn(\"Specified %s hours, but this is impossible. (resetting to %s hours)\" % (hours, max_walltime))\n            hours = max_walltime\n        named_args['requested_walltime'] = hours * hr\n\n        if cores:\n            named_args['requested_cores'] = cores\n        else:\n            self.log.warn(\"Number of cores to request not specified, falling back to whatever GC3Pie does by default\")\n\n        return Application(['/bin/sh', '-c', script], **named_args)\n\n    def queue(self, job, dependencies=frozenset()):\n        \"\"\"\n        Add a job to the queue, optionally specifying dependencies.\n\n        :param dependencies: jobs on which this job depends.\n        \"\"\"\n        self.jobs.add(job, dependencies)\n        # since it's not trivial to determine the correct job count from self.jobs, we keep track of a count ourselves\n        self.job_cnt += 1\n\n    def complete(self):\n        \"\"\"\n        Complete a bulk job submission.\n\n        Create engine, and progress it until all jobs have terminated.\n        \"\"\"\n        # create an instance of `Engine` using the list of configuration files\n        try:\n            self._engine = create_engine(*self.config_files, resource_errors_are_fatal=True)\n\n        except gc3libs.exceptions.Error as err:\n            raise EasyBuildError(\"Failed to create GC3Pie engine: %s\", err)\n\n        # make sure that all job log files end up in the same directory, rather than renaming the output directory\n        # see https://gc3pie.readthedocs.org/en/latest/programmers/api/gc3libs/core.html#gc3libs.core.Engine\n        self._engine.retrieve_overwrites = True\n\n        # some sites may not be happy with flooding the cluster with build jobs...\n        self._engine.max_in_flight = build_option('job_max_jobs')\n\n        # `Engine.stats()` (which is used later on in `_print_status_report()`)\n        # changed between 2.4.2 and 2.5.0.dev -- make sure we stay compatible\n        # with both\n        try:\n            self._engine.init_stats_for(Application)\n        except AttributeError:\n            _log.debug(\"No `init_stats_for` method in the Engine class;\"\n                       \" assuming pre-2.5.0 GC3Pie and ignoring error.\")\n\n        # Add your application to the engine. This will NOT submit\n        # your application yet, but will make the engine *aware* of\n        # the application.\n        self._engine.add(self.jobs)\n\n        # select a specific execution resource?\n        target_resource = build_option('job_target_resource')\n        if target_resource:\n            res = self._engine.select_resource(target_resource)\n            if res == 0:\n                raise EasyBuildError(\"Failed to select target resource '%s' in GC3Pie\", target_resource)\n\n        # Periodically check the status of your application.\n        while self.jobs.execution.state != Run.State.TERMINATED:\n            # `Engine.progress()` will do the GC3Pie magic:\n            # submit new jobs, update status of submitted jobs, get\n            # results of terminating jobs etc...\n            self._engine.progress()\n\n            # report progress\n            self._print_status_report()\n\n            # Wait a few seconds...\n            time.sleep(self.poll_interval)\n\n        # final status report\n        print_msg(\"Done processing jobs\", log=self.log, silent=build_option('silent'))\n        self._print_status_report()\n\n    def _print_status_report(self):\n        \"\"\"\n        Print a job status report to STDOUT and the log file.\n\n        The number of jobs in each state is reported; the\n        figures are extracted from the `stats()` method of the\n        currently-running GC3Pie engine.\n        \"\"\"\n        stats = self._engine.stats(only=Application)\n        states = ', '.join([\"%d %s\" % (stats[s], s.lower()) for s in stats if s != 'total' and stats[s]])\n        print_msg(\"GC3Pie job overview: %s (total: %s)\" % (states, self.job_cnt),\n                  log=self.log, silent=build_option('silent'))\n", "path": "easybuild/tools/job/gc3pie.py"}]}