fune/taskcluster/taskgraph/task.py
Andrew Halberstadt e07398454b Bug 1659187 - [taskgraph] Implement a mechanism to only run tasks when a specified dependency is present, r=bhearsum
Certain tasks are meant to perform follow-up work to another task. For example,
a 'signing' task signs an artifact from a dependency task. An 'upload-symbols'
tasks uploads an artifact from a dependency task.

In general, we only want to run these types of tasks when the dependency task
is running. But in practice, these tasks often cause the dependency to get
pulled in.

We've tried to fix this in the past by making the 'run-on-projects' and
'optimization' keys match their primary dependency. But it's very easy to
mess this up, and some optimizations (like bugbug) take the content of the
task definition into account. So even if both tasks use the same optimization,
they could have different results.

This revision adds a new 'if-dependencies' key that denotes a task that should
only run if one of the dependencies specified in this list is also run. It
will allow us to stop playing whack-a-mole when trying to make sure tasks
aren't being pulled in solely due to these types of dependencies.

This feature is implemented independently of the 'optimization', so tasks that
specify this can still be optimized even if their primary dependency does run.

Differential Revision: https://phabricator.services.mozilla.com/D80508
2020-08-27 20:45:33 +00:00

101 lines
3.8 KiB
Python

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import absolute_import, print_function, unicode_literals
import attr
@attr.s
class Task(object):
"""
Representation of a task in a TaskGraph. Each Task has, at creation:
- kind: the name of the task kind
- label; the label for this task
- attributes: a dictionary of attributes for this task (used for filtering)
- task: the task definition (JSON-able dictionary)
- optimization: optimization to apply to the task (see taskgraph.optimize)
- dependencies: tasks this one depends on, in the form {name: label}, for example
{'build': 'build-linux64/opt', 'docker-image': 'build-docker-image-desktop-test'}
- soft_dependencies: tasks this one may depend on if they are available post
optimisation. They are set as a list of tasks label.
- if_dependencies: only run this task if at least one of these dependencies
are present.
And later, as the task-graph processing proceeds:
- task_id -- TaskCluster taskId under which this task will be created
This class is just a convenience wrapper for the data type and managing
display, comparison, serialization, etc. It has no functionality of its own.
"""
kind = attr.ib()
label = attr.ib()
attributes = attr.ib()
task = attr.ib()
description = attr.ib(default="")
task_id = attr.ib(default=None, init=False)
optimization = attr.ib(default=None)
dependencies = attr.ib(factory=dict)
soft_dependencies = attr.ib(factory=list)
if_dependencies = attr.ib(factory=list)
release_artifacts = attr.ib(
converter=attr.converters.optional(frozenset),
default=None,
)
def __attrs_post_init__(self):
self.attributes['kind'] = self.kind
@property
def name(self):
if self.label.startswith(self.kind + "-"):
return self.label[len(self.kind)+1:]
elif self.label.startswith("build-docker-image-"):
return self.label[len("build-docker-image-"):]
else:
raise AttributeError("Task {} does not have a name.".format(self.label))
def to_json(self):
rv = {
'kind': self.kind,
'label': self.label,
'description': self.description,
'attributes': self.attributes,
'dependencies': self.dependencies,
'soft_dependencies': sorted(self.soft_dependencies),
'if_dependencies': self.if_dependencies,
'optimization': self.optimization,
'task': self.task,
}
if self.task_id:
rv['task_id'] = self.task_id
if self.release_artifacts:
rv['release_artifacts'] = sorted(self.release_artifacts)
return rv
@classmethod
def from_json(cls, task_dict):
"""
Given a data structure as produced by taskgraph.to_json, re-construct
the original Task object. This is used to "resume" the task-graph
generation process, for example in Action tasks.
"""
rv = cls(
kind=task_dict['kind'],
label=task_dict['label'],
description=task_dict.get('description', ""),
attributes=task_dict['attributes'],
task=task_dict['task'],
optimization=task_dict['optimization'],
dependencies=task_dict.get('dependencies'),
soft_dependencies=task_dict.get('soft_dependencies'),
if_dependencies=task_dict.get('if_dependencies'),
release_artifacts=task_dict.get('release-artifacts'),
)
if 'task_id' in task_dict:
rv.task_id = task_dict['task_id']
return rv