Commits

Andriy Kornatskyy committed f543f65

Introduced OnePass pattern as a solution to problem in patterns module.

Comments (0)

Files changed (4)

 		echo 'done.'; \
 	fi
 	$(EASY_INSTALL) -i $(PYPI) -O2 coverage nose pytest \
-		pytest-pep8 pytest-cov
+		pytest-pep8 pytest-cov mock
 	# The following packages available for python < 3.0
 	if [ "$$(echo $(VERSION) | sed 's/\.//')" -lt 30 ]; then \
 		$(EASY_INSTALL) -i $(PYPI) -U -O2 python-memcached; \
 .. automodule:: wheezy.caching.null
    :members:
 
-wheezy.caching.pools
---------------------
+wheezy.caching.patterns
+-----------------------
 
-.. automodule:: wheezy.caching.pools
+.. automodule:: wheezy.caching.patterns
    :members:
 
 wheezy.caching.pylibmc

src/wheezy/caching/patterns.py

+
+""" ``patterns`` module.
+"""
+
+from time import sleep
+from time import time
+
+
+class OnePass(object):
+    """ A solution to `Thundering head` problem.
+
+        see http://en.wikipedia.org/wiki/Thundering_herd_problem
+
+        Typical use::
+
+            with OnePass(cache, 'op:' + key) as one_pass:
+                if one_pass.acquired:
+                    # update *key* in cache
+                elif one_pass.wait():
+                    # obtain *key* from cache
+                else:
+                    # timeout
+    """
+
+    __slots__ = ('cache', 'key', 'time', 'namespace', 'acquired')
+
+    def __init__(self, cache, key, time=10, namespace=None):
+        self.cache = cache
+        self.key = key
+        self.time = time
+        self.namespace = namespace
+        self.acquired = False
+
+    def __enter__(self):
+        marker = int(time())
+        self.acquired = self.cache.add(self.key, marker, self.time,
+                                       self.namespace)
+        return self
+
+    def wait(self, timeout=None):
+        """ Wait *timeout* seconds for the one pass become available.
+
+            *timeout* - if not passed defaults to *time* used during
+            initialization.
+        """
+        assert not self.acquired
+        expected = marker = self.cache.get(self.key, self.namespace)
+        timeout = timeout or self.time
+        wait_time = 0.05
+        while timeout > 0.0 and expected == marker:
+            sleep(wait_time)
+            marker = self.cache.get(self.key, self.namespace)
+            if marker is None:  # deleted or timed out
+                return True
+            if wait_time < 0.8:
+                wait_time *= 2.0
+            timeout -= wait_time
+        return False
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        if self.acquired:
+            self.cache.delete(self.key, self.namespace)
+            self.acquired = False

src/wheezy/caching/tests/test_patterns.py

+
+""" Unit tests for ``wheezy.caching.patterns``.
+"""
+
+import unittest
+
+from mock import ANY
+from mock import Mock
+from mock import patch
+
+
+class OnePassTestCase(unittest.TestCase):
+
+    def setUp(self):
+        from wheezy.caching.patterns import OnePass
+        self.mock_cache = Mock()
+        self.one_pass = OnePass(self.mock_cache, 'key',
+                                time=10, namespace='ns')
+
+    def test_enter(self):
+        """ Enter returns one_pass instance.
+        """
+        self.mock_cache.add.return_value = True
+        assert self.one_pass == self.one_pass.__enter__()
+        assert self.one_pass.acquired
+        self.mock_cache.add.assert_called_once_with('key', ANY, 10, 'ns')
+
+    def test_exit_acquired(self):
+        """ Releases key if acquired.
+        """
+        self.mock_cache.add.return_value = True
+        self.one_pass.__enter__()
+        assert self.one_pass.acquired
+        self.one_pass.__exit__(None, None, None)
+        self.mock_cache.delete.assert_called_once_with('key', 'ns')
+        assert not self.one_pass.acquired
+
+    def test_exit_not_acquired(self):
+        """ If one pass was not acquired, do not release key.
+        """
+        self.mock_cache.add.return_value = False
+        self.one_pass.__enter__()
+        assert not self.one_pass.acquired
+        self.one_pass.__exit__(None, None, None)
+        assert not self.mock_cache.delete.called
+
+    @patch('wheezy.caching.patterns.sleep')
+    def test_wait_no_marker(self, mock_sleep):
+        """ Exit wait loop if there is no marker.
+        """
+        self.mock_cache.add.return_value = False
+        self.one_pass.__enter__()
+        assert not self.one_pass.acquired
+        self.mock_cache.get.return_value = None
+        assert True == self.one_pass.wait()
+
+    @patch('wheezy.caching.patterns.sleep')
+    def test_wait_timeout(self, mock_sleep):
+        """ Exit wait loop if there is timeout reached.
+        """
+        self.mock_cache.add.return_value = False
+        self.one_pass.__enter__()
+        assert not self.one_pass.acquired
+        self.mock_cache.get.return_value = 1
+        assert False == self.one_pass.wait()