# Copyright 2008 William T Katz
#
# 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.
#
# THIS LICENSE INFORMATION/ATTRIBUTION must be left in place.
import string
import random
import logging
from google.appengine.api import memcache
from google.appengine.ext import db
from google.appengine.runtime import apiproxy_errors
class MemcachedCount(object):
DELTA_ZERO = 500000 # Allows negative numbers in unsigned memcache
def __init__(self, name):
self.key = 'MemcachedCount' + name
def get_count(self):
value = memcache.get(self.key)
if value is None:
return 0
else:
return string.atoi(value) - MemcachedCount.DELTA_ZERO
def set_count(self, value):
memcache.set(self.key, str(MemcachedCount.DELTA_ZERO + value))
def delete_count(self):
memcache.delete(self.key)
count = property(get_count, set_count, delete_count)
def increment(self, incr=1):
value = memcache.get(self.key)
if value is None:
self.count = incr
elif incr > 0:
memcache.incr(self.key, incr)
elif incr < 0:
memcache.decr(self.key, -incr)
class Counter(object):
"""A counter using sharded writes to prevent contentions.
Should be used for counters that handle a lot of concurrent use.
Follows pattern described in Google I/O talk:
http://sites.google.com/site/io/building-scalable-web-applications-with-google-app-engine
Memcache is used for caching counts and if a cached count is available, it is
the most correct. If there are datastore put issues, we store the un-put values
into a delayed_incr memcache that will be applied as soon as the next shard put
is successful. Changes will only be lost if we lose memcache before a successful
datastore shard put or there's a failure/error in memcache.
Usage:
hits = Counter('hits')
hits.increment()
my_hits = hits.count
hits.get_count(nocache=True) # Forces non-cached count of all shards
hits.count = 6 # Set the counter to arbitrary value
hits.increment(incr=-1) # Decrement
hits.increment(10)
"""
NUM_SHARDS = 20
def __init__(self, name):
self.name = name
self.memcached = MemcachedCount('Counter' + name)
self.delayed_incr = MemcachedCount('DelayedIncr' + name)
def delete(self):
q = db.Query(CounterShard).filter('name =', self.name)
shards = q.fetch(limit=Counter.NUM_SHARDS)
db.delete(shards)
def get_count_and_cache(self):
q = db.Query(CounterShard).filter('name =', self.name)
shards = q.fetch(limit=Counter.NUM_SHARDS)
datastore_count = 0
for shard in shards:
datastore_count += shard.count
count = datastore_count + self.delayed_incr.count
self.memcached.count = count
return count
def get_count(self, nocache=False):
total = self.memcached.count
if nocache or total is None:
return self.get_count_and_cache()
else:
return int(total)
def set_count(self, value):
cur_value = self.get_count()
self.memcached.count = value
delta = value - cur_value
if delta != 0:
CounterShard.increment(self, incr=delta)
count = property(get_count, set_count)
def increment(self, incr=1, refresh=False):
CounterShard.increment(self, incr)
self.memcached.increment(incr)
class CounterShard(db.Model):
name = db.StringProperty(required=True)
count = db.IntegerProperty(default=0)
@classmethod
def increment(cls, counter, incr=1):
index = random.randint(1, Counter.NUM_SHARDS)
counter_name = counter.name
delayed_incr = counter.delayed_incr.count
shard_key_name = 'Shard' + counter_name + str(index)
def get_or_create_shard():
shard = CounterShard.get_by_key_name(shard_key_name)
if shard is None:
shard = CounterShard(key_name=shard_key_name, name=counter_name)
shard.count += incr + delayed_incr
key = shard.put()
try:
db.run_in_transaction(get_or_create_shard)
except (db.Error, apiproxy_errors.Error), e:
counter.delayed_incr.increment(incr)
logging.error("CounterShard (%s) delayed increment %d: %s",
counter_name, incr, e)
return False
if delayed_incr:
counter.delayed_incr.count = 0
return True