Coverage for tests/test_executors.py : 24%

Hot-keys 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 networkx as nx
27from multiprocessing import Manager
28import psutil
29import time
30from types import SimpleNamespace
31import unittest
32import warnings
34from lsst.ctrl.mpexec import MPGraphExecutor, MPGraphExecutorError, MPTimeoutError, QuantumExecutor
35from lsst.ctrl.mpexec.execFixupDataId import ExecFixupDataId
36from lsst.pipe.base import NodeId
39logging.basicConfig(level=logging.DEBUG)
41_LOG = logging.getLogger(__name__)
44class QuantumExecutorMock(QuantumExecutor):
45 """Mock class for QuantumExecutor
46 """
47 def __init__(self, mp=False):
48 self.quanta = []
49 if mp:
50 # in multiprocess mode use shared list
51 manager = Manager()
52 self.quanta = manager.list()
54 def execute(self, taskDef, quantum, butler):
55 _LOG.debug("QuantumExecutorMock.execute: taskDef=%s dataId=%s", taskDef, quantum.dataId)
56 if taskDef.taskClass:
57 # only works for TaskMockMP class below
58 taskDef.taskClass().runQuantum()
59 self.quanta.append(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 QuantumIterDataMock:
67 """Simple class to mock QuantumIterData.
68 """
69 def __init__(self, index, taskDef, **dataId):
70 self.index = index
71 self.taskDef = taskDef
72 self.quantum = SimpleNamespace(dataId=dataId)
73 self.dependencies = set()
74 self.nodeId = NodeId(index, "DummyBuildString")
77class QuantumGraphMock:
78 """Mock for quantum graph.
79 """
80 def __init__(self, qdata):
81 self._graph = nx.DiGraph()
82 previous = qdata[0]
83 for node in qdata[1:]:
84 self._graph.add_edge(previous, node)
85 previous = node
87 def __iter__(self):
88 yield from nx.topological_sort(self._graph)
90 def findTaskDefByLabel(self, label):
91 for q in self:
92 if q.taskDef.label == label:
93 return q.taskDef
95 def quantaForTask(self, taskDef):
96 quanta = set()
97 for q in self:
98 if q.taskDef == taskDef:
99 quanta.add(q)
100 return quanta
102 @property
103 def graph(self):
104 return self._graph
106 def findCycle(self):
107 return []
109 def determineInputsToQuantumNode(self, node):
110 result = set()
111 for n in node.dependencies:
112 for otherNode in self:
113 if otherNode.index == n:
114 result.add(otherNode)
115 return result
118class TaskMockMP:
119 """Simple mock class for task supporting multiprocessing.
120 """
121 canMultiprocess = True
123 def runQuantum(self):
124 _LOG.debug("TaskMockMP.runQuantum")
125 pass
128class TaskMockFail:
129 """Simple mock class for task which fails.
130 """
131 canMultiprocess = True
133 def runQuantum(self):
134 _LOG.debug("TaskMockFail.runQuantum")
135 raise ValueError("expected failure")
138class TaskMockSleep:
139 """Simple mock class for task which "runs" for some time.
140 """
141 canMultiprocess = True
143 def runQuantum(self):
144 _LOG.debug("TaskMockSleep.runQuantum")
145 time.sleep(5.)
148class TaskMockLongSleep:
149 """Simple mock class for task which "runs" for very long time.
150 """
151 canMultiprocess = True
153 def runQuantum(self):
154 _LOG.debug("TaskMockLongSleep.runQuantum")
155 time.sleep(100.)
158class TaskMockNoMP:
159 """Simple mock class for task not supporting multiprocessing.
160 """
161 canMultiprocess = False
164class TaskDefMock:
165 """Simple mock class for task definition in a pipeline.
166 """
167 def __init__(self, taskName="Task", config=None, taskClass=TaskMockMP, label="task1"):
168 self.taskName = taskName
169 self.config = config
170 self.taskClass = taskClass
171 self.label = label
173 def __str__(self):
174 return f"TaskDefMock(taskName={self.taskName}, taskClass={self.taskClass.__name__})"
177class MPGraphExecutorTestCase(unittest.TestCase):
178 """A test case for MPGraphExecutor class
179 """
181 def test_mpexec_nomp(self):
182 """Make simple graph and execute"""
184 taskDef = TaskDefMock()
185 qgraph = QuantumGraphMock([
186 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)
187 ])
189 # run in single-process mode
190 qexec = QuantumExecutorMock()
191 mpexec = MPGraphExecutor(numProc=1, timeout=100, quantumExecutor=qexec)
192 mpexec.execute(qgraph, butler=None)
193 self.assertEqual(qexec.getDataIds("detector"), [0, 1, 2])
195 def test_mpexec_mp(self):
196 """Make simple graph and execute"""
198 taskDef = TaskDefMock()
199 qgraph = QuantumGraphMock([
200 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)
201 ])
203 # run in multi-process mode, the order of results is not defined
204 qexec = QuantumExecutorMock(mp=True)
205 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec)
206 mpexec.execute(qgraph, butler=None)
207 self.assertCountEqual(qexec.getDataIds("detector"), [0, 1, 2])
209 def test_mpexec_nompsupport(self):
210 """Try to run MP for task that has no MP support which should fail
211 """
213 taskDef = TaskDefMock(taskClass=TaskMockNoMP)
214 qgraph = QuantumGraphMock([
215 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)
216 ])
218 # run in multi-process mode
219 qexec = QuantumExecutorMock()
220 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec)
221 with self.assertRaises(MPGraphExecutorError):
222 mpexec.execute(qgraph, butler=None)
224 def test_mpexec_fixup(self):
225 """Make simple graph and execute, add dependencies by executing fixup code
226 """
228 taskDef = TaskDefMock()
230 for reverse in (False, True):
231 qgraph = QuantumGraphMock([
232 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)
233 ])
235 qexec = QuantumExecutorMock()
236 fixup = ExecFixupDataId("task1", "detector", reverse=reverse)
237 mpexec = MPGraphExecutor(numProc=1, timeout=100, quantumExecutor=qexec,
238 executionGraphFixup=fixup)
239 mpexec.execute(qgraph, butler=None)
241 expected = [0, 1, 2]
242 if reverse:
243 expected = list(reversed(expected))
244 self.assertEqual(qexec.getDataIds("detector"), expected)
246 def test_mpexec_timeout(self):
247 """Fail due to timeout"""
249 taskDef = TaskDefMock()
250 taskDefSleep = TaskDefMock(taskClass=TaskMockSleep)
251 qgraph = QuantumGraphMock([
252 QuantumIterDataMock(index=0, taskDef=taskDef, detector=0),
253 QuantumIterDataMock(index=1, taskDef=taskDefSleep, detector=1),
254 QuantumIterDataMock(index=2, taskDef=taskDef, detector=2),
255 ])
257 # with failFast we'll get immediate MPTimeoutError
258 qexec = QuantumExecutorMock(mp=True)
259 mpexec = MPGraphExecutor(numProc=3, timeout=1, quantumExecutor=qexec, failFast=True)
260 with self.assertRaises(MPTimeoutError):
261 mpexec.execute(qgraph, butler=None)
263 # with failFast=False exception happens after last task finishes
264 qexec = QuantumExecutorMock(mp=True)
265 mpexec = MPGraphExecutor(numProc=3, timeout=3, quantumExecutor=qexec, failFast=False)
266 with self.assertRaises(MPTimeoutError):
267 mpexec.execute(qgraph, butler=None)
268 # We expect two tasks (0 and 2) to finish successfully and one task to
269 # timeout. Unfortunately on busy CPU there is no guarantee that tasks
270 # finish on time, so expect more timeouts and issue a warning.
271 detectorIds = set(qexec.getDataIds("detector"))
272 self.assertLess(len(detectorIds), 3)
273 if detectorIds != {0, 2}:
274 warnings.warn(f"Possibly timed out tasks, expected [0, 2], received {detectorIds}")
276 def test_mpexec_failure(self):
277 """Failure in one task should not stop other tasks"""
279 taskDef = TaskDefMock()
280 taskDefFail = TaskDefMock(taskClass=TaskMockFail)
281 qgraph = QuantumGraphMock([
282 QuantumIterDataMock(index=0, taskDef=taskDef, detector=0),
283 QuantumIterDataMock(index=1, taskDef=taskDefFail, detector=1),
284 QuantumIterDataMock(index=2, taskDef=taskDef, detector=2),
285 ])
287 qexec = QuantumExecutorMock(mp=True)
288 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec)
289 with self.assertRaises(MPGraphExecutorError):
290 mpexec.execute(qgraph, butler=None)
291 self.assertCountEqual(qexec.getDataIds("detector"), [0, 2])
293 def test_mpexec_failure_dep(self):
294 """Failure in one task should skip dependents"""
296 taskDef = TaskDefMock()
297 taskDefFail = TaskDefMock(taskClass=TaskMockFail)
298 qdata = [
299 QuantumIterDataMock(index=0, taskDef=taskDef, detector=0),
300 QuantumIterDataMock(index=1, taskDef=taskDefFail, detector=1),
301 QuantumIterDataMock(index=2, taskDef=taskDef, detector=2),
302 QuantumIterDataMock(index=3, taskDef=taskDef, detector=3),
303 QuantumIterDataMock(index=4, taskDef=taskDef, detector=4),
304 ]
305 qdata[2].dependencies.add(1)
306 qdata[4].dependencies.add(3)
307 qdata[4].dependencies.add(2)
309 qgraph = QuantumGraphMock(qdata)
311 qexec = QuantumExecutorMock(mp=True)
312 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec)
313 with self.assertRaises(MPGraphExecutorError):
314 mpexec.execute(qgraph, butler=None)
315 self.assertCountEqual(qexec.getDataIds("detector"), [0, 3])
317 def test_mpexec_failure_failfast(self):
318 """Fast fail stops quickly.
320 Timing delay of task #3 should be sufficient to process
321 failure and raise exception.
322 """
324 taskDef = TaskDefMock()
325 taskDefFail = TaskDefMock(taskClass=TaskMockFail)
326 taskDefLongSleep = TaskDefMock(taskClass=TaskMockLongSleep)
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=taskDefLongSleep, detector=3),
332 QuantumIterDataMock(index=4, taskDef=taskDef, detector=4),
333 ]
334 qdata[1].dependencies.add(0)
335 qdata[2].dependencies.add(1)
336 qdata[4].dependencies.add(3)
337 qdata[4].dependencies.add(2)
339 qgraph = QuantumGraphMock(qdata)
341 qexec = QuantumExecutorMock(mp=True)
342 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec, failFast=True)
343 with self.assertRaises(MPGraphExecutorError):
344 mpexec.execute(qgraph, butler=None)
345 self.assertCountEqual(qexec.getDataIds("detector"), [0])
347 def test_mpexec_num_fd(self):
348 """Check that number of open files stays reasonable
349 """
351 taskDef = TaskDefMock()
352 qgraph = QuantumGraphMock([
353 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(20)
354 ])
356 this_proc = psutil.Process()
357 num_fds_0 = this_proc.num_fds()
359 # run in multi-process mode, the order of results is not defined
360 qexec = QuantumExecutorMock(mp=True)
361 mpexec = MPGraphExecutor(numProc=3, timeout=100, quantumExecutor=qexec)
362 mpexec.execute(qgraph, butler=None)
364 num_fds_1 = this_proc.num_fds()
365 # They should be the same but allow small growth just in case.
366 # Without DM-26728 fix the difference would be equal to number of
367 # quanta (20).
368 self.assertLess(num_fds_1 - num_fds_0, 5)
371if __name__ == "__main__": 371 ↛ 372line 371 didn't jump to line 372, because the condition on line 371 was never true
372 unittest.main()