Skip to content

Commit 86b7aae

Browse files
HDDS-11205. Implement a search feature for users to locate keys pending Deletion within the OM Deleted Keys Insights section (#6969)
1 parent f7b428d commit 86b7aae

8 files changed

Lines changed: 928 additions & 180 deletions

File tree

hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconConstants.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,20 @@ private ReconConstants() {
4343
public static final int DISK_USAGE_TOP_RECORDS_LIMIT = 30;
4444
public static final String DEFAULT_OPEN_KEY_INCLUDE_NON_FSO = "false";
4545
public static final String DEFAULT_OPEN_KEY_INCLUDE_FSO = "false";
46-
public static final String DEFAULT_START_PREFIX = "/";
4746
public static final String DEFAULT_FETCH_COUNT = "1000";
4847
public static final String DEFAULT_KEY_SIZE = "0";
4948
public static final String DEFAULT_BATCH_NUMBER = "1";
5049
public static final String RECON_QUERY_BATCH_PARAM = "batchNum";
5150
public static final String RECON_QUERY_PREVKEY = "prevKey";
51+
public static final String RECON_QUERY_START_PREFIX = "startPrefix";
5252
public static final String RECON_OPEN_KEY_INCLUDE_NON_FSO = "includeNonFso";
5353
public static final String RECON_OPEN_KEY_INCLUDE_FSO = "includeFso";
54-
public static final String RECON_OPEN_KEY_DEFAULT_SEARCH_LIMIT = "1000";
55-
public static final String RECON_OPEN_KEY_SEARCH_DEFAULT_PREV_KEY = "";
54+
public static final String RECON_OM_INSIGHTS_DEFAULT_START_PREFIX = "/";
55+
public static final String RECON_OM_INSIGHTS_DEFAULT_SEARCH_LIMIT = "1000";
56+
public static final String RECON_OM_INSIGHTS_DEFAULT_SEARCH_PREV_KEY = "";
5657
public static final String RECON_QUERY_FILTER = "missingIn";
5758
public static final String PREV_CONTAINER_ID_DEFAULT_VALUE = "0";
58-
public static final String PREV_DELETED_BLOCKS_TRANSACTION_ID_DEFAULT_VALUE =
59-
"0";
59+
public static final String PREV_DELETED_BLOCKS_TRANSACTION_ID_DEFAULT_VALUE = "0";
6060
// Only include containers that are missing in OM by default
6161
public static final String DEFAULT_FILTER_FOR_MISSING_CONTAINERS = "SCM";
6262
public static final String RECON_QUERY_LIMIT = "limit";

hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconResponseUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static Response noMatchedKeysResponse(String startPrefix) {
4444
String jsonResponse = String.format(
4545
"{\"message\": \"No keys matched the search prefix: '%s'.\"}",
4646
startPrefix);
47-
return Response.status(Response.Status.NOT_FOUND)
47+
return Response.status(Response.Status.NO_CONTENT)
4848
.entity(jsonResponse)
4949
.type(MediaType.APPLICATION_JSON)
5050
.build();

hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconUtils.java

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,14 @@
3232
import java.text.ParseException;
3333
import java.text.SimpleDateFormat;
3434
import java.time.Instant;
35-
import java.util.List;
36-
import java.util.TimeZone;
35+
import java.util.ArrayList;
36+
import java.util.Collections;
3737
import java.util.Date;
38+
import java.util.LinkedHashMap;
39+
import java.util.List;
40+
import java.util.Map;
3841
import java.util.Set;
39-
import java.util.ArrayList;
42+
import java.util.TimeZone;
4043
import java.util.concurrent.BlockingQueue;
4144
import java.util.concurrent.ExecutorService;
4245
import java.util.concurrent.Executors;
@@ -54,6 +57,8 @@
5457
import org.apache.hadoop.hdds.scm.ha.SCMNodeDetails;
5558
import org.apache.hadoop.hdds.scm.server.SCMDatanodeHeartbeatDispatcher;
5659
import org.apache.hadoop.hdds.utils.HddsServerUtil;
60+
import org.apache.hadoop.hdds.utils.db.Table;
61+
import org.apache.hadoop.hdds.utils.db.TableIterator;
5762
import org.apache.hadoop.hdfs.web.URLConnectionFactory;
5863
import org.apache.hadoop.io.IOUtils;
5964

@@ -596,6 +601,109 @@ public static long convertToEpochMillis(String dateString, String dateFormat, Ti
596601
}
597602
}
598603

604+
public static boolean validateStartPrefix(String startPrefix) {
605+
606+
// Ensure startPrefix starts with '/' for non-empty values
607+
startPrefix = startPrefix.startsWith("/") ? startPrefix : "/" + startPrefix;
608+
609+
// Split the path to ensure it's at least at the bucket level (volume/bucket).
610+
String[] pathComponents = startPrefix.split("/");
611+
if (pathComponents.length < 3 || pathComponents[2].isEmpty()) {
612+
return false; // Invalid if not at bucket level or deeper
613+
}
614+
615+
return true;
616+
}
617+
618+
/**
619+
* Retrieves keys from the specified table based on pagination and prefix filtering.
620+
* This method handles different scenarios based on the presence of {@code startPrefix}
621+
* and {@code prevKey}, enabling efficient key retrieval from the table.
622+
*
623+
* The method handles the following cases:
624+
*
625+
* 1. {@code prevKey} provided, {@code startPrefix} empty:
626+
* - Seeks to {@code prevKey}, skips it, and returns subsequent records up to the limit.
627+
*
628+
* 2. {@code prevKey} empty, {@code startPrefix} empty:
629+
* - Iterates from the beginning of the table, retrieving all records up to the limit.
630+
*
631+
* 3. {@code startPrefix} provided, {@code prevKey} empty:
632+
* - Seeks to the first key matching {@code startPrefix} and returns all matching keys up to the limit.
633+
*
634+
* 4. {@code startPrefix} provided, {@code prevKey} provided:
635+
* - Seeks to {@code prevKey}, skips it, and returns subsequent keys that match {@code startPrefix},
636+
* up to the limit.
637+
*
638+
* This method also handles the following {@code limit} scenarios:
639+
* - If {@code limit == 0} or {@code limit < -1}, no records are returned.
640+
* - If {@code limit == -1}, all records are returned.
641+
* - For positive {@code limit}, it retrieves records up to the specified {@code limit}.
642+
*
643+
* @param table The table to retrieve keys from.
644+
* @param startPrefix The search prefix to match keys against.
645+
* @param limit The maximum number of keys to retrieve.
646+
* @param prevKey The key to start after for the next set of records.
647+
* @return A map of keys and their corresponding {@code OmKeyInfo} or {@code RepeatedOmKeyInfo} objects.
648+
* @throws IOException If there are problems accessing the table.
649+
*/
650+
public static <T> Map<String, T> extractKeysFromTable(
651+
Table<String, T> table, String startPrefix, int limit, String prevKey)
652+
throws IOException {
653+
654+
Map<String, T> matchedKeys = new LinkedHashMap<>();
655+
656+
// Null check for the table to prevent NPE during omMetaManager initialization
657+
if (table == null) {
658+
log.error("Table object is null. omMetaManager might still be initializing.");
659+
return Collections.emptyMap();
660+
}
661+
662+
// If limit = 0, return an empty result set
663+
if (limit == 0 || limit < -1) {
664+
return matchedKeys;
665+
}
666+
667+
// If limit = -1, set it to Integer.MAX_VALUE to return all records
668+
int actualLimit = (limit == -1) ? Integer.MAX_VALUE : limit;
669+
670+
try (TableIterator<String, ? extends Table.KeyValue<String, T>> keyIter = table.iterator()) {
671+
672+
// Scenario 1 & 4: prevKey is provided (whether startPrefix is empty or not)
673+
if (!prevKey.isEmpty()) {
674+
keyIter.seek(prevKey);
675+
if (keyIter.hasNext()) {
676+
keyIter.next(); // Skip the previous key record
677+
}
678+
} else if (!startPrefix.isEmpty()) {
679+
// Scenario 3: startPrefix is provided but prevKey is empty, so seek to startPrefix
680+
keyIter.seek(startPrefix);
681+
}
682+
683+
// Scenario 2: Both startPrefix and prevKey are empty (iterate from the start of the table)
684+
// No seeking needed; just start iterating from the first record in the table
685+
// This is implicit in the following loop, as the iterator will start from the beginning
686+
687+
// Iterate through the keys while adhering to the limit (if the limit is not zero)
688+
while (keyIter.hasNext() && matchedKeys.size() < actualLimit) {
689+
Table.KeyValue<String, T> entry = keyIter.next();
690+
String dbKey = entry.getKey();
691+
692+
// Scenario 3 & 4: If startPrefix is provided, ensure the key matches startPrefix
693+
if (!startPrefix.isEmpty() && !dbKey.startsWith(startPrefix)) {
694+
break; // If the key no longer matches the prefix, exit the loop
695+
}
696+
697+
// Add the valid key-value pair to the results
698+
matchedKeys.put(dbKey, entry.getValue());
699+
}
700+
} catch (IOException exception) {
701+
log.error("Error retrieving keys from table for path: {}", startPrefix, exception);
702+
throw exception;
703+
}
704+
return matchedKeys;
705+
}
706+
599707
/**
600708
* Finds all subdirectories under a parent directory in an FSO bucket. It builds
601709
* a list of paths for these subdirectories. These sub-directories are then used

0 commit comments

Comments
 (0)