Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.apache.hadoop.hdds.scm.cli.datanode;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.google.common.base.Strings;
import java.io.IOException;
import java.util.List;
Expand Down Expand Up @@ -76,8 +77,21 @@ public class ListInfoSubcommand extends ScmSubcommand {
defaultValue = "false")
private boolean json;

@CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1")
private UsageSortingOptions usageSortingOptions;

private List<Pipeline> pipelines;

static class UsageSortingOptions {
@CommandLine.Option(names = {"--most-used"},
description = "Show datanodes sorted by highest usage.")
Comment thread
aryangupta1998 marked this conversation as resolved.
Outdated
private boolean mostUsed;

@CommandLine.Option(names = {"--least-used"},
description = "Show datanodes sorted by lowest usage.")
Comment thread
aryangupta1998 marked this conversation as resolved.
Outdated
private boolean leastUsed;
}

@Override
public void execute(ScmClient scmClient) throws IOException {
pipelines = scmClient.listPipelines();
Expand Down Expand Up @@ -120,6 +134,37 @@ public void execute(ScmClient scmClient) throws IOException {

private List<DatanodeWithAttributes> getAllNodes(ScmClient scmClient)
throws IOException {

// If sorting is requested
if (usageSortingOptions != null && (usageSortingOptions.mostUsed || usageSortingOptions.leastUsed)) {
boolean sortByMostUsed = usageSortingOptions.mostUsed;
List<HddsProtos.DatanodeUsageInfoProto> usageInfos = scmClient.getDatanodeUsageInfo(sortByMostUsed,
Integer.MAX_VALUE);

return usageInfos.stream()
.map(p -> {
String uuidStr = p.getNode().getUuid();
UUID parsedUuid = UUID.fromString(uuidStr);

try {
HddsProtos.Node node = scmClient.queryNode(parsedUuid);
long used = p.getUsed();
long capacity = p.getCapacity();
double percentUsed = (capacity > 0) ? (used * 100.0) / capacity : 0.0;
return new DatanodeWithAttributes(
DatanodeDetails.getFromProtoBuf(node.getNodeID()),
node.getNodeOperationalStates(0),
node.getNodeStates(0),
used,
capacity,
percentUsed);
} catch (IOException e) {
return null;
Comment thread
aryangupta1998 marked this conversation as resolved.
}
})
.collect(Collectors.toList());
Comment thread
aryangupta1998 marked this conversation as resolved.
}

List<HddsProtos.Node> nodes = scmClient.queryNode(null,
null, HddsProtos.QueryScope.CLUSTER, "");

Expand Down Expand Up @@ -162,12 +207,23 @@ private void printDatanodeInfo(DatanodeWithAttributes dna) {
System.out.println("Operational State: " + dna.getOpState());
System.out.println("Health State: " + dna.getHealthState());
System.out.println("Related pipelines:\n" + pipelineListInfo);

if (dna.getUsed() != null && dna.getCapacity() != null && dna.getUsed() >= 0 && dna.getCapacity() > 0) {
System.out.println("Used: " + dna.getUsed());
System.out.println("Capacity: " + dna.getCapacity() + "\n");
Comment thread
aryangupta1998 marked this conversation as resolved.
Outdated
}
}

private static class DatanodeWithAttributes {
private DatanodeDetails datanodeDetails;
private HddsProtos.NodeOperationalState operationalState;
private HddsProtos.NodeState healthState;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Long used = null;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Long capacity = null;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Double percentUsed = null;

DatanodeWithAttributes(DatanodeDetails dn,
HddsProtos.NodeOperationalState opState,
Expand All @@ -177,6 +233,20 @@ private static class DatanodeWithAttributes {
this.healthState = healthState;
}

DatanodeWithAttributes(DatanodeDetails dn,
HddsProtos.NodeOperationalState opState,
HddsProtos.NodeState healthState,
long used,
long capacity,
double percentUsed) {
this.datanodeDetails = dn;
this.operationalState = opState;
this.healthState = healthState;
this.used = used;
this.capacity = capacity;
this.percentUsed = percentUsed;
}

public DatanodeDetails getDatanodeDetails() {
return datanodeDetails;
}
Expand All @@ -188,5 +258,17 @@ public HddsProtos.NodeOperationalState getOpState() {
public HddsProtos.NodeState getHealthState() {
return healthState;
}

public Long getUsed() {
return used;
}

public Long getCapacity() {
return capacity;
}

public Double getPercentUsed() {
return percentUsed;
}
}
}
Comment thread
Tejaskriya marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,22 @@

package org.apache.hadoop.hdds.scm.cli.datanode;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
Expand All @@ -49,6 +55,7 @@ public class TestListInfoSubcommand {
private final ByteArrayOutputStream errContent = new ByteArrayOutputStream();
private final PrintStream originalOut = System.out;
private final PrintStream originalErr = System.err;
private final ObjectMapper mapper = new ObjectMapper();
private static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.name();

@BeforeEach
Expand Down Expand Up @@ -127,6 +134,157 @@ public void testDataNodeByUuidOutput()
assertTrue(m.find());
}

@Test
public void testMostUsedOrderingAndOutput() throws Exception {
ScmClient scmClient = mock(ScmClient.class);
List<HddsProtos.DatanodeUsageInfoProto> usageList = new ArrayList<>();
List<HddsProtos.Node> nodeList = getNodeDetails();

// Decreasing usage: 400, 300, 200, 100
for (int i = 0; i < 4; i++) {
usageList.add(HddsProtos.DatanodeUsageInfoProto.newBuilder()
.setNode(nodeList.get(i).getNodeID())
.setUsed(100L * (4 - i))
.setCapacity(1000)
.build());

when(scmClient.queryNode(UUID.fromString(nodeList.get(i).getNodeID().getUuid())))
.thenReturn(nodeList.get(i));
}

when(scmClient.getDatanodeUsageInfo(true, Integer.MAX_VALUE)).thenReturn(usageList);
when(scmClient.listPipelines()).thenReturn(new ArrayList<>());

// ----- with JSON flag -----
CommandLine c = new CommandLine(cmd);
c.parseArgs("--most-used", "--json");
cmd.execute(scmClient);

String jsonOutput = outContent.toString(DEFAULT_ENCODING);
JsonNode root;
try {
root = mapper.readTree(jsonOutput);
} catch (IOException e) {
fail("Invalid JSON output:\n" + jsonOutput + "\nError: " + e.getMessage());
return;
}

assertTrue(root.isArray(), "JSON output should be an array");
assertEquals(4, root.size(), "Expected 4 nodes in JSON output");

// Check that each contains used, capacity, percentUsed
for (JsonNode node : root) {
assertTrue(node.has("used"), "JSON missing 'used'");
assertTrue(node.has("capacity"), "JSON missing 'capacity'");
assertTrue(node.has("percentUsed"), "JSON missing 'percentUsed'");
}

// Check order
for (int i = 0; i < root.size() - 1; i++) {
long usedCurrent = root.get(i).get("used").asLong();
long usedNext = root.get(i + 1).get("used").asLong();
assertTrue(usedCurrent >= usedNext,
"JSON used values not in descending order at index " + i + ": " +
usedCurrent + " < " + usedNext);
}

outContent.reset();
// ----- without JSON flag -----
c = new CommandLine(cmd);
c.parseArgs("--most-used");
cmd.execute(scmClient);

String textOutput = outContent.toString(DEFAULT_ENCODING);
Pattern pattern = Pattern.compile("Used: (\\d+)");
Matcher matcher = pattern.matcher(textOutput);
List<Long> usedValues = new ArrayList<>();

while (matcher.find()) {
usedValues.add(Long.parseLong(matcher.group(1)));
}

// Check order
List<Long> sorted = new ArrayList<>(usedValues);
sorted.sort(Collections.reverseOrder());
assertEquals(sorted, usedValues,
"values are not in descending order.");
}

@Test
public void testLeastUsedOrderingAndOutput() throws Exception {
ScmClient scmClient = mock(ScmClient.class);
List<HddsProtos.DatanodeUsageInfoProto> usageList = new ArrayList<>();
List<HddsProtos.Node> nodeList = getNodeDetails();

// Increasing usage: 100, 200, 300, 400
for (int i = 0; i < 4; i++) {
usageList.add(HddsProtos.DatanodeUsageInfoProto.newBuilder()
.setNode(nodeList.get(i).getNodeID())
.setUsed(100L * (i + 1))
.setCapacity(1000)
.build());

when(scmClient.queryNode(UUID.fromString(nodeList.get(i).getNodeID().getUuid())))
.thenReturn(nodeList.get(i));
}

when(scmClient.getDatanodeUsageInfo(false, Integer.MAX_VALUE)).thenReturn(usageList);
when(scmClient.listPipelines()).thenReturn(new ArrayList<>());

// ----- with JSON flag -----
CommandLine c = new CommandLine(cmd);
c.parseArgs("--least-used", "--json");
cmd.execute(scmClient);

String jsonOutput = outContent.toString(DEFAULT_ENCODING);
JsonNode root;
try {
root = mapper.readTree(jsonOutput);
} catch (IOException e) {
fail("Invalid JSON output:\n" + jsonOutput + "\nError: " + e.getMessage());
return;
}

assertTrue(root.isArray(), "JSON output should be an array");
assertEquals(4, root.size(), "Expected 4 nodes in JSON output");

// Check that each contains used, capacity, percentUsed
for (JsonNode node : root) {
assertTrue(node.has("used"), "JSON missing 'used'");
assertTrue(node.has("capacity"), "JSON missing 'capacity'");
assertTrue(node.has("percentUsed"), "JSON missing 'percentUsed'");
}

// Check order
for (int i = 0; i < root.size() - 1; i++) {
long usedCurrent = root.get(i).get("used").asLong();
long usedNext = root.get(i + 1).get("used").asLong();
assertTrue(usedCurrent <= usedNext,
"JSON used values not in ascending order at index " + i + ": " +
usedCurrent + " > " + usedNext);
}

outContent.reset();
// ----- without JSON flag -----
c = new CommandLine(cmd);
c.parseArgs("--least-used");
cmd.execute(scmClient);

String textOutput = outContent.toString(DEFAULT_ENCODING);
Pattern pattern = Pattern.compile("Used: (\\d+)");
Matcher matcher = pattern.matcher(textOutput);
List<Long> usedValues = new ArrayList<>();

while (matcher.find()) {
usedValues.add(Long.parseLong(matcher.group(1)));
}

// Check order
List<Long> sorted = new ArrayList<>(usedValues);
Collections.sort(sorted);
assertEquals(sorted, usedValues, "Values not in ascending order.");
}

private List<HddsProtos.Node> getNodeDetails() {
List<HddsProtos.Node> nodes = new ArrayList<>();

Expand Down