/*
* 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.openejb.core.stateful;
import org.apache.openejb.OpenEJBRuntimeException;
import org.apache.openejb.util.Duration;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class SimpleCache<K, V> implements Cache<K, V> {
public static final Logger logger = Logger.getInstance(LogCategory.OPENEJB, "org.apache.openejb.util.resources");
/**
* Map of all known values by key
*/
private final ConcurrentHashMap<K, Entry> cache = new ConcurrentHashMap<K, Entry>();
/**
* All values not in use in least resently used order
*/
private final Queue<Entry> lru = new LinkedBlockingQueue<Entry>();
/**
* Notified when values are loaded, stored, or timedOut
*/
private CacheListener<V> listener;
/**
* Used to load and store values
*/
private PassivationStrategy passivator;
/**
* Maximum number of values that should be in the LRU
*/
private int capacity;
/**
* When the LRU is exceeded, this is the is the number of beans stored.
* This helps to avoid passivating a bean at a time.
*/
private int bulkPassivate;
/**
* A bean may be destroyed if it isn't used in this length of time (in
* milliseconds).
* A time out of value -1 means a bean will never be destroyed due to time out.
* A time out of value 0 means a bean can be immediately destroyed.
*/
private long timeOut = -1;
private ScheduledExecutorService executor;
/**
* Specifies how often the cache is checked for timed out beans.
*/
private long frequency = 60 * 1000;
private ScheduledFuture future;
public SimpleCache() {
}
public SimpleCache(final CacheListener<V> listener, final PassivationStrategy passivator, final int capacity, final int bulkPassivate, final Duration timeOut) {
this.listener = listener;
this.passivator = passivator;
this.capacity = capacity;
this.bulkPassivate = bulkPassivate;
this.timeOut = timeOut.getTime(TimeUnit.MILLISECONDS);
}
public synchronized void init() {
if (frequency > 0 && future == null) {
initScheduledExecutorService();
// start any thread in container loader to avoid leaks
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(SimpleCache.class.getClassLoader());
try {
future = executor.scheduleWithFixedDelay(new Runnable() {
public void run() {
processLRU();
}
}, frequency, frequency, TimeUnit.MILLISECONDS);
} finally {
Thread.currentThread().setContextClassLoader(loader);
}
}
}
public synchronized void destroy() {
if (future != null) {
future.cancel(false);
}
}
private synchronized void initScheduledExecutorService() {
if (executor == null) {
executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
public Thread newThread(final Runnable runable) {
final Thread t = new Thread(runable, "Stateful cache");
t.setDaemon(true);
return t;
}
});
}
}
public synchronized CacheListener<V> getListener() {
return listener;
}
public synchronized void setListener(final CacheListener<V> listener) {
this.listener = listener;
}
public synchronized PassivationStrategy getPassivator() {
return passivator;
}
public synchronized void setPassivator(final PassivationStrategy passivator) {
this.passivator = passivator;
}
public synchronized void setPassivator(final Class<? extends PassivationStrategy> passivatorClass) throws Exception {
this.passivator = passivatorClass.newInstance();
}
public synchronized int getCapacity() {
return capacity;
}
public synchronized void setCapacity(final int capacity) {
this.capacity = capacity;
}
// Old configurations use "PoolSize" to configure max cache size
public synchronized void setPoolSize(final int capacity) {
this.capacity = capacity;
}
public synchronized int getBulkPassivate() {
return bulkPassivate;
}
public synchronized void setBulkPassivate(final int bulkPassivate) {
this.bulkPassivate = bulkPassivate;
}
public synchronized long getTimeOut() {
return timeOut;
}
private static long ms(final String durationValue, final TimeUnit defaultTU) {
final Duration duration = new Duration(durationValue.trim());
// default was minutes
// let say the user uses as before "1" as value
// time unit will be null so simply set the unit
// to the "old" default
if (duration.getUnit() == null) {
duration.setUnit(defaultTU);
}
return duration.getUnit().toMillis(duration.getTime());
}
public synchronized void setTimeOut(final String timeOut) {
this.timeOut = ms(timeOut, TimeUnit.MINUTES);
}
public void setScheduledExecutorService(final ScheduledExecutorService executor) {
this.executor = executor;
}
public ScheduledExecutorService getScheduledExecutorService() {
return executor;
}
public void setFrequency(final String frequency) {
this.frequency = ms(frequency, TimeUnit.SECONDS);
}
public long getFrequency() {
return frequency;
}
public void add(final K key, final V value) {
// find the existing entry
Entry entry = cache.get(key);
if (entry != null) {
entry.lock.lock();
try {
if (entry.getState() != EntryState.REMOVED) {
throw new IllegalStateException("An entry for the key " + key + " already exists");
}
// Entry has been removed between get and lock, simply remove the garbage entry
cache.remove(key);
lru.remove(entry);
} finally {
entry.lock.unlock();
}
}
entry = new Entry(key, value, EntryState.CHECKED_OUT);
cache.put(key, entry);
}
public V checkOut(final K key, final boolean loadEntryIfNotFound) throws Exception {
// attempt (up to 10 times) to obtain the entry from the cache
for (int i = 0; i < 10; i++) {
// find the entry
Entry entry = cache.get(key);
if (!loadEntryIfNotFound && entry == null) {
return null;
}
if (entry == null) {
entry = loadEntry(key);
if (entry == null) {
return null;
}
}
entry.lock.lock();
try {
// verfiy state
switch (entry.getState()) {
case AVAILABLE:
break;
case CHECKED_OUT:
return entry.getValue(); //throw new IllegalStateException("The entry " + key + " is already checked-out");
case PASSIVATED:
// Entry was passivated between get and lock, we need to load the Entry again
// If the cache somehow got corrupted by an entry containing in state PASSIVATED, this remove
// call will remove the corruption
cache.remove(key, entry);
continue;
case REMOVED:
// Entry has been removed between get and lock (most likely by undeploying the EJB), simply drop the instance
return null;
}
// mark entry as in-use
entry.setState(EntryState.CHECKED_OUT);
// entry is removed from the lru while in use
lru.remove(entry);
return entry.getValue();
} finally {
entry.lock.unlock();
}
}
// something is really messed up with this entry, try to cleanup before throwing an exception
final Entry entry = cache.remove(key);
if (entry != null) {
lru.remove(entry);
}
throw new OpenEJBRuntimeException("Cache is corrupted: the entry " + key + " in the Map 'cache' is in state PASSIVATED");
}
public void checkIn(final K key) {
// find the entry
final Entry entry = cache.get(key);
if (entry == null) {
return;
}
entry.lock.lock();
try {
// verfiy state
switch (entry.getState()) {
case AVAILABLE:
if (lru.contains(entry)) {
entry.resetTimeOut();
return;
} else {
throw new IllegalStateException("The entry " + key + " is not checked-out");
}
case PASSIVATED:
// An entry in-use should not be passivated so we can only assume
// that the caller never checked out the bean in the first place
throw new IllegalStateException("The entry " + key + " is not checked-out");
case REMOVED:
// Entry has been removed between get and lock (most likely by undeploying the EJB), simply drop the instance
return;
}
// mark entry as available
entry.setState(EntryState.AVAILABLE);
// add entry to lru
lru.add(entry);
entry.resetTimeOut();
} finally {
entry.lock.unlock();
}
if (frequency == 0) {
processLRU();
}
}
public V remove(final K key) {
// find the entry
final Entry entry = cache.get(key);
if (entry == null) {
return null;
}
entry.lock.lock();
try {
// remove the entry from the cache and lru
cache.remove(key);
lru.remove(entry);
// There is no need to check the state because users of the cache
// are responsible for maintaining references to beans in use
// mark the entry as removed
entry.setState(EntryState.REMOVED);
return entry.getValue();
} finally {
entry.lock.unlock();
}
}
public void removeAll(final CacheFilter<V> filter) {
for (final Iterator<Entry> iterator = cache.values().iterator(); iterator.hasNext(); ) {
final Entry entry = iterator.next();
entry.lock.lock();
try {
if (filter.matches(entry.getValue())) {
// remove the entry from the cache and lru
iterator.remove();
lru.remove(entry);
// There is no need to check the state because users of the cache
// are responsible for maintaining references to beans in use
// mark the entry as removed
entry.setState(EntryState.REMOVED);
}
} finally {
entry.lock.unlock();
}
}
}
public void processLRU() {
final CacheListener<V> listener = this.getListener();
// check for timed out entries
// go through all lru entries since even though entries are in
// least recently used order they might have different timeouts.
final Iterator<Entry> iterator = lru.iterator();
while (iterator.hasNext()) {
final Entry entry = iterator.next();
entry.lock.lock();
try {
switch (entry.getState()) {
case AVAILABLE:
break;
case CHECKED_OUT:
// bean is in use so cannot be passivated
continue;
case PASSIVATED:
// Entry was passivated between get and lock
iterator.remove();
continue;
case REMOVED:
// Entry was remmoved between get and lock
iterator.remove();
continue;
}
if (entry.isTimedOut()) {
iterator.remove();
cache.remove(entry.getKey());
entry.setState(EntryState.REMOVED);
// notify listener that the entry has been removed
if (listener != null) {
try {
listener.timedOut(entry.getValue());
} catch (final Exception e) {
logger.error("An unexpected exception occured from timedOut callback", e);
}
}
}
} finally {
entry.lock.unlock();
}
}
// if there are to many beans in the lru, shink is by on bulkPassivate size
// bulkPassivate size is just an estimate, as locked or timed out beans are skipped
if (lru.size() >= getCapacity()) {
final Map<K, V> valuesToStore = new LinkedHashMap<K, V>();
final List<Entry> entries = new ArrayList<Entry>();
int bulkPassivate = getBulkPassivate();
if (bulkPassivate < 1) {
bulkPassivate = 1;
}
for (int i = 0; i < bulkPassivate; i++) {
final Entry entry = lru.poll();
if (entry == null) {
// lru is empty
break;
}
if (!entry.lock.tryLock()) {
// If two threads are running in this method, you could get a deadlock
// due to lock acquisition order since this section gathers a group of
// locks. Simply skip beans we can not obtain a lock on
continue;
}
try {
switch (entry.getState()) {
case AVAILABLE:
break;
case CHECKED_OUT:
// bean is in use so cannot be passivated
continue;
case PASSIVATED:
// Entry was passivated between get and lock
lru.remove(entry);
continue;
case REMOVED:
// Entry was remmoved between get and lock
lru.remove(entry);
continue;
}
// remove it from the cache
cache.remove(entry.getKey());
// there is a race condition where the item could get added back into the lru
lru.remove(entry);
// if the entry is actually timed out we just destroy it; otherwise it is written to disk
if (entry.isTimedOut()) {
entry.setState(EntryState.REMOVED);
if (listener != null) {
try {
listener.timedOut(entry.getValue());
} catch (final Exception e) {
logger.error("An unexpected exception occured from timedOut callback", e);
}
}
} else {
// entry will be passivated, so we need to obtain an additional lock until the passivation is complete
entry.lock.lock();
entries.add(entry);
entry.setState(EntryState.PASSIVATED);
valuesToStore.put(entry.getKey(), entry.getValue());
}
} finally {
entry.lock.unlock();
}
}
if (!valuesToStore.isEmpty()) {
try {
storeEntries(valuesToStore);
} finally {
for (final Entry entry : entries) {
// release the extra passivation lock
entry.lock.unlock();
}
}
}
}
}
private Entry loadEntry(final K key) throws Exception {
final PassivationStrategy passivator = getPassivator();
if (passivator == null) {
return null;
}
V value = null;
try {
value = (V) passivator.activate(key);
} catch (final Exception e) {
logger.error("An unexpected exception occured while reading entries from disk", e);
}
if (value == null) {
return null;
}
final CacheListener<V> listener = this.getListener();
if (listener != null) {
listener.afterLoad(value);
}
final Entry entry = new Entry(key, value, EntryState.AVAILABLE);
cache.put(key, entry);
return entry;
}
private void storeEntries(final Map<K, V> entriesToStore) {
final CacheListener<V> listener = this.getListener();
for (final Iterator<Map.Entry<K, V>> iterator = entriesToStore.entrySet().iterator(); iterator.hasNext(); ) {
final Map.Entry<K, V> entry = iterator.next();
if (listener != null) {
try {
listener.beforeStore(entry.getValue());
} catch (final Exception e) {
iterator.remove();
logger.error("An unexpected exception occured from beforeStore callback", e);
}
}
}
final PassivationStrategy passivator = getPassivator();
if (passivator == null) {
return;
}
try {
passivator.passivate(entriesToStore);
} catch (final Exception e) {
logger.error("An unexpected exception occured while writting the entries to disk", e);
}
}
private enum EntryState {
AVAILABLE, CHECKED_OUT, PASSIVATED, REMOVED
}
private final class Entry {
private final K key;
private final V value;
private final ReentrantLock lock = new ReentrantLock();
private EntryState state;
private long lastAccess;
private final long timeOut;
private Entry(final K key, final V value, final EntryState state) {
this.key = key;
this.value = value;
this.state = state;
if (value instanceof Cache.TimeOut) {
final Duration duration = ((Cache.TimeOut) value).getTimeOut();
this.timeOut = duration != null ? duration.getTime(TimeUnit.MILLISECONDS) : getTimeOut();
} else {
this.timeOut = getTimeOut();
}
lastAccess = System.currentTimeMillis();
}
private K getKey() {
assertLockHeld();
return key;
}
private V getValue() {
assertLockHeld();
return value;
}
private EntryState getState() {
assertLockHeld();
return state;
}
private void setState(final EntryState state) {
assertLockHeld();
this.state = state;
}
private boolean isTimedOut() {
assertLockHeld();
if (timeOut < 0) {
return false;
} else if (timeOut == 0) {
return true;
} else {
final long now = System.currentTimeMillis();
return now - lastAccess > timeOut;
}
}
private void resetTimeOut() {
assertLockHeld();
if (timeOut > 0) {
lastAccess = System.currentTimeMillis();
}
}
private void assertLockHeld() {
if (!lock.isHeldByCurrentThread()) {
throw new IllegalStateException("Entry must be locked");
}
}
}
}