Coverage for tests/test_executors.py: 30%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of ctrl_mpexec.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22"""Simple unit test for cmdLineFwk module.
23"""
25import logging
26import sys
27import time
28import unittest
29import warnings
30from multiprocessing import Manager
32import networkx as nx
33import psutil
34from lsst.ctrl.mpexec import MPGraphExecutor, MPGraphExecutorError, MPTimeoutError, QuantumExecutor
35from lsst.ctrl.mpexec.execFixupDataId import ExecFixupDataId
36from lsst.pipe.base import NodeId
38logging.basicConfig(level=logging.DEBUG)
40_LOG = logging.getLogger(__name__)
43class QuantumExecutorMock(QuantumExecutor):
44 """Mock class for QuantumExecutor"""
46 def __init__(self, mp=False):
47 self.quanta = []
48 if mp:
49 # in multiprocess mode use shared list
50 manager = Manager()
51 self.quanta = manager.list()
53 def execute(self, taskDef, quantum, butler):
54 _LOG.debug("QuantumExecutorMock.execute: taskDef=%s dataId=%s", taskDef, quantum.dataId)
55 if taskDef.taskClass:
56 # only works for TaskMockMP class below
57 taskDef.taskClass().runQuantum()
58 self.quanta.append(quantum)
59 return quantum
61 def getDataIds(self, field):
62 """Returns values for dataId field for each visited quanta"""
63 return [quantum.dataId[field] for quantum in self.quanta]
66class QuantumMock:
67 def __init__(self, dataId):
68 self.dataId = dataId
70 def __eq__(self, other):
71 return self.dataId == other.dataId
73 def __hash__(self):
74 # dict.__eq__ is order-insensitive
75 return hash(tuple(sorted(kv for kv in self.dataId.items())))
78class QuantumIterDataMock:
79 """Simple class to mock QuantumIterData."""
81 def __init__(self, index, taskDef, **dataId):
82 self.index = index
83 self.taskDef = taskDef
84 self.quantum = QuantumMock(dataId)
85 self.dependencies = set()
86 self.nodeId = NodeId(index, "DummyBuildString")
89class QuantumGraphMock:
90 """Mock for quantum graph."""
92 def __init__(self, qdata):
93 self._graph = nx.DiGraph()
94 previous = qdata[0]
95 for node in qdata[1:]:
96 self._graph.add_edge(previous, node)
97 previous = node
99 def __iter__(self):
100 yield from nx.topological_sort(self._graph)
102 def __len__(self):
103 return len(self._graph)
105 def findTaskDefByLabel(self, label):
106 for q in self:
107 if q.taskDef.label == label:
108 return q.taskDef
110 def getQuantaForTask(self, taskDef):
111 nodes = self.getNodesForTask(taskDef)
112 return {q.quantum for q in nodes}
114 def getNodesForTask(self, taskDef):
115 quanta = set()
116 for q in self:
117 if q.taskDef == taskDef:
118 quanta.add(q)
119 return quanta
121 @property
122 def graph(self):
123 return self._graph
125 def findCycle(self):
126 return []
128 def determineInputsToQuantumNode(self, node):
129 result = set()
130 for n in node.dependencies:
131 for otherNode in self:
132 if otherNode.index == n:
133 result.add(otherNode)
134 return result
137class TaskMockMP:
138 """Simple mock class for task supporting multiprocessing."""
140 canMultiprocess = True
142 def runQuantum(self):
143 _LOG.debug("TaskMockMP.runQuantum")
144 pass
147class TaskMockFail:
148 """Simple mock class for task which fails."""
150 canMultiprocess = True
152 def runQuantum(self):
153 _LOG.debug("TaskMockFail.runQuantum")
154 raise ValueError("expected failure")
157class TaskMockSleep:
158 """Simple mock class for task which "runs" for some time."""
160 canMultiprocess = True
162 def runQuantum(self):
163 _LOG.debug("TaskMockSleep.runQuantum")
164 time.sleep(5.0)
167class TaskMockLongSleep:
168 """Simple mock class for task which "runs" for very long time."""
170 canMultiprocess = True
172 def runQuantum(self):
173 _LOG.debug("TaskMockLongSleep.runQuantum")
174 time.sleep(100.0)
177class TaskMockNoMP:
178 """Simple mock class for task not supporting multiprocessing."""
180 canMultiprocess = False
183class TaskDefMock:
184 """Simple mock class for task definition in a pipeline."""
186 def __init__(self, taskName="Task", config=None, taskClass=TaskMockMP, label="task1"):
187 self.taskName = taskName
188 self.config = config
189 self.taskClass = taskClass
190 self.label = label
192 def __str__(self):
193 return f"TaskDefMock(taskName={self.taskName}, taskClass={self.taskClass.__name__})"
196class MPGraphExecutorTestCase(unittest.TestCase):
197 """A test case for MPGraphExecutor class"""
199 def test_mpexec_nomp(self):
200 """Make simple graph and execute"""
202 taskDef = TaskDefMock()
203 qgraph = QuantumGraphMock(
204 [QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)]
205 )
207 # run in single-process mode
208 qexec = QuantumExecutorMock()
209 mpexec = MPGraphExecutor(numProc=1, timeout=100, quantumExecutor=qexec)
210 mpexec.execute(qgraph, butler=None)
211 self.assertEqual(qexec.getDataIds("detector"), [0, 1, 2])
213 def test_mpexec_mp(self):
214 """Make simple graph and execute"""
216 taskDef = TaskDefMock()
217 qgraph = QuantumGraphMock(
218 [QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)]
219 )
221 methods = ["spawn"]
222 if sys.platform == "linux":
223 methods.append("fork")
224 methods.append("forkserver")
226 for method in methods:
227 with self.subTest(startMethod=method):
228 # Run in multi-process mode, the order of results is not
229 # defined.
230 qexec = QuantumExecutorMock(mp=True)
231 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec, startMethod=method)
232 mpexec.execute(qgraph, butler=None)
233 self.assertCountEqual(qexec.getDataIds("detector"), [0, 1, 2])
235 def test_mpexec_nompsupport(self):
236 """Try to run MP for task that has no MP support which should fail"""
238 taskDef = TaskDefMock(taskClass=TaskMockNoMP)
239 qgraph = QuantumGraphMock(
240 [QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)]
241 )
243 # run in multi-process mode
244 qexec = QuantumExecutorMock()
245 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec)
246 with self.assertRaises(MPGraphExecutorError):
247 mpexec.execute(qgraph, butler=None)
249 def test_mpexec_fixup(self):
250 """Make simple graph and execute, add dependencies by executing fixup
251 code.
252 """
254 taskDef = TaskDefMock()
256 for reverse in (False, True):
257 qgraph = QuantumGraphMock(
258 [QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)]
259 )
261 qexec = QuantumExecutorMock()
262 fixup = ExecFixupDataId("task1", "detector", reverse=reverse)
263 mpexec = MPGraphExecutor(numProc=1, timeout=100, quantumExecutor=qexec, executionGraphFixup=fixup)
264 mpexec.execute(qgraph, butler=None)
266 expected = [0, 1, 2]
267 if reverse:
268 expected = list(reversed(expected))
269 self.assertEqual(qexec.getDataIds("detector"), expected)
271 def test_mpexec_timeout(self):
272 """Fail due to timeout"""
274 taskDef = TaskDefMock()
275 taskDefSleep = TaskDefMock(taskClass=TaskMockSleep)
276 qgraph = QuantumGraphMock(
277 [
278 QuantumIterDataMock(index=0, taskDef=taskDef, detector=0),
279 QuantumIterDataMock(index=1, taskDef=taskDefSleep, detector=1),
280 QuantumIterDataMock(index=2, taskDef=taskDef, detector=2),
281 ]
282 )
284 # with failFast we'll get immediate MPTimeoutError
285 qexec = QuantumExecutorMock(mp=True)
286 mpexec = MPGraphExecutor(numProc=3, timeout=1, quantumExecutor=qexec, failFast=True)
287 with self.assertRaises(MPTimeoutError):
288 mpexec.execute(qgraph, butler=None)
290 # with failFast=False exception happens after last task finishes
291 qexec = QuantumExecutorMock(mp=True)
292 mpexec = MPGraphExecutor(numProc=3, timeout=3, quantumExecutor=qexec, failFast=False)
293 with self.assertRaises(MPTimeoutError):
294 mpexec.execute(qgraph, butler=None)
295 # We expect two tasks (0 and 2) to finish successfully and one task to
296 # timeout. Unfortunately on busy CPU there is no guarantee that tasks
297 # finish on time, so expect more timeouts and issue a warning.
298 detectorIds = set(qexec.getDataIds("detector"))
299 self.assertLess(len(detectorIds), 3)
300 if detectorIds != {0, 2}:
301 warnings.warn(f"Possibly timed out tasks, expected [0, 2], received {detectorIds}")
303 def test_mpexec_failure(self):
304 """Failure in one task should not stop other tasks"""
306 taskDef = TaskDefMock()
307 taskDefFail = TaskDefMock(taskClass=TaskMockFail)
308 qgraph = QuantumGraphMock(
309 [
310 QuantumIterDataMock(index=0, taskDef=taskDef, detector=0),
311 QuantumIterDataMock(index=1, taskDef=taskDefFail, detector=1),
312 QuantumIterDataMock(index=2, taskDef=taskDef, detector=2),
313 ]
314 )
316 qexec = QuantumExecutorMock(mp=True)
317 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec)
318 with self.assertRaises(MPGraphExecutorError):
319 mpexec.execute(qgraph, butler=None)
320 self.assertCountEqual(qexec.getDataIds("detector"), [0, 2])
322 def test_mpexec_failure_dep(self):
323 """Failure in one task should skip dependents"""
325 taskDef = TaskDefMock()
326 taskDefFail = TaskDefMock(taskClass=TaskMockFail)
327 qdata = [
328 QuantumIterDataMock(index=0, taskDef=taskDef, detector=0),
329 QuantumIterDataMock(index=1, taskDef=taskDefFail, detector=1),
330 QuantumIterDataMock(index=2, taskDef=taskDef, detector=2),
331 QuantumIterDataMock(index=3, taskDef=taskDef, detector=3),
332 QuantumIterDataMock(index=4, taskDef=taskDef, detector=4),
333 ]
334 qdata[2].dependencies.add(1)
335 qdata[4].dependencies.add(3)
336 qdata[4].dependencies.add(2)
338 qgraph = QuantumGraphMock(qdata)
340 qexec = QuantumExecutorMock(mp=True)
341 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec)
342 with self.assertRaises(MPGraphExecutorError):
343 mpexec.execute(qgraph, butler=None)
344 self.assertCountEqual(qexec.getDataIds("detector"), [0, 3])
346 def test_mpexec_failure_failfast(self):
347 """Fast fail stops quickly.
349 Timing delay of task #3 should be sufficient to process
350 failure and raise exception.
351 """
353 taskDef = TaskDefMock()
354 taskDefFail = TaskDefMock(taskClass=TaskMockFail)
355 taskDefLongSleep = TaskDefMock(taskClass=TaskMockLongSleep)
356 qdata = [
357 QuantumIterDataMock(index=0, taskDef=taskDef, detector=0),
358 QuantumIterDataMock(index=1, taskDef=taskDefFail, detector=1),
359 QuantumIterDataMock(index=2, taskDef=taskDef, detector=2),
360 QuantumIterDataMock(index=3, taskDef=taskDefLongSleep, detector=3),
361 QuantumIterDataMock(index=4, taskDef=taskDef, detector=4),
362 ]
363 qdata[1].dependencies.add(0)
364 qdata[2].dependencies.add(1)
365 qdata[4].dependencies.add(3)
366 qdata[4].dependencies.add(2)
368 qgraph = QuantumGraphMock(qdata)
370 qexec = QuantumExecutorMock(mp=True)
371 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec, failFast=True)
372 with self.assertRaises(MPGraphExecutorError):
373 mpexec.execute(qgraph, butler=None)
374 self.assertCountEqual(qexec.getDataIds("detector"), [0])
376 def test_mpexec_num_fd(self):
377 """Check that number of open files stays reasonable"""
379 taskDef = TaskDefMock()
380 qgraph = QuantumGraphMock(
381 [QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(20)]
382 )
384 this_proc = psutil.Process()
385 num_fds_0 = this_proc.num_fds()
387 # run in multi-process mode, the order of results is not defined
388 qexec = QuantumExecutorMock(mp=True)
389 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec)
390 mpexec.execute(qgraph, butler=None)
392 num_fds_1 = this_proc.num_fds()
393 # They should be the same but allow small growth just in case.
394 # Without DM-26728 fix the difference would be equal to number of
395 # quanta (20).
396 self.assertLess(num_fds_1 - num_fds_0, 5)
399if __name__ == "__main__": 399 ↛ 400line 399 didn't jump to line 400, because the condition on line 399 was never true
400 unittest.main()