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 |