forked from catlee/buildbotcustom
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmisc_scheduler.py
316 lines (272 loc) · 11.7 KB
/
misc_scheduler.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# Additional Scheduler functions
# Contributor(s):
# Chris AtLee <catlee@mozilla.com>
# Lukas Blakk <lsblakk@mozilla.com>
import re, time
from twisted.python import log
from twisted.internet import defer
from twisted.web.client import getPage
from buildbot.sourcestamp import SourceStamp
import buildbotcustom.try_parser
reload(buildbotcustom.try_parser)
from buildbotcustom.try_parser import TryParser
from buildbotcustom.common import genBuildID, genBuildUID
from buildbot.process.properties import Properties
from buildbot.util import json
def tryChooser(s, all_changes):
log.msg("Looking at changes: %s" % all_changes)
buildersPerChange = {}
dl = []
def getJSON(data):
push = json.loads(data)
log.msg("Looking at the push json data for try comments")
for p in push:
pd = push[p]
changes = pd['changesets']
for change in reversed(changes):
match = re.search("try:", change['desc'])
if match:
return change['desc'].encode("utf8", "replace")
def parseData(comments, c):
if not comments:
# still need to parse a comment string to get the default set
log.msg("No comments, passing empty string which will result in default set")
comments = ""
customBuilders = TryParser(comments, s.builderNames, s.prettyNames, s.unittestPrettyNames,
s.unittestSuites, s.talosSuites)
buildersPerChange[c] = customBuilders
def parseDataError(failure, c):
log.msg("Couldn't parse data: Requesting default try set. %s" % failure)
parseData("", c)
for c in all_changes:
try:
match = re.search("try", c.branch)
if not match:
log.msg("Ignoring off-branch %s" % c.branch)
continue
# Look in comments first for try: syntax
match = re.search("try:", c.comments)
if match:
log.msg("Found try message in the change comments, ignoring push comments")
d = defer.succeed(c.comments)
# otherwise getPage from hg.m.o
else:
d = getPage(str("http://hg.mozilla.org/try/json-pushes?full=1&changeset=%s" % c.revision))
d.addCallback(getJSON)
except:
log.msg("Error in all_changes loop: sending default try set")
d = defer.succeed("")
d.addCallback(parseData, c)
d.addErrback(parseDataError, c)
dl.append(d)
d = defer.DeferredList(dl)
d.addCallback(lambda res: buildersPerChange)
return d
def buildIDSchedFunc(sched, t, ssid):
"""Generates a unique buildid for this change.
Returns a Properties instance with 'buildid' set to the buildid to use.
scheduler `sched`'s state is modified as a result."""
state = sched.get_state(t)
# Get the last buildid we scheduled from the database
lastid = state.get('last_buildid', 0)
newid = genBuildID()
# Our new buildid will be the highest of the last buildid+1 or the buildid
# based on the current date
newid = str(max(int(newid), int(lastid)+1))
# Save it in the scheduler's state so we don't generate the same one again.
state['last_buildid'] = newid
sched.set_state(t, state)
props = Properties()
props.setProperty('buildid', newid, 'buildIDSchedFunc')
return props
def buildUIDSchedFunc(sched, t, ssid):
"""Return a Properties instance with 'builduid' set to a randomly generated
id."""
props = Properties()
props.setProperty('builduid', genBuildUID(), 'buildUIDSchedFunc')
return props
# A version of changeEventGenerator that can be used within a db connector
# thread. Copied from buildbot/db/connector.py.
def changeEventGeneratorInTransaction(dbconn, t, branches=[],
categories=[], committers=[], minTime=0):
q = "SELECT changeid FROM changes"
args = []
if branches or categories or committers:
q += " WHERE "
pieces = []
if branches:
pieces.append("branch IN %s" % dbconn.parmlist(len(branches)))
args.extend(list(branches))
if categories:
pieces.append("category IN %s" % dbconn.parmlist(len(categories)))
args.extend(list(categories))
if committers:
pieces.append("author IN %s" % dbconn.parmlist(len(committers)))
args.extend(list(committers))
if minTime:
pieces.append("when_timestamp > %d" % minTime)
q += " AND ".join(pieces)
q += " ORDER BY changeid DESC"
t.execute(q, tuple(args))
for (changeid,) in t.fetchall():
yield dbconn._txn_getChangeNumberedNow(t, changeid)
def lastChange(db, t, branch):
"""Returns the revision for the last changeset on the given branch"""
#### NOTE: called in a thread!
for c in changeEventGeneratorInTransaction(db, t, branches=[branch]):
# Ignore DONTBUILD changes
if c.comments and "DONTBUILD" in c.comments:
continue
# Ignore changes which didn't come from the poller
if not c.revlink:
continue
return c
return None
def lastGoodRev(db, t, branch, builderNames, starttime, endtime):
"""Returns the revision for the latest green build among builders. If no
revision is all green, None is returned."""
# Get a list of branch, revision, buildername tuples from builds on
# `branch` that completed successfully or with warnings within [starttime,
# endtime] (a closed interval)
q = db.quoteq("""SELECT branch, revision, buildername FROM
sourcestamps,
buildsets,
buildrequests
WHERE
buildsets.sourcestampid = sourcestamps.id AND
buildrequests.buildsetid = buildsets.id AND
buildrequests.complete = 1 AND
buildrequests.results IN (0,1) AND
sourcestamps.revision IS NOT NULL AND
buildrequests.buildername in %s AND
sourcestamps.branch = ? AND
buildrequests.complete_at >= ? AND
buildrequests.complete_at <= ?
ORDER BY
buildsets.id DESC
""" % db.parmlist(len(builderNames)))
t.execute(q, tuple(builderNames) + (branch, starttime, endtime))
builds = t.fetchall()
builderNames = set(builderNames)
# Map of (branch, revision) to set of builders that passed
good_sourcestamps = {}
# Go through the results and group them by branch,revision.
# When we find a revision where all our required builders are listed, we've
# found a good revision!
count = 0
for (branch, revision, name) in builds:
count += 1
key = branch, revision
good_sourcestamps.setdefault(key, set()).add(name)
if good_sourcestamps[key] == builderNames:
# Looks like a winner!
log.msg("lastGood: ss %s good for everyone!" % (key,))
log.msg("lastGood: looked at %i builds" % count)
return revision
return None
def getLatestRev(db, t, branch, r1, r2):
"""Returns whichever of r1, r2 has the latest when_timestamp"""
if r1 == r2:
return r1
# Get the when_timestamp for these two revisions
q = db.quoteq("""SELECT revision FROM changes
WHERE
branch = ? AND
revision IN (?, ?)
ORDER BY
when_timestamp DESC
LIMIT 1""")
t.execute(q, (branch, r1, r2))
return t.fetchone()[0]
def getLastBuiltRevision(db, t, branch, builderNames):
"""Returns the latest revision that was built on builderNames"""
# Utility function to handle concatenation differently depending on what
# database we're talking to. mysql uses the CONCAT() function whereas
# sqlite uses the || operator.
def concat(a, b):
if 'sqlite' in db._spec.dbapiName:
return "%s || %s" % (a, b)
else:
# Make sure any % are escaped since mysql uses % as the parameter
# style.
return "CONCAT(%s, %s)" % (a.replace("%", "%%"), b.replace("%", "%%"))
# Find the latest revision we built on any one of builderNames.
# We match on changes.revision LIKE sourcestamps.revision + "%" in order to
# handle forced builds where only the short revision has been specified.
# This revision will show up as the sourcestamp's revision.
q = db.quoteq("""SELECT changes.revision FROM
buildrequests, buildsets, sourcestamps, changes
WHERE
buildrequests.buildsetid = buildsets.id AND
buildsets.sourcestampid = sourcestamps.id AND
changes.revision LIKE %s AND
changes.branch = ? AND
buildrequests.buildername IN %s
ORDER BY
changes.when_timestamp DESC
LIMIT 1""" % (
concat('sourcestamps.revision', "'%'"),
db.parmlist(len(builderNames)))
)
t.execute(q, (branch,) + tuple(builderNames))
result = t.fetchone()
if result:
return result[0]
return None
def lastGoodFunc(branch, builderNames, triggerBuildIfNoChanges=True, l10nBranch=None):
"""Returns a function that returns the latest revision on branch that was
green for all builders in builderNames.
If unable to find an all green build, fall back to the latest known
revision on this branch, or the tip of the default branch if we don't know
anything about this branch.
Also check that we don't schedule a build for a revision that is older that
the latest revision built on the scheduler's builders.
"""
def ssFunc(scheduler, t):
#### NOTE: called in a thread!
db = scheduler.parent.db
# Look back 24 hours for a good revision to build
start = time.time()
rev = lastGoodRev(db, t, branch, builderNames, start-(24*3600), start)
end = time.time()
log.msg("lastGoodRev: took %.2f seconds to run; returned %s" %
(end-start, rev))
if rev is None:
# Check if there are any recent l10n changes
if l10nBranch:
lastL10nChange = lastChange(db, t, l10nBranch)
if lastL10nChange:
lastL10nChange = lastL10nChange.when
else:
lastL10nChange = 0
else:
lastL10nChange = 0
# If there are no recent l10n changes, and we don't want to trigger
# builds if nothing has changed in the past 24 hours, then return
# None, indicating that no build should be scheduled
if not triggerBuildIfNoChanges:
if l10nBranch:
if (start-lastL10nChange) > (24*3600):
return None
else:
return None
# Couldn't find a good revision. Fall back to using the latest
# revision on this branch
c = lastChange(db, t, branch)
if c:
rev = c.revision
log.msg("lastChange returned %s" % (rev))
# Find the last revision our scheduler's builders have built. This can
# include forced builds.
last_built_rev = getLastBuiltRevision(db, t, branch,
scheduler.builderNames)
log.msg("lastNightlyRevision was %s" % last_built_rev)
if last_built_rev is not None:
# Make sure that rev is newer than the last revision we built.
later_rev = getLatestRev(db, t, branch, rev, last_built_rev)
if later_rev != rev:
log.msg("lastGoodRev: Building %s since it's newer than %s" %
(later_rev, rev))
rev = later_rev
return SourceStamp(branch=scheduler.branch, revision=rev)
return ssFunc