Merge "Add action plugins to restrict untrusted execution" into feature/zuulv3
diff --git a/tests/fixtures/config/single-tenant/git/layout-mutex-reconfiguration/playbooks/project-test1.yaml b/tests/fixtures/config/single-tenant/git/layout-mutex-reconfiguration/playbooks/project-test1.yaml
new file mode 100644
index 0000000..f679dce
--- /dev/null
+++ b/tests/fixtures/config/single-tenant/git/layout-mutex-reconfiguration/playbooks/project-test1.yaml
@@ -0,0 +1,2 @@
+- hosts: all
+  tasks: []
diff --git a/tests/fixtures/config/single-tenant/git/layout-mutex-reconfiguration/zuul.yaml b/tests/fixtures/config/single-tenant/git/layout-mutex-reconfiguration/zuul.yaml
new file mode 100644
index 0000000..12f1747
--- /dev/null
+++ b/tests/fixtures/config/single-tenant/git/layout-mutex-reconfiguration/zuul.yaml
@@ -0,0 +1,23 @@
+- pipeline:
+    name: check
+    manager: independent
+    source:
+      gerrit
+    trigger:
+      gerrit:
+        - event: patchset-created
+    success:
+      gerrit:
+        verified: 1
+    failure:
+      gerrit:
+        verified: -1
+
+- job:
+    name: project-test1
+
+- project:
+    name: org/project
+    check:
+      jobs:
+        - project-test1
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
index 03aff00..884bd9b 100755
--- a/tests/unit/test_scheduler.py
+++ b/tests/unit/test_scheduler.py
@@ -2250,6 +2250,66 @@
         self.assertEqual(B.reported, 1)
         self.assertFalse('test-mutex' in self.sched.mutex.mutexes)
 
+    def test_mutex_abandon(self):
+        "Test abandon with job mutexes"
+        self.updateConfigLayout('layout-mutex')
+        self.sched.reconfigure(self.config)
+
+        self.launch_server.hold_jobs_in_build = True
+
+        tenant = self.sched.abide.tenants.get('openstack')
+        check_pipeline = tenant.layout.pipelines['check']
+
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        self.assertFalse('test-mutex' in self.sched.mutex.mutexes)
+
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+
+        self.assertTrue('test-mutex' in self.sched.mutex.mutexes)
+
+        self.fake_gerrit.addEvent(A.getChangeAbandonedEvent())
+        self.waitUntilSettled()
+
+        # The check pipeline should be empty
+        items = check_pipeline.getAllItems()
+        self.assertEqual(len(items), 0)
+
+        # The mutex should be released
+        self.assertFalse('test-mutex' in self.sched.mutex.mutexes)
+
+        self.launch_server.hold_jobs_in_build = False
+        self.launch_server.release()
+        self.waitUntilSettled()
+
+    def test_mutex_reconfigure(self):
+        "Test reconfigure with job mutexes"
+        self.updateConfigLayout('layout-mutex')
+        self.sched.reconfigure(self.config)
+
+        self.launch_server.hold_jobs_in_build = True
+
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        self.assertFalse('test-mutex' in self.sched.mutex.mutexes)
+
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+
+        self.assertTrue('test-mutex' in self.sched.mutex.mutexes)
+
+        self.updateConfigLayout('layout-mutex-reconfiguration')
+        self.sched.reconfigure(self.config)
+        self.waitUntilSettled()
+
+        self.launch_server.release('project-test1')
+        self.waitUntilSettled()
+
+        # There should be no builds anymore
+        self.assertEqual(len(self.builds), 0)
+
+        # The mutex should be released
+        self.assertFalse('test-mutex' in self.sched.mutex.mutexes)
+
     def test_live_reconfiguration(self):
         "Test that live reconfiguration works"
         self.launch_server.hold_jobs_in_build = True
diff --git a/zuul/manager/__init__.py b/zuul/manager/__init__.py
index 6e5f567..18cf11b 100644
--- a/zuul/manager/__init__.py
+++ b/zuul/manager/__init__.py
@@ -409,6 +409,9 @@
             except:
                 self.log.exception("Exception while canceling build %s "
                                    "for change %s" % (build, item.change))
+            finally:
+                self.sched.mutex.release(build.build_set.item, build.job)
+
             if not was_running:
                 try:
                     nodeset = build.build_set.getJobNodeSet(build.job.name)
diff --git a/zuul/model.py b/zuul/model.py
index 7bcc8f6..5a9e367 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -787,6 +787,9 @@
         self.job = job
         self.job_trees = []
 
+    def __repr__(self):
+        return '<JobTree %s %s>' % (self.job, self.job_trees)
+
     def addJob(self, job):
         if job not in [x.job for x in self.job_trees]:
             t = JobTree(job)
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index c042e4f..1162f51 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -545,6 +545,8 @@
                     self.log.exception(
                         "Exception while canceling build %s "
                         "for change %s" % (build, item.change))
+                finally:
+                    self.mutex.release(build.build_set.item, build.job)
 
     def _reconfigureTenant(self, tenant):
         # This is called from _doReconfigureEvent while holding the