blob: 974fda59efcffa1628b61fc1227da64edcb00b62 [file] [log] [blame]
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""URL endpoint for a cron job to automatically run bisects."""
import datetime
from dashboard import bisect_group
from dashboard import datastore_hooks
from dashboard import request_handler
from dashboard import start_try_job
from dashboard.models import anomaly
from dashboard.models import try_job
# Days between successive bisect restarts.
_BISECT_RESTART_PERIOD_DAYS = [0, 1, 7, 14]
class AutoBisectHandler(request_handler.RequestHandler):
"""URL endpoint for a cron job to automatically run bisects."""
def get(self):
"""A get request is the same a post request for this endpoint."""
self.post()
def post(self):
"""Runs auto bisects."""
if 'stats' in self.request.query_string:
self.RenderHtml('result.html', _PrintStartedAndFailedBisectJobs())
return
datastore_hooks.SetPrivilegedRequest()
_RestartBisects()
def _RestartBisects():
"""Restarts failed bisect jobs.
For bisect job that failed for the first time, restart it immediately.
For following retries, use bisect_group to craft different bisect config
and restart the bisect. Bisect jobs that ran out of retries will be deleted.
"""
bisect_jobs = try_job.TryJob.query(try_job.TryJob.status == 'failed').fetch()
for job in bisect_jobs:
if job.run_count > 0:
if job.run_count <= len(_BISECT_RESTART_PERIOD_DAYS):
if _IsBisectJobDueForRestart(job):
if job.run_count == 1:
start_try_job.PerformBisect(job)
elif job.bug_id:
StartBisect(job)
else:
if job.bug_id:
comment = ('Failed to run bisect %s times.'
'Stopping automatic restart for this job.' %
job.run_count)
start_try_job.LogBisectResult(job.bug_id, comment)
job.key.delete()
def StartBisect(bisect_job):
"""Chooses a bisect config depending on job's run count and run bisect.
In this function we're setting the "config" property of the TryJob entity
that gets passed in, so it's OK to pass in a TryJob entity whose "config"
property is None.
Args:
bisect_job: TryJob entity with initialized bot name and config.
Returns:
A dictionary containing the result; if successful, this dictionary contains
the field "issue_id", otherwise it contains "error".
"""
if bisect_job.use_buildbucket:
return {'error': 'Buildbucket not yet supported for 3 or more retries.'}
anomalies = anomaly.Anomaly.query(
anomaly.Anomaly.bug_id == bisect_job.bug_id).fetch()
if not anomalies:
return {'error': 'No alerts found for this bug.'}
good_revision, bad_revision = bisect_group.ChooseRevisionRange(anomalies)
if not good_revision or not bad_revision:
return {'error': 'No good or bad revision found.'}
# TODO(qyearsley): Add test coverage. See http://crbug.com/447432
test = bisect_group.ChooseTest(anomalies, bisect_job.run_count)
if not test:
return {'error': 'Cannot figure out a test.'}
metric = start_try_job.GuessMetric(test.test_path)
bisect_bot = start_try_job.GuessBisectBot(test.bot_name)
if not bisect_bot or '_' not in bisect_bot:
return {'error': 'Cannot figure out a bisect bot.'}
new_bisect_config = start_try_job.GetBisectConfig(
bisect_bot, test.suite_name, metric, good_revision, bad_revision,
10, 20, 25, bisect_job.bug_id, use_archive='true')
if new_bisect_config.get('error'):
return new_bisect_config
bisect_job.config = new_bisect_config['config']
bisect_job.bot = bisect_bot
result = start_try_job.PerformBisect(bisect_job)
return result
def _IsBisectJobDueForRestart(bisect_job):
"""Whether bisect job is due for restart."""
old_timestamp = (datetime.datetime.now() - datetime.timedelta(
days=_BISECT_RESTART_PERIOD_DAYS[bisect_job.run_count - 1]))
if bisect_job.last_ran_timestamp < old_timestamp:
return True
return False
def _PrintStartedAndFailedBisectJobs():
"""Print started and failed bisect jobs in datastore."""
failed_jobs = try_job.TryJob.query(
try_job.TryJob.status == 'failed').fetch()
started_jobs = try_job.TryJob.query(
try_job.TryJob.status == 'started').fetch()
failed_jobs.sort(key=lambda b: b.run_count)
started_jobs.sort(key=lambda b: b.run_count)
return {
'headline': 'Bisect Jobs',
'results': [
_JobsListResult('Failed jobs', failed_jobs),
_JobsListResult('Started jobs', started_jobs),
]
}
def _JobsListResult(title, jobs):
"""Returns one item in a list of results to be displayed on result.html."""
return {
'name': '%s: %d' % (title, len(jobs)),
'value': '\n'.join(_JobLine(job) for job in jobs),
'class': 'results-pre'
}
def _JobLine(job):
"""Returns a string with information about one TryJob entity."""
config = job.config.replace('\n', '') if job.config else 'No config.'
return 'Run count %d. Bug ID %d. %s' % (job.run_count, job.bug_id, config)