
net.sf.ehcache.store.MemoryStore Maven / Gradle / Ivy
/**
* Copyright Terracotta, Inc.
*
* Licensed 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 net.sf.ehcache.store;
import net.sf.ehcache.CacheEntry;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.CacheOperationOutcomes.EvictionOutcome;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import net.sf.ehcache.Status;
import net.sf.ehcache.concurrent.CacheLockProvider;
import net.sf.ehcache.concurrent.ReadWriteLockSync;
import net.sf.ehcache.concurrent.Sync;
import net.sf.ehcache.config.CacheConfiguration;
import net.sf.ehcache.config.CacheConfigurationListener;
import net.sf.ehcache.config.PinningConfiguration;
import net.sf.ehcache.config.SizeOfPolicyConfiguration;
import net.sf.ehcache.event.RegisteredEventListeners;
import net.sf.ehcache.pool.Pool;
import net.sf.ehcache.pool.PoolAccessor;
import net.sf.ehcache.pool.PoolParticipant;
import net.sf.ehcache.pool.Size;
import net.sf.ehcache.pool.SizeOfEngine;
import net.sf.ehcache.pool.SizeOfEngineLoader;
import net.sf.ehcache.pool.impl.UnboundedPool;
import net.sf.ehcache.search.Attribute;
import net.sf.ehcache.search.attribute.AttributeExtractor;
import net.sf.ehcache.search.impl.SearchManager;
import net.sf.ehcache.store.StoreOperationOutcomes.GetOutcome;
import net.sf.ehcache.store.StoreOperationOutcomes.PutOutcome;
import net.sf.ehcache.store.StoreOperationOutcomes.RemoveOutcome;
import net.sf.ehcache.store.chm.SelectableConcurrentHashMap;
import net.sf.ehcache.store.disk.StoreUpdateException;
import net.sf.ehcache.writer.CacheWriterManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terracotta.statistics.OperationStatistic;
import org.terracotta.statistics.Statistic;
import org.terracotta.statistics.StatisticsManager;
import org.terracotta.statistics.derived.EventRateSimpleMovingAverage;
import org.terracotta.statistics.derived.OperationResultFilter;
import org.terracotta.statistics.observer.OperationObserver;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import static net.sf.ehcache.statistics.StatisticBuilder.operation;
/**
* A Store implementation suitable for fast, concurrent in memory stores. The policy is determined by that
* configured in the cache.
*
* @author Terracotta
* @version $Id: MemoryStore.java 10789 2018-04-26 02:08:13Z adahanne $
*/
public class MemoryStore extends AbstractStore implements CacheConfigurationListener, Store {
/**
* This is the default from {@link java.util.concurrent.ConcurrentHashMap}. It should never be used, because we size
* the map to the max size of the store.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* Set optimisation for 100 concurrent threads.
*/
private static final int CONCURRENCY_LEVEL = 100;
private static final int MAX_EVICTION_RATIO = 5;
private static final Logger LOG = LoggerFactory.getLogger(MemoryStore.class.getName());
private static final CopyStrategyHandler NO_COPY_STRATEGY_HANDLER = new CopyStrategyHandler(false, false, null, null);
/**
* Eviction outcome observer
*/
protected final OperationObserver evictionObserver = operation(EvictionOutcome.class).named("eviction").of(this).build();
/**
* The cache this store is associated with.
*/
private final Ehcache cache;
/**
* Map where items are stored by key.
*/
private final SelectableConcurrentHashMap map;
private final PoolAccessor poolAccessor;
private final OperationObserver getObserver = operation(GetOutcome.class).named("get").of(this).tag("local-heap").build();
private final OperationObserver putObserver = operation(PutOutcome.class).named("put").of(this).tag("local-heap").build();
private final OperationObserver removeObserver = operation(RemoveOutcome.class).named("remove").of(this).tag("local-heap").build();
private final boolean storePinned;
private final CopyStrategyHandler copyStrategyHandler;
/**
* The maximum size of the store (0 == no limit)
*/
private volatile int maximumSize;
/**
* status.
*/
private volatile Status status;
/**
* The eviction policy to use
*/
private volatile Policy policy;
/**
* The pool accessor
*/
private volatile CacheLockProvider lockProvider;
/**
* Constructs things that all MemoryStores have in common.
*
* @param cache the cache
* @param pool the pool tracking the on-heap usage
* @param searchManager the search manager
*/
protected MemoryStore(Ehcache cache, Pool pool, BackingFactory factory, final SearchManager searchManager) {
super(searchManager, cache.getName());
status = Status.STATUS_UNINITIALISED;
this.cache = cache;
this.maximumSize = (int) cache.getCacheConfiguration().getMaxEntriesLocalHeap();
this.policy = determineEvictionPolicy(cache);
if (pool instanceof UnboundedPool) {
this.poolAccessor = pool.createPoolAccessor(null, null);
} else {
this.poolAccessor = pool.createPoolAccessor(new Participant(),
SizeOfPolicyConfiguration.resolveMaxDepth(cache),
SizeOfPolicyConfiguration.resolveBehavior(cache).equals(SizeOfPolicyConfiguration.MaxDepthExceededBehavior.ABORT));
}
this.storePinned = determineStorePinned(cache.getCacheConfiguration());
int maximumCapacity = isClockEviction() && !storePinned ? maximumSize : 0;
RegisteredEventListeners eventListener = cache.getCacheEventNotificationService();
if (Boolean.getBoolean(MemoryStore.class.getName() + ".presize")) {
// create the CHM with initialCapacity sufficient to hold maximumSize
final float loadFactor = maximumSize == 1 ? 1 : DEFAULT_LOAD_FACTOR;
int initialCapacity = getInitialCapacityForLoadFactor(maximumSize, loadFactor);
this.map = factory.newBackingMap(poolAccessor, initialCapacity,
loadFactor, CONCURRENCY_LEVEL, maximumCapacity, eventListener);
} else {
this.map = factory.newBackingMap(poolAccessor, CONCURRENCY_LEVEL, maximumCapacity, eventListener);
}
this.status = Status.STATUS_ALIVE;
if (LOG.isDebugEnabled()) {
LOG.debug("Initialized " + this.getClass().getName() + " for " + cache.getName());
}
copyStrategyHandler = getCopyStrategyHandler(cache);
}
static CopyStrategyHandler getCopyStrategyHandler(final Ehcache cache) {
if (cache.getCacheConfiguration().isXaTransactional() || cache.getCacheConfiguration().isXaStrictTransactional()
|| cache.getCacheConfiguration().isLocalTransactional()) {
return new TxCopyStrategyHandler(cache.getCacheConfiguration().isCopyOnRead(),
cache.getCacheConfiguration().isCopyOnWrite(), cache.getCacheConfiguration().getCopyStrategy(),
cache.getCacheConfiguration().getClassLoader());
} else if (cache.getCacheConfiguration().isCopyOnRead() || cache.getCacheConfiguration().isCopyOnWrite()) {
return new CopyStrategyHandler(cache.getCacheConfiguration().isCopyOnRead(),
cache.getCacheConfiguration().isCopyOnWrite(), cache.getCacheConfiguration().getCopyStrategy(),
cache.getCacheConfiguration().getClassLoader());
} else {
return NO_COPY_STRATEGY_HANDLER;
}
}
private boolean determineStorePinned(CacheConfiguration cacheConfiguration) {
PinningConfiguration pinningConfiguration = cacheConfiguration.getPinningConfiguration();
if (pinningConfiguration == null) {
return false;
}
switch (pinningConfiguration.getStore()) {
case LOCALMEMORY:
return false;
case INCACHE:
return !cacheConfiguration.isOverflowToOffHeap() && !cacheConfiguration.isOverflowToDisk();
default:
throw new IllegalArgumentException();
}
}
/**
* Calculates the initialCapacity for a desired maximumSize goal and loadFactor.
*
* @param maximumSizeGoal the desired maximum size goal
* @param loadFactor the load factor
* @return the calculated initialCapacity. Returns 0 if the parameter maximumSizeGoal is less than or equal
* to 0
*/
protected static int getInitialCapacityForLoadFactor(int maximumSizeGoal, float loadFactor) {
double actualMaximum = Math.ceil(maximumSizeGoal / loadFactor);
return Math.max(0, actualMaximum >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) actualMaximum);
}
/**
* A factory method to create a MemoryStore.
*
* @param cache the cache
* @param pool the pool tracking the on-heap usage
* @return an instance of a NotifyingMemoryStore, configured with the appropriate eviction policy
*/
public static Store create(final Ehcache cache, Pool pool) {
CacheConfiguration cacheConfiguration = cache.getCacheConfiguration();
final BruteForceSearchManager searchManager = new BruteForceSearchManager(cache);
MemoryStore memoryStore = new MemoryStore(cache, pool, new BasicBackingFactory(), searchManager);
cacheConfiguration.addConfigurationListener(memoryStore);
searchManager.setBruteForceSource(createBruteForceSource(memoryStore, cache.getCacheConfiguration()));
return memoryStore;
}
/**
* Factory method to wrap the MemoryStore into a BruteForceSource, accounting for transactional and copy
* configuration
*
* @param memoryStore the underlying store acting as source
* @param cacheConfiguration the cache configuration
* @return a BruteForceSource connected to underlying MemoryStore and matching configuration
*/
protected static BruteForceSource createBruteForceSource(MemoryStore memoryStore, CacheConfiguration cacheConfiguration) {
BruteForceSource source = new MemoryStoreBruteForceSource(memoryStore, cacheConfiguration.getSearchable());
CopyStrategyHandler copyStrategyHandler = new CopyStrategyHandler(cacheConfiguration.isCopyOnRead(),
cacheConfiguration.isCopyOnWrite(),
cacheConfiguration.getCopyStrategy(), cacheConfiguration.getClassLoader());
if (cacheConfiguration.getTransactionalMode().isTransactional()) {
source = new TransactionalBruteForceSource(source, copyStrategyHandler);
} else if (cacheConfiguration.isCopyOnRead() || cacheConfiguration.isCopyOnWrite()) {
source = new CopyingBruteForceSource(source, copyStrategyHandler);
}
return source;
}
/**
* Puts an item in the store. Note that this automatically results in an eviction if the store is full.
*
* @param element the element to add
*/
public boolean put(final Element element) throws CacheException {
if (element == null) {
return false;
}
if (searchManager != null) {
searchManager.put(cache.getName(), -1, element, null, attributeExtractors, cache.getCacheConfiguration().getDynamicExtractor());
}
putObserver.begin();
long delta = poolAccessor.add(element.getObjectKey(), element.getObjectValue(), map.storedObject(element), storePinned);
if (delta > -1) {
Element old = map.put(element.getObjectKey(), element, delta);
checkCapacity(element);
if (old == null) {
putObserver.end(PutOutcome.ADDED);
return true;
} else {
putObserver.end(PutOutcome.UPDATED);
return false;
}
} else {
notifyDirectEviction(element);
putObserver.end(PutOutcome.ADDED);
return true;
}
}
/**
* {@inheritDoc}
*/
public boolean putWithWriter(Element element, CacheWriterManager writerManager) throws CacheException {
if (searchManager != null) {
searchManager.put(cache.getName(), -1, element, null, attributeExtractors, cache.getCacheConfiguration().getDynamicExtractor());
}
long delta = poolAccessor.add(element.getObjectKey(), element.getObjectValue(), map.storedObject(element), storePinned);
if (delta > -1) {
final ReentrantReadWriteLock lock = map.lockFor(element.getObjectKey());
lock.writeLock().lock();
try {
Element old = map.put(element.getObjectKey(), element, delta);
if (writerManager != null) {
try {
writerManager.put(element);
} catch (RuntimeException e) {
throw new StoreUpdateException(e, old != null);
}
}
checkCapacity(element);
return old == null;
} finally {
lock.writeLock().unlock();
}
} else {
notifyDirectEviction(element);
return true;
}
}
/**
* Gets an item from the cache.
*
* The last access time in {@link net.sf.ehcache.Element} is updated.
*
* @param key the key of the Element
* @return the element, or null if there was no match for the key
*/
public final Element get(final Object key) {
getObserver.begin();
if (key == null) {
getObserver.end(GetOutcome.MISS);
return null;
} else {
final Element e = map.get(key);
if (e == null) {
getObserver.end(GetOutcome.MISS);
return null;
} else {
getObserver.end(GetOutcome.HIT);
return e;
}
}
}
/**
* Gets an item from the cache, without updating statistics.
*
* @param key the cache key
* @return the element, or null if there was no match for the key
*/
public final Element getQuiet(Object key) {
return map.get(key);
}
/**
* Removes an Element from the store.
*
* @param key the key of the Element, usually a String
* @return the Element if one was found, else null
*/
public Element remove(final Object key) {
if (key == null) {
return null;
}
removeObserver.begin();
try {
return map.remove(key);
} finally {
removeObserver.end(RemoveOutcome.SUCCESS);
}
}
/**
* {@inheritDoc}
*/
public final Element removeWithWriter(Object key, CacheWriterManager writerManager) throws CacheException {
if (key == null) {
return null;
}
// remove single item.
Element element;
final ReentrantReadWriteLock.WriteLock writeLock = map.lockFor(key).writeLock();
writeLock.lock();
try {
element = map.remove(key);
if (writerManager != null) {
writerManager.remove(new CacheEntry(key, element));
}
} finally {
writeLock.unlock();
}
if (element == null && LOG.isDebugEnabled()) {
LOG.debug(cache.getName() + "Cache: Cannot remove entry as key " + key + " was not found");
}
return element;
}
/**
* Memory stores are never backed up and always return false
*/
public final boolean bufferFull() {
return false;
}
/**
* Expire all elements.
*
* This is a default implementation which does nothing. Expiration on demand is only implemented for disk stores.
*/
public void expireElements() {
for (Object key : keySet()) {
final Element element = expireElement(key);
if (element != null) {
cache.getCacheEventNotificationService()
.notifyElementExpiry(copyStrategyHandler.copyElementForReadIfNeeded(element), false);
}
}
}
/**
* Evicts the element for the given key, if it exists and is expired
* @param key the key
* @return the evicted element, if any. Otherwise null
*/
protected Element expireElement(final Object key) {
Element value = get(key);
return value != null && value.isExpired() && map.remove(key, value) ? value : null;
}
/**
* Chooses the Policy from the cache configuration
* @param cache the cache
* @return the chosen eviction policy
*/
static Policy determineEvictionPolicy(Ehcache cache) {
MemoryStoreEvictionPolicy policySelection = cache.getCacheConfiguration().getMemoryStoreEvictionPolicy();
if (policySelection.equals(MemoryStoreEvictionPolicy.LRU)) {
return new LruPolicy();
} else if (policySelection.equals(MemoryStoreEvictionPolicy.FIFO)) {
return new FifoPolicy();
} else if (policySelection.equals(MemoryStoreEvictionPolicy.LFU)) {
return new LfuPolicy();
} else if (policySelection.equals(MemoryStoreEvictionPolicy.CLOCK)) {
return null;
}
throw new IllegalArgumentException(policySelection + " isn't a valid eviction policy");
}
/**
* Remove all of the elements from the store.
*/
public final void removeAll() throws CacheException {
for (Object key : map.keySet()) {
remove(key);
}
}
/**
* Prepares for shutdown.
*/
public synchronized void dispose() {
if (status.equals(Status.STATUS_SHUTDOWN)) {
return;
}
status = Status.STATUS_SHUTDOWN;
flush();
poolAccessor.unlink();
}
/**
* Flush to disk only if the cache is diskPersistent.
*/
public void flush() {
if (cache.getCacheConfiguration().isClearOnFlush()) {
removeAll();
}
}
/**
* Gets an Array of the keys for all elements in the memory cache.
*
* Does not check for expired entries
*
* @return An List
*/
public final List> getKeys() {
return new ArrayList