Merge "Cancel jobs behind a failed change."
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index 605dea6..8d7bbef 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -268,6 +268,18 @@
**success-message (optional)**
The message that should be reported to Gerrit if the job fails.
+**hold-following-changes (optional)**
+ This is a boolean that indicates that changes that follow this
+ change in a dependent change queue should wait until this job
+ succeeds before launching. If this is applied to a very short job
+ that can predict whether longer jobs will fail early, this can be
+ used to reduce the number of jobs that Zuul will launch and
+ ultimately have to cancel. In that case, a small amount of
+ paralellization of jobs is traded for more efficient use of testing
+ resources. On the other hand, to apply this to a long running job
+ would largely defeat the parallelization of dependent change testing
+ that is the main feature of Zuul. The default is False.
+
**branch (optional)**
This job should only be run on matching branches. This field is
treated as a regular expression and multiple branches may be
diff --git a/zuul/model.py b/zuul/model.py
index b292b06..f95195d 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -56,10 +56,12 @@
class Job(object):
def __init__(self, name):
+ # If you add attributes here, be sure to add them to the copy method.
self.name = name
self.failure_message = None
self.success_message = None
self.parameter_function = None
+ self.hold_following_changes = False
self.event_filters = []
def __str__(self):
@@ -71,6 +73,8 @@
def copy(self, other):
self.failure_message = other.failure_message
self.success_message = other.failure_message
+ self.parameter_function = other.parameter_function
+ self.hold_following_changes = other.hold_following_changes
self.event_filters = other.event_filters[:]
def eventMatches(self, event):
@@ -328,6 +332,19 @@
def __repr__(self):
return '<Change 0x%x %s>' % (id(self), self._id())
+ def equals(self, other):
+ if self.number:
+ if (self.number == other.number and
+ self.patchset == other.patchset):
+ return True
+ return False
+ if self.ref:
+ if (self.ref == other.ref and
+ self.newrew == other.newrev):
+ return True
+ return False
+ return False
+
def _filterJobs(self, jobs):
return filter(lambda job: job.eventMatches(self.event), jobs)
@@ -374,6 +391,10 @@
for job in self._filterJobs(self.project.getJobs(self.queue_name)):
build = self.current_build_set.getBuild(job.name)
result = build.result
+ if result == 'SUCCESS' and job.success_message:
+ result = job.success_message
+ elif result == 'FAILURE' and job.failure_message:
+ result = job.failure_message
url = build.url
if not url:
url = job.name
@@ -404,8 +425,27 @@
fakebuild.result = 'SKIPPED'
self.addBuild(fakebuild)
+ def isHoldingFollowingChanges(self):
+ tree = self.project.getJobTreeForQueue(self.queue_name)
+ for job in self._filterJobs(tree.getJobs()):
+ if not job.hold_following_changes:
+ continue
+ build = self.current_build_set.getBuild(job.name)
+ if not build:
+ return True
+ if build.result != 'SUCCESS':
+ return True
+ if not self.change_ahead:
+ return False
+ return self.change_ahead.isHoldingFollowingChanges()
+
def _findJobsToRun(self, job_trees):
torun = []
+ if self.change_ahead:
+ # Only run our jobs if any 'hold' jobs on the change ahead
+ # have completed successfully.
+ if self.change_ahead.isHoldingFollowingChanges():
+ return []
for tree in job_trees:
job = tree.job
if not job.eventMatches(self.event):
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 40a21c4..7b28dec 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -102,6 +102,9 @@
m = config_job.get('success-message', None)
if m:
job.success_message = m
+ m = config_job.get('hold-following-changes', False)
+ if m:
+ job.hold_following_changes = True
fname = config_job.get('parameter-function', None)
if fname:
func = self._config_env.get(fname, None)
@@ -373,7 +376,11 @@
efilters += str(e)
if efilters:
efilters = ' ' + efilters
- self.log.info("%s%s%s" % (istr, repr(tree.job), efilters))
+ hold = ''
+ if tree.job.hold_following_changes:
+ hold = ' [hold]'
+ self.log.info("%s%s%s%s" % (istr, repr(tree.job),
+ efilters, hold))
for x in tree.job_trees:
log_jobs(x, indent + 2)
@@ -397,7 +404,16 @@
return True
return False
+ def isChangeAlreadyInQueue(self, change):
+ for c in self.getChangesInQueue():
+ if change.equals(c):
+ return True
+ return False
+
def addChange(self, change):
+ if self.isChangeAlreadyInQueue(change):
+ self.log.debug("Change %s is already in queue, ignoring" % change)
+ return
self.log.debug("Adding change %s" % change)
if self.start_action:
try:
@@ -507,11 +523,15 @@
self.updateBuildDescriptions(change.current_build_set)
return ret
- def formatStatusHTML(self):
+ def getChangesInQueue(self):
changes = []
for build, change in self.building_jobs.items():
if change not in changes:
changes.append(change)
+ return changes
+
+ def formatStatusHTML(self):
+ changes = self.getChangesInQueue()
ret = ''
for change in changes:
ret += change.formatStatus(html=True)
@@ -571,6 +591,9 @@
self.log.error("Unable to find change queue for project %s" % project)
def addChange(self, change):
+ if self.isChangeAlreadyInQueue(change):
+ self.log.debug("Change %s is already in queue, ignoring" % change)
+ return
self.log.debug("Adding change %s" % change)
change_queue = self.getQueue(change.project)
if change_queue:
@@ -679,6 +702,12 @@
possibly reporting" % (change.change_behind, change))
self.possiblyReportChange(change.change_behind)
+ def getChangesInQueue(self):
+ changes = []
+ for shared_queue in self.change_queues:
+ changes.extend(shared_queue.queue)
+ return changes
+
def formatStatusHTML(self):
ret = ''
ret += '\n'