blob: 308a2dfe323ef23be4a6f78470a59367ee2f5d24 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""A memcache viewer and editor UI.
Memcache associates a key with a value and an integer flag. The Go API maps
keys to strings and lets the user control the flag. Java, PHP and Python
map keys to an arbitrary type and uses the flag to indicate the type
information. Java, PHP and Python map types in inconsistent ways, see:
- google/appengine/api/memcache/__init__.py
- google/appengine/api/memcache/MemcacheSerialization.java
- google/appengine/runtime/MemcacheUtils.php
"""
import datetime
import logging
import urllib
from google.appengine.api import apiproxy_stub_map
from google.appengine.api import memcache
from google.appengine.api.memcache import memcache_service_pb
from google.appengine.tools.devappserver2.admin import admin_request_handler
class StringValueConverter(object):
memcache_type = memcache.TYPE_STR
placeholder = 'hello world!'
can_edit = True
friendly_type_name = 'String'
@staticmethod
def to_display(cache_value):
"""Convert a memcache string into a displayable representation.
Make a memcache string into a text string that can be displayed or edited.
While called a string, it is technically just an array of bytes. Because
we do not know what encoding the bytes are (and possibly they are not an
encoded text string - for example they could be an MD5 hash) we display
in string-escaped form.
Args:
cache_value: an array of bytes
Returns:
A unicode string that represents the sequence of bytes and can be
roundtripped back to the sequence of bytes.
"""
# As we don't know what encoding the bytes are, we string escape so any
# byte sequence is legal ASCII. Once we have a legal ASCII byte sequence
# we can safely convert to a unicode/text string.
return cache_value.encode('string-escape').decode('ascii')
@staticmethod
def to_cache(display_value):
"""Convert a displayable representation to a memcache string.
Take a displayable/editable text string and convert into a memcache string.
As a memcache string is technically an array of bytes, we only allow
characters from the ASCII range and require all other bytes to be indicated
via string escape. (because if we see the Unicode character Yen sign
(U+00A5) we don't know if they want the byte 0xA5 or the UTF-8 two byte
sequence 0xC2 0xA5).
Args:
display_value: a text (i.e. unicode string) using only ASCII characters;
non-ASCII characters must be represented string escapes.
Returns:
An array of bytes.
Raises:
UnicodeEncodeError: a non-ASCII character is part of the input.
"""
# Since we don't know how they want their Unicode encoded, this will raise
# an exception (which will be displayed nicely) if they include non-ASCII.
return display_value.encode('ascii').decode('string-escape')
class UnicodeValueConverter(object):
memcache_type = memcache.TYPE_UNICODE
# Hello world in Japanese.
placeholder = u'\u3053\u3093\u306b\u3061\u306f\u4e16\u754c'
can_edit = True
friendly_type_name = 'Unicode String'
@staticmethod
def to_display(cache_value):
return cache_value.decode('utf-8')
@staticmethod
def to_cache(display_value):
return display_value.encode('utf-8')
class BooleanValueConverter(object):
memcache_type = memcache.TYPE_BOOL
placeholder = 'true'
can_edit = True
friendly_type_name = 'Boolean'
@staticmethod
def to_display(cache_value):
if cache_value == '0':
return 'false'
elif cache_value == '1':
return 'true'
else:
raise ValueError('unexpected boolean %r' % cache_value)
@staticmethod
def to_cache(display_value):
if display_value.lower() in ('false', 'no', 'off', '0'):
return '0'
elif display_value.lower() in ('true', 'yes', 'on', '1'):
return '1'
raise ValueError(
'invalid literal for boolean: %s (must be "true" or "false")' %
display_value)
class IntValueConverter(object):
memcache_type = memcache.TYPE_INT
placeholder = '42'
can_edit = True
friendly_type_name = 'Integer'
@staticmethod
def to_display(cache_value):
return str(cache_value)
@staticmethod
def to_cache(display_value):
return str(int(display_value))
class OtherValueConverter(object):
memcache_type = None
placeholder = None
can_edit = False
friendly_type_name = 'Unknown Type'
@staticmethod
def to_display(cache_value):
return repr(cache_value)[1:-1]
@staticmethod
def to_cache(display_value):
raise NotImplementedError('cannot to a memcache value of unknown type')
class MemcacheViewerRequestHandler(admin_request_handler.AdminRequestHandler):
CONVERTERS = [StringValueConverter, UnicodeValueConverter,
BooleanValueConverter, IntValueConverter,
OtherValueConverter]
MEMCACHE_TYPE_TO_CONVERTER = {c.memcache_type: c for c in CONVERTERS
if c.memcache_type is not None}
FRIENDLY_TYPE_NAME_TO_CONVERTER = {c.friendly_type_name: c
for c in CONVERTERS}
def _get_memcache_value_and_flags(self, key):
"""Return a 2-tuple containing a memcache value and its flags."""
request = memcache_service_pb.MemcacheGetRequest()
response = memcache_service_pb.MemcacheGetResponse()
request.add_key(key)
apiproxy_stub_map.MakeSyncCall('memcache', 'Get', request, response)
assert response.item_size() < 2
if response.item_size() == 0:
return None, None
else:
return response.item(0).value(), response.item(0).flags()
def _set_memcache_value(self, key, value, flags):
"""Store a value in memcache."""
request = memcache_service_pb.MemcacheSetRequest()
response = memcache_service_pb.MemcacheSetResponse()
item = request.add_item()
item.set_key(key)
item.set_value(value)
item.set_flags(flags)
apiproxy_stub_map.MakeSyncCall('memcache', 'Set', request, response)
return (response.set_status(0) ==
memcache_service_pb.MemcacheSetResponse.STORED)
def get(self):
"""Show template and prepare stats and/or key+value to display/edit."""
values = {'request': self.request,
'message': self.request.get('message')}
edit = self.request.get('edit')
key = self.request.get('key')
if edit:
# Show the form to edit/create the value.
key = edit
values['show_stats'] = False
values['show_value'] = False
values['show_valueform'] = True
values['types'] = [type_value.friendly_type_name
for type_value in self.CONVERTERS
if type_value.can_edit]
elif key:
# A key was given, show it's value on the stats page.
values['show_stats'] = True
values['show_value'] = True
values['show_valueform'] = False
else:
# Plain stats display + key lookup form.
values['show_stats'] = True
values['show_valueform'] = False
values['show_value'] = False
if key:
values['key'] = key
memcache_value, memcache_flags = self._get_memcache_value_and_flags(key)
if memcache_value is not None:
converter = self.MEMCACHE_TYPE_TO_CONVERTER.get(memcache_flags,
OtherValueConverter)
try:
values['value'] = converter.to_display(memcache_value)
except ValueError:
# This exception is possible in the case where the value was set by
# Go, which allows for arbitrary user-assigned flag values.
logging.exception('Could not convert %s value %s',
converter.friendly_type_name, memcache_value)
converter = OtherValueConverter
values['value'] = converter.to_display(memcache_value)
values['type'] = converter.friendly_type_name
values['writable'] = converter.can_edit
values['key_exists'] = True
values['value_placeholder'] = converter.placeholder
else:
values['writable'] = True
values['key_exists'] = False
if values['show_stats']:
memcache_stats = memcache.get_stats()
if not memcache_stats:
# No stats means no memcache usage.
memcache_stats = {'hits': 0, 'misses': 0, 'byte_hits': 0, 'items': 0,
'bytes': 0, 'oldest_item_age': 0}
values['stats'] = memcache_stats
try:
hitratio = memcache_stats['hits'] * 100 / (memcache_stats['hits']
+ memcache_stats['misses'])
except ZeroDivisionError:
hitratio = 0
values['hitratio'] = hitratio
# TODO: oldest_item_age should be formatted in a more useful
# way.
delta_t = datetime.timedelta(seconds=memcache_stats['oldest_item_age'])
values['oldest_item_age'] = datetime.datetime.now() - delta_t
self.response.write(self.render('memcache_viewer.html', values))
def _urlencode(self, query):
"""Encode a dictionary into a URL query string.
In contrast to urllib this encodes unicode characters as UTF8.
Args:
query: Dictionary of key/value pairs.
Returns:
String.
"""
return '&'.join('%s=%s' % (urllib.quote_plus(k.encode('utf8')),
urllib.quote_plus(v.encode('utf8')))
for k, v in query.iteritems())
def post(self):
"""Handle modifying actions and/or redirect to GET page."""
next_param = {}
if self.request.get('action:flush'):
if memcache.flush_all():
next_param['message'] = 'Cache flushed, all keys dropped.'
else:
next_param['message'] = 'Flushing the cache failed. Please try again.'
elif self.request.get('action:display'):
next_param['key'] = self.request.get('key')
elif self.request.get('action:edit'):
next_param['edit'] = self.request.get('key')
elif self.request.get('action:delete'):
key = self.request.get('key')
result = memcache.delete(key)
if result == memcache.DELETE_NETWORK_FAILURE:
next_param['message'] = ('ERROR: Network failure, key "%s" not deleted.'
% key)
elif result == memcache.DELETE_ITEM_MISSING:
next_param['message'] = 'Key "%s" not in cache.' % key
elif result == memcache.DELETE_SUCCESSFUL:
next_param['message'] = 'Key "%s" deleted.' % key
else:
next_param['message'] = ('Unknown return value. Key "%s" might still '
'exist.' % key)
elif self.request.get('action:save'):
key = self.request.get('key')
value = self.request.get('value')
type_ = self.request.get('type')
next_param['key'] = key
converter = self.FRIENDLY_TYPE_NAME_TO_CONVERTER[type_]
try:
memcache_value = converter.to_cache(value)
except ValueError as e:
next_param['message'] = 'ERROR: Failed to save key "%s": %s.' % (key, e)
else:
if self._set_memcache_value(key,
memcache_value,
converter.memcache_type):
next_param['message'] = 'Key "%s" saved.' % key
else:
next_param['message'] = 'ERROR: Failed to save key "%s".' % key
elif self.request.get('action:cancel'):
next_param['key'] = self.request.get('key')
else:
next_param['message'] = 'Unknown action.'
next = self.request.path_url
if next_param:
next = '%s?%s' % (next, self._urlencode(next_param))
self.redirect(next)