Add pipeline precedence

Allow, eg, jobs in a gate pipeline to take precedence over
a check pipeline.

Change-Id: Idf91527704cc75b00a336291f91b908286f8e630
Reviewed-on: https://review.openstack.org/36552
Reviewed-by: Clark Boylan <clark.boylan@gmail.com>
Reviewed-by: Jeremy Stanley <fungi@yuggoth.org>
Approved: James E. Blair <corvus@inaugust.com>
Tested-by: Jenkins
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index 430acf0..d9e245b 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -316,6 +316,13 @@
   do when a change is added to the pipeline manager.  This can be used,
   for example, to reset the value of the Verified review category.
 
+**precedence**
+  Indicates how the build scheduler should prioritize jobs for
+  different pipelines.  Each pipeline may have one precedence, jobs
+  for pipelines with a higher precedence will be run before ones with
+  lower.  The value should be one of ``high``, ``normal``, or ``low``.
+  Default: ``normal``.
+
 Some example pipeline configurations are included in the sample layout
 file.  The first is called a *check* pipeline::
 
diff --git a/tests/fixtures/layout.yaml b/tests/fixtures/layout.yaml
index 2695719..37ea552 100644
--- a/tests/fixtures/layout.yaml
+++ b/tests/fixtures/layout.yaml
@@ -31,6 +31,7 @@
       verified: -2
     start:
       verified: 0
+    precedence: high
 
   - name: unused
     manager: IndependentPipelineManager
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index 023ed35..b67e09f 100644
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -495,7 +495,8 @@
         self.worker.build_history.append(
             BuildHistory(name=self.name, number=self.number,
                          result=result, changes=changes, node=self.node,
-                         uuid=self.unique, description=self.description)
+                         uuid=self.unique, description=self.description,
+                         pipeline=self.parameters['ZUUL_PIPELINE'])
             )
 
         self.job.sendWorkComplete(json.dumps(data))
@@ -2267,6 +2268,28 @@
         self.assertTrue(re.search("project-test2.*SUCCESS", desc))
         self.assertTrue(re.search("Reported result.*SUCCESS", desc))
 
+    def test_queue_precedence(self):
+        "Test that queue precedence works"
+
+        self.gearman_server.hold_jobs_in_queue = True
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        A.addApproval('CRVW', 2)
+        self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+
+        self.waitUntilSettled()
+        self.gearman_server.hold_jobs_in_queue = False
+        self.gearman_server.release()
+        self.waitUntilSettled()
+
+        self.log.debug(self.history)
+        self.assertEqual(self.history[0].pipeline, 'gate')
+        self.assertEqual(self.history[1].pipeline, 'check')
+        self.assertEqual(self.history[2].pipeline, 'gate')
+        self.assertEqual(self.history[3].pipeline, 'gate')
+        self.assertEqual(self.history[4].pipeline, 'check')
+        self.assertEqual(self.history[5].pipeline, 'check')
+
     def test_json_status(self):
         "Test that we can retrieve JSON status info"
         self.worker.hold_jobs_in_build = True
diff --git a/zuul/launcher/gearman.py b/zuul/launcher/gearman.py
index 9390eee..0f9cc54 100644
--- a/zuul/launcher/gearman.py
+++ b/zuul/launcher/gearman.py
@@ -19,6 +19,7 @@
 import threading
 from uuid import uuid4
 
+import zuul.model
 from zuul.model import Build
 
 
@@ -297,8 +298,15 @@
             self.onBuildCompleted(gearman_job, 'LOST')
             return build
 
+        if pipeline.precedence == zuul.model.PRECEDENCE_NORMAL:
+            precedence = gear.PRECEDENCE_NORMAL
+        elif pipeline.precedence == zuul.model.PRECEDENCE_HIGH:
+            precedence = gear.PRECEDENCE_HIGH
+        elif pipeline.precedence == zuul.model.PRECEDENCE_LOW:
+            precedence = gear.PRECEDENCE_LOW
+
         try:
-            self.gearman.submitJob(gearman_job)
+            self.gearman.submitJob(gearman_job, precedence=precedence)
         except Exception:
             self.log.exception("Unable to submit job to Gearman")
             self.onBuildCompleted(gearman_job, 'LOST')
@@ -407,7 +415,7 @@
                             json.dumps(data), unique=stop_uuid)
         self.meta_jobs[stop_uuid] = stop_job
         self.log.debug("Submitting stop job: %s", stop_job)
-        self.gearman.submitJob(stop_job)
+        self.gearman.submitJob(stop_job, precedence=gear.PRECEDENCE_HIGH)
         return True
 
     def setBuildDescription(self, build, desc):
@@ -428,7 +436,7 @@
         desc_job = gear.Job(name, json.dumps(data), unique=desc_uuid)
         self.meta_jobs[desc_uuid] = desc_job
         self.log.debug("Submitting describe job: %s", desc_job)
-        self.gearman.submitJob(desc_job)
+        self.gearman.submitJob(desc_job, precedence=gear.PRECEDENCE_LOW)
         return True
 
     def lookForLostBuilds(self):
diff --git a/zuul/layoutvalidator.py b/zuul/layoutvalidator.py
index 5588afe..724b236 100644
--- a/zuul/layoutvalidator.py
+++ b/zuul/layoutvalidator.py
@@ -30,6 +30,9 @@
 
     manager = v.Any('IndependentPipelineManager',
                     'DependentPipelineManager')
+
+    precedence = v.Any('normal', 'low', 'high')
+
     variable_dict = v.Schema({}, extra=True)
 
     trigger = {v.Required('event'): toList(v.Any('patchset-created',
@@ -47,6 +50,7 @@
 
     pipeline = {v.Required('name'): str,
                 v.Required('manager'): manager,
+                'precedence': precedence,
                 'description': str,
                 'success-message': str,
                 'failure-message': str,
diff --git a/zuul/model.py b/zuul/model.py
index ac475ba..6b83feb 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -22,6 +22,17 @@
 MERGE_IF_NECESSARY = 3
 CHERRY_PICK = 4
 
+PRECEDENCE_NORMAL = 0
+PRECEDENCE_LOW = 1
+PRECEDENCE_HIGH = 2
+
+PRECEDENCE_MAP = {
+    None: PRECEDENCE_NORMAL,
+    'low': PRECEDENCE_LOW,
+    'normal': PRECEDENCE_NORMAL,
+    'high': PRECEDENCE_HIGH,
+}
+
 
 class Pipeline(object):
     """A top-level pipeline such as check, gate, post, etc."""
@@ -34,6 +45,7 @@
         self.job_trees = {}  # project -> JobTree
         self.manager = None
         self.queues = []
+        self.precedence = PRECEDENCE_NORMAL
 
     def __repr__(self):
         return '<Pipeline %s>' % self.name
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 52df7db..338eb63 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -123,6 +123,8 @@
         for conf_pipeline in data.get('pipelines', []):
             pipeline = Pipeline(conf_pipeline['name'])
             pipeline.description = conf_pipeline.get('description')
+            precedence = model.PRECEDENCE_MAP[conf_pipeline.get('precedence')]
+            pipeline.precedence = precedence
             pipeline.failure_message = conf_pipeline.get('failure-message',
                                                          "Build failed.")
             pipeline.success_message = conf_pipeline.get('success-message',