/*
* 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.geode.internal.cache.lru;
import org.apache.geode.LogWriter;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.RegionDestroyedException;
import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.distributed.internal.OverflowQueueWithDMStats;
import org.apache.geode.internal.cache.*;
import org.apache.geode.internal.cache.control.HeapMemoryMonitor;
import org.apache.geode.internal.cache.control.InternalResourceManager;
import org.apache.geode.internal.cache.control.InternalResourceManager.ResourceType;
import org.apache.geode.internal.cache.control.MemoryEvent;
import org.apache.geode.internal.cache.control.ResourceListener;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.internal.logging.LoggingThreadGroup;
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
import org.apache.logging.log4j.Logger;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Triggers centralized eviction(asynchronously) when the ResourceManager sends an eviction event
* for on-heap regions. This is registered with the ResourceManager.
*
* @since GemFire 6.0
*
*/
public class HeapEvictor implements ResourceListener<MemoryEvent> {
private static final Logger logger = LogService.getLogger();
// Add 1 for the management task that's putting more eviction tasks on the queue
public static final int MAX_EVICTOR_THREADS = Integer.getInteger(
DistributionConfig.GEMFIRE_PREFIX + "HeapLRUCapacityController.MAX_EVICTOR_THREADS",
(Runtime.getRuntime().availableProcessors() * 4)) + 1;
public static final boolean DISABLE_HEAP_EVICTIOR_THREAD_POOL =
Boolean.getBoolean(DistributionConfig.GEMFIRE_PREFIX
+ "HeapLRUCapacityController.DISABLE_HEAP_EVICTIOR_THREAD_POOL");
public static final boolean EVICT_HIGH_ENTRY_COUNT_BUCKETS_FIRST =
Boolean
.valueOf(System.getProperty(DistributionConfig.GEMFIRE_PREFIX
+ "HeapLRUCapacityController.evictHighEntryCountBucketsFirst", "true"))
.booleanValue();
public static final int MINIMUM_ENTRIES_PER_BUCKET = Integer.getInteger(
DistributionConfig.GEMFIRE_PREFIX + "HeapLRUCapacityController.inlineEvictionThreshold", 0);
public static final long TOTAL_BYTES_TO_EVICT_FROM_HEAP;
public static final int BUCKET_SORTING_INTERVAL =
Integer
.getInteger(DistributionConfig.GEMFIRE_PREFIX
+ "HeapLRUCapacityController.higherEntryCountBucketCalculationInterval", 100)
.intValue();
private static final String EVICTOR_THREAD_GROUP_NAME = "EvictorThreadGroup";
private static final String EVICTOR_THREAD_NAME = "EvictorThread";
static {
float evictionBurstPercentage = Float.parseFloat(System.getProperty(
DistributionConfig.GEMFIRE_PREFIX + "HeapLRUCapacityController.evictionBurstPercentage",
"0.4"));
long maxTenuredBytes = HeapMemoryMonitor.getTenuredPoolMaxMemory();
TOTAL_BYTES_TO_EVICT_FROM_HEAP = (long) (maxTenuredBytes * 0.01 * evictionBurstPercentage);
}
private ThreadPoolExecutor evictorThreadPool = null;
private AtomicBoolean mustEvict = new AtomicBoolean(false);
protected final Cache cache;
private final ArrayList testTaskSetSizes = new ArrayList();
public volatile int testAbortAfterLoopCount = Integer.MAX_VALUE;
private BlockingQueue<Runnable> poolQueue;
private AtomicBoolean isRunning = new AtomicBoolean(true);
public HeapEvictor(Cache gemFireCache) {
this.cache = gemFireCache;
initializeEvictorThreadPool();
}
protected boolean includePartitionedRegion(PartitionedRegion region) {
return (region.getEvictionAttributes().getAlgorithm().isLRUHeap()
&& (region.getDataStore() != null) && !region.getAttributes().getOffHeap());
}
protected boolean includeLocalRegion(LocalRegion region) {
return (region.getEvictionAttributes().getAlgorithm().isLRUHeap()
&& !region.getAttributes().getOffHeap());
}
private List<LocalRegion> getAllRegionList() {
List<LocalRegion> allRegionList = new ArrayList<LocalRegion>();
InternalResourceManager irm = (InternalResourceManager) cache.getResourceManager();
for (ResourceListener<MemoryEvent> listener : irm.getResourceListeners(getResourceType())) {
if (listener instanceof PartitionedRegion) {
PartitionedRegion pr = (PartitionedRegion) listener;
if (includePartitionedRegion(pr)) {
allRegionList.addAll(pr.getDataStore().getAllLocalBucketRegions());
}
} else if (listener instanceof LocalRegion) {
LocalRegion lr = (LocalRegion) listener;
if (includeLocalRegion(lr)) {
allRegionList.add(lr);
}
}
}
if (HeapEvictor.MINIMUM_ENTRIES_PER_BUCKET > 0) {
Iterator<LocalRegion> iter = allRegionList.iterator();
while (iter.hasNext()) {
LocalRegion lr = iter.next();
if (lr instanceof BucketRegion) {
if (((BucketRegion) lr).getNumEntriesInVM() <= HeapEvictor.MINIMUM_ENTRIES_PER_BUCKET) {
iter.remove();
}
}
}
}
return allRegionList;
}
private List<LocalRegion> getAllSortedRegionList() {
List<LocalRegion> allRegionList = getAllRegionList();
// Capture the sizes so that they do not change while sorting
final Object2LongOpenHashMap sizes = new Object2LongOpenHashMap(allRegionList.size());
for (LocalRegion r : allRegionList) {
long size = r instanceof BucketRegion ? ((BucketRegion) r).getSizeForEviction() : r.size();
sizes.put(r, size);
}
// Sort with respect to other PR buckets also in case of multiple PRs
Collections.sort(allRegionList, new Comparator<LocalRegion>() {
public int compare(LocalRegion r1, LocalRegion r2) {
long numEntries1 = sizes.get(r1);
long numEntries2 = sizes.get(r2);
if (numEntries1 > numEntries2) {
return -1;
} else if (numEntries1 < numEntries2) {
return 1;
}
return 0;
}
});
return allRegionList;
}
public GemFireCacheImpl getGemFireCache() {
return (GemFireCacheImpl) this.cache;
}
private void initializeEvictorThreadPool() {
final ThreadGroup evictorThreadGroup =
LoggingThreadGroup.createThreadGroup(getEvictorThreadGroupName(), logger);
ThreadFactory evictorThreadFactory = new ThreadFactory() {
private int next = 0;
public Thread newThread(Runnable command) {
Thread t = new Thread(evictorThreadGroup, command, getEvictorThreadName() + next++);
t.setDaemon(true);
return t;
}
};
if (!DISABLE_HEAP_EVICTIOR_THREAD_POOL) {
this.poolQueue = new OverflowQueueWithDMStats(
getGemFireCache().getCachePerfStats().getEvictionQueueStatHelper());
this.evictorThreadPool = new ThreadPoolExecutor(MAX_EVICTOR_THREADS, MAX_EVICTOR_THREADS, 15,
TimeUnit.SECONDS, this.poolQueue, evictorThreadFactory);
}
}
/**
* The task(i.e the region on which eviction needs to be performed) is assigned to the threadpool.
*/
private void submitRegionEvictionTask(Callable<Object> task) {
evictorThreadPool.submit(task);
}
public ThreadPoolExecutor getEvictorThreadPool() {
if (isRunning.get()) {
return evictorThreadPool;
}
return null;
}
/**
* returns the total number of tasks that are currently being executed or queued for execution
*
* @return sum of scheduled and running tasks
*/
public int getRunningAndScheduledTasks() {
if (isRunning.get()) {
return this.evictorThreadPool.getActiveCount() + this.evictorThreadPool.getQueue().size();
}
return -1;
}
private void createAndSubmitWeightedRegionEvictionTasks() {
List<LocalRegion> allRegionList = getAllSortedRegionList();
float numEntriesInVm = 0;
for (LocalRegion lr : allRegionList) {
if (lr instanceof BucketRegion) {
numEntriesInVm = numEntriesInVm + ((BucketRegion) lr).getSizeForEviction();
} else {
numEntriesInVm = numEntriesInVm + lr.getRegionMap().sizeInVM();
}
}
for (LocalRegion lr : allRegionList) {
List<LocalRegion> regionsForSingleTask = new ArrayList<LocalRegion>(1);
float regionEntryCnt = 0;
if (lr instanceof BucketRegion) {
regionEntryCnt = ((BucketRegion) lr).getSizeForEviction();
} else {
regionEntryCnt = lr.getRegionMap().sizeInVM();
}
float percentage = (regionEntryCnt / numEntriesInVm);
long bytesToEvictPerTask = (long) (getTotalBytesToEvict() * percentage);
regionsForSingleTask.add(lr);
if (mustEvict()) {
submitRegionEvictionTask(
new RegionEvictorTask(regionsForSingleTask, this, bytesToEvictPerTask));
} else {
break;
}
}
}
private Set<Callable<Object>> createRegionEvictionTasks() {
Set<Callable<Object>> evictorTaskSet = new HashSet<Callable<Object>>();
int threadsAvailable = getEvictorThreadPool().getCorePoolSize();
long bytesToEvictPerTask = getTotalBytesToEvict() / threadsAvailable;
List<LocalRegion> allRegionList = getAllRegionList();
// This shuffling is not required when eviction triggered for the first time
Collections.shuffle(allRegionList);
int allRegionSetSize = allRegionList.size();
if (allRegionList.isEmpty()) {
return evictorTaskSet;
}
if (allRegionSetSize <= threadsAvailable) {
for (LocalRegion region : allRegionList) {
List<LocalRegion> regionList = new ArrayList<LocalRegion>(1);
regionList.add(region);
Callable<Object> task = new RegionEvictorTask(regionList, this, bytesToEvictPerTask);
evictorTaskSet.add(task);
}
Iterator iterator = evictorTaskSet.iterator();
while (iterator.hasNext()) {
RegionEvictorTask regionEvictorTask = (RegionEvictorTask) iterator.next();
testTaskSetSizes.add(regionEvictorTask.getRegionList().size());
}
return evictorTaskSet;
}
int numRegionsInTask = allRegionSetSize / threadsAvailable;
List<LocalRegion> regionsForSingleTask = null;
Iterator<LocalRegion> itr = allRegionList.iterator();
for (int i = 0; i < threadsAvailable; i++) {
int count = 1;
regionsForSingleTask = new ArrayList<LocalRegion>(numRegionsInTask);
while (count <= numRegionsInTask) {
if (itr.hasNext()) {
regionsForSingleTask.add(itr.next());
}
count++;
}
evictorTaskSet.add(new RegionEvictorTask(regionsForSingleTask, this, bytesToEvictPerTask));
}
// Add leftover regions to last task
while (itr.hasNext()) {
regionsForSingleTask.add(itr.next());
}
Iterator iterator = evictorTaskSet.iterator();
while (iterator.hasNext()) {
RegionEvictorTask regionEvictorTask = (RegionEvictorTask) iterator.next();
testTaskSetSizes.add(regionEvictorTask.getRegionList().size());
}
return evictorTaskSet;
}
// Since the amount of memory used is to a large degree dependent upon when
// garbage collection is run, it's difficult to determine when to stop
// evicting. So, an initial calculation is done to determine the number of
// evictions that are likely needed in order to bring memory usage below the
// eviction threshold. This number is stored in 'numFastLoops' and we
// quickly loop through this number performing evictions. We then continue
// to evict, but at a progressively slower rate waiting either for an event
// which indicates we've dropped below the eviction threshold or another
// eviction event with an updated "number of bytes used". If we get another
// eviction event with an updated "number of bytes used" then 'numFastLoops'
// is recalculated and we start over.
protected volatile int numEvictionLoopsCompleted = 0;
protected volatile int numFastLoops;
private long previousBytesUsed;
private final Object evictionLock = new Object();
@Override
public void onEvent(final MemoryEvent event) {
if (DISABLE_HEAP_EVICTIOR_THREAD_POOL) {
return;
}
// Do we care about eviction events and did the eviction event originate
// in this VM ...
if (this.isRunning.get() && event.isLocal()) {
if (event.getState().isEviction()) {
final LogWriter logWriter = cache.getLogger();
// Have we previously received an eviction event and already started eviction ...
if (this.mustEvict.get() == true) {
if (logWriter.fineEnabled()) {
logWriter.fine("Updating eviction in response to memory event: " + event
+ ". previousBytesUsed=" + previousBytesUsed);
}
// We lock here to make sure that the thread that was previously
// started and running eviction loops is in a state where it's okay
// to update the number of fast loops to perform.
synchronized (evictionLock) {
numEvictionLoopsCompleted = 0;
numFastLoops = (int) ((event.getBytesUsed()
- event.getThresholds().getEvictionThresholdClearBytes() + getTotalBytesToEvict())
/ getTotalBytesToEvict());
evictionLock.notifyAll();
}
// We've updated the number of fast loops to perform, and there's
// already a thread running the evictions, so we're done.
return;
}
if (!this.mustEvict.compareAndSet(false, true)) {
// Another thread just started evicting.
return;
}
numEvictionLoopsCompleted = 0;
numFastLoops =
(int) ((event.getBytesUsed() - event.getThresholds().getEvictionThresholdClearBytes()
+ getTotalBytesToEvict()) / getTotalBytesToEvict());
if (logWriter.fineEnabled()) {
logWriter.fine("Starting eviction in response to memory event: " + event);
}
// The new thread which will run in a loop performing evictions
final Runnable evictionManagerTask = new Runnable() {
@Override
public void run() {
// Has the test hook been set which will cause eviction to abort early
if (numEvictionLoopsCompleted < testAbortAfterLoopCount) {
try {
// Submit tasks into the queue to do the evictions
if (EVICT_HIGH_ENTRY_COUNT_BUCKETS_FIRST) {
createAndSubmitWeightedRegionEvictionTasks();
} else {
for (Callable<Object> task : createRegionEvictionTasks()) {
submitRegionEvictionTask(task);
}
}
RegionEvictorTask.setLastTaskCompletionTime(System.currentTimeMillis());
// Make sure that another thread isn't processing a new eviction event
// and changing the number of fast loops to perform.
synchronized (evictionLock) {
int delayTime = getEvictionLoopDelayTime();
if (logWriter.fineEnabled()) {
logWriter.fine("Eviction loop delay time calculated to be " + delayTime
+ " milliseconds. Fast Loops=" + numFastLoops + ", Loop #="
+ numEvictionLoopsCompleted + 1);
}
numEvictionLoopsCompleted++;
try {
// Wait and release the lock so that the number of fast loops
// needed can be updated by another thread processing a new
// eviction event.
evictionLock.wait(delayTime);
} catch (InterruptedException iex) {
// Loop and try again
}
}
// Do we think we're still above the eviction threshold ...
if (HeapEvictor.this.mustEvict.get()) {
// Submit this runnable back into the thread pool and execute
// another pass at eviction.
HeapEvictor.this.evictorThreadPool.submit(this);
}
} catch (RegionDestroyedException e) {
// A region destroyed exception might be thrown for Region.size() when a bucket
// moves due to rebalancing. retry submitting the eviction task without
// logging an error message. fixes bug 48162
if (HeapEvictor.this.mustEvict.get()) {
HeapEvictor.this.evictorThreadPool.submit(this);
}
}
}
}
};
// Submit the first pass at eviction into the pool
this.evictorThreadPool.execute(evictionManagerTask);
} else {
this.mustEvict.set(false);
}
}
}
protected int getEvictionLoopDelayTime() {
int delayTime = 850; // The waiting period when running fast loops
if ((numEvictionLoopsCompleted - numFastLoops) > 2) {
delayTime = 3000; // Way below the threshold
} else if (numEvictionLoopsCompleted >= numFastLoops) {
delayTime = (numEvictionLoopsCompleted - numFastLoops + 3) * 500; // Just below the threshold
}
return delayTime;
}
public boolean mustEvict() {
return this.mustEvict.get();
}
public void close() {
getEvictorThreadPool().shutdownNow();
isRunning.set(false);
}
public ArrayList testOnlyGetSizeOfTasks() {
if (isRunning.get())
return testTaskSetSizes;
return null;
}
protected String getEvictorThreadGroupName() {
return HeapEvictor.EVICTOR_THREAD_GROUP_NAME;
}
protected String getEvictorThreadName() {
return HeapEvictor.EVICTOR_THREAD_NAME;
}
public long getTotalBytesToEvict() {
return TOTAL_BYTES_TO_EVICT_FROM_HEAP;
}
protected ResourceType getResourceType() {
return ResourceType.HEAP_MEMORY;
}
}