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.
Files changed (146) hide show
  1. vortex/__init__.py +135 -0
  2. vortex/algo/__init__.py +12 -0
  3. vortex/algo/components.py +2136 -0
  4. vortex/algo/mpitools.py +1648 -0
  5. vortex/algo/mpitools_templates/envelope_wrapper_default.tpl +27 -0
  6. vortex/algo/mpitools_templates/envelope_wrapper_mpiauto.tpl +29 -0
  7. vortex/algo/mpitools_templates/wrapstd_wrapper_default.tpl +18 -0
  8. vortex/algo/serversynctools.py +170 -0
  9. vortex/config.py +115 -0
  10. vortex/data/__init__.py +13 -0
  11. vortex/data/abstractstores.py +1572 -0
  12. vortex/data/containers.py +780 -0
  13. vortex/data/contents.py +596 -0
  14. vortex/data/executables.py +284 -0
  15. vortex/data/flow.py +113 -0
  16. vortex/data/geometries.ini +2689 -0
  17. vortex/data/geometries.py +703 -0
  18. vortex/data/handlers.py +1021 -0
  19. vortex/data/outflow.py +67 -0
  20. vortex/data/providers.py +465 -0
  21. vortex/data/resources.py +201 -0
  22. vortex/data/stores.py +1271 -0
  23. vortex/gloves.py +282 -0
  24. vortex/layout/__init__.py +27 -0
  25. vortex/layout/appconf.py +109 -0
  26. vortex/layout/contexts.py +511 -0
  27. vortex/layout/dataflow.py +1069 -0
  28. vortex/layout/jobs.py +1276 -0
  29. vortex/layout/monitor.py +833 -0
  30. vortex/layout/nodes.py +1424 -0
  31. vortex/layout/subjobs.py +464 -0
  32. vortex/nwp/__init__.py +11 -0
  33. vortex/nwp/algo/__init__.py +12 -0
  34. vortex/nwp/algo/assim.py +483 -0
  35. vortex/nwp/algo/clim.py +920 -0
  36. vortex/nwp/algo/coupling.py +609 -0
  37. vortex/nwp/algo/eda.py +632 -0
  38. vortex/nwp/algo/eps.py +613 -0
  39. vortex/nwp/algo/forecasts.py +745 -0
  40. vortex/nwp/algo/fpserver.py +927 -0
  41. vortex/nwp/algo/ifsnaming.py +403 -0
  42. vortex/nwp/algo/ifsroot.py +311 -0
  43. vortex/nwp/algo/monitoring.py +202 -0
  44. vortex/nwp/algo/mpitools.py +554 -0
  45. vortex/nwp/algo/odbtools.py +974 -0
  46. vortex/nwp/algo/oopsroot.py +735 -0
  47. vortex/nwp/algo/oopstests.py +186 -0
  48. vortex/nwp/algo/request.py +579 -0
  49. vortex/nwp/algo/stdpost.py +1285 -0
  50. vortex/nwp/data/__init__.py +12 -0
  51. vortex/nwp/data/assim.py +392 -0
  52. vortex/nwp/data/boundaries.py +261 -0
  53. vortex/nwp/data/climfiles.py +539 -0
  54. vortex/nwp/data/configfiles.py +149 -0
  55. vortex/nwp/data/consts.py +929 -0
  56. vortex/nwp/data/ctpini.py +133 -0
  57. vortex/nwp/data/diagnostics.py +181 -0
  58. vortex/nwp/data/eda.py +148 -0
  59. vortex/nwp/data/eps.py +383 -0
  60. vortex/nwp/data/executables.py +1039 -0
  61. vortex/nwp/data/fields.py +96 -0
  62. vortex/nwp/data/gridfiles.py +308 -0
  63. vortex/nwp/data/logs.py +551 -0
  64. vortex/nwp/data/modelstates.py +334 -0
  65. vortex/nwp/data/monitoring.py +220 -0
  66. vortex/nwp/data/namelists.py +644 -0
  67. vortex/nwp/data/obs.py +748 -0
  68. vortex/nwp/data/oopsexec.py +72 -0
  69. vortex/nwp/data/providers.py +182 -0
  70. vortex/nwp/data/query.py +217 -0
  71. vortex/nwp/data/stores.py +147 -0
  72. vortex/nwp/data/surfex.py +338 -0
  73. vortex/nwp/syntax/__init__.py +9 -0
  74. vortex/nwp/syntax/stdattrs.py +375 -0
  75. vortex/nwp/tools/__init__.py +10 -0
  76. vortex/nwp/tools/addons.py +35 -0
  77. vortex/nwp/tools/agt.py +55 -0
  78. vortex/nwp/tools/bdap.py +48 -0
  79. vortex/nwp/tools/bdcp.py +38 -0
  80. vortex/nwp/tools/bdm.py +21 -0
  81. vortex/nwp/tools/bdmp.py +49 -0
  82. vortex/nwp/tools/conftools.py +1311 -0
  83. vortex/nwp/tools/drhook.py +62 -0
  84. vortex/nwp/tools/grib.py +268 -0
  85. vortex/nwp/tools/gribdiff.py +99 -0
  86. vortex/nwp/tools/ifstools.py +163 -0
  87. vortex/nwp/tools/igastuff.py +249 -0
  88. vortex/nwp/tools/mars.py +56 -0
  89. vortex/nwp/tools/odb.py +548 -0
  90. vortex/nwp/tools/partitioning.py +234 -0
  91. vortex/nwp/tools/satrad.py +56 -0
  92. vortex/nwp/util/__init__.py +6 -0
  93. vortex/nwp/util/async.py +184 -0
  94. vortex/nwp/util/beacon.py +40 -0
  95. vortex/nwp/util/diffpygram.py +359 -0
  96. vortex/nwp/util/ens.py +198 -0
  97. vortex/nwp/util/hooks.py +128 -0
  98. vortex/nwp/util/taskdeco.py +81 -0
  99. vortex/nwp/util/usepygram.py +591 -0
  100. vortex/nwp/util/usetnt.py +87 -0
  101. vortex/proxy.py +6 -0
  102. vortex/sessions.py +341 -0
  103. vortex/syntax/__init__.py +9 -0
  104. vortex/syntax/stdattrs.py +628 -0
  105. vortex/syntax/stddeco.py +176 -0
  106. vortex/toolbox.py +982 -0
  107. vortex/tools/__init__.py +11 -0
  108. vortex/tools/actions.py +457 -0
  109. vortex/tools/addons.py +297 -0
  110. vortex/tools/arm.py +76 -0
  111. vortex/tools/compression.py +322 -0
  112. vortex/tools/date.py +20 -0
  113. vortex/tools/ddhpack.py +10 -0
  114. vortex/tools/delayedactions.py +672 -0
  115. vortex/tools/env.py +513 -0
  116. vortex/tools/folder.py +663 -0
  117. vortex/tools/grib.py +559 -0
  118. vortex/tools/lfi.py +746 -0
  119. vortex/tools/listings.py +354 -0
  120. vortex/tools/names.py +575 -0
  121. vortex/tools/net.py +1790 -0
  122. vortex/tools/odb.py +10 -0
  123. vortex/tools/parallelism.py +336 -0
  124. vortex/tools/prestaging.py +186 -0
  125. vortex/tools/rawfiles.py +10 -0
  126. vortex/tools/schedulers.py +413 -0
  127. vortex/tools/services.py +871 -0
  128. vortex/tools/storage.py +1061 -0
  129. vortex/tools/surfex.py +61 -0
  130. vortex/tools/systems.py +3396 -0
  131. vortex/tools/targets.py +384 -0
  132. vortex/util/__init__.py +9 -0
  133. vortex/util/config.py +1071 -0
  134. vortex/util/empty.py +24 -0
  135. vortex/util/helpers.py +184 -0
  136. vortex/util/introspection.py +63 -0
  137. vortex/util/iosponge.py +76 -0
  138. vortex/util/roles.py +51 -0
  139. vortex/util/storefunctions.py +103 -0
  140. vortex/util/structs.py +26 -0
  141. vortex/util/worker.py +150 -0
  142. vortex_nwp-2.0.0b1.dist-info/LICENSE +517 -0
  143. vortex_nwp-2.0.0b1.dist-info/METADATA +50 -0
  144. vortex_nwp-2.0.0b1.dist-info/RECORD +146 -0
  145. vortex_nwp-2.0.0b1.dist-info/WHEEL +5 -0
  146. vortex_nwp-2.0.0b1.dist-info/top_level.txt +1 -0
@@ -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)