memcache race condition and combining incr and decr

This commit is contained in:
David Goetz 2010-10-22 15:25:22 -07:00
parent f494fc37a6
commit c08de81aeb
4 changed files with 43 additions and 43 deletions

View File

@ -168,49 +168,42 @@ class MemcacheRing(object):
def incr(self, key, delta=1, timeout=0): def incr(self, key, delta=1, timeout=0):
""" """
Increments a key which has a numeric value by delta. Increments a key which has a numeric value by delta.
If the key can't be found, it's added as delta. If the key can't be found, it's added as delta or 0 if delta < 0.
If passed a negative number, will use memcached's decr. Returns
the int stored in memcached
Note: The data memcached stores as the result of incr/decr is
an unsigned int. decr's that result in a number below 0 are
stored as 0.
:param key: key :param key: key
:param delta: amount to add to the value of key (or set as the value :param delta: amount to add to the value of key (or set as the value
if the key is not found) if the key is not found) will be cast to an int
:param timeout: ttl in memcache :param timeout: ttl in memcache
""" """
key = md5hash(key) key = md5hash(key)
command = 'incr'
if delta < 0:
command = 'decr'
delta = str(int(abs(delta)))
for (server, fp, sock) in self._get_conns(key): for (server, fp, sock) in self._get_conns(key):
try: try:
sock.sendall('incr %s %s\r\n' % (key, delta)) sock.sendall('%s %s %s\r\n' % (command, key, delta))
line = fp.readline().strip().split() line = fp.readline().strip().split()
if line[0].upper() == 'NOT_FOUND': if line[0].upper() == 'NOT_FOUND':
line[0] = str(delta) add_val = delta
sock.sendall('add %s %d %d %s noreply\r\n%s\r\n' % \ if command == 'decr':
(key, 0, timeout, len(line[0]), line[0])) add_val = '0'
ret = int(line[0].strip()) sock.sendall('add %s %d %d %s\r\n%s\r\n' % \
self._return_conn(server, fp, sock) (key, 0, timeout, len(add_val), add_val))
return ret line = fp.readline().strip().split()
except Exception, e: if line[0].upper() == 'NOT_STORED':
self._exception_occurred(server, e) sock.sendall('%s %s %s\r\n' % (command, key, delta))
line = fp.readline().strip().split()
def decr(self, key, delta=1, timeout=0): ret = int(line[0].strip())
""" else:
Decrements a key which has a numeric value by delta. ret = int(add_val)
If the key can't be found, it's added as 0. Memcached else:
will treat data values below 0 as 0 with incr/decr. ret = int(line[0].strip())
:param key: key
:param delta: amount to subtract to the value of key (or set
as the value if the key is not found)
:param timeout: ttl in memcache
"""
key = md5hash(key)
for (server, fp, sock) in self._get_conns(key):
try:
sock.sendall('decr %s %s\r\n' % (key, delta))
line = fp.readline().strip().split()
if line[0].upper() == 'NOT_FOUND':
line[0] = '0'
sock.sendall('add %s %d %d %s noreply\r\n%s\r\n' %
(key, 0, timeout, len(line[0]), line[0]))
ret = int(line[0].strip())
self._return_conn(server, fp, sock) self._return_conn(server, fp, sock)
return ret return ret
except Exception, e: except Exception, e:

View File

@ -148,7 +148,7 @@ class RateLimitMiddleware(object):
max_sleep_m = self.max_sleep_time_seconds * self.clock_accuracy max_sleep_m = self.max_sleep_time_seconds * self.clock_accuracy
if max_sleep_m - need_to_sleep_m <= self.clock_accuracy * 0.01: if max_sleep_m - need_to_sleep_m <= self.clock_accuracy * 0.01:
# treat as no-op decrement time # treat as no-op decrement time
self.memcache_client.decr(key, delta=time_per_request_m) self.memcache_client.incr(key, delta=-time_per_request_m)
raise MaxSleepTimeHit("Max Sleep Time Exceeded: %s" % raise MaxSleepTimeHit("Max Sleep Time Exceeded: %s" %
need_to_sleep_m) need_to_sleep_m)

View File

@ -36,15 +36,7 @@ class FakeMemcache(object):
return True return True
def incr(self, key, delta=1, timeout=0): def incr(self, key, delta=1, timeout=0):
if delta < 0: self.store[key] = int(self.store.setdefault(key, 0)) + int(delta)
raise "Cannot incr by a negative number"
self.store[key] = int(self.store.setdefault(key, 0)) + delta
return int(self.store[key])
def decr(self, key, delta=1, timeout=0):
if delta < 0:
raise "Cannot decr by a negative number"
self.store[key] = int(self.store.setdefault(key, 0)) - delta
if self.store[key] < 0: if self.store[key] < 0:
self.store[key] = 0 self.store[key] = 0
return int(self.store[key]) return int(self.store[key])

View File

@ -98,6 +98,17 @@ class MockMemcached(object):
self.outbuf += str(val[2]) + '\r\n' self.outbuf += str(val[2]) + '\r\n'
else: else:
self.outbuf += 'NOT_FOUND\r\n' self.outbuf += 'NOT_FOUND\r\n'
elif parts[0].lower() == 'decr':
if parts[1] in self.cache:
val = list(self.cache[parts[1]])
if int(val[2]) - int(parts[2]) > 0:
val[2] = str(int(val[2]) - int(parts[2]))
else:
val[2] = '0'
self.cache[parts[1]] = val
self.outbuf += str(val[2]) + '\r\n'
else:
self.outbuf += 'NOT_FOUND\r\n'
def readline(self): def readline(self):
if self.down: if self.down:
raise Exception('mock is down') raise Exception('mock is down')
@ -151,6 +162,10 @@ class TestMemcached(unittest.TestCase):
self.assertEquals(memcache_client.get('some_key'), '10') self.assertEquals(memcache_client.get('some_key'), '10')
memcache_client.incr('some_key', delta=1) memcache_client.incr('some_key', delta=1)
self.assertEquals(memcache_client.get('some_key'), '11') self.assertEquals(memcache_client.get('some_key'), '11')
memcache_client.incr('some_key', delta=-5)
self.assertEquals(memcache_client.get('some_key'), '6')
memcache_client.incr('some_key', delta=-15)
self.assertEquals(memcache_client.get('some_key'), '0')
def test_retry(self): def test_retry(self):
logging.getLogger().addHandler(NullLoggingHandler()) logging.getLogger().addHandler(NullLoggingHandler())