1# -*- coding: utf-8 -*- 2 3# This Source Code Form is subject to the terms of the Mozilla Public 4# License, v. 2.0. If a copy of the MPL was not distributed with this 5# file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 7from __future__ import absolute_import, print_function, unicode_literals 8 9import json 10import logging 11import re 12import sys 13from functools import partial 14 15 16from taskgraph.util.taskcluster import get_task_definition 17from .registry import register_callback_action 18from .util import ( 19 combine_task_graph_files, 20 create_tasks, 21 fetch_graph_and_labels, 22 get_decision_task_id, 23 get_pushes_from_params_input, 24 trigger_action, 25 get_downstream_browsertime_tasks, 26 rename_browsertime_vismet_task, 27) 28 29logger = logging.getLogger(__name__) 30SYMBOL_REGEX = re.compile("^(.*)-[a-z0-9]{11}-bk$") 31GROUP_SYMBOL_REGEX = re.compile("^(.*)-bk$") 32 33 34def input_for_support_action(revision, task, times=1, retrigger=True): 35 """Generate input for action to be scheduled. 36 37 Define what label to schedule with 'label'. 38 If it is a test task that uses explicit manifests add that information. 39 """ 40 input = { 41 "label": task["metadata"]["name"], 42 "revision": revision, 43 "times": times, 44 # We want the backfilled tasks to share the same symbol as the originating task 45 "symbol": task["extra"]["treeherder"]["symbol"], 46 "retrigger": retrigger, 47 } 48 49 # Support tasks that are using manifest based scheduling 50 if task["payload"]["env"].get("MOZHARNESS_TEST_PATHS"): 51 input["test_manifests"] = json.loads( 52 task["payload"]["env"]["MOZHARNESS_TEST_PATHS"] 53 ) 54 55 return input 56 57 58@register_callback_action( 59 title="Backfill", 60 name="backfill", 61 permission="backfill", 62 symbol="Bk", 63 description=("Given a task schedule it on previous pushes in the same project."), 64 order=200, 65 context=[{}], # This will be available for all tasks 66 schema={ 67 "type": "object", 68 "properties": { 69 "depth": { 70 "type": "integer", 71 "default": 19, 72 "minimum": 1, 73 "maximum": 25, 74 "title": "Depth", 75 "description": ( 76 "The number of previous pushes before the current " 77 "push to attempt to trigger this task on." 78 ), 79 }, 80 "inclusive": { 81 "type": "boolean", 82 "default": False, 83 "title": "Inclusive Range", 84 "description": ( 85 "If true, the backfill will also retrigger the task " 86 "on the selected push." 87 ), 88 }, 89 "times": { 90 "type": "integer", 91 "default": 1, 92 "minimum": 1, 93 "maximum": 10, 94 "title": "Times", 95 "description": ( 96 "The number of times to execute each job you are backfilling." 97 ), 98 }, 99 "retrigger": { 100 "type": "boolean", 101 "default": True, 102 "title": "Retrigger", 103 "description": ( 104 "If False, the task won't retrigger on pushes that have already " 105 "ran it." 106 ), 107 }, 108 }, 109 "additionalProperties": False, 110 }, 111 available=lambda parameters: True, 112) 113def backfill_action(parameters, graph_config, input, task_group_id, task_id): 114 """ 115 This action takes a task ID and schedules it on previous pushes (via support action). 116 117 To execute this action locally follow the documentation here: 118 https://firefox-source-docs.mozilla.org/taskcluster/actions.html#testing-the-action-locally 119 """ 120 task = get_task_definition(task_id) 121 pushes = get_pushes_from_params_input(parameters, input) 122 failed = False 123 input_for_action = input_for_support_action( 124 revision=parameters["head_rev"], 125 task=task, 126 times=input.get("times", 1), 127 retrigger=input.get("retrigger", True), 128 ) 129 130 for push_id in pushes: 131 try: 132 # The Gecko decision task can sometimes fail on a push and we need to handle 133 # the exception that this call will produce 134 push_decision_task_id = get_decision_task_id(parameters["project"], push_id) 135 except Exception: 136 logger.warning("Could not find decision task for push {}".format(push_id)) 137 # The decision task may have failed, this is common enough that we 138 # don't want to report an error for it. 139 continue 140 141 try: 142 trigger_action( 143 action_name="backfill-task", 144 # This lets the action know on which push we want to add a new task 145 decision_task_id=push_decision_task_id, 146 input=input_for_action, 147 ) 148 except Exception: 149 logger.exception("Failed to trigger action for {}".format(push_id)) 150 failed = True 151 152 if failed: 153 sys.exit(1) 154 155 156def add_backfill_suffix(regex, symbol, suffix): 157 m = regex.match(symbol) 158 if m is None: 159 symbol += suffix 160 return symbol 161 162 163def test_manifests_modifier(task, label, symbol, revision, test_manifests): 164 """In the case of test tasks we can modify the test paths they execute.""" 165 if task.label != label: 166 return task 167 168 logger.debug("Modifying test_manifests for {}".format(task.label)) 169 test_manifests = test_manifests 170 task.attributes["test_manifests"] = test_manifests 171 task.task["payload"]["env"]["MOZHARNESS_TEST_PATHS"] = json.dumps(test_manifests) 172 # The name/label might have been modify in new_label, thus, change it here as well 173 task.task["metadata"]["name"] = task.label 174 th_info = task.task["extra"]["treeherder"] 175 # Use a job symbol of the originating task as defined in the backfill action 176 th_info["symbol"] = add_backfill_suffix( 177 SYMBOL_REGEX, th_info["symbol"], "-{}-bk".format(revision[0:11]) 178 ) 179 if th_info.get("groupSymbol"): 180 # Group all backfilled tasks together 181 th_info["groupSymbol"] = add_backfill_suffix( 182 GROUP_SYMBOL_REGEX, th_info["groupSymbol"], "-bk" 183 ) 184 task.task["tags"]["action"] = "backfill-task" 185 return task 186 187 188def do_not_modify(task): 189 return task 190 191 192def new_label(label, tasks): 193 """This is to handle the case when a previous push does not contain a specific task label 194 and we try to find a label we can reuse. 195 196 For instance, we try to backfill chunk #3, however, a previous push does not contain such 197 chunk, thus, we try to reuse another task/label. 198 """ 199 begining_label, ending = label.rsplit("-", 1) 200 if ending.isdigit(): 201 # We assume that the taskgraph has chunk #1 OR unnumbered chunk and we hijack it 202 if begining_label in tasks: 203 return begining_label 204 elif begining_label + "-1" in tasks: 205 return begining_label + "-1" 206 else: 207 raise Exception( 208 "New label ({}) was not found in the task-graph".format(label) 209 ) 210 else: 211 raise Exception("{} was not found in the task-graph".format(label)) 212 213 214@register_callback_action( 215 name="backfill-task", 216 title="Backfill task on a push.", 217 permission="backfill", 218 symbol="backfill-task", 219 description="This action is normally scheduled by the backfill action. " 220 "The intent is to schedule a task on previous pushes.", 221 order=500, 222 context=[], 223 schema={ 224 "type": "object", 225 "properties": { 226 "label": {"type": "string", "description": "A task label"}, 227 "revision": { 228 "type": "string", 229 "description": "Revision of the original push from where we backfill.", 230 }, 231 "symbol": { 232 "type": "string", 233 "description": "Symbol to be used by the scheduled task.", 234 }, 235 "test_manifests": { 236 "type": "array", 237 "default": [], 238 "description": "An array of test manifest paths", 239 "items": {"type": "string"}, 240 }, 241 "times": { 242 "type": "integer", 243 "default": 1, 244 "minimum": 1, 245 "maximum": 10, 246 "title": "Times", 247 "description": ( 248 "The number of times to execute each job " "you are backfilling." 249 ), 250 }, 251 "retrigger": { 252 "type": "boolean", 253 "default": True, 254 "title": "Retrigger", 255 "description": ( 256 "If False, the task won't retrigger on pushes that have already " 257 "ran it." 258 ), 259 }, 260 }, 261 }, 262) 263def add_task_with_original_manifests( 264 parameters, graph_config, input, task_group_id, task_id 265): 266 """ 267 This action is normally scheduled by the backfill action. The intent is to schedule a test 268 task with the test manifests from the original task (if available). 269 270 The push in which we want to schedule a new task is defined by the parameters object. 271 272 To execute this action locally follow the documentation here: 273 https://firefox-source-docs.mozilla.org/taskcluster/actions.html#testing-the-action-locally 274 """ 275 # This step takes a lot of time when executed locally 276 logger.info("Retreving the full task graph and labels.") 277 decision_task_id, full_task_graph, label_to_taskid = fetch_graph_and_labels( 278 parameters, graph_config 279 ) 280 281 label = input.get("label") 282 if not input.get("retrigger") and label in label_to_taskid: 283 logger.info( 284 f"Skipping push with decision task ID {decision_task_id} as it already has this test." 285 ) 286 return 287 288 if label not in full_task_graph.tasks: 289 label = new_label(label, full_task_graph.tasks) 290 291 to_run = [label] 292 if "browsertime" in label: 293 if "vismet" in label: 294 label = rename_browsertime_vismet_task(label) 295 to_run = get_downstream_browsertime_tasks( 296 [label], full_task_graph, label_to_taskid 297 ) 298 299 modifier = do_not_modify 300 test_manifests = input.get("test_manifests") 301 # If the original task has defined test paths 302 if test_manifests: 303 modifier = partial( 304 test_manifests_modifier, 305 label=label, 306 revision=input.get("revision"), 307 symbol=input.get("symbol"), 308 test_manifests=test_manifests, 309 ) 310 311 logger.info("Creating tasks...") 312 times = input.get("times", 1) 313 for i in range(times): 314 create_tasks( 315 graph_config, 316 to_run, 317 full_task_graph, 318 label_to_taskid, 319 parameters, 320 decision_task_id, 321 suffix=i, 322 modifier=modifier, 323 ) 324 325 combine_task_graph_files(list(range(times))) 326