launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #16003
[Merge] lp:~wgrant/launchpad/ss-get-builder-as-needed into lp:launchpad
William Grant has proposed merging lp:~wgrant/launchpad/ss-get-builder-as-needed into lp:launchpad.
Commit message:
Adjust SlaveScanner to hit the DB a bit less. We now cache the build cookie while the BuildQueue stays the same, and checkCancellation only retrieves a builder from the DB when it actually needs one.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~wgrant/launchpad/ss-get-builder-as-needed/+merge/188982
Adjust SlaveScanner to hit the DB a bit less. We now cache the build cookie while the BuildQueue stays the same, and checkCancellation only retrieves a builder from the DB when it actually needs one.
--
https://code.launchpad.net/~wgrant/launchpad/ss-get-builder-as-needed/+merge/188982
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/ss-get-builder-as-needed into lp:launchpad.
=== modified file 'lib/lp/buildmaster/manager.py'
--- lib/lp/buildmaster/manager.py 2013-10-02 05:47:26 +0000
+++ lib/lp/buildmaster/manager.py 2013-10-03 04:20:42 +0000
@@ -159,6 +159,11 @@
self._clock = clock
self.date_cancel = None
+ # We cache the build cookie, keyed on the BuildQueue, to avoid
+ # hitting the DB on every scan.
+ self._cached_build_cookie = None
+ self._cached_build_queue = None
+
def startCycle(self):
"""Scan the builder and dispatch to it or deal with failures."""
self.loop = LoopingCall(self.singleCycle)
@@ -222,7 +227,7 @@
transaction.abort()
@defer.inlineCallbacks
- def checkCancellation(self, vitals, builder, slave, interactor):
+ def checkCancellation(self, vitals, slave, interactor):
"""See if there is a pending cancellation request.
If the current build is in status CANCELLING then terminate it
@@ -265,10 +270,28 @@
vitals.build_queue.cancel()
transaction.commit()
value = yield interactor.resetOrFail(
- vitals, slave, builder, self.logger, e)
+ vitals, slave, self.builders_cache[vitals.name], self.logger,
+ e)
# value is not None if we resumed a slave host.
defer.returnValue(value is not None)
+ def getExpectedCookie(self, vitals):
+ """Return the build cookie expected to be held by the slave.
+
+ Calculating this requires hitting the DB, so it's cached based
+ on the current BuildQueue.
+ """
+ if vitals.build_queue != self._cached_build_queue:
+ if vitals.build_queue is not None:
+ behavior = self.behavior_factory(
+ vitals.build_queue, self.builders_cache[vitals.name],
+ None)
+ self._cached_build_cookie = behavior.getBuildCookie()
+ else:
+ self._cached_build_cookie = None
+ self._cached_build_queue = vitals.build_queue
+ return self._cached_build_cookie
+
@defer.inlineCallbacks
def scan(self):
"""Probe the builder and update/dispatch/collect as appropriate.
@@ -289,16 +312,11 @@
if not vitals.builderok:
lost_reason = '%s is disabled' % vitals.name
else:
- builder = self.builders_cache[self.builder_name]
- cancelled = yield self.checkCancellation(
- vitals, builder, slave, interactor)
+ cancelled = yield self.checkCancellation(vitals, slave, interactor)
if cancelled:
return
- behavior = self.behavior_factory(
- vitals.build_queue, builder, slave)
- db_cookie = behavior.getBuildCookie() if behavior else None
lost = yield interactor.rescueIfLost(
- vitals, slave, db_cookie, self.logger)
+ vitals, slave, self.getExpectedCookie(vitals), self.logger)
if lost:
lost_reason = '%s is lost' % vitals.name
=== modified file 'lib/lp/buildmaster/tests/test_manager.py'
--- lib/lp/buildmaster/tests/test_manager.py 2013-09-30 08:12:17 +0000
+++ lib/lp/buildmaster/tests/test_manager.py 2013-10-03 04:20:42 +0000
@@ -453,13 +453,20 @@
class MockBuildersCache:
def __init__(self, builder, build_queue):
+ self.updateTestData(builder, build_queue)
+ self.get_call_count = 0
+ self.getVitals_call_count = 0
+
+ def updateTestData(self, builder, build_queue):
self._builder = builder
self._build_queue = build_queue
def __getitem__(self, name):
+ self.get_call_count += 1
return self._builder
def getVitals(self, name):
+ self.getVitals_call_count += 1
return extract_vitals_from_db(self._builder, self._build_queue)
@@ -540,6 +547,43 @@
self.assertEqual(['status', 'abort'], slave.call_log)
self.assertEqual(0, interactor.updateBuild.call_count)
+ def test_getExpectedCookie_caches(self):
+ bc = MockBuildersCache(MockBuilder(), FakeBuildQueue())
+ scanner = SlaveScanner(
+ 'mock', bc, BufferLogger(), interactor_factory=FakeMethod(None),
+ slave_factory=FakeMethod(None),
+ behavior_factory=FakeMethod(TrivialBehavior()))
+
+ def assertCounts(expected):
+ self.assertEqual(
+ expected,
+ (scanner.interactor_factory.call_count,
+ scanner.behavior_factory.call_count,
+ scanner.builders_cache.get_call_count))
+
+ # The first call will get a Builder and a BuildFarmJobBehavior.
+ assertCounts((0, 0, 0))
+ cookie1 = scanner.getExpectedCookie(bc.getVitals('foo'))
+ self.assertEqual('trivial', cookie1)
+ assertCounts((0, 1, 1))
+
+ # A second call with the same BuildQueue will not reretrieve them.
+ cookie2 = scanner.getExpectedCookie(bc.getVitals('foo'))
+ self.assertEqual(cookie1, cookie2)
+ assertCounts((0, 1, 1))
+
+ # But a call with a new BuildQueue will regrab.
+ bc.updateTestData(bc._builder, FakeBuildQueue())
+ cookie3 = scanner.getExpectedCookie(bc.getVitals('foo'))
+ self.assertEqual(cookie1, cookie3)
+ assertCounts((0, 2, 2))
+
+ # And unsetting the BuildQueue returns None again.
+ bc.updateTestData(bc._builder, None)
+ cookie4 = scanner.getExpectedCookie(bc.getVitals('foo'))
+ self.assertIs(None, cookie4)
+ assertCounts((0, 2, 2))
+
class TestCancellationChecking(TestCaseWithFactory):
"""Unit tests for the checkCancellation method."""
@@ -568,7 +612,7 @@
# If the builder is nonvirtual make sure we return False.
self.builder.virtualized = False
d = self._getScanner().checkCancellation(
- self.vitals, self.builder, None, self.interactor)
+ self.vitals, None, self.interactor)
return d.addCallback(self.assertFalse)
def test_ignores_no_buildqueue(self):
@@ -577,13 +621,13 @@
buildqueue = self.builder.currentjob
buildqueue.reset()
d = self._getScanner().checkCancellation(
- self.vitals, self.builder, None, self.interactor)
+ self.vitals, None, self.interactor)
return d.addCallback(self.assertFalse)
def test_ignores_build_not_cancelling(self):
# If the active build is not in a CANCELLING state, ignore it.
d = self._getScanner().checkCancellation(
- self.vitals, self.builder, None, self.interactor)
+ self.vitals, None, self.interactor)
return d.addCallback(self.assertFalse)
@defer.inlineCallbacks
@@ -599,14 +643,14 @@
scanner = self._getScanner(clock=clock)
result = yield scanner.checkCancellation(
- self.vitals, self.builder, slave, self.interactor)
+ self.vitals, slave, self.interactor)
self.assertNotIn("resume", slave.call_log)
self.assertFalse(result)
self.assertEqual(BuildStatus.CANCELLING, build.status)
clock.advance(SlaveScanner.CANCEL_TIMEOUT)
result = yield scanner.checkCancellation(
- self.vitals, self.builder, slave, self.interactor)
+ self.vitals, slave, self.interactor)
self.assertEqual(1, slave.call_log.count("resume"))
self.assertTrue(result)
self.assertEqual(BuildStatus.CANCELLED, build.status)
@@ -621,7 +665,7 @@
build = getUtility(IBinaryPackageBuildSet).getByQueueEntry(buildqueue)
build.updateStatus(BuildStatus.CANCELLING)
result = yield self._getScanner().checkCancellation(
- self.vitals, self.builder, slave, self.interactor)
+ self.vitals, slave, self.interactor)
self.assertEqual(1, slave.call_log.count("resume"))
self.assertTrue(result)
self.assertEqual(BuildStatus.CANCELLED, build.status)
Follow ups