[fix][offload] Cache and reuse entry offsets discovered while skipping in BlobStoreBackedReadHandleImpl - #26424
Conversation
…g in BlobStoreBackedReadHandleImpl seekToEntryOffset() falls back to skipPreviousEntry() when it has neither a cached nor a persisted precise offset for the target entry. That walk learns every entry's exact offset along the way but never records it in entryOffsetsCache, and step 2's cache lookup only ever probes for expectedEntryId - 1, so a cached offset is only reachable when consecutive reads land on strictly adjacent entry ids. Real cursor traffic on fragmented acks jumps by the gap between un-acked entries instead, so a cursor whose backlog is fragmented into many individually-acked ranges pays a full rescan of the offload block on every jump across an acked gap. entryOffsetsCache.put() is now called in skipPreviousEntry() at the point the entry id is confirmed, mirroring the existing call in the main read loop. seekToEntryOffset()'s step 2 becomes a bounded backward probe: walk from expectedEntryId - 1 down to whichever is larger of the sparse index marker's entry id or expectedEntryId - MAX_OFFSET_PROBE (default 1024, pulsar.jclouds.readhandleimpl.offsetprobe.max), seeking and skipping from the first cache hit. A later jump into an already-walked block now resolves in "gap" probes instead of a full rescan, and each walk extends the cached run forward, so only the first jump into a given region pays the full walk cost. Cache growth remains bounded by OffsetsCache's existing maximumSize/expireAfterAccess eviction.
Not directly related, but there's a slow path case for catch-up reads. Each time BlobStoreBackedReadHandleImpl is opened, it will read and parse the index. The BlobStoreBackedReadHandleImpl instance will get closed by default after 600 seconds of inactivity ( |
I took a closer look at OffsetsCache (originally added in #16417, refactored in #22679) and started to doubt the usefulness of it. I wouldn't expect that TreeMap.floorEntry is inefficient. I haven't run a deeper analysis yet.
|
Well now I get it. The index contains the offset of the first entry in the uploaded block (blocks are written every 5MB by default ( |
seekToEntryOffset() falls back to skipPreviousEntry() when it has neither a cached nor a persisted precise offset for the target entry. That walk learns every entry's exact offset along the way but never records it in entryOffsetsCache, and step 2's cache lookup only ever probes for expectedEntryId - 1, so a cached offset is only reachable when consecutive reads land on strictly adjacent entry ids. Real cursor traffic on fragmented acks jumps by the gap between un-acked entries instead, so a cursor whose backlog is fragmented into many individually-acked ranges pays a full rescan of the offload block on every jump across an acked gap.
entryOffsetsCache.put() is now called in skipPreviousEntry() at the point the entry id is confirmed, mirroring the existing call in the main read loop. seekToEntryOffset()'s step 2 becomes a bounded backward probe: walk from expectedEntryId - 1 down to whichever is larger of the sparse index marker's entry id or expectedEntryId - MAX_OFFSET_PROBE (default 1024, pulsar.jclouds.readhandleimpl.offsetprobe.max), seeking and skipping from the first cache hit. A later jump into an already-walked block now resolves in "gap" probes instead of a full rescan, and each walk extends the cached run forward, so only the first jump into a given region pays the full walk cost. Cache growth remains bounded by OffsetsCache's existing maximumSize/expireAfterAccess eviction.