Conversation
… contains a null slot
The size loop in OpReadEntry.internalReadEntriesComplete dereferences every
slot of the delivered batch. A null slot — a mixed range-cache read leaves
its slot null when it drops an out-of-range entry — currently surfaces as a
bare NullPointerException ("Cannot invoke Entry.getLength() ... because
List.get(int) is null") through the exception fallback, which also leaks the
whole batch's buffers: nothing on the fallback path releases the delivered
entries.
Detect the null slot up front, release the batch, and fail the read with a
proper ManagedLedgerException instead. The regression test delivers a batch
containing a null slot and asserts the explicit failure plus a zero refcount
on the valid sibling; without the guard it reproduces the NPE and the leak.
void-ptr974
left a comment
There was a problem hiding this comment.
Thanks for adding this safeguard. There is one execution path where this check is reached too late.
With the in-flight read limiter enabled, RangeEntryCacheImpl.doAsyncReadEntriesWithAcquiredPermits wraps the callback. Its readEntriesComplete method iterates every entry and calls ((EntryImpl) entry).onDeallocate(...) before invoking the original OpReadEntry callback.
Therefore, if the assembled result is [validEntry, null], the wrapper throws an NPE first and OpReadEntry.internalReadEntriesComplete is never called. The valid entries and the acquired limiter permit can remain retained, and the read callback is not completed by this path.
The existing regression test covers the direct OpReadEntry behavior. An additional case going through RangeEntryCacheImpl with the in-flight limiter enabled would help cover this callback path as well.
Good point, I'll check it |
…l slot With the in-flight reads limiter enabled, doAsyncReadEntriesWithAcquiredPermits wraps the read callback and iterates the delivered batch before the original callback runs, registering the permit-release hook on every slot. A batch containing a null slot (a mixed range-cache read leaves its slot null when the storage leg returns a short result) therefore threw a bare NPE inside the wrapper: the read callback was never completed, the valid entries were never released and the acquired limiter permit leaked. Treat a null slot as a share of the permits with no entry that will ever return it: consume its share immediately and still deliver the batch to the original callback, whose null-slot guard in OpReadEntry fails the read explicitly and releases the valid entries (whose hooks then return the remaining permits). Add a regression test driving a mixed read through RangeEntryCacheImpl with the limiter enabled: the batch reaches the callback with its null slot and every permit is returned once the delivered entries are handled.
|
Good catch — verified: with the limiter enabled the wrapper's Pushed a2f3a9d. The wrapper now treats a null slot as a share of the permits with no entry that will ever return it: The new regression test drives a mixed read through |
|
Thanks for adding the limiter coverage. It may also be helpful to cover the timeout path: when read timeout is enabled and the timeout completes before a sparse result arrives, The new limiter test exercises |
Motivation
The size loop in
OpReadEntry.internalReadEntriesCompletedereferences every slot of the delivered batch:A batch containing a null slot — a mixed range-cache read leaves its slot null when it drops an entry positioned outside the requested range — currently surfaces as a bare
NullPointerException:which is then funneled through the exception fallback (
Fallback to readEntriesFailed for exception in readEntriesComplete). Two problems with that shape:Modifications
internalReadEntriesComplete, release the batch (null-safe), and fail the read with a properManagedLedgerExceptionnaming the slot index — the existing fallback forwards it as a normal read failure.Verifying this change
./gradlew :managed-ledger:test --tests "org.apache.bookkeeper.mledger.impl.OpReadEntryNullSlotTest"— passes with the guard; removing the guard turns the test red with the original NPE.