vortex-nwp 2.0.0b1__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 +135 -0
- vortex/algo/__init__.py +12 -0
- vortex/algo/components.py +2136 -0
- vortex/algo/mpitools.py +1648 -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 +170 -0
- vortex/config.py +115 -0
- vortex/data/__init__.py +13 -0
- vortex/data/abstractstores.py +1572 -0
- vortex/data/containers.py +780 -0
- vortex/data/contents.py +596 -0
- vortex/data/executables.py +284 -0
- vortex/data/flow.py +113 -0
- vortex/data/geometries.ini +2689 -0
- vortex/data/geometries.py +703 -0
- vortex/data/handlers.py +1021 -0
- vortex/data/outflow.py +67 -0
- vortex/data/providers.py +465 -0
- vortex/data/resources.py +201 -0
- vortex/data/stores.py +1271 -0
- vortex/gloves.py +282 -0
- vortex/layout/__init__.py +27 -0
- vortex/layout/appconf.py +109 -0
- vortex/layout/contexts.py +511 -0
- vortex/layout/dataflow.py +1069 -0
- vortex/layout/jobs.py +1276 -0
- vortex/layout/monitor.py +833 -0
- vortex/layout/nodes.py +1424 -0
- vortex/layout/subjobs.py +464 -0
- vortex/nwp/__init__.py +11 -0
- vortex/nwp/algo/__init__.py +12 -0
- vortex/nwp/algo/assim.py +483 -0
- vortex/nwp/algo/clim.py +920 -0
- vortex/nwp/algo/coupling.py +609 -0
- vortex/nwp/algo/eda.py +632 -0
- vortex/nwp/algo/eps.py +613 -0
- vortex/nwp/algo/forecasts.py +745 -0
- vortex/nwp/algo/fpserver.py +927 -0
- vortex/nwp/algo/ifsnaming.py +403 -0
- vortex/nwp/algo/ifsroot.py +311 -0
- vortex/nwp/algo/monitoring.py +202 -0
- vortex/nwp/algo/mpitools.py +554 -0
- vortex/nwp/algo/odbtools.py +974 -0
- vortex/nwp/algo/oopsroot.py +735 -0
- vortex/nwp/algo/oopstests.py +186 -0
- vortex/nwp/algo/request.py +579 -0
- vortex/nwp/algo/stdpost.py +1285 -0
- vortex/nwp/data/__init__.py +12 -0
- vortex/nwp/data/assim.py +392 -0
- vortex/nwp/data/boundaries.py +261 -0
- vortex/nwp/data/climfiles.py +539 -0
- vortex/nwp/data/configfiles.py +149 -0
- vortex/nwp/data/consts.py +929 -0
- vortex/nwp/data/ctpini.py +133 -0
- vortex/nwp/data/diagnostics.py +181 -0
- vortex/nwp/data/eda.py +148 -0
- vortex/nwp/data/eps.py +383 -0
- vortex/nwp/data/executables.py +1039 -0
- vortex/nwp/data/fields.py +96 -0
- vortex/nwp/data/gridfiles.py +308 -0
- vortex/nwp/data/logs.py +551 -0
- vortex/nwp/data/modelstates.py +334 -0
- vortex/nwp/data/monitoring.py +220 -0
- vortex/nwp/data/namelists.py +644 -0
- vortex/nwp/data/obs.py +748 -0
- vortex/nwp/data/oopsexec.py +72 -0
- vortex/nwp/data/providers.py +182 -0
- vortex/nwp/data/query.py +217 -0
- vortex/nwp/data/stores.py +147 -0
- vortex/nwp/data/surfex.py +338 -0
- vortex/nwp/syntax/__init__.py +9 -0
- vortex/nwp/syntax/stdattrs.py +375 -0
- vortex/nwp/tools/__init__.py +10 -0
- vortex/nwp/tools/addons.py +35 -0
- vortex/nwp/tools/agt.py +55 -0
- vortex/nwp/tools/bdap.py +48 -0
- vortex/nwp/tools/bdcp.py +38 -0
- vortex/nwp/tools/bdm.py +21 -0
- vortex/nwp/tools/bdmp.py +49 -0
- vortex/nwp/tools/conftools.py +1311 -0
- vortex/nwp/tools/drhook.py +62 -0
- vortex/nwp/tools/grib.py +268 -0
- vortex/nwp/tools/gribdiff.py +99 -0
- vortex/nwp/tools/ifstools.py +163 -0
- vortex/nwp/tools/igastuff.py +249 -0
- vortex/nwp/tools/mars.py +56 -0
- vortex/nwp/tools/odb.py +548 -0
- vortex/nwp/tools/partitioning.py +234 -0
- vortex/nwp/tools/satrad.py +56 -0
- vortex/nwp/util/__init__.py +6 -0
- vortex/nwp/util/async.py +184 -0
- vortex/nwp/util/beacon.py +40 -0
- vortex/nwp/util/diffpygram.py +359 -0
- vortex/nwp/util/ens.py +198 -0
- vortex/nwp/util/hooks.py +128 -0
- vortex/nwp/util/taskdeco.py +81 -0
- vortex/nwp/util/usepygram.py +591 -0
- vortex/nwp/util/usetnt.py +87 -0
- vortex/proxy.py +6 -0
- vortex/sessions.py +341 -0
- vortex/syntax/__init__.py +9 -0
- vortex/syntax/stdattrs.py +628 -0
- vortex/syntax/stddeco.py +176 -0
- vortex/toolbox.py +982 -0
- vortex/tools/__init__.py +11 -0
- vortex/tools/actions.py +457 -0
- vortex/tools/addons.py +297 -0
- vortex/tools/arm.py +76 -0
- vortex/tools/compression.py +322 -0
- vortex/tools/date.py +20 -0
- vortex/tools/ddhpack.py +10 -0
- vortex/tools/delayedactions.py +672 -0
- vortex/tools/env.py +513 -0
- vortex/tools/folder.py +663 -0
- vortex/tools/grib.py +559 -0
- vortex/tools/lfi.py +746 -0
- vortex/tools/listings.py +354 -0
- vortex/tools/names.py +575 -0
- vortex/tools/net.py +1790 -0
- vortex/tools/odb.py +10 -0
- vortex/tools/parallelism.py +336 -0
- vortex/tools/prestaging.py +186 -0
- vortex/tools/rawfiles.py +10 -0
- vortex/tools/schedulers.py +413 -0
- vortex/tools/services.py +871 -0
- vortex/tools/storage.py +1061 -0
- vortex/tools/surfex.py +61 -0
- vortex/tools/systems.py +3396 -0
- vortex/tools/targets.py +384 -0
- vortex/util/__init__.py +9 -0
- vortex/util/config.py +1071 -0
- vortex/util/empty.py +24 -0
- vortex/util/helpers.py +184 -0
- vortex/util/introspection.py +63 -0
- vortex/util/iosponge.py +76 -0
- vortex/util/roles.py +51 -0
- vortex/util/storefunctions.py +103 -0
- vortex/util/structs.py +26 -0
- vortex/util/worker.py +150 -0
- vortex_nwp-2.0.0b1.dist-info/LICENSE +517 -0
- vortex_nwp-2.0.0b1.dist-info/METADATA +50 -0
- vortex_nwp-2.0.0b1.dist-info/RECORD +146 -0
- vortex_nwp-2.0.0b1.dist-info/WHEEL +5 -0
- vortex_nwp-2.0.0b1.dist-info/top_level.txt +1 -0
vortex/layout/monitor.py
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines generic classes that are used to check the state of a list of
|
|
3
|
+
sections
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import queue
|
|
7
|
+
|
|
8
|
+
from collections import defaultdict, namedtuple, OrderedDict
|
|
9
|
+
from itertools import islice, compress
|
|
10
|
+
import multiprocessing
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
import traceback
|
|
15
|
+
|
|
16
|
+
from bronx.fancies import loggers
|
|
17
|
+
from bronx.patterns import observer
|
|
18
|
+
from bronx.stdtypes import date
|
|
19
|
+
|
|
20
|
+
from vortex.tools.parallelism import ParallelSilencer, ParallelResultParser
|
|
21
|
+
|
|
22
|
+
logger = loggers.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
#: No automatic export.
|
|
25
|
+
__all__ = []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
#: Class for possible states of a :class:`InputMonitorEntry` object
|
|
29
|
+
EntryStateTuple = namedtuple('EntryStateTuple',
|
|
30
|
+
['ufo', 'expected', 'available', 'failed'])
|
|
31
|
+
|
|
32
|
+
#: Predefined :class:`InputMonitorEntry` state values
|
|
33
|
+
EntrySt = EntryStateTuple(ufo='ufo', expected='expected', available='available',
|
|
34
|
+
failed='failed')
|
|
35
|
+
|
|
36
|
+
#: Class for possible states of a :class:`_Gang` object
|
|
37
|
+
GangStateTuple = namedtuple('GangStateTuple',
|
|
38
|
+
['ufo', 'collectable', 'pcollectable', 'failed'])
|
|
39
|
+
|
|
40
|
+
#: Predefined :class:`_Gang` state values
|
|
41
|
+
GangSt = GangStateTuple(ufo='undecided', collectable='collectable',
|
|
42
|
+
pcollectable='collectable_partial', failed='failed')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class LayoutMonitorError(Exception):
|
|
46
|
+
"""The default exception for this module."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _StateFull:
|
|
51
|
+
"""Defines an abstract interface: a class with a state."""
|
|
52
|
+
|
|
53
|
+
_mystates = EntrySt # The name of possible states
|
|
54
|
+
|
|
55
|
+
def __init__(self):
|
|
56
|
+
"""Initialise the state attribute and setup the observer."""
|
|
57
|
+
self._state = self._mystates.ufo
|
|
58
|
+
self._obsboard = observer.SecludedObserverBoard()
|
|
59
|
+
self._obsboard.notify_new(self, dict(state=self._state))
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def observerboard(self):
|
|
63
|
+
"""The entry's observer board."""
|
|
64
|
+
return self._obsboard
|
|
65
|
+
|
|
66
|
+
def _state_changed(self, previous, new):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
def _get_state(self):
|
|
70
|
+
return self._state
|
|
71
|
+
|
|
72
|
+
def _set_state(self, newstate):
|
|
73
|
+
if newstate != self._state:
|
|
74
|
+
previous = self._state
|
|
75
|
+
self._state = newstate
|
|
76
|
+
self._state_changed(previous, self._state)
|
|
77
|
+
self._obsboard.notify_upd(self, dict(state=self._state,
|
|
78
|
+
previous_state=previous))
|
|
79
|
+
|
|
80
|
+
state = property(_get_state, _set_state, doc="The entry's state.")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _StateFullMembersList:
|
|
84
|
+
"""Defines an abstract interface: a class with members."""
|
|
85
|
+
|
|
86
|
+
_mstates = EntrySt # The name of possible member's states
|
|
87
|
+
_mcontainer = set # The container class for the members
|
|
88
|
+
|
|
89
|
+
def __init__(self):
|
|
90
|
+
"""Initialise the members list."""
|
|
91
|
+
self._members = dict()
|
|
92
|
+
for st in self._mstates:
|
|
93
|
+
self._members[st] = self._mcontainer()
|
|
94
|
+
|
|
95
|
+
def _unregister_i(self, item):
|
|
96
|
+
item.observerboard.unregister(self)
|
|
97
|
+
return item
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def members(self):
|
|
101
|
+
"""Members classified by state."""
|
|
102
|
+
return self._members
|
|
103
|
+
|
|
104
|
+
def _itermembers(self):
|
|
105
|
+
"""
|
|
106
|
+
Iterate over all members: not safe if a given member is move from a
|
|
107
|
+
queue to another. That's why it's not public.
|
|
108
|
+
"""
|
|
109
|
+
for st in self._mstates:
|
|
110
|
+
yield from self._members[st]
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def memberslist(self):
|
|
114
|
+
"""The list of all the members."""
|
|
115
|
+
return list(self._itermembers())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class InputMonitorEntry(_StateFull):
|
|
119
|
+
|
|
120
|
+
def __init__(self, section):
|
|
121
|
+
"""An entry manipulated by a :class:`BasicInputMonitor` object.
|
|
122
|
+
|
|
123
|
+
:param vortex.layout.dataflow.Section section: The section associated
|
|
124
|
+
with this entry
|
|
125
|
+
"""
|
|
126
|
+
_StateFull.__init__(self)
|
|
127
|
+
self._nchecks = 0
|
|
128
|
+
self._section = section
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def nchecks(self):
|
|
132
|
+
"""
|
|
133
|
+
The number of checks performed for this entry before it was moved to
|
|
134
|
+
`available` or `failed`.
|
|
135
|
+
"""
|
|
136
|
+
return self._nchecks
|
|
137
|
+
|
|
138
|
+
def check_done(self):
|
|
139
|
+
"""Internal use: increments the nchecks count."""
|
|
140
|
+
self._nchecks += 1
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def section(self):
|
|
144
|
+
"""The section associated with this entry."""
|
|
145
|
+
return self._section
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class _MonitorSilencer(ParallelSilencer):
|
|
149
|
+
"""My own Silencer."""
|
|
150
|
+
|
|
151
|
+
def export_result(self, key, ts, prevstate, state):
|
|
152
|
+
"""Returns the recorded data, plus state related informations."""
|
|
153
|
+
return dict(report=super().export_result(),
|
|
154
|
+
name="Input #{!s}".format(key), key=key,
|
|
155
|
+
prevstate=prevstate, state=state, timestamp=ts)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ManualInputMonitor(_StateFullMembersList):
|
|
159
|
+
"""
|
|
160
|
+
This object looks into the *targets* list of :class:`InputMonitorEntry`
|
|
161
|
+
objects and check regularly the status of each of the enclosed sections. If
|
|
162
|
+
an expected resource is found the "get" command is issued.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
_mcontainer = OrderedDict
|
|
166
|
+
|
|
167
|
+
def __init__(self, context, targets, caching_freq=20, crawling_threshold=100,
|
|
168
|
+
mute=False):
|
|
169
|
+
"""
|
|
170
|
+
If the list of inputs is too long (see the *crawling_threshold*
|
|
171
|
+
option), not all of the inputs will be checked at once: The first
|
|
172
|
+
*crawling_threshold* inputs will always be checked and an additional
|
|
173
|
+
batch of *crawling_threshold* other inputs will be checked (in a round
|
|
174
|
+
robin manner)
|
|
175
|
+
|
|
176
|
+
If the inputs we are looking at have a *term* attribute, the input lists
|
|
177
|
+
will automatically be ordered according to the *term*.
|
|
178
|
+
|
|
179
|
+
:param vortex.layout.contexts.Context context: The object that is used
|
|
180
|
+
as a source of inputs
|
|
181
|
+
:param targets: The list of :class:`InputMonitorEntry` to look after
|
|
182
|
+
:param int caching_freq: We will update the sections statuses every N
|
|
183
|
+
seconds
|
|
184
|
+
:param int crawling_threshold: Maximum number of section statuses to
|
|
185
|
+
update at once
|
|
186
|
+
|
|
187
|
+
:warning: The state of the sections is looked up by a background process.
|
|
188
|
+
Consequently the **stop** method must always be called when the
|
|
189
|
+
processing is done (in order for the background process to terminate).
|
|
190
|
+
"""
|
|
191
|
+
_StateFullMembersList.__init__(self)
|
|
192
|
+
|
|
193
|
+
self._ctx = context
|
|
194
|
+
self._seq = context.sequence
|
|
195
|
+
self._caching_freq = caching_freq
|
|
196
|
+
self._crawling_threshold = crawling_threshold
|
|
197
|
+
self._mute = mute
|
|
198
|
+
self._inactive_since = time.time()
|
|
199
|
+
self._last_healthcheck = 0
|
|
200
|
+
|
|
201
|
+
# Control objects for multiprocessing
|
|
202
|
+
self._mpqueue = multiprocessing.Queue(maxsize=0) # No limit !
|
|
203
|
+
self._mpquit = multiprocessing.Event()
|
|
204
|
+
self._mperror = multiprocessing.Event()
|
|
205
|
+
self._mpjob = None
|
|
206
|
+
|
|
207
|
+
# Generate the first list of sections
|
|
208
|
+
toclassify = list(targets)
|
|
209
|
+
|
|
210
|
+
# Sort the list of UFOs if sensible (i.e. if all resources have a term)
|
|
211
|
+
has_term = 0
|
|
212
|
+
map_term = defaultdict(int)
|
|
213
|
+
for e in toclassify:
|
|
214
|
+
if hasattr(e.section.rh.resource, 'term'):
|
|
215
|
+
has_term += 1
|
|
216
|
+
map_term[e.section.rh.resource.term.fmthm] += 1
|
|
217
|
+
if toclassify and has_term == len(toclassify):
|
|
218
|
+
toclassify.sort(key=lambda e: e.section.rh.resource.term)
|
|
219
|
+
# Use a crawling threshold that is large enough to span a little bit
|
|
220
|
+
# more than one term.
|
|
221
|
+
self._crawling_threshold = max(self._crawling_threshold,
|
|
222
|
+
int(max(map_term.values()) * 1.25))
|
|
223
|
+
|
|
224
|
+
# Create key/value pairs
|
|
225
|
+
toclassify = [(i, e) for i, e in enumerate(toclassify)]
|
|
226
|
+
|
|
227
|
+
# Classify the input depending on there stage
|
|
228
|
+
self._map_stages = dict(expected=EntrySt.expected,
|
|
229
|
+
get=EntrySt.available)
|
|
230
|
+
while toclassify:
|
|
231
|
+
e = toclassify.pop(0)
|
|
232
|
+
self._append_entry(self._find_state(e[1], onfails=EntrySt.ufo), e)
|
|
233
|
+
|
|
234
|
+
def start(self):
|
|
235
|
+
"""Start the background updater task."""
|
|
236
|
+
self._mpjob = multiprocessing.Process(
|
|
237
|
+
name='BackgroundUpdater',
|
|
238
|
+
target=self._background_updater_job,
|
|
239
|
+
args=()
|
|
240
|
+
)
|
|
241
|
+
self._mpjob.start()
|
|
242
|
+
|
|
243
|
+
def stop(self):
|
|
244
|
+
"""Ask the background process in charge of updates to stop."""
|
|
245
|
+
# Is the process still running ?
|
|
246
|
+
if self._mpjob.is_alive():
|
|
247
|
+
# Try to stop it nicely
|
|
248
|
+
self._mpquit.set()
|
|
249
|
+
t0 = date.now()
|
|
250
|
+
self._mpjob.join(5)
|
|
251
|
+
waiting = date.now() - t0
|
|
252
|
+
logger.info('Waiting for the background process to stop took %f seconds',
|
|
253
|
+
waiting.total_seconds())
|
|
254
|
+
# Be less nice if needed...
|
|
255
|
+
if self._mpjob.is_alive():
|
|
256
|
+
logger.warning('Force termination of the background process')
|
|
257
|
+
self._mpjob.terminate()
|
|
258
|
+
time.sleep(1) # Allow some time for the process to terminate
|
|
259
|
+
# Wrap up
|
|
260
|
+
rc = not self._mperror.is_set()
|
|
261
|
+
logger.info('Server still alive ? %s', str(self._mpjob.is_alive()))
|
|
262
|
+
if not rc:
|
|
263
|
+
raise LayoutMonitorError('The background process ended badly.')
|
|
264
|
+
|
|
265
|
+
def __enter__(self):
|
|
266
|
+
self.start()
|
|
267
|
+
return self
|
|
268
|
+
|
|
269
|
+
def __exit__(self, exctype, excvalue, exctb):
|
|
270
|
+
self.stop()
|
|
271
|
+
|
|
272
|
+
def _itermembers(self):
|
|
273
|
+
"""
|
|
274
|
+
Iterate over all members: not safe if a given member is move from a
|
|
275
|
+
queue to another. That's why it's not public.
|
|
276
|
+
"""
|
|
277
|
+
for st in self._mstates:
|
|
278
|
+
yield from self._members[st].values()
|
|
279
|
+
|
|
280
|
+
def _find_state(self, e, onfails=EntrySt.failed):
|
|
281
|
+
"""Find the entry's state given the section's stage."""
|
|
282
|
+
return self._map_stages.get(e.section.stage, onfails)
|
|
283
|
+
|
|
284
|
+
def _append_entry(self, queue, e):
|
|
285
|
+
"""Add an entry into one of the processing queues."""
|
|
286
|
+
self._members[queue][e[0]] = e[1]
|
|
287
|
+
e[1].state = queue
|
|
288
|
+
|
|
289
|
+
def _key_update(self, res):
|
|
290
|
+
"""Process a result dictionary of the _background_updater method."""
|
|
291
|
+
e = self._members[res['prevstate']].pop(res['key'], None)
|
|
292
|
+
# The entry might be missing if someone mess with the _memebers dicitonary
|
|
293
|
+
if e is not None:
|
|
294
|
+
self._append_entry(res['state'], (res['key'], e))
|
|
295
|
+
self._inactive_since = res['timestamp']
|
|
296
|
+
|
|
297
|
+
def _background_updater(self):
|
|
298
|
+
"""This method loops on itself regularly to update the entry's state."""
|
|
299
|
+
|
|
300
|
+
# Initialisation
|
|
301
|
+
last_refresh = 0
|
|
302
|
+
kangaroo_idx = 0
|
|
303
|
+
|
|
304
|
+
# Stop if we are asked to or if there is nothing more to do
|
|
305
|
+
while (not self._mpquit.is_set() and
|
|
306
|
+
not (len(self._members[EntrySt.expected]) == 0 and
|
|
307
|
+
len(self._members[EntrySt.ufo]) == 0)):
|
|
308
|
+
|
|
309
|
+
# Tweak the caching_frequency
|
|
310
|
+
if (len(self._members[EntrySt.ufo]) and
|
|
311
|
+
len(self._members[EntrySt.expected]) <= self._crawling_threshold and
|
|
312
|
+
not len(self._members[EntrySt.available])):
|
|
313
|
+
# If UFO are still there and not much resources are expected,
|
|
314
|
+
# decrease the caching time
|
|
315
|
+
eff_caching_freq = max(3, self._caching_freq / 5)
|
|
316
|
+
else:
|
|
317
|
+
eff_caching_freq = self._caching_freq
|
|
318
|
+
|
|
319
|
+
curtime = time.time()
|
|
320
|
+
# Crawl into the monitored input if sensible
|
|
321
|
+
if curtime > last_refresh + eff_caching_freq:
|
|
322
|
+
|
|
323
|
+
last_refresh = curtime
|
|
324
|
+
result_stack = list()
|
|
325
|
+
|
|
326
|
+
# Crawl into the ufo list
|
|
327
|
+
# Always process the first self._crawling_threshold elements
|
|
328
|
+
for k, e in islice(self._members[EntrySt.ufo].items(),
|
|
329
|
+
self._crawling_threshold):
|
|
330
|
+
if self._mpquit.is_set(): # Are we ordered to stop ?
|
|
331
|
+
break
|
|
332
|
+
with _MonitorSilencer(self._ctx, 'inputmonitor_updater') as psi:
|
|
333
|
+
logger.info("First get on local file: %s",
|
|
334
|
+
e.section.rh.container.localpath())
|
|
335
|
+
e.section.get(incache=True, fatal=False) # Do not crash at this stage
|
|
336
|
+
res = psi.export_result(k, curtime, e.state, self._find_state(e))
|
|
337
|
+
self._mpqueue.put_nowait(res)
|
|
338
|
+
result_stack.append(res)
|
|
339
|
+
|
|
340
|
+
# What are the expected elements we will look for ?
|
|
341
|
+
# 1. The first self._crawling_threshold elements
|
|
342
|
+
exp_compress = [1, ] * min(self._crawling_threshold,
|
|
343
|
+
len(self._members[EntrySt.expected]))
|
|
344
|
+
# 2. An additional set of self._crawling_threshold rotating elements
|
|
345
|
+
for i in range(max(0, len(self._members[EntrySt.expected]) - self._crawling_threshold)):
|
|
346
|
+
kdiff = i - kangaroo_idx
|
|
347
|
+
exp_compress.append(1 if kdiff >= 0 and kdiff < self._crawling_threshold else 0)
|
|
348
|
+
|
|
349
|
+
# Crawl into the chosen items of the expected list
|
|
350
|
+
(visited, found, kangaroo_incr) = (0, 0, 0)
|
|
351
|
+
for i, (k, e) in enumerate(compress(self._members[EntrySt.expected].items(),
|
|
352
|
+
exp_compress)):
|
|
353
|
+
if self._mpquit.is_set(): # Are we ordered to stop ?
|
|
354
|
+
break
|
|
355
|
+
|
|
356
|
+
# Kangaroo check ?
|
|
357
|
+
kangaroo = i >= self._crawling_threshold
|
|
358
|
+
kangaroo_incr += int(kangaroo)
|
|
359
|
+
if kangaroo and found > self._crawling_threshold / 2:
|
|
360
|
+
# If a lot of resources were already found, avoid harassment
|
|
361
|
+
break
|
|
362
|
+
|
|
363
|
+
logger.debug("Checking local file: %s (kangaroo=%s)",
|
|
364
|
+
e.section.rh.container.localpath(), kangaroo)
|
|
365
|
+
e.check_done()
|
|
366
|
+
# Is the promise file still there or not ?
|
|
367
|
+
if e.section.rh.is_grabable():
|
|
368
|
+
visited += 1
|
|
369
|
+
with _MonitorSilencer(self._ctx, 'inputmonitor_updater') as psi:
|
|
370
|
+
if e.section.rh.is_grabable(check_exists=True):
|
|
371
|
+
logger.info("The local resource %s becomes available",
|
|
372
|
+
e.section.rh.container.localpath())
|
|
373
|
+
# This will crash in case of an error, but this should
|
|
374
|
+
# not happen since we checked the resource just above
|
|
375
|
+
e.section.get(incache=True)
|
|
376
|
+
found += 1
|
|
377
|
+
res = psi.export_result(k, curtime, e.state, self._find_state(e))
|
|
378
|
+
else:
|
|
379
|
+
logger.warning("The local resource %s has failed",
|
|
380
|
+
e.section.rh.container.localpath())
|
|
381
|
+
res = psi.export_result(k, curtime, e.state, EntrySt.failed)
|
|
382
|
+
self._mpqueue.put_nowait(res)
|
|
383
|
+
result_stack.append(res)
|
|
384
|
+
|
|
385
|
+
# Update the kangaroo index
|
|
386
|
+
kangaroo_idx = kangaroo_idx + kangaroo_incr - visited
|
|
387
|
+
if kangaroo_idx > len(self._members[EntrySt.expected]) - self._crawling_threshold - 1:
|
|
388
|
+
kangaroo_idx = 0
|
|
389
|
+
|
|
390
|
+
# Effectively update the internal _members dictionary
|
|
391
|
+
for r in result_stack:
|
|
392
|
+
self._key_update(r)
|
|
393
|
+
|
|
394
|
+
# Do frequent checks to look carefully into the _mpquit event
|
|
395
|
+
time.sleep(0.25)
|
|
396
|
+
|
|
397
|
+
def _background_updater_job(self):
|
|
398
|
+
"""Start the updater and check for uncatched exceptions."""
|
|
399
|
+
self._ctx.system.signal_intercept_on()
|
|
400
|
+
try:
|
|
401
|
+
self._background_updater()
|
|
402
|
+
except Exception:
|
|
403
|
+
(exc_type, exc_value, exc_traceback) = sys.exc_info()
|
|
404
|
+
print('Exception type: {!s}'.format(exc_type))
|
|
405
|
+
print('Exception info: {!s}'.format(exc_value))
|
|
406
|
+
print('Traceback:')
|
|
407
|
+
print("\n".join(traceback.format_tb(exc_traceback)))
|
|
408
|
+
# Alert the main process of the error
|
|
409
|
+
self._mperror.set()
|
|
410
|
+
|
|
411
|
+
def _refresh(self):
|
|
412
|
+
"""Called whenever the user asks something."""
|
|
413
|
+
# Look into the result queue
|
|
414
|
+
prp = None
|
|
415
|
+
# That's bad...
|
|
416
|
+
if self._mperror.is_set():
|
|
417
|
+
self.stop()
|
|
418
|
+
raise LayoutMonitorError('The background process ended badly.')
|
|
419
|
+
# Process all the available update messages
|
|
420
|
+
while True:
|
|
421
|
+
try:
|
|
422
|
+
r = self._mpqueue.get_nowait()
|
|
423
|
+
except queue.Empty:
|
|
424
|
+
break
|
|
425
|
+
if prp is None:
|
|
426
|
+
prp = ParallelResultParser(self._ctx)
|
|
427
|
+
if not self._mute:
|
|
428
|
+
self._ctx.system.highlight("The InputMonitor got news for: {!s}"
|
|
429
|
+
.format(r['name']))
|
|
430
|
+
prp(r)
|
|
431
|
+
print()
|
|
432
|
+
self._key_update(r)
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def all_done(self):
|
|
436
|
+
"""Are there any ufo or expected sections left ?"""
|
|
437
|
+
self._refresh()
|
|
438
|
+
return (len(self._members[EntrySt.expected]) == 0 and
|
|
439
|
+
len(self._members[EntrySt.ufo]) == 0)
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def inactive_time(self):
|
|
443
|
+
"""The time (in sec) since the last action (successful or not)."""
|
|
444
|
+
return time.time() - self._inactive_since
|
|
445
|
+
|
|
446
|
+
@property
|
|
447
|
+
def ufo(self):
|
|
448
|
+
"""The dictionary of sections in an unknown state."""
|
|
449
|
+
self._refresh()
|
|
450
|
+
return self._members[EntrySt.ufo]
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def expected(self):
|
|
454
|
+
"""The dictionary of expected sections."""
|
|
455
|
+
self._refresh()
|
|
456
|
+
return self._members[EntrySt.expected]
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def available(self):
|
|
460
|
+
"""The dictionary of sections that were successfully fetched."""
|
|
461
|
+
self._refresh()
|
|
462
|
+
return self._members[EntrySt.available]
|
|
463
|
+
|
|
464
|
+
def pop_available(self):
|
|
465
|
+
"""Pop an entry in the 'available' dictionary."""
|
|
466
|
+
return self._unregister_i(self.available.popitem(last=False)[1])
|
|
467
|
+
|
|
468
|
+
@property
|
|
469
|
+
def failed(self):
|
|
470
|
+
"""The dictionary of failed sections."""
|
|
471
|
+
self._refresh()
|
|
472
|
+
return self._members[EntrySt.failed]
|
|
473
|
+
|
|
474
|
+
def health_check(self, interval=0):
|
|
475
|
+
"""Log the monitor's state.
|
|
476
|
+
|
|
477
|
+
:param int interval: Log something at most every *interval* seconds.
|
|
478
|
+
"""
|
|
479
|
+
time_now = time.time()
|
|
480
|
+
if time_now - self._last_healthcheck > interval:
|
|
481
|
+
self._last_healthcheck = time_now
|
|
482
|
+
logger.info("Still waiting (ufo=%d, expected=%d, available=%d, failed=%d)...",
|
|
483
|
+
len(self._members[EntrySt.ufo]), len(self._members[EntrySt.expected]),
|
|
484
|
+
len(self._members[EntrySt.available]), len(self._members[EntrySt.failed]))
|
|
485
|
+
|
|
486
|
+
def is_timedout(self, timeout, exception=None):
|
|
487
|
+
"""Check if a timeout occurred.
|
|
488
|
+
|
|
489
|
+
:param int timeout: The wanted timeout in seconds.
|
|
490
|
+
:param Exception exception: The exception that will be raised if a timeout occurs.
|
|
491
|
+
"""
|
|
492
|
+
rc = False
|
|
493
|
+
self._refresh()
|
|
494
|
+
if (timeout > 0) and (self.inactive_time > timeout):
|
|
495
|
+
logger.error("The waiting loop timed out (%d seconds)", timeout)
|
|
496
|
+
logger.error("The following files are still unaccounted for: %s",
|
|
497
|
+
",".join([e.section.rh.container.localpath()
|
|
498
|
+
for e in self.expected.values()]))
|
|
499
|
+
rc = True
|
|
500
|
+
if rc and exception is not None:
|
|
501
|
+
raise exception("The waiting loop timed-out")
|
|
502
|
+
return rc
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class BasicInputMonitor(ManualInputMonitor):
|
|
506
|
+
"""
|
|
507
|
+
This object looks into the effective_inputs and checks regularly the
|
|
508
|
+
status of each section. If an expected resource is found the "get"
|
|
509
|
+
command is issued.
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
_mcontainer = OrderedDict
|
|
513
|
+
|
|
514
|
+
def __init__(self, context, role=None, kind=None,
|
|
515
|
+
caching_freq=20, crawling_threshold=100, mute=False):
|
|
516
|
+
"""
|
|
517
|
+
If the list of inputs is too long (see the *crawling_threshold*
|
|
518
|
+
option), not all of the inputs will be checked at once: The first
|
|
519
|
+
*crawling_threshold* inputs will always be checked and an additional
|
|
520
|
+
batch of *crawling_threshold* other inputs will be checked (in a round
|
|
521
|
+
robin manner)
|
|
522
|
+
|
|
523
|
+
If the inputs we are looking at have a *term* attribute, the input lists
|
|
524
|
+
will automatically be ordered according to the *term*.
|
|
525
|
+
|
|
526
|
+
:param vortex.layout.contexts.Context context: The object that is used
|
|
527
|
+
as a source of inputs
|
|
528
|
+
:param str role: The role of the sections that will be watched
|
|
529
|
+
:param str kind: The kind of the sections that will be watched (used only
|
|
530
|
+
if role is not specified)
|
|
531
|
+
:param int caching_freq: We will update the sections statuses every N
|
|
532
|
+
seconds
|
|
533
|
+
:param int crawling_threshold: Maximum number of section statuses to
|
|
534
|
+
update at once
|
|
535
|
+
|
|
536
|
+
:warning: The state of the sections is looked up by a background process.
|
|
537
|
+
Consequently the **stop** method must always be called when the
|
|
538
|
+
processing is done (in order for the background process to terminate).
|
|
539
|
+
"""
|
|
540
|
+
self._role = role
|
|
541
|
+
self._kind = kind
|
|
542
|
+
assert not (self._role is None and self._kind is None)
|
|
543
|
+
ManualInputMonitor.__init__(self, context,
|
|
544
|
+
[InputMonitorEntry(x)
|
|
545
|
+
for x in context.sequence.filtered_inputs(role=self._role,
|
|
546
|
+
kind=self._kind)],
|
|
547
|
+
caching_freq=caching_freq,
|
|
548
|
+
crawling_threshold=crawling_threshold,
|
|
549
|
+
mute=mute)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
class _Gang(observer.Observer, _StateFull, _StateFullMembersList):
|
|
553
|
+
"""
|
|
554
|
+
A Gang is a collection of :class:`InputMonitorEntry` objects or a collection
|
|
555
|
+
of :class:`_Gang` objects.
|
|
556
|
+
|
|
557
|
+
The members of the Gang are classified depending on their state. The state
|
|
558
|
+
of each of the members may change, that's why the Gang registers as an
|
|
559
|
+
observer to its members.
|
|
560
|
+
|
|
561
|
+
The state of a Gang depends on the states of its members.
|
|
562
|
+
|
|
563
|
+
:note: Since a Gang may be a collection of Gangs, a Gang is also an observee.
|
|
564
|
+
"""
|
|
565
|
+
|
|
566
|
+
_mystates = GangSt
|
|
567
|
+
|
|
568
|
+
def __init__(self):
|
|
569
|
+
"""
|
|
570
|
+
|
|
571
|
+
:parameters: None
|
|
572
|
+
"""
|
|
573
|
+
_StateFull.__init__(self)
|
|
574
|
+
_StateFullMembersList.__init__(self)
|
|
575
|
+
self._nmembers = 0
|
|
576
|
+
self.info = dict()
|
|
577
|
+
self._t_lock = threading.RLock()
|
|
578
|
+
|
|
579
|
+
@property
|
|
580
|
+
def nickname(self):
|
|
581
|
+
"""A fancy representation of the Gang's motive."""
|
|
582
|
+
if not self.info:
|
|
583
|
+
return 'Anonymous'
|
|
584
|
+
else:
|
|
585
|
+
return ", ".join(['{:s}={!s}'.format(k, v)
|
|
586
|
+
for k, v in self.info.items()])
|
|
587
|
+
|
|
588
|
+
def add_member(self, *members):
|
|
589
|
+
"""Introduce one or several members to the Gang."""
|
|
590
|
+
with self._t_lock:
|
|
591
|
+
for member in members:
|
|
592
|
+
member.observerboard.register(self)
|
|
593
|
+
self._members[member.state].add(member)
|
|
594
|
+
self._nmembers += 1
|
|
595
|
+
self._refresh_state()
|
|
596
|
+
|
|
597
|
+
def __len__(self):
|
|
598
|
+
"""The number of gang members."""
|
|
599
|
+
return self._nmembers
|
|
600
|
+
|
|
601
|
+
def updobsitem(self, item, info):
|
|
602
|
+
"""React to an observee notification."""
|
|
603
|
+
with self._t_lock:
|
|
604
|
+
observer.Observer.updobsitem(self, item, info)
|
|
605
|
+
# Move the item around
|
|
606
|
+
self._members[info['previous_state']].remove(item)
|
|
607
|
+
self._members[info['state']].add(item)
|
|
608
|
+
# Update my own state
|
|
609
|
+
self._refresh_state()
|
|
610
|
+
|
|
611
|
+
def _is_collectable(self):
|
|
612
|
+
raise NotImplementedError
|
|
613
|
+
|
|
614
|
+
def _is_pcollectable(self):
|
|
615
|
+
raise NotImplementedError
|
|
616
|
+
|
|
617
|
+
def _is_undecided(self):
|
|
618
|
+
raise NotImplementedError
|
|
619
|
+
|
|
620
|
+
def _refresh_state(self):
|
|
621
|
+
"""Update the state of the Gang."""
|
|
622
|
+
if self._is_collectable():
|
|
623
|
+
self.state = self._mystates.collectable
|
|
624
|
+
elif self._is_pcollectable():
|
|
625
|
+
self.state = self._mystates.pcollectable
|
|
626
|
+
elif self._is_undecided():
|
|
627
|
+
self.state = self._mystates.ufo
|
|
628
|
+
else:
|
|
629
|
+
self.state = self._mystates.failed
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class BasicGang(_Gang):
|
|
633
|
+
"""A Gang of :class:`InputMonitorEntry` objects.
|
|
634
|
+
|
|
635
|
+
Such a Gang may have 4 states:
|
|
636
|
+
|
|
637
|
+
* undecided: Some of the members are still expected (and the
|
|
638
|
+
*waitlimit* time is not exhausted)
|
|
639
|
+
* collectable: All the members are available
|
|
640
|
+
* collectable_partial: At least *minsize* members are available, but some
|
|
641
|
+
of the members are late (because the *waitlimit* time is exceeded) or
|
|
642
|
+
have failed.
|
|
643
|
+
* failed: There are to many failed members (given *minsize*)
|
|
644
|
+
"""
|
|
645
|
+
|
|
646
|
+
_mstates = EntrySt
|
|
647
|
+
|
|
648
|
+
def __init__(self, minsize=0, waitlimit=0):
|
|
649
|
+
"""
|
|
650
|
+
|
|
651
|
+
:param int minsize: The minimum size for this Gang to be in a
|
|
652
|
+
collectable_partial state (0 means that all the
|
|
653
|
+
members must be available)
|
|
654
|
+
:param int waitlimit: If > 0, wait no more than N sec after the first change
|
|
655
|
+
of state
|
|
656
|
+
"""
|
|
657
|
+
self.minsize = minsize
|
|
658
|
+
self.waitlimit = waitlimit
|
|
659
|
+
self._waitlimit_timer = None
|
|
660
|
+
self._firstseen = None
|
|
661
|
+
super().__init__()
|
|
662
|
+
|
|
663
|
+
def _state_changed(self, previous, new):
|
|
664
|
+
super()._state_changed(previous, new)
|
|
665
|
+
# Remove the waitlimit timer
|
|
666
|
+
if self._waitlimit_timer is not None and not self._ufo_members:
|
|
667
|
+
self._waitlimit_timer.cancel()
|
|
668
|
+
logger.debug('Waitlimit Timer thread canceled: %s (Gang: %s)',
|
|
669
|
+
self._waitlimit_timer, self.nickname)
|
|
670
|
+
self._waitlimit_timer = None
|
|
671
|
+
# Print some diagnosis data
|
|
672
|
+
if self.info and new != self._mystates.ufo:
|
|
673
|
+
msg = ("State changed from {:s} to {:s} for Gang: {:s}"
|
|
674
|
+
.format(previous, new, self.nickname))
|
|
675
|
+
if new == self._mystates.pcollectable:
|
|
676
|
+
if self._ufo_members:
|
|
677
|
+
logger.warning("%s\nSome of the Gang's members are still expected " +
|
|
678
|
+
"but the %d seconds waitlimit is exhausted.",
|
|
679
|
+
msg, self.waitlimit)
|
|
680
|
+
else:
|
|
681
|
+
logger.warning("%s\nSome of the Gang's members have failed.", msg)
|
|
682
|
+
else:
|
|
683
|
+
logger.info(msg)
|
|
684
|
+
|
|
685
|
+
def _set_waitlimit_timer(self):
|
|
686
|
+
if self.waitlimit > 0:
|
|
687
|
+
|
|
688
|
+
def _waitlimit_check():
|
|
689
|
+
with self._t_lock:
|
|
690
|
+
self._refresh_state()
|
|
691
|
+
logger.debug('Waitlimit Timer thread done: %s (Gang: %s)',
|
|
692
|
+
self._waitlimit_timer, self.nickname)
|
|
693
|
+
self._waitlimit_timer = None
|
|
694
|
+
|
|
695
|
+
self._waitlimit_timer = threading.Timer(self.waitlimit + 1,
|
|
696
|
+
_waitlimit_check)
|
|
697
|
+
self._waitlimit_timer.daemon = True
|
|
698
|
+
self._waitlimit_timer.start()
|
|
699
|
+
logger.debug('Waitlimit Timer thread started: %s (Gang: %s)',
|
|
700
|
+
self._waitlimit_timer, self.nickname)
|
|
701
|
+
|
|
702
|
+
def add_member(self, *members):
|
|
703
|
+
with self._t_lock:
|
|
704
|
+
super().add_member(*members)
|
|
705
|
+
if self._firstseen is None and any([m.state == self._mstates.available
|
|
706
|
+
for m in members]):
|
|
707
|
+
self._firstseen = time.time()
|
|
708
|
+
self._set_waitlimit_timer()
|
|
709
|
+
|
|
710
|
+
def updobsitem(self, item, info):
|
|
711
|
+
with self._t_lock:
|
|
712
|
+
super().updobsitem(item, info)
|
|
713
|
+
if (self._firstseen is None and
|
|
714
|
+
info['state'] == self._mstates.available):
|
|
715
|
+
self._firstseen = time.time()
|
|
716
|
+
self._set_waitlimit_timer()
|
|
717
|
+
|
|
718
|
+
@property
|
|
719
|
+
def _eff_minsize(self):
|
|
720
|
+
"""If minsize==0, the effective minsize will be equal to the Gang's len."""
|
|
721
|
+
return self.minsize if self.minsize > 0 else len(self)
|
|
722
|
+
|
|
723
|
+
@property
|
|
724
|
+
def _ufo_members(self):
|
|
725
|
+
"""The number of ufo members (from a Gang point of view)."""
|
|
726
|
+
return len(self._members[self._mstates.ufo]) + len(self._members[self._mstates.expected])
|
|
727
|
+
|
|
728
|
+
def _is_collectable(self):
|
|
729
|
+
return len(self._members[self._mstates.available]) == len(self)
|
|
730
|
+
|
|
731
|
+
def _is_pcollectable(self):
|
|
732
|
+
return (len(self._members[self._mstates.available]) >= self._eff_minsize and
|
|
733
|
+
(self._ufo_members == 0 or
|
|
734
|
+
(self._firstseen is not None and
|
|
735
|
+
time.time() - self._firstseen > self.waitlimit > 0)
|
|
736
|
+
)
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
def _is_undecided(self):
|
|
740
|
+
return len(self._members[self._mstates.available]) + self._ufo_members >= self._eff_minsize
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
class MetaGang(_Gang):
|
|
744
|
+
"""A Gang of :class:`_Gang` objects.
|
|
745
|
+
|
|
746
|
+
Such a Gang may have 4 states:
|
|
747
|
+
|
|
748
|
+
* undecided: Some of the members are still undecided
|
|
749
|
+
* collectable: All the members are collectable
|
|
750
|
+
* collectable_partial: Some of the member are only collectable_partial
|
|
751
|
+
and the rest are collectable
|
|
752
|
+
* failed: One of the member has failed
|
|
753
|
+
"""
|
|
754
|
+
|
|
755
|
+
_mstates = GangSt
|
|
756
|
+
|
|
757
|
+
def has_ufo(self):
|
|
758
|
+
"""Is there at least one UFO member ?"""
|
|
759
|
+
return len(self._members[self._mstates.ufo])
|
|
760
|
+
|
|
761
|
+
def has_collectable(self):
|
|
762
|
+
"""Is there at least one collectable member ?"""
|
|
763
|
+
return len(self._members[self._mstates.collectable])
|
|
764
|
+
|
|
765
|
+
def has_pcollectable(self):
|
|
766
|
+
"""Is there at least one collectable or collectable_partial member ?"""
|
|
767
|
+
return (len(self._members[self._mstates.pcollectable]) +
|
|
768
|
+
len(self._members[self._mstates.collectable]))
|
|
769
|
+
|
|
770
|
+
def pop_collectable(self):
|
|
771
|
+
"""Retrieve a collectable member."""
|
|
772
|
+
return self._unregister_i(self._members[self._mstates.collectable].pop())
|
|
773
|
+
|
|
774
|
+
def pop_pcollectable(self):
|
|
775
|
+
"""Retrieve a collectable or a collectable_partial member."""
|
|
776
|
+
if self.has_collectable():
|
|
777
|
+
return self.pop_collectable()
|
|
778
|
+
else:
|
|
779
|
+
return self._unregister_i(self._members[self._mstates.pcollectable].pop())
|
|
780
|
+
|
|
781
|
+
def consume_colectable(self):
|
|
782
|
+
"""Retriece all collectable members (as a generator)."""
|
|
783
|
+
while self.has_collectable():
|
|
784
|
+
yield self.pop_collectable()
|
|
785
|
+
|
|
786
|
+
def consume_pcolectable(self):
|
|
787
|
+
"""Retriece all collectable or collectable_partial members (as a generator)."""
|
|
788
|
+
while self.has_pcollectable():
|
|
789
|
+
yield self.pop_pcollectable()
|
|
790
|
+
|
|
791
|
+
def _is_collectable(self):
|
|
792
|
+
return len(self._members[self._mstates.collectable]) == len(self)
|
|
793
|
+
|
|
794
|
+
def _is_pcollectable(self):
|
|
795
|
+
return (len(self._members[self._mstates.collectable]) +
|
|
796
|
+
len(self._members[self._mstates.pcollectable])) == len(self)
|
|
797
|
+
|
|
798
|
+
def _is_undecided(self):
|
|
799
|
+
return len(self._members[self._mstates.failed]) == 0
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
class AutoMetaGang(MetaGang):
|
|
803
|
+
"""
|
|
804
|
+
A :class:`MetaGang` with a method that automatically populates the Gang
|
|
805
|
+
given a :class:`BasicInputMonitor` object.
|
|
806
|
+
"""
|
|
807
|
+
|
|
808
|
+
def autofill(self, bm, grouping_keys, allowmissing=0, waitlimit=0):
|
|
809
|
+
"""
|
|
810
|
+
Crawl into the *bm* :class:`BasicInputMonitor`'s entries, create
|
|
811
|
+
:class:`BasicGang` objects based on the resource's attributes listed in
|
|
812
|
+
*grouping_keys* and finally add these gangs to the current object.
|
|
813
|
+
|
|
814
|
+
:param vortex.layout.monitor.BasicInputMonitor bm: The BasicInputMonitor
|
|
815
|
+
that will be explored
|
|
816
|
+
:param list[str] grouping_keys: The attributes that are used to discriminate the gangs
|
|
817
|
+
:param int allowmissing: The number of missing members allowed for a gang
|
|
818
|
+
(It will be used to initialise the member gangs *minsize* attribute)
|
|
819
|
+
:param int waitlimit: The *waitlimit* attribute of the member gangs
|
|
820
|
+
"""
|
|
821
|
+
# Initialise the gangs
|
|
822
|
+
mdict = defaultdict(list)
|
|
823
|
+
for entry in bm.memberslist:
|
|
824
|
+
entryid = tuple([entry.section.rh.wide_key_lookup(key)
|
|
825
|
+
for key in grouping_keys])
|
|
826
|
+
mdict[entryid].append(entry)
|
|
827
|
+
# Finalise the Gangs setup and use them...
|
|
828
|
+
for entryid, members in mdict.items():
|
|
829
|
+
gang = BasicGang(waitlimit=waitlimit,
|
|
830
|
+
minsize=len(members) - allowmissing)
|
|
831
|
+
gang.add_member(* members)
|
|
832
|
+
gang.info = {k: v for k, v in zip(grouping_keys, entryid)}
|
|
833
|
+
self.add_member(gang)
|