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