Skip to content

feat(java): add cluster metadata and leader redirection to TCP client - #3745

Merged
hubcio merged 3 commits into
masterfrom
java-leader-aware
Aug 4, 2026
Merged

feat(java): add cluster metadata and leader redirection to TCP client#3745
hubcio merged 3 commits into
masterfrom
java-leader-aware

Conversation

@mmodzelewski

Copy link
Copy Markdown
Member

Java clients connected to a VSR follower had no way to discover
the cluster leader, so operations against a multi-node cluster
failed until the user manually pointed the client at the right
node.

Add the GET_CLUSTER_METADATA command with cluster roster models
and a binary deserializer. After login the async TCP client
fetches the roster and, when a healthy leader with an enabled
TCP endpoint lives elsewhere, reconnects to it and replays the
login. Redirections are capped at 3 consecutive hops so two
nodes both claiming leadership cannot ping-pong a client, and
every failure on the redirect path is non-fatal: the client
stays on the current node. Mirrors the Rust and Go SDKs.

Covered by a leader_redirection BDD scenario wired into the
cluster docker-compose setup.

Java clients connected to a VSR follower had no way to discover
the cluster leader, so operations against a multi-node cluster
failed until the user manually pointed the client at the right
node.

Add the GET_CLUSTER_METADATA command with cluster roster models
and a binary deserializer. After login the async TCP client
fetches the roster and, when a healthy leader with an enabled
TCP endpoint lives elsewhere, reconnects to it and replays the
login. Redirections are capped at 3 consecutive hops so two
nodes both claiming leadership cannot ping-pong a client, and
every failure on the redirect path is non-fatal: the client
stays on the current node. Mirrors the Rust and Go SDKs.

Covered by a leader_redirection BDD scenario wired into the
cluster docker-compose setup.
@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Jul 24, 2026
@mmodzelewski mmodzelewski added the java Issues related to Java SDK label Jul 24, 2026
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.63158% with 66 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.75%. Comparing base (60b8789) to head (962f064).

Files with missing lines Patch % Lines
...ache/iggy/client/async/tcp/AsyncIggyTcpClient.java 47.77% 43 Missing and 4 partials ⚠️
.../apache/iggy/client/async/tcp/LeaderAwareness.java 92.63% 5 Missing and 2 partials ⚠️
...ache/iggy/client/async/tcp/AsyncTcpConnection.java 86.95% 3 Missing ⚠️
...lient/async/tcp/PersonalAccessTokensTcpClient.java 78.57% 2 Missing and 1 partial ⚠️
...g/apache/iggy/client/async/tcp/UsersTcpClient.java 84.21% 2 Missing and 1 partial ⚠️
...he/iggy/client/async/tcp/LoginRedirectionHook.java 0.00% 1 Missing ⚠️
.../java/org/apache/iggy/cluster/ClusterNodeRole.java 90.90% 1 Missing ⚠️
...ava/org/apache/iggy/cluster/ClusterNodeStatus.java 92.85% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3745      +/-   ##
============================================
- Coverage     76.27%   74.75%   -1.53%     
- Complexity      969     1046      +77     
============================================
  Files          1325     1313      -12     
  Lines        163541   148495   -15046     
  Branches     136421   123806   -12615     
============================================
- Hits         124742   111003   -13739     
+ Misses        35097    34015    -1082     
+ Partials       3702     3477     -225     
Components Coverage Δ
Rust Core 75.10% <ø> (-1.18%) ⬇️
Java SDK 63.67% <82.63%> (+0.95%) ⬆️
C# SDK 72.26% <ø> (-0.02%) ⬇️
Python SDK 92.27% <ø> (+2.81%) ⬆️
PHP SDK 84.52% <ø> (ø)
Node SDK 92.40% <ø> (-3.68%) ⬇️
Go SDK 43.08% <ø> (ø)
Files with missing lines Coverage Δ
...in/java/org/apache/iggy/client/ConnectionInfo.java 100.00% <100.00%> (ø)
...iggy/client/async/tcp/ConsumerGroupsTcpClient.java 100.00% <100.00%> (ø)
...ggy/client/async/tcp/ConsumerOffsetsTcpClient.java 92.85% <100.00%> (+0.26%) ⬆️
...pache/iggy/client/async/tcp/MessagesTcpClient.java 97.43% <100.00%> (+0.06%) ⬆️
...che/iggy/client/async/tcp/PartitionsTcpClient.java 100.00% <100.00%> (ø)
...apache/iggy/client/async/tcp/StreamsTcpClient.java 100.00% <100.00%> (ø)
.../apache/iggy/client/async/tcp/SystemTcpClient.java 100.00% <100.00%> (ø)
.../apache/iggy/client/async/tcp/TopicsTcpClient.java 95.83% <100.00%> (+0.48%) ⬆️
...he/iggy/client/blocking/http/SystemHttpClient.java 64.28% <100.00%> (+5.95%) ⬆️
...apache/iggy/client/blocking/tcp/IggyTcpClient.java 63.26% <100.00%> (+0.76%) ⬆️
... and 14 more

... and 168 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@slbotbm slbotbm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes look alright to me. One comment though:

Let us consider the following setup: users A and B, follower F, and leader L. users A and B login through follower using the following code:

AsyncIggyTcpClient client = ...;
client.users().login("userA", "passwordA");
client.users().login("userB", "passwordB");

If the two users utilize the same client as above, the following happens:

  1. A calls login(). Its initial authentication runs on F.
  2. B calls login() concurrently. Its initial authentication also runs on F.
  3. A enters the serialized redirection step, discovers L, switches the shared client connection to L, and replays A’s login there.
  4. B’s redirection step runs next. It checks the current connection, which now points to L.
  5. Because L is already the leader, B’s login is not replayed.
  6. B’s future returns B’s identity from the earlier login on F, but the active connection to L is authenticated as A.

This happens because the initial login happens before the serialization:

loginWithoutRedirect(username, password)
      .thenCompose(identity -> redirectionHook.afterLogin(...));

If this is to be fixed, you should serialize the complete login-and-redirection operation.

If this is not a concern, please feel free to merge the PR.

@mmodzelewski

Copy link
Copy Markdown
Member Author

@slbotbm Thanks for the review - the race you describe is real. I would merge it as is, as the scenario itself falls outside the client's contract: a TCP client wraps a single connection, and the server keeps one session per connection, so the client is inherently single-user. Even with the complete login-and-redirection operation serialized, the second login would still replace the connection's identity, and user A's caller would silently end up on a connection authenticated as user B. Multiple users need separate client instances.

@hubcio
hubcio merged commit fbf99a6 into master Aug 4, 2026
65 checks passed
@hubcio
hubcio deleted the java-leader-aware branch August 4, 2026 16:07
@github-actions github-actions Bot removed the S-waiting-on-review PR is waiting on a reviewer label Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

java Issues related to Java SDK

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants