| OLD | NEW |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. | 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """URL endpoint for a cron job to update bugs after bisects.""" | 5 """URL endpoint for a cron job to update bugs after bisects.""" |
| 6 | 6 |
| 7 import datetime | 7 import datetime |
| 8 import json | 8 import json |
| 9 import logging | 9 import logging |
| 10 import re | 10 import re |
| 11 import traceback | 11 import traceback |
| 12 | 12 |
| 13 from google.appengine.api import mail | 13 from google.appengine.api import mail |
| 14 from google.appengine.ext import ndb | 14 from google.appengine.ext import ndb |
| 15 | 15 |
| 16 from dashboard import bisect_fyi | 16 from dashboard import bisect_fyi |
| 17 from dashboard import bisect_report | 17 from dashboard import bisect_report |
| 18 from dashboard import buildbucket_service |
| 18 from dashboard import datastore_hooks | 19 from dashboard import datastore_hooks |
| 19 from dashboard import email_template | 20 from dashboard import email_template |
| 20 from dashboard import issue_tracker_service | 21 from dashboard import issue_tracker_service |
| 21 from dashboard import layered_cache | 22 from dashboard import layered_cache |
| 22 from dashboard import quick_logger | 23 from dashboard import quick_logger |
| 23 from dashboard import request_handler | 24 from dashboard import request_handler |
| 24 from dashboard import utils | 25 from dashboard import utils |
| 25 from dashboard.models import anomaly | 26 from dashboard.models import anomaly |
| 26 from dashboard.models import bug_data | 27 from dashboard.models import bug_data |
| 27 from dashboard.models import try_job | 28 from dashboard.models import try_job |
| (...skipping 10 matching lines...) Expand all Loading... |
| 38 === Auto-CCing suspected CL author %(author)s === | 39 === Auto-CCing suspected CL author %(author)s === |
| 39 | 40 |
| 40 Hi %(author)s, the bisect results pointed to your CL below as possibly | 41 Hi %(author)s, the bisect results pointed to your CL below as possibly |
| 41 causing a regression. Please have a look at this info and see whether | 42 causing a regression. Please have a look at this info and see whether |
| 42 your CL be related. | 43 your CL be related. |
| 43 | 44 |
| 44 """ | 45 """ |
| 45 | 46 |
| 46 _CONFIDENCE_LEVEL_TO_CC_AUTHOR = 95 | 47 _CONFIDENCE_LEVEL_TO_CC_AUTHOR = 95 |
| 47 | 48 |
| 49 _BUILD_FAILURE_REASON = { |
| 50 'BUILD_FAILURE': 'the build has failed', |
| 51 'INFRA_FAILURE': 'the build has failed due to infrastructure failure.', |
| 52 'BUILDBUCKET_FAILURE': 'the buildbucket service failure.', |
| 53 'INVALID_BUILD_DEFINITION': 'incorrect bisect configuation.', |
| 54 'CANCELED_EXPLICITLY': 'the build was canceled explicitly.', |
| 55 'TIMEOUT': 'the build was canceled by buildbot on timeout.', |
| 56 } |
| 57 |
| 58 class BisectJobFailure(Exception): |
| 59 pass |
| 60 |
| 48 | 61 |
| 49 class BugUpdateFailure(Exception): | 62 class BugUpdateFailure(Exception): |
| 50 pass | 63 pass |
| 51 | 64 |
| 52 | 65 |
| 53 class UpdateBugWithResultsHandler(request_handler.RequestHandler): | 66 class UpdateBugWithResultsHandler(request_handler.RequestHandler): |
| 54 """URL endpoint for a cron job to update bugs after bisects.""" | 67 """URL endpoint for a cron job to update bugs after bisects.""" |
| 55 | 68 |
| 56 def get(self): | 69 def get(self): |
| 57 """The get handler method is called from a cron job. | 70 """The get handler method is called from a cron job. |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 92 job: A TryJob entity, which represents one bisect try job. | 105 job: A TryJob entity, which represents one bisect try job. |
| 93 issue_tracker: An issue_tracker_service.IssueTrackerService instance. | 106 issue_tracker: An issue_tracker_service.IssueTrackerService instance. |
| 94 """ | 107 """ |
| 95 if _IsStale(job): | 108 if _IsStale(job): |
| 96 job.SetStaled() | 109 job.SetStaled() |
| 97 # TODO(chrisphan): Add a staled TryJob log. | 110 # TODO(chrisphan): Add a staled TryJob log. |
| 98 # TODO(chrisphan): Do we want to send a FYI Bisect email here? | 111 # TODO(chrisphan): Do we want to send a FYI Bisect email here? |
| 99 return | 112 return |
| 100 | 113 |
| 101 results_data = job.results_data | 114 results_data = job.results_data |
| 102 if not results_data or results_data['status'] not in [COMPLETED, FAILED]: | 115 # Skip this check for bisect fyi jobs, because if the job is fails due to |
| 116 # bisect recipe or infra failures then an alert message should be sent to the |
| 117 # team. |
| 118 if ((not results_data or results_data['status'] not in [COMPLETED, FAILED]) |
| 119 and job.job_type != 'bisect-fyi'): |
| 103 return | 120 return |
| 104 | 121 |
| 105 if job.job_type == 'perf-try': | 122 if job.job_type == 'perf-try': |
| 106 _SendPerfTryJobEmail(job) | 123 _SendPerfTryJobEmail(job) |
| 107 elif job.job_type == 'bisect-fyi': | 124 elif job.job_type == 'bisect-fyi': |
| 108 _CheckFYIBisectJob(job, issue_tracker) | 125 _CheckFYIBisectJob(job, issue_tracker) |
| 109 else: | 126 else: |
| 110 _CheckBisectJob(job, issue_tracker) | 127 _CheckBisectJob(job, issue_tracker) |
| 111 | 128 |
| 112 if results_data['status'] == COMPLETED: | 129 if results_data['status'] == COMPLETED: |
| 113 job.SetCompleted() | 130 job.SetCompleted() |
| 114 else: | 131 else: |
| 115 job.SetFailed() | 132 job.SetFailed() |
| 116 | 133 |
| 117 | 134 |
| 118 def _CheckBisectJob(job, issue_tracker): | 135 def _CheckBisectJob(job, issue_tracker): |
| 119 results_data = job.results_data | 136 results_data = job.results_data |
| 120 has_partial_result = ('revision_data' in results_data and | 137 has_partial_result = ('revision_data' in results_data and |
| 121 results_data['revision_data']) | 138 results_data['revision_data']) |
| 122 if results_data['status'] == FAILED and not has_partial_result: | 139 if results_data['status'] == FAILED and not has_partial_result: |
| 123 return | 140 return |
| 124 _PostResult(job, issue_tracker) | 141 _PostResult(job, issue_tracker) |
| 125 | 142 |
| 126 | 143 |
| 127 def _CheckFYIBisectJob(job, issue_tracker): | 144 def _CheckFYIBisectJob(job, issue_tracker): |
| 128 try: | 145 try: |
| 146 if not _IsBisectJobCompleted(job): |
| 147 return |
| 148 error_message = bisect_fyi.VerifyBisectFYIResults(job) |
| 129 _PostResult(job, issue_tracker) | 149 _PostResult(job, issue_tracker) |
| 130 error_message = bisect_fyi.VerifyBisectFYIResults(job) | |
| 131 if not bisect_fyi.IsBugUpdated(job, issue_tracker): | 150 if not bisect_fyi.IsBugUpdated(job, issue_tracker): |
| 132 error_message += '\nFailed to update bug with bisect results.' | 151 error_message += '\nFailed to update bug with bisect results.' |
| 152 except BisectJobFailure as e: |
| 153 error_message = 'Bisect job failed because, %s' % e |
| 133 except BugUpdateFailure as e: | 154 except BugUpdateFailure as e: |
| 134 error_message = 'Failed to update bug with bisect results: %s' % e | 155 error_message = 'Failed to update bug with bisect results: %s' % e |
| 135 if job.results_data['status'] == FAILED or error_message: | 156 if job.results_data['status'] == FAILED or error_message: |
| 157 job.SetFailed() |
| 136 _SendFYIBisectEmail(job, error_message) | 158 _SendFYIBisectEmail(job, error_message) |
| 137 | 159 |
| 138 | 160 |
| 139 def _SendPerfTryJobEmail(job): | 161 def _SendPerfTryJobEmail(job): |
| 140 """Sends an email to the user who started the perf try job.""" | 162 """Sends an email to the user who started the perf try job.""" |
| 141 if not job.email: | 163 if not job.email: |
| 142 return | 164 return |
| 143 email_report = email_template.GetPerfTryJobEmailReport(job) | 165 email_report = email_template.GetPerfTryJobEmailReport(job) |
| 144 if not email_report: | 166 if not email_report: |
| 145 return | 167 return |
| (...skipping 179 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 325 return | 347 return |
| 326 formatter = quick_logger.Formatter() | 348 formatter = quick_logger.Formatter() |
| 327 logger = quick_logger.QuickLogger('bisect_result', job.bug_id, formatter) | 349 logger = quick_logger.QuickLogger('bisect_result', job.bug_id, formatter) |
| 328 if job.log_record_id: | 350 if job.log_record_id: |
| 329 logger.Log(report, record_id=job.log_record_id) | 351 logger.Log(report, record_id=job.log_record_id) |
| 330 logger.Save() | 352 logger.Save() |
| 331 else: | 353 else: |
| 332 job.log_record_id = logger.Log(report) | 354 job.log_record_id = logger.Log(report) |
| 333 logger.Save() | 355 logger.Save() |
| 334 job.put() | 356 job.put() |
| 357 |
| 358 |
| 359 def _IsBisectJobCompleted(job): |
| 360 return _ValidateBuildbucketResponse( |
| 361 buildbucket_service.GetJobStatus(job.buildbucket_job_id), job) |
| 362 |
| 363 |
| 364 def _ValidateBuildbucketResponse(job_info, job): |
| 365 """Checks and validates the response from the buildbucket service for bisect. |
| 366 |
| 367 Args: |
| 368 job_info: A dictionary containing the response from the buildbucket service. |
| 369 |
| 370 Returns: |
| 371 True if bisect job is completed successfully and False for pending job. |
| 372 |
| 373 Raises: |
| 374 BisectJobFailure: When job is completed but build is failed or cancelled. |
| 375 """ |
| 376 job_info = job_info['build'] |
| 377 json_response = json.dumps(job_info) |
| 378 if not job_info: |
| 379 raise BisectJobFailure('No response from Buildbucket.') |
| 380 |
| 381 if job_info.get('status') in ['SCHEDULED', 'STARTED']: |
| 382 return False |
| 383 |
| 384 if not job.results_data: |
| 385 job.results_data = {} |
| 386 job.results_data['buildbot_log_url'] = str(job_info.get('url')) |
| 387 |
| 388 if job_info.get('result') is None: |
| 389 raise BisectJobFailure('No "result" in try job results. ' |
| 390 'Buildbucket response: %s' % json_response) |
| 391 |
| 392 # There are various failure and cancellation reasons for a buildbucket |
| 393 # job to fail as listed in https://goto.google.com/bb_status. |
| 394 if (job_info.get('status') == 'COMPLETED' and |
| 395 job_info.get('result') != 'SUCCESS'): |
| 396 reason = (job_info.get('cancelation_reason') or |
| 397 job_info.get('failure_reason')) |
| 398 raise BisectJobFailure(_BUILD_FAILURE_REASON.get(reason)) |
| 399 return True |
| OLD | NEW |