/*
* JBoss, Home of Professional Open Source.
* Copyright 2008, 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.web.tomcat.service.session;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import org.apache.catalina.Container;
import org.apache.catalina.Engine;
import org.apache.catalina.Host;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Manager;
import org.apache.catalina.core.ContainerBase;
import org.apache.catalina.util.LifecycleSupport;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tomcat.util.modeler.Registry;
import org.jboss.metadata.web.jboss.ReplicationGranularity;
import org.jboss.metadata.web.jboss.ReplicationTrigger;
import org.jboss.metadata.web.jboss.SnapshotMode;
import org.jboss.mx.util.MBeanServerLocator;
import org.jboss.web.tomcat.service.session.distributedcache.spi.DistributedCacheManagerFactoryFactory;
import org.jboss.web.tomcat.service.session.distributedcache.spi.OutgoingDistributableSessionData;
import org.jboss.web.tomcat.service.session.distributedcache.spi.TomcatClusterConfig;
import org.jboss.web.tomcat.service.session.distributedcache.spi.TomcatClusterDistributedCacheManagerFactory;
/**
* A Tomcat <code>Cluster</code> implementation that uses a JBoss
* <code>TreeCache</code> to support intra-cluster session replication.
* <p>
* This class registers a <code>TreeCache</code> in JMX, making it
* available to other users who wish to replicate data within the cluster.
* </p>
*
* @author Brian Stansberry
* @version $Revision: 87304 $
*/
public class JBossCacheCluster
implements TomcatClusterConfig, JBossCacheClusterMBean, Lifecycle
{
// ------------------------------------------------------- Static Fields
protected static final String info = "JBossCacheCluster/2.1";
public static Log log = LogFactory.getLog(JBossCacheCluster.class);
public static final String DEFAULT_CACHE_CONFIG_PATH = "conf/cluster-cache.xml";
// ------------------------------------------------------- Instance Fields
/** Parent container of this cluster. */
private Container container = null;
/** Our JMX Server. */
private MBeanServer mserver = null;
/** Name under which we are registered in JMX */
private ObjectName objectName = null;
/** The factory we use to create our DistributedCacheManager */
private TomcatClusterDistributedCacheManagerFactory factory = null;
/** Are we started? */
private boolean started = false;
/** The lifecycle event support for this component. */
private LifecycleSupport lifecycle = new LifecycleSupport(this);
/** Name under which our TreeCache is registered in JMX */
private String pojoCacheObjectName = "jboss.cache:service=TomcatClusteringCache";
/** Name of the tree cache's JGroups channel */
private String clusterName = null;
/** File name, URL or String to use to configure JGroups. */
private String cacheConfigPath = null;
/**
* Implementation of Manager to instantiate when
* createManager() is called.
*/
private String managerClassName = JBossCacheManager.class.getName();
/** Does the Engine in which we are running use mod_jk? */
private boolean useJK = false;
/** Whether our Managers should use a local cache. */
private boolean useLocalCache = false;
/**
* Default replication trigger to assign to our
* Managers that haven't had this property set.
*/
private ReplicationTrigger defaultReplicationTrigger = null;
/**
* Default replication granularity to assign to our Managers
* that haven't had this property set.
*/
private ReplicationGranularity defaultReplicationGranularity = null;
/**
* JBossCacheManager's snapshot mode.
*/
private SnapshotMode snapshotMode = null;
/**
* JBossCacheManager's snapshot interval.
*/
private int snapshotInterval = 0;
/** Whether we use batch mode replication for field level granularity */
private boolean replicationFieldBatchMode = true;
// ---------------------------------------------------------- Constructors
/**
* Default constructor.
*/
public JBossCacheCluster()
{
super();
}
// ------------------------------------------------------------ Properties
/**
* Gets a String representation of the JMX <code>ObjectName</code> under
* which our <code>TreeCache</code> is registered.
* <p>
* If this property is not explicitly set, the <code>TreeCache</code> will
* be registered under
* @{@link Tomcat6.DEFAULT_CACHE_NAME the default name used in
* embedded JBoss/Tomcat}.
* </p>
*
* @jmx.managed-attribute
*/
public String getCacheObjectName()
{
return pojoCacheObjectName;
}
/**
* Sets the JMX <code>ObjectName</code> under which our
* <code>TreeCache</code> is registered, if already created, or under
* which it should be registered if this object creates it.
*
* @jmx.managed-attribute
*/
public void setCacheObjectName(String objectName)
{
this.pojoCacheObjectName = objectName;
}
/**
* Sets the name of the <code>TreeCache</code>'s JGroups channel.
* <p>
* This property is ignored if a <code>TreeCache</code> is already
* registered under the provided
* {@link #setCacheObjectName cache object name}.
* </p>
*
* @jmx.managed-attribute
*/
public void setClusterName(String clusterName)
{
this.clusterName = clusterName;
}
/**
* Gets the filesystem path, which can either be absolute or a path
* relative to <code>$CATALINA_BASE</code>, where a
* a JBossCache configuration file can be found.
*
* @return a path, either absolute or relative to
* <code>$CATALINA_BASE</code>. Will return
* <code>null</code> if no such path was configured.
*
* @jmx.managed-attribute
*/
public String getCacheConfigPath()
{
return cacheConfigPath;
}
/**
* Sets the filesystem path, which can either be absolute or a path
* relative to <code>$CATALINA_BASE</code>, where a
* a JBossCache configuration file can be found.
* <p>
* This property is ignored if a <code>TreeCache</code> is already
* registered under the provided
* {@link #setCacheObjectName cache object name}.
* </p>
*
* @param cacheConfigPath a path, absolute or relative to
* <code>$CATALINA_BASE</code>,
* pointing to a JBossCache configuration file.
*
* @jmx.managed-attribute
*/
public void setCacheConfigPath(String cacheConfigPath)
{
this.cacheConfigPath = cacheConfigPath;
}
/**
* Get the current Catalina MBean Server.
*
* @return the mbean server
*/
public MBeanServer getMBeanServer()
{
if (mserver == null)
{
mserver = Registry.getRegistry(null, null).getMBeanServer();
}
return mserver;
}
/**
* Gets the name of the implementation of Manager to instantiate when
* createManager() is called.
*
* @jmx.managed-attribute
*/
public String getManagerClassName()
{
return managerClassName;
}
/**
* Sets the name of the implementation of Manager to instantiate when
* createManager() is called.
* <p>
* This should be {@link JBossCacheManager} (the default) or a subclass
* of it.
* </p>
*
* @jmx.managed-attribute
*/
public void setManagerClassName(String managerClassName)
{
this.managerClassName = managerClassName;
}
public void registerManager(Manager arg0)
{
// TODO tie this into the managerClassName
}
public void removeManager(Manager arg0)
{
// TODO tie this into the managerClassName
}
/**
* Gets whether the <code>Engine</code> in which we are running
* uses <code>mod_jk</code>.
*
* @jmx.managed-attribute
*/
public boolean isUseJK()
{
return useJK;
}
/**
* Sets whether the <code>Engine</code> in which we are running
* uses <code>mod_jk</code>.
*
* @jmx.managed-attribute
*/
public void setUseJK(boolean useJK)
{
this.useJK = useJK;
}
/**
* Gets the <code>JBossCacheManager</code>'s <code>useLocalCache</code>
* property.
*
* @jmx.managed-attribute
*/
public boolean isUseLocalCache()
{
return useLocalCache;
}
/**
* Sets the <code>JBossCacheManager</code>'s <code>useLocalCache</code>
* property.
*
* @jmx.managed-attribute
*/
public void setUseLocalCache(boolean useLocalCache)
{
this.useLocalCache = useLocalCache;
}
/**
* Gets the default granularity of session data replicated across the
* cluster; i.e. whether the entire session should be replicated when
* replication is triggered, or only modified attributes.
* <p>
* The value of this property is applied to <code>Manager</code> instances
* that did not have an equivalent property explicitly set in
* <code>context.xml</code> or <code>server.xml</code>.
* </p>
*
* @jmx.managed-attribute
*/
public String getDefaultReplicationGranularity()
{
return defaultReplicationGranularity == null ? null : defaultReplicationGranularity.toString();
}
/**
* Sets the granularity of session data replicated across the cluster.
* Valid values are:
* <ul>
* <li>SESSION</li>
* <li>ATTRIBUTE</li>
* <li>FIELD</li>
* </ul>
* @jmx.managed-attribute
*/
public void setDefaultReplicationGranularity(
String defaultReplicationGranularity)
{
this.defaultReplicationGranularity = (defaultReplicationGranularity == null ? null : ReplicationGranularity.fromString(defaultReplicationGranularity.toUpperCase()));
}
/**
* Gets the type of operations on a <code>HttpSession</code> that
* trigger replication.
* <p>
* The value of this property is applied to <code>Manager</code> instances
* that did not have an equivalent property explicitly set in
* <code>context.xml</code> or <code>server.xml</code>.
* </p>
*
* @jmx.managed-attribute
*/
public String getDefaultReplicationTrigger()
{
return defaultReplicationTrigger == null ? null : defaultReplicationTrigger.toString();
}
/**
* Sets the type of operations on a <code>HttpSession</code> that
* trigger replication. Valid values are:
* <ul>
* <li>SET_AND_GET</li>
* <li>SET_AND_NON_PRIMITIVE_GET</li>
* <li>SET</li>
* </ul>
*
* @jmx.managed-attribute
*/
public void setDefaultReplicationTrigger(String defaultTrigger)
{
this.defaultReplicationTrigger = (defaultTrigger == null ? null : ReplicationTrigger.fromString(defaultTrigger.toUpperCase()));
}
/**
* Gets whether Managers should use batch mode replication.
* Only meaningful if replication granularity is set to <code>FIELD</code>.
*
* @jmx.managed-attribute
*/
public boolean getDefaultReplicationFieldBatchMode()
{
return replicationFieldBatchMode;
}
/**
* Sets whether Managers should use batch mode replication.
* Only meaningful if replication granularity is set to <code>FIELD</code>.
*
* @jmx.managed-attribute
*/
public void setDefaultReplicationFieldBatchMode(boolean replicationFieldBatchMode)
{
this.replicationFieldBatchMode = replicationFieldBatchMode;
}
/**
* Gets when sessions are replicated to the other nodes.
* The default value, "instant", synchronously replicates changes
* to the other nodes. In this case, the "SnapshotInterval" attribute
* is not used.
* The "interval" mode, in association with the "SnapshotInterval"
* attribute, indicates that Tomcat will only replicate modified
* sessions every "SnapshotInterval" miliseconds at most.
*
* @see #getSnapshotInterval()
*
* @jmx.managed-attribute
*/
public String getSnapshotMode()
{
return snapshotMode == null ? null : snapshotMode.toString();
}
/**
* Sets when sessions are replicated to the other nodes. Valid values are:
* <ul>
* <li>instant</li>
* <li>interval</li>
* </ul>
*
* @jmx.managed-attribute
*/
public void setSnapshotMode(String snapshotMode)
{
this.snapshotMode = (snapshotMode == null ? null : SnapshotMode.fromString(snapshotMode.toUpperCase()));
}
/**
* Gets how often session changes should be replicated to other nodes.
* Only relevant if property {@link #getSnapshotMode() snapshotMode} is
* set to <code>interval</code>.
*
* @return the number of milliseconds between session replications.
*
* @jmx.managed-attribute
*/
public int getSnapshotInterval()
{
return snapshotInterval;
}
/**
* Sets how often session changes should be replicated to other nodes.
*
* @param snapshotInterval the number of milliseconds between
* session replications.
* @jmx.managed-attribute
*/
public void setSnapshotInterval(int snapshotInterval)
{
this.snapshotInterval = snapshotInterval;
}
// ---------------------------------------------------------------- Cluster
/**
* Gets the name of the <code>TreeCache</code>'s JGroups channel.
*
* @see org.apache.catalina.Cluster#getClusterName()
*/
public String getClusterName()
{
return clusterName;
}
/* (non-javadoc)
* @see org.apache.catalina.Cluster#getContainer()
*/
public Container getContainer()
{
return container;
}
/* (non-javadoc)
* @see org.apache.catalina.Cluster#setContainer()
*/
public void setContainer(Container container)
{
this.container = container;
}
/**
* @see org.apache.catalina.Cluster#getInfo()
*
* @jmx.managed-attribute access="read-only"
*/
public String getInfo()
{
return info;
}
/**
* @see org.apache.catalina.Cluster#createManager(java.lang.String)
*/
@SuppressWarnings("unchecked")
public Manager createManager(String name)
{
if (log.isDebugEnabled())
log.debug("Creating ClusterManager for context " + name
+ " using class " + getManagerClassName());
Manager manager = null;
String mgrClass = getManagerClassName();
if (mgrClass != null && !JBossCacheManager.class.getName().equals(mgrClass))
{
try
{
manager = (Manager) getClass().getClassLoader().loadClass(mgrClass).newInstance();
}
catch (Exception x)
{
log.error("Unable to load class " + mgrClass + " for replication manager; using JBossCacheManager", x);
}
}
if (manager == null)
{
if (factory == null)
throw new IllegalStateException("PojoCache not initialized");
manager = new JBossCacheManager(factory);
}
manager.setDistributable(true);
if (manager instanceof JBossCacheManager)
{
configureManager((JBossCacheManager) manager);
}
return manager;
}
/**
* Does nothing; tracking the status of other members of the cluster is
* provided by the JGroups layer.
*
* @see org.apache.catalina.Cluster#backgroundProcess()
*/
public void backgroundProcess()
{
; // no-op
}
// --------------------------------------------- Deprecated Cluster Methods
/**
* Returns <code>null</code>; method is deprecated.
*
* @return <code>null</code>, always.
*
* @see org.apache.catalina.Cluster#getProtocol()
*/
public String getProtocol()
{
return null;
}
/**
* Does nothing; method is deprecated.
*
* @see org.apache.catalina.Cluster#setProtocol(java.lang.String)
*/
public void setProtocol(String protocol)
{
; // no-op
}
/**
* Does nothing; method is deprecated.
*
* @see org.apache.catalina.Cluster#startContext(java.lang.String)
*/
public void startContext(String contextPath) throws IOException
{
; // no-op
}
/**
* Does nothing; method is deprecated.
*
* @see org.apache.catalina.Cluster#installContext(java.lang.String, java.net.URL)
*/
public void installContext(String contextPath, URL war)
{
; // no-op
}
/**
* Does nothing; method is deprecated.
*
* @see org.apache.catalina.Cluster#stop(java.lang.String)
*/
public void stop(String contextPath) throws IOException
{
; // no-op
}
// --------------------------------------------------------- Public Methods
/**
* Sets the cluster-wide properties of a <code>Manager</code> to
* match those of this cluster. Does not override
* <code>Manager</code>-specific properties with cluster-wide defaults
* if the <code>Manager</code>-specfic properties have already been set.
*/
public void configureManager(JBossCacheManager<? extends OutgoingDistributableSessionData> manager)
{
manager.setSnapshotMode(snapshotMode);
manager.setSnapshotInterval(snapshotInterval);
manager.setUseJK(useJK);
// Only set replication attributes if they were not
// already set via a <Manager> element in an XML config file
if (manager.getReplicationGranularity() == null)
{
manager.setReplicationGranularity(defaultReplicationGranularity);
}
if (manager.getReplicationTrigger() == null)
{
manager.setReplicationTrigger(defaultReplicationTrigger);
}
if (manager.isReplicationFieldBatchMode() == null)
{
manager.setReplicationFieldBatchMode(replicationFieldBatchMode);
}
}
// --------------------------------------------------------------- Lifecyle
/**
* Finds or creates a {@link TreeCache}; if created, starts the
* cache and registers it with our JMX server.
*
* @see org.apache.catalina.Lifecycle#start()
*/
public void start() throws LifecycleException
{
if (started)
{
throw new LifecycleException("Cluster already started");
}
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, this);
try
{
// Tell the JBoss MBeanServerLocator utility
// that Tomcat's MBean server is 'jboss'
// JBAS-4623 Only do this if there isn't already a 'jboss' server
try
{
MBeanServerLocator.locateJBoss();
}
catch (IllegalStateException ise)
{
// This is the expected condition when running in standalone Tomcat
MBeanServerLocator.setJBoss(getMBeanServer());
}
// Initialize the tree cache
factory = DistributedCacheManagerFactoryFactory.getInstance().getTomcatClusterDistributedCacheManagerFactory(this);
registerMBeans();
factory.start();
started = true;
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent(AFTER_START_EVENT, this);
}
catch (LifecycleException e)
{
throw e;
}
catch (Exception e)
{
log.error("Unable to start cluster.", e);
throw new LifecycleException(e);
}
}
/**
* If this object created its own {@link TreeCache}, stops it
* and unregisters it with JMX.
*
* @see org.apache.catalina.Lifecycle#stop()
*/
public void stop() throws LifecycleException
{
if (!started)
{
throw new IllegalStateException("Cluster not started");
}
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, this);
try
{
factory.stop();
}
catch (Exception e)
{
throw new LifecycleException("Failed to stop DistributedCacheManagerFactory", e);
}
started = false;
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, this);
unregisterMBeans();
}
/* (non-javadoc)
* @see org.apache.catalina.Lifecycle#addLifecycleListener()
*/
public void addLifecycleListener(LifecycleListener listener)
{
lifecycle.addLifecycleListener(listener);
}
/* (non-javadoc)
* @see org.apache.catalina.Lifecycle#findLifecycleListeners()
*/
public LifecycleListener[] findLifecycleListeners()
{
return lifecycle.findLifecycleListeners();
}
/* (non-javadoc)
* @see org.apache.catalina.Lifecycle#removeLifecycleListener()
*/
public void removeLifecycleListener(LifecycleListener listener)
{
lifecycle.removeLifecycleListener(listener);
}
// -------------------------------------------------------- Private Methods
public File getCacheConfigFile() throws FileNotFoundException
{
boolean useDefault = (this.cacheConfigPath == null);
String path = (useDefault) ? DEFAULT_CACHE_CONFIG_PATH : cacheConfigPath;
// See if clusterProperties points to a file relative
// to $CATALINA_BASE
File file = new File(path);
if (!file.isAbsolute())
{
file = new File(System.getProperty("catalina.base"), path);
}
if (file.exists())
{
return file;
}
else
{
// User provided config was invalid; throw the exception
String msg = "No tree cache config file found at " +
file.getAbsolutePath();
log.error(msg);
throw new IllegalStateException(msg);
}
}
/**
* Registers this object and the tree cache (if we created it) with JMX.
*/
private void registerMBeans()
{
try
{
MBeanServer server = getMBeanServer();
String domain;
if (container instanceof ContainerBase)
{
domain = ((ContainerBase) container).getDomain();
}
else
{
domain = server.getDefaultDomain();
}
String name = ":type=Cluster";
if (container instanceof Host) {
name += ",host=" + container.getName();
}
else if (container instanceof Engine)
{
name += ",engine=" + container.getName();
}
ObjectName clusterName = new ObjectName(domain + name);
if (server.isRegistered(clusterName))
{
log.warn("MBean " + clusterName + " already registered");
}
else
{
this.objectName = clusterName;
server.registerMBean(this, objectName);
}
}
catch (Exception ex)
{
log.error(ex.getMessage(), ex);
}
}
/**
* Unregisters this object and the tree cache (if we created it) with JMX.
*/
private void unregisterMBeans()
{
if (mserver != null)
{
try
{
if (objectName != null) {
mserver.unregisterMBean(objectName);
}
}
catch (Exception e)
{
log.error(e);
}
}
}
}