vortex-nwp 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vortex/__init__.py +159 -0
- vortex/algo/__init__.py +13 -0
- vortex/algo/components.py +2462 -0
- vortex/algo/mpitools.py +1953 -0
- vortex/algo/mpitools_templates/__init__.py +1 -0
- vortex/algo/mpitools_templates/envelope_wrapper_default.tpl +27 -0
- vortex/algo/mpitools_templates/envelope_wrapper_mpiauto.tpl +29 -0
- vortex/algo/mpitools_templates/wrapstd_wrapper_default.tpl +18 -0
- vortex/algo/serversynctools.py +171 -0
- vortex/config.py +112 -0
- vortex/data/__init__.py +19 -0
- vortex/data/abstractstores.py +1510 -0
- vortex/data/containers.py +835 -0
- vortex/data/contents.py +622 -0
- vortex/data/executables.py +275 -0
- vortex/data/flow.py +119 -0
- vortex/data/geometries.ini +2689 -0
- vortex/data/geometries.py +799 -0
- vortex/data/handlers.py +1230 -0
- vortex/data/outflow.py +67 -0
- vortex/data/providers.py +487 -0
- vortex/data/resources.py +207 -0
- vortex/data/stores.py +1390 -0
- vortex/data/sync_templates/__init__.py +0 -0
- vortex/gloves.py +309 -0
- vortex/layout/__init__.py +20 -0
- vortex/layout/contexts.py +577 -0
- vortex/layout/dataflow.py +1220 -0
- vortex/layout/monitor.py +969 -0
- vortex/nwp/__init__.py +14 -0
- vortex/nwp/algo/__init__.py +21 -0
- vortex/nwp/algo/assim.py +537 -0
- vortex/nwp/algo/clim.py +1086 -0
- vortex/nwp/algo/coupling.py +831 -0
- vortex/nwp/algo/eda.py +840 -0
- vortex/nwp/algo/eps.py +785 -0
- vortex/nwp/algo/forecasts.py +886 -0
- vortex/nwp/algo/fpserver.py +1303 -0
- vortex/nwp/algo/ifsnaming.py +463 -0
- vortex/nwp/algo/ifsroot.py +404 -0
- vortex/nwp/algo/monitoring.py +263 -0
- vortex/nwp/algo/mpitools.py +694 -0
- vortex/nwp/algo/odbtools.py +1258 -0
- vortex/nwp/algo/oopsroot.py +916 -0
- vortex/nwp/algo/oopstests.py +220 -0
- vortex/nwp/algo/request.py +660 -0
- vortex/nwp/algo/stdpost.py +1641 -0
- vortex/nwp/data/__init__.py +30 -0
- vortex/nwp/data/assim.py +380 -0
- vortex/nwp/data/boundaries.py +314 -0
- vortex/nwp/data/climfiles.py +521 -0
- vortex/nwp/data/configfiles.py +153 -0
- vortex/nwp/data/consts.py +954 -0
- vortex/nwp/data/ctpini.py +149 -0
- vortex/nwp/data/diagnostics.py +209 -0
- vortex/nwp/data/eda.py +147 -0
- vortex/nwp/data/eps.py +432 -0
- vortex/nwp/data/executables.py +1045 -0
- vortex/nwp/data/fields.py +111 -0
- vortex/nwp/data/gridfiles.py +380 -0
- vortex/nwp/data/logs.py +584 -0
- vortex/nwp/data/modelstates.py +363 -0
- vortex/nwp/data/monitoring.py +193 -0
- vortex/nwp/data/namelists.py +696 -0
- vortex/nwp/data/obs.py +840 -0
- vortex/nwp/data/oopsexec.py +74 -0
- vortex/nwp/data/providers.py +207 -0
- vortex/nwp/data/query.py +206 -0
- vortex/nwp/data/stores.py +160 -0
- vortex/nwp/data/surfex.py +337 -0
- vortex/nwp/syntax/__init__.py +9 -0
- vortex/nwp/syntax/stdattrs.py +437 -0
- vortex/nwp/tools/__init__.py +10 -0
- vortex/nwp/tools/addons.py +40 -0
- vortex/nwp/tools/agt.py +67 -0
- vortex/nwp/tools/bdap.py +59 -0
- vortex/nwp/tools/bdcp.py +41 -0
- vortex/nwp/tools/bdm.py +24 -0
- vortex/nwp/tools/bdmp.py +54 -0
- vortex/nwp/tools/conftools.py +1661 -0
- vortex/nwp/tools/drhook.py +66 -0
- vortex/nwp/tools/grib.py +294 -0
- vortex/nwp/tools/gribdiff.py +104 -0
- vortex/nwp/tools/ifstools.py +203 -0
- vortex/nwp/tools/igastuff.py +273 -0
- vortex/nwp/tools/mars.py +68 -0
- vortex/nwp/tools/odb.py +657 -0
- vortex/nwp/tools/partitioning.py +258 -0
- vortex/nwp/tools/satrad.py +71 -0
- vortex/nwp/util/__init__.py +6 -0
- vortex/nwp/util/async.py +212 -0
- vortex/nwp/util/beacon.py +40 -0
- vortex/nwp/util/diffpygram.py +447 -0
- vortex/nwp/util/ens.py +279 -0
- vortex/nwp/util/hooks.py +139 -0
- vortex/nwp/util/taskdeco.py +85 -0
- vortex/nwp/util/usepygram.py +697 -0
- vortex/nwp/util/usetnt.py +101 -0
- vortex/proxy.py +6 -0
- vortex/sessions.py +374 -0
- vortex/syntax/__init__.py +9 -0
- vortex/syntax/stdattrs.py +867 -0
- vortex/syntax/stddeco.py +185 -0
- vortex/toolbox.py +1117 -0
- vortex/tools/__init__.py +20 -0
- vortex/tools/actions.py +523 -0
- vortex/tools/addons.py +316 -0
- vortex/tools/arm.py +96 -0
- vortex/tools/compression.py +325 -0
- vortex/tools/date.py +27 -0
- vortex/tools/ddhpack.py +10 -0
- vortex/tools/delayedactions.py +782 -0
- vortex/tools/env.py +541 -0
- vortex/tools/folder.py +834 -0
- vortex/tools/grib.py +738 -0
- vortex/tools/lfi.py +953 -0
- vortex/tools/listings.py +423 -0
- vortex/tools/names.py +637 -0
- vortex/tools/net.py +2124 -0
- vortex/tools/odb.py +10 -0
- vortex/tools/parallelism.py +368 -0
- vortex/tools/prestaging.py +210 -0
- vortex/tools/rawfiles.py +10 -0
- vortex/tools/schedulers.py +480 -0
- vortex/tools/services.py +940 -0
- vortex/tools/storage.py +996 -0
- vortex/tools/surfex.py +61 -0
- vortex/tools/systems.py +3976 -0
- vortex/tools/targets.py +440 -0
- vortex/util/__init__.py +9 -0
- vortex/util/config.py +1122 -0
- vortex/util/empty.py +24 -0
- vortex/util/helpers.py +216 -0
- vortex/util/introspection.py +69 -0
- vortex/util/iosponge.py +80 -0
- vortex/util/roles.py +49 -0
- vortex/util/storefunctions.py +129 -0
- vortex/util/structs.py +26 -0
- vortex/util/worker.py +162 -0
- vortex_nwp-2.0.0.dist-info/METADATA +67 -0
- vortex_nwp-2.0.0.dist-info/RECORD +144 -0
- vortex_nwp-2.0.0.dist-info/WHEEL +5 -0
- vortex_nwp-2.0.0.dist-info/licenses/LICENSE +517 -0
- vortex_nwp-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2462 @@
|
|
|
1
|
+
# pylint: disable=unused-argument
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Abstract class for any AlgoComponent (:class:`AlgoComponent`) or AlgoComponent's
|
|
5
|
+
Mixins (:class:`AlgoComponentDecoMixin`).
|
|
6
|
+
|
|
7
|
+
Some very generic concrete AlgoComponent classes are also provided:
|
|
8
|
+
|
|
9
|
+
* :class:`Expresso`: launch a simple script;
|
|
10
|
+
* :class:`BlindRun`: launch a simple executable (no MPI);
|
|
11
|
+
* :class:`Parallel`: launch an MPI application.
|
|
12
|
+
|
|
13
|
+
Additional abstract classes provide multiprocessing support (through the
|
|
14
|
+
:mod:`taylorism` package):
|
|
15
|
+
|
|
16
|
+
* :class:`TaylorRun`: launch a piece of Python code on several processes;
|
|
17
|
+
* :class:`ParaExpresso`: launch a script multiple times (in parallel);
|
|
18
|
+
* :class:`ParaBlindRun`: launch an executable multiple times (in parallel).
|
|
19
|
+
|
|
20
|
+
Such classes are based on the :mod:`taylorism` (the developer should be familiar
|
|
21
|
+
with this package) and uses "Worker" classes provided in the
|
|
22
|
+
:mod:`vortex.tools.parallelism` package.
|
|
23
|
+
|
|
24
|
+
A few examples of AlgoComponent classes are shipped with the code
|
|
25
|
+
(see :ref:`examples_algo`). In addition to the documentation provided
|
|
26
|
+
in :ref:`stepbystep-index`, it might help.
|
|
27
|
+
|
|
28
|
+
When class inheritance is not applicable or ineffective, The AlgoComponent's
|
|
29
|
+
Mixins are a powerful tool to mutualise some pieces of code. See the
|
|
30
|
+
:class:`AlgoComponentDecoMixin` class documentation for more details.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import collections.abc
|
|
34
|
+
import contextlib
|
|
35
|
+
import copy
|
|
36
|
+
import functools
|
|
37
|
+
import importlib
|
|
38
|
+
import locale
|
|
39
|
+
import logging
|
|
40
|
+
import multiprocessing
|
|
41
|
+
import queue
|
|
42
|
+
import shlex
|
|
43
|
+
import sys
|
|
44
|
+
import tempfile
|
|
45
|
+
import traceback as py_traceback
|
|
46
|
+
|
|
47
|
+
from bronx.fancies import loggers
|
|
48
|
+
from bronx.stdtypes import date
|
|
49
|
+
from bronx.syntax.decorators import nicedeco
|
|
50
|
+
import footprints
|
|
51
|
+
from taylorism import Boss
|
|
52
|
+
import vortex
|
|
53
|
+
import vortex.config as config
|
|
54
|
+
from vortex.algo import mpitools
|
|
55
|
+
from vortex.syntax.stdattrs import DelayedEnvValue
|
|
56
|
+
from vortex.tools.parallelism import ParallelResultParser
|
|
57
|
+
|
|
58
|
+
#: No automatic export
|
|
59
|
+
__all__ = []
|
|
60
|
+
|
|
61
|
+
logger = loggers.getLogger(__name__)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AlgoComponentError(Exception):
|
|
65
|
+
"""Generic exception class for Algo Components."""
|
|
66
|
+
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AlgoComponentAssertionError(AlgoComponentError):
|
|
71
|
+
"""Assertion exception class for Algo Components."""
|
|
72
|
+
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DelayedAlgoComponentError(AlgoComponentError):
|
|
77
|
+
"""Triggered when exceptions occurred during the execution but were delayed."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, excs):
|
|
80
|
+
super().__init__("One or several errors occurred during the run.")
|
|
81
|
+
self._excs = excs
|
|
82
|
+
|
|
83
|
+
def __iter__(self):
|
|
84
|
+
yield from self._excs
|
|
85
|
+
|
|
86
|
+
def __str__(self):
|
|
87
|
+
outstr = "One or several errors occurred during the run. In order of appearance:\n"
|
|
88
|
+
outstr += "\n".join(
|
|
89
|
+
[
|
|
90
|
+
"{:3d}. {!s} (type: {!s})".format(i + 1, exc, type(exc))
|
|
91
|
+
for i, exc in enumerate(self)
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
return outstr
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ParallelInconsistencyAlgoComponentError(Exception):
|
|
98
|
+
"""Generic exception class for Algo Components."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, target):
|
|
101
|
+
msg = "The len of {:s} is inconsistent with the number or ResourceHandlers."
|
|
102
|
+
super().__init__(msg.format(target))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@nicedeco
|
|
106
|
+
def _clsmtd_mixin_locked(f):
|
|
107
|
+
"""
|
|
108
|
+
This is a utility decorator (for class methods) : it ensures that the method can only
|
|
109
|
+
be called on a bare :class:`AlgoComponentDecoMixin` class.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def wrapped_clsmethod(cls, *kargs, **kwargs):
|
|
113
|
+
if issubclass(cls, AlgoComponent):
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
"This class method should not be called once the mixin is in use."
|
|
116
|
+
)
|
|
117
|
+
return f(cls, *kargs, **kwargs)
|
|
118
|
+
|
|
119
|
+
return wrapped_clsmethod
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def algo_component_deco_mixin_autodoc(cls):
|
|
123
|
+
"""
|
|
124
|
+
Decorator that adds an automatic documentation on any :class:`AlgoComponentDecoMixin`
|
|
125
|
+
class.
|
|
126
|
+
"""
|
|
127
|
+
extradoc = ""
|
|
128
|
+
|
|
129
|
+
# Document extra footprints
|
|
130
|
+
if cls.MIXIN_AUTO_FPTWEAK and cls._MIXIN_EXTRA_FOOTPRINTS:
|
|
131
|
+
extradoc += "\nThe following footprints will be applied to the target classes:\n\n"
|
|
132
|
+
for fp in cls._MIXIN_EXTRA_FOOTPRINTS:
|
|
133
|
+
if isinstance(fp, footprints.Footprint):
|
|
134
|
+
extradoc += footprints.doc.format_docstring(
|
|
135
|
+
fp, footprints.setup.docstrings, abstractfpobj=True
|
|
136
|
+
)
|
|
137
|
+
extradoc += "\n"
|
|
138
|
+
|
|
139
|
+
# Document decorating classes
|
|
140
|
+
if cls.MIXIN_AUTO_DECO:
|
|
141
|
+
for what, desc in (
|
|
142
|
+
("PREPARE_PREHOOKS", "before the original ``prepare`` method"),
|
|
143
|
+
("PREPARE_HOOKS", "after the original ``prepare`` method"),
|
|
144
|
+
("POSTFIX_PREHOOKS", "before the original ``postfix`` method"),
|
|
145
|
+
("POSTFIX_HOOKS", "after the original ``postfix`` method"),
|
|
146
|
+
("SPAWN_HOOKS", "after the original ``spawn_hook`` method"),
|
|
147
|
+
(
|
|
148
|
+
"CLI_OPTS_EXTEND",
|
|
149
|
+
"to alter the result of the ``spawn_command_options`` method",
|
|
150
|
+
),
|
|
151
|
+
(
|
|
152
|
+
"STDIN_OPTS_EXTEND",
|
|
153
|
+
"to alter the result of the ``spawn_stdin_options`` method",
|
|
154
|
+
),
|
|
155
|
+
(
|
|
156
|
+
"_MIXIN_EXECUTE_OVERWRITE",
|
|
157
|
+
"instead of the original ``execute`` method",
|
|
158
|
+
),
|
|
159
|
+
(
|
|
160
|
+
"MPIBINS_HOOKS",
|
|
161
|
+
"to alter the result of the ``_bootstrap_mpibins_hack`` method",
|
|
162
|
+
),
|
|
163
|
+
(
|
|
164
|
+
"MPIENVELOPE_HOOKS",
|
|
165
|
+
"to alter the result of the ``_bootstrap_mpienvelope_hack`` method",
|
|
166
|
+
),
|
|
167
|
+
):
|
|
168
|
+
what = "_MIXIN_{:s}".format(what)
|
|
169
|
+
if getattr(cls, what, ()):
|
|
170
|
+
extradoc += "\nThe following method(s) will be called {:s}:\n\n".format(
|
|
171
|
+
desc
|
|
172
|
+
)
|
|
173
|
+
extradoc += "\n".join(
|
|
174
|
+
" * {!r}".format(cb) for cb in getattr(cls, what)
|
|
175
|
+
)
|
|
176
|
+
extradoc += "\n"
|
|
177
|
+
|
|
178
|
+
if extradoc:
|
|
179
|
+
extradoc = (
|
|
180
|
+
"\n .. note:: The following documentation is automatically generated. "
|
|
181
|
+
+ "From a developer point of view, using the present mixin class "
|
|
182
|
+
+ "will result in the following actions:\n"
|
|
183
|
+
+ " \n".join(
|
|
184
|
+
[" " + t if t else "" for t in extradoc.split("\n")]
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if isinstance(getattr(cls, "__doc__", None), str):
|
|
189
|
+
cls.__doc__ += "\n" + extradoc
|
|
190
|
+
else:
|
|
191
|
+
cls.__doc__ = extradoc
|
|
192
|
+
|
|
193
|
+
return cls
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class AlgoComponentDecoMixin:
|
|
197
|
+
"""
|
|
198
|
+
This is the base class for any Mixin class targeting :class:`AlgoComponent`
|
|
199
|
+
classes.
|
|
200
|
+
|
|
201
|
+
Like any Mixin class, this Mixin class primary use is to define methods that
|
|
202
|
+
will be available to the child class.
|
|
203
|
+
|
|
204
|
+
However, this class will also interact with the :class:`AlgoComponentMeta`
|
|
205
|
+
metaclass to alter the behaviour of the :class:`AlgoComponent` class it is
|
|
206
|
+
used with. Several "alterations" will be made to the resulting
|
|
207
|
+
:class:`AlgoComponent` class.
|
|
208
|
+
|
|
209
|
+
* A bunch of footprints' attribute can be added to the resulting class.
|
|
210
|
+
This is controlled by the :data:`MIXIN_AUTO_FPTWEAK` and
|
|
211
|
+
:data:`_MIXIN_EXTRA_FOOTPRINTS` class variables.
|
|
212
|
+
If :data:`MIXIN_AUTO_FPTWEAK` is ``True`` (which is the default), the
|
|
213
|
+
:class:`~footrprints.Footprint` objects listed in the
|
|
214
|
+
:data:`_MIXIN_EXTRA_FOOTPRINTS` tuple will be prepended to the resulting
|
|
215
|
+
:class:`AlgoComponent` class footprint definition.
|
|
216
|
+
|
|
217
|
+
* The ``execute`` method of the resulting class can be overwritten by
|
|
218
|
+
the method referenced in the :data:`_MIXIN_EXECUTE_OVERWRITE` class
|
|
219
|
+
variable. This is allowed only if no ``execute`` method is defined
|
|
220
|
+
manually and if no other :class:`AlgoComponentDecoMixin` tries to
|
|
221
|
+
overwrite it as well. If these two conditions are not met, a
|
|
222
|
+
:class:`RuntimeError` exception will be thrown by the the
|
|
223
|
+
:class:`AlgoComponentMeta` metaclass.
|
|
224
|
+
|
|
225
|
+
* A bunch of the :class:`AlgoComponent`'s methods can be decorated. This
|
|
226
|
+
is controlled by the :data:`MIXIN_AUTO_DECO` class variable (``True``
|
|
227
|
+
by default) and a bunch of other class variables containing tuples.
|
|
228
|
+
They are described below:
|
|
229
|
+
|
|
230
|
+
* :data:`_MIXIN_PREPARE_PREHOOKS`: Tuple of methods that will be
|
|
231
|
+
executed before the original prepare method. Such methods receive
|
|
232
|
+
the same arguments list than the original decorated method.
|
|
233
|
+
|
|
234
|
+
* :data:`_MIXIN_PREPARE_HOOKS`: Tuple of methods that will be
|
|
235
|
+
executed after the original prepare method. Such methods receive
|
|
236
|
+
the same arguments list than the original decorated method.
|
|
237
|
+
|
|
238
|
+
* :data:`_MIXIN_EXECUTE_FINALISE_HOOKS`: Tuple of method that will
|
|
239
|
+
be executed after any execution (even if the execution failed).
|
|
240
|
+
|
|
241
|
+
* :data:`_MIXIN_FAIL_EXECUTE_HOOKS`: Tuple of method that will
|
|
242
|
+
be executed if the execution fails (the original exception
|
|
243
|
+
will be re-raised afterwards)
|
|
244
|
+
|
|
245
|
+
* :data:`_MIXIN_POSTFIX_PREHOOKS`: Tuple of methods that will be
|
|
246
|
+
executed before the original postfix method. Such methods receive
|
|
247
|
+
the same arguments list than the original decorated method.
|
|
248
|
+
|
|
249
|
+
* :data:`_MIXIN_POSTFIX_HOOKS`: Tuple of methods that will be
|
|
250
|
+
executed after the original postfix method. Such methods receive
|
|
251
|
+
the same arguments list than the original decorated method.
|
|
252
|
+
|
|
253
|
+
* :data:`_MIXIN_SPAWN_HOOKS`: Tuple of methods that will be
|
|
254
|
+
executed after the original spawn_hook method. Such methods receive
|
|
255
|
+
the same arguments list than the original decorated method.
|
|
256
|
+
|
|
257
|
+
* :data:`_MIXIN_CLI_OPTS_EXTEND`: Tuple of methods that will be
|
|
258
|
+
executed after the original ``spawn_command_options`` method. Such
|
|
259
|
+
method will receive one argument (``self`` set aside): the value
|
|
260
|
+
returned by the original ``spawn_command_options`` method.
|
|
261
|
+
|
|
262
|
+
* :data:`_MIXIN_STDIN_OPTS_EXTEND`: Tuple of methods that will be
|
|
263
|
+
executed after the original ``spawn_stdin_options`` method. Such
|
|
264
|
+
method will receive one argument (``self`` set aside): the value
|
|
265
|
+
returned by the original ``spawn_stdin_options`` method.
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
MIXIN_AUTO_FPTWEAK = True
|
|
270
|
+
MIXIN_AUTO_DECO = True
|
|
271
|
+
|
|
272
|
+
_MIXIN_EXTRA_FOOTPRINTS = ()
|
|
273
|
+
|
|
274
|
+
_MIXIN_PREPARE_PREHOOKS = ()
|
|
275
|
+
_MIXIN_PREPARE_HOOKS = ()
|
|
276
|
+
_MIXIN_EXECUTE_FINALISE_HOOKS = ()
|
|
277
|
+
_MIXIN_FAIL_EXECUTE_HOOKS = ()
|
|
278
|
+
_MIXIN_POSTFIX_PREHOOKS = ()
|
|
279
|
+
_MIXIN_POSTFIX_HOOKS = ()
|
|
280
|
+
_MIXIN_SPAWN_HOOKS = ()
|
|
281
|
+
|
|
282
|
+
_MIXIN_CLI_OPTS_EXTEND = ()
|
|
283
|
+
_MIXIN_STDIN_OPTS_EXTEND = ()
|
|
284
|
+
|
|
285
|
+
_MIXIN_EXECUTE_OVERWRITE = None
|
|
286
|
+
|
|
287
|
+
def __new__(cls, *args, **kwargs):
|
|
288
|
+
if not issubclass(cls, AlgoComponent):
|
|
289
|
+
# This class cannot be instanciated by itself !
|
|
290
|
+
raise RuntimeError(
|
|
291
|
+
"< {0.__name__:s} > is a mixin class: it cannot be instantiated.".format(
|
|
292
|
+
cls
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
return super().__new__(cls)
|
|
297
|
+
|
|
298
|
+
@classmethod
|
|
299
|
+
@_clsmtd_mixin_locked
|
|
300
|
+
def mixin_tweak_footprint(cls, fplocal):
|
|
301
|
+
"""Update the footprint definition list."""
|
|
302
|
+
for fp in cls._MIXIN_EXTRA_FOOTPRINTS:
|
|
303
|
+
assert isinstance(fp, footprints.Footprint)
|
|
304
|
+
fplocal.insert(0, fp)
|
|
305
|
+
|
|
306
|
+
@classmethod
|
|
307
|
+
@_clsmtd_mixin_locked
|
|
308
|
+
def _get_algo_wrapped(
|
|
309
|
+
cls, targetcls, targetmtd, hooks, prehooks=(), reentering=False
|
|
310
|
+
):
|
|
311
|
+
"""Wraps **targetcls**'s **targetmtd** method."""
|
|
312
|
+
orig_mtd = getattr(targetcls, targetmtd)
|
|
313
|
+
if prehooks and reentering:
|
|
314
|
+
raise ValueError(
|
|
315
|
+
"Conflicting values between prehooks and reenterin."
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def wrapped_method(self, *kargs, **kwargs):
|
|
319
|
+
for phook in prehooks:
|
|
320
|
+
phook(self, *kargs, **kwargs)
|
|
321
|
+
rv = orig_mtd(self, *kargs, **kwargs)
|
|
322
|
+
if reentering:
|
|
323
|
+
kargs = [
|
|
324
|
+
rv,
|
|
325
|
+
] + list(kargs)
|
|
326
|
+
for phook in hooks:
|
|
327
|
+
rv = phook(self, *kargs, **kwargs)
|
|
328
|
+
if reentering:
|
|
329
|
+
kargs[0] = rv
|
|
330
|
+
if reentering:
|
|
331
|
+
return rv
|
|
332
|
+
|
|
333
|
+
wrapped_method.__name__ = orig_mtd.__name__
|
|
334
|
+
wrapped_method.__doc__ = (orig_mtd.__doc__ or "").rstrip(
|
|
335
|
+
"\n"
|
|
336
|
+
) + "\n\nDecorated by :class:`{0.__module__:s}{0.__name__:s}`.".format(
|
|
337
|
+
cls
|
|
338
|
+
)
|
|
339
|
+
wrapped_method.__dict__.update(orig_mtd.__dict__)
|
|
340
|
+
return wrapped_method
|
|
341
|
+
|
|
342
|
+
@classmethod
|
|
343
|
+
@_clsmtd_mixin_locked
|
|
344
|
+
def mixin_algo_deco(cls, targetcls):
|
|
345
|
+
"""
|
|
346
|
+
Applies all the necessary decorators to the **targetcls**
|
|
347
|
+
:class:`AlgoComponent` class.
|
|
348
|
+
"""
|
|
349
|
+
if not issubclass(targetcls, AlgoComponent):
|
|
350
|
+
raise RuntimeError(
|
|
351
|
+
"This class can only be mixed in AlgoComponent classes."
|
|
352
|
+
)
|
|
353
|
+
for targetmtd, hooks, prehooks, reenter in [
|
|
354
|
+
(
|
|
355
|
+
"prepare",
|
|
356
|
+
cls._MIXIN_PREPARE_HOOKS,
|
|
357
|
+
cls._MIXIN_PREPARE_PREHOOKS,
|
|
358
|
+
False,
|
|
359
|
+
),
|
|
360
|
+
("fail_execute", cls._MIXIN_FAIL_EXECUTE_HOOKS, (), False),
|
|
361
|
+
("execute_finalise", cls._MIXIN_EXECUTE_FINALISE_HOOKS, (), False),
|
|
362
|
+
(
|
|
363
|
+
"postfix",
|
|
364
|
+
cls._MIXIN_POSTFIX_HOOKS,
|
|
365
|
+
cls._MIXIN_POSTFIX_PREHOOKS,
|
|
366
|
+
False,
|
|
367
|
+
),
|
|
368
|
+
("spawn_hook", cls._MIXIN_SPAWN_HOOKS, (), False),
|
|
369
|
+
("spawn_command_options", cls._MIXIN_CLI_OPTS_EXTEND, (), True),
|
|
370
|
+
("spawn_stdin_options", cls._MIXIN_STDIN_OPTS_EXTEND, (), True),
|
|
371
|
+
]:
|
|
372
|
+
if hooks or prehooks:
|
|
373
|
+
setattr(
|
|
374
|
+
targetcls,
|
|
375
|
+
targetmtd,
|
|
376
|
+
cls._get_algo_wrapped(
|
|
377
|
+
targetcls, targetmtd, hooks, prehooks, reenter
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
return targetcls
|
|
381
|
+
|
|
382
|
+
@classmethod
|
|
383
|
+
@_clsmtd_mixin_locked
|
|
384
|
+
def mixin_execute_overwrite(cls):
|
|
385
|
+
return cls._MIXIN_EXECUTE_OVERWRITE
|
|
386
|
+
|
|
387
|
+
@classmethod
|
|
388
|
+
def mixin_execute_companion(cls):
|
|
389
|
+
"""Find on which class "super" should be called (if_MIXIN_EXECUTE_OVERWRITE is used)."""
|
|
390
|
+
comp = getattr(cls, "_algo_meta_execute_companion", ())
|
|
391
|
+
if not comp:
|
|
392
|
+
raise RuntimeError("unable to find a suitable companion class")
|
|
393
|
+
return comp
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class AlgoComponentMpiDecoMixin(AlgoComponentDecoMixin):
|
|
397
|
+
"""
|
|
398
|
+
This is the base class for Mixin class targeting :class:`Parallel`
|
|
399
|
+
classes.
|
|
400
|
+
|
|
401
|
+
It inherits all the behaviour of the :class:`AlgoComponentDecoMixin` base
|
|
402
|
+
class. But in addition, it allows to decorate additional :class:`Parallel`'s
|
|
403
|
+
methods using the following class variables:
|
|
404
|
+
|
|
405
|
+
* :data:`_MIXIN_MPIBINS_HOOKS`: Tuple of methods that will be
|
|
406
|
+
executed after the original ``_bootstrap_mpibins_hack`` method. Such
|
|
407
|
+
methods will receive five arguments (``self`` set aside):
|
|
408
|
+
|
|
409
|
+
* The list of :class:`mpitools.MpiBinaryDescription` objects returned
|
|
410
|
+
by the original ``_bootstrap_mpibins_hack`` method;
|
|
411
|
+
* The list of :class:`mpitools.MpiBinaryDescription` objects as
|
|
412
|
+
provided by the first caller;
|
|
413
|
+
* The list of binary ResourceHandlers as provided to the ``run``
|
|
414
|
+
method;
|
|
415
|
+
* A dictionary of options as provided to the ``run`` method;
|
|
416
|
+
* A boolean indicating if an MPI envelope is provided by the user.
|
|
417
|
+
|
|
418
|
+
* :data:`_MIXIN_MPIENVELOPE_HOOKS`: Tuple of methods that will be
|
|
419
|
+
executed after the original ``_bootstrap_mpienvelope_hack`` method. Such
|
|
420
|
+
methods will receive four arguments (``self`` set aside):
|
|
421
|
+
|
|
422
|
+
* The list of dictionaries describing the envelope returned
|
|
423
|
+
by the original``_bootstrap_mpienvelope_hack`` method;
|
|
424
|
+
* The list of dictionaries describing the envelope as
|
|
425
|
+
provided by the first caller;
|
|
426
|
+
* The list of binary ResourceHandlers as provided to the ``run``
|
|
427
|
+
method;
|
|
428
|
+
* A dictionary of options as provided to the ``run`` method;
|
|
429
|
+
* The :class:`mpitools.MpiTool` that is used to generate the
|
|
430
|
+
MPI command line
|
|
431
|
+
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
_MIXIN_MPIBINS_HOOKS = ()
|
|
435
|
+
_MIXIN_MPIENVELOPE_HOOKS = ()
|
|
436
|
+
_MIXIN_MPIENVELOPE_POSTHOOKS = ()
|
|
437
|
+
|
|
438
|
+
@classmethod
|
|
439
|
+
@_clsmtd_mixin_locked
|
|
440
|
+
def mixin_algo_deco(cls, targetcls):
|
|
441
|
+
"""
|
|
442
|
+
Applies all the necessary decorators to the **targetcls**
|
|
443
|
+
:class:`AlgoComponent` class.
|
|
444
|
+
"""
|
|
445
|
+
targetcls = AlgoComponentDecoMixin.mixin_algo_deco(targetcls)
|
|
446
|
+
if not issubclass(targetcls, Parallel):
|
|
447
|
+
raise RuntimeError(
|
|
448
|
+
"This class can only be mixed in Parallel classes."
|
|
449
|
+
)
|
|
450
|
+
for targetmtd, hooks, prehooks, reenter in [
|
|
451
|
+
("_bootstrap_mpibins_hack", cls._MIXIN_MPIBINS_HOOKS, (), True),
|
|
452
|
+
(
|
|
453
|
+
"_bootstrap_mpienvelope_hack",
|
|
454
|
+
cls._MIXIN_MPIENVELOPE_HOOKS,
|
|
455
|
+
(),
|
|
456
|
+
True,
|
|
457
|
+
),
|
|
458
|
+
(
|
|
459
|
+
"_bootstrap_mpienvelope_posthack",
|
|
460
|
+
cls._MIXIN_MPIENVELOPE_POSTHOOKS,
|
|
461
|
+
(),
|
|
462
|
+
True,
|
|
463
|
+
),
|
|
464
|
+
]:
|
|
465
|
+
if hooks or prehooks:
|
|
466
|
+
setattr(
|
|
467
|
+
targetcls,
|
|
468
|
+
targetmtd,
|
|
469
|
+
cls._get_algo_wrapped(
|
|
470
|
+
targetcls, targetmtd, hooks, prehooks, reenter
|
|
471
|
+
),
|
|
472
|
+
)
|
|
473
|
+
return targetcls
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class AlgoComponentMeta(footprints.FootprintBaseMeta):
|
|
477
|
+
"""Meta class for building :class:`AlgoComponent` classes.
|
|
478
|
+
|
|
479
|
+
In addition of performing footprints' usual stuff, it processes mixin classes
|
|
480
|
+
that derives from the :class:`AlgoComponentDecoMixin` class. See the
|
|
481
|
+
documentation of this class for more details.
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
def __new__(cls, n, b, d):
|
|
485
|
+
# Mixin candidates: a mixin must only be dealt with once hence the
|
|
486
|
+
# condition on issubclass(base, AlgoComponent)
|
|
487
|
+
candidates = [
|
|
488
|
+
base
|
|
489
|
+
for base in b
|
|
490
|
+
if (
|
|
491
|
+
issubclass(base, AlgoComponentDecoMixin)
|
|
492
|
+
and not issubclass(base, AlgoComponent)
|
|
493
|
+
)
|
|
494
|
+
]
|
|
495
|
+
# Tweak footprints
|
|
496
|
+
todobases = [base for base in candidates if base.MIXIN_AUTO_FPTWEAK]
|
|
497
|
+
if todobases:
|
|
498
|
+
fplocal = d.get("_footprint", list())
|
|
499
|
+
if not isinstance(fplocal, list):
|
|
500
|
+
fplocal = [
|
|
501
|
+
fplocal,
|
|
502
|
+
]
|
|
503
|
+
for base in todobases:
|
|
504
|
+
base.mixin_tweak_footprint(fplocal)
|
|
505
|
+
d["_footprint"] = fplocal
|
|
506
|
+
# Overwrite the execute method...
|
|
507
|
+
todobases_exc = [
|
|
508
|
+
base
|
|
509
|
+
for base in candidates
|
|
510
|
+
if base.mixin_execute_overwrite() is not None
|
|
511
|
+
]
|
|
512
|
+
if len(todobases_exc) > 1:
|
|
513
|
+
raise RuntimeError(
|
|
514
|
+
"Cannot overwrite < execute > multiple times: {:s}".format(
|
|
515
|
+
",".join([base.__name__ for base in todobases_exc])
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
if todobases_exc:
|
|
519
|
+
if "execute" in d:
|
|
520
|
+
raise RuntimeError(
|
|
521
|
+
"< execute > is already defined in the target class: cannot proceed"
|
|
522
|
+
)
|
|
523
|
+
d["execute"] = todobases_exc[0].mixin_execute_overwrite()
|
|
524
|
+
# Create the class as usual
|
|
525
|
+
fpcls = super().__new__(cls, n, b, d)
|
|
526
|
+
if todobases_exc:
|
|
527
|
+
setattr(fpcls, "_algo_meta_execute_companion", fpcls)
|
|
528
|
+
# Apply decorators
|
|
529
|
+
todobases = [base for base in candidates if base.MIXIN_AUTO_DECO]
|
|
530
|
+
for base in reversed(todobases):
|
|
531
|
+
base.mixin_algo_deco(fpcls)
|
|
532
|
+
return fpcls
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class AlgoComponent(footprints.FootprintBase, metaclass=AlgoComponentMeta):
|
|
536
|
+
"""Component in charge of any kind of processing."""
|
|
537
|
+
|
|
538
|
+
_SERVERSYNC_RAISEONEXIT = True
|
|
539
|
+
_SERVERSYNC_RUNONSTARTUP = True
|
|
540
|
+
_SERVERSYNC_STOPONEXIT = True
|
|
541
|
+
|
|
542
|
+
_abstract = True
|
|
543
|
+
_collector = ("component",)
|
|
544
|
+
_footprint = dict(
|
|
545
|
+
info="Abstract algo component",
|
|
546
|
+
attr=dict(
|
|
547
|
+
engine=dict(
|
|
548
|
+
info="The way the executable should be run.",
|
|
549
|
+
values=[
|
|
550
|
+
"algo",
|
|
551
|
+
],
|
|
552
|
+
),
|
|
553
|
+
flyput=dict(
|
|
554
|
+
info="Activate a background job in charge off on the fly processing.",
|
|
555
|
+
type=bool,
|
|
556
|
+
optional=True,
|
|
557
|
+
default=False,
|
|
558
|
+
access="rwx",
|
|
559
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
560
|
+
doc_zorder=-99,
|
|
561
|
+
),
|
|
562
|
+
flypoll=dict(
|
|
563
|
+
info="The system method called by the flyput background job.",
|
|
564
|
+
optional=True,
|
|
565
|
+
default="io_poll",
|
|
566
|
+
access="rwx",
|
|
567
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
568
|
+
doc_zorder=-99,
|
|
569
|
+
),
|
|
570
|
+
flyargs=dict(
|
|
571
|
+
info="Arguments for the *flypoll* method.",
|
|
572
|
+
type=footprints.FPTuple,
|
|
573
|
+
optional=True,
|
|
574
|
+
default=footprints.FPTuple(),
|
|
575
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
576
|
+
doc_zorder=-99,
|
|
577
|
+
),
|
|
578
|
+
flymapping=dict(
|
|
579
|
+
info="Allow renaming of output files during on the fly processing.",
|
|
580
|
+
optional=True,
|
|
581
|
+
default=False,
|
|
582
|
+
access="rwx",
|
|
583
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
584
|
+
doc_zorder=-99,
|
|
585
|
+
),
|
|
586
|
+
timeout=dict(
|
|
587
|
+
info="Default timeout (in sec.) used when waiting for an expected resource.",
|
|
588
|
+
type=int,
|
|
589
|
+
optional=True,
|
|
590
|
+
default=180,
|
|
591
|
+
doc_zorder=-50,
|
|
592
|
+
),
|
|
593
|
+
server_run=dict(
|
|
594
|
+
info="Run the executable as a server.",
|
|
595
|
+
type=bool,
|
|
596
|
+
optional=True,
|
|
597
|
+
values=[False],
|
|
598
|
+
default=False,
|
|
599
|
+
access="rwx",
|
|
600
|
+
doc_visibility=footprints.doc.visibility.ADVANCED,
|
|
601
|
+
),
|
|
602
|
+
serversync_method=dict(
|
|
603
|
+
info="The method that is used to synchronise with the server.",
|
|
604
|
+
optional=True,
|
|
605
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
606
|
+
),
|
|
607
|
+
serversync_medium=dict(
|
|
608
|
+
info="The medium that is used to synchronise with the server.",
|
|
609
|
+
optional=True,
|
|
610
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
611
|
+
),
|
|
612
|
+
extendpypath=dict(
|
|
613
|
+
info="The list of things to be prepended in the python's path.",
|
|
614
|
+
type=footprints.FPList,
|
|
615
|
+
default=footprints.FPList([]),
|
|
616
|
+
optional=True,
|
|
617
|
+
),
|
|
618
|
+
),
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
def __init__(self, *args, **kw):
|
|
622
|
+
logger.debug("Algo component init %s", self.__class__)
|
|
623
|
+
self._fslog = list()
|
|
624
|
+
self._promises = None
|
|
625
|
+
self._expected = None
|
|
626
|
+
self._delayed_excs = list()
|
|
627
|
+
self._server_synctool = None
|
|
628
|
+
self._server_process = None
|
|
629
|
+
super().__init__(*args, **kw)
|
|
630
|
+
|
|
631
|
+
@property
|
|
632
|
+
def realkind(self):
|
|
633
|
+
"""Default kind is ``algo``."""
|
|
634
|
+
return "algo"
|
|
635
|
+
|
|
636
|
+
@property
|
|
637
|
+
def fslog(self):
|
|
638
|
+
"""Changes on the filesystem during the execution."""
|
|
639
|
+
return self._fslog
|
|
640
|
+
|
|
641
|
+
def fstag(self):
|
|
642
|
+
"""Defines a tag specific to the current algo component."""
|
|
643
|
+
return "-".join((self.realkind, self.engine))
|
|
644
|
+
|
|
645
|
+
def fsstamp(self, opts):
|
|
646
|
+
"""Ask the current context to put a stamp on file system."""
|
|
647
|
+
self.context.fstrack_stamp(tag=self.fstag())
|
|
648
|
+
|
|
649
|
+
def fscheck(self, opts):
|
|
650
|
+
"""Ask the current context to check changes on file system since last stamp."""
|
|
651
|
+
self._fslog.append(self.context.fstrack_check(tag=self.fstag()))
|
|
652
|
+
|
|
653
|
+
@property
|
|
654
|
+
def promises(self):
|
|
655
|
+
"""Build and return list of actual promises of the current component."""
|
|
656
|
+
if self._promises is None:
|
|
657
|
+
self._promises = [
|
|
658
|
+
x
|
|
659
|
+
for x in self.context.sequence.outputs()
|
|
660
|
+
if x.rh.provider.expected
|
|
661
|
+
]
|
|
662
|
+
return self._promises
|
|
663
|
+
|
|
664
|
+
@property
|
|
665
|
+
def expected_resources(self):
|
|
666
|
+
"""Return the list of really expected inputs."""
|
|
667
|
+
if self._expected is None:
|
|
668
|
+
self._expected = [
|
|
669
|
+
x
|
|
670
|
+
for x in self.context.sequence.effective_inputs()
|
|
671
|
+
if x.rh.is_expected()
|
|
672
|
+
]
|
|
673
|
+
return self._expected
|
|
674
|
+
|
|
675
|
+
def delayed_exception_add(self, exc, traceback=True):
|
|
676
|
+
"""Store the exception so that it will be handled at the end of the run."""
|
|
677
|
+
logger.error("An exception is delayed")
|
|
678
|
+
if traceback:
|
|
679
|
+
(exc_type, exc_value, exc_traceback) = sys.exc_info()
|
|
680
|
+
print("Exception type: {!s}".format(exc_type))
|
|
681
|
+
print("Exception info: {!s}".format(exc_value))
|
|
682
|
+
print("Traceback:")
|
|
683
|
+
print("\n".join(py_traceback.format_tb(exc_traceback)))
|
|
684
|
+
self._delayed_excs.append(exc)
|
|
685
|
+
|
|
686
|
+
def algoassert(self, assertion, msg=""):
|
|
687
|
+
if not assertion:
|
|
688
|
+
raise AlgoComponentAssertionError(msg)
|
|
689
|
+
|
|
690
|
+
def grab(self, sec, comment="resource", sleep=10, timeout=None):
|
|
691
|
+
"""Wait for a given resource and get it if expected."""
|
|
692
|
+
local = sec.rh.container.localpath()
|
|
693
|
+
self.system.header("Wait for " + comment + " ... [" + local + "]")
|
|
694
|
+
if timeout is None:
|
|
695
|
+
timeout = self.timeout
|
|
696
|
+
if sec.rh.wait(timeout=timeout, sleep=sleep):
|
|
697
|
+
if sec.rh.is_expected():
|
|
698
|
+
sec.get(incache=True)
|
|
699
|
+
elif sec.fatal:
|
|
700
|
+
logger.critical("Missing expected resource <%s>", local)
|
|
701
|
+
raise ValueError("Could not get " + local)
|
|
702
|
+
else:
|
|
703
|
+
logger.error("Missing expected resource <%s>", local)
|
|
704
|
+
|
|
705
|
+
def export(self, packenv):
|
|
706
|
+
"""Export environment variables in given pack."""
|
|
707
|
+
for k, v in config.from_config(section=packenv).items():
|
|
708
|
+
if k not in self.env:
|
|
709
|
+
logger.info("Setting %s env %s = %s", packenv.upper(), k, v)
|
|
710
|
+
self.env[k] = v
|
|
711
|
+
|
|
712
|
+
def prepare(self, rh, opts):
|
|
713
|
+
"""Set some defaults env values."""
|
|
714
|
+
if config.is_defined(section="env"):
|
|
715
|
+
self.export("env")
|
|
716
|
+
|
|
717
|
+
def absexcutable(self, xfile):
|
|
718
|
+
"""Retuns the absolute pathname of the ``xfile`` executable."""
|
|
719
|
+
absx = self.system.path.abspath(xfile)
|
|
720
|
+
return absx
|
|
721
|
+
|
|
722
|
+
def flyput_method(self):
|
|
723
|
+
"""Check out what could be a valid io_poll command."""
|
|
724
|
+
return getattr(
|
|
725
|
+
self, "io_poll_method", getattr(self.system, self.flypoll, None)
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
def flyput_args(self):
|
|
729
|
+
"""Return actual io_poll prefixes."""
|
|
730
|
+
return getattr(self, "io_poll_args", tuple(self.flyargs))
|
|
731
|
+
|
|
732
|
+
def flyput_kwargs(self):
|
|
733
|
+
"""Return actual io_poll prefixes."""
|
|
734
|
+
return getattr(self, "io_poll_kwargs", dict())
|
|
735
|
+
|
|
736
|
+
def flyput_check(self):
|
|
737
|
+
"""Check default args for io_poll command."""
|
|
738
|
+
actual_args = list()
|
|
739
|
+
if self.flymapping:
|
|
740
|
+
# No checks when mapping is activated
|
|
741
|
+
return self.flyput_args()
|
|
742
|
+
else:
|
|
743
|
+
for arg in self.flyput_args():
|
|
744
|
+
logger.info("Check arg <%s>", arg)
|
|
745
|
+
if any(
|
|
746
|
+
[
|
|
747
|
+
x.rh.container.basename.startswith(arg)
|
|
748
|
+
for x in self.promises
|
|
749
|
+
]
|
|
750
|
+
):
|
|
751
|
+
logger.info(
|
|
752
|
+
"Match some promise %s",
|
|
753
|
+
str(
|
|
754
|
+
[
|
|
755
|
+
x.rh.container.basename
|
|
756
|
+
for x in self.promises
|
|
757
|
+
if x.rh.container.basename.startswith(arg)
|
|
758
|
+
]
|
|
759
|
+
),
|
|
760
|
+
)
|
|
761
|
+
actual_args.append(arg)
|
|
762
|
+
else:
|
|
763
|
+
logger.info(
|
|
764
|
+
"Do not match any promise %s",
|
|
765
|
+
str([x.rh.container.basename for x in self.promises]),
|
|
766
|
+
)
|
|
767
|
+
return actual_args
|
|
768
|
+
|
|
769
|
+
def flyput_sleep(self):
|
|
770
|
+
"""Return a sleeping time in seconds between io_poll commands."""
|
|
771
|
+
return getattr(
|
|
772
|
+
self, "io_poll_sleep", self.env.get("IO_POLL_SLEEP", 20)
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
def flyput_outputmapping(self, item):
|
|
776
|
+
"""Map output to another filename."""
|
|
777
|
+
return item, "unknown"
|
|
778
|
+
|
|
779
|
+
def _flyput_job_internal_search(
|
|
780
|
+
self, io_poll_method, io_poll_args, io_poll_kwargs
|
|
781
|
+
):
|
|
782
|
+
data = list()
|
|
783
|
+
for arg in io_poll_args:
|
|
784
|
+
logger.info("Polling check arg %s", arg)
|
|
785
|
+
rc = io_poll_method(arg, **io_poll_kwargs)
|
|
786
|
+
try:
|
|
787
|
+
data.extend(rc.result)
|
|
788
|
+
except AttributeError:
|
|
789
|
+
data.extend(rc)
|
|
790
|
+
data = [x for x in data if x]
|
|
791
|
+
logger.info("Polling retrieved data %s", str(data))
|
|
792
|
+
return data
|
|
793
|
+
|
|
794
|
+
def _flyput_job_internal_put(self, data):
|
|
795
|
+
for thisdata in data:
|
|
796
|
+
if self.flymapping:
|
|
797
|
+
mappeddata, mappedfmt = self.flyput_outputmapping(thisdata)
|
|
798
|
+
if not mappeddata:
|
|
799
|
+
raise AlgoComponentError(
|
|
800
|
+
"The mapping method failed for {:s}.".format(thisdata)
|
|
801
|
+
)
|
|
802
|
+
if thisdata != mappeddata:
|
|
803
|
+
logger.info(
|
|
804
|
+
"Linking <%s> to <%s> (fmt=%s) before put",
|
|
805
|
+
thisdata,
|
|
806
|
+
mappeddata,
|
|
807
|
+
mappedfmt,
|
|
808
|
+
)
|
|
809
|
+
self.system.cp(
|
|
810
|
+
thisdata, mappeddata, intent="in", fmt=mappedfmt
|
|
811
|
+
)
|
|
812
|
+
else:
|
|
813
|
+
mappeddata = thisdata
|
|
814
|
+
candidates = [
|
|
815
|
+
x
|
|
816
|
+
for x in self.promises
|
|
817
|
+
if x.rh.container.abspath
|
|
818
|
+
== self.system.path.abspath(mappeddata)
|
|
819
|
+
]
|
|
820
|
+
if candidates:
|
|
821
|
+
logger.info("Polled data is promised <%s>", mappeddata)
|
|
822
|
+
bingo = candidates.pop()
|
|
823
|
+
bingo.put(incache=True)
|
|
824
|
+
else:
|
|
825
|
+
logger.warning("Polled data not promised <%s>", mappeddata)
|
|
826
|
+
|
|
827
|
+
def flyput_job(
|
|
828
|
+
self,
|
|
829
|
+
io_poll_method,
|
|
830
|
+
io_poll_args,
|
|
831
|
+
io_poll_kwargs,
|
|
832
|
+
event_complete,
|
|
833
|
+
event_free,
|
|
834
|
+
queue_context,
|
|
835
|
+
):
|
|
836
|
+
"""Poll new data resources."""
|
|
837
|
+
logger.info("Polling with method %s", str(io_poll_method))
|
|
838
|
+
logger.info("Polling with args %s", str(io_poll_args))
|
|
839
|
+
|
|
840
|
+
time_sleep = self.flyput_sleep()
|
|
841
|
+
redo = True
|
|
842
|
+
|
|
843
|
+
# Start recording the changes in the current context
|
|
844
|
+
ctxrec = self.context.get_recorder()
|
|
845
|
+
|
|
846
|
+
while redo and not event_complete.is_set():
|
|
847
|
+
event_free.clear()
|
|
848
|
+
try:
|
|
849
|
+
data = self._flyput_job_internal_search(
|
|
850
|
+
io_poll_method, io_poll_args, io_poll_kwargs
|
|
851
|
+
)
|
|
852
|
+
self._flyput_job_internal_put(data)
|
|
853
|
+
except Exception as trouble:
|
|
854
|
+
logger.error(
|
|
855
|
+
"Polling trouble: %s. %s",
|
|
856
|
+
str(trouble),
|
|
857
|
+
py_traceback.format_exc(),
|
|
858
|
+
)
|
|
859
|
+
redo = False
|
|
860
|
+
finally:
|
|
861
|
+
event_free.set()
|
|
862
|
+
if redo and not data and not event_complete.is_set():
|
|
863
|
+
logger.info("Get asleep for %d seconds...", time_sleep)
|
|
864
|
+
self.system.sleep(time_sleep)
|
|
865
|
+
|
|
866
|
+
# Stop recording and send back the results
|
|
867
|
+
ctxrec.unregister()
|
|
868
|
+
logger.info("Sending the Context recorder to the master process.")
|
|
869
|
+
queue_context.put(ctxrec)
|
|
870
|
+
queue_context.close()
|
|
871
|
+
|
|
872
|
+
if redo:
|
|
873
|
+
logger.info("Polling exit on complete event")
|
|
874
|
+
else:
|
|
875
|
+
logger.warning("Polling exit on abort")
|
|
876
|
+
|
|
877
|
+
def flyput_begin(self):
|
|
878
|
+
"""Launch a co-process to handle promises."""
|
|
879
|
+
|
|
880
|
+
nope = (None, None, None, None)
|
|
881
|
+
if not self.flyput:
|
|
882
|
+
return nope
|
|
883
|
+
|
|
884
|
+
sh = self.system
|
|
885
|
+
sh.subtitle("On the fly - Begin")
|
|
886
|
+
|
|
887
|
+
if not self.promises:
|
|
888
|
+
logger.info("No promise, no co-process")
|
|
889
|
+
return nope
|
|
890
|
+
|
|
891
|
+
# Find out a polling method
|
|
892
|
+
io_poll_method = self.flyput_method()
|
|
893
|
+
if not io_poll_method:
|
|
894
|
+
logger.error(
|
|
895
|
+
"No method or shell function defined for polling data"
|
|
896
|
+
)
|
|
897
|
+
return nope
|
|
898
|
+
|
|
899
|
+
# Be sure that some default args could match local promises names
|
|
900
|
+
io_poll_args = self.flyput_check()
|
|
901
|
+
if not io_poll_args:
|
|
902
|
+
logger.error("Could not check default arguments for polling data")
|
|
903
|
+
return nope
|
|
904
|
+
|
|
905
|
+
# Additional named attributes
|
|
906
|
+
io_poll_kwargs = self.flyput_kwargs()
|
|
907
|
+
|
|
908
|
+
# Define events for a nice termination
|
|
909
|
+
event_stop = multiprocessing.Event()
|
|
910
|
+
event_free = multiprocessing.Event()
|
|
911
|
+
queue_ctx = multiprocessing.Queue()
|
|
912
|
+
|
|
913
|
+
p_io = multiprocessing.Process(
|
|
914
|
+
name=self.footprint_clsname(),
|
|
915
|
+
target=self.flyput_job,
|
|
916
|
+
args=(
|
|
917
|
+
io_poll_method,
|
|
918
|
+
io_poll_args,
|
|
919
|
+
io_poll_kwargs,
|
|
920
|
+
event_stop,
|
|
921
|
+
event_free,
|
|
922
|
+
queue_ctx,
|
|
923
|
+
),
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
# The co-process is started
|
|
927
|
+
p_io.start()
|
|
928
|
+
|
|
929
|
+
return (p_io, event_stop, event_free, queue_ctx)
|
|
930
|
+
|
|
931
|
+
def manual_flypolling(self):
|
|
932
|
+
"""Call the flyput method and returns the list of newly available files."""
|
|
933
|
+
# Find out a polling method
|
|
934
|
+
io_poll_method = self.flyput_method()
|
|
935
|
+
if not io_poll_method:
|
|
936
|
+
raise AlgoComponentError("Unable to find an io_poll_method")
|
|
937
|
+
# Find out some polling prefixes
|
|
938
|
+
io_poll_args = self.flyput_check()
|
|
939
|
+
if not io_poll_args:
|
|
940
|
+
raise AlgoComponentError("Unable to find an io_poll_args")
|
|
941
|
+
# Additional named attributes
|
|
942
|
+
io_poll_kwargs = self.flyput_kwargs()
|
|
943
|
+
# Starting polling each of the prefixes
|
|
944
|
+
return self._flyput_job_internal_search(
|
|
945
|
+
io_poll_method, io_poll_args, io_poll_kwargs
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
def manual_flypolling_job(self):
|
|
949
|
+
"""Call the flyput method and deal with promised files."""
|
|
950
|
+
data = self.manual_flypolling()
|
|
951
|
+
self._flyput_job_internal_put(data)
|
|
952
|
+
|
|
953
|
+
def flyput_end(self, p_io, e_complete, e_free, queue_ctx):
|
|
954
|
+
"""Wait for the co-process in charge of promises."""
|
|
955
|
+
e_complete.set()
|
|
956
|
+
logger.info("Waiting for polling process... <%s>", p_io.pid)
|
|
957
|
+
t0 = date.now()
|
|
958
|
+
e_free.wait(60)
|
|
959
|
+
# Get the Queue and update the context
|
|
960
|
+
time_sleep = self.flyput_sleep()
|
|
961
|
+
try:
|
|
962
|
+
# allow 5 sec to put data into queue (it should be more than enough)
|
|
963
|
+
ctxrec = queue_ctx.get(block=True, timeout=time_sleep + 5)
|
|
964
|
+
except queue.Empty:
|
|
965
|
+
logger.warning("Impossible to get the Context recorder")
|
|
966
|
+
ctxrec = None
|
|
967
|
+
finally:
|
|
968
|
+
queue_ctx.close()
|
|
969
|
+
if ctxrec is not None:
|
|
970
|
+
ctxrec.replay_in(self.context)
|
|
971
|
+
p_io.join(30)
|
|
972
|
+
t1 = date.now()
|
|
973
|
+
waiting = t1 - t0
|
|
974
|
+
logger.info(
|
|
975
|
+
"Waiting for polling process took %f seconds",
|
|
976
|
+
waiting.total_seconds(),
|
|
977
|
+
)
|
|
978
|
+
if p_io.is_alive():
|
|
979
|
+
logger.warning("Force termination of polling process")
|
|
980
|
+
p_io.terminate()
|
|
981
|
+
logger.info("Polling still alive ? %s", str(p_io.is_alive()))
|
|
982
|
+
return not p_io.is_alive()
|
|
983
|
+
|
|
984
|
+
def server_begin(self, rh, opts):
|
|
985
|
+
"""Start a subprocess and run the server in it."""
|
|
986
|
+
self._server_event = multiprocessing.Event()
|
|
987
|
+
self._server_process = multiprocessing.Process(
|
|
988
|
+
name=self.footprint_clsname(),
|
|
989
|
+
target=self.server_job,
|
|
990
|
+
args=(rh, opts),
|
|
991
|
+
)
|
|
992
|
+
self._server_process.start()
|
|
993
|
+
|
|
994
|
+
def server_job(self, rh, opts):
|
|
995
|
+
"""Actually run the server and catch all Exceptions.
|
|
996
|
+
|
|
997
|
+
If the server crashes, is killed or whatever, the Exception is displayed
|
|
998
|
+
and the appropriate Event is set.
|
|
999
|
+
"""
|
|
1000
|
+
self.system.signal_intercept_on()
|
|
1001
|
+
try:
|
|
1002
|
+
self.execute_single(rh, opts)
|
|
1003
|
+
except Exception:
|
|
1004
|
+
(exc_type, exc_value, exc_traceback) = sys.exc_info()
|
|
1005
|
+
print("Exception type: {!s}".format(exc_type))
|
|
1006
|
+
print("Exception info: {!s}".format(exc_value))
|
|
1007
|
+
print("Traceback:")
|
|
1008
|
+
print("\n".join(py_traceback.format_tb(exc_traceback)))
|
|
1009
|
+
# Alert the main process of the error
|
|
1010
|
+
self._server_event.set()
|
|
1011
|
+
|
|
1012
|
+
def server_alive(self):
|
|
1013
|
+
"""Is the server still running ?"""
|
|
1014
|
+
return (
|
|
1015
|
+
self._server_process is not None
|
|
1016
|
+
and self._server_process.is_alive()
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
def server_end(self):
|
|
1020
|
+
"""End the server.
|
|
1021
|
+
|
|
1022
|
+
A first attempt is made to terminate it nicely. If it doesn't work,
|
|
1023
|
+
a SIGTERM is sent.
|
|
1024
|
+
"""
|
|
1025
|
+
rc = False
|
|
1026
|
+
# This test should always succeed...
|
|
1027
|
+
if (
|
|
1028
|
+
self._server_synctool is not None
|
|
1029
|
+
and self._server_process is not None
|
|
1030
|
+
):
|
|
1031
|
+
# Is the process still running ?
|
|
1032
|
+
if self._server_process.is_alive():
|
|
1033
|
+
# Try to stop it nicely
|
|
1034
|
+
if (
|
|
1035
|
+
self._SERVERSYNC_STOPONEXIT
|
|
1036
|
+
and self._server_synctool.trigger_stop()
|
|
1037
|
+
):
|
|
1038
|
+
t0 = date.now()
|
|
1039
|
+
self._server_process.join(30)
|
|
1040
|
+
waiting = date.now() - t0
|
|
1041
|
+
logger.info(
|
|
1042
|
+
"Waiting for the server to stop took %f seconds",
|
|
1043
|
+
waiting.total_seconds(),
|
|
1044
|
+
)
|
|
1045
|
+
rc = not self._server_event.is_set()
|
|
1046
|
+
# Be less nice if needed...
|
|
1047
|
+
if (
|
|
1048
|
+
not self._SERVERSYNC_STOPONEXIT
|
|
1049
|
+
) or self._server_process.is_alive():
|
|
1050
|
+
logger.warning("Force termination of the server process")
|
|
1051
|
+
self._server_process.terminate()
|
|
1052
|
+
self.system.sleep(
|
|
1053
|
+
1
|
|
1054
|
+
) # Allow some time for the process to terminate
|
|
1055
|
+
if not self._SERVERSYNC_STOPONEXIT:
|
|
1056
|
+
rc = False
|
|
1057
|
+
else:
|
|
1058
|
+
rc = not self._server_event.is_set()
|
|
1059
|
+
logger.info(
|
|
1060
|
+
"Server still alive ? %s", str(self._server_process.is_alive())
|
|
1061
|
+
)
|
|
1062
|
+
# We are done with the server
|
|
1063
|
+
self._server_synctool = None
|
|
1064
|
+
self._server_process = None
|
|
1065
|
+
del self._server_event
|
|
1066
|
+
# Check the rc
|
|
1067
|
+
if not rc:
|
|
1068
|
+
raise AlgoComponentError("The server process ended badly.")
|
|
1069
|
+
return rc
|
|
1070
|
+
|
|
1071
|
+
def spawn_pre_dirlisting(self):
|
|
1072
|
+
"""Print a directory listing just before run."""
|
|
1073
|
+
self.system.subtitle(
|
|
1074
|
+
"{:s} : directory listing (pre-execution)".format(self.realkind)
|
|
1075
|
+
)
|
|
1076
|
+
self.system.dir(output=False, fatal=False)
|
|
1077
|
+
|
|
1078
|
+
def spawn_hook(self):
|
|
1079
|
+
"""Last chance to say something before execution."""
|
|
1080
|
+
pass
|
|
1081
|
+
|
|
1082
|
+
def spawn(self, args, opts, stdin=None):
|
|
1083
|
+
"""
|
|
1084
|
+
Spawn in the current system the command as defined in raw ``args``.
|
|
1085
|
+
|
|
1086
|
+
The followings environment variables could drive part of the execution:
|
|
1087
|
+
|
|
1088
|
+
* VORTEX_DEBUG_ENV : dump current environment before spawn
|
|
1089
|
+
"""
|
|
1090
|
+
sh = self.system
|
|
1091
|
+
|
|
1092
|
+
if self.env.true("vortex_debug_env"):
|
|
1093
|
+
sh.subtitle(
|
|
1094
|
+
"{:s} : dump environment (os bound: {!s})".format(
|
|
1095
|
+
self.realkind, self.env.osbound()
|
|
1096
|
+
)
|
|
1097
|
+
)
|
|
1098
|
+
self.env.osdump()
|
|
1099
|
+
|
|
1100
|
+
# On-the-fly coprocessing initialisation
|
|
1101
|
+
p_io, e_complete, e_free, q_ctx = self.flyput_begin()
|
|
1102
|
+
|
|
1103
|
+
sh.remove("core")
|
|
1104
|
+
sh.softlink("/dev/null", "core")
|
|
1105
|
+
self.spawn_hook()
|
|
1106
|
+
self.target.spawn_hook(sh)
|
|
1107
|
+
self.spawn_pre_dirlisting()
|
|
1108
|
+
sh.subtitle("{:s} : start execution".format(self.realkind))
|
|
1109
|
+
try:
|
|
1110
|
+
sh.spawn(
|
|
1111
|
+
args, output=False, stdin=stdin, fatal=opts.get("fatal", True)
|
|
1112
|
+
)
|
|
1113
|
+
finally:
|
|
1114
|
+
# On-the-fly coprocessing cleaning
|
|
1115
|
+
if p_io:
|
|
1116
|
+
self.flyput_end(p_io, e_complete, e_free, q_ctx)
|
|
1117
|
+
|
|
1118
|
+
def spawn_command_options(self):
|
|
1119
|
+
"""Prepare options for the resource's command line."""
|
|
1120
|
+
return dict()
|
|
1121
|
+
|
|
1122
|
+
def spawn_command_line(self, rh):
|
|
1123
|
+
"""Split the shell command line of the resource to be run."""
|
|
1124
|
+
opts = self.spawn_command_options()
|
|
1125
|
+
return shlex.split(rh.resource.command_line(**opts))
|
|
1126
|
+
|
|
1127
|
+
def spawn_stdin_options(self):
|
|
1128
|
+
"""Prepare options for the resource's stdin generator."""
|
|
1129
|
+
return dict()
|
|
1130
|
+
|
|
1131
|
+
def spawn_stdin(self, rh):
|
|
1132
|
+
"""Generate the stdin File-Like object of the resource to be run."""
|
|
1133
|
+
opts = self.spawn_stdin_options()
|
|
1134
|
+
stdin_text = rh.resource.stdin_text(**opts)
|
|
1135
|
+
if stdin_text is not None:
|
|
1136
|
+
plocale = locale.getlocale()[1] or "ascii"
|
|
1137
|
+
tmpfh = tempfile.TemporaryFile(dir=self.system.pwd(), mode="w+b")
|
|
1138
|
+
if isinstance(stdin_text, str):
|
|
1139
|
+
tmpfh.write(stdin_text.encode(plocale))
|
|
1140
|
+
else:
|
|
1141
|
+
tmpfh.write(stdin_text)
|
|
1142
|
+
tmpfh.seek(0)
|
|
1143
|
+
return tmpfh
|
|
1144
|
+
else:
|
|
1145
|
+
return None
|
|
1146
|
+
|
|
1147
|
+
def execute_single(self, rh, opts):
|
|
1148
|
+
"""Abstract method.
|
|
1149
|
+
|
|
1150
|
+
When server_run is True, this method is used to start the server.
|
|
1151
|
+
Otherwise, this method is called by each :meth:`execute` call.
|
|
1152
|
+
"""
|
|
1153
|
+
pass
|
|
1154
|
+
|
|
1155
|
+
def execute(self, rh, opts):
|
|
1156
|
+
"""Abstract method."""
|
|
1157
|
+
if self.server_run:
|
|
1158
|
+
# First time here ?
|
|
1159
|
+
if self._server_synctool is None:
|
|
1160
|
+
if self.serversync_method is None:
|
|
1161
|
+
raise ValueError("The serversync_method must be provided.")
|
|
1162
|
+
self._server_synctool = footprints.proxy.serversynctool(
|
|
1163
|
+
method=self.serversync_method,
|
|
1164
|
+
medium=self.serversync_medium,
|
|
1165
|
+
raiseonexit=self._SERVERSYNC_RAISEONEXIT,
|
|
1166
|
+
)
|
|
1167
|
+
self._server_synctool.set_servercheck_callback(
|
|
1168
|
+
self.server_alive
|
|
1169
|
+
)
|
|
1170
|
+
self.server_begin(rh, opts)
|
|
1171
|
+
# Wait for the first request
|
|
1172
|
+
self._server_synctool.trigger_wait()
|
|
1173
|
+
if self._SERVERSYNC_RUNONSTARTUP:
|
|
1174
|
+
self._server_synctool.trigger_run()
|
|
1175
|
+
else:
|
|
1176
|
+
# Acknowledge that we are ready and wait for the next request
|
|
1177
|
+
self._server_synctool.trigger_run()
|
|
1178
|
+
else:
|
|
1179
|
+
self.execute_single(rh, opts)
|
|
1180
|
+
|
|
1181
|
+
def fail_execute(self, e, rh, kw):
|
|
1182
|
+
"""This method is called if :meth:`execute` raise an exception."""
|
|
1183
|
+
pass
|
|
1184
|
+
|
|
1185
|
+
def execute_finalise(self, opts):
|
|
1186
|
+
"""Abstract method.
|
|
1187
|
+
|
|
1188
|
+
This method is called inconditionaly when :meth:`execute` exits (even
|
|
1189
|
+
if an Exception was raised).
|
|
1190
|
+
"""
|
|
1191
|
+
if self.server_run:
|
|
1192
|
+
self.server_end()
|
|
1193
|
+
|
|
1194
|
+
def postfix_post_dirlisting(self):
|
|
1195
|
+
self.system.subtitle(
|
|
1196
|
+
"{:s} : directory listing (post-run)".format(self.realkind)
|
|
1197
|
+
)
|
|
1198
|
+
self.system.dir(output=False, fatal=False)
|
|
1199
|
+
|
|
1200
|
+
def postfix(self, rh, opts):
|
|
1201
|
+
"""Some basic informations."""
|
|
1202
|
+
self.postfix_post_dirlisting()
|
|
1203
|
+
|
|
1204
|
+
def dumplog(self, opts):
|
|
1205
|
+
"""Dump to local file the internal log of the current algo component."""
|
|
1206
|
+
self.system.pickle_dump(self.fslog, "log." + self.fstag())
|
|
1207
|
+
|
|
1208
|
+
def delayed_exceptions(self, opts):
|
|
1209
|
+
"""Gather all the delayed exceptions and raises one if necessary."""
|
|
1210
|
+
if len(self._delayed_excs) > 0:
|
|
1211
|
+
excstmp = self._delayed_excs
|
|
1212
|
+
self._delayed_excs = list()
|
|
1213
|
+
raise DelayedAlgoComponentError(excstmp)
|
|
1214
|
+
|
|
1215
|
+
def valid_executable(self, rh):
|
|
1216
|
+
"""
|
|
1217
|
+
Return a boolean value according to the effective executable nature
|
|
1218
|
+
of the resource handler provided.
|
|
1219
|
+
"""
|
|
1220
|
+
return True
|
|
1221
|
+
|
|
1222
|
+
def abortfabrik(self, step, msg):
|
|
1223
|
+
"""A shortcut to avoid next steps of the run."""
|
|
1224
|
+
|
|
1225
|
+
def fastexit(self, *args, **kw):
|
|
1226
|
+
logger.warning(
|
|
1227
|
+
"Run <%s> skipped because abort occurred [%s]", step, msg
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
return fastexit
|
|
1231
|
+
|
|
1232
|
+
def abort(self, msg="Not documented"):
|
|
1233
|
+
"""A shortcut to avoid next steps of the run."""
|
|
1234
|
+
for step in ("prepare", "execute", "postfix"):
|
|
1235
|
+
setattr(self, step, self.abortfabrik(step, msg))
|
|
1236
|
+
|
|
1237
|
+
def run(self, rh=None, **kw):
|
|
1238
|
+
"""Sequence for execution : prepare / execute / postfix."""
|
|
1239
|
+
self._status = True
|
|
1240
|
+
|
|
1241
|
+
# Get instance shorcuts to context and system objects
|
|
1242
|
+
self.ticket = vortex.sessions.current()
|
|
1243
|
+
self.context = self.ticket.context
|
|
1244
|
+
self.system = self.context.system
|
|
1245
|
+
self.target = kw.pop("target", None)
|
|
1246
|
+
if self.target is None:
|
|
1247
|
+
self.target = self.system.default_target
|
|
1248
|
+
|
|
1249
|
+
# Before trying to do anything, check the executable
|
|
1250
|
+
if not self.valid_executable(rh):
|
|
1251
|
+
logger.warning(
|
|
1252
|
+
"Resource %s is not a valid executable", rh.resource
|
|
1253
|
+
)
|
|
1254
|
+
return False
|
|
1255
|
+
|
|
1256
|
+
# A cloned environment will be bound to the OS
|
|
1257
|
+
self.env = self.context.env.clone()
|
|
1258
|
+
with self.env:
|
|
1259
|
+
# The actual "run" recipe
|
|
1260
|
+
self.prepare(rh, kw) # 1
|
|
1261
|
+
self.fsstamp(kw) # 2
|
|
1262
|
+
try:
|
|
1263
|
+
self.execute(rh, kw) # 3
|
|
1264
|
+
except Exception as e:
|
|
1265
|
+
self.fail_execute(e, rh, kw) # 3.1
|
|
1266
|
+
raise
|
|
1267
|
+
finally:
|
|
1268
|
+
self.execute_finalise(kw) # 3.2
|
|
1269
|
+
self.fscheck(kw) # 4
|
|
1270
|
+
self.postfix(rh, kw) # 5
|
|
1271
|
+
self.dumplog(kw) # 6
|
|
1272
|
+
self.delayed_exceptions(kw) # 7
|
|
1273
|
+
|
|
1274
|
+
# Free local references
|
|
1275
|
+
self.env = None
|
|
1276
|
+
self.system = None
|
|
1277
|
+
|
|
1278
|
+
return self._status
|
|
1279
|
+
|
|
1280
|
+
def quickview(self, nb=0, indent=0):
|
|
1281
|
+
"""Standard glance to objects."""
|
|
1282
|
+
tab = " " * indent
|
|
1283
|
+
print("{}{:02d}. {:s}".format(tab, nb, repr(self)))
|
|
1284
|
+
for subobj in ("kind", "engine", "interpreter"):
|
|
1285
|
+
obj = getattr(self, subobj, None)
|
|
1286
|
+
if obj:
|
|
1287
|
+
print("{} {:s}: {!s}".format(tab, subobj, obj))
|
|
1288
|
+
print()
|
|
1289
|
+
|
|
1290
|
+
def setlink(
|
|
1291
|
+
self,
|
|
1292
|
+
initrole=None,
|
|
1293
|
+
initkind=None,
|
|
1294
|
+
initname=None,
|
|
1295
|
+
inittest=lambda x: True,
|
|
1296
|
+
):
|
|
1297
|
+
"""Set a symbolic link for actual resource playing defined role."""
|
|
1298
|
+
initsec = [
|
|
1299
|
+
x
|
|
1300
|
+
for x in self.context.sequence.effective_inputs(
|
|
1301
|
+
role=initrole, kind=initkind
|
|
1302
|
+
)
|
|
1303
|
+
if inittest(x.rh)
|
|
1304
|
+
]
|
|
1305
|
+
|
|
1306
|
+
if not initsec:
|
|
1307
|
+
logger.warning(
|
|
1308
|
+
"Could not find logical role %s with kind %s - assuming already renamed",
|
|
1309
|
+
initrole,
|
|
1310
|
+
initkind,
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
if len(initsec) > 1:
|
|
1314
|
+
logger.warning(
|
|
1315
|
+
"More than one role %s with kind %s", initrole, initkind
|
|
1316
|
+
)
|
|
1317
|
+
|
|
1318
|
+
if initname is not None:
|
|
1319
|
+
for l in [x.rh.container.localpath() for x in initsec]:
|
|
1320
|
+
if not self.system.path.exists(initname):
|
|
1321
|
+
self.system.symlink(l, initname)
|
|
1322
|
+
break
|
|
1323
|
+
|
|
1324
|
+
return initsec
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
class PythonFunction(AlgoComponent):
|
|
1328
|
+
"""Execute a function defined in Python module. The function is passed the
|
|
1329
|
+
current :class:`sequence <vortex.layout.dataflow.Sequence>`, as well as a
|
|
1330
|
+
keyword arguments described by attribute ``func_kwargs``. Example:
|
|
1331
|
+
|
|
1332
|
+
.. code-block:: python
|
|
1333
|
+
|
|
1334
|
+
>>> exe = toolbox.executable(
|
|
1335
|
+
... role = 'Script',
|
|
1336
|
+
... format = 'ascii',
|
|
1337
|
+
... hostname = 'localhost',
|
|
1338
|
+
... kind = 'script',
|
|
1339
|
+
... language = 'python',
|
|
1340
|
+
... local = 'module.py',
|
|
1341
|
+
... remote = '/path/to/module.py',
|
|
1342
|
+
... tube = 'file',
|
|
1343
|
+
... )
|
|
1344
|
+
>>> tbalgo = toolbox.algo(
|
|
1345
|
+
... engine="function",
|
|
1346
|
+
... func_name="my_plugin_entry_point_function",
|
|
1347
|
+
... func_kwargs={ntasks: 35, subnproc: 4},
|
|
1348
|
+
... )
|
|
1349
|
+
>>> tbalgo.run(exe[0])
|
|
1350
|
+
|
|
1351
|
+
.. code-block:: python
|
|
1352
|
+
|
|
1353
|
+
# /path/to/module.py
|
|
1354
|
+
# ...
|
|
1355
|
+
def my_plugin_entry_point_function(
|
|
1356
|
+
sequence, ntasks, subnproc,
|
|
1357
|
+
):
|
|
1358
|
+
for input in sequence.effective_inputs(role=gridpoint):
|
|
1359
|
+
# ...
|
|
1360
|
+
"""
|
|
1361
|
+
|
|
1362
|
+
_footprint = dict(
|
|
1363
|
+
info="Execute a Python function in a given module",
|
|
1364
|
+
attr=dict(
|
|
1365
|
+
engine=dict(values=["function"]),
|
|
1366
|
+
func_name=dict(info="The function's name"),
|
|
1367
|
+
func_kwargs=dict(
|
|
1368
|
+
info=(
|
|
1369
|
+
"A dictionary containing the function's keyword arguments"
|
|
1370
|
+
),
|
|
1371
|
+
type=footprints.FPDict,
|
|
1372
|
+
default=footprints.FPDict({}),
|
|
1373
|
+
optional=True,
|
|
1374
|
+
),
|
|
1375
|
+
),
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
def prepare(self, rh, opts):
|
|
1379
|
+
spec = importlib.util.spec_from_file_location(
|
|
1380
|
+
name="module", location=rh.container.localpath()
|
|
1381
|
+
)
|
|
1382
|
+
mod = importlib.util.module_from_spec(spec)
|
|
1383
|
+
sys.path.extend(self.extendpypath)
|
|
1384
|
+
try:
|
|
1385
|
+
spec.loader.exec_module(mod)
|
|
1386
|
+
except AttributeError:
|
|
1387
|
+
raise AttributeError
|
|
1388
|
+
self.func = getattr(mod, self.func_name)
|
|
1389
|
+
|
|
1390
|
+
def execute(self, rh, opts):
|
|
1391
|
+
self.func(
|
|
1392
|
+
self.context.sequence,
|
|
1393
|
+
**self.func_kwargs,
|
|
1394
|
+
)
|
|
1395
|
+
|
|
1396
|
+
def execute_finalise(self, opts):
|
|
1397
|
+
for p in self.extendpypath:
|
|
1398
|
+
sys.path.remove(p)
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
class ExecutableAlgoComponent(AlgoComponent):
|
|
1402
|
+
"""Component in charge of running executable resources."""
|
|
1403
|
+
|
|
1404
|
+
_abstract = True
|
|
1405
|
+
|
|
1406
|
+
def valid_executable(self, rh):
|
|
1407
|
+
"""
|
|
1408
|
+
Return a boolean value according to the effective executable nature
|
|
1409
|
+
of the resource handler provided.
|
|
1410
|
+
"""
|
|
1411
|
+
return rh is not None
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
class xExecutableAlgoComponent(ExecutableAlgoComponent):
|
|
1415
|
+
"""Component in charge of running executable resources."""
|
|
1416
|
+
|
|
1417
|
+
_abstract = True
|
|
1418
|
+
|
|
1419
|
+
def valid_executable(self, rh):
|
|
1420
|
+
"""
|
|
1421
|
+
Return a boolean value according to the effective executable nature
|
|
1422
|
+
of the resource handler provided.
|
|
1423
|
+
"""
|
|
1424
|
+
rc = super().valid_executable(rh)
|
|
1425
|
+
if rc:
|
|
1426
|
+
# Ensure that the input file is executable
|
|
1427
|
+
xrh = (
|
|
1428
|
+
rh
|
|
1429
|
+
if isinstance(rh, (list, tuple))
|
|
1430
|
+
else [
|
|
1431
|
+
rh,
|
|
1432
|
+
]
|
|
1433
|
+
)
|
|
1434
|
+
for arh in xrh:
|
|
1435
|
+
self.system.xperm(arh.container.localpath(), force=True)
|
|
1436
|
+
return rc
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
class TaylorRun(AlgoComponent):
|
|
1440
|
+
"""
|
|
1441
|
+
Run any taylorism Worker in the current environment.
|
|
1442
|
+
|
|
1443
|
+
This abstract class includes helpers to use the taylorism package in order
|
|
1444
|
+
to introduce an external parallelisation. It is designed to work well with a
|
|
1445
|
+
taylorism Worker class that inherits from
|
|
1446
|
+
:class:`vortex.tools.parallelism.TaylorVortexWorker`.
|
|
1447
|
+
"""
|
|
1448
|
+
|
|
1449
|
+
_abstract = True
|
|
1450
|
+
_footprint = dict(
|
|
1451
|
+
info="Abstract algo component based on the taylorism package.",
|
|
1452
|
+
attr=dict(
|
|
1453
|
+
kind=dict(),
|
|
1454
|
+
verbose=dict(
|
|
1455
|
+
info="Run in verbose mode",
|
|
1456
|
+
type=bool,
|
|
1457
|
+
default=False,
|
|
1458
|
+
optional=True,
|
|
1459
|
+
doc_zorder=-50,
|
|
1460
|
+
),
|
|
1461
|
+
ntasks=dict(
|
|
1462
|
+
info="The maximum number of parallel tasks",
|
|
1463
|
+
type=int,
|
|
1464
|
+
default=DelayedEnvValue("VORTEX_SUBMIT_TASKS", 1),
|
|
1465
|
+
optional=True,
|
|
1466
|
+
),
|
|
1467
|
+
),
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
def __init__(self, *kargs, **kwargs):
|
|
1471
|
+
super().__init__(*kargs, **kwargs)
|
|
1472
|
+
self._boss = None
|
|
1473
|
+
|
|
1474
|
+
def _default_common_instructions(self, rh, opts):
|
|
1475
|
+
"""Create a common instruction dictionary that will be used by the workers."""
|
|
1476
|
+
return dict(kind=self.kind, taskdebug=self.verbose)
|
|
1477
|
+
|
|
1478
|
+
def _default_pre_execute(self, rh, opts):
|
|
1479
|
+
"""Various initialisations. In particular it creates the task scheduler (Boss)."""
|
|
1480
|
+
# Start the task scheduler
|
|
1481
|
+
self._boss = Boss(
|
|
1482
|
+
verbose=self.verbose,
|
|
1483
|
+
scheduler=footprints.proxy.scheduler(
|
|
1484
|
+
limit="threads", max_threads=self.ntasks
|
|
1485
|
+
),
|
|
1486
|
+
)
|
|
1487
|
+
self._boss.make_them_work()
|
|
1488
|
+
|
|
1489
|
+
def _add_instructions(self, common_i, individual_i):
|
|
1490
|
+
"""Give a new set of instructions to the Boss."""
|
|
1491
|
+
self._boss.set_instructions(common_i, individual_i)
|
|
1492
|
+
|
|
1493
|
+
def _default_post_execute(self, rh, opts):
|
|
1494
|
+
"""Summarise the results of the various tasks that were run."""
|
|
1495
|
+
logger.info(
|
|
1496
|
+
"All the input files were dealt with: now waiting for the parallel processing to finish"
|
|
1497
|
+
)
|
|
1498
|
+
self._boss.wait_till_finished()
|
|
1499
|
+
logger.info(
|
|
1500
|
+
"The parallel processing has finished. here are the results:"
|
|
1501
|
+
)
|
|
1502
|
+
report = self._boss.get_report()
|
|
1503
|
+
prp = ParallelResultParser(self.context)
|
|
1504
|
+
for r in report["workers_report"]:
|
|
1505
|
+
rc = prp(r)
|
|
1506
|
+
if isinstance(rc, Exception):
|
|
1507
|
+
self.delayed_exception_add(rc, traceback=False)
|
|
1508
|
+
rc = False
|
|
1509
|
+
self._default_rc_action(rh, opts, r, rc)
|
|
1510
|
+
|
|
1511
|
+
def _default_rc_action(self, rh, opts, report, rc):
|
|
1512
|
+
"""How should we process the return code ?"""
|
|
1513
|
+
if not rc:
|
|
1514
|
+
logger.warning(
|
|
1515
|
+
"Apparently something went sideways with this task (rc=%s).",
|
|
1516
|
+
str(rc),
|
|
1517
|
+
)
|
|
1518
|
+
|
|
1519
|
+
def execute(self, rh, opts):
|
|
1520
|
+
"""
|
|
1521
|
+
This should be adapted to your needs...
|
|
1522
|
+
|
|
1523
|
+
A usual sequence is::
|
|
1524
|
+
|
|
1525
|
+
self._default_pre_execute(rh, opts)
|
|
1526
|
+
common_i = self._default_common_instructions(rh, opts)
|
|
1527
|
+
# Update the common instructions
|
|
1528
|
+
common_i.update(dict(someattribute='Toto', ))
|
|
1529
|
+
|
|
1530
|
+
# Your own code here
|
|
1531
|
+
|
|
1532
|
+
# Give some instructions to the boss
|
|
1533
|
+
self._add_instructions(common_i, dict(someattribute=['Toto', ],))
|
|
1534
|
+
|
|
1535
|
+
# Your own code here
|
|
1536
|
+
|
|
1537
|
+
self._default_post_execute(rh, opts)
|
|
1538
|
+
|
|
1539
|
+
"""
|
|
1540
|
+
raise NotImplementedError
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
class Expresso(ExecutableAlgoComponent):
|
|
1544
|
+
"""Run a script resource in the good environment."""
|
|
1545
|
+
|
|
1546
|
+
_footprint = dict(
|
|
1547
|
+
info="AlgoComponent that simply runs a script",
|
|
1548
|
+
attr=dict(
|
|
1549
|
+
interpreter=dict(
|
|
1550
|
+
info="The interpreter needed to run the script.",
|
|
1551
|
+
values=["current", "awk", "ksh", "bash", "perl", "python"],
|
|
1552
|
+
),
|
|
1553
|
+
interpreter_path=dict(
|
|
1554
|
+
info="The interpreter command.",
|
|
1555
|
+
optional=True,
|
|
1556
|
+
),
|
|
1557
|
+
engine=dict(values=["exec", "launch"]),
|
|
1558
|
+
),
|
|
1559
|
+
)
|
|
1560
|
+
|
|
1561
|
+
@property
|
|
1562
|
+
def _actual_interpreter(self):
|
|
1563
|
+
"""Return the interpreter command."""
|
|
1564
|
+
if self.interpreter == "current":
|
|
1565
|
+
if self.interpreter_path is not None:
|
|
1566
|
+
raise ValueError(
|
|
1567
|
+
"*interpreter=current* and *interpreter_path* attributes are incompatible"
|
|
1568
|
+
)
|
|
1569
|
+
return sys.executable
|
|
1570
|
+
else:
|
|
1571
|
+
if self.interpreter_path is None:
|
|
1572
|
+
return self.interpreter
|
|
1573
|
+
else:
|
|
1574
|
+
if self.system.xperm(self.interpreter_path):
|
|
1575
|
+
return self.interpreter_path
|
|
1576
|
+
else:
|
|
1577
|
+
raise AlgoComponentError(
|
|
1578
|
+
"The '{:s}' interpreter is not executable".format(
|
|
1579
|
+
self.interpreter_path
|
|
1580
|
+
)
|
|
1581
|
+
)
|
|
1582
|
+
|
|
1583
|
+
def _interpreter_args_fix(self, rh, opts):
|
|
1584
|
+
absexec = self.absexcutable(rh.container.localpath())
|
|
1585
|
+
if self.interpreter == "awk":
|
|
1586
|
+
return ["-f", absexec]
|
|
1587
|
+
else:
|
|
1588
|
+
return [
|
|
1589
|
+
absexec,
|
|
1590
|
+
]
|
|
1591
|
+
|
|
1592
|
+
def execute_single(self, rh, opts):
|
|
1593
|
+
"""
|
|
1594
|
+
Run the specified resource handler through the current interpreter,
|
|
1595
|
+
using the resource command_line method as args.
|
|
1596
|
+
"""
|
|
1597
|
+
# Generic config
|
|
1598
|
+
args = [
|
|
1599
|
+
self._actual_interpreter,
|
|
1600
|
+
]
|
|
1601
|
+
args.extend(self._interpreter_args_fix(rh, opts))
|
|
1602
|
+
args.extend(self.spawn_command_line(rh))
|
|
1603
|
+
logger.info("Run script %s", args)
|
|
1604
|
+
rh_stdin = self.spawn_stdin(rh)
|
|
1605
|
+
if rh_stdin is not None:
|
|
1606
|
+
plocale = locale.getlocale()[1] or "ascii"
|
|
1607
|
+
logger.info(
|
|
1608
|
+
"Script stdin:\n%s", rh_stdin.read().decode(plocale, "replace")
|
|
1609
|
+
)
|
|
1610
|
+
rh_stdin.seek(0)
|
|
1611
|
+
# Python path stuff
|
|
1612
|
+
newpypath = ":".join(self.extendpypath)
|
|
1613
|
+
if "pythonpath" in self.env:
|
|
1614
|
+
newpypath += ":{:s}".format(self.env.pythonpath)
|
|
1615
|
+
# launching the program...
|
|
1616
|
+
with self.env.delta_context(pythonpath=newpypath):
|
|
1617
|
+
self.spawn(args, opts, stdin=rh_stdin)
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
class ParaExpresso(TaylorRun):
|
|
1621
|
+
"""
|
|
1622
|
+
Run any script in the current environment.
|
|
1623
|
+
|
|
1624
|
+
This abstract class includes helpers to use the taylorism package in order
|
|
1625
|
+
to introduce an external parallelisation. It is designed to work well with a
|
|
1626
|
+
taylorism Worker class that inherits from
|
|
1627
|
+
:class:`vortex.tools.parallelism.VortexWorkerBlindRun`.
|
|
1628
|
+
"""
|
|
1629
|
+
|
|
1630
|
+
_abstract = True
|
|
1631
|
+
_footprint = dict(
|
|
1632
|
+
info="AlgoComponent that simply runs a script using the taylorism package.",
|
|
1633
|
+
attr=dict(
|
|
1634
|
+
interpreter=dict(
|
|
1635
|
+
info="The interpreter needed to run the script.",
|
|
1636
|
+
values=["current", "awk", "ksh", "bash", "perl", "python"],
|
|
1637
|
+
),
|
|
1638
|
+
engine=dict(values=["exec", "launch"]),
|
|
1639
|
+
interpreter_path=dict(
|
|
1640
|
+
info="The full path to the interpreter.",
|
|
1641
|
+
optional=True,
|
|
1642
|
+
),
|
|
1643
|
+
extendpypath=dict(
|
|
1644
|
+
info="The list of things to be prepended in the python's path.",
|
|
1645
|
+
type=footprints.FPList,
|
|
1646
|
+
default=footprints.FPList([]),
|
|
1647
|
+
optional=True,
|
|
1648
|
+
),
|
|
1649
|
+
),
|
|
1650
|
+
)
|
|
1651
|
+
|
|
1652
|
+
def valid_executable(self, rh):
|
|
1653
|
+
"""
|
|
1654
|
+
Return a boolean value according to the effective executable nature
|
|
1655
|
+
of the resource handler provided.
|
|
1656
|
+
"""
|
|
1657
|
+
return rh is not None
|
|
1658
|
+
|
|
1659
|
+
def _interpreter_args_fix(self, rh, opts):
|
|
1660
|
+
absexec = self.absexcutable(rh.container.localpath())
|
|
1661
|
+
if self.interpreter == "awk":
|
|
1662
|
+
return ["-f", absexec]
|
|
1663
|
+
else:
|
|
1664
|
+
return [
|
|
1665
|
+
absexec,
|
|
1666
|
+
]
|
|
1667
|
+
|
|
1668
|
+
def _default_common_instructions(self, rh, opts):
|
|
1669
|
+
"""Create a common instruction dictionary that will be used by the workers."""
|
|
1670
|
+
ddict = super()._default_common_instructions(rh, opts)
|
|
1671
|
+
actual_interpreter = (
|
|
1672
|
+
sys.executable
|
|
1673
|
+
if self.interpreter == "current"
|
|
1674
|
+
else self.interpreter
|
|
1675
|
+
)
|
|
1676
|
+
ddict["progname"] = actual_interpreter
|
|
1677
|
+
ddict["progargs"] = footprints.FPList(
|
|
1678
|
+
self._interpreter_args_fix(rh, opts) + self.spawn_command_line(rh)
|
|
1679
|
+
)
|
|
1680
|
+
ddict["progenvdelta"] = footprints.FPDict()
|
|
1681
|
+
# Deal with the python path
|
|
1682
|
+
newpypath = ":".join(self.extendpypath)
|
|
1683
|
+
if "pythonpath" in self.env:
|
|
1684
|
+
self.env.pythonpath += ":{:s}".format(newpypath)
|
|
1685
|
+
if newpypath:
|
|
1686
|
+
ddict["progenvdelta"]["pythonpath"] = newpypath
|
|
1687
|
+
return ddict
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
class BlindRun(xExecutableAlgoComponent):
|
|
1691
|
+
"""
|
|
1692
|
+
Run any executable resource in the current environment. Mandatory argument is:
|
|
1693
|
+
* engine ( values = blind )
|
|
1694
|
+
"""
|
|
1695
|
+
|
|
1696
|
+
_footprint = dict(
|
|
1697
|
+
info="AlgoComponent that simply runs a serial binary",
|
|
1698
|
+
attr=dict(engine=dict(values=["blind"])),
|
|
1699
|
+
)
|
|
1700
|
+
|
|
1701
|
+
def execute_single(self, rh, opts):
|
|
1702
|
+
"""
|
|
1703
|
+
Run the specified resource handler as an absolute executable,
|
|
1704
|
+
using the resource command_line method as args.
|
|
1705
|
+
"""
|
|
1706
|
+
|
|
1707
|
+
args = [self.absexcutable(rh.container.localpath())]
|
|
1708
|
+
args.extend(self.spawn_command_line(rh))
|
|
1709
|
+
logger.info("BlindRun executable resource %s", args)
|
|
1710
|
+
rh_stdin = self.spawn_stdin(rh)
|
|
1711
|
+
if rh_stdin is not None:
|
|
1712
|
+
plocale = locale.getlocale()[1] or "ascii"
|
|
1713
|
+
logger.info(
|
|
1714
|
+
"BlindRun executable stdin (fileno:%d):\n%s",
|
|
1715
|
+
rh_stdin.fileno(),
|
|
1716
|
+
rh_stdin.read().decode(plocale, "replace"),
|
|
1717
|
+
)
|
|
1718
|
+
rh_stdin.seek(0)
|
|
1719
|
+
self.spawn(args, opts, stdin=rh_stdin)
|
|
1720
|
+
|
|
1721
|
+
|
|
1722
|
+
class ParaBlindRun(TaylorRun):
|
|
1723
|
+
"""
|
|
1724
|
+
Run any executable resource (without MPI) in the current environment.
|
|
1725
|
+
|
|
1726
|
+
This abstract class includes helpers to use the taylorism package in order
|
|
1727
|
+
to introduce an external parallelisation. It is designed to work well with a
|
|
1728
|
+
taylorism Worker class that inherits from
|
|
1729
|
+
:class:`vortex.tools.parallelism.VortexWorkerBlindRun`.
|
|
1730
|
+
"""
|
|
1731
|
+
|
|
1732
|
+
_abstract = True
|
|
1733
|
+
_footprint = dict(
|
|
1734
|
+
info="Abstract AlgoComponent that runs a serial binary using the taylorism package.",
|
|
1735
|
+
attr=dict(
|
|
1736
|
+
engine=dict(values=["blind"]),
|
|
1737
|
+
taskset=dict(
|
|
1738
|
+
info="Topology/Method to set up the CPU affinity of the child task.",
|
|
1739
|
+
default=None,
|
|
1740
|
+
optional=True,
|
|
1741
|
+
values=[
|
|
1742
|
+
"{:s}{:s}".format(t, m)
|
|
1743
|
+
for t in ("raw", "socketpacked", "numapacked")
|
|
1744
|
+
for m in ("", "_taskset", "_gomp", "_omp", "_ompverbose")
|
|
1745
|
+
],
|
|
1746
|
+
),
|
|
1747
|
+
taskset_bsize=dict(
|
|
1748
|
+
info="The number of threads used by one task",
|
|
1749
|
+
type=int,
|
|
1750
|
+
default=1,
|
|
1751
|
+
optional=True,
|
|
1752
|
+
),
|
|
1753
|
+
),
|
|
1754
|
+
)
|
|
1755
|
+
|
|
1756
|
+
def valid_executable(self, rh):
|
|
1757
|
+
"""
|
|
1758
|
+
Return a boolean value according to the effective executable nature
|
|
1759
|
+
of the resource handler provided.
|
|
1760
|
+
"""
|
|
1761
|
+
rc = rh is not None
|
|
1762
|
+
if rc:
|
|
1763
|
+
# Ensure that the input file is executable
|
|
1764
|
+
xrh = (
|
|
1765
|
+
rh
|
|
1766
|
+
if isinstance(rh, (list, tuple))
|
|
1767
|
+
else [
|
|
1768
|
+
rh,
|
|
1769
|
+
]
|
|
1770
|
+
)
|
|
1771
|
+
for arh in xrh:
|
|
1772
|
+
self.system.xperm(arh.container.localpath(), force=True)
|
|
1773
|
+
return rc
|
|
1774
|
+
|
|
1775
|
+
def _default_common_instructions(self, rh, opts):
|
|
1776
|
+
"""Create a common instruction dictionary that will be used by the workers."""
|
|
1777
|
+
ddict = super()._default_common_instructions(rh, opts)
|
|
1778
|
+
ddict["progname"] = self.absexcutable(rh.container.localpath())
|
|
1779
|
+
ddict["progargs"] = footprints.FPList(self.spawn_command_line(rh))
|
|
1780
|
+
ddict["progtaskset"] = self.taskset
|
|
1781
|
+
ddict["progtaskset_bsize"] = self.taskset_bsize
|
|
1782
|
+
return ddict
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
class Parallel(xExecutableAlgoComponent):
|
|
1786
|
+
"""
|
|
1787
|
+
Run a binary launched with MPI support.
|
|
1788
|
+
"""
|
|
1789
|
+
|
|
1790
|
+
_footprint = dict(
|
|
1791
|
+
info="AlgoComponent that simply runs an MPI binary",
|
|
1792
|
+
attr=dict(
|
|
1793
|
+
engine=dict(values=["parallel"]),
|
|
1794
|
+
mpitool=dict(
|
|
1795
|
+
info="The object used to launch the parallel program",
|
|
1796
|
+
optional=True,
|
|
1797
|
+
type=mpitools.MpiTool,
|
|
1798
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
1799
|
+
),
|
|
1800
|
+
mpiname=dict(
|
|
1801
|
+
info=(
|
|
1802
|
+
"The mpiname of a class in the mpitool collector "
|
|
1803
|
+
+ "(used only if *mpitool* is not provided)"
|
|
1804
|
+
),
|
|
1805
|
+
optional=True,
|
|
1806
|
+
alias=["mpi"],
|
|
1807
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
1808
|
+
),
|
|
1809
|
+
mpiverbose=dict(
|
|
1810
|
+
info="Boost logging verbosity in mpitools",
|
|
1811
|
+
optional=True,
|
|
1812
|
+
default=False,
|
|
1813
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
1814
|
+
),
|
|
1815
|
+
binaries=dict(
|
|
1816
|
+
info="List of MpiBinaryDescription objects",
|
|
1817
|
+
optional=True,
|
|
1818
|
+
type=footprints.FPList,
|
|
1819
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
1820
|
+
),
|
|
1821
|
+
binarysingle=dict(
|
|
1822
|
+
info="If *binaries* is missing, the default binary role for single binaries",
|
|
1823
|
+
optional=True,
|
|
1824
|
+
default="basicsingle",
|
|
1825
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
1826
|
+
),
|
|
1827
|
+
binarymulti=dict(
|
|
1828
|
+
info="If *binaries* is missing, the default binary role for multiple binaries",
|
|
1829
|
+
type=footprints.FPList,
|
|
1830
|
+
optional=True,
|
|
1831
|
+
default=footprints.FPList(
|
|
1832
|
+
[
|
|
1833
|
+
"basic",
|
|
1834
|
+
]
|
|
1835
|
+
),
|
|
1836
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
1837
|
+
),
|
|
1838
|
+
),
|
|
1839
|
+
)
|
|
1840
|
+
|
|
1841
|
+
def _mpitool_attributes(self, opts):
|
|
1842
|
+
"""Return the dictionary of attributes needed to create the mpitool object."""
|
|
1843
|
+
# Read the appropriate configuration in the target file
|
|
1844
|
+
conf_dict = config.from_config(section="mpitool")
|
|
1845
|
+
if self.mpiname:
|
|
1846
|
+
conf_dict["mpiname"] = self.mpiname
|
|
1847
|
+
# Make "mpirun" the default mpi command name
|
|
1848
|
+
if "mpiname" not in conf_dict.keys():
|
|
1849
|
+
conf_dict["mpiname"] = "mpirun"
|
|
1850
|
+
possible_attrs = functools.reduce(
|
|
1851
|
+
lambda s, t: s | t,
|
|
1852
|
+
[
|
|
1853
|
+
set(cls.footprint_retrieve().attr.keys())
|
|
1854
|
+
for cls in footprints.proxy.mpitools
|
|
1855
|
+
],
|
|
1856
|
+
)
|
|
1857
|
+
nonkeys = set(conf_dict.keys()) - possible_attrs
|
|
1858
|
+
if nonkeys:
|
|
1859
|
+
msg = (
|
|
1860
|
+
"The following keywords are unknown configuration"
|
|
1861
|
+
'keys for section "mpitool":\n'
|
|
1862
|
+
)
|
|
1863
|
+
|
|
1864
|
+
raise ValueError(msg + "\n".join(nonkeys))
|
|
1865
|
+
return conf_dict
|
|
1866
|
+
|
|
1867
|
+
def spawn_command_line(self, rh):
|
|
1868
|
+
"""Split the shell command line of the resource to be run."""
|
|
1869
|
+
return [super(Parallel, self).spawn_command_line(r) for r in rh]
|
|
1870
|
+
|
|
1871
|
+
def _bootstrap_mpibins_hack(self, bins, rh, opts, use_envelope):
|
|
1872
|
+
return copy.deepcopy(bins)
|
|
1873
|
+
|
|
1874
|
+
def _bootstrap_mpienvelope_hack(self, envelope, rh, opts, mpi):
|
|
1875
|
+
return copy.deepcopy(envelope)
|
|
1876
|
+
|
|
1877
|
+
def _bootstrap_mpienvelope_posthack(self, envelope, rh, opts, mpi):
|
|
1878
|
+
return None
|
|
1879
|
+
|
|
1880
|
+
def _bootstrap_mpitool(self, rh, opts):
|
|
1881
|
+
"""Initialise the mpitool object and finds out the command line."""
|
|
1882
|
+
|
|
1883
|
+
# Rh is a list binaries...
|
|
1884
|
+
if not isinstance(rh, collections.abc.Iterable):
|
|
1885
|
+
rh = [
|
|
1886
|
+
rh,
|
|
1887
|
+
]
|
|
1888
|
+
|
|
1889
|
+
# Find the MPI launcher
|
|
1890
|
+
mpi = self.mpitool
|
|
1891
|
+
if not mpi:
|
|
1892
|
+
mpi = footprints.proxy.mpitool(
|
|
1893
|
+
sysname=self.system.sysname, **self._mpitool_attributes(opts)
|
|
1894
|
+
)
|
|
1895
|
+
if not mpi:
|
|
1896
|
+
logger.critical(
|
|
1897
|
+
"Component %s could not find any mpitool",
|
|
1898
|
+
self.footprint_clsname(),
|
|
1899
|
+
)
|
|
1900
|
+
raise AttributeError("No valid mpitool attr could be found.")
|
|
1901
|
+
|
|
1902
|
+
# Setup various useful things (env, system, ...)
|
|
1903
|
+
mpi.import_basics(self)
|
|
1904
|
+
|
|
1905
|
+
mpi_opts = opts.get("mpiopts", dict())
|
|
1906
|
+
|
|
1907
|
+
envelope = []
|
|
1908
|
+
use_envelope = "envelope" in mpi_opts
|
|
1909
|
+
if use_envelope:
|
|
1910
|
+
envelope = mpi_opts.pop("envelope")
|
|
1911
|
+
if envelope == "auto":
|
|
1912
|
+
blockspec = dict(
|
|
1913
|
+
nn=self.env.get("VORTEX_SUBMIT_NODES", 1),
|
|
1914
|
+
)
|
|
1915
|
+
if "VORTEX_SUBMIT_TASKS" in self.env:
|
|
1916
|
+
blockspec["nnp"] = self.env.get("VORTEX_SUBMIT_TASKS")
|
|
1917
|
+
else:
|
|
1918
|
+
raise ValueError(
|
|
1919
|
+
"when envelope='auto', VORTEX_SUBMIT_TASKS must be set up."
|
|
1920
|
+
)
|
|
1921
|
+
envelope = [
|
|
1922
|
+
blockspec,
|
|
1923
|
+
]
|
|
1924
|
+
elif isinstance(envelope, dict):
|
|
1925
|
+
envelope = [
|
|
1926
|
+
envelope,
|
|
1927
|
+
]
|
|
1928
|
+
elif isinstance(envelope, (list, tuple)):
|
|
1929
|
+
pass
|
|
1930
|
+
else:
|
|
1931
|
+
raise AttributeError("Invalid envelope specification")
|
|
1932
|
+
if envelope:
|
|
1933
|
+
envelope_ntasks = sum([d["nn"] * d["nnp"] for d in envelope])
|
|
1934
|
+
if not envelope:
|
|
1935
|
+
use_envelope = False
|
|
1936
|
+
|
|
1937
|
+
if not use_envelope:
|
|
1938
|
+
# Some MPI presets
|
|
1939
|
+
mpi_desc = dict()
|
|
1940
|
+
for mpi_k in ("tasks", "openmp"):
|
|
1941
|
+
mpi_kenv = "VORTEX_SUBMIT_" + mpi_k.upper()
|
|
1942
|
+
if mpi_kenv in self.env:
|
|
1943
|
+
mpi_desc[mpi_k] = self.env.get(mpi_kenv)
|
|
1944
|
+
|
|
1945
|
+
# Binaries may be grouped together on the same nodes
|
|
1946
|
+
bin_groups = mpi_opts.pop("groups", [])
|
|
1947
|
+
|
|
1948
|
+
# Find out the command line
|
|
1949
|
+
bargs = self.spawn_command_line(rh)
|
|
1950
|
+
|
|
1951
|
+
# Potential Source files
|
|
1952
|
+
sources = []
|
|
1953
|
+
|
|
1954
|
+
# The usual case: no indications, 1 binary + a potential ioserver
|
|
1955
|
+
if len(rh) == 1 and not self.binaries:
|
|
1956
|
+
# In such a case, defining group does not makes sense
|
|
1957
|
+
self.algoassert(
|
|
1958
|
+
not bin_groups,
|
|
1959
|
+
"With only one binary, groups should not be defined",
|
|
1960
|
+
)
|
|
1961
|
+
|
|
1962
|
+
# The main program
|
|
1963
|
+
allowbind = mpi_opts.pop("allowbind", True)
|
|
1964
|
+
distribution = mpi_opts.pop(
|
|
1965
|
+
"distribution",
|
|
1966
|
+
self.env.get("VORTEX_MPIBIN_DEF_DISTRIBUTION", None),
|
|
1967
|
+
)
|
|
1968
|
+
if use_envelope:
|
|
1969
|
+
master = footprints.proxy.mpibinary(
|
|
1970
|
+
kind=self.binarysingle,
|
|
1971
|
+
ranks=envelope_ntasks,
|
|
1972
|
+
openmp=self.env.get("VORTEX_SUBMIT_OPENMP", None),
|
|
1973
|
+
allowbind=allowbind,
|
|
1974
|
+
distribution=distribution,
|
|
1975
|
+
)
|
|
1976
|
+
else:
|
|
1977
|
+
master = footprints.proxy.mpibinary(
|
|
1978
|
+
kind=self.binarysingle,
|
|
1979
|
+
nodes=self.env.get("VORTEX_SUBMIT_NODES", 1),
|
|
1980
|
+
allowbind=allowbind,
|
|
1981
|
+
distribution=distribution,
|
|
1982
|
+
**mpi_desc,
|
|
1983
|
+
)
|
|
1984
|
+
master.options = mpi_opts
|
|
1985
|
+
master.master = self.absexcutable(rh[0].container.localpath())
|
|
1986
|
+
master.arguments = bargs[0]
|
|
1987
|
+
bins = [
|
|
1988
|
+
master,
|
|
1989
|
+
]
|
|
1990
|
+
# Source files ?
|
|
1991
|
+
if hasattr(rh[0].resource, "guess_binary_sources"):
|
|
1992
|
+
sources.extend(
|
|
1993
|
+
rh[0].resource.guess_binary_sources(rh[0].provider)
|
|
1994
|
+
)
|
|
1995
|
+
|
|
1996
|
+
# Multiple binaries are to be launched: no IO server support here.
|
|
1997
|
+
elif len(rh) > 1 and not self.binaries:
|
|
1998
|
+
# Binary roles
|
|
1999
|
+
if len(self.binarymulti) == 1:
|
|
2000
|
+
bnames = self.binarymulti * len(rh)
|
|
2001
|
+
else:
|
|
2002
|
+
if len(self.binarymulti) != len(rh):
|
|
2003
|
+
raise ParallelInconsistencyAlgoComponentError(
|
|
2004
|
+
"self.binarymulti"
|
|
2005
|
+
)
|
|
2006
|
+
bnames = self.binarymulti
|
|
2007
|
+
|
|
2008
|
+
# Check mpiopts shape
|
|
2009
|
+
for k, v in mpi_opts.items():
|
|
2010
|
+
if not isinstance(v, collections.abc.Iterable):
|
|
2011
|
+
raise ValueError(
|
|
2012
|
+
"In such a case, mpiopts must be Iterable"
|
|
2013
|
+
)
|
|
2014
|
+
if len(v) != len(rh):
|
|
2015
|
+
raise ParallelInconsistencyAlgoComponentError(
|
|
2016
|
+
"mpiopts[{:s}]".format(k)
|
|
2017
|
+
)
|
|
2018
|
+
# Check bin_group shape
|
|
2019
|
+
if bin_groups:
|
|
2020
|
+
if len(bin_groups) != len(rh):
|
|
2021
|
+
raise ParallelInconsistencyAlgoComponentError("bin_group")
|
|
2022
|
+
|
|
2023
|
+
# Create MpiBinaryDescription objects
|
|
2024
|
+
bins = list()
|
|
2025
|
+
allowbinds = mpi_opts.pop(
|
|
2026
|
+
"allowbind",
|
|
2027
|
+
[
|
|
2028
|
+
True,
|
|
2029
|
+
]
|
|
2030
|
+
* len(rh),
|
|
2031
|
+
)
|
|
2032
|
+
distributions = mpi_opts.pop(
|
|
2033
|
+
"distribution",
|
|
2034
|
+
[
|
|
2035
|
+
self.env.get("VORTEX_MPIBIN_DEF_DISTRIBUTION", None),
|
|
2036
|
+
]
|
|
2037
|
+
* len(rh),
|
|
2038
|
+
)
|
|
2039
|
+
for i, r in enumerate(rh):
|
|
2040
|
+
if use_envelope:
|
|
2041
|
+
bins.append(
|
|
2042
|
+
footprints.proxy.mpibinary(
|
|
2043
|
+
kind=bnames[i],
|
|
2044
|
+
allowbind=allowbinds[i],
|
|
2045
|
+
distribution=distributions[i],
|
|
2046
|
+
)
|
|
2047
|
+
)
|
|
2048
|
+
else:
|
|
2049
|
+
bins.append(
|
|
2050
|
+
footprints.proxy.mpibinary(
|
|
2051
|
+
kind=bnames[i],
|
|
2052
|
+
nodes=self.env.get("VORTEX_SUBMIT_NODES", 1),
|
|
2053
|
+
allowbind=allowbinds[i],
|
|
2054
|
+
distribution=distributions[i],
|
|
2055
|
+
**mpi_desc,
|
|
2056
|
+
)
|
|
2057
|
+
)
|
|
2058
|
+
# Reshape mpiopts
|
|
2059
|
+
bins[i].options = {k: v[i] for k, v in mpi_opts.items()}
|
|
2060
|
+
if bin_groups:
|
|
2061
|
+
bins[i].group = bin_groups[i]
|
|
2062
|
+
bins[i].master = self.absexcutable(r.container.localpath())
|
|
2063
|
+
bins[i].arguments = bargs[i]
|
|
2064
|
+
# Source files ?
|
|
2065
|
+
if hasattr(r.resource, "guess_binary_sources"):
|
|
2066
|
+
sources.extend(r.resource.guess_binary_sources(r.provider))
|
|
2067
|
+
|
|
2068
|
+
# Nothing to do: binary descriptions are provided by the user
|
|
2069
|
+
else:
|
|
2070
|
+
if len(self.binaries) != len(rh):
|
|
2071
|
+
raise ParallelInconsistencyAlgoComponentError("self.binaries")
|
|
2072
|
+
bins = self.binaries
|
|
2073
|
+
for i, r in enumerate(rh):
|
|
2074
|
+
bins[i].master = self.absexcutable(r.container.localpath())
|
|
2075
|
+
bins[i].arguments = bargs[i]
|
|
2076
|
+
|
|
2077
|
+
# The global envelope
|
|
2078
|
+
envelope = self._bootstrap_mpienvelope_hack(envelope, rh, opts, mpi)
|
|
2079
|
+
if envelope:
|
|
2080
|
+
mpi.envelope = envelope
|
|
2081
|
+
|
|
2082
|
+
# The binaries description
|
|
2083
|
+
mpi.binaries = self._bootstrap_mpibins_hack(
|
|
2084
|
+
bins, rh, opts, use_envelope
|
|
2085
|
+
)
|
|
2086
|
+
upd_envelope = self._bootstrap_mpienvelope_posthack(
|
|
2087
|
+
envelope, rh, opts, mpi
|
|
2088
|
+
)
|
|
2089
|
+
if upd_envelope:
|
|
2090
|
+
mpi.envelope = upd_envelope
|
|
2091
|
+
|
|
2092
|
+
# The source files
|
|
2093
|
+
mpi.sources = sources
|
|
2094
|
+
|
|
2095
|
+
if envelope:
|
|
2096
|
+
# Check the consistency between nranks and the total number of processes
|
|
2097
|
+
envelope_ntasks = sum([d.nprocs for d in mpi.envelope])
|
|
2098
|
+
mpibins_total = sum([m.nprocs for m in mpi.binaries])
|
|
2099
|
+
if not envelope_ntasks == mpibins_total:
|
|
2100
|
+
raise AlgoComponentError(
|
|
2101
|
+
(
|
|
2102
|
+
"The number of requested ranks ({:d}) must be equal "
|
|
2103
|
+
"to the number of processes available in the envelope ({:d})"
|
|
2104
|
+
).format(mpibins_total, envelope_ntasks)
|
|
2105
|
+
)
|
|
2106
|
+
|
|
2107
|
+
args = mpi.mkcmdline()
|
|
2108
|
+
for b in mpi.binaries:
|
|
2109
|
+
logger.info(
|
|
2110
|
+
"Run %s in parallel mode. Args: %s.",
|
|
2111
|
+
b.master,
|
|
2112
|
+
" ".join(b.arguments),
|
|
2113
|
+
)
|
|
2114
|
+
logger.info("Full MPI command line: %s", " ".join(args))
|
|
2115
|
+
|
|
2116
|
+
# Setup various useful things (env, system, ...)
|
|
2117
|
+
mpi.import_basics(self)
|
|
2118
|
+
|
|
2119
|
+
return mpi, args
|
|
2120
|
+
|
|
2121
|
+
@contextlib.contextmanager
|
|
2122
|
+
def _tweak_mpitools_logging(self):
|
|
2123
|
+
if self.mpiverbose:
|
|
2124
|
+
m_loggers = dict()
|
|
2125
|
+
for m_logger_name in [
|
|
2126
|
+
l for l in loggers.lognames if "mpitools" in l
|
|
2127
|
+
]:
|
|
2128
|
+
m_logger = loggers.getLogger(m_logger_name)
|
|
2129
|
+
m_loggers[m_logger] = m_logger.level
|
|
2130
|
+
m_logger.setLevel(logging.DEBUG)
|
|
2131
|
+
try:
|
|
2132
|
+
yield
|
|
2133
|
+
finally:
|
|
2134
|
+
for m_logger, prev_level in m_loggers.items():
|
|
2135
|
+
m_logger.setLevel(prev_level)
|
|
2136
|
+
else:
|
|
2137
|
+
yield
|
|
2138
|
+
|
|
2139
|
+
def execute_single(self, rh, opts):
|
|
2140
|
+
"""Run the specified resource handler through the `mpitool` launcher
|
|
2141
|
+
|
|
2142
|
+
An argument named `mpiopts` could be provided as a dictionary: it may
|
|
2143
|
+
contain indications on the number of nodes, tasks, ...
|
|
2144
|
+
"""
|
|
2145
|
+
|
|
2146
|
+
self.system.subtitle("{:s} : parallel engine".format(self.realkind))
|
|
2147
|
+
|
|
2148
|
+
with self._tweak_mpitools_logging():
|
|
2149
|
+
# Return a mpitool object and the mpicommand line
|
|
2150
|
+
mpi, args = self._bootstrap_mpitool(rh, opts)
|
|
2151
|
+
|
|
2152
|
+
# Specific parallel settings
|
|
2153
|
+
mpi.setup(opts)
|
|
2154
|
+
|
|
2155
|
+
# This is actual running command
|
|
2156
|
+
self.spawn(args, opts)
|
|
2157
|
+
|
|
2158
|
+
# Specific parallel cleaning
|
|
2159
|
+
mpi.clean(opts)
|
|
2160
|
+
|
|
2161
|
+
|
|
2162
|
+
@algo_component_deco_mixin_autodoc
|
|
2163
|
+
class ParallelIoServerMixin(AlgoComponentMpiDecoMixin):
|
|
2164
|
+
"""Adds an IOServer capabilities (footprints attributes + MPI bianries alteration)."""
|
|
2165
|
+
|
|
2166
|
+
_MIXIN_EXTRA_FOOTPRINTS = [
|
|
2167
|
+
footprints.Footprint(
|
|
2168
|
+
info="Abstract IoServer footprints' attributes.",
|
|
2169
|
+
attr=dict(
|
|
2170
|
+
ioserver=dict(
|
|
2171
|
+
info="The object used to launch the IOserver part of the binary.",
|
|
2172
|
+
type=mpitools.MpiBinaryIOServer,
|
|
2173
|
+
optional=True,
|
|
2174
|
+
default=None,
|
|
2175
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
2176
|
+
),
|
|
2177
|
+
ioname=dict(
|
|
2178
|
+
info=(
|
|
2179
|
+
"The binary_kind of a class in the mpibinary collector "
|
|
2180
|
+
+ "(used only if *ioserver* is not provided)"
|
|
2181
|
+
),
|
|
2182
|
+
optional=True,
|
|
2183
|
+
default="ioserv",
|
|
2184
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
2185
|
+
),
|
|
2186
|
+
iolocation=dict(
|
|
2187
|
+
info="Location of the IO server within the binary list",
|
|
2188
|
+
type=int,
|
|
2189
|
+
default=-1,
|
|
2190
|
+
optional=True,
|
|
2191
|
+
),
|
|
2192
|
+
),
|
|
2193
|
+
),
|
|
2194
|
+
]
|
|
2195
|
+
|
|
2196
|
+
def _bootstrap_mpibins_ioserver_hack(
|
|
2197
|
+
self, bins, bins0, rh, opts, use_envelope
|
|
2198
|
+
):
|
|
2199
|
+
"""If requested, adds an extra binary that will act as an IOServer."""
|
|
2200
|
+
master = bins[-1]
|
|
2201
|
+
# A potential IO server
|
|
2202
|
+
io = self.ioserver
|
|
2203
|
+
if not io and int(self.env.get("VORTEX_IOSERVER_NODES", -1)) >= 0:
|
|
2204
|
+
io = footprints.proxy.mpibinary(
|
|
2205
|
+
kind=self.ioname,
|
|
2206
|
+
nodes=self.env.VORTEX_IOSERVER_NODES,
|
|
2207
|
+
tasks=(
|
|
2208
|
+
self.env.VORTEX_IOSERVER_TASKS
|
|
2209
|
+
or master.options.get("nnp", master.tasks)
|
|
2210
|
+
),
|
|
2211
|
+
openmp=(
|
|
2212
|
+
self.env.VORTEX_IOSERVER_OPENMP
|
|
2213
|
+
or master.options.get("openmp", master.openmp)
|
|
2214
|
+
),
|
|
2215
|
+
iolocation=self.iolocation,
|
|
2216
|
+
)
|
|
2217
|
+
io.options = {
|
|
2218
|
+
x[3:]: opts[x] for x in opts.keys() if x.startswith("io_")
|
|
2219
|
+
}
|
|
2220
|
+
io.master = master.master
|
|
2221
|
+
io.arguments = master.arguments
|
|
2222
|
+
if (
|
|
2223
|
+
not io
|
|
2224
|
+
and int(self.env.get("VORTEX_IOSERVER_COMPANION_TASKS", -1)) >= 0
|
|
2225
|
+
):
|
|
2226
|
+
io = footprints.proxy.mpibinary(
|
|
2227
|
+
kind=self.ioname,
|
|
2228
|
+
nodes=master.options.get("nn", master.nodes),
|
|
2229
|
+
tasks=self.env.VORTEX_IOSERVER_COMPANION_TASKS,
|
|
2230
|
+
openmp=(
|
|
2231
|
+
self.env.VORTEX_IOSERVER_OPENMP
|
|
2232
|
+
or master.options.get("openmp", master.openmp)
|
|
2233
|
+
),
|
|
2234
|
+
)
|
|
2235
|
+
io.options = {
|
|
2236
|
+
x[3:]: opts[x] for x in opts.keys() if x.startswith("io_")
|
|
2237
|
+
}
|
|
2238
|
+
io.master = master.master
|
|
2239
|
+
io.arguments = master.arguments
|
|
2240
|
+
if master.group is not None:
|
|
2241
|
+
# The master binary is already in a group ! Use it.
|
|
2242
|
+
io.group = master.group
|
|
2243
|
+
else:
|
|
2244
|
+
io.group = "auto_masterwithio"
|
|
2245
|
+
master.group = "auto_masterwithio"
|
|
2246
|
+
if (
|
|
2247
|
+
not io
|
|
2248
|
+
and self.env.get("VORTEX_IOSERVER_INCORE_TASKS", None) is not None
|
|
2249
|
+
):
|
|
2250
|
+
if hasattr(master, "incore_iotasks"):
|
|
2251
|
+
master.incore_iotasks = self.env.VORTEX_IOSERVER_INCORE_TASKS
|
|
2252
|
+
if (
|
|
2253
|
+
not io
|
|
2254
|
+
and self.env.get("VORTEX_IOSERVER_INCORE_FIXER", None) is not None
|
|
2255
|
+
):
|
|
2256
|
+
if hasattr(master, "incore_iotasks_fixer"):
|
|
2257
|
+
master.incore_iotasks_fixer = (
|
|
2258
|
+
self.env.VORTEX_IOSERVER_INCORE_FIXER
|
|
2259
|
+
)
|
|
2260
|
+
if (
|
|
2261
|
+
not io
|
|
2262
|
+
and self.env.get("VORTEX_IOSERVER_INCORE_DIST", None) is not None
|
|
2263
|
+
):
|
|
2264
|
+
if hasattr(master, "incore_iodist"):
|
|
2265
|
+
master.incore_iodist = self.env.VORTEX_IOSERVER_INCORE_DIST
|
|
2266
|
+
if io:
|
|
2267
|
+
rh.append(rh[0])
|
|
2268
|
+
if master.group is None:
|
|
2269
|
+
if "nn" in master.options:
|
|
2270
|
+
master.options["nn"] = (
|
|
2271
|
+
master.options["nn"] - io.options["nn"]
|
|
2272
|
+
)
|
|
2273
|
+
else:
|
|
2274
|
+
logger.warning(
|
|
2275
|
+
'The "nn" option is not available in the master binary '
|
|
2276
|
+
+ "mpi options. Consequently it can be fixed..."
|
|
2277
|
+
)
|
|
2278
|
+
if self.iolocation >= 0:
|
|
2279
|
+
bins.insert(self.iolocation, io)
|
|
2280
|
+
else:
|
|
2281
|
+
bins.append(io)
|
|
2282
|
+
return bins
|
|
2283
|
+
|
|
2284
|
+
_MIXIN_MPIBINS_HOOKS = (_bootstrap_mpibins_ioserver_hack,)
|
|
2285
|
+
|
|
2286
|
+
|
|
2287
|
+
@algo_component_deco_mixin_autodoc
|
|
2288
|
+
class ParallelOpenPalmMixin(AlgoComponentMpiDecoMixin):
|
|
2289
|
+
"""Class mixin to be used with OpenPALM programs.
|
|
2290
|
+
|
|
2291
|
+
It will automatically add the OpenPALM driver binary to the list of
|
|
2292
|
+
binaries. The location of the OpenPALM driver should be automatically
|
|
2293
|
+
detected provided that a section with ``role=OpenPALM Driver`` lies in the
|
|
2294
|
+
input's sequence. Alternatively, the path to the OpenPALM driver can be
|
|
2295
|
+
provided using the **openpalm_driver** footprint's argument.
|
|
2296
|
+
"""
|
|
2297
|
+
|
|
2298
|
+
_MIXIN_EXTRA_FOOTPRINTS = [
|
|
2299
|
+
footprints.Footprint(
|
|
2300
|
+
info="Abstract OpenPALM footprints' attributes.",
|
|
2301
|
+
attr=dict(
|
|
2302
|
+
openpalm_driver=dict(
|
|
2303
|
+
info=(
|
|
2304
|
+
"The path to the OpenPALM driver binary. "
|
|
2305
|
+
+ "When omitted, the input sequence is looked up "
|
|
2306
|
+
+ "for section with ``role=OpenPALM Driver``."
|
|
2307
|
+
),
|
|
2308
|
+
optional=True,
|
|
2309
|
+
doc_visibility=footprints.doc.visibility.ADVANCED,
|
|
2310
|
+
),
|
|
2311
|
+
openpalm_overcommit=dict(
|
|
2312
|
+
info=(
|
|
2313
|
+
"Run the OpenPALM driver on the first node in addition "
|
|
2314
|
+
+ "to existing tasks. Otherwise dedicated tasks are used."
|
|
2315
|
+
),
|
|
2316
|
+
type=bool,
|
|
2317
|
+
default=True,
|
|
2318
|
+
optional=True,
|
|
2319
|
+
doc_visibility=footprints.doc.visibility.ADVANCED,
|
|
2320
|
+
),
|
|
2321
|
+
openpalm_binddriver=dict(
|
|
2322
|
+
info="Try to bind the OpenPALM driver binary.",
|
|
2323
|
+
type=bool,
|
|
2324
|
+
optional=True,
|
|
2325
|
+
default=False,
|
|
2326
|
+
doc_visibility=footprints.doc.visibility.ADVANCED,
|
|
2327
|
+
),
|
|
2328
|
+
openpalm_binkind=dict(
|
|
2329
|
+
info="The binary kind for the OpenPALM driver.",
|
|
2330
|
+
optional=True,
|
|
2331
|
+
default="basic",
|
|
2332
|
+
doc_visibility=footprints.doc.visibility.GURU,
|
|
2333
|
+
),
|
|
2334
|
+
),
|
|
2335
|
+
),
|
|
2336
|
+
]
|
|
2337
|
+
|
|
2338
|
+
@property
|
|
2339
|
+
def _actual_openpalm_driver(self):
|
|
2340
|
+
"""Returns the OpenPALM's driver location."""
|
|
2341
|
+
path = self.openpalm_driver
|
|
2342
|
+
if path is None:
|
|
2343
|
+
drivers = self.context.sequence.effective_inputs(
|
|
2344
|
+
role="OpenPALMDriver"
|
|
2345
|
+
)
|
|
2346
|
+
if not drivers:
|
|
2347
|
+
raise AlgoComponentError("No OpenPALM driver was provided.")
|
|
2348
|
+
elif len(drivers) > 1:
|
|
2349
|
+
raise AlgoComponentError(
|
|
2350
|
+
"Several OpenPALM driver were provided."
|
|
2351
|
+
)
|
|
2352
|
+
path = drivers[0].rh.container.localpath()
|
|
2353
|
+
else:
|
|
2354
|
+
if not self.system.path.exists(path):
|
|
2355
|
+
raise AlgoComponentError(
|
|
2356
|
+
"No OpenPALM driver was provider ({:s} does not exists).".format(
|
|
2357
|
+
path
|
|
2358
|
+
)
|
|
2359
|
+
)
|
|
2360
|
+
return path
|
|
2361
|
+
|
|
2362
|
+
def _bootstrap_mpibins_openpalm_hack(
|
|
2363
|
+
self, bins, bins0, rh, opts, use_envelope
|
|
2364
|
+
):
|
|
2365
|
+
"""Adds the OpenPALM driver to the binary list."""
|
|
2366
|
+
single_bin = len(bins) == 1
|
|
2367
|
+
master = bins[0]
|
|
2368
|
+
driver = footprints.proxy.mpibinary(
|
|
2369
|
+
kind=self.openpalm_binkind,
|
|
2370
|
+
nodes=1,
|
|
2371
|
+
tasks=self.env.VORTEX_OPENPALM_DRV_TASKS or 1,
|
|
2372
|
+
openmp=self.env.VORTEX_OPENPALM_DRV_OPENMP or 1,
|
|
2373
|
+
allowbind=opts.pop(
|
|
2374
|
+
"palmdrv_bind",
|
|
2375
|
+
self.env.get(
|
|
2376
|
+
"VORTEX_OPENPALM_DRV_BIND", self.openpalm_binddriver
|
|
2377
|
+
),
|
|
2378
|
+
),
|
|
2379
|
+
)
|
|
2380
|
+
driver.options = {
|
|
2381
|
+
x[8:]: opts[x] for x in opts.keys() if x.startswith("palmdrv_")
|
|
2382
|
+
}
|
|
2383
|
+
driver.master = self._actual_openpalm_driver
|
|
2384
|
+
self.system.xperm(driver.master, force=True)
|
|
2385
|
+
bins.insert(0, driver)
|
|
2386
|
+
if not self.openpalm_overcommit and single_bin:
|
|
2387
|
+
# Tweak the number of tasks of the master program in order to accommodate
|
|
2388
|
+
# the driver
|
|
2389
|
+
# NB: If multiple binaries are provided, the user must do this by
|
|
2390
|
+
# himself (i.e. leave enough room for the driver's task).
|
|
2391
|
+
if "nn" in master.options:
|
|
2392
|
+
master.options["nn"] = master.options["nn"] - 1
|
|
2393
|
+
else:
|
|
2394
|
+
# Ok, tweak nprocs instead (an envelope might be defined)
|
|
2395
|
+
try:
|
|
2396
|
+
nprocs = master.nprocs
|
|
2397
|
+
except mpitools.MpiException:
|
|
2398
|
+
logger.error(
|
|
2399
|
+
'Neither the "nn" option nor the nprocs is '
|
|
2400
|
+
+ "available for the master binary. Consequently "
|
|
2401
|
+
+ "it can be fixed..."
|
|
2402
|
+
)
|
|
2403
|
+
else:
|
|
2404
|
+
master.options["np"] = nprocs - driver.nprocs
|
|
2405
|
+
return bins
|
|
2406
|
+
|
|
2407
|
+
_MIXIN_MPIBINS_HOOKS = (_bootstrap_mpibins_openpalm_hack,)
|
|
2408
|
+
|
|
2409
|
+
def _bootstrap_mpienvelope_openpalm_posthack(
|
|
2410
|
+
self, env, env0, rh, opts, mpi
|
|
2411
|
+
):
|
|
2412
|
+
"""
|
|
2413
|
+
Tweak the MPI envelope in order to execute the OpenPALM driver on the
|
|
2414
|
+
appropriate node.
|
|
2415
|
+
"""
|
|
2416
|
+
master = mpi.binaries[
|
|
2417
|
+
1
|
|
2418
|
+
] # The first "real" program that will be launched
|
|
2419
|
+
driver = mpi.binaries[0] # The OpenPALM driver
|
|
2420
|
+
if self.openpalm_overcommit:
|
|
2421
|
+
# Execute the driver on the first compute node
|
|
2422
|
+
if env or env0:
|
|
2423
|
+
env = env or copy.deepcopy(env0)
|
|
2424
|
+
# An envelope is already defined... update it
|
|
2425
|
+
if not ("nn" in env[0] and "nnp" in env[0]):
|
|
2426
|
+
raise AlgoComponentError(
|
|
2427
|
+
"'nn' and 'nnp' must be defined in the envelope"
|
|
2428
|
+
)
|
|
2429
|
+
if env[0]["nn"] > 1:
|
|
2430
|
+
env[0]["nn"] -= 1
|
|
2431
|
+
newenv = copy.copy(env[0])
|
|
2432
|
+
newenv["nn"] = 1
|
|
2433
|
+
newenv["nnp"] += driver.nprocs
|
|
2434
|
+
env.insert(0, newenv)
|
|
2435
|
+
else:
|
|
2436
|
+
env[0]["nnp"] += driver.nprocs
|
|
2437
|
+
else:
|
|
2438
|
+
# Setup a new envelope
|
|
2439
|
+
if not ("nn" in master.options and "nnp" in master.options):
|
|
2440
|
+
raise AlgoComponentError(
|
|
2441
|
+
"'nn' and 'nnp' must be defined for the master executable"
|
|
2442
|
+
)
|
|
2443
|
+
env = [
|
|
2444
|
+
dict(
|
|
2445
|
+
nn=1,
|
|
2446
|
+
nnp=master.options["nnp"] + driver.nprocs,
|
|
2447
|
+
openmp=master.options.get("openmp", 1),
|
|
2448
|
+
)
|
|
2449
|
+
]
|
|
2450
|
+
if master.options["nn"] > 1:
|
|
2451
|
+
env.append(
|
|
2452
|
+
dict(
|
|
2453
|
+
nn=master.options["nn"] - 1,
|
|
2454
|
+
nnp=master.options["nnp"],
|
|
2455
|
+
openmp=master.options.get("openmp", 1),
|
|
2456
|
+
)
|
|
2457
|
+
)
|
|
2458
|
+
if len(mpi.binaries) > 2:
|
|
2459
|
+
env.extend([b.options for b in mpi.binaries[2:]])
|
|
2460
|
+
return env
|
|
2461
|
+
|
|
2462
|
+
_MIXIN_MPIENVELOPE_POSTHOOKS = (_bootstrap_mpienvelope_openpalm_posthack,)
|