OLD | NEW |
| (Empty) |
1 # coding=utf8 | |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 """Sends patches to the Try server and reads back results. | |
6 | |
7 - RietveldTryJobs contains RietveldTryJob, one per try job on a builder. | |
8 - TryRunnerRietveld uses Rietveld to signal and poll job results. | |
9 """ | |
10 | |
11 import collections | |
12 import errno | |
13 import logging | |
14 import re | |
15 import socket | |
16 import time | |
17 import urllib2 | |
18 | |
19 import buildbot_json | |
20 import model | |
21 from verification import base | |
22 from verification import try_job_steps | |
23 | |
24 # A build running for longer than this is considered to be timed out. | |
25 TIMED_OUT = 12 * 60 * 60 | |
26 | |
27 | |
28 def is_job_expired(now, revision, timestamp, checkout): | |
29 """Returns False if the job result is still somewhat valid. | |
30 | |
31 A job that occured more than 4 days ago or more than 200 commits behind | |
32 is 'expired'. | |
33 """ | |
34 if timestamp < (now - 4*24*60*60): | |
35 return True | |
36 if checkout.revisions(revision, None) >= 200: | |
37 return True | |
38 return False | |
39 | |
40 | |
41 TryJobProperties = collections.namedtuple( | |
42 'TryJobProperties', | |
43 ['key', 'parent_key', 'builder', 'build', 'buildnumber', 'properties']) | |
44 | |
45 | |
46 def filter_jobs(try_job_results, watched_builders, current_irrelevant_keys, | |
47 status): | |
48 """For each try jobs results, query the Try Server for updated status and | |
49 returns details about each job in a TryJobProperties. | |
50 | |
51 Returns a list of namedtuple describing the updated results and and the new | |
52 list of irrelevant keys. | |
53 | |
54 It adds the build to the ignored list if the build doesn't exist on the Try | |
55 Server anymore (usually it's too old) or if the try job was not triggered by | |
56 the Commit Queue itself. | |
57 """ | |
58 irrelevant = set(current_irrelevant_keys) | |
59 try_jobs_with_props = [] | |
60 for result in try_job_results: | |
61 key = result['key'] | |
62 assert key | |
63 if key in current_irrelevant_keys: | |
64 continue | |
65 builder = result['builder'] | |
66 try: | |
67 buildnumber = int(result['buildnumber']) | |
68 except (TypeError, ValueError): | |
69 continue | |
70 if buildnumber < 0: | |
71 logging.debug('Ignoring %s/%d; invalid', builder, buildnumber) | |
72 irrelevant.add(key) | |
73 continue | |
74 | |
75 if builder not in watched_builders: | |
76 logging.debug('Ignoring %s/%d; no step verifier is examining it', builder, | |
77 buildnumber) | |
78 irrelevant.add(key) | |
79 continue | |
80 | |
81 # Constructing the object itself doesn't throw an exception, it's reading | |
82 # its properties that throws. | |
83 build = status.builders[builder].builds[buildnumber] | |
84 try: | |
85 props = build.properties_as_dict | |
86 except IOError: | |
87 logging.info( | |
88 'Build %s/%s is not on the try server anymore', | |
89 builder, buildnumber) | |
90 irrelevant.add(key) | |
91 continue | |
92 parent_key = props.get('parent_try_job_key') | |
93 if parent_key: | |
94 # Triggered build | |
95 key = '%s/%d_triggered_%s' % (builder, buildnumber, parent_key) | |
96 elif props.get('try_job_key') != key: | |
97 # not triggered, not valid | |
98 logging.debug( | |
99 'Ignoring %s/%d; not from rietveld', builder, buildnumber) | |
100 irrelevant.add(key) | |
101 continue | |
102 | |
103 try_jobs_with_props.append( | |
104 TryJobProperties(key, parent_key, builder, build, buildnumber, props)) | |
105 | |
106 # Sort the non-triggered builds first so triggered jobs | |
107 # can expect their parent to be added to self.try_jobs | |
108 try_jobs_with_props.sort(key=lambda tup: tup.parent_key) | |
109 | |
110 return try_jobs_with_props, list(irrelevant) | |
111 | |
112 | |
113 def _is_skip_try_job(pending): | |
114 """Returns True if a description contains NOTRY=true.""" | |
115 match = re.search(r'^NOTRY=(.*)$', pending.description, re.MULTILINE) | |
116 return match and match.group(1).lower() == 'true' | |
117 | |
118 | |
119 class RietveldTryJobPending(model.PersistentMixIn): | |
120 """Represents a pending try job for a pending commit that we care about. | |
121 | |
122 It is immutable. | |
123 """ | |
124 builder = unicode | |
125 revision = (None, unicode, int) | |
126 requested_steps = list | |
127 clobber = bool | |
128 # Number of retries for this configuration. Initial try is 1. | |
129 tries = int | |
130 init_time = float | |
131 | |
132 def __init__(self, **kwargs): | |
133 required = set(self._persistent_members()) | |
134 actual = set(kwargs) | |
135 assert required == actual, (required - actual, required, actual) | |
136 super(RietveldTryJobPending, self).__init__(**kwargs) | |
137 # Then mark it read-only. | |
138 self._read_only = True | |
139 | |
140 | |
141 class RietveldTryJob(model.PersistentMixIn): | |
142 """Represents a try job for a pending commit that we care about. | |
143 | |
144 This data can be regenerated by parsing all the try job names but it is a bit | |
145 hard on the try server. | |
146 | |
147 It is immutable. | |
148 """ | |
149 builder = unicode | |
150 build = int | |
151 revision = (None, unicode, int) | |
152 requested_steps = list | |
153 # The timestamp when the build started. buildbot_json returns int. | |
154 started = int | |
155 steps_passed = list | |
156 steps_failed = list | |
157 clobber = bool | |
158 completed = bool | |
159 # Number of retries for this configuration. Initial try is 1. | |
160 tries = int | |
161 parent_key = (None, unicode) | |
162 init_time = float | |
163 | |
164 def __init__(self, **kwargs): | |
165 required = set(self._persistent_members()) | |
166 actual = set(kwargs) | |
167 assert required == actual, (required - actual, required, actual) | |
168 super(RietveldTryJob, self).__init__(**kwargs) | |
169 # Then mark it read-only. | |
170 self._read_only = True | |
171 | |
172 @property | |
173 @model.immutable | |
174 def result(self): | |
175 if self.steps_failed: | |
176 return buildbot_json.FAILURE | |
177 if self.completed: | |
178 return buildbot_json.SUCCESS | |
179 return None | |
180 | |
181 | |
182 class RietveldTryJobs(base.IVerifierStatus): | |
183 """A set of try jobs that were sent for a specific patch. | |
184 | |
185 Multiple concurrent try jobs can be sent on a single builder. For example, a | |
186 previous valid try job could have been triggered by the user but was not | |
187 completed so another was sent with the missing tests. | |
188 Also, a try job is sent as soon as a test failure is detected. | |
189 """ | |
190 # An dict of RietveldTryJob objects per key. | |
191 try_jobs = dict | |
192 # The try job keys we ignore because they can't be used to give a good | |
193 # signal: either they are too old (old revision) or they were not triggerd | |
194 # by Rietveld, so we don't know if the diff is 100% good. | |
195 irrelevant = list | |
196 # When NOTRY=true is specified. | |
197 skipped = bool | |
198 # List of test verifiers. All the logic to decide when they are | |
199 # and what bots they trigger is hidden inside. | |
200 step_verifiers = list | |
201 # Jobs that have been sent but are not found yet. Likely a builder is fully | |
202 # utilized or the try server hasn't polled Rietveld yet. list of | |
203 # RietveldTryJobPending() instances. | |
204 pendings = list | |
205 | |
206 @model.immutable | |
207 def get_state(self): | |
208 """Returns the state of this verified. | |
209 | |
210 Failure can be from: | |
211 - For each entry in self.step_verifiers: | |
212 - A Try Job in self.try_jobs has been retried too often. | |
213 | |
214 In particular, there is no need to wait for every Try Job to complete. | |
215 """ | |
216 if self.error_message: | |
217 return base.FAILED | |
218 if not self.tests_waiting_for_result(): | |
219 return base.SUCCEEDED | |
220 return base.PROCESSING | |
221 | |
222 @model.immutable | |
223 def tests_need_to_be_run(self, now): | |
224 """Returns which tests need to be run. | |
225 | |
226 These are the tests that are not pending on any try job, either running or | |
227 in the pending list. | |
228 """ | |
229 # Skipped or failed, nothing to do. | |
230 if self.skipped or self.error_message: | |
231 return {} | |
232 | |
233 # What originally needed to be run. | |
234 # All_tests is {builder_name: set(test_name*)} | |
235 all_tests = {} | |
236 for verifier in self.step_verifiers: | |
237 (builder, tests) = verifier.need_to_trigger(self.try_jobs, now) | |
238 if tests: | |
239 all_tests.setdefault(builder, set()).update(tests) | |
240 | |
241 # Removes what is queued to be run but hasn't started yet. | |
242 for try_job in self.pendings: | |
243 if try_job.builder in all_tests: | |
244 all_tests[try_job.builder] -= set(try_job.requested_steps) | |
245 | |
246 return dict( | |
247 (builder, sorted(tests)) for builder, tests in all_tests.iteritems() | |
248 if tests) | |
249 | |
250 @model.immutable | |
251 def tests_waiting_for_result(self): | |
252 """Returns the tests that we are waiting for results on pending or running | |
253 builds. | |
254 """ | |
255 # Skipped or failed, nothing to do. | |
256 if self.skipped or self.error_message: | |
257 return {} | |
258 | |
259 # What originally needed to be run. | |
260 all_tests = {} | |
261 for verification in self.step_verifiers: | |
262 (builder, tests) = verification.waiting_for(self.try_jobs) | |
263 if tests: | |
264 all_tests.setdefault(builder, set()).update(tests) | |
265 | |
266 # Removes what was run. | |
267 for try_job in self.try_jobs.itervalues(): | |
268 if try_job.builder in all_tests: | |
269 all_tests[try_job.builder] -= set(try_job.steps_passed) | |
270 | |
271 return dict( | |
272 (builder, list(tests)) for builder, tests in all_tests.iteritems() | |
273 if tests) | |
274 | |
275 @model.immutable | |
276 def watched_builders(self): | |
277 """Marks all the jobs that the step_verifiers don't examine as | |
278 irrelevant. | |
279 """ | |
280 # Generate the list of builders to keep. | |
281 watched_builders = set() | |
282 for step_verifier in self.step_verifiers: | |
283 watched_builders.add(step_verifier.builder_name) | |
284 if isinstance(step_verifier, try_job_steps.TryJobTriggeredSteps): | |
285 watched_builders.add(step_verifier.trigger_name) | |
286 | |
287 return watched_builders | |
288 | |
289 def update_jobs_from_rietveld( | |
290 self, data, status, checkout, now): | |
291 """Retrieves the jobs statuses from rietveld and updates its state. | |
292 | |
293 Args: | |
294 owner: Owner of the CL. | |
295 data: Patchset properties as returned from Rietveld. | |
296 status: A buildbot_json.Buildbot instance. | |
297 checkout: A depot_tools' Checkout instance. | |
298 now: epoch time of what should be considered to be 'now'. | |
299 | |
300 Returns: | |
301 Keys which were updated. | |
302 """ | |
303 updated = [] | |
304 try_job_results = data.get('try_job_results', []) | |
305 logging.debug('Found %d entries', len(try_job_results)) | |
306 | |
307 try_jobs_with_props, self.irrelevant = filter_jobs( | |
308 try_job_results, self.watched_builders() , self.irrelevant, status) | |
309 | |
310 # Ensure that all irrelevant jobs have been removed from the set of valid | |
311 # try jobs. | |
312 for irrelevant_key in self.irrelevant: | |
313 if irrelevant_key in self.try_jobs: | |
314 del self.try_jobs[irrelevant_key] | |
315 if irrelevant_key + '_old' in self.try_jobs: | |
316 del self.try_jobs[irrelevant_key + '_old'] | |
317 | |
318 for i in try_jobs_with_props: | |
319 if self._update_try_job_status(checkout, i, now): | |
320 updated.append(i.key) | |
321 return updated | |
322 | |
323 def _update_try_job_status(self, checkout, try_job_properties, now): | |
324 """Updates status of a specific RietveldTryJob. | |
325 | |
326 try_job_property is an instance of TryJobProperties. | |
327 | |
328 Returns True if it was updated. | |
329 """ | |
330 key = try_job_properties.key | |
331 builder = try_job_properties.builder | |
332 buildnumber = try_job_properties.buildnumber | |
333 if key in self.irrelevant: | |
334 logging.debug('Ignoring %s/%d; irrelevant', builder, buildnumber) | |
335 return False | |
336 if (try_job_properties.parent_key and | |
337 try_job_properties.parent_key not in self.try_jobs): | |
338 logging.debug('Ignoring %s, parent unknown', key) | |
339 return False | |
340 | |
341 requested_steps = [] | |
342 # Set it to 0 as the default value since when the job is new and previous | |
343 # try jobs are found, we don't want to count them as tries. | |
344 tries = 0 | |
345 job = self.try_jobs.get(key) | |
346 build = try_job_properties.build | |
347 if job: | |
348 if job.completed: | |
349 logging.debug('Ignoring %s/%d; completed', builder, buildnumber) | |
350 return False | |
351 else: | |
352 if now - job.started > TIMED_OUT: | |
353 # Flush it and start over. | |
354 self.irrelevant.append(key) | |
355 del self.try_jobs[key] | |
356 return False | |
357 requested_steps = job.requested_steps | |
358 tries = job.tries | |
359 init_time = job.init_time | |
360 else: | |
361 # This try job is new. See if we triggered it previously by | |
362 # looking in self.pendings. | |
363 for index, pending_job in enumerate(self.pendings): | |
364 if pending_job.builder == builder: | |
365 # Reuse its item. | |
366 requested_steps = pending_job.requested_steps | |
367 tries = pending_job.tries | |
368 self.pendings.pop(index) | |
369 break | |
370 else: | |
371 # Is this a good build? It must not be too old and triggered by | |
372 # rietveld. | |
373 if is_job_expired(now, build.revision, build.start_time, checkout): | |
374 logging.debug('Ignoring %s/%d; expired', builder, buildnumber) | |
375 self.irrelevant.append(key) | |
376 return False | |
377 init_time = now | |
378 | |
379 passed = [s.name for s in build.steps if s.simplified_result] | |
380 failed = [s.name for s in build.steps if s.simplified_result is False] | |
381 # The steps in neither passed or failed were skipped. | |
382 new_job = RietveldTryJob( | |
383 init_time=init_time, | |
384 builder=builder, | |
385 build=buildnumber, | |
386 revision=build.revision, | |
387 requested_steps=requested_steps, | |
388 started=build.start_time, | |
389 steps_passed=passed, | |
390 steps_failed=failed, | |
391 clobber=bool(try_job_properties.properties.get('clobber')), | |
392 completed=build.completed, | |
393 tries=tries, | |
394 parent_key=try_job_properties.parent_key) | |
395 if job and job.build and new_job.build and job.build != new_job.build: | |
396 # It's tricky because 'key' is the same for both. The trick is to create | |
397 # a fake key for the old build and mark it as completed. Note that | |
398 # Rietveld is confused by it too. | |
399 logging.warning( | |
400 'Try Server was restarted and restarted builds with the same keys. ' | |
401 'I\'m confused. %s: %d != %d', job.builder, job.build, new_job.build) | |
402 # Resave the old try job and mark it as completed. | |
403 self.try_jobs[key + '_old'] = RietveldTryJob( | |
404 init_time=job.init_time, | |
405 builder=job.builder, | |
406 build=job.build, | |
407 revision=job.revision, | |
408 requested_steps=job.requested_steps, | |
409 started=build.start_time, | |
410 steps_passed=job.steps_passed, | |
411 steps_failed=job.steps_failed, | |
412 clobber=job.clobber, | |
413 completed=True, | |
414 tries=job.tries, | |
415 parent_key=job.parent_key) | |
416 if not job or not model.is_equivalent(new_job, job): | |
417 logging.info( | |
418 'Job update: %s: %s/%d', | |
419 try_job_properties.properties.get('issue'), | |
420 builder, | |
421 buildnumber) | |
422 self.try_jobs[key] = new_job | |
423 return key | |
424 | |
425 def signal_as_failed_if_needed(self, job, url, now): | |
426 """Detects if the RietveldTryJob instance is in a state where it is | |
427 impossible to make progress. | |
428 | |
429 If so, mark ourself as failed by setting self.error_message and return True. | |
430 """ | |
431 if self.skipped or self.error_message: | |
432 return False | |
433 # Figure out steps that should be retried for this builder. | |
434 missing_tests = self.tests_need_to_be_run(now).get(job.builder, []) | |
435 if not missing_tests: | |
436 return False | |
437 if job.tries > 2: | |
438 self.error_message = ( | |
439 'Retried try job too often on %s for step(s) %s\n%s' % | |
440 (job.builder, ', '.join(missing_tests), url)) | |
441 logging.info(self.error_message) | |
442 return True | |
443 return False | |
444 | |
445 @model.immutable | |
446 def why_not(self): | |
447 # Skipped or failed, nothing to do. | |
448 if self.skipped or self.error_message: | |
449 return None | |
450 waiting = self.tests_waiting_for_result() | |
451 if waiting: | |
452 out = 'Waiting for the following jobs:\n' | |
453 for builder in sorted(waiting): | |
454 out += ' %s: %s\n' % (builder, ','.join(waiting[builder])) | |
455 return out | |
456 | |
457 | |
458 class TryRunnerRietveld(base.VerifierCheckout): | |
459 """Stateless communication with a try server. | |
460 | |
461 Uses Rietveld to trigger the try job and reads try job status with the json | |
462 API. | |
463 | |
464 Analysis goes as following: | |
465 - compile step itself is not flaky. compile.py already takes care of most | |
466 flakiness and clobber build is done by default. If compile step fails, try | |
467 again with clobber=True | |
468 - test steps are flaky and can be retried as necessary. | |
469 | |
470 1. For each existing try jobs from rietveld. | |
471 1. Fetch result from try server. | |
472 2. If try job was generated from rietveld; | |
473 1. If not is_job_expired(); | |
474 1. Skip any scheduled test that succeeded on this builder. | |
475 2. For each builder with tests scheduled; | |
476 1. If no step waiting to be triggered, skip this builder completely. | |
477 2. For each non succeeded job; | |
478 1. Send try jobs to rietveld. | |
479 | |
480 Note: It needs rietveld, hence it uses VerifierCheckout, but it doesn't need a | |
481 checkout. | |
482 """ | |
483 name = 'try job rietveld' | |
484 | |
485 # Only updates a job status once every 60 seconds. | |
486 update_latency = 60 | |
487 | |
488 def __init__( | |
489 self, context_obj, try_server_url, commit_user, step_verifiers, | |
490 ignored_steps, solution): | |
491 super(TryRunnerRietveld, self).__init__(context_obj) | |
492 self.try_server_url = try_server_url.rstrip('/') | |
493 self.commit_user = commit_user | |
494 # TODO(maruel): Have it be overridden by presubmit_support.DoGetTrySlaves. | |
495 self.step_verifiers = step_verifiers | |
496 self.ignored_steps = set(ignored_steps) | |
497 # Time to poll the Try Server, and not Rietveld. | |
498 self.last_update = time.time() - self.update_latency | |
499 self.solution = solution | |
500 | |
501 def verify(self, pending): | |
502 """Sends a try job to the try server and returns a RietveldTryJob list. | |
503 | |
504 This function is called synchronously. | |
505 """ | |
506 jobs = pending.verifications.setdefault(self.name, RietveldTryJobs()) | |
507 if _is_skip_try_job(pending): | |
508 # Do not run try job for it. | |
509 jobs.skipped = True | |
510 return | |
511 | |
512 # Overridde any previous list from the last restart. | |
513 jobs.step_verifiers = [] | |
514 for step in self.step_verifiers: | |
515 if isinstance(step, try_job_steps.TryJobTriggeredOrNormalSteps): | |
516 # Since the steps are immutable, create a new step so that swarm | |
517 # can be enabled. | |
518 jobs.step_verifiers.append(try_job_steps.TryJobTriggeredOrNormalSteps( | |
519 builder_name=step.builder_name, | |
520 trigger_name=step.trigger_name, | |
521 steps=step.steps, | |
522 trigger_bot_steps=step.trigger_bot_steps, | |
523 use_triggered_bot=True)) | |
524 else: | |
525 jobs.step_verifiers.append(step) | |
526 | |
527 # First, update the status of the current try jobs on Rietveld. | |
528 now = time.time() | |
529 self._update_jobs_from_rietveld(pending, jobs, False, now) | |
530 | |
531 # Add anything that is missing. | |
532 self._send_jobs(pending, jobs, now) | |
533 | |
534 # Slightly postpone next check. | |
535 self.last_update = min(now, self.last_update + (self.update_latency / 4)) | |
536 | |
537 def update_status(self, queue): | |
538 """Grabs the current status of all try jobs and update self.queue. | |
539 | |
540 Note: it would be more efficient to be event based. | |
541 """ | |
542 if not queue: | |
543 logging.debug('The list is empty, nothing to do') | |
544 return | |
545 | |
546 # Hard code 'now' to the value before querying and sending them. This will | |
547 # cause some issues when querying state or sending the jobs takes a | |
548 # non-trivial amount of time but in general it will be fine. | |
549 now = time.time() | |
550 if now - self.last_update < self.update_latency: | |
551 logging.debug('TS: Throttling updates') | |
552 return | |
553 self.last_update = now | |
554 | |
555 # Update the status of the current pending CLs on Rietveld. | |
556 for pending, jobs in self.loop(queue, RietveldTryJobs, True): | |
557 # Update 'now' since querying the try jobs may take a significant amount | |
558 # of time. | |
559 now = time.time() | |
560 if self._update_jobs_from_rietveld(pending, jobs, True, now): | |
561 # Send any necessary job. Noop if not needed. | |
562 self._send_jobs(pending, jobs, now) | |
563 | |
564 def _add_pending_job_and_send_if_needed(self, builder, steps, jobs, | |
565 send_job, pending, now): | |
566 # Find if there was a previous try. | |
567 previous_jobs = [ | |
568 job for job in jobs.try_jobs.itervalues() if job.builder == builder | |
569 ] | |
570 if previous_jobs: | |
571 tries = max(job.tries for job in previous_jobs) | |
572 clobber = max( | |
573 (job.clobber or 'compile' in job.steps_failed) | |
574 for job in previous_jobs) | |
575 else: | |
576 tries = 0 | |
577 clobber = False | |
578 if tries > 4: | |
579 # Fail safe. | |
580 jobs.error_message = ( | |
581 ( 'The commit queue went berserk retrying too often for a\n' | |
582 'seemingly flaky test on builder %s:\n%s') % | |
583 ( builder, | |
584 '\n'.join(self._build_status_url(j) for j in previous_jobs))) | |
585 return False | |
586 | |
587 # Don't always send the job (triggered bots don't need to send there own | |
588 # request). | |
589 if send_job: | |
590 logging.debug( | |
591 'Sending job %s for %s: %s', pending.issue, builder, ','.join(steps)) | |
592 try: | |
593 self.context.rietveld.trigger_try_jobs( | |
594 pending.issue, pending.patchset, 'CQ', clobber, 'HEAD', | |
595 {builder: steps}) | |
596 except urllib2.HTTPError as e: | |
597 if e.code == 400: | |
598 # This probably mean a new patchset was uploaded since the last poll, | |
599 # so it's better to drop the CL. | |
600 jobs.error_message = 'Failed to trigger a try job on %s\n%s' % ( | |
601 builder, e) | |
602 return False | |
603 else: | |
604 raise | |
605 | |
606 # Set the status of this pending job here and on the CQ page. | |
607 jobs.pendings.append( | |
608 RietveldTryJobPending( | |
609 init_time=now, | |
610 builder=builder, | |
611 revision=None, | |
612 requested_steps=steps, | |
613 clobber=clobber, | |
614 tries=tries + 1)) | |
615 # Update the status on the AppEngine status to signal a new try job was | |
616 # sent. | |
617 info = { | |
618 'builder': builder, | |
619 'clobber': clobber, | |
620 'job_name': 'CQ', | |
621 'revision': None, #revision, | |
622 } | |
623 self.send_status(pending, info) | |
624 return True | |
625 | |
626 def _get_triggered_bots(self, builder, steps): | |
627 """Returns a dict of all the (builder, steps) pairs of bots that will get | |
628 triggered by the given builder steps combination.""" | |
629 triggered_bots = {} | |
630 for verifier in self.step_verifiers: | |
631 builder, steps = verifier.get_triggered_steps(builder, steps) | |
632 if steps: | |
633 triggered_bots[builder] = steps | |
634 | |
635 return triggered_bots | |
636 | |
637 def _send_jobs(self, pending, jobs, now): | |
638 """Prepares the RietveldTryJobs instance |jobs| to send try jobs to the try | |
639 server. | |
640 """ | |
641 if jobs.error_message: | |
642 # Too late. | |
643 return | |
644 remaining = jobs.tests_need_to_be_run(now) | |
645 if not remaining: | |
646 return | |
647 # Send them in order to simplify testing. | |
648 for builder in sorted(remaining): | |
649 tests = remaining[builder] | |
650 if not self._add_pending_job_and_send_if_needed(builder, tests, jobs, | |
651 True, pending, now): | |
652 # If the main job wasn't sent, we can't skip the triggered jobs since | |
653 # they won't get triggered. | |
654 continue | |
655 | |
656 # Add any pending bots that will be triggered from this build. | |
657 triggered_bots = self._get_triggered_bots(builder, tests) | |
658 for builder, steps in triggered_bots.iteritems(): | |
659 self._add_pending_job_and_send_if_needed(builder, steps, jobs, False, | |
660 pending, now) | |
661 | |
662 @model.immutable | |
663 def _build_status_url(self, job): | |
664 """Html url for this try job.""" | |
665 assert job.build is not None, str(job) | |
666 return '%s/buildstatus?builder=%s&number=%s' % ( | |
667 self.try_server_url, job.builder, job.build) | |
668 | |
669 @model.immutable | |
670 def _update_dashboard(self, pending, job): | |
671 """Updates the CQ dashboard with the current Try Job state as known to the | |
672 CQ. | |
673 """ | |
674 logging.debug('_update_dashboard(%s/%s)', job.builder, job.build) | |
675 info = { | |
676 'build': job.build, | |
677 'builder': job.builder, | |
678 'job_name': 'CQ', | |
679 'result': job.result, | |
680 'revision': job.revision, | |
681 'url': self._build_status_url(job), | |
682 } | |
683 self.send_status(pending, info) | |
684 | |
685 def _update_jobs_from_rietveld(self, pending, jobs, handle, now): | |
686 """Grabs data from Rietveld and pass it to | |
687 RietveldTryJobs.update_jobs_from_rietveld(). | |
688 | |
689 Returns True on success. | |
690 """ | |
691 status = buildbot_json.Buildbot(self.try_server_url) | |
692 try: | |
693 try: | |
694 data = self.context.rietveld.get_patchset_properties( | |
695 pending.issue, pending.patchset) | |
696 except urllib2.HTTPError as e: | |
697 if e.code == 404: | |
698 # TODO(phajdan.jr): Maybe generate a random id to correlate the user's | |
699 # error message and exception in the logs. | |
700 # Don't put exception traceback in the user-visible message to avoid | |
701 # leaking sensitive CQ data (passwords etc). | |
702 jobs.error_message = ('Failed to get patchset properties (patchset ' | |
703 'not found?)') | |
704 logging.error(str(e)) | |
705 return False | |
706 else: | |
707 raise | |
708 | |
709 # Update the RietvedTryJobs object. | |
710 keys = jobs.update_jobs_from_rietveld( | |
711 data, | |
712 status, | |
713 self.context.checkout, | |
714 now) | |
715 except urllib2.HTTPError as e: | |
716 if e.code in (500, 502, 503): | |
717 # Temporary AppEngine hiccup. Just log it and return failure. | |
718 logging.warning('%s while accessing %s. Ignoring error.' % ( | |
719 str(e), e.url)) | |
720 return False | |
721 else: | |
722 raise | |
723 except urllib2.URLError as e: | |
724 if 'timed out' in e.reason: | |
725 # Handle timeouts gracefully. | |
726 logging.warning('%s while updating tryserver status for ' | |
727 'rietveld issue %s', e, pending.issue) | |
728 return False | |
729 else: | |
730 raise | |
731 except socket.error as e: | |
732 # Temporary AppEngine hiccup. Just log it and return failure. | |
733 if e.errno == errno.ECONNRESET: | |
734 logging.warning( | |
735 '%s while updating tryserver status for rietveld issue %s.' % ( | |
736 str(e), str(pending.issue))) | |
737 return False | |
738 else: | |
739 raise | |
740 except IOError as e: | |
741 # Temporary AppEngine hiccup. Just log it and return failure. | |
742 if e.errno == 'socket error': | |
743 logging.warning( | |
744 '%s while updating tryserver status for rietveld issue %s.' % ( | |
745 str(e), str(pending.issue))) | |
746 return False | |
747 raise | |
748 | |
749 if handle: | |
750 for updated_key in keys: | |
751 job = jobs.try_jobs[updated_key] | |
752 self._update_dashboard(pending, job) | |
753 jobs.signal_as_failed_if_needed(job, self._build_status_url(job), now) | |
754 | |
755 return True | |
OLD | NEW |