Source code for sheraf.transactions

import functools
import logging
import random
import time

import sheraf

logger = logging.getLogger(__name__)

[docs]def attempt( function, attempts=ATTEMPTS, commit=lambda: True, log_exceptions=True, args=None, kwargs=None, also_except=None, on_failure=lambda nb_attempt: time.sleep(random.uniform(0, nb_attempt)), ): """This method attemps to execute a function several times until no ConflictError is encountered. :param function: The function to execute. :param args: The positionnal arguments to pass to the function. :param kwargs: The keyword arguments to pass to the function. :param attempts: The number of attempts. :param commit: An executable returning a boolean indicating wether to commit after executing `function`. :param log_exceptions: A boolean indicating wether to log exceptions. :param also_except: A tuple containing exception classes. If one of those exceptions is catched during the attempt, a new execution will be attempted. :param on_failure: Callback called after each failed attempt except the last one. By default it waits a random time between 0 and `attempts`. The callback takes one argument ``nb_attempt``. :return: The return value of `function`. Example: >>> with sheraf.connection(): ... try: ... sheraf.attempt(do_amazing_stuff, attempts=5, args=("DANGER",)) ... except ZODB.POSException.ConflictError: ... print("Impossible to commit do_amazing_stuff after 5 attempts.") ... >>> with sheraf.connection(): ... try: ... sheraf.attempt(lambda: 1/0, also_except=(ZeroDivisionError,)) ... except ZeroDivisionError: ... print("This has very few chances to be successful ¯\\\\_(ツ)_/¯") This has very few chances to be successful ¯\\_(ツ)_/¯ """ import ZODB.POSException args = args or () kwargs = kwargs or {} if also_except: also_except = also_except if isinstance(also_except, tuple) else (also_except,) exception_classes = (ZODB.POSException.ConflictError,) + (also_except or ()) _exc = Exception with sheraf.connection(reuse=True) as connection: for x in range(attempts): start_time = time.time() start_commit_time = None try: connection.transaction_manager.begin() _response = function(*args, **kwargs) if commit(): start_commit_time = time.time() connection.transaction_manager.commit() else: connection.transaction_manager.abort() return _response except exception_classes as exc: # Not so easy to change the message of an exception. Just adding an 'extra_info' field # # now = time.time() execution_time = now - start_time if start_commit_time: commit_time = now - start_commit_time exc.extra_info = "Execution n°{} took {:.5f}s of which {:5f}s for the main function and {:5f}s to commit\n".format( x + 1, execution_time, execution_time - commit_time, commit_time ) else: exc.extra_info = "Execution n°{} took {:.5f}s\n".format( x + 1, execution_time ) if log_exceptions: extra = { "attempt": x + 1, "exception": exc, "total_time": execution_time, "stack": True, # Sentry parameter: show full stack } if start_commit_time: extra["commit_time"] = commit_time logger.warning(exc.extra_info, exc_info=True, extra=extra) connection.transaction_manager.abort() if x != attempts - 1: if on_failure: on_failure(x) if x > 0: exc.extra_info += _exc.extra_info _exc = exc except Exception: connection.transaction_manager.abort() raise raise _exc
[docs]def commit(f=None): """ Wrapper shortcut for :func:`~sheraf.transactions.attempt`. >>> class Cowboy(sheraf.Model): ... table = "cowboy" ... dead = sheraf.BooleanAttribute(default=False) ... >>> @sheraf.commit ... def fight(winner, loser): ... loser.dead = True ... >>> with sheraf.connection(commit=True): ... winner = Cowboy.create() ... loser = Cowboy.create() ... >>> with sheraf.connection(): ... fight(winner, loser) ... >>> with sheraf.connection(): ... True """ if f is None: connection = sheraf.Database.current_connection() if not connection: raise sheraf.exceptions.NotConnectedException() connection.transaction_manager.commit() return @functools.wraps(f) def wrapper(*args, **kwargs): return attempt(f, args=args, kwargs=kwargs) return wrapper