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 9c48dac9d7b Close the posting list stream in OffHeapJsonIndexCreator 
(#19925)
9c48dac9d7b is described below

commit 9c48dac9d7ba7d6a9e2086ab32961dfa2a10c254
Author: Deepak kumar <[email protected]>
AuthorDate: Mon Oct 26 10:44:10 2026 -0700

    Close the posting list stream in OffHeapJsonIndexCreator (#19925)
---
 .../impl/inv/json/OffHeapJsonIndexCreator.java     | 25 +++++-
 .../segment/local/segment/index/JsonIndexTest.java | 97 ++++++++++++++++++++++
 2 files changed, 121 insertions(+), 1 deletion(-)

diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/inv/json/OffHeapJsonIndexCreator.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/inv/json/OffHeapJsonIndexCreator.java
index 8234e6ecc4e..526fc116a36 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/inv/json/OffHeapJsonIndexCreator.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/inv/json/OffHeapJsonIndexCreator.java
@@ -74,7 +74,18 @@ public class OffHeapJsonIndexCreator extends 
BaseJsonIndexCreator {
       throws IOException {
     super(indexDir, columnName, tableNameWithType, continueOnError, 
jsonIndexConfig);
     _postingListFile = new File(_tempDir, POSTING_LIST_FILE_NAME);
-    _postingListOutputStream = new DataOutputStream(new 
BufferedOutputStream(new FileOutputStream(_postingListFile)));
+    try {
+      _postingListOutputStream = new DataOutputStream(new 
BufferedOutputStream(new FileOutputStream(_postingListFile)));
+    } catch (Throwable t) {
+      // The base constructor already created the temp dir, but a failure here 
leaves the caller without an instance to
+      // close, so the temp dir has to be cleaned up here. Call super.close() 
because close() dereferences the stream.
+      try {
+        super.close();
+      } catch (Throwable cleanupFailure) {
+        t.addSuppressed(cleanupFailure);
+      }
+      throw t;
+    }
   }
 
   @Override
@@ -225,6 +236,18 @@ public class OffHeapJsonIndexCreator extends 
BaseJsonIndexCreator {
     _numPostingLists++;
   }
 
+  @Override
+  public void close()
+      throws IOException {
+    // NOTE: seal() already closes the stream before reading the posting list 
file back, but it is not reached when
+    //       add() fails. Closing the stream again here is safe because 
nothing is written in between.
+    try {
+      _postingListOutputStream.close();
+    } finally {
+      super.close();
+    }
+  }
+
   private static class ChunkIterator implements Iterator<PostingListEntry> {
     final PinotDataBuffer _dataBuffer;
     final long _bufferSize;
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/JsonIndexTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/JsonIndexTest.java
index 477e0d993a2..193f19351a5 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/JsonIndexTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/JsonIndexTest.java
@@ -22,8 +22,10 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.StreamReadConstraints;
 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
 import com.google.common.collect.Lists;
+import java.io.DataOutputStream;
 import java.io.File;
 import java.io.IOException;
+import java.lang.reflect.Field;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.util.Arrays;
@@ -57,6 +59,7 @@ import org.apache.pinot.spi.utils.JsonUtils;
 import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.roaringbitmap.RoaringBitmap;
 import org.roaringbitmap.buffer.ImmutableRoaringBitmap;
+import org.testng.SkipException;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -1104,6 +1107,100 @@ public class JsonIndexTest implements 
PinotBuffersAfterMethodCheckRule {
     createIndex(true, jsonIndexConfig, records);
   }
 
+  /// The off-heap creator opens the posting list stream in the constructor, 
but seal() is never reached when add()
+  /// fails. close() must still release the stream and clean up the temp 
directory, and must stay idempotent so that
+  /// the normal seal()-then-close() path keeps working.
+  @Test
+  public void testOffHeapCreatorCloseWithoutSeal()
+      throws Exception {
+    String columnName = "offHeapUnsealed";
+    File tempDir = new File(INDEX_DIR, columnName + ".json.idx.tmp");
+
+    OffHeapJsonIndexCreator creator =
+        new OffHeapJsonIndexCreator(INDEX_DIR, columnName, "myTable_OFFLINE", 
false, getIndexConfig());
+    try {
+      // the braces don't match and cannot be parsed
+      assertThrows(JsonProcessingException.class, () -> 
creator.add("{\"key1\":\"va\""));
+      // Sanity check that the probe below can observe an open stream, so that 
the assertion after close() cannot pass
+      // vacuously
+      assertTrue(isPostingListStreamOpen(creator));
+    } finally {
+      creator.close();
+    }
+    assertFalse(isPostingListStreamOpen(creator));
+    assertFalse(tempDir.exists());
+
+    // Closing twice must not fail
+    creator.close();
+
+    // The regular path closes the stream in seal() and again in close()
+    OffHeapJsonIndexCreator sealedCreator =
+        new OffHeapJsonIndexCreator(INDEX_DIR, columnName, "myTable_OFFLINE", 
false, getIndexConfig());
+    try {
+      sealedCreator.add(TEST_RECORD);
+      sealedCreator.seal();
+    } finally {
+      sealedCreator.close();
+    }
+    assertFalse(isPostingListStreamOpen(sealedCreator));
+    assertTrue(new File(INDEX_DIR, columnName + 
V1Constants.Indexes.JSON_INDEX_FILE_EXTENSION).exists());
+  }
+
+  /// The base constructor creates the temp directory before the off-heap 
creator opens the posting list stream. When
+  /// opening the stream fails, the caller never gets an instance to close, so 
the constructor has to clean up itself.
+  @Test
+  public void testOffHeapCreatorConstructorFailureCleansUpTempDir()
+      throws Exception {
+    String columnName = "offHeapCtorFailure";
+    File tempDir = new File(INDEX_DIR, columnName + ".json.idx.tmp");
+    assertTrue(tempDir.mkdir());
+    try {
+      // Make the posting list file impossible to create
+      tempDir.setWritable(false, false);
+      if (canCreateFileIn(tempDir)) {
+        throw new SkipException("Running as a user that bypasses file 
permissions, cannot fail the stream creation");
+      }
+      assertThrows(IOException.class,
+          () -> new OffHeapJsonIndexCreator(INDEX_DIR, columnName, 
"myTable_OFFLINE", false, getIndexConfig()));
+      assertFalse(tempDir.exists());
+    } finally {
+      tempDir.setWritable(true, false);
+      FileUtils.deleteQuietly(tempDir);
+    }
+  }
+
+  /// Returns whether a file can be created in the given directory, to detect 
environments (e.g. running as root) where
+  /// making a directory read-only does not actually prevent writes.
+  private static boolean canCreateFileIn(File dir) {
+    File probe = new File(dir, "probe");
+    try {
+      if (!probe.createNewFile()) {
+        return false;
+      }
+    } catch (IOException e) {
+      return false;
+    }
+    FileUtils.deleteQuietly(probe);
+    return true;
+  }
+
+  /// Returns whether the posting list stream of the given creator is still 
holding its file descriptor, by writing a
+  /// byte and checking whether it reaches the underlying file. The stream is 
private with no accessor, and the
+  /// buffering layer accepts writes without touching the file descriptor, so 
this needs both reflection and a flush.
+  private static boolean isPostingListStreamOpen(OffHeapJsonIndexCreator 
creator)
+      throws Exception {
+    Field field = 
OffHeapJsonIndexCreator.class.getDeclaredField("_postingListOutputStream");
+    field.setAccessible(true);
+    DataOutputStream postingListOutputStream = (DataOutputStream) 
field.get(creator);
+    try {
+      postingListOutputStream.writeByte(0);
+      postingListOutputStream.flush();
+      return true;
+    } catch (IOException e) {
+      return false;
+    }
+  }
+
   @Test
   public void testGetMatchingValDocIdsPairForArrayPath() throws Exception {
     String[] records = Arrays.asList(


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

Reply via email to