You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bookkeeper.apache.org by si...@apache.org on 2018/04/06 23:39:36 UTC

[bookkeeper] branch master updated: ISSUE #1316: A bookie with non-writable dirs should be able to start in readonly mode

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

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


The following commit(s) were added to refs/heads/master by this push:
     new ea1a75c  ISSUE #1316: A bookie with non-writable dirs should be able to start in readonly mode
ea1a75c is described below

commit ea1a75c0622a93abbd16fd71820f3c0cb5fc3ab0
Author: Sijie Guo <si...@apache.org>
AuthorDate: Fri Apr 6 16:39:28 2018 -0700

    ISSUE #1316: A bookie with non-writable dirs should be able to start in readonly mode
    
    Descriptions of the changes in this PR:
    
    *Problem*
    
    Bookie failed to start when it doesn't have non-writable dirs. Issue is reported at #1316
    
    *Solution*
    
    - Introduce a setting `minUsageSizeForEntryLogCreation` to allow creating entry logs even when there is no writable dirs. This setting is replacing `getIsForceGCAllowWhenNoSpace` for creating new log, since entry log files are created not only for gc/compaction. It can also happen on startup and also journal replays.
    
    - Defer creating entry log files until first write happens, when there is no writable ledger dirs.
    
    - add bunch of tests for EntryLog and Bookie initialization.
    
    Master Issue: #1316
    Related Issue: #190
    
    Author: Sijie Guo <si...@apache.org>
    
    Reviewers: Charan Reddy Guttapalem <re...@gmail.com>
    
    This closes #1319 from sijie/allow_bookie_to_start_with_no_writable_bookies, closes #1316
---
 .../org/apache/bookkeeper/bookie/EntryLogger.java  |  25 ++-
 .../bookkeeper/bookie/LedgerDirsManager.java       |  23 +-
 .../bookkeeper/conf/ServerConfiguration.java       |  41 +++-
 .../bookie/BookieInitializationTest.java           |  20 +-
 .../org/apache/bookkeeper/bookie/EntryLogTest.java | 244 +++++++++++----------
 .../bookie/SingleBookieInitializationTest.java     | 134 +++++++++++
 .../bookkeeper/bookie/TestLedgerDirsManager.java   |  24 +-
 .../apache/bookkeeper/test/ReadOnlyBookieTest.java |   1 +
 8 files changed, 363 insertions(+), 149 deletions(-)

diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/EntryLogger.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/EntryLogger.java
index 66efbaa..99b0f49 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/EntryLogger.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/EntryLogger.java
@@ -25,6 +25,7 @@ import static com.google.common.base.Charsets.UTF_8;
 import static org.apache.bookkeeper.bookie.TransactionalEntryLogCompactor.COMPACTING_SUFFIX;
 import static org.apache.bookkeeper.util.BookKeeperConstants.MAX_LOG_SIZE_LIMIT;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.MapMaker;
 import com.google.common.collect.Sets;
@@ -84,6 +85,9 @@ import org.slf4j.LoggerFactory;
 public class EntryLogger {
     private static final Logger LOG = LoggerFactory.getLogger(EntryLogger.class);
 
+    @VisibleForTesting
+    static final int UNINITIALIZED_LOG_ID = -0xDEAD;
+
     static class BufferedLogChannel extends BufferedChannel {
         private final long logId;
         private final EntryLogMetadata entryLogMetadata;
@@ -285,7 +289,7 @@ public class EntryLogger {
         for (File dir : ledgerDirsManager.getAllLedgerDirs()) {
             if (!dir.exists()) {
                 throw new FileNotFoundException(
-                        "Entry log directory does not exist");
+                        "Entry log directory '" + dir + "' does not exist");
             }
             long lastLogId = getLastLogId(dir);
             if (lastLogId > logId) {
@@ -398,7 +402,12 @@ public class EntryLogger {
     }
 
     synchronized long getCurrentLogId() {
-        return logChannel.getLogId();
+        BufferedLogChannel channel = logChannel;
+        if (null == channel) {
+            return UNINITIALIZED_LOG_ID;
+        } else {
+            return channel.getLogId();
+        }
     }
 
     /**
@@ -416,8 +425,10 @@ public class EntryLogger {
     protected void initialize() throws IOException {
         // Register listener for disk full notifications.
         ledgerDirsManager.addLedgerDirsListener(getLedgerDirsListener());
-        // create a new log to write
-        createNewLog();
+
+        if (ledgerDirsManager.hasWritableLedgerDirs()) {
+            createNewLog();
+        }
     }
 
     private LedgerDirsListener getLedgerDirsListener() {
@@ -870,6 +881,12 @@ public class EntryLogger {
     };
 
     public synchronized long addEntry(long ledger, ByteBuf entry, boolean rollLog) throws IOException {
+        if (null == logChannel) {
+            // log channel can be null because the file is deferred to be created when no writable ledger directory
+            // is available.
+            createNewLog();
+        }
+
         int entrySize = entry.readableBytes() + 4; // Adding 4 bytes to prepend the size
         boolean reachEntryLogLimit =
             rollLog ? reachEntryLogLimit(entrySize) : readEntryLogHardLimit(entrySize);
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/LedgerDirsManager.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/LedgerDirsManager.java
index 5d2c11f..a615cfa 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/LedgerDirsManager.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/LedgerDirsManager.java
@@ -53,7 +53,7 @@ public class LedgerDirsManager {
     private final ConcurrentMap<File, Float> diskUsages =
             new ConcurrentHashMap<File, Float>();
     private final long entryLogSize;
-    private boolean forceGCAllowWhenNoSpace;
+    private long minUsableSizeForEntryLogCreation;
     private long minUsableSizeForIndexFileCreation;
 
     private final DiskChecker diskChecker;
@@ -69,9 +69,9 @@ public class LedgerDirsManager {
         this.writableLedgerDirectories = new ArrayList<File>(ledgerDirectories);
         this.filledDirs = new ArrayList<File>();
         this.listeners = new ArrayList<LedgerDirsListener>();
-        this.forceGCAllowWhenNoSpace = conf.getIsForceGCAllowWhenNoSpace();
         this.entryLogSize = conf.getEntryLogSizeLimit();
         this.minUsableSizeForIndexFileCreation = conf.getMinUsableSizeForIndexFileCreation();
+        this.minUsableSizeForEntryLogCreation = conf.getMinUsableSizeForEntryLogCreation();
         for (File dir : ledgerDirectories) {
             diskUsages.put(dir, 0f);
             String statName = "dir_" + dir.getParent().replace('/', '_') + "_usage";
@@ -176,19 +176,10 @@ public class LedgerDirsManager {
             return writableLedgerDirectories;
         }
 
-        // If Force GC is not allowed under no space
-        if (!forceGCAllowWhenNoSpace) {
-            String errMsg = "All ledger directories are non writable and force GC is not enabled.";
-            NoWritableLedgerDirException e = new NoWritableLedgerDirException(errMsg);
-            LOG.error(errMsg, e);
-            throw e;
-        }
-
-        // We don't have writable Ledger Dirs.
-        // That means we must have turned readonly but the compaction
-        // must have started running and it needs to allocate
-        // a new log file to move forward with the compaction.
-        return getDirsAboveUsableThresholdSize((long) (this.entryLogSize * 1.2));
+        // We don't have writable Ledger Dirs. But we are still okay to create new entry log files if we have enough
+        // disk spaces. This allows bookie can still function at readonly mode. Because compaction, journal replays
+        // can still write data to disks.
+        return getDirsAboveUsableThresholdSize(minUsableSizeForEntryLogCreation);
     }
 
     List<File> getDirsAboveUsableThresholdSize(long thresholdSize) throws NoWritableLedgerDirException {
@@ -202,7 +193,7 @@ public class LedgerDirsManager {
 
         if (!fullLedgerDirsToAccomodate.isEmpty()) {
             LOG.info("No writable ledger dirs below diskUsageThreshold. "
-                    + "But Dirs that can accomodate {} are: {}", thresholdSize, fullLedgerDirsToAccomodate);
+                    + "But Dirs that can accommodate {} are: {}", thresholdSize, fullLedgerDirsToAccomodate);
             return fullLedgerDirsToAccomodate;
         }
 
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java
index cb39821..904ca3f 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java
@@ -162,6 +162,7 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
     protected static final String BOOKIE_AUTH_PROVIDER_FACTORY_CLASS = "bookieAuthProviderFactoryClass";
 
     protected static final String MIN_USABLESIZE_FOR_INDEXFILE_CREATION = "minUsableSizeForIndexFileCreation";
+    protected static final String MIN_USABLESIZE_FOR_ENTRYLOG_CREATION = "minUsableSizeForEntryLogCreation";
     protected static final String MIN_USABLESIZE_FOR_HIGH_PRIORITY_WRITES = "minUsableSizeForHighPriorityWrites";
 
     protected static final String ALLOW_MULTIPLEDIRS_UNDER_SAME_DISKPARTITION =
@@ -2510,7 +2511,8 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
      * Gets the minimum safe Usable size to be available in index directory for Bookie to create Index File while
      * replaying journal at the time of Bookie Start in Readonly Mode (in bytes).
      *
-     * @return
+     * @return minimum safe usable size to be available in index directory for bookie to create index files.
+     * @see #setMinUsableSizeForIndexFileCreation(long)
      */
     public long getMinUsableSizeForIndexFileCreation() {
         return this.getLong(MIN_USABLESIZE_FOR_INDEXFILE_CREATION, 100 * 1024 * 1024L);
@@ -2520,8 +2522,12 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
      * Sets the minimum safe Usable size to be available in index directory for Bookie to create Index File while
      * replaying journal at the time of Bookie Start in Readonly Mode (in bytes).
      *
-     * @param minUsableSizeForIndexFileCreation
-     * @return
+     * <p>This parameter allows creating index files when there are enough disk spaces, even when the bookie
+     * is running at readonly mode because of the disk usage is exceeding {@link #getDiskUsageThreshold()}. Because
+     * compaction, journal replays can still write index files to disks when a bookie is readonly.
+     *
+     * @param minUsableSizeForIndexFileCreation min usable size for index file creation
+     * @return server configuration
      */
     public ServerConfiguration setMinUsableSizeForIndexFileCreation(long minUsableSizeForIndexFileCreation) {
         this.setProperty(MIN_USABLESIZE_FOR_INDEXFILE_CREATION, minUsableSizeForIndexFileCreation);
@@ -2529,14 +2535,39 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
     }
 
     /**
+     * Gets the minimum safe usable size to be available in ledger directory for Bookie to create entry log files.
+     *
+     * @return minimum safe usable size to be available in ledger directory for entry log file creation.
+     * @see #setMinUsableSizeForEntryLogCreation(long)
+     */
+    public long getMinUsableSizeForEntryLogCreation() {
+        return this.getLong(MIN_USABLESIZE_FOR_ENTRYLOG_CREATION, (long) 1.2 * getEntryLogSizeLimit());
+    }
+
+    /**
+     * Sets the minimum safe usable size to be available in ledger directory for Bookie to create entry log files.
+     *
+     * <p>This parameter allows creating entry log files when there are enough disk spaces, even when the bookie
+     * is running at readonly mode because of the disk usage is exceeding {@link #getDiskUsageThreshold()}. Because
+     * compaction, journal replays can still write data to disks when a bookie is readonly.
+     *
+     * @param minUsableSizeForEntryLogCreation minimum safe usable size to be available in ledger directory
+     * @return server configuration
+     */
+    public ServerConfiguration setMinUsableSizeForEntryLogCreation(long minUsableSizeForEntryLogCreation) {
+        this.setProperty(MIN_USABLESIZE_FOR_ENTRYLOG_CREATION, minUsableSizeForEntryLogCreation);
+        return this;
+    }
+
+    /**
      * Gets the minimum safe usable size to be available in ledger directory for Bookie to accept high priority writes.
      *
-     * <p>If not set, it is two times of {@link #getEntryLogSizeLimit()}.
+     * <p>If not set, it is the value of {@link #getMinUsableSizeForEntryLogCreation()}.
      *
      * @return the minimum safe usable size per ledger directory for bookie to accept high priority writes.
      */
     public long getMinUsableSizeForHighPriorityWrites() {
-        return this.getLong(MIN_USABLESIZE_FOR_HIGH_PRIORITY_WRITES, 2 * getEntryLogSizeLimit());
+        return this.getLong(MIN_USABLESIZE_FOR_HIGH_PRIORITY_WRITES, getMinUsableSizeForEntryLogCreation());
     }
 
     /**
diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/BookieInitializationTest.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/BookieInitializationTest.java
index 96790ac..714a1c7 100644
--- a/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/BookieInitializationTest.java
+++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/BookieInitializationTest.java
@@ -94,6 +94,7 @@ public class BookieInitializationTest extends BookKeeperClusterTestCase {
         String ledgersPath = "/" + "ledgers" + runtime.getMethodName();
         baseClientConf.setZkLedgersRootPath(ledgersPath);
         baseConf.setZkLedgersRootPath(ledgersPath);
+        baseConf.setMinUsableSizeForEntryLogCreation(Long.MAX_VALUE);
     }
 
     @Override
@@ -556,6 +557,7 @@ public class BookieInitializationTest extends BookKeeperClusterTestCase {
         long usableSpace = tmpDir.getUsableSpace();
         long totalSpace = tmpDir.getTotalSpace();
         final ServerConfiguration conf = TestBKConfiguration.newServerConfiguration()
+                .setLedgerStorageClass(InterleavedLedgerStorage.class.getName())
                 .setJournalDirName(tmpDir.getPath())
                 .setLedgerDirNames(new String[] { tmpDir.getPath() })
                 .setDiskCheckInterval(1000)
@@ -567,7 +569,7 @@ public class BookieInitializationTest extends BookKeeperClusterTestCase {
         // if isForceGCAllowWhenNoSpace or readOnlyModeEnabled is not set and Bookie is
         // started when Disk is full, then it will fail to start with NoWritableLedgerDirException
 
-        conf.setIsForceGCAllowWhenNoSpace(false)
+        conf.setMinUsableSizeForEntryLogCreation(Long.MAX_VALUE)
             .setReadOnlyModeEnabled(false);
         try {
             new Bookie(conf);
@@ -576,7 +578,7 @@ public class BookieInitializationTest extends BookKeeperClusterTestCase {
             // expected
         }
 
-        conf.setIsForceGCAllowWhenNoSpace(true)
+        conf.setMinUsableSizeForEntryLogCreation(Long.MIN_VALUE)
             .setReadOnlyModeEnabled(false);
         try {
             new Bookie(conf);
@@ -585,13 +587,19 @@ public class BookieInitializationTest extends BookKeeperClusterTestCase {
             // expected
         }
 
-        conf.setIsForceGCAllowWhenNoSpace(false)
+        conf.setMinUsableSizeForEntryLogCreation(Long.MAX_VALUE)
             .setReadOnlyModeEnabled(true);
+        Bookie bookie = null;
         try {
-            new Bookie(conf);
-            fail("NoWritableLedgerDirException expected");
+            // bookie is okay to start up when readonly mode is enabled because entry log file creation
+            // is deferred.
+            bookie = new Bookie(conf);
         } catch (NoWritableLedgerDirException e) {
-            // expected
+            fail("NoWritableLedgerDirException unexpected");
+        } finally {
+            if (null != bookie) {
+                bookie.shutdown();
+            }
         }
     }
 
diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/EntryLogTest.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/EntryLogTest.java
index cc02e4e..b7f286c 100644
--- a/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/EntryLogTest.java
+++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/EntryLogTest.java
@@ -44,6 +44,7 @@ import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
+import org.apache.bookkeeper.bookie.LedgerDirsManager.NoWritableLedgerDirException;
 import org.apache.bookkeeper.conf.ServerConfiguration;
 import org.apache.bookkeeper.conf.TestBKConfiguration;
 import org.apache.bookkeeper.util.DiskChecker;
@@ -51,6 +52,7 @@ import org.apache.bookkeeper.util.IOUtils;
 import org.apache.commons.io.FileUtils;
 import org.junit.After;
 import org.junit.Assert;
+import org.junit.Before;
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -69,8 +71,33 @@ public class EntryLogTest {
         return dir;
     }
 
+    private File rootDir;
+    private File curDir;
+    private ServerConfiguration conf;
+    private LedgerDirsManager dirsMgr;
+    private EntryLogger entryLogger;
+
+    @Before
+    public void setUp() throws Exception {
+        this.rootDir = createTempDir("bkTest", ".dir");
+        this.curDir = Bookie.getCurrentDirectory(rootDir);
+        Bookie.checkDirectoryStructure(curDir);
+        this.conf = TestBKConfiguration.newServerConfiguration();
+        this.dirsMgr = new LedgerDirsManager(
+            conf,
+            new File[] { rootDir },
+            new DiskChecker(
+                conf.getDiskUsageThreshold(),
+                conf.getDiskUsageWarnThreshold()));
+        this.entryLogger = new EntryLogger(conf, dirsMgr);
+    }
+
     @After
     public void tearDown() throws Exception {
+        if (null != this.entryLogger) {
+            entryLogger.shutdown();
+        }
+
         for (File dir : tempDirs) {
             FileUtils.deleteDirectory(dir);
         }
@@ -78,33 +105,70 @@ public class EntryLogTest {
     }
 
     @Test
-    public void testCorruptEntryLog() throws Exception {
-        File tmpDir = createTempDir("bkTest", ".dir");
-        File curDir = Bookie.getCurrentDirectory(tmpDir);
-        Bookie.checkDirectoryStructure(curDir);
+    public void testDeferCreateNewLog() throws Exception {
+        entryLogger.shutdown();
+
+        // mark `curDir` as filled
+        this.conf.setMinUsableSizeForEntryLogCreation(1);
+        this.dirsMgr = new LedgerDirsManager(
+            conf,
+            new File[] { rootDir },
+            new DiskChecker(
+                conf.getDiskUsageThreshold(),
+                conf.getDiskUsageWarnThreshold()));
+        this.dirsMgr.addToFilledDirs(curDir);
+
+        entryLogger = new EntryLogger(conf, dirsMgr);
+        assertEquals(EntryLogger.UNINITIALIZED_LOG_ID, entryLogger.getCurrentLogId());
+
+        // add the first entry will trigger file creation
+        entryLogger.addEntry(1, generateEntry(1, 1).nioBuffer());
+        assertEquals(2L, entryLogger.getCurrentLogId());
+    }
 
-        int gcWaitTime = 1000;
-        ServerConfiguration conf = TestBKConfiguration.newServerConfiguration();
-        conf.setJournalDirName(tmpDir.toString());
+    @Test
+    public void testDeferCreateNewLogWithoutEnoughDiskSpaces() throws Exception {
+        entryLogger.shutdown();
+
+        // mark `curDir` as filled
+        this.conf.setMinUsableSizeForEntryLogCreation(Long.MAX_VALUE);
+        this.dirsMgr = new LedgerDirsManager(
+            conf,
+            new File[] { rootDir },
+            new DiskChecker(
+                conf.getDiskUsageThreshold(),
+                conf.getDiskUsageWarnThreshold()));
+        this.dirsMgr.addToFilledDirs(curDir);
+
+        entryLogger = new EntryLogger(conf, dirsMgr);
+        assertEquals(EntryLogger.UNINITIALIZED_LOG_ID, entryLogger.getCurrentLogId());
+
+        // add the first entry will trigger file creation
+        try {
+            entryLogger.addEntry(1, generateEntry(1, 1).nioBuffer());
+            fail("Should fail to append entry if there is no enough reserved space left");
+        } catch (NoWritableLedgerDirException e) {
+            assertEquals(EntryLogger.UNINITIALIZED_LOG_ID, entryLogger.getCurrentLogId());
+        }
+    }
 
-        conf.setGcWaitTime(gcWaitTime);
-        conf.setLedgerDirNames(new String[] {tmpDir.toString()});
-        Bookie bookie = new Bookie(conf);
+    @Test
+    public void testCorruptEntryLog() throws Exception {
         // create some entries
-        EntryLogger logger = ((InterleavedLedgerStorage) bookie.ledgerStorage).entryLogger;
-        logger.addEntry(1, generateEntry(1, 1).nioBuffer());
-        logger.addEntry(3, generateEntry(3, 1).nioBuffer());
-        logger.addEntry(2, generateEntry(2, 1).nioBuffer());
-        logger.flush();
+        entryLogger.addEntry(1, generateEntry(1, 1).nioBuffer());
+        entryLogger.addEntry(3, generateEntry(3, 1).nioBuffer());
+        entryLogger.addEntry(2, generateEntry(2, 1).nioBuffer());
+        entryLogger.flush();
+        entryLogger.shutdown();
         // now lets truncate the file to corrupt the last entry, which simulates a partial write
         File f = new File(curDir, "0.log");
         RandomAccessFile raf = new RandomAccessFile(f, "rw");
         raf.setLength(raf.length() - 10);
         raf.close();
         // now see which ledgers are in the log
-        logger = new EntryLogger(conf, bookie.getLedgerDirsManager());
+        entryLogger = new EntryLogger(conf, dirsMgr);
 
-        EntryLogMetadata meta = logger.getEntryLogMetadata(0L);
+        EntryLogMetadata meta = entryLogger.getEntryLogMetadata(0L);
         LOG.info("Extracted Meta From Entry Log {}", meta);
         assertTrue(meta.getLedgersMap().containsKey(1L));
         assertFalse(meta.getLedgersMap().containsKey(2L));
@@ -126,14 +190,6 @@ public class EntryLogTest {
 
     @Test
     public void testMissingLogId() throws Exception {
-        File tmpDir = createTempDir("entryLogTest", ".dir");
-        File curDir = Bookie.getCurrentDirectory(tmpDir);
-        Bookie.checkDirectoryStructure(curDir);
-
-        ServerConfiguration conf = TestBKConfiguration.newServerConfiguration();
-        conf.setJournalDirName(tmpDir.toString());
-        conf.setLedgerDirNames(new String[] {tmpDir.toString()});
-        Bookie bookie = new Bookie(conf);
         // create some entries
         int numLogs = 3;
         int numEntries = 10;
@@ -141,12 +197,12 @@ public class EntryLogTest {
         for (int i = 0; i < numLogs; i++) {
             positions[i] = new long[numEntries];
 
-            EntryLogger logger = new EntryLogger(conf,
-                    bookie.getLedgerDirsManager());
+            EntryLogger logger = new EntryLogger(conf, dirsMgr);
             for (int j = 0; j < numEntries; j++) {
                 positions[i][j] = logger.addEntry(i, generateEntry(i, j).nioBuffer());
             }
             logger.flush();
+            logger.shutdown();
         }
         // delete last log id
         File lastLogId = new File(curDir, "lastId");
@@ -156,16 +212,15 @@ public class EntryLogTest {
         for (int i = numLogs; i < 2 * numLogs; i++) {
             positions[i] = new long[numEntries];
 
-            EntryLogger logger = new EntryLogger(conf,
-                    bookie.getLedgerDirsManager());
+            EntryLogger logger = new EntryLogger(conf, dirsMgr);
             for (int j = 0; j < numEntries; j++) {
                 positions[i][j] = logger.addEntry(i, generateEntry(i, j).nioBuffer());
             }
             logger.flush();
+            logger.shutdown();
         }
 
-        EntryLogger newLogger = new EntryLogger(conf,
-                bookie.getLedgerDirsManager());
+        EntryLogger newLogger = new EntryLogger(conf, dirsMgr);
         for (int i = 0; i < (2 * numLogs + 1); i++) {
             File logFile = new File(curDir, Long.toHexString(i) + ".log");
             assertTrue(logFile.exists());
@@ -186,22 +241,20 @@ public class EntryLogTest {
         }
     }
 
-    @Test
     /**
      * Test that EntryLogger Should fail with FNFE, if entry logger directories does not exist.
      */
+    @Test
     public void testEntryLoggerShouldThrowFNFEIfDirectoriesDoesNotExist()
             throws Exception {
         File tmpDir = createTempDir("bkTest", ".dir");
-        ServerConfiguration conf = TestBKConfiguration.newServerConfiguration();
-        conf.setLedgerDirNames(new String[] { tmpDir.toString() });
         EntryLogger entryLogger = null;
         try {
-            entryLogger = new EntryLogger(conf, new LedgerDirsManager(conf, conf.getLedgerDirs(),
+            entryLogger = new EntryLogger(conf, new LedgerDirsManager(conf, new File[] { tmpDir },
                     new DiskChecker(conf.getDiskUsageThreshold(), conf.getDiskUsageWarnThreshold())));
             fail("Expecting FileNotFoundException");
         } catch (FileNotFoundException e) {
-            assertEquals("Entry log directory does not exist", e
+            assertEquals("Entry log directory '" + tmpDir +  "/current' does not exist", e
                     .getLocalizedMessage());
         } finally {
             if (entryLogger != null) {
@@ -243,32 +296,19 @@ public class EntryLogTest {
     }
 
     /**
-     * Explicitely try to recover using the ledgers map index at the end of the entry log.
+     * Explicitly try to recover using the ledgers map index at the end of the entry log.
      */
     @Test
     public void testRecoverFromLedgersMap() throws Exception {
-        File tmpDir = createTempDir("bkTest", ".dir");
-        File curDir = Bookie.getCurrentDirectory(tmpDir);
-        Bookie.checkDirectoryStructure(curDir);
-
-        int gcWaitTime = 1000;
-        ServerConfiguration conf = TestBKConfiguration.newServerConfiguration();
-        conf.setJournalDirName(tmpDir.toString());
-
-        conf.setGcWaitTime(gcWaitTime);
-        conf.setLedgerDirNames(new String[] {tmpDir.toString()});
-        Bookie bookie = new Bookie(conf);
-
         // create some entries
-        EntryLogger logger = ((InterleavedLedgerStorage) bookie.ledgerStorage).entryLogger;
-        logger.addEntry(1, generateEntry(1, 1).nioBuffer());
-        logger.addEntry(3, generateEntry(3, 1).nioBuffer());
-        logger.addEntry(2, generateEntry(2, 1).nioBuffer());
-        logger.addEntry(1, generateEntry(1, 2).nioBuffer());
-        logger.rollLog();
-        logger.flushRotatedLogs();
-
-        EntryLogMetadata meta = logger.extractEntryLogMetadataFromIndex(0L);
+        entryLogger.addEntry(1, generateEntry(1, 1).nioBuffer());
+        entryLogger.addEntry(3, generateEntry(3, 1).nioBuffer());
+        entryLogger.addEntry(2, generateEntry(2, 1).nioBuffer());
+        entryLogger.addEntry(1, generateEntry(1, 2).nioBuffer());
+        entryLogger.rollLog();
+        entryLogger.flushRotatedLogs();
+
+        EntryLogMetadata meta = entryLogger.extractEntryLogMetadataFromIndex(0L);
         LOG.info("Extracted Meta From Entry Log {}", meta);
         assertEquals(60, meta.getLedgersMap().get(1L));
         assertEquals(30, meta.getLedgersMap().get(2L));
@@ -279,28 +319,17 @@ public class EntryLogTest {
     }
 
     /**
-     * Explicitely try to recover using the ledgers map index at the end of the entry log.
+     * Explicitly try to recover using the ledgers map index at the end of the entry log.
      */
     @Test
     public void testRecoverFromLedgersMapOnV0EntryLog() throws Exception {
-        File tmpDir = createTempDir("bkTest", ".dir");
-        File curDir = Bookie.getCurrentDirectory(tmpDir);
-        Bookie.checkDirectoryStructure(curDir);
-
-        int gcWaitTime = 1000;
-        ServerConfiguration conf = TestBKConfiguration.newServerConfiguration();
-        conf.setGcWaitTime(gcWaitTime);
-        conf.setLedgerDirNames(new String[] { tmpDir.toString() });
-        conf.setJournalDirName(tmpDir.toString());
-        Bookie bookie = new Bookie(conf);
-
         // create some entries
-        EntryLogger logger = ((InterleavedLedgerStorage) bookie.ledgerStorage).entryLogger;
-        logger.addEntry(1, generateEntry(1, 1).nioBuffer());
-        logger.addEntry(3, generateEntry(3, 1).nioBuffer());
-        logger.addEntry(2, generateEntry(2, 1).nioBuffer());
-        logger.addEntry(1, generateEntry(1, 2).nioBuffer());
-        logger.rollLog();
+        entryLogger.addEntry(1, generateEntry(1, 1).nioBuffer());
+        entryLogger.addEntry(3, generateEntry(3, 1).nioBuffer());
+        entryLogger.addEntry(2, generateEntry(2, 1).nioBuffer());
+        entryLogger.addEntry(1, generateEntry(1, 2).nioBuffer());
+        entryLogger.rollLog();
+        entryLogger.shutdown();
 
         // Rewrite the entry log header to be on V0 format
         File f = new File(curDir, "0.log");
@@ -311,17 +340,17 @@ public class EntryLogTest {
         raf.close();
 
         // now see which ledgers are in the log
-        logger = new EntryLogger(conf, bookie.getLedgerDirsManager());
+        entryLogger = new EntryLogger(conf, dirsMgr);
 
         try {
-            logger.extractEntryLogMetadataFromIndex(0L);
+            entryLogger.extractEntryLogMetadataFromIndex(0L);
             fail("Should not be possible to recover from ledgers map index");
         } catch (IOException e) {
             // Ok
         }
 
         // Public method should succeed by falling back to scanning the file
-        EntryLogMetadata meta = logger.getEntryLogMetadata(0L);
+        EntryLogMetadata meta = entryLogger.getEntryLogMetadata(0L);
         LOG.info("Extracted Meta From Entry Log {}", meta);
         assertEquals(60, meta.getLedgersMap().get(1L));
         assertEquals(30, meta.getLedgersMap().get(2L));
@@ -337,37 +366,30 @@ public class EntryLogTest {
      */
     @Test
     public void testPreAllocateLog() throws Exception {
-        File tmpDir = createTempDir("bkTest", ".dir");
-        File curDir = Bookie.getCurrentDirectory(tmpDir);
-        Bookie.checkDirectoryStructure(curDir);
+        entryLogger.shutdown();
 
         // enable pre-allocation case
-        ServerConfiguration conf = TestBKConfiguration.newServerConfiguration();
-        conf.setLedgerDirNames(new String[] {tmpDir.toString()});
         conf.setEntryLogFilePreAllocationEnabled(true);
-        Bookie bookie = new Bookie(conf);
+
+        entryLogger = new EntryLogger(conf, dirsMgr);
         // create a logger whose initialization phase allocating a new entry log
-        EntryLogger logger = ((InterleavedLedgerStorage) bookie.ledgerStorage).entryLogger;
-        assertNotNull(logger.getEntryLoggerAllocator().getPreallocationFuture());
+        assertNotNull(entryLogger.getEntryLoggerAllocator().getPreallocationFuture());
 
-        logger.addEntry(1, generateEntry(1, 1).nioBuffer());
+        entryLogger.addEntry(1, generateEntry(1, 1).nioBuffer());
         // the Future<BufferedLogChannel> is not null all the time
-        assertNotNull(logger.getEntryLoggerAllocator().getPreallocationFuture());
+        assertNotNull(entryLogger.getEntryLoggerAllocator().getPreallocationFuture());
+        entryLogger.shutdown();
 
         // disable pre-allocation case
-        ServerConfiguration conf2 = TestBKConfiguration.newServerConfiguration();
-        conf2.setLedgerDirNames(new String[] {tmpDir.toString()});
-        conf2.setEntryLogFilePreAllocationEnabled(false);
-        Bookie bookie2 = new Bookie(conf2);
+        conf.setEntryLogFilePreAllocationEnabled(false);
         // create a logger
-        EntryLogger logger2 = ((InterleavedLedgerStorage) bookie2.ledgerStorage).entryLogger;
-        assertNull(logger2.getEntryLoggerAllocator().getPreallocationFuture());
+        entryLogger = new EntryLogger(conf, dirsMgr);
+        assertNull(entryLogger.getEntryLoggerAllocator().getPreallocationFuture());
 
-        logger2.addEntry(2, generateEntry(1, 1).nioBuffer());
+        entryLogger.addEntry(2, generateEntry(1, 1).nioBuffer());
 
         // the Future<BufferedLogChannel> is null all the time
-        assertNull(logger2.getEntryLoggerAllocator().getPreallocationFuture());
-
+        assertNull(entryLogger.getEntryLoggerAllocator().getPreallocationFuture());
     }
 
     /**
@@ -375,30 +397,18 @@ public class EntryLogTest {
      */
     @Test
     public void testGetEntryLogsSet() throws Exception {
-        File tmpDir = createTempDir("bkTest", ".dir");
-        File curDir = Bookie.getCurrentDirectory(tmpDir);
-        Bookie.checkDirectoryStructure(curDir);
-
-        int gcWaitTime = 1000;
-        ServerConfiguration conf = TestBKConfiguration.newServerConfiguration();
-        conf.setGcWaitTime(gcWaitTime);
-        conf.setLedgerDirNames(new String[] { tmpDir.toString() });
-        Bookie bookie = new Bookie(conf);
-
         // create some entries
-        EntryLogger logger = ((InterleavedLedgerStorage) bookie.ledgerStorage).entryLogger;
-
-        assertEquals(Sets.newHashSet(0L, 1L), logger.getEntryLogsSet());
+        assertEquals(Sets.newHashSet(0L, 1L), entryLogger.getEntryLogsSet());
 
-        logger.rollLog();
-        logger.flushRotatedLogs();
+        entryLogger.rollLog();
+        entryLogger.flushRotatedLogs();
 
-        assertEquals(Sets.newHashSet(0L, 1L, 2L), logger.getEntryLogsSet());
+        assertEquals(Sets.newHashSet(0L, 1L, 2L), entryLogger.getEntryLogsSet());
 
-        logger.rollLog();
-        logger.flushRotatedLogs();
+        entryLogger.rollLog();
+        entryLogger.flushRotatedLogs();
 
-        assertEquals(Sets.newHashSet(0L, 1L, 2L, 3L), logger.getEntryLogsSet());
+        assertEquals(Sets.newHashSet(0L, 1L, 2L, 3L), entryLogger.getEntryLogsSet());
     }
 
     static class LedgerStorageWriteTask implements Callable<Boolean> {
diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SingleBookieInitializationTest.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SingleBookieInitializationTest.java
new file mode 100644
index 0000000..fe147cb
--- /dev/null
+++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SingleBookieInitializationTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+import org.apache.bookkeeper.bookie.BookieException.Code;
+import org.apache.bookkeeper.bookie.LedgerDirsManager.NoWritableLedgerDirException;
+import org.apache.bookkeeper.conf.ServerConfiguration;
+import org.apache.bookkeeper.conf.TestBKConfiguration;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Test a single bookie at readonly mode.
+ */
+public class SingleBookieInitializationTest {
+
+    @Rule
+    public final TemporaryFolder testDir = new TemporaryFolder();
+
+    private File journalDir;
+    private File ledgerDir;
+    private ServerConfiguration conf;
+    private Bookie bookie;
+
+    @Before
+    public void setUp() throws Exception {
+        this.journalDir = testDir.newFolder("journal");
+        this.ledgerDir = testDir.newFolder("ledgers");
+
+        this.conf = TestBKConfiguration.newServerConfiguration();
+        this.conf.setJournalDirsName(new String[] { journalDir.getAbsolutePath() });
+        this.conf.setLedgerDirNames(new String[] { ledgerDir.getAbsolutePath() });
+        this.conf.setMetadataServiceUri(null);
+        this.conf.setZkServers(null);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (null != this.bookie) {
+            this.bookie.shutdown();
+        }
+    }
+
+    private static String generateDataString(long ledger, long entry) {
+        return ("ledger-" + ledger + "-" + entry);
+    }
+
+    private static ByteBuf generateEntry(long ledger, long entry) {
+        byte[] data = generateDataString(ledger, entry).getBytes();
+        ByteBuf bb = Unpooled.buffer(8 + 8 + data.length);
+        bb.writeLong(ledger);
+        bb.writeLong(entry);
+        bb.writeBytes(data);
+        return bb;
+    }
+
+    @Test
+    public void testInitBookieNoWritableDirsButHasEnoughSpaces() throws Exception {
+        float usage = 1.0f - ((float) ledgerDir.getUsableSpace()) / ledgerDir.getTotalSpace();
+        conf.setDiskUsageThreshold(usage / 2);
+        conf.setDiskUsageWarnThreshold(usage / 3);
+        conf.setMinUsableSizeForEntryLogCreation(Long.MIN_VALUE);
+        conf.setLedgerStorageClass(InterleavedLedgerStorage.class.getName());
+
+        bookie = new Bookie(conf);
+        bookie.start();
+
+        CompletableFuture<Integer> writeFuture = new CompletableFuture<>();
+        bookie.addEntry(
+            generateEntry(1L, 2L),
+            false,
+            (rc, ledgerId, entryId, addr, ctx) -> writeFuture.complete(rc),
+            null,
+            new byte[0]
+        );
+        assertEquals(Code.OK, writeFuture.get().intValue());
+    }
+
+    @Test
+    public void testInitBookieNoWritableDirsAndNoEnoughSpaces() throws Exception {
+        float usage = 1.0f - ((float) ledgerDir.getUsableSpace()) / ledgerDir.getTotalSpace();
+        conf.setDiskUsageThreshold(usage / 2);
+        conf.setDiskUsageWarnThreshold(usage / 3);
+        conf.setMinUsableSizeForEntryLogCreation(Long.MAX_VALUE);
+        conf.setLedgerStorageClass(InterleavedLedgerStorage.class.getName());
+
+        bookie = new Bookie(conf);
+        bookie.start();
+
+        try {
+            bookie.addEntry(
+                generateEntry(1L, 2L),
+                false,
+                (rc, ledgerId, entryId, addr, ctx) -> {},
+                null,
+                new byte[0]
+            );
+            fail("Should fail on creating new entry log file"
+                + " since there is no enough disk space to accommodate writes");
+        } catch (IOException ioe) {
+            // expected
+            assertTrue(ioe.getCause() instanceof NoWritableLedgerDirException);
+        }
+    }
+
+}
diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/TestLedgerDirsManager.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/TestLedgerDirsManager.java
index 1731b9b..b8555ca 100644
--- a/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/TestLedgerDirsManager.java
+++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/TestLedgerDirsManager.java
@@ -100,6 +100,7 @@ public class TestLedgerDirsManager {
         conf.setDiskLowWaterMarkUsageThreshold(conf.getDiskUsageThreshold());
         conf.setDiskCheckInterval(diskCheckInterval);
         conf.setIsForceGCAllowWhenNoSpace(true);
+        conf.setMinUsableSizeForEntryLogCreation(Long.MIN_VALUE);
 
         executor = PowerMockito.mock(ScheduledExecutorService.class);
         executorController = new MockExecutorController()
@@ -165,7 +166,7 @@ public class TestLedgerDirsManager {
         List<File> writeDirs;
         try {
             dirsManager.addToFilledDirs(curDir);
-            writeDirs = dirsManager.getWritableLedgerDirs();
+            dirsManager.getWritableLedgerDirs();
             fail("Should not reach here due to there is no writable ledger dir.");
         } catch (NoWritableLedgerDirException nwlde) {
             // expected to fail with no writable ledger dir
@@ -180,6 +181,27 @@ public class TestLedgerDirsManager {
     }
 
     @Test
+    public void testGetWritableDirForLogNoEnoughDiskSpace() throws Exception {
+        conf.setMinUsableSizeForEntryLogCreation(curDir.getUsableSpace() + 1024);
+        dirsManager = new LedgerDirsManager(conf, conf.getLedgerDirs(),
+            new DiskChecker(conf.getDiskUsageThreshold(), conf.getDiskUsageWarnThreshold()), statsLogger);
+        try {
+            dirsManager.addToFilledDirs(curDir);
+            dirsManager.getWritableLedgerDirs();
+            fail("Should not reach here due to there is no writable ledger dir.");
+        } catch (NoWritableLedgerDirException nwlde) {
+            // expected to fail with no writable ledger dir
+            // Now make sure we can get one for log
+            try {
+                dirsManager.getWritableLedgerDirsForNewLog();
+                fail("Should not reach here due to there is no enough disk space left");
+            } catch (NoWritableLedgerDirException e) {
+                // expected.
+            }
+        }
+    }
+
+    @Test
     public void testLedgerDirsMonitorDuringTransition() throws Exception {
         testLedgerDirsMonitorDuringTransition(true);
     }
diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/test/ReadOnlyBookieTest.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/test/ReadOnlyBookieTest.java
index a34bbcb..75aaa93 100644
--- a/bookkeeper-server/src/test/java/org/apache/bookkeeper/test/ReadOnlyBookieTest.java
+++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/test/ReadOnlyBookieTest.java
@@ -46,6 +46,7 @@ public class ReadOnlyBookieTest extends BookKeeperClusterTestCase {
         super(2);
         baseConf.setLedgerStorageClass(InterleavedLedgerStorage.class.getName());
         baseConf.setEntryLogFilePreAllocationEnabled(false);
+        baseConf.setMinUsableSizeForEntryLogCreation(Long.MAX_VALUE);
     }
 
     /**

-- 
To stop receiving notification emails like this one, please contact
sijie@apache.org.