Skip to content

Commit 671c895

Browse files
Add kill dependent and group dependent features (#1115)
* Add kill dependent and group dependent features Migrate features from old cue. Kill dependent will suggest killing dependent job every time a job with dependencies gets killed. Group dependents adds a checkbox to cuetopia to allow grouping dependent jobs in the JobMonitor treeView. * Fix MenuActions unittest and pylint Co-authored-by: dtavares <[email protected]>
1 parent 5536904 commit 671c895

File tree

4 files changed

+221
-20
lines changed

4 files changed

+221
-20
lines changed

cuegui/cuegui/JobMonitorTree.py

Lines changed: 115 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from __future__ import print_function
2121
from __future__ import division
2222

23-
23+
from future.utils import iteritems
2424
from builtins import map
2525
import time
2626

@@ -71,6 +71,7 @@ class JobMonitorTree(cuegui.AbstractTreeWidget.AbstractTreeWidget):
7171
"""Tree widget to display a list of monitored jobs."""
7272

7373
__loadMine = True
74+
__groupDependent = True
7475
view_object = QtCore.Signal(object)
7576

7677
def __init__(self, parent):
@@ -151,7 +152,9 @@ def __init__(self, parent):
151152

152153
self.__jobTimeLoaded = {}
153154
self.__userColors = {}
154-
155+
self.__dependentJobs = {}
156+
self._dependent_items = {}
157+
self.__reverseDependents = {}
155158
# Used to build right click context menus
156159
self.__menuActions = cuegui.MenuActions.MenuActions(
157160
self, self.updateSoon, self.selectedObjects)
@@ -180,8 +183,21 @@ def tick(self):
180183
self._update()
181184
return
182185

186+
self.updateJobCount()
183187
self.ticksWithoutUpdate += 1
184188

189+
def updateJobCount(self):
190+
"""Called at every tick. The total number of monitored
191+
jobs is added to the column header
192+
"""
193+
count = 0
194+
iterator = QtWidgets.QTreeWidgetItemIterator(self)
195+
while iterator.value():
196+
count += 1
197+
iterator += 1
198+
199+
self.headerItem().setText(0, "Job [Total Count: {}]".format(count))
200+
185201
def __itemSingleClickedCopy(self, item, col):
186202
"""Called when an item is clicked on. Copies selected object names to
187203
the middle click selection clip board.
@@ -226,13 +242,21 @@ def setLoadMine(self, value):
226242
@type value: boolean or QtCore.Qt.Checked or QtCore.Qt.Unchecked"""
227243
self.__loadMine = (value is True or value == QtCore.Qt.Checked)
228244

229-
def addJob(self, job, timestamp=None):
245+
def setGroupDependent(self, value):
246+
"""Enables or disables the auto grouping of the dependent jobs
247+
@param value: New groupDependent state
248+
@type value: boolean or QtCore.Qt.Checked or QtCore.Qt.Unchecked"""
249+
self.__groupDependent = (value is True or value == QtCore.Qt.Checked)
250+
self.updateRequest()
251+
252+
def addJob(self, job, timestamp=None, loading_from_config=False):
230253
"""Adds a job to the list. With locking"
231254
@param job: Job can be None, a job object, or a job name.
232255
@type job: job, string, None
233-
@param timestamp: UTC time of the specific date the job was
234-
added to be monitored
235-
@type timestamp: float"""
256+
@param loading_from_config: Whether or not this method is being called
257+
for loading jobs found in user config
258+
@type loading_from_config: bool
259+
"""
236260
newJobObj = cuegui.Utils.findJob(job)
237261
self.ticksLock.lock()
238262
try:
@@ -241,6 +265,42 @@ def addJob(self, job, timestamp=None):
241265
if not self.__groupDependent:
242266
self.__load[jobKey] = newJobObj
243267
self.__jobTimeLoaded[jobKey] = timestamp if timestamp else time.time()
268+
else:
269+
# We'll only add the new job if it's not already listed
270+
# as a dependent on another job
271+
if jobKey not in self.__reverseDependents.keys():
272+
self.__load[jobKey] = newJobObj
273+
274+
# when we are adding jobs manually, we want to calculate
275+
# all dependencies (active or not), so the user can see
276+
# all the dependent jobs, even after the main/parent job
277+
# has finished.
278+
# When we're loading jobs from user config, we want to
279+
# only include the active dependents. This is because
280+
# the dependencies have already been calculated and
281+
# listed in the config as a flat list, so attempting
282+
# to re-add them will result in duplicates that will
283+
# throw off the cleanup loop at the end of this method
284+
active_only = not loading_from_config
285+
dep = self.__menuActions.jobs(
286+
).getRecursiveDependentJobs([newJobObj],
287+
active_only=active_only)
288+
self.__dependentJobs[jobKey] = dep
289+
# we'll also store a reversed dictionary for
290+
# dependencies with the dependent as key and the main
291+
# job as the value, this will be used in step 2
292+
# below to remove jobs that are added here
293+
# as dependents
294+
for j in dep:
295+
depKey = cuegui.Utils.getObjectKey(j)
296+
self.__reverseDependents[depKey] = newJobObj
297+
self.__jobTimeLoaded[depKey] = time.time()
298+
self.__jobTimeLoaded[jobKey] = time.time()
299+
300+
for j in self.__reverseDependents:
301+
if j in self.__load:
302+
del self.__load[j]
303+
244304
finally:
245305
self.ticksLock.unlock()
246306

@@ -274,6 +334,20 @@ def _removeItem(self, item):
274334
# pylint: enable=no-member
275335
cuegui.AbstractTreeWidget.AbstractTreeWidget._removeItem(self, item)
276336
self.__jobTimeLoaded.pop(item.rpcObject, "")
337+
try:
338+
jobKey = cuegui.Utils.getObjectKey(item)
339+
# Remove the item from the main _items dictionary as well as the
340+
# __dependentJobs and the reverseDependent dictionaries
341+
cuegui.AbstractTreeWidget.AbstractTreeWidget._removeItem(self, item)
342+
dependent_jobs = self.__dependentJobs.get(jobKey, [])
343+
for djob in dependent_jobs:
344+
del self.__reverseDependents[djob]
345+
del self.__reverseDependents[jobKey]
346+
except KeyError:
347+
# Dependent jobs are not stored in as keys the main self._items
348+
# dictionary, trying to remove dependent jobs from self._items
349+
# raises a KeyError, which we can safely ignore
350+
pass
277351

278352
def removeAllItems(self):
279353
"""Notifies the other widgets of each item being unmonitored, then calls
@@ -284,6 +358,8 @@ def removeAllItems(self):
284358
# pylint: enable=no-member
285359
if proxy in self.__jobTimeLoaded:
286360
del self.__jobTimeLoaded[proxy]
361+
self.__dependentJobs.clear()
362+
self.__reverseDependents.clear()
287363
cuegui.AbstractTreeWidget.AbstractTreeWidget.removeAllItems(self)
288364

289365
def removeFinishedItems(self):
@@ -296,6 +372,7 @@ def contextMenuEvent(self, e):
296372
@param e: Right click QEvent
297373
@type e: QEvent"""
298374
menu = QtWidgets.QMenu()
375+
menu.setToolTipsVisible(True)
299376

300377
__selectedObjects = self.selectedObjects()
301378
__count = len(__selectedObjects)
@@ -304,6 +381,7 @@ def contextMenuEvent(self, e):
304381
self.__menuActions.jobs().addAction(menu, "unmonitor")
305382
self.__menuActions.jobs().addAction(menu, "view")
306383
self.__menuActions.jobs().addAction(menu, "emailArtist")
384+
self.__menuActions.jobs().addAction(menu, "showProgBar")
307385
self.__menuActions.jobs().addAction(menu, "viewComments")
308386
self.__menuActions.jobs().addAction(menu, "useLocalCores")
309387

@@ -404,11 +482,21 @@ def _getUpdate(self):
404482
# Gather list of all other jobs to update
405483
monitored_proxies.append(objectKey)
406484

485+
# Refresh the dependent proxies for the next update
486+
for job, dependents in iteritems(self.__dependentJobs):
487+
ids = [d.id() for d in dependents]
488+
# If the job has no dependents, then ids is an empty list,
489+
# The getJobs call returns every job on the cue when called
490+
# an empty list for the id argument!
491+
if not ids:
492+
continue
493+
tmp = opencue.api.getJobs(id=ids, all=True)
494+
self.__dependentJobs[job] = tmp
495+
407496
if self.__loadMine:
408497
# This auto-loads all the users jobs
409498
for job in opencue.api.getJobs(user=[cuegui.Utils.getUsername()]):
410-
objectKey = cuegui.Utils.getObjectKey(job)
411-
jobs[objectKey] = job
499+
self.addJob(job)
412500

413501
# Prune the users jobs from the remaining proxies to update
414502
for proxy, job in list(jobs.items()):
@@ -438,30 +526,46 @@ def _processUpdate(self, work, rpcObjects):
438526
for proxy, item in list(self._items.items()):
439527
if not proxy in rpcObjects:
440528
rpcObjects[proxy] = item.rpcObject
441-
529+
# pylint: disable=too-many-nested-blocks
442530
try:
443531
selectedKeys = [
444532
cuegui.Utils.getObjectKey(item.rpcObject) for item in self.selectedItems()]
445533
scrolled = self.verticalScrollBar().value()
534+
expanded = [cuegui.Utils.getObjectKey(item.rpcObject)
535+
for item in self._items.values() if item.isExpanded()]
446536

447537
# Store the creation time for the current item
448538
for item in list(self._items.values()):
449539
self.__jobTimeLoaded[cuegui.Utils.getObjectKey(item.rpcObject)] = item.created
540+
# Store the creation time for the dependent jobs
541+
for item in self._dependent_items.values():
542+
self.__jobTimeLoaded[cuegui.Utils.getObjectKey(item.rpcObject)] = item.created
450543

451544
self._items = {}
452545
self.clear()
453546

454-
for proxy, job in list(rpcObjects.items()):
547+
for proxy, job in iteritems(rpcObjects):
455548
self._items[proxy] = JobWidgetItem(job,
456549
self.invisibleRootItem(),
457550
self.__jobTimeLoaded.get(proxy, None))
458551
if proxy in self.__userColors:
459552
self._items[proxy].setUserColor(self.__userColors[proxy])
553+
if self.__groupDependent:
554+
dependent_jobs = self.__dependentJobs.get(proxy, [])
555+
for djob in dependent_jobs:
556+
item = JobWidgetItem(djob,
557+
self._items[proxy],
558+
self.__jobTimeLoaded.get(proxy, None))
559+
dkey = cuegui.Utils.getObjectKey(djob)
560+
self._dependent_items[dkey] = item
561+
if dkey in self.__userColors:
562+
self._dependent_items[dkey].setUserColor(
563+
self.__userColors[dkey])
460564

461565
self.verticalScrollBar().setRange(scrolled, len(rpcObjects.keys()) - scrolled)
462566
list(map(lambda key: self._items[key].setSelected(True),
463567
[key for key in selectedKeys if key in self._items]))
464-
568+
list(self._items[key].setExpanded(True) for key in expanded if key in self._items)
465569
except opencue.exception.CueException as e:
466570
list(map(logger.warning, cuegui.Utils.exceptionOutput(e)))
467571
finally:

cuegui/cuegui/MenuActions.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,13 +360,78 @@ def resume(self, rpcObjects=None):
360360
def kill(self, rpcObjects=None):
361361
jobs = self._getOnlyJobObjects(rpcObjects)
362362
if jobs:
363-
if cuegui.Utils.questionBoxYesNo(self._caller, "Kill jobs?",
364-
"Are you sure you want to kill these jobs?",
363+
msg = ("Are you sure you want to kill these jobs?\n\n"
364+
"** Note: This will stop all running frames and "
365+
"permanently remove the jobs from the cue. "
366+
"The jobs will NOT be able to return once killed.")
367+
if cuegui.Utils.questionBoxYesNo(self._caller, "Kill jobs?", msg,
365368
[job.data.name for job in jobs]):
366369
for job in jobs:
367370
job.kill()
371+
self.killDependents(jobs)
368372
self._update()
369373

374+
def killDependents(self, jobs):
375+
dependents = self.getRecursiveDependentJobs(jobs)
376+
if not dependents:
377+
return
378+
if cuegui.Utils.questionBoxYesNo(self._caller,
379+
"Kill depending jobs?",
380+
"The jobs have been killed. "
381+
"Do you want to kill %s jobs that depend on it?" %
382+
len(dependents),
383+
sorted([dep.name() for dep in dependents])):
384+
for depJob in dependents:
385+
try:
386+
depJob.kill()
387+
except opencue.exception.CueException as e:
388+
errMsg = "Failed to kill depending job: %s - %s" % (depJob.name(), e)
389+
logger.warning(errMsg)
390+
else:
391+
# Drop only direct dependents.
392+
for job in dependents:
393+
try:
394+
self.dropJobsDependingOnThis(job)
395+
except opencue.exception.CueException as e:
396+
logger.warning("Failed to drop dependencies: %s", e)
397+
398+
def getRecursiveDependentJobs(self, jobs, seen=None, active_only=True):
399+
seen = set() if seen is None else seen
400+
dependents = []
401+
if not jobs:
402+
return dependents
403+
for job in jobs:
404+
for dep in self.getExternalDependentNames(job, active_only):
405+
if dep.data.name not in seen:
406+
dependents.append(dep)
407+
seen.add(dep.data.name)
408+
return dependents + self.getRecursiveDependentJobs(dependents,
409+
seen,
410+
active_only)
411+
412+
def getExternalDependentNames(self, job, active_only=True):
413+
# pylint: disable=consider-using-set-comprehension
414+
job_names = set([dep.dependErJob()
415+
for dep in job.getWhatDependsOnThis()
416+
if (not dep.isInternal())
417+
and (dep.isActive() if active_only else True)])
418+
419+
return [self.getJobByName(job_name) for job_name in job_names]
420+
421+
def getJobByName(self, job_name):
422+
jobs = opencue.api.getJobs(substr=[job_name], include_finished=True)
423+
if not jobs:
424+
raise Exception("Job %s not found" % job_name)
425+
return jobs[0]
426+
427+
def dropJobsDependingOnThis(self, job):
428+
for dep in job.getWhatDependsOnThis():
429+
if not dep.isInternal():
430+
# pylint: disable=no-member
431+
job = self.getJobByName(self, dep.dependOnJob())
432+
job.dropDepends(opencue.wrappers.depend.DependTarget.EXTERNAL)
433+
# pylint: enable=no-member
434+
370435
eatDead_info = ["Eat dead frames", None, "eat"]
371436

372437
def eatDead(self, rpcObjects=None):

cuegui/cuegui/plugins/MonitorJobsPlugin.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,21 @@ def __init__(self, parent):
9191
self.jobMonitor.setColumnWidths),
9292
("columnOrder",
9393
self.jobMonitor.getColumnOrder,
94-
self.jobMonitor.setColumnOrder)])
94+
self.jobMonitor.setColumnOrder),
95+
("grpDependentCb",
96+
self.getGrpDependent,
97+
self.setGrpDependent),
98+
("autoLoadMineCb",
99+
self.getAutoLoadMine,
100+
self.setAutoLoadMine)])
95101

96102
def addJob(self, rpcObject):
97103
"""Adds a job to be monitored."""
98104
if cuegui.Utils.isProc(rpcObject):
99105
rpcObject = cuegui.Utils.findJob(rpcObject.data.job_name)
100106
elif not cuegui.Utils.isJob(rpcObject):
101107
return
102-
self.jobMonitor.addJob(rpcObject)
108+
self.jobMonitor.addJob(rpcObject, loading_from_config=True)
103109
self.raise_()
104110

105111
def getJobIds(self):
@@ -202,6 +208,22 @@ def _regexLoadJobsHandle(self):
202208
for job in opencue.api.getJobs(regex=[substring]):
203209
self.jobMonitor.addJob(job)
204210

211+
def getGrpDependent(self):
212+
"""Is group dependent checked"""
213+
return bool(self.grpDependentCb.isChecked())
214+
215+
def setGrpDependent(self, state):
216+
"""Set group dependent"""
217+
self.grpDependentCb.setChecked(bool(state))
218+
219+
def getAutoLoadMine(self):
220+
"""Is autoload mine checked"""
221+
return bool(self.autoLoadMineCb.isChecked())
222+
223+
def setAutoLoadMine(self, state):
224+
"""Set autoload mine"""
225+
self.autoLoadMineCb.setChecked(bool(state))
226+
205227
def _buttonSetup(self, layout):
206228
clearButton = QtWidgets.QPushButton("Clr")
207229
clearButton.setFocusPolicy(QtCore.Qt.NoFocus)
@@ -213,14 +235,22 @@ def _buttonSetup(self, layout):
213235
spacer.setFixedWidth(20)
214236
layout.addWidget(spacer)
215237

216-
mineCheckbox = QtWidgets.QCheckBox("Autoload Mine")
217-
mineCheckbox.setFocusPolicy(QtCore.Qt.NoFocus)
218-
mineCheckbox.setChecked(True)
219-
layout.addWidget(mineCheckbox)
220-
mineCheckbox.stateChanged.connect(self.jobMonitor.setLoadMine) # pylint: disable=no-member
238+
self.autoLoadMineCb = QtWidgets.QCheckBox("Autoload Mine")
239+
self.autoLoadMineCb.setFocusPolicy(QtCore.Qt.NoFocus)
240+
self.autoLoadMineCb.setChecked(True)
241+
layout.addWidget(self.autoLoadMineCb)
242+
self.autoLoadMineCb.stateChanged.connect(self.jobMonitor.setLoadMine) # pylint: disable=no-member
221243

222244
self._loadFinishedJobsSetup(self.__toolbar)
223245

246+
self.grpDependentCb = QtWidgets.QCheckBox("Group Dependent")
247+
self.grpDependentCb.setFocusPolicy(QtCore.Qt.NoFocus)
248+
self.grpDependentCb.setChecked(True)
249+
layout.addWidget(self.grpDependentCb)
250+
# pylint: disable=no-member
251+
self.grpDependentCb.stateChanged.connect(self.jobMonitor.setGroupDependent)
252+
# pylint: enable=no-member
253+
224254
finishedButton = QtWidgets.QPushButton(QtGui.QIcon(":eject.png"), "Finished")
225255
finishedButton.setToolTip("Unmonitor finished jobs")
226256
finishedButton.setFocusPolicy(QtCore.Qt.NoFocus)

0 commit comments

Comments
 (0)