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

adoroszlai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 7b6eeb4a379 HDDS-14370. RandomAccessFileChannel to implement Closeable 
(#9905)
7b6eeb4a379 is described below

commit 7b6eeb4a37958033dd8394d61a6da74497ce4f58
Author: KUAN-HAO HUANG <[email protected]>
AuthorDate: Fri Mar 27 20:02:27 2026 +0800

    HDDS-14370. RandomAccessFileChannel to implement Closeable (#9905)
---
 .../hdds/utils/io/RandomAccessFileChannel.java     |  37 +++--
 .../hdds/utils/io/TestRandomAccessFileChannel.java | 153 +++++++++++++++++++++
 2 files changed, 178 insertions(+), 12 deletions(-)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/io/RandomAccessFileChannel.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/io/RandomAccessFileChannel.java
index 5c7fcee6afb..e004c513889 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/io/RandomAccessFileChannel.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/io/RandomAccessFileChannel.java
@@ -17,6 +17,7 @@
 
 package org.apache.hadoop.hdds.utils.io;
 
+import java.io.Closeable;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -29,7 +30,7 @@
 import org.slf4j.LoggerFactory;
 
 /** {@link RandomAccessFile} and its {@link FileChannel}. */
-public class RandomAccessFileChannel {
+public class RandomAccessFileChannel implements Closeable {
   private static final Logger LOG = 
LoggerFactory.getLogger(RandomAccessFileChannel.class);
 
   private File blockFile;
@@ -47,9 +48,12 @@ public synchronized boolean isOpen() {
   /** Open the given file in read-only mode. */
   public synchronized void open(File file) throws FileNotFoundException {
     Preconditions.assertNull(blockFile, "blockFile");
-    blockFile = Objects.requireNonNull(file, "blockFile == null");
-    raf = new RandomAccessFile(blockFile, "r");
-    channel = raf.getChannel();
+    final File f = Objects.requireNonNull(file, "blockFile == null");
+    final RandomAccessFile newRaf = new RandomAccessFile(f, "r");
+    final FileChannel newChannel = newRaf.getChannel();
+    blockFile = f;
+    raf = newRaf;
+    channel = newChannel;
   }
 
   /** Similar to {@link FileChannel#position(long)}. */
@@ -67,7 +71,7 @@ public synchronized void position(long newPosition) throws 
IOException {
    * this method tries to fill up the buffer until either
    * (1) the buffer is full, or (2) it has reached end-of-stream.
    *
-   * @return ture if the caller should continue to read;
+   * @return true if the caller should continue to read;
    *         otherwise, it has reached end-of-stream, return false;
    */
   public synchronized boolean read(ByteBuffer buffer) throws IOException {
@@ -86,22 +90,31 @@ public synchronized boolean read(ByteBuffer buffer) throws 
IOException {
    * In case of exception, this method catches the exception, logs a warning 
message,
    * and then continue closing the remaining resources.
    */
+  @Override
   public synchronized void close() {
-    if (blockFile == null) {
+    final File fileToClose = blockFile;
+    if (fileToClose == null) {
       return;
     }
     blockFile = null;
+
     try {
-      channel.close();
-      channel = null;
+      if (channel != null) {
+        channel.close();
+      }
     } catch (IOException e) {
-      LOG.warn("Failed to close channel for {}", blockFile, e);
+      LOG.warn("Failed to close channel for {}", fileToClose, e);
+    } finally {
+      channel = null;
     }
     try {
-      raf.close();
-      raf = null;
+      if (raf != null) {
+        raf.close();
+      }
     } catch (IOException e) {
-      LOG.warn("Failed to close RandomAccessFile for {}", blockFile, e);
+      LOG.warn("Failed to close RandomAccessFile for {}", fileToClose, e);
+    } finally {
+      raf = null;
     }
   }
 }
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/utils/io/TestRandomAccessFileChannel.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/utils/io/TestRandomAccessFileChannel.java
new file mode 100644
index 00000000000..51463f4325b
--- /dev/null
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/utils/io/TestRandomAccessFileChannel.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.hdds.utils.io;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Path;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class TestRandomAccessFileChannel {
+  @TempDir
+  private Path tempDir;
+
+  @Test
+  void openFailureDoesNotLeaveOpenAndCloseIsSafe() {
+    final RandomAccessFileChannel c = new RandomAccessFileChannel();
+    final File missing = tempDir.resolve("missing-file").toFile();
+
+    assertThrows(FileNotFoundException.class, () -> c.open(missing));
+    assertFalse(c.isOpen());
+    assertDoesNotThrow(c::close);
+  }
+
+  @Test
+  void closeIsIdempotent() throws Exception {
+    final RandomAccessFileChannel c = new RandomAccessFileChannel();
+    final File f = tempDir.resolve("file").toFile();
+    try (RandomAccessFile raf = new RandomAccessFile(f, "rw")) {
+      raf.write(1);
+    }
+
+    c.open(f);
+    assertTrue(c.isOpen());
+
+    assertDoesNotThrow(c::close);
+    assertFalse(c.isOpen());
+    assertDoesNotThrow(c::close);
+  }
+
+  @Test
+  void closeContinuesToCloseRafEvenIfChannelCloseFails() throws Exception {
+    final RandomAccessFileChannel c = new RandomAccessFileChannel();
+    final File f = tempDir.resolve("file-to-close").toFile();
+    try (RandomAccessFile raf = new RandomAccessFile(f, "rw")) {
+      raf.write(1);
+    }
+
+    final FileChannel failingChannel = mock(FileChannel.class);
+    doThrow(new IOException("simulated close 
failure")).when(failingChannel).close();
+    final RandomAccessFile spyRaf = spy(new RandomAccessFile(f, "rw"));
+    setField(c, "blockFile", f);
+    setField(c, "channel", failingChannel);
+    setField(c, "raf", spyRaf);
+
+    assertDoesNotThrow(c::close);
+    verify(failingChannel).close();
+    verify(spyRaf).close();
+  }
+
+  @Test
+  void closeDoesNotThrowWhenRafAndChannelAreNull() throws Exception {
+    final RandomAccessFileChannel c = new RandomAccessFileChannel();
+    setField(c, "blockFile", tempDir.resolve("dummy").toFile());
+    setField(c, "channel", null);
+    setField(c, "raf", null);
+    assertDoesNotThrow(c::close);
+  }
+
+  @Test
+  void readWithZeroSizedBuffer() throws Exception {
+    final RandomAccessFileChannel c = new RandomAccessFileChannel();
+    final File f = tempDir.resolve("test-file").toFile();
+    try (RandomAccessFile raf = new RandomAccessFile(f, "rw")) {
+      raf.write(new byte[]{1, 2, 3, 4, 5});
+    }
+
+    c.open(f);
+    assertTrue(c.isOpen());
+
+    final ByteBuffer zeroSizedBuffer = ByteBuffer.allocate(0);
+    // Should return immediately without reading (buffer has no remaining 
capacity)
+    assertTrue(c.read(zeroSizedBuffer), "read() should return true for 
zero-sized buffer");
+    // Verify buffer state unchanged
+    assertEquals(0, zeroSizedBuffer.remaining());
+    assertEquals(0, zeroSizedBuffer.position());
+    assertEquals(0, zeroSizedBuffer.limit());
+
+    c.close();
+  }
+
+  @Test
+  void tryWithResourcesClosesAutomatically() throws Exception {
+    final File f = tempDir.resolve("try-with-resources").toFile();
+    try (RandomAccessFile raf = new RandomAccessFile(f, "rw")) {
+      raf.write(new byte[]{10, 20, 30});
+    }
+
+    final RandomAccessFileChannel c = new RandomAccessFileChannel();
+    c.open(f);
+    assertTrue(c.isOpen());
+    // Closeable contract: close via try-with-resources helper
+    closeAndVerify(c);
+    assertFalse(c.isOpen(), "should be closed after try-with-resources");
+    assertDoesNotThrow(c::close, "double close should be safe");
+  }
+
+  private static void closeAndVerify(Closeable closeable) throws IOException {
+    try (Closeable c = closeable) {
+      assertNotNull(c);
+    }
+  }
+
+  private static void setField(Object target, String name, Object value)
+      throws ReflectiveOperationException {
+    final Field f = RandomAccessFileChannel.class.getDeclaredField(name);
+    f.setAccessible(true);
+    f.set(target, value);
+  }
+}
+


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

Reply via email to