diff --git a/keystonemiddleware/auth_token/__init__.py b/keystonemiddleware/auth_token/__init__.py index 343bec22..9e84c951 100644 --- a/keystonemiddleware/auth_token/__init__.py +++ b/keystonemiddleware/auth_token/__init__.py @@ -225,6 +225,8 @@ from keystoneauth1 import loading from keystoneauth1.loading import session as session_loading from keystoneclient.common import cms from keystoneclient import exceptions as ksc_exceptions +import oslo_cache +from oslo_config import cfg from oslo_log import log as logging from oslo_serialization import jsonutils import webob.dec @@ -245,6 +247,7 @@ from keystonemiddleware.i18n import _ _LOG = logging.getLogger(__name__) _CACHE_INVALID_INDICATOR = 'invalid' +oslo_cache.configure(cfg.CONF) AUTH_TOKEN_OPTS = [ @@ -952,7 +955,15 @@ class AuthProtocol(BaseAuthProtocol): include_service_catalog=self._include_service_catalog, requested_auth_version=auth_version) + def _create_oslo_cache(self): + # having this as a function makes test mocking easier + conf = cfg.CONF + region = oslo_cache.create_region() + oslo_cache.configure_cache_region(conf, region) + return region + def _token_cache_factory(self): + security_strategy = self._conf.get('memcache_security_strategy') cache_kwargs = dict( diff --git a/keystonemiddleware/auth_token/_cache.py b/keystonemiddleware/auth_token/_cache.py index 43ce923d..28010778 100644 --- a/keystonemiddleware/auth_token/_cache.py +++ b/keystonemiddleware/auth_token/_cache.py @@ -13,13 +13,13 @@ import contextlib import hashlib +from oslo_cache import _memcache_pool as memcache_pool from oslo_serialization import jsonutils from oslo_utils import timeutils import six from keystonemiddleware.auth_token import _exceptions as exc from keystonemiddleware.auth_token import _memcache_crypt as memcache_crypt -from keystonemiddleware.auth_token import _memcache_pool as memcache_pool from keystonemiddleware.i18n import _ diff --git a/keystonemiddleware/auth_token/_memcache_pool.py b/keystonemiddleware/auth_token/_memcache_pool.py deleted file mode 100644 index 2b3853ab..00000000 --- a/keystonemiddleware/auth_token/_memcache_pool.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright 2014 Mirantis Inc -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Thread-safe connection pool for python-memcached.""" - -# NOTE(yorik-sar): this file is copied between keystone and keystonemiddleware -# and should be kept in sync until we can use external library for this. - -import collections -import contextlib -import itertools -import time - -from oslo_log import log as logging -from six.moves import queue - -_PoolItem = collections.namedtuple('_PoolItem', ['ttl', 'connection']) - - -class ConnectionGetTimeoutException(Exception): - pass - - -class ConnectionPool(queue.Queue): - """Base connection pool class. - - This class implements the basic connection pool logic as an abstract base - class. - """ - - def __init__(self, maxsize, unused_timeout, conn_get_timeout=None): - """Initialize the connection pool. - - :param maxsize: maximum number of client connections for the pool - :type maxsize: int - :param unused_timeout: idle time to live for unused clients (in - seconds). If a client connection object has been - in the pool and idle for longer than the - unused_timeout, it will be reaped. This is to - ensure resources are released as utilization - goes down. - :type unused_timeout: int - :param conn_get_timeout: maximum time in seconds to wait for a - connection. If set to `None` timeout is - indefinite. - :type conn_get_timeout: int - """ - queue.Queue.__init__(self, maxsize) - self._unused_timeout = unused_timeout - self._connection_get_timeout = conn_get_timeout - self._acquired = 0 - self._LOG = logging.getLogger(__name__) - - def _create_connection(self): - raise NotImplementedError - - def _destroy_connection(self, conn): - raise NotImplementedError - - @contextlib.contextmanager - def acquire(self): - try: - conn = self.get(timeout=self._connection_get_timeout) - except queue.Empty: - self._LOG.critical('Unable to get a connection from pool id ' - '%(id)s after %(seconds)s seconds.', - {'id': id(self), - 'seconds': self._connection_get_timeout}) - raise ConnectionGetTimeoutException() - try: - yield conn - finally: - self.put(conn) - - def _qsize(self): - return self.maxsize - self._acquired - - if not hasattr(queue.Queue, '_qsize'): - qsize = _qsize - - def _get(self): - if self.queue: - conn = self.queue.pop().connection - else: - conn = self._create_connection() - self._acquired += 1 - return conn - - def _put(self, conn): - self.queue.append(_PoolItem( - ttl=time.time() + self._unused_timeout, - connection=conn, - )) - self._acquired -= 1 - # Drop all expired connections from the right end of the queue - now = time.time() - while self.queue and self.queue[0].ttl < now: - conn = self.queue.popleft().connection - self._destroy_connection(conn) - - -class MemcacheClientPool(ConnectionPool): - def __init__(self, urls, dead_retry=None, socket_timeout=None, **kwargs): - ConnectionPool.__init__(self, **kwargs) - self._urls = urls - self._dead_retry = dead_retry - self._socket_timeout = socket_timeout - - # NOTE(morganfainberg): The host objects expect an int for the - # deaduntil value. Initialize this at 0 for each host with 0 indicating - # the host is not dead. - self._hosts_deaduntil = [0] * len(urls) - - # NOTE(morganfainberg): Lazy import to allow middleware to work with - # python 3k even if memcache will not due to python 3k - # incompatibilities within the python-memcache library. - global memcache - import memcache - - # This 'class' is taken from http://stackoverflow.com/a/22520633/238308 - # Don't inherit client from threading.local so that we can reuse - # clients in different threads - MemcacheClient = type('_MemcacheClient', (object,), - dict(memcache.Client.__dict__)) - - self._memcache_client_class = MemcacheClient - - def _create_connection(self): - return self._memcache_client_class(self._urls, - dead_retry=self._dead_retry, - socket_timeout=self._socket_timeout) - - def _destroy_connection(self, conn): - conn.disconnect_all() - - def _get(self): - conn = ConnectionPool._get(self) - try: - # Propagate host state known to us to this client's list - now = time.time() - for deaduntil, host in zip(self._hosts_deaduntil, conn.servers): - if deaduntil > now and host.deaduntil <= now: - host.mark_dead('propagating death mark from the pool') - host.deaduntil = deaduntil - except Exception: - # We need to be sure that connection doesn't leak from the pool. - # This code runs before we enter context manager's try-finally - # block, so we need to explicitly release it here - ConnectionPool._put(self, conn) - raise - return conn - - def _put(self, conn): - try: - # If this client found that one of the hosts is dead, mark it as - # such in our internal list - now = time.time() - for i, deaduntil, host in zip(itertools.count(), - self._hosts_deaduntil, - conn.servers): - # Do nothing if we already know this host is dead - if deaduntil <= now: - if host.deaduntil > now: - self._hosts_deaduntil[i] = host.deaduntil - else: - self._hosts_deaduntil[i] = 0 - # If all hosts are dead we should forget that they're dead. This - # way we won't get completely shut off until dead_retry seconds - # pass, but will be checking servers as frequent as we can (over - # way smaller socket_timeout) - if all(deaduntil > now for deaduntil in self._hosts_deaduntil): - self._hosts_deaduntil[:] = [0] * len(self._hosts_deaduntil) - finally: - ConnectionPool._put(self, conn) diff --git a/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py b/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py index a130b199..5c59d145 100644 --- a/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py +++ b/keystonemiddleware/tests/unit/auth_token/test_auth_token_middleware.py @@ -28,6 +28,7 @@ from keystoneauth1 import session from keystoneclient.common import cms from keystoneclient import exceptions as ksc_exceptions import mock +import oslo_cache from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import timeutils @@ -252,6 +253,17 @@ class v3CompositeFakeApp(CompositeBase, v3FakeApp): v3_default_service_env_additions) +class FakeOsloCache(_cache._FakeClient): + """A fake oslo_cache object. + + The memcache and oslo_cache interfaces are almost the same except we need + to return NO_VALUE when not found. + """ + + def get(self, key): + return super(FakeOsloCache, self).get(key) or oslo_cache.NO_VALUE + + class BaseAuthTokenMiddlewareTest(base.BaseAuthTokenTestCase): """Base test class for auth_token middleware. @@ -270,6 +282,12 @@ class BaseAuthTokenMiddlewareTest(base.BaseAuthTokenTestCase): super(BaseAuthTokenMiddlewareTest, self).setUp() self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) + + # the default oslo_cache is null cache, always use an in-mem cache + self.useFixture(fixtures.MockPatchObject(auth_token.AuthProtocol, + '_create_oslo_cache', + return_value=FakeOsloCache())) + self.expected_env = expected_env or dict() self.fake_app = fake_app or FakeApp self.middleware = None diff --git a/keystonemiddleware/tests/unit/auth_token/test_connection_pool.py b/keystonemiddleware/tests/unit/auth_token/test_connection_pool.py index 074d1e5d..699b5b5a 100644 --- a/keystonemiddleware/tests/unit/auth_token/test_connection_pool.py +++ b/keystonemiddleware/tests/unit/auth_token/test_connection_pool.py @@ -13,11 +13,11 @@ import time import mock +from oslo_cache import _memcache_pool from six.moves import queue import testtools from testtools import matchers -from keystonemiddleware.auth_token import _memcache_pool from keystonemiddleware.tests.unit import utils @@ -109,7 +109,7 @@ class TestConnectionPool(utils.TestCase): # Make sure we've consumed the only available connection from the pool conn = connection_pool.get_nowait() - self.assertRaises(_memcache_pool.ConnectionGetTimeoutException, + self.assertRaises(_memcache_pool.exception.QueueEmpty, _acquire_connection) # Put the connection back and ensure we can acquire the connection diff --git a/requirements.txt b/requirements.txt index 063f47c9..7b839c77 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ # process, which may cause wedges in the gate later. keystoneauth1>=3.2.0 # Apache-2.0 +oslo.cache>=1.26.0 # Apache-2.0 oslo.config>=4.6.0 # Apache-2.0 oslo.context>=2.19.2 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0