codelipenghui commented on code in PR #4901:
URL: https://github.com/apache/bookkeeper/pull/4901#discussion_r4260813570


##########
bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/SortedLedgerStorage.java:
##########
@@ -240,7 +240,39 @@ public ByteBuf getEntry(long ledgerId, long entryId) 
throws IOException, BookieE
 
     @Override
     public long getLastAddConfirmed(long ledgerId) throws IOException {
-        return interleavedLedgerStorage.getLastAddConfirmed(ledgerId);
+        // The index can still expose the committed LAC while a newer entry is 
in the memtable.
+        Long memTableLac = lastAddConfirmedFromMemTable(ledgerId);
+        try {
+            long persistedLac = 
interleavedLedgerStorage.getLastAddConfirmed(ledgerId);
+            if (memTableLac == null) {
+                return persistedLac;
+            }
+            return Math.max(persistedLac, memTableLac);

Review Comment:
   The returned value is now correct while the newer entries are still in the 
memtable. But `interleavedLedgerStorage.getLastAddConfirmed()` still caches the 
index-derived LAC in the reloaded `FileInfo` 
(`InterleavedLedgerStorage#getLastAddConfirmed` calls 
`ledgerCache.updateLastAddConfirmed`). Nothing writes the memtable value back.
   
   Repro, using a real `SortedLedgerStorage` with `CheckpointSource.DEFAULT` so 
`flush()` really flushes:
   1. Add entries 0–4 (LAC = entryId − 1) and flush. Add entries 5–9. Evict 
ledger 0's FileInfo.
   2. `getLastAddConfirmed(0)` returns 8, which is correct. But the ledger 
cache now holds 3.
   3. Call `flush()` while the FileInfo is still cached. 
`getLastAddConfirmed(0)` now returns **3**.
   
   So once the rest of the ledger is flushed, the LAC still goes backwards (8 → 
3), which is the case @djsweet described. It stays at 3 until a newer LAC 
arrives or the FileInfo is evicted again.
   
   Writing the winning value back fixes it. `FileInfo#setLastAddConfirmed` only 
moves forward and also notifies long-poll watchers:
   
   ```java
   if (memTableLac == null || memTableLac <= persistedLac) {
       return persistedLac;
   }
   return interleavedLedgerStorage.ledgerCache.updateLastAddConfirmed(ledgerId, 
memTableLac);
   ```
   
   The catch branch needs the same write-back for `latestMemTableLac`, ignoring 
`NoLedgerException`. With that, the repro returns 8 both before and after the 
flush. Alternatively, check the cached LAC first (see the next comment).



##########
bookkeeper-server/src/main/java/org/apache/bookkeeper/bookie/SortedLedgerStorage.java:
##########
@@ -240,7 +240,39 @@ public ByteBuf getEntry(long ledgerId, long entryId) 
throws IOException, BookieE
 
     @Override
     public long getLastAddConfirmed(long ledgerId) throws IOException {
-        return interleavedLedgerStorage.getLastAddConfirmed(ledgerId);
+        // The index can still expose the committed LAC while a newer entry is 
in the memtable.
+        Long memTableLac = lastAddConfirmedFromMemTable(ledgerId);

Review Comment:
   This puts a memtable lookup in front of every call: a read lock, up to two 
skip-list `floorKey` calls, and a get-entry stats event in 
`EntryMemTable#getLastEntry`. That includes the common case where the FileInfo 
already has the LAC cached, and `ReadEntryProcessorV3` calls this on every V3 
entry read.
   
   I ran a quick local loop: 1000 ledgers × 200 unflushed entries, LAC cached.
   - Single thread: about 38 ns per call on master, about 160 ns with this 
change.
   - 8 threads: about 8.5 M calls/s on master, about 3.9 M with this change.
   
   Every LAC read also counts as a memtable get in the skip-list stats.
   
   Checking the cache first keeps the common path the same as master and only 
touches the memtable after an eviction. This is essentially the order #4902 
uses:
   
   ```java
   LedgerCache ledgerCache = interleavedLedgerStorage.ledgerCache;
   Long lac = null;
   try {
       lac = ledgerCache.getLastAddConfirmed(ledgerId);
   } catch (Bookie.NoLedgerException nle) {
       // no FileInfo/index; the memtable may still have the ledger
   }
   if (lac != null) {
       return lac;
   }
   Long memLac = lastAddConfirmedFromMemTable(ledgerId);
   if (memLac != null) {
       try {
           return ledgerCache.updateLastAddConfirmed(ledgerId, memLac);
       } catch (Bookie.NoLedgerException nle) {
           return memLac;
       }
   }
   // nothing unflushed (or it was just flushed): the index is complete
   return interleavedLedgerStorage.getLastAddConfirmed(ledgerId);
   ```
   
   This is safe against a concurrent flush. `flushSnapshot` writes every 
snapshot entry to the index before `clearSnapshot`, and `getLastEntry` still 
sees the snapshot until then. So if the memtable has nothing, the index already 
has everything, and the retry is no longer needed.
   
   Locally this passes the eviction test, the partially flushed repro above and 
an end-to-end V3 read, at the same per-call cost as master.



##########
bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SortedLedgerStorageTest.java:
##########
@@ -191,4 +196,101 @@ public void testGetListOfEntriesOfLedgerAfterFlush() 
throws IOException {
             }));
         }
     }
+
+    /**
+     * An unflushed ledger's LAC lives in a FileInfo cache entry. Once that 
entry is
+     * evicted, the index file still does not exist, so the LAC has to come 
from the memtable.
+     */
+    @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()));
+        SortedLedgerStorage storage = new SortedLedgerStorage();
+        storage.initialize(limited, null, dirs, dirs,
+                statsProvider.getStatsLogger(BOOKIE_SCOPE), 
UnpooledByteBufAllocator.DEFAULT);
+        storage.setCheckpointSource(checkpointSource);
+        storage.setCheckpointer(checkpointer);
+        try {
+            long lac = 3L;
+            int ledgers = 12;
+            for (long ledgerId = 0; ledgerId < ledgers; ledgerId++) {
+                storage.setMasterKey(ledgerId, ("ledger-" + 
ledgerId).getBytes());
+                ByteBuf entry = Unpooled.buffer(128);
+                entry.writeLong(ledgerId);
+                entry.writeLong(0L);
+                entry.writeLong(lac);
+                entry.writeByte(1);
+                storage.addEntry(entry);
+            }
+            assertEquals("LAC of an unflushed ledger after file-info eviction",
+                    lac, storage.getLastAddConfirmed(0L));
+            storage.flush();

Review Comment:
   This `flush()` doesn't flush the memtable. In this class, 
`checkpointSource.newCheckpoint()` returns `Checkpoint.MAX`. 
`EntryMemTable#snapshot(Checkpoint.MAX)` only takes a snapshot when 
`kvmap.cp.compareTo(MAX) < 0`, which is never true for `MAX`.
   
   I checked that entry 0 is still in `storage.memTable` afterwards. So the 
next assertion runs the memtable path again, not the post-flush index path.
   
   Using `CheckpointSource.DEFAULT` (which returns `Checkpoint.MIN`) for this 
storage, and asserting `storage.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.



##########
bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SortedLedgerStorageTest.java:
##########
@@ -191,4 +196,101 @@ public void testGetListOfEntriesOfLedgerAfterFlush() 
throws IOException {
             }));
         }
     }
+
+    /**
+     * An unflushed ledger's LAC lives in a FileInfo cache entry. Once that 
entry is
+     * evicted, the index file still does not exist, so the LAC has to come 
from the memtable.
+     */
+    @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()));
+        SortedLedgerStorage storage = new SortedLedgerStorage();
+        storage.initialize(limited, null, dirs, dirs,
+                statsProvider.getStatsLogger(BOOKIE_SCOPE), 
UnpooledByteBufAllocator.DEFAULT);
+        storage.setCheckpointSource(checkpointSource);
+        storage.setCheckpointer(checkpointer);
+        try {
+            long lac = 3L;
+            int ledgers = 12;
+            for (long ledgerId = 0; ledgerId < ledgers; ledgerId++) {
+                storage.setMasterKey(ledgerId, ("ledger-" + 
ledgerId).getBytes());
+                ByteBuf entry = Unpooled.buffer(128);
+                entry.writeLong(ledgerId);
+                entry.writeLong(0L);
+                entry.writeLong(lac);
+                entry.writeByte(1);
+                storage.addEntry(entry);
+            }
+            assertEquals("LAC of an unflushed ledger after file-info eviction",
+                    lac, storage.getLastAddConfirmed(0L));
+            storage.flush();
+            assertEquals("LAC still readable after the memtable is flushed",
+                    lac, storage.getLastAddConfirmed(0L));
+            try {
+                storage.getLastAddConfirmed(9_999L);
+                assertTrue("missing ledger should not report a LAC", false);
+            } catch (Bookie.NoLedgerException | Bookie.NoEntryException 
expected) {
+                // index has no file and the memtable has no entry
+            }

Review Comment:
   Nit: `assertThrows(IOException.class, () -> 
storage.getLastAddConfirmed(9_999L))` reads better than `assertTrue(msg, 
false)`.
   
   Also, entry 0 can't carry LAC 3 (`lac = 3L` above), because a piggybacked 
LAC is always lower than the entry id. Writing a few entries with `LAC = 
entryId - 1` would keep the test realistic.



##########
bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SortedLedgerStorageTest.java:
##########
@@ -191,4 +196,101 @@ public void testGetListOfEntriesOfLedgerAfterFlush() 
throws IOException {
             }));
         }
     }
+
+    /**
+     * An unflushed ledger's LAC lives in a FileInfo cache entry. Once that 
entry is
+     * evicted, the index file still does not exist, so the LAC has to come 
from the memtable.
+     */
+    @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()));
+        SortedLedgerStorage storage = new SortedLedgerStorage();
+        storage.initialize(limited, null, dirs, dirs,
+                statsProvider.getStatsLogger(BOOKIE_SCOPE), 
UnpooledByteBufAllocator.DEFAULT);
+        storage.setCheckpointSource(checkpointSource);
+        storage.setCheckpointer(checkpointer);
+        try {
+            long lac = 3L;
+            int ledgers = 12;
+            for (long ledgerId = 0; ledgerId < ledgers; ledgerId++) {
+                storage.setMasterKey(ledgerId, ("ledger-" + 
ledgerId).getBytes());
+                ByteBuf entry = Unpooled.buffer(128);
+                entry.writeLong(ledgerId);
+                entry.writeLong(0L);
+                entry.writeLong(lac);
+                entry.writeByte(1);
+                storage.addEntry(entry);
+            }
+            assertEquals("LAC of an unflushed ledger after file-info eviction",
+                    lac, storage.getLastAddConfirmed(0L));
+            storage.flush();
+            assertEquals("LAC still readable after the memtable is flushed",
+                    lac, storage.getLastAddConfirmed(0L));
+            try {
+                storage.getLastAddConfirmed(9_999L);
+                assertTrue("missing ledger should not report a LAC", false);
+            } catch (Bookie.NoLedgerException | Bookie.NoEntryException 
expected) {
+                // index has no file and the memtable has no entry
+            }
+        } finally {
+            storage.shutdown();
+        }
+    }
+
+    /**
+     * A committed LAC can be stale while a newer entry is still in the 
memtable.
+     */
+    @Test
+    public void testGetLastAddConfirmedPrefersMemTableForExistingLedger() 
throws Exception {
+        InterleavedLedgerStorage underlying = 
mock(InterleavedLedgerStorage.class);
+        when(underlying.getLastAddConfirmed(0L)).thenReturn(0L);

Review Comment:
   Mocking `InterleavedLedgerStorage` hides the side effect behind the 
remaining bug. The real `getLastAddConfirmed()` also stores the index-derived 
LAC in the FileInfo cache, and that is what later comes back as the stale 
value. So this test passes while a real storage still goes 8 → 3 (see the 
comment on `SortedLedgerStorage`). Both mock tests would also need rework with 
either fix, because a mock's `ledgerCache` field is `null`.
   
   Could this be a real-storage test instead? Roughly:
   1. Use `CheckpointSource.DEFAULT` so `flush()` really flushes.
   2. Add entries 0–4 and flush, then add 5–9.
   3. Evict ledger 0's FileInfo, either with `openFileLimit` pressure as in the 
test above or by invalidating both `IndexPersistenceMgr` caches from the test 
package.
   4. Assert 8, call `flush()`, and assert 8 again.
   
   The last assertion fails on the current head.



##########
bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SortedLedgerStorageTest.java:
##########
@@ -191,4 +196,101 @@ public void testGetListOfEntriesOfLedgerAfterFlush() 
throws IOException {
             }));
         }
     }
+
+    /**
+     * An unflushed ledger's LAC lives in a FileInfo cache entry. Once that 
entry is
+     * evicted, the index file still does not exist, so the LAC has to come 
from the memtable.

Review Comment:
   Nit: the index file does exist after eviction. 
`FileInfoBackingCache#releaseFileInfo` calls `FileInfo.close(true)`, which 
creates a header-only `.idx` (1024 bytes). What's missing are the index 
entries. That's why the lookup throws `NoEntryException`, and why master 
returns `ENOENTRY` here rather than `ENOLEDGER`.



-- 
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