retrigger.py (9779B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 6 import itertools 7 import logging 8 import sys 9 import textwrap 10 11 from taskgraph.util.taskcluster import get_task_definition, rerun_task 12 13 from gecko_taskgraph.util.taskcluster import state_task 14 15 from .registry import register_callback_action 16 from .util import ( 17 create_task_from_def, 18 create_tasks, 19 fetch_graph_and_labels, 20 get_tasks_with_downstream, 21 relativize_datestamps, 22 ) 23 24 logger = logging.getLogger(__name__) 25 26 RERUN_STATES = ("exception", "failed") 27 28 29 def _should_retrigger(task_graph, label): 30 """ 31 Return whether a given task in the taskgraph should be retriggered. 32 33 This handles the case where the task isn't there by assuming it should not be. 34 """ 35 if label not in task_graph: 36 logger.info( 37 f"Task {label} not in full taskgraph, assuming task should not be retriggered." 38 ) 39 return False 40 return task_graph[label].attributes.get("retrigger", False) 41 42 43 @register_callback_action( 44 title="Retrigger", 45 name="retrigger", 46 symbol="rt", 47 cb_name="retrigger-decision", 48 description=textwrap.dedent( 49 """\ 50 Create a clone of the task (retriggering decision, action, and cron tasks requires 51 special scopes).""" 52 ), 53 order=11, 54 context=[ 55 {"kind": "decision-task"}, 56 {"kind": "action-callback"}, 57 {"kind": "cron-task"}, 58 {"action": "backfill-task"}, 59 ], 60 ) 61 def retrigger_decision_action(parameters, graph_config, input, task_group_id, task_id): 62 """For a single task, we try to just run exactly the same task once more. 63 It's quite possible that we don't have the scopes to do so (especially for 64 an action), but this is best-effort.""" 65 66 # make all of the timestamps relative; they will then be turned back into 67 # absolute timestamps relative to the current time. 68 task = get_task_definition(task_id) 69 task = relativize_datestamps(task) 70 create_task_from_def( 71 task, parameters["level"], action_tag="retrigger-decision-task" 72 ) 73 74 75 @register_callback_action( 76 title="Retrigger", 77 name="retrigger", 78 symbol="rt", 79 description=("Create a clone of the task."), 80 order=19, # must be greater than other orders in this file, as this is the fallback version 81 context=[{"retrigger": "true"}], 82 schema={ 83 "type": "object", 84 "properties": { 85 "downstream": { 86 "type": "boolean", 87 "description": ( 88 "If true, downstream tasks from this one will be cloned as well. " 89 "The dependencies will be updated to work with the new task at the root." 90 ), 91 "default": False, 92 }, 93 "times": { 94 "type": "integer", 95 "default": 1, 96 "minimum": 1, 97 "maximum": 100, 98 "title": "Times", 99 "description": "How many times to run each task.", 100 }, 101 }, 102 }, 103 ) 104 @register_callback_action( 105 title="Retrigger (disabled)", 106 name="retrigger", 107 cb_name="retrigger-disabled", 108 symbol="rt", 109 description=( 110 "Create a clone of the task.\n\n" 111 "This type of task should typically be re-run instead of re-triggered." 112 ), 113 order=20, # must be greater than other orders in this file, as this is the fallback version 114 context=[{}], 115 schema={ 116 "type": "object", 117 "properties": { 118 "downstream": { 119 "type": "boolean", 120 "description": ( 121 "If true, downstream tasks from this one will be cloned as well. " 122 "The dependencies will be updated to work with the new task at the root." 123 ), 124 "default": False, 125 }, 126 "times": { 127 "type": "integer", 128 "default": 1, 129 "minimum": 1, 130 "maximum": 100, 131 "title": "Times", 132 "description": "How many times to run each task.", 133 }, 134 "force": { 135 "type": "boolean", 136 "default": False, 137 "description": ( 138 "This task should not be re-triggered. " 139 "This can be overridden by passing `true` here." 140 ), 141 }, 142 }, 143 }, 144 ) 145 def retrigger_action(parameters, graph_config, input, task_group_id, task_id): 146 decision_task_id, full_task_graph, label_to_taskid, _ = fetch_graph_and_labels( 147 parameters, graph_config 148 ) 149 150 task = get_task_definition(task_id) 151 label = task["metadata"]["name"] 152 153 with_downstream = " " 154 to_run = [label] 155 156 if not input.get("force", None) and not _should_retrigger(full_task_graph, label): 157 logger.error( 158 f"fatal error: Not retriggering task {label}, task should not be retriggered " 159 "and force not specified." 160 ) 161 sys.exit(1) 162 163 if input.get("downstream"): 164 to_run = get_tasks_with_downstream(to_run, full_task_graph, label_to_taskid) 165 with_downstream = " (with downstream) " 166 167 times = input.get("times", 1) 168 169 def modifier(task): 170 task.attributes["task_duplicates"] = times 171 return task 172 173 create_tasks( 174 graph_config, 175 to_run, 176 full_task_graph, 177 label_to_taskid, 178 parameters, 179 decision_task_id, 180 action_tag="retrigger-task", 181 modifier=modifier, 182 ) 183 logger.info(f"Scheduled {label}{with_downstream}({times} times)") 184 185 186 @register_callback_action( 187 title="Rerun", 188 name="rerun", 189 symbol="rr", 190 description=( 191 "Rerun a task.\n\n" 192 "This only works on failed or exception tasks in the original taskgraph," 193 " and is CoT friendly." 194 ), 195 order=300, 196 context=[{}], 197 schema={"type": "object", "properties": {}}, 198 ) 199 def rerun_action(parameters, graph_config, input, task_group_id, task_id): 200 task = get_task_definition(task_id) 201 parameters = dict(parameters) 202 ( 203 decision_task_id, 204 full_task_graph, 205 label_to_taskid, 206 label_to_taskids, 207 ) = fetch_graph_and_labels(parameters, graph_config) 208 label = task["metadata"]["name"] 209 if task_id not in itertools.chain(*label_to_taskid.values()): 210 # XXX the error message is wrong, we're also looking at label_to_taskid 211 # from action and cron tasks on that revision 212 logger.error( 213 f"Refusing to rerun {label}: taskId {task_id} not in decision task {decision_task_id} label_to_taskid!" 214 ) 215 216 _rerun_task(task_id, label) 217 218 219 def _rerun_task(task_id, label): 220 state = state_task(task_id) 221 if state not in RERUN_STATES: 222 logger.warning( 223 f"No need to rerun {label}: state '{state}' not in {RERUN_STATES}!" 224 ) 225 return 226 rerun_task(task_id) 227 logger.info(f"Reran {label}") 228 229 230 @register_callback_action( 231 title="Retrigger", 232 name="retrigger-multiple", 233 symbol="rt", 234 description=("Create a clone of the task."), 235 context=[], 236 schema={ 237 "type": "object", 238 "properties": { 239 "requests": { 240 "type": "array", 241 "items": { 242 "type": "object", 243 "properties": { 244 "tasks": { 245 "type": "array", 246 "description": "An array of task labels", 247 "items": {"type": "string"}, 248 }, 249 "times": { 250 "type": "integer", 251 "minimum": 1, 252 "maximum": 100, 253 "title": "Times", 254 "description": "How many times to run each task.", 255 }, 256 }, 257 "additionalProperties": False, 258 }, 259 }, 260 "additionalProperties": False, 261 }, 262 }, 263 ) 264 def retrigger_multiple(parameters, graph_config, input, task_group_id, task_id): 265 ( 266 decision_task_id, 267 full_task_graph, 268 label_to_taskid, 269 label_to_taskids, 270 ) = fetch_graph_and_labels(parameters, graph_config) 271 272 def modifier(task): 273 for request in input.get("requests", []): 274 if task.attributes.get("retrigger", False) and task.label in request.get( 275 "tasks" 276 ): 277 task.attributes["task_duplicates"] = request.get("times", 1) 278 return task 279 280 retrigger_tasks = [] 281 282 for request in input.get("requests", []): 283 rerun_tasks = [ 284 label 285 for label in request.get("tasks") 286 if not _should_retrigger(full_task_graph, label) 287 ] 288 for label in rerun_tasks: 289 # XXX we should not re-run tasks pulled in from other pushes 290 # In practice, this shouldn't matter, as only completed tasks 291 # are pulled in from other pushes and treeherder won't pass 292 # those labels. 293 for rerun_taskid in label_to_taskids[label]: 294 _rerun_task(rerun_taskid, label) 295 296 retrigger_tasks.extend([ 297 label 298 for label in request.get("tasks") 299 if _should_retrigger(full_task_graph, label) 300 ]) 301 302 create_tasks( 303 graph_config, 304 retrigger_tasks, 305 full_task_graph, 306 label_to_taskid, 307 parameters, 308 decision_task_id, 309 modifier=modifier, 310 action_tag="retrigger-multiple-task", 311 )