/* * 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); } } } }