/*
* JBoss, Home of Professional Open Source.
* Copyright 2006, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.ejb3.cache.tree;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.ejb.EJBException;
import javax.ejb.NoSuchEJBException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import org.jboss.aop.Advisor;
import org.jboss.cache.Cache;
import org.jboss.cache.CacheException;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.Fqn;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.Node;
import org.jboss.cache.Region;
import org.jboss.cache.config.EvictionPolicyConfig;
import org.jboss.cache.config.Option;
import org.jboss.cache.eviction.LRUConfiguration;
import org.jboss.cache.jmx.CacheJmxWrapperMBean;
import org.jboss.cache.notifications.annotation.CacheListener;
import org.jboss.cache.notifications.annotation.NodeActivated;
import org.jboss.cache.notifications.annotation.NodePassivated;
import org.jboss.cache.notifications.event.NodeActivatedEvent;
import org.jboss.cache.notifications.event.NodePassivatedEvent;
import org.jboss.ejb3.Container;
import org.jboss.ejb3.EJBContainer;
import org.jboss.ejb3.annotation.CacheConfig;
import org.jboss.ejb3.cache.ClusteredStatefulCache;
import org.jboss.ejb3.pool.Pool;
import org.jboss.ejb3.stateful.NestedStatefulBeanContext;
import org.jboss.ejb3.stateful.ProxiedStatefulBeanContext;
import org.jboss.ejb3.stateful.StatefulBeanContext;
import org.jboss.logging.Logger;
import org.jboss.mx.util.MBeanProxyExt;
import org.jboss.mx.util.MBeanServerLocator;
import org.jboss.util.id.GUID;
/**
* Clustered SFSB cache that uses JBoss Cache to cache and replicate
* bean contexts.
*
* @author <a href="mailto:bill@jboss.org">Bill Burke</a>
* @author Brian Stansberry
*
* @version $Revision: 69080 $
*/
public class StatefulTreeCache implements ClusteredStatefulCache
{
private static final int FQN_SIZE = 3; // depth of fqn that we store the session in.
private static final int DEFAULT_BUCKET_COUNT = 100;
private static final String[] DEFAULT_HASH_BUCKETS = new String[DEFAULT_BUCKET_COUNT];
private static Option LOCAL_ONLY_OPTION = new Option();
private static Option GRAVITATE_OPTION = new Option();
static
{
LOCAL_ONLY_OPTION.setCacheModeLocal(true);
GRAVITATE_OPTION.setForceDataGravitation(true);
for (int i = 0; i < DEFAULT_HASH_BUCKETS.length; i++)
{
DEFAULT_HASH_BUCKETS[i] = String.valueOf(i);
}
}
private ThreadLocal<Boolean> localActivity = new ThreadLocal<Boolean>();
private Logger log = Logger.getLogger(StatefulTreeCache.class);
private Pool pool;
private WeakReference<ClassLoader> classloader;
private Cache cache;
private Fqn cacheNode;
private Region region;
private ClusteredStatefulCacheListener listener;
public static long MarkInUseWaitTime = 15000;
protected String[] hashBuckets = DEFAULT_HASH_BUCKETS;
protected int createCount = 0;
protected int passivatedCount = 0;
protected int removeCount = 0;
protected long removalTimeout = 0;
protected RemovalTimeoutTask removalTask = null;
protected boolean running = true;
protected Map<Object, Long> beans = new ConcurrentHashMap<Object, Long>();
protected EJBContainer ejbContainer;
public StatefulBeanContext create()
{
StatefulBeanContext ctx = null;
try
{
ctx = (StatefulBeanContext) pool.get();
if (log.isTraceEnabled())
{
log.trace("Caching context " + ctx.getId() + " of type " + ctx.getClass());
}
putInCache(ctx);
ctx.setInUse(true);
ctx.lastUsed = System.currentTimeMillis();
++createCount;
beans.put(ctx.getId(), new Long(ctx.lastUsed));
}
catch (EJBException e)
{
throw e;
}
catch (Exception e)
{
throw new EJBException(e);
}
return ctx;
}
public StatefulBeanContext create(Class[] initTypes, Object[] initValues)
{
StatefulBeanContext ctx = null;
try
{
ctx = (StatefulBeanContext) pool.get(initTypes, initValues);
if (log.isTraceEnabled())
{
log.trace("Caching context " + ctx.getId() + " of type " + ctx.getClass());
}
putInCache(ctx);
ctx.setInUse(true);
ctx.lastUsed = System.currentTimeMillis();
++createCount;
beans.put(ctx.getId(), new Long(ctx.lastUsed));
}
catch (EJBException e)
{
throw e;
}
catch (Exception e)
{
throw new EJBException(e);
}
return ctx;
}
public StatefulBeanContext get(Object key) throws EJBException
{
return get(key, true);
}
public StatefulBeanContext get(Object key, boolean markInUse) throws EJBException
{
StatefulBeanContext entry = null;
Fqn id = getFqn(key, false);
Boolean active = localActivity.get();
try
{
localActivity.set(Boolean.TRUE);
// If need be, gravitate
InvocationContext ictx = cache.getInvocationContext();
ictx.setOptionOverrides(getGravitateOption());
entry = (StatefulBeanContext) cache.get(id, "bean");
}
catch (CacheException e)
{
RuntimeException re = convertToRuntimeException(e);
throw re;
}
finally
{
localActivity.set(active);
}
if (entry == null)
{
throw new NoSuchEJBException("Could not find stateful bean: " + key);
}
else if (markInUse && entry.isRemoved())
{
throw new NoSuchEJBException("Could not find stateful bean: " + key +
" (bean was marked as removed)");
}
entry.postReplicate();
if (markInUse)
{
entry.setInUse(true);
// Mark the Fqn telling the eviction thread not to passivate it yet.
// Note the Fqn we use is relative to the region!
region.markNodeCurrentlyInUse(new Fqn(key.toString()), MarkInUseWaitTime);
entry.lastUsed = System.currentTimeMillis();
beans.put(key, new Long(entry.lastUsed));
}
if(log.isTraceEnabled())
{
log.trace("get: retrieved bean with cache id " +id.toString());
}
return entry;
}
public StatefulBeanContext peek(Object key) throws NoSuchEJBException
{
return get(key, false);
}
public void remove(Object key)
{
Fqn id = getFqn(key, false);
try
{
if(log.isTraceEnabled())
{
log.trace("remove: cache id " +id.toString());
}
InvocationContext ictx = cache.getInvocationContext();
ictx.setOptionOverrides(getGravitateOption());
StatefulBeanContext ctx = (StatefulBeanContext) cache.get(id, "bean");
if (ctx != null)
{
if (!ctx.isRemoved())
pool.remove(ctx);
if (ctx.getCanRemoveFromCache())
{
// Do a cluster-wide removal of the ctx
cache.removeNode(id);
}
else
{
// We can't remove the ctx as it contains live nested beans
// But, we must replicate it so other nodes know the parent is removed!
putInCache(ctx);
}
++removeCount;
beans.remove(key);
}
}
catch (CacheException e)
{
RuntimeException re = convertToRuntimeException(e);
throw re;
}
}
public void release(StatefulBeanContext ctx)
{
synchronized (ctx)
{
ctx.setInUse(false);
ctx.lastUsed = System.currentTimeMillis();
beans.put(ctx.getId(), new Long(ctx.lastUsed));
// OK, it is free to passivate now.
// Note the Fqn we use is relative to the region!
region.unmarkNodeCurrentlyInUse(getFqn(ctx.getId(), true));
}
}
public void replicate(StatefulBeanContext ctx)
{
// StatefulReplicationInterceptor should only pass us the ultimate
// parent context for a tree of nested beans, which should always be
// a standard StatefulBeanContext
if (ctx instanceof NestedStatefulBeanContext)
{
throw new IllegalArgumentException("Received unexpected replicate call for nested context " + ctx.getId());
}
try
{
putInCache(ctx);
}
catch (CacheException e)
{
RuntimeException re = convertToRuntimeException(e);
throw re;
}
}
public void initialize(Container container) throws Exception
{
this.ejbContainer = (EJBContainer) container;
log = Logger.getLogger(getClass().getName() + "." + this.ejbContainer.getEjbName());
this.pool = this.ejbContainer.getPool();
ClassLoader cl = this.ejbContainer.getClassloader();
this.classloader = new WeakReference<ClassLoader>(cl);
Advisor advisor = this.ejbContainer;
CacheConfig config = (CacheConfig) advisor.resolveAnnotation(CacheConfig.class);
MBeanServer server = MBeanServerLocator.locateJBoss();
String name = config.name();
if (name == null || name.trim().length() == 0)
name = CacheConfig.DEFAULT_CLUSTERED_OBJECT_NAME;
ObjectName cacheON = new ObjectName(name);
CacheJmxWrapperMBean mbean = (CacheJmxWrapperMBean) MBeanProxyExt.create(CacheJmxWrapperMBean.class, cacheON, server);
cache = mbean.getCache();
cacheNode = new Fqn(new Object[] { this.ejbContainer.getDeploymentQualifiedName() });
// Try to create an eviction region per ejb
region = cache.getRegion(cacheNode, true);
EvictionPolicyConfig epc = getEvictionPolicyConfig((int) config.idleTimeoutSeconds(),
config.maxSize());
region.setEvictionPolicy(epc);
// JBCACHE-1136. There's no reason to have state in an inactive region
cleanBeanRegion();
// Transfer over the state for the region
region.registerContextClassLoader(cl);
region.activate();
log.debug("initialize(): created region: " +region + " for ejb: " + this.ejbContainer.getEjbName());
removalTimeout = config.removalTimeoutSeconds();
if (removalTimeout > 0)
removalTask = new RemovalTimeoutTask("SFSB Removal Thread - " + this.ejbContainer.getObjectName().getCanonicalName());
}
protected EvictionPolicyConfig getEvictionPolicyConfig(int timeToLiveSeconds, int maxNodes)
{
LRUConfiguration epc = new LRUConfiguration();
// Override the standard policy class
epc.setEvictionPolicyClass(AbortableLRUPolicy.class.getName());
epc.setTimeToLiveSeconds(timeToLiveSeconds);
epc.setMaxNodes(maxNodes);
return epc;
}
public void start()
{
// register to listen for cache events
// TODO this approach may not be scalable when there are many beans
// since then we will need to go thru N listeners to figure out which
// one this event belongs to. Consider having a singleton listener
listener = new ClusteredStatefulCacheListener();
cache.addCacheListener(listener);
if (removalTask != null)
removalTask.start();
running = true;
}
public void stop()
{
running = false;
if (cache != null)
{
// Remove the listener
if (listener != null)
cache.removeCacheListener(listener);
// Remove locally. We do this to clean up the persistent store,
// which is not affected by the inactivateRegion call below.
cleanBeanRegion();
try {
// Remove locally. We do this to clean up the persistent store,
// which is not affected by the region.deactivate call below.
InvocationContext ctx = cache.getInvocationContext();
ctx.setOptionOverrides(getLocalOnlyOption());
cache.removeNode(cacheNode);
}
catch (CacheException e)
{
log.error("stop(): can't remove bean from the underlying distributed cache");
}
if (region != null)
{
region.deactivate();
region.unregisterContextClassLoader();
// FIXME this method needs to be in Cache
((CacheSPI) cache).getRegionManager().removeRegion(region.getFqn());
// Clear any queues
region.resetEvictionQueues();
region = null;
}
}
classloader = null;
if (removalTask != null)
removalTask.interrupt();
log.debug("stop(): StatefulTreeCache stopped successfully for " +cacheNode);
}
public int getCacheSize()
{
int count = 0;
try
{
Set children = null;
for (int i = 0; i < hashBuckets.length; i++)
{
Node node = cache.getRoot().getChild(new Fqn(cacheNode, hashBuckets[i]));
if (node != null)
{
children = node.getChildrenNames();
count += (children == null ? 0 : children.size());
}
}
count = count - passivatedCount;
}
catch (CacheException e)
{
log.error("Caught exception calculating cache size", e);
count = -1;
}
return count;
}
public int getTotalSize()
{
return beans.size();
}
public int getCreateCount()
{
return createCount;
}
public int getPassivatedCount()
{
return passivatedCount;
}
public int getRemoveCount()
{
return removeCount;
}
public int getAvailableCount()
{
return -1;
}
public int getMaxSize()
{
return -1;
}
public int getCurrentSize()
{
return getCacheSize();
}
private void putInCache(StatefulBeanContext ctx)
{
Boolean active = localActivity.get();
try
{
localActivity.set(Boolean.TRUE);
ctx.preReplicate();
cache.put(getFqn(ctx.getId(), false), "bean", ctx);
ctx.markedForReplication = false;
}
finally
{
localActivity.set(active);
}
}
private Fqn getFqn(Object id, boolean regionRelative)
{
String beanId = id.toString();
int index;
if (id instanceof GUID)
{
index = (id.hashCode()& 0x7FFFFFFF) % hashBuckets.length;
}
else
{
index = (beanId.hashCode()& 0x7FFFFFFF) % hashBuckets.length;
}
if (regionRelative)
return new Fqn( new Object[] {hashBuckets[index], beanId} );
else
return new Fqn(cacheNode, hashBuckets[index], beanId);
}
private void cleanBeanRegion()
{
try {
// Remove locally.
cache.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
cache.removeNode(cacheNode);
}
catch (CacheException e)
{
log.error("Stop(): can't remove bean from the underlying distributed cache");
}
}
/**
* Creates a RuntimeException, but doesn't pass CacheException as the cause
* as it is a type that likely doesn't exist on a client.
* Instead creates a RuntimeException with the original exception's
* stack trace.
*/
private RuntimeException convertToRuntimeException(CacheException e)
{
RuntimeException re = new RuntimeException(e.getClass().getName() + " " + e.getMessage());
re.setStackTrace(e.getStackTrace());
return re;
}
/**
* A CacheListener that allows us to get notifications of passivations and
* activations and thus notify the cached StatefulBeanContext.
*/
@CacheListener
public class ClusteredStatefulCacheListener
{
@NodeActivated
public void nodeActivated(NodeActivatedEvent event)
{
// Ignore everything but "post" events for nodes in our region
if(event.isPre()) return;
Map nodeData = event.getData();
if (nodeData == null) return;
Fqn fqn = event.getFqn();
if(fqn.size() != FQN_SIZE) return;
if(!fqn.isChildOrEquals(cacheNode)) return;
// Don't activate a bean just so we can replace the object
// with a replicated one
if (Boolean.TRUE != localActivity.get())
{
// But we do want to record that the bean's now in memory
--passivatedCount;
return;
}
StatefulBeanContext bean = (StatefulBeanContext) nodeData.get("bean");
if(bean == null)
{
throw new IllegalStateException("nodeLoaded(): null bean instance.");
}
--passivatedCount;
if(log.isTraceEnabled())
{
log.trace("nodeLoaded(): send postActivate event to bean at fqn: " +fqn);
}
ClassLoader oldCl = Thread.currentThread().getContextClassLoader();
try
{
ClassLoader cl = classloader.get();
if (cl != null)
{
Thread.currentThread().setContextClassLoader(cl);
}
bean.activateAfterReplication();
}
finally
{
Thread.currentThread().setContextClassLoader(oldCl);
}
}
@NodePassivated
public void nodePassivated(NodePassivatedEvent event)
{
// Ignore everything but "pre" events for nodes in our region
if(!event.isPre()) return;
Fqn fqn = event.getFqn();
if(fqn.size() != FQN_SIZE) return;
if(!fqn.isChildOrEquals(cacheNode)) return;
StatefulBeanContext bean = null;
ClassLoader oldCl = Thread.currentThread().getContextClassLoader();
Boolean active = localActivity.get();
try
{
localActivity.set(Boolean.TRUE);
bean = (StatefulBeanContext) event.getData().get("bean");
if (bean != null)
{
ClassLoader cl = classloader.get();
if (cl != null)
{
Thread.currentThread().setContextClassLoader(cl);
}
if (!bean.getCanPassivate())
{
// Abort the eviction
throw new ContextInUseException("Cannot passivate bean " + fqn +
" -- it or one if its children is currently in use");
}
if(log.isTraceEnabled())
{
log.trace("nodePassivated(): send prePassivate event to bean at fqn: " +fqn);
}
bean.passivateAfterReplication();
++passivatedCount;
}
}
catch (NoSuchEJBException e)
{
// TODO is this still necessary? Don't think we
// should have orphaned proxies any more
if (bean instanceof ProxiedStatefulBeanContext)
{
// This is probably an orphaned proxy; double check and remove it
try
{
bean.getContainedIn();
// If that didn't fail, it's not an orphan
throw e;
}
catch (NoSuchEJBException n)
{
log.debug("nodePassivated(): removing orphaned proxy at " + fqn);
try
{
cache.removeNode(fqn);
}
catch (CacheException c)
{
log.error("nodePassivated(): could not remove orphaned proxy at " + fqn, c);
// Just fall through and let the eviction try
}
}
}
else
{
throw e;
}
}
finally
{
Thread.currentThread().setContextClassLoader(oldCl);
localActivity.set(active);
}
}
}
private static Option getLocalOnlyOption()
{
//try
//{
return LOCAL_ONLY_OPTION.clone();
//}
//catch (CloneNotSupportedException e)
//{
// throw new RuntimeException(e);
//}
}
private static Option getGravitateOption()
{
//try
//{
return GRAVITATE_OPTION.clone();
//}
//catch (CloneNotSupportedException e)
//{
// throw new RuntimeException(e);
//}
}
private class RemovalTimeoutTask extends Thread
{
public RemovalTimeoutTask(String name)
{
super(name);
}
public void run()
{
while (running)
{
try
{
Thread.sleep(removalTimeout * 1000);
}
catch (InterruptedException e)
{
running = false;
return;
}
try
{
long now = System.currentTimeMillis();
Iterator<Map.Entry<Object, Long>> it = beans.entrySet().iterator();
while (it.hasNext())
{
Map.Entry<Object, Long> entry = it.next();
long lastUsed = entry.getValue().longValue();
if (now - lastUsed >= removalTimeout * 1000)
{
remove(entry.getKey());
}
}
}
catch (Exception ex)
{
log.error("problem removing SFSB thread", ex);
}
}
}
}
}