|
48 | 48 | import java.nio.channels.FileChannel; |
49 | 49 | import java.nio.charset.Charset; |
50 | 50 | import java.util.ArrayList; |
| 51 | +import java.util.Collection; |
51 | 52 | import java.util.HashSet; |
52 | 53 | import java.util.LinkedList; |
53 | 54 | import java.util.List; |
|
65 | 66 | import java.util.concurrent.locks.Lock; |
66 | 67 | import org.apache.bookkeeper.bookie.DefaultEntryLogger.BufferedLogChannel; |
67 | 68 | import org.apache.bookkeeper.bookie.LedgerDirsManager.NoWritableLedgerDirException; |
| 69 | +import org.apache.bookkeeper.bookie.storage.CompactionEntryLog; |
| 70 | +import org.apache.bookkeeper.bookie.storage.EntryLogScanner; |
68 | 71 | import org.apache.bookkeeper.common.testing.annotations.FlakyTest; |
69 | 72 | import org.apache.bookkeeper.conf.ServerConfiguration; |
70 | 73 | import org.apache.bookkeeper.conf.TestBKConfiguration; |
@@ -983,6 +986,165 @@ public void testReadEntryWithoutLedgerID() throws Exception { |
983 | 986 | } |
984 | 987 | } |
985 | 988 |
|
| 989 | + @Test |
| 990 | + public void testRecoverCompactedLogUsesHexFinalLogFileNameForLogIdTen() throws Exception { |
| 991 | + // The next compaction log id is previousLogId + 1. Log id 10 is named "a.log", |
| 992 | + // so recovery must not rebuild the final log file as the decimal name "10.log". |
| 993 | + verifyRecoveredCompactionLogUsesExpectedFinalLogFileName(9, "a.log", "10.log"); |
| 994 | + } |
| 995 | + |
| 996 | + @Test |
| 997 | + public void testRecoverCompactedLogUsesHexFinalLogFileNameForLogIdSixteen() throws Exception { |
| 998 | + // Log id 16 is named "10.log". This covers a numeric-looking hex name that |
| 999 | + // was previously rebuilt as the decimal name "16.log" during recovery. |
| 1000 | + verifyRecoveredCompactionLogUsesExpectedFinalLogFileName(15, "10.log", "16.log"); |
| 1001 | + } |
| 1002 | + |
| 1003 | + @Test |
| 1004 | + public void testRecoverCompactedLogUsesHexFinalLogFileNameForLargerLogId() throws Exception { |
| 1005 | + // Use a larger id to make the decimal/hex mismatch obvious: 500 decimal is |
| 1006 | + // "1f4" in the entry log file name. |
| 1007 | + verifyRecoveredCompactionLogUsesExpectedFinalLogFileName(499, "1f4.log", "500.log"); |
| 1008 | + } |
| 1009 | + |
| 1010 | + @Test |
| 1011 | + public void testRecoverCompactedLogKeepsSingleDigitFinalLogFileName() throws Exception { |
| 1012 | + // Single-digit ids have the same decimal and hex file name. Keep this case to |
| 1013 | + // show the recovery path remains compatible when the old and new names match. |
| 1014 | + verifyRecoveredCompactionLogUsesExpectedFinalLogFileName(8, "9.log", null); |
| 1015 | + } |
| 1016 | + |
| 1017 | + @Test |
| 1018 | + public void testNewCompactionLogKeepsHexFinalLogFileName() throws Exception { |
| 1019 | + long sourceLogId = 1L; |
| 1020 | + long previousLogId = 499L; |
| 1021 | + long expectedCompactionLogId = previousLogId + 1; |
| 1022 | + String expectedFinalLogFileName = "1f4.log"; |
| 1023 | + long ledgerId = 1234L; |
| 1024 | + long entryId = 5L; |
| 1025 | + ByteBuf entry = generateEntry(ledgerId, entryId); |
| 1026 | + |
| 1027 | + resetEntryLoggerWithLastLogId(previousLogId); |
| 1028 | + |
| 1029 | + // This is the normal, non-recovery compaction path. finalLogFile is derived |
| 1030 | + // from the current compaction log file name, so it should already be hex. |
| 1031 | + CompactionEntryLog compactionLog = entryLogger.newCompactionLog(sourceLogId); |
| 1032 | + assertEquals(expectedCompactionLogId, compactionLog.getDstLogId()); |
| 1033 | + |
| 1034 | + try { |
| 1035 | + compactionLog.addEntry(ledgerId, entry); |
| 1036 | + compactionLog.flush(); |
| 1037 | + // markCompacted() leaves a recovery marker such as |
| 1038 | + // "1f4.log.1.compacted" and removes the temporary compacting file. |
| 1039 | + compactionLog.markCompacted(); |
| 1040 | + |
| 1041 | + File compactedFile = new File(curDir, expectedFinalLogFileName + "." |
| 1042 | + + DefaultEntryLogger.logId2HexString(sourceLogId) |
| 1043 | + + TransactionalEntryLogCompactor.COMPACTED_SUFFIX); |
| 1044 | + File finalLogFile = new File(curDir, expectedFinalLogFileName); |
| 1045 | + assertTrue(compactedFile.exists(), "Compacted recovery marker should exist"); |
| 1046 | + assertFalse(finalLogFile.exists(), "Final log file should not exist before makeAvailable"); |
| 1047 | + |
| 1048 | + // makeAvailable() is the point where the final entry log file is created |
| 1049 | + // on disk. Normal compaction must create "1f4.log", not "500.log". |
| 1050 | + compactionLog.makeAvailable(); |
| 1051 | + assertTrue(finalLogFile.exists(), "Normal compaction path should keep the hex-named final log file"); |
| 1052 | + assertFalse(new File(curDir, "500.log").exists(), |
| 1053 | + "Normal compaction path should not create a decimal-named log file"); |
| 1054 | + |
| 1055 | + assertCompactionLogContainsEntry(compactionLog, ledgerId, entryId); |
| 1056 | + } finally { |
| 1057 | + ReferenceCountUtil.release(entry); |
| 1058 | + } |
| 1059 | + } |
| 1060 | + |
| 1061 | + private void verifyRecoveredCompactionLogUsesExpectedFinalLogFileName(long previousLogId, |
| 1062 | + String expectedFinalLogFileName, |
| 1063 | + String wrongDecimalLogFileName) |
| 1064 | + throws Exception { |
| 1065 | + long sourceLogId = 1L; |
| 1066 | + long expectedCompactionLogId = previousLogId + 1; |
| 1067 | + long ledgerId = 1234L; |
| 1068 | + long entryId = 5L; |
| 1069 | + ByteBuf entry = generateEntry(ledgerId, entryId); |
| 1070 | + |
| 1071 | + // Force the next compaction log id to a known value. This lets the test |
| 1072 | + // control both the compacted marker name and the expected final log name. |
| 1073 | + resetEntryLoggerWithLastLogId(previousLogId); |
| 1074 | + |
| 1075 | + CompactionEntryLog compactionLog = entryLogger.newCompactionLog(sourceLogId); |
| 1076 | + assertEquals(expectedCompactionLogId, compactionLog.getDstLogId()); |
| 1077 | + |
| 1078 | + try { |
| 1079 | + compactionLog.addEntry(ledgerId, entry); |
| 1080 | + compactionLog.flush(); |
| 1081 | + // Simulate a restart after the compaction log was marked compacted but |
| 1082 | + // before makeAvailable()/index recovery finished. The remaining file is |
| 1083 | + // named "<dst-hex>.log.<src-hex>.compacted". |
| 1084 | + compactionLog.markCompacted(); |
| 1085 | + |
| 1086 | + File compactedFile = new File(curDir, expectedFinalLogFileName + "." |
| 1087 | + + DefaultEntryLogger.logId2HexString(sourceLogId) |
| 1088 | + + TransactionalEntryLogCompactor.COMPACTED_SUFFIX); |
| 1089 | + assertTrue(compactedFile.exists(), "Compacted recovery marker should exist"); |
| 1090 | + assertFalse(new File(curDir, expectedFinalLogFileName).exists(), |
| 1091 | + "Final log file should not exist before recovery"); |
| 1092 | + |
| 1093 | + // incompleteCompactionLogs() rebuilds EntryLoggerCompactionEntryLog from |
| 1094 | + // the marker. The bug was here: the rebuilt finalLogFile used the decimal |
| 1095 | + // compactionLogId instead of the original hex entry log file name. |
| 1096 | + Collection<CompactionEntryLog> incompleteLogs = entryLogger.incompleteCompactionLogs(); |
| 1097 | + assertEquals(1, incompleteLogs.size()); |
| 1098 | + |
| 1099 | + CompactionEntryLog recoveredLog = incompleteLogs.iterator().next(); |
| 1100 | + assertEquals(expectedCompactionLogId, recoveredLog.getDstLogId()); |
| 1101 | + assertEquals(sourceLogId, recoveredLog.getSrcLogId()); |
| 1102 | + |
| 1103 | + // Recovery creates the final entry log when makeAvailable() is called. |
| 1104 | + // It must create the hex name and must not leave a decimal-named log. |
| 1105 | + recoveredLog.makeAvailable(); |
| 1106 | + assertTrue(new File(curDir, expectedFinalLogFileName).exists(), |
| 1107 | + "Recovery should make the hex-named log file available"); |
| 1108 | + if (wrongDecimalLogFileName != null) { |
| 1109 | + assertFalse(new File(curDir, wrongDecimalLogFileName).exists(), |
| 1110 | + "Recovery should not create a decimal-named log file"); |
| 1111 | + } |
| 1112 | + |
| 1113 | + assertCompactionLogContainsEntry(recoveredLog, ledgerId, entryId); |
| 1114 | + } finally { |
| 1115 | + ReferenceCountUtil.release(entry); |
| 1116 | + } |
| 1117 | + } |
| 1118 | + |
| 1119 | + private void assertCompactionLogContainsEntry(CompactionEntryLog compactionLog, |
| 1120 | + long ledgerId, |
| 1121 | + long entryId) throws Exception { |
| 1122 | + AtomicBoolean foundEntry = new AtomicBoolean(false); |
| 1123 | + // Scanning uses the compaction log id to resolve the final log file. This |
| 1124 | + // catches cases where makeAvailable() created a file under the wrong name. |
| 1125 | + compactionLog.scan(new EntryLogScanner() { |
| 1126 | + @Override |
| 1127 | + public boolean accept(long ledgerId) { |
| 1128 | + return true; |
| 1129 | + } |
| 1130 | + |
| 1131 | + @Override |
| 1132 | + public void process(long ledgerIdFromScanner, long offset, ByteBuf recoveredEntry) { |
| 1133 | + assertEquals(ledgerId, ledgerIdFromScanner); |
| 1134 | + assertEquals(ledgerId, recoveredEntry.getLong(recoveredEntry.readerIndex())); |
| 1135 | + assertEquals(entryId, recoveredEntry.getLong(recoveredEntry.readerIndex() + Long.BYTES)); |
| 1136 | + foundEntry.set(true); |
| 1137 | + } |
| 1138 | + }); |
| 1139 | + assertTrue(foundEntry.get(), "Compacted log should be readable through the compaction log id"); |
| 1140 | + } |
| 1141 | + |
| 1142 | + private void resetEntryLoggerWithLastLogId(long lastLogId) throws Exception { |
| 1143 | + entryLogger.getEntryLoggerAllocator().setLastLogId(curDir, lastLogId); |
| 1144 | + entryLogger.close(); |
| 1145 | + entryLogger = new DefaultEntryLogger(conf, dirsMgr); |
| 1146 | + } |
| 1147 | + |
986 | 1148 |
|
987 | 1149 | /* |
988 | 1150 | * tests basic logic of EntryLogManager interface for |
|
0 commit comments