6262)
6363from kazoo .protocol .states import (
6464 Callback ,
65+ CLOSED_STATES ,
6566 EventType ,
6667 KazooState ,
6768 KeeperState ,
8889 from kazoo .handlers .gevent import SequentialGeventHandler
8990
9091
91- CLOSED_STATES = (
92- KeeperState .EXPIRED_SESSION ,
93- KeeperState .AUTH_FAILED ,
94- KeeperState .CLOSED ,
95- )
9692ENVI_VERSION = re .compile (r"([\d\.]*).*" , re .DOTALL )
9793ENVI_VERSION_KEY = "zookeeper.version"
9894log = logging .getLogger (__name__ )
@@ -394,6 +390,8 @@ def __init__(
394390 self ._stopped = self .handler .event_object ()
395391 self ._stopped .set ()
396392 self ._writer_stopped .set ()
393+ self ._conn_result : IAsyncResult | None = None
394+ self ._auth_error : Exception | None = None
397395
398396 # FIXME This is kind of gross but we need to set these to something so
399397 # that the type checker will understand that they are set by the time
@@ -722,10 +720,26 @@ def _session_callback(self, state: KeeperState) -> None:
722720 "Zookeeper connection established, " "state: %s" , state
723721 )
724722 self ._live .set ()
723+ if self ._conn_result is not None and not self ._conn_result .ready ():
724+ self ._conn_result .set (True )
725725 self ._make_state_change (KazooState .CONNECTED )
726726 elif state in CLOSED_STATES :
727727 self .logger .info ("Zookeeper session closed, state: %s" , state )
728728 self ._live .clear ()
729+ if self ._conn_result is not None and not self ._conn_result .ready ():
730+ if state == KeeperState .AUTH_FAILED :
731+ if isinstance (self ._auth_error , AuthFailedError ):
732+ err : Exception = self ._auth_error
733+ elif self ._auth_error is not None :
734+ err = AuthFailedError ("Authentication failed" )
735+ err .__cause__ = self ._auth_error
736+ else :
737+ err = AuthFailedError ("Authentication failed" )
738+ self ._conn_result .set_exception (err )
739+ elif state == KeeperState .CLOSED :
740+ self ._conn_result .set_exception (
741+ ConnectionClosedError ("Connection closed" )
742+ )
729743 self ._make_state_change (KazooState .LOST )
730744 self ._notify_pending (state )
731745 self ._reset ()
@@ -766,6 +780,10 @@ def _notify_pending(self, state: KeeperState) -> None:
766780
767781 def _safe_close (self ) -> None :
768782 self .handler .stop ()
783+ if self ._conn_result is not None and not self ._conn_result .ready ():
784+ self ._conn_result .set_exception (
785+ ConnectionClosedError ("Connection closed" )
786+ )
769787 timeout = self ._session_timeout // 1000
770788 if timeout < 10 :
771789 timeout = 10
@@ -823,33 +841,49 @@ def start(self, timeout: float = 15.0) -> None:
823841 :raises: :attr:`~kazoo.interfaces.IHandler.timeout_exception`
824842 if the connection wasn't established within `timeout`
825843 seconds.
844+ :raises: :exc:`~kazoo.exceptions.AuthFailedError` if authentication
845+ failed during connection establishment.
826846
827847 """
828- event = self .start_async ()
829- event .wait (timeout = timeout )
830- if not self .connected :
831- # We time-out, ensure we are disconnected
848+ if self ._live .is_set ():
849+ return
850+
851+ async_result = self .start_async ()
852+ try :
853+ async_result .get (timeout = timeout )
854+ except Exception :
832855 self .stop ()
833856 self .close ()
834- raise self . handler . timeout_exception ( "Connection time-out" )
857+ raise
835858
836859 if self .chroot and not self .exists ("/" ):
837860 warnings .warn (
838861 "No chroot path exists, the chroot path "
839862 "should be created before normal use."
840863 )
841864
842- def start_async (self ) -> Event :
865+ def start_async (self ) -> IAsyncResult :
843866 """Asynchronously initiate connection to ZK.
844867
845- :returns: An event object that can be checked to see if the
846- connection is alive.
847- :rtype: :class:`~threading.Event` compatible object .
868+ :returns: An :class:`~kazoo.interfaces.IAsyncResult` instance that
869+ resolves to `True` when connected or fails with an
870+ exception on terminal failure .
848871
849872 """
850- # If we're already connected, ignore
873+ # If we're already connected, return an already-completed result
851874 if self ._live .is_set ():
852- return self ._live
875+ if self ._conn_result is None :
876+ # Defensive fallback to catch poor mocking in KazooClient
877+ # internals (tests or mocks that manually set self._live.set()
878+ # without going through start_async()). In production,
879+ # self._conn_result is always initialized during start_async()
880+ # and should *never* be None while self._live is set.
881+ self ._conn_result = self .handler .async_result ()
882+ self ._conn_result .set (True )
883+ return self ._conn_result
884+
885+ if self ._conn_result is not None and not self ._conn_result .ready ():
886+ return self ._conn_result
853887
854888 # Make sure we're safely closed
855889 self ._safe_close ()
@@ -858,13 +892,14 @@ def start_async(self) -> Event:
858892 # thread indicator
859893 self ._stopped .clear ()
860894 self ._writer_stopped .clear ()
895+ self ._conn_result = self .handler .async_result ()
861896
862897 # Start the handler
863898 self .handler .start ()
864899
865900 # Start the connection
866901 self ._connection .start ()
867- return self ._live
902+ return self ._conn_result
868903
869904 def stop (self ) -> None :
870905 """Gracefully stop this Zookeeper session.
@@ -884,11 +919,8 @@ def stop(self) -> None:
884919 self ._stopped .set ()
885920 self ._queue .append ((CloseInstance , cast ("IAsyncResult" , None )))
886921 try :
887- # This assert should never fail since the connection should
888- # have been started but I'm not sure how to persaude mypy of that
889- self ._connection ._write_sock .send ( # type: ignore[union-attr]
890- b"\0 "
891- )
922+ if self ._connection ._write_sock is not None :
923+ self ._connection ._write_sock .send (b"\0 " )
892924 finally :
893925 self ._safe_close ()
894926
0 commit comments