This includes an own simple cache implementation and an
interface to a memcache instance.
---
 lib/cache.py                  |  308 +++++++++++++++++++++++++++++++++++++++++
 test/ganeti.cache_unittest.py |  104 ++++++++++++++
 2 files changed, 412 insertions(+), 0 deletions(-)
 create mode 100644 lib/cache.py
 create mode 100755 test/ganeti.cache_unittest.py

diff --git a/lib/cache.py b/lib/cache.py
new file mode 100644
index 0000000..7789fb0
--- /dev/null
+++ b/lib/cache.py
@@ -0,0 +1,308 @@
+#
+#
+
+# Copyright (C) 2011 Google Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+
+"""This module implements caching."""
+
+
+import threading
+import time
+
+from ganeti import locking
+from ganeti import serializer
+
+try:
+  _has_memcache = True
+  import memcache
+except ImportError:
+  _has_memcache = False
+
+
+_cache = None
+_cache_lock = threading.Lock()
+
+
+def Cache():
+  """Factory method to return appropriate cache.
+
+  """
+  # We need to use the global keyword here
+  global _cache # pylint: disable-msg=W0603
+
+  if not _cache:
+    _cache_lock.acquire()
+    try:
+      if not _cache:
+        if _has_memcache:
+          cache_obj = MemCache()
+        else:
+          cache_obj = SimpleCache()
+        # W0621: Redefine '_cache' from outer scope (used for singleton)
+        _cache = cache_obj # pylint: disable-msg=W0621
+    finally:
+      _cache_lock.release()
+
+  # Make sure we have a fresh internal state and are ready to serve
+  _cache.ResetState()
+  return _cache
+
+
+class CacheBase:
+  """This is the base class for all caches.
+
+  """
+  def __init__(self):
+    """Base init method.
+
+    """
+
+  def Store(self, key, value, ttl=0):
+    """Stores key with value in the cache.
+
+    @param key: The key to associate this cached value
+    @param value: The value to cache
+    @param ttl: TTL in seconds after when this entry is considered outdated
+    @returns True on success, False on failure
+
+    """
+    raise NotImplementedError
+
+  def Get(self, keys):
+    """Retrieve the value from the cache.
+
+    @param keys: The keys to retrieve
+    @returns The list of values
+
+    """
+    raise NotImplementedError
+
+  def Invalidate(self, keys):
+    """Invalidate given keys.
+
+    @param keys: The list of keys to invalidate
+    @returns True on success, False otherwise
+
+    """
+    raise NotImplementedError
+
+  def Flush(self):
+    """Invalidates all of the keys and flushes the cache.
+
+    """
+    raise NotImplementedError
+
+  def ResetState(self):
+    """Used to reset the state of the cache.
+
+    This can be used to reinstantiate connection or any other state refresh
+
+    """
+
+  def Cleanup(self):
+    """Cleanup the cache from expired entries.
+
+    """
+
+
+class SimpleCache(CacheBase):
+  """Implements a very simple, dict base cache.
+
+  """
+  CLEANUP_ROUND = 1800
+
+  def __init__(self, _time_fn=time.time):
+    """Initialize this class.
+
+    @param _time_fn: Function used to return time (unittest only)
+    """
+    CacheBase.__init__(self)
+
+    self._time_fn = _time_fn
+
+    self.cache = {}
+    self.lock = locking.SharedLock("SimpleCache-lock")
+    self.last_cleanup = self._time_fn()
+
+  def _UnlockedCleanup(self):
+    """Does cleanup of the cache.
+
+    """
+    check_time = self._time_fn()
+    if (self.last_cleanup + self.CLEANUP_ROUND) <= check_time:
+      keys = []
+      for key, value in self.cache.items():
+        if not value["ttl"]:
+          continue
+
+        expired = value["timestamp"] + value["ttl"]
+        if expired < check_time:
+          keys.append(key)
+      self._UnlockedInvalidate(keys)
+      self.last_cleanup = check_time
+
+  @locking.ssynchronized("lock")
+  def Cleanup(self):
+    """Cleanup our cache.
+
+    """
+    self._UnlockedCleanup()
+
+  @locking.ssynchronized("lock")
+  def Store(self, key, value, ttl=0):
+    """Stores a value at key in the cache.
+
+    See L{CacheBase} for parameter description
+
+    """
+    assert ttl >= 0
+    self._UnlockedCleanup()
+    val = serializer.Dump(value)
+    cache_val = {
+      "timestamp": self._time_fn(),
+      "ttl": ttl,
+      "value": val
+      }
+    self.cache[key] = cache_val
+    return True
+
+  @locking.ssynchronized("lock", shared=1)
+  def Get(self, keys):
+    """Retrieve the values of keys from cache.
+
+    See L{CacheBase} for parameter description
+
+    """
+    return [self._ExtractValue(key) for key in keys]
+
+  @locking.ssynchronized("lock")
+  def Invalidate(self, keys):
+    """Invalidates value for keys in cache.
+
+    See L{CacheBase} for parameter description
+
+    """
+    self._UnlockedInvalidate(keys)
+    return True
+
+  @locking.ssynchronized("lock")
+  def Flush(self):
+    """Invalidates all keys and values in cache.
+
+    See L{CacheBase} for parameter description
+
+    """
+    self.cache.clear()
+    self.last_cleanup = self._time_fn()
+
+  def _UnlockedInvalidate(self, keys):
+    """Invalidate keys in cache.
+
+    This is the unlocked version, see L{Invalidate} for parameter description
+
+    """
+    for key in keys:
+      if key in self.cache:
+        del self.cache[key]
+
+  def _ExtractValue(self, key):
+    """Extracts just the value for a key.
+
+    This method is taking care if the value did not expire ans returns it
+
+    @param key: The key to look for
+    @returns The value if key is not expired, None otherwise
+
+    """
+    if key in self.cache:
+      cache_val = self.cache[key]
+      if cache_val["ttl"] == 0:
+        return serializer.Load(cache_val["value"])
+      else:
+        expired = cache_val["timestamp"] + cache_val["ttl"]
+
+        if self._time_fn() <= expired:
+          return serializer.Load(cache_val["value"])
+        else:
+          return None
+    else:
+      return None
+
+
+class MemCache(CacheBase):
+  """Implements the memcache cache.
+
+  """
+  def __init__(self, servers=("localhost:11211",)):
+    """Initialize this class.
+
+    @param servers: List of memcache servers in the format <host>:<port>
+
+    """
+    CacheBase.__init__(self)
+
+    self.mc = memcache.Client(servers)
+
+  def Store(self, key, value, ttl=0):
+    """Stores key with value in memcache.
+
+    See L{CacheBase} for parameter description
+
+    """
+    return (self.mc.add(key, value, time=ttl) or
+            self.mc.replace(key, value, time=ttl))
+
+  def Get(self, keys):
+    """Retrieve the list of keys from memcache.
+
+    See L{CacheBase} for parameter description
+
+    """
+    result = self.mc.get_multi(keys)
+    return_result = []
+    for key in keys:
+      if key in result:
+        return_result.append(result[key])
+      else:
+        return_result.append(None)
+    return return_result
+
+  def Invalidate(self, keys):
+    """Invalidates given keys in memcache.
+
+    See L{CacheBase} for parameter description
+
+    """
+    return self.mc.delete_multi(self, keys) == 1
+
+  def Flush(self):
+    """Invalidates all keys in memcache.
+
+    See L{CacheBase} for parameter description
+
+    """
+    self.mc.flush_all()
+
+  def ResetState(self):
+    """Resets the memcache state.
+
+    This basically means try to reconnect to possible memcache servers
+
+    """
+    self.mc.forget_dead_hosts()
diff --git a/test/ganeti.cache_unittest.py b/test/ganeti.cache_unittest.py
new file mode 100755
index 0000000..bf4944c
--- /dev/null
+++ b/test/ganeti.cache_unittest.py
@@ -0,0 +1,104 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2011 Google Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+"""Script for testing ganeti.cache"""
+
+import testutils
+import unittest
+
+from ganeti import cache
+
+
+class ReturnStub:
+  def __init__(self, values):
+    self.values = values
+
+  def __call__(self):
+    assert self.values
+    return self.values.pop(0)
+
+
+class SimpleCacheTest(unittest.TestCase):
+  def setUp(self):
+    self.cache = cache.SimpleCache()
+
+  def testNoKey(self):
+    self.assertEqual(self.cache.Get(["i-dont-exist", "neither-do-i", "no"]),
+                     [None, None, None])
+
+  def testCache(self):
+    value = 0xc0ffee
+    self.assert_(self.cache.Store("i-exist", value))
+    self.assertEqual(self.cache.Get(["i-exist"]), [value])
+
+  def testMixed(self):
+    value = 0xb4dc0de
+    self.assert_(self.cache.Store("i-exist", value))
+    self.assertEqual(self.cache.Get(["i-exist", "i-dont"]), [value, None])
+
+  def testTtl(self):
+    my_times = ReturnStub([0, 1, 1, 2, 3, 5])
+    ttl_cache = cache.SimpleCache(_time_fn=my_times)
+    self.assert_(ttl_cache.Store("test-expire", 0xdeadbeef, ttl=2))
+
+    # At this point time will return 2, 1 (start) + 2 (ttl) = 3, still valid
+    self.assertEqual(ttl_cache.Get(["test-expire"]), [0xdeadbeef])
+
+    # At this point time will return 3, 1 (start) + 2 (ttl) = 3, still valid
+    self.assertEqual(ttl_cache.Get(["test-expire"]), [0xdeadbeef])
+
+    # We are at 5, < 3, invalid
+    self.assertEqual(ttl_cache.Get(["test-expire"]), [None])
+    self.assertFalse(my_times.values)
+
+  def testCleanup(self):
+    my_times = ReturnStub([0, 1, 1, 2, 2, 3, 3, 5, 5,
+                           21 + cache.SimpleCache.CLEANUP_ROUND,
+                           34 + cache.SimpleCache.CLEANUP_ROUND,
+                           55 + cache.SimpleCache.CLEANUP_ROUND * 2,
+                           89 + cache.SimpleCache.CLEANUP_ROUND * 3])
+    # Index 0
+    ttl_cache = cache.SimpleCache(_time_fn=my_times)
+    # Index 1, 2
+    self.assert_(ttl_cache.Store("foobar", 0x1dea, ttl=6))
+    # Index 3, 4
+    self.assert_(ttl_cache.Store("baz", 0xc0dea55, ttl=11))
+    # Index 6, 7
+    self.assert_(ttl_cache.Store("long-foobar", "pretty long",
+                                 ttl=(22 + cache.SimpleCache.CLEANUP_ROUND)))
+    # Index 7, 8
+    self.assert_(ttl_cache.Store("foobazbar", "alive forever"))
+
+    self.assertEqual(set(ttl_cache.cache.keys()),
+                     set(["foobar", "baz", "long-foobar", "foobazbar"]))
+    ttl_cache.Cleanup()
+    self.assertEqual(set(ttl_cache.cache.keys()),
+                     set(["long-foobar", "foobazbar"]))
+    ttl_cache.Cleanup()
+    self.assertEqual(set(ttl_cache.cache.keys()),
+                     set(["long-foobar", "foobazbar"]))
+    ttl_cache.Cleanup()
+    self.assertEqual(set(ttl_cache.cache.keys()), set(["foobazbar"]))
+    ttl_cache.Cleanup()
+    self.assertEqual(set(ttl_cache.cache.keys()), set(["foobazbar"]))
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
-- 
1.7.3.1

Reply via email to