pottery/exceptions.py
class ExtendUnlockedLock(PrimitiveError, RuntimeError):
    'Attempting to extend an unlocked Redlock.'
pottery/exceptions.py
class ReleaseUnlockedLock(PrimitiveError, RuntimeError):
    'Attempting to release an unlocked Redlock.'
pottery/exceptions.py
class QuorumNotAchieved(PrimitiveError, RuntimeError):
    'Consensus-based algorithm could not achieve quorum.'
pottery/timer.py
class ContextTimer:
    '''Measure the execution time of small code snippets.

    Note that ContextTimer measures wall (real-world) time, not CPU time; and
    that .elapsed() returns time in milliseconds.

    You can use ContextTimer stand-alone...

        >>> import time
        >>> timer = ContextTimer()
        >>> timer.start()
        >>> time.sleep(0.1)
        >>> 100 <= timer.elapsed() < 200
        True
        >>> timer.stop()
        >>> time.sleep(0.1)
        >>> 100 <= timer.elapsed() < 200
        True

    ...or as a context manager:

        >>> tests = []
        >>> with ContextTimer() as timer:
        ...     time.sleep(0.1)
        ...     tests.append(100 <= timer.elapsed() < 200)
        >>> time.sleep(0.1)
        >>> tests.append(100 <= timer.elapsed() < 200)
        >>> tests
        [True, True]
    '''

    __slots__ = ('_started', '_stopped')

    def __init__(self) -> None:
        self._started = 0.0
        self._stopped = 0.0

    def __enter__(self) -> ContextTimer:
        self.__start()
        return self

    @overload
    def __exit__(self,
                 exc_type: None,
                 exc_value: None,
                 exc_traceback: None,
                 ) -> Literal[False]:
        raise NotImplementedError

    @overload
    def __exit__(self,
                 exc_type: Type[BaseException],
                 exc_value: BaseException,
                 exc_traceback: TracebackType,
                 ) -> Literal[False]:
        raise NotImplementedError

    def __exit__(self,
                 exc_type: Type[BaseException] | None,
                 exc_value: BaseException | None,
                 exc_traceback: TracebackType | None,
                 ) -> Literal[False]:
        self.__stop()
        return False

    def start(self) -> None:
        if self._stopped:
            raise RuntimeError('timer has already been stopped')
        elif self._started:
            raise RuntimeError('timer has already been started')
        else:
            self._started = timeit.default_timer()

    # Preserve the Open-Closed Principle with name mangling.
    #   https://youtu.be/miGolgp9xq8?t=2086
    #   https://stackoverflow.com/a/38534939
    __start = start

    def stop(self) -> None:
        if self._stopped:
            raise RuntimeError('timer has already been stopped')
        elif self._started:
            self._stopped = timeit.default_timer()
        else:
            raise RuntimeError("timer hasn't yet been started")

    __stop = stop

    def elapsed(self) -> int:
        if self._started:
            stopped_or_current = self._stopped or timeit.default_timer()
            elapsed = stopped_or_current - self._started
            return round(elapsed * 1000)
        else:
            raise RuntimeError("timer hasn't yet been started")
pottery/base.py
def random_key(*,
               redis: Redis,
               prefix: str = 'pottery:',
               num_tries: int = 3,
               ) -> str:
def _connection_args(redis: Redis) -> Tuple[str, int, int]:
    def __init__(self, *, redis: Redis | None = None, key: str = '') -> None:
    def __del__(self) -> None:
    def redis(self) -> Redis:
    def redis(self, value: Redis | None) -> None:
    def key(self) -> str:
    def key(self, value: str) -> None:
    def _random_key(self) -> str:
    def _encode(decoded_value: JSONTypes) -> str:
    def _decode(encoded_value: AnyStr) -> JSONTypes:
    def redis(self) -> Redis:  # pragma: no cover
    def key(self) -> str:  # pragma: no cover
    def clear(self: _HasRedisClientAndKey) -> None:
    def redis(self) -> Redis:
    def key(self) -> str:
    def __watch_keys(self, *keys: str) -> Generator[Pipeline, None, None]:
    def __context_managers(self,
                           *others: Any,
                           ) -> Generator[ContextManager[Pipeline], None, None]:
    def _watch(self, *others: Any) -> Generator[Pipeline, None, None]:
    def redis(self) -> Redis:
    def key(self) -> str:
    def _watch(self, *others: Any) -> Generator[Pipeline, None, None]:
    def _same_redis(self, *others: Any) -> bool:
    def __eq__(self, other: Any) -> bool:
    def __iter__(self) -> Generator[JSONTypes, None, None]:
    def _KEY_PREFIX(self) -> str:
    def __init__(self,
                 *,
                 key: str,
                 masters: Iterable[Redis] = frozenset(),
                 raise_on_redis_errors: bool = False,
                 ) -> None:
    def key(self) -> str:
    def key(self, value: str) -> None:
    def _check_enough_masters_up(self,
                                 raise_on_redis_errors: bool | None,
                                 redis_errors: List[RedisError],
                                 ) -> None:
class _Common:
class _Encodable:
class _HasRedisClientAndKey(Protocol):
class _Clearable:
class _Pipelined(abc.ABC):
class _Comparable(abc.ABC):
class Container(_Common, _Encodable, _Clearable, _Pipelined, _Comparable):
class Iterable_(abc.ABC):
class Primitive(abc.ABC):
class AIOPrimitive(Primitive):
    _RANDOM_KEY_PREFIX: ClassVar[str] = 'pottery:'
    _DEFAULT_MASTERS: ClassVar[FrozenSet[Redis]] = frozenset({_default_redis})
    _DEFAULT_MASTERS: ClassVar[FrozenSet[AIORedis]] = frozenset()  # type: ignore
pottery/redlock.py
class Redlock(Scripts, Primitive):
    '''Distributed Redis-powered lock.

    This algorithm safely and reliably provides a mutually-exclusive locking
    primitive to protect a resource shared across threads, processes, and even
    machines, without a single point of failure.

    Rationale and algorithm description:
        http://redis.io/topics/distlock

    Usage:

        >>> from redis import Redis
        >>> redis = Redis()
        >>> printer_lock = Redlock(key='printer', masters={redis}, auto_release_time=.2)
        >>> bool(printer_lock.locked())
        False
        >>> if printer_lock.acquire():
        ...     # Critical section - print stuff here.
        ...     print('printer_lock is locked')
        printer_lock is locked
        >>> printer_lock.release()
        >>> bool(printer_lock.locked())
        False

    Redlocks time out (by default, after 10 seconds).  You should take care to
    ensure that your critical section completes well within the timeout.  The
    reasons that Redlocks time out are to preserve "liveness"
    (http://redis.io/topics/distlock#liveness-arguments) and to avoid deadlocks
    (in the event that a process dies inside a critical section before it
    releases its lock).

        >>> if printer_lock.acquire():
        ...     # Critical section - print stuff here.
        ...     time.sleep(printer_lock.auto_release_time)
        >>> bool(printer_lock.locked())
        False

    If 10 seconds isn't enough to complete executing your critical section,
    then you can specify your own timeout:

        >>> printer_lock = Redlock(key='printer', masters={redis}, auto_release_time=.2)
        >>> if printer_lock.acquire():
        ...     # Critical section - print stuff here.
        ...     time.sleep(printer_lock.auto_release_time / 2)
        >>> bool(printer_lock.locked())
        True
        >>> time.sleep(printer_lock.auto_release_time / 2)
        >>> bool(printer_lock.locked())
        False

    You can use a Redlock as a context manager:

        >>> with Redlock(key='printer', masters={redis}) as printer_lock:
        ...     # Critical section - print stuff here.
        ...     print('printer_lock is locked')
        printer_lock is locked
        >>> bool(printer_lock.locked())
        False

        >>> with printer_lock:
        ...     # Critical section - print stuff here.
        ...     print('printer_lock is locked')
        printer_lock is locked
        >>> bool(printer_lock.locked())
        False
    '''

    __slots__ = (
        'auto_release_time',
        'num_extensions',
        'context_manager_blocking',
        'context_manager_timeout',
        '_uuid',
        '_extension_num',
    )

    _KEY_PREFIX: ClassVar[str] = 'redlock'
    _AUTO_RELEASE_TIME: ClassVar[float] = 10
    _CLOCK_DRIFT_FACTOR: ClassVar[float] = 0.01
    _RETRY_DELAY: ClassVar[float] = .2
    _NUM_EXTENSIONS: ClassVar[int] = 3

    def __init__(self,
                 *,
                 key: str,
                 masters: Iterable[Redis] = frozenset(),
                 raise_on_redis_errors: bool = False,
                 auto_release_time: float = _AUTO_RELEASE_TIME,
                 num_extensions: int = _NUM_EXTENSIONS,
                 context_manager_blocking: bool = True,
                 context_manager_timeout: float = -1,
                 ) -> None:
        '''Initialize a Redlock.

        Keyword arguments:
            key -- a string that identifies your resource
            masters -- the Redis clients used to achieve quorum for this
                Redlock's state
            raise_on_redis_errors -- whether to raise the QuorumIsImplssible
                exception when too many Redis masters throw errors
            auto_release_time -- the timeout in seconds by which to
                automatically release this Redlock, unless it's already been
                released
            num_extensions -- the number of times that this Redlock's lease can
                be extended
            context_manager_blocking -- when using this Redlock as a context
                manager, whether to block when acquiring
            context_manager_timeout -- if context_manager_blocking, how long to
                wait when acquiring before giving up and raising the
                QuorumNotAchieved exception
        '''
        if not context_manager_blocking and context_manager_timeout != -1:
            raise ValueError("can't specify a timeout for a non-blocking call")

        super().__init__(
            key=key,
            masters=masters,
            raise_on_redis_errors=raise_on_redis_errors,
        )
        self.auto_release_time = auto_release_time
        self.num_extensions = num_extensions
        self.context_manager_blocking = context_manager_blocking
        self.context_manager_timeout = context_manager_timeout
        self._uuid = ''
        self._extension_num = 0

    def __acquire_master(self, master: Redis) -> bool:
        acquired = master.set(
            self.key,
            self._uuid,
            px=int(self.auto_release_time * 1000),
            nx=True,
        )
        return bool(acquired)

    def __acquired_master(self, master: Redis) -> int:
        if self._uuid:
            ttl: int = cast(Script, self._acquired_script)(
                keys=(self.key,),
                args=(self._uuid,),
                client=master,
            )
        else:
            ttl = 0
        return ttl

    def __extend_master(self, master: Redis) -> bool:
        auto_release_time_ms = int(self.auto_release_time * 1000)
        extended = cast(Script, self._extend_script)(
            keys=(self.key,),
            args=(self._uuid, auto_release_time_ms),
            client=master,
        )
        return bool(extended)

    def __release_master(self, master: Redis) -> bool:
        released = cast(Script, self._release_script)(
            keys=(self.key,),
            args=(self._uuid,),
            client=master,
        )
        return bool(released)

    def __drift(self) -> float:
        return self.auto_release_time * self._CLOCK_DRIFT_FACTOR + .002

    def _acquire_masters(self,
                         *,
                         raise_on_redis_errors: bool | None = None,
                         ) -> bool:
        self._uuid = str(uuid.uuid4())
        self._extension_num = 0

        with ContextTimer() as timer, BailOutExecutor() as executor:
            futures = set()
            for master in self.masters:
                future = executor.submit(self.__acquire_master, master)
                futures.add(future)

            num_masters_acquired, redis_errors = 0, []
            for future in concurrent.futures.as_completed(futures):
                try:
                    num_masters_acquired += future.result()
                except RedisError as error:
                    redis_errors.append(error)
                    logger.exception(
                        '%s.__acquire_masters() caught %s',
                        self.__class__.__name__,
                        error.__class__.__name__,
                    )
                else:
                    if num_masters_acquired > len(self.masters) // 2:
                        validity_time = self.auto_release_time
                        validity_time -= self.__drift()
                        validity_time -= timer.elapsed() / 1000
                        if validity_time > 0:
                            return True

        with contextlib.suppress(ReleaseUnlockedLock):
            self.__release(raise_on_redis_errors=raise_on_redis_errors)
        self._check_enough_masters_up(raise_on_redis_errors, redis_errors)
        return False

    __acquire_masters = _acquire_masters

    def acquire(self,
                *,
                blocking: bool = True,
                timeout: float = -1,
                raise_on_redis_errors: bool | None = None,
                ) -> bool:
        '''Lock the lock.

        If blocking is True and timeout is -1, then wait for as long as
        necessary to acquire the lock.  Return True.

            >>> from redis import Redis
            >>> redis = Redis()
            >>> printer_lock_1 = Redlock(key='printer', masters={redis}, auto_release_time=.2)
            >>> printer_lock_1.acquire()
            True
            >>> timer = ContextTimer()
            >>> timer.start()
            >>> printer_lock_2 = Redlock(key='printer', masters={redis})
            >>> printer_lock_2.acquire()
            True
            >>> timer.elapsed() > printer_lock_1.auto_release_time * 1000
            True
            >>> printer_lock_2.release()

        If blocking is True and timeout is not -1, then wait for up to timeout
        seconds to acquire the lock.  Return True if the lock was acquired;
        False if it wasn't.

            >>> printer_lock_1.acquire()
            True
            >>> printer_lock_2.acquire(timeout=.5)
            True
            >>> printer_lock_2.release()

            >>> printer_lock_1.acquire()
            True
            >>> printer_lock_2.acquire(timeout=.1)
            False
            >>> with contextlib.suppress(ReleaseUnlockedLock):
            ...     printer_lock_1.release()

        If blocking is False and timeout is -1, then try just once right now to
        acquire the lock.  Return True if the lock was acquired; False if it
        wasn't.

            >>> printer_lock_1.acquire()
            True
            >>> printer_lock_2.acquire(blocking=False)
            False
            >>> printer_lock_1.release()
        '''
        acquire_masters = functools.partial(
            self.__acquire_masters,
            raise_on_redis_errors=raise_on_redis_errors,
        )

        if blocking:
            enqueued = False
            with ContextTimer() as timer:
                while timeout == -1 or timer.elapsed() / 1000 < timeout:
                    if acquire_masters():
                        if enqueued:
                            self.__log_time_enqueued(timer, True)
                        return True
                    enqueued = True
                    delay = random.uniform(0, self._RETRY_DELAY)  # nosec
                    time.sleep(delay)
            if enqueued:  # pragma: no cover
                self.__log_time_enqueued(timer, False)
            return False  # pragma: no cover

        if timeout == -1:
            return acquire_masters()

        raise ValueError("can't specify a timeout for a non-blocking call")

    __acquire = acquire

    def __log_time_enqueued(self, timer: ContextTimer, acquired: bool) -> None:
        key_suffix = self.key.split(':', maxsplit=1)[1]
        time_enqueued = math.ceil(timer.elapsed())
        logger.info(
            'source=pottery sample#redlock.enqueued.%s=%dms sample#redlock.acquired.%s=%d',
            key_suffix,
            time_enqueued,
            key_suffix,
            acquired,
        )

    def locked(self, *, raise_on_redis_errors: bool | None = None) -> float:
        '''How much longer we'll hold the lock (unless we extend or release it).

        If we don't currently hold the lock, then this method returns 0.

            >>> from redis import Redis
            >>> redis = Redis()
            >>> printer_lock_1 = Redlock(key='printer', masters={redis})
            >>> printer_lock_1.locked()
            0

            >>> printer_lock_2 = Redlock(key='printer', masters={redis})
            >>> printer_lock_2.acquire()
            True
            >>> printer_lock_1.locked()
            0
            >>> printer_lock_2.release()

        If we do currently hold the lock, then this method returns the current
        lease's Time To Live (TTL) in ms.

            >>> printer_lock_1.acquire()
            True
            >>> 9 < printer_lock_1.locked() < 10
            True
            >>> printer_lock_1.release()
        '''
        with ContextTimer() as timer, BailOutExecutor() as executor:
            futures = set()
            for master in self.masters:
                future = executor.submit(self.__acquired_master, master)
                futures.add(future)

            ttls, redis_errors = [], []
            for future in concurrent.futures.as_completed(futures):
                try:
                    ttl = future.result() / 1000
                except RedisError as error:
                    redis_errors.append(error)
                    logger.exception(
                        '%s.locked() caught %s',
                        self.__class__.__name__,
                        error.__class__.__name__,
                    )
                else:
                    if ttl:
                        ttls.append(ttl)
                        if len(ttls) > len(self.masters) // 2:  # pragma: no cover
                            validity_time = min(ttls)
                            validity_time -= self.__drift()
                            validity_time -= timer.elapsed() / 1000
                            return max(validity_time, 0)

        self._check_enough_masters_up(raise_on_redis_errors, redis_errors)
        return 0

    def extend(self, *, raise_on_redis_errors: bool | None = None) -> None:
        '''Extend our hold on the lock (if we currently hold it).

        Usage:

            >>> from redis import Redis
            >>> redis = Redis()
            >>> printer_lock = Redlock(key='printer', masters={redis})
            >>> printer_lock.acquire()
            True
            >>> 9 < printer_lock.locked() < 10
            True
            >>> time.sleep(1)
            >>> 8 < printer_lock.locked() < 9
            True
            >>> printer_lock.extend()
            >>> 9 < printer_lock.locked() < 10
            True
            >>> printer_lock.release()
        '''
        if self._extension_num >= self.num_extensions:
            raise TooManyExtensions(self.key, self.masters)

        with BailOutExecutor() as executor:
            futures = set()
            for master in self.masters:
                future = executor.submit(self.__extend_master, master)
                futures.add(future)

            num_masters_extended, redis_errors = 0, []
            for future in concurrent.futures.as_completed(futures):
                try:
                    num_masters_extended += future.result()
                except RedisError as error:
                    redis_errors.append(error)
                    logger.exception(
                        '%s.extend() caught %s',
                        self.__class__.__name__,
                        error.__class__.__name__,
                    )
                else:
                    if num_masters_extended > len(self.masters) // 2:
                        self._extension_num += 1
                        return

        self._check_enough_masters_up(raise_on_redis_errors, redis_errors)
        raise ExtendUnlockedLock(
            self.key,
            self.masters,
            redis_errors=redis_errors,
        )

    def release(self, *, raise_on_redis_errors: bool | None = None) -> None:
        '''Unlock the lock.

        Usage:

            >>> from redis import Redis
            >>> redis = Redis()
            >>> printer_lock = Redlock(key='printer', masters={redis})
            >>> bool(printer_lock.locked())
            False
            >>> printer_lock.acquire()
            True
            >>> bool(printer_lock.locked())
            True
            >>> printer_lock.release()
            >>> bool(printer_lock.locked())
            False
        '''
        with BailOutExecutor() as executor:
            futures = set()
            for master in self.masters:
                future = executor.submit(self.__release_master, master)
                futures.add(future)

            num_masters_released, redis_errors = 0, []
            for future in concurrent.futures.as_completed(futures):
                try:
                    num_masters_released += future.result()
                except RedisError as error:
                    redis_errors.append(error)
                    logger.exception(
                        '%s.release() caught %s',
                        self.__class__.__name__,
                        error.__class__.__name__,
                    )
                else:
                    if num_masters_released > len(self.masters) // 2:
                        return

        self._check_enough_masters_up(raise_on_redis_errors, redis_errors)
        raise ReleaseUnlockedLock(
            self.key,
            self.masters,
            redis_errors=redis_errors,
        )

    __release = release

    def __enter__(self) -> Redlock:
        '''You can use a Redlock as a context manager.

        Usage:

            >>> from redis import Redis
            >>> redis = Redis()
            >>> with Redlock(key='printer', masters={redis}) as printer_lock:
            ...     # Critical section - print stuff here.
            ...     print('printer_lock is locked')
            printer_lock is locked
            >>> bool(printer_lock.locked())
            False

            >>> with printer_lock:
            ...     # Critical section - print stuff here.
            ...     print('printer_lock is locked')
            printer_lock is locked
            >>> bool(printer_lock.locked())
            False
        '''
        acquired = self.__acquire(
            blocking=self.context_manager_blocking,
            timeout=self.context_manager_timeout,
        )
        if acquired:
            return self
        raise QuorumNotAchieved(self.key, self.masters)

    @overload
    def __exit__(self,
                 exc_type: None,
                 exc_value: None,
                 exc_traceback: None,
                 ) -> Literal[False]:
        raise NotImplementedError

    @overload
    def __exit__(self,
                 exc_type: Type[BaseException],
                 exc_value: BaseException,
                 exc_traceback: TracebackType,
                 ) -> Literal[False]:
        raise NotImplementedError

    def __exit__(self,
                 exc_type: Type[BaseException] | None,
                 exc_value: BaseException | None,
                 traceback: TracebackType | None,
                 ) -> Literal[False]:
        '''You can use a Redlock as a context manager.

        Usage:

            >>> from redis import Redis
            >>> redis = Redis()
            >>> with Redlock(key='printer', masters={redis}) as printer_lock:
            ...     # Critical section - print stuff here.
            ...     print('printer_lock is locked')
            printer_lock is locked
            >>> bool(printer_lock.locked())
            False

            >>> with printer_lock:
            ...     # Critical section - print stuff here.
            ...     print('printer_lock is locked')
            printer_lock is locked
            >>> bool(printer_lock.locked())
            False
        '''
        self.__release()
        return False

    def __repr__(self) -> str:
        return f'<{self.__class__.__name__} key={self.key}>'
tests/base.py
class TestCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        logger.setLevel(logging.CRITICAL)
        warnings.filterwarnings('ignore', category=PotteryWarning)

    def setUp(self) -> None:
        super().setUp()

        # Choose a random Redis database for this test.
        self.redis_db = random.randint(1, 15)  # nosec
        self.redis_url = f'redis://localhost:6379/{self.redis_db}'

        # Set up our Redis clients.
        self.redis = Redis.from_url(self.redis_url, socket_timeout=1)
        self.redis_decoded_responses = Redis.from_url(
            self.redis_url,
            socket_timeout=1,
            decode_responses=True,
        )

        # Clean up the Redis database before and after the test.
        self.redis.flushdb()
        self.addCleanup(self.redis.flushdb)
pottery/exceptions.py
class QuorumIsImpossible(PrimitiveError, RuntimeError):
    'Too many Redis masters threw RedisErrors; quorum can not be achieved.'
pottery/redlock.py
def synchronize(*,
                key: str,
                masters: Iterable[Redis] = frozenset(),
                raise_on_redis_errors: bool = False,
                auto_release_time: float = Redlock._AUTO_RELEASE_TIME,
                blocking: bool = True,
                timeout: float = -1,
                ) -> Callable[[F], F]:
    '''Decorator to synchronize a function's execution across threads.

    synchronize() is a decorator that allows only one thread to execute a
    function at a time.  Under the hood, synchronize() uses a Redlock.  See
    help(Redlock) for more details.

    Keyword arguments:
        key -- a string that identifies your resource
        masters -- the Redis clients used to achieve quorum for this Redlock's
            state
        raise_on_redis_errors -- whether to raise the QuorumIsImplssible
            exception when too many Redis masters throw errors
        auto_release_time -- the timeout in seconds by which to automatically
            release this Redlock, unless it's already been released
        num_extensions -- the number of times that this Redlock's lease can be
            extended
        context_manager_blocking -- when using this Redlock as a context
            manager, whether to block when acquiring
        context_manager_timeout -- if context_manager_blocking, how long to wait
            when acquiring before giving up and raising the QuorumNotAchieved
            exception

    Usage:

        >>> @synchronize(key='synchronized-func', auto_release_time=1.5)
        ... def func():
        ...     # Only one thread can execute this function at a time.
        ...     return True
        ...
        >>> func()
        True
    '''
    RedlockFactory = functools.partial(
        Redlock,
        key=key,
        masters=masters,
        raise_on_redis_errors=raise_on_redis_errors,
        auto_release_time=auto_release_time,
        context_manager_blocking=blocking,
        context_manager_timeout=timeout,
    )

    def decorator(func: F) -> F:
        @functools.wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            redlock = RedlockFactory()
            waiting_timer, holding_timer = ContextTimer(), ContextTimer()

            try:
                waiting_timer.start()
                with redlock:
                    waiting_timer.stop()
                    holding_timer.start()
                    return_value = func(*args, **kwargs)
                holding_timer.stop()
            finally:
                _log_synchronize(func, redlock, waiting_timer, holding_timer)

            return return_value
        return cast(F, wrapper)
    return decorator
pottery/exceptions.py
class TooManyExtensions(PrimitiveError, RuntimeError):
    'Redlock has been extended too many times.'


Question: import concurrent.futures
import contextlib
import os
import time
import unittest.mock
from redis import Redis
from redis.commands.core import Script
from redis.exceptions import TimeoutError
from pottery import ContextTimer
from pottery import ExtendUnlockedLock
from pottery import QuorumIsImpossible
from pottery import QuorumNotAchieved
from pottery import Redlock
from pottery import ReleaseUnlockedLock
from pottery import TooManyExtensions
from pottery import synchronize
from pottery.base import logger
from tests.base import TestCase
# --------------------------------------------------------------------------- #
#   test_redlock.py                                                           #
#                                                                             #
#   Copyright © 2015-2022, Rajiv Bakulesh Shah, original author.              #
#                                                                             #
#   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.                                            #
# --------------------------------------------------------------------------- #
'Distributed Redis-powered lock tests.'






class RedlockTests(TestCase):
    'Distributed Redis-powered lock tests.'

    def setUp(self):
        super().setUp()
        self.redlock = Redlock(
            masters={self.redis},
            key='printer',
            auto_release_time=.2,
        )

    def test_acquire_fails_within_auto_release_time(self):
        self.redlock.auto_release_time = .001
        assert not self.redlock._acquire_masters()

    def test_acquire_and_time_out(self):
        assert not self.redis.exists(self.redlock.key)
        assert self.redlock.acquire()
        assert self.redis.exists(self.redlock.key)
        time.sleep(self.redlock.auto_release_time * 2)
        assert not self.redis.exists(self.redlock.key)

    def test_acquire_same_lock_twice_blocking_without_timeout(self):
        assert not self.redis.exists(self.redlock.key)
        with ContextTimer() as timer, \
             unittest.mock.patch.object(logger, 'info') as info:
            assert self.redlock.acquire()
            assert self.redis.exists(self.redlock.key)
            assert self.redlock.acquire()
            assert self.redis.exists(self.redlock.key)
            assert timer.elapsed() / 1000 >= self.redlock.auto_release_time
            assert info.call_count == 1, f'_logger.info() called {info.call_count} times'

    @unittest.skipIf('CI' in os.environ, 'this unit test is flaky on CI')  # pragma: no cover
    def test_acquire_same_lock_twice_blocking_with_timeout(self):
        with unittest.mock.patch.object(logger, 'info') as info:
            assert not self.redis.exists(self.redlock.key)
            assert self.redlock.acquire()
            assert self.redis.exists(self.redlock.key)
            assert not self.redlock.acquire(timeout=0)
            assert not self.redlock.acquire(timeout=0.025)
            assert self.redis.exists(self.redlock.key)
            assert info.call_count == 1, f'_logger.info() called {info.call_count} times'

    def test_acquire_same_lock_twice_non_blocking_without_timeout(self):
        assert not self.redis.exists(self.redlock.key)
        assert self.redlock.acquire()
        assert self.redis.exists(self.redlock.key)
        assert not self.redlock.acquire(blocking=False)
        assert self.redis.exists(self.redlock.key)
        time.sleep(self.redlock.auto_release_time * 2)
        assert not self.redis.exists(self.redlock.key)

    def test_acquire_same_lock_twice_non_blocking_with_timeout(self):
        assert not self.redis.exists(self.redlock.key)
        assert self.redlock.acquire()
        assert self.redis.exists(self.redlock.key)
        with self.assertRaises(ValueError):
            self.redlock.acquire(blocking=False, timeout=0)
        assert self.redis.exists(self.redlock.key)

    def test_acquired(self):
        assert not self.redis.exists(self.redlock.key)
        assert not self.redlock.locked()
        assert self.redlock.acquire()
        assert self.redis.exists(self.redlock.key)
        assert self.redlock.locked()
        time.sleep(self.redlock.auto_release_time * 2)
        assert not self.redis.exists(self.redlock.key)
        assert not self.redlock.locked()

    def test_extend(self):
        assert not self.redis.exists(self.redlock.key)
        with self.assertRaises(ExtendUnlockedLock):
            self.redlock.extend()
        assert self.redlock.acquire()
        for extension_num in range(Redlock._NUM_EXTENSIONS):
            with self.subTest(extension_num=extension_num):
                self.redlock.extend()

Answer:         with self.assertRaises(TooManyExtensions):