Changelog update.
[debian_buildbot.git] / master / buildbot / schedulers / timed.py
blob86b1f78544bb98370f62e5a6a81144c11f92a80a
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.
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.
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.
14 # Copyright Buildbot Team Members
16 import time
17 from twisted.internet import defer
18 from twisted.python import log
19 from buildbot.sourcestamp import SourceStamp
20 from buildbot.schedulers import base
22 class TimedBuildMixin:
24 def start_HEAD_build(self, t):
25 # start a build (of the tip of self.branch)
26 db = self.parent.db
27 ss = SourceStamp(branch=self.branch)
28 ssid = db.get_sourcestampid(ss, t)
29 self.create_buildset(ssid, self.reason, t)
31 def start_requested_build(self, t, relevant_changes):
32 # start a build with the requested list of changes on self.branch
33 db = self.parent.db
34 ss = SourceStamp(branch=self.branch, changes=relevant_changes)
35 ssid = db.get_sourcestampid(ss, t)
36 self.create_buildset(ssid, self.reason, t)
38 def update_last_build(self, t, when):
39 # and record when we did it
40 state = self.get_state(t)
41 state["last_build"] = when
42 self.set_state(t, state)
44 class Periodic(base.BaseScheduler, TimedBuildMixin):
45 """Instead of watching for Changes, this Scheduler can just start a build
46 at fixed intervals. The C{periodicBuildTimer} parameter sets the number
47 of seconds to wait between such periodic builds. The first build will be
48 run immediately."""
50 # TODO: consider having this watch another (changed-based) scheduler and
51 # merely enforce a minimum time between builds.
52 compare_attrs = ('name', 'builderNames', 'periodicBuildTimer', 'branch',
53 'properties')
55 def __init__(self, name, builderNames, periodicBuildTimer,
56 branch=None, properties={}):
57 base.BaseScheduler.__init__(self, name, builderNames, properties)
58 self.periodicBuildTimer = periodicBuildTimer
59 self.branch = branch
60 self.reason = ("The Periodic scheduler named '%s' triggered this build"
61 % name)
63 def get_initial_state(self, max_changeid):
64 return {"last_build": None}
66 def getPendingBuildTimes(self):
67 db = self.parent.db
68 s = db.runInteractionNow(self.get_state)
69 last_build = s["last_build"]
70 now = time.time()
71 if last_build is None:
72 return [now]
73 return [last_build + self.periodicBuildTimer]
75 def run(self):
76 db = self.parent.db
77 d = db.runInteraction(self._run)
78 return d
80 def _run(self, t):
81 now = time.time()
82 s = self.get_state(t)
83 last_build = s["last_build"]
84 if last_build is None:
85 self.start_HEAD_build(t)
86 self.update_last_build(t, now)
87 last_build = now
88 when = last_build + self.periodicBuildTimer
89 if when < now:
90 self.start_HEAD_build(t)
91 self.update_last_build(t, now)
92 last_build = now
93 when = now + self.periodicBuildTimer
94 return when + 1.0
97 class Nightly(base.BaseScheduler, base.ClassifierMixin, TimedBuildMixin):
98 """Imitate 'cron' scheduling. This can be used to schedule a nightly
99 build, or one which runs are certain times of the day, week, or month.
101 Pass some subset of minute, hour, dayOfMonth, month, and dayOfWeek; each
102 may be a single number or a list of valid values. The builds will be
103 triggered whenever the current time matches these values. Wildcards are
104 represented by a '*' string. All fields default to a wildcard except
105 'minute', so with no fields this defaults to a build every hour, on the
106 hour.
108 For example, the following master.cfg clause will cause a build to be
109 started every night at 3:00am::
111 s = Nightly(name='nightly', builderNames=['builder1', 'builder2'],
112 hour=3, minute=0)
113 c['schedules'].append(s)
115 This scheduler will perform a build each monday morning at 6:23am and
116 again at 8:23am::
118 s = Nightly(name='BeforeWork', builderNames=['builder1'],
119 dayOfWeek=0, hour=[6,8], minute=23)
121 The following runs a build every two hours::
123 s = Nightly(name='every2hours', builderNames=['builder1'],
124 hour=range(0, 24, 2))
126 And this one will run only on December 24th::
128 s = Nightly(name='SleighPreflightCheck',
129 builderNames=['flying_circuits', 'radar'],
130 month=12, dayOfMonth=24, hour=12, minute=0)
132 For dayOfWeek and dayOfMonth, builds are triggered if the date matches
133 either of them. All time values are compared against the tuple returned
134 by time.localtime(), so month and dayOfMonth numbers start at 1, not
135 zero. dayOfWeek=0 is Monday, dayOfWeek=6 is Sunday.
137 When onlyIfChanged is True, the build is triggered only if changes have
138 arrived on the given branch since the last build was performed. As a
139 further restriction, if fileIsImportant= is provided (a one-argument
140 callable which takes a Change object and returns a bool), then the build
141 will be triggered only if at least one of those changes qualifies as
142 'important'. The following example will run a build at 3am, but only when
143 a source code file (.c/.h) has been changed:
145 def isSourceFile(change):
146 for fn in change.files:
147 if fn.endswith('.c') or fn.endswith('.h'):
148 return True
149 return False
150 s = Nightly(name='nightly-when-changed', builderNames=['builder1'],
151 hour=3, minute=0,
152 onlyIfChanged=True, fileIsImportant=isSourceFile)
154 onlyIfChanged defaults to False, which means a build will be performed
155 even if nothing has changed.
158 compare_attrs = ('name', 'builderNames',
159 'minute', 'hour', 'dayOfMonth', 'month',
160 'dayOfWeek', 'branch', 'onlyIfChanged',
161 'fileIsImportant', 'properties')
163 def __init__(self, name, builderNames, minute=0, hour='*',
164 dayOfMonth='*', month='*', dayOfWeek='*',
165 branch=None, fileIsImportant=None, onlyIfChanged=False,
166 properties={}):
167 # Setting minute=0 really makes this an 'Hourly' scheduler. This
168 # seemed like a better default than minute='*', which would result in
169 # a build every 60 seconds.
170 base.BaseScheduler.__init__(self, name, builderNames, properties)
171 self.minute = minute
172 self.hour = hour
173 self.dayOfMonth = dayOfMonth
174 self.month = month
175 self.dayOfWeek = dayOfWeek
176 self.branch = branch
177 self.onlyIfChanged = onlyIfChanged
178 self.delayedRun = None
179 self.nextRunTime = None
180 self.reason = ("The Nightly scheduler named '%s' triggered this build"
181 % name)
182 self.fileIsImportant = None
183 if fileIsImportant:
184 assert callable(fileIsImportant)
185 self.fileIsImportant = fileIsImportant
186 self._start_time = time.time()
188 # this scheduler does not support filtering, but ClassifierMixin needs a
189 # filter anyway
190 self.make_filter()
192 def get_initial_state(self, max_changeid):
193 return {
194 "last_build": None,
195 "last_processed": max_changeid,
198 def getPendingBuildTimes(self):
199 now = time.time()
200 next = self._calculateNextRunTimeFrom(now)
201 # note: this ignores onlyIfChanged
202 return [next]
204 def run(self):
205 d = defer.succeed(None)
206 db = self.parent.db
207 if self.onlyIfChanged:
208 # call classify_changes, so that we can keep last_processed
209 # up to date, in case we are configured with onlyIfChanged.
210 d.addCallback(lambda ign: db.runInteraction(self.classify_changes))
211 d.addCallback(lambda ign: db.runInteraction(self._check_timer))
212 return d
214 def _check_timer(self, t):
215 now = time.time()
216 s = self.get_state(t)
217 last_build = s["last_build"]
218 if last_build is None:
219 next = self._calculateNextRunTimeFrom(self._start_time)
220 else:
221 next = self._calculateNextRunTimeFrom(last_build)
223 # not ready to fire yet
224 if next >= now:
225 return next + 1.0
227 self._maybe_start_build(t)
228 self.update_last_build(t, now)
230 # reschedule for the next timer
231 return self._check_timer(t)
233 def _maybe_start_build(self, t):
234 db = self.parent.db
235 if self.onlyIfChanged:
236 res = db.scheduler_get_classified_changes(self.schedulerid, t)
237 (important, unimportant) = res
238 if not important:
239 log.msg("Nightly Scheduler <%s>: "
240 "skipping build - No important change" % self.name)
241 return
242 relevant_changes = [c for c in (important + unimportant) if
243 c.branch == self.branch]
244 if not relevant_changes:
245 log.msg("Nightly Scheduler <%s>: "
246 "skipping build - No relevant change on branch" %
247 self.name)
248 return
249 self.start_requested_build(t, relevant_changes)
250 # retire the changes
251 changeids = [c.number for c in relevant_changes]
252 db.scheduler_retire_changes(self.schedulerid, changeids, t)
253 else:
254 # start it unconditionally
255 self.start_HEAD_build(t)
257 # Retire any changes on this scheduler
258 res = db.scheduler_get_classified_changes(self.schedulerid, t)
259 (important, unimportant) = res
260 changeids = [c.number for c in important + unimportant]
261 db.scheduler_retire_changes(self.schedulerid, changeids, t)
263 def _addTime(self, timetuple, secs):
264 return time.localtime(time.mktime(timetuple)+secs)
266 def _isRunTime(self, timetuple):
267 def check(ourvalue, value):
268 if ourvalue == '*': return True
269 if isinstance(ourvalue, int): return value == ourvalue
270 return (value in ourvalue)
272 if not check(self.minute, timetuple[4]):
273 #print 'bad minute', timetuple[4], self.minute
274 return False
276 if not check(self.hour, timetuple[3]):
277 #print 'bad hour', timetuple[3], self.hour
278 return False
280 if not check(self.month, timetuple[1]):
281 #print 'bad month', timetuple[1], self.month
282 return False
284 if self.dayOfMonth != '*' and self.dayOfWeek != '*':
285 # They specified both day(s) of month AND day(s) of week.
286 # This means that we only have to match one of the two. If
287 # neither one matches, this time is not the right time.
288 if not (check(self.dayOfMonth, timetuple[2]) or
289 check(self.dayOfWeek, timetuple[6])):
290 #print 'bad day'
291 return False
292 else:
293 if not check(self.dayOfMonth, timetuple[2]):
294 #print 'bad day of month'
295 return False
297 if not check(self.dayOfWeek, timetuple[6]):
298 #print 'bad day of week'
299 return False
301 return True
303 def _calculateNextRunTimeFrom(self, now):
304 dateTime = time.localtime(now)
306 # Remove seconds by advancing to at least the next minute
307 dateTime = self._addTime(dateTime, 60-dateTime[5])
309 # Now we just keep adding minutes until we find something that matches
311 # It not an efficient algorithm, but it'll *work* for now
312 yearLimit = dateTime[0]+2
313 while not self._isRunTime(dateTime):
314 dateTime = self._addTime(dateTime, 60)
315 #print 'Trying', time.asctime(dateTime)
316 assert dateTime[0] < yearLimit, 'Something is wrong with this code'
317 return time.mktime(dateTime)