Repository: buildbot/buildbot

Issue:
Renderable locks fail with 2.10
Hello,

I have a buildbot setup with a bunch of DockerLatentWorkers and I use a renderable lock to avoid launching too much Docker builds on a physical machine. This worked fine with 2.8.3 but stopped working when i updated buildbot to 2.10. The error reported is:

```
2021-01-18 01:56:45+0000 [-] added buildset 182 to database
2021-01-18 01:56:45+0000 [-] from maybeStartBuild for builder 'llvm_100_pthread'
	Traceback (most recent call last):
	  File "/home/alarm/VENV/lib/python3.9/site-packages/buildbot/process/buildrequestdistributor.py", line 194, in popNextBuild
	    canStart = yield self.canStartBuild(worker, breq)
	  File "/home/alarm/VENV/lib/python3.9/site-packages/buildbot/process/buildrequestdistributor.py", line 267, in canStartBuild
	    return self.bldr.canStartBuild(worker, breq)
	  File "/usr/lib/python3.9/site-packages/twisted/internet/defer.py", line 1613, in unwindGenerator
	    return _cancellableInlineCallbacks(gen)
	  File "/usr/lib/python3.9/site-packages/twisted/internet/defer.py", line 1529, in _cancellableInlineCallbacks
	    _inlineCallbacks(None, g, status)
	--- <exception caught here> ---
	  File "/home/alarm/VENV/lib/python3.9/site-packages/buildbot/process/buildrequestdistributor.py", line 478, in _activityLoop
	    yield self._maybeStartBuildsOnBuilder(bldr)
	  File "/home/alarm/VENV/lib/python3.9/site-packages/buildbot/process/buildrequestdistributor.py", line 495, in _maybeStartBuildsOnBuilder
	    worker, breqs = yield bc.chooseNextBuild()
	  File "/home/alarm/VENV/lib/python3.9/site-packages/buildbot/process/buildrequestdistributor.py", line 62, in chooseNextBuild
	    worker, breq = yield self.popNextBuild()
	  File "/home/alarm/VENV/lib/python3.9/site-packages/buildbot/process/buildrequestdistributor.py", line 194, in popNextBuild
	    canStart = yield self.canStartBuild(worker, breq)
	  File "/usr/lib/python3.9/site-packages/twisted/internet/defer.py", line 1418, in _inlineCallbacks
	    result = g.send(result)
	  File "/home/alarm/VENV/lib/python3.9/site-packages/buildbot/process/builder.py", line 300, in canStartBuild
	    locks = yield props.render(locks)
	builtins.AttributeError: 'NoneType' object has no attribute 'render'
```

I'll create a PR with a fix.


Discussion:
Ok, so this simple patch on `buildbot/process/builder.py`

```
         def setupPropsIfNeeded(props):
             if props is not None:
-                return None
+                return props
             props = Properties()
```

... fixes the issue on my buildbot instance, but running `trial` shows failures. I don't have the time to debug this further ATM, so if someone else can confirm this is the correct fix, that would be great.

Resolving patch:
diff --git a/master/buildbot/process/builder.py b/master/buildbot/process/builder.py
--- a/master/buildbot/process/builder.py
+++ b/master/buildbot/process/builder.py
@@ -263,7 +263,7 @@ def canStartBuild(self, workerforbuilder, buildrequest):
         # don't unnecessarily setup properties for build
         def setupPropsIfNeeded(props):
             if props is not None:
-                return None
+                return props
             props = Properties()
             Build.setupPropertiesKnownBeforeBuildStarts(props, [buildrequest],
                                                         self, workerforbuilder)


Test patch:
diff --git a/master/buildbot/test/unit/process/test_builder.py b/master/buildbot/test/unit/process/test_builder.py
--- a/master/buildbot/test/unit/process/test_builder.py
+++ b/master/buildbot/test/unit/process/test_builder.py
@@ -84,6 +84,22 @@ def __init__(self, workername):
         self.workername = workername
 
 
+class FakeLatentWorker(AbstractLatentWorker):
+    builds_may_be_incompatible = True
+
+    def __init__(self, is_compatible_with_build):
+        self.is_compatible_with_build = is_compatible_with_build
+
+    def isCompatibleWithBuild(self, build_props):
+        return defer.succeed(self.is_compatible_with_build)
+
+    def checkConfig(self, name, _, **kwargs):
+        pass
+
+    def reconfigService(self, name, _, **kwargs):
+        pass
+
+
 class TestBuilder(TestReactorMixin, BuilderMixin, unittest.TestCase):
 
     def setUp(self):
@@ -337,29 +353,43 @@ def rendered_locks(props):
     def test_canStartBuild_with_incompatible_latent_worker(self):
         yield self.makeBuilder()
 
-        class FakeLatentWorker(AbstractLatentWorker):
-            builds_may_be_incompatible = True
+        wfb = mock.Mock()
+        wfb.worker = FakeLatentWorker(is_compatible_with_build=False)
 
-            def __init__(self):
-                pass
+        with mock.patch(
+                'buildbot.process.build.Build.setupPropertiesKnownBeforeBuildStarts',
+                mock.Mock()):
+            startable = yield self.bldr.canStartBuild(wfb, 100)
+        self.assertFalse(startable)
 
-            def isCompatibleWithBuild(self, build_props):
-                return defer.succeed(False)
+    @defer.inlineCallbacks
+    def test_canStartBuild_with_renderable_locks_with_compatible_latent_worker(self):
+        yield self.makeBuilder()
 
-            def checkConfig(self, name, _, **kwargs):
-                pass
+        self.bldr.botmaster.getLockFromLockAccesses = mock.Mock(return_value=[mock.Mock()])
 
-            def reconfigService(self, name, _, **kwargs):
-                pass
+        rendered_locks = [False]
+
+        @renderer
+        def locks_renderer(props):
+            rendered_locks[0] = True
+            return [mock.Mock()]
+
+        self.bldr.config.locks = locks_renderer
 
         wfb = mock.Mock()
-        wfb.worker = FakeLatentWorker()
+        wfb.worker = FakeLatentWorker(is_compatible_with_build=True)
 
         with mock.patch(
-                'buildbot.process.build.Build.setupPropertiesKnownBeforeBuildStarts',
-                mock.Mock()):
-            startable = yield self.bldr.canStartBuild(wfb, 100)
+                'buildbot.process.build.Build._canAcquireLocks',
+                mock.Mock(return_value=False)):
+            with mock.patch(
+                    'buildbot.process.build.Build.setupPropertiesKnownBeforeBuildStarts',
+                    mock.Mock()):
+                startable = yield self.bldr.canStartBuild(wfb, 100)
+                self.assertEqual(startable, False)
         self.assertFalse(startable)
+        self.assertTrue(rendered_locks[0])
 
     @defer.inlineCallbacks
     def test_canStartBuild_enforceChosenWorker(self):