Hide keyboard shortcuts

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

21 

22"""Simple unit test for cmdLineFwk module. 

23""" 

24 

25import logging 

26import networkx as nx 

27from multiprocessing import Manager 

28import psutil 

29import time 

30from types import SimpleNamespace 

31import unittest 

32import warnings 

33 

34from lsst.ctrl.mpexec import MPGraphExecutor, MPGraphExecutorError, MPTimeoutError, QuantumExecutor 

35from lsst.ctrl.mpexec.execFixupDataId import ExecFixupDataId 

36from lsst.pipe.base import NodeId 

37 

38 

39logging.basicConfig(level=logging.DEBUG) 

40 

41_LOG = logging.getLogger(__name__) 

42 

43 

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

53 

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) 

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

75 

76 

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 

86 

87 def __iter__(self): 

88 yield from nx.topological_sort(self._graph) 

89 

90 def findTaskDefByLabel(self, label): 

91 for q in self: 

92 if q.taskDef.label == label: 

93 return q.taskDef 

94 

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 

101 

102 @property 

103 def graph(self): 

104 return self._graph 

105 

106 def findCycle(self): 

107 return [] 

108 

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 

116 

117 

118class TaskMockMP: 

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

120 """ 

121 canMultiprocess = True 

122 

123 def runQuantum(self): 

124 _LOG.debug("TaskMockMP.runQuantum") 

125 pass 

126 

127 

128class TaskMockFail: 

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

130 """ 

131 canMultiprocess = True 

132 

133 def runQuantum(self): 

134 _LOG.debug("TaskMockFail.runQuantum") 

135 raise ValueError("expected failure") 

136 

137 

138class TaskMockSleep: 

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

140 """ 

141 canMultiprocess = True 

142 

143 def runQuantum(self): 

144 _LOG.debug("TaskMockSleep.runQuantum") 

145 time.sleep(5.) 

146 

147 

148class TaskMockLongSleep: 

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

150 """ 

151 canMultiprocess = True 

152 

153 def runQuantum(self): 

154 _LOG.debug("TaskMockLongSleep.runQuantum") 

155 time.sleep(100.) 

156 

157 

158class TaskMockNoMP: 

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

160 """ 

161 canMultiprocess = False 

162 

163 

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 

172 

173 def __str__(self): 

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

175 

176 

177class MPGraphExecutorTestCase(unittest.TestCase): 

178 """A test case for MPGraphExecutor class 

179 """ 

180 

181 def test_mpexec_nomp(self): 

182 """Make simple graph and execute""" 

183 

184 taskDef = TaskDefMock() 

185 qgraph = QuantumGraphMock([ 

186 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3) 

187 ]) 

188 

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

194 

195 def test_mpexec_mp(self): 

196 """Make simple graph and execute""" 

197 

198 taskDef = TaskDefMock() 

199 qgraph = QuantumGraphMock([ 

200 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3) 

201 ]) 

202 

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

208 

209 def test_mpexec_nompsupport(self): 

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

211 """ 

212 

213 taskDef = TaskDefMock(taskClass=TaskMockNoMP) 

214 qgraph = QuantumGraphMock([ 

215 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3) 

216 ]) 

217 

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) 

223 

224 def test_mpexec_fixup(self): 

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

226 """ 

227 

228 taskDef = TaskDefMock() 

229 

230 for reverse in (False, True): 

231 qgraph = QuantumGraphMock([ 

232 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(3) 

233 ]) 

234 

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) 

240 

241 expected = [0, 1, 2] 

242 if reverse: 

243 expected = list(reversed(expected)) 

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

245 

246 def test_mpexec_timeout(self): 

247 """Fail due to timeout""" 

248 

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

256 

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) 

262 

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

275 

276 def test_mpexec_failure(self): 

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

278 

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

286 

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

292 

293 def test_mpexec_failure_dep(self): 

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

295 

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) 

308 

309 qgraph = QuantumGraphMock(qdata) 

310 

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

316 

317 def test_mpexec_failure_failfast(self): 

318 """Fast fail stops quickly. 

319 

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

321 failure and raise exception. 

322 """ 

323 

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) 

338 

339 qgraph = QuantumGraphMock(qdata) 

340 

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

346 

347 def test_mpexec_num_fd(self): 

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

349 """ 

350 

351 taskDef = TaskDefMock() 

352 qgraph = QuantumGraphMock([ 

353 QuantumIterDataMock(index=i, taskDef=taskDef, detector=i) for i in range(20) 

354 ]) 

355 

356 this_proc = psutil.Process() 

357 num_fds_0 = this_proc.num_fds() 

358 

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) 

363 

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) 

369 

370 

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

372 unittest.main()