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

213 statements  

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/>. 

21 

22"""Simple unit test for cmdLineFwk module. 

23""" 

24 

25import logging 

26import sys 

27import time 

28import unittest 

29import warnings 

30from multiprocessing import Manager 

31 

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 

37 

38logging.basicConfig(level=logging.DEBUG) 

39 

40_LOG = logging.getLogger(__name__) 

41 

42 

43class QuantumExecutorMock(QuantumExecutor): 

44 """Mock class for QuantumExecutor""" 

45 

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() 

52 

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 

60 

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] 

64 

65 

66class QuantumMock: 

67 def __init__(self, dataId): 

68 self.dataId = dataId 

69 

70 def __eq__(self, other): 

71 return self.dataId == other.dataId 

72 

73 def __hash__(self): 

74 # dict.__eq__ is order-insensitive 

75 return hash(tuple(sorted(kv for kv in self.dataId.items()))) 

76 

77 

78class QuantumIterDataMock: 

79 """Simple class to mock QuantumIterData.""" 

80 

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") 

87 

88 

89class QuantumGraphMock: 

90 """Mock for quantum graph.""" 

91 

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 

98 

99 def __iter__(self): 

100 yield from nx.topological_sort(self._graph) 

101 

102 def __len__(self): 

103 return len(self._graph) 

104 

105 def findTaskDefByLabel(self, label): 

106 for q in self: 

107 if q.taskDef.label == label: 

108 return q.taskDef 

109 

110 def getQuantaForTask(self, taskDef): 

111 nodes = self.getNodesForTask(taskDef) 

112 return {q.quantum for q in nodes} 

113 

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 

120 

121 @property 

122 def graph(self): 

123 return self._graph 

124 

125 def findCycle(self): 

126 return [] 

127 

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 

135 

136 

137class TaskMockMP: 

138 """Simple mock class for task supporting multiprocessing.""" 

139 

140 canMultiprocess = True 

141 

142 def runQuantum(self): 

143 _LOG.debug("TaskMockMP.runQuantum") 

144 pass 

145 

146 

147class TaskMockFail: 

148 """Simple mock class for task which fails.""" 

149 

150 canMultiprocess = True 

151 

152 def runQuantum(self): 

153 _LOG.debug("TaskMockFail.runQuantum") 

154 raise ValueError("expected failure") 

155 

156 

157class TaskMockSleep: 

158 """Simple mock class for task which "runs" for some time.""" 

159 

160 canMultiprocess = True 

161 

162 def runQuantum(self): 

163 _LOG.debug("TaskMockSleep.runQuantum") 

164 time.sleep(5.0) 

165 

166 

167class TaskMockLongSleep: 

168 """Simple mock class for task which "runs" for very long time.""" 

169 

170 canMultiprocess = True 

171 

172 def runQuantum(self): 

173 _LOG.debug("TaskMockLongSleep.runQuantum") 

174 time.sleep(100.0) 

175 

176 

177class TaskMockNoMP: 

178 """Simple mock class for task not supporting multiprocessing.""" 

179 

180 canMultiprocess = False 

181 

182 

183class TaskDefMock: 

184 """Simple mock class for task definition in a pipeline.""" 

185 

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 

191 

192 def __str__(self): 

193 return f"TaskDefMock(taskName={self.taskName}, taskClass={self.taskClass.__name__})" 

194 

195 

196class MPGraphExecutorTestCase(unittest.TestCase): 

197 """A test case for MPGraphExecutor class""" 

198 

199 def test_mpexec_nomp(self): 

200 """Make simple graph and execute""" 

201 

202 taskDef = TaskDefMock() 

203 qgraph = QuantumGraphMock( 

204 [QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)] 

205 ) 

206 

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]) 

212 

213 def test_mpexec_mp(self): 

214 """Make simple graph and execute""" 

215 

216 taskDef = TaskDefMock() 

217 qgraph = QuantumGraphMock( 

218 [QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)] 

219 ) 

220 

221 methods = ["spawn"] 

222 if sys.platform == "linux": 

223 methods.append("fork") 

224 methods.append("forkserver") 

225 

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]) 

234 

235 def test_mpexec_nompsupport(self): 

236 """Try to run MP for task that has no MP support which should fail""" 

237 

238 taskDef = TaskDefMock(taskClass=TaskMockNoMP) 

239 qgraph = QuantumGraphMock( 

240 [QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)] 

241 ) 

242 

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) 

248 

249 def test_mpexec_fixup(self): 

250 """Make simple graph and execute, add dependencies by executing fixup 

251 code. 

252 """ 

253 

254 taskDef = TaskDefMock() 

255 

256 for reverse in (False, True): 

257 qgraph = QuantumGraphMock( 

258 [QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3)] 

259 ) 

260 

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) 

265 

266 expected = [0, 1, 2] 

267 if reverse: 

268 expected = list(reversed(expected)) 

269 self.assertEqual(qexec.getDataIds("detector"), expected) 

270 

271 def test_mpexec_timeout(self): 

272 """Fail due to timeout""" 

273 

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 ) 

283 

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) 

289 

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}") 

302 

303 def test_mpexec_failure(self): 

304 """Failure in one task should not stop other tasks""" 

305 

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 ) 

315 

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]) 

321 

322 def test_mpexec_failure_dep(self): 

323 """Failure in one task should skip dependents""" 

324 

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) 

337 

338 qgraph = QuantumGraphMock(qdata) 

339 

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]) 

345 

346 def test_mpexec_failure_failfast(self): 

347 """Fast fail stops quickly. 

348 

349 Timing delay of task #3 should be sufficient to process 

350 failure and raise exception. 

351 """ 

352 

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) 

367 

368 qgraph = QuantumGraphMock(qdata) 

369 

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]) 

375 

376 def test_mpexec_num_fd(self): 

377 """Check that number of open files stays reasonable""" 

378 

379 taskDef = TaskDefMock() 

380 qgraph = QuantumGraphMock( 

381 [QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(20)] 

382 ) 

383 

384 this_proc = psutil.Process() 

385 num_fds_0 = this_proc.num_fds() 

386 

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) 

391 

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) 

397 

398 

399if __name__ == "__main__": 399 ↛ 400line 399 didn't jump to line 400, because the condition on line 399 was never true

400 unittest.main()