/*
* 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 java.util.Properties;
import org.apache.geode.StatisticDescriptor;
import org.apache.geode.StatisticsType;
import org.apache.geode.StatisticsTypeFactory;
import org.apache.geode.cache.Declarable;
import org.apache.geode.cache.EvictionAction;
import org.apache.geode.cache.EvictionAlgorithm;
import org.apache.geode.cache.EvictionAttributes;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.util.ObjectSizer;
import org.apache.geode.internal.ClassPathLoader;
import org.apache.geode.internal.statistics.StatisticsTypeFactoryImpl;
import org.apache.geode.internal.cache.AbstractLRURegionMap.CDValueWrapper;
import org.apache.geode.internal.cache.CachedDeserializableFactory;
import org.apache.geode.internal.cache.Token;
import org.apache.geode.internal.i18n.LocalizedStrings;
/**
* A <code>CapacityController</code> that will remove the least recently used (LRU) entry from a
* region once the region reaches a certain byte {@linkplain #setMaximumMegabytes capacity}.
* Capacity is determined by monitoring the size of entries added and evicted. Capacity is specified
* in terms of megabytes. GemFire uses an efficient algorithm to determine the amount of space a
* region entry occupies in the VM. However, this algorithm may not yield optimal results for all
* kinds of data. The user may provide his or her own algorithm for determining the size of objects
* by implementing an {@link ObjectSizer}.
*
* <P>
* MemLRUCapacityController must be set in the {@link RegionAttributes} before the region is
* created. A Region with MemLRUCapacityController set will throw an {@link IllegalStateException}
* if an attempt is made to replace the Region's capacity controller. While the capacity controller
* cannot be replaced, it does support changing the limit with the {@link #setMaximumMegabytes}
* method.
*
* <P>
* If you are using a <code>cache.xml</code> file to create a JCache region declaratively, you can
* include the following to associate a <code>MemLRUCapacityController</code> with a region:
*
* <pre>
* <region-attributes>
* <capacity-controller>
* <classname>org.apache.geode.cache.MemLRUCapacityController</classname>
* <parameter name="maximum-megabytes">
* <string>50</string>
* </parameter>
* <parameter name="eviction-action">
* <string>overflow-to-disk</string>
* </parameter>
* </capacity-controller>
* </region-attributes>
* </pre>
*
* @see LRUCapacityController
*
*
* @since GemFire 2.0.2
*/
public final class MemLRUCapacityController extends LRUAlgorithm implements Declarable {
private static final long serialVersionUID = 6364183985590572514L;
private static final int OVERHEAD_PER_ENTRY = 250;
/**
* The default maximum number of entries allowed by MemLRU capacity controller is 10 megabytes.
*/
public static final int DEFAULT_MAXIMUM_MEGABYTES = EvictionAttributes.DEFAULT_MEMORY_MAXIMUM;
/**
* The key for setting the maximum-entries property declaratively.
*
* @see #init
*/
public static final String MAXIMUM_MEGABYTES = "maximum-megabytes";
/**
* The {@link #init initialization} property that specifies the name of the {@link ObjectSizer}
* implementation class.
*/
public static final String SIZER_IMPL = "sizer";
private static final long ONE_MEG = 1024L * 1024L;
protected static final StatisticsType statType;
static {
// create the stats type for MemLRU.
StatisticsTypeFactory f = StatisticsTypeFactoryImpl.singleton();
final String bytesAllowedDesc = "Number of total bytes allowed in this region.";
final String byteCountDesc = "Number of bytes in region.";
final String lruEvictionsDesc = "Number of total entry evictions triggered by LRU.";
final String lruDestroysDesc =
"Number of entries destroyed in the region through both destroy cache operations and eviction. Reset to zero each time it exceeds lruDestroysLimit.";
final String lruDestroysLimitDesc =
"Maximum number of entry destroys triggered by LRU before scan occurs.";
final String lruEvaluationsDesc = "Number of entries evaluated during LRU operations.";
final String lruGreedyReturnsDesc = "Number of non-LRU entries evicted during LRU operations";
statType = f.createType("MemLRUStatistics",
"Statistics about byte based Least Recently Used region entry disposal",
new StatisticDescriptor[] {f.createLongGauge("bytesAllowed", bytesAllowedDesc, "bytes"),
f.createLongGauge("byteCount", byteCountDesc, "bytes"),
f.createLongCounter("lruEvictions", lruEvictionsDesc, "entries"),
f.createLongCounter("lruDestroys", lruDestroysDesc, "entries"),
f.createLongGauge("lruDestroysLimit", lruDestroysLimitDesc, "entries"),
f.createLongCounter("lruEvaluations", lruEvaluationsDesc, "entries"),
f.createLongCounter("lruGreedyReturns", lruGreedyReturnsDesc, "entries"),});
}
//////////////////// Instance Fields ////////////////////
private long limit = (DEFAULT_MAXIMUM_MEGABYTES) * ONE_MEG;
private ObjectSizer sizer;
private int perEntryOverHead = OVERHEAD_PER_ENTRY;
private final boolean isOffHeap;
/////////////////////// Constructors ///////////////////////
/**
* Create an instance of the capacity controller with default settings. The default settings are 0
* <code>maximum-megabytes</code> and a default <code>sizer</code>, requiring either the
* {@link #init} method to be called, or the {@link #setMaximumMegabytes} method.
*/
public MemLRUCapacityController(Region region) {
this(DEFAULT_MAXIMUM_MEGABYTES, region);
}
/**
* Create an instance of the capacity controller the given settings.
*
* @param megabytes the amount of memory allowed in this region specified in megabytes.<br>
* <p>
* For a region with {@link org.apache.geode.cache.DataPolicy#PARTITION}, it is overridden
* by {@link org.apache.geode.cache.PartitionAttributesFactory#setLocalMaxMemory(int) "
* local max memory "} specified for the
* {@link org.apache.geode.cache.PartitionAttributes}. It signifies the amount of memory
* allowed in the region, collectively for its primary buckets and redundant copies for
* this VM. It can be different for the same region in different VMs.
*/
public MemLRUCapacityController(int megabytes, Region region) {
this(megabytes, null /* sizerImpl */, region);
}
/**
* Create an instance of the capacity controller the given settings.
*
* @param megabytes the amount of memory allowed in this region specified in megabytes.<br>
* <p>
* For a region with {@link org.apache.geode.cache.DataPolicy#PARTITION}, it is overridden
* by {@link org.apache.geode.cache.PartitionAttributesFactory#setLocalMaxMemory(int) "
* local max memory "} specified for the
* {@link org.apache.geode.cache.PartitionAttributes}. It signifies the amount of memory
* allowed in the region, collectively for its primary buckets and redundant copies for
* this VM. It can be different for the same region in different VMs.
* @param sizerImpl classname of a class that implements ObjectSizer, used to compute object sizes
* for MemLRU
*/
public MemLRUCapacityController(int megabytes, ObjectSizer sizerImpl, Region region) {
this(megabytes, sizerImpl, EvictionAction.DEFAULT_EVICTION_ACTION, region, false);
}
/**
* Create an instance of the capacity controller the given settings.
*
* @param megabytes the amount of memory allowed in this region specified in megabytes.<br>
* <p>
* For a region with {@link org.apache.geode.cache.DataPolicy#PARTITION}, it is overridden
* by {@link org.apache.geode.cache.PartitionAttributesFactory#setLocalMaxMemory(int) "
* local max memory "} specified for the
* {@link org.apache.geode.cache.PartitionAttributes}. It signifies the amount of memory
* allowed in the region, collectively for its primary buckets and redundant copies for
* this VM. It can be different for the same region in different VMs.
* @param sizerImpl classname of a class that implements ObjectSizer, used to compute object sizes
* for MemLRU
* @param isOffHeap true if the region that owns this cc is stored off heap
*/
public MemLRUCapacityController(int megabytes, ObjectSizer sizerImpl,
EvictionAction evictionAction, Region region, boolean isOffHeap) {
super(evictionAction, region);
this.isOffHeap = isOffHeap;
setMaximumMegabytes(megabytes);
setSizer(sizerImpl);
}
////////////////////// Instance Methods /////////////////////
/**
* Declaratively initializes this capacity controller. Supported properties are:
*
* <ul>
* <li>{@link #MAXIMUM_MEGABYTES maximum-megabytes}: The number of megabytes to limit the region
* to.</li>
* <li>{@link #EVICTION_ACTION eviction-action}: The action to perform when the LRU region entry
* is evicted.</li>
* <li>{@link #SIZER_IMPL sizer}: The name of the {@link ObjectSizer} implementation class to use
* for computing the size of region entries.</li>
* </ul>
*
* @throws NumberFormatException The <code>maximum-megabytes</code> property cannot be parsed as
* an integer
* @throws IllegalArgumentException The value of the <code>eviction-action</code> property is not
* recoginzed.
*/
public void init(Properties props) {
String prop = null;
String sizerStr = null;
if ((sizerStr = props.getProperty(SIZER_IMPL)) != null) {
try {
Class c = ClassPathLoader.getLatest().forName(sizerStr);
setSizer((ObjectSizer) c.newInstance());
} catch (Exception e) {
IllegalArgumentException ex = new IllegalArgumentException(
LocalizedStrings.MemLRUCapacityController_COULD_NOT_CREATE_SIZER_INSTANCE_GIVEN_THE_CLASS_NAME_0
.toLocalizedString(sizer));
ex.initCause(e);
throw ex;
}
}
if ((prop = props.getProperty(MAXIMUM_MEGABYTES)) != null) {
this.limit = Integer.parseInt(prop) * ONE_MEG;
}
if ((prop = props.getProperty(EVICTION_ACTION)) != null) {
setEvictionAction(EvictionAction.parseAction(prop));
}
}
// Candidate for removal since capacity controller no longer part of
// cache.xml
@Override
public Properties getProperties() {
Properties props = new Properties();
if (this.evictionAction != EvictionAction.DEFAULT_EVICTION_ACTION) {
props.setProperty(EVICTION_ACTION, this.evictionAction.toString());
}
long megLimit = this.limit / ONE_MEG;
if (megLimit != DEFAULT_MAXIMUM_MEGABYTES) {
props.setProperty(MAXIMUM_MEGABYTES, String.valueOf(megLimit));
}
if (this.sizer != null) {
props.setProperty(SIZER_IMPL, this.sizer.getClass().getName());
}
return props;
}
/**
* Reset the maximum allowed limit on memory to use for this region. This change takes effect on
* next region operation that could increase the region's byte size. If the region is shared, this
* change is seen by all vms on using the same GemFire shared memory system.
*/
public void setMaximumMegabytes(int megabytes) {
if (megabytes <= 0) {
throw new IllegalArgumentException(
LocalizedStrings.MemLRUCapacityController_MEMLRUCONTROLLER_LIMIT_MUST_BE_POSTIVE_0
.toLocalizedString(Integer.valueOf(megabytes)));
}
this.limit = (megabytes) * ONE_MEG;
if (bucketRegion != null) {
bucketRegion.setLimit(this.limit);
} else if (this.stats != null) {
this.stats.setLimit(this.limit);
}
}
@Override
public void setLimit(int maximum) {
setMaximumMegabytes(maximum);
}
/**
* Sets the the number of bytes of overhead each object occupies in the VM. This value may vary
* between VM implementations.
*/
public void setEntryOverHead(int entryOverHead) {
this.perEntryOverHead = entryOverHead;
}
// public void writeExternal(ObjectOutput out)
// throws IOException {
// super.writeExternal(out);
// if (this.stats != null) {
// long limit = this.getLRUHelper().limit();
// Assert.assertTrue(limit > 0);
// out.writeLong(limit);
// } else {
// Assert.assertTrue(this.limit > 0);
// out.writeLong(this.limit);
// }
// if (this.sizer != null) {
// out.writeBoolean(true);
// out.writeUTF(this.sizer.getClass().getName());
// } else {
// out.writeBoolean(false);
// }
// }
// public void readExternal(ObjectInput in)
// throws IOException, ClassNotFoundException {
// super.readExternal(in);
// long limit = in.readLong();
// setMaximumMegabytes((int) limit);
// if (in.readBoolean()) {
// String className = in.readUTF();
// setSizer(className);
// }
// }
/**
* Sets the {@link ObjectSizer} used to calculate the size of objects placed in the cache.
*
* @param sizer The name of the sizer class
*/
private void setSizer(ObjectSizer sizer) {
this.sizer = sizer;
}
@Override
public long getLimit() {
return this.limit;
}
@Override
protected EnableLRU createLRUHelper() {
return new AbstractEnableLRU() {
/**
* Indicate what kind of <code>EvictionAlgorithm</code> this helper implements
*/
public EvictionAlgorithm getEvictionAlgorithm() {
return EvictionAlgorithm.LRU_MEMORY;
}
/**
* compute the size of storing a key/value pair in the cache..
*/
public int entrySize(Object key, Object value) throws IllegalArgumentException {
if (value == Token.TOMBSTONE) {
return 0;
}
int size = 0;
int keySize = 0;
if (!MemLRUCapacityController.this.isOffHeap) {
size += MemLRUCapacityController.this.getPerEntryOverhead();
keySize = sizeof(key);
}
int valueSize = sizeof(value);
// org.apache.geode.internal.cache.GemFireCacheImpl.getInstance().getLogger().info("DEBUG
// MemLRUCC: overhead=" + size
// + " keySize=" + keySize
// + " valueSize=" + valueSize);
size += keySize;
size += valueSize;
return size;
}
public StatisticsType getStatisticsType() {
return statType;
}
public String getStatisticsName() {
return "MemLRUStatistics";
}
public int getLimitStatId() {
return statType.nameToId("bytesAllowed");
}
public int getCountStatId() {
return statType.nameToId("byteCount");
}
public int getEvictionsStatId() {
return statType.nameToId("lruEvictions");
}
public int getDestroysStatId() {
return statType.nameToId("lruDestroys");
}
public int getDestroysLimitStatId() {
return statType.nameToId("lruDestroysLimit");
}
public int getEvaluationsStatId() {
return statType.nameToId("lruEvaluations");
}
public int getGreedyReturnsStatId() {
return statType.nameToId("lruGreedyReturns");
}
public boolean mustEvict(LRUStatistics stats, Region region, int delta) {
return stats.getCounter() + delta > stats.getLimit();
}
};
}
// added to fix bug 40718
static int basicSizeof(Object o, ObjectSizer sizer) throws IllegalArgumentException {
final boolean cdChangingForm = o instanceof CDValueWrapper;
if (cdChangingForm) {
o = ((CDValueWrapper) o).getValue();
}
if (o == null || o == Token.INVALID || o == Token.LOCAL_INVALID || o == Token.DESTROYED
|| o == Token.TOMBSTONE) {
return 0;
}
int size;
// Shouldn't we defer to the user's object sizer for these things?
if (o instanceof byte[] || o instanceof String) {
size = ObjectSizer.DEFAULT.sizeof(o);
} else if (o instanceof Sizeable) {
size = ((Sizeable) o).getSizeInBytes();
} else if (sizer != null) {
size = sizer.sizeof(o);
} else {
size = ObjectSizer.DEFAULT.sizeof(o);
}
if (cdChangingForm) {
size += CachedDeserializableFactory.overhead();
}
return size;
}
/**
* Return the size of an object as stored in GemFire... Typically this is the serialized size in
* bytes.. This implementation is slow.... Need to add Sizer interface and call it for customer
* objects.
*/
protected int sizeof(Object o) throws IllegalArgumentException {
return basicSizeof(o, this.sizer);
}
public int getPerEntryOverhead() {
return perEntryOverHead;
}
@Override
public boolean equals(Object cc) {
if (!super.equals(cc))
return false;
MemLRUCapacityController other = (MemLRUCapacityController) cc;
if (this.limit != other.limit)
return false;
return true;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*
* Note that we just need to make sure that equal objects return equal hashcodes; nothing really
* elaborate is done here.
*/
@Override
public int hashCode() {
int result = super.hashCode();
result += this.limit;
return result;
}
/**
* Returns a brief description of this capacity controller.
*
* @since GemFire 4.0
*/
@Override
public String toString() {
return "MemLRUCapacityController with a capacity of " + this.getLimit()
+ " megabytes and and eviction action " + this.getEvictionAction();
}
}