1# Copyright 2015 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""A Model that represents one bisect or perf test try job.
6
7TryJob entities are checked in /update_bug_with_results to check completed
8bisect jobs and update bugs with results.
9
10They are also used in /auto_bisect to restart unsuccessful bisect jobs.
11"""
12
13import datetime
14import json
15import logging
16
17from google.appengine.ext import ndb
18
19from dashboard import bisect_stats
20from dashboard import buildbucket_service
21from dashboard.models import bug_data
22from dashboard.models import internal_only_model
23
24
25class TryJob(internal_only_model.InternalOnlyModel):
26  """Stores config and tracking info about a single try job."""
27  bot = ndb.StringProperty()
28  config = ndb.TextProperty()
29  bug_id = ndb.IntegerProperty()
30  email = ndb.StringProperty()
31  rietveld_issue_id = ndb.IntegerProperty()
32  rietveld_patchset_id = ndb.IntegerProperty()
33  master_name = ndb.StringProperty(default='ChromiumPerf', indexed=False)
34  buildbucket_job_id = ndb.StringProperty()
35  internal_only = ndb.BooleanProperty(default=False, indexed=True)
36
37  # Bisect run status (e.g., started, failed).
38  status = ndb.StringProperty(
39      default='pending',
40      choices=[
41          'pending',  # Created, but job start has not been confirmed.
42          'started',  # Job is confirmed started.
43          'failed',   # Job terminated, red build.
44          'staled',   # No updates from bots.
45          'completed',  # Job terminated, green build.
46          'aborted',  # Job terminated with abort (purple, early abort).
47      ],
48      indexed=True)
49
50  # Number of times this job has been tried.
51  run_count = ndb.IntegerProperty(default=0)
52
53  # Last time this job was started.
54  last_ran_timestamp = ndb.DateTimeProperty()
55
56  job_type = ndb.StringProperty(
57      default='bisect',
58      choices=['bisect', 'bisect-fyi', 'perf-try'])
59
60  # job_name attribute is used by try jobs of bisect FYI.
61  job_name = ndb.StringProperty(default=None)
62
63  # Results data coming from bisect bots.
64  results_data = ndb.JsonProperty(indexed=False)
65
66  log_record_id = ndb.StringProperty(indexed=False)
67
68  # Sets of emails of users who has confirmed this TryJob result is bad.
69  bad_result_emails = ndb.PickleProperty()
70
71  def SetStarted(self):
72    self.status = 'started'
73    self.run_count += 1
74    self.last_ran_timestamp = datetime.datetime.now()
75    self.put()
76    if self.bug_id:
77      bug_data.SetBisectStatus(self.bug_id, 'started')
78
79  def SetFailed(self):
80    self.status = 'failed'
81    self.put()
82    if self.bug_id:
83      bug_data.SetBisectStatus(self.bug_id, 'failed')
84    bisect_stats.UpdateBisectStats(self.bot, 'failed')
85
86  def SetStaled(self):
87    self.status = 'staled'
88    self.put()
89    logging.info('Updated status to staled')
90    # TODO(sullivan, dtu): what is the purpose of 'staled' status? Doesn't it
91    # just prevent updating jobs older than 24 hours???
92    # TODO(chrisphan): Add 'staled' state to bug_data and bisect_stats.
93    if self.bug_id:
94      bug_data.SetBisectStatus(self.bug_id, 'failed')
95    bisect_stats.UpdateBisectStats(self.bot, 'failed')
96
97  def SetCompleted(self):
98    logging.info('Updated status to completed')
99    self.status = 'completed'
100    self.put()
101    if self.bug_id:
102      bug_data.SetBisectStatus(self.bug_id, 'completed')
103    bisect_stats.UpdateBisectStats(self.bot, 'completed')
104
105  def GetConfigDict(self):
106    return json.loads(self.config.split('=', 1)[1])
107
108  def CheckFailureFromBuildBucket(self):
109    # Buildbucket job id is not always set.
110    if not self.buildbucket_job_id:
111      return
112    job_info = buildbucket_service.GetJobStatus(self.buildbucket_job_id)
113    data = job_info.get('build', {})
114
115    # Since the job is completed successfully, results_data must
116    # have been set appropriately by the bisector.
117    # The buildbucket job's 'status' and 'result' fields are documented here:
118    # https://goto.google.com/bb_status
119    if data.get('status') == 'COMPLETED' and data.get('result') == 'SUCCESS':
120      return
121
122    # Proceed if the job failed or cancelled
123    logging.info('Job failed. Buildbucket id %s', self.buildbucket_job_id)
124    data['result_details'] = json.loads(data['result_details_json'])
125    # There are various failure and cancellation reasons for a buildbucket
126    # job to fail as listed in https://goto.google.com/bb_status.
127    job_updates = {
128        'status': 'failed',
129        'failure_reason': (data.get('cancelation_reason') or
130                           data.get('failure_reason')),
131        'buildbot_log_url': data.get('url')
132    }
133    details = data.get('result_details')
134    if details:
135      properties = details.get('properties')
136      if properties:
137        job_updates['bisect_bot'] = properties.get('buildername')
138        job_updates['extra_result_code'] = properties.get(
139            'extra_result_code')
140        bisect_config = properties.get('bisect_config')
141        if bisect_config:
142          job_updates['try_job_id'] = bisect_config.get('try_job_id')
143          job_updates['bug_id'] = bisect_config.get('bug_id')
144          job_updates['command'] = bisect_config.get('command')
145          job_updates['test_type'] = bisect_config.get('test_type')
146          job_updates['metric'] = bisect_config.get('metric')
147          job_updates['good_revision'] = bisect_config.get('good_revision')
148          job_updates['bad_revision'] = bisect_config.get('bad_revision')
149    if not self.results_data:
150      self.results_data = {}
151    self.results_data.update(job_updates)
152    self.status = 'failed'
153    self.last_ran_timestamp = datetime.datetime.fromtimestamp(
154        float(data['updated_ts'])/1000000)
155    self.put()
156    logging.info('updated status to failed.')
157
158