ó
    �2¨jyê ã                  ó^  • % S r SSKJr  SSKrSSKrSSKrSSKrSSKrSSKrSSK	r	SSK
r
SSKrSSKrSSKrSSKrSSKrSSKrSSKrSSKrSSKrSSKJr  SSKJrJrJr  \R4                  " \5      rSrSrSrSr       S"S	 jr!\RD                  \RF                  \$4r%S
\&S'    " S S\RD                  5      r' " S S\(5      r)S#S jr*\RV                  \RX                  \RZ                  /\*" S5      Q7r.S
\&S'   S$S jr/\R`                  \1SS/S4   r2\R`                  S/S4   r3 " S S\Rh                  5      r5 " S S\Rh                  5      r6 " S S\7\Rh                  5      r8 " S S\Rr                  5      r: " S S\Rv                  Rx                  5      r< " S  S!\Rz                  S!   5      r>g)%aX  Distributed locking through Redis pubsub instead of expiring keys.

The usual way to build a Redis lock is a key with a time to live: the
holder writes ``SET <name> <token> NX PX <ttl>`` and keeps refreshing it.
When a holder crashes or stops refreshing the key, every other process
waits out the remaining TTL. A lost connection does not establish whether
the holder has stopped running. Shortening the TTL narrows that window
but risks a slow holder losing a lock it still believes it owns.

`RedisLock` keeps the lock in a *subscription* rather than in a key. A
holder subscribes to the lock channel and a background thread keeps
reading from it, so ownership is a property of a live connection. Redis
releases ownership when it removes the subscription, without waiting for
a key to expire. A clean release normally closes the connection promptly,
but detecting a crash or network partition can take time. There is no
lease to refresh, though connection health checks and optional self-checks
still generate traffic. Each lock attempt asks the channel who is there.

Asking is a ping/pong on the channel itself::

    acquire()
      |
      +- subscribe to the channel, naming the connection `client_name`
      +- count subscribers; being the only one means the lock is free
      +- otherwise publish a ping carrying a private response channel,
      |  which every subscriber answers with a record of its holder id
      |  and its current mode (see `RedisLockHolder`)
      +- decide (see `RedisLock._resolve_lock_holders`):
           compatible holders     -> join them, the lock is held
           foreign elected writer -> defer to it, back off and retry
           elected, no readers    -> take the lock exclusively
           anything else          -> unsubscribe and retry
      +- confirm a promotion (writers only, see
         `RedisLock._confirm_exclusive_promotion`): with the new
         exclusive record visible, count and probe once more. A rival
         that promoted on an equally stale view is resolved by holder
         id and the loser demotes

Shared readers hold the lock together, an exclusive writer holds it
alone. There is no coordinator and no lock to take before taking the
lock, so competing writers agree on a single winner by sorting the
pending holder ids they all saw. See `RedisLock._writer_is_elected`.
Subscribers that Redis still counts can stop answering without their
processes having crashed. Contended acquisition attempts can reap these
unresponsive subscriptions by killing their connections.

Redis and the holder can observe a connection failure at different times.
Since 4.2.0 the holder records loss when its subscription worker detects
a connection error or a failed self-check. The subscription lives on a
dedicated connection that never reconnects: a resurrected subscription
would be a silent re-acquisition that skipped the election. Once the
worker records loss, `RedisLock.ensure_held` raises
:exc:`~portalocker.exceptions.LockLostError`, the ``with`` block exit raises
it too if the body finished cleanly, and an ``on_lost`` callback fires on
the reader thread. By default, a loss also requests a main-thread
``KeyboardInterrupt`` (``interrupt_on_lost``, opt-in from 5.0.0 onwards).
That interruption is best-effort. `RedisLock.ensure_held` checks local
recorded state without contacting Redis, so it cannot detect a partition
the worker has not observed. Caveats that follow from this design:

- When ``socket_timeout`` is set, a read exceeding it raises
  ``TimeoutError`` and counts as a loss. Slow links can produce false
  loss reports.
- The zero-reconnect policy is applied on a RESP2 connection because
  RESP3 maintenance notifications carry their own reconnect path that
  bypasses the retry policy. Callers who need RESP3 on the subscription
  must supply ``subscription_connection_factory`` and disable
  maintenance notifications themselves.
- A holder running portalocker 4.1 or older still resubscribes silently
  after a kill, so the loss guarantee only covers channels where every
  participant runs 4.2 or later.
- A half-open connection may deliver neither a TCP reset nor data. Its
  failure can remain undetected until traffic or transport timeouts
  reveal it. With ``health_check_interval=0`` (redis-py's default for a
  connection you supply yourself), there are no periodic health-check
  pings. A nonzero interval creates traffic but does not itself impose
  a deadline for every kind of partition. The opt-in
  ``self_check_interval`` enables an application-level check: the holder
  periodically pings itself through its channel and records loss if
  the echo misses its deadline (see `RedisLockSelfCheckError`).
- After revocation, a new holder can acquire while the old holder still
  runs. Observing loss and stopping application work are separate steps,
  and neither happens atomically with a write to your resource. The
  opt-in ``fencing`` parameter gives every exclusive grant a token
  (`RedisLock.fence_token`). To reject stale writes, the resource itself
  must check that token.

``health_check_interval`` is part of `RedisLock.DEFAULT_REDIS_KWARGS` and
defaults to ten seconds. If you supply a connection directly, that
connection's settings apply instead. Health checks generate periodic
traffic, but Redis and the holder can still detect a failure at different
times.

Example:
    >>> import fakeredis
    >>> import portalocker
    >>> connection = fakeredis.FakeStrictRedis(
    ...     server=fakeredis.FakeServer(), decode_responses=True
    ... )
    >>> lock = portalocker.RedisLock('some_channel', connection=connection)
    >>> with lock:
    ...     print('do something here')
    do something here
é    )ÚannotationsN)Ú
exceptionsé   )Ú	constantsr   Úutilsgš™™™™™¹?g{®Gázt?c                ó<   • U c  U$ [         R                  SU5        U $ )a  Pick the error a multi-step teardown should re-raise.

`RedisLock.release` and `RedisLock._unsubscribe` run every teardown
step even when an earlier one fails, so they can end up holding more
than one error. The first one describes what actually went wrong,
and the later ones are usually the same dead connection hitting the
next step. The first error is therefore kept and any later error is
logged instead of raised, so it cannot replace the original cause.

Args:
    first_error: The error kept so far, or `None` when every earlier
        step succeeded.
    error: The error raised by the step that just failed.

Returns:
    `error` when it is the first failure, otherwise `first_error`
    unchanged, with `error` logged as a suppressed secondary
    failure.
z9Suppressed secondary error while releasing redis lock: %r)ÚloggerÚwarning)Úfirst_errorÚerrors     ÚŸC:\Users\johnn\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.13_qbz5n2kfra8p0\LocalCache\local-packages\Python313\site-packages\portalocker/redis.pyÚ_keep_first_errorr   ¤   s)   € ð. ÑØˆÜ
‡N�NØCØôð Ðó    útuple[type[BaseException], ...]Ú_CONNECTION_LOSS_ERRORSc                  ó   • \ rS rSrSrSrg)ÚRedisLockSelfCheckErroréÑ   a  A held lock could not deliver a message to itself in time.

Raised on the keep-alive worker thread when the periodic self-check
(the opt-in ``self_check_interval`` of `RedisLock`) publishes a ping
to the lock's own channel and this holder's own reply does not come
back through the response-channel machinery within the deadline.

It subclasses ``redis.exceptions.ConnectionError`` deliberately: a
holder that cannot complete its own round trip is, for locking
purposes, disconnected. It can no longer observe revocations and it
cannot answer another prober's ping either, so the next contended
probe would reap its connection anyway. `RedisLock` therefore
classifies a failed self-check as a connection loss and surfaces it
exactly like a socket error: `RedisLock.lost` turns `True`,
`RedisLock.ensure_held` and the ``with`` block exit raise
:exc:`~portalocker.exceptions.LockLostError` with this error as
``__cause__``, ``on_lost`` fires, and ``interrupt_on_lost`` behaves
as it would for a dead socket.

.. versionadded:: 4.2.0
© N©Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__Ú__static_attributes__r   r   r   r   r   Ñ   s   † ôr   r   c                  ó   • \ rS rSrSrSrg)Ú_SelfCheckAbandonedéé   a˜  Internal control flow: the lock left ``HELD`` mid-self-check.

Raised by `RedisLock._await_self_check_frame` when a concurrent
`RedisLock.release` moved the state on while a self-check was still
waiting for a frame, and caught by `RedisLock._self_check_tick`: a
lock that stopped being held has nothing left to verify, so the
check simply stops, declaring neither success nor loss. Never
leaves this module.
r   Nr   r   r   r   r   r   é   s   † ôr   r   c                ó2   • [        [        U S5      nUc  gU4$ )a  Look up a redis-py exception class that may not exist yet.

portalocker supports redis-py 5.0 and newer, while some exception
classes only appeared later, so tables of exception types cannot
always reference them directly. The lookup returns a tuple so a
missing name simply contributes nothing when splatted into such a
table.

Args:
    name: Attribute name to look up on ``redis.exceptions``.

Returns:
    A one-element tuple with the class, or an empty tuple when this
    redis-py release does not define it.
Nr   )ÚgetattrÚredis_exceptions)ÚnameÚerror_classs     r   Ú_optional_redis_errorsr%   õ   s*   € ô  /6ÜØØó/€Kð
 ÑØØˆ>Ðr   ÚExternalAuthProviderErrorÚ_NON_TRANSIENT_SUBSCRIBE_ERRORSc                óŒ   • [        U [        R                  [        R                  45      (       d  g[        U [        5      (       + $ )a•  Report whether ``error`` is a retry-worthy connection blip.

The one classification both halves of an acquire attempt share: a
subscribe failure in `RedisLock._try_subscribe` and a probe failure
in `RedisLock._acquire_attempt` cost one attempt when the error is
connection weather, and are terminal otherwise. Weather means a
``redis_exceptions.ConnectionError`` or ``TimeoutError`` that is not
one of the `_NON_TRANSIENT_SUBSCRIBE_ERRORS`, which repeat
identically on every retry.

Args:
    error: Whatever the attempt raised.

Returns:
    True when the caller should burn one attempt and retry, False
    when the error must propagate after a full release.
F)Ú
isinstancer"   ÚConnectionErrorÚTimeoutErrorr'   )r   s    r   Ú_is_transient_connection_errorr,   .  s?   € ô$ ØÜ	×	)Ñ	)Ô+;×+HÑ+HÐI÷ñ ð Ü˜%Ô!@ÓAÔAÐAr   úredis.client.PubSubúredis.client.PubSubWorkerThreadc                  ó(   • \ rS rSrSrSrSrSrSrSr	g)	Ú
_LockStatei]  aé  Lifecycle of one `RedisLock` instance.

The state answers one question for the failure paths: when the
keep-alive worker dies, was this lock merely trying to acquire
(`ACQUIRING`, the failure costs one attempt) or did it own the lock
(`HELD`, the failure is a loss the application must hear about)?
`RedisLock._on_worker_exception` makes that call on the worker
thread while `RedisLock.acquire` transitions on the calling thread,
so every transition and read runs under ``RedisLock._state_lock``.

`LOST` is deliberately sticky: `RedisLock.release` keeps it (and the
causal error) so the loss stays observable through
`RedisLock.lost` and the ``with`` block exit after the teardown ran.
Only the next `RedisLock.acquire` resets a lost instance.
ÚidleÚ	acquiringÚheldÚlostr   N)
r   r   r   r   r   ÚIDLEÚ	ACQUIRINGÚHELDÚLOSTr   r   r   r   r0   r0   ]  s!   † ñð" €Dð €Ià€Dð ƒDr   r0   c                  ó$   • \ rS rSrSrSrSrSrSrg)Ú_ConfirmVerdictiz  ak  Outcome of one confirm-probe round after an exclusive promotion.

Produced by `RedisLock._confirm_probe_verdict` and consumed by
`RedisLock._confirm_exclusive_promotion`: one clean round confirms
the promotion, an outranking holder demotes it, and anything
undecided makes the round inconclusive so the confirm asks again
instead of concluding from noise (#145).
Ú	confirmedÚretryÚdemoter   N)	r   r   r   r   r   Ú	CONFIRMEDÚRETRYÚDEMOTEr   r   r   r   r:   r:   z  s   † ñð €Ið €EàƒFr   r:   c                  ó$   • \ rS rSrSrSrSrSrSrg)ÚRedisLockModei�  aø  What a participant claims to be doing with the lock.

The member values are the strings that travel in the ``mode`` field of
a holder record, which is why this is a `str` subclass: a member
compares equal to its own wire value.

`PENDING` is the member that makes the protocol work. An exclusive
waiter announces itself on the channel before it owns anything, which
is what lets competing writers elect a single winner among themselves
and what stops an endless stream of readers from starving a writer.
Ú	exclusiveÚpendingÚsharedr   N)	r   r   r   r   r   Ú	EXCLUSIVEÚPENDINGÚSHAREDr   r   r   r   rB   rB   �  s   † ñ
ð €Ið €Gð ƒFr   rB   c                  óJ   • \ rS rSr% SrS\S'   S\S'   SrS\S	'   S
rS\S'   Srg
)ÚRedisLockHolderi¤  a¯  One participant's answer to a liveness ping.

A probe collects one of these per subscriber on the lock channel and
the whole acquisition decision is made from the resulting list.

Example:
    >>> from portalocker import redis
    >>> holder = redis.RedisLockHolder('a1b2', redis.RedisLockMode.SHARED)
    >>> holder.holder_id, holder.mode.value, holder.legacy
    ('a1b2', 'shared', False)
    >>> holder.elected is None
    True
ÚstrÚ	holder_idrB   ÚmodeFÚboolÚlegacyNúbool | NoneÚelectedr   )	r   r   r   r   r   Ú__annotations__rO   rQ   r   r   r   r   rJ   rJ   ¤  s,   ‡ ñð  ƒNà
Óð €FˆDÓð  €Gˆ[Ör   rJ   c                  ó�   ^ • \ rS rSr% SrS\S'   S\S'      S           SU 4S jjjrSS jrSS	 jr      SS
 jr	Sr
U =r$ )ÚPubSubWorkerThreadiÉ  a	  redis-py's pubsub reader thread, with failures routed to the lock.

The subscription this thread services *is* the lock. While it runs,
the holder answers liveness pings and Redis keeps counting it as a
subscriber. If the thread were to die quietly - a dropped connection,
a protocol error - the owning process would carry on believing it
still holds a lock that every other process now considers released,
which is exactly the split-brain this lock exists to avoid.

redis-py's read loop already catches `BaseException` and hands it to
the ``exception_handler`` passed at construction, which is
`RedisLock._on_worker_exception` here. That handler decides whether
the failure is a loss (the lock was held) or a failed attempt (the
lock was still being acquired). This subclass adds two things.

It owns the read loop instead of inheriting it, because redis-py's
loop sets its running flag from *inside* the thread and ``stop()``
clears that same flag: a ``stop()`` issued after ``start()`` but
before the new thread reached ``run()`` was overwritten by the
thread's own set, the loop then ran forever, and the ``join()`` in
`RedisLock._unsubscribe` hung with it. `RedisLock.acquire` tears a
fresh subscription down microseconds after starting it whenever a
confirm is refused, an election is lost or ``fail_when_locked``
gives up, so a CPU-starved worker thread made that ordering real.
Here `stop` records the request in an event that `run` consults
before every read, so a stop can never be lost, however early it
lands. redis-py's own ``_running`` flag is left unused.

It also adds a last-ditch layer: an exception escaping ``run``
itself - a failure inside the handler, or inside the
``pubsub.close()`` that runs after the loop - is routed into the
same handler instead of dying with the thread.
zthreading.EventÚ_stop_requestedú_WorkerTick | NoneÚ_tickc           	     ó�   >• [         TU ]  UUU[        R                  " SU5      S9  [        R
                  " 5       U l        XPl        g)a´  Create the reader thread without starting it.

Args:
    pubsub: The subscribed pubsub to read from.
    sleep_time: Seconds each ``get_message`` waits for a frame,
        which is also how long a `stop` takes to be noticed.
    daemon: Whether the thread may be abandoned at interpreter
        exit. `RedisLock` always passes `True`.
    exception_handler: Receives everything the read loop raises.
        Without one, errors propagate and end the thread.
    tick: Optional hook run once per loop iteration, after the
        read, with the pubsub as its argument. `RedisLock` uses
        it for the opt-in self-check, and `None` (the default)
        keeps the loop a plain read loop.
ú
typing.Any)ÚdaemonÚexception_handlerN)ÚsuperÚ__init__ÚtypingÚcastÚ	threadingÚEventrU   rW   )ÚselfÚpubsubÚ
sleep_timerZ   r[   ÚtickÚ	__class__s         €r   r]   ÚPubSubWorkerThread.__init__ô  sE   ø€ ô4 	‰ÑØØØÜ$Ÿkšk¨,Ð8IÓJð	 	ñ 	
ô  )ŸšÓ0ˆÔØ�
r   c                ó8   • U R                   R                  5         g)zëAsk the read loop to end after its current read.

Safe to call at any point in the thread's life, including before
`run` started and after it ended. The loop closes the pubsub on
its way out, which disconnects the subscription socket.
N)rU   Úset©rb   s    r   ÚstopÚPubSubWorkerThread.stop  s   € ð 	×Ñ× Ñ Õ"r   c                óô   • [         R                  " SU R                  5      n[         R                  " SU R                  5      n U R	                  X5        g! [
         a  nUc  e U" X1U 5         SnAgSnAff = f)a<  Read from the subscription, routing every failure to the lock.

Raises:
    BaseException: Whatever escaped the underlying reader, only
        when no ``exception_handler`` was registered. With a
        handler registered nothing propagates: the handler is
        the escalation path and this thread simply ends.
r-   ú_WorkerExceptionHandler | NoneN)r^   r_   rc   r[   Ú_read_until_stoppedÚBaseException©rb   rc   Úhandlerr   s       r   ÚrunÚPubSubWorkerThread.run   sp   € ô '-§k¢kØ!Ø�K‰Kó'
ˆô 39·+²+Ø,Ø×"Ñ"ó3
ˆð	)Ø×$Ñ$ VÕ5øÜó 	)Ø‰ØÙ�E 4×(Ñ(ûð	)ús   ÁA Á
A7Á A2Á2A7c                ó^  • U R                   R                  5       (       dZ   UR                  SU R                  S9  U R                  b  U R	                  U5        U R                   R                  5       (       d  MZ  UR                  5         g! [
         a  nUc  e U" X1U 5         SnANNSnAff = f)af  Poll the subscription until `stop` was called, then close it.

The loop body is redis-py's: one ``get_message`` per iteration,
bounded by ``sleep_time``, with every failure handed to the
handler (the handler stops the thread, so a dead socket ends the
loop through it). Two things differ: the loop condition (see the
class docstring), and an optional ``tick`` hook run after every
read inside the same protection, so a failing tick escalates
exactly like a failing read.

Args:
    pubsub: The subscribed pubsub to read from.
    handler: The failure handler, or `None` to let failures
        propagate to `run`.
T©Úignore_subscribe_messagesÚtimeoutN)rU   Úis_setÚget_messagerd   rW   rp   Úcloserq   s       r   ro   Ú&PubSubWorkerThread._read_until_stopped;  sœ   € ð( ×&Ñ&×-Ñ-×/Ñ/ð
-Ø×"Ñ"Ø.2Ø ŸO™Oð #ñ ð —:‘:Ñ)Ø—J‘J˜vÔ&ð ×&Ñ&×-Ñ-×/Ó/ð 	�‰�øô	 !ó -Ø‘?ØÙ˜ t×,Ñ,ûð-ús   ¡8B Â
B,ÂB'Â'B,)rU   rW   )FNN)rc   r-   rd   ÚfloatrZ   rN   r[   rn   re   rV   ÚreturnÚNone©r~   r   )rc   r-   rr   rn   r~   r   )r   r   r   r   r   rR   r]   rk   rs   ro   r   Ú__classcell__©rf   s   @r   rT   rT   É  s™   ø‡ ñ ðJ %Ó$ð Óð Ø<@Ø#'ð!à#ð!ð ð!ð ð	!ð
 :ð!ð !ð!ð 
÷!ð !ôF#ô)ð6 à#ð ð 0ð ð 
÷	 ò  r   rT   c                  óF  ^ • \ rS rSr% SrS\S'   S\S'   S\S'   S	\S
'   S\S'   SrS\S'   S\S'   S\S'   S\S'   S\S'   S\S'   S\S'   S\S'   S\S'   S\S'   S\S'   S\S '   S\S!'   S"\S#'   S$\S%'   S&\S''   S	\S('   S"\S)'   S*\S+'   \" S,S-S.9rS/\S0'   SSSS1\	\
S\R                  R                  SSSSS14                             ShU 4S2 jjjrSiS3 jr    SjS4 jr  SkU 4S5 jjrSlS6 jrSmS7 jr\SnS8 j5       r\SnS9 j5       r      SoS: jr    SpS; jr    SqS< jr    SrS= jr        SsS> jrStS? jrSuS@ jrSuSA jr            SvSB jrSwSC jr SxSD jr!SxSE jr"StSF jr#\SxSG j5       r$StSH jr%SySI jr&        SzSJ jr'      S{SK jr(      S|SL jr)          S}SM jr* S~         SSN jjr+    S€SO jr,    S€SP jr-    S€SQ jr.      S�SR jr/      S‚SS jr0    SƒST jr1      S„SU jr2S…SV jr3   S†       S‡SW jjr4      S„SX jr5      S„SY jr6SˆSZ jr7StS[ jr8SxS\ jr9\SnS] j5       r:\S‰S^ j5       r;SqS_ jr< SŠ   S‹S` jjr=      SŒSa jr>SxSb jr?StSc jr@ S~     S�Sd jjrAStSe jrBStSf jrCSgrDU =rE$ )ŽÚ	RedisLocki^  až   A Redis lock held by a pub/sub subscription.

A keep-alive thread maintains the subscription. Ownership ends when
Redis removes it, without waiting for a lease to expire. Detecting a
crashed process or broken connection can still take time, especially
during a network partition.

The holder observes loss separately, when its keep-alive thread
detects the failed connection. `lost` turns True, `ensure_held` and
the ``with``
block exit raise :exc:`~portalocker.exceptions.LockLostError`, an
optional `on_lost` callback fires, and (by default in 4.2, opt-in
from 5.0.0) the main thread is interrupted. The subscription lives
on a dedicated connection that never retries or reconnects, because
a transparently resurrected subscription would be a silent
re-acquisition. A read exceeding the configured ``socket_timeout``
also counts as a loss.

A note on ``os.fork``: a forked child inherits the lock object and
the parent's sockets. The child's `release` (or garbage collection
of its copy) only drops the child's local references; the network
teardown is skipped in any process other than the one that
subscribed, because an UNSUBSCRIBE over the inherited socket would
silently revoke the *parent's* lock without the parent ever being
told. A child that needs the lock must construct its own instance.

Set `health_check_interval` when creating the Redis connection to help
detect failures on idle connections. Health checks and socket timeouts
do not provide a fixed detection bound for every network failure.

Mixing versions on one channel has a known limitation: portalocker
3.2.0 and older holders all share one connection name, so one live
plus one crashed legacy holder cannot be told apart and the channel
stays blocked until the crashed holder's TCP connection dies on its
own (see `legacy_client_name`).

The lock requires a single standalone Redis endpoint. Every
acquisition decision starts from ``PUBSUB NUMSUB``, which is
node-local in Redis Cluster and replica setups while message delivery
is cluster-wide, so two writers subscribed through different nodes
would each count one subscriber and both take the uncontended fast
path. Point every participant at the same standalone server.

Args:
    channel: the redis channel to use as locking key.
    connection: an optional redis connection if you already have one
        or if you need to specify the redis connection. A connection
        given here is never closed by the lock; one created by the
        lock itself is closed on release.
    timeout: timeout when trying to acquire a lock
    check_interval: check interval while waiting
    fail_when_locked: Raise :exc:`~portalocker.exceptions.AlreadyLocked`
        when the first acquisition attempt is blocked, without
        retrying until the timeout.
    thread_sleep_time: sleep time between fetching messages from redis to
        prevent a busy/wait loop. In the case of lock conflicts this
        increases the time it takes to resolve the conflict. This should
        be smaller than the `check_interval` to be useful.
    unavailable_timeout: How long a probe waits for a conflicting
        holder to reply. Probing can extend the total acquisition
        time beyond `timeout`.
    redis_kwargs: The redis connection arguments if no connection is
        given. The `DEFAULT_REDIS_KWARGS` are used as default, if you want
        to override these you need to explicitly specify a value (e.g.
        `health_check_interval=0`)
    flags: `LockFlags.EXCLUSIVE` (the default) or `LockFlags.SHARED`.
        Shared holders may coexist, while an exclusive holder waits for
        all shared holders to release. Other flag combinations are
        rejected; use `fail_when_locked` for non-blocking acquisition.
    on_lost: Callback invoked exactly once when a held lock is lost,
        with this lock as its only argument. It runs on the
        keep-alive worker thread, so keep it short, do not block in
        it, and do not take application locks inside it; anything it
        raises is caught and logged rather than propagated.
        Calling `release` on the lost lock inside the callback is
        allowed: the teardown skips joining the worker thread it is
        running on, and that thread exits on its own right after
        the callback returns. The
        loss is recorded first and the callback runs afterwards, so
        `lost` can already be `True` while the callback has not run
        yet: code that needs the callback to have completed must
        wait on the callback, not on `lost`. `None` (the default)
        disables the callback.
    interrupt_on_lost: Whether losing a held lock also interrupts
        the main thread with a `KeyboardInterrupt`. The default
        (`None`) currently behaves as `True` and emits a
        `DeprecationWarning` at the moment a loss actually triggers
        the interrupt: portalocker 5.0.0 flips the default to
        `False`, surfacing losses only through
        :exc:`~portalocker.exceptions.LockLostError`, `ensure_held`,
        `lost`, the ``with`` block exit and `on_lost`. Pass an
        explicit `True` or `False` to opt out of the warning.
        Delivery of the interrupt is best effort either way: it is
        a no-op under a custom ``SIGINT`` disposition, deferred
        while the main thread blocks in a C call, and catchable as
        an ordinary `KeyboardInterrupt`.
    subscription_connection_factory: Escape hatch for connection
        setups the built-in derivation cannot reproduce (Sentinel,
        cluster, custom pools). When given, every subscription
        attempt calls it for a fresh client instead of deriving one
        from the command connection, and the lock closes that
        client again when the attempt ends. The returned client
        must yield connections that do not retry or reconnect, or
        the loss guarantee above silently disappears.
    self_check_interval: Seconds between opt-in end-to-end
        self-checks of a held lock, or `None` (the default) for
        none. Socket-level loss detection cannot see a half-open
        link - a partition that never delivers a TCP reset - so
        when set, the holder periodically publishes a liveness ping
        to its own channel and requires its own reply back through
        the response-channel machinery within
        ``min(self_check_interval, unavailable_timeout)`` seconds.
        A failed check is a loss, classified and surfaced exactly
        like a socket error (`RedisLockSelfCheckError` as the
        ``__cause__``). Costs one publish, one short-lived
        response subscription and one channel-wide reply round per
        interval per holder, which is why it is opt-in. Note the
        check verifies the whole loop through the command
        connection too, so a holder whose command path is down
        also loses the lock - deliberately, since such a holder
        cannot answer other probers either.
    fencing: When `True`, every exclusive grant draws a
        monotonically increasing fencing token (``INCR`` on
        `fence_key`, stored in `fence_token`) that resources able
        to check fences can use to reject writes from a stale
        holder. Off by default because it reintroduces key state:
        the counter key never expires, by design, since
        monotonicity must survive idle periods. Shared grants
        never draw a token, and only grants from locks with
        fencing enabled bump the counter, so the ordering
        guarantee covers exactly the exclusive grants of
        fencing-enabled 4.2+ writers on the channel.

Example:
    Two readers can hold the same channel at the same time, while a
    writer would have to wait for both of them:

    >>> import fakeredis
    >>> import portalocker
    >>> connection = fakeredis.FakeStrictRedis(
    ...     server=fakeredis.FakeServer(), decode_responses=True
    ... )
    >>> reader = portalocker.RedisLock(
    ...     'shared_channel',
    ...     connection=connection,
    ...     flags=portalocker.LockFlags.SHARED,
    ... )
    >>> other_reader = portalocker.RedisLock(
    ...     'shared_channel',
    ...     connection=connection,
    ...     flags=portalocker.LockFlags.SHARED,
    ... )
    >>> with reader, other_reader:
    ...     print('both readers are in')
    both readers are in
údict[str, typing.Any]Úredis_kwargszPubSubWorkerThread | NoneÚthreadrK   Úchannelr}   rx   úredis.client.Redis | NoneÚ
connectionNzredis.client.PubSub | Nonerc   rN   Úclose_connectionúconstants.LockFlagsÚflagsrL   rB   rM   Úwriter_electedú)typing.Callable[[RedisLock], None] | NoneÚon_lostÚinterrupt_on_lostú.typing.Callable[[], redis.client.Redis] | NoneÚsubscription_connection_factoryúfloat | NoneÚself_check_intervalÚfencingÚ_interrupt_on_lost_setÚ_subscription_clientú
int | NoneÚ_subscription_pidr0   Ú_lock_stateúBaseException | NoneÚ_lost_errorÚ_next_self_checkÚ_fence_tokenzthreading.LockÚ
_mode_locké
   T)Úhealth_check_intervalÚdecode_responsesz&typing.ClassVar[dict[str, typing.Any]]ÚDEFAULT_REDIS_KWARGSFc                óÆ  >• U(       + U l         SU l        Xl        X l        X`l        Xpl        U=(       d
    [        5       U l        U	[        R                  R                  [        R                  R                  4;  a  [        S5      eX�l        Ub  US::  a  [        SU< 35      eXÐl        Xàl        [         R"                  " 5       R$                  U l        SU l        X l        Uc  SOUU l        USLU l        XÀl        SU l        SU l        [6        R8                  U l        SU l        SU l        SU l         [B        RD                  " 5       U l#        U	[        R                  R                  :X  a  [H        R                  O[H        RJ                  U l&        U RN                  RQ                  5        H"  u  nnU R                  RS                  UU5        M$     [T        TU ]­  UUUS9  g)	a  Configure the lock without touching Redis.

Nothing is connected, subscribed or published here. The
constructor only records the configuration, allocates the
`holder_id` that identifies this instance on the wire, and picks
the starting `mode`: `RedisLockMode.SHARED` for a reader, or
`RedisLockMode.PENDING` for a writer, which only becomes
`RedisLockMode.EXCLUSIVE` once `acquire` succeeds. The first
contact with Redis happens in `acquire`.

Every instance gets its own random `holder_id`, so two locks in
the same process are two independent holders and neither can
satisfy the other's lock.

See the class docstring for the argument reference.

Raises:
    ValueError: `flags` was neither exactly
        `constants.LockFlags.EXCLUSIVE` nor exactly
        `constants.LockFlags.SHARED`. Combinations such as
        ``EXCLUSIVE | NON_BLOCKING`` have no meaning here; use
        `fail_when_locked` for non-blocking behaviour. Also
        raised for a ``self_check_interval`` that is neither
        `None` nor positive.
NzSRedisLock flags must contain exactly one of LockFlags.EXCLUSIVE or LockFlags.SHAREDr   z2self_check_interval must be positive or None, got FTç        )rx   Úcheck_intervalÚfail_when_locked),r‹   r‡   rˆ   rŠ   Úthread_sleep_timeÚunavailable_timeoutÚdictr†   r   Ú	LockFlagsrF   rH   Ú
ValueErrorr�   r•   r–   ÚuuidÚuuid4ÚhexrL   rŽ   r�   r‘   r—   r“   r˜   rš   r0   r5   r›   r�   rž   rŸ   r`   ÚLockr    rB   rG   rM   r¤   ÚitemsÚ
setdefaultr\   r]   )rb   rˆ   rŠ   rx   r§   r¨   r©   rª   r†   r�   r�   r‘   r“   r•   r–   ÚkeyÚvaluerf   s                    €r   r]   ÚRedisLock.__init__U  sË  ø€ ðZ %/¤ˆÔàˆŒØŒØ$ŒØ!2ÔØ#6Ô Ø(×2¬D«FˆÔØÜ×Ñ×)Ñ)Ü×Ñ×&Ñ&ð
ó 
ô ð:óð ð Œ
ØÑ*Ð/BÀaÓ/GÜØDØ&Ñ)ð+óð ð $7Ô ØŒÜŸš›×)Ñ)ˆŒØ#ˆÔØŒð &Ñ-‰DÐ3Dð 	Ôð '8¸tÐ&CˆÔ#Ø/NÔ,Ø$(ˆÔ!Ø!%ˆÔô &Ÿ?™?ˆÔØˆÔØ #ˆÔØ ˆÔô
 $Ÿ.š.Ó*ˆŒð œ	×+Ñ+×2Ñ2Ó2ô × Ò ä×&Ñ&ð 	Œ	ð ×3Ñ3×9Ñ9Ö;‰JˆC�Ø×Ñ×(Ñ(¨¨eÖ4ñ <ô 	‰ÑØØ)Ø-ð 	ò 	
r   c                óš   • U R                   (       d/  [        R                  R                  " S0 U R                  D6U l         U R                   $ )a¶  Return the Redis connection, creating one on first use.

A connection handed to the constructor is returned unchanged and
is never closed by this class; the caller owns it. A connection
created here is owned by the lock, is built from `redis_kwargs`
(with `DEFAULT_REDIS_KWARGS` filled in), and is closed by
`release`, after which the next call creates a fresh one.

Returns:
    The connection every command from this lock is issued on.
r   )rŠ   ÚredisÚclientÚRedisr†   rj   s    r   Úget_connectionÚRedisLock.get_connectionÃ  s4   € ð ��Ü#Ÿl™l×0Ò0ÑE°4×3DÑ3DÑEˆDŒOà�‰Ðr   c                óL   • [         R                  " SUR                  5       5      $ )z;Typed wrapper, `Redis.pubsub()` is unannotated in redis-py.r-   )r^   r_   rc   ©rb   rŠ   s     r   Ú_get_pubsubÚRedisLock._get_pubsubÔ  s$   € ô
 �{Š{Ø!Ø×ÑÓó
ð 	
r   c                ó>   >• / [         TU ]  5       QU R                  P7$ )aT  Report the Python locks a forked child must reinitialize.

Adds `_mode_lock` to the base class's state lock: the worker
thread takes it for every ping snapshot and `acquire` for every
promotion, so a fork landing inside either scope hands the
child a lock owned by a thread that does not exist there, and
the child's `release` (or garbage collection) would block on it
forever. The after-fork hook resets each lock separately and
never acquires them, so the no-path-holds-both-locks invariant
between the state lock and the mode lock is untouched.

Returns:
    The state lock and the mode lock.
)r\   Ú_fork_reinit_locksr    )rb   rf   s    €r   rÂ   ÚRedisLock._fork_reinit_locksÞ  s"   ø€ ð" @”‘Ñ+Ó-Ð?¨t¯©Ñ?Ð?r   c                óD   • UR                  U R                  5      S   S   $ )z)Get the subscriber count for our channel.r   r   )Úpubsub_numsubrˆ   r¾   s     r   Ú_get_subscriber_countÚRedisLock._get_subscriber_countñ  s!   € à×'Ñ'¨¯©Ó5°aÑ8¸Ñ;Ð;r   c           
     ó  • UR                  S5      S:w  a  gUR                  S5      nU(       d  g [        R                  " U5      n[        U[        5      (       d  g[        R                  " SU5      nUR                  S5      n[        U[        5      (       a  U(       d  gU R                  nUc!  [
        R                  SU R                  5        gU R                     U R                  nU R                   nU R"                  n	SSS5        UR%                  U[        R&                  " WWR(                  [*        W	S	.5      5        g! [        R                  [        4 a    [
        R                  SU5         gf = f! , (       d  f       N~= f)
a˜  Answer a liveness ping with this holder's record.

Registered as the subscription callback in `_start_subscription`,
so it runs on the `PubSubWorkerThread` for every message
published to `channel`. Anything that is not a JSON object with a
non-empty ``response_channel`` string is ignored, which keeps
unrelated traffic on the channel harmless.

The reply is published on the private response channel the prober
asked for and carries `holder_id`, the *current* `mode`,
`REDIS_LOCK_PROTOCOL_VERSION` and an ``elected`` boolean
mirroring `writer_elected`. Answering with the live mode rather
than a stored one is what makes the protocol truthful: a writer
that is still `RedisLockMode.PENDING` says so, and a probe
therefore learns the state as it was at the moment it asked. The
``elected`` field is how an incumbent stays visible while it
waits for readers to drain: later writers defer to an advertised
election instead of rerunning the id sort against it (see
`_writer_is_elected`). Records still carry protocol version 1,
so portalocker 4.0 and 4.1 parse them unchanged and simply
ignore the extra key.

The ``(holder_id, mode, elected)`` triple is snapshotted under
`_mode_lock`, the lock every promotion takes, and published
outside it so no network I/O runs under the lock. That
serializes each answer against promotions: an answer either
completes its snapshot before a promotion starts or observes the
promoted state, and it can never read `mode` halfway through a
transition. An answer snapshotted just before a promotion still
truthfully reports the older mode. The prober's own count checks
are what invalidate a decision built on such an answer.

A probing lock is subscribed to its own channel, so it answers
its own ping and appears in its own holder list.

Args:
    message: A redis-py pubsub message. Only messages of type
        ``message`` are answered; subscribe confirmations and
        other control frames are dropped.
ÚtypeÚmessageNÚdatazInvalid Redis lock message: %rr…   Úresponse_channelz6Redis lock %s cannot answer ping: connection is closed)rL   rM   ÚprotocolrQ   )ÚgetÚjsonÚloadsÚJSONDecodeErrorÚ	TypeErrorr	   Údebugr)   r«   r^   r_   rK   rŠ   r   rL   r    rM   rŽ   ÚpublishÚdumpsrµ   ÚREDIS_LOCK_PROTOCOL_VERSION)
rb   rÊ   Úraw_datarË   Ú	data_dictrÌ   rŠ   rL   rM   rQ   s
             r   Úchannel_handlerÚRedisLock.channel_handlerõ  sL  € ðR �;‰;�vÓ )Ó+Øà&Ÿ{™{¨6Ó2ˆÞØð	Ü#Ÿzšz¨(Ó3ˆDô ˜$¤×%Ñ%ØÜ+1¯;ª;Ø#Øó,
ˆ	ð (1§}¡}Ð5GÓ'HÐÜÐ*¬C×0Ñ0Ö8HØà04·±ˆ
ØÑô �L‰LØHØ—‘ôð Ø�_‹_Ø!Ÿ^™^ˆIØ"&§)¡)ˆDØ ×/Ñ/ˆG÷ ð 	×ÑØÜ�JŠJà!*Ø ŸJ™JÜ ;Ø&ñ	óõ
	
øôE ×$Ñ$¤iÐ0ó 	Ü�L‰LÐ9¸7ÔCÙð	ú÷< �_ús   ±E Ã%E;Å0E8Å7E8Å;
F	c                ó8   • U R                    SU R                   3$ )a   Name given to this holder's subscriber connection.

`_make_subscription_client` sets this as the connection-level
``client_name`` of the dedicated subscription client, so the
name is part of the handshake of the connection that actually
holds the subscription and shows up against it in ``CLIENT
LIST``. `_kill_unavailable_locks` reads it back the other way
around: a listed connection whose name carries a `holder_id`
that did not answer the last ping belongs to a crashed holder,
and killing it releases the lock.

Returns:
    `legacy_client_name` with this instance's `holder_id`
    appended.

Example:
    >>> from portalocker import redis
    >>> lock = redis.RedisLock('some_channel')
    >>> lock.client_name == f'some_channel-lock-{lock.holder_id}'
    True
Ú-)Úlegacy_client_namerL   rj   s    r   Úclient_nameÚRedisLock.client_nameU  s!   € ð. ×)Ñ)Ð*¨!¨D¯N©NÐ+;Ð<Ð<r   c                ó    • U R                    S3$ )a°  Connection name used by portalocker 3.2.0 and older.

Up to and including portalocker 3.2.0 every holder on a channel
named its connection ``<channel>-lock``, with no per-holder
suffix, and answered a ping with a bare timestamp string instead
of a record. Shared locks need holders to be individually
identifiable, so 4.0.0 appended `holder_id` to the name (see
`client_name`) and replaced the timestamp with a JSON record.

The old name is still recognised so that a 4.0.0 holder sharing a
channel with an older one stays correct: an old reply is recorded
as a single legacy `RedisLockMode.EXCLUSIVE` holder, which blocks
readers and writers alike, and an old connection that stops
answering is still reaped by name.

One legacy mix stays unresolvable, because legacy holders are
indistinguishable by name: with one live and one crashed 3.2.0
holder on the same channel, the live reply spares every legacy
connection from reaping, so waiters stay blocked until the
crashed holder's TCP connection dies on its own.

Returns:
    The unsuffixed ``<channel>-lock`` name.

Example:
    >>> from portalocker import redis
    >>> redis.RedisLock('some_channel').legacy_client_name
    'some_channel-lock'
z-lock©rˆ   rj   s    r   rÝ   ÚRedisLock.legacy_client_namen  s   € ð> —,‘,�˜uÐ%Ð%r   c              #  óP  #   • Uc  SnUc  U R                   nUS:”  a  UOU R                   n[        R                  " 5       U-   nSv •   U[        R                  " 5       -
  nUS::  a  g[        R                  " [	        US[
        R
                  " 5       -   -  U5      5        Sv •  M^  7f)aƒ  Yield once per attempt until the deadline passes.

Overrides `utils.LockBase._timeout_generator` because a Redis
retry loop has different needs from a filesystem one:

- The first yield is immediate, so an uncontended acquire makes
  its first attempt without waiting out an interval. The poll
  loops driven by this generator do not busy-spin as a result:
  their ``get_message(timeout=...)`` calls already block for
  the check interval when no message is waiting.
- Every interval between attempts is scaled by a random factor
  in ``[0.5, 1.5)``. Contenders that started together would
  otherwise retry in lockstep and keep colliding round after
  round; the jitter spreads them out so one of them gets a
  clean probe.
- The deadline is taken from `time.monotonic`, so adjusting the
  system clock mid-wait cannot stretch or cut short a timeout.

Like the base class it always yields at least once, even with a
zero or negative timeout, so ``fail_when_locked`` still makes one
real attempt rather than failing without asking.

Args:
    timeout: Seconds to keep yielding for. `None` means zero,
        which still yields exactly once.
    check_interval: Base seconds to sleep between attempts.
        `None` or a non-positive value falls back to
        `thread_sleep_time`.

Yields:
    A constant ``0``. Unlike the base class this is not an
    attempt counter; callers iterate for the timing alone.
Nr¦   r   g      à?)r©   ÚtimeÚ	monotonicÚsleepÚminÚrandom)rb   rx   r§   Úeffective_intervalÚdeadlineÚ	remainings         r   Ú_timeout_generatorÚRedisLock._timeout_generator�  s£   é € ðH ‰?ØˆGØÑ!Ø!×3Ñ3ˆNà,¨qÓ0‰N°d×6LÑ6Lð 	ô Ÿ.š.Ó*¨WÑ4ˆØŠØØ'¬$¯.ª.Ó*:Ñ:ˆIØ˜A‹~ØÜ�JŠJÜÐ&¨#´·²³Ñ*?Ñ@À)ÓLôð
 ŠGñ ùs   ‚B$B&c           	     óô  • U R                   b  U R                  5       $  UR                  n[        R                  " SUR	                  5       5      nUR                  5        VVs0 s H  u  pESU;  d  M  US:w  d  M  XE_M     nnnUR                  [        R                  R                  [        R                  R                  5       SSS9/ SU R                  S	S
S9  [        U5      " SSUR                  0UD6n[        R*                  R-                  US9$ s  snnf ! [        [         4 aL  n["        R$                  " ["        R$                  R&                  S[        U5      R(                   S35      UeSnAff = f)aˆ  Build the dedicated client the subscription will live on.

The subscription is the lock, so its connection follows a
stricter policy than the command connection (#137):

- ``retry=Retry(NoBackoff(), 0, supported_errors=())``. redis-py
  wraps every pubsub read in the connection's retry policy, and
  its failure callback reconnects *before* the retry budget is
  checked, so even ``retries=0`` resurrects the connection once.
  A reconnected pubsub resubscribes with its handlers intact,
  which silently re-acquires a lock this holder may have lost to
  somebody else in the meantime. Only an empty
  ``supported_errors`` tuple makes the retry machinery catch
  nothing at all, so a dead socket kills the subscription with
  zero reconnects and the worker reports the loss instead.
  ``retry_on_error`` and ``retry_on_timeout`` are cleared too,
  because redis-py merges them back into the retry policy's
  supported errors.
- ``client_name`` at the connection level, so the name is part
  of the handshake rather than a separately sent command.
- ``protocol=2`` with the maintenance-notification kwargs
  stripped, because RESP3 maintenance notifications drive a
  second reconnect path in redis-py's pubsub that ignores the
  retry policy entirely.
- Decoded responses, because `channel_handler` compares the
  decoded channel name and payload.

``health_check_interval`` is deliberately *not* overridden: the
subscription inherits whatever the command connection uses, as
it did when both lived on one connection. A lock-created
connection carries the `DEFAULT_REDIS_KWARGS` value of ten
seconds through the clone, and a caller-supplied connection
keeps the caller's choice, which is what the module docs ask
them to set. Forcing the lock's default onto a connection whose
owner chose otherwise also has a nasty failure mode on
fakeredis: its ``read_response`` never advances redis-py's
``next_health_check`` clock, so any non-zero interval makes the
worker send a health-check ``PING`` on every ``get_message``
poll (on the order of 100k per second), and the spinning worker
starves the command connection's ``PUBSUB NUMSUB`` for tens of
milliseconds.

The client is built on a fresh connection pool cloned from the
command connection's pool (same connection class, same
connection arguments, the overrides above applied), never by
mutating a pooled connection: a mutated connection would go back
into the caller's pool on release and hand some later, unrelated
command a zero-retry connection named like a lock holder, which
the reaper would then kill. Cloning the pool works for both a
connection the lock created itself and one the caller supplied,
including one built around a custom pool.

Args:
    connection: The command connection to derive the
        subscription client from. Only its pool's class and
        connection arguments are read; the connection itself is
        not touched.

Returns:
    A client owned by this lock, with
    `subscription_connection_factory` taking precedence over the
    derivation when the caller supplied one.

Raises:
    ~portalocker.exceptions.LockException: The pool could not be
        cloned, typically because an exotic pool class takes
        constructor arguments this derivation does not know
        about, or because a cluster-style client carries no
        connection pool at all. The message points at
        `subscription_connection_factory`, which exists for
        exactly that situation.
Nr…   ÚmaintÚconnection_classr   r   )ÚretriesÚsupported_errorsFé   T)r<   Úretry_on_errorÚretry_on_timeoutrÞ   rÍ   r£   z8RedisLock could not derive a subscription client from a zG connection; pass subscription_connection_factory to build one yourself)Úconnection_pool)r“   rö   r^   r_   Úget_connection_kwargsr²   Úupdater¸   r<   ÚRetryÚbackoffÚ	NoBackoffrÞ   rÉ   rð   rÒ   ÚAttributeErrorr   ÚLockExceptionÚLOCK_FAILEDr   r¹   rº   )	rb   rŠ   ÚpoolÚconnection_kwargsr´   rµ   Úsubscription_kwargsÚsubscription_poolr   s	            r   Ú_make_subscription_clientÚ#RedisLock._make_subscription_clientÇ  s£  € ðX ×/Ñ/Ñ;Ø×7Ñ7Ó9Ð9ð)	Ø4>×4NÑ4NˆDô 8>·{²{Ø'Ø×0Ñ0Ó2ó8Ðð #4×"9Ñ"9Ô";ô:â";‘J�Cð  #Ñ%ó ð
 +.Ð1CÑ*Có �’
Ù";ð  ñ :ð  ×&Ñ&Ü—k‘k×'Ñ'Ü—M‘M×+Ñ+Ó-ØØ%'ð (ð ð
  "Ø!&Ø ×,Ñ,ØØ!%ð 'ñ ô BFÀdÄñ BØ!%×!6Ñ!6ðBà%ñBÐô �|‰|×!Ñ!Ð2CÐ!ÐDÐDùóE:øô0 œ>Ð*ó 		ô ×*Ò*Ü×(Ñ(×4Ñ4ðÜ˜*Ó%×.Ñ.Ð/ð 0HðHóð
 ðûð			ús8   ŸAD Á#DÁ3DÁ;DÂA7D ÄD ÄE7Ä+AE2Å2E7c                ó²  • U R                      SU l        SSS5        U R                  U5      nX l        [        R
                  " 5       U l        U R                  U5      nX0l         UR                  " S0 U R                  U R                  0D6  U R                  U5        [        UU R                  SU R                  U R                   b  U R"                  OSS9U l        U R$                  R'                  5         g! , (       d  f       Nê= f! [(         aB     U R+                  5         e ! [(         a"    [,        R/                  SU R0                  SS9   e f = ff = f)aí  Subscribe to the lock channel and start the keep-alive thread.

This is where the lock comes into existence. Once the
subscription is live this process is counted by ``PUBSUB
NUMSUB``, answers pings through `channel_handler`, and stays a
holder for exactly as long as the connection survives.

The order of operations matters:

1. The subscription gets its own client (see
   `_make_subscription_client`): connection-level
   ``client_name`` so the connection that actually holds the
   subscription is the one `_kill_unavailable_locks` finds in
   ``CLIENT LIST``, and a zero-reconnect retry policy so a
   revoked subscription dies loudly instead of resurrecting
   itself. The command connection is left completely alone.
2. The subscription is registered with `channel_handler` as its
   callback, so pings are answered from now on.
3. The server's subscribe confirmation is drained here, on the
   calling thread, before the worker thread exists (see
   `_wait_for_subscribe_confirmation`). Processing it proves the
   server registered the subscription, so the ``PUBSUB NUMSUB``
   that `acquire` runs next is guaranteed to count this holder.
   Without that proof a delayed ``SUBSCRIBE`` would let `acquire`
   read ``subscribers == 1`` while another holder exists and take
   the uncontended fast path against a contended channel.
4. A `PubSubWorkerThread` starts reading, with
   `_on_worker_exception` registered as its exception handler,
   so any failure of the reader - `BaseException` included -
   lands in the loss classifier instead of dying with the
   thread. It is a daemon thread: an unreleased lock must never
   keep the interpreter alive, and since losing the connection
   *is* releasing the lock, dying at process exit is the
   correct behaviour rather than a leak.

Any failure rolls the whole thing back through `_unsubscribe`
before re-raising, leaving `pubsub` as `None`. Without that
rollback a failed `acquire` would leave half a subscription
behind and the already-active guard at the top of `acquire`
would refuse every later retry on the same object. The rollback
deliberately keeps the command connection: `acquire` retries a
transient failure on that same connection, and closing a
lock-created connection here would leave the retry holding a
subscription whose ping handler has nothing left to answer on
(the terminal cleanup for errors that do propagate lives in
`_try_subscribe`). A rollback that fails as well - usually the
same dead Redis that broke the subscribe - is logged rather
than raised, so the original error is what propagates.

A leftover `_lost_error` from a worker that died during the
previous teardown is cleared before subscribing, so the fresh
attempt cannot be refused by `_confirm_held` over a failure
that belonged to a subscription which no longer exists. The
current pid is recorded as `_subscription_pid`, which is what
lets a later teardown detect that it runs in a forked child.

Args:
    connection: The command connection the subscription client
        is derived from.

Raises:
    ~portalocker.exceptions.LockException: The server did not
        confirm the subscription within `unavailable_timeout`
        seconds, raised after the rollback.
    Exception: Anything the Redis client raises while
        connecting, subscribing or starting the thread,
        re-raised unchanged after the rollback. `acquire`
        treats a ``redis_exceptions.ConnectionError`` or
        ``redis_exceptions.TimeoutError`` from here as one
        failed attempt and retries within its timeout budget.
NT)rd   rZ   r[   re   z7Redis lock %s failed to roll back a broken subscription©Úexc_infor   )Ú_state_lockr�   r  r˜   ÚosÚgetpidrš   r¿   rc   Ú	subscriberˆ   rÙ   Ú _wait_for_subscribe_confirmationrT   r©   Ú_on_worker_exceptionr•   Ú_self_check_tickr‡   ÚstartÚ	ExceptionÚ_unsubscriber	   r
   rL   )rb   rŠ   Úsubscription_clientrc   s       r   Ú_start_subscriptionÚRedisLock._start_subscriptionB  sF  € ðV ×ÓØ#ˆDÔ÷ ð ×*Ñ*¨:Ó6ð 	ð %8Ô!Ü!#§¢£ˆÔØ&*×&6Ñ&6Ð7JÓ&KˆØŒð 	Ø×ÒÑD §¡¨d×.BÑ.BÐCÒDØ×1Ñ1°&Ô9ô -ØØ×1Ñ1ØØ"&×";Ñ";ð
 ×/Ñ/Ñ;ð ×)Ò)àñˆDŒKð �K‰K×ÑÕ÷9 Õûô: ó 	ðØ×!Ñ!Ô#ð øô ó Ü—‘ØMØ—N‘NØ!ð ò ð
 ðúð	ús<   �C9Á&BD
 Ã9
DÄ

EÄD&Ä%EÄ&)EÅEÅEÅEc                ó  • [        U R                  U R                  S-  5      n[        R                  " 5       U R                  -   nSnU(       d  [        R                  " 5       U:  ag  Sn[
        R                  " SUR                  US95      nU(       a  UR                  S5      S:X  a  gU(       a  ML  [        R                  " 5       U:  a  Mg  [        R                  " [        R                  R                  S	U R                   S
35      e)aâ  Block until the server confirms the channel subscription.

Redis sends a ``subscribe`` frame after it has registered the
subscription, so reading that frame here establishes a
happens-before edge: any command issued afterwards - in
particular the ``PUBSUB NUMSUB`` in `acquire` - runs against a
server that already counts this holder. This wait replaces the
``time.sleep(0.01)`` that used to stand in for it, which bounded
nothing: a single TCP retransmit delays a ``SUBSCRIBE`` far
longer than 10ms.

Note that redis-py's ``PubSub.subscribed`` is no substitute: it
is set the moment the ``SUBSCRIBE`` command is *sent*, not when
the server confirms it.

A ping that arrives while draining is not lost: ``get_message``
dispatches it to `channel_handler` and returns `None`, so the
loop keeps waiting for the confirmation frame while the ping is
answered as usual.

Args:
    pubsub: The freshly subscribed pubsub to read frames from,
        before any worker thread consumes them invisibly.

Raises:
    ~portalocker.exceptions.LockException: No confirmation
        arrived within `unavailable_timeout` seconds. Proceeding
        without it would reopen the miscount this wait exists to
        prevent, so the subscription attempt fails instead.
r¡   TFúdict[str, typing.Any] | None©rx   rÉ   r  Nz;Redis did not confirm the lock channel subscription within ú seconds)rç   r©   rª   rä   rå   r^   r_   rz   rÎ   r   rý   rþ   )rb   rc   r§   rê   ÚfirstÚconfirmations         r   r  Ú*RedisLock._wait_for_subscribe_confirmation¸  så   € ôD !$Ø×"Ñ"Ø×$Ñ$ rÑ)ó!
ˆô Ÿ.š.Ó*¨T×-EÑ-EÑEˆØˆÞ”t—~’~Ó'¨(Ó2ØˆEÜ9?¿ºØ.Ø×"Ñ"¨>Ð"Ð:ó:ˆLö  × 0Ñ 0°Ó 8¸KÓ GØ÷ ˆe”t—~’~Ó'¨(Õ2ô ×&Ò&Ü×$Ñ$×0Ñ0ØIØ×'Ñ'Ð(¨ð2ó
ð 	
r   c                ó   • AU R                      U R                  [        R                  L nU R                  c  Xl        U R                  [        R
                  L nU(       a  [        R                  U l        SSS5        UR                  5         W(       a"  [        R                  SU R                  U5        g[        U[        5      nW(       aÑ  U(       a-  [        R                  SU R                  U R                  U5        O*[        R                  SU R                  U R                  US9  U R                  5         U R                  (       aQ  [         R"                  " 5         U R$                  (       d*  [&        R(                  " SU R                  < S3[*        SS	9  gggU(       a-  [        R-                  S
U R                  U R                  U5        g[        R-                  SU R                  U R                  US9  g! , (       d  f       GN�= f)a  Classify a keep-alive worker failure and escalate a loss.

Registered as the redis-py ``exception_handler`` of the worker
thread, so it runs *on* that thread for anything the reader
raises, `BaseException` included (redis-py catches that wide
deliberately, and a `KeyboardInterrupt` landing on the worker
must not vanish either). `PubSubWorkerThread.run` additionally
routes anything that escapes the handler itself back in here, so
a bug in this method still ends in the classifier on its second
pass instead of dying silently (#141).

What happens depends on where the lock is in its lifecycle,
decided under `_state_lock`:

- `_LockState.HELD`: the lock is lost. The state moves to
  `_LockState.LOST`, the error is recorded for
  :exc:`~portalocker.exceptions.LockLostError`, `on_lost` fires, and
  when `interrupt_on_lost` is set the main thread is
  interrupted. Connection errors log as an error without a
  traceback (an expected lifecycle event, just a bad one),
  anything else logs with the traceback because it is a handler
  or library bug.
- Any other state: the failure is scoped to the running acquire
  attempt. It is recorded so `acquire` notices, logged as a
  warning, and nothing escalates - a waiter that loses its
  subscription simply retries (#141).
- `_LockState.LOST` already: a second failure from the same
  teardown (usually the ``pubsub.close()`` after the loop dying
  on the same dead socket). The first error is kept and the
  repeat is logged at debug level.

The worker is always told to stop, which makes redis-py's read
loop exit and close the pubsub.

Args:
    error: What the reader (or a previous pass of this handler)
        raised.
    pubsub: The pubsub the worker was reading; unused, part of
        the redis-py handler signature.
    worker: The worker thread to stop.
NzARedis lock %s worker raised again after the loss was recorded: %rzKRedis lock %s lost its subscription connection while holding channel %r: %rzFRedis lock %s worker failed while holding channel %r, the lock is lostr  z!portalocker.RedisLock on channel zà lost a held lock and interrupted the main thread because interrupt_on_lost defaults to True in portalocker 4.2. portalocker 5.0.0 flips that default to False; pass interrupt_on_lost explicitly to keep or drop the interrupt.r   ©Ú
stacklevelz\Redis lock %s lost its subscription while waiting for channel %r, the attempt is retried: %rzPRedis lock %s worker failed while waiting for channel %r, the attempt is retried)r  r›   r0   r8   r�   r7   rk   r	   rÓ   rL   r)   r   r   rˆ   Ú_fire_on_lostr‘   Ú_threadÚinterrupt_mainr—   ÚwarningsÚwarnÚDeprecationWarningr
   )rb   r   rc   ÚworkerÚalready_lostÚwas_heldÚconnection_losts          r   r  ÚRedisLock._on_worker_exceptionî  s¬  € ð^ Ø×ÓØ!%×!1Ñ!1´Z·_±_Ð!DˆLØ×ÑÑ'Ø#(Ô Ø!×-Ñ-´·±Ð@ˆHÞÜ#-§?¡?�Ô ÷ ð 	�‰ŒÞÜ�L‰Lðà—‘Øô	ð Ü *¨5Ô2IÓ JˆÞÞÜ—‘ð-à—N‘NØ—L‘LØõô —‘ð'à—N‘NØ—L‘LØ"ð ñ ð ×ÑÔ Ø×%×%ô ×&Ò&Ô(Ø×2×2ô —M’MØ;ØŸ<™<Ñ*ð +DðDô +Ø#$ó
ð 3ð &ö, Ü�N‰Nð9à—‘Ø—‘Øõô �N‰Nð)à—‘Ø—‘Øð ò ÷E Öús   ŽA(G>Ç>
Hc                ó–   • U R                   nUc  g U" U 5        g! [         a#    [        R                  SU R                  5         gf = f)aØ  Invoke the `on_lost` callback, containing whatever it raises.

Runs on the worker thread as part of the HELD to LOST
transition. A callback failure must not break that transition,
the main-thread interrupt that follows it, or the thread
teardown around it, so anything the callback raises - including
a `BaseException` such as `SystemExit`, which would otherwise
skip the interrupt and redis-py's post-loop ``pubsub.close()`` -
is logged with its traceback and swallowed.
Nz%Redis lock %s on_lost callback failed)r�   rp   r	   Ú	exceptionrL   )rb   Úcallbacks     r   r  ÚRedisLock._fire_on_losth  sJ   € ð ?C¿l¹lˆØÑØð	Ù�T�NøÜó 	Ü×ÑØ7Ø—‘öð	ús   ’ ›*AÁAc                ó  • [         R                  " SU R                  5      nU R                     U R                  [
        R                  La
   SSS5        g[        R                  " 5       U R                  :  a
   SSS5        g SSS5         U R                  U5        U R                     [        R                  " 5       U-   U l	        SSS5        g! , (       d  f       NS= f! [         a     gf = f! , (       d  f       g= f)a[  Run one due self-check, or nothing at all.

Registered as the keep-alive worker's per-iteration tick, and
only when ``self_check_interval`` is set, so it runs on the
worker thread between subscription reads: the natural cadence
for a periodic holder-side check, and the one place that needs
no third thread. While the lock is anything but
`_LockState.HELD` this is a no-op - an idle, acquiring or lost
lock has no held subscription whose delivery could be
verified - and while held, a check runs once the gate armed by
`_confirm_held` expires, re-arming it after every passed check.

A failed check raises out of this method into the worker's
read loop, which routes it through `_on_worker_exception`
exactly like a socket error: `RedisLockSelfCheckError` is a
``redis.exceptions.ConnectionError``, so the loss is
classified as a connection loss and every loss channel behaves
identically to a socket-detected revocation.

Both reads of the gate run under `_state_lock`, the same lock
`_confirm_held` arms it under. `_mode_lock` is never taken
here, so the no-path-holds-both-locks invariant between the
two locks stands.

Args:
    held_pubsub: The held subscription the worker is reading.
        Passed through to `_run_self_check`, which keeps
        servicing it so pings - the check's own included - are
        still answered while the check waits for its reply.
r}   N)r^   r_   r•   r  r›   r0   r7   rä   rå   rž   Ú_run_self_checkr   )rb   Úheld_pubsubÚintervals      r   r  ÚRedisLock._self_check_tick~  sÎ   € ôB !Ÿ+š+ g¨t×/GÑ/GÓHˆØ×ÓØ×Ñ¤z§¡Ò6Ø÷ Ðô �~Š~Ó $×"7Ñ"7Ó7Ø÷	 Ðð 8÷ ð
	Ø× Ñ  Ô-ð
 ×ÓÜ$(§N¢NÓ$4°xÑ$?ˆDÔ!÷ Ð÷ Õûô #ó 	ñ ð	ú÷ Õús/   ®CÁ$CÂC" Â*C2Ã
CÃ"
C/Ã.C/Ã2
D c           	     ó  • [        [        R                  " SU R                  5      U R                  5      n[
        R                  " 5       U-   n[        U R                  US-  5      nU R                  5       nU R                   S[        R                  " 5       R                   3nU R                  U5      n UR                  U5        U R                  UUUUS 5      (       ai  UR!                  U R                  ["        R$                  " SUS.5      5        U R                  UUUUU R&                  5      (       a   UR)                  5         g[+        SU R,                   S	U R                  < S
U S35      e! UR)                  5         f = f)aÔ  Verify the held subscription end to end, or raise.

Publishes an ordinary liveness ping to the lock's own channel -
the same wire shape `_collect_lock_holders` sends, carrying a
private single-use response channel - and requires this
holder's *own* reply to arrive through that response channel in
time. The reply can only arrive when the whole loop works: the
publish reached the server, the server delivered the ping to
the held subscription, `channel_handler` answered it, and the
answer travelled back through a fresh subscription. That is
what closes the half-open-link hole socket-level detection
leaves open: a subscription whose socket is open but silently
delivers nothing fails this check within one interval instead
of surviving until the kernel gives up on retransmits.

The reply deadline is ``min(self_check_interval,
unavailable_timeout)``: `unavailable_timeout` is already the
protocol's bound on how long a healthy holder may take to
answer a ping - any contended prober reaps this holder after
that long - and capping at the interval keeps at most one
check in flight per interval. The deadline covers the response
channel's subscribe confirmation too, so a command path that
cannot even set up the return channel fails the check as well.
Deliberate, because such a holder cannot answer other probers
either, though it does mean a command-connection outage can
cost a hold whose subscription was actually fine.

The check never disturbs the channel: it reads no subscriber
count, reaps nobody, and leaves the held subscription alone
apart from servicing it. Other holders answer the ping like
any probe ping and their replies are ignored.

Args:
    held_pubsub: The held subscription to keep servicing while
        the check waits.

Raises:
    RedisLockSelfCheckError: The subscribe confirmation or this
        holder's own reply did not arrive within the deadline.
    _SelfCheckAbandoned: The lock stopped being held mid-check
        (propagated from `_await_self_check_frame`).
r}   r¡   rÜ   c                ó*   • U R                  S5      S:H  $ )NrÉ   r  )rÎ   )Úframes    r   Ú<lambda>Ú+RedisLock._run_self_check.<locals>.<lambda>î  s   € ˜eŸi™i¨Ó/°;Ò>r   Úping©rÊ   rÌ   NúRedis lock z5 did not receive its own self-check reply on channel z within r  )rç   r^   r_   r•   rª   rä   rå   r©   r»   rˆ   r®   r¯   r°   r¿   r  Ú_await_self_check_framerÔ   rÏ   rÕ   Ú_is_own_probe_replyr{   r   rL   )rb   r0  Úbudgetrê   ÚpollrŠ   rÌ   Úresponse_pubsubs           r   r/  ÚRedisLock._run_self_check®  s€  € ôV Ü�KŠK˜ ×!9Ñ!9Ó:Ø×$Ñ$ó
ˆô Ÿ.š.Ó*¨VÑ3ˆÜ˜$×0Ñ0°&¸2±+Ó>ˆØ)-×)<Ñ)<Ó)>ˆ
Ø#'§<¡< .°´$·*²*³,×2BÑ2BÐ1CÐ DÐØ/3×/?Ñ/?À
Ó/Kˆð%	$Ø×%Ñ%Ð&6Ô7Ø×+Ñ+ØØØØñ ?÷ñ ð ×"Ñ"Ø—L‘LÜ—J’Jà'-Ø0@ñóôð ×/Ñ/Ø#ØØØØ×,Ñ,÷ñ ð ð ×!Ñ!Õ#ô *Ø˜dŸn™nÐ-ð ./Ø/3¯|©|Ñ.>¸hØ�(˜(ð$óð øð ×!Ñ!Õ#ús   Â7BE6 Å*E6 Å6Fc                óÜ  • [         R                  " 5       U:  aÂ  U R                     U R                  [        R
                  La  [        e SSS5        [        R                  " SUR                  US95      nUb7  U" U5      (       a  g[        R                  " SUR                  SS95      nUb  M7  UR                  SSS9  [         R                  " 5       U:  a  MÂ  g! , (       d  f       N˜= f)aÓ  Wait for a response-channel frame matching ``predicate``.

Each polling interval drains every buffered response frame and
then services the held subscription with one non-blocking
read. The servicing is what makes the self-check sound rather
than self-defeating: this method runs on the worker thread,
whose ordinary read loop is paused for the duration, and the
check's own ping arrives through exactly that held
subscription - without the read here, `channel_handler` would
never see the ping and every check would time out. Other
probers' pings keep being answered through the same read.

Args:
    response_pubsub: The check's own response subscription.
    held_pubsub: The held subscription to service every
        interval.
    deadline: Monotonic instant at which the wait gives up.
    poll: Seconds each response read may block, pacing the
        loop.
    predicate: Decides whether a frame is the one awaited.

Returns:
    True when a matching frame arrived, False when the
    deadline passed first.

Raises:
    _SelfCheckAbandoned: The lock left `_LockState.HELD`,
        which a concurrent `release` does before it stops the
        worker. The check is moot then and must declare
        neither success nor loss.
Nr  r  Tr   rv   F)
rä   rå   r  r›   r0   r7   r   r^   r_   rz   )rb   r?  r0  rê   r>  Ú	predicater5  s          r   r;  Ú!RedisLock._await_self_check_frame	  sÝ   € ôN �nŠnÓ Ó)Ø×!Ó!Ø×#Ñ#¬:¯?©?Ò:Ü-Ð-ð ;÷ "ô 39·+²+Ø.Ø×+Ñ+°DÐ+Ð9ó3ˆEð Ñ#Ù˜U×#Ñ#ØÜŸšØ2Ø#×/Ñ/¸Ð/Ð:ó�ð Ó#ð ×#Ñ#Ø*.Øð $ñ ô �nŠnÓ Õ)ð& ÷% "Õ!ús   ¦$CÃ
C+c                óv   • U R                  UR                  S5      S5      nUR                  U R                  :H  $ )a:  Report whether ``frame`` carries this holder's own record.

A self-check ping is an ordinary probe ping, so every holder
on the channel answers it, and only the echo of this lock's
own `channel_handler` proves the delivery path. Foreign replies
parse to their own ids, and a frame that does not parse as a
protocol record at all parses to a synthetic ``legacy-<n>``
id, which can never equal a uuid4 hex `holder_id`, so both are
ignored by the caller.

Args:
    frame: A pubsub frame from the response channel.

Returns:
    True when the frame is this holder's reply.
rË   r   )Ú_parse_lock_responserÎ   rL   )rb   r5  Úholders      r   r<  ÚRedisLock._is_own_probe_replyE  s:   € ð" #'×";Ñ";Ø�I‰I�fÓØó#
ˆð ×Ñ 4§>¡>Ñ1Ð1r   c                ón  • U R                   nU R                     U R                  b
   SSS5        gUb  UR                  5       (       d
   SSS5        g[        R
                  U l        U R                  b'  [        R                  " 5       U R                  -   U l
         SSS5        g! , (       d  f       g= f)aT  Promote a won acquisition to `_LockState.HELD`, or refuse.

The confirm handshake that closes the flag-transition race: the
subscription connection can die in the microseconds between the
probe result that decided the acquisition and the state update
that records it. Both `acquire` success sites call this under
`_state_lock`, the same lock `_on_worker_exception` takes, so
exactly two interleavings exist. Handler first: the error is
recorded while the state is still `_LockState.ACQUIRING`, the
handler takes its quiet attempt-scoped path, and this method
sees the error and refuses. Confirm first: the state is
`_LockState.HELD` by the time the handler runs, so the loud
LOST path fires. In neither ordering does `acquire` return
success with a dead worker and no notification.

The liveness check additionally covers a worker that died
without the handler running at all, which redis-py permits when
``pubsub.close()`` raises after a clean stop.

Returns:
    True when the lock is now held. False when the attempt must
    be treated as failed because the worker already died or
    recorded an error.
NFT)r‡   r  r�   Úis_aliver0   r7   r›   r•   rä   rå   rž   ©rb   r‡   s     r   Ú_confirm_heldÚRedisLock._confirm_held\  s•   € ð2 -1¯K©KˆØ×ÓØ×ÑÑ+Ø÷ Ðð ‰~ V§_¡_×%6Ñ%6Ø÷	 Ðô
  *Ÿ™ˆDÔØ×'Ñ'Ñ3ô
 —N’NÓ$ t×'?Ñ'?Ñ?ð Ô%ð ÷ ×Öús   ™B&±B&ÁA
B&Â&
B4c                óÖ   • U R                      U R                  b
   SSS5        g SSS5        U R                  nUSL =(       d    UR                  5       (       + $ ! , (       d  f       N:= f)a  Report whether the current attempt's worker is already dead.

Checked by `acquire` before reusing an existing subscription for
the next attempt. A recorded error or a worker thread that is no
longer alive both mean the subscription backing this attempt is
gone: Redis dropped the subscriber with the connection, so
continuing to probe on top of it would wait on a lock this
process is no longer counted for.

Returns:
    True when the attempt must be abandoned and the next one
    should subscribe from scratch.
NT)r  r�   r‡   rI  rJ  s     r   Ú_waiting_attempt_failedÚ!RedisLock._waiting_attempt_failed…  sY   € ð ×ÓØ×ÑÑ+Ø÷ ÐØ+÷ ð -1¯K©KˆØ˜ˆ~×6 V§_¡_Ó%6Ô!6Ð6÷	 Õús   �AÁ
A(c                óê  • U R                      SU l        [        R                  U l        SU l        SSS5        U R                  [        R                  R                  :X  a1  U R                     [        R                  U l        SU l        SSS5         U R                  5         g! , (       d  f       Ny= f! , (       d  f       N1= f! [          a"    ["        R%                  SU R&                  SS9   gf = f)a¹  Consume a failed attempt so the next one starts from scratch.

Clears the recorded worker error, resets the lifecycle to
`_LockState.ACQUIRING`, rolls an exclusive lock's mode back to
`RedisLockMode.PENDING` (forgetting any election, which a lock
without a live subscription may not advertise), and tears the
dead subscription down. The teardown is best effort: it usually
runs against the same dead connection that killed the attempt,
so a teardown failure is logged rather than allowed to abort
the retry loop that exists to survive exactly these failures.

Also used by `acquire` to reset an instance whose previous hold
ended in `_LockState.LOST`, which is what makes lost instances
reusable.
NFz=Redis lock %s failed to tear down a dead subscription attemptTr  )r  r�   r0   r6   r›   rŸ   r�   r   r¬   rF   r    rB   rG   rM   rŽ   r  r  r	   r
   rL   rj   s    r   Ú_abandon_failed_attemptÚ!RedisLock._abandon_failed_attempt™  s¿   € ð  ×ÓØ#ˆDÔÜ)×3Ñ3ˆDÔð !%ˆDÔ÷ ð �:‰:œ×,Ñ,×6Ñ6Ó6Ø—“Ü)×1Ñ1�”	Ø&+�Ô#÷ !ð	Ø×ÑÕ÷ Õú÷ !•ûô
 ó 	Ü�N‰Nðà—‘Øð	 ó ð	ús)   �$B$Á-B5ÂC Â$
B2Â5
CÃ)C2Ã1C2c                óˆ   • U R                      U R                  [        R                  L sSSS5        $ ! , (       d  f       g= f)a  Whether a held lock was revoked and the loss is unhandled.

True from the moment the keep-alive worker observed the
revocation until the next `acquire` resets the instance.
Deliberately still True after `release`, so code using bare
``acquire()``/``release()`` can check it afterwards; the raising
counterpart is `ensure_held`.

Returns:
    True when the lock is in the lost state.
N)r  r›   r0   r8   rj   s    r   r4   ÚRedisLock.lost¾  s+   € ð ×ÓØ×#Ñ#¤z§¡Ð6÷ ××ús	   �3³
Ac                óF   • U R                   (       a  U R                  5       eg)a›  Raise if the lock was lost, return quietly otherwise.

The check to sprinkle through a long critical section: cheap
(one mutex acquisition, no network traffic), and the only way a
loss interrupts a running body deterministically, since the
optional main-thread interrupt is best effort by nature.

This reports revocation, not acquisition: it also returns
quietly on a lock that is idle or still acquiring, so calling
it only makes sense between a successful `acquire` and the
matching `release`.

Raises:
    ~portalocker.exceptions.LockLostError: The lock was revoked
        while held. The error that killed the subscription is
        attached as ``__cause__``.
N)r4   Ú_lock_lost_errorrj   s    r   Úensure_heldÚRedisLock.ensure_heldÎ  s   € ð$ �9�9Ø×'Ñ'Ó)Ð)ð r   c                ó6  • U R                      U R                  nSSS5        [        R                  " [        R                  R
                  SU R                   SU R                  < S3U R                  U R                  S9nWUl        U$ ! , (       d  f       Nv= f)a  Build the `LockLostError` describing this lock's loss.

Returns:
    The error, carrying `channel` and `holder_id`, with the
    exception that killed the keep-alive worker attached as
    ``__cause__`` so tracebacks show the revocation and its
    cause as one chain.
Nr:  z on channel z was revoked while held)rˆ   rL   )	r  r�   r   ÚLockLostErrorrý   rþ   rL   rˆ   Ú	__cause__)rb   Úcauser   s      r   rV  ÚRedisLock._lock_lost_errorã  sƒ   € ð ×ÓØ*.×*:Ñ*:ˆE÷ ä*4×*BÒ*BÜ×$Ñ$×0Ñ0Ø˜$Ÿ.™.Ð)¨°d·l±lÑ5Eð F!ð !à—L‘LØ—n‘nñ+
ˆð  ˆŒØˆ÷ Õús   �B
Â

Bc                óÒ   •  U R                  5         Uc!  U R                  (       a  U R                  5       eg! [         a%  nUc  e [        R                  " UU5         SnAgSnAff = f)aþ  Release the lock, raising `LockLostError` for a silent loss.

A lock that was lost mid-block and whose body nevertheless
finished cleanly would otherwise end the ``with`` statement
looking successful, which is exactly the silent divergence a
revoked lock must not produce. The loss is therefore re-raised
here - after the release, so the teardown always runs - but only
when no exception is already propagating out of the body: the
body's own failure is the more specific signal and must not be
masked (the loss stays observable through `lost` either way).
`lost` is read *after* the release: the LOST state is sticky
through `release`, so reading it afterwards also catches a
revocation that lands in the instant the block is already
exiting.

A release failure follows `Lock.__exit__`'s discipline: with an
exception already leaving the body it is chained onto that
exception as its ``__context__`` (with a note attached) instead
of replacing it, and only with a clean body does the release
error itself propagate.

Args:
    exc_type: Type of the exception leaving the block, if any.
    exc_value: The exception instance, if any.
    traceback: The traceback of that exception, if any.

Returns:
    `None`, so a body exception keeps propagating once the lock
    has been released.

Raises:
    ~portalocker.exceptions.LockLostError: The lock was revoked
        while the block ran and the block raised nothing itself.
    Exception: Whatever `release` raises, but only when the
        block itself ended without an exception.
N)Úreleaser  r   Ú_chain_release_errorr4   rV  )rb   Úexc_typeÚ	exc_valueÚ	tracebackÚrelease_errors        r   Ú__exit__ÚRedisLock.__exit__ø  sg   € ðT	Ø�L‰LŒNð Ñ §	§	Ø×'Ñ'Ó)Ð)Øøô ó 		ØÑ ð Ü×&Ò&ØØôô ûð		ús   ‚7 ·
A&ÁA!Á!A&c                óè  •  [         R                  " U5      nUR                  S5      nUR                  S5      nUR                  S5      nU[        :X  ah  [	        U[
        5      (       aS  [	        U[
        5      (       a>  UR                  S5      n[	        U[        5      (       a  UOSn[        U[        U5      US9$ [        SU 3[        R                  SS	9$ ! [        [        [        4 a     N3f = f)
a'  Turn one raw ping reply into a `RedisLockHolder`.

A reply is accepted as a protocol record only if it decodes to a
mapping whose ``protocol`` equals `REDIS_LOCK_PROTOCOL_VERSION`
and whose ``holder_id`` and ``mode`` are both strings, with
``mode`` naming a known `RedisLockMode`. Everything else - a bare
timestamp from portalocker 3.2.0, a truncated payload, a mode
some future version invents - falls back to a synthetic legacy
holder.

That fallback is deliberately pessimistic. It reports
`RedisLockMode.EXCLUSIVE`, so anything unrecognised blocks
readers and writers alike. Assuming `RedisLockMode.SHARED` for a
participant whose intentions could not be read would hand out
overlapping locks, which is the one outcome a lock may never
produce.

Legacy replies carry no identity, so they cannot be de-duplicated
by holder id the way protocol replies are. `legacy_index` gives
each one a distinct ``legacy-<n>`` id, so two silent old holders
count as two holders instead of collapsing into one and leaving
the caller short of the subscriber count it expected.

The optional ``elected`` key is read as a strict boolean.
Anything else, including its absence, parses as `None`, which
`RedisLockHolder.elected` documents as "the holder predates the
field". Records from portalocker 4.0 and 4.1 lack the key, so
they land on `None` here without falling back to a legacy
holder: the record itself is still a perfectly valid protocol
version 1 record.

Args:
    raw_data: The message payload exactly as received, normally a
        JSON string but not required to be anything in
        particular.
    legacy_index: How many legacy replies this probe has already
        seen. Used only to build a unique fallback holder id.

Returns:
    The parsed holder, or a legacy exclusive holder when the
    reply could not be interpreted.
rL   rM   rÍ   rQ   N)rL   rM   rQ   zlegacy-T)rL   rM   rO   )rÏ   rÐ   rÎ   rÖ   r)   rK   rN   rJ   rB   rü   rÒ   r­   rF   )	rb   r×   Úlegacy_indexrË   rL   rM   rÍ   Úelected_valuerQ   s	            r   rE  ÚRedisLock._parse_lock_response2  sä   € ð^	Ü#Ÿzšz¨(Ó3ˆDØ$(§H¡H¨[Ó$9ˆIØ#Ÿx™x¨Ó/ˆDØ#'§8¡8¨JÓ#7ˆHàÔ7Ó7Ü˜y¬#×.Ñ.Ü˜t¤S×)Ñ)à,0¯H©H°YÓ,?�ä%/°¼t×%DÑ%D‘MÈ$ð ô 'Ø'Ü& tÓ,Ø#ñð ô Ø ˜~Ð.Ü×(Ñ(Øñ
ð 	
øô ¤	¬:Ð6ó 	Ùð	ús   ‚B:C ÃC1Ã0C1c                óŒ  • [        U5      nU Vs1 s H!  oDR                  (       a  M  UR                  iM#     nn[        S U 5       5      n[        R
                  " [        R                  " U R                  5      S-   5      nUR                  5       nU H§  n	U	R                  SS5      n
UR                  U
5      nX R                  :H  =(       a    U(       + =(       d    USL=(       a    UR                  S5      U;  nU(       d  Mq  [        R                  SU	5        UR                  U	R                  S5      5        M©     gs  snf )	aû  Kill the connections of holders that did not answer the ping.

Called only when a probe collected fewer replies than there are
subscribers on the channel. A subscriber Redis still counts but
that no longer answers is a crashed or wedged process; because
ownership lives in the connection, killing that connection is
precisely what releases its lock, and the next probe then sees a
consistent channel again.

Every entry in ``CLIENT LIST`` is matched against the two naming
schemes this package has used:

- exactly `legacy_client_name`: a portalocker 3.2.0 or older
  holder. Killed unless some legacy reply arrived in this probe,
  since old holders are indistinguishable from one another.
- `legacy_client_name` plus a `holder_id` suffix. The suffix must
  be exactly 32 lowercase hex characters, the shape of the uuid4
  hex `holder_id` every lock allocates, and the holder is killed
  unless that exact id replied.

The suffix shape is validated because channel names may
themselves contain ``-lock-``. A holder of channel ``a-lock-b``
is named ``a-lock-b-lock-<id>``, which starts with channel
``a``'s prefix but does not parse as one of its holders. A bare
prefix match here used to kill those healthy neighbours, along
with any unrelated client that coincidentally shared the prefix
(#142).

This lock names its own connection the same way, so the caller
must include its own reply in `responding_holders` or it will
kill itself. It does: a probing lock is subscribed to its own
channel and answers its own ping.

Args:
    connection: The connection to run ``CLIENT LIST`` and
        ``CLIENT KILL`` on.
    responding_holders: The holders that did answer this probe.
        Anything named like a holder of this channel but absent
        from this list is considered crashed.
c              3  ó8   #   • U  H  oR                   v •  M     g 7f©N©rO   ©Ú.0rF  s     r   Ú	<genexpr>Ú4RedisLock._kill_unavailable_locks.<locals>.<genexpr>®  s   é € Ð$IÂ°v§]¦]Âùó   ‚z-(?P<holder_id>[0-9a-f]{32})r#   Ú NrL   ú$Killing unavailable redis client: %rÚid)ÚlistrO   rL   ÚanyÚreÚcompileÚescaperÝ   Úclient_listrÎ   Ú	fullmatchÚgroupr	   r
   Úclient_kill_filter)rb   rŠ   Úresponding_holdersÚholdersrF  Úresponding_holder_idsÚlegacy_respondedÚholder_name_patternÚclientsÚclient_rÞ   ÚmatchÚunavailables                r   Ú_kill_unavailable_locksÚ!RedisLock._kill_unavailable_locks}  s,  € ôZ *.Ð.@Ó)Aˆá+2ó+
Ú+2 ¿-½-ÓˆF×Ô©7ð 	ð +
ô "%Ñ$IÁÓ$IÓ!IÐô 02¯zªzÜ�IŠI�d×-Ñ-Ó.Ð1OÑOó0
Ðð )3×(>Ñ(>Ó(@ˆÛˆGØ&Ÿ{™{¨6°2Ó6ˆKØ*=×*GÑ*GØó+ˆEð ×6Ñ6Ñ6×OÐ?OÔ;O÷!ð ˜TÐ!÷ JØ—K‘K Ó,Ð4IÑIð	 ÷ ˆ{Ü—‘Ø:Øôð ×-Ñ-¨g¯k©k¸$Ó.?Ö@ò! ùò+
s
   �E¨Ec                ó  • [        [        U5      n[        R                  " SUR	                  US95      nUbË  UR                  S5      S:X  a_  [        S UR                  5        5       5      nU R                  UR                  S5      U5      nX‚UR                  '   [        U5      U:¼  a  g[        R                  " SUR	                  SS95      nUc*  US:”  a$  [        R                  " SUR	                  US95      nUb  MÊ  gg)	a²  Read every buffered reply within one polling interval.

Reading a single message per interval would cap the throughput
at one reply per interval, and a channel with more holders than
intervals could then never be probed conclusively, so every
probe would kill healthy holders (#138). The first read waits up
to `check_interval` for a reply, the follow-up reads first empty
the local buffer at ``timeout=0`` and then, when that comes up
empty, poll once with the short `_PROBE_REPLY_GRACE` timeout.
Holders answer one after another, so the next reply is usually a
few milliseconds behind the previous one: without the grace poll
the ``timeout=0`` read missed it almost every time and the probe
slept a full jittered drain interval per reply, which is what
stretched every reply-staleness window in this protocol to
around a hundred milliseconds (#145). Draining stops early once
`expected_subscribers` distinct holders have replied.

Args:
    pubsub: The probe's own subscription to read replies from.
    holders: The replies collected so far, keyed by holder id
        and extended in place. The number of legacy entries in
        it doubles as the index for the next synthetic
        ``legacy-<n>`` id.
    expected_subscribers: Reply count at which draining stops.
    check_interval: Seconds the first read may wait for a reply.
        Also caps the grace poll, so a zero interval keeps the
        drain strictly non-blocking after the first read.
r  r  NrÉ   rÊ   c              3  ó8   #   • U  H  oR                   v •  M     g 7frm  rn  ro  s     r   rq  Ú1RedisLock._drain_probe_replies.<locals>.<genexpr>ñ  s   é € ð (Ú0@ f—M–MÒ0@ùrs  rË   r   )rç   Ú_PROBE_REPLY_GRACEr^   r_   rz   rÎ   ÚsumÚvaluesrE  rL   Úlen)	rb   rc   r�  Úexpected_subscribersr§   ÚgracerÊ   rh  Úholder_records	            r   Ú_drain_probe_repliesÚRedisLock._drain_probe_repliesÇ  s  € ôF Ô-¨~Ó>ˆÜ06·²Ø*Ø×Ñ ~ÐÐ6ó1
ˆð Ñ!Ø�{‰{˜6Ó" iÓ/Ü$'ñ (Ø07·±Ô0@ó(ó %�ð 26×1JÑ1JØ—K‘K Ó'Ø ó2�ð 4A˜×/Ñ/Ñ0Ü�w“<Ð#7Ó7ØÜ—k’kØ.Ø×"Ñ"¨1Ð"Ð-óˆGð ‰ 5¨1£9ô !Ÿ+š+Ø2Ø×&Ñ&¨uÐ&Ð5ó�ð) Õ!r   c                óÄ  • U R                    S[        R                  " 5       R                   3n[	        U R
                  US-  5      nU R                  U5      n0 n UR                  U5        U R                  X65       HG  n	[        R                  " SUR                  US95      n
U
(       d  M0  U
R                  S5      S:X  d  MG    O   U R                  U5      nX²:w  a   UR                  5         gUR                  U R                   [         R"                  " SUS	.5      5        U R                  X65       HO  n	U R%                  UUUU5        ['        U5      U:¼  a    O+U R                  U5      U:w  d  M?    UR                  5         g   U R                  U5      n[(        R+                  S
U R,                  U['        U5      U5        XÂ:w  a   UR                  5         g['        U5      U:  a9  U(       a   U R/                  UUR1                  5       5         UR                  5         g[3        UR1                  5       5      UR                  5         $ ! UR                  5         f = f)a1  Probe the channel and report who is holding the lock.

Publishes a ping on the lock channel carrying a private,
single-use response channel (``<channel>-<uuid4>``) and gathers
the answers. That channel is subscribed *and its subscription
confirmed* before the ping goes out, so a fast holder cannot
reply into a subscription that is not listening yet; the
confirmation frame is drained here rather than counted as a
reply. That wait is bounded by ``timeout``: if no confirmation
arrives in time the ping is published anyway, and replies that
beat the subscription are missed, which shows up as an
inconclusive probe. The probe uses its own short-lived pubsub,
which is always closed again, so the subscription that holds the
lock is never disturbed.

Answers are keyed by holder id, so a holder that answers twice is
counted once, and collection stops as soon as
`expected_subscribers` distinct holders have replied. Every
polling interval drains all the replies that are already buffered
instead of reading a single one, so the interval paces the
polling rather than capping the reply throughput. A channel with
more holders than the timeout has intervals can therefore still
be probed conclusively.

Returning `None` is not the same as returning an empty list, and
the difference is the reason `_resolve_lock_holders` guards every
branch on ``holders is not None``:

- `None` means *could not determine*. The probe is unusable and
  the only safe response is to try again. Reading it as "nobody
  holds the lock" would hand out a lock that somebody else may
  well be holding.
- A list is a positive statement about who is there, and can be
  acted on. It always contains this lock itself, because a
  probing lock is subscribed to its own channel and answers its
  own ping. An empty list would mean nobody at all is there, but
  `acquire` never asks in that situation: it short-circuits when
  it is the only subscriber.

A probe is inconclusive in three ways:

1. The subscriber count no longer matched ``expected_subscribers``
   immediately before the ping went out. The expectation was
   already stale, so the probe is abandoned before it puts any
   traffic on the channel.
2. The count changed while the replies were being collected,
   checked each polling interval while replies are still
   outstanding and once more after collection. Someone joined
   or left, so the replies describe a channel that no longer
   exists in that shape, and a decision made from them could be
   wrong for either party. The interval check also stops an
   incomplete collection from waiting out the full timeout for
   a reply whose sender already left the channel.
3. Fewer distinct holder ids replied than there were subscribers.
   Somebody counted by Redis is not answering, which normally
   means it crashed, though a holder too slow to answer inside
   ``timeout`` is treated the same way.
   `_kill_unavailable_locks` reaps those connections and the
   probe still reports inconclusive so that the caller retries
   against the cleaned-up channel.

The two count checks bracket the collection window, but ``PUBSUB
NUMSUB`` reports how many subscribers there are, not who they
are, so count-preserving churn between the two checks - one
holder leaving while another joins - remains undetectable and
such a probe passes as conclusive. Losing waiters produce exactly
that churn routinely, since they unsubscribe between attempts. A
single probe can therefore still be built on a stale sample, and
what contains the damage is what happens after. A writer that
promoted
on such a sample is visible as `RedisLockMode.EXCLUSIVE` to every
later probe, so retrying losers back off, and its own promotion
does not stand on this one probe either: the confirm probe in
`_confirm_exclusive_promotion` re-checks the channel while the
promotion is already on the wire and demotes when a churn-hidden
holder surfaces (#145). Only a rival hidden from *that* probe as
well - a second count-preserving join landing in the instant
between its count pre-check and its ping - stays unseen, on both
sides at once for an actual double.

Args:
    connection: The connection to publish the ping on and to run
        the subscriber count against.
    expected_subscribers: How many subscribers the caller counted
        just before probing. This is both the target number of
        replies and the value the count is re-checked against.
    timeout: Seconds to spend waiting for the subscribe
        confirmation and, separately, for the replies. Polling
        uses `thread_sleep_time` or a tenth of this timeout,
        whichever is smaller.
    reap: Whether inconclusive case 3 above may kill the silent
        subscribers. `acquire` reaps; the read-only `probe`
        passes False so observing a channel can never modify
        it.

Returns:
    The holders that answered, or `None` when the probe was
    inconclusive.
rÜ   r¡   r  r  rÉ   r  Nr8  r9  z6Redis lock %s probe expected=%d received=%d current=%d)rˆ   r®   r¯   r°   rç   r©   r¿   r  rì   r^   r_   rz   rÎ   rÆ   r{   rÔ   rÏ   rÕ   r•  r‘  r	   rÓ   rL   r‰  r�  rw  )rb   rŠ   r’  rx   ÚreaprÌ   r§   rc   r�  Ú_r  Úprecheck_subscribersÚcurrent_subscriberss                r   Ú_collect_lock_holdersÚRedisLock._collect_lock_holders	  s1  € ðT $(§<¡< .°´$·*²*³,×2BÑ2BÐ1CÐ DÐÜ # D×$:Ñ$:¸GÀb¹LÓ IˆØ&*×&6Ñ&6°zÓ&BˆØ.0ˆðG	Ø×ÑÐ-Ô.Ø×,Ñ,¨WÖE�Ü=C¿[º[Ø2Ø×&Ñ&¨~Ð&Ð>ó>�÷  �< L×$4Ñ$4°VÓ$<ÀÕ$KÙñ Fð )-×(BÑ(BØó)Ð ð $Ó;Øðn �L‰L�Nðk ×ÑØ—‘Ü—
’
à#)Ø,<ñóôð ×,Ñ,¨WÖE�Ø×)Ñ)ØØØ(Ø"ô	ô �w“<Ð#7Ó7Ùð ×.Ñ.¨zÓ:Ø+õ,ð  ð* �L‰L�NñW Fð0 (,×'AÑ'AÀ*Ó'MÐÜ�L‰LØHØ—‘Ø$Ü�G“Ø#ôð #Ó:Øð �L‰L�Nô �7‹|Ð2Ó2ÞØ×0Ñ0Ø"ØŸ™Ó(ôð ð �L‰L�Nô ˜Ÿ™Ó(Ó)à�L‰L�NøˆF�L‰L�Nús9   ÁAI Â.I ÃI Ã1A?I Å4I ÆAI Ç6I È$I ÉIc                ó‚   • U R                   [        R                  R                  :H  =(       a    [	        S U 5       5      $ )a«  Report whether this lock may join the holders already present.

The whole compatibility matrix fits in one expression because
only one combination is allowed::

    our flags   the holders present       result
    ---------   -------------------       ------
    SHARED      all SHARED                compatible
    SHARED      any PENDING or EXCLUSIVE  not compatible
    EXCLUSIVE   anything at all           not compatible

An exclusive lock never joins anybody; it goes through the
election in `_writer_is_elected` instead. A reader joins only a
set of pure readers, and a single `RedisLockMode.PENDING` holder
is enough to keep it out - that is the rule that stops an endless
stream of readers from starving a waiting writer. Legacy holders
are recorded as `RedisLockMode.EXCLUSIVE`, so an old participant
blocks readers too.

This lock's own reply is part of `holders`, and a shared lock
advertises `RedisLockMode.SHARED` from construction onwards, so
seeing itself in the list never makes it incompatible with
itself.

Args:
    holders: The holders returned by a conclusive probe.

Returns:
    True when this lock can be added to the existing holders
    without violating exclusivity.
c              3  óX   #   • U  H   oR                   [        R                  L v •  M"     g 7frm  ©rM   rB   rH   ro  s     r   rq  Ú4RedisLock._holders_are_compatible.<locals>.<genexpr>â	  s!   é € ð @
Ú>E°F�K‰Kœ=×/Ñ/Õ/ºgùó   ‚(*)r�   r   r¬   rH   Úall)rb   r�  s     r   Ú_holders_are_compatibleÚ!RedisLock._holders_are_compatible¿	  s=   € ðF �z‰zœY×0Ñ0×7Ñ7Ñ7÷ 
¼Cñ @
Ù>Eó@
ó =
ð 	
r   c                óÖ   ^ • [        S U 5       5      (       a  g[        U 4S jU 5       5      (       a  g[        S U 5       5      n[        U=(       a    US   T R                  :H  5      $ )a   Report whether this lock is the writer elected to go next.

Returns False at once if any holder is already
`RedisLockMode.EXCLUSIVE`: the lock is owned, so there is
nothing to elect. It also returns False when any other holder
advertises ``elected``: that peer won a previous election and is
waiting for the readers to drain, so a fresh contender defers to
it instead of rerunning the sort against it (#143). Otherwise
the ids of every `RedisLockMode.PENDING` holder are sorted and
the lowest one wins.

Sorting is the entire trick, and it is worth spelling out why it
works. There is no coordinator here, and no lock to take before
taking the lock. What every contender does have is the result of
its own probe, and each probe asked the same channel at
approximately the same time, so they all see the same set of
pending ids. Sorting that set is a pure function of its contents,
so every contender independently computes the *same* winner, and
only the one whose own `holder_id` sorts first concludes that it
won. No message needs to be exchanged to agree.

Contenders whose probes disagree - someone joined halfway
through - do not corrupt the outcome, because a probe taken
during a membership change is discarded upstream:
`_collect_lock_holders` returns `None` when the subscriber count
moved. The losers simply retry, and a loser that keeps losing is
bounded by the caller's timeout, not by the election.

Winning here is also not the last word. A reply is a snapshot,
so a contender can win this sort from a reply that predates a
peer's own promotion - most easily the uncontended fast path,
which never probes at all. The promotion that follows a win is
therefore verified by `_confirm_exclusive_promotion` before
`acquire` reports success, and a winner whose view turns out
stale demotes there instead of standing on it (#145).

The deference rule is what makes an election stick. Winning is
remembered in `writer_elected` and advertised on every ping
reply, so later writers see the incumbent and stand down no
matter how their ids compare. Without it a lower-id newcomer
would win the rerun sort and usurp a writer that had already
been waiting (#143). Among fresh contenders the sort stays what
it always was: holder ids are uuid4 hex strings, so the order is
arbitrary but stable, a tie-break rather than a priority, and a
contender that loses it simply retries.

`RedisLockMode.SHARED` holders do not block the election, so a
writer can be elected while readers are still draining. Being
elected is not the same as holding the lock; only
`_resolve_lock_holders` converts election into ownership, and
only once no shared holder is left.

Args:
    holders: The holders returned by a conclusive probe,
        including this lock's own record.

Returns:
    True when this lock is the pending writer with the lowest
    holder id, nobody holds the lock exclusively, and nobody
    else advertises a won election.

Example:
    >>> from portalocker import redis
    >>> lock = redis.RedisLock('some_channel')
    >>> lock.holder_id = 'bbb'
    >>> pending = redis.RedisLockMode.PENDING
    >>> lock._writer_is_elected(
    ...     [
    ...         redis.RedisLockHolder('aaa', pending),
    ...         redis.RedisLockHolder('bbb', pending),
    ...     ]
    ... )
    False
    >>> lock._writer_is_elected(
    ...     [
    ...         redis.RedisLockHolder('bbb', pending),
    ...         redis.RedisLockHolder('ccc', pending),
    ...     ]
    ... )
    True

    Even the lowest id defers to an advertised incumbent:

    >>> lock._writer_is_elected(
    ...     [
    ...         redis.RedisLockHolder('bbb', pending),
    ...         redis.RedisLockHolder('zzz', pending, elected=True),
    ...     ]
    ... )
    False
c              3  óX   #   • U  H   oR                   [        R                  L v •  M"     g 7frm  )rM   rB   rF   ro  s     r   rq  Ú/RedisLock._writer_is_elected.<locals>.<genexpr>E
  s   é € ÐLÂG¸&�{‰{œm×5Ñ5Õ5ÂGùr¢  Fc              3  ó|   >#   • U  H1  nUR                   =(       a    UR                  TR                  :g  v •  M3     g 7frm  )rQ   rL   ©rp  rF  rb   s     €r   rq  r¨  G
  s3   øé € ð 
â!�ð �N‰N×A˜v×/Ñ/°4·>±>ÑAÔAÚ!ùs   ƒ9<c              3  óx   #   • U  H0  nUR                   [        R                  L d  M"  UR                  v •  M2     g 7frm  )rM   rB   rG   rL   ro  s     r   rq  r¨  L
  s1   é € ð /
â!�Ø�{‰{œm×3Ñ3Ð3ó ˆF×ÖÚ!ùs   ‚!:§:r   )rx  ÚsortedrN   rL   )rb   r�  Úpending_holder_idss   `  r   Ú_writer_is_electedÚRedisLock._writer_is_electedæ	  sw   ø€ ô~ ÑLÁGÓL×LÑLØÜô 
á!ó
÷ 
ñ 
ð Ü(.ñ /
á!ó/
ó )
Ðô
 Ø×JÐ#5°aÑ#8¸D¿N¹NÑ#Jó
ð 	
r   c                ó`  • U H¨  nUR                   U R                   :X  a  M  UR                  [        R                  L a    gUR                   U R                   :  nUR                  (       a	  U(       a    gUR                  [        R
                  L d  M�  UR                  b  MŸ  U(       d  M¨    g   g)a©  Report whether an elected writer must give its election up.

An election is worthless the moment somebody else demonstrably
outranks it, and this is the complete list of who does. Every
holder in a conclusive probe, excluding this lock's own record,
is checked against three rules:

1. A `RedisLockMode.EXCLUSIVE` holder. Somebody owns the lock,
   and legacy replies are recorded as exclusive too, so this
   rule also covers every peer the incumbent cannot reason
   about. The probe outranks our memory.
2. A holder advertising ``elected`` with a lower holder id. Two
   incumbents can arise when a ping was answered just before the
   winner's flag became visible, so both sides resolve the
   conflict from the same records: the lower id keeps the
   election, the higher id forfeits. That settles it once both
   flags are visible on the wire. For the round in which a
   stale reply still hides one of them, the hold-off in
   `_resolve_exclusive_writer` keeps either side from
   promoting past the other.
3. A `RedisLockMode.PENDING` holder whose record lacks the
   ``elected`` field (`RedisLockHolder.elected` is `None`) and
   whose id sorts below ours. That peer runs portalocker 4.0 or
   4.1, will elect itself by the id sort, and cannot be told to
   defer. Forfeiting reproduces the pre-4.2 semantics exactly,
   so a mixed channel is never less safe than 4.1, merely as
   unfair as it always was.

A 4.2 pending writer with a lower id and ``elected: false``
matches none of the rules, so the election is kept, but that
record alone does not make promotion safe: it cannot show
whether the peer probed before or after this election reached
the wire, so the peer may be electing itself from a stale view
right now. `_resolve_exclusive_writer` therefore keeps the
election without promoting while such a record is present, and
the next probe round tells a deferring peer (gone from the
channel) apart from a competing one (visible as elected or
exclusive, rules 1 and 2 above) (#143).

Args:
    holders: The holders returned by a conclusive probe,
        including this lock's own record.

Returns:
    True when the election must be forfeited and this writer
    should retry as a fresh contender.
TF)rL   rM   rB   rF   rQ   rG   )rb   r�  rF  Úlowers       r   Ú_must_forfeitÚRedisLock._must_forfeitU
  s‚   € óf ˆFØ×Ñ 4§>¡>Ó1ÙØ�{‰{œm×5Ñ5Ò5ÙØ ×*Ñ*¨T¯^©^Ñ;ˆEØ�~�~¦%Ùà—‘œ}×4Ñ4Ô4Ø—N‘NÓ*ß�Eáñ ð r   c                ó   • Ub  U R                  U5      (       a  gUb?  U R                  [        R                  R                  :X  a  U R                  UU5      nUb  U$ Uc  U R                  (       a  gU R                     SU l        SSS5        U R                  5         [        R                  SU R                  5        U(       a)  Uc  gU R                  5         [        R                  " 5       eg! , (       d  f       No= f)aI  Decide what to do with the result of one probe.

This is the only place where an observation becomes an action.
Every branch is guarded on ``holders is not None``, because an
inconclusive probe must never be read as "the lock is free". The
election branches themselves live in `_resolve_exclusive_writer`
and its `None` result routes into the give-up branch here.

The outcomes, in the order they are tested:

- **Compatible holders** (see `_holders_are_compatible`): the
  lock is now held. The subscription opened for the probe is
  exactly the subscription that holds it, so nothing more is
  needed.
- **Already-elected writer, conclusive probe**: the election is
  not rerun against newcomers. Unless `_must_forfeit` says the
  probe outranks it, the incumbent keeps waiting while
  `RedisLockMode.SHARED` holders drain and while any undecided
  lower-id peer is still visible, and promotes `mode` to
  `RedisLockMode.EXCLUSIVE` once the probe is clear of both
  (see `_resolve_exclusive_writer` for the hold-off). A forfeit
  falls through to the give-up branch below.
- **Fresh election won, no readers left** (see
  `_writer_is_elected`): `mode` is promoted to
  `RedisLockMode.EXCLUSIVE` and the lock is held. This runs
  before the `fail_when_locked` check, so the winner of two
  non-blocking contenders takes a reader-free channel instead of
  raising. The loser's probe shows the winner as pending with a
  winning id or as already exclusive, and it raises, so exactly
  one of the two succeeds (#143).
- **Fresh election won, readers still holding**: with
  `fail_when_locked` the lock genuinely cannot be taken without
  waiting for the readers, so everything is released and
  `AlreadyLocked` is raised, without ever setting
  `writer_elected`: this instance is about to leave the channel
  and must not advertise an election it will not use. A blocking
  caller instead remembers the win in `writer_elected` and keeps
  the subscription, so its `RedisLockMode.PENDING` record keeps
  new readers out while the existing ones drain, and its ping
  replies advertise the election so later writers defer.
- **Inconclusive probe while already elected**: the subscription
  is kept and the caller retries. An elected writer must not
  drop its pending record over a single noisy probe. Doing so
  would let new readers in and could forfeit an election it had
  already won, since `release` clears `writer_elected`.
- **Anything else**: somebody incompatible is there, the
  election was lost or forfeited, or the probe was inconclusive
  for an unelected lock. The subscription is dropped before
  retrying (`_unsubscribe`, which keeps the connection for the
  next attempt). That unsubscribe is the point rather than a
  detail: a waiter that stayed subscribed would keep inflating
  the subscriber count that everybody else's probe has to match
  exactly.

With `fail_when_locked` the caller does not want to wait for a
held channel, so a conclusive probe that reaches the give-up
branch releases everything and raises. An inconclusive probe is
different: it is noise, not contention, because nobody
demonstrably holds the channel, so the attempt returns False and
retries within the caller's timeout. That also lets a
non-blocking acquire succeed right after reaping a crashed
holder instead of failing spuriously. Callers that want a hard
single attempt pass ``timeout=0``, which bounds the retry loop
in `acquire` to one iteration.

Args:
    holders: A conclusive probe result, or `None` when the probe
        could not determine the state of the channel.
    fail_when_locked: Give up on conclusive contention instead
        of waiting for the holders to leave.

Returns:
    True when the lock is now held and `acquire` may return;
    False when the caller should retry.

Raises:
    AlreadyLocked: `fail_when_locked` was set and a conclusive
        probe showed the channel actually held.
NTFz#Redis lock %s unsubscribed to retry)r¤  r�   r   r¬   rF   Ú_resolve_exclusive_writerrŽ   r    r  r	   rÓ   rL   r_  r   ÚAlreadyLocked)rb   r�  r¨   Ú
resolutions       r   Ú_resolve_lock_holdersÚRedisLock._resolve_lock_holders˜
  sÐ   € ðh Ñ 4×#?Ñ#?À×#HÑ#HØàÑ 4§:¡:´×1DÑ1D×1NÑ1NÓ#NØ&*×&DÑ&DØØ ó'ˆJð Ñ%Ø!Ð!à‰?˜t×2×2Øà�_‹_Ø"'ˆDÔ÷ à×ÑÔÜ�‰Ð:¸D¿N¹NÔKÞØ‰ØØ�L‰LŒNÜ×*Ò*Ó,Ð,Ø÷ �_ús   Á>C/Ã/
C=c                ó°  ^ • [        S U 5       5      nT R                  (       ag  T R                  U5      (       a  gU(       a  g[        U 4S jU 5       5      nU(       a  gT R                     [        R
                  T l        SSS5        gT R                  U5      (       d  gU(       d2  T R                     ST l        [        R
                  T l        SSS5        gU(       a%  T R                  5         [        R                  " 5       eT R                     ST l        SSS5        g! , (       d  f       g= f! , (       d  f       g= f! , (       d  f       g= f)aU
  Decide for an exclusive writer holding a conclusive probe.

The election half of `_resolve_lock_holders`, split out so each
method carries one decision. An incumbent (``writer_elected``
set) never reruns the election: it keeps its win unless
`_must_forfeit` says the probe outranks it. A fresh contender
runs `_writer_is_elected`, and a winner promotes before the
`fail_when_locked` check so a non-blocking writer can take a
reader-free channel (#143).

The incumbent promotes only once the probe is clear of both
shared holders and undecided lower-id peers. A pending record
carrying ``elected: false`` from a lower id is undecided
because a ping reply is a snapshot that can be a full drain
interval old: it cannot show whether that peer probed before or
after this election became visible, so the peer may be about to
promote on its own stale view. Holding the promotion for one
round separates the cases, since a deferring peer has left the
channel by the next probe and a promoting one shows up as
exclusive or elected. Rerunning the election instead would
reintroduce the usurpation this branch exists to prevent, and
promoting anyway is exactly the double-holder interleaving the
#143 review reproduced.

The reply-staleness window around the uncontended fast path -
a contender promoting on a stale reply answered between another
writer's subscriber count and its fast-path promotion - is not
this method's to close: every promotion this method makes, the
`fail_when_locked` winners included, is verified against the
live channel by `_confirm_exclusive_promotion` before `acquire`
reports success, and the writer whose view was stale demotes
there (#145). What the confirm does and does not cover is
documented on that method.

Both promotion sites take `_mode_lock` and `writer_elected` is
assigned under it as well, so a ping answered by the worker
thread can never observe the ``(mode, elected)`` pair halfway
through a transition (see `channel_handler`).

Args:
    holders: The holders returned by a conclusive probe,
        including this lock's own record.
    fail_when_locked: Give up on conclusive contention instead
        of waiting for the holders to leave.

Returns:
    True when the lock is now held, False when the caller
    should keep the subscription and retry as an elected
    writer, or `None` when the election is lost or forfeited
    and `_resolve_lock_holders` should run its give-up branch.

Raises:
    AlreadyLocked: `fail_when_locked` was set, the election was
        won, and shared holders still hold the lock, raised
        after a full `release` and without ever setting
        `writer_elected`.
c              3  óX   #   • U  H   oR                   [        R                  L v •  M"     g 7frm  r   ro  s     r   rq  Ú6RedisLock._resolve_exclusive_writer.<locals>.<genexpr>C  s    é € ð #
Ú>E°F�K‰Kœ=×/Ñ/Õ/ºgùr¢  NFc              3  ó  >#   • U  Hv  nUR                   TR                   :g  =(       aQ    UR                  [        R                  L =(       a.    UR                  S L =(       a    UR                   TR                   :  v •  Mx     g7f)FN)rL   rM   rB   rG   rQ   rª  s     €r   rq  r¼  V  sq   øé € ð +ò
 &�Fð	 × Ñ  D§N¡NÑ2÷ 6Ø—K‘K¤=×#8Ñ#8Ð8÷6à—N‘N eÐ+÷6ð ×$Ñ$ t§~¡~Ñ5ô6ò &ùs   ƒA>BT)rx  rŽ   r²  r    rB   rF   rM   r®  r_  r   r¶  )rb   r�  r¨   Úshared_presentÚundecided_lower_ids   `    r   rµ  Ú#RedisLock._resolve_exclusive_writer  s  ø€ ô|  #ñ #
Ù>Eó#
ó  
ˆð ××Ø×!Ñ! '×*Ñ*ØÞØô (+ô +ñ
 &ó+ó (Ðö "ØØ—“Ü)×3Ñ3�”	÷ !àà×&Ñ& w×/Ñ/ØÞØ—“Ø&*�Ô#Ü)×3Ñ3�”	÷ !ð ÞØ�L‰LŒNÜ×*Ò*Ó,Ð,Ø�_‹_Ø"&ˆDÔ÷ à÷! !”àú÷
 !”ð ú÷ Œ_àús$   Á-D%Â6D6ÄEÄ%
D3Ä6
EÅ
Ec                ó  • [         R                  nU Hõ  nUR                  U R                  :X  a  M  UR                  [        R
                  L a  [         R                  s  $ UR                  U R                  :  nUR                  [        R                  L a9  UR                  (       d  UR                  b  U(       a  [         R                  s  $ M½  U(       d  MÆ  UR                  c  [         R                  s  $ [         R                  nM÷     U$ )aË  Judge one conclusive confirm probe of a freshly promoted writer.

Runs with this lock already advertising
`RedisLockMode.EXCLUSIVE` on the wire, so every rule reasons
about what a *rival* record means next to a promotion that is
now visible. The rules mirror `_must_forfeit`, applied to the
post-promotion shape of the same conflicts:

- A `RedisLockMode.SHARED` holder outranks the promotion. A
  reader can only coexist with an exclusive writer through a
  stale or churned sample on one of the two sides, and the
  reader holds without probing again, so the writer is the side
  that can still yield.
- A `RedisLockMode.EXCLUSIVE` rival that is legacy or pre-4.2
  (`RedisLockHolder.elected` is `None`) outranks the promotion
  regardless of id: such a rival never runs a confirm probe, so
  it will not demote itself.
- A 4.2 `RedisLockMode.EXCLUSIVE` rival resolves by the same
  deterministic rule two elected incumbents use: the lower id
  keeps, the higher id demotes. Seeing a lower id is a demote.
  A higher id is left to demote itself, which it does, because
  its own confirm cannot conclude while this lock is visible as
  pending (retry below) or as exclusive (this rule).
- A `RedisLockMode.PENDING` peer with a lower id and no
  ``elected`` field runs portalocker 4.0 or 4.1: it wins the id
  sort, cannot be told to defer and will promote, so the
  promotion is given up at once, exactly like `_must_forfeit`
  rule 3.
- A `RedisLockMode.PENDING` peer with a lower id that does
  speak 4.2 is undecided, the same ambiguity the #143 hold-off
  waits out: its reply cannot show whether its current probe
  predates this promotion, so it may be about to promote on a
  stale view. The round is inconclusive and the confirm asks
  again. By the next round the peer has either deferred (gone
  from the channel) or promoted (the exclusive rules above
  settle it).
- Higher-id pending peers lose the sort against a visible
  exclusive record and defer on their own, so they do not block
  the confirmation.

Args:
    holders: The holders returned by a conclusive probe,
        including this lock's own record.

Returns:
    The verdict, with `_ConfirmVerdict.DEMOTE` taking
    precedence over `_ConfirmVerdict.RETRY` when both apply.
)r:   r>   rL   rM   rB   rH   r@   rF   rO   rQ   r?   )rb   r�  ÚverdictrF  r±  s        r   Ú_confirm_probe_verdictÚ RedisLock._confirm_probe_verdictq  sÂ   € ôh $3×#<Ñ#<ˆÛˆFØ×Ñ 4§>¡>Ó1ÙØ�{‰{œm×2Ñ2Ò2Ü&×-Ñ-Ò-Ø ×*Ñ*¨T¯^©^Ñ;ˆEØ�{‰{œm×5Ñ5Ò5Ø—=—= F§N¡NÑ$:¾eÜ*×1Ñ1Ò1ÙÞÙØ�~‰~Ñ%Ü&×-Ñ-Ò-Ü%×+Ñ+ŠGñ ð ˆr   c                ó°  • U R                  U R                  S5       H¥  nU R                  U5      nUS::  a    gU R                  UUU R                  5      nUc  M>  U R	                  U5      n[
        R                  SU R                  UR                  U5        U[        R                  L a    gU[        R                  L d  M¥    O   U R                  U5        g)a7  Verify a fresh exclusive promotion against the live channel.

The promotion that led here was decided from ping replies, and
a reply is a snapshot that can predate the peer's own fast-path
promotion: the peer counted ``subscribers == 1`` moments
earlier and promoted without ever probing, while its worker
still answered this lock's ping as pending. Deciding from such
a reply is how two writers end up exclusive at once (#145).
This confirm runs after *every* promotion, fast path and
election alike, with the new `RedisLockMode.EXCLUSIVE` record
already visible on the wire: that visibility is what breaks the
symmetry, because from now on every fresh probe any peer takes
shows this lock as exclusive, and two freshly promoted rivals
each see the other and resolve deterministically by holder id
through `_confirm_probe_verdict`.

A subscriber count of one confirms without probing: ownership
*is* the subscription, so a rival that promoted is necessarily
still subscribed and counted, and being alone proves there is
nobody to conflict with. That single ``PUBSUB NUMSUB`` round
trip is the entire cost on the uncontended fast path, which is
why the confirm is unconditional rather than gated on a
count-moved heuristic: the recount is the heuristic, and unlike
one it is conclusive when it comes back one.

Inconclusive rounds (a churning count, a silent subscriber, an
undecided lower-id peer) are retried within an
`unavailable_timeout` budget, never concluded from. A confirm
that cannot reach one clean round demotes when the budget runs
out: giving up a promotion that could not be verified is the
safe direction, and costs one attempt in `acquire`'s retry
loop. Under `fail_when_locked` that exhausted budget is
terminal instead: the demotion releases fully and raises
`AlreadyLocked`, even when every round was inconclusive noise
rather than contention.

This is a channel-level check and deliberately distinct from
`_confirm_held`, the worker-death handshake: this method asks
"is my promotion consistent with everybody else", the state
handshake asks "is my own subscription still alive". The caller
runs them in that order, so a subscription dying mid-confirm is
still caught before `acquire` reports success.

Two residues stay open, and they bound what "confirmed" means.
Count-preserving churn can still hide a subscribed rival from a
single conclusive probe, this one included, but only when a
join lands in the instant between the probe's count pre-check
and its ping. Producing two confirmed holders needs that join
to hide the rival from the one confirm that would otherwise
demote, on top of the double promotion itself, so it is the
product of two rare events. And a pre-4.2 rival that promotes
on a stale view *after* this confirm concluded is not seen by
anybody, since it never confirms. Among 4.2+ writers
that ordering is impossible, because a rival mid-decision is
still subscribed and therefore visible to this confirm as
pending or exclusive.

Args:
    connection: The command connection to count and probe on.
    fail_when_locked: Forwarded to the demotion: a non-blocking
        writer raises instead of retrying.

Returns:
    True when the promotion stands. False when the writer
    demoted and the attempt should be retried.

Raises:
    AlreadyLocked: `fail_when_locked` was set and the writer
        demoted, raised after a full `release`.
Nr   Tz1Redis lock %s confirm probe verdict=%s holders=%rF)rì   rª   rÆ   rœ  rÃ  r	   rÓ   rL   rµ   r:   r>   r@   Ú_demote_unconfirmed_promotion)rb   rŠ   r¨   r™  Úsubscribersr�  rÂ  s          r   Ú_confirm_exclusive_promotionÚ&RedisLock._confirm_exclusive_promotion·  sÏ   € ðV ×(Ñ(¨×)AÑ)AÀ4ÖHˆAØ#×9Ñ9¸*ÓEˆKØ˜aÓÙØ48×4NÑ4NØØØ×(Ñ(ó5ˆGð
 ‰ÙØ'+×'BÑ'BÀ7Ó'KˆGÜ�L‰LØCØ—‘Ø—‘Øô	ð œ/×3Ñ3Ò3ÙØœ/×0Ñ0Ô0Ùñ+ Ið, 	×*Ñ*Ð+;Ô<Ør   c                ó@  • U R                      [        R                  U l        SU l        SSS5        U R                  5         [        R                  SU R                  5        U(       a%  U R                  5         [        R                  " 5       eg! , (       d  f       Nk= f)a-  Give up a promotion the confirm probe could not stand behind.

The mirror of the give-up branch in `_resolve_lock_holders`,
with the mode rolled back first: the lock is advertising
`RedisLockMode.EXCLUSIVE`, so the record returns to
`RedisLockMode.PENDING` (forgetting any election, exactly like
a forfeit) before the subscription is dropped. The next attempt
then rejoins as a fresh contender, and the rival that outranked
this promotion is visible to that attempt's ordinary probe.

Args:
    fail_when_locked: When set, the demotion is terminal for
        this acquisition: everything is released and
        `AlreadyLocked` is raised, mirroring how a conclusive
        lost election ends a non-blocking acquire.

Raises:
    AlreadyLocked: `fail_when_locked` was set, raised after the
        full release.
FNz7Redis lock %s demoted an unconfirmed promotion to retry)r    rB   rG   rM   rŽ   r  r	   rÓ   rL   r_  r   r¶  )rb   r¨   s     r   rÆ  Ú'RedisLock._demote_unconfirmed_promotion  ss   € ð* �_‹_Ü%×-Ñ-ˆDŒIØ"'ˆDÔ÷ ð 	×ÑÔÜ�‰ØEØ�N‰Nô	
ö Ø�L‰LŒNÜ×*Ò*Ó,Ð,ð ÷ �_ús   �BÂ
Bc                óÄ  • [         R                  " S[        R                  " XR                  S5      5      n[         R                  " S[        R                  " X R
                  S5      5      n[         R                  " S[        R                  " X0R                  S5      5      nU R                  (       a  U R                  5         U R                  b  [        R                  " S5      eU R                     SU l        [        R                  U l        SU l        SSS5        U R$                  [&        R(                  R*                  :X  a1  U R,                     [.        R0                  U l        SU l        SSS5         U R7                  5       n[8        R:                  " 5       U-   nSn	U R=                  UU5       HA  n
U	(       d  [8        R:                  " 5       U:¼  a    O!Sn	U R?                  Xv5      (       d  M?  U s  $     U RE                  5         [        RF                  " 5       e! , (       d  f       GN= f! , (       d  f       NÈ= f! [@         a    U RC                  5         e f = f)aþ  Acquire the lock, retrying until it is free or time runs out.

Every attempt subscribes to the channel if this lock is not
subscribed already, then either takes the lock outright or hands
a probe result to `_resolve_lock_holders`:

1. Count the subscribers. Being the only one means nobody else is
   on the channel, so the lock is free and is taken immediately;
   an exclusive lock promotes itself from `RedisLockMode.PENDING`
   to `RedisLockMode.EXCLUSIVE` here. This is the uncontended
   fast path and costs one round trip.
2. Otherwise probe the channel with `_collect_lock_holders` and
   let `_resolve_lock_holders` decide.
3. A writer that promoted - on either path - verifies the
   promotion against the live channel with
   `_confirm_exclusive_promotion` and demotes when a rival
   promoted on an equally stale view and outranks it. On the
   fast path this costs one extra subscriber count.

A failed attempt normally unsubscribes again, so the next
iteration subscribes from scratch; an elected writer is the
exception and holds on to its subscription between attempts.

Either way, success is only reported after `_confirm_held`
verified - under the state lock the worker's failure handler
also takes, and after the channel-level confirm above - that
the keep-alive worker is still alive and recorded no error. A
subscription that died in the microseconds after the winning
probe therefore costs one attempt instead of producing a lock
that is held in this process's imagination only.

`fail_when_locked` means the caller will not wait for a held
channel: the first conclusive probe showing the lock actually
held raises `AlreadyLocked` instead of polling the holder until
the timeout expires. A writer that wins the election on a
channel with no readers left is not blocked by anybody, so it
takes the lock rather than raising, and exactly one of two
non-blocking contenders succeeds on a free channel (#143).
Inconclusive probes are noise rather than contention and are
retried within the timeout. Pass ``timeout=0`` to bound a
non-blocking acquisition to exactly one attempt.

Transient connection trouble while merely *waiting* is scoped
to the attempt (#141): a subscribe or probe that fails with a
``redis_exceptions.ConnectionError`` or ``TimeoutError``, and a
keep-alive worker that dies before the lock is held, all count
as one failed attempt and are retried within the timeout
budget. Any other subscribe or probe failure releases
everything first and then propagates, with the lock left
inactive, off the channel and usable again - an error may
never strand a live subscription behind a failed ``acquire``,
because such a zombie record would block every other writer
until someone released this instance by hand.

Calling this on an instance whose previous hold ended in a loss
resets it: the recorded error is consumed, the dead
subscription is torn down, and the acquisition proceeds from
scratch.

Args:
    timeout: Seconds to keep retrying. Defaults to the instance's
        `timeout`, itself defaulting to `utils.DEFAULT_TIMEOUT`.
        Zero still makes exactly one attempt.
    check_interval: Base seconds to wait between attempts,
        jittered by `_timeout_generator`. Defaults to the
        instance's `check_interval`.
    fail_when_locked: Raise `AlreadyLocked` on the first
        conclusive probe that shows the channel held, instead of
        waiting for the holders to leave. Defaults to the
        instance's `fail_when_locked`.

Returns:
    This lock, so ``with RedisLock(...) as lock`` binds the lock
    itself rather than a file handle.

Raises:
    ~portalocker.exceptions.AlreadyLocked: The timeout expired
        without acquiring the lock, or `fail_when_locked` was set
        and a conclusive probe showed the channel held.
    ~portalocker.exceptions.LockException: This instance is
        already holding a lock. A `RedisLock` is not reentrant
        and holds at most one lock at a time; use a second
        instance, which gets its own `holder_id`. A single
        instance is not thread-safe either: two threads racing
        `acquire` on one instance can both pass this guard.
        Before 4.2.0 this misuse raised `AssertionError`, which
        ``python -O`` strips.

Example:
    >>> import fakeredis
    >>> import portalocker
    >>> connection = fakeredis.FakeStrictRedis(
    ...     server=fakeredis.FakeServer(), decode_responses=True
    ... )
    >>> lock = portalocker.RedisLock(
    ...     'some_channel', connection=connection
    ... )
    >>> lock.acquire(timeout=1) is lock
    True
    >>> lock.release()
r}   r¦   rN   FNzThis lock is already activeT)$r^   r_   r   Úcoalescerx   r§   r¨   r4   rQ  rc   r   rý   r  r�   r0   r6   r›   rŸ   r�   r   r¬   rF   r    rB   rG   rM   rŽ   r»   rä   rå   rì   Ú_acquire_attemptrp   Ú#_roll_back_terminal_acquire_failurer_  r¶  )rb   rx   r§   r¨   Úeffective_timeoutÚeffective_check_intervalÚeffective_fail_when_lockedrŠ   rê   Úfirst_attemptr™  s              r   ÚacquireÚRedisLock.acquire<  sà  € ôV $*§;¢;ØÜ�NŠN˜7§L¡L°#Ó6ó$
Ðô +1¯+ª+ØÜ�NŠN˜>×+>Ñ+>ÀÓDó+
Ð ô ,2¯;ª;ØÜ�NŠNÐ+×-BÑ-BÀEÓJó,
Ð"ð
 �9�9ð ×(Ñ(Ô*Ø�;‰;Ñ"Ü×*Ò*Ð+HÓIÐIØ×ÓØ#ˆDÔÜ)×3Ñ3ˆDÔð !%ˆDÔ÷ ð �:‰:œ×,Ñ,×6Ñ6Ó6Ø—“Ü)×1Ñ1�”	Ø&+�Ô#÷ !ð	Ø-1×-@Ñ-@Ó-BˆJÜ"ŸnšnÓ.Ð1BÑBˆHØ"&ˆMØ×,Ñ,Ø!Ø(ö�ö %¬¯ªÓ)9¸XÓ)EÙØ %�Ø×(Ñ(Ø÷ó ð  ’Kòð$ 	�‰ŒÜ×&Ò&Ó(Ð(÷E Öú÷ !•ûô$ ó 	ð ×4Ñ4Ô6Øð	ús7   Ã3$H ÅH2Å9A I ÇI Ç4I Ç8I È 
H/È2
I ÉIc                óÞ  • U R                   b%  U R                  5       (       a  U R                  5         U R                   c  U R                  U5      (       d  g U R	                  X5      $ ! [
        R                   a    e [         ae  n[        U5      (       a?  [        R                  SU R                  U R                  SS9  U R                  5          SnAgU R                  5         e SnAff = f)a~  Run one iteration of `acquire`'s retry loop.

Ensures a live subscription exists (abandoning one whose worker
died while waiting, see `_waiting_attempt_failed`), then runs
the probe half through `_probe_and_decide`.

Once the subscription is live, this instance is a counted,
ping-answering participant on the channel, so an error leaving
this method must never strand that subscription: a stranded one
is a zombie pending record that blocks every other writer while
this instance refuses its next ``acquire`` as already active.
A probe error that is mere connection weather therefore burns
this attempt (`_abandon_failed_attempt`, same as a transient
subscribe failure), and everything else - `AlreadyLocked` from
`_resolve_lock_holders` excepted, which already released - runs
a full `release` before propagating, interrupts included.

Args:
    connection: The command connection of this acquisition.
    fail_when_locked: Forwarded to `_resolve_lock_holders`.

Returns:
    True when the lock is now held and confirmed. False when
    this attempt failed and `acquire` should retry within its
    timeout budget.

Raises:
    AlreadyLocked: Propagated from `_resolve_lock_holders` when
        `fail_when_locked` is set and the channel is
        conclusively held, after its own release.
    BaseException: A non-transient subscription or probe
        failure, re-raised after the terminal rollback.
NFzmRedis lock %s lost its command connection while probing channel %r, the attempt is retried within the timeoutTr  )rc   rN  rQ  Ú_try_subscribeÚ_probe_and_decider   r¶  rp   r,   r	   r
   rL   rˆ   rÏ  )rb   rŠ   r¨   r   s       r   rÎ  ÚRedisLock._acquire_attemptÞ  sÕ   € ðL �;‰;Ñ" t×'CÑ'C×'EÑ'Eð ×(Ñ(Ô*Ø�;‰;Ñ t×':Ñ':¸:×'FÑ'FØð	Ø×)Ñ)¨*ÓGÐGøÜ×'Ñ'ó 	ð Üó 	Ü-¨e×4Ñ4Ü—‘ð"ð —N‘NØ—L‘LØ!ð ñ ð ×,Ñ,Ô.ÜØ×4Ñ4Ô6Øûð	ús   ÁA) Á)C,ÂA
C'ÃC'Ã'C,c                óÒ  • U R                  U5      n[        R                  SU R                  U R                  R
                  U5        US:X  aT  U R                  [        R                  R                  :X  a+  U R                     [        R                  U l        SSS5        OVOUU R                  UUU R                  5      n[        R                  SU R                  U5        U R                  XB5      (       d  gU R                  [        R                  R                  :X  a)  U R                  UU5      (       d  gU R!                  U5        U R#                  5       $ ! , (       d  f       No= f)aÞ  Probe the channel and turn the result into an attempt outcome.

The decision half of `_acquire_attempt`, run under its rollback
protection: takes the uncontended fast path or hands a probe to
`_resolve_lock_holders`, verifies an exclusive promotion
against the live channel through
`_confirm_exclusive_promotion`, draws the optional fencing
token through `_draw_fence_token`, and finally confirms the
win through `_confirm_or_abandon`. The confirm probe runs for
every exclusive promotion - the fast path and both election
paths - because each of them decides from information that can
be a reply-staleness window old. A shared join needs no
confirm, since readers admit each other and a conclusive probe
of pure readers has no promotion in it to race, and it draws
no fencing token either (see `fence_token`).

Args:
    connection: The command connection of this acquisition.
    fail_when_locked: Forwarded to `_resolve_lock_holders` and
        to the confirm probe's demotion.

Returns:
    True when the lock is now held and confirmed, False when
    the attempt failed.

Raises:
    AlreadyLocked: Propagated from `_resolve_lock_holders` or
        from a `fail_when_locked` confirm demotion when the
        channel is conclusively held.
z-Redis lock %s mode=%s observed %d subscribersr   Nz!Redis lock %s observed holders=%rF)rÆ   r	   rÓ   rL   rM   rµ   r�   r   r¬   rF   r    rB   rœ  rª   r¸  rÈ  Ú_draw_fence_tokenÚ_confirm_or_abandon)rb   rŠ   r¨   rÇ  r�  s        r   rØ  ÚRedisLock._probe_and_decide   s(  € ðF  ×5Ñ5°jÓAˆÜ�‰Ø;Ø�N‰NØ�I‰I�O‰OØô		
ð ˜!ÓØ�z‰zœY×0Ñ0×:Ñ:Ó:Ø—_“_Ü -× 7Ñ 7�D”I÷ %�_ð ;ð 59×4NÑ4NØØØ×(Ñ(ó5ˆGô
 �L‰LØ3Ø—‘Øôð
 ×-Ñ-¨g×HÑHØØ�:‰:œ×,Ñ,×6Ñ6Ó6Ø×4Ñ4ØØ ÷ñ ð Ø×"Ñ" :Ô.Ø×'Ñ'Ó)Ð)÷- %•_ús   ÂEÅ
E&c                óÖ   •  U R                  U5        g! [         aJ  n[        U5      (       d  U R                  5         e [        R                  SU R                  SS9   SnAgSnAff = f)a©  Subscribe for one attempt, absorbing transient failures.

A subscribe that dies with a connection or timeout error is the
waiter-side blip `acquire` promises to tolerate: the rollback
inside `_start_subscription` already ran (keeping the command
connection alive for the retry, which is what keeps the ping
handler answerable after the blip), so this only logs and
reports the attempt as failed. Every other error propagates,
because an unexpected failure should surface rather than burn
the whole timeout retrying it, and on that terminal path a full
`release` runs first so a lock-created command connection is
closed exactly when nobody is going to retry on it.

Not every ``ConnectionError`` is a blip: redis-py derives its
credential and pool-exhaustion failures from it, and those
repeat identically on every retry, so
`_is_transient_connection_error` routes them onto the same
terminal path. A wrong password therefore raises
``AuthenticationError`` promptly instead of burning the whole
timeout and ending in a misleading ``AlreadyLocked``.

Args:
    connection: The command connection to derive the
        subscription client from.

Returns:
    True when the subscription is live, False when the attempt
    failed transiently and should be retried.

Raises:
    Exception: The non-transient subscription failure, re-raised
        after the terminal cleanup. A cleanup failure is logged
        so it cannot replace the original error.
z>Redis lock %s could not subscribe, retrying within the timeoutTr  NF)r  r  r,   rÏ  r	   r
   rL   )rb   rŠ   r   s      r   r×  ÚRedisLock._try_subscribed  sl   € ðF	Ø×$Ñ$ ZÔ0ð øô ó 
	Ü1°%×8Ñ8Ø×8Ñ8Ô:ØÜ�N‰Nðà—‘Øð	 ñ ô ûð
	ús   ‚ ”
A(žA A#Á#A(c                ó„   •  U R                  5         g! [         a"    [        R                  SU R                  SS9   gf = f)aÀ  Restore the fully inactive state before an error propagates.

The terminal arm shared by `_try_subscribe` and
`_acquire_attempt`: when a subscription or probe failure is
about to leave `acquire`, a full `release` runs so the
subscription stops being counted and a lock-created command
connection is closed exactly when nobody is going to retry on
it. A cleanup failure is logged so it cannot replace the
original error, which the caller re-raises.
z=Redis lock %s failed to roll back after a failed subscriptionTr  N)r_  r  r	   r
   rL   rj   s    r   rÏ  Ú-RedisLock._roll_back_terminal_acquire_failure–  s?   € ð	Ø�L‰L�NøÜó 	Ü�N‰Nðà—‘Øð	 ó ð	ús   ‚ “)?¾?c                óP   • U R                  5       (       a  gU R                  5         g)ac  Confirm a won attempt, or abandon it for the next round.

Wraps `_confirm_held` so every `acquire` success site treats a
refused confirmation the same way: the attempt is consumed by
`_abandon_failed_attempt` and the retry loop carries on.

Returns:
    True when the lock is held, False when the attempt failed at
    the last moment and was cleaned up.
TF)rK  rQ  rj   s    r   rÜ  ÚRedisLock._confirm_or_abandon«  s%   € ð ×Ñ×ÑØØ×$Ñ$Ô&Ør   c                ó    • U R                    S3$ )aÏ  Name of the key the fencing counter lives in.

One counter per channel, shared by every fencing-enabled
writer on it, so tokens drawn by different processes are
ordered by the same ``INCR``. The key never expires, by
design: monotonicity has to survive idle periods, so `release`
deliberately leaves it behind and nothing in this class ever
deletes it. Delete it by hand only when no fenced resource
remembers a token from it any more, because a reset counter
hands out tokens that stale resources would consider current.

Returns:
    ``<channel>-fence``.

Example:
    >>> from portalocker import redis
    >>> redis.RedisLock('some_channel', fencing=True).fence_key
    'some_channel-fence'

.. versionadded:: 4.2.0
z-fencerá   rj   s    r   Ú	fence_keyÚRedisLock.fence_key»  s   € ð. —,‘,�˜vÐ&Ð&r   c                óh   • U R                      U R                  sSSS5        $ ! , (       d  f       g= f)a®  Token of the current fenced grant, or the most recent one.

`None` until this lock - constructed with ``fencing=True`` and
exclusive flags - completes an exclusive grant, and from then
on the token drawn for that grant. The value deliberately
survives a loss and a `release`, so bare
``acquire()``/``release()`` callers and forensic logging can
still read which token the hold carried. The next `acquire`
resets it to `None`. With fencing enabled the lock is never
reported held without a token, because the token is drawn
before `_confirm_held` runs.

Shared holders never carry a token, ``fencing=True`` or not:
shared grants coexist, so a per-grant monotonic token would
order nothing. Fencing is a writer-side guarantee.

The guarantee is also only as wide as the writers that
participate: a holder running portalocker 4.1 or older, or a
4.2+ writer constructed without ``fencing=True``, takes the
channel without bumping the counter, so the tokens order
exactly the exclusive grants of fencing-enabled writers and
nothing else. Mixed channels degrade silently, which is one of
the reasons fencing is opt-in.

Returns:
    The token, or `None` when no fenced grant happened since
    the last `acquire` started.

Example:
    >>> import fakeredis
    >>> import portalocker
    >>> connection = fakeredis.FakeStrictRedis(
    ...     server=fakeredis.FakeServer(), decode_responses=True
    ... )
    >>> lock = portalocker.RedisLock(
    ...     'fenced_channel', connection=connection, fencing=True
    ... )
    >>> lock.fence_token is None
    True
    >>> with lock:
    ...     lock.fence_token
    1
    >>> lock.fence_token  # survives release until the next acquire
    1

.. versionadded:: 4.2.0
N)r  rŸ   rj   s    r   Úfence_tokenÚRedisLock.fence_tokenÔ  s#   € ðb ×ÓØ×$Ñ$÷ ××ús   �#£
1c                ó   • U R                   (       d  g[        UR                  U R                  5      5      nU R                     X l        SSS5        [        R                  SU R                  UU R                  5        g! , (       d  f       N;= f)aš  Draw the fencing token for a just-confirmed exclusive grant.

Runs after `_confirm_exclusive_promotion` and before
`_confirm_held`, which is what upholds the fencing invariant:
with fencing enabled the lock can never report held without a
token, because an ``INCR`` that fails leaves the state at
`_LockState.ACQUIRING` and the attempt never confirms.

The failure discipline is `_acquire_attempt`'s, shared with
every other command on the acquire path: a transient
connection error burns this attempt and the retry draws a
fresh token (a token possibly burned by the failed attempt is
a gap in the counter, which monotonicity does not mind), while
anything else - most likely a ``WRONGTYPE`` reply because
something unrelated wrote to `fence_key` - releases everything
and propagates, since it would repeat identically on every
retry and burning the timeout on it would end in a misleading
``AlreadyLocked``.

A no-op when `fencing` is disabled. Only exclusive grants
reach this method, since shared joins return earlier in
`_probe_and_decide`.

Args:
    connection: The command connection to run ``INCR`` on.

Raises:
    Exception: Whatever ``INCR`` raised, unhandled here.
Nz/Redis lock %s drew fence token %d on channel %r)
r–   ÚintÚincrrå  r  rŸ   r	   rÓ   rL   rˆ   )rb   rŠ   Útokens      r   rÛ  ÚRedisLock._draw_fence_token  sb   € ð< �|�|ØÜ˜Ÿ™¨¯©Ó8Ó9ˆØ×ÓØ %Ô÷ ä�‰Ø=Ø�N‰NØØ�L‰Lõ		
÷ Õús   ÁA?Á?
Bc                ó`  • Ub  UOU R                   nU R                  5       nU R                  US5       H7  nU R                  U5      nUS:X  a  / s  $ U R	                  UUUSS9nUc  M5  Us  $    [
        R                  " [
        R                  R                  SU R                  < SU S35      e)a	  Report who currently holds the channel, changing nothing.

The read-only companion to `acquire` and the replacement for the
deprecated `check_or_kill_lock`: it publishes the same liveness
ping a real acquisition would, collects the answers, and stops
there. No connection is killed, no subscription outlives the
call, and the lock's own state does not change, so this is safe
to run against a channel in production to see who is on it.

When this lock currently holds the channel its own record is
part of the answer, since its keep-alive worker answers the ping
like any other holder's.

Args:
    timeout: Seconds to wait for the holders to answer,
        defaulting to `unavailable_timeout`. Inconclusive probes
        are retried within this budget.

Returns:
    One `RedisLockHolder` per subscriber, or an empty list when
    nobody is subscribed to the channel.

Raises:
    ~portalocker.exceptions.LockException: The channel stayed
        inconclusive for the whole timeout: subscribers kept
        joining or leaving mid-probe, or a counted subscriber
        never answered. An unanswered probe is deliberately not
        reported as an empty channel, because treating it as
        one is exactly the misreading that hands out double
        locks; `acquire` is the code path that may reap such a
        silent subscriber.

Example:
    >>> import fakeredis
    >>> import portalocker
    >>> connection = fakeredis.FakeStrictRedis(
    ...     server=fakeredis.FakeServer(), decode_responses=True
    ... )
    >>> lock = portalocker.RedisLock(
    ...     'probed_channel', connection=connection
    ... )
    >>> lock.probe()
    []
    >>> _ = lock.acquire()
    >>> [holder.mode.value for holder in lock.probe()]
    ['exclusive']
    >>> lock.release()

.. versionadded:: 4.2.0
Nr   F)r˜  zRedis lock channel z) could not be probed conclusively within r  )	rª   r»   rì   rÆ   rœ  r   rý   rþ   rˆ   )rb   rx   rÐ  rŠ   r™  rÇ  r�  s          r   ÚprobeÚRedisLock.probe2  sÑ   € ðn Ñ*‰G°×0HÑ0Hð 	ð *.×)<Ñ)<Ó)>ˆ
Ø×(Ñ(Ð):¸DÖAˆAØ#×9Ñ9¸*ÓEˆKØ˜aÓØ’	Ø48×4NÑ4NØØØ!Øð	 5Oð 5ˆGð Ó"Ø’ñ Bô ×&Ò&Ü×$Ñ$×0Ñ0Ø! $§,¡,Ñ!1ð 2#Ø#4Ð"5°Xð?ó
ð 	
r   c           
     ó&  • [         R                  " S[        SS9  U R                   S[        R                  " 5        3n[        U R                  US-  5      nU R                  U5      n UR                  U5        U R                  X$5       HG  n[        R                  " SUR                  US95      nU(       d  M0  UR                  S5      S	:X  d  MG    O   UR                  U R                  [        R                   " [#        US
S95      5        U R                  X$5       HW  n[        R                  " SUR                  US95      nU(       d  M0  UR                  S5      S:X  d  MG    UR%                  5         g   UR'                  S5      n	U	 HZ  n
U
R                  S5      U R(                  :X  d  M$  [*        R-                  SU
5        UR/                  U
R                  S5      5        M\      UR%                  5         g! UR%                  5         f = f)aX  Ask whether anyone is alive on the channel, and reap if not.

.. deprecated:: 4.2.0
    Use `probe` for a read-only view of the channel; the
    reaping of crashed holders happens inside `acquire`, where
    the protocol's timeout discipline protects live-but-slow
    holders from a caller-chosen timeout. This method will be
    removed in portalocker 5.0.0.

The public liveness check from before 4.0.0. `acquire` no longer
uses it: it probes with `_collect_lock_holders` and reaps with
`_kill_unavailable_locks`, which understand individual holders
and lock modes. This method predates holder ids and answers only
the coarser question "is anybody answering on this channel?".

The ping is published only after the subscription's own
confirmation frame has been consumed, or the wait for it times
out. Redis queues a ``subscribe`` confirmation the moment a
subscription is made; before 4.0.0 the reply poll accepted any
message, so that confirmation was read as a reply, a crashed
holder was reported as alive and stale locks were never reaped.
The poll below now also requires ``type == 'message'``, and
draining the confirmation first guarantees that the subscription
is active before the ping goes out, so a real reply cannot be
published into a subscription that is not listening yet.

Note:
    The reap step matches ``CLIENT LIST`` entries against
    `client_name`, which since 4.0.0 carries this instance's own
    `holder_id`. On a live server that name identifies only this
    lock's own connection, so another process's crashed holder is
    not matched here; reaping across holders is what
    `_kill_unavailable_locks` does during `acquire`.

Args:
    connection: The connection to probe and to reap on.
    timeout: Seconds to wait for the subscribe confirmation and,
        separately, for a reply, so a fully silent channel can
        take up to twice this long.

Returns:
    True as soon as any reply arrives. `None` when nothing
    answered in time, after killing the matching pubsub
    connections. False is never returned.

Warns:
    DeprecationWarning: Always, naming `probe` as the
        replacement.
z¨check_or_kill_lock is deprecated and will be removed in portalocker 5.0.0; use probe() for a read-only view of the channel. Crashed holders are reaped inside acquire().ró   r  rÜ   r¡   r  r  rÉ   r  r8  )rÌ   rÊ   rÊ   Trc   r#   ru  rv  N)r"  r#  r$  rˆ   rè   rç   r©   r¿   r  rì   r^   r_   rz   rÎ   rÔ   rÏ   rÕ   r«   r{   r|  rÞ   r	   r
   r  )rb   rŠ   rx   rÌ   r§   rc   r™  r  rÊ   r…  r†  s              r   Úcheck_or_kill_lockÚRedisLock.check_or_kill_lock~  sÓ  € ôl 	�ŠðDô Øò	
ð #Ÿl™l˜^¨1¬V¯]ª]«_Ð,=Ð>ÐÜ˜T×3Ñ3°W¸r±\ÓBˆà×!Ñ! *Ó-ˆð-	Ø×ÑÐ-Ô.ð ×,Ñ,¨WÖE�Ü%Ÿ{š{Ø2Ø×&Ñ&¨~Ð&Ð>ó �÷  �< L×$4Ñ$4°VÓ$<ÀÕ$KÙñ Fð ×ÑØ—‘Ü—
’
ÜØ)9Ø &ñóôð ×,Ñ,¨WÖE�Ü Ÿ+š+Ø2Ø×&Ñ&¨~Ð&Ð>ó�÷ �7˜wŸ{™{¨6Ó2°iÕ?Øð �L‰L�Nñ% Fð -7×,BÑ,BÀ8Ó,LˆGÛ"�Ø—;‘;˜vÓ&¨$×*:Ñ*:Õ:Ü—N‘NØ>Øôð ×1Ñ1°'·+±+¸dÓ2CÖDñ #ð à�L‰L�NøˆF�L‰L�Nús2   Á*AG> Â<G> ÃA9G> ÅG> Å'G> Å95G> Æ2:G> Ç>Hc                ó`   • U R                   nUSL =(       d    U[        R                  " 5       :H  $ )aä  Report whether this process created the current subscription.

A forked child inherits the lock object together with the
parent's sockets, so a teardown running in the child must not
talk to them: an UNSUBSCRIBE sent over the inherited socket
releases the *parent's* lock, and the parent - whose connection
stays perfectly healthy - is never told (redis-py's connection
pools guard the same hazard with their ``_checkpid`` dance).
`_unsubscribe` and `release` therefore only drop local
references when this returns False.

Returns:
    True when no subscription exists or the current pid is the
    one `_start_subscription` recorded, so socket teardown is
    safe. False in a process that inherited the subscription
    through ``fork``.
N)rš   r	  r
  )rb   Úsubscription_pids     r   Ú_in_subscribing_processÚ!RedisLock._in_subscribing_processï  s,   € ð$ (,×'=Ñ'=ÐØ 4Ð'×JÐ+;¼r¿yºy»{Ñ+JÐJr   c                óú  • SnU R                  5       nSU l        U R                  nSU l        Ubc  U(       a\   UR                  5         UR                  b>  U[
        R                  " 5       La&  UR                  5         [        R                  " S5        U R                  nSU l        UbX  U(       aQ   [        R                  " SUR                  5      nUb  UR!                  U R"                  5         UR%                  5         U R'                  UUS9nUb  Ueg! [         a  n[        X5      n SnANžSnAff = f! [         a  n[        X5      n SnANcSnAff = f! [         a  n[        X5      n SnANtSnAff = f)a"  Drop the subscription but keep the command connection.

Stops and joins the keep-alive thread, closes the pubsub
(unsubscribing first when the pubsub still owns a connection),
and closes the dedicated subscription client and its pool.
This is the back-off between attempts: `_resolve_lock_holders`
calls it after an unsuccessful probe so that a waiting lock
stops being counted as a subscriber, and the next attempt
subscribes from scratch, on a fresh subscription client but the
same command connection.

In a forked child (see `_in_subscribing_process`) all of that
shrinks to dropping the references: the thread does not exist
in the child, and every socket in sight is the parent's live
subscription, which the child may not touch.

The teardown is exception safe. `thread` and `pubsub` are
cleared before their cleanup steps run, every step runs even
when an earlier one fails, and only the first failure is
re-raised once the rest has run (see `_keep_first_error`). One
failed unsubscribe therefore cannot leave a stale `pubsub`
behind that would trip the ``assert not self.pubsub`` guard on
every later `acquire`.

The unsubscribe is skipped when the pubsub no longer owns a
connection. The worker thread closes the pubsub itself when it
is stopped (redis-py's ``PubSubWorkerThread.run`` behaviour), so
in the normal release path the subscription is already gone by
the time this method looks at it. Unsubscribing then would check
a fresh connection out of the pool and reconnect purely to
unsubscribe from a subscription the server already discarded.
Only a pubsub that still owns a connection - the
`_start_subscription` rollback path, where the thread never
ran - has anything left to unsubscribe.

A thread is only joined when its `start` succeeded. A thread
whose `start` raised has no ``ident`` yet, and joining it would
raise ``RuntimeError`` instead of the error that actually broke
the acquire. ``is_alive`` would be the wrong test: a thread that
already finished is no longer alive but can still be joined.

The join is also skipped when this method runs *on* the worker
thread itself, because joining the current thread raises
``RuntimeError`` too. That is a supported path, not an anomaly:
an ``on_lost`` callback calling `release` executes on the
worker thread, and the worker is already stopping by then, so
skipping the join only means the thread finishes on its own a
moment after the release returns.

Keeping the connection alive here is not an optimisation but a
correctness requirement. `channel_handler` answers pings over
`connection` from the worker thread, and `acquire` keeps working
with the connection it fetched before its retry loop. Closing
and clearing the connection between attempts is exactly the bug
that used to interrupt the whole process: the next attempt
resubscribed on the stale reference while ``self.connection``
was `None`, the handler's assert fired on the worker thread, and
`PubSubWorkerThread.run` escalated it to the main thread.

Raises:
    Exception: The first error any teardown step raised,
        re-raised after the remaining steps have run.
Ng{®Gáz„?zobject | None)Úteardown)r÷  rš   r‡   rk   Úidentr`   Úcurrent_threadÚjoinrä   ræ   r  r   rc   r^   r_   rŠ   Úunsubscriberˆ   r{   Ú_close_subscription_client)rb   r   Úsame_processr‡   r   rc   Úpubsub_connections          r   r  ÚRedisLock._unsubscribe  sk  € ð@ )-ˆØ!×9Ñ9Ó;ˆØ!%ˆÔà,0¯K©KˆØˆŒð Ñ¦,ð	DØ—‘”à—L‘LÑ,Ø¤i×&>Ò&>Ó&@Ò@à—K‘K”MÜ—J’J˜tÔ$ð .2¯[©[ˆØˆŒØÑ¦,ðDô
 4:·;²;Ø#Ø×%Ñ%ó4Ð!ð %Ñ0à×&Ñ&ØŸ™ôð
DØ—‘”ð ×5Ñ5ØØ!ð 6ð 
ˆð
 Ñ"ØÐð #øôA ó DÜ/°ÓC•ûðDûô( ó DÜ/°ÓC•ûðDûô ó DÜ/°ÓC•ûðDúsH   ¸AD Â1?D9 Ã1E Ä
D6Ä!D1Ä1D6Ä9
EÅEÅEÅ
E:Å%E5Å5E:c                ó&  • U R                   nSU l         Ub5  U(       a.   UR                  5          UR                  R                  5         U$ U$ ! [         a  n[        X5      n SnAN9SnAff = f! [         a  n[        X5      n SnAU$ SnAff = f)a  Close the dedicated subscription client and its pool.

The client is per attempt and always owned by this lock, also
when it came from `subscription_connection_factory`, so it goes
down with the subscription. Its pool is disconnected explicitly
because ``Redis.close`` leaves an externally supplied pool
alone, and the pubsub connection was checked out of exactly
that pool. Follows the same exception-safe teardown discipline
as the rest of `_unsubscribe`: the reference is cleared first,
both steps always run, failures are folded through
`_keep_first_error`.

Args:
    first_error: The error the surrounding teardown kept so
        far, or `None`.
    teardown: Whether the client's sockets may be touched at
        all. `_unsubscribe` passes False in a forked child,
        where the reference is dropped but the connections
        belong to the parent.

Returns:
    The error the caller should keep: `first_error` when it was
    already set, otherwise the first failure raised here, or
    `None` when everything succeeded.
N)r˜   r{   r  r   rö   Ú
disconnect)rb   r   rú  r  r   s        r   rÿ  Ú$RedisLock._close_subscription_clienty  sœ   € ð> ×%Ñ%ð 	ð %)ˆÔ!ØÑ*®xðDØ#×)Ñ)Ô+ðDØ#×3Ñ3×>Ñ>Ô@ð Ðˆ{Ðøô ó DÜ/°ÓC•ûðDûô ó DÜ/°ÓC”ØÐûðDús.   ŸA °A0 Á
A-ÁA(Á(A-Á0
BÁ:BÂBc                ót  • SnU R                  5       nU R                     SU l        SSS5        U R                     U R                  [
        R                  La  [
        R                  U l        SU l        SSS5         U R                  5         U R                  (       a8  U R                  b+  U R                  nSU l        U(       a   UR                  5         Ub  Ueg! , (       d  f       N¼= f! , (       d  f       N= f! [         a  nUn SnAN‚SnAff = f! [         a  n[        X5      n SnAN[SnAff = f)a8	  Give up the lock and undo everything `acquire` set up.

Stops and joins the keep-alive thread, closes the pubsub (see
`_unsubscribe`), and forgets any election this lock had won. A
connection the lock created itself is closed and cleared, so the
next `get_connection` builds a fresh one. A connection supplied
by the caller is left alone.

Dropping the subscription is not merely cleanup, it *is* the
release: other processes learn the lock is free by no longer
seeing this subscriber, with no key to delete and no expiry to
wait for.

The teardown is exception safe: every step runs even when an
earlier one fails, `thread`, `pubsub` and a self-created
`connection` are cleared regardless, and only the first failure
is re-raised once everything has run (see `_keep_first_error`).
A release interrupted by a dead Redis therefore still leaves the
instance ready for a later `acquire` instead of permanently
tripping its ``assert not self.pubsub`` guard.

This is the terminal teardown. The back-off between attempts is
`_unsubscribe`, which keeps the connection so the retry loop and
the ping handler can keep using it; this method is for when the
lock is done, either released by the caller or giving up with
`AlreadyLocked`. `_try_subscribe` also calls it when a
subscription failure is about to propagate out of `acquire`,
which is terminal too.

Calling this when nothing was acquired is harmless - it still
closes a self-created connection if one exists - which is what
makes both that rollback and `__del__` safe.

In a forked child every socket in sight belongs to the parent,
so the teardown only drops this process's references (see
`_in_subscribing_process`): the parent keeps its lock, and the
child's `release` (or garbage collection) cannot silently
revoke it. The child must not otherwise use an inherited lock
instance; it should build its own.

A loss is *not* erased here: releasing a lock that was revoked
while held leaves `lost` True (and the causal error in place)
until the next `acquire`, so the loss stays observable after
the teardown - the ``with`` block exit and code checking `lost`
after a bare `release` both rely on that. Releasing never
raises on account of a loss; only genuine teardown failures
propagate.

Raises:
    Exception: The first error any teardown step raised,
        re-raised after the remaining steps have run.
NF)r÷  r    rŽ   r  r›   r0   r8   r5   r�   r  r  r‹   rŠ   r{   r   )rb   r   r   r   rŠ   s        r   r_  ÚRedisLock.release¦  s  € ðj )-ˆð "×9Ñ9Ó;ˆØ�_‹_Ø"'ˆDÔ÷ à×ÓØ×Ñ¤z§¡Ò6Ü#-§?¡?�Ô Ø#'�Ô ÷ ð	 Ø×ÑÔð × ×  T§_¡_Ñ%@Ø-1¯_©_ˆJØ"ˆDŒOÞðHØ×$Ñ$Ô&ð Ñ"ØÐð #÷3 �_úçÕûô ó 	 Ø�Kûð	 ûô !ó HÜ"3°KÓ"G•KûðHúsF   ŸC»:C.Á>C? ÃD Ã
C+Ã.
C<Ã?
DÄ	DÄDÄ
D7Ä"D2Ä2D7c                óŒ   • [         R                  " [        5         U R                  5         SSS5        g! , (       d  f       g= f)a{  Release the lock when the object is garbage collected.

A safety net for a lock that was never released explicitly, not a
substitute for `release` or a ``with`` block. CPython's reference
counting usually runs this promptly, but PyPy collects whenever
it sees fit, and nothing runs at all if the interpreter exits
while a reference is still alive.

That last case is harmless here, which is the advantage of
holding a lock in a connection rather than in a key: the process
exits, the daemon reader thread goes with it, the socket closes
and Redis releases the lock.

Every error is suppressed here, deliberately. A
finalizer often runs during interpreter shutdown, where even the
import a reconnect attempt triggers can fail (``ImportError:
sys.meta_path is None``), and raising from here only produces an
"Exception ignored in" message that the code dropping the
reference can never catch.
N)Ú
contextlibÚsuppressr  r_  rj   s    r   Ú__del__ÚRedisLock.__del__ü  s'   € ô* × Ò ¤Õ+Ø�L‰LŒN÷ ,×+Ö+ús	   ›5µ
A)rŸ   r—   r›   r�   r    rž   r˜   rš   rˆ   r‹   rŠ   r–   r�   rL   r‘   rM   r�   rc   r†   r•   r“   r‡   r©   rª   rŽ   )rˆ   rK   rŠ   r‰   rx   r”   r§   r”   r¨   rP   r©   r}   rª   r}   r†   r  r�   rŒ   r�   r�   r‘   rP   r“   r’   r•   r”   r–   rN   r~   r   )r~   úredis.client.Redis)rŠ   r  r~   r-   )r~   z,tuple[threading.Lock | threading.RLock, ...])rŠ   r  r~   rë  )rÊ   zdict[str, str]r~   r   )r~   rK   )rx   r”   r§   r”   r~   ztyping.Iterator[int])rŠ   r  r~   r  )rŠ   r  r~   r   )rc   r-   r~   r   )r   rp   rc   r-   r%  r.   r~   r   r€   )r0  r-   r~   r   )r?  r-   r0  r-   rê   r}   r>  r}   rB  z.typing.Callable[[dict[str, typing.Any]], bool]r~   rN   )r5  r…   r~   rN   )r~   rN   )r~   zexceptions.LockLostError)ra  ztype[BaseException] | Nonerb  rœ   rc  rY   r~   rP   )r×   rY   rh  rë  r~   rJ   )rŠ   r  r€  z typing.Iterable[RedisLockHolder]r~   r   )
rc   r-   r�  zdict[str, RedisLockHolder]r’  rë  r§   r}   r~   r   )T)
rŠ   r  r’  rë  rx   r}   r˜  rN   r~   úlist[RedisLockHolder] | None)r�  úlist[RedisLockHolder]r~   rN   )r�  r  r¨   rN   r~   rN   )r�  r  r¨   rN   r~   rP   )r�  r  r~   r:   )rŠ   r  r¨   rN   r~   rN   )r¨   rN   r~   r   )NNN)rx   r”   r§   r”   r¨   rP   r~   r„   )rŠ   r  r~   rN   )r~   r™   rm  )rx   r”   r~   r  )rŠ   r  rx   r}   r~   rP   )r   úException | Nonerú  rN   r~   r  )Fr   r   r   r   r   rR   rc   r«   r¤   ÚDEFAULT_THREAD_SLEEP_TIMEÚDEFAULT_UNAVAILABLE_TIMEOUTr   r¬   rF   r]   r»   r¿   rÂ   rÆ   rÙ   ÚpropertyrÞ   rÝ   rì   r  r  r  r  r  r  r/  r;  r<  rK  rN  rQ  r4   rW  rV  re  rE  r‰  r•  rœ  r¤  r®  r²  r¸  rµ  rÃ  rÈ  rÆ  rÔ  rÎ  rØ  r×  rÏ  rÜ  rå  rè  rÛ  rð  ró  r÷  r  rÿ  r_  r  r   r�   r‚   s   @r   r„   r„   ^  sf  ø‡ ñ[ðz (Ó'Ø%Ó%ØƒLØƒNØ)Ó)Ø)-€FÐ&Ó-ØÓØÓØƒNØ
ÓØÓà6Ó6ð Óð 	7óð
 &Ó%ð ƒMð !Ó ð
 4Ó3ð "Ó!ð Óð &Ó%ð
 Óð Óð ÓáCGØ ØñDÐÐ@ó ð 15Ø $Ø'+Ø(-Ø#<Ø%@Ø59Ø%.×%8Ñ%8×%BÑ%BØ=AØ)-ð Ø,0Øð#l
àðl
ð .ðl
ð ð	l
ð
 %ðl
ð &ðl
ð !ðl
ð #ðl
ð 3ðl
ð #ðl
ð ;ðl
ð 'ðl
ð ;ðl
ð  *ð!l
ð" ð#l
ð$ 
÷%l
ð l
ô\ð"
à&ð
ð 
ô
ð@à	5÷@ô&<ô^
ð@ ó=ó ð=ð0 ó&ó ð&ð@6Ø#ð6Ø5Að6à	ô6ðpyEà&ðyEð 
ôyEðvtà&ðtð 
ôtðl4
à#ð4
ð 
ô4
ðlxàðxð $ðxð 0ð	xð
 
ôxôtô,.@ô`Y$ðv:à,ð:ð )ð:ð ð	:ð
 ð:ð Bð:ð 
ô:ôx2ô.'ôR7ô(#ðJ ó7ó ð7ô*ô*ð*8à,ð8ð (ð8ð ð	8ð
 
ô8ðtI
àðI
ð ðI
ð 
ô	I
ðVHAà&ðHAð =ðHAð 
ô	HAðT?à#ð?ð ,ð?ð "ð	?ð
 ð?ð 
ô?ðL ðuà&ðuð "ðuð ð	uð
 ðuð 
&õuðn%
à&ð%
ð 
ô%
ðNm
à&ðm
ð 
ôm
ð^Aà&ðAð 
ôAðFkà-ðkð ðkð 
ô	kðZjà&ðjð ðjð 
ô	jðXDà&ðDð 
ôDðLbà&ðbð ðbð 
ô	bôH-ðF !%Ø'+Ø(,ð	`)àð`)ð %ð`)ð &ð	`)ð
 
õ`)ðD@à&ð@ð ð@ð 
ô	@ðDB*à&ðB*ð ðB*ð 
ô	B*ôH0ôdô*ð  ó'ó ð'ð0 ó1%ó ð1%ôf(
ðX !%ðJ
àðJ
ð 
õJ
ðXoà&ðoð ðoð 
ô	oôbKô*sðp ð+à%ð+ð ð+ð 
õ	+ôZT÷lò r   r„   )r   r  r   r  r~   r  )r#   rK   r~   r   )r   rp   r~   rN   )?r   Ú
__future__r   r   r	  ÚenumrÏ   Úloggingr	  rè   ry  r`   rä   r^   r®   r"  Úredis.backoffr¸   Úredis.clientÚredis.connectionÚredis.retryr   r"   rt  r   r   Ú	getLoggerr   r	   r  r  rÖ   rŽ  r   r*   r+   ÚOSErrorr   rR   r   r  r   r%   ÚAuthenticationErrorÚAuthorizationErrorÚMaxConnectionsErrorr'   r,   ÚCallablerp   Ú_WorkerExceptionHandlerÚ_WorkerTickÚEnumr0   r:   rK   rB   Ú
NamedTuplerJ   r¹   rT   ÚLockBaser„   r   r   r   Ú<module>r&     sç  ðò
gõR #ã Û Û Û Û Û 	Û Û 	Û Û Û Û Û ã Û Û Û õ 1ç *Ñ *à	×	Ò	˜8Ó	$€ð  Ð ð  Ð ð  Ð ð Ð ðØ!ðàðð ôðN ×$Ñ$Ø×!Ñ!Øð<Ð Ð8ó ôÐ.×>Ñ>ô ô0	˜)ô 	ôðd ×(Ñ(Ø×'Ñ'Ø×(Ñ(ðDñ Ð7Ó8ñ	DÐ Ð!@ó ôBð< !Ÿ/™/àØØ)ðð
 	ð
ñÐ ð �o‰oÐ4Ð5°tÐ;Ñ<€ô�—‘ô ô:�d—i‘iô ô&�C˜Ÿ™ô ô." �f×'Ñ'ô " ôJR˜Ÿ™×8Ñ8ô Rôjt6�—‘˜{Ñ+õ t6r   