This is an automated email from the ASF dual-hosted git repository.

Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new fdebb59d7b4 Throw IOException instead of NPE when listing an invalid 
path in LocalPinotFS (#19250)
fdebb59d7b4 is described below

commit fdebb59d7b4ebae4f5e6690c1f9935036a6d5649
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Thu Aug 13 12:42:29 2026 -0700

    Throw IOException instead of NPE when listing an invalid path in 
LocalPinotFS (#19250)
---
 .../helix/core/retention/RetentionManager.java      | 11 ++++++++++-
 .../helix/core/retention/RetentionManagerTest.java  |  6 ++++++
 .../apache/pinot/spi/filesystem/LocalPinotFS.java   | 20 ++++++++++++++++++--
 .../pinot/spi/filesystem/LocalPinotFSTest.java      | 21 +++++++++++++++++++++
 4 files changed, 55 insertions(+), 3 deletions(-)

diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/retention/RetentionManager.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/retention/RetentionManager.java
index 79bad7f046d..b5ba82d3133 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/retention/RetentionManager.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/retention/RetentionManager.java
@@ -378,7 +378,7 @@ public class RetentionManager extends 
ControllerPeriodicTask<Void> {
       }
     } catch (IOException e) {
       LOGGER.warn("Unable to fetch segments from deep store that are beyond 
retention period for table: {}",
-          tableNameWithType);
+          tableNameWithType, e);
     }
 
     return segmentsToDelete;
@@ -406,6 +406,15 @@ public class RetentionManager extends 
ControllerPeriodicTask<Void> {
     URI tableDataUri = 
URIUtils.getUri(_pinotHelixResourceManager.getDataDir(), rawTableName);
     PinotFS pinotFS = PinotFSFactory.create(tableDataUri.getScheme());
 
+    // The data dir is created when the first segment is pushed, so it is 
legitimately absent for a table that has
+    // never had a segment in deep store. Such a table has no untracked 
segments to delete, and listing a
+    // non-existent directory fails on most file systems.
+    if (!pinotFS.exists(tableDataUri)) {
+      LOGGER.info("Skipping deep store scan for untracked segments for table: 
{} as data dir: {} does not exist",
+          tableNameWithType, tableDataUri);
+      return segmentsToDelete;
+    }
+
     long startTimeMs = System.currentTimeMillis();
 
     List<FileMetadata> deepstoreFiles = 
pinotFS.listFilesWithMetadata(tableDataUri, false);
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/retention/RetentionManagerTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/retention/RetentionManagerTest.java
index 349e7d18e0b..7c1cf544a04 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/retention/RetentionManagerTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/retention/RetentionManagerTest.java
@@ -966,6 +966,12 @@ public class RetentionManagerTest {
 
   public static class FakePinotFs extends LocalPinotFS {
 
+    @Override
+    public boolean exists(URI fileUri) {
+      // The fake deep store is not backed by a real directory, but always has 
the segments listed below
+      return true;
+    }
+
     @Override
     public List<FileMetadata> listFilesWithMetadata(URI fileUri, boolean 
recursive)
         throws IOException {
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/filesystem/LocalPinotFS.java 
b/pinot-spi/src/main/java/org/apache/pinot/spi/filesystem/LocalPinotFS.java
index 2d312a07fce..dd01a16f616 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/filesystem/LocalPinotFS.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/filesystem/LocalPinotFS.java
@@ -115,7 +115,8 @@ public class LocalPinotFS extends BasePinotFS {
       throws IOException {
     File file = toFile(fileUri);
     if (!recursive) {
-      return Arrays.stream(file.list()).map(s -> new File(file, 
s)).map(File::getAbsolutePath).toArray(String[]::new);
+      return Arrays.stream(listFileNames(file)).map(s -> new File(file, 
s)).map(File::getAbsolutePath)
+          .toArray(String[]::new);
     } else {
       try (Stream<Path> pathStream = Files.walk(Paths.get(fileUri))) {
         return pathStream.filter(s -> 
!s.equals(file.toPath())).map(Path::toString).toArray(String[]::new);
@@ -128,7 +129,8 @@ public class LocalPinotFS extends BasePinotFS {
       throws IOException {
     File file = toFile(fileUri);
     if (!recursive) {
-      return Arrays.stream(file.list()).map(s -> getFileMetadata(new 
File(file, s))).collect(Collectors.toList());
+      return Arrays.stream(listFileNames(file)).map(s -> getFileMetadata(new 
File(file, s)))
+          .collect(Collectors.toList());
     } else {
       try (Stream<Path> pathStream = Files.walk(Paths.get(fileUri))) {
         return pathStream.filter(s -> !s.equals(file.toPath()))
@@ -138,6 +140,20 @@ public class LocalPinotFS extends BasePinotFS {
     }
   }
 
+  /// Returns the names of the entries directly under the given directory.
+  ///
+  /// [File#list()] returns `null` instead of throwing when the path does not 
exist, is not a directory, or cannot be
+  /// read, so translate that into the `IOException` the listing methods are 
contracted to throw for an invalid path.
+  private static String[] listFileNames(File file)
+      throws IOException {
+    String[] fileNames = file.list();
+    if (fileNames == null) {
+      throw new IOException("Failed to list files under: " + 
file.getAbsolutePath()
+          + " because it does not exist, is not a directory, or cannot be 
read");
+    }
+    return fileNames;
+  }
+
   private static FileMetadata getFileMetadata(File file) {
     return new FileMetadata.Builder().setFilePath(file.getAbsolutePath())
         .setLastModifiedTime(file.lastModified())
diff --git 
a/pinot-spi/src/test/java/org/apache/pinot/spi/filesystem/LocalPinotFSTest.java 
b/pinot-spi/src/test/java/org/apache/pinot/spi/filesystem/LocalPinotFSTest.java
index 7b3338bd111..4ada83680e5 100644
--- 
a/pinot-spi/src/test/java/org/apache/pinot/spi/filesystem/LocalPinotFSTest.java
+++ 
b/pinot-spi/src/test/java/org/apache/pinot/spi/filesystem/LocalPinotFSTest.java
@@ -310,4 +310,25 @@ public class LocalPinotFSTest {
         
expectedRecursive.containsAll(fileMetadata.stream().map(FileMetadata::getFilePath).collect(Collectors.toSet())),
         fileMetadata.toString());
   }
+
+  @Test
+  public void testListFilesOnPathThatIsNotADirectory()
+      throws IOException {
+    LocalPinotFS localPinotFS = new LocalPinotFS();
+    File tempDirPath = new File(_absoluteTmpDirPath, 
"test-list-files-invalid-path");
+    Assert.assertTrue(tempDirPath.mkdirs());
+
+    File nonExistentDir = new File(tempDirPath, "nonExistentDir");
+    URI nonExistentUri = nonExistentDir.toURI();
+    Assert.assertThrows(IOException.class, () -> 
localPinotFS.listFiles(nonExistentUri, false));
+    Assert.assertThrows(IOException.class, () -> 
localPinotFS.listFilesWithMetadata(nonExistentUri, false));
+    Assert.assertThrows(IOException.class, () -> 
localPinotFS.listFiles(nonExistentUri, true));
+    Assert.assertThrows(IOException.class, () -> 
localPinotFS.listFilesWithMetadata(nonExistentUri, true));
+
+    File testFile = new File(tempDirPath, "testFile");
+    Assert.assertTrue(testFile.createNewFile());
+    URI fileUri = testFile.toURI();
+    Assert.assertThrows(IOException.class, () -> 
localPinotFS.listFiles(fileUri, false));
+    Assert.assertThrows(IOException.class, () -> 
localPinotFS.listFilesWithMetadata(fileUri, false));
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to