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
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
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)
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
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
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',
55 def __init__(self
, name
, builderNames
, periodicBuildTimer
,
56 branch
=None, properties
={}):
57 base
.BaseScheduler
.__init
__(self
, name
, builderNames
, properties
)
58 self
.periodicBuildTimer
= periodicBuildTimer
60 self
.reason
= ("The Periodic scheduler named '%s' triggered this build"
63 def get_initial_state(self
, max_changeid
):
64 return {"last_build": None}
66 def getPendingBuildTimes(self
):
68 s
= db
.runInteractionNow(self
.get_state
)
69 last_build
= s
["last_build"]
71 if last_build
is None:
73 return [last_build
+ self
.periodicBuildTimer
]
77 d
= db
.runInteraction(self
._run
)
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
)
88 when
= last_build
+ self
.periodicBuildTimer
90 self
.start_HEAD_build(t
)
91 self
.update_last_build(t
, now
)
93 when
= now
+ self
.periodicBuildTimer
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
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'],
113 c['schedules'].append(s)
115 This scheduler will perform a build each monday morning at 6:23am and
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'):
150 s = Nightly(name='nightly-when-changed', builderNames=['builder1'],
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,
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
)
173 self
.dayOfMonth
= dayOfMonth
175 self
.dayOfWeek
= dayOfWeek
177 self
.onlyIfChanged
= onlyIfChanged
178 self
.delayedRun
= None
179 self
.nextRunTime
= None
180 self
.reason
= ("The Nightly scheduler named '%s' triggered this build"
182 self
.fileIsImportant
= None
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
192 def get_initial_state(self
, max_changeid
):
195 "last_processed": max_changeid
,
198 def getPendingBuildTimes(self
):
200 next
= self
._calculateNextRunTimeFrom
(now
)
201 # note: this ignores onlyIfChanged
205 d
= defer
.succeed(None)
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
))
214 def _check_timer(self
, t
):
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
)
221 next
= self
._calculateNextRunTimeFrom
(last_build
)
223 # not ready to fire yet
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
):
235 if self
.onlyIfChanged
:
236 res
= db
.scheduler_get_classified_changes(self
.schedulerid
, t
)
237 (important
, unimportant
) = res
239 log
.msg("Nightly Scheduler <%s>: "
240 "skipping build - No important change" % self
.name
)
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" %
249 self
.start_requested_build(t
, relevant_changes
)
251 changeids
= [c
.number
for c
in relevant_changes
]
252 db
.scheduler_retire_changes(self
.schedulerid
, changeids
, t
)
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
276 if not check(self
.hour
, timetuple
[3]):
277 #print 'bad hour', timetuple[3], self.hour
280 if not check(self
.month
, timetuple
[1]):
281 #print 'bad month', timetuple[1], self.month
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])):
293 if not check(self
.dayOfMonth
, timetuple
[2]):
294 #print 'bad day of month'
297 if not check(self
.dayOfWeek
, timetuple
[6]):
298 #print 'bad day of week'
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
)