Copilot commented on code in PR #4860:
URL: https://github.com/apache/bookkeeper/pull/4860#discussion_r3974833479


##########
bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/EntryLoggerAllocator.java:
##########
@@ -228,7 +241,13 @@ void setLastLogId(File dir, long logId) throws IOException 
{
      */
     void stop() {
         // wait until the preallocation finished.
-        allocatorExecutor.execute(this::closePreAllocateLog);
+        if (!allocatorExecutor.isShutdown()) {
+            try {
+                allocatorExecutor.execute(this::closePreAllocateLog);
+            } catch (RejectedExecutionException e) {
+                log.debug("Skipping preallocated entry log cleanup because 
allocator is stopping", e);
+            }

Review Comment:
   If `closePreAllocateLog` is skipped due to `RejectedExecutionException`, the 
preallocated log may remain open/unclosed, which can leak file descriptors at 
shutdown. A safer pattern is to perform `closePreAllocateLog()` inline when 
task submission is rejected (or before shutting down the executor), so cleanup 
is guaranteed even during racey shutdown sequences.



##########
bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/BufferedChannel.java:
##########
@@ -295,4 +319,17 @@ public synchronized int getNumOfBytesInWriteBuffer() {
     long getUnpersistedBytes() {
         return unpersistedBytes.get();
     }
-}
\ No newline at end of file
+
+    final void checkWritable() throws IOException {
+        IOException failure = writeFailure;
+        if (failure != null) {

Review Comment:
   `checkWritable()` always throws a new generic `IOException` when the channel 
is poisoned. If the original failure is an `EntryLogWriteException`, this loses 
the exception type and can prevent upstream `catch (EntryLogWriteException)` 
blocks from triggering fail-closed behavior. Consider rethrowing the original 
exception when it is already an `EntryLogWriteException` (or otherwise 
preserving its type), while still wrapping non-fatal `IOException`s.



##########
bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/storage/ldb/DbLedgerStorageEntryLogFlushFailureE2ETest.java:
##########
@@ -0,0 +1,75 @@
+/*
+ *
+ * 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.bookkeeper.bookie.storage.ldb;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import java.util.concurrent.TimeUnit;
+import org.apache.bookkeeper.bookie.BookieImpl;
+import org.apache.bookkeeper.client.BKException;
+import org.apache.bookkeeper.client.BookKeeper.DigestType;
+import org.apache.bookkeeper.client.LedgerHandle;
+import org.apache.bookkeeper.test.BookKeeperClusterTestCase;
+import org.awaitility.Awaitility;
+import org.junit.Test;
+
+public class DbLedgerStorageEntryLogFlushFailureE2ETest extends 
BookKeeperClusterTestCase {
+    private static final byte[] PASSWD = "passwd".getBytes(UTF_8);
+
+    public DbLedgerStorageEntryLogFlushFailureE2ETest() {
+        super(1);
+        
baseConf.setLedgerStorageClass(FailOnFlushDbLedgerStorage.class.getName());
+        baseConf.setFlushInterval(60000);
+        baseConf.setGcWaitTime(60000);
+        baseConf.setProperty(DbLedgerStorage.WRITE_CACHE_MAX_SIZE_MB, 1);
+        baseConf.setProperty(DbLedgerStorage.MAX_THROTTLE_TIME_MILLIS, 1000);
+        baseClientConf.setAddEntryTimeout(5);
+    }
+
+    @Test
+    public void 
testClientWriteFailsAndBookieShutsDownAfterEntryLogFlushFailure() throws 
Exception {
+        BookieImpl bookie = (BookieImpl) serverByIndex(0).getBookie();
+        LedgerHandle lh = bkc.createLedger(1, 1, 1, DigestType.CRC32, PASSWD);
+        byte[] payload = new byte[100 * 1024];
+        BKException clientFailure = null;
+
+        FailOnFlushDbLedgerStorage.injectFailureOnNextFlush();
+        try {

Review Comment:
   `LedgerHandle lh` is never closed. Even if the cluster teardown closes the 
client, explicitly closing the ledger handle in a `finally` block makes the 
test more robust and avoids leaking resources (especially if this test is run 
in isolation or failures occur mid-test).



##########
bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/storage/ldb/DbLedgerStorageTest.java:
##########
@@ -252,7 +260,63 @@ public void testBookieCompaction() throws Exception {
         ByteBuf res = storage.getEntry(4, 3);
         System.out.println("res:       " + ByteBufUtil.hexDump(res));
         System.out.println("newEntry3: " + ByteBufUtil.hexDump(newEntry3));
-        assertEquals(newEntry3, res);
+        assertByteBufEqualsAndRelease(newEntry3, res);
+    }
+
+    @Test
+    public void testPerLedgerEvictionFailurePropagatesToDbFatalErrorListener() 
throws Exception {
+        File perLedgerDir = File.createTempFile("bkTestPerLedger", ".dir");
+        perLedgerDir.delete();
+        perLedgerDir.mkdir();
+        File curDir = BookieImpl.getCurrentDirectory(perLedgerDir);
+        BookieImpl.checkDirectoryStructure(curDir);
+
+        ServerConfiguration conf = 
TestBKConfiguration.newServerConfiguration();
+        conf.setGcWaitTime(1000);
+        conf.setLedgerStorageClass(DbLedgerStorage.class.getName());
+        conf.setLedgerDirNames(new String[] { perLedgerDir.toString() });
+        conf.setEntryLogFilePreAllocationEnabled(false);
+        conf.setEntryLogPerLedgerEnabled(true);
+        conf.setEntrylogMapAccessExpiryTimeInSeconds(1);
+
+        BookieImpl bookie = new TestBookieImpl(conf);
+        DbLedgerStorage storage = (DbLedgerStorage) bookie.getLedgerStorage();
+        CountDownLatch fatalLatch = new CountDownLatch(1);
+        storage.setFatalErrorListener(new LedgerDirsListener() {
+            @Override
+            public void fatalError() {
+                fatalLatch.countDown();
+            }
+        });
+
+        try {
+            SingleDirectoryDbLedgerStorage singleDirStorage = 
storage.getLedgerStorageList().get(0);
+            DefaultEntryLogger entryLogger = (DefaultEntryLogger) 
singleDirStorage.getEntryLogger();
+            long ledgerId = 4L;
+            ByteBuf entry = Unpooled.buffer(1024);
+            try {
+                entry.writeLong(ledgerId);
+                entry.writeLong(1L);
+                entry.writeBytes("entry-1".getBytes());
+                entryLogger.addEntry(ledgerId, entry);
+            } finally {
+                ReferenceCountUtil.release(entry);
+            }
+
+            Object entryLogManager = getEntryLogManager(entryLogger);
+            BufferedChannel currentLogChannel = (BufferedChannel) 
invoke(entryLogManager,
+                    "getCurrentLogForLedger", new Class<?>[] { long.class }, 
ledgerId);
+            closeUnderlyingFileChannel(currentLogChannel);
+
+            Thread.sleep(TimeUnit.SECONDS.toMillis(2));
+            invoke(entryLogManager, "doEntryLogMapCleanup", new Class<?>[] { 
});
+
+            assertTrue("Per-ledger eviction failure should propagate through 
DbLedgerStorage fatal listener",
+                    fatalLatch.await(10, TimeUnit.SECONDS));

Review Comment:
   This test relies on fixed `Thread.sleep(...)` timing to trigger eviction 
behavior, which makes it prone to flakiness under slow/loaded CI. Consider 
replacing the sleep with a polling wait (e.g., Awaitility) that repeatedly 
triggers/observes the eviction condition until it holds or a timeout is reached.



##########
bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/storage/ldb/SingleDirectoryDbLedgerStorageShutdownTest.java:
##########
@@ -0,0 +1,130 @@
+/*
+ *
+ * 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.bookkeeper.bookie.storage.ldb;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import io.netty.buffer.ByteBufAllocator;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.concurrent.ExecutorService;
+import org.apache.bookkeeper.bookie.BookieImpl;
+import org.apache.bookkeeper.bookie.EntryLogWriteException;
+import org.apache.bookkeeper.bookie.GarbageCollectorThread;
+import org.apache.bookkeeper.bookie.LedgerDirsManager;
+import org.apache.bookkeeper.bookie.storage.EntryLogger;
+import org.apache.bookkeeper.conf.ServerConfiguration;
+import org.apache.bookkeeper.conf.TestBKConfiguration;
+import org.apache.bookkeeper.meta.LedgerManager;
+import org.apache.bookkeeper.stats.NullStatsLogger;
+import org.apache.bookkeeper.stats.StatsLogger;
+import org.apache.bookkeeper.util.DiskChecker;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests shutdown cleanup for {@link SingleDirectoryDbLedgerStorage}.
+ */
+public class SingleDirectoryDbLedgerStorageShutdownTest {
+
+    private static final long MB = 1024 * 1024;
+
+    private File tmpDir;
+    private EntryLogger entryLogger;
+    private FailingFlushSingleDirectoryDbLedgerStorage storage;
+
+    @Before
+    public void setup() throws Exception {
+        tmpDir = File.createTempFile("bkTest", ".dir");
+        tmpDir.delete();
+        tmpDir.mkdir();
+        File curDir = BookieImpl.getCurrentDirectory(tmpDir);
+        BookieImpl.checkDirectoryStructure(curDir);
+
+        ServerConfiguration conf = 
TestBKConfiguration.newServerConfiguration();
+        conf.setGcWaitTime(1000);
+        conf.setLedgerDirNames(new String[] { tmpDir.toString() });
+        DiskChecker diskChecker = new 
DiskChecker(conf.getDiskUsageThreshold(), conf.getDiskUsageWarnThreshold());
+        LedgerDirsManager ledgerDirsManager = new LedgerDirsManager(conf, 
conf.getLedgerDirs(), diskChecker);
+        LedgerDirsManager indexDirsManager = new LedgerDirsManager(conf, 
conf.getLedgerDirs(), diskChecker);
+        entryLogger = mock(EntryLogger.class);
+
+        storage = new FailingFlushSingleDirectoryDbLedgerStorage(conf, 
mock(LedgerManager.class),
+                ledgerDirsManager, indexDirsManager, entryLogger, 
NullStatsLogger.INSTANCE,
+                ByteBufAllocator.DEFAULT, MB, MB, 1, 1024);
+    }
+
+    @After
+    public void teardown() throws Exception {
+        if (storage != null) {
+            storage.shutdown();
+        }
+        FileUtils.deleteDirectory(tmpDir);
+    }
+
+    @Test
+    public void shutdownContinuesCleanupAfterFlushFailure() throws Exception {
+        storage.shutdown();
+        verify(entryLogger).close();
+        assertFalse(isGcThreadRunning());
+        assertTrue(getCleanupExecutor().isShutdown());
+        storage = null;
+    }
+
+    private boolean isGcThreadRunning() throws Exception {
+        Field gcThreadField = 
SingleDirectoryDbLedgerStorage.class.getDeclaredField("gcThread");
+        gcThreadField.setAccessible(true);
+        GarbageCollectorThread gcThread = (GarbageCollectorThread) 
gcThreadField.get(storage);
+
+        Field runningField = 
GarbageCollectorThread.class.getDeclaredField("running");
+        runningField.setAccessible(true);
+        return runningField.getBoolean(gcThread);
+    }
+
+    private ExecutorService getCleanupExecutor() throws Exception {
+        Field cleanupExecutorField = 
SingleDirectoryDbLedgerStorage.class.getDeclaredField("cleanupExecutor");
+        cleanupExecutorField.setAccessible(true);
+        return (ExecutorService) cleanupExecutorField.get(storage);
+    }

Review Comment:
   The test uses reflection to reach into private fields (`gcThread`, 
`running`, `cleanupExecutor`), making it brittle to internal refactors (e.g., 
field renames). If feasible, expose minimal test-only accessors (e.g., 
package-private getters annotated/marked for testing) or observable behaviors 
that avoid reflection while still validating that GC and cleanup executors are 
stopped.



##########
bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/storage/ldb/FailOnFlushDbLedgerStorage.java:
##########
@@ -0,0 +1,73 @@
+/*
+ *
+ * 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.bookkeeper.bookie.storage.ldb;
+
+import io.netty.buffer.ByteBufAllocator;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.bookkeeper.bookie.EntryLogWriteException;
+import org.apache.bookkeeper.bookie.LedgerDirsManager;
+import org.apache.bookkeeper.bookie.storage.EntryLogger;
+import org.apache.bookkeeper.conf.ServerConfiguration;
+import org.apache.bookkeeper.meta.LedgerManager;
+import org.apache.bookkeeper.stats.StatsLogger;
+
+public class FailOnFlushDbLedgerStorage extends DbLedgerStorage {
+    private static final AtomicBoolean failNextFlushWithEntryLogWriteException 
= new AtomicBoolean(false);
+
+    public static void injectFailureOnNextFlush() {
+        failNextFlushWithEntryLogWriteException.set(true);
+    }
+
+    public static void resetFailure() {
+        failNextFlushWithEntryLogWriteException.set(false);
+    }

Review Comment:
   The failure injection flag is `static`, so it is shared across all instances 
and tests in the same JVM. This can cause cross-test interference if tests are 
ever run concurrently or re-ordered unexpectedly. Prefer instance-scoped 
injection (e.g., a field on the storage instance) or a mechanism tied to the 
specific Bookie/storage under test to improve test isolation.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to