numinnex commented on code in PR #3944:
URL: https://github.com/apache/iggy/pull/3944#discussion_r3852351058


##########
core/sdk/src/tcp/tcp_client.rs:
##########
@@ -358,6 +403,15 @@ impl TcpClient {
                         return Err(IggyError::CannotEstablishConnection);

Review Comment:
   Fixed. The `reconnection.enabled` check moved below the candidate advance, 
so it is the sweep that the reconnection settings apply to rather than a single 
dial: with reconnection off every failover address still gets its one turn, and 
only an exhausted sweep bails. Every early return now goes through 
`fail_connect()`, which puts the state back to `Disconnected` and publishes the 
event, so a later `connect()` dials instead of returning ok at the top.
   
   Two unit tests, both red against the old code: 
`a_client_with_reconnection_disabled_still_sweeps_its_failover_endpoints` and 
`a_connect_that_exhausts_every_endpoint_leaves_the_client_disconnected` (which 
asserts the second `connect()` fails again rather than reporting success).



##########
core/sdk/src/tcp/tcp_client.rs:
##########
@@ -387,6 +441,10 @@ impl TcpClient {
                     error!("Failed to establish TCP connection to the server: 
{error}",);
                     IggyError::CannotEstablishConnection
                 })?;
+                // The endpoint that answered is where this client now lives:
+                // the leader check compares against it, and the next
+                // reconnect starts from it.
+                *self.current_server_address.lock().await = 
server_address.clone();

Review Comment:
   Fixed. The dial, the socket options and the TLS handshake moved into 
`establish()`, and `current_server_address` / `client_address` are written only 
after it hands back a usable stream, so a node that accepts TCP and fails TLS 
leaves no trace for the next pass to lead with.
   
   A handshake failure is now a failed dial: it maps to 
`CannotEstablishConnection`, the sweep continues, and an exhausted sweep 
returns `CannotEstablishConnection`. A configuration fault 
(`InvalidTlsCertificatePath`, `InvalidTlsCertificate`, `InvalidTlsDomain`) 
returns straight away instead of looping forever under `max_retries = None`, as 
you asked.
   
   While there: the bound now covers the handshake too (`establish_bounded`). A 
peer that accepts TCP and never answers the ClientHello had no deadline 
anywhere, and 
`an_endpoint_that_never_answers_the_handshake_does_not_hold_up_the_sweep` hung 
the test binary until that changed. 
`an_endpoint_that_fails_the_tls_handshake_does_not_become_the_current_one` pins 
the stickiness fix (red without it: the current address became the plaintext 
endpoint).



##########
core/sdk/src/tcp/tcp_client.rs:
##########
@@ -155,20 +178,30 @@ impl BinaryTransport for TcpClient {
             return Err(error);
         }
 
+        // A stale-client eviction is the server ending this session
+        // authoritatively, like a logout: the remembered sign-in ends with it,
+        // so only a configured auto-login may bring the session back.
+        // Remembered credentials exist for transport loss, where the session
+        // died with the socket rather than by anyone's decision.
+        if matches!(error, IggyError::StaleClient) {
+            self.forget_session_credentials().await;
+        }
+
         if !self.config.reconnection.enabled {
             return Err(IggyError::Disconnected);
         }
 
-        if matches!(self.config.auto_login, AutoLogin::Disabled) && 
!is_login_register_code(code) {
-            // Without auto-login a reconnect cannot re-establish the session,
-            // so non-login requests fail fast. Login/register itself is the
+        if !is_login_register_code(code) && 
self.sign_in_credentials().await.is_none() {

Review Comment:
   Restricted, rather than documented. After the reconnect the request is 
replayed only when it provably never reached the log:
   
   - the error was raised before the frame was written (`NotConnected`, 
`CannotEstablishConnection`) or is an explicit server refusal that precedes 
execution (`Unauthenticated`, `StaleClient`);
   - or the operation is `Operation::NonReplicated`, which is never 
deduplicated in the first place;
   - or it is login/register, where the replay is the protocol (the server 
stays silent on a transient register failure and relies on the client 
resending).
   
   Everything else — `Disconnected`, `EmptyResponse`, `TcpError` on a 
replicated op — still reconnects, so the transport is healed for the next call, 
then returns the original error with a warn instead of replaying it. Same rule 
as the Go `canReplay` comment.



##########
core/sdk/src/tcp/tcp_client.rs:
##########
@@ -320,34 +363,36 @@ impl TcpClient {
             }
 
             self.set_state(ClientState::Connecting).await;
-            if let Some(connected_at) = 
self.connected_at.lock().await.as_ref() {
-                let now = IggyTimestamp::now();
-                let elapsed = now.as_micros() - connected_at.as_micros();
-                let interval = 
self.config.reconnection.reestablish_after.as_micros();
-                trace!(
-                    "Elapsed time since last connection: {}",
-                    IggyDuration::from(elapsed)
-                );
-                if elapsed < interval {
-                    let remaining = IggyDuration::from(interval - elapsed);
-                    info!("Trying to connect to the server in: {remaining}",);
-                    sleep(remaining.get_duration()).await;
-                }
+            let candidates = self.dial_candidates().await;
+            // The reestablish delay paces reconnects to the one endpoint a
+            // single-address client has. With other endpoints known there is
+            // somewhere else to go, and pausing first only pushes the
+            // failover past the window the caller is willing to wait; the
+            // retry interval still paces the loop.
+            let reestablish_wait = if candidates.len() > 1 {

Review Comment:
   Applied to the current endpoint only. When a `reestablish_after` window is 
still pending and there are other candidates, the list is rotated so the paced 
endpoint goes last, and the remaining window is waited out immediately before 
dialing it — recomputed at that moment, so the time spent on the other 
endpoints counts against it. A single-endpoint client behaves exactly as 
before, so `with_reestablish_after` keeps its promise.
   
   Both halves are pinned: 
`a_pending_reestablish_pause_does_not_delay_dialing_another_endpoint` (10s 
window, connect completes in under 2s on the survivor) and 
`the_reestablish_pause_still_applies_to_the_endpoint_that_was_lost` (1s window, 
at least 700ms elapsed). The second is red against the old code.
   
   Go got the same semantics, with 
`TestFailover_DoesNotSpendTheLostEndpointsPauseOnAnotherEndpoint` / 
`TestFailover_KeepsTheReestablishPauseForTheEndpointThatWasLost`.



##########
core/sdk/src/tcp/tcp_client.rs:
##########
@@ -155,20 +178,30 @@ impl BinaryTransport for TcpClient {
             return Err(error);
         }
 
+        // A stale-client eviction is the server ending this session
+        // authoritatively, like a logout: the remembered sign-in ends with it,
+        // so only a configured auto-login may bring the session back.
+        // Remembered credentials exist for transport loss, where the session
+        // died with the socket rather than by anyone's decision.
+        if matches!(error, IggyError::StaleClient) {
+            self.forget_session_credentials().await;

Review Comment:
   Removed. You are right that a GC pause or a laptop sleep is not caller 
intent, and wedging a manually signed-in `IggyConsumer` while an auto-login one 
recovers is the asymmetry this PR is supposed to remove. `StaleClient` now 
stays in the replay-safe set, so the reconnect signs in again with the 
remembered credentials.
   
   Flagging one inconsistency so it is a decision rather than an accident: Go, 
C# and Java still drop the remembered sign-in on a stale-client eviction. C# 
because `EvictedClient_WithoutAutoLogin_Should_FailFast_And_NotReconnect` 
asserts exactly that contract, and Java's is now scoped to clients with no 
configured credentials (a client built with credentials still re-authenticates, 
which its own `shouldReplayTransientImplicitLoginAfterEviction` requires). If 
you would rather have one rule everywhere, say which way and I will move the 
other three.



##########
core/sdk/src/tcp/tcp_client.rs:
##########
@@ -485,24 +543,24 @@ impl TcpClient {
             };
 
             // Handle auto-login
-            let should_redirect = match &self.config.auto_login {
-                AutoLogin::Disabled => {
-                    info!("Automatic sign-in is disabled.");
+            let should_redirect = match self.sign_in_credentials().await {
+                None => {
+                    info!("No credentials to sign in with.");
                     // Only `IggyClient` redirects after a manual sign-in, so
                     // a raw transport can stay on a backup: its first
                     // replicated write gets `TransientNotAccepted`, the
                     // redirect drops the session, and the retry fails
                     // `Unauthenticated` until the caller signs in again.
                     false
                 }
-                AutoLogin::Enabled(credentials) => {
+                Some(credentials) => {
                     if skip_auto_login {
                         info!("Skipping automatic sign-in for a retried 
login/register request.");
                         false
                     } else {
                         info!("{NAME} client: {client_address} is signing 
in...");
                         self.set_state(ClientState::Authenticating).await;
-                        match credentials {
+                        match &credentials {
                             Credentials::UsernamePassword(username, password) 
=> {

Review Comment:
   Both halves fixed.
   
   The triple login is gone: `binary_users.rs` and 
`binary_personal_access_tokens.rs` now check `redirect_login_settled()` after 
the reconnect and return the identity they already have when the client is 
`Authenticated` and no `AutoLogin` is configured. It is not a plain 
short-circuit — with `AutoLogin::Enabled(A)` plus a manual `login_user(B)` the 
reconnect signed in A, so the login still runs and the client ends as B.
   
   And a failed sign-in in `connect()` no longer leaves the state at 
`Authenticating`: it goes back to `Connected` (the transport is up, the session 
is not), and when the server rejected the credentials outright 
(`InvalidCredentials`, `InvalidUsername`, `InvalidPassword`, `Unauthenticated`) 
the remembered sign-in is dropped so a stale password is not replayed, and an 
argon2 paid for, on every reconnect. Configured credentials stay as configured; 
those are the caller's to fix.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to