Francesco Romani has uploaded a new change for review.

Change subject: mom: port ExpiringCache from VDSM
......................................................................

mom: port ExpiringCache from VDSM

import monotonic_time, ExpiringCache and its test
from VDSM codebase, to avoid an hidden dependency
to VDSM.

Change-Id: I819c179c000aabfd77cfd83e613e68ea362ad8dd
Signed-off-by: Francesco Romani <from...@redhat.com>
---
A mom/ExpiringCache.py
A tests/ExpiringCacheTests.py
M tests/Makefile.am
3 files changed, 240 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.ovirt.org:29418/mom refs/changes/79/40779/1

diff --git a/mom/ExpiringCache.py b/mom/ExpiringCache.py
new file mode 100644
index 0000000..fed4d4b
--- /dev/null
+++ b/mom/ExpiringCache.py
@@ -0,0 +1,113 @@
+#
+# Copyright 2014-2015 Red Hat, 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
+#
+# Refer to the README and COPYING files for full details of the license
+#
+
+import os
+import threading
+
+
+def monotonic_time():
+    """
+    Return the amount of time, in secs, elapsed since a fixed
+    arbitrary point in time in the past.
+    This function is useful if the client just
+    needs to use the difference between two given time points.
+
+    With repect to time.time():
+    * The resolution of this function is lower. On Linux,
+      the resolution is 1/_SC_CLK_TCK, which in turn depend on
+      the value of HZ configured in the kernel. A commonly
+      found resolution is 10 (ten) ms.
+    * This functions is resilient with respect to system clock
+      adjustments.
+    """
+    return os.times()[4]
+
+
+class ItemExpired(KeyError):
+    pass
+
+
+class ExpiringCache(object):
+    """
+    ExpiringCache behaves like a dict, but an expiration time
+    is attached to each key. Thread safe.
+
+    Parameters:
+    ttl: items will expire ttl seconds after they were inserted.
+    clock: time.time() or monotonic_time()-like callable.
+
+    Expired keys are removed on the first attempt to access them.
+    """
+    def __init__(self, ttl, clock=monotonic_time):
+        self._ttl = ttl
+        self._clock = clock
+        self._items = {}
+        self._lock = threading.Lock()
+
+    def get(self, key, default=None):
+        try:
+            return self._get_live(key)
+        except KeyError:
+            return default
+
+    def clear(self):
+        with self._lock:
+            self._items.clear()
+
+    def __setitem__(self, key, value):
+        item = self._clock() + self._ttl, value
+        with self._lock:
+            self._items[key] = item
+
+    def __getitem__(self, key):
+        """
+        If no value is stored for key, raises KeyError.
+        If an item expired, raises ItemExpired (a subclass of KeyError).
+        """
+        return self._get_live(key)
+
+    def __delitem__(self, key):
+        with self._lock:
+            del self._items[key]
+
+    def __nonzero__(self):
+        now = self._clock()
+        with self._lock:
+            expired_keys = [
+                key for key, (expiration, _)
+                in self._items.iteritems()
+                if expiration <= now]
+            for key in expired_keys:
+                del self._items[key]
+
+            return bool(self._items)
+
+    # private
+
+    def _get_live(self, key):
+        now = self._clock()
+        with self._lock:
+            expiration, value = self._items[key]
+
+            if expiration <= now:
+                del self._items[key]
+                raise ItemExpired
+
+            return value
diff --git a/tests/ExpiringCacheTests.py b/tests/ExpiringCacheTests.py
new file mode 100644
index 0000000..6e4245e
--- /dev/null
+++ b/tests/ExpiringCacheTests.py
@@ -0,0 +1,126 @@
+#
+# Copyright 2015 Red Hat, 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
+#
+# Refer to the README and COPYING files for full details of the license
+#
+
+import unittest
+from mom.ExpiringCache import ExpiringCache
+
+
+class ExpiringCacheOperationTests(unittest.TestCase):
+    def setUp(self):
+        self.cache = ExpiringCache(ttl=20)
+
+    def test_setitem_getitem_same_key(self):
+        self.cache['the answer'] = 42
+        self.assertEqual(42, self.cache['the answer'])
+
+    def test_setitem_get_same_key(self):
+        self.cache['the answer'] = 42
+        self.assertEqual(42, self.cache.get('the answer'))
+
+    def test_setitem_get_same_key_with_default(self):
+        self.cache['the answer'] = 42
+        self.assertEqual(42, self.cache.get('the answer', 'default'))
+
+    def test_setitem_get_different_key_with_default(self):
+        value = self.cache.get('a different answer', 'default')
+        self.assertEqual(value, 'default')
+
+    def test_get_key_without_explicit_default(self):
+        self.assertEqual(None, self.cache.get('a key noone added'))
+
+    def test_getitem_missing_key(self):
+        self.assertRaises(KeyError,
+                          lambda key: self.cache[key],
+                          'FIZZBUZZ')
+
+    def test_delitem_existing_key(self):
+        self.cache['the answer'] = 42
+        del self.cache['the answer']
+        self.assertEquals(self.cache.get('the answer'), None)
+
+    def test_delitem_missing_key(self):
+        def _del(key):
+            del self.cache[key]
+        self.assertRaises(KeyError,
+                          _del,
+                          'this key does not exist')
+
+    def test_clear(self):
+        ITEMS = 10
+        for i in range(ITEMS):
+            self.cache[i] = 'foobar-%d' % i
+
+        self.cache.clear()
+
+        for i in range(ITEMS):
+            self.cache.get(i) is None
+
+    def test_nonzero(self):
+        self.assertFalse(self.cache)
+        self.cache['foo'] = 'bar'
+        self.assertTrue(self.cache)
+
+
+class FakeClock(object):
+    def __init__(self, now):
+        self.now = now
+
+    def __call__(self):
+        return self.now
+
+
+class ExpirationTests(unittest.TestCase):
+    def test_key_expiration(self):
+        clock = FakeClock(0.0)
+        cache = ExpiringCache(ttl=1.0, clock=clock)
+        cache['the answer'] = 42
+        clock.now = 0.999999
+        self.assertEqual(42, cache['the answer'])
+        clock.now = 1.0
+        self.assertEqual(None, cache.get('the answer'))
+        clock.now = 1.000001
+        self.assertEqual(None, cache.get('the answer'))
+
+    def test_nonzero_full_expiration(self):
+        clock = FakeClock(0.0)
+        cache = ExpiringCache(ttl=1.0, clock=clock)
+
+        ITEMS = 10
+        for i in range(ITEMS):
+            cache[i] = 'foobar-%d' % i
+        self.assertTrue(cache)
+
+        clock.now = 1.1
+        self.assertFalse(cache)
+
+    def test_nonzero_partial_expiration(self):
+        clock = FakeClock(0.0)
+        cache = ExpiringCache(ttl=2.0, clock=clock)
+
+        cache['a'] = 1
+        clock.now = 1.0
+        self.assertTrue(cache)
+
+        cache['b'] = 2
+        clock.now = 2.0
+        self.assertTrue(cache)
+
+        clock.now = 3.0
+        self.assertFalse(cache)
diff --git a/tests/Makefile.am b/tests/Makefile.am
index d80983d..770aacf 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -20,6 +20,7 @@
        $(NULL)
 
 dist_noinst_PYTHON = \
+       ExpiringCacheTests.py \
        GeneralTests.py \
        ParserTests.py \
        testrunner.py \


-- 
To view, visit https://gerrit.ovirt.org/40779
To unsubscribe, visit https://gerrit.ovirt.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I819c179c000aabfd77cfd83e613e68ea362ad8dd
Gerrit-PatchSet: 1
Gerrit-Project: mom
Gerrit-Branch: master
Gerrit-Owner: Francesco Romani <from...@redhat.com>
_______________________________________________
Engine-patches mailing list
Engine-patches@ovirt.org
http://lists.ovirt.org/mailman/listinfo/engine-patches

Reply via email to