blob: 8cf9d2f7346118c2a7ef5ef106d15bc5557c4a70 [file] [log] [blame]
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""URL endpoint to add new graph data to the datastore."""
import json
import logging
from google.appengine.api import datastore_errors
from google.appengine.ext import ndb
from dashboard import add_point
from dashboard import datastore_hooks
from dashboard import find_anomalies
from dashboard import graph_revisions
from dashboard import request_handler
from dashboard import stored_object
from dashboard import units_to_direction
from dashboard import utils
from dashboard.models import anomaly
from dashboard.models import graph_data
BOT_WHITELIST_KEY = 'bot_whitelist'
class AddPointQueueHandler(request_handler.RequestHandler):
"""Request handler to process points and add them to the datastore.
This request handler is intended to be used only by requests using the
task queue; it shouldn't be directly from outside.
"""
def get(self):
"""A get request is the same a post request for this endpoint."""
self.post()
def post(self):
"""Adds a set of points from the post data.
Request parameters:
data: JSON encoding of a list of dictionaries. Each dictionary represents
one point to add. For each dict, one Row entity will be added, and
any required Test or Master or Bot entities will be created.
"""
datastore_hooks.SetPrivilegedRequest()
data = json.loads(self.request.get('data'))
_PrewarmGets(data)
bot_whitelist = stored_object.Get(BOT_WHITELIST_KEY)
all_put_futures = []
added_rows = []
monitored_test_keys = []
for row_dict in data:
try:
new_row, parent_test, put_futures = _AddRow(row_dict, bot_whitelist)
added_rows.append(new_row)
is_monitored = parent_test.sheriff and parent_test.has_rows
if is_monitored:
monitored_test_keys.append(parent_test.key)
all_put_futures.extend(put_futures)
except add_point.BadRequestError as e:
logging.error('Could not add %s, it was invalid.', e.message)
except datastore_errors.BadRequestError as e:
logging.error('Datastore request failed: %s.', e.message)
return
ndb.Future.wait_all(all_put_futures)
# Updating of the cached graph revisions should happen after put because
# it requires the new row to have a timestamp, which happens upon put.
graph_revisions.AddRowsToCache(added_rows)
for test_key in monitored_test_keys:
if not _IsRefBuild(test_key):
find_anomalies.ProcessTest(test_key)
else:
logging.warn('Ref data marked as monitored: %s', str(test_key))
def _PrewarmGets(data):
"""Prepares the cache so that fetching is faster later.
The add_point request handler does a LOT of gets, and it's possible for
each to take seconds.
However, NDB will does automatic in-context caching:
https://developers.google.com/appengine/docs/python/ndb/cache#incontext
This means that doing an async get() at the start will cache the result, so
that we can prewarm the cache for everything we'll need throughout the
request at the start.
Args:
data: The request json.
"""
# Prewarm lookups of masters, bots, and tests.
master_keys = {ndb.Key('Master', r['master']) for r in data}
bot_keys = {ndb.Key('Master', r['master'], 'Bot', r['bot']) for r in data}
test_keys = set()
for row in data:
start = ['Master', row['master'], 'Bot', row['bot']]
test_parts = row['test'].split('/')
for part in test_parts:
if not part:
break
start += ['Test', part]
test_keys.add(ndb.Key(*start))
ndb.get_multi_async(list(master_keys) + list(bot_keys) + list(test_keys))
def _AddRow(row_dict, bot_whitelist):
"""Adds a Row entity to the datastore.
There are three main things that are needed in order to make a new entity;
the ID, the parent key, and all of the properties. Making these three
things, and validating the related input fields, are delegated to
sub-functions.
Args:
row_dict: A dictionary obtained from the JSON that was received.
bot_whitelist: A list of whitelisted bots names.
Returns:
A triple: The new row, the parent test, and a list of entity put futures.
Raises:
add_point.BadRequestError: The input dict was invalid.
RuntimeError: The required parent entities couldn't be created.
"""
parent_test = _GetParentTest(row_dict, bot_whitelist)
test_container_key = utils.GetTestContainerKey(parent_test.key)
columns = add_point.GetAndValidateRowProperties(row_dict)
columns['internal_only'] = parent_test.internal_only
row_id = add_point.GetAndValidateRowId(row_dict)
# Update the last-added revision record for this test.
master, bot, test = row_dict['master'], row_dict['bot'], row_dict['test']
test_path = '%s/%s/%s' % (master, bot, test)
last_added_revision_entity = graph_data.LastAddedRevision(
id=test_path, revision=row_id)
entity_put_futures = []
entity_put_futures.append(last_added_revision_entity.put_async())
# If the row ID isn't the revision, that means that the data is Chrome OS
# data, and we want the default revision to be Chrome version.
if row_id != row_dict.get('revision'):
columns['a_default_rev'] = 'r_chrome_version'
# Create the entity and add it asynchronously.
new_row = graph_data.Row(id=row_id, parent=test_container_key, **columns)
entity_put_futures.append(new_row.put_async())
return new_row, parent_test, entity_put_futures
def _GetParentTest(row_dict, bot_whitelist):
"""Gets the parent test for a Row based on an input dictionary.
Args:
row_dict: A dictionary from the data parameter.
bot_whitelist: A list of whitelisted bot names.
Returns:
A Test entity.
Raises:
RuntimeError: Something went wrong when trying to get the parent Test.
"""
master_name = row_dict.get('master')
bot_name = row_dict.get('bot')
test_name = row_dict.get('test').strip('/')
units = row_dict.get('units')
higher_is_better = row_dict.get('higher_is_better')
improvement_direction = _ImprovementDirection(higher_is_better)
internal_only = _BotInternalOnly(bot_name, bot_whitelist)
benchmark_description = row_dict.get('benchmark_description')
parent_test = _GetOrCreateAncestors(
master_name, bot_name, test_name, units=units,
improvement_direction=improvement_direction,
internal_only=internal_only,
benchmark_description=benchmark_description)
return parent_test
def _ImprovementDirection(higher_is_better):
"""Returns an improvement direction (constant from alerts_data) or None."""
if higher_is_better is None:
return None
return anomaly.UP if higher_is_better else anomaly.DOWN
def _BotInternalOnly(bot_name, bot_whitelist):
"""Checks whether a given bot name is internal-only.
If a bot name is internal only, then new data for that bot should be marked
as internal-only.
"""
if not bot_whitelist:
logging.warning(
'No bot whitelist available. All data will be internal-only. If this '
'is not intended, please add a bot whitelist using /edit_site_config.')
return True
return bot_name not in bot_whitelist
def _GetOrCreateAncestors(
master_name, bot_name, test_name, units=None,
improvement_direction=None, internal_only=True, benchmark_description=''):
"""Gets or creates all necessary Master, Bot and Test entities for a Row."""
master_entity = _GetOrCreateMaster(master_name)
bot_entity = _GetOrCreateBot(
bot_name, master_entity.key, internal_only)
# Add all ancestor tests to the datastore in order.
ancestor_test_parts = test_name.split('/')
parent = bot_entity
suite = None
for index, ancestor_test_name in enumerate(ancestor_test_parts):
# Certain properties should only be updated if the Test is a leaf test.
is_leaf_test = (index == len(ancestor_test_parts) - 1)
test_properties = {
'units': units if is_leaf_test else None,
'improvement_direction': (improvement_direction
if is_leaf_test else None),
'internal_only': internal_only,
}
ancestor_test = _GetOrCreateTest(
ancestor_test_name, parent.key, test_properties)
if index == 0:
suite = ancestor_test
parent = ancestor_test
if benchmark_description and suite.description != benchmark_description:
suite.description = benchmark_description
return parent
def _GetOrCreateMaster(name):
"""Gets or creates a new Master."""
existing = graph_data.Master.get_by_id(name)
if existing:
return existing
new_entity = graph_data.Master(id=name)
new_entity.put()
return new_entity
def _GetOrCreateBot(name, parent_key, internal_only):
"""Gets or creates a new Bot under the given Master."""
existing = graph_data.Bot.get_by_id(name)
if existing:
return existing
new_entity = graph_data.Bot(
id=name, parent=parent_key, internal_only=internal_only)
new_entity.put()
return new_entity
def _GetOrCreateTest(name, parent_key, properties):
"""Either gets an entity if it already exists, or creates one.
If the entity already exists but the properties are different than the ones
specified, then the properties will be updated first. This implies that a
new point is being added for an existing Test, so if the Test has been
previously marked as deprecated or associated with a stoppage alert, then it
can be updated and marked as non-deprecated.
If the entity doesn't yet exist, a new one will be created with the given
properties.
Args:
name: The string ID of the Test to get or create.
parent_key: The key of the parent entity.
properties: A dictionary of properties that should be set.
Returns:
An entity (which has already been put).
Raises:
datastore_errors.BadRequestError: Something went wrong getting the entity.
"""
existing = graph_data.Test.get_by_id(name, parent_key)
if not existing:
# Add improvement direction if this is a new test.
if 'units' in properties:
units = properties['units']
direction = units_to_direction.GetImprovementDirection(units)
properties['improvement_direction'] = direction
new_entity = graph_data.Test(id=name, parent=parent_key, **properties)
new_entity.put()
return new_entity
# Flag indicating whether we want to re-put the entity before returning.
properties_changed = False
if existing.deprecated:
existing.deprecated = False
properties_changed = True
if existing.stoppage_alert:
alert = existing.stoppage_alert.get()
if alert:
alert.recovered = True
alert.put()
else:
logging.warning('Stoppage alert %s not found.', existing.stoppage_alert)
existing.stoppage_alert = None
properties_changed = True
# Special case to update improvement direction from units for Test entities
# when units are being updated. If an improvement direction is explicitly
# provided in the properties, then it will be updated again below.
units = properties.get('units')
if units:
direction = units_to_direction.GetImprovementDirection(units)
if direction != existing.improvement_direction:
existing.improvement_direction = direction
properties_changed = True
# Go through the list of general properties and update if necessary.
for prop, value in properties.items():
if (hasattr(existing, prop) and value is not None and
getattr(existing, prop) != value):
setattr(existing, prop, value)
properties_changed = True
if properties_changed:
existing.put()
return existing
def _IsRefBuild(test_key):
"""Checks whether a Test is for a reference build test run."""
key_path = test_key.flat()
return key_path[-1] == 'ref' or key_path[-1].endswith('_ref')