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'