Merge ~cjwatson/launchpad:buildmaster-fetch-in-process into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: f9fe0ee862dd34739f3847d7907b15bf319a1a2d
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:buildmaster-fetch-in-process
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:buildmaster-refactor-handle-status-tests
Diff against target: 698 lines (+275/-23)
12 files modified
lib/lp/buildmaster/downloader.py (+66/-0)
lib/lp/buildmaster/interactor.py (+88/-8)
lib/lp/buildmaster/tests/mock_slaves.py (+3/-2)
lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py (+21/-3)
lib/lp/buildmaster/tests/test_interactor.py (+45/-7)
lib/lp/buildmaster/tests/test_manager.py (+11/-0)
lib/lp/code/model/tests/test_recipebuilder.py (+4/-0)
lib/lp/oci/tests/test_ocirecipebuildbehaviour.py (+23/-3)
lib/lp/services/job/runner.py (+2/-0)
lib/lp/snappy/tests/test_snapbuildbehaviour.py (+4/-0)
lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py (+4/-0)
lib/lp/soyuz/tests/test_livefsbuildbehaviour.py (+4/-0)
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+385252@code.launchpad.net

Commit message

Rewrite buildd-manager file fetching using subprocesses

Description of the change

This is similar to commit 8a6418c886433597e8aa113f53bd0b7dad397df2, but using a process pool (via `ampoule`) rather than a thread pool. The threaded approach did a poor job of servicing established connections when we deployed in on production, but it's possible that we were running into trouble with Python's GIL, so let's see if subprocesses will fare any better.

The new process pool is only used if the feature rule "buildmaster.download_in_subprocess" is set, so we can easily switch between the old and the new behaviour until we're confident.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
f9fe0ee... by Colin Watson

Add comment about subprocess download strategy

Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/buildmaster/downloader.py b/lib/lp/buildmaster/downloader.py
2new file mode 100644
3index 0000000..9da2b89
4--- /dev/null
5+++ b/lib/lp/buildmaster/downloader.py
6@@ -0,0 +1,66 @@
7+# Copyright 2020 Canonical Ltd. This software is licensed under the
8+# GNU Affero General Public License version 3 (see the file LICENSE).
9+
10+"""Download subprocess support for buildd-manager.
11+
12+To minimise subprocess memory use, this intentionally avoids importing
13+anything from the rest of Launchpad.
14+"""
15+
16+from __future__ import absolute_import, print_function, unicode_literals
17+
18+__metaclass__ = type
19+__all__ = [
20+ 'DownloadCommand',
21+ 'DownloadProcess',
22+ ]
23+
24+import os.path
25+import tempfile
26+
27+from ampoule.child import AMPChild
28+from requests import (
29+ RequestException,
30+ Session,
31+ )
32+from requests_toolbelt.downloadutils import stream
33+from requests_toolbelt.exceptions import StreamingError
34+from twisted.protocols import amp
35+
36+
37+class DownloadCommand(amp.Command):
38+
39+ arguments = [
40+ (b"file_url", amp.Unicode()),
41+ (b"path_to_write", amp.Unicode()),
42+ (b"timeout", amp.Integer()),
43+ ]
44+ response = []
45+ errors = {
46+ RequestException: b"REQUEST_ERROR",
47+ StreamingError: b"STREAMING_ERROR",
48+ }
49+
50+
51+class DownloadProcess(AMPChild):
52+ """A subprocess that downloads a file to disk."""
53+
54+ @DownloadCommand.responder
55+ def downloadCommand(self, file_url, path_to_write, timeout):
56+ session = Session()
57+ session.trust_env = False
58+ response = session.get(file_url, timeout=timeout, stream=True)
59+ response.raise_for_status()
60+ f = tempfile.NamedTemporaryFile(
61+ mode="wb", prefix=os.path.basename(path_to_write) + "_",
62+ dir=os.path.dirname(path_to_write), delete=False)
63+ try:
64+ stream.stream_response_to_file(response, path=f)
65+ except Exception:
66+ f.close()
67+ os.unlink(f.name)
68+ raise
69+ else:
70+ f.close()
71+ os.rename(f.name, path_to_write)
72+ return {}
73diff --git a/lib/lp/buildmaster/interactor.py b/lib/lp/buildmaster/interactor.py
74index 7618da5..6d4ee49 100644
75--- a/lib/lp/buildmaster/interactor.py
76+++ b/lib/lp/buildmaster/interactor.py
77@@ -15,6 +15,7 @@ import sys
78 import tempfile
79 import traceback
80
81+from ampoule.pool import ProcessPool
82 from six.moves.urllib.parse import urlparse
83 import transaction
84 from twisted.internet import (
85@@ -33,6 +34,10 @@ from zope.security.proxy import (
86 removeSecurityProxy,
87 )
88
89+from lp.buildmaster.downloader import (
90+ DownloadCommand,
91+ DownloadProcess,
92+ )
93 from lp.buildmaster.enums import (
94 BuilderCleanStatus,
95 BuilderResetProtocol,
96@@ -47,6 +52,11 @@ from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
97 IBuildFarmJobBehaviour,
98 )
99 from lp.services.config import config
100+from lp.services.features import getFeatureFlag
101+from lp.services.job.runner import (
102+ QuietAMPConnector,
103+ VirtualEnvProcessStarter,
104+ )
105 from lp.services.twistedsupport import cancel_on_timeout
106 from lp.services.twistedsupport.processmonitor import ProcessWithTimeout
107 from lp.services.webapp import urlappend
108@@ -128,6 +138,8 @@ class LimitedHTTPConnectionPool(HTTPConnectionPool):
109
110
111 _default_pool = None
112+_default_process_pool = None
113+_default_process_pool_shutdown = None
114
115
116 def default_pool(reactor=None):
117@@ -147,6 +159,44 @@ def default_pool(reactor=None):
118 return _default_pool
119
120
121+def make_download_process_pool(**kwargs):
122+ """Make a pool of processes for downloading files."""
123+ env = {"PATH": os.environ["PATH"]}
124+ if "LPCONFIG" in os.environ:
125+ env["LPCONFIG"] = os.environ["LPCONFIG"]
126+ starter = VirtualEnvProcessStarter(env=env)
127+ starter.connectorFactory = QuietAMPConnector
128+ kwargs = dict(kwargs)
129+ kwargs.setdefault("max", config.builddmaster.download_connections)
130+ return ProcessPool(DownloadProcess, starter=starter, **kwargs)
131+
132+
133+def default_process_pool(reactor=None):
134+ global _default_process_pool, _default_process_pool_shutdown
135+ if reactor is None:
136+ reactor = default_reactor
137+ if _default_process_pool is None:
138+ _default_process_pool = make_download_process_pool()
139+ _default_process_pool.start()
140+ shutdown_id = reactor.addSystemEventTrigger(
141+ "during", "shutdown", _default_process_pool.stop)
142+ _default_process_pool_shutdown = (reactor, shutdown_id)
143+ return _default_process_pool
144+
145+
146+@defer.inlineCallbacks
147+def shut_down_default_process_pool():
148+ """Shut down the default process pool. Used in test cleanup."""
149+ global _default_process_pool, _default_process_pool_shutdown
150+ if _default_process_pool is not None:
151+ yield _default_process_pool.stop()
152+ _default_process_pool = None
153+ if _default_process_pool_shutdown is not None:
154+ reactor, shutdown_id = _default_process_pool_shutdown
155+ reactor.removeSystemEventTrigger(shutdown_id)
156+ _default_process_pool_shutdown = None
157+
158+
159 class BuilderSlave(object):
160 """Add in a few useful methods for the XMLRPC slave.
161
162@@ -159,7 +209,8 @@ class BuilderSlave(object):
163 # many false positives in your test run and will most likely break
164 # production.
165
166- def __init__(self, proxy, builder_url, vm_host, timeout, reactor, pool):
167+ def __init__(self, proxy, builder_url, vm_host, timeout, reactor,
168+ pool=None, process_pool=None):
169 """Initialize a BuilderSlave.
170
171 :param proxy: An XML-RPC proxy, implementing 'callRemote'. It must
172@@ -175,13 +226,21 @@ class BuilderSlave(object):
173 if reactor is None:
174 reactor = default_reactor
175 self.reactor = reactor
176+ self._download_in_subprocess = bool(
177+ getFeatureFlag('buildmaster.download_in_subprocess'))
178 if pool is None:
179 pool = default_pool(reactor=reactor)
180 self.pool = pool
181+ if self._download_in_subprocess:
182+ if process_pool is None:
183+ process_pool = default_process_pool(reactor=reactor)
184+ self.process_pool = process_pool
185+ else:
186+ self.process_pool = None
187
188 @classmethod
189 def makeBuilderSlave(cls, builder_url, vm_host, timeout, reactor=None,
190- proxy=None, pool=None):
191+ proxy=None, pool=None, process_pool=None):
192 """Create and return a `BuilderSlave`.
193
194 :param builder_url: The URL of the slave buildd machine,
195@@ -191,6 +250,7 @@ class BuilderSlave(object):
196 :param reactor: Used by tests to override the Twisted reactor.
197 :param proxy: Used By tests to override the xmlrpc.Proxy.
198 :param pool: Used by tests to override the HTTPConnectionPool.
199+ :param process_pool: Used by tests to override the ProcessPool.
200 """
201 rpc_url = urlappend(builder_url.encode('utf-8'), 'rpc')
202 if proxy is None:
203@@ -199,7 +259,9 @@ class BuilderSlave(object):
204 server_proxy.queryFactory = QuietQueryFactory
205 else:
206 server_proxy = proxy
207- return cls(server_proxy, builder_url, vm_host, timeout, reactor, pool)
208+ return cls(
209+ server_proxy, builder_url, vm_host, timeout, reactor,
210+ pool=pool, process_pool=process_pool)
211
212 def _with_timeout(self, d, timeout=None):
213 return cancel_on_timeout(d, timeout or self.timeout, self.reactor)
214@@ -251,11 +313,29 @@ class BuilderSlave(object):
215 """
216 file_url = self.getURL(sha_sum)
217 try:
218- response = yield Agent(self.reactor, pool=self.pool).request(
219- "GET", file_url)
220- finished = defer.Deferred()
221- response.deliverBody(FileWritingProtocol(finished, path_to_write))
222- yield finished
223+ # Select download behaviour according to the
224+ # buildmaster.download_in_subprocess feature rule: if enabled,
225+ # defer the download to a subprocess; if disabled, download the
226+ # file asynchronously in Twisted. We've found that in practice
227+ # the asynchronous approach only works well up to a bit over a
228+ # hundred builders, and beyond that it struggles to keep up with
229+ # incoming packets in time to avoid TCP timeouts (perhaps
230+ # because of too much synchronous work being done on the reactor
231+ # thread). The exact reason for this is as yet unproven, so we
232+ # use a feature rule to allow us to try out different
233+ # approaches.
234+ if self._download_in_subprocess:
235+ yield self.process_pool.doWork(
236+ DownloadCommand,
237+ file_url=file_url, path_to_write=path_to_write,
238+ timeout=self.timeout)
239+ else:
240+ response = yield Agent(self.reactor, pool=self.pool).request(
241+ "GET", file_url)
242+ finished = defer.Deferred()
243+ response.deliverBody(
244+ FileWritingProtocol(finished, path_to_write))
245+ yield finished
246 if logger is not None:
247 logger.info("Grabbed %s" % file_url)
248 except Exception as e:
249diff --git a/lib/lp/buildmaster/tests/mock_slaves.py b/lib/lp/buildmaster/tests/mock_slaves.py
250index 3a42f8c..9c99137 100644
251--- a/lib/lp/buildmaster/tests/mock_slaves.py
252+++ b/lib/lp/buildmaster/tests/mock_slaves.py
253@@ -315,14 +315,15 @@ class SlaveTestHelpers(fixtures.Fixture):
254 lambda: open(tachandler.logfile, 'r').readlines()))
255 return tachandler
256
257- def getClientSlave(self, reactor=None, proxy=None, pool=None):
258+ def getClientSlave(self, reactor=None, proxy=None,
259+ pool=None, process_pool=None):
260 """Return a `BuilderSlave` for use in testing.
261
262 Points to a fixed URL that is also used by `BuilddSlaveTestSetup`.
263 """
264 return BuilderSlave.makeBuilderSlave(
265 self.base_url, 'vmhost', config.builddmaster.socket_timeout,
266- reactor=reactor, proxy=proxy, pool=pool)
267+ reactor=reactor, proxy=proxy, pool=pool, process_pool=process_pool)
268
269 def makeCacheFile(self, tachandler, filename, contents=b'something'):
270 """Make a cache file available on the remote slave.
271diff --git a/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py b/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py
272index cc77f56..f837bb0 100644
273--- a/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py
274+++ b/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py
275@@ -13,6 +13,7 @@ import os
276 import shutil
277 import tempfile
278
279+from testscenarios import WithScenarios
280 from testtools import ExpectedException
281 from testtools.twistedsupport import AsynchronousDeferredRunTest
282 from twisted.internet import defer
283@@ -24,7 +25,10 @@ from lp.buildmaster.enums import (
284 BuildBaseImageType,
285 BuildStatus,
286 )
287-from lp.buildmaster.interactor import BuilderInteractor
288+from lp.buildmaster.interactor import (
289+ BuilderInteractor,
290+ shut_down_default_process_pool,
291+ )
292 from lp.buildmaster.interfaces.builder import BuildDaemonError
293 from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
294 IBuildFarmJobBehaviour,
295@@ -40,6 +44,7 @@ from lp.buildmaster.tests.mock_slaves import (
296 )
297 from lp.registry.interfaces.pocket import PackagePublishingPocket
298 from lp.services.config import config
299+from lp.services.features.testing import FeatureFixture
300 from lp.services.log.logger import BufferLogger
301 from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
302 from lp.testing import (
303@@ -316,8 +321,17 @@ class TestVerifySuccessfulBuildMixin:
304 self.assertRaises(AssertionError, behaviour.verifySuccessfulBuild)
305
306
307-class TestHandleStatusMixin:
308- """Tests for `IPackageBuild`s handleStatus method."""
309+class TestHandleStatusMixin(WithScenarios):
310+ """Tests for `IPackageBuild`s handleStatus method.
311+
312+ This should be run in a test file with
313+ `load_tests = load_tests_apply_scenarios`.
314+ """
315+
316+ scenarios = [
317+ ('download_in_twisted', {'download_in_subprocess': False}),
318+ ('download_in_subprocess', {'download_in_subprocess': True}),
319+ ]
320
321 layer = LaunchpadZopelessLayer
322 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=30)
323@@ -328,6 +342,9 @@ class TestHandleStatusMixin:
324
325 def setUp(self):
326 super(TestHandleStatusMixin, self).setUp()
327+ if self.download_in_subprocess:
328+ self.useFixture(FeatureFixture(
329+ {'buildmaster.download_in_subprocess': 'on'}))
330 self.factory = LaunchpadObjectFactory()
331 self.build = self.makeBuild()
332 # For the moment, we require a builder for the build so that
333@@ -339,6 +356,7 @@ class TestHandleStatusMixin:
334 self.interactor = BuilderInteractor()
335 self.behaviour = self.interactor.getBuildBehaviour(
336 self.build.buildqueue_record, self.builder, self.slave)
337+ self.addCleanup(shut_down_default_process_pool)
338
339 # We overwrite the buildmaster root to use a temp directory.
340 tempdir = tempfile.mkdtemp()
341diff --git a/lib/lp/buildmaster/tests/test_interactor.py b/lib/lp/buildmaster/tests/test_interactor.py
342index 76b958c..0c4cf14 100644
343--- a/lib/lp/buildmaster/tests/test_interactor.py
344+++ b/lib/lp/buildmaster/tests/test_interactor.py
345@@ -18,6 +18,10 @@ import tempfile
346 from lpbuildd.slave import BuilderStatus
347 from lpbuildd.tests.harness import BuilddSlaveTestSetup
348 from six.moves import xmlrpc_client
349+from testscenarios import (
350+ load_tests_apply_scenarios,
351+ WithScenarios,
352+ )
353 from testtools.matchers import (
354 ContainsAll,
355 HasLength,
356@@ -49,6 +53,8 @@ from lp.buildmaster.interactor import (
357 BuilderSlave,
358 extract_vitals_from_db,
359 LimitedHTTPConnectionPool,
360+ make_download_process_pool,
361+ shut_down_default_process_pool,
362 )
363 from lp.buildmaster.interfaces.builder import (
364 BuildDaemonIsolationError,
365@@ -67,6 +73,7 @@ from lp.buildmaster.tests.mock_slaves import (
366 WaitingSlave,
367 )
368 from lp.services.config import config
369+from lp.services.features.testing import FeatureFixture
370 from lp.services.twistedsupport.testing import TReqFixture
371 from lp.services.twistedsupport.treq import check_status
372 from lp.soyuz.model.binarypackagebuildbehaviour import (
373@@ -123,6 +130,10 @@ class TestBuilderInteractor(TestCase):
374
375 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
376
377+ def setUp(self):
378+ super(TestBuilderInteractor, self).setUp()
379+ self.addCleanup(shut_down_default_process_pool)
380+
381 def test_extractBuildStatus_baseline(self):
382 # extractBuildStatus picks the name of the build status out of a
383 # dict describing the slave's status.
384@@ -478,6 +489,7 @@ class TestSlave(TestCase):
385 def setUp(self):
386 super(TestSlave, self).setUp()
387 self.slave_helper = self.useFixture(SlaveTestHelpers())
388+ self.addCleanup(shut_down_default_process_pool)
389
390 def test_abort(self):
391 slave = self.slave_helper.getClientSlave()
392@@ -679,6 +691,7 @@ class TestSlaveTimeouts(TestCase):
393 self.proxy = DeadProxy("url")
394 self.slave = self.slave_helper.getClientSlave(
395 reactor=self.clock, proxy=self.proxy)
396+ self.addCleanup(shut_down_default_process_pool)
397
398 def assertCancelled(self, d, timeout=None):
399 self.clock.advance((timeout or config.builddmaster.socket_timeout) + 1)
400@@ -719,6 +732,7 @@ class TestSlaveConnectionTimeouts(TestCase):
401 super(TestSlaveConnectionTimeouts, self).setUp()
402 self.slave_helper = self.useFixture(SlaveTestHelpers())
403 self.clock = Clock()
404+ self.addCleanup(shut_down_default_process_pool)
405
406 def tearDown(self):
407 # We need to remove any DelayedCalls that didn't actually get called.
408@@ -744,16 +758,26 @@ class TestSlaveConnectionTimeouts(TestCase):
409 return assert_fails_with(d, defer.CancelledError)
410
411
412-class TestSlaveWithLibrarian(TestCaseWithFactory):
413+class TestSlaveWithLibrarian(WithScenarios, TestCaseWithFactory):
414 """Tests that need more of Launchpad to run."""
415
416+ scenarios = [
417+ ('download_in_twisted', {'download_in_subprocess': False}),
418+ ('download_in_subprocess', {'download_in_subprocess': True}),
419+ ]
420+
421 layer = LaunchpadZopelessLayer
422 run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory(
423 timeout=20)
424
425 def setUp(self):
426 super(TestSlaveWithLibrarian, self).setUp()
427+ if self.download_in_subprocess:
428+ self.useFixture(FeatureFixture(
429+ {'buildmaster.download_in_subprocess': 'on'}))
430 self.slave_helper = self.useFixture(SlaveTestHelpers())
431+ if self.download_in_subprocess:
432+ self.addCleanup(shut_down_default_process_pool)
433
434 def test_ensurepresent_librarian(self):
435 # ensurepresent, when given an http URL for a file will download the
436@@ -835,10 +859,17 @@ class TestSlaveWithLibrarian(TestCaseWithFactory):
437 def test_getFiles_open_connections(self):
438 # getFiles honours the configured limit on active download
439 # connections.
440- pool = LimitedHTTPConnectionPool(default_reactor, 2)
441 contents = [self.factory.getUniqueString() for _ in range(10)]
442 self.slave_helper.getServerSlave()
443- slave = self.slave_helper.getClientSlave(pool=pool)
444+ pool = LimitedHTTPConnectionPool(default_reactor, 2)
445+ if self.download_in_subprocess:
446+ process_pool = make_download_process_pool(min=1, max=2)
447+ process_pool.start()
448+ self.addCleanup(process_pool.stop)
449+ else:
450+ process_pool = None
451+ slave = self.slave_helper.getClientSlave(
452+ pool=pool, process_pool=process_pool)
453 files = []
454 content_map = {}
455
456@@ -848,11 +879,15 @@ class TestSlaveWithLibrarian(TestCaseWithFactory):
457 for sha1, local_file in files:
458 with open(local_file) as f:
459 self.assertEqual(content_map[sha1], f.read())
460- # Only two connections were used.
461 port = BuilddSlaveTestSetup().daemon_port
462- self.assertThat(
463- slave.pool._connections,
464- MatchesDict({("http", "localhost", port): HasLength(2)}))
465+ if self.download_in_subprocess:
466+ # Only two workers were used.
467+ self.assertEqual(2, len(process_pool.processes))
468+ else:
469+ # Only two connections were used.
470+ self.assertThat(
471+ slave.pool._connections,
472+ MatchesDict({("http", "localhost", port): HasLength(2)}))
473 return slave.pool.closeCachedConnections()
474
475 def finished_uploading(ignored):
476@@ -887,3 +922,6 @@ class TestSlaveWithLibrarian(TestCaseWithFactory):
477 with open(temp_name) as f:
478 self.assertEqual(b'', f.read())
479 yield slave.pool.closeCachedConnections()
480+
481+
482+load_tests = load_tests_apply_scenarios
483diff --git a/lib/lp/buildmaster/tests/test_manager.py b/lib/lp/buildmaster/tests/test_manager.py
484index 97de637..b0d56ed 100644
485--- a/lib/lp/buildmaster/tests/test_manager.py
486+++ b/lib/lp/buildmaster/tests/test_manager.py
487@@ -37,6 +37,7 @@ from lp.buildmaster.interactor import (
488 BuilderInteractor,
489 BuilderSlave,
490 extract_vitals_from_db,
491+ shut_down_default_process_pool,
492 )
493 from lp.buildmaster.interfaces.builder import (
494 BuildDaemonIsolationError,
495@@ -116,6 +117,7 @@ class TestSlaveScannerScan(TestCaseWithFactory):
496 hoary = ubuntu.getSeries('hoary')
497 self.test_publisher.setUpDefaultDistroSeries(hoary)
498 self.test_publisher.addFakeChroots(db_only=True)
499+ self.addCleanup(shut_down_default_process_pool)
500
501 def _resetBuilder(self, builder):
502 """Reset the given builder and its job."""
503@@ -653,6 +655,10 @@ class TestSlaveScannerWithLibrarian(TestCaseWithFactory):
504 layer = LaunchpadZopelessLayer
505 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=20)
506
507+ def setUp(self):
508+ super(TestSlaveScannerWithLibrarian, self).setUp()
509+ self.addCleanup(shut_down_default_process_pool)
510+
511 @defer.inlineCallbacks
512 def test_end_to_end(self):
513 # Test that SlaveScanner.scan() successfully finds, dispatches,
514@@ -884,6 +890,10 @@ class TestSlaveScannerWithoutDB(TestCase):
515
516 run_tests_with = AsynchronousDeferredRunTest
517
518+ def setUp(self):
519+ super(TestSlaveScannerWithoutDB, self).setUp()
520+ self.addCleanup(shut_down_default_process_pool)
521+
522 def getScanner(self, builder_factory=None, interactor=None, slave=None,
523 behaviour=None):
524 if builder_factory is None:
525@@ -1079,6 +1089,7 @@ class TestCancellationChecking(TestCaseWithFactory):
526 builder_name = BOB_THE_BUILDER_NAME
527 self.builder = getUtility(IBuilderSet)[builder_name]
528 self.builder.virtualized = True
529+ self.addCleanup(shut_down_default_process_pool)
530
531 @property
532 def vitals(self):
533diff --git a/lib/lp/code/model/tests/test_recipebuilder.py b/lib/lp/code/model/tests/test_recipebuilder.py
534index 8acc5b8..1ef74fe 100644
535--- a/lib/lp/code/model/tests/test_recipebuilder.py
536+++ b/lib/lp/code/model/tests/test_recipebuilder.py
537@@ -11,6 +11,7 @@ import os.path
538 import shutil
539 import tempfile
540
541+from testscenarios import load_tests_apply_scenarios
542 from testtools.matchers import MatchesListwise
543 from testtools.twistedsupport import AsynchronousDeferredRunTest
544 import transaction
545@@ -457,3 +458,6 @@ class TestVerifySuccessfulBuildForSPRBuild(
546 class TestHandleStatusForSPRBuild(
547 MakeSPRecipeBuildMixin, TestHandleStatusMixin, TestCaseWithFactory):
548 """IPackageBuild.handleStatus works with SPRecipe builds."""
549+
550+
551+load_tests = load_tests_apply_scenarios
552diff --git a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
553index cf0056b..bdfbf89 100644
554--- a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
555+++ b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py
556@@ -19,6 +19,10 @@ import uuid
557 import fixtures
558 from fixtures import MockPatch
559 from six.moves.urllib_parse import urlsplit
560+from testscenarios import (
561+ load_tests_apply_scenarios,
562+ WithScenarios,
563+ )
564 from testtools import ExpectedException
565 from testtools.matchers import (
566 AfterPreprocessing,
567@@ -41,7 +45,10 @@ from lp.buildmaster.enums import (
568 BuildBaseImageType,
569 BuildStatus,
570 )
571-from lp.buildmaster.interactor import BuilderInteractor
572+from lp.buildmaster.interactor import (
573+ BuilderInteractor,
574+ shut_down_default_process_pool,
575+ )
576 from lp.buildmaster.interfaces.builder import (
577 BuildDaemonError,
578 CannotBuild,
579@@ -393,7 +400,8 @@ class TestAsyncOCIRecipeBuildBehaviour(MakeOCIBuildMixin, TestCaseWithFactory):
580 self.assertEqual(distroseries.name, slave.call_log[1][5]['series'])
581
582
583-class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
584+class TestHandleStatusForOCIRecipeBuild(WithScenarios,
585+ MakeOCIBuildMixin,
586 TestCaseWithFactory):
587 # This is mostly copied from TestHandleStatusMixin, however
588 # we can't use all of those tests, due to the way OCIRecipeBuildBehaviour
589@@ -401,6 +409,11 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
590 # available. There's also some differences in the filemap handling, as
591 # we need a much more complex filemap here.
592
593+ scenarios = [
594+ ('download_in_twisted', {'download_in_subprocess': False}),
595+ ('download_in_subprocess', {'download_in_subprocess': True}),
596+ ]
597+
598 layer = LaunchpadZopelessLayer
599 run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory(
600 timeout=20)
601@@ -414,7 +427,10 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
602 def setUp(self):
603 super(TestHandleStatusForOCIRecipeBuild, self).setUp()
604 self.useFixture(fixtures.FakeLogger())
605- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
606+ features = {OCI_RECIPE_ALLOW_CREATE: 'on'}
607+ if self.download_in_subprocess:
608+ features['buildmaster.download_in_subprocess'] = 'on'
609+ self.useFixture(FeatureFixture(features))
610 self.build = self.makeBuild()
611 # For the moment, we require a builder for the build so that
612 # handleStatus_OK can get a reference to the slave.
613@@ -425,6 +441,7 @@ class TestHandleStatusForOCIRecipeBuild(MakeOCIBuildMixin,
614 self.interactor = BuilderInteractor()
615 self.behaviour = self.interactor.getBuildBehaviour(
616 self.build.buildqueue_record, self.builder, self.slave)
617+ self.addCleanup(shut_down_default_process_pool)
618
619 # We overwrite the buildmaster root to use a temp directory.
620 tempdir = tempfile.mkdtemp()
621@@ -683,3 +700,6 @@ class TestGetUploadMethodsForOCIRecipeBuild(
622 def setUp(self):
623 self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
624 super(TestGetUploadMethodsForOCIRecipeBuild, self).setUp()
625+
626+
627+load_tests = load_tests_apply_scenarios
628diff --git a/lib/lp/services/job/runner.py b/lib/lp/services/job/runner.py
629index c7b6284..8f80294 100644
630--- a/lib/lp/services/job/runner.py
631+++ b/lib/lp/services/job/runner.py
632@@ -12,7 +12,9 @@ __all__ = [
633 'celery_enabled',
634 'JobRunner',
635 'JobRunnerProcess',
636+ 'QuietAMPConnector',
637 'TwistedJobRunner',
638+ 'VirtualEnvProcessStarter',
639 ]
640
641
642diff --git a/lib/lp/snappy/tests/test_snapbuildbehaviour.py b/lib/lp/snappy/tests/test_snapbuildbehaviour.py
643index 81344b1..883ad47 100644
644--- a/lib/lp/snappy/tests/test_snapbuildbehaviour.py
645+++ b/lib/lp/snappy/tests/test_snapbuildbehaviour.py
646@@ -19,6 +19,7 @@ import fixtures
647 from pymacaroons import Macaroon
648 import pytz
649 from six.moves.urllib_parse import urlsplit
650+from testscenarios import load_tests_apply_scenarios
651 from testtools import ExpectedException
652 from testtools.matchers import (
653 AfterPreprocessing,
654@@ -844,3 +845,6 @@ class TestVerifySuccessfulBuildForSnapBuild(
655 class TestHandleStatusForSnapBuild(
656 MakeSnapBuildMixin, TestHandleStatusMixin, TestCaseWithFactory):
657 """IPackageBuild.handleStatus works with Snap builds."""
658+
659+
660+load_tests = load_tests_apply_scenarios
661diff --git a/lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py b/lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py
662index aa79d81..d445091 100644
663--- a/lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py
664+++ b/lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py
665@@ -13,6 +13,7 @@ import shutil
666 import tempfile
667
668 from storm.store import Store
669+from testscenarios import load_tests_apply_scenarios
670 from testtools.matchers import MatchesListwise
671 from testtools.twistedsupport import AsynchronousDeferredRunTest
672 import transaction
673@@ -635,3 +636,6 @@ class TestVerifySuccessfulBuildForBinaryPackageBuild(
674 class TestHandleStatusForBinaryPackageBuild(
675 MakeBinaryPackageBuildMixin, TestHandleStatusMixin, TestCaseWithFactory):
676 """IPackageBuild.handleStatus works with binary builds."""
677+
678+
679+load_tests = load_tests_apply_scenarios
680diff --git a/lib/lp/soyuz/tests/test_livefsbuildbehaviour.py b/lib/lp/soyuz/tests/test_livefsbuildbehaviour.py
681index 4820132..3c3ee29 100644
682--- a/lib/lp/soyuz/tests/test_livefsbuildbehaviour.py
683+++ b/lib/lp/soyuz/tests/test_livefsbuildbehaviour.py
684@@ -11,6 +11,7 @@ from datetime import datetime
685 import os.path
686
687 import pytz
688+from testscenarios import load_tests_apply_scenarios
689 from testtools.matchers import MatchesListwise
690 from testtools.twistedsupport import AsynchronousDeferredRunTest
691 import transaction
692@@ -342,3 +343,6 @@ class TestVerifySuccessfulBuildForLiveFSBuild(
693 class TestHandleStatusForLiveFSBuild(
694 MakeLiveFSBuildMixin, TestHandleStatusMixin, TestCaseWithFactory):
695 """IPackageBuild.handleStatus works with LiveFS builds."""
696+
697+
698+load_tests = load_tests_apply_scenarios

Subscribers

People subscribed via source and target branches

to status/vote changes: