def test_retry_fail_no_args(mocked_sleep, exc, calls, sleep_calls):
    """Using @retry with no arguments should raise the exception raised by the decorated function if not cathced."""
    func = _generate_mocked_function(calls)
    with pytest.raises(exc, match='error'):
        retry(func)()

    func.assert_has_calls([mock.call()] * len(calls))
    mocked_sleep.assert_has_calls([mock.call(i) for i in sleep_calls])
def test_retry_fail_args(mocked_sleep, exc, kwargs):
    """Using @retry with arguments should raise the exception raised by the decorated function if not cathced."""
    func = _generate_mocked_function([exc('error')])
    kwargs['tries'] = 1
    with pytest.raises(exc, match='error'):
        retry(**kwargs)(func)()

    func.assert_called_once_with()
    assert not mocked_sleep.called
def test_retry_fail_chained_exceptions(mocked_sleep, caplog):
    """When @retry catches a chained exception, it should log exception messages all the way down the chain."""
    def side_effect():
        try:
            raise WmflibError('error2') from WmflibError('error3')
        except WmflibError:
            raise WmflibError('error1')  # pylint: disable=raise-missing-from
    func = _generate_mocked_function(side_effect)
    with pytest.raises(WmflibError, match='error1'):
        retry(func)()

    assert 'error1\nRaised while handling: error2\nCaused by: error3' in caplog.text
    assert mocked_sleep.call_count == 2
Esempio n. 4
0
def locked_open(file_path: PathLike,
                file_mode: str = 'r',
                *,
                timeout: int = 10) -> Generator[IO, None, None]:
    """Context manager to open a file with an exclusive lock on it and a retry logic.

    Examples:
        ::

            from wmflib.fileio import locked_open
            with locked_open('existing.file') as f:
                text = f.read()

            with locked_open('new.out', 'w') as f:
                f.write('Some text')

    Arguments:
        file_path (os.PathLike): the file path to open.
        file_mode (str, optional): the mode in which the file is opened, see :py:func:`open` for details.
        timeout (int, optional): the total timeout in seconds to wait to acquire the exclusive lock before giving up.
            Ten tries will be attempted to acquire the lock within the timeout.

    Raises:
        wmflib.fileio.LockError: on failure to acquire the exclusive lock on the file.

    Yields:
        file object: the open file with an exclusive lock on it.

    """
    tries = 10
    with open(file_path, file_mode, encoding='utf-8') as fd:
        try:
            # Decorate the call to the locking function to retry acquiring the lock:
            # decorator(decorator_args)(function)(function_args)
            # no-value-for-parameter is needed because pylint is confused by @ensure_wraps
            retry(  # pylint: disable=no-value-for-parameter
                tries=tries,
                delay=timedelta(seconds=timeout / tries),
                backoff_mode='constant',
                exceptions=(OSError, BlockingIOError))(fcntl.flock)(
                    fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
            logger.debug('Acquired exclusive lock on %s', file_path)
        except OSError as e:
            raise LockError(
                f'Unable to acquire exclusive lock on {file_path}') from e

        try:
            yield fd
        finally:
            fcntl.flock(fd, fcntl.LOCK_UN)
            logger.debug('Released exclusive lock on %s', file_path)
def test_retry_pass_no_args(mocked_sleep, calls, sleep_calls):
    """Using @retry with no arguments should use the default values."""
    func = _generate_mocked_function(calls)
    ret = retry(func)()
    assert ret
    func.assert_has_calls([mock.call()] * len(calls))
    mocked_sleep.assert_has_calls([mock.call(i) for i in sleep_calls])
def test_retry_failure_message(mocked_sleep, failure_message, expected_log, caplog):
    """Using @retry with a failure_message should log that message when an exception is caught."""
    func = _generate_mocked_function([WmflibError('error1'), True])
    ret = retry(failure_message=failure_message)(func)()  # pylint: disable=no-value-for-parameter

    assert ret
    assert expected_log in caplog.text
    assert 'error1' in caplog.text
    assert mocked_sleep.call_count == 1
def test_retry_dynamic_params_callback(mocked_sleep):
    """It should execute the given callback and use the new parameters."""
    def callback(params, _func, _args, _kwargs):
        """Alter the tries value."""
        params.tries = 2

    func = _generate_mocked_function([WmflibError('error1'), True])
    ret = retry(tries=1, dynamic_params_callbacks=(callback,))(func)()  # pylint: disable=no-value-for-parameter

    assert ret
    assert mocked_sleep.call_count == 1