/* * 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(); } }