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