Bug 1720181 - migration to remove SETA table (#7371)

This commit is contained in:
Sebastian Hengst 2022-01-31 22:06:15 +01:00 коммит произвёл GitHub
Родитель 3064c54faa
Коммит 5541f65cef
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
34 изменённых файлов: 3 добавлений и 1842 удалений

Просмотреть файл

@ -54,7 +54,7 @@ elif [ "$1" == "worker_log_parser_fail_json_unsheriffed" ]; then
# Tasks that don't need a dedicated worker.
elif [ "$1" == "worker_misc" ]; then
export REMAP_SIGTERM=SIGQUIT
exec newrelic-admin run-program celery worker -A treeherder --without-gossip --without-mingle --without-heartbeat -Q default,generate_perf_alerts,pushlog,seta_analyze_failures --concurrency=3
exec newrelic-admin run-program celery worker -A treeherder --without-gossip --without-mingle --without-heartbeat -Q default,generate_perf_alerts,pushlog --concurrency=3
# Cron jobs
elif [ "$1" == "run_intermittents_commenter" ]; then

Просмотреть файл

@ -1,85 +0,0 @@
# SETA
SETA finds the minimum set of jobs to run in order to catch all failures that our automation has found in the recent past on Firefox development repositories.
There's one main API that SETA offers consumers (e.g. the Gecko decision task) in order to show which jobs are consider low value
(less likely to catch a regression). After a certain number of calls, the API will return all jobs that need to be run.
SETA creates job priorities for all jobs found in the runnable-jobs API for that repository.
Initially all jobs will be treated as low value, however, once we run the test to analyze past
failures we will mark certain jobs as high value (priority=1).
Also note that jobs from different platforms (linux64 vs win7) or different CI systems (Buildbot vs TaskCluster)
will be treated the same (use the same job priority). In other words, a job priority can represent a multiple
number of jobs.
Jobs that appear on Treeherder for the first time will be treated as a job with high priority for a couple of
weeks since we don't have historical data to determine how likely they're to catch a code regression.
In order to find open bugs for SETA visit list of [SETA bugs].
[seta bugs]: https://bugzilla.mozilla.org/buglist.cgi?product=Tree%20Management&component=Treeherder%3A%20SETA&resolution=---
## API
- `/api/project/{project}/seta/job-priorities/`
- This is the API that consumers like the Gecko decision task will use
- Currently only available for `autoland` and `try`
## Local set up
- Follow the steps at [Starting a local Treeherder instance].
- Basically `docker-compose up`. This will initialize SETA's data
- Try out the various APIs:
- <http://localhost:8000/api/project/autoland/seta/job-priorities/>
[starting a local treeherder instance]: installation.md#starting-a-local-treeherder-instance
## Local development
If you have ingested invalid `preseed.json` data you can clear like this:
```bash
./manage.py initialize_seta --clear-job-priority-table
```
If you want to validate `preseed.json` you can do so like this:
```bash
./manage.py load_preseed --validate
```
## Maintenance
Sometimes the default behaviour of SETA is not adequate (e.g. new jobs noticed get a 2 week expiration date & a high priority) when adding new platforms (e.g. stylo).
Instead of investing more on accommodating for various scenarios weve decided to document how to make changes in the DB when we have to.
If you want to inspect the priorities for various jobs and platforms you can query the JobPriority table from reDash.
Use this query as a starting point:
<https://sql.telemetry.mozilla.org/queries/14771/source#table>
### Steps for adjusting jobs
Sometimes, before you can adjust priorities of the jobs, you need to make sure they make it into the JobPriority table.
In order to do so we need to update the job priority table from the shell. You will need cloudOps to do this for you:
Open the Python shell using `./manage.py shell`, then enter:
```python
from treeherder.seta.update_job_priority import update_job_priority_table
update_job_priority_table()
```
If you want to remove the 2 week grace period and make the job low priority (priority=5) do something similar to this:
```python
from treeherder.seta.models import JobPriority;
# Inspect the jobs you want to change
# Change the values appropriately
JobPriority.objects.filter(platform="windows7-32-stylo", priority=1)
JobPriority.objects.filter(platform="windows7-32-stylo", expiration_date__isnull=False)
# Once satisfied
JobPriority.objects.filter(platform="windows7-32-stylo", priority=1).update(priority=5);
JobPriority.objects.filter(platform="windows7-32-stylo", expiration_date__isnull=False).update(expiration_date=None)
```

Просмотреть файл

@ -10,8 +10,6 @@ if [ "${DATABASE_URL}" == "mysql://root@mysql/treeherder" ] ||
echo '-----> Running Django migrations and loading reference data'
./manage.py migrate --noinput
./manage.py load_initial_data
echo '-----> Initialize SETA'
./manage.py initialize_seta
fi
exec "$@"

Просмотреть файл

@ -47,5 +47,4 @@ nav:
- Accessing data: 'accessing_data.md'
- Data retention: 'data_cycling.md'
- Submitting data: 'submitting_data.md'
- SETA: 'seta.md'
- Manual test cases: 'testcases.md'

Просмотреть файл

@ -679,7 +679,7 @@
},
{
"context": [],
"description": "Run tests in the selected push that were optimized away, usually by SETA.\nThis action is for use on pushes that will be merged into another branch,to check that optimization hasn't hidden any failures.",
"description": "Run tests in the selected push that were optimized away.\nThis action is for use on pushes that will be merged into another branch, to check that optimization hasn't hidden any failures.",
"extra": {
"actionPerm": "generic"
},
@ -689,7 +689,7 @@
"decision": {
"action": {
"cb_name": "run-missing-tests",
"description": "Run tests in the selected push that were optimized away, usually by SETA.\nThis action is for use on pushes that will be merged into another branch,to check that optimization hasn't hidden any failures.",
"description": "Run tests in the selected push that were optimized away.\nThis action is for use on pushes that will be merged into another branch, to check that optimization hasn't hidden any failures.",
"name": "run-missing-tests",
"symbol": "rmt",
"taskGroupId": "f7Jj_h6MTEKr5Ln_7aFqbw",

Просмотреть файл

@ -1,206 +0,0 @@
import pytest
from treeherder.model.models import Job, JobNote
from treeherder.seta.common import job_priority_index
from treeherder.seta.models import JobPriority
from treeherder.seta.settings import SETA_HIGH_VALUE_PRIORITY, SETA_LOW_VALUE_PRIORITY
from treeherder.seta.update_job_priority import _sanitize_data
@pytest.fixture
def runnable_jobs_data():
repository_name = 'test_treeherder_jobs'
runnable_jobs = [
{
"build_system_type": "buildbot",
"job_type_name": "W3C Web Platform Tests",
"platform": "windows8-64",
"platform_option": "debug",
"ref_data_name": "Windows 8 64-bit {} debug test web-platform-tests-1".format(
repository_name
),
},
{
"build_system_type": "buildbot",
"job_type_name": "Reftest e10s",
"platform": "linux32",
"platform_option": "opt",
"ref_data_name": "Ubuntu VM 12.04 {} opt test reftest-e10s-1".format(repository_name),
},
{
"build_system_type": "buildbot",
"job_type_name": "Build",
"platform": "osx-10-7",
"platform_option": "opt",
"ref_data_name": "OS X 10.7 {} build".format(repository_name),
},
{
"build_system_type": "taskcluster",
"job_type_name": "test-linux32/opt-reftest-e10s-1",
"platform": "linux32",
"platform_option": "opt",
"ref_data_name": "test-linux32/opt-reftest-e10s-1",
},
{
"build_system_type": "taskcluster",
"job_type_name": "test-linux64/opt-reftest-e10s-2",
"platform": "linux64",
"platform_option": "opt",
"ref_data_name": "test-linux64/opt-reftest-e10s-2",
},
]
return runnable_jobs
@pytest.fixture
def tc_latest_gecko_decision_index(test_repository):
return {
"namespace": "gecko.v2.{}.latest.taskgraph.decision".format(test_repository),
"taskId": "XVDNiP07RNaaEghhvkZJWg",
"rank": 0,
"data": {},
"expires": "2018-01-04T20:36:11.375Z",
}
@pytest.fixture
def sanitized_data(runnable_jobs_data):
return _sanitize_data(runnable_jobs_data)
@pytest.fixture
def all_job_priorities_stored(job_priority_list):
"""Stores sample job priorities
If you include this fixture in your tests it will guarantee
to insert job priority data into the temporary database.
"""
for jp in job_priority_list:
jp.save()
return job_priority_list
@pytest.fixture
def job_priority_list(sanitized_data):
jp_list = []
for datum in sanitized_data:
jp_list.append(
JobPriority(
testtype=datum['testtype'],
buildtype=datum['platform_option'],
platform=datum['platform'],
buildsystem=datum['build_system_type'],
priority=SETA_HIGH_VALUE_PRIORITY,
)
)
# Mark the reftest-e10s-2 TC job as low priority (unique to TC)
if datum['testtype'] == 'reftest-e10s-2':
jp_list[-1].priority = SETA_LOW_VALUE_PRIORITY
# Mark the web-platform-tests-1 BB job as low priority (unique to BB)
if datum['testtype'] == 'web-platform-tests-1':
jp_list[-1].priority = SETA_LOW_VALUE_PRIORITY
return jp_list
@pytest.fixture
def jp_index_fixture(job_priority_list):
return job_priority_index(job_priority_list)
@pytest.fixture
def fifteen_jobs_with_notes(
eleven_jobs_stored, taskcluster_jobs_stored, test_user, failure_classifications
):
"""provide 15 jobs with job notes."""
counter = 0
for job in Job.objects.all():
counter += 1
# add 5 valid job notes related to 'this is revision x'
if counter < 6:
JobNote.objects.create(
job=job, failure_classification_id=2, user=test_user, text="this is revision x"
)
continue
# add 3 valid job notes with raw revision 31415926535
if counter < 9:
JobNote.objects.create(
job=job, failure_classification_id=2, user=test_user, text="314159265358"
)
continue
# Add 3 job notes with full url to revision, expected to map to 31415926535
if counter < 12:
JobNote.objects.create(
job=job,
failure_classification_id=2,
user=test_user,
text="http://hg.mozilla.org/mozilla-central/314159265358",
)
continue
# Add 1 valid job with trailing slash, expected to map to 31415926535
if counter < 13:
JobNote.objects.create(
job=job,
failure_classification_id=2,
user=test_user,
text="http://hg.mozilla.org/mozilla-central/314159265358/",
)
continue
# Add 1 job with invalid revision text, expect it to be ignored
if counter < 14:
# We will ignore this based on text length
JobNote.objects.create(
job=job, failure_classification_id=2, user=test_user, text="too short"
)
continue
# Add 1 job with no revision text, expect it to be ignored
if counter < 15:
# We will ignore this based on blank note
JobNote.objects.create(job=job, failure_classification_id=2, user=test_user, text="")
continue
# Add 1 more job with invalid revision text, expect it to be ignored
if counter < 16:
# We will ignore this based on effectively blank note
JobNote.objects.create(job=job, failure_classification_id=2, user=test_user, text="/")
continue
# if we have any more jobs defined it will break this test, ignore
continue
@pytest.fixture
def failures_fixed_by_commit():
# We expect 'windows8-32', 'windowsxp' and 'osx-10-7' jobs to be excluded because those
# platforms are currently unsupported SETA platforms (see treeherder/seta/settings.py)
# Also we expect any jobs with invalid job notes to be excluded
return {
u'this is revision x': [
(u'b2g_mozilla-release_emulator-jb-debug_dep', u'debug', u'b2g-emu-jb'),
(u'b2g_mozilla-release_emulator-jb_dep', u'opt', u'b2g-emu-jb'),
(u'mochitest-browser-chrome', u'debug', u'osx-10-6'),
(u'mochitest-browser-chrome', u'debug', u'windows7-32'),
],
u'314159265358': [
(u'b2g_mozilla-release_emulator-jb-debug_dep', u'debug', u'b2g-emu-jb'),
(u'b2g_mozilla-release_emulator-debug_dep', u'debug', u'b2g-emu-ics'),
(u'b2g_mozilla-release_inari_dep', u'opt', u'b2g-device-image'),
(u'b2g_mozilla-release_nexus-4_dep', u'opt', u'b2g-device-image'),
(u'mochitest-devtools-chrome-3', u'debug', u'linux64'),
],
}
@pytest.fixture
def patched_seta_fixed_by_commit_repos(monkeypatch, test_repository):
patched = [test_repository.name]
monkeypatch.setattr("treeherder.seta.analyze_failures.SETA_FIXED_BY_COMMIT_REPOS", patched)
return patched

Просмотреть файл

@ -1,16 +0,0 @@
import pytest
from treeherder.seta.analyze_failures import get_failures_fixed_by_commit
@pytest.mark.django_db()
def test_analyze_failures(
fifteen_jobs_with_notes, failures_fixed_by_commit, patched_seta_fixed_by_commit_repos
):
ret = get_failures_fixed_by_commit()
exp = failures_fixed_by_commit
assert sorted(ret.keys()) == sorted(exp.keys())
for key in exp:
assert sorted(ret[key]) == sorted(exp[key])

Просмотреть файл

@ -1,8 +0,0 @@
import pytest
from treeherder.seta.high_value_jobs import get_high_value_jobs
@pytest.mark.django_db()
def test_get_high_value_jobs(fifteen_jobs_with_notes, failures_fixed_by_commit):
get_high_value_jobs(failures_fixed_by_commit)

Просмотреть файл

@ -1,39 +0,0 @@
import datetime
import pytest
from mock import patch
from treeherder.seta.job_priorities import SetaError, seta_job_scheduling
@pytest.mark.django_db()
@patch('treeherder.seta.job_priorities.SETAJobPriorities._validate_request', return_value=None)
@patch('treeherder.etl.seta.list_runnable_jobs')
def test_gecko_decision_task(
runnable_jobs_list,
validate_request,
test_repository,
runnable_jobs_data,
all_job_priorities_stored,
):
"""
When the Gecko decision task calls SETA it will return all jobs that are less likely to catch
a regression (low value jobs).
"""
runnable_jobs_list.return_value = runnable_jobs_data
jobs = seta_job_scheduling(project=test_repository.name, build_system_type='taskcluster')
assert len(jobs['jobtypes'][str(datetime.date.today())]) == 1
def test_gecko_decision_task_invalid_repo():
"""
When the Gecko decision task calls SETA it will return all jobs that are less likely to catch
a regression (low value jobs).
"""
with pytest.raises(SetaError) as exception_info:
seta_job_scheduling(project='mozilla-repo-x', build_system_type='taskcluster')
assert (
str(exception_info.value) == "The specified project repo 'mozilla-repo-x' "
"is not supported by SETA."
)

Просмотреть файл

@ -1,63 +0,0 @@
import datetime
import pytest
from django.db.utils import IntegrityError
from django.utils import timezone
from treeherder.seta.models import JobPriority
TOMORROW = timezone.now() + datetime.timedelta(days=1)
YESTERDAY = timezone.now() - datetime.timedelta(days=1)
# JobPriority tests
def test_expired_job_priority():
jp = JobPriority(
testtype='web-platform-tests-1',
buildtype='opt',
platform='windows8-64',
priority=1,
expiration_date=YESTERDAY,
buildsystem='taskcluster',
)
assert jp.has_expired()
def test_not_expired_job_priority():
jp = JobPriority(
testtype='web-platform-tests-1',
buildtype='opt',
platform='windows8-64',
priority=1,
expiration_date=TOMORROW,
buildsystem='taskcluster',
)
assert not jp.has_expired()
@pytest.mark.django_db()
def test_null_testtype():
'''The expiration date accepts null values'''
with pytest.raises(IntegrityError):
JobPriority.objects.create(
testtype=None,
buildtype='opt',
platform='windows8-64',
priority=1,
expiration_date=TOMORROW,
buildsystem='taskcluster',
)
@pytest.mark.django_db()
def test_null_expiration_date():
'''The expiration date accepts null values'''
jp = JobPriority.objects.create(
testtype='web-platform-tests-2',
buildtype='opt',
platform='windows8-64',
priority=1,
expiration_date=None,
buildsystem='taskcluster',
)
assert jp.expiration_date is None

Просмотреть файл

@ -1,98 +0,0 @@
import pytest
from mock import patch
from treeherder.seta.models import JobPriority
from treeherder.seta.update_job_priority import (
_initialize_values,
_sanitize_data,
_unique_key,
_update_table,
query_sanitized_data,
)
def test_unique_key():
new_job = {
'build_system_type': 'buildbot',
'platform': 'windows8-64',
'platform_option': 'opt',
'testtype': 'web-platform-tests-1',
}
assert _unique_key(new_job), ('web-platform-tests-1', 'opt', 'windows8-64')
def test_sanitize_data(runnable_jobs_data):
data = _sanitize_data(runnable_jobs_data)
bb_jobs = 0
tc_jobs = 0
for datum in data:
if datum['build_system_type'] in ('taskcluster', '*'):
tc_jobs += 1
if datum['build_system_type'] in ('buildbot', '*'):
bb_jobs += 1
assert bb_jobs == 2
assert tc_jobs == 2
@patch('treeherder.seta.update_job_priority.list_runnable_jobs')
def test_query_sanitized_data(list_runnable_jobs, runnable_jobs_data, sanitized_data):
list_runnable_jobs.return_value = runnable_jobs_data
data = query_sanitized_data()
assert data == sanitized_data
@pytest.mark.django_db()
def test_initialize_values_no_data():
results = _initialize_values()
assert results == ({}, 5, None)
@patch.object(JobPriority, 'save')
@patch('treeherder.seta.update_job_priority._initialize_values')
def test_update_table_empty_table(initial_values, jp_save, sanitized_data):
"""
We test that starting from an empty table
"""
# This set of values is when we're bootstrapping the service (aka empty table)
initial_values.return_value = {}, 5, None
jp_save.return_value = None # Since we don't want to write to the DB
assert _update_table(sanitized_data) == (3, 0, 0)
@pytest.mark.django_db()
def test_update_table_job_from_other_buildsysten(all_job_priorities_stored):
# We already have a TaskCluster job like this in the DB
# The DB entry should be changed to '*'
data = {
'build_system_type': 'buildbot',
'platform': 'linux64',
'platform_option': 'opt',
'testtype': 'reftest-e10s-2',
}
# Before calling update_table the priority is only for TaskCluster
assert (
len(
JobPriority.objects.filter(
buildsystem='taskcluster',
buildtype=data['platform_option'],
platform=data['platform'],
testtype=data['testtype'],
)
)
== 1
)
# We are checking that only 1 job was updated
ret_val = _update_table([data])
assert ret_val == (0, 0, 1)
assert (
len(
JobPriority.objects.filter(
buildsystem='*',
buildtype=data['platform_option'],
platform=data['platform'],
testtype=data['testtype'],
)
)
== 1
)

Просмотреть файл

@ -77,7 +77,6 @@ INSTALLED_APPS = [
'treeherder.log_parser',
'treeherder.etl',
'treeherder.perf',
'treeherder.seta',
'treeherder.intermittents_commenter',
'treeherder.changelog',
]
@ -320,7 +319,6 @@ CELERY_TASK_QUEUES = [
Queue('generate_perf_alerts', Exchange('default'), routing_key='generate_perf_alerts'),
Queue('store_pulse_tasks', Exchange('default'), routing_key='store_pulse_tasks'),
Queue('store_pulse_pushes', Exchange('default'), routing_key='store_pulse_pushes'),
Queue('seta_analyze_failures', Exchange('default'), routing_key='seta_analyze_failures'),
]
# Force all queues to be explicitly listed in `CELERY_TASK_QUEUES` to help prevent typos
@ -365,12 +363,6 @@ CELERY_BEAT_SCHEDULE = {
'relative': True,
'options': {"queue": "pushlog"},
},
'seta-analyze-failures': {
'task': 'seta-analyze-failures',
'schedule': timedelta(days=1),
'relative': True,
'options': {'queue': "seta_analyze_failures"},
},
}
# CORS Headers

Просмотреть файл

@ -1,120 +0,0 @@
import logging
from django.core.cache import cache
from treeherder.etl.runnable_jobs import list_runnable_jobs
from treeherder.seta.common import convert_job_type_name_to_testtype, unique_key
from treeherder.seta.models import JobPriority
from treeherder.seta.settings import (
SETA_REF_DATA_NAMES_CACHE_TIMEOUT,
SETA_SUPPORTED_TC_JOBTYPES,
SETA_UNSUPPORTED_PLATFORMS,
SETA_UNSUPPORTED_TESTTYPES,
)
logger = logging.getLogger(__name__)
def is_job_blacklisted(testtype):
if not testtype:
return True
return testtype in SETA_UNSUPPORTED_TESTTYPES
def parse_testtype(build_system_type, job_type_name, platform_option, ref_data_name):
"""
Buildbot Taskcluster
----------- -----------
build_system_type buildbot taskcluster
job_type_name Mochitest task label
platform_option debug,opt,pgo debug,opt,pgo
ref_data_name buildername task label OR signature hash
"""
# XXX: Figure out how to ignore build, lint, etc. jobs
# https://bugzilla.mozilla.org/show_bug.cgi?id=1318659
testtype = None
if build_system_type == 'buildbot':
# The testtype of builbot job can been found in 'ref_data_name'
# like web-platform-tests-4 in "Ubuntu VM 12.04 x64 mozilla-inbound
# opt test web-platform-tests-4"
testtype = ref_data_name.split(' ')[-1]
else:
if job_type_name.startswith(tuple(SETA_SUPPORTED_TC_JOBTYPES)):
# we should get "jittest-3" as testtype for a job_type_name like
# test-linux64/debug-jittest-3
testtype = convert_job_type_name_to_testtype(job_type_name)
return testtype
def valid_platform(platform):
# We only care about in-tree scheduled tests and ignore out of band system like autophone.
return platform not in SETA_UNSUPPORTED_PLATFORMS
def job_priorities_to_jobtypes():
jobtypes = []
for jp in JobPriority.objects.all():
jobtypes.append(jp.unique_identifier())
return jobtypes
# The only difference between projects is that their list will be based
# on their own specific runnable_jobs.json artifact
def get_reference_data_names(project="autoland", build_system="taskcluster"):
"""
We want all reference data names for every task that runs on a specific project.
For example: "test-linux64/opt-mochitest-webgl-e10s-1"
"""
# we cache the reference data names in order to reduce API calls
cache_key = '{}-{}-ref_data_names_cache'.format(project, build_system)
ref_data_names_map = cache.get(cache_key)
if ref_data_names_map:
return ref_data_names_map
logger.debug("We did not hit the cache.")
# cache expired so re-build the reference data names map; the map
# contains the ref_data_name of every Treeherder task for this project
ignored_jobs = []
ref_data_names = {}
runnable_jobs = list_runnable_jobs(project)
for job in runnable_jobs:
# get testtype e.g. web-platform-tests-4
testtype = parse_testtype(
build_system_type=job['build_system_type'],
job_type_name=job['job_type_name'],
platform_option=job['platform_option'],
ref_data_name=job['ref_data_name'],
)
if not valid_platform(job['platform']):
continue
if is_job_blacklisted(testtype):
ignored_jobs.append(job['ref_data_name'])
if testtype:
logger.debug(
'get_reference_data_names: blacklisted testtype {} for job {}'.format(
testtype, job
)
)
continue
key = unique_key(
testtype=testtype, buildtype=job['platform_option'], platform=job['platform']
)
if build_system == '*':
ref_data_names[key] = job['ref_data_name']
elif job['build_system_type'] == build_system:
ref_data_names[key] = job['ref_data_name']
logger.debug('Ignoring %s', ', '.join(sorted(ignored_jobs)))
# update the cache
cache.set(cache_key, ref_data_names_map, SETA_REF_DATA_NAMES_CACHE_TIMEOUT)
return ref_data_names

Просмотреть файл

@ -16,4 +16,3 @@ class Command(BaseCommand):
'performance_bug_templates',
'performance_tag',
)
call_command('load_preseed')

Просмотреть файл

Просмотреть файл

@ -1,149 +0,0 @@
import logging
from collections import defaultdict
from datetime import timedelta
from django.utils import timezone
from treeherder.etl.seta import is_job_blacklisted, parse_testtype
from treeherder.model import models
from treeherder.seta.common import unique_key
from treeherder.seta.high_value_jobs import get_high_value_jobs
from treeherder.seta.models import JobPriority
from treeherder.seta.settings import (
SETA_FIXED_BY_COMMIT_DAYS,
SETA_FIXED_BY_COMMIT_REPOS,
SETA_SUPPORTED_TC_JOBTYPES,
SETA_UNSUPPORTED_PLATFORMS,
)
from treeherder.seta.update_job_priority import update_job_priority_table
logger = logging.getLogger(__name__)
class AnalyzeFailures:
def __init__(self, **options):
self.dry_run = options.get('dry_run', False)
def run(self):
fixed_by_commit_jobs = get_failures_fixed_by_commit()
if fixed_by_commit_jobs:
# We need to update the job priority table before we can call get_high_value_jobs()
update_job_priority_table()
high_value_jobs = get_high_value_jobs(fixed_by_commit_jobs)
if not self.dry_run:
logger.warning("Let's see if we need to increase the priority of any job")
JobPriority.objects.clear_expiration_field_for_expired_jobs()
JobPriority.objects.adjust_jobs_priority(high_value_jobs)
def get_failures_fixed_by_commit():
"""Return all job failures annotated with "fixed by commit" grouped by reason given for annotation.
It returns a dictionary with a revision or bug ID as the key (bug ID is used for
intermittent failures and the revision is used for real failures). For SETA's purposes
we only care about revisions (real failures).
The failures for *real failures* will contain all jobs that have been starred as "fixed by commit".
Notice that the data does not tell you on which repository a root failure was fixed.
For instance, in the raw data you might see a reference to 9fa614d8310d which is a back out
and it is referenced by 12 starred jobs:
https://treeherder.mozilla.org/#/jobs?repo=autoland&filter-searchStr=android%20debug%20cpp&tochange=9fa614d8310db9aabe85cc3c3cff6281fe1edb0c
The raw data will show those 12 jobs.
The returned data will look like this:
{
"44d29bac3654": [
["android-4-0-armv7-api15", "opt", "android-lint"],
["android-4-0-armv7-api15", "opt", "android-api-15-gradle-dependencies"],
]
}
"""
failures = defaultdict(list)
option_collection_map = models.OptionCollection.objects.get_option_collection_map()
fixed_by_commit_data_set = (
models.JobNote.objects.filter(
failure_classification=2,
created__gt=timezone.now() - timedelta(days=SETA_FIXED_BY_COMMIT_DAYS),
text__isnull=False,
job__repository__name__in=SETA_FIXED_BY_COMMIT_REPOS,
)
.exclude(job__signature__build_platform__in=SETA_UNSUPPORTED_PLATFORMS)
.exclude(text="")
.select_related('job', 'job__signature', 'job__job_type')
)
# check if at least one fixed by commit job meets our requirements without populating queryset
if not fixed_by_commit_data_set.exists():
logger.warning("We couldn't find any fixed-by-commit jobs")
return failures
# now process the fixed by commit jobs in batches using django's queryset iterator
for job_note in fixed_by_commit_data_set.iterator():
# if we have http://hg.mozilla.org/rev/<rev> and <rev>, we will only use <rev>
revision_id = job_note.text.strip('/')
revision_id = revision_id.split('/')[-1]
# This prevents the empty string case and ignores bug ids
if not revision_id or len(revision_id) < 12:
continue
# We currently don't guarantee that text is actually a revision
# Even if not perfect the main idea is that a bunch of jobs were annotated with
# a unique identifier. The assumption is that the text is unique
#
# I've seen these values being used:
# * 12 char revision
# * 40 char revision
# * link to revision on hg
# * revisionA & revisionB
# * should be fixed by <revision>
# * bug id
#
# Note that if some jobs are annotated with the 12char revision and others with the
# 40char revision we will have two disjunct set of failures
#
# Some of this will be improved in https://bugzilla.mozilla.org/show_bug.cgi?id=1323536
try:
# check if jobtype is supported by SETA (see treeherder/seta/settings.py)
if job_note.job.signature.build_system_type != 'buildbot':
if not job_note.job.job_type.name.startswith(tuple(SETA_SUPPORTED_TC_JOBTYPES)):
continue
testtype = parse_testtype(
build_system_type=job_note.job.signature.build_system_type, # e.g. taskcluster
job_type_name=job_note.job.job_type.name, # e.g. Mochitest
platform_option=job_note.job.get_platform_option(
option_collection_map
), # e.g. 'opt'
ref_data_name=job_note.job.signature.name, # buildername or task label
)
if testtype:
if is_job_blacklisted(testtype):
continue
else:
logger.warning(
'We were unable to parse %s/%s',
job_note.job.job_type.name,
job_note.job.signature.name,
)
continue
# we now have a legit fixed-by-commit job failure
failures[revision_id].append(
unique_key(
testtype=testtype,
buildtype=job_note.job.get_platform_option(option_collection_map), # e.g. 'opt'
platform=job_note.job.signature.build_platform,
)
)
except models.Job.DoesNotExist:
logger.warning('job_note %s has no job associated to it', job_note.id)
continue
logger.warning("Number of fixed_by_commit revisions: %s", len(failures))
return failures

Просмотреть файл

@ -1,79 +0,0 @@
import logging
import re
logger = logging.getLogger(__name__)
class DuplicateKeyError(Exception):
pass
def unique_key(testtype, buildtype, platform):
'''This makes sure that we order consistently this unique identifier'''
return (testtype, buildtype, platform)
def job_priority_index(job_priorities):
'''This structure helps with finding data from the job priorities table'''
jp_index = {}
# Creating this data structure which reduces how many times we iterate through the DB rows
for jp in job_priorities:
key = jp.unique_identifier()
# This is guaranteed by a unique composite index for these 3 fields in models.py
if key in jp_index:
msg = '"{}" should be a unique job priority and that is unexpected.'.format(key)
raise DuplicateKeyError(msg)
# (testtype, buildtype, platform)
jp_index[key] = {'pk': jp.id, 'build_system_type': jp.buildsystem}
return jp_index
# The order of this is list is important as the more specific patterns
# will be processed before the less specific ones. This must be kept up
# to date with SETA_SUPPORTED_TC_JOBTYPES in settings.py.
RE_JOB_TYPE_NAMES = [
{'name': 'test', 'pattern': re.compile('test-[^/]+/[^-]+-(.*)$')},
{'name': 'desktop-test', 'pattern': re.compile('desktop-test-[^/]+/[^-]+-(.*)$')},
{'name': 'android-test', 'pattern': re.compile('android-test-[^/]+/[^-]+-(.*)$')},
{'name': 'source-test', 'pattern': re.compile('(source-test-[^/]+)(?:/.*)?$')},
{'name': 'build', 'pattern': re.compile('(build-[^/]+)/[^-]+$')},
{'name': 'spidermonkey', 'pattern': re.compile('(spidermonkey-[^/]+)/[^-]+$')},
{'name': 'iris', 'pattern': re.compile('(iris-[^/]+)/[^-]+$')},
{'name': 'webrender', 'pattern': re.compile('(webrender-.*)-(?:opt|debug|pgo)$')},
]
def convert_job_type_name_to_testtype(job_type_name):
"""job_type_names are essentially free form though there are
several patterns used in job_type_names.
test-<platform>/<buildtype>-<testtype> test-linux1804-64-shippable-qr/opt-reftest-e10s-5
build-<platform>/<buildtype> build-linux64-asan-fuzzing/opt
<testtype>-<buildtype> webrender-android-hw-p2-debug
Prior to Bug 1608427, only non-build tasks were eligible for
optimization using seta strategies and Treeherder's handling of
possible task labels failed to properly account for the different
job_type_names possible with build tasks. While investigating this
failure to support build tasks, it was discovered that other test
tasks did not match the expected job_type_name pattern. This
function ensures that job_type_names are converted to seta
testtypes in a consistent fashion.
"""
testtype = None
if not job_type_name.startswith('[funsize'):
for re_job_type_name in RE_JOB_TYPE_NAMES:
m = re_job_type_name['pattern'].match(job_type_name)
if m:
testtype = m.group(1)
break
if not testtype:
logger.warning(
'convert_job_type_name_to_testtype("{}") not matched. '
'Using job_type_name as is.'.format(job_type_name)
)
testtype = job_type_name
return testtype

Просмотреть файл

@ -1,102 +0,0 @@
import logging
from treeherder.etl.seta import job_priorities_to_jobtypes
logger = logging.getLogger(__name__)
def is_matched(failure, removals):
found = False
if failure in removals:
found = True
return found
def check_removal(failures, removals):
results = {}
for failure in failures:
results[failure] = []
for failure_job in failures[failure]:
found = is_matched(failure_job, removals)
# we will add the test to the resulting structure unless we find a match
# in the jobtype we are trying to ignore.
if not found:
results[failure].append(failure_job)
if not results[failure]:
del results[failure]
return results
def build_removals(active_jobs, failures, target):
"""
active_jobs - all possible desktop & android jobs on Treeherder (no PGO)
failures - list of all failures
target - percentage of failures we're going to process
Return list of jobs to remove and list of revisions that are regressed
"""
# Determine the number of failures we're going to process
# A failure is a revision + all of the jobs that were fixed by it
number_of_failures = int((target / 100) * len(failures))
low_value_jobs = []
for jobtype in active_jobs:
# Determine if removing an active job will reduce the number of failures we would catch
# or stay the same
remaining_failures = check_removal(failures, [jobtype])
if len(remaining_failures) >= number_of_failures:
low_value_jobs.append(jobtype)
failures = remaining_failures
else:
failed_revisions = []
for revision in failures:
if revision not in remaining_failures:
failed_revisions.append(revision)
logger.info(
"jobtype: %s is the root failure(s) of these %s revisions",
jobtype,
failed_revisions,
)
return low_value_jobs
def get_high_value_jobs(fixed_by_commit_jobs, target=100):
"""
fixed_by_commit_jobs:
Revisions and jobs that have been starred that are fixed with a push or a bug
target:
Percentage of failures to analyze
"""
total = len(fixed_by_commit_jobs)
logger.info("Processing %s revision(s)", total)
active_jobs = job_priorities_to_jobtypes()
low_value_jobs = build_removals(
active_jobs=active_jobs, failures=fixed_by_commit_jobs, target=target
)
# Only return high value jobs
for low_value_job in low_value_jobs:
try:
active_jobs.remove(low_value_job)
except ValueError:
logger.warning("%s is missing from the job list", low_value_job)
total = len(fixed_by_commit_jobs)
total_detected = check_removal(fixed_by_commit_jobs, low_value_jobs)
percent_detected = 100 * len(total_detected) / total
logger.info(
"We will detect %.2f%% (%s) of the %s failures",
percent_detected,
len(total_detected),
total,
)
return active_jobs

Просмотреть файл

@ -1,92 +0,0 @@
import datetime
import logging
from treeherder.etl.seta import get_reference_data_names, is_job_blacklisted, valid_platform
from treeherder.seta.models import JobPriority
from treeherder.seta.settings import SETA_LOW_VALUE_PRIORITY, SETA_PROJECTS, THE_FUTURE
logger = logging.getLogger(__name__)
class SetaError(Exception):
pass
class SETAJobPriorities:
"""
SETA JobPriority Implementation
"""
def _process(self, project, build_system, job_priorities):
'''Return list of ref_data_name for job_priorities'''
if not job_priorities:
raise SetaError("Call docker-compose run backend ./manage.py initialize_seta")
jobs = []
ref_data_names_map = get_reference_data_names(project, build_system)
# now check the JobPriority table against the list of valid runnable
for jp in job_priorities:
# if this JobPriority entry is no longer supported in SETA then ignore it
if not valid_platform(jp.platform):
continue
if is_job_blacklisted(jp.testtype):
continue
key = jp.unique_identifier()
if key in ref_data_names_map:
# e.g. desktop-test-linux64-pgo/opt-reftest-13 or builder name
jobs.append(ref_data_names_map[key])
else:
logger.warning(
'Job priority key %s for (%s) not found in accepted jobs list', key, jp
)
return jobs
def _query_job_priorities(self, priority, excluded_build_system_type):
job_priorities = JobPriority.objects.all()
if priority:
job_priorities = job_priorities.filter(priority=priority)
if excluded_build_system_type:
job_priorities = job_priorities.exclude(buildsystem=excluded_build_system_type)
return job_priorities
def _validate_request(self, build_system_type, project):
if build_system_type not in ('buildbot', 'taskcluster', '*'):
raise SetaError('Valid build_system_type values are buildbot or taskcluster.')
if project not in SETA_PROJECTS:
raise SetaError("The specified project repo '%s' is not supported by SETA." % project)
def seta_job_scheduling(self, project, build_system_type, priority=None):
self._validate_request(build_system_type, project)
if build_system_type == 'taskcluster':
if priority is None:
priority = SETA_LOW_VALUE_PRIORITY
job_priorities = []
for jp in self._query_job_priorities(
priority=priority, excluded_build_system_type='buildbot'
):
if jp.has_expired() or jp.expiration_date == THE_FUTURE:
job_priorities.append(jp)
ref_data_names = self._process(
project, build_system='taskcluster', job_priorities=job_priorities
)
else:
excluded_build_system_type = None
if build_system_type != '*':
excluded_build_system_type = (
'taskcluster' if build_system_type == 'buildbot' else 'buildbot'
)
job_priorities = self._query_job_priorities(priority, excluded_build_system_type)
ref_data_names = self._process(project, build_system_type, job_priorities)
# We don't really need 'jobtypes' and today's date in the returning data
# Getting rid of it will require the consumers to not expect it.
# https://bugzilla.mozilla.org/show_bug.cgi?id=1325405
return {'jobtypes': {str(datetime.date.today()): sorted(ref_data_names)}}
# create an instance of this class, and expose `seta_job_scheduling`
seta_job_scheduling = SETAJobPriorities().seta_job_scheduling

Просмотреть файл

Просмотреть файл

Просмотреть файл

@ -1,32 +0,0 @@
from django.core.management.base import BaseCommand
from treeherder.seta.analyze_failures import AnalyzeFailures
class Command(BaseCommand):
help = (
'Analyze jobs that failed and got tagged with fixed_by_commit '
'and change the priority and timeout of such job.'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def add_arguments(self, parser):
parser.add_argument(
"--dry-run",
action="store_true",
dest="dry_run",
help="This mode is for analyzing failures without " "updating the job priority table.",
)
parser.add_argument(
"--ignore-failures",
type=int,
dest="ignore_failures",
default=0,
help="If a job fails less than N times we don't take that job" "into account.",
)
def handle(self, *args, **options):
AnalyzeFailures(**options).run()

Просмотреть файл

@ -1,46 +0,0 @@
import logging
from django.core.management.base import BaseCommand
from treeherder.seta.models import JobPriority
from treeherder.seta.preseed import load_preseed
from treeherder.seta.update_job_priority import update_job_priority_table
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = 'Initialize or update SETA data; It causes no harm to run on production'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def add_arguments(self, parser):
parser.add_argument(
'--clear-job-priority-table',
action='store_true',
dest='clear_jp_table',
default=False,
help='Delete all entries in the JobPriority table.',
)
def clear_job_priority_table(self):
logger.info('Number of items in table: %d', JobPriority.objects.count())
logger.info('Deleting all entries in the job priority table.')
JobPriority.objects.all().delete()
logger.info('Number of items in table: %d', JobPriority.objects.count())
def initialize_seta(self):
logger.info('Updating JobPriority table.')
logger.info('Number of items in table: %d', JobPriority.objects.count())
update_job_priority_table()
logger.info('Loading preseed table.')
logger.info('Number of items in table: %d', JobPriority.objects.count())
load_preseed()
logger.info('Number of items in table: %d', JobPriority.objects.count())
def handle(self, *args, **options):
if options['clear_jp_table']:
self.clear_job_priority_table()
else:
self.initialize_seta()

Просмотреть файл

@ -1,20 +0,0 @@
from django.core.management.base import BaseCommand
from treeherder.seta.preseed import load_preseed
class Command(BaseCommand):
help = 'Update job priority table with data based on preseed.json'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def add_arguments(self, parser):
parser.add_argument(
"--validate",
action="store_true",
help="This will validate that all entries in preseed.json are valid",
)
def handle(self, *args, **options):
load_preseed(options.get("validate"))

Просмотреть файл

@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-01-26 15:42
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='JobPriority',
fields=[
(
'id',
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
),
),
('testtype', models.CharField(max_length=128)),
('buildsystem', models.CharField(max_length=64)),
('buildtype', models.CharField(max_length=64)),
('platform', models.CharField(max_length=64)),
('priority', models.IntegerField()),
('expiration_date', models.DateTimeField(null=True)),
],
),
]

Просмотреть файл

Просмотреть файл

@ -1,66 +0,0 @@
import logging
from django.db import models
from django.utils import timezone
from treeherder.seta.common import unique_key
from treeherder.seta.settings import SETA_LOW_VALUE_PRIORITY
logger = logging.getLogger(__name__)
class JobPriorityManager(models.Manager):
def clear_expiration_field_for_expired_jobs(self):
'''Set the expiration date of every job that has expired.'''
# Only select rows where there is an expiration date set
for job in JobPriority.objects.filter(expiration_date__isnull=False):
if job.has_expired():
job.expiration_date = None
job.save()
def adjust_jobs_priority(self, high_value_jobs, priority=1):
"""For every job priority determine if we need to increase or decrease the job priority
Currently, high value jobs have a priority of 1 and a timeout of 0.
"""
# Only job priorities that don't have an expiration date (2 weeks for new jobs or year 2100
# for jobs update via load_preseed) are updated
for jp in JobPriority.objects.filter(expiration_date__isnull=True):
if jp.unique_identifier() not in high_value_jobs:
if jp.priority != SETA_LOW_VALUE_PRIORITY:
logger.warning('Decreasing priority of %s', jp.unique_identifier())
jp.priority = SETA_LOW_VALUE_PRIORITY
jp.save(update_fields=['priority'])
elif jp.priority != priority:
logger.warning('Increasing priority of %s', jp.unique_identifier())
jp.priority = priority
jp.save(update_fields=['priority'])
class JobPriority(models.Model):
# Use custom manager
objects = JobPriorityManager()
# This field is sanitized to unify name from Buildbot and TaskCluster
testtype = models.CharField(max_length=128) # e.g. web-platform-tests-1
buildsystem = models.CharField(max_length=64)
buildtype = models.CharField(max_length=64) # e.g. {opt,pgo,debug}
platform = models.CharField(max_length=64) # e.g. windows8-64
priority = models.IntegerField() # 1 or 5
expiration_date = models.DateTimeField(null=True)
# Q: Do we need indexing?
unique_together = ('testtype', 'buildtype', 'platform')
def has_expired(self):
now = timezone.now()
if self.expiration_date:
return self.expiration_date < now
else:
return True
def unique_identifier(self):
return unique_key(testtype=self.testtype, buildtype=self.buildtype, platform=self.platform)
def __str__(self):
return ','.join((self.buildsystem, self.testtype, self.buildtype, self.platform))

Просмотреть файл

@ -1,82 +0,0 @@
[
{
"buildtype": "asan",
"testtype": "build-android-x86_64-asan-fuzzing/opt",
"platform": "android-5-0-x86_64",
"priority": 5,
"expiration_date": "*",
"buildsystem": "taskcluster"
},
{
"buildtype": "asan",
"testtype": "build-linux64-asan-fuzzing/opt",
"platform": "linux64",
"priority": 5,
"expiration_date": "*",
"buildsystem": "taskcluster"
},
{
"buildtype": "opt",
"testtype": "build-linux64-fuzzing-ccov/opt",
"platform": "linux64",
"priority": 5,
"expiration_date": "*",
"buildsystem": "taskcluster"
},
{
"buildtype": "debug",
"testtype": "build-linux64-fuzzing/debug",
"platform": "linux64",
"priority": 5,
"expiration_date": "*",
"buildsystem": "taskcluster"
},
{
"buildtype": "asan",
"testtype": "build-macosx64-asan-fuzzing/opt",
"platform": "osx-cross",
"priority": 5,
"expiration_date": "*",
"buildsystem": "taskcluster"
},
{
"buildtype": "debug",
"testtype": "build-macosx64-fuzzing/debug",
"platform": "osx-cross",
"priority": 5,
"expiration_date": "*",
"buildsystem": "taskcluster"
},
{
"buildtype": "asan",
"testtype": "build-win64-asan-fuzzing/opt",
"platform": "windows2012-64",
"priority": 5,
"expiration_date": "*",
"buildsystem": "taskcluster"
},
{
"buildtype": "debug",
"testtype": "build-win64-fuzzing/debug",
"platform": "windows2012-64",
"priority": 5,
"expiration_date": "*",
"buildsystem": "taskcluster"
},
{
"buildtype": "opt",
"testtype": "spidermonkey-sm-fuzzing-linux64/opt",
"platform": "linux64",
"priority": 5,
"expiration_date": "*",
"buildsystem": "taskcluster"
},
{
"buildtype": "debug",
"testtype": "build-android-x86-fuzzing/debug",
"platform": "android-4-2-x86",
"priority": 5,
"expiration_date": "*",
"buildsystem": "taskcluster"
}
]

Просмотреть файл

@ -1,172 +0,0 @@
import json
import logging
import os
from treeherder.etl.seta import get_reference_data_names
from treeherder.seta.common import convert_job_type_name_to_testtype
from treeherder.seta.models import JobPriority
from treeherder.seta.settings import SETA_LOW_VALUE_PRIORITY, THE_FUTURE
logger = logging.getLogger(__name__)
'''preseed.json entries have fields: buildtype, testtype, platform,
priority, expiration_date. They must match the corresponding entry in
runnable-jobs.json.
buildtype should match the attribute name in the runnable jobs collection.
testtype should match the full task label.
platform should match the platform.
priority can be 1 to signify high value tasks or 5 to signify low
value tasks. The default priority is 1.
expiration_date must be "*" to signify no expiration.
buildsystem should always be "taskcluster".
Example:
runnable-jobs.json:
"build-android-x86_64-asan-fuzzing/opt": {
"collection": {
"asan": true
},
"platform": "android-5-0-x86_64",
"symbol": "Bof"
},
entry in preseed.json
{
"buildtype": "asan",
"testtype": "build-android-x86_64-asan-fuzzing/opt",
"platform": "android-5-0-x86_64",
"priority": 5,
"expiration_date": "*",
"buildsytem": "taskcluster"
}
'''
def validate_preseed_entry(entry, ref_names):
assert entry["testtype"] != "*"
assert entry["buildtype"] != "*"
assert entry["platform"] != "*"
# We also support *, however, that was only useful with Buildbot
assert entry["buildsystem"] == "taskcluster"
# We support values different than *, however, it is not useful for preseed
assert entry["expiration_date"] == "*"
assert 1 <= entry["priority"] <= SETA_LOW_VALUE_PRIORITY
# Collect potential matches
potential_matches = []
for unique_identifier, ref_name in ref_names.items():
# XXX: Now that we have fuzzy build the term testtypes is not accurate
if ref_name == entry["testtype"]:
potential_matches.append(unique_identifier)
assert len(potential_matches) > 0, Exception(
"%s is not valid. Please check runnable_jobs.json from a Gecko decision task.",
entry["testtype"],
)
testtype = convert_job_type_name_to_testtype(entry["testtype"])
if not testtype:
logger.warning(
"Preseed.json entry testtype %s is not a valid task name:", entry["testtype"]
)
raise Exception("preseed.json entry contains invalid testtype. Please check output above.")
unique_identifier = (
testtype,
entry["buildtype"],
entry["platform"],
)
try:
ref_names[unique_identifier]
except KeyError:
logger.warning("Preseed.json entry %s matches the following:", unique_identifier)
logger.warning(potential_matches)
raise Exception("We failed to match your preseed.json entry. Please check output above.")
def load_preseed(validate=False):
""" Update JobPriority information from preseed.json"""
logger.info("About to load preseed.json")
preseed = preseed_data()
if validate:
logger.info("We are going to validate the values from preseed.json")
ref_names = get_reference_data_names()
for job in preseed:
if validate:
validate_preseed_entry(job, ref_names)
logger.debug("Processing %s", (job["testtype"], job["buildtype"], job["platform"]))
queryset = JobPriority.objects.all()
for field in ('testtype', 'buildtype', 'platform'):
if job[field] != '*':
# The JobPriority table does not contain the raw
# testtype value seen in the preseed.json file. We
# must convert the job[field] value to the appropriate
# value before performing the query.
field_value = (
convert_job_type_name_to_testtype(job[field])
if field == 'testtype'
else job[field]
)
queryset = queryset.filter(**{field: field_value})
# Deal with the case where we have a new entry in preseed
if not queryset:
create_new_entry(job)
else:
# We can have wildcards, so loop on all returned values in data
for jp in queryset:
process_job_priority(jp, job)
logger.debug("Finished")
def preseed_data():
with open(os.path.join(os.path.dirname(__file__), 'preseed.json'), 'r') as fd:
preseed = json.load(fd)
return preseed
def create_new_entry(job):
if job['expiration_date'] == '*':
job['expiration_date'] = THE_FUTURE
logger.info("Adding a new job priority to the database: %s", job)
testtype = convert_job_type_name_to_testtype(job['testtype'])
JobPriority.objects.create(
testtype=testtype,
buildtype=job['buildtype'],
platform=job['platform'],
priority=job['priority'],
expiration_date=job['expiration_date'],
buildsystem=job['buildsystem'],
)
def process_job_priority(jp, job):
update_fields = []
# Updating the buildtype can be dangerous as analyze_failures can set it to '*' while in here
# we can change it back to 'taskcluster'. For now, we will assume that creating a new entry
# will add the right value at the beginning
if jp.__getattribute__('priority') != job['priority']:
jp.__setattr__('priority', job['priority'])
update_fields.append('priority')
if job['expiration_date'] == '*' and jp.expiration_date != THE_FUTURE:
jp.expiration_date = THE_FUTURE
update_fields.append('expiration_date')
if update_fields:
logger.info("Updating (%s) for these fields %s", jp, ','.join(update_fields))
jp.save(update_fields=update_fields)

Просмотреть файл

@ -1,87 +0,0 @@
import datetime
THE_FUTURE = datetime.datetime(2100, 12, 31)
# repos that SETA supports
SETA_PROJECTS = [
'autoland',
'try',
]
# for taskcluster, only jobs that start with any of these names
# will be supported i.e. may be optimized out by SETA
SETA_SUPPORTED_TC_JOBTYPES = [
'test-',
'source-test-',
'desktop-test',
'android-test',
'iris-',
'webrender-',
'build-android-x86-fuzzing',
'build-android-x86_64-asan-fuzzing',
'build-linux64-asan-fuzzing-ccov',
'build-linux64-asan-fuzzing',
'build-linux64-fuzzing-ccov',
'build-linux64-fuzzing',
'build-linux64-tsan-fuzzing',
'build-macosx64-asan-fuzzing',
'build-macosx64-fuzzing',
'build-win64-asan-fuzzing',
'build-win64-fuzzing',
'spidermonkey-sm-fuzzing-linux64',
]
# platforms listed here will not be supported by SETA
# i.e. these will never be optimized out by SETA
SETA_UNSUPPORTED_PLATFORMS = [
'android-4-2-armv7-api15',
'android-4-4-armv7-api15',
'android-5-0-armv8-api15',
'android-5-1-armv7-api15',
'android-6-0-armv8-api15',
'osx-10-7', # Build
'osx-10-9',
'osx-10-11',
'other',
'taskcluster-images',
'windows7-64', # We don't test 64-bit builds on Windows 7 test infra
'windows8-32', # We don't test 32-bit builds on Windows 8 test infra
'Win 6.3.9600 x86_64',
'linux64-stylo',
'windowsxp',
]
# testtypes listed here will not be supported by SETA
# i.e. these will never be optimized out by SETA
SETA_UNSUPPORTED_TESTTYPES = [
'dep',
'nightly',
'non-unified',
'valgrind',
'Opt',
'Debug',
'Dbg',
'(opt)',
'PGO Opt',
'Valgrind Opt',
'Artifact Opt',
'(debug)',
]
# SETA job priority values
SETA_HIGH_VALUE_PRIORITY = 1
SETA_LOW_VALUE_PRIORITY = 5
# analyze_failures retrieves jobs marked 'fixed by commit' for these repos
SETA_FIXED_BY_COMMIT_REPOS = [
'autoland',
'mozilla-central',
]
# analyze_failures retrieves jobs marked 'fixed by commit' for the past N days
SETA_FIXED_BY_COMMIT_DAYS = 90
# when retrieving taskcluster runnable jobs, and processing
# them, cache the resulting reference data names map for N seconds; this
# helps reduce the number of API calls when getting job priorities
SETA_REF_DATA_NAMES_CACHE_TIMEOUT = 3600

Просмотреть файл

@ -1,8 +0,0 @@
from treeherder.seta.analyze_failures import AnalyzeFailures
from treeherder.workers.task import retryable_task
@retryable_task(name='seta-analyze-failures', max_retries=3, soft_time_limit=5 * 60)
def seta_analyze_failures():
'''We analyze all starred test failures from the last four months which were fixed by a commit.'''
AnalyzeFailures().run()

Просмотреть файл

@ -1,197 +0,0 @@
'''This module is used to add new jobs to the job priority table.
This will query the Treeherder runnable api based on the latest task ID from
autoland's TaskCluster index.
Known bug:
* Only considering autoland makes SETA act similarly in all repositories where it is
active. Right now, this works for integration repositories since they tend to have
the same set of jobs. Unfortunately, this is less than ideal if we want to make this
work for project repositories.
'''
import logging
from treeherder.etl.runnable_jobs import list_runnable_jobs
from treeherder.etl.seta import parse_testtype, valid_platform
from treeherder.seta.common import job_priority_index, unique_key
from treeherder.seta.models import JobPriority
from treeherder.seta.settings import SETA_LOW_VALUE_PRIORITY
logger = logging.getLogger(__name__)
def update_job_priority_table():
"""Use it to update the job priority table with data from the runnable api."""
# XXX: We are assuming that the jobs accross 'mozilla-inbound' and 'autoland'
# are equivalent. This could cause issues in the future
data = query_sanitized_data(repo_name='autoland')
if data:
return _update_table(data)
else:
# XXX: Should we do this differently?
logger.warning('We received an empty data set')
return
def _unique_key(job):
"""Return a key to query our uniqueness mapping system.
This makes sure that we use a consistent key between our code and selecting jobs from the
table.
"""
testtype = str(job['testtype'])
if not testtype:
raise Exception('Bad job {}'.format(job))
return unique_key(
testtype=testtype, buildtype=str(job['platform_option']), platform=str(job['platform'])
)
def _sanitize_data(runnable_jobs_data):
"""We receive data from runnable jobs api and return the sanitized data that meets our needs.
This is a loop to remove duplicates (including buildsystem -> * transformations if needed)
By doing this, it allows us to have a single database query
It returns sanitized_list which will contain a subset which excludes:
* jobs that don't specify the platform
* jobs that don't specify the testtype
* if the job appears again, we replace build_system_type with '*'. By doing so, if a job appears
under both 'buildbot' and 'taskcluster', its build_system_type will be '*'
"""
job_build_system_type = {}
sanitized_list = []
for job in runnable_jobs_data:
if not valid_platform(job['platform']):
logger.debug('Invalid platform %s', job['platform'])
continue
testtype = parse_testtype(
build_system_type=job['build_system_type'],
job_type_name=job['job_type_name'],
platform_option=job['platform_option'],
ref_data_name=job['ref_data_name'],
)
if not testtype:
continue
# NOTE: This is *all* the data we need from the runnable API
new_job = {
'build_system_type': job['build_system_type'], # e.g. {buildbot,taskcluster,*}
'platform': job['platform'], # e.g. windows8-64
'platform_option': job['platform_option'], # e.g. {opt,debug}
'testtype': testtype, # e.g. web-platform-tests-1
}
key = _unique_key(new_job)
# Let's build a map of all the jobs and if duplicated change the build_system_type to *
if key not in job_build_system_type:
job_build_system_type[key] = job['build_system_type']
sanitized_list.append(new_job)
elif new_job['build_system_type'] != job_build_system_type[key]:
new_job['build_system_type'] = job_build_system_type[key]
# This will *replace* the previous build system type with '*'
# This guarantees that we don't have duplicates
sanitized_list[sanitized_list.index(new_job)]['build_system_type'] = '*'
return sanitized_list
def query_sanitized_data(repo_name='autoland'):
"""Return sanitized jobs data based on runnable api. None if failed to obtain or no new data.
We need to find the latest gecko decision task ID (by querying the index [1][2]).
[1] firefox-ci-tc.services.mozilla.com/api/index/v1/task/gecko.v2.%s.latest.taskgraph.decision/
[2] Index's data structure:
{
"namespace": "gecko.v2.autoland.latest.taskgraph.decision",
"taskId": "Dh9ZvFk5QCSprJ877cgUmw",
"rank": 0,
"data": {},
"expires": "2017-10-06T18:30:18.428Z"
}
"""
runnable_jobs = list_runnable_jobs(repo_name)
return _sanitize_data(runnable_jobs)
def _initialize_values():
logger.info('Fetch all rows from the job priority table.')
# Get all rows of job priorities
jp_index = job_priority_index(JobPriority.objects.all())
return jp_index, SETA_LOW_VALUE_PRIORITY, None
def _update_table(data):
"""Add new jobs to the priority table and update the build system if required.
data - it is a list of dictionaries that describe a job type
returns the number of new, failed and updated jobs
"""
jp_index, priority, expiration_date = _initialize_values()
total_jobs = len(data)
new_jobs, failed_changes, updated_jobs = 0, 0, 0
# Loop through sanitized jobs, add new jobs and update the build system if needed
for job in data:
key = _unique_key(job)
if key in jp_index:
# We already know about this job, we might need to update the build system
# We're seeing the job again with another build system (e.g. buildbot vs
# taskcluster). We need to change it to '*'
if (
jp_index[key]['build_system_type'] != '*'
and jp_index[key]['build_system_type'] != job["build_system_type"]
):
db_job = JobPriority.objects.get(pk=jp_index[key]['pk'])
db_job.buildsystem = '*'
db_job.save()
logger.info(
'Updated %s/%s from %s to %s',
db_job.testtype,
db_job.buildtype,
job['build_system_type'],
db_job.buildsystem,
)
updated_jobs += 1
else:
# We have a new job from runnablejobs to add to our master list
try:
jobpriority = JobPriority(
testtype=str(job["testtype"]),
buildtype=str(job["platform_option"]),
platform=str(job["platform"]),
priority=priority,
expiration_date=expiration_date,
buildsystem=job["build_system_type"],
)
jobpriority.save()
logger.debug(
'New job was found (%s,%s,%s,%s)',
job['testtype'],
job['platform_option'],
job['platform'],
job["build_system_type"],
)
new_jobs += 1
except Exception as error:
logger.warning(str(error))
failed_changes += 1
logger.info(
'We have %s new jobs and %s updated jobs out of %s total jobs processed.',
new_jobs,
updated_jobs,
total_jobs,
)
if failed_changes != 0:
logger.warning(
'We have failed %s changes out of %s total jobs processed.', failed_changes, total_jobs
)
return new_jobs, failed_changes, updated_jobs

Просмотреть файл

@ -1,25 +0,0 @@
from rest_framework import status, viewsets
from rest_framework.response import Response
from treeherder.seta.job_priorities import SetaError, seta_job_scheduling
class SetaJobPriorityViewSet(viewsets.ViewSet):
def list(self, request, project):
"""Routing to /api/project/{project}/seta/job-priorities/
This API can potentially have these consumers:
* Buildbot
* build_system_type=buildbot
* priority=5
* format=json
* TaskCluster (Gecko decision task)
* build_system_type=taskcluster
* format=json
"""
build_system_type = request.query_params.get('build_system_type', '*')
priority = request.query_params.get('priority')
try:
return Response(seta_job_scheduling(project, build_system_type, priority))
except SetaError as e:
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)

Просмотреть файл

@ -19,7 +19,6 @@ from treeherder.webapp.api import (
performance_data,
push,
refdata,
seta,
)
# router for views that are bound to a project
@ -36,10 +35,6 @@ project_bound_router.register(
basename='jobs',
)
project_bound_router.register(
r'seta/job-priorities', seta.SetaJobPriorityViewSet, basename='seta-job-priorities'
)
project_bound_router.register(
r'push',
push.PushViewSet,