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