+from .http_tools import errors as httperrors
+from .utils import Attempt, get_arg, inject_args
+
+
+RECOVERY_TIME = 3
+
+_ATTEMPT = Attempt()
+
+
+def with_transaction(fn=None, cursor=None):
+ """Decorator that can be used together with instances of the ``db_base``
+ class, to perform database actions wrapped in a commit/rollback fashion
+
+ This decorator basically executes the function inside the context object
+ given by the ``transaction`` method in ``db_base``
+
+ Arguments:
+ cursor: [Optional] cursor class
+ """
+ if fn is None: # Allows calling the decorator directly or with parameters
+ return partial(with_transaction, cursor=cursor)
+
+ @wraps(fn)
+ def _wrapper(self, *args, **kwargs):
+ cursor_type = None
+ if cursor == 'dict':
+ # MySQLdB define the "cursors" module attribute lazily,
+ # so we have to defer references to mdb.cursors.DictCursor
+ cursor_type = mdb.cursors.DictCursor
+
+ with self.transaction(cursor_type):
+ return fn(self, *args, **kwargs)
+
+ return _wrapper
+
+
+def retry(fn=None, max_attempts=Attempt.MAX, **info):
+ """Decorator that can be used together with instances of the ``db_base``
+ class, to replay a method again after a unexpected error.
+
+ The function being decorated needs to either be a method of ``db_base``
+ subclasses or accept an ``db_base`` instance as the first parameter.
+
+ All the extra keyword arguments will be passed to the ``_format_error``
+ method
+ """
+ if fn is None: # Allows calling the decorator directly or with parameters
+ return partial(retry, max_attempts=max_attempts, **info)
+
+ @wraps(fn)
+ def _wrapper(*args, **kwargs):
+ self = args[0]
+ info.setdefault('table', get_arg('table', fn, args, kwargs))
+ attempt = Attempt(max_attempts=max_attempts, info=info)
+ while attempt.countdown >= 0:
+ try:
+ return inject_args(fn, attempt=attempt)(*args, **kwargs)
+ except (mdb.Error, AttributeError) as ex:
+ self.logger.debug("Attempt #%d", attempt.number)
+ try:
+ # The format error will throw exceptions, however it can
+ # tolerate a certain amount of retries if it judges that
+ # the error can be solved with retrying
+ self._format_error(ex, attempt.countdown, **attempt.info)
+ # Anyway, unexpected/unknown errors can still be retried
+ except db_base_Exception as db_ex:
+ if (attempt.countdown < 0 or db_ex.http_code !=
+ httperrors.Internal_Server_Error):
+ raise
+
+ attempt.count += 1
+
+ return _wrapper
+