launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28252
[Merge] ~cjwatson/launchpad:ci-build-fix-dispatch into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:ci-build-fix-dispatch into launchpad:master.
Commit message:
Fix dispatch of CI build jobs
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/417644
`CIBuild.getConfiguration` calls out to an external service, and so can't be called from buildd-manager's main thread because it would block the Twisted reactor. A better approach would be to store the relevant bits of job configuration in the database, but a quick fix for now is to defer this request to a thread, similar to the approach taken in `lp.soyuz.adapters.archivedependencies._get_sources_list_for_dependencies` for retrieving GPG keys from the keyserver.
To test this, I extended `GitHostingFixture` to detect the case where the default timeout hasn't been set, which would cause `GitHostingClient` to fail in a real environment. I'd like to make the fixture do this by default, but attempting that caused a number of other (probably artificial) test failures which would need to be fixed first.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:ci-build-fix-dispatch into launchpad:master.
diff --git a/lib/lp/code/model/cibuildbehaviour.py b/lib/lp/code/model/cibuildbehaviour.py
index 77f04af..7cd13a0 100644
--- a/lib/lp/code/model/cibuildbehaviour.py
+++ b/lib/lp/code/model/cibuildbehaviour.py
@@ -11,6 +11,7 @@ import json
import os
from twisted.internet import defer
+from twisted.internet.threads import deferToThread
from zope.component import adapter
from zope.interface import implementer
@@ -24,6 +25,11 @@ from lp.buildmaster.model.buildfarmjobbehaviour import (
BuildFarmJobBehaviourBase,
)
from lp.code.interfaces.cibuild import ICIBuild
+from lp.services.timeout import default_timeout
+from lp.services.webapp.interaction import (
+ ANONYMOUS,
+ setupInteraction,
+ )
from lp.soyuz.adapters.archivedependencies import (
get_sources_list_for_building,
)
@@ -65,10 +71,26 @@ class CIBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
def extraBuildArgs(self, logger=None):
"""Return extra builder arguments for this build."""
build = self.build
- try:
- configuration = self.build.getConfiguration(logger=logger)
- except Exception as e:
- raise CannotBuild(str(e))
+ # Preload the build's repository so that it can be accessed from
+ # another thread.
+ build.git_repository.id
+
+ # XXX cjwatson 2022-03-24: Work around a design error. We ought to
+ # have arranged to store the relevant bits of the configuration
+ # (i.e. `stages` below) in the database so that we don't need to
+ # fetch it again here. It isn't safe to run blocking network
+ # requests in buildd-manager's main thread, since that would block
+ # the Twisted reactor; defer the request to a thread for now, but
+ # we'll need to work out a better fix once we have time.
+ def get_configuration():
+ setupInteraction(ANONYMOUS)
+ with default_timeout(15.0):
+ try:
+ return build.getConfiguration(logger=logger)
+ except Exception as e:
+ raise CannotBuild(str(e))
+
+ configuration = yield deferToThread(get_configuration)
stages = []
if not configuration.pipeline:
raise CannotBuild(
diff --git a/lib/lp/code/model/tests/test_cibuildbehaviour.py b/lib/lp/code/model/tests/test_cibuildbehaviour.py
index 808144a..9e3b904 100644
--- a/lib/lp/code/model/tests/test_cibuildbehaviour.py
+++ b/lib/lp/code/model/tests/test_cibuildbehaviour.py
@@ -69,10 +69,6 @@ from lp.services.log.logger import (
DevNullLogger,
)
from lp.services.statsd.tests import StatsMixin
-from lp.services.timeout import (
- get_default_timeout_function,
- set_default_timeout_function,
- )
from lp.services.webapp import canonical_url
from lp.soyuz.adapters.archivedependencies import (
get_sources_list_for_building,
@@ -182,9 +178,6 @@ class TestAsyncCIBuildBehaviour(StatsMixin, TestCIBuildBehaviourBase):
self.useFixture(MockPatch("time.time", return_value=self.now))
self.addCleanup(shut_down_default_process_pool)
self.setUpStats()
- self.addCleanup(
- set_default_timeout_function, get_default_timeout_function())
- set_default_timeout_function(lambda: None)
def makeJob(self, configuration=_unset, **kwargs):
# We need a builder in these tests, in order that requesting a proxy
@@ -208,7 +201,7 @@ class TestAsyncCIBuildBehaviour(StatsMixin, TestCIBuildBehaviourBase):
job.build.distro_arch_series.distroseries.name,
job.build.distro_arch_series.architecturetag)).encode()
hosting_fixture = self.useFixture(
- GitHostingFixture(blob=configuration))
+ GitHostingFixture(blob=configuration, enforce_timeout=True))
if configuration is None:
hosting_fixture.getBlob.failure = GitRepositoryBlobNotFound(
job.build.git_repository.getInternalPath(), ".launchpad.yaml",
diff --git a/lib/lp/code/tests/helpers.py b/lib/lp/code/tests/helpers.py
index f566b60..d368213 100644
--- a/lib/lp/code/tests/helpers.py
+++ b/lib/lp/code/tests/helpers.py
@@ -45,6 +45,7 @@ from lp.registry.interfaces.series import SeriesStatus
from lp.services.database.sqlbase import cursor
from lp.services.memcache.testing import MemcacheFixture
from lp.services.propertycache import get_property_cache
+from lp.services.timeout import get_default_timeout_function
from lp.testing import (
run_with_login,
time_counter,
@@ -303,30 +304,48 @@ class BranchHostingFixture(fixtures.Fixture):
self.memcache_fixture = self.useFixture(MemcacheFixture())
+class FakeMethodEnforceTimeout(FakeMethod):
+ """A variant of `FakeMethod` that requires a default timeout.
+
+ Since `GitHostingClient` requires that `set_default_timeout_function` has
+ been called, `GitHostingFixture` requires that as well.
+ """
+
+ def __call__(self, *args, **kwargs):
+ if get_default_timeout_function() is None:
+ raise AssertionError("No default timeout function was set.")
+ return super().__call__(*args, **kwargs)
+
+
class GitHostingFixture(fixtures.Fixture):
"""A fixture that temporarily registers a fake Git hosting client."""
def __init__(self, default_branch="refs/heads/master",
refs=None, commits=None, log=None, diff=None, merge_diff=None,
- merges=None, blob=None, disable_memcache=True):
- self.create = FakeMethod()
- self.getProperties = FakeMethod(
+ merges=None, blob=None, disable_memcache=True,
+ enforce_timeout=False):
+ fake_method_factory = (
+ FakeMethodEnforceTimeout if enforce_timeout else FakeMethod)
+ self.create = fake_method_factory()
+ self.getProperties = fake_method_factory(
result={"default_branch": default_branch, "is_available": True})
- self.setProperties = FakeMethod()
- self.getRefs = FakeMethod(result=({} if refs is None else refs))
- self.getCommits = FakeMethod(
+ self.setProperties = fake_method_factory()
+ self.getRefs = fake_method_factory(
+ result=({} if refs is None else refs))
+ self.getCommits = fake_method_factory(
result=([] if commits is None else commits))
- self.getLog = FakeMethod(result=([] if log is None else log))
- self.getDiff = FakeMethod(result=({} if diff is None else diff))
- self.getMergeDiff = FakeMethod(
+ self.getLog = fake_method_factory(result=([] if log is None else log))
+ self.getDiff = fake_method_factory(
+ result=({} if diff is None else diff))
+ self.getMergeDiff = fake_method_factory(
result={} if merge_diff is None else merge_diff)
- self.detectMerges = FakeMethod(
+ self.detectMerges = fake_method_factory(
result=({} if merges is None else merges))
- self.getBlob = FakeMethod(result=blob)
- self.delete = FakeMethod()
+ self.getBlob = fake_method_factory(result=blob)
+ self.delete = fake_method_factory()
self.disable_memcache = disable_memcache
- self.repackRepository = FakeMethod()
- self.collectGarbage = FakeMethod()
+ self.repackRepository = fake_method_factory()
+ self.collectGarbage = fake_method_factory()
def _setUp(self):
self.useFixture(ZopeUtilityFixture(self, IGitHostingClient))