Repository navigation
Issue 4900: Read an unflushed ledger LAC from the memtable #4901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
0af2c62
11eba81
4eba457
1d1543b
6768dd3
b0a6cad
da60ca0
3c64475
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,13 +23,20 @@ | |
| import static org.apache.bookkeeper.bookie.BookKeeperServerStats.BOOKIE_SCOPE; | ||
| import static org.junit.Assert.assertEquals; | ||
| import static org.junit.Assert.assertFalse; | ||
| import static org.junit.Assert.assertThrows; | ||
| import static org.junit.Assert.assertTrue; | ||
| import static org.mockito.Mockito.mock; | ||
| import static org.mockito.Mockito.spy; | ||
| import static org.mockito.Mockito.times; | ||
| import static org.mockito.Mockito.verify; | ||
| import static org.mockito.Mockito.when; | ||
|
|
||
| import io.netty.buffer.ByteBuf; | ||
| import io.netty.buffer.Unpooled; | ||
| import io.netty.buffer.UnpooledByteBufAllocator; | ||
| import java.io.File; | ||
| import java.io.IOException; | ||
| import java.nio.ByteBuffer; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.PrimitiveIterator.OfLong; | ||
|
|
@@ -41,7 +48,9 @@ | |
| import org.apache.bookkeeper.test.TestStatsProvider; | ||
| import org.apache.bookkeeper.util.DiskChecker; | ||
| import org.junit.Before; | ||
| import org.junit.Rule; | ||
| import org.junit.Test; | ||
| import org.junit.rules.TemporaryFolder; | ||
| import org.junit.runner.RunWith; | ||
| import org.junit.runners.Parameterized; | ||
|
|
||
|
|
@@ -56,6 +65,9 @@ public class SortedLedgerStorageTest { | |
| LedgerDirsManager ledgerDirsManager; | ||
| SortedLedgerStorage sortedLedgerStorage = new SortedLedgerStorage(); | ||
|
|
||
| @Rule | ||
| public TemporaryFolder temporaryFolder = new TemporaryFolder(); | ||
|
|
||
| final long numWrites = 2000; | ||
| final long moreNumOfWrites = 3000; | ||
| final long entriesPerWrite = 2; | ||
|
|
@@ -191,4 +203,231 @@ public void testGetListOfEntriesOfLedgerAfterFlush() throws IOException { | |
| })); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A newer memtable LAC must remain visible after an evicted FileInfo is flushed. | ||
| */ | ||
| @Test | ||
| public void testGetLastAddConfirmedAfterFileInfoEviction() throws Exception { | ||
| File tmpDir = File.createTempFile("bkLac", ".dir"); | ||
| tmpDir.delete(); | ||
| tmpDir.mkdir(); | ||
| BookieImpl.checkDirectoryStructure(BookieImpl.getCurrentDirectory(tmpDir)); | ||
|
|
||
| ServerConfiguration limited = TestBKConfiguration.newServerConfiguration(); | ||
| limited.setEntryLogPerLedgerEnabled(conf.isEntryLogPerLedgerEnabled()); | ||
| limited.setLedgerDirNames(new String[] { tmpDir.toString() }); | ||
| limited.setOpenFileLimit(2); | ||
| limited.setNumAddWorkerThreads(1); | ||
| limited.setNumReadWorkerThreads(1); | ||
| limited.setSkipListSizeLimit(64 * 1024 * 1024); | ||
|
|
||
| LedgerDirsManager dirs = new LedgerDirsManager(limited, limited.getLedgerDirs(), | ||
| new DiskChecker(limited.getDiskUsageThreshold(), limited.getDiskUsageWarnThreshold())); | ||
| InterleavedLedgerStorage underlying = new InterleavedLedgerStorage(); | ||
| SortedLedgerStorage storage = new SortedLedgerStorage(underlying); | ||
| storage.initialize(limited, null, dirs, dirs, | ||
| statsProvider.getStatsLogger(BOOKIE_SCOPE), UnpooledByteBufAllocator.DEFAULT); | ||
| storage.setCheckpointSource(CheckpointSource.DEFAULT); | ||
| storage.setCheckpointer(checkpointer); | ||
| try { | ||
| long ledgerId = 0L; | ||
| storage.setMasterKey(ledgerId, ("ledger-" + ledgerId).getBytes()); | ||
| for (long entryId = 0; entryId < 5; entryId++) { | ||
| ByteBuf entry = Unpooled.buffer(128); | ||
| try { | ||
| entry.writeLong(ledgerId); | ||
| entry.writeLong(entryId); | ||
| entry.writeLong(entryId - 1); | ||
| entry.writeByte(1); | ||
| storage.addEntry(entry); | ||
| } finally { | ||
| entry.release(); | ||
| } | ||
| } | ||
| storage.flush(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since entries 0–4 are flushed here, the index lookup in I checked locally with two changes to
Removing the write-back in the try branch does fail this test, so it's a good guard for the partially flushed case. Could you keep this test and add a fully unflushed case next to it? With the same setup:
In my local checks, assertions like these fail under both changes above. |
||
|
|
||
| for (long entryId = 5; entryId < 10; entryId++) { | ||
| ByteBuf entry = Unpooled.buffer(128); | ||
| try { | ||
| entry.writeLong(ledgerId); | ||
| entry.writeLong(entryId); | ||
| entry.writeLong(entryId - 1); | ||
| entry.writeByte(1); | ||
| storage.addEntry(entry); | ||
| } finally { | ||
| entry.release(); | ||
| } | ||
| } | ||
| LedgerCacheImpl ledgerCache = (LedgerCacheImpl) underlying.ledgerCache; | ||
| IndexPersistenceMgr indexPersistenceManager = ledgerCache.getIndexPersistenceManager(); | ||
| indexPersistenceManager.readFileInfoCache.invalidate(ledgerId); | ||
| indexPersistenceManager.writeFileInfoCache.invalidate(ledgerId); | ||
| indexPersistenceManager.readFileInfoCache.cleanUp(); | ||
| indexPersistenceManager.writeFileInfoCache.cleanUp(); | ||
|
|
||
| assertEquals("LAC of an evicted ledger must include newer memtable entries", | ||
| 8L, storage.getLastAddConfirmed(ledgerId)); | ||
| storage.flush(); | ||
| assertTrue("memtable must be empty after a real flush", storage.memTable.isEmpty()); | ||
| assertEquals("LAC must not move backwards after the memtable is flushed", | ||
| 8L, storage.getLastAddConfirmed(ledgerId)); | ||
| assertThrows(Bookie.NoLedgerException.class, () -> storage.getLastAddConfirmed(9_999L)); | ||
| } finally { | ||
| storage.shutdown(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * An unflushed LAC must be written back to the ledger cache after FileInfo eviction. | ||
| */ | ||
| @Test | ||
| public void testGetLastAddConfirmedAfterUnflushedFileInfoEviction() throws Exception { | ||
| File tmpDir = temporaryFolder.newFolder("bkLacUnflushed"); | ||
| BookieImpl.checkDirectoryStructure(BookieImpl.getCurrentDirectory(tmpDir)); | ||
|
|
||
| ServerConfiguration limited = TestBKConfiguration.newServerConfiguration(); | ||
| limited.setEntryLogPerLedgerEnabled(conf.isEntryLogPerLedgerEnabled()); | ||
| limited.setLedgerDirNames(new String[] { tmpDir.toString() }); | ||
| limited.setOpenFileLimit(2); | ||
| limited.setNumAddWorkerThreads(1); | ||
| limited.setNumReadWorkerThreads(1); | ||
| limited.setSkipListSizeLimit(64 * 1024 * 1024); | ||
|
|
||
| LedgerDirsManager dirs = new LedgerDirsManager(limited, limited.getLedgerDirs(), | ||
| new DiskChecker(limited.getDiskUsageThreshold(), limited.getDiskUsageWarnThreshold())); | ||
| InterleavedLedgerStorage underlying = new InterleavedLedgerStorage(); | ||
| SortedLedgerStorage storage = new SortedLedgerStorage(underlying); | ||
| boolean initialized = false; | ||
| try { | ||
| storage.initialize(limited, null, dirs, dirs, | ||
| statsProvider.getStatsLogger(BOOKIE_SCOPE), UnpooledByteBufAllocator.DEFAULT); | ||
| initialized = true; | ||
| storage.setCheckpointSource(CheckpointSource.DEFAULT); | ||
| storage.setCheckpointer(checkpointer); | ||
| long ledgerId = 0L; | ||
| storage.setMasterKey(ledgerId, ("ledger-" + ledgerId).getBytes()); | ||
| for (long entryId = 0; entryId < 3; entryId++) { | ||
| ByteBuf entry = Unpooled.buffer(128); | ||
| try { | ||
| entry.writeLong(ledgerId); | ||
| entry.writeLong(entryId); | ||
| entry.writeLong(entryId - 1); | ||
| entry.writeByte(1); | ||
| storage.addEntry(entry); | ||
| } finally { | ||
| entry.release(); | ||
| } | ||
| } | ||
| LedgerCacheImpl ledgerCache = (LedgerCacheImpl) underlying.ledgerCache; | ||
| IndexPersistenceMgr indexPersistenceManager = ledgerCache.getIndexPersistenceManager(); | ||
| indexPersistenceManager.readFileInfoCache.invalidate(ledgerId); | ||
| indexPersistenceManager.writeFileInfoCache.invalidate(ledgerId); | ||
| indexPersistenceManager.readFileInfoCache.cleanUp(); | ||
| indexPersistenceManager.writeFileInfoCache.cleanUp(); | ||
|
|
||
| assertEquals("LAC must include unflushed memtable entries", 1L, storage.getLastAddConfirmed(ledgerId)); | ||
| assertFalse("LAC write-back must make the update immediately visible to long polls", | ||
| storage.waitForLastAddConfirmedUpdate(ledgerId, 0L, notification -> { })); | ||
| storage.flush(); | ||
| assertTrue("memtable must be empty after a real flush", storage.memTable.isEmpty()); | ||
| assertEquals("LAC must remain visible after the memtable is flushed", | ||
| 1L, storage.getLastAddConfirmed(ledgerId)); | ||
| } finally { | ||
| if (initialized) { | ||
| storage.shutdown(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testGetLastAddConfirmedRetriesAfterIndexMiss() throws Exception { | ||
| InterleavedLedgerStorage underlying = mock(InterleavedLedgerStorage.class); | ||
| LedgerCache ledgerCache = mock(LedgerCache.class); | ||
| when(underlying.getLedgerCache()).thenReturn(ledgerCache); | ||
| when(ledgerCache.getLastAddConfirmed(0L)).thenReturn(null); | ||
| when(underlying.getLastAddConfirmed(0L)) | ||
| .thenThrow(new Bookie.NoEntryException(0L, -1L)) | ||
| .thenReturn(1L); | ||
| SortedLedgerStorage storage = new SortedLedgerStorage(underlying); | ||
| storage.initialize(conf, null, ledgerDirsManager, ledgerDirsManager, | ||
| statsProvider.getStatsLogger(BOOKIE_SCOPE), UnpooledByteBufAllocator.DEFAULT); | ||
| storage.setCheckpointSource(checkpointSource); | ||
| storage.setCheckpointer(checkpointer); | ||
| try { | ||
| assertEquals(1L, storage.getLastAddConfirmed(0L)); | ||
| verify(underlying, times(2)).getLastAddConfirmed(0L); | ||
| verify(underlying, times(0)).ledgerExists(0L); | ||
| } finally { | ||
| storage.shutdown(); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testGetLastAddConfirmedUsesCachedValueBeforeMemTable() throws Exception { | ||
| InterleavedLedgerStorage underlying = mock(InterleavedLedgerStorage.class); | ||
| LedgerCache ledgerCache = mock(LedgerCache.class); | ||
| when(underlying.getLedgerCache()).thenReturn(ledgerCache); | ||
| when(ledgerCache.getLastAddConfirmed(0L)).thenReturn(8L); | ||
| SortedLedgerStorage storage = new SortedLedgerStorage(underlying); | ||
| storage.initialize(conf, null, ledgerDirsManager, ledgerDirsManager, | ||
| statsProvider.getStatsLogger(BOOKIE_SCOPE), UnpooledByteBufAllocator.DEFAULT); | ||
| storage.setCheckpointSource(checkpointSource); | ||
| storage.setCheckpointer(checkpointer); | ||
| EntryMemTable memTable = spy(storage.memTable); | ||
| storage.memTable = memTable; | ||
| try { | ||
| assertEquals(8L, storage.getLastAddConfirmed(0L)); | ||
| verify(memTable, times(0)).getLastEntry(0L); | ||
| verify(underlying, times(0)).getLastAddConfirmed(0L); | ||
| verify(underlying, times(0)).ledgerExists(0L); | ||
| } finally { | ||
| storage.shutdown(); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testGetLastAddConfirmedFallsBackToMemTableWhenCacheMisses() throws Exception { | ||
| InterleavedLedgerStorage underlying = mock(InterleavedLedgerStorage.class); | ||
| LedgerCache ledgerCache = mock(LedgerCache.class); | ||
| when(underlying.getLedgerCache()).thenReturn(ledgerCache); | ||
| when(ledgerCache.getLastAddConfirmed(0L)).thenReturn(null); | ||
| when(ledgerCache.updateLastAddConfirmed(0L, 8L)).thenReturn(8L); | ||
| SortedLedgerStorage storage = new SortedLedgerStorage(underlying); | ||
| storage.initialize(conf, null, ledgerDirsManager, ledgerDirsManager, | ||
| statsProvider.getStatsLogger(BOOKIE_SCOPE), UnpooledByteBufAllocator.DEFAULT); | ||
| storage.setCheckpointSource(checkpointSource); | ||
| storage.setCheckpointer(checkpointer); | ||
| try { | ||
| ByteBuffer entry = ByteBuffer.allocate(3 * Long.BYTES); | ||
| entry.putLong(0L).putLong(9L).putLong(8L).flip(); | ||
| storage.memTable.addEntry(0L, 9L, entry, storage); | ||
|
|
||
| assertEquals(8L, storage.getLastAddConfirmed(0L)); | ||
| verify(ledgerCache).updateLastAddConfirmed(0L, 8L); | ||
| verify(underlying, times(0)).getLastAddConfirmed(0L); | ||
| verify(underlying, times(0)).ledgerExists(0L); | ||
| } finally { | ||
| storage.shutdown(); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testGetLastAddConfirmedDoesNotReadDeletedLedgerFromMemTable() throws Exception { | ||
| long ledgerId = 0L; | ||
| sortedLedgerStorage.setMasterKey(ledgerId, ("ledger-" + ledgerId).getBytes()); | ||
| ByteBuf entry = Unpooled.buffer(128); | ||
| entry.writeLong(ledgerId); | ||
| entry.writeLong(0L); | ||
| entry.writeLong(-1L); | ||
| entry.writeByte(1); | ||
| try { | ||
| sortedLedgerStorage.addEntry(entry); | ||
| sortedLedgerStorage.deleteLedger(ledgerId); | ||
| assertThrows(Bookie.NoLedgerException.class, | ||
| () -> sortedLedgerStorage.getLastAddConfirmed(ledgerId)); | ||
| } finally { | ||
| entry.release(); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This
flush()doesn't flush the memtable. In this class,checkpointSource.newCheckpoint()returnsCheckpoint.MAX.EntryMemTable#snapshot(Checkpoint.MAX)only takes a snapshot whenkvmap.cp.compareTo(MAX) < 0, which is never true forMAX.I checked that entry 0 is still in
storage.memTableafterwards. So the next assertion runs the memtable path again, not the post-flush index path.Using
CheckpointSource.DEFAULT(which returnsCheckpoint.MIN) for this storage, and assertingstorage.memTable.isEmpty()after the flush, would make it test what the message says. The current code does return the right LAC from the index after a real flush.