VladRodionov commented on code in PR #8653: URL: https://github.com/apache/hbase/pull/8653#discussion_r4067671727
########## hbase-server/src/main/java/org/apache/hadoop/hbase/io/hfile/cache/LruCacheEngine.java: ########## @@ -0,0 +1,1549 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.io.hfile.cache; + +import java.lang.ref.WeakReference; +import java.util.EnumMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.PriorityQueue; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; +import java.util.concurrent.locks.ReentrantLock; +import org.apache.commons.lang3.mutable.MutableBoolean; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.HBaseInterfaceAudience; +import org.apache.hadoop.hbase.io.HeapSize; +import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding; +import org.apache.hadoop.hbase.io.hfile.BlockCacheKey; +import org.apache.hadoop.hbase.io.hfile.BlockCacheUtil; +import org.apache.hadoop.hbase.io.hfile.BlockPriority; +import org.apache.hadoop.hbase.io.hfile.BlockType; +import org.apache.hadoop.hbase.io.hfile.CacheStats; +import org.apache.hadoop.hbase.io.hfile.Cacheable; +import org.apache.hadoop.hbase.io.hfile.CachedBlock; +import org.apache.hadoop.hbase.io.hfile.HFileBlock; +import org.apache.hadoop.hbase.io.hfile.LruCachedBlock; +import org.apache.hadoop.hbase.io.hfile.LruCachedBlockQueue; +import org.apache.hadoop.hbase.util.ClassSize; +import org.apache.hadoop.util.StringUtils; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hbase.thirdparty.com.google.common.base.MoreObjects; +import org.apache.hbase.thirdparty.com.google.common.base.Objects; +import org.apache.hbase.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder; + +/** + * Native LRU {@link CacheEngine} implementation. + * <p> + * This cache is memory-aware using {@link HeapSize}, memory-bound using an LRU eviction algorithm, + * and concurrent. It is backed by a {@link ConcurrentHashMap} and can use a non-blocking eviction + * thread, providing constant-time {@link #cacheBlock(BlockCacheKey, Cacheable, boolean)} and + * {@link #getBlock(BlockCacheKey, boolean, boolean, boolean)} operations. + * </p> + * <p> + * The cache maintains three block-priority levels to provide scan resistance and support in-memory + * column families: + * </p> + * <ul> + * <li>single-access blocks</li> + * <li>multiple-access blocks</li> + * <li>in-memory blocks</li> + * </ul> + * <p> + * Each priority is assigned a portion of the total cache capacity. During eviction the cache tries + * to preserve the configured relative sizes while allowing unused capacity in one priority to be + * consumed by another. + * </p> + * <p> + * This class is a storage engine only. It does not perform L1/L2 orchestration, victim-cache + * delegation, tier placement, admission control, promotion, or demotion. Those responsibilities + * belong to the cache topology and policy layers. + * </p> + */ [email protected] +public class LruCacheEngine implements CacheEngine, HeapSize, Iterable<CachedBlock> { + + private static final Logger LOG = LoggerFactory.getLogger(LruCacheEngine.class); + + /** + * Percentage of total size that eviction will evict until. + */ + private static final String LRU_MIN_FACTOR_CONFIG_NAME = "hbase.lru.blockcache.min.factor"; + + /** + * Acceptable cache size above which eviction is triggered. + */ + private static final String LRU_ACCEPTABLE_FACTOR_CONFIG_NAME = + "hbase.lru.blockcache.acceptable.factor"; + + /** + * Hard capacity limit. Inserts are rejected once the cache exceeds this factor multiplied by the + * acceptable size. + */ + static final String LRU_HARD_CAPACITY_LIMIT_FACTOR_CONFIG_NAME = + "hbase.lru.blockcache.hard.capacity.limit.factor"; + + private static final String LRU_SINGLE_PERCENTAGE_CONFIG_NAME = + "hbase.lru.blockcache.single.percentage"; + + private static final String LRU_MULTI_PERCENTAGE_CONFIG_NAME = + "hbase.lru.blockcache.multi.percentage"; + + private static final String LRU_MEMORY_PERCENTAGE_CONFIG_NAME = + "hbase.lru.blockcache.memory.percentage"; + + /** + * Configuration key that gives data blocks from in-memory HFiles higher eviction priority. + */ + private static final String LRU_IN_MEMORY_FORCE_MODE_CONFIG_NAME = + "hbase.lru.rs.inmemoryforcemode"; + + static final float DEFAULT_LOAD_FACTOR = 0.75f; + static final int DEFAULT_CONCURRENCY_LEVEL = 16; + + private static final float DEFAULT_MIN_FACTOR = 0.95f; + static final float DEFAULT_ACCEPTABLE_FACTOR = 0.99f; + + private static final float DEFAULT_SINGLE_FACTOR = 0.25f; + private static final float DEFAULT_MULTI_FACTOR = 0.50f; + private static final float DEFAULT_MEMORY_FACTOR = 0.25f; + + private static final float DEFAULT_HARD_CAPACITY_LIMIT_FACTOR = 1.2f; + + private static final boolean DEFAULT_IN_MEMORY_FORCE_MODE = false; + + private static final int STAT_THREAD_PERIOD = 60 * 5; + + private static final String LRU_MAX_BLOCK_SIZE = "hbase.lru.max.block.size"; + + private static final long DEFAULT_MAX_BLOCK_SIZE = 16L * 1024L * 1024L; + + /** + * Fixed heap overhead of an LRU cache engine instance. + */ + public static final long CACHE_FIXED_OVERHEAD = + ClassSize.estimateBase(LruCacheEngine.class, false); + + /** + * Cached blocks keyed by their HFile block cache key. + * <p> + * A {@link ConcurrentHashMap} is required because {@link #getBlock} and eviction depend on the + * atomicity guarantees of {@code computeIfPresent}. + * </p> + */ + private transient final ConcurrentHashMap<BlockCacheKey, LruCachedBlock> map; + + /** Lock protecting the eviction process. */ + private transient final ReentrantLock evictionLock = new ReentrantLock(true); + + /** Maximum size of an individual block accepted by this cache. */ + private final long maxBlockSize; + + /** Whether an eviction pass is currently running. */ + private volatile boolean evictionInProgress; + + /** Optional background eviction thread. */ + private transient final EvictionThread evictionThread; + + /** + * Listener notified about capacity-driven block evictions. + */ + private volatile CacheEvictionListener evictionListener; + + /** Executor used to periodically report cache statistics. */ + private transient final ScheduledExecutorService scheduleThreadPool = + Executors.newScheduledThreadPool(1, new ThreadFactoryBuilder() + .setNameFormat("LruCacheEngineStatsExecutor").setDaemon(true).build()); + + /** Current total heap size used by the cache. */ + private final AtomicLong size; + + /** Current heap size of data blocks. */ + private final LongAdder dataBlockSize = new LongAdder(); + + /** Current heap size of index blocks. */ + private final LongAdder indexBlockSize = new LongAdder(); + + /** Current heap size of bloom blocks. */ + private final LongAdder bloomBlockSize = new LongAdder(); + + /** Current number of cached blocks. */ + private final AtomicLong elements; + + /** Current number of cached data blocks. */ + private final LongAdder dataBlockElements = new LongAdder(); + + /** Current number of cached index blocks. */ + private final LongAdder indexBlockElements = new LongAdder(); + + /** Current number of cached bloom blocks. */ + private final LongAdder bloomBlockElements = new LongAdder(); + + /** Sequential cache access identifier. */ + private final AtomicLong count; + + /** Hard cache capacity limit factor. */ + private float hardCapacityLimitFactor; + + /** Cache statistics. */ + private final CacheStats stats; + + /** Maximum cache size in bytes. */ + private long maxSize; + + /** Expected average block size. */ + private long blockSize; + + /** Cache size factor at which eviction is triggered. */ + private float acceptableFactor; + + /** Cache size factor to which an eviction pass should reduce the cache. */ + private float minFactor; + + /** Fraction of capacity assigned to single-access blocks. */ + private float singleFactor; + + /** Fraction of capacity assigned to multiple-access blocks. */ + private float multiFactor; + + /** Fraction of capacity assigned to in-memory blocks. */ + private float memoryFactor; + + /** Heap overhead of the cache structure itself. */ + private long overhead; + + /** Whether data blocks from in-memory HFiles receive stronger retention priority. */ + private boolean forceInMemory; + + /** + * Creates an LRU cache engine using default configuration values. + * @param maxSize maximum size of the cache, in bytes + * @param blockSize expected average block size, in bytes + */ + public LruCacheEngine(long maxSize, long blockSize) { + this(maxSize, blockSize, true); + } + + /** + * Creates an LRU cache engine and optionally enables the background eviction thread. + * @param maxSize maximum size of the cache, in bytes + * @param blockSize expected average block size, in bytes + * @param evictionThread whether background eviction should be enabled + */ + public LruCacheEngine(long maxSize, long blockSize, boolean evictionThread) { + this(maxSize, blockSize, evictionThread, (int) Math.ceil(1.2 * maxSize / blockSize), + DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, DEFAULT_MIN_FACTOR, DEFAULT_ACCEPTABLE_FACTOR, + DEFAULT_SINGLE_FACTOR, DEFAULT_MULTI_FACTOR, DEFAULT_MEMORY_FACTOR, + DEFAULT_HARD_CAPACITY_LIMIT_FACTOR, false, DEFAULT_MAX_BLOCK_SIZE); + } + + /** + * Creates an LRU cache engine using values from the supplied configuration. + * @param maxSize maximum size of the cache, in bytes + * @param blockSize expected average block size, in bytes + * @param evictionThread whether background eviction should be enabled + * @param conf cache configuration + */ + public LruCacheEngine(long maxSize, long blockSize, boolean evictionThread, Configuration conf) { + this(maxSize, blockSize, evictionThread, (int) Math.ceil(1.2 * maxSize / blockSize), + DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, + conf.getFloat(LRU_MIN_FACTOR_CONFIG_NAME, DEFAULT_MIN_FACTOR), + conf.getFloat(LRU_ACCEPTABLE_FACTOR_CONFIG_NAME, DEFAULT_ACCEPTABLE_FACTOR), + conf.getFloat(LRU_SINGLE_PERCENTAGE_CONFIG_NAME, DEFAULT_SINGLE_FACTOR), + conf.getFloat(LRU_MULTI_PERCENTAGE_CONFIG_NAME, DEFAULT_MULTI_FACTOR), + conf.getFloat(LRU_MEMORY_PERCENTAGE_CONFIG_NAME, DEFAULT_MEMORY_FACTOR), + conf.getFloat(LRU_HARD_CAPACITY_LIMIT_FACTOR_CONFIG_NAME, DEFAULT_HARD_CAPACITY_LIMIT_FACTOR), + conf.getBoolean(LRU_IN_MEMORY_FORCE_MODE_CONFIG_NAME, DEFAULT_IN_MEMORY_FORCE_MODE), + conf.getLong(LRU_MAX_BLOCK_SIZE, DEFAULT_MAX_BLOCK_SIZE)); + } + + /** + * Creates an LRU cache engine using the supplied configuration and background eviction. + * @param maxSize maximum size of the cache, in bytes + * @param blockSize expected average block size, in bytes + * @param conf cache configuration + */ + public LruCacheEngine(long maxSize, long blockSize, Configuration conf) { + this(maxSize, blockSize, true, conf); + } + + /** + * Creates a fully configured LRU cache engine. + * @param maxSize maximum size of this cache, in bytes + * @param blockSize expected average size of blocks, in bytes + * @param evictionThread whether to run eviction in a background thread + * @param mapInitialSize initial size of the backing map + * @param mapLoadFactor load factor of the backing map + * @param mapConcurrencyLevel concurrency level of the backing map + * @param minFactor fraction of maximum size retained after eviction + * @param acceptableFactor fraction of maximum size that triggers eviction + * @param singleFactor fraction assigned to single-access blocks + * @param multiFactor fraction assigned to multiple-access blocks + * @param memoryFactor fraction assigned to in-memory blocks + * @param hardLimitFactor hard capacity limit factor + * @param forceInMemory whether in-memory HFile blocks receive stronger retention priority + * @param maxBlockSize largest individual block accepted by this cache + */ + public LruCacheEngine(long maxSize, long blockSize, boolean evictionThread, int mapInitialSize, + float mapLoadFactor, int mapConcurrencyLevel, float minFactor, float acceptableFactor, + float singleFactor, float multiFactor, float memoryFactor, float hardLimitFactor, + boolean forceInMemory, long maxBlockSize) { + this.maxBlockSize = maxBlockSize; + + if ( + singleFactor + multiFactor + memoryFactor != 1 || singleFactor < 0 || multiFactor < 0 + || memoryFactor < 0 + ) { + throw new IllegalArgumentException( + "Single, multi, and memory factors should be non-negative and total 1.0"); + } + + if (minFactor >= acceptableFactor) { + throw new IllegalArgumentException("minFactor must be smaller than acceptableFactor"); + } + + if (minFactor >= 1.0f || acceptableFactor >= 1.0f) { + throw new IllegalArgumentException("all factors must be < 1"); + } + + this.maxSize = maxSize; + this.blockSize = blockSize; + this.forceInMemory = forceInMemory; + this.map = new ConcurrentHashMap<>(mapInitialSize, mapLoadFactor, mapConcurrencyLevel); + this.minFactor = minFactor; + this.acceptableFactor = acceptableFactor; + this.singleFactor = singleFactor; + this.multiFactor = multiFactor; + this.memoryFactor = memoryFactor; + this.stats = new CacheStats(getClass().getSimpleName()); + this.count = new AtomicLong(0); + this.elements = new AtomicLong(0); + this.overhead = calculateOverhead(maxSize, blockSize, mapConcurrencyLevel); + this.size = new AtomicLong(this.overhead); + this.hardCapacityLimitFactor = hardLimitFactor; + + if (evictionThread) { + this.evictionThread = new EvictionThread(this); + this.evictionThread.start(); + } else { + this.evictionThread = null; + } + + this.scheduleThreadPool.scheduleAtFixedRate(new StatisticsThread(this), STAT_THREAD_PERIOD, + STAT_THREAD_PERIOD, TimeUnit.SECONDS); + } + + /** + * Returns the human-readable name of this cache engine. + * @return cache engine name + */ + @Override + public String getName() { + return getClass().getSimpleName(); + } + + /** + * Updates the maximum size of this cache. + * <p> + * If the cache is already larger than the new acceptable size, an eviction pass is started. + * </p> + * @param maxSize new maximum size, in bytes + */ + public void setMaxSize(long maxSize) { + this.maxSize = maxSize; + if (size.get() > acceptableSize() && !evictionInProgress) { + runEviction(); + } + } + + /** + * Returns a heap-backed reference suitable for storage in this cache. + * <p> + * Shared-memory {@link HFileBlock}s are cloned onto the heap. Other blocks are retained before + * being referenced by the cache. + * </p> + * @param buf block to convert + * @return heap-backed retained block + */ + private Cacheable asReferencedHeapBlock(Cacheable buf) { + if (buf instanceof HFileBlock) { + HFileBlock block = (HFileBlock) buf; + if (block.isSharedMem()) { + return HFileBlock.deepCloneOnHeap(block); + } + } + + return buf.retain(); + } + + /** + * Caches the specified block. + * @param cacheKey block cache key + * @param buf block contents + * @param inMemory whether the block should receive in-memory priority + */ + @Override + public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf, boolean inMemory) { + if (buf.heapSize() > maxBlockSize) { + if (stats.failInsert() % 50 == 0) { + LOG.warn("Trying to cache too large a block " + cacheKey.getHfileName() + " @ " + + cacheKey.getOffset() + " is " + buf.heapSize() + " which is larger than " + + maxBlockSize); + } + return; + } + + LruCachedBlock cachedBlock = map.get(cacheKey); + if ( + cachedBlock != null && !BlockCacheUtil.shouldReplaceExistingCacheBlock(this, cacheKey, buf) + ) { + return; + } + + long currentSize = size.get(); + long currentAcceptableSize = acceptableSize(); + long hardLimitSize = (long) (hardCapacityLimitFactor * currentAcceptableSize); + + if (currentSize >= hardLimitSize) { + stats.failInsert(); + if (LOG.isTraceEnabled()) { + LOG.trace("LruCacheEngine current size " + StringUtils.byteDesc(currentSize) + + " has exceeded acceptable size " + StringUtils.byteDesc(currentAcceptableSize) + "." + + " The hard limit size is " + StringUtils.byteDesc(hardLimitSize) + + ", failed to put cacheKey:" + cacheKey + " into LruCacheEngine."); + } + if (!evictionInProgress) { + runEviction(); + } + return; + } + + Cacheable referencedBlock = asReferencedHeapBlock(buf); + cachedBlock = new LruCachedBlock(cacheKey, referencedBlock, count.incrementAndGet(), inMemory); + + long newSize = updateSizeMetrics(cachedBlock, false); + map.put(cacheKey, cachedBlock); + + long elementCount = elements.incrementAndGet(); + if (referencedBlock.getBlockType().isBloom()) { + bloomBlockElements.increment(); + } else if (referencedBlock.getBlockType().isIndex()) { + indexBlockElements.increment(); + } else if (referencedBlock.getBlockType().isData()) { + dataBlockElements.increment(); + } + + if (LOG.isTraceEnabled()) { + assertCounterSanity(map.size(), elementCount); + } + + if (newSize > currentAcceptableSize && !evictionInProgress) { + runEviction(); + } + } + + /** + * Caches the specified block with normal cache priority. + * @param cacheKey block cache key + * @param buf block contents + */ + @Override + public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf) { + cacheBlock(cacheKey, buf, false); + } + + /** + * Caches the specified block. + * <p> + * LRU insertion is synchronous, so {@code waitWhenCache} has no effect. + * </p> + * @param cacheKey block cache key + * @param buf block contents + * @param inMemory whether the block should receive in-memory priority + * @param waitWhenCache whether the caller requests synchronous completion + */ + @Override + public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf, boolean inMemory, + boolean waitWhenCache) { + cacheBlock(cacheKey, buf, inMemory); + } + + /** + * Checks consistency between the backing-map size and the element counter. + * <p> + * This method is intended for TRACE-level diagnostics and assertion-enabled JVMs. + * </p> + * @param mapSize current backing-map size + * @param counterVal current element-counter value + */ + private static void assertCounterSanity(long mapSize, long counterVal) { + if (counterVal < 0) { + LOG.trace("counterVal overflow. Assertions unreliable. counterVal=" + counterVal + + ", mapSize=" + mapSize); + return; + } + + if (mapSize < Integer.MAX_VALUE) { + double percentageDifference = Math.abs((((double) counterVal) / ((double) mapSize)) - 1.0); + if (percentageDifference > 0.05) { + LOG.trace("delta between reported and actual size > 5%. counterVal=" + counterVal + + ", mapSize=" + mapSize); + } + } + } + + /** + * Updates total and block-type-specific size metrics. + * @param cachedBlock cached block whose size should be applied + * @param evict whether this operation represents removal + * @return new total cache size + */ + private long updateSizeMetrics(LruCachedBlock cachedBlock, boolean evict) { + long heapSize = cachedBlock.heapSize(); + BlockType blockType = cachedBlock.getBuffer().getBlockType(); + + if (evict) { + heapSize *= -1; + } + + if (blockType != null) { + if (blockType.isBloom()) { + bloomBlockSize.add(heapSize); + } else if (blockType.isIndex()) { + indexBlockSize.add(heapSize); + } else if (blockType.isData()) { + dataBlockSize.add(heapSize); + } + } + + return size.addAndGet(heapSize); + } + + /** + * Returns the cached block associated with the specified key. + * <p> + * Lookup is strictly local to this cache engine. A miss is returned to the topology layer rather + * than being delegated to another cache tier. + * </p> + * @param cacheKey block cache key + * @param caching whether the caller caches blocks on misses + * @param repeat whether this is a repeated lookup for the same block + * @param updateCacheMetrics whether cache statistics should be updated + * @return cached block, or {@code null} if not present + */ + @Override + public Cacheable getBlock(BlockCacheKey cacheKey, boolean caching, boolean repeat, + boolean updateCacheMetrics) { + LruCachedBlock cachedBlock = map.computeIfPresent(cacheKey, (key, value) -> { + value.getBuffer().retain(); + return value; + }); + + if (cachedBlock == null) { + if (!repeat && updateCacheMetrics) { + stats.miss(caching, cacheKey.isPrimary(), cacheKey.getBlockType()); + } + return null; + } + + if (updateCacheMetrics) { + stats.hit(caching, cacheKey.isPrimary(), cacheKey.getBlockType()); + } + + cachedBlock.access(count.incrementAndGet()); + return cachedBlock.getBuffer(); + } + + /** + * Returns whether the specified block is currently cached. + * @param cacheKey block cache key + * @return {@code true} if the block is present + */ + public boolean containsBlock(BlockCacheKey cacheKey) { + return map.containsKey(cacheKey); + } + + /** + * Returns whether the specified block is currently cached. + * @param cacheKey block cache key + * @return optional containing the local cache-membership result + */ + @Override + public Optional<Boolean> isAlreadyCached(BlockCacheKey cacheKey) { + return Optional.of(containsBlock(cacheKey)); + } + + /** + * Evicts the specified block. + * @param cacheKey block cache key + * @return {@code true} if a block was found and evicted + */ + @Override + public boolean evictBlock(BlockCacheKey cacheKey) { + LruCachedBlock cachedBlock = map.get(cacheKey); + return cachedBlock != null && evictBlock(cachedBlock, false) > 0; + } + + /** + * Evicts all cached blocks belonging to the specified HFile. + * <p> + * This is a linear scan over the cache contents. + * </p> + * @param hfileName HFile name + * @return number of blocks evicted + */ + @Override + public int evictBlocksByHfileName(String hfileName) { + int numEvicted = 0; + + for (BlockCacheKey key : map.keySet()) { + if (key.getHfileName().equals(hfileName) && evictBlock(key)) { + numEvicted++; + } + } + + return numEvicted; + } + + /** + * Evicts the specified block from this cache. + * <p> + * For capacity-driven evictions, the configured eviction listener is notified while a temporary + * reference to the evicted block is retained. Explicit invalidations do not generate eviction + * notifications. + * </p> + * @param block block to evict + * @param evictedByEvictionProcess whether the eviction was caused by cache pressure + * @return heap size of the evicted block, or {@code 0} if the block was not present + */ + protected long evictBlock(LruCachedBlock block, boolean evictedByEvictionProcess) { + final MutableBoolean evicted = new MutableBoolean(false); + final CacheEvictionListener listener = evictedByEvictionProcess ? evictionListener : null; + final Cacheable buffer = block.getBuffer(); + + if (listener != null) { + buffer.retain(); + } + + try { + map.computeIfPresent(block.getCacheKey(), (key, value) -> { + value.getBuffer().release(); + evicted.setTrue(); + return null; + }); Review Comment: Good catch. The eviction candidate can indeed become stale before `evictBlock(...)` runs. I updated the method to verify that the current mapped value is still the same `LruCachedBlock`, retain the mapped buffer inside `computeIfPresent()`, and perform all accounting and listener notification using the block that was actually removed. If the mapping has been replaced concurrently, the stale eviction candidate is ignored. -- 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]
