Package buildbot :: Package status :: Module builder
[frames] | no frames]

Source Code for Module buildbot.status.builder

  1  # This file is part of Buildbot.  Buildbot is free software: you can 
  2  # redistribute it and/or modify it under the terms of the GNU General Public 
  3  # License as published by the Free Software Foundation, version 2. 
  4  # 
  5  # This program is distributed in the hope that it will be useful, but WITHOUT 
  6  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
  8  # details. 
  9  # 
 10  # You should have received a copy of the GNU General Public License along with 
 11  # this program; if not, write to the Free Software Foundation, Inc., 51 
 12  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 13  # 
 14  # Copyright Buildbot Team Members 
 15   
 16   
 17  import weakref 
 18  import os, re, itertools 
 19  from cPickle import load, dump 
 20   
 21  from zope.interface import implements 
 22  from twisted.python import log, runtime 
 23  from twisted.persisted import styles 
 24  from buildbot.process import metrics 
 25  from buildbot import interfaces, util 
 26  from buildbot.status.event import Event 
 27  from buildbot.status.build import BuildStatus 
 28  from buildbot.status.buildrequest import BuildRequestStatus 
 29   
 30  # user modules expect these symbols to be present here 
 31  from buildbot.status.results import SUCCESS, WARNINGS, FAILURE, SKIPPED 
 32  from buildbot.status.results import EXCEPTION, RETRY, Results, worst_status 
 33  _hush_pyflakes = [ SUCCESS, WARNINGS, FAILURE, SKIPPED, 
 34                     EXCEPTION, RETRY, Results, worst_status ] 
 35   
36 -class BuilderStatus(styles.Versioned):
37 """I handle status information for a single process.build.Builder object. 38 That object sends status changes to me (frequently as Events), and I 39 provide them on demand to the various status recipients, like the HTML 40 waterfall display and the live status clients. It also sends build 41 summaries to me, which I log and provide to status clients who aren't 42 interested in seeing details of the individual build steps. 43 44 I am responsible for maintaining the list of historic Events and Builds, 45 pruning old ones, and loading them from / saving them to disk. 46 47 I live in the buildbot.process.build.Builder object, in the 48 .builder_status attribute. 49 50 @type category: string 51 @ivar category: user-defined category this builder belongs to; can be 52 used to filter on in status clients 53 """ 54 55 implements(interfaces.IBuilderStatus, interfaces.IEventSource) 56 57 persistenceVersion = 1 58 persistenceForgets = ( 'wasUpgraded', ) 59 60 category = None 61 currentBigState = "offline" # or idle/waiting/interlocked/building 62 basedir = None # filled in by our parent 63
64 - def __init__(self, buildername, category, master):
65 self.name = buildername 66 self.category = category 67 self.master = master 68 69 self.slavenames = [] 70 self.events = [] 71 # these three hold Events, and are used to retrieve the current 72 # state of the boxes. 73 self.lastBuildStatus = None 74 #self.currentBig = None 75 #self.currentSmall = None 76 self.currentBuilds = [] 77 self.nextBuild = None 78 self.watchers = [] 79 self.buildCache = weakref.WeakValueDictionary() 80 self.buildCache_LRU = []
81 82 # persistence 83
84 - def __getstate__(self):
85 # when saving, don't record transient stuff like what builds are 86 # currently running, because they won't be there when we start back 87 # up. Nor do we save self.watchers, nor anything that gets set by our 88 # parent like .basedir and .status 89 d = styles.Versioned.__getstate__(self) 90 d['watchers'] = [] 91 del d['buildCache'] 92 del d['buildCache_LRU'] 93 for b in self.currentBuilds: 94 b.saveYourself() 95 # TODO: push a 'hey, build was interrupted' event 96 del d['currentBuilds'] 97 d.pop('pendingBuilds', None) 98 del d['currentBigState'] 99 del d['basedir'] 100 del d['status'] 101 del d['nextBuildNumber'] 102 del d['master'] 103 return d
104
105 - def __setstate__(self, d):
106 # when loading, re-initialize the transient stuff. Remember that 107 # upgradeToVersion1 and such will be called after this finishes. 108 styles.Versioned.__setstate__(self, d) 109 self.buildCache = weakref.WeakValueDictionary() 110 self.buildCache_LRU = [] 111 self.currentBuilds = [] 112 self.watchers = [] 113 self.slavenames = []
114 # self.basedir must be filled in by our parent 115 # self.status must be filled in by our parent 116 # self.master must be filled in by our parent 117
118 - def upgradeToVersion1(self):
119 if hasattr(self, 'slavename'): 120 self.slavenames = [self.slavename] 121 del self.slavename 122 if hasattr(self, 'nextBuildNumber'): 123 del self.nextBuildNumber # determineNextBuildNumber chooses this 124 self.wasUpgraded = True
125
126 - def determineNextBuildNumber(self):
127 """Scan our directory of saved BuildStatus instances to determine 128 what our self.nextBuildNumber should be. Set it one larger than the 129 highest-numbered build we discover. This is called by the top-level 130 Status object shortly after we are created or loaded from disk. 131 """ 132 existing_builds = [int(f) 133 for f in os.listdir(self.basedir) 134 if re.match("^\d+$", f)] 135 if existing_builds: 136 self.nextBuildNumber = max(existing_builds) + 1 137 else: 138 self.nextBuildNumber = 0
139
140 - def saveYourself(self):
141 for b in self.currentBuilds: 142 if not b.isFinished: 143 # interrupted build, need to save it anyway. 144 # BuildStatus.saveYourself will mark it as interrupted. 145 b.saveYourself() 146 filename = os.path.join(self.basedir, "builder") 147 tmpfilename = filename + ".tmp" 148 try: 149 dump(self, open(tmpfilename, "wb"), -1) 150 if runtime.platformType == 'win32': 151 # windows cannot rename a file on top of an existing one 152 if os.path.exists(filename): 153 os.unlink(filename) 154 os.rename(tmpfilename, filename) 155 except: 156 log.msg("unable to save builder %s" % self.name) 157 log.err()
158 159 160 # build cache management 161
162 - def makeBuildFilename(self, number):
163 return os.path.join(self.basedir, "%d" % number)
164
165 - def touchBuildCache(self, build):
166 self.buildCache[build.number] = build 167 if build in self.buildCache_LRU: 168 self.buildCache_LRU.remove(build) 169 cache_size = self.master.config.caches['Builds'] 170 self.buildCache_LRU = self.buildCache_LRU[-(cache_size-1):] + [ build ] 171 return build
172
173 - def getBuildByNumber(self, number):
174 # first look in currentBuilds 175 for b in self.currentBuilds: 176 if b.number == number: 177 return self.touchBuildCache(b) 178 179 # then in the buildCache 180 try: 181 b = self.buildCache[number] 182 except KeyError: 183 metrics.MetricCountEvent.log("buildCache.misses", 1) 184 else: 185 metrics.MetricCountEvent.log("buildCache.hits", 1) 186 return self.touchBuildCache(b) 187 188 # then fall back to loading it from disk 189 filename = self.makeBuildFilename(number) 190 try: 191 log.msg("Loading builder %s's build %d from on-disk pickle" 192 % (self.name, number)) 193 build = load(open(filename, "rb")) 194 build.setProcessObjects(self, self.master) 195 196 # (bug #1068) if we need to upgrade, we probably need to rewrite 197 # this pickle, too. We determine this by looking at the list of 198 # Versioned objects that have been unpickled, and (after doUpgrade) 199 # checking to see if any of them set wasUpgraded. The Versioneds' 200 # upgradeToVersionNN methods all set this. 201 versioneds = styles.versionedsToUpgrade 202 styles.doUpgrade() 203 if True in [ hasattr(o, 'wasUpgraded') for o in versioneds.values() ]: 204 log.msg("re-writing upgraded build pickle") 205 build.saveYourself() 206 207 # check that logfiles exist 208 build.checkLogfiles() 209 return self.touchBuildCache(build) 210 except IOError: 211 raise IndexError("no such build %d" % number) 212 except EOFError: 213 raise IndexError("corrupted build pickle %d" % number)
214
215 - def prune(self, events_only=False):
216 # begin by pruning our own events 217 eventHorizon = self.master.config.eventHorizon 218 self.events = self.events[-eventHorizon:] 219 220 if events_only: 221 return 222 223 # get the horizons straight 224 buildHorizon = self.master.config.buildHorizon 225 if buildHorizon is not None: 226 earliest_build = self.nextBuildNumber - self.buildHorizon 227 else: 228 earliest_build = 0 229 230 logHorizon = self.master.config.logHorizon 231 if logHorizon is not None: 232 earliest_log = self.nextBuildNumber - logHorizon 233 else: 234 earliest_log = 0 235 236 if earliest_log < earliest_build: 237 earliest_log = earliest_build 238 239 if earliest_build == 0: 240 return 241 242 # skim the directory and delete anything that shouldn't be there anymore 243 build_re = re.compile(r"^([0-9]+)$") 244 build_log_re = re.compile(r"^([0-9]+)-.*$") 245 # if the directory doesn't exist, bail out here 246 if not os.path.exists(self.basedir): 247 return 248 249 for filename in os.listdir(self.basedir): 250 num = None 251 mo = build_re.match(filename) 252 is_logfile = False 253 if mo: 254 num = int(mo.group(1)) 255 else: 256 mo = build_log_re.match(filename) 257 if mo: 258 num = int(mo.group(1)) 259 is_logfile = True 260 261 if num is None: continue 262 if num in self.buildCache: continue 263 264 if (is_logfile and num < earliest_log) or num < earliest_build: 265 pathname = os.path.join(self.basedir, filename) 266 log.msg("pruning '%s'" % pathname) 267 try: os.unlink(pathname) 268 except OSError: pass
269 270 # IBuilderStatus methods
271 - def getName(self):
272 return self.name
273
274 - def getState(self):
275 return (self.currentBigState, self.currentBuilds)
276
277 - def getSlaves(self):
278 return [self.status.getSlave(name) for name in self.slavenames]
279
281 db = self.status.master.db 282 d = db.buildrequests.getBuildRequests(claimed=False, 283 buildername=self.name) 284 def make_statuses(brdicts): 285 return [BuildRequestStatus(self.name, brdict['brid'], 286 self.status) 287 for brdict in brdicts]
288 d.addCallback(make_statuses) 289 return d
290
291 - def getCurrentBuilds(self):
292 return self.currentBuilds
293
294 - def getLastFinishedBuild(self):
295 b = self.getBuild(-1) 296 if not (b and b.isFinished()): 297 b = self.getBuild(-2) 298 return b
299
300 - def getCategory(self):
301 return self.category
302
303 - def getBuild(self, number):
304 if number < 0: 305 number = self.nextBuildNumber + number 306 if number < 0 or number >= self.nextBuildNumber: 307 return None 308 309 try: 310 return self.getBuildByNumber(number) 311 except IndexError: 312 return None
313
314 - def getEvent(self, number):
315 try: 316 return self.events[number] 317 except IndexError: 318 return None
319
320 - def generateFinishedBuilds(self, branches=[], 321 num_builds=None, 322 max_buildnum=None, 323 finished_before=None, 324 max_search=200):
325 got = 0 326 for Nb in itertools.count(1): 327 if Nb > self.nextBuildNumber: 328 break 329 if Nb > max_search: 330 break 331 build = self.getBuild(-Nb) 332 if build is None: 333 continue 334 if max_buildnum is not None: 335 if build.getNumber() > max_buildnum: 336 continue 337 if not build.isFinished(): 338 continue 339 if finished_before is not None: 340 start, end = build.getTimes() 341 if end >= finished_before: 342 continue 343 if branches: 344 if build.getSourceStamp().branch not in branches: 345 continue 346 got += 1 347 yield build 348 if num_builds is not None: 349 if got >= num_builds: 350 return
351
352 - def eventGenerator(self, branches=[], categories=[], committers=[], minTime=0):
353 """This function creates a generator which will provide all of this 354 Builder's status events, starting with the most recent and 355 progressing backwards in time. """ 356 357 # remember the oldest-to-earliest flow here. "next" means earlier. 358 359 # TODO: interleave build steps and self.events by timestamp. 360 # TODO: um, I think we're already doing that. 361 362 # TODO: there's probably something clever we could do here to 363 # interleave two event streams (one from self.getBuild and the other 364 # from self.getEvent), which would be simpler than this control flow 365 366 eventIndex = -1 367 e = self.getEvent(eventIndex) 368 for Nb in range(1, self.nextBuildNumber+1): 369 b = self.getBuild(-Nb) 370 if not b: 371 # HACK: If this is the first build we are looking at, it is 372 # possible it's in progress but locked before it has written a 373 # pickle; in this case keep looking. 374 if Nb == 1: 375 continue 376 break 377 if b.getTimes()[0] < minTime: 378 break 379 if branches and not b.getSourceStamp().branch in branches: 380 continue 381 if categories and not b.getBuilder().getCategory() in categories: 382 continue 383 if committers and not [True for c in b.getChanges() if c.who in committers]: 384 continue 385 steps = b.getSteps() 386 for Ns in range(1, len(steps)+1): 387 if steps[-Ns].started: 388 step_start = steps[-Ns].getTimes()[0] 389 while e is not None and e.getTimes()[0] > step_start: 390 yield e 391 eventIndex -= 1 392 e = self.getEvent(eventIndex) 393 yield steps[-Ns] 394 yield b 395 while e is not None: 396 yield e 397 eventIndex -= 1 398 e = self.getEvent(eventIndex) 399 if e and e.getTimes()[0] < minTime: 400 break
401
402 - def subscribe(self, receiver):
403 # will get builderChangedState, buildStarted, buildFinished, 404 # requestSubmitted, requestCancelled. Note that a request which is 405 # resubmitted (due to a slave disconnect) will cause requestSubmitted 406 # to be invoked multiple times. 407 self.watchers.append(receiver) 408 self.publishState(receiver) 409 # our parent Status provides requestSubmitted and requestCancelled 410 self.status._builder_subscribe(self.name, receiver)
411
412 - def unsubscribe(self, receiver):
413 self.watchers.remove(receiver) 414 self.status._builder_unsubscribe(self.name, receiver)
415 416 ## Builder interface (methods called by the Builder which feeds us) 417
418 - def setSlavenames(self, names):
419 self.slavenames = names
420
421 - def addEvent(self, text=[]):
422 # this adds a duration event. When it is done, the user should call 423 # e.finish(). They can also mangle it by modifying .text 424 e = Event() 425 e.started = util.now() 426 e.text = text 427 self.events.append(e) 428 self.prune(events_only=True) 429 return e # they are free to mangle it further
430
431 - def addPointEvent(self, text=[]):
432 # this adds a point event, one which occurs as a single atomic 433 # instant of time. 434 e = Event() 435 e.started = util.now() 436 e.finished = 0 437 e.text = text 438 self.events.append(e) 439 self.prune(events_only=True) 440 return e # for consistency, but they really shouldn't touch it
441
442 - def setBigState(self, state):
443 needToUpdate = state != self.currentBigState 444 self.currentBigState = state 445 if needToUpdate: 446 self.publishState()
447
448 - def publishState(self, target=None):
449 state = self.currentBigState 450 451 if target is not None: 452 # unicast 453 target.builderChangedState(self.name, state) 454 return 455 for w in self.watchers: 456 try: 457 w.builderChangedState(self.name, state) 458 except: 459 log.msg("Exception caught publishing state to %r" % w) 460 log.err()
461
462 - def newBuild(self):
463 """The Builder has decided to start a build, but the Build object is 464 not yet ready to report status (it has not finished creating the 465 Steps). Create a BuildStatus object that it can use.""" 466 number = self.nextBuildNumber 467 self.nextBuildNumber += 1 468 # TODO: self.saveYourself(), to make sure we don't forget about the 469 # build number we've just allocated. This is not quite as important 470 # as it was before we switch to determineNextBuildNumber, but I think 471 # it may still be useful to have the new build save itself. 472 s = BuildStatus(self, self.master, number) 473 s.waitUntilFinished().addCallback(self._buildFinished) 474 return s
475 476 # buildStarted is called by our child BuildStatus instances
477 - def buildStarted(self, s):
478 """Now the BuildStatus object is ready to go (it knows all of its 479 Steps, its ETA, etc), so it is safe to notify our watchers.""" 480 481 assert s.builder is self # paranoia 482 assert s not in self.currentBuilds 483 self.currentBuilds.append(s) 484 self.touchBuildCache(s) 485 486 # now that the BuildStatus is prepared to answer queries, we can 487 # announce the new build to all our watchers 488 489 for w in self.watchers: # TODO: maybe do this later? callLater(0)? 490 try: 491 receiver = w.buildStarted(self.getName(), s) 492 if receiver: 493 if type(receiver) == type(()): 494 s.subscribe(receiver[0], receiver[1]) 495 else: 496 s.subscribe(receiver) 497 d = s.waitUntilFinished() 498 d.addCallback(lambda s: s.unsubscribe(receiver)) 499 except: 500 log.msg("Exception caught notifying %r of buildStarted event" % w) 501 log.err()
502
503 - def _buildFinished(self, s):
504 assert s in self.currentBuilds 505 s.saveYourself() 506 self.currentBuilds.remove(s) 507 508 name = self.getName() 509 results = s.getResults() 510 for w in self.watchers: 511 try: 512 w.buildFinished(name, s, results) 513 except: 514 log.msg("Exception caught notifying %r of buildFinished event" % w) 515 log.err() 516 517 self.prune() # conserve disk
518 519
520 - def asDict(self):
521 result = {} 522 # Constant 523 # TODO(maruel): Fix me. We don't want to leak the full path. 524 result['basedir'] = os.path.basename(self.basedir) 525 result['category'] = self.category 526 result['slaves'] = self.slavenames 527 result['schedulers'] = [ s.name 528 for s in self.status.master.allSchedulers() 529 if self.name in s.builderNames ] 530 #result['url'] = self.parent.getURLForThing(self) 531 # TODO(maruel): Add cache settings? Do we care? 532 533 # Transient 534 # Collect build numbers. 535 # Important: Only grab the *cached* builds numbers to reduce I/O. 536 current_builds = [b.getNumber() for b in self.currentBuilds] 537 cached_builds = list(set(self.buildCache.keys() + current_builds)) 538 cached_builds.sort() 539 result['cachedBuilds'] = cached_builds 540 result['currentBuilds'] = current_builds 541 result['state'] = self.getState()[0] 542 # lies, but we don't have synchronous access to this info; use 543 # asDict_async instead 544 result['pendingBuilds'] = 0 545 return result
546
547 - def asDict_async(self):
548 """Just like L{asDict}, but with a nonzero pendingBuilds.""" 549 result = self.asDict() 550 d = self.getPendingBuildRequestStatuses() 551 def combine(statuses): 552 result['pendingBuilds'] = len(statuses) 553 return result
554 d.addCallback(combine) 555 return d 556
557 - def getMetrics(self):
558 return self.botmaster.parent.metrics
559 560 # vim: set ts=4 sts=4 sw=4 et: 561