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