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
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