blob: 2c51264f6a603a80a7b4233bfb877013bb9037e2 [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.
"""Caches processed query results in memcache and datastore.
Memcache is not very reliable for the perf dashboard. Prometheus team explained
that memcache is LRU and shared between multiple applications, so their activity
may result in our data being evicted. To prevent this, we cache processed
query results in the data store. Using NDB, the values are also cached in
memcache if possible. This improves performance because doing a get()
for a key which has a single BlobProperty is much quicker than a complex query
over a large dataset.
(Background: http://g/prometheus-discuss/othVtufGIyM/wjAS5djyG8kJ)
When an item is cached, layered_cache does the following:
1) Namespaces the key based on whether datastore_hooks says the request is
internal_only.
2) Pickles the value (memcache does this internally), and adds a data store
entity with the key and a BlobProperty with the pickled value.
Retrieving values checks memcache via NDB first, and if datastore is used it
unpickles.
When an item is removed from the the cache, it is removed from both internal and
external caches, since removals are usually caused by large changes that affect
both caches.
Although this module contains ndb.Model classes, these are not intended
to be used directly by other modules.
"""
import cPickle
import datetime
import logging
from google.appengine.api import datastore_errors
from google.appengine.ext import ndb
from dashboard import datastore_hooks
from dashboard import request_handler
class DeleteExpiredEntitiesHandler(request_handler.RequestHandler):
"""URL endpoint for a cron job to delete expired entities from datastore."""
def get(self):
"""This get handler is called from cron.
It deletes only expired CachedPickledString entities from the datastore.
"""
DeleteAllExpiredEntities()
class CachedPickledString(ndb.Model):
value = ndb.BlobProperty()
expire_time = ndb.DateTimeProperty()
@classmethod
def GetExpiredKeys(cls):
"""Gets keys of expired entities.
Returns:
List of keys for items which are expired.
"""
current_time = datetime.datetime.now()
query = cls.query(cls.expire_time < current_time)
query = query.filter(cls.expire_time != None)
return query.fetch(keys_only=True)
def _NamespaceKey(key, namespace=None):
if not namespace:
namespace = datastore_hooks.GetNamespace()
return '%s__%s' % (namespace, key)
def Prewarm(keys):
"""Prewarms the NDB in-context cache by doing async_get for the keys.
For requests like /add_point which can get/set dozens of keys, contention
occasionally causes the gets to take several seconds. But they will be
cached in context by NDB if they are requested at the start of the request.
Args:
keys: List of string keys.
"""
to_get = []
for key in keys:
to_get.append(ndb.Key('CachedPickledString',
_NamespaceKey(key, datastore_hooks.EXTERNAL)))
to_get.append(ndb.Key('CachedPickledString',
_NamespaceKey(key, datastore_hooks.INTERNAL)))
ndb.get_multi_async(to_get)
def Get(key):
"""Gets the value from the datastore."""
namespaced_key = _NamespaceKey(key)
entity = ndb.Key('CachedPickledString', namespaced_key).get(
read_policy=ndb.EVENTUAL_CONSISTENCY)
if entity:
return cPickle.loads(entity.value)
return None
def GetExternal(key):
"""Gets the value from the datastore for the externally namespaced key."""
namespaced_key = _NamespaceKey(key, datastore_hooks.EXTERNAL)
entity = ndb.Key('CachedPickledString', namespaced_key).get(
read_policy=ndb.EVENTUAL_CONSISTENCY)
if entity:
return cPickle.loads(entity.value)
return None
def Set(key, value, days_to_keep=None, namespace=None):
"""Sets the value in the datastore.
Args:
key: The key name, which will be namespaced.
value: The value to set.
days_to_keep: Number of days to keep entity in datastore, default is None.
Entity will not expire when this value is 0 or None.
namespace: Optional namespace, otherwise namespace will be retrieved
using datastore_hooks.GetNamespace().
"""
# When number of days to keep is given, calculate expiration time for
# the entity and store it in datastore.
# Once the entity expires, it will be deleted from the datastore.
expire_time = None
if days_to_keep:
expire_time = datetime.datetime.now() + datetime.timedelta(
days=days_to_keep)
namespaced_key = _NamespaceKey(key, namespace)
try:
CachedPickledString(id=namespaced_key,
value=cPickle.dumps(value),
expire_time=expire_time).put()
except datastore_errors.BadRequestError as e:
logging.warning('BadRequestError for key %s: %s', key, e)
def SetExternal(key, value, days_to_keep=None):
"""Sets the value in the datastore for the externally namespaced key.
Needed for things like /add_point that update internal/external data at the
same time.
Args:
key: The key name, which will be namespaced as externally_visible.
value: The value to set.
days_to_keep: Number of days to keep entity in datastore, default is None.
Entity will not expire when this value is 0 or None.
"""
Set(key, value, days_to_keep, datastore_hooks.EXTERNAL)
def Delete(key):
"""Clears the value from the datastore."""
internal_key = _NamespaceKey(key, namespace=datastore_hooks.INTERNAL)
external_key = _NamespaceKey(key, namespace=datastore_hooks.EXTERNAL)
ndb.delete_multi([ndb.Key('CachedPickledString', internal_key),
ndb.Key('CachedPickledString', external_key)])
def DeleteAllExpiredEntities():
"""Deletes all expired entities from the datastore."""
ndb.delete_multi(CachedPickledString.GetExpiredKeys())