org.apache.geronimo.jcache.simple.SimpleCache Maven / Gradle / Ivy
/*
* 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.geronimo.jcache.simple;
import static org.apache.geronimo.jcache.simple.Asserts.assertNotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import javax.cache.Cache;
import javax.cache.CacheException;
import javax.cache.CacheManager;
import javax.cache.configuration.CacheEntryListenerConfiguration;
import javax.cache.configuration.Configuration;
import javax.cache.configuration.Factory;
import javax.cache.event.CacheEntryEvent;
import javax.cache.event.EventType;
import javax.cache.expiry.Duration;
import javax.cache.expiry.EternalExpiryPolicy;
import javax.cache.expiry.ExpiryPolicy;
import javax.cache.integration.CacheLoader;
import javax.cache.integration.CacheLoaderException;
import javax.cache.integration.CacheWriter;
import javax.cache.integration.CacheWriterException;
import javax.cache.integration.CompletionListener;
import javax.cache.processor.EntryProcessor;
import javax.cache.processor.EntryProcessorException;
import javax.cache.processor.EntryProcessorResult;
import javax.management.ObjectName;
public class SimpleCache implements Cache {
private final SimpleManager manager;
private final SimpleConfiguration config;
private final CacheLoader loader;
private final CacheWriter super K, ? super V> writer;
private final ExpiryPolicy expiryPolicy;
private final ObjectName cacheConfigObjectName;
private final ObjectName cacheStatsObjectName;
private final String name;
private final ConcurrentHashMap, SimpleElement> delegate;
private final Map, SimpleListener> listeners = new ConcurrentHashMap<>();
private final Statistics statistics = new Statistics();
private final ExecutorService pool;
private final Serializations serializations;
private final Collection> poolTasks = new CopyOnWriteArraySet<>();
private volatile boolean closed = false;
public SimpleCache(final ClassLoader classLoader, final SimpleManager mgr, final String cacheName,
final SimpleConfiguration configuration, final Properties properties,
final ExecutorService executorService) {
manager = mgr;
name = cacheName;
final int capacity = Integer.parseInt(property(properties, cacheName, "capacity", "1000"));
final float loadFactor = Float.parseFloat(property(properties, cacheName, "loadFactor", "0.75"));
final int concurrencyLevel = Integer.parseInt(property(properties, cacheName, "concurrencyLevel", "16"));
delegate = new ConcurrentHashMap<>(capacity, loadFactor, concurrencyLevel);
config = configuration;
pool = executorService;
final long evictionPause = Long.parseLong(
properties.getProperty(cacheName + ".evictionPause", properties.getProperty("evictionPause", "30000")));
if (evictionPause > 0) {
final long maxDeleteByEvictionRun = Long.parseLong(property(properties, cacheName, "maxDeleteByEvictionRun", "100"));
addPoolTask(new EvictionThread(evictionPause, maxDeleteByEvictionRun));
}
serializations = new Serializations(property(properties, cacheName, "serialization.whitelist", null));
final Factory> cacheLoaderFactory = configuration.getCacheLoaderFactory();
if (cacheLoaderFactory == null) {
loader = NoLoader.INSTANCE;
} else {
loader = ExceptionWrapperHandler.newProxy(classLoader, cacheLoaderFactory.create(), CacheLoaderException.class,
CacheLoader.class);
}
final Factory> cacheWriterFactory = configuration.getCacheWriterFactory();
if (cacheWriterFactory == null) {
writer = NoWriter.INSTANCE;
} else {
writer = ExceptionWrapperHandler.newProxy(classLoader, cacheWriterFactory.create(), CacheWriterException.class,
CacheWriter.class);
}
final Factory expiryPolicyFactory = configuration.getExpiryPolicyFactory();
if (expiryPolicyFactory == null) {
expiryPolicy = new EternalExpiryPolicy();
} else {
expiryPolicy = expiryPolicyFactory.create();
}
for (final CacheEntryListenerConfiguration listener : config.getCacheEntryListenerConfigurations()) {
listeners.put(listener, new SimpleListener<>(listener));
}
statistics.setActive(config.isStatisticsEnabled());
final String mgrStr = manager.getURI().toString().replaceAll(",|:|=|\n", ".");
final String cacheStr = name.replaceAll(",|:|=|\n", ".");
try {
cacheConfigObjectName = new ObjectName(
"javax.cache:type=CacheConfiguration," + "CacheManager=" + mgrStr + "," + "Cache=" + cacheStr);
cacheStatsObjectName = new ObjectName(
"javax.cache:type=CacheStatistics," + "CacheManager=" + mgrStr + "," + "Cache=" + cacheStr);
} catch (final Exception e) {
throw new IllegalArgumentException(e);
}
if (config.isManagementEnabled()) {
JMXs.register(cacheConfigObjectName, new SimpleCacheMXBean(this));
}
if (config.isStatisticsEnabled()) {
JMXs.register(cacheStatsObjectName, new SimpleCacheStatisticsMXBean(statistics));
}
}
private void assertNotClosed() {
if (isClosed()) {
throw new IllegalStateException("cache closed");
}
}
@Override
public V get(final K key) {
assertNotClosed();
assertNotNull(key, "key");
final long getStart = Times.now(false);
return doGetControllingExpiry(getStart, key, true, false, false, true, loader);
}
private V doLoad(final K key, final boolean update, final boolean propagateLoadException, final CacheLoader loader) {
V v = null;
try {
v = loader.load(key);
} catch (final CacheLoaderException e) {
if (propagateLoadException) {
throw e;
}
}
if (v != null) {
final Duration duration = update ? expiryPolicy.getExpiryForUpdate() : expiryPolicy.getExpiryForCreation();
if (isNotZero(duration)) {
delegate.put(new SimpleKey<>(key), new SimpleElement<>(v, duration));
}
}
return v;
}
private void touch(final SimpleKey key, final SimpleElement element) {
if (config.isStoreByValue()) {
delegate.put(new SimpleKey<>(serializations.copy(manager.getClassLoader(), key.getKey())), element);
}
}
@Override
public Map getAll(final Set extends K> keys) {
assertNotClosed();
for (final K k : keys) {
assertNotNull(k, "key");
}
final Map result = new HashMap<>();
for (final K key : keys) {
assertNotNull(key, "key");
final SimpleKey simpleKey = new SimpleKey<>(key);
final SimpleElement elt = delegate.get(simpleKey);
V val = elt != null ? elt.getElement() : null;
if (val == null && config.isReadThrough()) {
val = doLoad(key, false, false, loader);
if (val != null) {
result.put(key, val);
}
} else if (elt != null) {
final Duration expiryForAccess = expiryPolicy.getExpiryForAccess();
if (isNotZero(expiryForAccess)) {
touch(simpleKey, elt);
result.put(key, val);
} else {
expires(simpleKey);
}
}
}
return result;
}
@Override
public boolean containsKey(final K key) {
assertNotClosed();
assertNotNull(key, "key");
return delegate.get(new SimpleKey<>(key)) != null;
}
@Override
public void put(final K key, final V rawValue) {
assertNotClosed();
assertNotNull(key, "key");
assertNotNull(rawValue, "value");
final boolean storeByValue = config.isStoreByValue();
final SimpleKey simpleKey = new SimpleKey<>(storeByValue ? serializations.copy(manager.getClassLoader(), key) : key);
final SimpleElement oldElt = delegate.get(simpleKey);
final V old = oldElt != null ? oldElt.getElement() : null;
final V value = storeByValue ? serializations.copy(manager.getClassLoader(), rawValue) : rawValue;
final boolean created = old == null;
final Duration duration = created ? expiryPolicy.getExpiryForCreation() : expiryPolicy.getExpiryForUpdate();
if (isNotZero(duration)) {
final boolean statisticsEnabled = config.isStatisticsEnabled();
final long start = Times.now(false);
writer.write(new SimpleEntry<>(key, value));
delegate.put(simpleKey, new SimpleElement<>(value, duration));
if (!listeners.isEmpty()) {
for (final SimpleListener listener : listeners.values()) {
if (created) {
listener.onCreated(Collections.> singletonList(
new SimpleEvent<>(this, EventType.CREATED, null, key, value)));
} else
listener.onUpdated(Collections.> singletonList(
new SimpleEvent<>(this, EventType.UPDATED, old, key, value)));
}
}
if (statisticsEnabled) {
statistics.increasePuts(1);
statistics.addPutTime(Times.now(false) - start);
}
} else {
if (!created) {
expires(simpleKey);
}
}
}
private void expires(final SimpleKey cacheKey) {
final SimpleElement elt = delegate.get(cacheKey);
delegate.remove(cacheKey);
onExpired(cacheKey, elt);
}
private void onExpired(final SimpleKey cacheKey, final SimpleElement elt) {
for (final SimpleListener listener : listeners.values()) {
listener.onExpired(Collections.> singletonList(
new SimpleEvent<>(this, EventType.REMOVED, null, cacheKey.getKey(), elt.getElement())));
}
}
@Override
public V getAndPut(final K key, final V value) {
assertNotClosed();
assertNotNull(key, "key");
assertNotNull(value, "value");
final long getStart = Times.now(false);
final V v = doGetControllingExpiry(getStart, key, false, false, true, false, loader);
put(key, value);
return v;
}
@Override
public void putAll(final Map extends K, ? extends V> map) {
assertNotClosed();
final TempStateCacheView view = new TempStateCacheView(this);
for (final Map.Entry extends K, ? extends V> e : map.entrySet()) {
view.put(e.getKey(), e.getValue());
}
view.merge();
}
@Override
public boolean putIfAbsent(final K key, final V value) {
final boolean statisticsEnabled = config.isStatisticsEnabled();
if (!containsKey(key)) {
if (statisticsEnabled) {
statistics.increaseMisses(1);
}
put(key, value);
return true;
} else {
if (statisticsEnabled) {
statistics.increaseHits(1);
}
}
return false;
}
@Override
public boolean remove(final K key) {
assertNotClosed();
assertNotNull(key, "key");
final boolean statisticsEnabled = config.isStatisticsEnabled();
final long start = Times.now(!statisticsEnabled);
writer.delete(key);
final SimpleKey cacheKey = new SimpleKey<>(key);
final SimpleElement v = delegate.remove(cacheKey);
if (v == null || v.isExpired()) {
return false;
}
final V value = v.getElement();
for (final SimpleListener listener : listeners.values()) {
listener.onRemoved(Collections.> singletonList(
new SimpleEvent<>(this, EventType.REMOVED, value, key, value)));
}
if (statisticsEnabled) {
statistics.increaseRemovals(1);
statistics.addRemoveTime(Times.now(false) - start);
}
return true;
}
@Override
public boolean remove(final K key, final V oldValue) {
assertNotClosed();
assertNotNull(key, "key");
assertNotNull(oldValue, "oldValue");
final long getStart = Times.now(false);
final V v = doGetControllingExpiry(getStart, key, false, false, false, false, loader);
if (oldValue.equals(v)) {
remove(key);
return true;
} else if (v != null) {
// weird but just for stats to be right
// (org.jsr107.tck.expiry.CacheExpiryTest.removeSpecifiedEntryShouldNotCallExpiryPolicyMethods())
expiryPolicy.getExpiryForAccess();
}
return false;
}
@Override
public V getAndRemove(final K key) {
assertNotClosed();
assertNotNull(key, "key");
final long getStart = Times.now(false);
final V v = doGetControllingExpiry(getStart, key, false, false, true, false, loader);
remove(key);
return v;
}
private V doGetControllingExpiry(final long getStart, final K key, final boolean updateAcess, final boolean forceDoLoad,
final boolean skipLoad, final boolean propagateLoadException, final CacheLoader loader) {
final boolean statisticsEnabled = config.isStatisticsEnabled();
final SimpleKey simpleKey = new SimpleKey<>(key);
final SimpleElement elt = delegate.get(simpleKey);
V v = elt != null ? elt.getElement() : null;
if (v == null && (config.isReadThrough() || forceDoLoad)) {
if (!skipLoad) {
v = doLoad(key, false, propagateLoadException, loader);
}
} else if (statisticsEnabled) {
if (v != null) {
statistics.increaseHits(1);
} else {
statistics.increaseMisses(1);
}
}
if (updateAcess && elt != null) {
final Duration expiryForAccess = expiryPolicy.getExpiryForAccess();
if (!isNotZero(expiryForAccess)) {
expires(simpleKey);
}
}
if (statisticsEnabled && v != null) {
statistics.addGetTime(Times.now(false) - getStart);
}
return v;
}
@Override
public boolean replace(final K key, final V oldValue, final V newValue) {
assertNotClosed();
assertNotNull(key, "key");
assertNotNull(oldValue, "oldValue");
assertNotNull(newValue, "newValue");
final V value = doGetControllingExpiry(Times.now(config.isStatisticsEnabled()), key, false, config.isReadThrough(), false,
true, loader);
if (value != null && value.equals(oldValue)) {
put(key, newValue);
return true;
} else if (value != null) {
expiryPolicy.getExpiryForAccess();
}
return false;
}
@Override
public boolean replace(final K key, final V value) {
assertNotClosed();
assertNotNull(key, "key");
assertNotNull(value, "value");
boolean statisticsEnabled = config.isStatisticsEnabled();
if (containsKey(key)) {
if (statisticsEnabled) {
statistics.increaseHits(1);
}
put(key, value);
return true;
} else if (statisticsEnabled) {
statistics.increaseMisses(1);
}
return false;
}
@Override
public V getAndReplace(final K key, final V value) {
assertNotClosed();
assertNotNull(key, "key");
assertNotNull(value, "value");
final boolean statisticsEnabled = config.isStatisticsEnabled();
final SimpleElement elt = delegate.get(new SimpleKey<>(key));
if (elt != null) {
V oldValue = elt.getElement();
if (oldValue == null && config.isReadThrough()) {
oldValue = doLoad(key, false, false, loader);
} else if (statisticsEnabled) {
statistics.increaseHits(1);
}
put(key, value);
return oldValue;
} else if (statisticsEnabled) {
statistics.increaseMisses(1);
}
return null;
}
@Override
public void removeAll(final Set extends K> keys) {
assertNotClosed();
assertNotNull(keys, "keys");
for (final K k : keys) {
remove(k);
}
}
@Override
public void removeAll() {
assertNotClosed();
for (final SimpleKey k : delegate.keySet()) {
remove(k.getKey());
}
}
@Override
public void clear() {
assertNotClosed();
delegate.clear();
}
@Override
public > C2 getConfiguration(final Class clazz) {
assertNotClosed();
return clazz.cast(config);
}
@Override
public void loadAll(final Set extends K> keys, final boolean replaceExistingValues,
final CompletionListener completionListener) {
assertNotClosed();
assertNotNull(keys, "keys");
if (loader == null) { // quick exit path
if (completionListener != null) {
completionListener.onCompletion();
}
return;
}
for (final K k : keys) {
assertNotNull(k, "a key");
}
addPoolTask(new Runnable() {
@Override
public void run() {
doLoadAll(keys, replaceExistingValues, completionListener);
}
});
}
private void addPoolTask(final Runnable runnable) {
final AtomicReference> ref = new AtomicReference<>();
final CountDownLatch refIsSet = new CountDownLatch(1);
ref.set(pool.submit(new Runnable() {
@Override
public void run() {
try {
runnable.run();
} finally {
try {
refIsSet.await();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
poolTasks.remove(ref.get());
}
}
}));
refIsSet.countDown();
poolTasks.add(ref.get());
}
private void doLoadAll(final Set extends K> keys, final boolean replaceExistingValues,
final CompletionListener completionListener) {
try {
final long now = Times.now(false);
final Map kvMap = loader.loadAll(keys);
if (kvMap == null) {
return;
}
final CacheLoader preloaded = new MapLoader<>(kvMap);
for (final K k : keys) {
if (replaceExistingValues) {
doLoad(k, containsKey(k), completionListener != null, preloaded);
} else if (!containsKey(k)) {
doGetControllingExpiry(now, k, true, true, false, completionListener != null, preloaded);
}
}
} catch (final RuntimeException e) {
if (completionListener != null) {
completionListener.onException(e);
return;
}
}
if (completionListener != null) {
completionListener.onCompletion();
}
}
@Override
public T invoke(final K key, final EntryProcessor entryProcessor, final Object... arguments)
throws EntryProcessorException {
final TempStateCacheView view = new TempStateCacheView(this);
final T t = doInvoke(view, key, entryProcessor, arguments);
view.merge();
return t;
}
private T doInvoke(final TempStateCacheView view, final K key, final EntryProcessor entryProcessor,
final Object... arguments) {
assertNotClosed();
assertNotNull(entryProcessor, "entryProcessor");
assertNotNull(key, "key");
try {
if (config.isStatisticsEnabled()) {
if (containsKey(key)) {
statistics.increaseHits(1);
} else {
statistics.increaseMisses(1);
}
}
return entryProcessor.process(new SimpleMutableEntry<>(view, key), arguments);
} catch (final Exception ex) {
return throwEntryProcessorException(ex);
}
}
@Override
public Map> invokeAll(final Set extends K> keys,
final EntryProcessor entryProcessor, final Object... arguments) {
assertNotClosed();
assertNotNull(entryProcessor, "entryProcessor");
final Map> results = new HashMap<>();
for (final K k : keys) {
try {
final T invoke = invoke(k, entryProcessor, arguments);
if (invoke != null) {
results.put(k, new EntryProcessorResult() {
@Override
public T get() throws EntryProcessorException {
return invoke;
}
});
}
} catch (final Exception e) {
results.put(k, new EntryProcessorResult() {
@Override
public T get() throws EntryProcessorException {
return throwEntryProcessorException(e);
}
});
}
}
return results;
}
@Override
public void registerCacheEntryListener(final CacheEntryListenerConfiguration cacheEntryListenerConfiguration) {
assertNotClosed();
if (listeners.containsKey(cacheEntryListenerConfiguration)) {
throw new IllegalArgumentException(cacheEntryListenerConfiguration + " already registered");
}
listeners.put(cacheEntryListenerConfiguration, new SimpleListener<>(cacheEntryListenerConfiguration));
config.addListener(cacheEntryListenerConfiguration);
}
@Override
public void deregisterCacheEntryListener(final CacheEntryListenerConfiguration cacheEntryListenerConfiguration) {
assertNotClosed();
listeners.remove(cacheEntryListenerConfiguration);
config.removeListener(cacheEntryListenerConfiguration);
}
@Override
public Iterator> iterator() {
assertNotClosed();
final Iterator> keys = new HashSet<>(delegate.keySet()).iterator();
return new Iterator>() {
private K lastKey = null;
@Override
public boolean hasNext() {
return keys.hasNext();
}
@Override
public Entry next() {
lastKey = keys.next().getKey();
return new SimpleEntry<>(lastKey, get(lastKey));
}
@Override
public void remove() {
if (isClosed() || lastKey == null) {
throw new IllegalStateException(isClosed() ? "cache closed" : "call next() before remove()");
}
SimpleCache.this.remove(lastKey);
}
};
}
@Override
public String getName() {
assertNotClosed();
return name;
}
@Override
public CacheManager getCacheManager() {
assertNotClosed();
return manager;
}
@Override
public synchronized void close() {
if (isClosed()) {
return;
}
for (final Future> task : poolTasks) {
task.cancel(true);
}
final CacheException ce = new CacheException();
manager.release(getName());
closed = true;
close(loader, ce);
close(writer, ce);
close(expiryPolicy, ce);
for (final SimpleListener listener : listeners.values()) {
try {
listener.close();
} catch (final Exception e) {
ce.addSuppressed(e);
}
}
listeners.clear();
JMXs.unregister(cacheConfigObjectName);
JMXs.unregister(cacheStatsObjectName);
delegate.clear();
if (ce.getSuppressed().length > 0) {
throw ce;
}
}
@Override
public boolean isClosed() {
return closed;
}
@Override
public T unwrap(final Class clazz) {
assertNotClosed();
if (clazz.isInstance(this)) {
return clazz.cast(this);
}
if (clazz.isAssignableFrom(Map.class) || clazz.isAssignableFrom(ConcurrentMap.class)) {
return clazz.cast(delegate);
}
throw new IllegalArgumentException(clazz.getName() + " not supported in unwrap");
}
public Statistics getStatistics() {
return statistics;
}
public void enableManagement() {
config.managementEnabled();
JMXs.register(cacheConfigObjectName, new SimpleCacheMXBean(this));
}
public void disableManagement() {
config.managementDisabled();
JMXs.unregister(cacheConfigObjectName);
}
public void enableStatistics() {
config.statisticsEnabled();
statistics.setActive(true);
JMXs.register(cacheStatsObjectName, new SimpleCacheStatisticsMXBean(statistics));
}
public void disableStatistics() {
config.statisticsDisabled();
statistics.setActive(false);
JMXs.unregister(cacheStatsObjectName);
}
private static String property(final Properties properties, final String cacheName, final String name,
final String defaultValue) {
return properties.getProperty(cacheName + "." + name, properties.getProperty(name, defaultValue));
}
private static boolean isNotZero(final Duration duration) {
return duration == null || !duration.isZero();
}
private static T throwEntryProcessorException(final Exception ex) {
if (EntryProcessorException.class.isInstance(ex)) {
throw EntryProcessorException.class.cast(ex);
}
throw new EntryProcessorException(ex);
}
private static void close(final Object potentiallyCloseable, final CacheException wrapper) {
if (AutoCloseable.class.isInstance(potentiallyCloseable)) {
try {
AutoCloseable.class.cast(potentiallyCloseable).close();
} catch (final Exception re) {
wrapper.addSuppressed(re);
}
}
}
private class EvictionThread implements Runnable {
private final long pause;
private final long maxDelete;
private EvictionThread(final long evictionPause, final long maxDelete) {
this.pause = evictionPause;
this.maxDelete = maxDelete;
}
@Override
public void run() {
while (!isClosed()) {
try {
Thread.sleep(pause);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
if (delegate.isEmpty()) {
continue;
}
try {
final List> keys = new ArrayList<>(delegate.keySet());
Collections.sort(keys, new Comparator>() {
@Override
public int compare(final SimpleKey o1, final SimpleKey o2) {
final long l = o1.lastAccess() - o2.lastAccess();
if (l == 0) {
return keys.indexOf(o1) - keys.indexOf(o2);
}
return (int) l;
}
});
int delete = 0;
for (final SimpleKey key : keys) {
final SimpleElement elt = delegate.get(key);
if (elt != null && elt.isExpired()) {
delegate.remove(key);
statistics.increaseEvictions(1);
onExpired(key, elt);
delete++;
if (delete >= maxDelete) {
break;
}
}
}
} catch (final Exception e) {
// no-op
}
}
}
}
private static class MapLoader implements CacheLoader {
private final Map loaded;
private MapLoader(final Map loaded) {
this.loaded = loaded;
}
@Override
public V load(final K key) throws CacheLoaderException {
return loaded.get(key);
}
@Override
public Map loadAll(final Iterable extends K> keys) throws CacheLoaderException {
throw new UnsupportedOperationException();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy