Merge ~cjwatson/launchpad:buildmaster-fetch-in-process into launchpad:master
- Git
- lp:~cjwatson/launchpad
- buildmaster-fetch-in-process
- Merge into 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) |
Related bugs: |
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 8a6418c88643359
The new process pool is only used if the feature rule "buildmaster.
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
1 | diff --git a/lib/lp/buildmaster/downloader.py b/lib/lp/buildmaster/downloader.py |
2 | new file mode 100644 |
3 | index 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 {} |
73 | diff --git a/lib/lp/buildmaster/interactor.py b/lib/lp/buildmaster/interactor.py |
74 | index 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: |
249 | diff --git a/lib/lp/buildmaster/tests/mock_slaves.py b/lib/lp/buildmaster/tests/mock_slaves.py |
250 | index 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. |
271 | diff --git a/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py b/lib/lp/buildmaster/tests/test_buildfarmjobbehaviour.py |
272 | index 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() |
341 | diff --git a/lib/lp/buildmaster/tests/test_interactor.py b/lib/lp/buildmaster/tests/test_interactor.py |
342 | index 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 |
483 | diff --git a/lib/lp/buildmaster/tests/test_manager.py b/lib/lp/buildmaster/tests/test_manager.py |
484 | index 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): |
533 | diff --git a/lib/lp/code/model/tests/test_recipebuilder.py b/lib/lp/code/model/tests/test_recipebuilder.py |
534 | index 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 |
552 | diff --git a/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py b/lib/lp/oci/tests/test_ocirecipebuildbehaviour.py |
553 | index 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 |
628 | diff --git a/lib/lp/services/job/runner.py b/lib/lp/services/job/runner.py |
629 | index 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 | |
642 | diff --git a/lib/lp/snappy/tests/test_snapbuildbehaviour.py b/lib/lp/snappy/tests/test_snapbuildbehaviour.py |
643 | index 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 |
661 | diff --git a/lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py b/lib/lp/soyuz/tests/test_binarypackagebuildbehaviour.py |
662 | index 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 |
680 | diff --git a/lib/lp/soyuz/tests/test_livefsbuildbehaviour.py b/lib/lp/soyuz/tests/test_livefsbuildbehaviour.py |
681 | index 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 |