# Copyright (c) 2013 Rackspace Hosting # 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. """Multiple DB API backend support. A DB backend module should implement a method named 'get_backend' which takes no arguments. The method can return any object that implements DB API methods. """ import functools import logging import time from designate.openstack.common.db import exception from designate.openstack.common.gettextutils import _ # noqa from designate.openstack.common import importutils LOG = logging.getLogger(__name__) def safe_for_db_retry(f): """Enable db-retry for decorated function, if config option enabled.""" f.__dict__['enable_retry'] = True return f class wrap_db_retry(object): """Retry db.api methods, if DBConnectionError() raised Retry decorated db.api methods. If we enabled `use_db_reconnect` in config, this decorator will be applied to all db.api functions, marked with @safe_for_db_retry decorator. Decorator catchs DBConnectionError() and retries function in a loop until it succeeds, or until maximum retries count will be reached. """ def __init__(self, retry_interval, max_retries, inc_retry_interval, max_retry_interval): super(wrap_db_retry, self).__init__() self.retry_interval = retry_interval self.max_retries = max_retries self.inc_retry_interval = inc_retry_interval self.max_retry_interval = max_retry_interval def __call__(self, f): @functools.wraps(f) def wrapper(*args, **kwargs): next_interval = self.retry_interval remaining = self.max_retries while True: try: return f(*args, **kwargs) except exception.DBConnectionError as e: if remaining == 0: LOG.exception(_('DB exceeded retry limit.')) raise exception.DBError(e) if remaining != -1: remaining -= 1 LOG.exception(_('DB connection error.')) # NOTE(vsergeyev): We are using patched time module, so # this effectively yields the execution # context to another green thread. time.sleep(next_interval) if self.inc_retry_interval: next_interval = min( next_interval * 2, self.max_retry_interval ) return wrapper class DBAPI(object): def __init__(self, backend_name, backend_mapping=None, **kwargs): """Initialize the choosen DB API backend. :param backend_name: name of the backend to load :type backend_name: str :param backend_mapping: backend name -> module/class to load mapping :type backend_mapping: dict Keyword arguments: :keyword use_db_reconnect: retry DB transactions on disconnect or not :type use_db_reconnect: bool :keyword retry_interval: seconds between transaction retries :type retry_interval: int :keyword inc_retry_interval: increase retry interval or not :type inc_retry_interval: bool :keyword max_retry_interval: max interval value between retries :type max_retry_interval: int :keyword max_retries: max number of retries before an error is raised :type max_retries: int """ if backend_mapping is None: backend_mapping = {} # Import the untranslated name if we don't have a # mapping. backend_path = backend_mapping.get(backend_name, backend_name) backend_mod = importutils.import_module(backend_path) self.__backend = backend_mod.get_backend() self.use_db_reconnect = kwargs.get('use_db_reconnect', False) self.retry_interval = kwargs.get('retry_interval', 1) self.inc_retry_interval = kwargs.get('inc_retry_interval', True) self.max_retry_interval = kwargs.get('max_retry_interval', 10) self.max_retries = kwargs.get('max_retries', 20) def __getattr__(self, key): attr = getattr(self.__backend, key) if not hasattr(attr, '__call__'): return attr # NOTE(vsergeyev): If `use_db_reconnect` option is set to True, retry # DB API methods, decorated with @safe_for_db_retry # on disconnect. if self.use_db_reconnect and hasattr(attr, 'enable_retry'): attr = wrap_db_retry( retry_interval=self.retry_interval, max_retries=self.max_retries, inc_retry_interval=self.inc_retry_interval, max_retry_interval=self.max_retry_interval)(attr) return attr