xiangfu0 commented on code in PR #18755:
URL: https://github.com/apache/pinot/pull/18755#discussion_r3408701985
##########
pinot-clients/pinot-java-client/src/main/java/org/apache/pinot/client/admin/PinotAdminTransport.java:
##########
@@ -443,24 +443,42 @@ public List<String> parseStringArray(JsonNode response,
String fieldName)
if (arrayNode == null) {
throw new PinotAdminException("Response missing '" + fieldName + "'
field");
}
+ return parseStringArrayNode(arrayNode);
+ }
+ /**
+ * Parses a JSON node that is itself a string array (or a comma-separated
textual value) into a list of strings.
+ *
+ * <p>This is the single implementation shared by {@link
#parseStringArray(JsonNode, String)} (which first extracts a
+ * named field) and the admin clients that consume controller endpoints
returning a bare JSON array (for example
+ * {@code GET /schemas} and {@code DELETE /tables/{tableName}/rebalance}).
+ *
+ * @param arrayNode the node expected to be a JSON array (or a
comma-separated string)
+ * @return the parsed list of strings
+ * @throws PinotAdminException if {@code arrayNode} is {@code null} or is
neither an array nor a string
+ */
+ static List<String> parseStringArrayNode(JsonNode arrayNode)
+ throws PinotAdminException {
+ if (arrayNode == null || arrayNode.isNull()) {
+ throw new PinotAdminException("Expected a JSON array but got a null
node");
+ }
if (arrayNode.isArray()) {
// Handle JSON array format
- List<String> result = new ArrayList<>();
+ List<String> result = new ArrayList<>(arrayNode.size());
for (JsonNode element : arrayNode) {
result.add(element.asText());
}
return result;
- } else if (arrayNode.isTextual()) {
+ }
+ if (arrayNode.isTextual()) {
// Handle comma-separated string format for backward compatibility
String text = arrayNode.asText().trim();
if (text.isEmpty()) {
return Collections.emptyList();
Review Comment:
Fixed — the CSV fallback in `parseStringArrayNode` now trims each token and
drops empty entries, matching the JSON-array path.
##########
pinot-clients/pinot-java-client/src/main/java/org/apache/pinot/client/admin/InstanceAdminClient.java:
##########
@@ -248,19 +250,31 @@ public String validateDropInstances(String instanceNames)
/**
* Validates whether it's safe to update the tags of the given instances.
*
+ * <p>The controller endpoint reads a {@code List<InstanceTagUpdateRequest>}
from the request body (not query
+ * params). This method builds one request per instance name, all sharing
the given tags. Use
+ * {@link #validateInstanceTagUpdates(String)} when you need to supply
per-instance tags.
+ *
* @param instanceNames Comma-separated list of instance names to validate
- * @param newTags New tags to assign
+ * @param newTags Comma-separated list of tags to assign to every listed
instance
* @return Validation response as JSON string
* @throws PinotAdminException If the request fails
*/
public String validateUpdateInstanceTags(String instanceNames, String
newTags)
throws PinotAdminException {
- Map<String, String> queryParams = new HashMap<>();
- queryParams.put("instanceNames", instanceNames);
- queryParams.put("newTags", newTags);
+ List<String> tags = new ArrayList<>();
+ for (String tag : newTags.split(",")) {
+ tags.add(tag.trim());
+ }
+ List<Map<String, Object>> requestBody = new ArrayList<>();
Review Comment:
Fixed — empty/whitespace-only instance names and tags are now skipped before
building the request body.
##########
pinot-clients/pinot-java-client/src/main/java/org/apache/pinot/client/admin/TaskAdminClient.java:
##########
@@ -106,7 +106,7 @@ public Map<String, TaskState> getTaskStatesByTable(String
taskType, String table
JsonNode response =
_transport.executeGet(_controllerAddress, "/tasks/" + taskType + "/" +
tableNameWithType + "/state",
null, _headers);
- return
PinotAdminTransport.getObjectMapper().convertValue(response.get("taskStates"),
Map.class);
+ return PinotAdminTransport.getObjectMapper().convertValue(response,
Map.class);
}
Review Comment:
Fixed — now deserializes with `TypeReference<Map<String, TaskState>>` so the
values are real `TaskState` enums.
##########
pinot-clients/pinot-java-client/src/main/java/org/apache/pinot/client/admin/TaskAdminClient.java:
##########
@@ -257,7 +260,7 @@ public String getTaskDebugInfo(String taskName, int
verbosity, @Nullable String
public Map<String, TaskState> getTaskStates(String taskType)
throws PinotAdminException {
JsonNode response = _transport.executeGet(_controllerAddress, "/tasks/" +
taskType + "/taskstates", null, _headers);
- return
PinotAdminTransport.getObjectMapper().convertValue(response.get("taskStates"),
Map.class);
+ return PinotAdminTransport.getObjectMapper().convertValue(response,
Map.class);
}
Review Comment:
Fixed — now uses `TypeReference<Map<String, TaskState>>` so Jackson converts
the values to `TaskState` enums.
##########
pinot-clients/pinot-java-client/src/test/java/org/apache/pinot/client/admin/PinotAdminClientTest.java:
##########
@@ -432,4 +435,201 @@ public void
testRebalanceAndQueryClientsUseDedicatedEndpoints()
verify(_mockTransport).executeDelete(eq(CONTROLLER_ADDRESS),
eq("/clientQuery/client-query-1"), isNull(),
eq(HEADERS));
}
+
+ @Test
+ public void testListLiveInstancesUsesInstancesField()
+ throws Exception {
+ // GET /liveinstances returns an Instances wrapper serialized as
{"instances": [...]}.
+ JsonNode mockResponse = new ObjectMapper().readTree("{\"instances\":
[\"Server_1\", \"Broker_1\"]}");
+ lenient().when(_mockTransport.executeGet(anyString(), anyString(), any(),
any())).thenReturn(mockResponse);
+
+ List<String> instances =
_adminClient.getInstanceClient().listLiveInstances();
+
+ assertEquals(instances, List.of("Server_1", "Broker_1"));
+ verify(_mockTransport).executeGet(eq(CONTROLLER_ADDRESS),
eq("/liveinstances"), isNull(), eq(HEADERS));
+ }
+
+ @Test
+ public void testValidateUpdateInstanceTagsSendsRequestBody()
+ throws Exception {
+ JsonNode mockResponse = new ObjectMapper().readTree("[]");
+ lenient().when(_mockTransport.executePost(anyString(), anyString(), any(),
any(), any()))
+ .thenReturn(mockResponse);
+
+
_adminClient.getInstanceClient().validateUpdateInstanceTags("Server_1,Server_2",
"tag1,tag2");
+
+ // Controller reads a List<InstanceTagUpdateRequest> from the body; no
query params are used.
+ List<Map<String, Object>> expectedBody = List.of(
+ Map.of("instanceName", "Server_1", "newTags", List.of("tag1", "tag2")),
+ Map.of("instanceName", "Server_2", "newTags", List.of("tag1",
"tag2")));
+ verify(_mockTransport).executePost(eq(CONTROLLER_ADDRESS),
eq("/instances/updateTags/validate"), eq(expectedBody),
+ isNull(), eq(HEADERS));
+ }
+
+ @Test
+ public void testCancelRebalanceParsesBareArray()
+ throws Exception {
+ // DELETE /tables/{tableName}/rebalance returns a bare JSON array of
cancelled job IDs.
+ JsonNode mockResponse = new ObjectMapper().readTree("[\"job-1\",
\"job-2\"]");
+ lenient().when(_mockTransport.executeDelete(anyString(), anyString(),
any(), any())).thenReturn(mockResponse);
+
+ List<String> jobIds =
_adminClient.getTableClient().cancelRebalance("tbl1_OFFLINE");
+
+ assertEquals(jobIds, List.of("job-1", "job-2"));
+ verify(_mockTransport).executeDelete(eq(CONTROLLER_ADDRESS),
eq("/tables/tbl1_OFFLINE/rebalance"), isNull(),
+ eq(HEADERS));
+ }
+
+ @Test
+ public void testRebalanceTableMapsDowntimeAndMinAvailableReplicas()
+ throws Exception {
+ JsonNode mockResponse = new ObjectMapper().readTree("{\"status\":\"OK\"}");
+ lenient().when(_mockTransport.executePost(anyString(), anyString(), any(),
any(), any()))
+ .thenReturn(mockResponse);
+
+ _adminClient.getTableClient().rebalanceTable("tbl1_OFFLINE", true, 2);
+
+ verify(_mockTransport).executePost(eq(CONTROLLER_ADDRESS),
eq("/tables/tbl1_OFFLINE/rebalance"), isNull(),
+ eq(Map.of("downtime", "false", "minAvailableReplicas", "2")),
eq(HEADERS));
+ }
+
+ @Test
+ public void testSelectSegmentsUsesStartEndTimestampParams()
+ throws Exception {
+ JsonNode mockResponse = new ObjectMapper().readTree("[]");
+ lenient().when(_mockTransport.executeGet(anyString(), anyString(), any(),
any())).thenReturn(mockResponse);
+
+ _adminClient.getSegmentClient().selectSegments("tbl1", "OFFLINE", 100L,
200L, true);
+
+ verify(_mockTransport).executeGet(eq(CONTROLLER_ADDRESS),
eq("/segments/tbl1/select"),
+ eq(Map.of("startTimestamp", "100", "endTimestamp", "200",
"excludeReplacedSegments", "true", "type",
+ "OFFLINE")), eq(HEADERS));
+ }
+
+ @Test
+ public void testGetZookeeperMetadataParsesBareMap()
+ throws Exception {
+ JsonNode mockResponse = new ObjectMapper()
+
.readTree("{\"segment_1\":{\"segment.crc\":\"123\"},\"segment_2\":{\"segment.crc\":\"456\"}}");
+ lenient().when(_mockTransport.executeGet(anyString(), anyString(), any(),
any())).thenReturn(mockResponse);
+
+ Map<String, Map<String, String>> zkMetadata =
_adminClient.getSegmentClient().getZookeeperMetadata("tbl1_OFFLINE");
+
+ assertEquals(zkMetadata.size(), 2);
+ assertEquals(zkMetadata.get("segment_1").get("segment.crc"), "123");
+ verify(_mockTransport).executeGet(eq(CONTROLLER_ADDRESS),
eq("/segments/tbl1_OFFLINE/zkmetadata"), isNull(),
+ eq(HEADERS));
+ }
+
+ @Test
+ public void testTaskEndpointsParseBareValues()
+ throws Exception {
+ ObjectMapper mapper = new ObjectMapper();
+ // Every task endpoint resumes a bare value (array/string/int/map), not a
wrapper object.
+ lenient().when(_mockTransport.executeGet(eq(CONTROLLER_ADDRESS),
eq("/tasks/tasktypes"), any(), any()))
+ .thenReturn(mapper.readTree("[\"TaskA\", \"TaskB\"]"));
+ lenient().when(_mockTransport.executeGet(eq(CONTROLLER_ADDRESS),
eq("/tasks/TaskA/state"), any(), any()))
+ .thenReturn(mapper.readTree("\"IN_PROGRESS\""));
+ lenient().when(_mockTransport.executeGet(eq(CONTROLLER_ADDRESS),
eq("/tasks/TaskA/tasks"), any(), any()))
+ .thenReturn(mapper.readTree("[\"Task_TaskA_1\"]"));
+ lenient().when(_mockTransport.executeGet(eq(CONTROLLER_ADDRESS),
eq("/tasks/TaskA/tasks/count"), any(), any()))
+ .thenReturn(mapper.readTree("5"));
+ lenient().when(_mockTransport.executeGet(eq(CONTROLLER_ADDRESS),
eq("/tasks/TaskA/taskstates"), any(), any()))
+ .thenReturn(mapper.readTree("{\"Task_TaskA_1\":\"COMPLETED\"}"));
+ lenient().when(_mockTransport.executeGet(eq(CONTROLLER_ADDRESS),
eq("/tasks/TaskA/tbl1_OFFLINE/state"), any(),
+
any())).thenReturn(mapper.readTree("{\"Task_TaskA_1\":\"COMPLETED\"}"));
+ lenient().when(_mockTransport.executeGet(eq(CONTROLLER_ADDRESS),
eq("/tasks/subtask/Task_TaskA_1/state"), any(),
+
any())).thenReturn(mapper.readTree("{\"Task_TaskA_1_0\":\"RUNNING\"}"));
+
+ TaskAdminClient taskClient = _adminClient.getTaskClient();
+ assertEquals(taskClient.listTaskTypes(), Set.of("TaskA", "TaskB"));
+ assertEquals(taskClient.getTaskQueueState("TaskA"), TaskState.IN_PROGRESS);
+ assertEquals(taskClient.getTasks("TaskA"), Set.of("Task_TaskA_1"));
+ assertEquals(taskClient.getTasksCount("TaskA"), 5);
+ assertEquals(taskClient.getTaskStates("TaskA"), Map.of("Task_TaskA_1",
"COMPLETED"));
+ assertEquals(taskClient.getTaskStatesByTable("TaskA", "tbl1_OFFLINE"),
Map.of("Task_TaskA_1", "COMPLETED"));
Review Comment:
Fixed — the assertions now expect `TaskState.COMPLETED` enums, aligning the
test with the declared `Map<String, TaskState>` API.
--
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]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]