| OLD | NEW |
| 1 # This file is part of Buildbot. Buildbot is free software: you can | 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 | 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. | 3 # License as published by the Free Software Foundation, version 2. |
| 4 # | 4 # |
| 5 # This program is distributed in the hope that it will be useful, but WITHOUT | 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 | 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 | 7 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
| 8 # details. | 8 # details. |
| 9 # | 9 # |
| 10 # You should have received a copy of the GNU General Public License along with | 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 | 11 # this program; if not, write to the Free Software Foundation, Inc., 51 |
| 12 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | 12 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| 13 # | 13 # |
| 14 # Copyright Buildbot Team Members | 14 # Copyright Buildbot Team Members |
| 15 | 15 |
| 16 | 16 |
| 17 import weakref | 17 import weakref |
| 18 import gc | 18 import gc |
| 19 import os, re, itertools | 19 import os, re, itertools |
| 20 from cPickle import load, dump | 20 from cPickle import load, dump |
| 21 | 21 |
| 22 from zope.interface import implements | 22 from zope.interface import implements |
| 23 from twisted.python import log, runtime | 23 from twisted.python import log, runtime |
| 24 from twisted.persisted import styles | 24 from twisted.persisted import styles |
| 25 from buildbot.process import metrics | 25 from buildbot.process import metrics |
| 26 from buildbot import interfaces, util | 26 from buildbot import interfaces, util |
| 27 from buildbot.util.lru import SyncLRUCache |
| 27 from buildbot.status.event import Event | 28 from buildbot.status.event import Event |
| 28 from buildbot.status.build import BuildStatus | 29 from buildbot.status.build import BuildStatus |
| 29 from buildbot.status.buildrequest import BuildRequestStatus | 30 from buildbot.status.buildrequest import BuildRequestStatus |
| 30 | 31 |
| 31 # user modules expect these symbols to be present here | 32 # user modules expect these symbols to be present here |
| 32 from buildbot.status.results import SUCCESS, WARNINGS, FAILURE, SKIPPED | 33 from buildbot.status.results import SUCCESS, WARNINGS, FAILURE, SKIPPED |
| 33 from buildbot.status.results import EXCEPTION, RETRY, Results, worst_status | 34 from buildbot.status.results import EXCEPTION, RETRY, Results, worst_status |
| 34 _hush_pyflakes = [ SUCCESS, WARNINGS, FAILURE, SKIPPED, | 35 _hush_pyflakes = [ SUCCESS, WARNINGS, FAILURE, SKIPPED, |
| 35 EXCEPTION, RETRY, Results, worst_status ] | 36 EXCEPTION, RETRY, Results, worst_status ] |
| 36 | 37 |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 79 self.slavenames = [] | 80 self.slavenames = [] |
| 80 self.events = [] | 81 self.events = [] |
| 81 # these three hold Events, and are used to retrieve the current | 82 # these three hold Events, and are used to retrieve the current |
| 82 # state of the boxes. | 83 # state of the boxes. |
| 83 self.lastBuildStatus = None | 84 self.lastBuildStatus = None |
| 84 #self.currentBig = None | 85 #self.currentBig = None |
| 85 #self.currentSmall = None | 86 #self.currentSmall = None |
| 86 self.currentBuilds = [] | 87 self.currentBuilds = [] |
| 87 self.nextBuild = None | 88 self.nextBuild = None |
| 88 self.watchers = [] | 89 self.watchers = [] |
| 89 self.buildCache = weakref.WeakValueDictionary() | 90 self.buildCache = SyncLRUCache(self.cacheMiss, self.buildCacheSize) |
| 90 self.buildCache_LRU = [] | |
| 91 self.logCompressionLimit = False # default to no compression for tests | 91 self.logCompressionLimit = False # default to no compression for tests |
| 92 self.logCompressionMethod = "bz2" | 92 self.logCompressionMethod = "bz2" |
| 93 self.logMaxSize = None # No default limit | 93 self.logMaxSize = None # No default limit |
| 94 self.logMaxTailSize = None # No tail buffering | 94 self.logMaxTailSize = None # No tail buffering |
| 95 | 95 |
| 96 # persistence | 96 # persistence |
| 97 | 97 |
| 98 def __getstate__(self): | 98 def __getstate__(self): |
| 99 # when saving, don't record transient stuff like what builds are | 99 # when saving, don't record transient stuff like what builds are |
| 100 # currently running, because they won't be there when we start back | 100 # currently running, because they won't be there when we start back |
| 101 # up. Nor do we save self.watchers, nor anything that gets set by our | 101 # up. Nor do we save self.watchers, nor anything that gets set by our |
| 102 # parent like .basedir and .status | 102 # parent like .basedir and .status |
| 103 d = styles.Versioned.__getstate__(self) | 103 d = styles.Versioned.__getstate__(self) |
| 104 d['watchers'] = [] | 104 d['watchers'] = [] |
| 105 del d['buildCache'] | 105 del d['buildCache'] |
| 106 del d['buildCache_LRU'] | |
| 107 for b in self.currentBuilds: | 106 for b in self.currentBuilds: |
| 108 b.saveYourself() | 107 b.saveYourself() |
| 109 # TODO: push a 'hey, build was interrupted' event | 108 # TODO: push a 'hey, build was interrupted' event |
| 110 del d['currentBuilds'] | 109 del d['currentBuilds'] |
| 111 d.pop('pendingBuilds', None) | 110 d.pop('pendingBuilds', None) |
| 112 del d['currentBigState'] | 111 del d['currentBigState'] |
| 113 del d['basedir'] | 112 del d['basedir'] |
| 114 del d['status'] | 113 del d['status'] |
| 115 del d['nextBuildNumber'] | 114 del d['nextBuildNumber'] |
| 116 return d | 115 return d |
| 117 | 116 |
| 118 def __setstate__(self, d): | 117 def __setstate__(self, d): |
| 119 # when loading, re-initialize the transient stuff. Remember that | 118 # when loading, re-initialize the transient stuff. Remember that |
| 120 # upgradeToVersion1 and such will be called after this finishes. | 119 # upgradeToVersion1 and such will be called after this finishes. |
| 121 styles.Versioned.__setstate__(self, d) | 120 styles.Versioned.__setstate__(self, d) |
| 122 self.buildCache = weakref.WeakValueDictionary() | 121 self.buildCache = SyncLRUCache(self.cacheMiss, self.buildCacheSize) |
| 123 self.buildCache_LRU = [] | |
| 124 self.currentBuilds = [] | 122 self.currentBuilds = [] |
| 125 self.watchers = [] | 123 self.watchers = [] |
| 126 self.slavenames = [] | 124 self.slavenames = [] |
| 127 # self.basedir must be filled in by our parent | 125 # self.basedir must be filled in by our parent |
| 128 # self.status must be filled in by our parent | 126 # self.status must be filled in by our parent |
| 129 | 127 |
| 130 def reconfigFromBuildmaster(self, buildmaster): | 128 def reconfigFromBuildmaster(self, buildmaster): |
| 131 # Note that we do not hang onto the buildmaster, since this object | 129 # Note that we do not hang onto the buildmaster, since this object |
| 132 # gets pickled and unpickled. | 130 # gets pickled and unpickled. |
| 133 if buildmaster.buildCacheSize is not None: | 131 if buildmaster.buildCacheSize is not None: |
| 134 self.buildCacheSize = buildmaster.buildCacheSize | 132 self.buildCacheSize = buildmaster.buildCacheSize |
| 133 self.buildCache.set_max_size(buildmaster.buildCacheSize) |
| 135 | 134 |
| 136 def upgradeToVersion1(self): | 135 def upgradeToVersion1(self): |
| 137 if hasattr(self, 'slavename'): | 136 if hasattr(self, 'slavename'): |
| 138 self.slavenames = [self.slavename] | 137 self.slavenames = [self.slavename] |
| 139 del self.slavename | 138 del self.slavename |
| 140 if hasattr(self, 'nextBuildNumber'): | 139 if hasattr(self, 'nextBuildNumber'): |
| 141 del self.nextBuildNumber # determineNextBuildNumber chooses this | 140 del self.nextBuildNumber # determineNextBuildNumber chooses this |
| 142 self.wasUpgraded = True | 141 self.wasUpgraded = True |
| 143 | 142 |
| 144 def determineNextBuildNumber(self): | 143 def determineNextBuildNumber(self): |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 179 try: | 178 try: |
| 180 dump(self, open(tmpfilename, "wb"), -1) | 179 dump(self, open(tmpfilename, "wb"), -1) |
| 181 if runtime.platformType == 'win32': | 180 if runtime.platformType == 'win32': |
| 182 # windows cannot rename a file on top of an existing one | 181 # windows cannot rename a file on top of an existing one |
| 183 if os.path.exists(filename): | 182 if os.path.exists(filename): |
| 184 os.unlink(filename) | 183 os.unlink(filename) |
| 185 os.rename(tmpfilename, filename) | 184 os.rename(tmpfilename, filename) |
| 186 except: | 185 except: |
| 187 log.msg("unable to save builder %s" % self.name) | 186 log.msg("unable to save builder %s" % self.name) |
| 188 log.err() | 187 log.err() |
| 189 | 188 |
| 190 | 189 |
| 191 # build cache management | 190 # build cache management |
| 192 | 191 |
| 193 def makeBuildFilename(self, number): | 192 def makeBuildFilename(self, number): |
| 194 return os.path.join(self.basedir, "%d" % number) | 193 return os.path.join(self.basedir, "%d" % number) |
| 195 | 194 |
| 196 def touchBuildCache(self, build): | 195 def getBuildByNumber(self, number): |
| 197 self.buildCache[build.number] = build | 196 return self.buildCache.get(number) |
| 198 if build in self.buildCache_LRU: | |
| 199 self.buildCache_LRU.remove(build) | |
| 200 self.buildCache_LRU = self.buildCache_LRU[-(self.buildCacheSize-1):] + [
build ] | |
| 201 return build | |
| 202 | 197 |
| 203 def getBuildByNumber(self, number): | 198 def loadBuildFromFile(self, number): |
| 204 # first look in currentBuilds | |
| 205 for b in self.currentBuilds: | |
| 206 if b.number == number: | |
| 207 return self.touchBuildCache(b) | |
| 208 | |
| 209 # then in the buildCache | |
| 210 if number in self.buildCache: | |
| 211 metrics.MetricCountEvent.log("buildCache.hits", 1) | |
| 212 return self.touchBuildCache(self.buildCache[number]) | |
| 213 metrics.MetricCountEvent.log("buildCache.misses", 1) | |
| 214 | |
| 215 # then fall back to loading it from disk | |
| 216 filename = self.makeBuildFilename(number) | 199 filename = self.makeBuildFilename(number) |
| 217 try: | 200 try: |
| 218 log.msg("Loading builder %s's build %d from on-disk pickle" | 201 log.msg("Loading builder %s's build %d from on-disk pickle" |
| 219 % (self.name, number)) | 202 % (self.name, number)) |
| 220 build = load(open(filename, "rb")) | 203 build = load(open(filename, "rb")) |
| 221 build.builder = self | 204 build.builder = self |
| 222 | 205 |
| 223 # (bug #1068) if we need to upgrade, we probably need to rewrite | 206 # (bug #1068) if we need to upgrade, we probably need to rewrite |
| 224 # this pickle, too. We determine this by looking at the list of | 207 # this pickle, too. We determine this by looking at the list of |
| 225 # Versioned objects that have been unpickled, and (after doUpgrade) | 208 # Versioned objects that have been unpickled, and (after doUpgrade) |
| 226 # checking to see if any of them set wasUpgraded. The Versioneds' | 209 # checking to see if any of them set wasUpgraded. The Versioneds' |
| 227 # upgradeToVersionNN methods all set this. | 210 # upgradeToVersionNN methods all set this. |
| 228 versioneds = styles.versionedsToUpgrade | 211 versioneds = styles.versionedsToUpgrade |
| 229 styles.doUpgrade() | 212 styles.doUpgrade() |
| 230 if True in [ hasattr(o, 'wasUpgraded') for o in versioneds.values()
]: | 213 if True in [ hasattr(o, 'wasUpgraded') for o in versioneds.values()
]: |
| 231 log.msg("re-writing upgraded build pickle") | 214 log.msg("re-writing upgraded build pickle") |
| 232 build.saveYourself() | 215 build.saveYourself() |
| 233 | 216 |
| 234 # handle LogFiles from after 0.5.0 and before 0.6.5 | 217 # handle LogFiles from after 0.5.0 and before 0.6.5 |
| 235 build.upgradeLogfiles() | 218 build.upgradeLogfiles() |
| 236 # check that logfiles exist | 219 # check that logfiles exist |
| 237 build.checkLogfiles() | 220 build.checkLogfiles() |
| 238 return self.touchBuildCache(build) | 221 return build |
| 239 except IOError: | 222 except IOError: |
| 240 raise IndexError("no such build %d" % number) | 223 raise IndexError("no such build %d" % number) |
| 241 except EOFError: | 224 except EOFError: |
| 242 raise IndexError("corrupted build pickle %d" % number) | 225 raise IndexError("corrupted build pickle %d" % number) |
| 243 | 226 |
| 227 def cacheMiss(self, number): |
| 228 # first look in currentBuilds |
| 229 for b in self.currentBuilds: |
| 230 if b.number == number: |
| 231 return b |
| 232 # then fall back to loading it from disk |
| 233 return self.loadBuildFromFile(number) |
| 234 |
| 244 def prune(self, events_only=False): | 235 def prune(self, events_only=False): |
| 245 # begin by pruning our own events | 236 # begin by pruning our own events |
| 246 self.events = self.events[-self.eventHorizon:] | 237 self.events = self.events[-self.eventHorizon:] |
| 247 | 238 |
| 248 if events_only: | 239 if events_only: |
| 249 return | 240 return |
| 250 | 241 |
| 251 gc.collect() | 242 gc.collect() |
| 252 | 243 |
| 253 # get the horizons straight | 244 # get the horizons straight |
| (...skipping 26 matching lines...) Expand all Loading... |
| 280 is_logfile = False | 271 is_logfile = False |
| 281 if mo: | 272 if mo: |
| 282 num = int(mo.group(1)) | 273 num = int(mo.group(1)) |
| 283 else: | 274 else: |
| 284 mo = build_log_re.match(filename) | 275 mo = build_log_re.match(filename) |
| 285 if mo: | 276 if mo: |
| 286 num = int(mo.group(1)) | 277 num = int(mo.group(1)) |
| 287 is_logfile = True | 278 is_logfile = True |
| 288 | 279 |
| 289 if num is None: continue | 280 if num is None: continue |
| 290 if num in self.buildCache: continue | 281 if num in self.buildCache.cache: continue |
| 291 | 282 |
| 292 if (is_logfile and num < earliest_log) or num < earliest_build: | 283 if (is_logfile and num < earliest_log) or num < earliest_build: |
| 293 pathname = os.path.join(self.basedir, filename) | 284 pathname = os.path.join(self.basedir, filename) |
| 294 log.msg("pruning '%s'" % pathname) | 285 log.msg("pruning '%s'" % pathname) |
| 295 try: os.unlink(pathname) | 286 try: os.unlink(pathname) |
| 296 except OSError: pass | 287 except OSError: pass |
| 297 | 288 |
| 298 # IBuilderStatus methods | 289 # IBuilderStatus methods |
| 299 def getName(self): | 290 def getName(self): |
| 300 return self.name | 291 return self.name |
| (...skipping 202 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 503 | 494 |
| 504 # buildStarted is called by our child BuildStatus instances | 495 # buildStarted is called by our child BuildStatus instances |
| 505 def buildStarted(self, s): | 496 def buildStarted(self, s): |
| 506 """Now the BuildStatus object is ready to go (it knows all of its | 497 """Now the BuildStatus object is ready to go (it knows all of its |
| 507 Steps, its ETA, etc), so it is safe to notify our watchers.""" | 498 Steps, its ETA, etc), so it is safe to notify our watchers.""" |
| 508 | 499 |
| 509 assert s.builder is self # paranoia | 500 assert s.builder is self # paranoia |
| 510 assert s.number == self.nextBuildNumber - 1 | 501 assert s.number == self.nextBuildNumber - 1 |
| 511 assert s not in self.currentBuilds | 502 assert s not in self.currentBuilds |
| 512 self.currentBuilds.append(s) | 503 self.currentBuilds.append(s) |
| 513 self.touchBuildCache(s) | 504 self.buildCache.put(s.number, s) |
| 514 | 505 |
| 515 # now that the BuildStatus is prepared to answer queries, we can | 506 # now that the BuildStatus is prepared to answer queries, we can |
| 516 # announce the new build to all our watchers | 507 # announce the new build to all our watchers |
| 517 | 508 |
| 518 for w in self.watchers: # TODO: maybe do this later? callLater(0)? | 509 for w in self.watchers: # TODO: maybe do this later? callLater(0)? |
| 519 try: | 510 try: |
| 520 receiver = w.buildStarted(self.getName(), s) | 511 receiver = w.buildStarted(self.getName(), s) |
| 521 if receiver: | 512 if receiver: |
| 522 if type(receiver) == type(()): | 513 if type(receiver) == type(()): |
| 523 s.subscribe(receiver[0], receiver[1]) | 514 s.subscribe(receiver[0], receiver[1]) |
| (...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 613 result['basedir'] = os.path.basename(self.basedir) | 604 result['basedir'] = os.path.basename(self.basedir) |
| 614 result['category'] = self.category | 605 result['category'] = self.category |
| 615 result['slaves'] = self.slavenames | 606 result['slaves'] = self.slavenames |
| 616 #result['url'] = self.parent.getURLForThing(self) | 607 #result['url'] = self.parent.getURLForThing(self) |
| 617 # TODO(maruel): Add cache settings? Do we care? | 608 # TODO(maruel): Add cache settings? Do we care? |
| 618 | 609 |
| 619 # Transient | 610 # Transient |
| 620 # Collect build numbers. | 611 # Collect build numbers. |
| 621 # Important: Only grab the *cached* builds numbers to reduce I/O. | 612 # Important: Only grab the *cached* builds numbers to reduce I/O. |
| 622 current_builds = [b.getNumber() for b in self.currentBuilds] | 613 current_builds = [b.getNumber() for b in self.currentBuilds] |
| 623 cached_builds = list(set(self.buildCache.keys() + current_builds)) | 614 cached_builds = list(set(self.buildCache.cache.keys() + current_builds)) |
| 624 cached_builds.sort() | 615 cached_builds.sort() |
| 625 result['cachedBuilds'] = cached_builds | 616 result['cachedBuilds'] = cached_builds |
| 626 result['currentBuilds'] = current_builds | 617 result['currentBuilds'] = current_builds |
| 627 result['state'] = self.getState()[0] | 618 result['state'] = self.getState()[0] |
| 628 # lies, but we don't have synchronous access to this info; use | 619 # lies, but we don't have synchronous access to this info; use |
| 629 # asDict_async instead | 620 # asDict_async instead |
| 630 result['pendingBuilds'] = 0 | 621 result['pendingBuilds'] = 0 |
| 631 return result | 622 return result |
| 632 | 623 |
| 633 def asDict_async(self): | 624 def asDict_async(self): |
| 634 """Just like L{asDict}, but with a nonzero pendingBuilds.""" | 625 """Just like L{asDict}, but with a nonzero pendingBuilds.""" |
| 635 result = self.asDict() | 626 result = self.asDict() |
| 636 d = self.getPendingBuildRequestStatuses() | 627 d = self.getPendingBuildRequestStatuses() |
| 637 def combine(statuses): | 628 def combine(statuses): |
| 638 result['pendingBuilds'] = len(statuses) | 629 result['pendingBuilds'] = len(statuses) |
| 639 return result | 630 return result |
| 640 d.addCallback(combine) | 631 d.addCallback(combine) |
| 641 return d | 632 return d |
| 642 | 633 |
| 643 def getMetrics(self): | 634 def getMetrics(self): |
| 644 return self.botmaster.parent.metrics | 635 return self.botmaster.parent.metrics |
| 645 | 636 |
| 646 # vim: set ts=4 sts=4 sw=4 et: | 637 # vim: set ts=4 sts=4 sw=4 et: |
| OLD | NEW |