Merge lp:~wgrant/launchpad/bug-1221002 into lp:launchpad

Proposed by William Grant
Status: Merged
Approved by: Steve Kowalik
Approved revision: no longer in the source branch.
Merged at revision: 16760
Proposed branch: lp:~wgrant/launchpad/bug-1221002
Merge into: lp:launchpad
Diff against target: 614 lines (+211/-191)
4 files modified
lib/lp/buildmaster/interactor.py (+35/-54)
lib/lp/buildmaster/manager.py (+46/-64)
lib/lp/buildmaster/tests/test_interactor.py (+47/-62)
lib/lp/buildmaster/tests/test_manager.py (+83/-11)
To merge this branch: bzr merge lp:~wgrant/launchpad/bug-1221002
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code Approve
Review via email: mp+184030@code.launchpad.net

Commit message

Rework SlaveScanner.scan() to make a bit more sense.

Description of the change

Rework SlaveScanner.scan() to make a bit more sense. It now ensures that the slave and DB states agree at the start, rescuing the slave and job if the early check fails. This means that the builderok == False case is now handled early and actually prevents all slave communication, as you'd think it would.

If the slave and DB states match, we update or dispatch as appropriate. BuilderInteractor.isAvailable() dies, since we know after the rescue that the slave is IDLE iff currentjob in the DB is None.

SlaveScanner.scan() now takes builder and interactor arguments for overriding things in tests.

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/buildmaster/interactor.py'
2--- lib/lp/buildmaster/interactor.py 2013-09-03 09:35:07 +0000
3+++ lib/lp/buildmaster/interactor.py 2013-09-05 22:24:08 +0000
4@@ -8,9 +8,7 @@
5 ]
6
7 import logging
8-import socket
9 from urlparse import urlparse
10-import xmlrpclib
11
12 import transaction
13 from twisted.internet import defer
14@@ -288,28 +286,17 @@
15 status['logtail'] = status_sentence[2]
16 defer.returnValue((status_sentence, status))
17
18- @defer.inlineCallbacks
19- def isAvailable(self):
20- """Whether or not a builder is available for building new jobs.
21-
22- :return: A Deferred that fires with True or False, depending on
23- whether the builder is available or not.
24- """
25- if not self.builder.builderok:
26- defer.returnValue(False)
27- try:
28- status = yield self.slave.status()
29- except (xmlrpclib.Fault, socket.error):
30- defer.returnValue(False)
31- defer.returnValue(status[0] == 'BuilderStatus.IDLE')
32-
33- def verifySlaveBuildCookie(self, slave_build_cookie):
34+ def verifySlaveBuildCookie(self, slave_cookie):
35 """See `IBuildFarmJobBehavior`."""
36 if self._current_build_behavior is None:
37- raise CorruptBuildCookie('No job assigned to builder')
38- good_cookie = self._current_build_behavior.getBuildCookie()
39- if slave_build_cookie != good_cookie:
40- raise CorruptBuildCookie("Invalid slave build cookie.")
41+ if slave_cookie is not None:
42+ raise CorruptBuildCookie('Slave building when should be idle.')
43+ else:
44+ good_cookie = self._current_build_behavior.getBuildCookie()
45+ if slave_cookie != good_cookie:
46+ raise CorruptBuildCookie(
47+ "Invalid slave build cookie: got %r, expected %r."
48+ % (slave_cookie, good_cookie))
49
50 @defer.inlineCallbacks
51 def rescueIfLost(self, logger=None):
52@@ -322,7 +309,8 @@
53 `IBuildFarmJobBehavior.verifySlaveBuildCookie`.
54
55 :return: A Deferred that fires when the dialog with the slave is
56- finished. It does not have a return value.
57+ finished. Its return value is True if the slave is lost,
58+ False otherwise.
59 """
60 # 'ident_position' dict relates the position of the job identifier
61 # token in the sentence received from status(), according to the
62@@ -330,38 +318,40 @@
63 # for further information about sentence format.
64 ident_position = {
65 'BuilderStatus.BUILDING': 1,
66+ 'BuilderStatus.ABORTING': 1,
67 'BuilderStatus.WAITING': 2
68 }
69
70- # If slave is not building nor waiting, it's not in need of
71- # rescuing.
72+ # Determine the slave's current build cookie. For BUILDING, ABORTING
73+ # and WAITING we extract the string from the slave status
74+ # sentence, and for IDLE it is None.
75 status_sentence = yield self.slave.status()
76 status = status_sentence[0]
77 if status not in ident_position.keys():
78- return
79- slave_build_id = status_sentence[ident_position[status]]
80+ slave_cookie = None
81+ else:
82+ slave_cookie = status_sentence[ident_position[status]]
83+
84+ # verifySlaveBuildCookie will raise CorruptBuildCookie if the
85+ # slave cookie doesn't match the expected one, including
86+ # verifying that the slave cookie is None iff we expect the
87+ # slave to be idle.
88 try:
89- self.verifySlaveBuildCookie(slave_build_id)
90+ self.verifySlaveBuildCookie(slave_cookie)
91+ defer.returnValue(False)
92 except CorruptBuildCookie as reason:
93+ # An IDLE slave doesn't need rescuing (SlaveScanner.scan
94+ # will rescue the DB side instead), and we just have to wait
95+ # out an ABORTING one.
96 if status == 'BuilderStatus.WAITING':
97 yield self.cleanSlave()
98- else:
99+ elif status == 'BuilderStatus.BUILDING':
100 yield self.requestAbort()
101 if logger:
102 logger.info(
103 "Builder '%s' rescued from '%s': '%s'" %
104- (self.builder.name, slave_build_id, reason))
105-
106- def updateStatus(self, logger=None):
107- """Update the builder's status by probing it.
108-
109- :return: A Deferred that fires when the dialog with the slave is
110- finished. It does not have a return value.
111- """
112- if logger:
113- logger.debug('Checking %s' % self.builder.name)
114-
115- return self.rescueIfLost(logger)
116+ (self.builder.name, slave_cookie, reason))
117+ defer.returnValue(True)
118
119 def cleanSlave(self):
120 """Clean any temporary files from the slave.
121@@ -524,8 +514,11 @@
122
123 :return: A Deferred that fires when the slave dialog is finished.
124 """
125+ # IDLE is deliberately not handled here, because it should be
126+ # impossible to get past rescueIfLost unless the slave matches
127+ # the DB, and this method isn't called unless the DB says
128+ # there's a job.
129 builder_status_handlers = {
130- 'BuilderStatus.IDLE': self.updateBuild_IDLE,
131 'BuilderStatus.BUILDING': self.updateBuild_BUILDING,
132 'BuilderStatus.ABORTING': self.updateBuild_ABORTING,
133 'BuilderStatus.WAITING': self.updateBuild_WAITING,
134@@ -539,18 +532,6 @@
135 method = builder_status_handlers[builder_status]
136 yield method(queueItem, status_sentence, status_dict, logger)
137
138- def updateBuild_IDLE(self, queueItem, status_sentence, status_dict,
139- logger):
140- """Somehow the builder forgot about the build job.
141-
142- Log this and reset the record.
143- """
144- logger.warn(
145- "Builder %s forgot about buildqueue %d -- resetting buildqueue "
146- "record" % (queueItem.builder.url, queueItem.id))
147- queueItem.reset()
148- transaction.commit()
149-
150 def updateBuild_BUILDING(self, queueItem, status_sentence, status_dict,
151 logger):
152 """Build still building, collect the logtail"""
153
154=== modified file 'lib/lp/buildmaster/manager.py'
155--- lib/lp/buildmaster/manager.py 2013-09-03 04:06:08 +0000
156+++ lib/lp/buildmaster/manager.py 2013-09-05 22:24:08 +0000
157@@ -250,82 +250,64 @@
158 defer.returnValue(value is not None)
159
160 @defer.inlineCallbacks
161- def scan(self):
162+ def scan(self, builder=None, interactor=None):
163 """Probe the builder and update/dispatch/collect as appropriate.
164
165- There are several steps to scanning:
166-
167- 1. If the builder is marked as "ok" then probe it to see what state
168- it's in. This is where lost jobs are rescued if we think the
169- builder is doing something that it later tells us it's not,
170- and also where the multi-phase abort procedure happens.
171- See IBuilder.rescueIfLost, which is called by
172- IBuilder.updateStatus().
173- 2. If the builder is still happy, we ask it if it has an active build
174- and then either update the build in Launchpad or collect the
175- completed build. (builder.updateBuild)
176- 3. If the builder is not happy or it was marked as unavailable
177- mid-build, we need to reset the job that we thought it had, so
178- that the job is dispatched elsewhere.
179- 4. If the builder is idle and we have another build ready, dispatch
180- it.
181-
182- :return: A Deferred that fires when the scan is complete, whose
183- value is A `BuilderSlave` if we dispatched a job to it, or None.
184+ :return: A Deferred that fires when the scan is complete.
185 """
186- # We need to re-fetch the builder object on each cycle as the
187- # Storm store is invalidated over transaction boundaries.
188-
189- self.builder = get_builder(self.builder_name)
190- self.interactor = BuilderInteractor(self.builder)
191-
192- if self.builder.builderok:
193+ self.logger.debug("Scanning %s." % self.builder_name)
194+ # Commit and refetch the Builder object to ensure we have the
195+ # latest data from the DB.
196+ transaction.commit()
197+ self.builder = builder or get_builder(self.builder_name)
198+ self.interactor = interactor or BuilderInteractor(self.builder)
199+
200+ # Confirm that the DB and slave sides are in a valid, mutually
201+ # agreeable state.
202+ lost_reason = None
203+ if not self.builder.builderok:
204+ lost_reason = '%s is disabled' % self.builder.name
205+ else:
206 cancelled = yield self.checkCancellation(self.builder)
207 if cancelled:
208 return
209- yield self.interactor.updateStatus(self.logger)
210-
211- # Commit the changes done while possibly rescuing jobs, to
212- # avoid holding table locks.
213- transaction.commit()
214-
215- buildqueue = self.builder.currentjob
216- if buildqueue is not None:
217+ lost = yield self.interactor.rescueIfLost(self.logger)
218+ if lost:
219+ lost_reason = '%s is lost' % self.builder.name
220+
221+ # The slave is lost or the builder is disabled. We can't
222+ # continue to update the job status or dispatch a new job, so
223+ # just rescue the assigned job, if any, so it can be dispatched
224+ # to another slave.
225+ if lost_reason is not None:
226+ if self.builder.currentjob is not None:
227+ self.logger.warn(
228+ "%s. Resetting BuildQueue %d.", lost_reason,
229+ self.builder.currentjob.id)
230+ self.builder.currentjob.reset()
231+ transaction.commit()
232+ return
233+
234+ # We've confirmed that the slave state matches the DB. Continue
235+ # with updating the job status, or dispatching a new job if the
236+ # builder is idle.
237+ if self.builder.currentjob is not None:
238 # Scan the slave and get the logtail, or collect the build
239 # if it's ready. Yes, "updateBuild" is a bad name.
240- yield self.interactor.updateBuild(buildqueue)
241-
242- # If the builder is in manual mode, don't dispatch anything.
243- if self.builder.manual:
244+ yield self.interactor.updateBuild(self.builder.currentjob)
245+ elif self.builder.manual:
246+ # If the builder is in manual mode, don't dispatch anything.
247 self.logger.debug(
248 '%s is in manual mode, not dispatching.' %
249 self.builder.name)
250- return
251-
252- # If the builder is marked unavailable, don't dispatch anything.
253- # Additionaly, because builders can be removed from the pool at
254- # any time, we need to see if we think there was a build running
255- # on it before it was marked unavailable. In this case we reset
256- # the build thusly forcing it to get re-dispatched to another
257- # builder.
258- available = yield self.interactor.isAvailable()
259- if not available:
260- job = self.builder.currentjob
261- if job is not None and not self.builder.builderok:
262- self.logger.info(
263- "%s was made unavailable, resetting attached "
264- "job" % self.builder.name)
265- job.reset()
266+ else:
267+ # See if there is a job we can dispatch to the builder slave.
268+ yield self.interactor.findAndStartJob()
269+ if self.builder.currentjob is not None:
270+ # After a successful dispatch we can reset the
271+ # failure_count.
272+ self.builder.resetFailureCount()
273 transaction.commit()
274- return
275-
276- # See if there is a job we can dispatch to the builder slave.
277- yield self.interactor.findAndStartJob()
278- if self.builder.currentjob is not None:
279- # After a successful dispatch we can reset the
280- # failure_count.
281- self.builder.resetFailureCount()
282- transaction.commit()
283
284
285 class NewBuildersScanner:
286
287=== modified file 'lib/lp/buildmaster/tests/test_interactor.py'
288--- lib/lp/buildmaster/tests/test_interactor.py 2013-09-02 12:45:50 +0000
289+++ lib/lp/buildmaster/tests/test_interactor.py 2013-09-05 22:24:08 +0000
290@@ -33,7 +33,6 @@
291 )
292 from lp.buildmaster.tests.mock_slaves import (
293 AbortingSlave,
294- BrokenSlave,
295 BuildingSlave,
296 DeadProxy,
297 LostBuildingBrokenSlave,
298@@ -80,28 +79,33 @@
299 self.assertRaises(
300 AssertionError, interactor.extractBuildStatus, slave_status)
301
302- def test_verifySlaveBuildCookie_good(self):
303+ def test_verifySlaveBuildCookie_building_match(self):
304 interactor = BuilderInteractor(MockBuilder(), None, TrivialBehavior())
305 interactor.verifySlaveBuildCookie('trivial')
306
307- def test_verifySlaveBuildCookie_bad(self):
308+ def test_verifySlaveBuildCookie_building_mismatch(self):
309 interactor = BuilderInteractor(MockBuilder(), None, TrivialBehavior())
310 self.assertRaises(
311 CorruptBuildCookie,
312 interactor.verifySlaveBuildCookie, 'difficult')
313
314- def test_verifySlaveBuildCookie_idle(self):
315+ def test_verifySlaveBuildCookie_idle_match(self):
316+ interactor = BuilderInteractor(MockBuilder())
317+ self.assertIs(None, interactor._current_build_behavior)
318+ interactor.verifySlaveBuildCookie(None)
319+
320+ def test_verifySlaveBuildCookie_idle_mismatch(self):
321 interactor = BuilderInteractor(MockBuilder())
322 self.assertIs(None, interactor._current_build_behavior)
323 self.assertRaises(
324 CorruptBuildCookie, interactor.verifySlaveBuildCookie, 'foo')
325
326- def test_updateStatus_aborts_lost_and_broken_slave(self):
327+ def test_rescueIfLost_aborts_lost_and_broken_slave(self):
328 # A slave that's 'lost' should be aborted; when the slave is
329 # broken then abort() should also throw a fault.
330 slave = LostBuildingBrokenSlave()
331 interactor = BuilderInteractor(MockBuilder(), slave, TrivialBehavior())
332- d = interactor.updateStatus(DevNullLogger())
333+ d = interactor.rescueIfLost(DevNullLogger())
334
335 def check_slave_status(failure):
336 self.assertIn('abort', slave.call_log)
337@@ -170,31 +174,37 @@
338 interactor = BuilderInteractor(builder)
339 self.assertEqual(5, interactor.slave.timeout)
340
341+ @defer.inlineCallbacks
342+ def test_recover_idle_slave(self):
343+ # An idle slave is not rescued, even if it's not meant to be
344+ # idle. SlaveScanner.scan() will clean up the DB side, because
345+ # we still report that it's lost.
346+ slave = OkSlave()
347+ lost = yield BuilderInteractor(
348+ MockBuilder(), slave, TrivialBehavior()).rescueIfLost()
349+ self.assertTrue(lost)
350+ self.assertEqual([], slave.call_log)
351+
352+ @defer.inlineCallbacks
353 def test_recover_ok_slave(self):
354- # An idle slave is not rescued.
355+ # An idle slave that's meant to be idle is not rescued.
356 slave = OkSlave()
357- d = BuilderInteractor(
358- MockBuilder(), slave, TrivialBehavior()).rescueIfLost()
359-
360- def check_slave_calls(ignored):
361- self.assertNotIn('abort', slave.call_log)
362- self.assertNotIn('clean', slave.call_log)
363-
364- return d.addCallback(check_slave_calls)
365-
366+ lost = yield BuilderInteractor(
367+ MockBuilder(), slave, None).rescueIfLost()
368+ self.assertFalse(lost)
369+ self.assertEqual([], slave.call_log)
370+
371+ @defer.inlineCallbacks
372 def test_recover_waiting_slave_with_good_id(self):
373 # rescueIfLost does not attempt to abort or clean a builder that is
374 # WAITING.
375 waiting_slave = WaitingSlave(build_id='trivial')
376- d = BuilderInteractor(
377+ lost = yield BuilderInteractor(
378 MockBuilder(), waiting_slave, TrivialBehavior()).rescueIfLost()
379-
380- def check_slave_calls(ignored):
381- self.assertNotIn('abort', waiting_slave.call_log)
382- self.assertNotIn('clean', waiting_slave.call_log)
383-
384- return d.addCallback(check_slave_calls)
385-
386+ self.assertFalse(lost)
387+ self.assertEqual(['status'], waiting_slave.call_log)
388+
389+ @defer.inlineCallbacks
390 def test_recover_waiting_slave_with_bad_id(self):
391 # If a slave is WAITING with a build for us to get, and the build
392 # cookie cannot be verified, which means we don't recognize the build,
393@@ -202,40 +212,30 @@
394 # builder is reset for a new build, and the corrupt build is
395 # discarded.
396 waiting_slave = WaitingSlave(build_id='non-trivial')
397- d = BuilderInteractor(
398+ lost = yield BuilderInteractor(
399 MockBuilder(), waiting_slave, TrivialBehavior()).rescueIfLost()
400-
401- def check_slave_calls(ignored):
402- self.assertNotIn('abort', waiting_slave.call_log)
403- self.assertIn('clean', waiting_slave.call_log)
404-
405- return d.addCallback(check_slave_calls)
406-
407+ self.assertTrue(lost)
408+ self.assertEqual(['status', 'clean'], waiting_slave.call_log)
409+
410+ @defer.inlineCallbacks
411 def test_recover_building_slave_with_good_id(self):
412 # rescueIfLost does not attempt to abort or clean a builder that is
413 # BUILDING.
414 building_slave = BuildingSlave(build_id='trivial')
415- d = BuilderInteractor(
416+ lost = yield BuilderInteractor(
417 MockBuilder(), building_slave, TrivialBehavior()).rescueIfLost()
418-
419- def check_slave_calls(ignored):
420- self.assertNotIn('abort', building_slave.call_log)
421- self.assertNotIn('clean', building_slave.call_log)
422-
423- return d.addCallback(check_slave_calls)
424-
425+ self.assertFalse(lost)
426+ self.assertEqual(['status'], building_slave.call_log)
427+
428+ @defer.inlineCallbacks
429 def test_recover_building_slave_with_bad_id(self):
430 # If a slave is BUILDING with a build id we don't recognize, then we
431 # abort the build, thus stopping it in its tracks.
432 building_slave = BuildingSlave(build_id='non-trivial')
433- d = BuilderInteractor(
434+ lost = yield BuilderInteractor(
435 MockBuilder(), building_slave, TrivialBehavior()).rescueIfLost()
436-
437- def check_slave_calls(ignored):
438- self.assertIn('abort', building_slave.call_log)
439- self.assertNotIn('clean', building_slave.call_log)
440-
441- return d.addCallback(check_slave_calls)
442+ self.assertTrue(lost)
443+ self.assertEqual(['status', 'abort'], building_slave.call_log)
444
445
446 class TestBuilderInteractorSlaveStatus(TestCase):
447@@ -285,21 +285,6 @@
448 self.assertStatus(
449 AbortingSlave(), builder_status='BuilderStatus.ABORTING')
450
451- def test_isAvailable_with_not_builderok(self):
452- # isAvailable() is a wrapper around BuilderSlave.status()
453- builder = MockBuilder()
454- builder.builderok = False
455- d = BuilderInteractor(builder).isAvailable()
456- return d.addCallback(self.assertFalse)
457-
458- def test_isAvailable_with_slave_fault(self):
459- d = BuilderInteractor(MockBuilder(), BrokenSlave()).isAvailable()
460- return d.addCallback(self.assertFalse)
461-
462- def test_isAvailable_with_slave_idle(self):
463- d = BuilderInteractor(MockBuilder(), OkSlave()).isAvailable()
464- return d.addCallback(self.assertTrue)
465-
466
467 class TestBuilderInteractorDB(TestCaseWithFactory):
468 """BuilderInteractor tests that need a DB."""
469
470=== modified file 'lib/lp/buildmaster/tests/test_manager.py'
471--- lib/lp/buildmaster/tests/test_manager.py 2013-09-03 03:41:02 +0000
472+++ lib/lp/buildmaster/tests/test_manager.py 2013-09-05 22:24:08 +0000
473@@ -8,7 +8,6 @@
474 import time
475 import xmlrpclib
476
477-from lpbuildd.tests import BuilddSlaveTestSetup
478 from testtools.deferredruntest import (
479 assert_fails_with,
480 AsynchronousDeferredRunTest,
481@@ -47,7 +46,9 @@
482 BuildingSlave,
483 LostBuildingBrokenSlave,
484 make_publisher,
485+ MockBuilder,
486 OkSlave,
487+ TrivialBehavior,
488 )
489 from lp.registry.interfaces.distribution import IDistributionSet
490 from lp.services.config import config
491@@ -178,28 +179,30 @@
492 build = getUtility(IBinaryPackageBuildSet).getByQueueEntry(job)
493 self.assertEqual(build.status, BuildStatus.NEEDSBUILD)
494
495+ @defer.inlineCallbacks
496 def testScanRescuesJobFromBrokenBuilder(self):
497 # The job assigned to a broken builder is rescued.
498- self.useFixture(BuilddSlaveTestSetup())
499-
500 # Sampledata builder is enabled and is assigned to an active job.
501 builder = getUtility(IBuilderSet)[BOB_THE_BUILDER_NAME]
502+ self.patch(
503+ BuilderSlave, 'makeBuilderSlave',
504+ FakeMethod(BuildingSlave(build_id='PACKAGEBUILD-8')))
505 self.assertTrue(builder.builderok)
506 job = builder.currentjob
507 self.assertBuildingJob(job, builder)
508
509+ scanner = self._getScanner()
510+ yield scanner.scan()
511+ self.assertIsNot(None, builder.currentjob)
512+
513 # Disable the sampledata builder
514- login('foo.bar@canonical.com')
515 builder.builderok = False
516 transaction.commit()
517- login(ANONYMOUS)
518
519 # Run 'scan' and check its result.
520- switch_dbuser(config.builddmaster.dbuser)
521- scanner = self._getScanner()
522- d = defer.maybeDeferred(scanner.scan)
523- d.addCallback(self._checkJobRescued, builder, job)
524- return d
525+ slave = yield scanner.scan()
526+ self.assertIs(None, builder.currentjob)
527+ self._checkJobRescued(slave, builder, job)
528
529 def _checkJobUpdated(self, slave, builder, job):
530 """`SlaveScanner.scan` updates legitimate jobs.
531@@ -223,7 +226,7 @@
532 login('foo.bar@canonical.com')
533 builder.builderok = True
534 self.patch(BuilderSlave, 'makeBuilderSlave',
535- FakeMethod(BuildingSlave(build_id='8-1')))
536+ FakeMethod(BuildingSlave(build_id='PACKAGEBUILD-8')))
537 transaction.commit()
538 login(ANONYMOUS)
539
540@@ -437,6 +440,75 @@
541 self.assertEqual(BuildStatus.CANCELLED, build.status)
542
543
544+class FakeBuildQueue:
545+
546+ def __init__(self):
547+ self.id = 1
548+ self.reset = FakeMethod()
549+
550+
551+class TestSlaveScannerWithoutDB(TestCase):
552+
553+ run_tests_with = AsynchronousDeferredRunTest
554+
555+ @defer.inlineCallbacks
556+ def test_scan_with_job(self):
557+ # SlaveScanner.scan calls updateBuild() when a job is building.
558+ interactor = BuilderInteractor(
559+ MockBuilder(), BuildingSlave('trivial'), TrivialBehavior())
560+ scanner = SlaveScanner('mock', BufferLogger())
561+
562+ # Instrument updateBuild and currentjob.reset
563+ interactor.updateBuild = FakeMethod()
564+ interactor.builder.currentjob = FakeBuildQueue()
565+ # XXX: checkCancellation needs more than a FakeBuildQueue.
566+ scanner.checkCancellation = FakeMethod(defer.succeed(False))
567+
568+ yield scanner.scan(builder=interactor.builder, interactor=interactor)
569+ self.assertEqual(['status'], interactor.slave.call_log)
570+ self.assertEqual(1, interactor.updateBuild.call_count)
571+ self.assertEqual(0, interactor.builder.currentjob.reset.call_count)
572+
573+ @defer.inlineCallbacks
574+ def test_scan_aborts_lost_slave_with_job(self):
575+ # SlaveScanner.scan uses BuilderInteractor.rescueIfLost to abort
576+ # slaves that don't have the expected job.
577+ interactor = BuilderInteractor(
578+ MockBuilder(), BuildingSlave('nontrivial'), TrivialBehavior())
579+ scanner = SlaveScanner('mock', BufferLogger())
580+
581+ # Instrument updateBuild and currentjob.reset
582+ interactor.updateBuild = FakeMethod()
583+ interactor.builder.currentjob = FakeBuildQueue()
584+ # XXX: checkCancellation needs more than a FakeBuildQueue.
585+ scanner.checkCancellation = FakeMethod(defer.succeed(False))
586+
587+ # A single scan will call status(), notice that the slave is
588+ # lost, abort() the slave, then reset() the job without calling
589+ # updateBuild().
590+ yield scanner.scan(builder=interactor.builder, interactor=interactor)
591+ self.assertEqual(['status', 'abort'], interactor.slave.call_log)
592+ self.assertEqual(0, interactor.updateBuild.call_count)
593+ self.assertEqual(1, interactor.builder.currentjob.reset.call_count)
594+
595+ @defer.inlineCallbacks
596+ def test_scan_aborts_lost_slave_when_idle(self):
597+ # SlaveScanner.scan uses BuilderInteractor.rescueIfLost to abort
598+ # slaves that aren't meant to have a job.
599+ interactor = BuilderInteractor(MockBuilder(), BuildingSlave(), None)
600+ scanner = SlaveScanner('mock', BufferLogger())
601+
602+ # Instrument updateBuild.
603+ interactor.updateBuild = FakeMethod()
604+
605+ # A single scan will call status(), notice that the slave is
606+ # lost, abort() the slave, then reset() the job without calling
607+ # updateBuild().
608+ yield scanner.scan(builder=interactor.builder, interactor=interactor)
609+ self.assertEqual(['status', 'abort'], interactor.slave.call_log)
610+ self.assertEqual(0, interactor.updateBuild.call_count)
611+
612+
613 class TestCancellationChecking(TestCaseWithFactory):
614 """Unit tests for the checkCancellation method."""
615