Conversation
e7a9031 to
000cb9f
Compare
000cb9f to
6db2207
Compare
|
This change is solves all sort of race conditions during failover/reconnection (where there is a lot of queued requests). It is also a slight departure from what the Java ZK client does. It uses all sort of sending queue flags to filter what gets send so it does not transmit any other client request before auth is done. I did not want to bring this complexity in our codebase if I could avoid it. That being said, there will be a behavior change, in the failure paths:
In practice, I do not think it matters but I am flagging it for transparency. |
6db2207 to
fa828a1
Compare
|
Thank you. To be honest, I would prefer we continue to follow the official developer guide https://zookeeper.apache.org/doc/r3.9.5/zookeeperProgrammers.html (see |
|
You are talking about the state machine diagram in https://zookeeper.apache.org/doc/r3.9.5/zookeeperProgrammers.html#ch_zkSessions, right? There is a CONNECTING state => AUTH_FAILED transition to the left, without going through the CONNECTED state. It does mean some auth exchanges are happening before the connection gets to the CONNECTED state, don't you agree? |
e0a86bd to
d1cf626
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #808 +/- ##
==========================================
- Coverage 94.90% 94.81% -0.10%
==========================================
Files 27 27
Lines 3811 3841 +30
==========================================
+ Hits 3617 3642 +25
- Misses 194 199 +5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
d1cf626 to
9500cc6
Compare
f9d69fb to
9a1eb46
Compare
9a1eb46 to
432386f
Compare
…tart_async() semantics Addresses python-zk#594 Addresses python-zk#727
432386f to
6e550e6
Compare
Addresses #594
Addresses #727
Summary & Motivation
In ZooKeeper, session state (
session_id, watches, ephemeral nodes) is shared across an ensemble, but authentication is connection-bound. Every new TCP connection—whether on initial start, network reconnect, or cluster failover—must be authenticated with the server before the connection is authorized to perform operations for that identity.Previously,
client._session_callback(KeeperState.CONNECTED)(andCONNECTED_RO) was dispatched immediately after raw TCP connect response, before_authenticate_with_saslcompleted and beforeclient.auth_datacredentials were sent over the wire.This caused severe race conditions across connection establishment, network reconnect, and ensemble node failover:
NoAuthErroron reconnect / failover (#594): Application listeners registered viaadd_listeneror threads waiting onwait_for_connection()were notified ofCONNECTEDprematurely. Operations dispatched on reconnect (such as re-acquiring locks, leader election, or checking znodes) hit the server before the new socket was authenticated, failing withNoAuthError.-Dzookeeper.sessionRequireClientSASLAuth=trueorenforce.auth.enabled=true), any non-auth request received before SASL negotiation finishes causes ZooKeeper to immediately close the session with error code -124 (SessionClosedRequireSaslError/ZSESSIONCLOSEDREQUIRESASLAUTH).CONNECTEDduring reconnect attempted to calladd_auth(), mutatingclient.auth_datawhile_connect()was still iterating.Comparison with Apache ZooKeeper Java Client
In the official ZooKeeper Java client, non-auth packet submission is strictly prevented while SASL negotiation is in progress, and auth failure is treated as a terminal state:
1. Outgoing Packet Gating During SASL Negotiation
In Java
ClientCnxn, the client splits connection handling acrossSendThreadandEventThread. When SASL authentication is active, the client prevents normal request packets from being dispatched over the socket until SASL completes:ClientCnxnSocketNIO.javalines 157–185 (findSendablePacket): Inspects the outgoing queue. IfzooKeeperSaslClient.clientTunneledAuthenticationInProgress()is true, it only allows priming packets or SASL packets to be sent, skipping all regular operation packets.ClientCnxnSocketNIO.javalines 88–92: Explicitly enables socket write interest (SelectionKey.OP_WRITE) only after SASL authentication has completed.ClientCnxn.javalines 1451–1470 (tunnelAuthInProgress): Queries whether SASL negotiation is in progress before selecting packets for wire transmission.ZooKeeperSaslClient.javalines 415–448 (clientTunneledAuthenticationInProgress): Tracks SASL negotiation states (INITIAL,INTERMEDIATE,COMPLETE,FAILED).In Kazoo, rather than managing a complex packet-filtering selector in the connection loop,
ConnectionHandler._connect_attempt()dispatches SASL and all initialauth_datacredentials synchronously during socket establishment. Deferringclient._session_callback(KeeperState.CONNECTED)until after this stage guarantees that no user requests or state listeners resume until the socket is authorized.2. Terminal
AuthFailedState HandlingIn Java ZooKeeper, an authentication failure immediately marks the client as failed, publishes
AuthFailedto watchers, and permanently stops the connection thread:ClientCnxn.javalines 1194–1221: On SASL failure inSendThread, setsstate = States.AUTH_FAILED, posts aWatchedEvent(None, AuthFailed, null)toEventThread, and sends aneventOfDeathto stop the thread loop.ClientCnxn.javalines 903–908: If the server returns an auth failure reply, the client immediately transitions toStates.AUTH_FAILED.ZooKeeperSaslClient.javalines 381–395 (getKeeperState): Resolves the Keeper state toAuthFailedupon authentication failure, orSaslAuthenticatedupon success.Changes in this PR
Defer
CONNECTED/CONNECTED_ROuntil Auth Finishes:kazoo.protocol.connection.ConnectionHandler, moveclient._session_callback(KeeperState.CONNECTED)to execute strictly after_authenticate_with_sasl()completes and all initialauth_datapackets have been dispatched.Unify
start_async()Return Value withIAsyncResult:start_async()returnedself._live(threading.Event), which was only set on success. Purely async clients (res = zk.start_async()) were unable to receive terminal failure notifications or exceptions, hanging indefinitely onres.wait().start_async()now returns anIAsyncResultinstance—matching every other_asyncmethod in Kazoo.IAsyncResultcompletes withTruewhenCONNECTED/CONNECTED_ROis reached, or is set with an exception (AuthFailedError,SessionExpiredError, orConnectionClosedError) if a terminal failure occurs.IAsyncResult.wait(timeout)has the identical signature asthreading.Event.wait(timeout)(wait(timeout=None) -> bool). Existing code callingevent = zk.start_async(); event.wait(30)continues to work unmodified, but now unblocks immediately on terminal failure instead of hanging.res.ready(),res.successful(),res.exception, callres.get(), or attach async callbacks withres.rawlink().Immediate Unblock and Exception in
KazooClient.start():start(timeout=15.0)is now a clean consumer ofself.start_async().get(timeout=timeout).KeeperState.AUTH_FAILED),start()unblocks immediately (<10ms) and raisesAuthFailedErrorwith the underlying cause instead of hanging for the full 15-second timeout and raisingKazooTimeoutError.Preserve Terminal States in
zk_loop:ConnectionHandler.zk_loop, thefinally:block now preserves terminalCLOSED_STATES(AUTH_FAILED,EXPIRED_SESSION) rather than clobberingclient._statewithCLOSED.Documentation & Unit Tests:
docs/async_usage.rstto document that all_asyncmethods returnIAsyncResult.kazoo.tests.test_client.TestSessionCallbacksverifying immediate failure onAUTH_FAILED, error propagation with custom causes, andstart_async()IAsyncResultcompletion semantics.