org.jboss.web.tomcat.tc5.session.JBossCacheCluster.java Source code

Java tutorial

Introduction

Here is the source code for org.jboss.web.tomcat.tc5.session.JBossCacheCluster.java

Source

/*
 * 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.web.tomcat.tc5.session;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;

import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
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.jboss.cache.BatchModeTransactionManagerLookup;
import org.jboss.cache.PropertyConfigurator;
import org.jboss.cache.aop.PojoCache;
import org.jboss.cache.aop.PojoCacheMBean;
import org.jboss.mx.util.MBeanProxyExt;
import org.jboss.mx.util.MBeanServerLocator;
import org.jboss.web.tomcat.tc5.Tomcat5;

/**
 * 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: 57206 $
 */
public class JBossCacheCluster implements 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_CLUSTER_NAME = "Tomcat-Cluster";

    /** TreeCache's isolation level */
    public static final String DEFAULT_ISOLATION_LEVEL = "REPEATABLE_READ";

    /** TreeCache's cache mode */
    public static final String DEFAULT_CACHE_MODE = "REPL_ASYNC";

    /** TreeCache's lock aquisition timeout */
    public static final long DEFAULT_LOCK_TIMEOUT = 15000;

    /** TransactionManagerLookup implementation that the TreeCache should use. */
    public static final String DEFAULT_TM_LOOKUP = BatchModeTransactionManagerLookup.class.getName();

    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;

    /** Are we started? */
    private boolean started = false;

    /** The lifecycle event support for this component. */
    private LifecycleSupport lifecycle = new LifecycleSupport(this);

    /** Our tree cache */
    private PojoCacheMBean treeCache = null;

    /** Name under which our TreeCache is registered in JMX */
    private String treeCacheObjectName = Tomcat5.DEFAULT_CACHE_NAME;

    /** Did we create the tree cache, or was it already registered in JMX? */
    private boolean treeCacheLocal = false;

    /** 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 String defaultReplicationTrigger = null;

    /**
     * Default replication granularity to assign to our Managers
     * that haven't had this property set.
     */
    private String defaultReplicationGranularity = null;

    /**
     * JBossCacheManager's snapshot mode.
     */
    private String snapshotMode = null;

    /**
     * JBossCacheManager's snapshot interval.
     */
    private int snapshotInterval = 0;

    /** Whether we use batch mode replication for field level granularity */
    private boolean replicationFieldBatchMode;

    //  ----------------------------------------------------------  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 Tomcat5.DEFAULT_CACHE_NAME the default name used in 
     * embedded JBoss/Tomcat}.
     * </p>
     * 
     * @jmx.managed-attribute
     */
    public String getCacheObjectName() {
        return treeCacheObjectName;
    }

    /**
     * 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.treeCacheObjectName = 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;
    }

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

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

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

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

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

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

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

    /**
     * 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)
     */
    public Manager createManager(String name) {
        if (log.isDebugEnabled())
            log.debug("Creating ClusterManager for context " + name + " using class " + getManagerClassName());
        Manager manager = null;
        try {
            manager = (Manager) getClass().getClassLoader().loadClass(getManagerClassName()).newInstance();
        } catch (Exception x) {
            log.error("Unable to load class for replication manager", x);
            manager = new JBossCacheManager();
        } finally {
            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 manager) {
        manager.setSnapshotMode(snapshotMode);
        manager.setSnapshotInterval(snapshotInterval);
        manager.setUseJK(useJK);
        manager.setUseLocalCache(useLocalCache);
        manager.setCacheObjectNameString(treeCacheObjectName);

        // Only set replication attributes if they were not
        // already set via a <Manager> element in an XML config file

        if (manager.getReplicationGranularityString() == null) {
            manager.setReplicationGranularityString(defaultReplicationGranularity);
        }

        if (manager.getReplicationTriggerString() == null) {
            manager.setReplicationTriggerString(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'
            MBeanServerLocator.setJBoss(getMBeanServer());

            // Initialize the tree cache
            PojoCacheMBean cache = getTreeCache();

            if (treeCacheLocal) {
                cache.createService();
                cache.startService();
            }

            registerMBeans();

            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);

        if (treeCacheLocal) {
            treeCache.stopService();
            treeCache.destroyService();
        }

        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

    /**
     * Gets our TreeCache, either from a local reference or the JMX
     * server.  If one is not found, creates and configures it.
     */
    private PojoCacheMBean getTreeCache() throws Exception {
        if (treeCache == null) {

            MBeanServer server = getMBeanServer();
            ObjectName objName = new ObjectName(treeCacheObjectName);
            if (server.isRegistered(objName)) {
                // Get a proxy to the existing TreeCache
                treeCache = (PojoCacheMBean) MBeanProxyExt.create(PojoCacheMBean.class, objName);
            } else {
                // Create our own tree cache
                treeCache = new PojoCache();

                // See if there is an XML descriptor file to configure the cache
                InputStream configIS = getCacheConfigStream();

                if (configIS != null) {
                    PropertyConfigurator config = new PropertyConfigurator();
                    config.configure(treeCache, configIS);
                    try {
                        configIS.close();
                    } catch (IOException io) {
                        // ignore
                    }

                    if (clusterName != null) {
                        // Override the XML config with the name provided in
                        // server.xml.  Method setClusterName is specified in the
                        // Cluster interface, otherwise we would not do this
                        treeCache.setClusterName(clusterName);
                    }
                } else {
                    // User did not try to configure the cache.
                    // Configure it using defaults.  Only exception
                    // is the clusterName, which user can specify in server.xml.
                    String channelName = (clusterName == null) ? DEFAULT_CLUSTER_NAME : clusterName;
                    treeCache.setClusterName(channelName);
                    treeCache.setIsolationLevel(DEFAULT_ISOLATION_LEVEL);
                    treeCache.setCacheMode(DEFAULT_CACHE_MODE);
                    treeCache.setLockAcquisitionTimeout(DEFAULT_LOCK_TIMEOUT);
                    treeCache.setTransactionManagerLookupClass(DEFAULT_TM_LOOKUP);
                }

                treeCacheLocal = true;
            }
        }
        return treeCache;
    }

    private InputStream getCacheConfigStream() 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);
        }

        try {
            return new FileInputStream(file);
        } catch (FileNotFoundException fnf) {
            if (useDefault) {
                // Not a problem, just means user did not try to
                // configure the cache.  Return null and let the cache
                // be configured from defaults.
                return null;
            } else {
                // User provided config was invalid; throw the exception
                log.error("No tree cache config file found at " + file.getAbsolutePath());
                throw fnf;
            }
        }
    }

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

            if (treeCacheLocal) {
                // Register the treeCache
                ObjectName treeCacheName = new ObjectName(treeCacheObjectName);
                server.registerMBean(getTreeCache(), treeCacheName);
            }

        } 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);
                }
                if (treeCacheLocal) {
                    mserver.unregisterMBean(new ObjectName(treeCacheObjectName));
                }
            } catch (Exception e) {
                log.error(e);
            }
        }
    }

    /**
     * Get the current Catalina MBean Server.
     * 
     * @return
     * @throws Exception
     */
    private MBeanServer getMBeanServer() throws Exception {
        if (mserver == null) {
            ArrayList servers = MBeanServerFactory.findMBeanServer(null);
            if (servers.size() > 0) {
                mserver = (MBeanServer) servers.get(0);
            } else {
                mserver = MBeanServerFactory.createMBeanServer();
            }
        }
        return mserver;
    }

}