Package buildbot :: Package schedulers :: Module timed
[frames] | no frames]

Source Code for Module buildbot.schedulers.timed

  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  from zope.interface import implements 
 17   
 18  from buildbot import util 
 19  from buildbot.interfaces import ITriggerableScheduler 
 20  from buildbot.process import buildstep, properties 
 21  from buildbot.schedulers import base 
 22  from twisted.internet import defer, reactor 
 23  from twisted.python import log 
 24  from buildbot import config 
 25  from buildbot.changes import filter 
26 27 -class Timed(base.BaseScheduler):
28 """ 29 Parent class for timed schedulers. This takes care of the (surprisingly 30 subtle) mechanics of ensuring that each timed actuation runs to completion 31 before the service stops. 32 """ 33 34 compare_attrs = base.BaseScheduler.compare_attrs 35
36 - def __init__(self, name, builderNames, properties={}, **kwargs):
37 base.BaseScheduler.__init__(self, name, builderNames, properties, 38 **kwargs) 39 40 # tracking for when to start the next build 41 self.lastActuated = None 42 43 # A lock to make sure that each actuation occurs without interruption. 44 # This lock governs actuateAt, actuateAtTimer, and actuateOk 45 self.actuationLock = defer.DeferredLock() 46 self.actuateOk = False 47 self.actuateAt = None 48 self.actuateAtTimer = None 49 50 self._reactor = reactor # patched by tests
51
52 - def startService(self):
53 base.BaseScheduler.startService(self) 54 55 # no need to lock this; nothing else can run before the service is started 56 self.actuateOk = True 57 58 # get the scheduler's last_build time (note: only done at startup) 59 d = self.getState('last_build', None) 60 def set_last(lastActuated): 61 self.lastActuated = lastActuated
62 d.addCallback(set_last) 63 64 # schedule the next build 65 d.addCallback(lambda _ : self.scheduleNextBuild()) 66 67 # give subclasses a chance to start up 68 d.addCallback(lambda _ : self.startTimedSchedulerService()) 69 70 # startService does not return a Deferred, so handle errors with a traceback 71 d.addErrback(log.err, "while initializing %s '%s'" % 72 (self.__class__.__name__, self.name))
73
74 - def startTimedSchedulerService(self):
75 """Hook for subclasses to participate in the L{startService} process; 76 can return a Deferred"""
77
78 - def stopService(self):
79 # shut down any pending actuation, and ensure that we wait for any 80 # current actuation to complete by acquiring the lock. This ensures 81 # that no build will be scheduled after stopService is complete. 82 d = self.actuationLock.acquire() 83 def stop_actuating(_): 84 self.actuateOk = False 85 self.actuateAt = None 86 if self.actuateAtTimer: 87 self.actuateAtTimer.cancel() 88 self.actuateAtTimer = None
89 d.addCallback(stop_actuating) 90 d.addCallback(lambda _ : self.actuationLock.release()) 91 92 # and chain to the parent class 93 d.addCallback(lambda _ : base.BaseScheduler.stopService(self)) 94 return d 95 96 ## Scheduler methods 97
98 - def getPendingBuildTimes(self):
99 # take the latest-calculated value of actuateAt as a reasonable 100 # estimate 101 return [ self.actuateAt ]
102 103 ## Timed methods 104
105 - def startBuild(self):
106 """The time has come to start a new build. Returns a Deferred. 107 Override in subclasses.""" 108 raise NotImplementedError
109
110 - def getNextBuildTime(self, lastActuation):
111 """ 112 Called by to calculate the next time to actuate a BuildSet. Override 113 in subclasses. To trigger a fresh call to this method, use 114 L{rescheduleNextBuild}. 115 116 @param lastActuation: the time of the last actuation, or None for never 117 118 @returns: a Deferred firing with the next time a build should occur (in 119 the future), or None for never. 120 """ 121 raise NotImplementedError
122
123 - def scheduleNextBuild(self):
124 """ 125 Schedule the next build, re-invoking L{getNextBuildTime}. This can be 126 called at any time, and it will avoid contention with builds being 127 started concurrently. 128 129 @returns: Deferred 130 """ 131 d = self.actuationLock.acquire() 132 d.addCallback(lambda _ : self._scheduleNextBuild_locked()) 133 # always release the lock 134 def release(x): 135 self.actuationLock.release() 136 return x
137 d.addBoth(release) 138 return d 139 140 ## utilities 141
142 - def now(self):
143 "Similar to util.now, but patchable by tests" 144 return util.now(self._reactor)
145
146 - def _scheduleNextBuild_locked(self):
147 # clear out the existing timer 148 if self.actuateAtTimer: 149 self.actuateAtTimer.cancel() 150 self.actuateAtTimer = None 151 152 # calculate the new time 153 d = self.getNextBuildTime(self.lastActuated) 154 155 # set up the new timer 156 def set_timer(actuateAt): 157 now = self.now() 158 self.actuateAt = max(actuateAt, now) 159 if actuateAt is not None: 160 untilNext = self.actuateAt - now 161 if untilNext == 0: 162 log.msg(("%s: missed scheduled build time, so building " 163 "immediately") % self.name) 164 self.actuateAtTimer = self._reactor.callLater(untilNext, 165 self._actuate)
166 d.addCallback(set_timer) 167 168 return d 169
170 - def _actuate(self):
171 # called from the timer when it's time to start a build 172 self.actuateAtTimer = None 173 self.lastActuated = self.actuateAt 174 175 d = self.actuationLock.acquire() 176 177 @defer.inlineCallbacks 178 def set_state_and_start(_): 179 # bail out if we shouldn't be actuating anymore 180 if not self.actuateOk: 181 return 182 183 # mark the last build time 184 self.actuateAt = None 185 yield self.setState('last_build', self.lastActuated) 186 187 # start the build 188 yield self.startBuild() 189 190 # schedule the next build (noting the lock is already held) 191 yield self._scheduleNextBuild_locked()
192 d.addCallback(set_state_and_start) 193 194 def unlock(x): 195 self.actuationLock.release() 196 return x 197 d.addBoth(unlock) 198 199 # this function can't return a deferred, so handle any failures via 200 # log.err 201 d.addErrback(log.err, 'while actuating') 202
203 204 -class Periodic(Timed):
205 compare_attrs = Timed.compare_attrs + ('periodicBuildTimer', 'branch',) 206
207 - def __init__(self, name, builderNames, periodicBuildTimer, 208 branch=None, properties={}, onlyImportant=False):
209 Timed.__init__(self, name=name, builderNames=builderNames, 210 properties=properties) 211 if periodicBuildTimer <= 0: 212 config.error( 213 "periodicBuildTimer must be positive") 214 self.periodicBuildTimer = periodicBuildTimer 215 self.branch = branch 216 self.reason = "The Periodic scheduler named '%s' triggered this build" % self.name
217
218 - def getNextBuildTime(self, lastActuated):
219 if lastActuated is None: 220 return defer.succeed(self.now()) # meaning "ASAP" 221 else: 222 return defer.succeed(lastActuated + self.periodicBuildTimer)
223
224 - def startBuild(self):
225 return self.addBuildsetForLatest(reason=self.reason, branch=self.branch)
226
227 -class NightlyBase(Timed):
228 compare_attrs = (Timed.compare_attrs 229 + ('minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek')) 230
231 - def __init__(self, name, builderNames, minute=0, hour='*', 232 dayOfMonth='*', month='*', dayOfWeek='*', 233 properties={}, codebases=base.BaseScheduler.DefaultCodebases):
234 Timed.__init__(self, name=name, builderNames=builderNames, 235 properties=properties, codebases=codebases) 236 237 self.minute = minute 238 self.hour = hour 239 self.dayOfMonth = dayOfMonth 240 self.month = month 241 self.dayOfWeek = dayOfWeek
242
243 - def _timeToCron(self, time, isDayOfWeek = False):
244 if isinstance(time, int): 245 if isDayOfWeek: 246 time = (time + 1) % 7 # Convert from Mon = 0 format to Sun = 0 format for use in croniter 247 return time 248 249 if isinstance(time, basestring): 250 return time 251 252 if isDayOfWeek: 253 time = [ (t + 1) % 7 for t in time ] # Conversion for croniter (see above) 254 255 return ','.join([ str(s) for s in time ]) # Convert the list to a string
256
257 - def getNextBuildTime(self, lastActuated):
258 # deferred import in case python-dateutil is not present 259 from buildbot.util import croniter 260 261 dateTime = lastActuated or self.now() 262 sched = '%s %s %s %s %s' % (self._timeToCron(self.minute), 263 self._timeToCron(self.hour), 264 self._timeToCron(self.dayOfMonth), 265 self._timeToCron(self.month), 266 self._timeToCron(self.dayOfWeek, True)) 267 cron = croniter.croniter(sched, dateTime) 268 nextdate = cron.get_next(float) 269 return defer.succeed(nextdate)
270
271 -class Nightly(NightlyBase):
272 compare_attrs = (NightlyBase.compare_attrs 273 + ('branch', 'onlyIfChanged', 'fileIsImportant', 274 'change_filter', 'onlyImportant',)) 275
276 - class NoBranch: pass
277 - def __init__(self, name, builderNames, minute=0, hour='*', 278 dayOfMonth='*', month='*', dayOfWeek='*', 279 branch=NoBranch, fileIsImportant=None, onlyIfChanged=False, 280 properties={}, change_filter=None, onlyImportant=False, 281 codebases = base.BaseScheduler.DefaultCodebases):
282 NightlyBase.__init__(self, name=name, builderNames=builderNames, 283 minute=minute, hour=hour, dayOfWeek=dayOfWeek, dayOfMonth=dayOfMonth, 284 properties=properties, codebases=codebases) 285 286 # If True, only important changes will be added to the buildset. 287 self.onlyImportant = onlyImportant 288 289 if fileIsImportant and not callable(fileIsImportant): 290 config.error( 291 "fileIsImportant must be a callable") 292 293 if branch is Nightly.NoBranch: 294 config.error( 295 "Nightly parameter 'branch' is required") 296 297 self.branch = branch 298 self.onlyIfChanged = onlyIfChanged 299 self.fileIsImportant = fileIsImportant 300 self.change_filter = filter.ChangeFilter.fromSchedulerConstructorArgs( 301 change_filter=change_filter) 302 self.reason = "The Nightly scheduler named '%s' triggered this build" % self.name
303
305 if self.onlyIfChanged: 306 return self.startConsumingChanges(fileIsImportant=self.fileIsImportant, 307 change_filter=self.change_filter, 308 onlyImportant=self.onlyImportant) 309 else: 310 return self.master.db.schedulers.flushChangeClassifications(self.objectid)
311
312 - def gotChange(self, change, important):
313 # both important and unimportant changes on our branch are recorded, as 314 # we will include all such changes in any buildsets we start. Note 315 # that we must check the branch here because it is not included in the 316 # change filter. 317 if change.branch != self.branch: 318 return defer.succeed(None) # don't care about this change 319 return self.master.db.schedulers.classifyChanges( 320 self.objectid, { change.number : important })
321 322 @defer.inlineCallbacks
323 - def startBuild(self):
324 scheds = self.master.db.schedulers 325 # if onlyIfChanged is True, then we will skip this build if no 326 # important changes have occurred since the last invocation 327 if self.onlyIfChanged: 328 classifications = \ 329 yield scheds.getChangeClassifications(self.objectid) 330 331 # see if we have any important changes 332 for imp in classifications.itervalues(): 333 if imp: 334 break 335 else: 336 log.msg(("Nightly Scheduler <%s>: skipping build " + 337 "- No important changes on configured branch") % self.name) 338 return 339 340 changeids = sorted(classifications.keys()) 341 yield self.addBuildsetForChanges(reason=self.reason, 342 changeids=changeids) 343 344 max_changeid = changeids[-1] # (changeids are sorted) 345 yield scheds.flushChangeClassifications(self.objectid, 346 less_than=max_changeid+1) 347 else: 348 # start a build of the latest revision, whatever that is 349 yield self.addBuildsetForLatest(reason=self.reason, 350 branch=self.branch)
351
352 -class NightlyTriggerable(NightlyBase):
353 implements(ITriggerableScheduler)
354 - def __init__(self, name, builderNames, minute=0, hour='*', 355 dayOfMonth='*', month='*', dayOfWeek='*', 356 properties={}, codebases=base.BaseScheduler.DefaultCodebases):
357 NightlyBase.__init__(self, name=name, builderNames=builderNames, minute=minute, hour=hour, 358 dayOfWeek=dayOfWeek, dayOfMonth=dayOfMonth, properties=properties, codebases=codebases) 359 360 self._lastTrigger = None 361 self.reason = "The NightlyTriggerable scheduler named '%s' triggered this build" % self.name
362
363 - def startService(self):
364 NightlyBase.startService(self) 365 366 # get the scheduler's lastTrigger time (note: only done at startup) 367 d = self.getState('lastTrigger', None) 368 def setLast(lastTrigger): 369 try: 370 if lastTrigger: 371 assert isinstance(lastTrigger[0], dict) 372 self._lastTrigger = (lastTrigger[0], properties.Properties.fromDict(lastTrigger[1])) 373 except: 374 # If the lastTrigger isn't of the right format, ignore it 375 log.msg("NightlyTriggerable Scheduler <%s>: bad lastTrigger: %r" % (self.name, lastTrigger))
376 d.addCallback(setLast)
377
378 - def trigger(self, sourcestamps, set_props=None):
379 """Trigger this scheduler with the given sourcestamp ID. Returns a 380 deferred that will fire when the buildset is finished.""" 381 self._lastTrigger = (sourcestamps, set_props) 382 383 # record the trigger in the db 384 if set_props: 385 propsDict = set_props.asDict() 386 else: 387 propsDict = {} 388 d = self.setState('lastTrigger', 389 (sourcestamps, propsDict)) 390 391 ## Trigger expects a callback with the success of the triggered 392 ## build, if waitForFinish is True. 393 ## Just return SUCCESS, to indicate that the trigger was succesful, 394 ## don't want for the nightly to run. 395 return d.addCallback(lambda _: buildstep.SUCCESS)
396 397 @defer.inlineCallbacks
398 - def startBuild(self):
399 if self._lastTrigger is None: 400 defer.returnValue(None) 401 402 (sourcestamps, set_props) = self._lastTrigger 403 self._lastTrigger = None 404 yield self.setState('lastTrigger', None) 405 406 # properties for this buildset are composed of our own properties, 407 # potentially overridden by anything from the triggering build 408 props = properties.Properties() 409 props.updateFromProperties(self.properties) 410 if set_props: 411 props.updateFromProperties(set_props) 412 413 yield self.addBuildsetForSourceStampSetDetails(reason=self.reason, sourcestamps=sourcestamps, 414 properties=props)
415