Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.4k
HBASE-22422 Retain an ByteBuff with refCnt=0 when getBlock from LRUCache#242
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -313,10 +313,13 @@ public BlockWithScanInfo loadDataBlockWithScanInfo(Cell key, HFileBlock currentB | ||
| int index = -1; | ||
| HFileBlock block = null; | ||
| boolean dataBlock = false; | ||
| KeyOnlyKeyValue tmpNextIndexKV = new KeyValue.KeyOnlyKeyValue(); | ||
| while (true) { | ||
| try { | ||
| // Must initialize it with null here, because if don't and once an exception happen in | ||
| // readBlock, then we'll release the previous assigned block twice in the finally block. | ||
| // (See HBASE-22422) | ||
| block = null; | ||
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. Good catch. | ||
| if (currentBlock != null && currentBlock.getOffset() == currentOffset) { | ||
| // Avoid reading the same block again, even with caching turned off. | ||
| // This is crucial for compaction-type workload which might have | ||
| @@ -336,9 +339,8 @@ public BlockWithScanInfo loadDataBlockWithScanInfo(Cell key, HFileBlock currentB | ||
| // this also accounts for ENCODED_DATA | ||
| expectedBlockType = BlockType.DATA; | ||
| } | ||
| block = | ||
| cachingBlockReader.readBlock(currentOffset, currentOnDiskSize, shouldCache, pread, | ||
| isCompaction, true, expectedBlockType, expectedDataBlockEncoding); | ||
| block = cachingBlockReader.readBlock(currentOffset, currentOnDiskSize, shouldCache, | ||
| pread, isCompaction, true, expectedBlockType, expectedDataBlockEncoding); | ||
| } | ||
| if (block == null) { | ||
| @@ -348,7 +350,6 @@ public BlockWithScanInfo loadDataBlockWithScanInfo(Cell key, HFileBlock currentB | ||
| // Found a data block, break the loop and check our level in the tree. | ||
| if (block.getBlockType().isData()) { | ||
| dataBlock = true; | ||
| break; | ||
| } | ||
| @@ -381,15 +382,15 @@ public BlockWithScanInfo loadDataBlockWithScanInfo(Cell key, HFileBlock currentB | ||
| nextIndexedKey = tmpNextIndexKV; | ||
| } | ||
| } finally { | ||
| if (!dataBlock && block != null) { | ||
| if (block != null && !block.getBlockType().isData()) { | ||
| // Release the block immediately if it is not the data block | ||
| block.release(); | ||
| } | ||
| } | ||
| } | ||
| if (lookupLevel != searchTreeLevel) { | ||
| assert dataBlock == true; | ||
| assert block.getBlockType().isData(); | ||
| // Though we have retrieved a data block we have found an issue | ||
| // in the retrieved data block. Hence returned the block so that | ||
| // the ref count can be decremented | ||
| @@ -401,8 +402,7 @@ public BlockWithScanInfo loadDataBlockWithScanInfo(Cell key, HFileBlock currentB | ||
| } | ||
| // set the next indexed key for the current block. | ||
| BlockWithScanInfo blockWithScanInfo = new BlockWithScanInfo(block, nextIndexedKey); | ||
| return blockWithScanInfo; | ||
| return new BlockWithScanInfo(block, nextIndexedKey); | ||
| } | ||
| @Override | ||
| @@ -576,8 +576,7 @@ public HFileBlock seekToDataBlock(final Cell key, HFileBlock currentBlock, boole | ||
| boolean pread, boolean isCompaction, DataBlockEncoding expectedDataBlockEncoding) | ||
| throws IOException { | ||
| BlockWithScanInfo blockWithScanInfo = loadDataBlockWithScanInfo(key, currentBlock, | ||
| cacheBlocks, | ||
| pread, isCompaction, expectedDataBlockEncoding); | ||
| cacheBlocks, pread, isCompaction, expectedDataBlockEncoding); | ||
| if (blockWithScanInfo == null) { | ||
| return null; | ||
| } else { | ||
| @@ -600,9 +599,8 @@ public HFileBlock seekToDataBlock(final Cell key, HFileBlock currentBlock, boole | ||
| * @throws IOException | ||
| */ | ||
| public abstract BlockWithScanInfo loadDataBlockWithScanInfo(Cell key, HFileBlock currentBlock, | ||
| boolean cacheBlocks, | ||
| boolean pread, boolean isCompaction, DataBlockEncoding expectedDataBlockEncoding) | ||
| throws IOException; | ||
| boolean cacheBlocks, boolean pread, boolean isCompaction, | ||
| DataBlockEncoding expectedDataBlockEncoding) throws IOException; | ||
| /** | ||
| * An approximation to the {@link HFile}'s mid-key. Operates on block | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1128,15 +1128,13 @@ protected void readAndUpdateNewBlock(long firstDataBlockOffset) throws IOExcepti | ||
| updateCurrentBlock(newBlock); | ||
| } | ||
| protected int loadBlockAndSeekToKey(HFileBlock seekToBlock, Cell nextIndexedKey, | ||
| boolean rewind, Cell key, boolean seekBefore) throws IOException { | ||
| if (this.curBlock == null | ||
| || this.curBlock.getOffset() != seekToBlock.getOffset()) { | ||
| protected int loadBlockAndSeekToKey(HFileBlock seekToBlock, Cell nextIndexedKey, boolean rewind, | ||
| Cell key, boolean seekBefore) throws IOException { | ||
| if (this.curBlock == null || this.curBlock.getOffset() != seekToBlock.getOffset()) { | ||
| updateCurrentBlock(seekToBlock); | ||
| } else if (rewind) { | ||
| blockBuffer.rewind(); | ||
| } | ||
| // Update the nextIndexedKey | ||
| this.nextIndexedKey = nextIndexedKey; | ||
| return blockSeek(key, seekBefore); | ||
| @@ -1473,9 +1471,11 @@ public HFileBlock readBlock(long dataBlockOffset, long onDiskBlockSize, | ||
| // Validate encoding type for data blocks. We include encoding | ||
| // type in the cache key, and we expect it to match on a cache hit. | ||
| if (cachedBlock.getDataBlockEncoding() != dataBlockEncoder.getDataBlockEncoding()) { | ||
| // Remember to release the block when in exceptional path. | ||
| cachedBlock.release(); | ||
| throw new IOException("Cached block under key " + cacheKey + " " | ||
| + "has wrong encoding: " + cachedBlock.getDataBlockEncoding() + " (expected: " | ||
| + dataBlockEncoder.getDataBlockEncoding() + ")"); | ||
| + "has wrong encoding: " + cachedBlock.getDataBlockEncoding() + " (expected: " | ||
| + dataBlockEncoder.getDataBlockEncoding() + ")"); | ||
| } | ||
| } | ||
| // Cache-hit. Return! | ||
| @@ -1499,15 +1499,14 @@ public HFileBlock readBlock(long dataBlockOffset, long onDiskBlockSize, | ||
| BlockType.BlockCategory category = hfileBlock.getBlockType().getCategory(); | ||
| // Cache the block if necessary | ||
| AtomicBoolean cachedRaw = new AtomicBoolean(false); | ||
| cacheConf.getBlockCache().ifPresent(cache -> { | ||
| if (cacheBlock && cacheConf.shouldCacheBlockOnRead(category)) { | ||
| cachedRaw.set(cacheConf.shouldCacheCompressed(category)); | ||
| cache.cacheBlock(cacheKey, cachedRaw.get() ? hfileBlock : unpacked, | ||
| cache.cacheBlock(cacheKey, | ||
| cacheConf.shouldCacheCompressed(category) ? hfileBlock : unpacked, | ||
| cacheConf.isInMemory()); | ||
| } | ||
| }); | ||
| if (unpacked != hfileBlock && !cachedRaw.get()) { | ||
| if (unpacked != hfileBlock) { | ||
Apache9 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // End of life here if hfileBlock is an independent block. | ||
| hfileBlock.release(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -504,7 +504,14 @@ private long updateSizeMetrics(LruCachedBlock cb, boolean evict) { | ||
| @Override | ||
| public Cacheable getBlock(BlockCacheKey cacheKey, boolean caching, boolean repeat, | ||
| boolean updateCacheMetrics) { | ||
| LruCachedBlock cb = map.get(cacheKey); | ||
| LruCachedBlock cb = map.computeIfPresent(cacheKey, (key, val) -> { | ||
| // It will be referenced by RPC path, so increase here. NOTICE: Must do the retain inside | ||
| // this block. because if retain outside the map#computeIfPresent, the evictBlock may remove | ||
| // the block and release, then we're retaining a block with refCnt=0 which is disallowed. | ||
| // see HBASE-22422. | ||
| val.getBuffer().retain(); | ||
| return val; | ||
| }); | ||
| if (cb == null) { | ||
| if (!repeat && updateCacheMetrics) { | ||
| stats.miss(caching, cacheKey.isPrimary(), cacheKey.getBlockType()); | ||
| @@ -532,10 +539,10 @@ public Cacheable getBlock(BlockCacheKey cacheKey, boolean caching, boolean repea | ||
| } | ||
| return null; | ||
| } | ||
| if (updateCacheMetrics) stats.hit(caching, cacheKey.isPrimary(), cacheKey.getBlockType()); | ||
| if (updateCacheMetrics) { | ||
| stats.hit(caching, cacheKey.isPrimary(), cacheKey.getBlockType()); | ||
| } | ||
| cb.access(count.incrementAndGet()); | ||
| // It will be referenced by RPC path, so increase here. | ||
| cb.getBuffer().retain(); | ||
| return cb.getBuffer(); | ||
| } | ||
| @@ -592,16 +599,14 @@ protected long evictBlock(LruCachedBlock block, boolean evictedByEvictionProcess | ||
| if (previous == null) { | ||
| return 0; | ||
| } | ||
| // Decrease the block's reference count, and if refCount is 0, then it'll auto-deallocate. | ||
| previous.getBuffer().release(); | ||
| updateSizeMetrics(block, true); | ||
| long val = elements.decrementAndGet(); | ||
| if (LOG.isTraceEnabled()) { | ||
| long size = map.size(); | ||
| assertCounterSanity(size, val); | ||
| } | ||
| if (block.getBuffer().getBlockType().isData()) { | ||
| dataBlockElements.decrement(); | ||
| dataBlockElements.decrement(); | ||
| } | ||
| if (evictedByEvictionProcess) { | ||
| // When the eviction of the block happened because of invalidation of HFiles, no need to | ||
| @@ -611,6 +616,10 @@ protected long evictBlock(LruCachedBlock block, boolean evictedByEvictionProcess | ||
| victimHandler.cacheBlock(block.getCacheKey(), block.getBuffer()); | ||
| } | ||
| } | ||
| // Decrease the block's reference count, and if refCount is 0, then it'll auto-deallocate. DO | ||
| // NOT move this up because if do that then the victimHandler may access the buffer with | ||
| // refCnt = 0 which is disallowed. | ||
| previous.getBuffer().release(); | ||
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. So this is the problem. Mind explaining more? Why in victimHandler we will access the previous? And is it possible to add a UT?
| ||
| return block.heapSize(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -42,6 +42,7 @@ | ||
| import java.util.concurrent.Executors; | ||
| import java.util.concurrent.ScheduledExecutorService; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
| import java.util.concurrent.atomic.AtomicLong; | ||
| import java.util.concurrent.atomic.LongAdder; | ||
| import java.util.concurrent.locks.Lock; | ||
| @@ -1533,21 +1534,28 @@ public boolean containsKey(BlockCacheKey key) { | ||
| } | ||
| public RAMQueueEntry get(BlockCacheKey key) { | ||
| RAMQueueEntry re = delegate.get(key); | ||
| if (re != null) { | ||
| // It'll be referenced by RPC, so retain here. | ||
| return delegate.computeIfPresent(key, (k, re) -> { | ||
| // It'll be referenced by RPC, so retain atomically here. if the get and retain is not | ||
| // atomic, another thread may remove and release the block, when retaining in this thread we | ||
| // may retain a block with refCnt=0 which is disallowed. (see HBASE-22422) | ||
| re.getData().retain(); | ||
| } | ||
| return re; | ||
| return re; | ||
| }); | ||
| } | ||
| /** | ||
| * Return the previous associated value, or null if absent. It has the same meaning as | ||
| * {@link ConcurrentMap#putIfAbsent(Object, Object)} | ||
| */ | ||
| public RAMQueueEntry putIfAbsent(BlockCacheKey key, RAMQueueEntry entry) { | ||
| RAMQueueEntry previous = delegate.putIfAbsent(key, entry); | ||
| if (previous == null) { | ||
| AtomicBoolean absent = new AtomicBoolean(false); | ||
| RAMQueueEntry re = delegate.computeIfAbsent(key, k -> { | ||
| // The RAMCache reference to this entry, so reference count should be increment. | ||
| entry.getData().retain(); | ||
| } | ||
| return previous; | ||
| absent.set(true); | ||
| return entry; | ||
| }); | ||
| return absent.get() ? null : re; | ||
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. I think I got it now. You have changed the get() to computeIfPresent(). So if the remove has already removed the entry then the get() cannot do a retain(). So a similar change is also needed for putIfAbsent() also? Rest looks good to me. MemberAuthor 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. Yeah, if don't make the put and retain in atomic , then if remove & release happen between the put and retain, finally we 're retain a block with refCnt=0 which is also disallowed. Thanks. | ||
| } | ||
| public boolean remove(BlockCacheKey key) { | ||
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. While removing the atomicity is not needed? Because we do computeIfAbsent and that is already now atomically guarded? | ||
| @@ -1576,8 +1584,9 @@ public boolean isEmpty() { | ||
| public void clear() { | ||
| Iterator<Map.Entry<BlockCacheKey, RAMQueueEntry>> it = delegate.entrySet().iterator(); | ||
| while (it.hasNext()) { | ||
| it.next().getValue().getData().release(); | ||
| RAMQueueEntry re = it.next().getValue(); | ||
| it.remove(); | ||
| re.getData().release(); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
Oh, this means we may get NPE in the past?
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.
Yes, see the LruBlockCache#cacheBlock:
The existence pre-check and accessing block in shouldReplaceExistingCacheBlock is not atomic op. so if any eviction happen between them, the NPE will happen. I added a UT testMultiThreadGetAndEvictBlock to address this.