Repository navigation
pycue: add cloneJob() to duplicate an existing job for resubmission #2549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,6 +18,9 @@ | |||||||||||
| from __future__ import print_function | ||||||||||||
| from __future__ import division | ||||||||||||
|
|
||||||||||||
| import logging | ||||||||||||
| from xml.etree import ElementTree as Et | ||||||||||||
|
|
||||||||||||
| from opencue_proto import comment_pb2 | ||||||||||||
| from opencue_proto import criterion_pb2 | ||||||||||||
| from opencue_proto import cue_pb2 | ||||||||||||
|
|
@@ -59,6 +62,9 @@ | |||||||||||
| from . import util | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| logger = logging.getLogger("opencue") | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| __protobufs = [comment_pb2, criterion_pb2, cue_pb2, department_pb2, depend_pb2, facility_pb2, | ||||||||||||
| filter_pb2, host_pb2, job_pb2, renderPartition_pb2, report_pb2, service_pb2, | ||||||||||||
| show_pb2, subscription_pb2, task_pb2] | ||||||||||||
|
|
@@ -403,6 +409,107 @@ def launchSpecAndWait(spec): | |||||||||||
| return [Job(j) for j in jobSeq.jobs] | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _addTextElement(parent, tag, text): | ||||||||||||
| """Convenience method to create a sub element with text.""" | ||||||||||||
| element = Et.SubElement(parent, tag) | ||||||||||||
| element.text = text | ||||||||||||
| return element | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def cloneJob(job, name=None, user=None, frame_range=None, layer_frame_ranges=None): | ||||||||||||
| """Duplicates an existing job's submission-time configuration into a new job spec | ||||||||||||
| and launches it. | ||||||||||||
|
|
||||||||||||
| This reconstructs a launchable spec from the job and layer data available through the | ||||||||||||
| API (commands, services, frame ranges, core/memory/gpu requirements, tags, and limits), | ||||||||||||
| so a job can be re-run without needing its original outline/pyoutline submission script. | ||||||||||||
| Only ``Render``, ``Util``, and ``Post`` layers can be cloned this way; any ``PreProcess`` | ||||||||||||
| layers are skipped, as are layers with no command to re-run. | ||||||||||||
|
|
||||||||||||
| :type job: opencue.wrappers.job.Job | ||||||||||||
| :param job: the job to clone | ||||||||||||
| :type name: str | ||||||||||||
| :param name: name for the cloned job, defaults to "<original-name>_clone" | ||||||||||||
| :type user: str | ||||||||||||
| :param user: submitting user for the new job, defaults to the original job's user | ||||||||||||
| :type frame_range: str | ||||||||||||
| :param frame_range: frame range override applied to every cloned layer | ||||||||||||
| :type layer_frame_ranges: dict[str, str] | ||||||||||||
| :param layer_frame_ranges: per-layer frame range overrides, keyed by layer name; takes | ||||||||||||
| precedence over `frame_range` for the layers it names | ||||||||||||
| :rtype: list[opencue.wrappers.job.Job] | ||||||||||||
| :return: the newly launched job(s) | ||||||||||||
| """ | ||||||||||||
| layer_frame_ranges = layer_frame_ranges or {} | ||||||||||||
| layer_type_names = { | ||||||||||||
| job_pb2.RENDER: 'Render', | ||||||||||||
| job_pb2.UTIL: 'Util', | ||||||||||||
| job_pb2.POST: 'Post', | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| root = Et.Element('spec') | ||||||||||||
| _addTextElement(root, 'facility', job.data.facility) | ||||||||||||
| _addTextElement(root, 'show', job.data.show) | ||||||||||||
| _addTextElement(root, 'shot', job.data.shot) | ||||||||||||
| _addTextElement(root, 'user', user or job.data.user) | ||||||||||||
|
|
||||||||||||
| jobEl = Et.SubElement(root, 'job', {'name': name or '%s_clone' % job.data.name}) | ||||||||||||
| _addTextElement(jobEl, 'priority', str(job.data.priority)) | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: rg -n "is_paused|auto_eat|autoeat|<paused|paused.*job|Et.SubElement.*paused" proto pyoutline pycue cuebot | head -250
sed -n '445,475p' pycue/opencue/api.pyRepository: AcademySoftwareFoundation/OpenCue Length of output: 15950 🏁 Script executed: set -e
printf '%s\n' '--- cloneJob and nearby tests ---'
sed -n '420,525p' pycue/opencue/api.py
rg -n -A35 -B12 "cloneJob|resubmit|resubmission|autoeat|is_paused|paused" pycue/tests pycue/opencue pyoutline/outline/backend/cue.py | head -320
printf '%s\n' '--- JobSpec parser ---'
rg -n -A45 -B18 "isPaused|paused|isAutoEat|autoeat|stringToBool" cuebot/src/main/java/com/imageworks/spcue/service/JobSpec.java
printf '%s\n' '--- DTD and canonical serializer ---'
sed -n '1,40p' cuebot/src/main/resources/public/dtd/cjsl-1.13.dtd
sed -n '245,285p' pyoutline/outline/backend/cue.py
printf '%s\n' '--- protobuf fields and wrapper clone-related code ---'
sed -n '625,665p' proto/src/job.proto
sed -n '1120,1150p' pycue/opencue/wrappers/job.pyRepository: AcademySoftwareFoundation/OpenCue Length of output: 44488 🏁 Script executed: set -e
printf '%s\n' '--- Convert.stringToBool binding ---'
rg -n -A30 -B10 "stringToBool" cuebot/src/main/java cuebot/src/test/java
printf '%s\n' '--- clone/resubmit implementations and docs ---'
rg -n -i -A25 -B12 "clone(job|.*job)|resubmit|re.?submit|duplicate.*job|job.*clone|copy.*job" --glob '!**/build/**' pycue cuebot pyoutline docs README* | head -500
printf '%s\n' '--- test fixture setup for clone source Job ---'
sed -n '250,380p' pycue/tests/test_api.py
printf '%s\n' '--- parser tests for paused/autoeat ---'
rg -n -A25 -B15 "isPaused|isAutoEat|autoeat|paused" cuebot/src/test/java/com/imageworks/spcue/test | head -300Repository: AcademySoftwareFoundation/OpenCue Length of output: 50391 Preserve
The launch schema accepts both elements. Cuebot's parser accepts the Proposed fix+ _addTextElement(jobEl, 'paused', str(job.data.is_paused))
_addTextElement(jobEl, 'priority', str(job.data.priority))
if job.data.max_cores:
_addTextElement(jobEl, 'maxcores', str(job.data.max_cores))
if job.data.max_gpus:
_addTextElement(jobEl, 'maxgpus', str(job.data.max_gpus))
+ _addTextElement(jobEl, 'autoeat', str(job.data.auto_eat))
if job.data.os:
_addTextElement(jobEl, 'os', job.data.os)🤖 Prompt for AI Agents |
||||||||||||
| if job.data.max_cores: | ||||||||||||
| _addTextElement(jobEl, 'maxcores', str(job.data.max_cores)) | ||||||||||||
| if job.data.max_gpus: | ||||||||||||
| _addTextElement(jobEl, 'maxgpus', str(job.data.max_gpus)) | ||||||||||||
| if job.data.os: | ||||||||||||
| _addTextElement(jobEl, 'os', job.data.os) | ||||||||||||
|
|
||||||||||||
| layersEl = Et.SubElement(jobEl, 'layers') | ||||||||||||
| for layer in job.getLayers(): | ||||||||||||
| typeName = layer_type_names.get(layer.data.type) | ||||||||||||
| if typeName is None: | ||||||||||||
| logger.warning( | ||||||||||||
| "cloneJob: skipping layer %s, %s layers can't be cloned directly.", | ||||||||||||
| layer.data.name, Layer.LayerType(layer.data.type).name) | ||||||||||||
| continue | ||||||||||||
| if not layer.data.command: | ||||||||||||
| logger.warning( | ||||||||||||
| "cloneJob: skipping layer %s, it has no command to re-run.", layer.data.name) | ||||||||||||
| continue | ||||||||||||
|
|
||||||||||||
| layerEl = Et.SubElement(layersEl, 'layer', {'name': layer.data.name, 'type': typeName}) | ||||||||||||
| _addTextElement(layerEl, 'cmd', layer.data.command) | ||||||||||||
| _addTextElement( | ||||||||||||
| layerEl, 'range', | ||||||||||||
| layer_frame_ranges.get(layer.data.name) or frame_range or layer.data.range) | ||||||||||||
| _addTextElement(layerEl, 'chunk', str(layer.data.chunk_size or 1)) | ||||||||||||
| if layer.data.min_cores: | ||||||||||||
| _addTextElement(layerEl, 'cores', '%0.1f' % layer.data.min_cores) | ||||||||||||
| _addTextElement(layerEl, 'threadable', 'True' if layer.data.is_threadable else 'False') | ||||||||||||
| if layer.data.min_memory: | ||||||||||||
| _addTextElement(layerEl, 'memory', '%sm' % (layer.data.min_memory / 1024.0)) | ||||||||||||
| if layer.data.min_gpus or layer.data.min_gpu_memory: | ||||||||||||
| _addTextElement(layerEl, 'gpus', str(int(round(layer.data.min_gpus)) or 1)) | ||||||||||||
| _addTextElement( | ||||||||||||
| layerEl, 'gpu_memory', | ||||||||||||
| '%sm' % (layer.data.min_gpu_memory / 1024.0) if layer.data.min_gpu_memory | ||||||||||||
| else '1g') | ||||||||||||
| if layer.data.timeout: | ||||||||||||
| _addTextElement(layerEl, 'timeout', str(layer.data.timeout)) | ||||||||||||
| if layer.data.timeout_llu: | ||||||||||||
| _addTextElement(layerEl, 'timeout_llu', str(layer.data.timeout_llu)) | ||||||||||||
| if layer.data.tags: | ||||||||||||
| _addTextElement(layerEl, 'tags', '|'.join(layer.data.tags)) | ||||||||||||
| if layer.data.limits: | ||||||||||||
| limitsEl = Et.SubElement(layerEl, 'limits') | ||||||||||||
| for limitName in layer.data.limits: | ||||||||||||
| _addTextElement(limitsEl, 'limit', limitName) | ||||||||||||
|
|
||||||||||||
| servicesEl = Et.SubElement(layerEl, 'services') | ||||||||||||
| for serviceName in layer.data.services: | ||||||||||||
| _addTextElement(servicesEl, 'service', serviceName) | ||||||||||||
|
|
||||||||||||
| return launchSpecAndWait(Et.tostring(root, encoding='unicode')) | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject clones that contain no runnable layers. If every layer is unsupported or has no command, this line submits Proposed fix+ if len(layersEl) == 0:
+ raise ValueError("Cannot clone job: no runnable layers were found")
+
return launchSpecAndWait(Et.tostring(root, encoding='unicode'))📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| # | ||||||||||||
| # Job Names | ||||||||||||
| # | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: AcademySoftwareFoundation/OpenCue
Length of output: 25973
🏁 Script executed:
Repository: AcademySoftwareFoundation/OpenCue
Length of output: 50389
🤖 get_repo_knowledge executed:
get_repo_knowledge AcademySoftwareFoundation/OpenCue /tmp/coderabbit-repo-knowledge/academysoftwarefoundation-opencue-dd2a06f5/architecture /tmp/coderabbit-repo-knowledge/academysoftwarefoundation-opencue-dd2a06f5/learningsLength of output: 46638
🏁 Script executed:
Repository: AcademySoftwareFoundation/OpenCue
Length of output: 50391
Handle clone names when the submitting user changes.
Cuebot’s
JobSpec.conformJobNameremoves the exact<show>-<shot>-<user>_prefix before persisting a job name (cuebot/src/main/java/com/imageworks/spcue/service/JobSpec.java:124-156). Therefore, the current default is correct when the clone uses the original user.When
userchanges, the current value still contains the original full prefix. Cuebot then persists both prefixes, such as<show>-<shot>-<new-user>_<show>-<shot>-<old-user>_..._clone. Do not derive a base name by splitting arbitrary names. Either strip the exact original prefix after validating it, or require an explicitnamewhen cloning under a different user. Add coverage for that case; keep the existing same-user expectation.🤖 Prompt for AI Agents