chia7712 commented on code in PR #22531:
URL: https://github.com/apache/kafka/pull/22531#discussion_r3488234183


##########
raft/src/main/java/org/apache/kafka/raft/KafkaRaftClient.java:
##########
@@ -2276,39 +2281,82 @@ private CompletionStage<AddRaftVoterResponseData> 
handleAddVoterRequest(
             );
         }
 
-        Optional<ReplicaKey> newVoter = RaftUtil.addVoterRequestVoterKey(data);
-        if (newVoter.isEmpty() || newVoter.get().directoryId().isEmpty()) {
-            return completedFuture(
-                new AddRaftVoterResponseData()
-                    .setErrorCode(Errors.INVALID_REQUEST.code())
-                    .setErrorMessage("Add voter request didn't include a valid 
voter")
-            );
-        }
-
-        Endpoints newVoterEndpoints = 
Endpoints.fromAddVoterRequest(data.listeners());
-        if (newVoterEndpoints.address(channel.listenerName()).isEmpty()) {
-            return completedFuture(
-                new AddRaftVoterResponseData()
-                    .setErrorCode(Errors.INVALID_REQUEST.code())
-                    .setErrorMessage(
-                        String.format(
-                            "Add voter request didn't include the endpoint 
(%s) for the default listener %s",
-                            newVoterEndpoints,
-                            channel.listenerName()
-                        )
-                    )
-            );
+        final ReplicaKey newVoter;
+        final Endpoints newVoterEndpoints;
+        try {
+            newVoter = resolveAddVoterKey(data, requestMetadata.apiVersion(), 
currentTimeMs);
+            newVoterEndpoints = resolveAddVoterEndpoints(data, 
requestMetadata.apiVersion());
+        } catch (IllegalArgumentException e) {
+            return 
completedFuture(RaftUtil.addVoterResponse(Errors.INVALID_REQUEST, 
e.getMessage()));
         }
 
         return addVoterHandler.handleAddVoterRequest(
             quorum.leaderStateOrThrow(),
-            newVoter.get(),
+            newVoter,
             newVoterEndpoints,
             data.ackWhenCommitted(),
             currentTimeMs
         );
     }
 
+    private ReplicaKey resolveAddVoterKey(
+        AddRaftVoterRequestData data,
+        short apiVersion,
+        long currentTimeMs
+    ) {
+        if (apiVersion >= 2 && data.voterDirectoryId().equals(Uuid.ZERO_UUID)) 
{
+            List<ReplicaKey> matchingObservers = quorum.leaderStateOrThrow()
+                .observerStates(currentTimeMs)
+                .keySet()
+                .stream()
+                .filter(key -> key.id() == data.voterId())
+                .toList();
+            if (matchingObservers.size() > 1) {
+                throw new IllegalArgumentException(String.format(
+                    "Multiple observers with node ID %d were found: %s. " +
+                        "Remove all but one of these observers and try again.",

Review Comment:
   I guess this scenario usually happens when a node recovers with a new log 
directory? If so, the error message could be more precise by explicitly 
reminding users to remove the stale/inactive observer.



##########
clients/src/main/java/org/apache/kafka/clients/admin/Admin.java:
##########
@@ -1864,6 +1864,22 @@ default ListClientMetricsResourcesResult 
listClientMetricsResources() {
      */
     Uuid clientInstanceId(Duration timeout);
 
+    /**
+     * Add a new voter node to the KRaft metadata quorum.
+     *
+     * <p>
+     * This is a convenience method that derives the voter's directory ID and 
endpoints
+     * from the active controller's in-memory state. It is not idempotent.
+     * For scenarios such as node disk failure where multiple observers may 
share the same
+     * node ID with different directory UUIDs, use
+     * {@link #addRaftVoter(int, Uuid, Set, AddRaftVoterOptions)} instead.
+     *
+     * @param voterId The node ID of the voter to add.
+     */
+    default AddRaftVoterResult addRaftVoter(int voterId) {

Review Comment:
   I noticed a usability gap with the current convenience method: users who 
want to use `AddRaftVoterOptions` (e.g., to verify the `clusterId`) are forced 
to fall back to the verbose API.
   
   Since this PR effectively shifts `voterDirectoryId` and endpoints from being 
required payloads to optional guard/validation conditions, wouldn't it make 
sense to move them into `AddRaftVoterOptions`? They now serve a very similar 
purpose to `clusterId` in verifying the target.
   
   If we do this, we could converge on `addRaftVoter(int voterId, 
AddRaftVoterOptions options)` as the standard API and deprecate the older, more 
verbose methods. WDYT?



##########
core/src/main/scala/kafka/server/SharedServer.scala:
##########
@@ -115,6 +115,22 @@ class SharedServer(
   val brokerConfig = new KafkaConfig(sharedServerConfig.props, false)
   val controllerConfig = new KafkaConfig(sharedServerConfig.props, false)
   val supportedConfigChecker: SupportedConfigChecker = new 
DefaultSupportedConfigChecker()
+  val controllerRegistrationsPublisher: 
Option[ControllerRegistrationsPublisher] =

Review Comment:
   we need to call `controllerRegistrationsPublisher#close` on the stop phase, 
right?



##########
raft/src/main/java/org/apache/kafka/raft/KafkaRaftClient.java:
##########
@@ -2415,22 +2463,42 @@ private CompletionStage<RemoveRaftVoterResponseData> 
handleRemoveVoterRequest(
             );
         }
 
-        Optional<ReplicaKey> oldVoter = 
RaftUtil.removeVoterRequestVoterKey(data);
-        if (oldVoter.isEmpty() || oldVoter.get().directoryId().isEmpty()) {
+        final ReplicaKey oldVoter;
+        try {
+            oldVoter = resolveRemoveVoterKey(data, 
requestMetadata.apiVersion());
+        } catch (IllegalArgumentException e) {

Review Comment:
   Could we leverage `RaftUtil.removeVoterResponse`?



##########
clients/src/main/resources/common/message/AddRaftVoterRequest.json:
##########
@@ -19,7 +19,9 @@
   "listeners": ["controller", "broker"],
   "name": "AddRaftVoterRequest",
   // Version 1 adds the AckWhenCommitted field.
-  "validVersions": "0-1",
+  // Version 2 allows VoterDirectoryId to be ZERO_UUID and Listeners to be 
empty, in which

Review Comment:
   Should we add checks to `AddRaftVoterRequest#build` to fail fast?



-- 
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