• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2# Copyright (c) 2019 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
6"""Script that triggers and waits for g3 compile tasks."""
7
8import json
9import math
10import optparse
11import os
12import subprocess
13import sys
14import time
15
16INFRA_BOTS_DIR = os.path.abspath(os.path.realpath(os.path.join(
17    os.path.dirname(os.path.abspath(__file__)), os.pardir)))
18sys.path.insert(0, INFRA_BOTS_DIR)
19import utils
20
21
22G3_COMPILE_BUCKET = 'g3-compile-tasks'
23
24GS_RETRIES = 5
25GS_RETRY_WAIT_BASE = 15
26
27POLLING_FREQUENCY_SECS = 10
28DEADLINE_SECS = 60 * 60  # 60 minutes.
29
30INFRA_FAILURE_ERROR_MSG = (
31    '\n\n'
32    'Your run failed due to unknown infrastructure failures.\n'
33    'Please contact rmistry@ or the trooper from '
34    'http://skia-tree-status.appspot.com/trooper\n'
35    'Sorry for the inconvenience!\n'
36)
37MISSING_APPROVAL_ERROR_MSG = (
38    '\n\n'
39    'To run the G3 tryjob, changes must be either owned and authored by \n'
40    'Googlers or approved (Code-Review+1) by Googlers.\n'
41)
42MERGE_CONFLICT_ERROR_MSG = (
43    '\n\n'
44    'G3 tryjob failed because the change is causing a merge conflict when \n'
45    'applying it to the Skia hash in G3.\n'
46)
47PATCHING_INFORMATION = (
48    '\n\n'
49    'Tip: If needed, could try patching in the CL into a local G3 client \n'
50    'with "g4 patch" and then hacking on it.'
51)
52
53
54class G3CompileException(Exception):
55  pass
56
57
58def _create_task_dict(options):
59  """Creates a dict representation of the requested task."""
60  params = {}
61  params['issue'] = options.issue
62  params['patchset'] = options.patchset
63  return params
64
65
66def _get_gs_bucket():
67  """Returns the Google storage bucket with the gs:// prefix."""
68  return 'gs://%s' % G3_COMPILE_BUCKET
69
70
71def _write_to_storage(task):
72  """Writes the specified compile task to Google storage."""
73  with utils.tmp_dir():
74    json_file = os.path.join(os.getcwd(), _get_task_file_name(task))
75    with open(json_file, 'w') as f:
76      json.dump(task, f)
77    subprocess.check_call(['gsutil', 'cp', json_file, '%s/' % _get_gs_bucket()])
78    print 'Created %s/%s' % (_get_gs_bucket(), os.path.basename(json_file))
79
80
81def _get_task_file_name(task):
82  """Returns the file name of the compile task. Eg: ${issue}-${patchset}.json"""
83  return '%s-%s.json' % (task['issue'], task['patchset'])
84
85
86def _does_running_task_exist_in_storage(task):
87  """Checks to see if the task file exists in storage and is running."""
88  gs_file = '%s/%s' % (_get_gs_bucket(), _get_task_file_name(task))
89  try:
90    # Read without exponential backoff because it is unlikely that the file
91    # already exists and we do not want to waste minutes every time.
92    taskJSON = _read_from_storage(gs_file, use_expo_retries=False)
93  except (subprocess.CalledProcessError, ValueError):
94    return False
95  if taskJSON.get('status'):
96    print 'Task exists in Google storage and has completed.'
97    return False
98  print 'Task exists in Google storage and is still running.'
99  return True
100
101
102def _trigger_task(options):
103  """Triggers a g3 compile task by creating a file in storage."""
104  task = _create_task_dict(options)
105  # Check to see if the task is already running in Google Storage.
106  if not _does_running_task_exist_in_storage(task):
107    _write_to_storage(task)
108  return task
109
110
111def _read_from_storage(gs_file, use_expo_retries=True):
112  """Returns the contents of the specified file from storage."""
113  num_retries = GS_RETRIES if use_expo_retries else 1
114  for retry in range(num_retries):
115    try:
116      output = subprocess.check_output(['gsutil', 'cat', gs_file])
117      ret = json.loads(output)
118      return ret
119    except (subprocess.CalledProcessError, ValueError), e:
120      print 'Error when reading and loading %s: %s' % (gs_file, e)
121      if retry == (num_retries-1):
122        print '%d retries did not help' % num_retries
123        raise
124      waittime = GS_RETRY_WAIT_BASE * math.pow(2, retry)
125      print 'Retry in %d seconds.' % waittime
126      time.sleep(waittime)
127      continue
128
129
130def trigger_and_wait(options):
131  """Triggers a g3 compile task and waits for it to complete."""
132  task = _trigger_task(options)
133  print 'G3 Compile Task for %d/%d has been successfully added to %s.' % (
134      options.issue, options.patchset, G3_COMPILE_BUCKET)
135  print '%s will be polled every %d seconds.' % (G3_COMPILE_BUCKET,
136                                                 POLLING_FREQUENCY_SECS)
137
138  # Now poll the Google storage file till the task completes or till deadline
139  # is hit.
140  time_started_polling = time.time()
141  while True:
142    if (time.time() - time_started_polling) > DEADLINE_SECS:
143      raise G3CompileException(
144          'Task did not complete in the deadline of %s seconds.' % (
145              DEADLINE_SECS))
146
147    # Get the status of the task.
148    gs_file = '%s/%s' % (_get_gs_bucket(), _get_task_file_name(task))
149    ret = _read_from_storage(gs_file)
150
151    if ret.get('status'):
152      # The task is done, delete the file.
153      subprocess.check_call(['gsutil', 'rm', gs_file])
154      if options.output_file:
155        # Write the task to the output_file.
156        with open(options.output_file, 'w') as output_file:
157          json.dump(ret, output_file)
158
159      # Now either raise an Exception or return success based on the status.
160      if ret['status'] == 'exception':
161        if ret.get('error'):
162          raise G3CompileException('Run failed with:\n\n%s\n' % ret['error'])
163        else:
164          # Use a general purpose error message.
165          raise G3CompileException(INFRA_FAILURE_ERROR_MSG)
166      elif ret['status'] == 'missing_approval':
167          raise G3CompileException(MISSING_APPROVAL_ERROR_MSG)
168      elif ret['status'] == 'merge_conflict':
169          raise G3CompileException(MERGE_CONFLICT_ERROR_MSG)
170      elif ret['status'] == 'failure':
171        raise G3CompileException(
172            '\n\nRun failed G3 TAP: cl/%s' % ret['cl'] + PATCHING_INFORMATION)
173      elif ret['status'] == 'success':
174        print '\n\nRun passed G3 TAP: cl/%s' % ret['cl']
175        return 0
176      else:
177        # Not sure what happened here. Use a general purpose error message.
178        raise G3CompileException(INFRA_FAILURE_ERROR_MSG)
179
180    # Print status of the task.
181    print 'Task: %s\n' % pretty_task_str(ret)
182    time.sleep(POLLING_FREQUENCY_SECS)
183
184
185def pretty_task_str(task):
186  if task.get('result'):
187    status = task['result']
188  else:
189    status = 'Task not started yet'
190  return '[status: %s, cl: %s]' % (status, task.get('cl'))
191
192
193def main():
194  option_parser = optparse.OptionParser()
195  option_parser.add_option(
196      '', '--issue', type=int, default=0,
197      help='The Gerrit change number to get the patch from.')
198  option_parser.add_option(
199      '', '--patchset', type=int, default=0,
200      help='The Gerrit change patchset to use.')
201  option_parser.add_option(
202      '', '--output_file', type=str,
203      help='The file to write the task to.')
204  options, _ = option_parser.parse_args()
205  sys.exit(trigger_and_wait(options))
206
207
208if __name__ == '__main__':
209  main()
210