Skip to content

Commit ccfabfa

Browse files
authored
fix Bookkeeper can't startup cause by 'IOException: Recovery log xxx is missing' (apache#4740)
* fix apache#4105 When journal and dbledgerstorage on the same disk, concurrent checkpointComplete calls from SyncThread and SingleDirectoryDbLedgerStorage.flush() can overwrite lastMark backwards,causing journal files to be deleted while still referenced by the older mark. * fix apache#4105 When singleLedgerDirs=true, bookie can crash on restart with "Recovery log is missing" due to lastMark file being overwritten backwards. Root cause SyncThread.flush() has a nested call pattern that causes lastMark regression: SyncThread.flush(): 1. outerCheckpoint = newCheckpoint() → mark = (file 5, offset 100) 2. ledgerStorage.flush() → SingleDirectoryDbLedgerStorage.flush(): a. innerCheckpoint = newCheckpoint() → mark = (file 7, offset 200), journal has advanced b. flushes data to disk c. checkpointComplete(mark=7, compact=true) → persists lastMark=7, deletes journal files with id < 7 (including file 5) 3. checkpointComplete(mark=5, compact=false) → persists lastMark=5, overwriting the 7 written in step 2c 4. Bookie restarts → reads lastMark=5 → file 5 no longer exists → crash Step 1 captures the checkpoint before step 2 runs, so it is always older. Step 2c advances lastMark and garbage-collects old journals. Step 3 then overwrites lastMark backwards to a position whose journal file was already deleted. Conditions - singleLedgerDirs=true (journal and ledger on the same disk, so SingleDirectoryDbLedgerStorage.flush() calls checkpointComplete internally) - Journal file rotates between the two newCheckpoint() calls (requires sufficient write throughput) - maxBackupJournals small enough for old files to actually be deleted Fix Add a monotonic guard in checkpointComplete(): track the highest mark persisted so far, skip any call with an older mark. This prevents rollLog from overwriting lastMark backwards. * fix apache#4105 When singleLedgerDirs=true, bookie can crash on restart with "Recovery log is missing" due to lastMark file being overwritten backwards. Root cause SyncThread.flush() has a nested call pattern that causes lastMark regression: SyncThread.flush(): 1. outerCheckpoint = newCheckpoint() → mark = (file 5, offset 100) 2. ledgerStorage.flush() → SingleDirectoryDbLedgerStorage.flush(): a. innerCheckpoint = newCheckpoint() → mark = (file 7, offset 200), journal has advanced b. flushes data to disk c. checkpointComplete(mark=7, compact=true) → persists lastMark=7, deletes journal files with id < 7 (including file 5) 3. checkpointComplete(mark=5, compact=false) → persists lastMark=5, overwriting the 7 written in step 2c 4. Bookie restarts → reads lastMark=5 → file 5 no longer exists → crash Step 1 captures the checkpoint before step 2 runs, so it is always older. Step 2c advances lastMark and garbage-collects old journals. Step 3 then overwrites lastMark backwards to a position whose journal file was already deleted. Conditions - singleLedgerDirs=true (journal and ledger on the same disk, so SingleDirectoryDbLedgerStorage.flush() calls checkpointComplete internally) - Journal file rotates between the two newCheckpoint() calls (requires sufficient write throughput) - maxBackupJournals small enough for old files to actually be deleted Fix Add a monotonic guard in checkpointComplete(): track the highest mark persisted so far, skip any call with an older mark. This prevents rollLog from overwriting lastMark backwards. * fix apache#4105 When singleLedgerDirs=true, bookie can crash on restart with "Recovery log is missing" due to lastMark file being overwritten backwards. Root cause SyncThread.flush() has a nested call pattern that causes lastMark regression: SyncThread.flush(): 1. outerCheckpoint = newCheckpoint() → mark = (file 5, offset 100) 2. ledgerStorage.flush() → SingleDirectoryDbLedgerStorage.flush(): a. innerCheckpoint = newCheckpoint() → mark = (file 7, offset 200), journal has advanced b. flushes data to disk c. checkpointComplete(mark=7, compact=true) → persists lastMark=7, deletes journal files with id < 7 (including file 5) 3. checkpointComplete(mark=5, compact=false) → persists lastMark=5, overwriting the 7 written in step 2c 4. Bookie restarts → reads lastMark=5 → file 5 no longer exists → crash Step 1 captures the checkpoint before step 2 runs, so it is always older. Step 2c advances lastMark and garbage-collects old journals. Step 3 then overwrites lastMark backwards to a position whose journal file was already deleted. Conditions - singleLedgerDirs=true (journal and ledger on the same disk, so SingleDirectoryDbLedgerStorage.flush() calls checkpointComplete internally) - Journal file rotates between the two newCheckpoint() calls (requires sufficient write throughput) - maxBackupJournals small enough for old files to actually be deleted Fix Add a monotonic guard in checkpointComplete(): track the highest mark persisted so far, skip any call with an older mark. This prevents rollLog from overwriting lastMark backwards. * Fix lastMark regression in checkpoint completion
1 parent 8e88b03 commit ccfabfa

2 files changed

Lines changed: 269 additions & 1 deletion

File tree

bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/Journal.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,10 @@ static void writePaddingBytes(JournalChannel jc, ByteBuf paddingBuffer, int jour
628628

629629
private final LastLogMark lastLogMark = new LastLogMark(0, 0);
630630

631+
// Ensures lastMark only advances forward across concurrent checkpointComplete calls.
632+
private final Object checkpointLock = new Object();
633+
private final LogMark lastPersistedMark = new LogMark(0, 0);
634+
631635
private static final String LAST_MARK_DEFAULT_NAME = "lastMark";
632636

633637
private final String lastMarkFileName;
@@ -705,6 +709,8 @@ public Journal(int journalIndex, File journalDirectory, ServerConfiguration conf
705709
lastMarkFileName = LAST_MARK_DEFAULT_NAME + "." + journalIndex;
706710
}
707711
lastLogMark.readLog();
712+
lastPersistedMark.setLogMark(
713+
lastLogMark.getCurMark().getLogFileId(), lastLogMark.getCurMark().getLogFileOffset());
708714
log.debug().attr("lastMark", () -> lastLogMark.getCurMark()).log("Last Log Mark");
709715

710716
try {
@@ -771,6 +777,9 @@ public Checkpoint newCheckpoint() {
771777
/**
772778
* Telling journal a checkpoint is finished.
773779
*
780+
* <p>Skips if the checkpoint is older than the last persisted mark,
781+
* preventing lastMark from regressing backwards.
782+
*
774783
* @throws IOException
775784
*/
776785
@Override
@@ -781,7 +790,16 @@ public void checkpointComplete(Checkpoint checkpoint, boolean compact) throws IO
781790
LogMarkCheckpoint lmcheckpoint = (LogMarkCheckpoint) checkpoint;
782791
LastLogMark mark = lmcheckpoint.mark;
783792

784-
mark.rollLog(mark);
793+
// See #4105: keep the monotonic decision and lastMark persistence together.
794+
synchronized (checkpointLock) {
795+
if (mark.getCurMark().compare(lastPersistedMark) < 0) {
796+
return;
797+
}
798+
persistLastLogMark(mark);
799+
lastPersistedMark.setLogMark(
800+
mark.getCurMark().getLogFileId(), mark.getCurMark().getLogFileOffset());
801+
}
802+
785803
if (compact) {
786804
// list the journals that have been marked
787805
List<Long> logs = listJournalIds(journalDirectory, new JournalRollingFilter(mark));
@@ -803,6 +821,16 @@ public void checkpointComplete(Checkpoint checkpoint, boolean compact) throws IO
803821
}
804822
}
805823

824+
@VisibleForTesting
825+
protected void persistLastLogMark(LastLogMark mark) throws NoWritableLedgerDirException {
826+
mark.rollLog(mark);
827+
}
828+
829+
@VisibleForTesting
830+
protected boolean isCheckpointCompleteLockHeldByCurrentThread() {
831+
return Thread.holdsLock(checkpointLock);
832+
}
833+
806834
/**
807835
* Scan the journal.
808836
*

bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/storage/ldb/DbLedgerStorageTest.java

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,17 @@
3232
import io.netty.util.ReferenceCountUtil;
3333
import java.io.File;
3434
import java.io.FileInputStream;
35+
import java.io.FileOutputStream;
3536
import java.io.IOException;
3637
import java.lang.reflect.Field;
3738
import java.nio.ByteBuffer;
3839
import java.util.List;
3940
import java.util.Set;
41+
import java.util.concurrent.CountDownLatch;
42+
import java.util.concurrent.ExecutorService;
43+
import java.util.concurrent.Executors;
44+
import java.util.concurrent.Future;
45+
import java.util.concurrent.TimeUnit;
4046
import org.apache.bookkeeper.bookie.Bookie;
4147
import org.apache.bookkeeper.bookie.Bookie.NoEntryException;
4248
import org.apache.bookkeeper.bookie.BookieException;
@@ -45,14 +51,17 @@
4551
import org.apache.bookkeeper.bookie.CheckpointSourceList;
4652
import org.apache.bookkeeper.bookie.DefaultEntryLogger;
4753
import org.apache.bookkeeper.bookie.EntryLocation;
54+
import org.apache.bookkeeper.bookie.Journal;
4855
import org.apache.bookkeeper.bookie.LedgerDirsManager;
56+
import org.apache.bookkeeper.bookie.LedgerDirsManager.NoWritableLedgerDirException;
4957
import org.apache.bookkeeper.bookie.LedgerStorage;
5058
import org.apache.bookkeeper.bookie.LogMark;
5159
import org.apache.bookkeeper.bookie.TestBookieImpl;
5260
import org.apache.bookkeeper.bookie.storage.EntryLogger;
5361
import org.apache.bookkeeper.conf.ServerConfiguration;
5462
import org.apache.bookkeeper.conf.TestBKConfiguration;
5563
import org.apache.bookkeeper.proto.BookieProtocol;
64+
import org.apache.bookkeeper.util.DiskChecker;
5665
import org.junit.After;
5766
import org.junit.Assert;
5867
import org.junit.Before;
@@ -881,4 +890,235 @@ public void testSingleLedgerDirectoryCheckpointTriggerRemovePendingDeletedLedger
881890
bookie.getLedgerStorage().flush();
882891
Assert.assertEquals(pendingDeletedLedgers.size(), 0);
883892
}
893+
894+
@Test
895+
public void testCheckpointCompleteSkipsStaleCheckpointAfterNewerCheckpoint() throws Exception {
896+
File baseDir = new File(tmpDir, "journalStaleCheckpointTest");
897+
File ledgerDir = new File(baseDir, "ledger");
898+
File journalBaseDir = new File(baseDir, "journal");
899+
File journalDir = BookieImpl.getCurrentDirectory(journalBaseDir);
900+
ServerConfiguration conf = TestBKConfiguration.newServerConfiguration();
901+
conf.setGcWaitTime(1000);
902+
conf.setProperty(DbLedgerStorage.WRITE_CACHE_MAX_SIZE_MB, 4);
903+
conf.setProperty(DbLedgerStorage.READ_AHEAD_CACHE_MAX_SIZE_MB, 4);
904+
conf.setLedgerStorageClass(DbLedgerStorage.class.getName());
905+
conf.setLedgerDirNames(new String[] { ledgerDir.getCanonicalPath() });
906+
conf.setJournalDirName(journalBaseDir.getCanonicalPath());
907+
conf.setMaxBackupJournals(0);
908+
909+
BookieImpl.checkDirectoryStructure(journalDir);
910+
LedgerDirsManager dirsManager = new LedgerDirsManager(conf, conf.getLedgerDirs(),
911+
new DiskChecker(0.95f, 0.90f));
912+
Journal journal = new Journal(0, journalDir, conf, dirsManager);
913+
File ledgerDirMark = new File(BookieImpl.getCurrentDirectory(ledgerDir), "lastMark");
914+
createJournalFiles(journalDir, 3, 10);
915+
916+
CheckpointSource checkpointSource = new CheckpointSourceList(Lists.newArrayList(journal));
917+
918+
journal.getLastLogMark().getCurMark().setLogMark(7, 100);
919+
CheckpointSource.Checkpoint staleCheckpoint = checkpointSource.newCheckpoint();
920+
921+
journal.getLastLogMark().getCurMark().setLogMark(9, 300);
922+
CheckpointSource.Checkpoint newerCheckpoint = checkpointSource.newCheckpoint();
923+
924+
checkpointSource.checkpointComplete(newerCheckpoint, true);
925+
checkpointSource.checkpointComplete(staleCheckpoint, true);
926+
927+
LogMark markAfterStaleCheckpoint = readLogMark(ledgerDirMark);
928+
assertEquals("lastMark must stay at newer checkpoint",
929+
9, markAfterStaleCheckpoint.getLogFileId());
930+
assertEquals(300, markAfterStaleCheckpoint.getLogFileOffset());
931+
932+
for (long id = 3; id <= 8; id++) {
933+
File journalFile = new File(journalDir, Long.toHexString(id) + ".txn");
934+
assertFalse("Journal " + id + " should have been garbage collected", journalFile.exists());
935+
}
936+
for (long id = 9; id <= 10; id++) {
937+
File journalFile = new File(journalDir, Long.toHexString(id) + ".txn");
938+
assertTrue("Journal " + id + " should still exist", journalFile.exists());
939+
}
940+
}
941+
942+
@Test
943+
public void testCheckpointCompleteUsesReloadedLastMarkAsPersistedMark() throws Exception {
944+
File baseDir = new File(tmpDir, "journalReloadedLastMarkTest");
945+
File ledgerDir = new File(baseDir, "ledger");
946+
File journalBaseDir = new File(baseDir, "journal");
947+
File journalDir = BookieImpl.getCurrentDirectory(journalBaseDir);
948+
ServerConfiguration conf = TestBKConfiguration.newServerConfiguration();
949+
conf.setGcWaitTime(1000);
950+
conf.setProperty(DbLedgerStorage.WRITE_CACHE_MAX_SIZE_MB, 4);
951+
conf.setProperty(DbLedgerStorage.READ_AHEAD_CACHE_MAX_SIZE_MB, 4);
952+
conf.setLedgerStorageClass(DbLedgerStorage.class.getName());
953+
conf.setLedgerDirNames(new String[] { ledgerDir.getCanonicalPath() });
954+
conf.setJournalDirName(journalBaseDir.getCanonicalPath());
955+
conf.setMaxBackupJournals(0);
956+
957+
BookieImpl.checkDirectoryStructure(journalDir);
958+
LedgerDirsManager dirsManager = new LedgerDirsManager(conf, conf.getLedgerDirs(),
959+
new DiskChecker(0.95f, 0.90f));
960+
File ledgerDirMark = new File(BookieImpl.getCurrentDirectory(ledgerDir), "lastMark");
961+
writeLogMark(ledgerDirMark, 9, 300);
962+
createJournalFiles(journalDir, 7, 10);
963+
964+
Journal journal = new Journal(0, journalDir, conf, dirsManager);
965+
CheckpointSource checkpointSource = new CheckpointSourceList(Lists.newArrayList(journal));
966+
967+
journal.getLastLogMark().getCurMark().setLogMark(7, 100);
968+
CheckpointSource.Checkpoint staleCheckpoint = checkpointSource.newCheckpoint();
969+
checkpointSource.checkpointComplete(staleCheckpoint, true);
970+
971+
LogMark markAfterStaleCheckpoint = readLogMark(ledgerDirMark);
972+
assertEquals("Reloaded lastMark must seed the monotonic guard",
973+
9, markAfterStaleCheckpoint.getLogFileId());
974+
assertEquals(300, markAfterStaleCheckpoint.getLogFileOffset());
975+
}
976+
977+
@Test
978+
public void testConcurrentCheckpointCompleteLastMarkRegression() throws Exception {
979+
File baseDir = new File(tmpDir, "journalMissingTest");
980+
File ledgerDir = new File(baseDir, "ledger");
981+
File journalBaseDir = new File(baseDir, "journal");
982+
File journalDir = BookieImpl.getCurrentDirectory(journalBaseDir);
983+
ServerConfiguration conf = TestBKConfiguration.newServerConfiguration();
984+
conf.setGcWaitTime(1000);
985+
conf.setProperty(DbLedgerStorage.WRITE_CACHE_MAX_SIZE_MB, 4);
986+
conf.setProperty(DbLedgerStorage.READ_AHEAD_CACHE_MAX_SIZE_MB, 4);
987+
conf.setLedgerStorageClass(DbLedgerStorage.class.getName());
988+
conf.setLedgerDirNames(new String[] { ledgerDir.getCanonicalPath() });
989+
conf.setJournalDirName(journalBaseDir.getCanonicalPath());
990+
conf.setMaxBackupJournals(0);
991+
992+
BookieImpl.checkDirectoryStructure(journalDir);
993+
LedgerDirsManager dirsManager = new LedgerDirsManager(conf, conf.getLedgerDirs(),
994+
new DiskChecker(0.95f, 0.90f));
995+
CountDownLatch stalePersistStarted = new CountDownLatch(1);
996+
CountDownLatch releaseStalePersist = new CountDownLatch(1);
997+
CountDownLatch newerPersisted = new CountDownLatch(1);
998+
BlockingJournal journal = new BlockingJournal(0, journalDir, conf, dirsManager,
999+
stalePersistStarted, releaseStalePersist, newerPersisted);
1000+
File ledgerDirMark = new File(BookieImpl.getCurrentDirectory(ledgerDir), "lastMark");
1001+
createJournalFiles(journalDir, 3, 10);
1002+
1003+
CheckpointSource checkpointSource = new CheckpointSourceList(Lists.newArrayList(journal));
1004+
1005+
journal.getLastLogMark().getCurMark().setLogMark(7, 100);
1006+
CheckpointSource.Checkpoint staleCheckpoint = checkpointSource.newCheckpoint();
1007+
1008+
journal.getLastLogMark().getCurMark().setLogMark(9, 300);
1009+
CheckpointSource.Checkpoint newerCheckpoint = checkpointSource.newCheckpoint();
1010+
1011+
ExecutorService executor = Executors.newFixedThreadPool(2);
1012+
try {
1013+
Future<?> staleFuture = executor.submit(() -> {
1014+
checkpointSource.checkpointComplete(staleCheckpoint, true);
1015+
return null;
1016+
});
1017+
assertTrue("Stale checkpoint should reach lastMark persistence",
1018+
stalePersistStarted.await(10, TimeUnit.SECONDS));
1019+
1020+
Future<?> newerFuture = executor.submit(() -> {
1021+
checkpointSource.checkpointComplete(newerCheckpoint, true);
1022+
return null;
1023+
});
1024+
releaseStalePersist.countDown();
1025+
1026+
newerFuture.get(10, TimeUnit.SECONDS);
1027+
staleFuture.get(10, TimeUnit.SECONDS);
1028+
} finally {
1029+
executor.shutdownNow();
1030+
}
1031+
1032+
LogMark markAfterFlush = readLogMark(ledgerDirMark);
1033+
assertEquals("lastMark must not regress after concurrent checkpoints",
1034+
9, markAfterFlush.getLogFileId());
1035+
assertEquals(300, markAfterFlush.getLogFileOffset());
1036+
1037+
for (long id = 3; id <= 8; id++) {
1038+
File journalFile = new File(journalDir, Long.toHexString(id) + ".txn");
1039+
assertFalse("Journal " + id + " should have been garbage collected", journalFile.exists());
1040+
}
1041+
for (long id = 9; id <= 10; id++) {
1042+
File journalFile = new File(journalDir, Long.toHexString(id) + ".txn");
1043+
assertTrue("Journal " + id + " should still exist", journalFile.exists());
1044+
}
1045+
1046+
journal.getLastLogMark().getCurMark().setLogMark(0, 0);
1047+
journal.getLastLogMark().readLog();
1048+
LogMark restartMark = journal.getLastLogMark().getCurMark();
1049+
assertEquals("Reloaded lastMark should be 9", 9, restartMark.getLogFileId());
1050+
assertEquals("Reloaded lastMark offset should be restored from disk",
1051+
300, restartMark.getLogFileOffset());
1052+
1053+
File markedJournal = new File(journalDir,
1054+
Long.toHexString(restartMark.getLogFileId()) + ".txn");
1055+
assertTrue("Journal file " + restartMark.getLogFileId() + " must exist for recovery",
1056+
markedJournal.exists());
1057+
1058+
List<Long> replayLogs = Journal.listJournalIds(journalDir,
1059+
journalId -> journalId >= restartMark.getLogFileId());
1060+
assertTrue("Replay journal list must contain the marked journal",
1061+
replayLogs.size() > 0 && replayLogs.get(0) == restartMark.getLogFileId());
1062+
}
1063+
1064+
private static void createJournalFiles(File journalDir, long startId, long endId) throws IOException {
1065+
for (long id = startId; id <= endId; id++) {
1066+
File journalFile = new File(journalDir, Long.toHexString(id) + ".txn");
1067+
assertTrue("Failed to create journal file " + id, journalFile.createNewFile());
1068+
}
1069+
}
1070+
1071+
private static void writeLogMark(File file, long logFileId, long logFileOffset) throws IOException {
1072+
byte[] buff = new byte[16];
1073+
ByteBuffer bb = ByteBuffer.wrap(buff);
1074+
LogMark mark = new LogMark(logFileId, logFileOffset);
1075+
mark.writeLogMark(bb);
1076+
try (FileOutputStream fos = new FileOutputStream(file)) {
1077+
fos.write(buff);
1078+
fos.getChannel().force(true);
1079+
}
1080+
}
1081+
1082+
private static void awaitLatch(CountDownLatch latch, String message) {
1083+
try {
1084+
assertTrue(message, latch.await(10, TimeUnit.SECONDS));
1085+
} catch (InterruptedException e) {
1086+
Thread.currentThread().interrupt();
1087+
throw new AssertionError(message, e);
1088+
}
1089+
}
1090+
1091+
private static final class BlockingJournal extends Journal {
1092+
private static final long STALE_LOG_ID = 7;
1093+
private static final long NEWER_LOG_ID = 9;
1094+
1095+
private final CountDownLatch stalePersistStarted;
1096+
private final CountDownLatch releaseStalePersist;
1097+
private final CountDownLatch newerPersisted;
1098+
1099+
BlockingJournal(int journalIndex, File journalDirectory, ServerConfiguration conf,
1100+
LedgerDirsManager ledgerDirsManager, CountDownLatch stalePersistStarted,
1101+
CountDownLatch releaseStalePersist, CountDownLatch newerPersisted) {
1102+
super(journalIndex, journalDirectory, conf, ledgerDirsManager);
1103+
this.stalePersistStarted = stalePersistStarted;
1104+
this.releaseStalePersist = releaseStalePersist;
1105+
this.newerPersisted = newerPersisted;
1106+
}
1107+
1108+
@Override
1109+
protected void persistLastLogMark(LastLogMark mark) throws NoWritableLedgerDirException {
1110+
long logFileId = mark.getCurMark().getLogFileId();
1111+
if (logFileId == STALE_LOG_ID) {
1112+
stalePersistStarted.countDown();
1113+
awaitLatch(releaseStalePersist, "Timed out waiting to release stale checkpoint");
1114+
if (!isCheckpointCompleteLockHeldByCurrentThread()) {
1115+
awaitLatch(newerPersisted, "Timed out waiting for newer checkpoint to persist");
1116+
}
1117+
}
1118+
super.persistLastLogMark(mark);
1119+
if (logFileId == NEWER_LOG_ID) {
1120+
newerPersisted.countDown();
1121+
}
1122+
}
1123+
}
8841124
}

0 commit comments

Comments
 (0)