1
2 import random, weakref
3 from zope.interface import implements
4 from twisted.python import log
5 from twisted.python.failure import Failure
6 from twisted.spread import pb
7 from twisted.application import service, internet
8 from twisted.internet import defer
9
10 from buildbot import interfaces, util
11 from buildbot.status.progress import Expectations
12 from buildbot.status.builder import RETRY
13 from buildbot.process.properties import Properties
14 from buildbot.util.eventual import eventually
15
16 (ATTACHING,
17 IDLE,
18 PINGING,
19 BUILDING,
20 LATENT,
21 SUBSTANTIATING,
22 ) = range(6)
23
24
26 """I am the master-side representative for one of the
27 L{buildbot.slave.bot.SlaveBuilder} objects that lives in a remote
28 buildbot. When a remote builder connects, I query it for command versions
29 and then make it available to any Builds that are ready to run. """
30
32 self.ping_watchers = []
33 self.state = None
34 self.remote = None
35 self.slave = None
36 self.builder_name = None
37
39 r = ["<", self.__class__.__name__]
40 if self.builder_name:
41 r.extend([" builder=", repr(self.builder_name)])
42 if self.slave:
43 r.extend([" slave=", repr(self.slave.slavename)])
44 r.append(">")
45 return ''.join(r)
46
50
52 if self.remoteCommands is None:
53
54 return oldversion
55 return self.remoteCommands.get(command)
56
68
71
74
78
79 - def attached(self, slave, remote, commands):
80 """
81 @type slave: L{buildbot.buildslave.BuildSlave}
82 @param slave: the BuildSlave that represents the buildslave as a
83 whole
84 @type remote: L{twisted.spread.pb.RemoteReference}
85 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
86 @type commands: dict: string -> string, or None
87 @param commands: provides the slave's version of each RemoteCommand
88 """
89 self.state = ATTACHING
90 self.remote = remote
91 self.remoteCommands = commands
92 if self.slave is None:
93 self.slave = slave
94 self.slave.addSlaveBuilder(self)
95 else:
96 assert self.slave == slave
97 log.msg("Buildslave %s attached to %s" % (slave.slavename,
98 self.builder_name))
99 d = self.remote.callRemote("setMaster", self)
100 d.addErrback(self._attachFailure, "Builder.setMaster")
101 d.addCallback(self._attached2)
102 return d
103
105 d = self.remote.callRemote("print", "attached")
106 d.addErrback(self._attachFailure, "Builder.print 'attached'")
107 d.addCallback(self._attached3)
108 return d
109
111
112 self.state = IDLE
113 return self
114
116 assert isinstance(where, str)
117 log.msg(where)
118 log.err(why)
119 return why
120
121 - def prepare(self, builder_status):
122 return defer.succeed(None)
123
124 - def ping(self, status=None):
125 """Ping the slave to make sure it is still there. Returns a Deferred
126 that fires with True if it is.
127
128 @param status: if you point this at a BuilderStatus, a 'pinging'
129 event will be pushed.
130 """
131 oldstate = self.state
132 self.state = PINGING
133 newping = not self.ping_watchers
134 d = defer.Deferred()
135 self.ping_watchers.append(d)
136 if newping:
137 if status:
138 event = status.addEvent(["pinging"])
139 d2 = defer.Deferred()
140 d2.addCallback(self._pong_status, event)
141 self.ping_watchers.insert(0, d2)
142
143
144 Ping().ping(self.remote).addCallback(self._pong)
145
146 def reset_state(res):
147 if self.state == PINGING:
148 self.state = oldstate
149 return res
150 d.addCallback(reset_state)
151 return d
152
154 watchers, self.ping_watchers = self.ping_watchers, []
155 for d in watchers:
156 d.callback(res)
157
159 if res:
160 event.text = ["ping", "success"]
161 else:
162 event.text = ["ping", "failed"]
163 event.finish()
164
173
174
176 running = False
177
178 - def ping(self, remote):
179 assert not self.running
180 if not remote:
181
182 return defer.succeed(False)
183 self.running = True
184 log.msg("sending ping")
185 self.d = defer.Deferred()
186
187
188 remote.callRemote("print", "ping").addCallbacks(self._pong,
189 self._ping_failed,
190 errbackArgs=(remote,))
191 return self.d
192
194 log.msg("ping finished: success")
195 self.d.callback(True)
196
198 log.msg("ping finished: failure")
199
200
201
202 remote.broker.transport.loseConnection()
203
204
205
206 self.d.callback(False)
207
208
232
233
243
244 - def prepare(self, builder_status):
253 d.addErrback(substantiation_failed)
254 return d
255
257 self.state = SUBSTANTIATING
258 d = self.slave.substantiate(self)
259 if not self.slave.substantiated:
260 event = self.builder.builder_status.addEvent(
261 ["substantiating"])
262 def substantiated(res):
263 msg = ["substantiate", "success"]
264 if isinstance(res, basestring):
265 msg.append(res)
266 elif isinstance(res, (tuple, list)):
267 msg.extend(res)
268 event.text = msg
269 event.finish()
270 return res
271 def substantiation_failed(res):
272 event.text = ["substantiate", "failed"]
273
274 event.finish()
275 return res
276 d.addCallbacks(substantiated, substantiation_failed)
277 return d
278
282
286
290
294
295 - def ping(self, status=None):
301
302
303 -class Builder(pb.Referenceable, service.MultiService):
304 """I manage all Builds of a given type.
305
306 Each Builder is created by an entry in the config file (the c['builders']
307 list), with a number of parameters.
308
309 One of these parameters is the L{buildbot.process.factory.BuildFactory}
310 object that is associated with this Builder. The factory is responsible
311 for creating new L{Build<buildbot.process.base.Build>} objects. Each
312 Build object defines when and how the build is performed, so a new
313 Factory or Builder should be defined to control this behavior.
314
315 The Builder holds on to a number of L{base.BuildRequest} objects in a
316 list named C{.buildable}. Incoming BuildRequest objects will be added to
317 this list, or (if possible) merged into an existing request. When a slave
318 becomes available, I will use my C{BuildFactory} to turn the request into
319 a new C{Build} object. The C{BuildRequest} is forgotten, the C{Build}
320 goes into C{.building} while it runs. Once the build finishes, I will
321 discard it.
322
323 I maintain a list of available SlaveBuilders, one for each connected
324 slave that the C{slavenames} parameter says we can use. Some of these
325 will be idle, some of them will be busy running builds for me. If there
326 are multiple slaves, I can run multiple builds at once.
327
328 I also manage forced builds, progress expectation (ETA) management, and
329 some status delivery chores.
330
331 @type buildable: list of L{buildbot.process.base.BuildRequest}
332 @ivar buildable: BuildRequests that are ready to build, but which are
333 waiting for a buildslave to be available.
334
335 @type building: list of L{buildbot.process.base.Build}
336 @ivar building: Builds that are actively running
337
338 @type slaves: list of L{buildbot.buildslave.BuildSlave} objects
339 @ivar slaves: the slaves currently available for building
340 """
341
342 expectations = None
343 CHOOSE_SLAVES_RANDOMLY = True
344
345 - def __init__(self, setup, builder_status):
346 """
347 @type setup: dict
348 @param setup: builder setup data, as stored in
349 BuildmasterConfig['builders']. Contains name,
350 slavename(s), builddir, slavebuilddir, factory, locks.
351 @type builder_status: L{buildbot.status.builder.BuilderStatus}
352 """
353 service.MultiService.__init__(self)
354 self.name = setup['name']
355 self.slavenames = []
356 if setup.has_key('slavename'):
357 self.slavenames.append(setup['slavename'])
358 if setup.has_key('slavenames'):
359 self.slavenames.extend(setup['slavenames'])
360 self.builddir = setup['builddir']
361 self.slavebuilddir = setup['slavebuilddir']
362 self.buildFactory = setup['factory']
363 self.nextSlave = setup.get('nextSlave')
364 if self.nextSlave is not None and not callable(self.nextSlave):
365 raise ValueError("nextSlave must be callable")
366 self.locks = setup.get("locks", [])
367 self.env = setup.get('env', {})
368 assert isinstance(self.env, dict)
369 if setup.has_key('periodicBuildTime'):
370 raise ValueError("periodicBuildTime can no longer be defined as"
371 " part of the Builder: use scheduler.Periodic"
372 " instead")
373 self.nextBuild = setup.get('nextBuild')
374 if self.nextBuild is not None and not callable(self.nextBuild):
375 raise ValueError("nextBuild must be callable")
376 self.buildHorizon = setup.get('buildHorizon')
377 self.logHorizon = setup.get('logHorizon')
378 self.eventHorizon = setup.get('eventHorizon')
379 self.mergeRequests = setup.get('mergeRequests', True)
380 self.properties = setup.get('properties', {})
381
382
383 self.building = []
384
385 self.old_building = weakref.WeakKeyDictionary()
386
387
388
389 self.attaching_slaves = []
390
391
392
393
394 self.slaves = []
395
396 self.builder_status = builder_status
397 self.builder_status.setSlavenames(self.slavenames)
398 self.builder_status.buildHorizon = self.buildHorizon
399 self.builder_status.logHorizon = self.logHorizon
400 self.builder_status.eventHorizon = self.eventHorizon
401 t = internet.TimerService(10*60, self.reclaimAllBuilds)
402 t.setServiceParent(self)
403
404
405 self.watchers = {'attach': [], 'detach': [], 'detach_all': [],
406 'idle': []}
407 self.run_count = 0
408
410 self.botmaster = botmaster
411 self.db = botmaster.db
412 self.master_name = botmaster.master_name
413 self.master_incarnation = botmaster.master_incarnation
414
416 diffs = []
417 setup_slavenames = []
418 if setup.has_key('slavename'):
419 setup_slavenames.append(setup['slavename'])
420 setup_slavenames.extend(setup.get('slavenames', []))
421 if setup_slavenames != self.slavenames:
422 diffs.append('slavenames changed from %s to %s' \
423 % (self.slavenames, setup_slavenames))
424 if setup['builddir'] != self.builddir:
425 diffs.append('builddir changed from %s to %s' \
426 % (self.builddir, setup['builddir']))
427 if setup['slavebuilddir'] != self.slavebuilddir:
428 diffs.append('slavebuilddir changed from %s to %s' \
429 % (self.slavebuilddir, setup['slavebuilddir']))
430 if setup['factory'] != self.buildFactory:
431 diffs.append('factory changed')
432 if setup.get('locks', []) != self.locks:
433 diffs.append('locks changed from %s to %s' % (self.locks, setup.get('locks')))
434 if setup.get('nextSlave') != self.nextSlave:
435 diffs.append('nextSlave changed from %s to %s' % (self.nextSlave, setup.get('nextSlave')))
436 if setup.get('nextBuild') != self.nextBuild:
437 diffs.append('nextBuild changed from %s to %s' % (self.nextBuild, setup.get('nextBuild')))
438 if setup['buildHorizon'] != self.buildHorizon:
439 diffs.append('buildHorizon changed from %s to %s' % (self.buildHorizon, setup['buildHorizon']))
440 if setup['logHorizon'] != self.logHorizon:
441 diffs.append('logHorizon changed from %s to %s' % (self.logHorizon, setup['logHorizon']))
442 if setup['eventHorizon'] != self.eventHorizon:
443 diffs.append('eventHorizon changed from %s to %s' % (self.eventHorizon, setup['eventHorizon']))
444 return diffs
445
447 return "<Builder '%r' at %d>" % (self.name, id(self))
448
451
453 """Check for work to be done. This should be called any time I might
454 be able to start a job:
455
456 - when the Builder is first created
457 - when a new job has been added to the [buildrequests] DB table
458 - when a slave has connected
459
460 If I have both an available slave and the database contains a
461 BuildRequest that I can handle, I will claim the BuildRequest and
462 start the build. When the build finishes, I will retire the
463 BuildRequest.
464 """
465
466
467
468 assert self.running
469 log.msg("Builder.run %s: %s" % (self, self.slaves))
470 self.run_count += 1
471
472 available_slaves = [sb for sb in self.slaves if sb.isAvailable()]
473 if not available_slaves:
474 self.updateBigStatus()
475 return
476 d = self.db.runInteraction(self._claim_buildreqs, available_slaves)
477 d.addCallback(self._start_builds)
478 return d
479
480
481
482 RECLAIM_INTERVAL = 1*3600
483
485
486 now = util.now()
487 old = now - self.RECLAIM_INTERVAL
488 requests = self.db.get_unclaimed_buildrequests(self.name, old,
489 self.master_name,
490 self.master_incarnation,
491 t)
492
493 assignments = {}
494 while requests and available_slaves:
495 sb = self._choose_slave(available_slaves)
496 if not sb:
497 log.msg("%s: want to start build, but we don't have a remote"
498 % self)
499 break
500 available_slaves.remove(sb)
501 breq = self._choose_build(requests)
502 if not breq:
503 log.msg("%s: went to start build, but nextBuild said not to"
504 % self)
505 break
506 requests.remove(breq)
507 merged_requests = [breq]
508 for other_breq in requests[:]:
509 if (self.mergeRequests and
510 self.botmaster.shouldMergeRequests(self, breq, other_breq)
511 ):
512 requests.remove(other_breq)
513 merged_requests.append(other_breq)
514 assignments[sb] = merged_requests
515 brids = [br.id for br in merged_requests]
516 self.db.claim_buildrequests(now, self.master_name,
517 self.master_incarnation, brids, t)
518 return assignments
519
521
522
523 if self.nextSlave:
524 try:
525 return self.nextSlave(self, available_slaves)
526 except:
527 log.msg("Exception choosing next slave")
528 log.err(Failure())
529 return None
530 if self.CHOOSE_SLAVES_RANDOMLY:
531 return random.choice(available_slaves)
532 return available_slaves[0]
533
535 if self.nextBuild:
536 try:
537 return self.nextBuild(self, buildable)
538 except:
539 log.msg("Exception choosing next build")
540 log.err(Failure())
541 return None
542 return buildable[0]
543
557
558
568
570 """Returns the timestamp of the oldest build request for this builder.
571
572 If there are no build requests, None is returned."""
573 buildable = self.getBuildable()
574 if buildable:
575
576 return buildable[0].getSubmitTime()
577 return None
578
581
583 """Suck the brain out of an old Builder.
584
585 This takes all the runtime state from an existing Builder and moves
586 it into ourselves. This is used when a Builder is changed in the
587 master.cfg file: the new Builder has a different factory, but we want
588 all the builds that were queued for the old one to get processed by
589 the new one. Any builds which are already running will keep running.
590 The new Builder will get as many of the old SlaveBuilder objects as
591 it wants."""
592
593 log.msg("consumeTheSoulOfYourPredecessor: %s feeding upon %s" %
594 (self, old))
595
596
597
598
599
600
601
602
603
604 if old.building:
605 self.builder_status.setBigState("building")
606
607
608
609
610 for b in old.building:
611 self.old_building[b] = None
612 for b in old.old_building:
613 self.old_building[b] = None
614
615
616
617 for sb in old.slaves[:]:
618 if sb.slave.slavename in self.slavenames:
619 log.msg(" stealing buildslave %s" % sb)
620 self.slaves.append(sb)
621 old.slaves.remove(sb)
622 sb.setBuilder(self)
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643 return
644
646 now = util.now()
647 brids = set()
648 for b in self.building:
649 brids.update([br.id for br in b.requests])
650 for b in self.old_building:
651 brids.update([br.id for br in b.requests])
652 self.db.claim_buildrequests(now, self.master_name,
653 self.master_incarnation, brids)
654
663
671
683
684 - def attached(self, slave, remote, commands):
685 """This is invoked by the BuildSlave when the self.slavename bot
686 registers their builder.
687
688 @type slave: L{buildbot.buildslave.BuildSlave}
689 @param slave: the BuildSlave that represents the buildslave as a whole
690 @type remote: L{twisted.spread.pb.RemoteReference}
691 @param remote: a reference to the L{buildbot.slave.bot.SlaveBuilder}
692 @type commands: dict: string -> string, or None
693 @param commands: provides the slave's version of each RemoteCommand
694
695 @rtype: L{twisted.internet.defer.Deferred}
696 @return: a Deferred that fires (with 'self') when the slave-side
697 builder is fully attached and ready to accept commands.
698 """
699 for s in self.attaching_slaves + self.slaves:
700 if s.slave == slave:
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717 return defer.succeed(self)
718
719 sb = SlaveBuilder()
720 sb.setBuilder(self)
721 self.attaching_slaves.append(sb)
722 d = sb.attached(slave, remote, commands)
723 d.addCallback(self._attached)
724 d.addErrback(self._not_attached, slave)
725 return d
726
735
745
779
788
790 """Start a build on the given slave.
791 @param build: the L{base.Build} to start
792 @param sb: the L{SlaveBuilder} which will host this build
793
794 @return: a Deferred which fires with a
795 L{buildbot.interfaces.IBuildControl} that can be used to stop the
796 Build, or to access a L{buildbot.interfaces.IBuildStatus} which will
797 watch the Build as it runs. """
798
799 self.building.append(build)
800 self.updateBigStatus()
801 log.msg("starting build %s using slave %s" % (build, sb))
802 d = sb.prepare(self.builder_status)
803 def _ping(ign):
804
805
806
807
808
809
810
811
812 log.msg("starting build %s.. pinging the slave %s" % (build, sb))
813 return sb.ping()
814 d.addCallback(_ping)
815 d.addCallback(self._startBuild_1, build, sb)
816 return d
817
819 if not res:
820 return self._startBuildFailed("slave ping failed", build, sb)
821
822
823
824 sb.buildStarted()
825 d = sb.remote.callRemote("startBuild")
826 d.addCallbacks(self._startBuild_2, self._startBuildFailed,
827 callbackArgs=(build,sb), errbackArgs=(build,sb))
828 return d
829
844
846
847 log.msg("I tried to tell the slave that the build %s started, but "
848 "remote_startBuild failed: %s" % (build, why))
849
850
851
852 sb.buildFinished()
853
854 log.msg("re-queueing the BuildRequest")
855 self.building.remove(build)
856 self._resubmit_buildreqs(build).addErrback(log.err)
857
863
882
886
888 """Mark the build as successful and update expectations for the next
889 build. Only call this when the build did not fail in any way that
890 would invalidate the time expectations generated by it. (if the
891 compile failed and thus terminated early, we can't use the last
892 build to predict how long the next one will take).
893 """
894 if self.expectations:
895 self.expectations.update(progress)
896 else:
897
898
899 self.expectations = Expectations(progress)
900 log.msg("new expectations: %s seconds" % \
901 self.expectations.expectedBuildTime())
902
906
907
909 implements(interfaces.IBuilderControl)
910
914
920
921 - def rebuildBuild(self, bs, reason="<rebuild, no reason given>", extraProperties=None):
934
942
945
947 if not self.original.slaves:
948 self.original.builder_status.addPointEvent(["ping", "no slave"])
949 return defer.succeed(False)
950 dl = []
951 for s in self.original.slaves:
952 dl.append(s.ping(self.original.builder_status))
953 d = defer.DeferredList(dl)
954 d.addCallback(self._gatherPingResults)
955 return d
956
958 for ignored,success in res:
959 if not success:
960 return False
961 return True
962
964 implements(interfaces.IBuildRequestControl)
965
967 self.original_builder = builder
968 self.original_request = request
969 self.brid = request.id
970
972 raise NotImplementedError
973
975 raise NotImplementedError
976
979