From: fhanik
+ * If this application is successfully installed locally,
+ * a ContainerEvent of type
+ * Title: Description: Copyright: Copyright (c) 2005 Company: RuleSet for processing the contents of a
+ * Cluster definition element. Add the set of Rule instances defined in this RuleSet to the
+ * specified
+ * A farm war deployer is a class that is able to deploy/undeploy web
+ * applications in WAR form within the cluster.
+ *
+ * If this application is successfully installed locally, a ContainerEvent
+ * of type
+ * The WarWatcher watches the deployDir for changes made to the
+ * directory (adding new WAR files->deploy or remove WAR files->undeploy) And
+ * notifies a listener of the changes made
+ * This package contains code for Clustering, the base class
+of a Cluster is The only Cluster protocol currently implemented is a JavaGroups based
+ * After this method executes, and if the object implements
+ *
+ * After this method executes, and if the object implements
+ *
+ * IMPLEMENTATION NOTE : The reference to the owning Manager is not
+ * restored by this method, and must be set explicitly.
+ *
+ * @param stream
+ * The input stream to read from
+ *
+ * @exception ClassNotFoundException
+ * if an unknown class is specified
+ * @exception IOException
+ * if an input/output error occurs
+ */
+ private void readObject(ObjectInput stream) throws ClassNotFoundException, IOException {
+
+ // Deserialize the scalar instance variables (except Manager)
+ authType = null; // Transient only
+ creationTime = ( (Long) stream.readObject()).longValue();
+ lastAccessedTime = ( (Long) stream.readObject()).longValue();
+ maxInactiveInterval = ( (Integer) stream.readObject()).intValue();
+ isNew = ( (Boolean) stream.readObject()).booleanValue();
+ isValid = ( (Boolean) stream.readObject()).booleanValue();
+ thisAccessedTime = ( (Long) stream.readObject()).longValue();
+ version = ( (Long) stream.readObject()).longValue();
+ boolean hasPrincipal = stream.readBoolean();
+ principal = null;
+ if (hasPrincipal) {
+ principal = SerializablePrincipal.readPrincipal(stream,getManager().getContainer().getRealm());
+ }
+
+ // setId((String) stream.readObject());
+ id = (String) stream.readObject();
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaSession.readSession", id));
+
+ // Deserialize the attribute count and attribute values
+ if (attributes == null) attributes = new Hashtable();
+ int n = ( (Integer) stream.readObject()).intValue();
+ boolean isValidSave = isValid;
+ isValid = true;
+ for (int i = 0; i < n; i++) {
+ String name = (String) stream.readObject();
+ Object value = (Object) stream.readObject();
+ if ( (value instanceof String) && (value.equals(NOT_SERIALIZED)))
+ continue;
+ attributes.put(name, value);
+ }
+ isValid = isValidSave;
+
+ if (listeners == null) {
+ listeners = new ArrayList();
+ }
+
+ if (notes == null) {
+ notes = new Hashtable();
+ }
+ }
+
+ public synchronized void writeExternal(ObjectOutput out ) throws java.io.IOException {
+ writeObject(out);
+ }
+
+
+ /**
+ * Write a serialized version of this session object to the specified object
+ * output stream.
+ *
+ * IMPLEMENTATION NOTE : The owning Manager will not be stored in the
+ * serialized representation of this Session. After calling
+ *
+ * IMPLEMENTATION NOTE : Any attribute that is not Serializable will
+ * be unbound from the session, with appropriate actions if it implements
+ * HttpSessionBindingListener. If you do not want any such attributes, be
+ * sure the Implementation of a Valve that logs interesting contents from the
+ * specified Request (before processing) and the corresponding Response
+ * (after processing). It is especially useful in debugging problems
+ * related to headers and cookies. This Valve may be attached to any Container, depending on the granularity
+ * of the logging you wish to perform. primaryIndicator=true, then the request attribute org.apache.catalina.ha.tcp.isPrimarySession.
+ * is set true, when request processing is at sessions primary node.
+ * INSTALL_EVENT will be sent to all registered listeners,
+ * with the newly created Context as an argument.
+ *
+ * @param contextPath The context path to which this application should
+ * be installed (must be unique)
+ * @param war A URL of type "jar:" that points to a WAR file, or type
+ * "file:" that points to an unpacked directory structure containing
+ * the web application to be installed
+ *
+ * @exception IllegalArgumentException if the specified context path
+ * is malformed (it must be "" or start with a slash)
+ * @exception IllegalStateException if the specified context path
+ * is already attached to an existing web application
+ * @exception IOException if an input/output error was encountered
+ * during installation
+ */
+ public void install(String contextPath, URL war) throws IOException;
+
+ /**
+ * Remove an existing web application, attached to the specified context
+ * path. If this application is successfully removed, a
+ * ContainerEvent of type REMOVE_EVENT will be sent to all
+ * registered listeners, with the removed Context as
+ * an argument. Deletes the web application war file and/or directory
+ * if they exist in the Host's appBase.
+ *
+ * @param contextPath The context path of the application to be removed
+ * @param undeploy boolean flag to remove web application from server
+ *
+ * @exception IllegalArgumentException if the specified context path
+ * is malformed (it must be "" or start with a slash)
+ * @exception IllegalArgumentException if the specified context path does
+ * not identify a currently installed web application
+ * @exception IOException if an input/output error occurs during
+ * removal
+ */
+ public void remove(String contextPath, boolean undeploy) throws IOException;
+
+ /**
+ * call from container Background Process
+ */
+ public void backgroundProcess();
+
+ /**
+ * Returns the cluster the cluster deployer is associated with
+ * @return CatalinaCluster
+ */
+ public CatalinaCluster getCluster();
+
+ /**
+ * Associates the cluster deployer with a cluster
+ * @param cluster CatalinaCluster
+ */
+ public void setCluster(CatalinaCluster cluster);
+
+}
diff --git a/java/org/apache/catalina/ha/ClusterListener.java b/java/org/apache/catalina/ha/ClusterListener.java
new file mode 100644
index 000000000..6d7aa6087
--- /dev/null
+++ b/java/org/apache/catalina/ha/ClusterListener.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha;
+
+
+
+
+import org.apache.catalina.tribes.ChannelListener;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.util.StringManager;
+import java.io.Serializable;
+import org.apache.catalina.tribes.Member;
+
+
+/**
+ * Receive SessionID cluster change from other backup node after primary session
+ * node is failed.
+ *
+ * @author Peter Rossbach
+ * @author Filip Hanik
+ * @version $Revision: 378258 $ $Date: 2006-02-16 08:42:35 -0600 (Thu, 16 Feb 2006) $
+ */
+public abstract class ClusterListener implements ChannelListener {
+
+ public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ClusterListener.class);
+
+
+ //--Instance Variables--------------------------------------
+
+ /**
+ * The string manager for this package.
+ */
+ protected StringManager sm = StringManager.getManager(Constants.Package);
+
+ protected CatalinaCluster cluster = null;
+
+ //--Constructor---------------------------------------------
+
+ public ClusterListener() {
+ }
+
+ //--Instance Getters/Setters--------------------------------
+
+ public CatalinaCluster getCluster() {
+ return cluster;
+ }
+
+ public void setCluster(CatalinaCluster cluster) {
+ if (log.isDebugEnabled()) {
+ if (cluster != null)
+ log.debug("add ClusterListener " + this.toString() + " to cluster" + cluster);
+ else
+ log.debug("remove ClusterListener " + this.toString() + " from cluster");
+ }
+ this.cluster = cluster;
+ }
+
+ public boolean equals(Object listener) {
+ return super.equals(listener);
+ }
+
+ public int hashCode() {
+ return super.hashCode();
+ }
+
+ //--Logic---------------------------------------------------
+
+ public final void messageReceived(Serializable msg, Member member) {
+ if ( msg instanceof ClusterMessage ) messageReceived((ClusterMessage)msg);
+ }
+ public final boolean accept(Serializable msg, Member member) {
+ if ( msg instanceof ClusterMessage ) return true;
+ return false;
+ }
+
+
+
+ /**
+ * Callback from the cluster, when a message is received, The cluster will
+ * broadcast it invoking the messageReceived on the receiver.
+ *
+ * @param msg
+ * ClusterMessage - the message received from the cluster
+ */
+ public abstract void messageReceived(ClusterMessage msg) ;
+
+
+ /**
+ * Accept only SessionIDMessages
+ *
+ * @param msg
+ * ClusterMessage
+ * @return boolean - returns true to indicate that messageReceived should be
+ * invoked. If false is returned, the messageReceived method will
+ * not be invoked.
+ */
+ public abstract boolean accept(ClusterMessage msg) ;
+
+}
diff --git a/java/org/apache/catalina/ha/ClusterManager.java b/java/org/apache/catalina/ha/ClusterManager.java
new file mode 100644
index 000000000..67e662d79
--- /dev/null
+++ b/java/org/apache/catalina/ha/ClusterManager.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha;
+
+
+import org.apache.catalina.Manager;
+import java.io.IOException;
+import org.apache.catalina.tribes.io.ReplicationStream;
+
+
+/**
+ * The common interface used by all cluster manager.
+ * This is so that we can have a more pluggable way
+ * of swapping session managers for different algorithms.
+ *
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ */
+public interface ClusterManager extends Manager {
+
+ /**
+ * A message was received from another node, this
+ * is the callback method to implement if you are interested in
+ * receiving replication messages.
+ * @param msg - the message received.
+ */
+ public void messageDataReceived(ClusterMessage msg);
+
+ /**
+ * When the request has been completed, the replication valve
+ * will notify the manager, and the manager will decide whether
+ * any replication is needed or not.
+ * If there is a need for replication, the manager will
+ * create a session message and that will be replicated.
+ * The cluster determines where it gets sent.
+ * @param sessionId - the sessionId that just completed.
+ * @return a SessionMessage to be sent.
+ */
+ public ClusterMessage requestCompleted(String sessionId);
+
+ /**
+ * When the manager expires session not tied to a request.
+ * The cluster will periodically ask for a list of sessions
+ * that should expire and that should be sent across the wire.
+ * @return String[] The invalidated sessions
+ */
+ public String[] getInvalidatedSessions();
+
+ /**
+ * Return the name of the manager, at host /context name and at engine hostname+/context.
+ * @return String
+ * @since 5.5.10
+ */
+ public String getName();
+
+ /**
+ * Set the name of the manager, at host /context name and at engine hostname+/context
+ * @param name
+ * @since 5.5.10
+ */
+ public void setName(String name);
+
+ public CatalinaCluster getCluster();
+
+ public void setCluster(CatalinaCluster cluster);
+
+ /**
+ * @return Manager send only to same cluster domain.
+ * @since 5.5.10
+ */
+ public boolean isSendClusterDomainOnly();
+
+ /**
+ * @param sendClusterDomainOnly Flag value.
+ * @since 5.5.10
+ */
+ public void setSendClusterDomainOnly(boolean sendClusterDomainOnly);
+
+ /**
+ * @param mode The mode
+ * @since 5.5.10
+ */
+ public void setDefaultMode(boolean mode);
+
+ /**
+ * @since 5.5.10
+ */
+ public boolean isDefaultMode();
+
+ public ReplicationStream getReplicationStream(byte[] data) throws IOException;
+
+ public ReplicationStream getReplicationStream(byte[] data, int offset, int length) throws IOException;
+
+ public boolean isNotifyListenersOnReplication();
+
+}
diff --git a/java/org/apache/catalina/ha/ClusterMessage.java b/java/org/apache/catalina/ha/ClusterMessage.java
new file mode 100644
index 000000000..d33f9e84c
--- /dev/null
+++ b/java/org/apache/catalina/ha/ClusterMessage.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999,2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha;
+
+import java.io.Serializable;
+import org.apache.catalina.tribes.Member;
+
+
+/**
+ * @author Filip Hanik
+ *
+ */
+public interface ClusterMessage extends Serializable {
+ public Member getAddress();
+ public void setAddress(Member member);
+ public String getUniqueId();
+ public void setUniqueId(String id);
+ public long getTimestamp();
+ public void setTimestamp(long timestamp);
+}
diff --git a/java/org/apache/catalina/ha/ClusterMessageBase.java b/java/org/apache/catalina/ha/ClusterMessageBase.java
new file mode 100644
index 000000000..63be81109
--- /dev/null
+++ b/java/org/apache/catalina/ha/ClusterMessageBase.java
@@ -0,0 +1,61 @@
+package org.apache.catalina.ha;
+
+import org.apache.catalina.tribes.Member;
+
+
+/**
+ * RuleSet with the default
+ * matching pattern prefix.
+ */
+ public ClusterRuleSet() {
+
+ this("");
+
+ }
+
+
+ /**
+ * Construct an instance of this RuleSet with the specified
+ * matching pattern prefix.
+ *
+ * @param prefix Prefix for matching pattern rules (including the
+ * trailing slash character)
+ */
+ public ClusterRuleSet(String prefix) {
+ super();
+ this.namespaceURI = null;
+ this.prefix = prefix;
+ }
+
+
+ // --------------------------------------------------------- Public Methods
+
+
+ /**
+ * Digester instance, associating them with
+ * our namespace URI (if any). This method should only be called
+ * by a Digester instance.org.apache.catalina.ha
+ * package.
+ *
+ * @author Bip Thelin
+ * @version $Revision: 302726 $, $Date: 2004-02-27 08:59:07 -0600 (Fri, 27 Feb 2004) $
+ */
+
+public final class Constants {
+ public static final String Package = "org.apache.catalina.ha";
+}
diff --git a/java/org/apache/catalina/ha/LocalStrings.properties b/java/org/apache/catalina/ha/LocalStrings.properties
new file mode 100644
index 000000000..0dafa4672
--- /dev/null
+++ b/java/org/apache/catalina/ha/LocalStrings.properties
@@ -0,0 +1 @@
+cluster.mbean.register.already=MBean {0} already registered!
diff --git a/java/org/apache/catalina/ha/context/ReplicatedContext.java b/java/org/apache/catalina/ha/context/ReplicatedContext.java
new file mode 100644
index 000000000..680fae0f9
--- /dev/null
+++ b/java/org/apache/catalina/ha/context/ReplicatedContext.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.context;
+
+import org.apache.catalina.core.StandardContext;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.tribes.tipis.ReplicatedMap;
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.Loader;
+import org.apache.catalina.core.ApplicationContext;
+import org.apache.catalina.Globals;
+import javax.servlet.ServletContext;
+import java.util.HashMap;
+import org.apache.catalina.tribes.tipis.LazyReplicatedMap;
+
+/**
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class ReplicatedContext extends StandardContext {
+ private int mapSendOptions = Channel.SEND_OPTIONS_DEFAULT;
+ public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( ReplicatedContext.class );
+
+ protected static long DEFAULT_REPL_TIMEOUT = 15000;//15 seconds
+
+
+
+ public synchronized void start() throws LifecycleException {
+ if ( this.started ) return;
+ try {
+ CatalinaCluster catclust = (CatalinaCluster)this.getCluster();
+ if (this.context == null) this.context = new ReplApplContext(this.getBasePath(), this);
+ if ( catclust != null ) {
+ ReplicatedMap map = new ReplicatedMap(this,catclust.getChannel(),DEFAULT_REPL_TIMEOUT,
+ getName(),getClassLoaders());
+ map.setChannelSendOptions(mapSendOptions);
+ ((ReplApplContext)this.context).setAttributeMap(map);
+ if (getAltDDName() != null) context.setAttribute(Globals.ALT_DD_ATTR, getAltDDName());
+ }
+ super.start();
+ } catch ( Exception x ) {
+ log.error("Unable to start ReplicatedContext",x);
+ throw new LifecycleException("Failed to start ReplicatedContext",x);
+ }
+ }
+
+ public synchronized void stop() throws LifecycleException
+ {
+ ReplicatedMap map = (ReplicatedMap)((ReplApplContext)this.context).getAttributeMap();
+ if ( map!=null ) {
+ map.breakdown();
+ }
+ if ( !this.started ) return;
+ try {
+ } catch ( Exception x ){
+ log.error("Unable to stop ReplicatedContext",x);
+ throw new LifecycleException("Failed to stop ReplicatedContext",x);
+ } finally {
+ super.stop();
+ }
+
+ }
+
+
+ public void setMapSendOptions(int mapSendOptions) {
+ this.mapSendOptions = mapSendOptions;
+ }
+
+ public int getMapSendOptions() {
+ return mapSendOptions;
+ }
+
+ public ClassLoader[] getClassLoaders() {
+ Loader loader = null;
+ ClassLoader classLoader = null;
+ loader = this.getLoader();
+ if (loader != null) classLoader = loader.getClassLoader();
+ if ( classLoader == null ) classLoader = Thread.currentThread().getContextClassLoader();
+ if ( classLoader == Thread.currentThread().getContextClassLoader() ) {
+ return new ClassLoader[] {classLoader};
+ } else {
+ return new ClassLoader[] {classLoader,Thread.currentThread().getContextClassLoader()};
+ }
+ }
+
+ public ServletContext getServletContext() {
+ return ((ReplApplContext)context).getFacade();
+
+ }
+
+
+ protected static class ReplApplContext extends ApplicationContext {
+ public ReplApplContext(String basePath, StandardContext context) {
+ super(basePath,context);
+ }
+
+ protected ServletContext getFacade() {
+ return super.getFacade();
+ }
+
+ public HashMap getAttributeMap() {
+ return (HashMap)this.attributes;
+ }
+ public void setAttributeMap(HashMap map) {
+ this.attributes = map;
+ }
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/ha/deploy/FarmWarDeployer.java b/java/org/apache/catalina/ha/deploy/FarmWarDeployer.java
new file mode 100644
index 000000000..98b6b4415
--- /dev/null
+++ b/java/org/apache/catalina/ha/deploy/FarmWarDeployer.java
@@ -0,0 +1,741 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.deploy;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.util.HashMap;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.Engine;
+import org.apache.catalina.Host;
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.ha.ClusterDeployer;
+import org.apache.catalina.ha.ClusterListener;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.tomcat.util.modeler.Registry;
+
+
+/**
+ *
+ *
+ * Currently we only support deployment of WAR files since they are easier to
+ * send across the wire.
+ *
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ * @version $Revision: 390639 $
+ */
+public class FarmWarDeployer extends ClusterListener implements ClusterDeployer, FileChangeListener {
+ /*--Static Variables----------------------------------------*/
+ public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory
+ .getLog(FarmWarDeployer.class);
+ /**
+ * The descriptive information about this implementation.
+ */
+ private static final String info = "FarmWarDeployer/1.2";
+
+ /*--Instance Variables--------------------------------------*/
+ protected CatalinaCluster cluster = null;
+
+ protected boolean started = false; //default 5 seconds
+
+ protected HashMap fileFactories = new HashMap();
+
+ protected String deployDir;
+
+ protected String tempDir;
+
+ protected String watchDir;
+
+ protected boolean watchEnabled = false;
+
+ protected WarWatcher watcher = null;
+
+ /**
+ * Iteration count for background processing.
+ */
+ private int count = 0;
+
+ /**
+ * Frequency of the Farm watchDir check. Cluster wide deployment will be
+ * done once for the specified amount of backgrondProcess calls (ie, the
+ * lower the amount, the most often the checks will occur).
+ */
+ protected int processDeployFrequency = 2;
+
+ /**
+ * Path where context descriptors should be deployed.
+ */
+ protected File configBase = null;
+
+ /**
+ * The associated host.
+ */
+ protected Host host = null;
+
+ /**
+ * The host appBase.
+ */
+ protected File appBase = null;
+
+ /**
+ * MBean server.
+ */
+ protected MBeanServer mBeanServer = null;
+
+ /**
+ * The associated deployer ObjectName.
+ */
+ protected ObjectName oname = null;
+
+ /*--Constructor---------------------------------------------*/
+ public FarmWarDeployer() {
+ }
+
+ /**
+ * Return descriptive information about this deployer implementation and the
+ * corresponding version number, in the format
+ * <description>/<version>.
+ */
+ public String getInfo() {
+
+ return (info);
+
+ }
+
+ /*--Logic---------------------------------------------------*/
+ public void start() throws Exception {
+ if (started)
+ return;
+ getCluster().addClusterListener(this);
+ if (watchEnabled) {
+ watcher = new WarWatcher(this, new File(getWatchDir()));
+ if (log.isInfoEnabled())
+ log.info("Cluster deployment is watching " + getWatchDir()
+ + " for changes.");
+ }
+
+ // Check to correct engine and host setup
+ host = (Host) getCluster().getContainer();
+ Engine engine = (Engine) host.getParent();
+ try {
+ oname = new ObjectName(engine.getName() + ":type=Deployer,host="
+ + host.getName());
+ } catch (Exception e) {
+ log.error("Can't construct MBean object name" + e);
+ }
+ configBase = new File(System.getProperty("catalina.base"), "conf");
+ if (engine != null) {
+ configBase = new File(configBase, engine.getName());
+ }
+ if (host != null) {
+ configBase = new File(configBase, host.getName());
+ }
+
+ // Retrieve the MBean server
+ mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
+
+ started = true;
+ count = 0;
+ if (log.isInfoEnabled())
+ log.info("Cluster FarmWarDeployer started.");
+ }
+
+ /*
+ * stop cluster wide deployments
+ *
+ * @see org.apache.catalina.cluster.ClusterDeployer#stop()
+ */
+ public void stop() throws LifecycleException {
+ started = false;
+ getCluster().removeClusterListener(this);
+ count = 0;
+ if (watcher != null) {
+ watcher.clear();
+ watcher = null;
+
+ }
+ if (log.isInfoEnabled())
+ log.info("Cluster FarmWarDeployer stopped.");
+ }
+
+ public void cleanDeployDir() {
+ throw new java.lang.UnsupportedOperationException(
+ "Method cleanDeployDir() not yet implemented.");
+ }
+
+ /**
+ * Callback from the cluster, when a message is received, The cluster will
+ * broadcast it invoking the messageReceived on the receiver.
+ *
+ * @param msg
+ * ClusterMessage - the message received from the cluster
+ */
+ public void messageReceived(ClusterMessage msg) {
+ try {
+ if (msg instanceof FileMessage && msg != null) {
+ FileMessage fmsg = (FileMessage) msg;
+ if (log.isDebugEnabled())
+ log.debug("receive cluster deployment [ path: "
+ + fmsg.getContextPath() + " war: "
+ + fmsg.getFileName() + " ]");
+ FileMessageFactory factory = getFactory(fmsg);
+ // TODO correct second try after app is in service!
+ if (factory.writeMessage(fmsg)) {
+ //last message received war file is completed
+ String name = factory.getFile().getName();
+ if (!name.endsWith(".war"))
+ name = name + ".war";
+ File deployable = new File(getDeployDir(), name);
+ try {
+ String path = fmsg.getContextPath();
+ if (!isServiced(path)) {
+ addServiced(path);
+ try {
+ remove(path);
+ factory.getFile().renameTo(deployable);
+ check(path);
+ } finally {
+ removeServiced(path);
+ }
+ if (log.isDebugEnabled())
+ log.debug("deployment from " + path
+ + " finished.");
+ } else
+ log.error("Application " + path
+ + " in used. touch war file " + name
+ + " again!");
+ } catch (Exception ex) {
+ log.error(ex);
+ } finally {
+ removeFactory(fmsg);
+ }
+ }
+ } else if (msg instanceof UndeployMessage && msg != null) {
+ try {
+ UndeployMessage umsg = (UndeployMessage) msg;
+ String path = umsg.getContextPath();
+ if (log.isDebugEnabled())
+ log.debug("receive cluster undeployment from " + path);
+ if (!isServiced(path)) {
+ addServiced(path);
+ try {
+ remove(path);
+ } finally {
+ removeServiced(path);
+ }
+ if (log.isDebugEnabled())
+ log.debug("undeployment from " + path
+ + " finished.");
+ } else
+ log.error("Application "
+ + path
+ + " in used. Sorry not remove from backup cluster nodes!");
+ } catch (Exception ex) {
+ log.error(ex);
+ }
+ }
+ } catch (java.io.IOException x) {
+ log.error("Unable to read farm deploy file message.", x);
+ }
+ }
+
+ /**
+ * create factory for all transported war files
+ *
+ * @param msg
+ * @return Factory for all app message (war files)
+ * @throws java.io.FileNotFoundException
+ * @throws java.io.IOException
+ */
+ public synchronized FileMessageFactory getFactory(FileMessage msg)
+ throws java.io.FileNotFoundException, java.io.IOException {
+ File tmpFile = new File(msg.getFileName());
+ File writeToFile = new File(getTempDir(), tmpFile.getName());
+ FileMessageFactory factory = (FileMessageFactory) fileFactories.get(msg
+ .getFileName());
+ if (factory == null) {
+ factory = FileMessageFactory.getInstance(writeToFile, true);
+ fileFactories.put(msg.getFileName(), factory);
+ }
+ return factory;
+ }
+
+ /**
+ * Remove file (war) from messages)
+ *
+ * @param msg
+ */
+ public void removeFactory(FileMessage msg) {
+ fileFactories.remove(msg.getFileName());
+ }
+
+ /**
+ * Before the cluster invokes messageReceived the cluster will ask the
+ * receiver to accept or decline the message, In the future, when messages
+ * get big, the accept method will only take a message header
+ *
+ * @param msg
+ * ClusterMessage
+ * @return boolean - returns true to indicate that messageReceived should be
+ * invoked. If false is returned, the messageReceived method will
+ * not be invoked.
+ */
+ public boolean accept(ClusterMessage msg) {
+ return (msg instanceof FileMessage) || (msg instanceof UndeployMessage);
+ }
+
+ /**
+ * Install a new web application, whose web application archive is at the
+ * specified URL, into this container and all the other members of the
+ * cluster with the specified context path. A context path of "" (the empty
+ * string) should be used for the root application for this container.
+ * Otherwise, the context path must start with a slash.
+ * INSTALL_EVENT will be sent to all registered
+ * listeners, with the newly created Context as an argument.
+ *
+ * @param contextPath
+ * The context path to which this application should be installed
+ * (must be unique)
+ * @param war
+ * A URL of type "jar:" that points to a WAR file, or type
+ * "file:" that points to an unpacked directory structure
+ * containing the web application to be installed
+ *
+ * @exception IllegalArgumentException
+ * if the specified context path is malformed (it must be ""
+ * or start with a slash)
+ * @exception IllegalStateException
+ * if the specified context path is already attached to an
+ * existing web application
+ * @exception IOException
+ * if an input/output error was encountered during
+ * installation
+ */
+ public void install(String contextPath, URL war) throws IOException {
+ Member[] members = getCluster().getMembers();
+ Member localMember = getCluster().getLocalMember();
+ FileMessageFactory factory = FileMessageFactory.getInstance(new File(
+ war.getFile()), false);
+ FileMessage msg = new FileMessage(localMember, war.getFile(),
+ contextPath);
+ if(log.isDebugEnabled())
+ log.debug("Send cluster war deployment [ path:"
+ + contextPath + " war: " + war + " ] started.");
+ msg = factory.readMessage(msg);
+ while (msg != null) {
+ for (int i = 0; i < members.length; i++) {
+ if (log.isDebugEnabled())
+ log.debug("Send cluster war fragment [ path: "
+ + contextPath + " war: " + war + " to: " + members[i] + " ]");
+ getCluster().send(msg, members[i]);
+ }
+ msg = factory.readMessage(msg);
+ }
+ if(log.isDebugEnabled())
+ log.debug("Send cluster war deployment [ path: "
+ + contextPath + " war: " + war + " ] finished.");
+ }
+
+ /**
+ * Remove an existing web application, attached to the specified context
+ * path. If this application is successfully removed, a ContainerEvent of
+ * type REMOVE_EVENT will be sent to all registered
+ * listeners, with the removed Context as an argument.
+ * Deletes the web application war file and/or directory if they exist in
+ * the Host's appBase.
+ *
+ * @param contextPath
+ * The context path of the application to be removed
+ * @param undeploy
+ * boolean flag to remove web application from server
+ *
+ * @exception IllegalArgumentException
+ * if the specified context path is malformed (it must be ""
+ * or start with a slash)
+ * @exception IllegalArgumentException
+ * if the specified context path does not identify a
+ * currently installed web application
+ * @exception IOException
+ * if an input/output error occurs during removal
+ */
+ public void remove(String contextPath, boolean undeploy) throws IOException {
+ if (log.isInfoEnabled())
+ log.info("Cluster wide remove of web app " + contextPath);
+ Member localMember = getCluster().getLocalMember();
+ UndeployMessage msg = new UndeployMessage(localMember, System
+ .currentTimeMillis(), "Undeploy:" + contextPath + ":"
+ + System.currentTimeMillis(), contextPath, undeploy);
+ if (log.isDebugEnabled())
+ log.debug("Send cluster wide undeployment from "
+ + contextPath );
+ cluster.send(msg);
+ // remove locally
+ if (undeploy) {
+ try {
+ if (!isServiced(contextPath)) {
+ addServiced(contextPath);
+ try {
+ remove(contextPath);
+ } finally {
+ removeServiced(contextPath);
+ }
+ } else
+ log.error("Local remove from " + contextPath
+ + "failed, other manager has app in service!");
+
+ } catch (Exception ex) {
+ log.error("local remove from " + contextPath + " failed", ex);
+ }
+ }
+
+ }
+
+ /*
+ * Modifcation from watchDir war detected!
+ *
+ * @see org.apache.catalina.cluster.deploy.FileChangeListener#fileModified(java.io.File)
+ */
+ public void fileModified(File newWar) {
+ try {
+ File deployWar = new File(getDeployDir(), newWar.getName());
+ copy(newWar, deployWar);
+ String contextName = getContextName(deployWar);
+ if (log.isInfoEnabled())
+ log.info("Installing webapp[" + contextName + "] from "
+ + deployWar.getAbsolutePath());
+ try {
+ remove(contextName, false);
+ } catch (Exception x) {
+ log.error("No removal", x);
+ }
+ install(contextName, deployWar.toURL());
+ } catch (Exception x) {
+ log.error("Unable to install WAR file", x);
+ }
+ }
+
+ /*
+ * War remvoe from watchDir
+ *
+ * @see org.apache.catalina.cluster.deploy.FileChangeListener#fileRemoved(java.io.File)
+ */
+ public void fileRemoved(File removeWar) {
+ try {
+ String contextName = getContextName(removeWar);
+ if (log.isInfoEnabled())
+ log.info("Removing webapp[" + contextName + "]");
+ remove(contextName, true);
+ } catch (Exception x) {
+ log.error("Unable to remove WAR file", x);
+ }
+ }
+
+ /**
+ * Create a context path from war
+ * @param war War filename
+ * @return '/filename' or if war name is ROOT.war context name is empty string ''
+ */
+ protected String getContextName(File war) {
+ String contextName = "/"
+ + war.getName().substring(0,
+ war.getName().lastIndexOf(".war"));
+ if("/ROOT".equals(contextName))
+ contextName= "" ;
+ return contextName ;
+ }
+
+ /**
+ * Given a context path, get the config file name.
+ */
+ protected String getConfigFile(String path) {
+ String basename = null;
+ if (path.equals("")) {
+ basename = "ROOT";
+ } else {
+ basename = path.substring(1).replace('/', '#');
+ }
+ return (basename);
+ }
+
+ /**
+ * Given a context path, get the config file name.
+ */
+ protected String getDocBase(String path) {
+ String basename = null;
+ if (path.equals("")) {
+ basename = "ROOT";
+ } else {
+ basename = path.substring(1);
+ }
+ return (basename);
+ }
+
+ /**
+ * Return a File object representing the "application root" directory for
+ * our associated Host.
+ */
+ protected File getAppBase() {
+
+ if (appBase != null) {
+ return appBase;
+ }
+
+ File file = new File(host.getAppBase());
+ if (!file.isAbsolute())
+ file = new File(System.getProperty("catalina.base"), host
+ .getAppBase());
+ try {
+ appBase = file.getCanonicalFile();
+ } catch (IOException e) {
+ appBase = file;
+ }
+ return (appBase);
+
+ }
+
+ /**
+ * Invoke the remove method on the deployer.
+ */
+ protected void remove(String path) throws Exception {
+ // TODO Handle remove also work dir content !
+ // Stop the context first to be nicer
+ Context context = (Context) host.findChild(path);
+ if (context != null) {
+ if(log.isDebugEnabled())
+ log.debug("Undeploy local context " +path );
+ ((Lifecycle) context).stop();
+ File war = new File(getAppBase(), getDocBase(path) + ".war");
+ File dir = new File(getAppBase(), getDocBase(path));
+ File xml = new File(configBase, getConfigFile(path) + ".xml");
+ if (war.exists()) {
+ war.delete();
+ } else if (dir.exists()) {
+ undeployDir(dir);
+ } else {
+ xml.delete();
+ }
+ // Perform new deployment and remove internal HostConfig state
+ check(path);
+ }
+
+ }
+
+ /**
+ * Delete the specified directory, including all of its contents and
+ * subdirectories recursively.
+ *
+ * @param dir
+ * File object representing the directory to be deleted
+ */
+ protected void undeployDir(File dir) {
+
+ String files[] = dir.list();
+ if (files == null) {
+ files = new String[0];
+ }
+ for (int i = 0; i < files.length; i++) {
+ File file = new File(dir, files[i]);
+ if (file.isDirectory()) {
+ undeployDir(file);
+ } else {
+ file.delete();
+ }
+ }
+ dir.delete();
+
+ }
+
+ /*
+ * Call watcher to check for deploy changes
+ *
+ * @see org.apache.catalina.cluster.ClusterDeployer#backgroundProcess()
+ */
+ public void backgroundProcess() {
+ if (started) {
+ count = (count + 1) % processDeployFrequency;
+ if (count == 0 && watchEnabled) {
+ watcher.check();
+ }
+ }
+
+ }
+
+ /*--Deployer Operations ------------------------------------*/
+
+ /**
+ * Invoke the check method on the deployer.
+ */
+ protected void check(String name) throws Exception {
+ String[] params = { name };
+ String[] signature = { "java.lang.String" };
+ mBeanServer.invoke(oname, "check", params, signature);
+ }
+
+ /**
+ * Invoke the check method on the deployer.
+ */
+ protected boolean isServiced(String name) throws Exception {
+ String[] params = { name };
+ String[] signature = { "java.lang.String" };
+ Boolean result = (Boolean) mBeanServer.invoke(oname, "isServiced",
+ params, signature);
+ return result.booleanValue();
+ }
+
+ /**
+ * Invoke the check method on the deployer.
+ */
+ protected void addServiced(String name) throws Exception {
+ String[] params = { name };
+ String[] signature = { "java.lang.String" };
+ mBeanServer.invoke(oname, "addServiced", params, signature);
+ }
+
+ /**
+ * Invoke the check method on the deployer.
+ */
+ protected void removeServiced(String name) throws Exception {
+ String[] params = { name };
+ String[] signature = { "java.lang.String" };
+ mBeanServer.invoke(oname, "removeServiced", params, signature);
+ }
+
+ /*--Instance Getters/Setters--------------------------------*/
+ public CatalinaCluster getCluster() {
+ return cluster;
+ }
+
+ public void setCluster(CatalinaCluster cluster) {
+ this.cluster = cluster;
+ }
+
+ public boolean equals(Object listener) {
+ return super.equals(listener);
+ }
+
+ public int hashCode() {
+ return super.hashCode();
+ }
+
+ public String getDeployDir() {
+ return deployDir;
+ }
+
+ public void setDeployDir(String deployDir) {
+ this.deployDir = deployDir;
+ }
+
+ public String getTempDir() {
+ return tempDir;
+ }
+
+ public void setTempDir(String tempDir) {
+ this.tempDir = tempDir;
+ }
+
+ public String getWatchDir() {
+ return watchDir;
+ }
+
+ public void setWatchDir(String watchDir) {
+ this.watchDir = watchDir;
+ }
+
+ public boolean isWatchEnabled() {
+ return watchEnabled;
+ }
+
+ public boolean getWatchEnabled() {
+ return watchEnabled;
+ }
+
+ public void setWatchEnabled(boolean watchEnabled) {
+ this.watchEnabled = watchEnabled;
+ }
+
+ /**
+ * Return the frequency of watcher checks.
+ */
+ public int getProcessDeployFrequency() {
+
+ return (this.processDeployFrequency);
+
+ }
+
+ /**
+ * Set the watcher checks frequency.
+ *
+ * @param processExpiresFrequency
+ * the new manager checks frequency
+ */
+ public void setProcessDeployFrequency(int processExpiresFrequency) {
+
+ if (processExpiresFrequency <= 0) {
+ return;
+ }
+ this.processDeployFrequency = processExpiresFrequency;
+ }
+
+ /**
+ * Copy a file to the specified temp directory.
+ * @param from copy from temp
+ * @param to to host appBase directory
+ * @return true, copy successful
+ */
+ protected boolean copy(File from, File to) {
+ try {
+ if (!to.exists())
+ to.createNewFile();
+ java.io.FileInputStream is = new java.io.FileInputStream(from);
+ java.io.FileOutputStream os = new java.io.FileOutputStream(to,
+ false);
+ byte[] buf = new byte[4096];
+ while (true) {
+ int len = is.read(buf);
+ if (len < 0)
+ break;
+ os.write(buf, 0, len);
+ }
+ is.close();
+ os.close();
+ } catch (IOException e) {
+ log.error("Unable to copy file from:" + from + " to:" + to, e);
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/java/org/apache/catalina/ha/deploy/FileChangeListener.java b/java/org/apache/catalina/ha/deploy/FileChangeListener.java
new file mode 100644
index 000000000..3468622c8
--- /dev/null
+++ b/java/org/apache/catalina/ha/deploy/FileChangeListener.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.deploy;
+import java.io.File;
+
+public interface FileChangeListener {
+ public void fileModified(File f);
+ public void fileRemoved(File f);
+}
diff --git a/java/org/apache/catalina/ha/deploy/FileMessage.java b/java/org/apache/catalina/ha/deploy/FileMessage.java
new file mode 100644
index 000000000..ab2847ec3
--- /dev/null
+++ b/java/org/apache/catalina/ha/deploy/FileMessage.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.deploy;
+
+import java.io.Serializable;
+
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.ha.ClusterMessageBase;
+
+/**
+ * Contains the data for a file being transferred over TCP, this is
+ * essentially a fragment of a file, read and written by the FileMessageFactory
+ * @author Filip Hanik
+ * @version 1.0
+ */
+
+public class FileMessage extends ClusterMessageBase implements ClusterMessage, Serializable {
+ private int messageNumber;
+ private byte[] data;
+ private int dataLength;
+
+ private long totalLength;
+ private long totalNrOfMsgs;
+ private String fileName;
+ private String contextPath;
+
+ public FileMessage(Member source,
+ String fileName,
+ String contextPath) {
+ this.address=source;
+ this.fileName=fileName;
+ this.contextPath=contextPath;
+ }
+
+ /*
+ public void writeExternal(ObjectOutput out) throws IOException {
+
+ }
+
+ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+
+ }
+ */
+
+ public int getMessageNumber() {
+ return messageNumber;
+ }
+ public void setMessageNumber(int messageNumber) {
+ this.messageNumber = messageNumber;
+ }
+ public long getTotalNrOfMsgs() {
+ return totalNrOfMsgs;
+ }
+ public void setTotalNrOfMsgs(long totalNrOfMsgs) {
+ this.totalNrOfMsgs = totalNrOfMsgs;
+ }
+ public byte[] getData() {
+ return data;
+ }
+ public void setData(byte[] data, int length) {
+ this.data = data;
+ this.dataLength = length;
+ }
+ public int getDataLength() {
+ return dataLength;
+ }
+ public void setDataLength(int dataLength) {
+ this.dataLength = dataLength;
+ }
+ public long getTotalLength() {
+ return totalLength;
+ }
+ public void setTotalLength(long totalLength) {
+ this.totalLength = totalLength;
+ }
+
+ public String getUniqueId() {
+ StringBuffer result = new StringBuffer(getFileName());
+ result.append("#-#");
+ result.append(getMessageNumber());
+ result.append("#-#");
+ result.append(System.currentTimeMillis());
+ return result.toString();
+ }
+
+
+ public String getFileName() {
+ return fileName;
+ }
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+ public String getContextPath() {
+ return contextPath;
+ }
+
+}
diff --git a/java/org/apache/catalina/ha/deploy/FileMessageFactory.java b/java/org/apache/catalina/ha/deploy/FileMessageFactory.java
new file mode 100644
index 000000000..43354eaa5
--- /dev/null
+++ b/java/org/apache/catalina/ha/deploy/FileMessageFactory.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.deploy;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileNotFoundException;
+
+/**
+ * This factory is used to read files and write files by splitting them up into
+ * smaller messages. So that entire files don't have to be read into memory.
+ *
+ * The factory can be used as a reader or writer but not both at the same time.
+ * When done reading or writing the factory will close the input or output
+ * streams and mark the factory as closed. It is not possible to use it after
+ * that.
+ * To force a cleanup, call cleanup() from the calling object.
+ * This class is not thread safe.
+ *
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class FileMessageFactory {
+ /*--Static Variables----------------------------------------*/
+ public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory
+ .getLog(FileMessageFactory.class);
+
+ /**
+ * The number of bytes that we read from file
+ */
+ public static final int READ_SIZE = 1024 * 10; //10kb
+
+ /**
+ * The file that we are reading/writing
+ */
+ protected File file = null;
+
+ /**
+ * True means that we are writing with this factory. False means that we are
+ * reading with this factory
+ */
+ protected boolean openForWrite;
+
+ /**
+ * Once the factory is used, it can not be reused.
+ */
+ protected boolean closed = false;
+
+ /**
+ * When openForWrite=false, the input stream is held by this variable
+ */
+ protected FileInputStream in;
+
+ /**
+ * When openForWrite=true, the output stream is held by this variable
+ */
+ protected FileOutputStream out;
+
+ /**
+ * The number of messages we have read or written
+ */
+ protected int nrOfMessagesProcessed = 0;
+
+ /**
+ * The total size of the file
+ */
+ protected long size = 0;
+
+ /**
+ * The total number of packets that we split this file into
+ */
+ protected long totalNrOfMessages = 0;
+
+ /**
+ * The bytes that we hold the data in, not thread safe.
+ */
+ protected byte[] data = new byte[READ_SIZE];
+
+ /**
+ * Private constructor, either instantiates a factory to read or write.
+ * When openForWrite==true, then a the file, f, will be created and an
+ * output stream is opened to write to it.
+ * When openForWrite==false, an input stream is opened, the file has to
+ * exist.
+ *
+ * @param f
+ * File - the file to be read/written
+ * @param openForWrite
+ * boolean - true means we are writing to the file, false means
+ * we are reading from the file
+ * @throws FileNotFoundException -
+ * if the file to be read doesn't exist
+ * @throws IOException -
+ * if the system fails to open input/output streams to the file
+ * or if it fails to create the file to be written to.
+ */
+ private FileMessageFactory(File f, boolean openForWrite)
+ throws FileNotFoundException, IOException {
+ this.file = f;
+ this.openForWrite = openForWrite;
+ if (log.isDebugEnabled())
+ log.debug("open file " + f + " write " + openForWrite);
+ if (openForWrite) {
+ if (!file.exists())
+ file.createNewFile();
+ out = new FileOutputStream(f);
+ } else {
+ size = file.length();
+ totalNrOfMessages = (size / READ_SIZE) + 1;
+ in = new FileInputStream(f);
+ }//end if
+
+ }
+
+ /**
+ * Creates a factory to read or write from a file. When opening for read,
+ * the readMessage can be invoked, and when opening for write the
+ * writeMessage can be invoked.
+ *
+ * @param f
+ * File - the file to be read or written
+ * @param openForWrite
+ * boolean - true, means we are writing to the file, false means
+ * we are reading from it
+ * @throws FileNotFoundException -
+ * if the file to be read doesn't exist
+ * @throws IOException -
+ * if it fails to create the file that is to be written
+ * @return FileMessageFactory
+ */
+ public static FileMessageFactory getInstance(File f, boolean openForWrite)
+ throws FileNotFoundException, IOException {
+ return new FileMessageFactory(f, openForWrite);
+ }
+
+ /**
+ * Reads file data into the file message and sets the size, totalLength,
+ * totalNrOfMsgs and the message number
+ * If EOF is reached, the factory returns null, and closes itself, otherwise
+ * the same message is returned as was passed in. This makes sure that not
+ * more memory is ever used. To remember, neither the file message or the
+ * factory are thread safe. dont hand off the message to one thread and read
+ * the same with another.
+ *
+ * @param f
+ * FileMessage - the message to be populated with file data
+ * @throws IllegalArgumentException -
+ * if the factory is for writing or is closed
+ * @throws IOException -
+ * if a file read exception occurs
+ * @return FileMessage - returns the same message passed in as a parameter,
+ * or null if EOF
+ */
+ public FileMessage readMessage(FileMessage f)
+ throws IllegalArgumentException, IOException {
+ checkState(false);
+ int length = in.read(data);
+ if (length == -1) {
+ cleanup();
+ return null;
+ } else {
+ f.setData(data, length);
+ f.setTotalLength(size);
+ f.setTotalNrOfMsgs(totalNrOfMessages);
+ f.setMessageNumber(++nrOfMessagesProcessed);
+ return f;
+ }//end if
+ }
+
+ /**
+ * Writes a message to file. If (msg.getMessageNumber() ==
+ * msg.getTotalNrOfMsgs()) the output stream will be closed after writing.
+ *
+ * @param msg
+ * FileMessage - message containing data to be written
+ * @throws IllegalArgumentException -
+ * if the factory is opened for read or closed
+ * @throws IOException -
+ * if a file write error occurs
+ * @return returns true if the file is complete and outputstream is closed,
+ * false otherwise.
+ */
+ public boolean writeMessage(FileMessage msg)
+ throws IllegalArgumentException, IOException {
+ if (!openForWrite)
+ throw new IllegalArgumentException(
+ "Can't write message, this factory is reading.");
+ if (log.isDebugEnabled())
+ log.debug("Message " + msg + " data " + msg.getData()
+ + " data length " + msg.getDataLength() + " out " + out);
+ if (out != null) {
+ out.write(msg.getData(), 0, msg.getDataLength());
+ nrOfMessagesProcessed++;
+ out.flush();
+ if (msg.getMessageNumber() == msg.getTotalNrOfMsgs()) {
+ out.close();
+ cleanup();
+ return true;
+ }//end if
+ } else {
+ if (log.isWarnEnabled())
+ log.warn("Receive Message again -- Sender ActTimeout to short [ path: "
+ + msg.getContextPath()
+ + " war: "
+ + msg.getFileName()
+ + " data: "
+ + msg.getData()
+ + " data length: " + msg.getDataLength() + " ]");
+ }
+ return false;
+ }//writeMessage
+
+ /**
+ * Closes the factory, its streams and sets all its references to null
+ */
+ public void cleanup() {
+ if (in != null)
+ try {
+ in.close();
+ } catch (Exception ignore) {
+ }
+ if (out != null)
+ try {
+ out.close();
+ } catch (Exception ignore) {
+ }
+ in = null;
+ out = null;
+ size = 0;
+ closed = true;
+ data = null;
+ nrOfMessagesProcessed = 0;
+ totalNrOfMessages = 0;
+ }
+
+ /**
+ * Check to make sure the factory is able to perform the function it is
+ * asked to do. Invoked by readMessage/writeMessage before those methods
+ * proceed.
+ *
+ * @param openForWrite
+ * boolean
+ * @throws IllegalArgumentException
+ */
+ protected void checkState(boolean openForWrite)
+ throws IllegalArgumentException {
+ if (this.openForWrite != openForWrite) {
+ cleanup();
+ if (openForWrite)
+ throw new IllegalArgumentException(
+ "Can't write message, this factory is reading.");
+ else
+ throw new IllegalArgumentException(
+ "Can't read message, this factory is writing.");
+ }
+ if (this.closed) {
+ cleanup();
+ throw new IllegalArgumentException("Factory has been closed.");
+ }
+ }
+
+ /**
+ * Example usage.
+ *
+ * @param args
+ * String[], args[0] - read from filename, args[1] write to
+ * filename
+ * @throws Exception
+ */
+ public static void main(String[] args) throws Exception {
+
+ System.out
+ .println("Usage: FileMessageFactory fileToBeRead fileToBeWritten");
+ System.out
+ .println("Usage: This will make a copy of the file on the local file system");
+ FileMessageFactory read = getInstance(new File(args[0]), false);
+ FileMessageFactory write = getInstance(new File(args[1]), true);
+ FileMessage msg = new FileMessage(null, args[0], args[0]);
+ msg = read.readMessage(msg);
+ System.out.println("Expecting to write " + msg.getTotalNrOfMsgs()
+ + " messages.");
+ int cnt = 0;
+ while (msg != null) {
+ write.writeMessage(msg);
+ cnt++;
+ msg = read.readMessage(msg);
+ }//while
+ System.out.println("Actually wrote " + cnt + " messages.");
+ }///main
+
+ public File getFile() {
+ return file;
+ }
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/ha/deploy/UndeployMessage.java b/java/org/apache/catalina/ha/deploy/UndeployMessage.java
new file mode 100644
index 000000000..1003f3c90
--- /dev/null
+++ b/java/org/apache/catalina/ha/deploy/UndeployMessage.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.deploy;
+
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.tribes.Member;
+import java.io.Serializable;
+public class UndeployMessage implements ClusterMessage,Serializable {
+ private Member address;
+ private long timestamp;
+ private String uniqueId;
+ private String contextPath;
+ private boolean undeploy;
+ private int resend = 0;
+ private int compress = 0;
+
+ public UndeployMessage() {} //for serialization
+ public UndeployMessage(Member address,
+ long timestamp,
+ String uniqueId,
+ String contextPath,
+ boolean undeploy) {
+ this.address = address;
+ this.timestamp= timestamp;
+ this.undeploy = undeploy;
+ this.uniqueId = uniqueId;
+ this.undeploy = undeploy;
+ this.contextPath = contextPath;
+ }
+
+ public Member getAddress() {
+ return address;
+ }
+
+ public void setAddress(Member address) {
+ this.address = address;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public String getUniqueId() {
+ return uniqueId;
+ }
+
+ public void setUniqueId(String uniqueId) {
+ this.uniqueId = uniqueId;
+ }
+
+ public String getContextPath() {
+ return contextPath;
+ }
+
+ public void setContextPath(String contextPath) {
+ this.contextPath = contextPath;
+ }
+
+ public boolean getUndeploy() {
+ return undeploy;
+ }
+
+ public void setUndeploy(boolean undeploy) {
+ this.undeploy = undeploy;
+ }
+ /**
+ * @return Returns the compress.
+ * @since 5.5.10
+ */
+ public int getCompress() {
+ return compress;
+ }
+ /**
+ * @param compress The compress to set.
+ * @since 5.5.10
+ */
+ public void setCompress(int compress) {
+ this.compress = compress;
+ }
+ /**
+ * @return Returns the resend.
+ * @since 5.5.10
+ */
+ public int getResend() {
+ return resend;
+ }
+ /**
+ * @param resend The resend to set.
+ * @since 5.5.10
+ */
+ public void setResend(int resend) {
+ this.resend = resend;
+ }
+
+}
diff --git a/java/org/apache/catalina/ha/deploy/WarWatcher.java b/java/org/apache/catalina/ha/deploy/WarWatcher.java
new file mode 100644
index 000000000..f7aaa4ce0
--- /dev/null
+++ b/java/org/apache/catalina/ha/deploy/WarWatcher.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.deploy;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Iterator;
+
+/**
+ * org.apache.catalina.Cluster implementations
+of this class is done when implementing a new Cluster protocol
+ JGCluster.java
+configure(),
+ * and before any of the public methods of the component are utilized.
+ * Starts the cluster communication channel, this will connect with the other nodes
+ * in the cluster, and request the current session state to be transferred to this node.
+ * @exception IllegalStateException if this component has already been
+ * started
+ * @exception LifecycleException if this component detects a fatal error
+ * that prevents this component from being used
+ */
+ public void start() throws LifecycleException {
+ if ( this.started ) return;
+ try {
+ CatalinaCluster catclust = (CatalinaCluster)cluster;
+ catclust.addManager(getName(), this);
+ LazyReplicatedMap map = new LazyReplicatedMap(this,
+ catclust.getChannel(),
+ DEFAULT_REPL_TIMEOUT,
+ getMapName(),
+ getClassLoaders());
+ map.setChannelSendOptions(mapSendOptions);
+ this.sessions = map;
+ super.start();
+ } catch ( Exception x ) {
+ log.error("Unable to start BackupManager",x);
+ throw new LifecycleException("Failed to start BackupManager",x);
+ }
+ }
+
+ public String getMapName() {
+ CatalinaCluster catclust = (CatalinaCluster)cluster;
+ String name = catclust.getManagerName(getName(),this)+"-"+"";
+ if ( log.isDebugEnabled() ) log.debug("Backup manager, Setting map name to:"+name);
+ return name;
+ }
+
+ /**
+ * Gracefully terminate the active use of the public methods of this
+ * component. This method should be the last one called on a given
+ * instance of this component.
+ * This will disconnect the cluster communication channel and stop the listener thread.
+ * @exception IllegalStateException if this component has not been started
+ * @exception LifecycleException if this component detects a fatal error
+ * that needs to be reported
+ */
+ public void stop() throws LifecycleException
+ {
+
+ LazyReplicatedMap map = (LazyReplicatedMap)sessions;
+ if ( map!=null ) {
+ map.breakdown();
+ }
+ if ( !this.started ) return;
+ try {
+ } catch ( Exception x ){
+ log.error("Unable to stop BackupManager",x);
+ throw new LifecycleException("Failed to stop BackupManager",x);
+ } finally {
+ super.stop();
+ }
+ cluster.removeManager(getName(),this);
+
+ }
+
+ public void setDistributable(boolean dist) {
+ this.distributable = dist;
+ }
+
+ public boolean getDistributable() {
+ return distributable;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ public boolean isNotifyListenersOnReplication() {
+ return notifyListenersOnReplication;
+ }
+ public void setNotifyListenersOnReplication(boolean notifyListenersOnReplication) {
+ this.notifyListenersOnReplication = notifyListenersOnReplication;
+ }
+
+ public void setMapSendOptions(int mapSendOptions) {
+ this.mapSendOptions = mapSendOptions;
+ }
+
+ /*
+ * @see org.apache.catalina.ha.ClusterManager#getCluster()
+ */
+ public CatalinaCluster getCluster() {
+ return cluster;
+ }
+
+ public int getMapSendOptions() {
+ return mapSendOptions;
+ }
+
+ public String[] getInvalidatedSessions() {
+ return new String[0];
+ }
+
+}
diff --git a/java/org/apache/catalina/ha/session/ClusterManagerBase.java b/java/org/apache/catalina/ha/session/ClusterManagerBase.java
new file mode 100644
index 000000000..71e04038a
--- /dev/null
+++ b/java/org/apache/catalina/ha/session/ClusterManagerBase.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.session;
+
+import org.apache.catalina.ha.ClusterManager;
+import java.beans.PropertyChangeListener;
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.session.ManagerBase;
+import org.apache.catalina.Loader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import org.apache.catalina.tribes.io.ReplicationStream;
+import org.apache.catalina.Container;
+
+/**
+ *
+ * @author Filip Hanik
+ * @version $Revision: 380100 $ $Date: 2006-02-23 06:08:14 -0600 (Thu, 23 Feb 2006) $
+ */
+
+public abstract class ClusterManagerBase extends ManagerBase implements Lifecycle, PropertyChangeListener, ClusterManager{
+
+
+ public static ClassLoader[] getClassLoaders(Container container) {
+ Loader loader = null;
+ ClassLoader classLoader = null;
+ if (container != null) loader = container.getLoader();
+ if (loader != null) classLoader = loader.getClassLoader();
+ else classLoader = Thread.currentThread().getContextClassLoader();
+ if ( classLoader == Thread.currentThread().getContextClassLoader() ) {
+ return new ClassLoader[] {classLoader};
+ } else {
+ return new ClassLoader[] {classLoader,Thread.currentThread().getContextClassLoader()};
+ }
+ }
+
+
+ public ClassLoader[] getClassLoaders() {
+ return getClassLoaders(container);
+ }
+
+ /**
+ * Open Stream and use correct ClassLoader (Container) Switch
+ * ThreadClassLoader
+ *
+ * @param data
+ * @return The object input stream
+ * @throws IOException
+ */
+ public ReplicationStream getReplicationStream(byte[] data) throws IOException {
+ return getReplicationStream(data,0,data.length);
+ }
+
+ public ReplicationStream getReplicationStream(byte[] data, int offset, int length) throws IOException {
+ ByteArrayInputStream fis = new ByteArrayInputStream(data, offset, length);
+ return new ReplicationStream(fis, getClassLoaders());
+ }
+
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/ha/session/ClusterSessionListener.java b/java/org/apache/catalina/ha/session/ClusterSessionListener.java
new file mode 100644
index 000000000..3150ec937
--- /dev/null
+++ b/java/org/apache/catalina/ha/session/ClusterSessionListener.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.session;
+
+import java.util.Map;
+
+import org.apache.catalina.ha.ClusterManager;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.ha.*;
+
+/**
+ * Receive replicated SessionMessage form other cluster node.
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ * @version $Revision: 378258 $ $Date: 2006-02-16 08:42:35 -0600 (Thu, 16 Feb 2006) $
+ */
+public class ClusterSessionListener extends ClusterListener {
+
+ /**
+ * The descriptive information about this implementation.
+ */
+ protected static final String info = "org.apache.catalina.session.ClusterSessionListener/1.1";
+
+ //--Constructor---------------------------------------------
+
+ public ClusterSessionListener() {
+ }
+
+ //--Logic---------------------------------------------------
+
+ /**
+ * Return descriptive information about this implementation.
+ */
+ public String getInfo() {
+
+ return (info);
+
+ }
+
+ /**
+ * Callback from the cluster, when a message is received, The cluster will
+ * broadcast it invoking the messageReceived on the receiver.
+ *
+ * @param myobj
+ * ClusterMessage - the message received from the cluster
+ */
+ public void messageReceived(ClusterMessage myobj) {
+ if (myobj != null && myobj instanceof SessionMessage) {
+ SessionMessage msg = (SessionMessage) myobj;
+ String ctxname = msg.getContextName();
+ //check if the message is a EVT_GET_ALL_SESSIONS,
+ //if so, wait until we are fully started up
+ Map managers = cluster.getManagers() ;
+ if (ctxname == null) {
+ java.util.Iterator i = managers.keySet().iterator();
+ while (i.hasNext()) {
+ String key = (String) i.next();
+ ClusterManager mgr = (ClusterManager) managers.get(key);
+ if (mgr != null)
+ mgr.messageDataReceived(msg);
+ else {
+ //this happens a lot before the system has started
+ // up
+ if (log.isDebugEnabled())
+ log.debug("Context manager doesn't exist:"
+ + key);
+ }
+ }
+ } else {
+ ClusterManager mgr = (ClusterManager) managers.get(ctxname);
+ if (mgr != null)
+ mgr.messageDataReceived(msg);
+ else if (log.isWarnEnabled())
+ log.warn("Context manager doesn't exist:" + ctxname);
+ }
+ }
+ return;
+ }
+
+ /**
+ * Accept only SessionMessage
+ *
+ * @param msg
+ * ClusterMessage
+ * @return boolean - returns true to indicate that messageReceived should be
+ * invoked. If false is returned, the messageReceived method will
+ * not be invoked.
+ */
+ public boolean accept(ClusterMessage msg) {
+ return (msg instanceof SessionMessage);
+ }
+}
+
diff --git a/java/org/apache/catalina/ha/session/Constants.java b/java/org/apache/catalina/ha/session/Constants.java
new file mode 100644
index 000000000..9240308e3
--- /dev/null
+++ b/java/org/apache/catalina/ha/session/Constants.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.session;
+
+/**
+ * Manifest constants for the org.apache.catalina.ha.session
+ * package.
+ *
+ * @author Peter Rossbach Pero
+ */
+
+public class Constants {
+
+ public static final String Package = "org.apache.catalina.ha.session";
+
+}
diff --git a/java/org/apache/catalina/ha/session/DeltaManager.java b/java/org/apache/catalina/ha/session/DeltaManager.java
new file mode 100644
index 000000000..545b637bc
--- /dev/null
+++ b/java/org/apache/catalina/ha/session/DeltaManager.java
@@ -0,0 +1,1499 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.session;
+
+import java.beans.PropertyChangeEvent;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+
+import org.apache.catalina.Cluster;
+import org.apache.catalina.Container;
+import org.apache.catalina.Context;
+import org.apache.catalina.Engine;
+import org.apache.catalina.Host;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.Session;
+import org.apache.catalina.Valve;
+import org.apache.catalina.core.StandardContext;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.ha.tcp.ReplicationValve;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.io.ReplicationStream;
+import org.apache.catalina.util.LifecycleSupport;
+import org.apache.catalina.util.StringManager;
+
+/**
+ * The DeltaManager manages replicated sessions by only replicating the deltas
+ * in data. For applications written to handle this, the DeltaManager is the
+ * optimal way of replicating data.
+ *
+ * This code is almost identical to StandardManager with a difference in how it
+ * persists sessions and some modifications to it.
+ *
+ * IMPLEMENTATION NOTE : Correct behavior of session storing and
+ * reloading depends upon external calls to the start() and
+ * stop() methods of this class at the correct times.
+ *
+ * @author Filip Hanik
+ * @author Craig R. McClanahan
+ * @author Jean-Francois Arcand
+ * @author Peter Rossbach
+ * @version $Revision: 380100 $ $Date: 2006-02-23 06:08:14 -0600 (Thu, 23 Feb 2006) $
+ */
+
+public class DeltaManager extends ClusterManagerBase{
+
+ // ---------------------------------------------------- Security Classes
+ public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(DeltaManager.class);
+
+ /**
+ * The string manager for this package.
+ */
+ protected static StringManager sm = StringManager.getManager(Constants.Package);
+
+ // ----------------------------------------------------- Instance Variables
+
+ /**
+ * The descriptive information about this implementation.
+ */
+ private static final String info = "DeltaManager/2.1";
+
+ /**
+ * Has this component been started yet?
+ */
+ private boolean started = false;
+
+ /**
+ * The descriptive name of this Manager implementation (for logging).
+ */
+ protected static String managerName = "DeltaManager";
+ protected String name = null;
+ protected boolean defaultMode = false;
+ private CatalinaCluster cluster = null;
+
+ /**
+ * cached replication valve cluster container!
+ */
+ private ReplicationValve replicationValve = null ;
+
+ /**
+ * The lifecycle event support for this component.
+ */
+ protected LifecycleSupport lifecycle = new LifecycleSupport(this);
+
+ /**
+ * The maximum number of active Sessions allowed, or -1 for no limit.
+ */
+ private int maxActiveSessions = -1;
+ private boolean expireSessionsOnShutdown = false;
+ private boolean notifyListenersOnReplication = true;
+ private boolean notifySessionListenersOnReplication = true;
+ private boolean stateTransfered = false ;
+ private int stateTransferTimeout = 60;
+ private boolean sendAllSessions = true;
+ private boolean sendClusterDomainOnly = true ;
+ private int sendAllSessionsSize = 1000 ;
+
+ /**
+ * wait time between send session block (default 2 sec)
+ */
+ private int sendAllSessionsWaitTime = 2 * 1000 ;
+ private ArrayList receivedMessageQueue = new ArrayList() ;
+ private boolean receiverQueue = false ;
+ private boolean stateTimestampDrop = true ;
+ private long stateTransferCreateSendTime;
+
+ // ------------------------------------------------------------------ stats attributes
+
+ int rejectedSessions = 0;
+ private long sessionReplaceCounter = 0 ;
+ long processingTime = 0;
+ private long counterReceive_EVT_GET_ALL_SESSIONS = 0 ;
+ private long counterSend_EVT_ALL_SESSION_DATA = 0 ;
+ private long counterReceive_EVT_ALL_SESSION_DATA = 0 ;
+ private long counterReceive_EVT_SESSION_CREATED = 0 ;
+ private long counterReceive_EVT_SESSION_EXPIRED = 0;
+ private long counterReceive_EVT_SESSION_ACCESSED = 0 ;
+ private long counterReceive_EVT_SESSION_DELTA = 0;
+ private long counterSend_EVT_GET_ALL_SESSIONS = 0 ;
+ private long counterSend_EVT_SESSION_CREATED = 0;
+ private long counterSend_EVT_SESSION_DELTA = 0 ;
+ private long counterSend_EVT_SESSION_ACCESSED = 0;
+ private long counterSend_EVT_SESSION_EXPIRED = 0;
+ private int counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE = 0 ;
+ private int counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE = 0 ;
+ private int counterNoStateTransfered = 0 ;
+
+
+ // ------------------------------------------------------------- Constructor
+ public DeltaManager() {
+ super();
+ }
+
+ // ------------------------------------------------------------- Properties
+
+ /**
+ * Return descriptive information about this Manager implementation and the
+ * corresponding version number, in the format
+ * <description>/<version>.
+ */
+ public String getInfo() {
+ return info;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Return the descriptive short name of this Manager implementation.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @return Returns the counterSend_EVT_GET_ALL_SESSIONS.
+ */
+ public long getCounterSend_EVT_GET_ALL_SESSIONS() {
+ return counterSend_EVT_GET_ALL_SESSIONS;
+ }
+
+ /**
+ * @return Returns the counterSend_EVT_SESSION_ACCESSED.
+ */
+ public long getCounterSend_EVT_SESSION_ACCESSED() {
+ return counterSend_EVT_SESSION_ACCESSED;
+ }
+
+ /**
+ * @return Returns the counterSend_EVT_SESSION_CREATED.
+ */
+ public long getCounterSend_EVT_SESSION_CREATED() {
+ return counterSend_EVT_SESSION_CREATED;
+ }
+
+ /**
+ * @return Returns the counterSend_EVT_SESSION_DELTA.
+ */
+ public long getCounterSend_EVT_SESSION_DELTA() {
+ return counterSend_EVT_SESSION_DELTA;
+ }
+
+ /**
+ * @return Returns the counterSend_EVT_SESSION_EXPIRED.
+ */
+ public long getCounterSend_EVT_SESSION_EXPIRED() {
+ return counterSend_EVT_SESSION_EXPIRED;
+ }
+
+ /**
+ * @return Returns the counterSend_EVT_ALL_SESSION_DATA.
+ */
+ public long getCounterSend_EVT_ALL_SESSION_DATA() {
+ return counterSend_EVT_ALL_SESSION_DATA;
+ }
+
+ /**
+ * @return Returns the counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE.
+ */
+ public int getCounterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE() {
+ return counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE;
+ }
+
+ /**
+ * @return Returns the counterReceive_EVT_ALL_SESSION_DATA.
+ */
+ public long getCounterReceive_EVT_ALL_SESSION_DATA() {
+ return counterReceive_EVT_ALL_SESSION_DATA;
+ }
+
+ /**
+ * @return Returns the counterReceive_EVT_GET_ALL_SESSIONS.
+ */
+ public long getCounterReceive_EVT_GET_ALL_SESSIONS() {
+ return counterReceive_EVT_GET_ALL_SESSIONS;
+ }
+
+ /**
+ * @return Returns the counterReceive_EVT_SESSION_ACCESSED.
+ */
+ public long getCounterReceive_EVT_SESSION_ACCESSED() {
+ return counterReceive_EVT_SESSION_ACCESSED;
+ }
+
+ /**
+ * @return Returns the counterReceive_EVT_SESSION_CREATED.
+ */
+ public long getCounterReceive_EVT_SESSION_CREATED() {
+ return counterReceive_EVT_SESSION_CREATED;
+ }
+
+ /**
+ * @return Returns the counterReceive_EVT_SESSION_DELTA.
+ */
+ public long getCounterReceive_EVT_SESSION_DELTA() {
+ return counterReceive_EVT_SESSION_DELTA;
+ }
+
+ /**
+ * @return Returns the counterReceive_EVT_SESSION_EXPIRED.
+ */
+ public long getCounterReceive_EVT_SESSION_EXPIRED() {
+ return counterReceive_EVT_SESSION_EXPIRED;
+ }
+
+
+ /**
+ * @return Returns the counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE.
+ */
+ public int getCounterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE() {
+ return counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE;
+ }
+
+ /**
+ * @return Returns the processingTime.
+ */
+ public long getProcessingTime() {
+ return processingTime;
+ }
+
+ /**
+ * @return Returns the sessionReplaceCounter.
+ */
+ public long getSessionReplaceCounter() {
+ return sessionReplaceCounter;
+ }
+
+ /**
+ * Number of session creations that failed due to maxActiveSessions
+ *
+ * @return The count
+ */
+ public int getRejectedSessions() {
+ return rejectedSessions;
+ }
+
+ public void setRejectedSessions(int rejectedSessions) {
+ this.rejectedSessions = rejectedSessions;
+ }
+
+ /**
+ * @return Returns the counterNoStateTransfered.
+ */
+ public int getCounterNoStateTransfered() {
+ return counterNoStateTransfered;
+ }
+
+ public int getReceivedQueueSize() {
+ return receivedMessageQueue.size() ;
+ }
+
+ /**
+ * @return Returns the stateTransferTimeout.
+ */
+ public int getStateTransferTimeout() {
+ return stateTransferTimeout;
+ }
+ /**
+ * @param timeoutAllSession The timeout
+ */
+ public void setStateTransferTimeout(int timeoutAllSession) {
+ this.stateTransferTimeout = timeoutAllSession;
+ }
+
+ /**
+ * is session state transfered complete?
+ *
+ */
+ public boolean getStateTransfered() {
+ return stateTransfered;
+ }
+
+ /**
+ * set that state ist complete transfered
+ * @param stateTransfered
+ */
+ public void setStateTransfered(boolean stateTransfered) {
+ this.stateTransfered = stateTransfered;
+ }
+
+ /**
+ * @return Returns the sendAllSessionsWaitTime in msec
+ */
+ public int getSendAllSessionsWaitTime() {
+ return sendAllSessionsWaitTime;
+ }
+
+ /**
+ * @param sendAllSessionsWaitTime The sendAllSessionsWaitTime to set at msec.
+ */
+ public void setSendAllSessionsWaitTime(int sendAllSessionsWaitTime) {
+ this.sendAllSessionsWaitTime = sendAllSessionsWaitTime;
+ }
+
+ /**
+ * @return Returns the sendClusterDomainOnly.
+ */
+ public boolean isSendClusterDomainOnly() {
+ return sendClusterDomainOnly;
+ }
+
+ /**
+ * @param sendClusterDomainOnly The sendClusterDomainOnly to set.
+ */
+ public void setSendClusterDomainOnly(boolean sendClusterDomainOnly) {
+ this.sendClusterDomainOnly = sendClusterDomainOnly;
+ }
+
+ /**
+ * @return Returns the stateTimestampDrop.
+ */
+ public boolean isStateTimestampDrop() {
+ return stateTimestampDrop;
+ }
+
+ /**
+ * @param isTimestampDrop The new flag value
+ */
+ public void setStateTimestampDrop(boolean isTimestampDrop) {
+ this.stateTimestampDrop = isTimestampDrop;
+ }
+
+ /**
+ * Return the maximum number of active Sessions allowed, or -1 for no limit.
+ */
+ public int getMaxActiveSessions() {
+ return (this.maxActiveSessions);
+ }
+
+ /**
+ * Set the maximum number of actives Sessions allowed, or -1 for no limit.
+ *
+ * @param max
+ * The new maximum number of sessions
+ */
+ public void setMaxActiveSessions(int max) {
+ int oldMaxActiveSessions = this.maxActiveSessions;
+ this.maxActiveSessions = max;
+ support.firePropertyChange("maxActiveSessions", new Integer(oldMaxActiveSessions), new Integer(this.maxActiveSessions));
+ }
+
+ /**
+ *
+ * @return Returns the sendAllSessions.
+ */
+ public boolean isSendAllSessions() {
+ return sendAllSessions;
+ }
+
+ /**
+ * @param sendAllSessions The sendAllSessions to set.
+ */
+ public void setSendAllSessions(boolean sendAllSessions) {
+ this.sendAllSessions = sendAllSessions;
+ }
+
+ /**
+ * @return Returns the sendAllSessionsSize.
+ */
+ public int getSendAllSessionsSize() {
+ return sendAllSessionsSize;
+ }
+
+ /**
+ * @param sendAllSessionsSize The sendAllSessionsSize to set.
+ */
+ public void setSendAllSessionsSize(int sendAllSessionsSize) {
+ this.sendAllSessionsSize = sendAllSessionsSize;
+ }
+
+ /**
+ * @return Returns the notifySessionListenersOnReplication.
+ */
+ public boolean isNotifySessionListenersOnReplication() {
+ return notifySessionListenersOnReplication;
+ }
+
+ /**
+ * @param notifyListenersCreateSessionOnReplication The notifySessionListenersOnReplication to set.
+ */
+ public void setNotifySessionListenersOnReplication(boolean notifyListenersCreateSessionOnReplication) {
+ this.notifySessionListenersOnReplication = notifyListenersCreateSessionOnReplication;
+ }
+
+
+ public boolean isExpireSessionsOnShutdown() {
+ return expireSessionsOnShutdown;
+ }
+
+ public void setExpireSessionsOnShutdown(boolean expireSessionsOnShutdown) {
+ this.expireSessionsOnShutdown = expireSessionsOnShutdown;
+ }
+
+ public boolean isNotifyListenersOnReplication() {
+ return notifyListenersOnReplication;
+ }
+
+ public void setNotifyListenersOnReplication(boolean notifyListenersOnReplication) {
+ this.notifyListenersOnReplication = notifyListenersOnReplication;
+ }
+
+
+ /**
+ * @return Returns the defaultMode.
+ */
+ public boolean isDefaultMode() {
+ return defaultMode;
+ }
+ /**
+ * @param defaultMode The defaultMode to set.
+ */
+ public void setDefaultMode(boolean defaultMode) {
+ this.defaultMode = defaultMode;
+ }
+
+ public CatalinaCluster getCluster() {
+ return cluster;
+ }
+
+ public void setCluster(CatalinaCluster cluster) {
+ this.cluster = cluster;
+ }
+
+ /**
+ * Set the Container with which this Manager has been associated. If it is a
+ * Context (the usual case), listen for changes to the session timeout
+ * property.
+ *
+ * @param container
+ * The associated Container
+ */
+ public void setContainer(Container container) {
+ // De-register from the old Container (if any)
+ if ((this.container != null) && (this.container instanceof Context))
+ ((Context) this.container).removePropertyChangeListener(this);
+
+ // Default processing provided by our superclass
+ super.setContainer(container);
+
+ // Register with the new Container (if any)
+ if ((this.container != null) && (this.container instanceof Context)) {
+ setMaxInactiveInterval(((Context) this.container).getSessionTimeout() * 60);
+ ((Context) this.container).addPropertyChangeListener(this);
+ }
+
+ }
+
+ // --------------------------------------------------------- Public Methods
+
+ /**
+ * Construct and return a new session object, based on the default settings
+ * specified by this Manager's properties. The session id will be assigned
+ * by this method, and available via the getId() method of the returned
+ * session. If a new session cannot be created for any reason, return
+ * null.
+ *
+ * @exception IllegalStateException
+ * if a new session cannot be instantiated for any reason
+ *
+ * Construct and return a new session object, based on the default settings
+ * specified by this Manager's properties. The session id will be assigned
+ * by this method, and available via the getId() method of the returned
+ * session. If a new session cannot be created for any reason, return
+ * null.
+ *
+ * @exception IllegalStateException
+ * if a new session cannot be instantiated for any reason
+ */
+ public Session createSession(String sessionId) {
+ return createSession(sessionId, true);
+ }
+
+ /**
+ * create new session with check maxActiveSessions and send session creation
+ * to other cluster nodes.
+ *
+ * @param distribute
+ * @return The session
+ */
+ public Session createSession(String sessionId, boolean distribute) {
+ if ((maxActiveSessions >= 0) && (sessions.size() >= maxActiveSessions)) {
+ rejectedSessions++;
+ throw new IllegalStateException(sm.getString("deltaManager.createSession.ise"));
+ }
+ DeltaSession session = (DeltaSession) super.createSession(sessionId) ;
+ if (distribute) {
+ sendCreateSession(session.getId(), session);
+ }
+ if (log.isDebugEnabled())
+ log.debug(sm.getString("deltaManager.createSession.newSession",session.getId(), new Integer(sessions.size())));
+ return (session);
+
+ }
+
+ /**
+ * Send create session evt to all backup node
+ * @param sessionId
+ * @param session
+ */
+ protected void sendCreateSession(String sessionId, DeltaSession session) {
+ if(cluster.getMembers().length > 0 ) {
+ SessionMessage msg =
+ new SessionMessageImpl(getName(),
+ SessionMessage.EVT_SESSION_CREATED,
+ null,
+ sessionId,
+ sessionId + "-" + System.currentTimeMillis());
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.sendMessage.newSession",name, sessionId));
+ msg.setTimestamp(session.getCreationTime());
+ counterSend_EVT_SESSION_CREATED++;
+ send(msg);
+ }
+ }
+
+ /**
+ * Send messages to other backup member (domain or all)
+ * @param msg Session message
+ */
+ protected void send(SessionMessage msg) {
+ if(cluster != null) {
+ if(isSendClusterDomainOnly())
+ cluster.sendClusterDomain(msg);
+ else
+ cluster.send(msg);
+ }
+ }
+
+ /**
+ * Create DeltaSession
+ * @see org.apache.catalina.Manager#createEmptySession()
+ */
+ public Session createEmptySession() {
+ return getNewDeltaSession() ;
+ }
+
+ /**
+ * Get new session class to be used in the doLoad() method.
+ */
+ protected DeltaSession getNewDeltaSession() {
+ return new DeltaSession(this);
+ }
+
+ /**
+ * Load Deltarequest from external node
+ * Load the Class at container classloader
+ * @see DeltaRequest#readExternal(java.io.ObjectInput)
+ * @param session
+ * @param data message data
+ * @return The request
+ * @throws ClassNotFoundException
+ * @throws IOException
+ */
+ protected DeltaRequest deserializeDeltaRequest(DeltaSession session, byte[] data) throws ClassNotFoundException, IOException {
+ ReplicationStream ois = getReplicationStream(data);
+ session.getDeltaRequest().readExternal(ois);
+ ois.close();
+ return session.getDeltaRequest();
+ }
+
+ /**
+ * serialize DeltaRequest
+ * @see DeltaRequest#writeExternal(java.io.ObjectOutput)
+ *
+ * @param deltaRequest
+ * @return serialized delta request
+ * @throws IOException
+ */
+ protected byte[] serializeDeltaRequest(DeltaRequest deltaRequest) throws IOException {
+ return deltaRequest.serialize();
+ }
+
+ /**
+ * Load sessions from other cluster node.
+ * FIXME replace currently sessions with same id without notifcation.
+ * FIXME SSO handling is not really correct with the session replacement!
+ * @exception ClassNotFoundException
+ * if a serialized class cannot be found during the reload
+ * @exception IOException
+ * if an input/output error occurs
+ */
+ protected void deserializeSessions(byte[] data) throws ClassNotFoundException,IOException {
+
+ // Initialize our internal data structures
+ //sessions.clear(); //should not do this
+ // Open an input stream to the specified pathname, if any
+ ClassLoader originalLoader = Thread.currentThread().getContextClassLoader();
+ ObjectInputStream ois = null;
+ // Load the previously unloaded active sessions
+ try {
+ ois = getReplicationStream(data);
+ Integer count = (Integer) ois.readObject();
+ int n = count.intValue();
+ for (int i = 0; i < n; i++) {
+ DeltaSession session = (DeltaSession) createEmptySession();
+ session.readObjectData(ois);
+ session.setManager(this);
+ session.setValid(true);
+ session.setPrimarySession(false);
+ //in case the nodes in the cluster are out of
+ //time synch, this will make sure that we have the
+ //correct timestamp, isValid returns true, cause
+ // accessCount=1
+ session.access();
+ //make sure that the session gets ready to expire if
+ // needed
+ session.setAccessCount(0);
+ session.resetDeltaRequest();
+ // FIXME How inform other session id cache like SingleSignOn
+ // increment sessionCounter to correct stats report
+ if (findSession(session.getIdInternal()) == null ) {
+ sessionCounter++;
+ } else {
+ sessionReplaceCounter++;
+ // FIXME better is to grap this sessions again !
+ if (log.isWarnEnabled()) log.warn(sm.getString("deltaManager.loading.existing.session",session.getIdInternal()));
+ }
+ add(session);
+ }
+ } catch (ClassNotFoundException e) {
+ log.error(sm.getString("deltaManager.loading.cnfe", e), e);
+ throw e;
+ } catch (IOException e) {
+ log.error(sm.getString("deltaManager.loading.ioe", e), e);
+ throw e;
+ } finally {
+ // Close the input stream
+ try {
+ if (ois != null) ois.close();
+ } catch (IOException f) {
+ // ignored
+ }
+ ois = null;
+ if (originalLoader != null) Thread.currentThread().setContextClassLoader(originalLoader);
+ }
+
+ }
+
+
+
+ /**
+ * Save any currently active sessions in the appropriate persistence
+ * mechanism, if any. If persistence is not supported, this method returns
+ * without doing anything.
+ *
+ * @exception IOException
+ * if an input/output error occurs
+ */
+ protected byte[] serializeSessions(Session[] currentSessions) throws IOException {
+
+ // Open an output stream to the specified pathname, if any
+ ByteArrayOutputStream fos = null;
+ ObjectOutputStream oos = null;
+
+ try {
+ fos = new ByteArrayOutputStream();
+ oos = new ObjectOutputStream(new BufferedOutputStream(fos));
+ oos.writeObject(new Integer(currentSessions.length));
+ for(int i=0 ; i < currentSessions.length;i++) {
+ ((DeltaSession)currentSessions[i]).writeObjectData(oos);
+ }
+ // Flush and close the output stream
+ oos.flush();
+ } catch (IOException e) {
+ log.error(sm.getString("deltaManager.unloading.ioe", e), e);
+ throw e;
+ } finally {
+ if (oos != null) {
+ try {
+ oos.close();
+ } catch (IOException f) {
+ ;
+ }
+ oos = null;
+ }
+ }
+ // send object data as byte[]
+ return fos.toByteArray();
+ }
+
+ // ------------------------------------------------------ Lifecycle Methods
+
+ /**
+ * Add a lifecycle event listener to this component.
+ *
+ * @param listener
+ * The listener to add
+ */
+ public void addLifecycleListener(LifecycleListener listener) {
+ lifecycle.addLifecycleListener(listener);
+ }
+
+ /**
+ * Get the lifecycle listeners associated with this lifecycle. If this
+ * Lifecycle has no listeners registered, a zero-length array is returned.
+ */
+ public LifecycleListener[] findLifecycleListeners() {
+ return lifecycle.findLifecycleListeners();
+ }
+
+ /**
+ * Remove a lifecycle event listener from this component.
+ *
+ * @param listener
+ * The listener to remove
+ */
+ public void removeLifecycleListener(LifecycleListener listener) {
+ lifecycle.removeLifecycleListener(listener);
+ }
+
+ /**
+ * Prepare for the beginning of active use of the public methods of this
+ * component. This method should be called after configure(),
+ * and before any of the public methods of the component are utilized.
+ *
+ * @exception LifecycleException
+ * if this component detects a fatal error that prevents this
+ * component from being used
+ */
+ public void start() throws LifecycleException {
+ if (!initialized) init();
+
+ // Validate and update our current component state
+ if (started) {
+ return;
+ }
+ started = true;
+ lifecycle.fireLifecycleEvent(START_EVENT, null);
+
+ // Force initialization of the random number generator
+ generateSessionId();
+
+ // Load unloaded sessions, if any
+ try {
+ //the channel is already running
+ Cluster cluster = getCluster() ;
+ // stop remove cluster binding
+ //wow, how many nested levels of if statements can we have ;)
+ if(cluster == null) {
+ Container context = getContainer() ;
+ if(context != null && context instanceof Context) {
+ Container host = context.getParent() ;
+ if(host != null && host instanceof Host) {
+ cluster = host.getCluster();
+ if(cluster != null && cluster instanceof CatalinaCluster) {
+ setCluster((CatalinaCluster) cluster) ;
+ } else {
+ Container engine = host.getParent() ;
+ if(engine != null && engine instanceof Engine) {
+ cluster = engine.getCluster();
+ if(cluster != null && cluster instanceof CatalinaCluster) {
+ setCluster((CatalinaCluster) cluster) ;
+ }
+ } else {
+ cluster = null ;
+ }
+ }
+ }
+ }
+ }
+ if (cluster == null) {
+ log.error(sm.getString("deltaManager.noCluster", getName()));
+ return;
+ } else {
+ if (log.isInfoEnabled()) {
+ String type = "unknown" ;
+ if( cluster.getContainer() instanceof Host){
+ type = "Host" ;
+ } else if( cluster.getContainer() instanceof Engine){
+ type = "Engine" ;
+ }
+ log.info(sm.getString("deltaManager.registerCluster", getName(), type, cluster.getClusterName()));
+ }
+ }
+ if (log.isInfoEnabled()) log.info(sm.getString("deltaManager.startClustering", getName()));
+ //to survice context reloads, as only a stop/start is called, not
+ // createManager
+ ((CatalinaCluster)cluster).addManager(getName(), this);
+
+ getAllClusterSessions();
+
+ } catch (Throwable t) {
+ log.error(sm.getString("deltaManager.managerLoad"), t);
+ }
+ }
+
+ /**
+ * get from first session master the backup from all clustered sessions
+ * @see #findSessionMasterMember()
+ */
+ public synchronized void getAllClusterSessions() {
+ if (cluster != null && cluster.getMembers().length > 0) {
+ long beforeSendTime = System.currentTimeMillis();
+ Member mbr = findSessionMasterMember();
+ if(mbr == null) { // No domain member found
+ return;
+ }
+ SessionMessage msg = new SessionMessageImpl(this.getName(),SessionMessage.EVT_GET_ALL_SESSIONS, null, "GET-ALL","GET-ALL-" + getName());
+ // set reference time
+ stateTransferCreateSendTime = beforeSendTime ;
+ // request session state
+ counterSend_EVT_GET_ALL_SESSIONS++;
+ stateTransfered = false ;
+ // FIXME This send call block the deploy thread, when sender waitForAck is enabled
+ try {
+ synchronized(receivedMessageQueue) {
+ receiverQueue = true ;
+ }
+ cluster.send(msg, mbr);
+ if (log.isWarnEnabled()) log.warn(sm.getString("deltaManager.waitForSessionState",getName(), mbr));
+ // FIXME At sender ack mode this method check only the state transfer and resend is a problem!
+ waitForSendAllSessions(beforeSendTime);
+ } finally {
+ synchronized(receivedMessageQueue) {
+ for (Iterator iter = receivedMessageQueue.iterator(); iter.hasNext();) {
+ SessionMessage smsg = (SessionMessage) iter.next();
+ if (!stateTimestampDrop) {
+ messageReceived(smsg, smsg.getAddress() != null ? (Member) smsg.getAddress() : null);
+ } else {
+ if (smsg.getEventType() != SessionMessage.EVT_GET_ALL_SESSIONS && smsg.getTimestamp() >= stateTransferCreateSendTime) {
+ // FIXME handle EVT_GET_ALL_SESSIONS later
+ messageReceived(smsg,smsg.getAddress() != null ? (Member) smsg.getAddress() : null);
+ } else {
+ if (log.isWarnEnabled()) {
+ log.warn(sm.getString("deltaManager.dropMessage",getName(), smsg.getEventTypeString(),new Date(stateTransferCreateSendTime), new Date(smsg.getTimestamp())));
+ }
+ }
+ }
+ }
+ receivedMessageQueue.clear();
+ receiverQueue = false ;
+ }
+ }
+ } else {
+ if (log.isInfoEnabled()) log.info(sm.getString("deltaManager.noMembers", getName()));
+ }
+ }
+
+ /**
+ * Register cross context session at replication valve thread local
+ * @param session cross context session
+ */
+ protected void registerSessionAtReplicationValve(DeltaSession session) {
+ if(replicationValve == null) {
+ if(container instanceof StandardContext && ((StandardContext)container).getCrossContext()) {
+ Cluster cluster = getCluster() ;
+ if(cluster != null && cluster instanceof CatalinaCluster) {
+ Valve[] valves = ((CatalinaCluster)cluster).getValves();
+ if(valves != null && valves.length > 0) {
+ for(int i=0; replicationValve == null && i < valves.length ; i++ ){
+ if(valves[i] instanceof ReplicationValve) replicationValve = (ReplicationValve)valves[i] ;
+ }//for
+
+ if(replicationValve == null && log.isDebugEnabled()) {
+ log.debug("no ReplicationValve found for CrossContext Support");
+ }//endif
+ }//end if
+ }//endif
+ }//end if
+ }//end if
+ if(replicationValve != null) {
+ replicationValve.registerReplicationSession(session);
+ }
+ }
+
+ /**
+ * Find the master of the session state
+ * @return master member of sessions
+ */
+ protected Member findSessionMasterMember() {
+ Member mbr = null;
+ Member mbrs[] = cluster.getMembers();
+ if(mbrs.length != 0 ) mbr = mbrs[0];
+ if(mbr == null && log.isWarnEnabled()) log.warn(sm.getString("deltaManager.noMasterMember",getName(), ""));
+ if(mbr != null && log.isDebugEnabled()) log.warn(sm.getString("deltaManager.foundMasterMember",getName(), mbr));
+ return mbr;
+ }
+
+ /**
+ * Wait that cluster session state is transfer or timeout after 60 Sec
+ * With stateTransferTimeout == -1 wait that backup is transfered (forever mode)
+ */
+ protected void waitForSendAllSessions(long beforeSendTime) {
+ long reqStart = System.currentTimeMillis();
+ long reqNow = reqStart ;
+ boolean isTimeout = false;
+ if(getStateTransferTimeout() > 0) {
+ // wait that state is transfered with timeout check
+ do {
+ try {
+ Thread.sleep(100);
+ } catch (Exception sleep) {
+ //
+ }
+ reqNow = System.currentTimeMillis();
+ isTimeout = ((reqNow - reqStart) > (1000 * getStateTransferTimeout()));
+ } while ((!getStateTransfered()) && (!isTimeout));
+ } else {
+ if(getStateTransferTimeout() == -1) {
+ // wait that state is transfered
+ do {
+ try {
+ Thread.sleep(100);
+ } catch (Exception sleep) {
+ }
+ } while ((!getStateTransfered()));
+ reqNow = System.currentTimeMillis();
+ }
+ }
+ if (isTimeout || (!getStateTransfered())) {
+ counterNoStateTransfered++ ;
+ log.error(sm.getString("deltaManager.noSessionState",getName(),new Date(beforeSendTime),new Long(reqNow - beforeSendTime)));
+ } else {
+ if (log.isInfoEnabled())
+ log.info(sm.getString("deltaManager.sessionReceived",getName(), new Date(beforeSendTime), new Long(reqNow - beforeSendTime)));
+ }
+ }
+
+ /**
+ * Gracefully terminate the active use of the public methods of this
+ * component. This method should be the last one called on a given instance
+ * of this component.
+ *
+ * @exception LifecycleException
+ * if this component detects a fatal error that needs to be
+ * reported
+ */
+ public void stop() throws LifecycleException {
+
+ if (log.isDebugEnabled())
+ log.debug(sm.getString("deltaManager.stopped", getName()));
+
+
+ // Validate and update our current component state
+ if (!started)
+ throw new LifecycleException(sm.getString("deltaManager.notStarted"));
+ lifecycle.fireLifecycleEvent(STOP_EVENT, null);
+ started = false;
+
+ // Expire all active sessions
+ if (log.isInfoEnabled()) log.info(sm.getString("deltaManager.expireSessions", getName()));
+ Session sessions[] = findSessions();
+ for (int i = 0; i < sessions.length; i++) {
+ DeltaSession session = (DeltaSession) sessions[i];
+ if (!session.isValid())
+ continue;
+ try {
+ session.expire(true, isExpireSessionsOnShutdown());
+ } catch (Throwable ignore) {
+ ;
+ }
+ }
+
+ // Require a new random number generator if we are restarted
+ this.random = null;
+ getCluster().removeManager(getName(),this);
+ replicationValve = null;
+ if (initialized) {
+ destroy();
+ }
+ }
+
+ // ----------------------------------------- PropertyChangeListener Methods
+
+ /**
+ * Process property change events from our associated Context.
+ *
+ * @param event
+ * The property change event that has occurred
+ */
+ public void propertyChange(PropertyChangeEvent event) {
+
+ // Validate the source of this event
+ if (!(event.getSource() instanceof Context))
+ return;
+ // Process a relevant property change
+ if (event.getPropertyName().equals("sessionTimeout")) {
+ try {
+ setMaxInactiveInterval(((Integer) event.getNewValue()).intValue() * 60);
+ } catch (NumberFormatException e) {
+ log.error(sm.getString("deltaManager.sessionTimeout", event.getNewValue()));
+ }
+ }
+
+ }
+
+ // -------------------------------------------------------- Replication
+ // Methods
+
+ /**
+ * A message was received from another node, this is the callback method to
+ * implement if you are interested in receiving replication messages.
+ *
+ * @param cmsg -
+ * the message received.
+ */
+ public void messageDataReceived(ClusterMessage cmsg) {
+ if (cmsg != null && cmsg instanceof SessionMessage) {
+ SessionMessage msg = (SessionMessage) cmsg;
+ switch (msg.getEventType()) {
+ case SessionMessage.EVT_GET_ALL_SESSIONS:
+ case SessionMessage.EVT_SESSION_CREATED:
+ case SessionMessage.EVT_SESSION_EXPIRED:
+ case SessionMessage.EVT_SESSION_ACCESSED:
+ case SessionMessage.EVT_SESSION_DELTA: {
+ synchronized(receivedMessageQueue) {
+ if(receiverQueue) {
+ receivedMessageQueue.add(msg);
+ return ;
+ }
+ }
+ break;
+ }
+ default: {
+ //we didn't queue, do nothing
+ break;
+ }
+ } //switch
+
+ messageReceived(msg, msg.getAddress() != null ? (Member) msg.getAddress() : null);
+ }
+ }
+
+ /**
+ * When the request has been completed, the replication valve will notify
+ * the manager, and the manager will decide whether any replication is
+ * needed or not. If there is a need for replication, the manager will
+ * create a session message and that will be replicated. The cluster
+ * determines where it gets sent.
+ *
+ * @param sessionId -
+ * the sessionId that just completed.
+ * @return a SessionMessage to be sent,
+ */
+ public ClusterMessage requestCompleted(String sessionId) {
+ try {
+ DeltaSession session = (DeltaSession) findSession(sessionId);
+ DeltaRequest deltaRequest = session.getDeltaRequest();
+ SessionMessage msg = null;
+ boolean isDeltaRequest = false ;
+ synchronized(deltaRequest) {
+ isDeltaRequest = deltaRequest.getSize() > 0 ;
+ if (isDeltaRequest) {
+ counterSend_EVT_SESSION_DELTA++;
+ byte[] data = serializeDeltaRequest(deltaRequest);
+ msg = new SessionMessageImpl(getName(),
+ SessionMessage.EVT_SESSION_DELTA,
+ data,
+ sessionId,
+ sessionId + "-" + System.currentTimeMillis());
+ session.resetDeltaRequest();
+ }
+ }
+ if(!isDeltaRequest) {
+ if(!session.isPrimarySession()) {
+ counterSend_EVT_SESSION_ACCESSED++;
+ msg = new SessionMessageImpl(getName(),
+ SessionMessage.EVT_SESSION_ACCESSED,
+ null,
+ sessionId,
+ sessionId + "-" + System.currentTimeMillis());
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("deltaManager.createMessage.accessChangePrimary",getName(), sessionId));
+ }
+ }
+ } else { // log only outside synch block!
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("deltaManager.createMessage.delta",getName(), sessionId));
+ }
+ }
+ session.setPrimarySession(true);
+ //check to see if we need to send out an access message
+ if ((msg == null)) {
+ long replDelta = System.currentTimeMillis() - session.getLastTimeReplicated();
+ if (replDelta > (getMaxInactiveInterval() * 1000)) {
+ counterSend_EVT_SESSION_ACCESSED++;
+ msg = new SessionMessageImpl(getName(),
+ SessionMessage.EVT_SESSION_ACCESSED,
+ null,
+ sessionId,
+ sessionId + "-" + System.currentTimeMillis());
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("deltaManager.createMessage.access", getName(),sessionId));
+ }
+ }
+
+ }
+
+ //update last replicated time
+ if (msg != null) session.setLastTimeReplicated(System.currentTimeMillis());
+ return msg;
+ } catch (IOException x) {
+ log.error(sm.getString("deltaManager.createMessage.unableCreateDeltaRequest",sessionId), x);
+ return null;
+ }
+
+ }
+ /**
+ * Reset manager statistics
+ */
+ public synchronized void resetStatistics() {
+ processingTime = 0 ;
+ expiredSessions = 0 ;
+ rejectedSessions = 0 ;
+ sessionReplaceCounter = 0 ;
+ counterNoStateTransfered = 0 ;
+ maxActive = getActiveSessions() ;
+ sessionCounter = getActiveSessions() ;
+ counterReceive_EVT_ALL_SESSION_DATA = 0;
+ counterReceive_EVT_GET_ALL_SESSIONS = 0;
+ counterReceive_EVT_SESSION_ACCESSED = 0 ;
+ counterReceive_EVT_SESSION_CREATED = 0 ;
+ counterReceive_EVT_SESSION_DELTA = 0 ;
+ counterReceive_EVT_SESSION_EXPIRED = 0 ;
+ counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE = 0;
+ counterSend_EVT_ALL_SESSION_DATA = 0;
+ counterSend_EVT_GET_ALL_SESSIONS = 0;
+ counterSend_EVT_SESSION_ACCESSED = 0 ;
+ counterSend_EVT_SESSION_CREATED = 0 ;
+ counterSend_EVT_SESSION_DELTA = 0 ;
+ counterSend_EVT_SESSION_EXPIRED = 0 ;
+ counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE = 0;
+
+ }
+
+ // -------------------------------------------------------- persistence handler
+
+ public void load() {
+
+ }
+
+ public void unload() {
+
+ }
+
+ // -------------------------------------------------------- expire
+
+ /**
+ * send session expired to other cluster nodes
+ *
+ * @param id
+ * session id
+ */
+ protected void sessionExpired(String id) {
+ counterSend_EVT_SESSION_EXPIRED++ ;
+ SessionMessage msg = new SessionMessageImpl(getName(),SessionMessage.EVT_SESSION_EXPIRED, null, id, id+ "-EXPIRED-MSG");
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.createMessage.expire",getName(), id));
+ send(msg);
+ }
+
+ /**
+ * Exipre all find sessions.
+ */
+ public void expireAllLocalSessions()
+ {
+ long timeNow = System.currentTimeMillis();
+ Session sessions[] = findSessions();
+ int expireDirect = 0 ;
+ int expireIndirect = 0 ;
+
+ if(log.isDebugEnabled()) log.debug("Start expire all sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length);
+ for (int i = 0; i < sessions.length; i++) {
+ if (sessions[i] instanceof DeltaSession) {
+ DeltaSession session = (DeltaSession) sessions[i];
+ if (session.isPrimarySession()) {
+ if (session.isValid()) {
+ session.expire();
+ expireDirect++;
+ } else {
+ expireIndirect++;
+ }//end if
+ }//end if
+ }//end if
+ }//for
+ long timeEnd = System.currentTimeMillis();
+ if(log.isDebugEnabled()) log.debug("End expire sessions " + getName() + " exipre processingTime " + (timeEnd - timeNow) + " expired direct sessions: " + expireDirect + " expired direct sessions: " + expireIndirect);
+
+ }
+
+ /**
+ * When the manager expires session not tied to a request. The cluster will
+ * periodically ask for a list of sessions that should expire and that
+ * should be sent across the wire.
+ *
+ * @return The invalidated sessions array
+ */
+ public String[] getInvalidatedSessions() {
+ return new String[0];
+ }
+
+ // -------------------------------------------------------- message receive
+
+ /**
+ * Test that sender and local domain is the same
+ */
+ protected boolean checkSenderDomain(SessionMessage msg,Member sender) {
+ boolean sameDomain= true;
+ if (!sameDomain && log.isWarnEnabled()) {
+ log.warn(sm.getString("deltaManager.receiveMessage.fromWrongDomain",
+ new Object[] {getName(),
+ msg.getEventTypeString(),
+ sender,
+ "",
+ "" }));
+ }
+ return sameDomain ;
+ }
+
+ /**
+ * This method is called by the received thread when a SessionMessage has
+ * been received from one of the other nodes in the cluster.
+ *
+ * @param msg -
+ * the message received
+ * @param sender -
+ * the sender of the message, this is used if we receive a
+ * EVT_GET_ALL_SESSION message, so that we only reply to the
+ * requesting node
+ */
+ protected void messageReceived(SessionMessage msg, Member sender) {
+ if(isSendClusterDomainOnly() && !checkSenderDomain(msg,sender)) {
+ return;
+ }
+ ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
+ try {
+
+ ClassLoader[] loaders = getClassLoaders();
+ if ( loaders != null && loaders.length > 0) Thread.currentThread().setContextClassLoader(loaders[0]);
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.eventType",getName(), msg.getEventTypeString(), sender));
+
+ switch (msg.getEventType()) {
+ case SessionMessage.EVT_GET_ALL_SESSIONS: {
+ handleGET_ALL_SESSIONS(msg,sender);
+ break;
+ }
+ case SessionMessage.EVT_ALL_SESSION_DATA: {
+ handleALL_SESSION_DATA(msg,sender);
+ break;
+ }
+ case SessionMessage.EVT_ALL_SESSION_TRANSFERCOMPLETE: {
+ handleALL_SESSION_TRANSFERCOMPLETE(msg,sender);
+ break;
+ }
+ case SessionMessage.EVT_SESSION_CREATED: {
+ handleSESSION_CREATED(msg,sender);
+ break;
+ }
+ case SessionMessage.EVT_SESSION_EXPIRED: {
+ handleSESSION_EXPIRED(msg,sender);
+ break;
+ }
+ case SessionMessage.EVT_SESSION_ACCESSED: {
+ handleSESSION_ACCESSED(msg,sender);
+ break;
+ }
+ case SessionMessage.EVT_SESSION_DELTA: {
+ handleSESSION_DELTA(msg,sender);
+ break;
+ }
+ default: {
+ //we didn't recognize the message type, do nothing
+ break;
+ }
+ } //switch
+ } catch (Exception x) {
+ log.error(sm.getString("deltaManager.receiveMessage.error",getName()), x);
+ } finally {
+ Thread.currentThread().setContextClassLoader(contextLoader);
+ }
+ }
+
+ // -------------------------------------------------------- message receiver handler
+
+
+ /**
+ * handle receive session state is complete transfered
+ * @param msg
+ * @param sender
+ */
+ protected void handleALL_SESSION_TRANSFERCOMPLETE(SessionMessage msg, Member sender) {
+ counterReceive_EVT_ALL_SESSION_TRANSFERCOMPLETE++ ;
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.transfercomplete",getName(), sender.getHost(), new Integer(sender.getPort())));
+ stateTransferCreateSendTime = msg.getTimestamp() ;
+ stateTransfered = true ;
+ }
+
+ /**
+ * handle receive session delta
+ * @param msg
+ * @param sender
+ * @throws IOException
+ * @throws ClassNotFoundException
+ */
+ protected void handleSESSION_DELTA(SessionMessage msg, Member sender) throws IOException, ClassNotFoundException {
+ counterReceive_EVT_SESSION_DELTA++;
+ byte[] delta = msg.getSession();
+ DeltaSession session = (DeltaSession) findSession(msg.getSessionID());
+ if (session != null) {
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.delta",getName(), msg.getSessionID()));
+ DeltaRequest dreq = deserializeDeltaRequest(session, delta);
+ dreq.execute(session, notifyListenersOnReplication);
+ session.setPrimarySession(false);
+ }
+ }
+
+ /**
+ * handle receive session is access at other node ( primary session is now false)
+ * @param msg
+ * @param sender
+ * @throws IOException
+ */
+ protected void handleSESSION_ACCESSED(SessionMessage msg,Member sender) throws IOException {
+ counterReceive_EVT_SESSION_ACCESSED++;
+ DeltaSession session = (DeltaSession) findSession(msg.getSessionID());
+ if (session != null) {
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.accessed",getName(), msg.getSessionID()));
+ session.access();
+ session.setPrimarySession(false);
+ session.endAccess();
+ }
+ }
+
+ /**
+ * handle receive session is expire at other node ( expire session also here)
+ * @param msg
+ * @param sender
+ * @throws IOException
+ */
+ protected void handleSESSION_EXPIRED(SessionMessage msg,Member sender) throws IOException {
+ counterReceive_EVT_SESSION_EXPIRED++;
+ DeltaSession session = (DeltaSession) findSession(msg.getSessionID());
+ if (session != null) {
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.expired",getName(), msg.getSessionID()));
+ session.expire(notifySessionListenersOnReplication, false);
+ }
+ }
+
+ /**
+ * handle receive new session is created at other node (create backup - primary false)
+ * @param msg
+ * @param sender
+ */
+ protected void handleSESSION_CREATED(SessionMessage msg,Member sender) {
+ counterReceive_EVT_SESSION_CREATED++;
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.createNewSession",getName(), msg.getSessionID()));
+ DeltaSession session = (DeltaSession) createEmptySession();
+ session.setManager(this);
+ session.setValid(true);
+ session.setPrimarySession(false);
+ session.setCreationTime(msg.getTimestamp());
+ session.access();
+ if(notifySessionListenersOnReplication)
+ session.setId(msg.getSessionID());
+ else
+ session.setIdInternal(msg.getSessionID());
+ session.resetDeltaRequest();
+ session.endAccess();
+
+ }
+
+ /**
+ * handle receive sessions from other not ( restart )
+ * @param msg
+ * @param sender
+ * @throws ClassNotFoundException
+ * @throws IOException
+ */
+ protected void handleALL_SESSION_DATA(SessionMessage msg,Member sender) throws ClassNotFoundException, IOException {
+ counterReceive_EVT_ALL_SESSION_DATA++;
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.allSessionDataBegin",getName()));
+ byte[] data = msg.getSession();
+ deserializeSessions(data);
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.allSessionDataAfter",getName()));
+ //stateTransferred = true;
+ }
+
+ /**
+ * handle receive that other node want all sessions ( restart )
+ * a) send all sessions with one message
+ * b) send session at blocks
+ * After sending send state is complete transfered
+ * @param msg
+ * @param sender
+ * @throws IOException
+ */
+ protected void handleGET_ALL_SESSIONS(SessionMessage msg, Member sender) throws IOException {
+ counterReceive_EVT_GET_ALL_SESSIONS++;
+ //get a list of all the session from this manager
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.unloadingBegin", getName()));
+ // Write the number of active sessions, followed by the details
+ // get all sessions and serialize without sync
+ Session[] currentSessions = findSessions();
+ long findSessionTimestamp = System.currentTimeMillis() ;
+ if (isSendAllSessions()) {
+ sendSessions(sender, currentSessions, findSessionTimestamp);
+ } else {
+ // send session at blocks
+ int len = currentSessions.length < getSendAllSessionsSize() ? currentSessions.length : getSendAllSessionsSize();
+ Session[] sendSessions = new Session[len];
+ for (int i = 0; i < currentSessions.length; i += getSendAllSessionsSize()) {
+ len = i + getSendAllSessionsSize() > currentSessions.length ? currentSessions.length - i : getSendAllSessionsSize();
+ System.arraycopy(currentSessions, i, sendSessions, 0, len);
+ sendSessions(sender, sendSessions,findSessionTimestamp);
+ if (getSendAllSessionsWaitTime() > 0) {
+ try {
+ Thread.sleep(getSendAllSessionsWaitTime());
+ } catch (Exception sleep) {
+ }
+ }//end if
+ }//for
+ }//end if
+
+ SessionMessage newmsg = new SessionMessageImpl(name,SessionMessage.EVT_ALL_SESSION_TRANSFERCOMPLETE, null,"SESSION-STATE-TRANSFERED", "SESSION-STATE-TRANSFERED"+ getName());
+ newmsg.setTimestamp(findSessionTimestamp);
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.createMessage.allSessionTransfered",getName()));
+ counterSend_EVT_ALL_SESSION_TRANSFERCOMPLETE++;
+ cluster.send(newmsg, sender);
+ }
+
+
+ /**
+ * send a block of session to sender
+ * @param sender
+ * @param currentSessions
+ * @param sendTimestamp
+ * @throws IOException
+ */
+ protected void sendSessions(Member sender, Session[] currentSessions,long sendTimestamp) throws IOException {
+ byte[] data = serializeSessions(currentSessions);
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.receiveMessage.unloadingAfter",getName()));
+ SessionMessage newmsg = new SessionMessageImpl(name,SessionMessage.EVT_ALL_SESSION_DATA, data,"SESSION-STATE", "SESSION-STATE-" + getName());
+ newmsg.setTimestamp(sendTimestamp);
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaManager.createMessage.allSessionData",getName()));
+ counterSend_EVT_ALL_SESSION_DATA++;
+ cluster.send(newmsg, sender);
+ }
+
+}
diff --git a/java/org/apache/catalina/ha/session/DeltaRequest.java b/java/org/apache/catalina/ha/session/DeltaRequest.java
new file mode 100644
index 000000000..e81c07f3a
--- /dev/null
+++ b/java/org/apache/catalina/ha/session/DeltaRequest.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.session;
+
+/**
+ * This class is used to track the series of actions that happens when
+ * a request is executed. These actions will then translate into invokations of methods
+ * on the actual session.
+ * This class is NOT thread safe. One DeltaRequest per session
+ * @author Filip Hanik
+ * @version 1.0
+ */
+
+import java.io.Externalizable;
+import java.security.Principal;
+import java.util.LinkedList;
+
+import org.apache.catalina.realm.GenericPrincipal;
+import org.apache.catalina.util.StringManager;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+
+
+public class DeltaRequest implements Externalizable {
+
+ public static org.apache.commons.logging.Log log =
+ org.apache.commons.logging.LogFactory.getLog( DeltaRequest.class );
+
+ /**
+ * The string manager for this package.
+ */
+ protected static StringManager sm = StringManager
+ .getManager(Constants.Package);
+
+ public static final int TYPE_ATTRIBUTE = 0;
+ public static final int TYPE_PRINCIPAL = 1;
+ public static final int TYPE_ISNEW = 2;
+ public static final int TYPE_MAXINTERVAL = 3;
+
+ public static final int ACTION_SET = 0;
+ public static final int ACTION_REMOVE = 1;
+
+ public static final String NAME_PRINCIPAL = "__SET__PRINCIPAL__";
+ public static final String NAME_MAXINTERVAL = "__SET__MAXINTERVAL__";
+ public static final String NAME_ISNEW = "__SET__ISNEW__";
+
+ private String sessionId;
+ private LinkedList actions = new LinkedList();
+ private LinkedList actionPool = new LinkedList();
+
+ private boolean recordAllActions = false;
+
+ public DeltaRequest() {
+
+ }
+
+ public DeltaRequest(String sessionId, boolean recordAllActions) {
+ this.recordAllActions=recordAllActions;
+ if(sessionId != null)
+ setSessionId(sessionId);
+ }
+
+
+ public void setAttribute(String name, Object value) {
+ int action = (value==null)?ACTION_REMOVE:ACTION_SET;
+ addAction(TYPE_ATTRIBUTE,action,name,value);
+ }
+
+ public void removeAttribute(String name) {
+ int action = ACTION_REMOVE;
+ addAction(TYPE_ATTRIBUTE,action,name,null);
+ }
+
+ public void setMaxInactiveInterval(int interval) {
+ int action = ACTION_SET;
+ addAction(TYPE_MAXINTERVAL,action,NAME_MAXINTERVAL,new Integer(interval));
+ }
+
+ /**
+ * convert principal at SerializablePrincipal for backup nodes.
+ * Only support principals from type {@link GenericPrincipal GenericPrincipal}
+ * @param p Session principal
+ * @see GenericPrincipal
+ */
+ public void setPrincipal(Principal p) {
+ int action = (p==null)?ACTION_REMOVE:ACTION_SET;
+ SerializablePrincipal sp = null;
+ if ( p != null ) {
+ if(p instanceof GenericPrincipal) {
+ sp = SerializablePrincipal.createPrincipal((GenericPrincipal)p);
+ if(log.isDebugEnabled())
+ log.debug(sm.getString("deltaRequest.showPrincipal", p.getName() , getSessionId()));
+ } else
+ log.error(sm.getString("deltaRequest.wrongPrincipalClass",p.getClass().getName()));
+ }
+ addAction(TYPE_PRINCIPAL,action,NAME_PRINCIPAL,sp);
+ }
+
+ public void setNew(boolean n) {
+ int action = ACTION_SET;
+ addAction(TYPE_ISNEW,action,NAME_ISNEW,new Boolean(n));
+ }
+
+ protected synchronized void addAction(int type,
+ int action,
+ String name,
+ Object value) {
+ AttributeInfo info = null;
+ if ( this.actionPool.size() > 0 ) {
+ try {
+ info = (AttributeInfo) actionPool.removeFirst();
+ }catch ( Exception x ) {
+ log.error("Unable to remove element:",x);
+ info = new AttributeInfo(type, action, name, value);
+ }
+ info.init(type,action,name,value);
+ } else {
+ info = new AttributeInfo(type, action, name, value);
+ }
+ //if we have already done something to this attribute, make sure
+ //we don't send multiple actions across the wire
+ if ( !recordAllActions) {
+ try {
+ actions.remove(info);
+ } catch (java.util.NoSuchElementException x) {
+ //do nothing, we wanted to remove it anyway
+ }
+ }
+ //add the action
+ actions.addLast(info);
+ }
+
+ public void execute(DeltaSession session) {
+ execute(session,true);
+ }
+
+ public synchronized void execute(DeltaSession session, boolean notifyListeners) {
+ if ( !this.sessionId.equals( session.getId() ) )
+ throw new java.lang.IllegalArgumentException("Session id mismatch, not executing the delta request");
+ session.access();
+ for ( int i=0; iisNew flag for this session.
+ *
+ * @param isNew
+ * The new value for the isNew flag
+ */
+ public void setNew(boolean isNew) {
+ setNew(isNew, true);
+ }
+
+ public void setNew(boolean isNew, boolean addDeltaRequest) {
+ super.setNew(isNew);
+ if (addDeltaRequest && (deltaRequest != null))
+ deltaRequest.setNew(isNew);
+ }
+
+ /**
+ * Set the authenticated Principal that is associated with this Session.
+ * This provides an Authenticator with a means to cache a
+ * previously authenticated Principal, and avoid potentially expensive
+ * Realm.authenticate() calls on every request.
+ *
+ * @param principal
+ * The new Principal, or null if none
+ */
+ public void setPrincipal(Principal principal) {
+ setPrincipal(principal, true);
+ }
+
+ public void setPrincipal(Principal principal, boolean addDeltaRequest) {
+ try {
+ lock();
+ super.setPrincipal(principal);
+ if (addDeltaRequest && (deltaRequest != null))
+ deltaRequest.setPrincipal(principal);
+ } finally {
+ unlock();
+ }
+ }
+
+ /**
+ * Return the isValid flag for this session.
+ */
+ public boolean isValid() {
+ if (this.expiring) {
+ return true;
+ }
+ if (!this.isValid) {
+ return false;
+ }
+ if (accessCount.get() > 0) {
+ return true;
+ }
+ if (maxInactiveInterval >= 0) {
+ long timeNow = System.currentTimeMillis();
+ int timeIdle = (int) ( (timeNow - thisAccessedTime) / 1000L);
+ if (isPrimarySession()) {
+ if (timeIdle >= maxInactiveInterval) {
+ expire(true);
+ }
+ } else {
+ if (timeIdle >= (2 * maxInactiveInterval)) {
+ //if the session has been idle twice as long as allowed,
+ //the primary session has probably crashed, and no other
+ //requests are coming in. that is why we do this. otherwise
+ //we would have a memory leak
+ expire(true, false);
+ }
+ }
+ }
+ return (this.isValid);
+ }
+
+ // ------------------------------------------------- Session Public Methods
+
+ /**
+ * Perform the internal processing required to invalidate this session,
+ * without triggering an exception if the session has already expired.
+ *
+ * @param notify
+ * Should we notify listeners about the demise of this session?
+ */
+ public void expire(boolean notify) {
+ expire(notify, true);
+ }
+
+ public void expire(boolean notify, boolean notifyCluster) {
+ String expiredId = getIdInternal();
+ super.expire(notify);
+
+ if (notifyCluster) {
+ if (log.isDebugEnabled())
+ log.debug(sm.getString("deltaSession.notifying",
+ ((DeltaManager)manager).getName(),
+ new Boolean(isPrimarySession()),
+ expiredId));
+ if ( manager instanceof DeltaManager ) {
+ ( (DeltaManager) manager).sessionExpired(expiredId);
+ }
+ }
+ }
+
+ /**
+ * Release all object references, and initialize instance variables, in
+ * preparation for reuse of this object.
+ */
+ public void recycle() {
+ super.recycle();
+ deltaRequest.clear();
+ }
+
+
+ /**
+ * Return a string representation of this object.
+ */
+ public String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("DeltaSession[");
+ sb.append(id);
+ sb.append("]");
+ return (sb.toString());
+ }
+
+ // ------------------------------------------------ Session Package Methods
+
+ public synchronized void readExternal(ObjectInput in) throws IOException,ClassNotFoundException {
+ readObjectData(in);
+ }
+
+
+ /**
+ * Read a serialized version of the contents of this session object from the
+ * specified object input stream, without requiring that the StandardSession
+ * itself have been serialized.
+ *
+ * @param stream
+ * The object input stream to read from
+ *
+ * @exception ClassNotFoundException
+ * if an unknown class is specified
+ * @exception IOException
+ * if an input/output error occurs
+ */
+ public void readObjectData(ObjectInput stream) throws ClassNotFoundException, IOException {
+ readObject(stream);
+ }
+
+ /**
+ * Write a serialized version of the contents of this session object to the
+ * specified object output stream, without requiring that the
+ * StandardSession itself have been serialized.
+ *
+ * @param stream
+ * The object output stream to write to
+ *
+ * @exception IOException
+ * if an input/output error occurs
+ */
+ public void writeObjectData(ObjectOutput stream) throws IOException {
+ writeObject(stream);
+ }
+
+ public void resetDeltaRequest() {
+ if (deltaRequest == null) {
+ deltaRequest = new DeltaRequest(getIdInternal(), false);
+ } else {
+ deltaRequest.reset();
+ deltaRequest.setSessionId(getIdInternal());
+ }
+ }
+
+ public DeltaRequest getDeltaRequest() {
+ if (deltaRequest == null) resetDeltaRequest();
+ return deltaRequest;
+ }
+
+ // ------------------------------------------------- HttpSession Properties
+
+ // ----------------------------------------------HttpSession Public Methods
+
+
+
+ /**
+ * Remove the object bound with the specified name from this session. If the
+ * session does not have an object bound with this name, this method does
+ * nothing.
+ * HttpSessionBindingListener, the container calls
+ * valueUnbound() on the object.
+ *
+ * @param name
+ * Name of the object to remove from this session.
+ * @param notify
+ * Should we notify interested listeners that this attribute is
+ * being removed?
+ *
+ * @exception IllegalStateException
+ * if this method is called on an invalidated session
+ */
+ public void removeAttribute(String name, boolean notify) {
+ removeAttribute(name, notify, true);
+ }
+
+ public void removeAttribute(String name, boolean notify,boolean addDeltaRequest) {
+ // Validate our current state
+ if (!isValid()) throw new IllegalStateException(sm.getString("standardSession.removeAttribute.ise"));
+ removeAttributeInternal(name, notify, addDeltaRequest);
+ }
+
+ /**
+ * Bind an object to this session, using the specified name. If an object of
+ * the same name is already bound to this session, the object is replaced.
+ * HttpSessionBindingListener, the container calls
+ * valueBound() on the object.
+ *
+ * @param name
+ * Name to which the object is bound, cannot be null
+ * @param value
+ * Object to be bound, cannot be null
+ *
+ * @exception IllegalArgumentException
+ * if an attempt is made to add a non-serializable object in
+ * an environment marked distributable.
+ * @exception IllegalStateException
+ * if this method is called on an invalidated session
+ */
+ public void setAttribute(String name, Object value) {
+ setAttribute(name, value, true, true);
+ }
+
+ public void setAttribute(String name, Object value, boolean notify,boolean addDeltaRequest) {
+
+ // Name cannot be null
+ if (name == null) throw new IllegalArgumentException(sm.getString("standardSession.setAttribute.namenull"));
+
+ // Null value is the same as removeAttribute()
+ if (value == null) {
+ removeAttribute(name);
+ return;
+ }
+
+ try {
+ lock();
+ super.setAttribute(name,value, notify);
+ if (addDeltaRequest && (deltaRequest != null)) deltaRequest.setAttribute(name, value);
+ } finally {
+ unlock();
+ }
+ }
+
+ // -------------------------------------------- HttpSession Private Methods
+
+ /**
+ * Read a serialized version of this session object from the specified
+ * object input stream.
+ * readObject(), you must set the associated Manager
+ * explicitly.
+ * distributable property of the associated Manager
+ * is set to true.
+ *
+ * @param stream
+ * The output stream to write to
+ *
+ * @exception IOException
+ * if an input/output error occurs
+ */
+ private void writeObject(ObjectOutput stream) throws IOException {
+
+ // Write the scalar instance variables (except Manager)
+ stream.writeObject(new Long(creationTime));
+ stream.writeObject(new Long(lastAccessedTime));
+ stream.writeObject(new Integer(maxInactiveInterval));
+ stream.writeObject(new Boolean(isNew));
+ stream.writeObject(new Boolean(isValid));
+ stream.writeObject(new Long(thisAccessedTime));
+ stream.writeObject(new Long(version));
+ stream.writeBoolean(getPrincipal() != null);
+ if (getPrincipal() != null) {
+ SerializablePrincipal.writePrincipal((GenericPrincipal) principal,stream);
+ }
+
+ stream.writeObject(id);
+ if (log.isDebugEnabled()) log.debug(sm.getString("deltaSession.writeSession", id));
+
+ // Accumulate the names of serializable and non-serializable attributes
+ String keys[] = keys();
+ ArrayList saveNames = new ArrayList();
+ ArrayList saveValues = new ArrayList();
+ for (int i = 0; i < keys.length; i++) {
+ Object value = null;
+ value = attributes.get(keys[i]);
+ if (value == null)
+ continue;
+ else if (value instanceof Serializable) {
+ saveNames.add(keys[i]);
+ saveValues.add(value);
+ }
+ }
+
+ // Serialize the attribute count and the Serializable attributes
+ int n = saveNames.size();
+ stream.writeObject(new Integer(n));
+ for (int i = 0; i < n; i++) {
+ stream.writeObject( (String) saveNames.get(i));
+ try {
+ stream.writeObject(saveValues.get(i));
+ } catch (NotSerializableException e) {
+ log.error(sm.getString("standardSession.notSerializable",saveNames.get(i), id), e);
+ stream.writeObject(NOT_SERIALIZED);
+ log.error(" storing attribute '" + saveNames.get(i)+ "' with value NOT_SERIALIZED");
+ }
+ }
+
+ }
+
+ // -------------------------------------------------------- Private Methods
+
+
+
+ /**
+ * Return the value of an attribute without a check for validity.
+ */
+ protected Object getAttributeInternal(String name) {
+ return (attributes.get(name));
+ }
+
+ protected void removeAttributeInternal(String name, boolean notify,
+ boolean addDeltaRequest) {
+ try {
+ lock();
+ // Remove this attribute from our collection
+ Object value = attributes.get(name);
+ if (value == null) return;
+
+ super.removeAttributeInternal(name,notify);
+ if (addDeltaRequest && (deltaRequest != null)) deltaRequest.removeAttribute(name);
+
+ }finally {
+ unlock();
+ }
+ }
+
+ protected long getLastTimeReplicated() {
+ return lastTimeReplicated;
+ }
+
+ public long getVersion() {
+ return version;
+ }
+
+ protected void setLastTimeReplicated(long lastTimeReplicated) {
+ this.lastTimeReplicated = lastTimeReplicated;
+ }
+
+ public void setVersion(long version) {
+ this.version = version;
+ }
+
+ protected void setAccessCount(int count) {
+ super.accessCount.set(count);
+ }
+}
+
+// -------------------------------------------------------------- Private Class
+
+/**
+ * This class is a dummy implementation of the HttpSessionContext
+ * interface, to conform to the requirement that such an object be returned when
+ * HttpSession.getSessionContext() is called.
+ *
+ * @author Craig R. McClanahan
+ *
+ * @deprecated As of Java Servlet API 2.1 with no replacement. The interface
+ * will be removed in a future version of this API.
+ */
+
+final class StandardSessionContext
+ implements HttpSessionContext {
+
+ private HashMap dummy = new HashMap();
+
+ /**
+ * Return the session identifiers of all sessions defined within this
+ * context.
+ *
+ * @deprecated As of Java Servlet API 2.1 with no replacement. This method
+ * must return an empty Enumeration and will be
+ * removed in a future version of the API.
+ */
+ public Enumeration getIds() {
+ return (new Enumerator(dummy));
+ }
+
+ /**
+ * Return the HttpSession associated with the specified
+ * session identifier.
+ *
+ * @param id
+ * Session identifier for which to look up a session
+ *
+ * @deprecated As of Java Servlet API 2.1 with no replacement. This method
+ * must return null and will be removed in a future version of
+ * the API.
+ */
+ public HttpSession getSession(String id) {
+ return (null);
+ }
+
+}
diff --git a/java/org/apache/catalina/ha/session/JvmRouteBinderValve.java b/java/org/apache/catalina/ha/session/JvmRouteBinderValve.java
new file mode 100644
index 000000000..e77c3b322
--- /dev/null
+++ b/java/org/apache/catalina/ha/session/JvmRouteBinderValve.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.session;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+
+import org.apache.catalina.Container;
+import org.apache.catalina.Context;
+import org.apache.catalina.Engine;
+import org.apache.catalina.Globals;
+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.Session;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.ha.ClusterManager;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.ha.ClusterValve;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.catalina.session.ManagerBase;
+import org.apache.catalina.util.LifecycleSupport;
+import org.apache.catalina.util.StringManager;
+import org.apache.catalina.valves.ValveBase;
+
+/**
+ * Valve to handle Tomcat jvmRoute takeover using mod_jk module after node
+ * failure. After a node crashed the next request going to other cluster node.
+ * Now the answering from apache is slower ( make some error handshaking. Very
+ * bad with apache at my windows.). We rewrite now the cookie jsessionid
+ * information to the backup cluster node. After the next response all client
+ * request goes direct to the backup node. The change sessionid send also to all
+ * other cluster nodes. Well, now the session stickyness work directly to the
+ * backup node and traffic don't go back too restarted cluster nodes!
+ *
+ * At all cluster node you must configure the as ClusterListener since 5.5.10
+ * {@link org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener JvmRouteSessionIDBinderListener}
+ * or before with
+ * org.apache.catalina.ha.session.JvmRouteSessionIDBinderListenerLifecycle.
+ *
+ * Add this Valve to your host definition at conf/server.xml .
+ *
+ * Since 5.5.10 as direct cluster valve:
+ *
+ * <Cluster>
+ * <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" />
+ * </Cluster>
+ *
+ *
+ * Before 5.5.10 as Host element:
+ *
+ * <Hostr>
+ * <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" />
+ * </Hostr>
+ *
+ *
+ * Trick:
+ * You can enable this mod_jk turnover mode via JMX before you drop a node to all backup nodes!
+ * Set enable true on all JvmRouteBinderValve backups, disable worker at mod_jk
+ * and then drop node and restart it! Then enable mod_jk Worker and disable JvmRouteBinderValves again.
+ * This use case means that only requested session are migrated.
+ *
+ * @author Peter Rossbach
+ * @version $Revision: 326110 $ $Date: 2005-10-18 09:08:36 -0500 (Tue, 18 Oct 2005) $
+ */
+public class JvmRouteBinderValve extends ValveBase implements ClusterValve, Lifecycle {
+
+ /*--Static Variables----------------------------------------*/
+ public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory
+ .getLog(JvmRouteBinderValve.class);
+
+ /**
+ * The descriptive information about this implementation.
+ */
+ protected static final String info = "org.apache.catalina.ha.session.JvmRouteBinderValve/1.2";
+
+ /*--Instance Variables--------------------------------------*/
+
+ /**
+ * the cluster
+ */
+ protected CatalinaCluster cluster;
+
+ /**
+ * The string manager for this package.
+ */
+ protected StringManager sm = StringManager.getManager(Constants.Package);
+
+ /**
+ * Has this component been started yet?
+ */
+ protected boolean started = false;
+
+ /**
+ * enabled this component
+ */
+ protected boolean enabled = true;
+
+ /**
+ * number of session that no at this tomcat instanz hosted
+ */
+ protected long numberOfSessions = 0;
+
+ protected String sessionIdAttribute = "org.apache.catalina.ha.session.JvmRouteOrignalSessionID";
+
+ /**
+ * The lifecycle event support for this component.
+ */
+ protected LifecycleSupport lifecycle = new LifecycleSupport(this);
+
+ /*--Logic---------------------------------------------------*/
+
+ /**
+ * Return descriptive information about this implementation.
+ */
+ public String getInfo() {
+
+ return (info);
+
+ }
+
+ /**
+ * set session id attribute to failed node for request.
+ *
+ * @return Returns the sessionIdAttribute.
+ */
+ public String getSessionIdAttribute() {
+ return sessionIdAttribute;
+ }
+
+ /**
+ * get name of failed reqeust session attribute
+ *
+ * @param sessionIdAttribute
+ * The sessionIdAttribute to set.
+ */
+ public void setSessionIdAttribute(String sessionIdAttribute) {
+ this.sessionIdAttribute = sessionIdAttribute;
+ }
+
+ /**
+ * @return Returns the number of migrated sessions.
+ */
+ public long getNumberOfSessions() {
+ return numberOfSessions;
+ }
+
+ /**
+ * @return Returns the enabled.
+ */
+ public boolean getEnabled() {
+ return enabled;
+ }
+
+ /**
+ * @param enabled
+ * The enabled to set.
+ */
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ /**
+ * Detect possible the JVMRoute change at cluster backup node..
+ *
+ * @param request
+ * tomcat request being processed
+ * @param response
+ * tomcat response being processed
+ * @exception IOException
+ * if an input/output error has occurred
+ * @exception ServletException
+ * if a servlet error has occurred
+ */
+ public void invoke(Request request, Response response) throws IOException,
+ ServletException {
+
+ if (getEnabled()
+ && getCluster() != null
+ && request.getContext() != null
+ && request.getContext().getDistributable() ) {
+ // valve cluster can access manager - other cluster handle turnover
+ // at host level - hopefully!
+ Manager manager = request.getContext().getManager();
+ if (manager != null && manager instanceof ClusterManager
+ && getCluster().getManager(((ClusterManager)manager).getName()) != null)
+ handlePossibleTurnover(request, response);
+ }
+ // Pass this request on to the next valve in our pipeline
+ getNext().invoke(request, response);
+ }
+
+ /**
+ * handle possible session turn over.
+ *
+ * @see JvmRouteBinderValve#handleJvmRoute(Request, Response, String, String)
+ * @param request current request
+ * @param response current response
+ */
+ protected void handlePossibleTurnover(Request request, Response response) {
+ Session session = request.getSessionInternal(false);
+ if (session != null) {
+ long t1 = System.currentTimeMillis();
+ String jvmRoute = getLocalJvmRoute(request);
+ if (jvmRoute == null) {
+ if (log.isWarnEnabled())
+ log.warn(sm.getString("jvmRoute.missingJvmRouteAttribute"));
+ return;
+ }
+ handleJvmRoute( request, response,session.getIdInternal(), jvmRoute);
+ if (log.isDebugEnabled()) {
+ long t2 = System.currentTimeMillis();
+ long time = t2 - t1;
+ log.debug(sm.getString("jvmRoute.turnoverInfo", new Long(time)));
+ }
+ }
+ }
+
+ /**
+ * get jvmroute from engine
+ *
+ * @param request current request
+ * @return return jvmRoute from ManagerBase or null
+ */
+ protected String getLocalJvmRoute(Request request) {
+ Manager manager = getManager(request);
+ if(manager instanceof ManagerBase)
+ return ((ManagerBase) manager).getJvmRoute();
+ return null ;
+ }
+
+ /**
+ * get Cluster DeltaManager
+ *
+ * @param request current request
+ * @return manager or null
+ */
+ protected Manager getManager(Request request) {
+ Manager manager = request.getContext().getManager();
+ if (log.isDebugEnabled()) {
+ if(manager != null)
+ log.debug(sm.getString("jvmRoute.foundManager", manager, request.getContext().getName()));
+ else
+ log.debug(sm.getString("jvmRoute.notFoundManager", manager, request.getContext().getName()));
+ }
+ return manager;
+ }
+
+ /**
+ * @return Returns the cluster.
+ */
+ public CatalinaCluster getCluster() {
+ return cluster;
+ }
+
+ /**
+ * @param cluster The cluster to set.
+ */
+ public void setCluster(CatalinaCluster cluster) {
+ this.cluster = cluster;
+ }
+
+ /**
+ * Handle jvmRoute stickyness after tomcat instance failed. After this
+ * correction a new Cookie send to client with new jvmRoute and the
+ * SessionID change propage to the other cluster nodes.
+ *
+ * @param request current request
+ * @param response
+ * Tomcat Response
+ * @param sessionId
+ * request SessionID from Cookie
+ * @param localJvmRoute
+ * local jvmRoute
+ */
+ protected void handleJvmRoute(
+ Request request, Response response,String sessionId, String localJvmRoute) {
+ // get requested jvmRoute.
+ String requestJvmRoute = null;
+ int index = sessionId.indexOf(".");
+ if (index > 0) {
+ requestJvmRoute = sessionId
+ .substring(index + 1, sessionId.length());
+ }
+ if (requestJvmRoute != null && !requestJvmRoute.equals(localJvmRoute)) {
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("jvmRoute.failover", requestJvmRoute,
+ localJvmRoute, sessionId));
+ }
+ // OK - turnover the session ?
+ String newSessionID = sessionId.substring(0, index) + "."
+ + localJvmRoute;
+ Session catalinaSession = null;
+ try {
+ catalinaSession = getManager(request).findSession(sessionId);
+ } catch (IOException e) {
+ // Hups!
+ }
+ if (catalinaSession != null) {
+ changeSessionID(request, response, sessionId, newSessionID,
+ catalinaSession);
+ numberOfSessions++;
+ } else {
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("jvmRoute.cannotFindSession",
+ sessionId));
+ }
+ }
+ }
+ }
+
+ /**
+ * change session id and send to all cluster nodes
+ *
+ * @param request current request
+ * @param response current response
+ * @param sessionId
+ * original session id
+ * @param newSessionID
+ * new session id for node migration
+ * @param catalinaSession
+ * current session with original session id
+ */
+ protected void changeSessionID(Request request,
+ Response response, String sessionId, String newSessionID, Session catalinaSession) {
+ lifecycle.fireLifecycleEvent("Before session migration",
+ catalinaSession);
+ request.setRequestedSessionId(newSessionID);
+ catalinaSession.setId(newSessionID);
+ if (catalinaSession instanceof DeltaSession)
+ ((DeltaSession) catalinaSession).resetDeltaRequest();
+ if(request.isRequestedSessionIdFromCookie()) setNewSessionCookie(request, response,newSessionID);
+ // set orginal sessionid at request, to allow application detect the
+ // change
+ if (sessionIdAttribute != null && !"".equals(sessionIdAttribute)) {
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("jvmRoute.set.orignalsessionid",sessionIdAttribute,sessionId));
+ }
+ request.setAttribute(sessionIdAttribute, sessionId);
+ }
+ // now sending the change to all other clusternode!
+ ClusterManager manager = (ClusterManager)catalinaSession.getManager();
+ sendSessionIDClusterBackup(manager,request,sessionId, newSessionID);
+ lifecycle
+ .fireLifecycleEvent("After session migration", catalinaSession);
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("jvmRoute.changeSession", sessionId,
+ newSessionID));
+ }
+ }
+
+ /**
+ * Send the changed Sessionid to all clusternodes.
+ *
+ * @see JvmRouteSessionIDBinderListener#messageReceived(ClusterMessage)
+ * @param manager
+ * ClusterManager
+ * @param sessionId
+ * current failed sessionid
+ * @param newSessionID
+ * new session id, bind to the new cluster node
+ */
+ protected void sendSessionIDClusterBackup(ClusterManager manager,Request request,String sessionId,
+ String newSessionID) {
+ SessionIDMessage msg = new SessionIDMessage();
+ msg.setOrignalSessionID(sessionId);
+ msg.setBackupSessionID(newSessionID);
+ Context context = request.getContext();
+ msg.setContextPath(context.getPath());
+ msg.setHost(context.getParent().getName());
+ if(manager.isSendClusterDomainOnly())
+ cluster.sendClusterDomain(msg);
+ else
+ cluster.send(msg);
+ }
+
+ /**
+ * Sets a new cookie for the given session id and response and see
+ * {@link org.apache.catalina.connector.Request#configureSessionCookie(javax.servlet.http.Cookie)}
+ *
+ * @param request current request
+ * @param response Tomcat Response
+ * @param sessionId The session id
+ */
+ protected void setNewSessionCookie(Request request,
+ Response response, String sessionId) {
+ if (response != null) {
+ Context context = request.getContext();
+ if (context.getCookies()) {
+ // set a new session cookie
+ Cookie newCookie = new Cookie(Globals.SESSION_COOKIE_NAME,
+ sessionId);
+ newCookie.setMaxAge(-1);
+ String contextPath = null;
+ if (!response.getConnector().getEmptySessionPath()
+ && (context != null)) {
+ contextPath = context.getEncodedPath();
+ }
+ if ((contextPath != null) && (contextPath.length() > 0)) {
+ newCookie.setPath(contextPath);
+ } else {
+ newCookie.setPath("/");
+ }
+ if (request.isSecure()) {
+ newCookie.setSecure(true);
+ }
+ if (log.isDebugEnabled()) {
+ log.debug(sm.getString("jvmRoute.newSessionCookie",
+ sessionId, Globals.SESSION_COOKIE_NAME, newCookie
+ .getPath(), new Boolean(newCookie
+ .getSecure())));
+ }
+ response.addCookie(newCookie);
+ }
+ }
+ }
+
+ // ------------------------------------------------------ Lifecycle Methods
+
+ /**
+ * Add a lifecycle event listener to this component.
+ *
+ * @param listener
+ * The listener to add
+ */
+ public void addLifecycleListener(LifecycleListener listener) {
+
+ lifecycle.addLifecycleListener(listener);
+
+ }
+
+ /**
+ * Get the lifecycle listeners associated with this lifecycle. If this
+ * Lifecycle has no listeners registered, a zero-length array is returned.
+ */
+ public LifecycleListener[] findLifecycleListeners() {
+
+ return lifecycle.findLifecycleListeners();
+
+ }
+
+ /**
+ * Remove a lifecycle event listener from this component.
+ *
+ * @param listener
+ * The listener to add
+ */
+ public void removeLifecycleListener(LifecycleListener listener) {
+
+ lifecycle.removeLifecycleListener(listener);
+
+ }
+
+ /**
+ * Prepare for the beginning of active use of the public methods of this
+ * component. This method should be called after configure(),
+ * and before any of the public methods of the component are utilized.
+ *
+ * @exception LifecycleException
+ * if this component detects a fatal error that prevents this
+ * component from being used
+ */
+ public void start() throws LifecycleException {
+
+ // Validate and update our current component state
+ if (started)
+ throw new LifecycleException(sm
+ .getString("jvmRoute.valve.alreadyStarted"));
+ lifecycle.fireLifecycleEvent(START_EVENT, null);
+ started = true;
+ if (cluster == null) {
+ Container hostContainer = getContainer();
+ // compatibility with JvmRouteBinderValve version 1.1
+ // ( setup at context.xml or context.xml.default )
+ if (!(hostContainer instanceof Host)) {
+ if (log.isWarnEnabled())
+ log.warn(sm.getString("jvmRoute.configure.warn"));
+ hostContainer = hostContainer.getParent();
+ }
+ if (hostContainer instanceof Host
+ && ((Host) hostContainer).getCluster() != null) {
+ cluster = (CatalinaCluster) ((Host) hostContainer).getCluster();
+ } else {
+ Container engine = hostContainer.getParent() ;
+ if (engine instanceof Engine
+ && ((Engine) engine).getCluster() != null) {
+ cluster = (CatalinaCluster) ((Engine) engine).getCluster();
+ }
+ }
+ }
+ if (cluster == null) {
+ throw new RuntimeException("No clustering support at container "
+ + container.getName());
+ }
+
+ if (log.isInfoEnabled())
+ log.info(sm.getString("jvmRoute.valve.started"));
+
+ }
+
+ /**
+ * Gracefully terminate the active use of the public methods of this
+ * component. This method should be the last one called on a given instance
+ * of this component.
+ *
+ * @exception LifecycleException
+ * if this component detects a fatal error that needs to be
+ * reported
+ */
+ public void stop() throws LifecycleException {
+
+ // Validate and update our current component state
+ if (!started)
+ throw new LifecycleException(sm
+ .getString("jvmRoute.valve.notStarted"));
+ lifecycle.fireLifecycleEvent(STOP_EVENT, null);
+ started = false;
+ cluster = null;
+ numberOfSessions = 0;
+ if (log.isInfoEnabled())
+ log.info(sm.getString("jvmRoute.valve.stopped"));
+
+ }
+
+}
diff --git a/java/org/apache/catalina/ha/session/JvmRouteSessionIDBinderLifecycleListener.java b/java/org/apache/catalina/ha/session/JvmRouteSessionIDBinderLifecycleListener.java
new file mode 100644
index 000000000..316238be2
--- /dev/null
+++ b/java/org/apache/catalina/ha/session/JvmRouteSessionIDBinderLifecycleListener.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.session;
+
+import javax.management.DynamicMBean;
+import javax.management.MBeanServer;
+import javax.management.MBeanServerFactory;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleEvent;
+import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.core.StandardHost;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.ha.ClusterListener;
+import org.apache.catalina.util.StringManager;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.tomcat.util.modeler.ManagedBean;
+import org.apache.tomcat.util.modeler.Registry;
+
+/**
+ * Register new JvmRouteSessionIDBinderListener to receive Session ID changes.
+ *
+ * add following at your server.xml Host section
+ *
+ *
+ * <Host >...
+ * <Listener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderLifecycleListener" />
+ * <Cluster ...>
+ * </Host>
+ *
+ * FIXME add Engine support
+ * @deprecated
+ * @author Peter Rossbach
+ */
+public class JvmRouteSessionIDBinderLifecycleListener implements
+ LifecycleListener {
+ private static Log log = LogFactory
+ .getLog(JvmRouteSessionIDBinderLifecycleListener.class);
+
+ /**
+ * The descriptive information string for this implementation.
+ */
+ private static final String info = "org.apache.catalina.ha.session.JvmRouteSessionIDBinderLifecycleListener/1.0";
+
+ /**
+ * The string resources for this package.
+ */
+ protected static final StringManager sm = StringManager
+ .getManager(Constants.Package);
+
+ private boolean enabled = true;
+
+ private MBeanServer mserver = null;
+
+ private Registry registry = null;
+
+ private ClusterListener sessionMoverListener;
+
+ /*
+ * start and stop cluster
+ *
+ * @see org.apache.catalina.LifecycleListener#lifecycleEvent(org.apache.catalina.LifecycleEvent)
+ */
+ public void lifecycleEvent(LifecycleEvent event) {
+
+ if (enabled && event.getSource() instanceof StandardHost) {
+
+ if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) {
+ if (log.isDebugEnabled())
+ log.debug(sm.getString("jvmRoute.listener.started"));
+ startSessionIDListener((StandardHost) event.getSource());
+ } else if (Lifecycle.BEFORE_STOP_EVENT.equals(event.getType())) {
+ if (log.isDebugEnabled())
+ log.debug(sm.getString("jvmRoute.listener.stopped"));
+ stopSessionIDListener((StandardHost) event.getSource());
+ }
+ }
+ }
+
+ /**
+ * stop sessionID binder at cluster
+ *
+ * @param host
+ * clustered host
+ */
+ protected void stopSessionIDListener(StandardHost host) {
+ if (sessionMoverListener != null) {
+ CatalinaCluster cluster = (CatalinaCluster) host.getCluster();
+ cluster.removeClusterListener(sessionMoverListener);
+ if (mserver != null) {
+ try {
+ ObjectName objectName = getObjectName(host);
+ mserver.unregisterMBean(objectName);
+ } catch (Exception e) {
+ log.error(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param host
+ * @return The object name
+ * @throws MalformedObjectNameException
+ */
+ protected ObjectName getObjectName(StandardHost host) throws MalformedObjectNameException {
+ ObjectName objectName = new ObjectName(
+ host.getDomain()
+ + ":type=Listener,name=JvmRouteSessionIDBinderListener,host=" + host.getName());
+ return objectName;
+ }
+
+ /**
+ * start sessionID mover at cluster
+ *
+ * @param host
+ * clustered host
+ */
+ protected void startSessionIDListener(StandardHost host) {
+ try {
+ ObjectName objectName = null;
+ getMBeanServer();
+ objectName = getObjectName(host);
+ if (mserver.isRegistered(objectName)) {
+ if (log.isInfoEnabled())
+ log.info(sm.getString("jvmRoute.run.already"));
+ return;
+ }
+ sessionMoverListener = new JvmRouteSessionIDBinderListener();
+ mserver.registerMBean(getManagedBean(sessionMoverListener),
+ objectName);
+ CatalinaCluster cluster = (CatalinaCluster) host.getCluster();
+ sessionMoverListener.setCluster(cluster);
+ ((JvmRouteSessionIDBinderListener) sessionMoverListener).start();
+ } catch (Exception ex) {
+ log.error(ex.getMessage(), ex);
+ }
+ }
+
+ protected MBeanServer getMBeanServer() throws Exception {
+ if (mserver == null) {
+ if (MBeanServerFactory.findMBeanServer(null).size() > 0) {
+ mserver = (MBeanServer) MBeanServerFactory
+ .findMBeanServer(null).get(0);
+ } else {
+ mserver = MBeanServerFactory.createMBeanServer();
+ }
+ registry = Registry.getRegistry(null, null);
+ registry.loadMetadata(this.getClass().getResourceAsStream(
+ "mbeans-descriptors.xml"));
+ }
+ return (mserver);
+ }
+
+ /**
+ * Returns the ModelMBean
+ *
+ * @param object
+ * The Object to get the ModelMBean for
+ * @return The ModelMBean
+ * @throws Exception
+ * If an error occurs this constructors throws this exception
+ */
+ protected DynamicMBean getManagedBean(Object object) throws Exception {
+ DynamicMBean mbean = null;
+ if (registry != null) {
+ ManagedBean managedBean = registry.findManagedBean(object
+ .getClass().getName());
+ mbean = managedBean.createMBean(object);
+ }
+ return mbean;
+ }
+
+ /**
+ * @return Returns the enabled.
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * @param enabled
+ * The enabled to set.
+ */
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ /**
+ * Return descriptive information about this Listener implementation and the
+ * corresponding version number, in the format
+ * <description>/<version>.
+ */
+ public String getInfo() {
+
+ return (info);
+
+ }
+}
diff --git a/java/org/apache/catalina/ha/session/JvmRouteSessionIDBinderListener.java b/java/org/apache/catalina/ha/session/JvmRouteSessionIDBinderListener.java
new file mode 100644
index 000000000..3d23939a9
--- /dev/null
+++ b/java/org/apache/catalina/ha/session/JvmRouteSessionIDBinderListener.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.session;
+
+import java.io.IOException;
+
+import org.apache.catalina.Container;
+import org.apache.catalina.Context;
+import org.apache.catalina.Engine;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.Session;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.core.StandardEngine;
+import org.apache.catalina.ha.*;
+
+/**
+ * Receive SessionID cluster change from other backup node after primary session
+ * node is failed.
+ *
+ * @author Peter Rossbach
+ * @version $Revision: 378258 $ $Date: 2006-02-16 08:42:35 -0600 (Thu, 16 Feb 2006) $
+ */
+public class JvmRouteSessionIDBinderListener extends ClusterListener {
+
+ /**
+ * The descriptive information about this implementation.
+ */
+ protected static final String info = "org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener/1.1";
+
+ //--Instance Variables--------------------------------------
+
+
+ protected boolean started = false;
+
+ /**
+ * number of session that goes to this cluster node
+ */
+ private long numberOfSessions = 0;
+
+ //--Constructor---------------------------------------------
+
+ public JvmRouteSessionIDBinderListener() {
+ }
+
+ //--Logic---------------------------------------------------
+
+ /**
+ * Return descriptive information about this implementation.
+ */
+ public String getInfo() {
+
+ return (info);
+
+ }
+
+ /**
+ * @return Returns the numberOfSessions.
+ */
+ public long getNumberOfSessions() {
+ return numberOfSessions;
+ }
+
+ /**
+ * Add this Mover as Cluster Listener ( receiver)
+ *
+ * @throws LifecycleException
+ */
+ public void start() throws LifecycleException {
+ if (started)
+ return;
+ getCluster().addClusterListener(this);
+ started = true;
+ if (log.isInfoEnabled())
+ log.info(sm.getString("jvmRoute.clusterListener.started"));
+ }
+
+ /**
+ * Remove this from Cluster Listener
+ *
+ * @throws LifecycleException
+ */
+ public void stop() throws LifecycleException {
+ started = false;
+ getCluster().removeClusterListener(this);
+ if (log.isInfoEnabled())
+ log.info(sm.getString("jvmRoute.clusterListener.stopped"));
+ }
+
+ /**
+ * Callback from the cluster, when a message is received, The cluster will
+ * broadcast it invoking the messageReceived on the receiver.
+ *
+ * @param msg
+ * ClusterMessage - the message received from the cluster
+ */
+ public void messageReceived(ClusterMessage msg) {
+ if (msg instanceof SessionIDMessage && msg != null) {
+ SessionIDMessage sessionmsg = (SessionIDMessage) msg;
+ if (log.isDebugEnabled())
+ log.debug(sm.getString(
+ "jvmRoute.receiveMessage.sessionIDChanged", sessionmsg
+ .getOrignalSessionID(), sessionmsg
+ .getBackupSessionID(), sessionmsg
+ .getContextPath()));
+ Container container = getCluster().getContainer();
+ Container host = null ;
+ if(container instanceof Engine) {
+ host = container.findChild(sessionmsg.getHost());
+ } else {
+ host = container ;
+ }
+ if (host != null) {
+ Context context = (Context) host.findChild(sessionmsg
+ .getContextPath());
+ if (context != null) {
+ try {
+ Session session = context.getManager().findSession(
+ sessionmsg.getOrignalSessionID());
+ if (session != null) {
+ session.setId(sessionmsg.getBackupSessionID());
+ } else if (log.isInfoEnabled())
+ log.info(sm.getString("jvmRoute.lostSession",
+ sessionmsg.getOrignalSessionID(),
+ sessionmsg.getContextPath()));
+ } catch (IOException e) {
+ log.error(e);
+ }
+
+ } else if (log.isErrorEnabled())
+ log.error(sm.getString("jvmRoute.contextNotFound",
+ sessionmsg.getContextPath(), ((StandardEngine) host
+ .getParent()).getJvmRoute()));
+ } else if (log.isErrorEnabled())
+ log.error(sm.getString("jvmRoute.hostNotFound", sessionmsg.getContextPath()));
+ }
+ return;
+ }
+
+ /**
+ * Accept only SessionIDMessages
+ *
+ * @param msg
+ * ClusterMessage
+ * @return boolean - returns true to indicate that messageReceived should be
+ * invoked. If false is returned, the messageReceived method will
+ * not be invoked.
+ */
+ public boolean accept(ClusterMessage msg) {
+ return (msg instanceof SessionIDMessage);
+ }
+}
+
diff --git a/java/org/apache/catalina/ha/session/LocalStrings.properties b/java/org/apache/catalina/ha/session/LocalStrings.properties
new file mode 100644
index 000000000..acf470549
--- /dev/null
+++ b/java/org/apache/catalina/ha/session/LocalStrings.properties
@@ -0,0 +1,96 @@
+deltaManager.createSession.ise=createSession: Too many active sessions
+deltaManager.createSession.newSession=Created a DeltaSession with Id [{0}] Total count={1}
+deltaManager.createMessage.access=Manager [{0}]: create session message [{1}] access.
+deltaManager.createMessage.accessChangePrimary=Manager [{0}]: create session message [{1}] access to change primary.
+deltaManager.createMessage.allSessionData=Manager [{0}] send all session data.
+deltaManager.createMessage.allSessionTransfered=Manager [{0}] send all session data transfered
+deltaManager.createMessage.delta=Manager [{0}]: create session message [{1}] delta request.
+deltaManager.createMessage.expire=Manager [{0}]: create session message [{1}] expire.
+deltaManager.createMessage.unableCreateDeltaRequest=Unable to serialize delta request for sessionid [{0}]
+deltaManager.dropMessage=Manager [{0}]: Drop message {1} inside GET_ALL_SESSIONS sync phase start date {2} message date {3}
+deltaManager.foundMasterMember=Found for context [{0}] the replication master member [{1}]
+deltaManager.loading.cnfe=ClassNotFoundException while loading persisted sessions: {0}
+deltaManager.loading.existing.session=overload existing session {0}
+deltaManager.loading.ioe=IOException while loading persisted sessions: {0}
+deltaManager.loading.withContextClassLoader=Manager [{0}]: Loading the object data with a context class loader.
+deltaManager.loading.withoutClassLoader=Manager [{0}]: Loading the object data without a context class loader.
+deltaManager.managerLoad=Exception loading sessions from persistent storage
+deltaManager.noCluster=Starting... no cluster associated with this context: [{0}]
+deltaManager.noMasterMember=Starting... with no other member for context [{0}] at domain [{1}]
+deltaManager.noMembers=Manager [{0}]: skipping state transfer. No members active in cluster group.
+deltaManager.noSessionState=Manager [{0}]: No session state send at {1} received, timing out after {2} ms.
+deltaManager.notStarted=Manager has not yet been started
+deltaManager.sendMessage.newSession=Manager [{0}] send new session ({1})
+deltaManager.expireSessions=Manager [{0}] expiring sessions upon shutdown
+deltaManager.receiveMessage.accessed=Manager [{0}]: received session [{1}] accessed.
+deltaManager.receiveMessage.createNewSession=Manager [{0}]: received session [{1}] created.
+deltaManager.receiveMessage.delta=Manager [{0}]: received session [{1}] delta.
+deltaManager.receiveMessage.error=Manager [{0}]: Unable to receive message through TCP channel
+deltaManager.receiveMessage.eventType=Manager [{0}]: Received SessionMessage of type=({1}) from [{2}]
+deltaManager.receiveMessage.expired=Manager [{0}]: received session [{1}] expired.
+deltaManager.receiveMessage.transfercomplete=Manager [{0}] received from node [{1}:{2}] session state transfered.
+deltaManager.receiveMessage.unloadingAfter=Manager [{0}]: unloading sessions complete
+deltaManager.receiveMessage.unloadingBegin=Manager [{0}]: start unloading sessions
+deltaManager.receiveMessage.allSessionDataAfter=Manager [{0}]: session state deserialized
+deltaManager.receiveMessage.allSessionDataBegin=Manager [{0}]: received session state data
+deltaManager.receiveMessage.fromWrongDomain=Manager [{0}]: Received wrong SessionMessage of type=({1}) from [{2}] with domain [{3}] (localdomain [{4}]
+deltaManager.registerCluster=Register manager {0} to cluster element {1} with name {2}
+deltaManager.sessionReceived=Manager [{0}]; session state send at {1} received in {2} ms.
+deltaManager.sessionTimeout=Invalid session timeout setting {0}
+deltaManager.startClustering=Starting clustering manager at {0}
+deltaManager.stopped=Manager [{0}] is stopping
+deltaManager.unloading.ioe=IOException while saving persisted sessions: {0}
+deltaManager.waitForSessionState=Manager [{0}], requesting session state from {1}. This operation will timeout if no session state has been received within 60 seconds.
+deltaRequest.showPrincipal=Principal [{0}] is set to session {1}
+deltaRequest.wrongPrincipalClass=DeltaManager only support GenericPrincipal. Your realm used principal class {0}.
+deltaSession.notifying=Notifying cluster of expiration primary={0} sessionId [{1}]
+deltaSession.valueBound.ex=Session bound listener throw an exception
+deltaSession.valueBinding.ex=Session binding listener throw an exception
+deltaSession.valueUnbound.ex=Session unbound listener throw an exception
+deltaSession.readSession=readObject() loading session [{0}]
+deltaSession.readAttribute=session [{0}] loading attribute '{1}' with value '{2}'
+deltaSession.writeSession=writeObject() storing session [{0}]
+jvmRoute.cannotFindSession=Can't find session [{0}]
+jvmRoute.changeSession=Changed session from [{0}] to [{1}]
+jvmRoute.clusterListener.started=Cluster JvmRouteSessionIDBinderListener started
+jvmRoute.clusterListener.stopped=Cluster JvmRouteSessionIDBinderListener stopped
+jvmRoute.configure.warn=Please, setup your JvmRouteBinderValve at host valve, not at context valve!
+jvmRoute.contextNotFound=Context [{0}] not found at node [{1}]!
+jvmRoute.failover=Detected a failover with different jvmRoute - orginal route: [{0}] new one: [{1}] at session id [{2}]
+jvmRoute.foundManager=Found Cluster DeltaManager {0} at {1}
+jvmRoute.hostNotFound=No host found [{0}]
+jvmRoute.listener.started=SessionID Binder Listener started
+jvmRoute.listener.stopped=SessionID Binder Listener stopped
+jvmRoute.lostSession=Lost Session [{0}] at path [{1}]
+jvmRoute.missingJvmRouteAttribute=No engine jvmRoute attribute configured!
+jvmRoute.newSessionCookie=Setting cookie with session id [{0}] name: [{1}] path: [{2}] secure: [{3}]
+jvmRoute.notFoundManager=Not found Cluster DeltaManager {0} at {1}
+jvmRoute.receiveMessage.sessionIDChanged=Cluster JvmRouteSessionIDBinderListener received orginal session ID [{0}] set to new id [{1}] for context path [{2}]
+jvmRoute.run.already=jvmRoute SessionID receiver run already
+jvmRoute.skipURLSessionIDs=Skip reassign jvm route check, sessionid comes from URL!
+jvmRoute.turnoverInfo=Turnover Check time {0} msec
+jvmRoute.valve.alreadyStarted=jvmRoute backup sessionID correction is started
+jvmRoute.valve.notStarted=jvmRoute backup sessionID correction run already
+jvmRoute.valve.started=JvmRouteBinderValve started
+jvmRoute.valve.stopped=JvmRouteBinderValve stopped
+jvmRoute.set.orignalsessionid=Set Orginal Session id at request attriute {0} value: {1}
+standardSession.getId.ise=getId: Session already invalidated
+standardSession.attributeEvent=Session attribute event listener threw exception
+standardSession.attributeEvent=Session attribute event listener threw exception
+standardSession.bindingEvent=Session binding event listener threw exception
+standardSession.invalidate.ise=invalidate: Session already invalidated
+standardSession.isNew.ise=isNew: Session already invalidated
+standardSession.getAttribute.ise=getAttribute: Session already invalidated
+standardSession.getAttributeNames.ise=getAttributeNames: Session already invalidated
+standardSession.getCreationTime.ise=getCreationTime: Session already invalidated
+standardSession.getLastAccessedTime.ise=getLastAccessedTime: Session already invalidated
+standardSession.getId.ise=getId: Session already invalidated
+standardSession.getMaxInactiveInterval.ise=getMaxInactiveInterval: Session already invalidated
+standardSession.getValueNames.ise=getValueNames: Session already invalidated
+standardSession.notSerializable=Cannot serialize session attribute {0} for session {1}
+standardSession.removeAttribute.ise=removeAttribute: Session already invalidated
+standardSession.sessionEvent=Session event listener threw exception
+standardSession.setAttribute.iae=setAttribute: Non-serializable attribute
+standardSession.setAttribute.ise=setAttribute: Session already invalidated
+standardSession.setAttribute.namenull=setAttribute: name parameter cannot be null
+standardSession.sessionCreated=Created Session id = {0}
diff --git a/java/org/apache/catalina/ha/session/ReplicatedSession.java b/java/org/apache/catalina/ha/session/ReplicatedSession.java
new file mode 100644
index 000000000..7f4017a19
--- /dev/null
+++ b/java/org/apache/catalina/ha/session/ReplicatedSession.java
@@ -0,0 +1,285 @@
+
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.session;
+
+/**
+ * Title: Tomcat Session Replication for Tomcat 4.0
+ * Description: A very simple straight forward implementation of
+ * session replication of servers in a cluster.
+ * This session replication is implemented "live". By live
+ * I mean, when a session attribute is added into a session on Node A
+ * a message is broadcasted to other messages and setAttribute is called on the replicated
+ * sessions.
+ * A full description of this implementation can be found under
+ *
+ *
+ * Copyright: See apache license
+ * @author Filip Hanik
+ * @version $Revision: 303842 $ $Date: 2005-04-10 11:20:46 -0500 (Sun, 10 Apr 2005) $
+ * Description:
+ * The ReplicatedSession class is a simple extension of the StandardSession class
+ * It overrides a few methods (setAttribute, removeAttribute, expire, access) and has
+ * hooks into the InMemoryReplicationManager to broadcast and receive events from the cluster.
+ * This class inherits the readObjectData and writeObject data methods from the StandardSession
+ * and does not contain any serializable elements in addition to the inherited ones from the StandardSession
+ *
+ */
+import org.apache.catalina.Manager;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.security.Principal;
+
+public class ReplicatedSession extends org.apache.catalina.session.StandardSession
+implements org.apache.catalina.ha.ClusterSession{
+
+ private transient Manager mManager = null;
+ protected boolean isDirty = false;
+ private transient long lastAccessWasDistributed = System.currentTimeMillis();
+ private boolean isPrimarySession=true;
+
+
+ public ReplicatedSession(Manager manager) {
+ super(manager);
+ mManager = manager;
+ }
+
+
+ public boolean isDirty()
+ {
+ return isDirty;
+ }
+
+ public void setIsDirty(boolean dirty)
+ {
+ isDirty = dirty;
+ }
+
+
+ public void setLastAccessWasDistributed(long time) {
+ lastAccessWasDistributed = time;
+ }
+
+ public long getLastAccessWasDistributed() {
+ return lastAccessWasDistributed;
+ }
+
+
+ public void removeAttribute(String name) {
+ setIsDirty(true);
+ super.removeAttribute(name);
+ }
+
+ /**
+ * see parent description,
+ * plus we also notify other nodes in the cluster
+ */
+ public void removeAttribute(String name, boolean notify) {
+ setIsDirty(true);
+ super.removeAttribute(name,notify);
+ }
+
+
+ /**
+ * Sets an attribute and notifies the other nodes in the cluster
+ */
+ public void setAttribute(String name, Object value)
+ {
+ if ( value == null ) {
+ removeAttribute(name);
+ return;
+ }
+ if (!(value instanceof java.io.Serializable))
+ throw new java.lang.IllegalArgumentException("Value for attribute "+name+" is not serializable.");
+ setIsDirty(true);
+ super.setAttribute(name,value);
+ }
+
+ public void setMaxInactiveInterval(int interval) {
+ setIsDirty(true);
+ super.setMaxInactiveInterval(interval);
+ }
+
+
+ /**
+ * Sets the manager for this session
+ * @param mgr - the servers InMemoryReplicationManager
+ */
+ public void setManager(SimpleTcpReplicationManager mgr)
+ {
+ mManager = mgr;
+ super.setManager(mgr);
+ }
+
+
+ /**
+ * Set the authenticated Principal that is associated with this Session.
+ * This provides an Authenticator with a means to cache a
+ * previously authenticated Principal, and avoid potentially expensive
+ * Realm.authenticate() calls on every request.
+ *
+ * @param principal The new Principal, or null if none
+ */
+ public void setPrincipal(Principal principal) {
+ super.setPrincipal(principal);
+ setIsDirty(true);
+ }
+
+ public void expire() {
+ SimpleTcpReplicationManager mgr =(SimpleTcpReplicationManager)getManager();
+ mgr.sessionInvalidated(getIdInternal());
+ setIsDirty(true);
+ super.expire();
+ }
+
+ public void invalidate() {
+ SimpleTcpReplicationManager mgr =(SimpleTcpReplicationManager)getManager();
+ mgr.sessionInvalidated(getIdInternal());
+ setIsDirty(true);
+ super.invalidate();
+ }
+
+
+ /**
+ * Read a serialized version of the contents of this session object from
+ * the specified object input stream, without requiring that the
+ * StandardSession itself have been serialized.
+ *
+ * @param stream The object input stream to read from
+ *
+ * @exception ClassNotFoundException if an unknown class is specified
+ * @exception IOException if an input/output error occurs
+ */
+ public void readObjectData(ObjectInputStream stream)
+ throws ClassNotFoundException, IOException {
+
+ super.readObjectData(stream);
+
+ }
+
+
+ /**
+ * Write a serialized version of the contents of this session object to
+ * the specified object output stream, without requiring that the
+ * StandardSession itself have been serialized.
+ *
+ * @param stream The object output stream to write to
+ *
+ * @exception IOException if an input/output error occurs
+ */
+ public void writeObjectData(ObjectOutputStream stream)
+ throws IOException {
+
+ super.writeObjectData(stream);
+
+ }
+
+ public void setId(String id, boolean tellNew) {
+
+ if ((this.id != null) && (manager != null))
+ manager.remove(this);
+
+ this.id = id;
+
+ if (manager != null)
+ manager.add(this);
+ if (tellNew) tellNew();
+ }
+
+
+
+
+
+
+
+
+ /**
+ * returns true if this session is the primary session, if that is the
+ * case, the manager can expire it upon timeout.
+ */
+ public boolean isPrimarySession() {
+ return isPrimarySession;
+ }
+
+ /**
+ * Sets whether this is the primary session or not.
+ * @param primarySession Flag value
+ */
+ public void setPrimarySession(boolean primarySession) {
+ this.isPrimarySession=primarySession;
+ }
+
+
+
+
+ /**
+ * Implements a log method to log through the manager
+ */
+ protected void log(String message) {
+
+ if ((mManager != null) && (mManager instanceof SimpleTcpReplicationManager)) {
+ ((SimpleTcpReplicationManager) mManager).log.debug("ReplicatedSession: " + message);
+ } else {
+ System.out.println("ReplicatedSession: " + message);
+ }
+
+ }
+
+ protected void log(String message, Throwable x) {
+
+ if ((mManager != null) && (mManager instanceof SimpleTcpReplicationManager)) {
+ ((SimpleTcpReplicationManager) mManager).log.error("ReplicatedSession: " + message,x);
+ } else {
+ System.out.println("ReplicatedSession: " + message);
+ x.printStackTrace();
+ }
+
+ }
+
+ public String toString() {
+ StringBuffer buf = new StringBuffer("ReplicatedSession id=");
+ buf.append(getIdInternal()).append(" ref=").append(super.toString()).append("\n");
+ java.util.Enumeration e = getAttributeNames();
+ while ( e.hasMoreElements() ) {
+ String name = (String)e.nextElement();
+ Object value = getAttribute(name);
+ buf.append("\tname=").append(name).append("; value=").append(value).append("\n");
+ }
+ buf.append("\tLastAccess=").append(getLastAccessedTime()).append("\n");
+ return buf.toString();
+ }
+ public int getAccessCount() {
+ return accessCount.get();
+ }
+ public void setAccessCount(int accessCount) {
+ this.accessCount.set(accessCount);
+ }
+ public long getLastAccessedTime() {
+ return lastAccessedTime;
+ }
+ public void setLastAccessedTime(long lastAccessedTime) {
+ this.lastAccessedTime = lastAccessedTime;
+ }
+ public long getThisAccessedTime() {
+ return thisAccessedTime;
+ }
+ public void setThisAccessedTime(long thisAccessedTime) {
+ this.thisAccessedTime = thisAccessedTime;
+ }
+
+}
diff --git a/java/org/apache/catalina/ha/session/SerializablePrincipal.java b/java/org/apache/catalina/ha/session/SerializablePrincipal.java
new file mode 100644
index 000000000..08518b6ce
--- /dev/null
+++ b/java/org/apache/catalina/ha/session/SerializablePrincipal.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.session;
+
+
+import java.util.Arrays;
+import java.util.List;
+import org.apache.catalina.Realm;
+
+
+/**
+ * Generic implementation of java.security.Principal that
+ * is available for use by Realm implementations.
+ * The GenericPrincipal does NOT implement serializable and I didn't want to change that implementation
+ * hence I implemented this one instead.
+ * @author Filip Hanik
+ * @version $Revision: 303587 $ $Date: 2004-12-09 08:36:43 -0600 (Thu, 09 Dec 2004) $
+ */
+import org.apache.catalina.realm.GenericPrincipal;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+public class SerializablePrincipal implements java.io.Serializable {
+
+
+ // ----------------------------------------------------------- Constructors
+
+ public SerializablePrincipal()
+ {
+ super();
+ }
+ /**
+ * Construct a new Principal, associated with the specified Realm, for the
+ * specified username and password.
+ *
+ * @param realm The Realm that owns this Principal
+ * @param name The username of the user represented by this Principal
+ * @param password Credentials used to authenticate this user
+ */
+ public SerializablePrincipal(Realm realm, String name, String password) {
+
+ this(realm, name, password, null);
+
+ }
+
+
+ /**
+ * Construct a new Principal, associated with the specified Realm, for the
+ * specified username and password, with the specified role names
+ * (as Strings).
+ *
+ * @param realm The Realm that owns this principal
+ * @param name The username of the user represented by this Principal
+ * @param password Credentials used to authenticate this user
+ * @param roles List of roles (must be Strings) possessed by this user
+ */
+ public SerializablePrincipal(Realm realm, String name, String password,
+ List roles) {
+
+ super();
+ this.realm = realm;
+ this.name = name;
+ this.password = password;
+ if (roles != null) {
+ this.roles = new String[roles.size()];
+ this.roles = (String[]) roles.toArray(this.roles);
+ if (this.roles.length > 0)
+ Arrays.sort(this.roles);
+ }
+
+ }
+
+
+ // ------------------------------------------------------------- Properties
+
+
+ /**
+ * The username of the user represented by this Principal.
+ */
+ protected String name = null;
+
+ public String getName() {
+ return (this.name);
+ }
+
+
+ /**
+ * The authentication credentials for the user represented by
+ * this Principal.
+ */
+ protected String password = null;
+
+ public String getPassword() {
+ return (this.password);
+ }
+
+
+ /**
+ * The Realm with which this Principal is associated.
+ */
+ protected transient Realm realm = null;
+
+ public Realm getRealm() {
+ return (this.realm);
+ }
+
+ public void setRealm(Realm realm) {
+ this.realm = realm;
+ }
+
+
+
+
+ /**
+ * The set of roles associated with this user.
+ */
+ protected String roles[] = new String[0];
+
+ public String[] getRoles() {
+ return (this.roles);
+ }
+
+
+ // --------------------------------------------------------- Public Methods
+
+
+
+
+ /**
+ * Return a String representation of this object, which exposes only
+ * information that should be public.
+ */
+ public String toString() {
+
+ StringBuffer sb = new StringBuffer("SerializablePrincipal[");
+ sb.append(this.name);
+ sb.append("]");
+ return (sb.toString());
+
+ }
+
+ public static SerializablePrincipal createPrincipal(GenericPrincipal principal)
+ {
+ if ( principal==null) return null;
+ return new SerializablePrincipal(principal.getRealm(),
+ principal.getName(),
+ principal.getPassword(),
+ principal.getRoles()!=null?Arrays.asList(principal.getRoles()):null);
+ }
+
+ public GenericPrincipal getPrincipal( Realm realm )
+ {
+ return new GenericPrincipal(realm,name,password,getRoles()!=null?Arrays.asList(getRoles()):null);
+ }
+
+ public static GenericPrincipal readPrincipal(ObjectInput in, Realm realm) throws java.io.IOException{
+ String name = in.readUTF();
+ boolean hasPwd = in.readBoolean();
+ String pwd = null;
+ if ( hasPwd ) pwd = in.readUTF();
+ int size = in.readInt();
+ String[] roles = new String[size];
+ for ( int i=0; i
+ * The SessionMessage class is a class that is used when a session has been
+ * created, modified, expired in a Tomcat cluster node.
+ *
+ * The following events are currently available:
+ *
+ *
+ *
+ */
+
+public interface SessionMessage extends ClusterMessage, java.io.Serializable
+{
+
+ /**
+ * Event type used when a session has been created on a node
+ */
+ public static final int EVT_SESSION_CREATED = 1;
+ /**
+ * Event type used when a session has expired
+ */
+ public static final int EVT_SESSION_EXPIRED = 2;
+
+ /**
+ * Event type used when a session has been accessed (ie, last access time
+ * has been updated. This is used so that the replicated sessions will not expire
+ * on the network
+ */
+ public static final int EVT_SESSION_ACCESSED = 3;
+ /**
+ * Event type used when a server comes online for the first time.
+ * The first thing the newly started server wants to do is to grab the
+ * all the sessions from one of the nodes and keep the same state in there
+ */
+ public static final int EVT_GET_ALL_SESSIONS = 4;
+ /**
+ * Event type used when an attribute has been added to a session,
+ * the attribute will be sent to all the other nodes in the cluster
+ */
+ public static final int EVT_SESSION_DELTA = 13;
+
+ /**
+ * When a session state is transferred, this is the event.
+ */
+ public static final int EVT_ALL_SESSION_DATA = 12;
+
+ /**
+ * When a session state is complete transferred, this is the event.
+ */
+ public static final int EVT_ALL_SESSION_TRANSFERCOMPLETE = 14;
+
+
+
+ public String getContextName();
+
+ public String getEventTypeString();
+
+ /**
+ * returns the event type
+ * @return one of the event types EVT_XXXX
+ */
+ public int getEventType();
+ /**
+ * @return the serialized data for the session
+ */
+ public byte[] getSession();
+ /**
+ * @return the session ID for the session
+ */
+ public String getSessionID();
+
+
+
+}//SessionMessage
diff --git a/java/org/apache/catalina/ha/session/SessionMessageImpl.java b/java/org/apache/catalina/ha/session/SessionMessageImpl.java
new file mode 100644
index 000000000..f08d62345
--- /dev/null
+++ b/java/org/apache/catalina/ha/session/SessionMessageImpl.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.session;
+
+
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.ha.ClusterMessageBase;
+
+/**
+ * Session cluster message
+ *
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ *
+ * @version $Revision: 326110 $ $Date: 2005-10-18 09:08:36 -0500 (Tue, 18 Oct 2005) $
+ */
+public class SessionMessageImpl extends ClusterMessageBase implements SessionMessage, java.io.Serializable {
+
+ public SessionMessageImpl() {
+ }
+
+
+ /*
+
+ * Private serializable variables to keep the messages state
+ */
+ private int mEvtType = -1;
+ private byte[] mSession;
+ private String mSessionID;
+
+ private String mContextName;
+ private long serializationTimestamp;
+ private boolean timestampSet = false ;
+ private String uniqueId;
+
+
+ private SessionMessageImpl( String contextName,
+ int eventtype,
+ byte[] session,
+ String sessionID)
+ {
+ mEvtType = eventtype;
+ mSession = session;
+ mSessionID = sessionID;
+ mContextName = contextName;
+ uniqueId = sessionID;
+ }
+
+ /**
+ * Creates a session message. Depending on what event type you want this
+ * message to represent, you populate the different parameters in the constructorpublic static final int EVT_SESSION_CREATED
public static final int EVT_SESSION_ACCESSED
public static final int EVT_ATTRIBUTE_ADDED
public static final int EVT_ATTRIBUTE_REMOVED
public static final int EVT_SESSION_EXPIRED_WONOTIFY
public static final int EVT_SESSION_EXPIRED_WNOTIFY
public static final int EVT_GET_ALL_SESSIONS
public static final int EVT_SET_USER_PRINCIPAL
public static final int EVT_SET_SESSION_NOTE
public static final int EVT_REMOVE_SESSION_NOTE
+ * The following rules apply dependent on what event type argument you use:
+ * EVT_SESSION_CREATED
+ * The parameters: session, sessionID must be set.
+ * EVT_SESSION_EXPIRED
+ * The parameters: sessionID must be set.
+ * EVT_SESSION_ACCESSED
+ * The parameters: sessionID must be set.
+ * EVT_SESSION_EXPIRED_XXXX
+ * The parameters: sessionID must be set.
+ * EVT_SESSION_DELTA
+ * Send attribute delta (add,update,remove attribute or principal, ...).
+ * EVT_ALL_SESSION_DATA
+ * Send complete serializes session list
+ * EVT_ALL_SESSION_TRANSFERCOMPLETE
+ * send that all session state information are transfered
+ * after GET_ALL_SESSION received from this sender.
+ * @param contextName - the name of the context (application
+ * @param eventtype - one of the 8 event type defined in this class
+ * @param session - the serialized byte array of the session itself
+ * @param sessionID - the id that identifies this session
+ * @param uniqueID - the id that identifies this message
+ */
+ public SessionMessageImpl( String contextName,
+ int eventtype,
+ byte[] session,
+ String sessionID,
+ String uniqueID)
+ {
+ this(contextName,eventtype,session,sessionID);
+ uniqueId = uniqueID;
+ }
+
+ /**
+ * returns the event type
+ * @return one of the event types EVT_XXXX
+ */
+ public int getEventType() { return mEvtType; }
+
+ /**
+ * @return the serialized data for the session
+ */
+ public byte[] getSession() { return mSession;}
+
+ /**
+ * @return the session ID for the session
+ */
+ public String getSessionID(){ return mSessionID; }
+
+ /**
+ * set message send time but only the first setting works (one shot)
+ */
+ public void setTimestamp(long time) {
+ synchronized(this) {
+ if(!timestampSet) {
+ serializationTimestamp=time;
+ timestampSet = true ;
+ }
+ }
+ }
+
+ public long getTimestamp() { return serializationTimestamp;}
+
+ /**
+ * clear text event type name (for logging purpose only)
+ * @return the event type in a string representating, useful for debugging
+ */
+ public String getEventTypeString()
+ {
+ switch (mEvtType)
+ {
+ case EVT_SESSION_CREATED : return "SESSION-MODIFIED";
+ case EVT_SESSION_EXPIRED : return "SESSION-EXPIRED";
+ case EVT_SESSION_ACCESSED : return "SESSION-ACCESSED";
+ case EVT_GET_ALL_SESSIONS : return "SESSION-GET-ALL";
+ case EVT_SESSION_DELTA : return "SESSION-DELTA";
+ case EVT_ALL_SESSION_DATA : return "ALL-SESSION-DATA";
+ case EVT_ALL_SESSION_TRANSFERCOMPLETE : return "SESSION-STATE-TRANSFERED";
+ default : return "UNKNOWN-EVENT-TYPE";
+ }
+ }
+
+ public String getContextName() {
+ return mContextName;
+ }
+ public String getUniqueId() {
+ return uniqueId;
+ }
+ public void setUniqueId(String uniqueId) {
+ this.uniqueId = uniqueId;
+ }
+
+}
diff --git a/java/org/apache/catalina/ha/session/SimpleTcpReplicationManager.java b/java/org/apache/catalina/ha/session/SimpleTcpReplicationManager.java
new file mode 100644
index 000000000..6dfb76978
--- /dev/null
+++ b/java/org/apache/catalina/ha/session/SimpleTcpReplicationManager.java
@@ -0,0 +1,690 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.session;
+
+import java.io.IOException;
+
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.Session;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.ha.ClusterManager;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.realm.GenericPrincipal;
+import org.apache.catalina.session.StandardManager;
+import org.apache.catalina.tribes.io.ReplicationStream;
+import java.io.ByteArrayInputStream;
+import org.apache.catalina.Loader;
+
+/**
+ * Title: Tomcat Session Replication for Tomcat 4.0
+ * Description: A very simple straight forward implementation of
+ * session replication of servers in a cluster.
+ * This session replication is implemented "live". By live
+ * I mean, when a session attribute is added into a session on Node A
+ * a message is broadcasted to other messages and setAttribute is called on the
+ * replicated sessions.
+ * A full description of this implementation can be found under
+ *
+ *
+ * Copyright: See apache license
+ * Company: www.filip.net
+ * @author Filip Hanik
+ * @author Bela Ban (modifications for synchronous replication)
+ * @version 1.0 for TC 4.0
+ * Description: The InMemoryReplicationManager is a session manager that replicated
+ * session information in memory. It uses JavaGroups as
+ * a communication protocol to ensure guaranteed and ordered message delivery.
+ * JavaGroups also provides a very flexible protocol stack to ensure that the replication
+ * can be used in any environment.
+ *
+ * The InMemoryReplicationManager extends the StandardManager hence it allows for us
+ * to inherit all the basic session management features like expiration, session listeners etc
+ *
+ * To communicate with other nodes in the cluster, the InMemoryReplicationManager sends out 7 different type of multicast messages
+ * all defined in the SessionMessage class.
+ * When a session is replicated (not an attribute added/removed) the session is serialized into
+ * a byte array using the StandardSession.readObjectData, StandardSession.writeObjectData methods.
+ */
+public class SimpleTcpReplicationManager extends StandardManager implements ClusterManager
+{
+ public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( SimpleTcpReplicationManager.class );
+
+ //the channel configuration
+ protected String mChannelConfig = null;
+
+ //the group name
+ protected String mGroupName = "TomcatReplication";
+
+ //somehow start() gets called more than once
+ protected boolean mChannelStarted = false;
+
+ //log to screen
+ protected boolean mPrintToScreen = true;
+
+ protected boolean defaultMode = false;
+
+ protected boolean mManagerRunning = false;
+
+ /** Use synchronous rather than asynchronous replication. Every session modification (creation, change, removal etc)
+ * will be sent to all members. The call will then wait for max milliseconds, or forever (if timeout is 0) for
+ * all responses.
+ */
+ protected boolean synchronousReplication=true;
+
+ /** Set to true if we don't want the sessions to expire on shutdown */
+ protected boolean mExpireSessionsOnShutdown = true;
+
+ protected boolean useDirtyFlag = false;
+
+ protected String name;
+
+ protected boolean distributable = true;
+
+ protected CatalinaCluster cluster;
+
+ protected java.util.HashMap invalidatedSessions = new java.util.HashMap();
+
+ /**
+ * Flag to keep track if the state has been transferred or not
+ * Assumes false.
+ */
+ protected boolean stateTransferred = false;
+ private boolean notifyListenersOnReplication;
+ private boolean sendClusterDomainOnly = true ;
+
+ /**
+ * Constructor, just calls super()
+ *
+ */
+ public SimpleTcpReplicationManager()
+ {
+ super();
+ }
+
+ public boolean isSendClusterDomainOnly() {
+ return sendClusterDomainOnly;
+ }
+
+ /**
+ * @param sendClusterDomainOnly The sendClusterDomainOnly to set.
+ */
+ public void setSendClusterDomainOnly(boolean sendClusterDomainOnly) {
+ this.sendClusterDomainOnly = sendClusterDomainOnly;
+ }
+
+ /**
+ * @return Returns the defaultMode.
+ */
+ public boolean isDefaultMode() {
+ return defaultMode;
+ }
+ /**
+ * @param defaultMode The defaultMode to set.
+ */
+ public void setDefaultMode(boolean defaultMode) {
+ this.defaultMode = defaultMode;
+ }
+
+ public boolean isManagerRunning()
+ {
+ return mManagerRunning;
+ }
+
+ public void setUseDirtyFlag(boolean usedirtyflag)
+ {
+ this.useDirtyFlag = usedirtyflag;
+ }
+
+ public void setExpireSessionsOnShutdown(boolean expireSessionsOnShutdown)
+ {
+ mExpireSessionsOnShutdown = expireSessionsOnShutdown;
+ }
+
+ public void setCluster(CatalinaCluster cluster) {
+ if(log.isDebugEnabled())
+ log.debug("Cluster associated with SimpleTcpReplicationManager");
+ this.cluster = cluster;
+ }
+
+ public boolean getExpireSessionsOnShutdown()
+ {
+ return mExpireSessionsOnShutdown;
+ }
+
+ public void setPrintToScreen(boolean printtoscreen)
+ {
+ if(log.isDebugEnabled())
+ log.debug("Setting screen debug to:"+printtoscreen);
+ mPrintToScreen = printtoscreen;
+ }
+
+ public void setSynchronousReplication(boolean flag)
+ {
+ synchronousReplication=flag;
+ }
+
+ /**
+ * Override persistence since they don't go hand in hand with replication for now.
+ */
+ public void unload() throws IOException {
+ if ( !getDistributable() ) {
+ super.unload();
+ }
+ }
+
+ /**
+ * Creates a HTTP session.
+ * Most of the code in here is copied from the StandardManager.
+ * This is not pretty, yeah I know, but it was necessary since the
+ * StandardManager had hard coded the session instantiation to the a
+ * StandardSession, when we actually want to instantiate a ReplicatedSession
+ * If the call comes from the Tomcat servlet engine, a SessionMessage goes out to the other
+ * nodes in the cluster that this session has been created.
+ * @param notify - if set to true the other nodes in the cluster will be notified.
+ * This flag is needed so that we can create a session before we deserialize
+ * a replicated one
+ *
+ * @see ReplicatedSession
+ */
+ protected Session createSession(String sessionId, boolean notify, boolean setId)
+ {
+
+ //inherited from the basic manager
+ if ((getMaxActiveSessions() >= 0) &&
+ (sessions.size() >= getMaxActiveSessions()))
+ throw new IllegalStateException(sm.getString("standardManager.createSession.ise"));
+
+
+ Session session = new ReplicatedSession(this);
+
+ // Initialize the properties of the new session and return it
+ session.setNew(true);
+ session.setValid(true);
+ session.setCreationTime(System.currentTimeMillis());
+ session.setMaxInactiveInterval(this.maxInactiveInterval);
+ if(sessionId == null)
+ sessionId = generateSessionId();
+ if ( setId ) session.setId(sessionId);
+ if ( notify && (cluster!=null) ) {
+ ((ReplicatedSession)session).setIsDirty(true);
+ }
+ return (session);
+ }//createSession
+
+ //=========================================================================
+ // OVERRIDE THESE METHODS TO IMPLEMENT THE REPLICATION
+ //=========================================================================
+
+ /**
+ * Construct and return a new session object, based on the default
+ * settings specified by this Manager's properties. The session
+ * id will be assigned by this method, and available via the getId()
+ * method of the returned session. If a new session cannot be created
+ * for any reason, return null.
+ *
+ * @exception IllegalStateException if a new session cannot be
+ * instantiated for any reason
+ */
+ public Session createSession(String sessionId)
+ {
+ //create a session and notify the other nodes in the cluster
+ Session session = createSession(sessionId,getDistributable(),true);
+ add(session);
+ return session;
+ }
+
+ public void sessionInvalidated(String sessionId) {
+ synchronized ( invalidatedSessions ) {
+ invalidatedSessions.put(sessionId, sessionId);
+ }
+ }
+
+ public String[] getInvalidatedSessions() {
+ synchronized ( invalidatedSessions ) {
+ String[] result = new String[invalidatedSessions.size()];
+ invalidatedSessions.values().toArray(result);
+ return result;
+ }
+
+ }
+
+ public ClusterMessage requestCompleted(String sessionId)
+ {
+ if ( !getDistributable() ) {
+ log.warn("Received requestCompleted message, although this context["+
+ getName()+"] is not distributable. Ignoring message");
+ return null;
+ }
+ //notify javagroups
+ try
+ {
+ if ( invalidatedSessions.get(sessionId) != null ) {
+ synchronized ( invalidatedSessions ) {
+ invalidatedSessions.remove(sessionId);
+ SessionMessage msg = new SessionMessageImpl(name,
+ SessionMessage.EVT_SESSION_EXPIRED,
+ null,
+ sessionId,
+ sessionId);
+ return msg;
+ }
+ } else {
+ ReplicatedSession session = (ReplicatedSession) findSession(
+ sessionId);
+ if (session != null) {
+ //return immediately if the session is not dirty
+ if (useDirtyFlag && (!session.isDirty())) {
+ //but before we return doing nothing,
+ //see if we should send
+ //an updated last access message so that
+ //sessions across cluster dont expire
+ long interval = session.getMaxInactiveInterval();
+ long lastaccdist = System.currentTimeMillis() -
+ session.getLastAccessWasDistributed();
+ if ( ((interval*1000) / lastaccdist)< 3 ) {
+ SessionMessage accmsg = new SessionMessageImpl(name,
+ SessionMessage.EVT_SESSION_ACCESSED,
+ null,
+ sessionId,
+ sessionId);
+ session.setLastAccessWasDistributed(System.currentTimeMillis());
+ return accmsg;
+ }
+ return null;
+ }
+
+ session.setIsDirty(false);
+ if (log.isDebugEnabled()) {
+ try {
+ log.debug("Sending session to cluster=" + session);
+ }
+ catch (Exception ignore) {}
+ }
+ SessionMessage msg = new SessionMessageImpl(name,
+ SessionMessage.EVT_SESSION_CREATED,
+ writeSession(session),
+ session.getIdInternal(),
+ session.getIdInternal());
+ return msg;
+ } //end if
+ }//end if
+ }
+ catch (Exception x )
+ {
+ log.error("Unable to replicate session",x);
+ }
+ return null;
+ }
+
+ /**
+ * Serialize a session into a byte array
+ * This method simple calls the writeObjectData method on the session
+ * and returns the byte data from that call
+ * @param session - the session to be serialized
+ * @return a byte array containing the session data, null if the serialization failed
+ */
+ protected byte[] writeSession( Session session )
+ {
+ try
+ {
+ java.io.ByteArrayOutputStream session_data = new java.io.ByteArrayOutputStream();
+ java.io.ObjectOutputStream session_out = new java.io.ObjectOutputStream(session_data);
+ session_out.flush();
+ boolean hasPrincipal = session.getPrincipal() != null;
+ session_out.writeBoolean(hasPrincipal);
+ if ( hasPrincipal )
+ {
+ session_out.writeObject(SerializablePrincipal.createPrincipal((GenericPrincipal)session.getPrincipal()));
+ }//end if
+ ((ReplicatedSession)session).writeObjectData(session_out);
+ return session_data.toByteArray();
+
+ }
+ catch ( Exception x )
+ {
+ log.error("Failed to serialize the session!",x);
+ }
+ return null;
+ }
+
+ /**
+ * Open Stream and use correct ClassLoader (Container) Switch
+ * ThreadClassLoader
+ *
+ * @param data
+ * @return The object input stream
+ * @throws IOException
+ */
+ public ReplicationStream getReplicationStream(byte[] data) throws IOException {
+ return getReplicationStream(data,0,data.length);
+ }
+
+ public ReplicationStream getReplicationStream(byte[] data, int offset, int length) throws IOException {
+ ByteArrayInputStream fis =null;
+ ReplicationStream ois = null;
+ Loader loader = null;
+ ClassLoader classLoader = null;
+ //fix to be able to run the DeltaManager
+ //stand alone without a container.
+ //use the Threads context class loader
+ if (container != null)
+ loader = container.getLoader();
+ if (loader != null)
+ classLoader = loader.getClassLoader();
+ else
+ classLoader = Thread.currentThread().getContextClassLoader();
+ //end fix
+ fis = new ByteArrayInputStream(data, offset, length);
+ if ( classLoader == Thread.currentThread().getContextClassLoader() ) {
+ ois = new ReplicationStream(fis, new ClassLoader[] {classLoader});
+ } else {
+ ois = new ReplicationStream(fis, new ClassLoader[] {classLoader,Thread.currentThread().getContextClassLoader()});
+ }
+ return ois;
+ }
+
+
+
+
+ /**
+ * Reinstantiates a serialized session from the data passed in.
+ * This will first call createSession() so that we get a fresh instance with all
+ * the managers set and all the transient fields validated.
+ * Then it calls Session.readObjectData(byte[]) to deserialize the object
+ * @param data - a byte array containing session data
+ * @return a valid Session object, null if an error occurs
+ *
+ */
+ protected Session readSession( byte[] data, String sessionId )
+ {
+ try
+ {
+ ReplicationStream session_in = getReplicationStream(data);
+
+ Session session = sessionId!=null?this.findSession(sessionId):null;
+ boolean isNew = (session==null);
+ //clear the old values from the existing session
+ if ( session!=null ) {
+ ReplicatedSession rs = (ReplicatedSession)session;
+ rs.expire(false); //cleans up the previous values, since we are not doing removes
+ session = null;
+ }//end if
+
+ if (session==null) {
+ session = createSession(null,false, false);
+ sessions.remove(session.getIdInternal());
+ }
+
+
+ boolean hasPrincipal = session_in.readBoolean();
+ SerializablePrincipal p = null;
+ if ( hasPrincipal )
+ p = (SerializablePrincipal)session_in.readObject();
+ ((ReplicatedSession)session).readObjectData(session_in);
+ if ( hasPrincipal )
+ session.setPrincipal(p.getPrincipal(getContainer().getRealm()));
+ ((ReplicatedSession)session).setId(sessionId,isNew);
+ ReplicatedSession rsession = (ReplicatedSession)session;
+ rsession.setAccessCount(1);
+ session.setManager(this);
+ session.setValid(true);
+ rsession.setLastAccessedTime(System.currentTimeMillis());
+ rsession.setThisAccessedTime(System.currentTimeMillis());
+ ((ReplicatedSession)session).setAccessCount(0);
+ session.setNew(false);
+ if(log.isTraceEnabled())
+ log.trace("Session loaded id="+sessionId +
+ " actualId="+session.getId()+
+ " exists="+this.sessions.containsKey(sessionId)+
+ " valid="+rsession.isValid());
+ return session;
+
+ }
+ catch ( Exception x )
+ {
+ log.error("Failed to deserialize the session!",x);
+ }
+ return null;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+ /**
+ * Prepare for the beginning of active use of the public methods of this
+ * component. This method should be called after configure(),
+ * and before any of the public methods of the component are utilized.
+ * Starts the cluster communication channel, this will connect with the other nodes
+ * in the cluster, and request the current session state to be transferred to this node.
+ * @exception IllegalStateException if this component has already been
+ * started
+ * @exception LifecycleException if this component detects a fatal error
+ * that prevents this component from being used
+ */
+ public void start() throws LifecycleException {
+ mManagerRunning = true;
+ super.start();
+ //start the javagroups channel
+ try {
+ //the channel is already running
+ if ( mChannelStarted ) return;
+ if(log.isInfoEnabled())
+ log.info("Starting clustering manager...:"+getName());
+ if ( cluster == null ) {
+ log.error("Starting... no cluster associated with this context:"+getName());
+ return;
+ }
+ cluster.addManager(getName(),this);
+
+ if (cluster.getMembers().length > 0) {
+ Member mbr = cluster.getMembers()[0];
+ SessionMessage msg =
+ new SessionMessageImpl(this.getName(),
+ SessionMessage.EVT_GET_ALL_SESSIONS,
+ null,
+ "GET-ALL",
+ "GET-ALL-"+this.getName());
+ cluster.send(msg, mbr);
+ if(log.isWarnEnabled())
+ log.warn("Manager["+getName()+"], requesting session state from "+mbr+
+ ". This operation will timeout if no session state has been received within "+
+ "60 seconds");
+ long reqStart = System.currentTimeMillis();
+ long reqNow = 0;
+ boolean isTimeout=false;
+ do {
+ try {
+ Thread.sleep(100);
+ }catch ( Exception sleep) {}
+ reqNow = System.currentTimeMillis();
+ isTimeout=((reqNow-reqStart)>(1000*60));
+ } while ( (!isStateTransferred()) && (!isTimeout));
+ if ( isTimeout || (!isStateTransferred()) ) {
+ log.error("Manager["+getName()+"], No session state received, timing out.");
+ }else {
+ if(log.isInfoEnabled())
+ log.info("Manager["+getName()+"], session state received in "+(reqNow-reqStart)+" ms.");
+ }
+ } else {
+ if(log.isInfoEnabled())
+ log.info("Manager["+getName()+"], skipping state transfer. No members active in cluster group.");
+ }//end if
+ mChannelStarted = true;
+ } catch ( Exception x ) {
+ log.error("Unable to start SimpleTcpReplicationManager",x);
+ }
+ }
+
+ /**
+ * Gracefully terminate the active use of the public methods of this
+ * component. This method should be the last one called on a given
+ * instance of this component.
+ * This will disconnect the cluster communication channel and stop the listener thread.
+ * @exception IllegalStateException if this component has not been started
+ * @exception LifecycleException if this component detects a fatal error
+ * that needs to be reported
+ */
+ public void stop() throws LifecycleException
+ {
+ mManagerRunning = false;
+ mChannelStarted = false;
+ super.stop();
+ //stop the javagroup channel
+ try
+ {
+ this.sessions.clear();
+ cluster.removeManager(getName(),this);
+// mReplicationListener.stopListening();
+// mReplicationTransmitter.stop();
+// service.stop();
+// service = null;
+ }
+ catch ( Exception x )
+ {
+ log.error("Unable to stop SimpleTcpReplicationManager",x);
+ }
+ }
+
+ public void setDistributable(boolean dist) {
+ this.distributable = dist;
+ }
+
+ public boolean getDistributable() {
+ return distributable;
+ }
+
+ /**
+ * This method is called by the received thread when a SessionMessage has
+ * been received from one of the other nodes in the cluster.
+ * @param msg - the message received
+ * @param sender - the sender of the message, this is used if we receive a
+ * EVT_GET_ALL_SESSION message, so that we only reply to
+ * the requesting node
+ */
+ protected void messageReceived( SessionMessage msg, Member sender ) {
+ try {
+ if(log.isInfoEnabled()) {
+ log.debug("Received SessionMessage of type="+msg.getEventTypeString());
+ log.debug("Received SessionMessage sender="+sender);
+ }
+ switch ( msg.getEventType() ) {
+ case SessionMessage.EVT_GET_ALL_SESSIONS: {
+ //get a list of all the session from this manager
+ Object[] sessions = findSessions();
+ java.io.ByteArrayOutputStream bout = new java.io.ByteArrayOutputStream();
+ java.io.ObjectOutputStream oout = new java.io.ObjectOutputStream(bout);
+ oout.writeInt(sessions.length);
+ for (int i=0; iorg.apache.catalina.ha.tcp
+ * package.
+ *
+ * @author Peter Rossbach
+ * @version $Revision: 303753 $ $Date: 2005-03-14 15:24:30 -0600 (Mon, 14 Mar 2005) $
+ */
+
+public class Constants {
+
+ public static final String Package = "org.apache.catalina.ha.tcp";
+
+}
diff --git a/java/org/apache/catalina/ha/tcp/LocalStrings.properties b/java/org/apache/catalina/ha/tcp/LocalStrings.properties
new file mode 100644
index 000000000..91b2d3773
--- /dev/null
+++ b/java/org/apache/catalina/ha/tcp/LocalStrings.properties
@@ -0,0 +1,72 @@
+AsyncSocketSender.create.thread=Create sender [{0}:{1,number,integer}] queue thread to tcp background replication
+AsyncSocketSender.queue.message=Queue message to [{0}:{1,number,integer}] id=[{2}] size={3}
+AsyncSocketSender.send.error=Unable to asynchronously send session with id=[{0}] - message will be ignored.
+AsyncSocketSender.queue.empty=Queue in sender [{0}:{1,number,integer}] returned null element!
+cluster.mbean.register.already=MBean {0} already registered!
+FastAsyncSocketSender.setThreadPriority=[{0}:{1,number,integer}] set priority to {2}
+FastAsyncSocketSender.min.exception=[{0}:{1,number,integer}] new priority {2} < MIN_PRIORITY
+FastAsyncSocketSender.max.exception=[{0}:{1,number,integer}] new priority {2} > MAX_PRIORITY
+IDataSender.ack.eof=EOF reached at local port [{0}:{1,number,integer}]
+IDataSender.ack.receive=Got ACK at local port [{0}:{1,number,integer}]
+IDataSender.ack.missing=Unable to read acknowledgement from [{0}:{1,number,integer}] in {2,number,integer} ms. Disconnecting socket, and trying again.
+IDataSender.ack.read=Read wait ack char '{2}' [{0}:{1,number,integer}]
+IDataSender.ack.start=Waiting for ACK message [{0}:{1,number,integer}]
+IDataSender.ack.wrong=Missing correct ACK after 10 bytes read at local port [{0}:{1,number,integer}]
+IDataSender.closeSocket=Sender close socket to [{0}:{1,number,integer}] (close count {2,number,integer})
+IDataSender.connect=Sender connect to [{0}:{1,number,integer}] (connect count {2,number,integer})
+IDataSender.create=Create sender [{0}:{1,number,integer}]
+IDataSender.disconnect=Sender disconnect from [{0}:{1,number,integer}] (disconnect count {2,number,integer})
+IDataSender.message.disconnect=Message transfered: Sender can't disconnect from [{0}:{1,number,integer}]
+IDataSender.message.create=Message transfered: Sender can't create current socket [{0}:{1,number,integer}]
+IDataSender.openSocket=Sender open socket to [{0}:{1,number,integer}] (open count {2,number,integer})
+IDataSender.openSocket.failure=Open sender socket [{0}:{1,number,integer}] failure! (open failure count {2,number,integer})
+IDataSender.send.again=Send data again to [{0}:{1,number,integer}]
+IDataSender.send.crash=Send message crashed [{0}:{1,number,integer}] type=[{2}], id=[{3}]
+IDataSender.send.message=Send message to [{0}:{1,number,integer}] id=[{2}] size={3,number,integer}
+IDataSender.send.lost=Message lost: [{0}:{1,number,integer}] type=[{2}], id=[{3}]
+IDataSender.senderModes.Configured=Configured a data replication sender for mode {0}
+IDataSender.senderModes.Instantiate=Can't instantiate a data replication sender of class {0}
+IDataSender.senderModes.Missing=Can't configure a data replication sender for mode {0}
+IDataSender.senderModes.Resources=Can't load data replication sender mapping list
+IDataSender.stats=Send stats from [{0}:{1,number,integer}], Nr of bytes sent={2,number,integer} over {3} = {4,number,integer} bytes/request, processing time {5,number,integer} msec, avg processing time {6,number,integer} msec
+PoolSocketSender.senderQueue.sender.failed=PoolSocketSender create new sender to [{0}:{1,number,integer}] failed
+PoolSocketSender.noMoreSender=No socket sender available for client [{0}:{1,number,integer}] did it disappeared?
+ReplicationTransmitter.getProperty=get property {0}
+ReplicationTransmitter.setProperty=set property {0}: {1} old value {2}
+ReplicationTransmitter.started=Start ClusterSender at cluster {0} with name {1}
+ReplicationTransmitter.stopped=Stopped ClusterSender at cluster {0} with name {1}
+ReplicationValve.crossContext.add=add Cross Context session replication container to replicationValve threadlocal
+ReplicationValve.crossContext.registerSession=register Cross context session id={0} from context {1}
+ReplicationValve.crossContext.remove=remove Cross Context session replication container from replicationValve threadlocal
+ReplicationValve.crossContext.sendDelta=send Cross Context session delta from context {0}.
+ReplicationValve.filter.loading=Loading request filters={0}
+ReplicationValve.filter.token=Request filter={0}
+ReplicationValve.filter.token.failure=Unable to compile filter={0}
+ReplicationValve.invoke.uri=Invoking replication request on {0}
+ReplicationValve.nocluster=No cluster configured for this request.
+ReplicationValve.resetDeltaRequest=Cluster is standalone: reset Session Request Delta at context {0}
+ReplicationValve.send.failure=Unable to perform replication request.
+ReplicationValve.send.invalid.failure=Unable to send session [id={0}] invalid message over cluster.
+ReplicationValve.session.found=Context {0}: Found session {1} but it isn't a ClusterSession.
+ReplicationValve.session.indicator=Context {0}: Primarity of session {0} in request attribute {1} is {2}.
+ReplicationValve.session.invalid=Context {0}: Requested session {1} is invalid, removed or not replicated at this node.
+ReplicationValve.stats=Average request time= {0} ms for Cluster overhead time={1} ms for {2} requests {3} filter requests {4} send requests {5} cross context requests (Request={6} ms Cluster={7} ms).
+SimpleTcpCluster.event.log=Cluster receive listener event {0} with data {1}
+SimpleTcpCluster.getProperty=get property {0}
+SimpleTcpCluster.setProperty=set property {0}: {1} old value {2}
+SimpleTcpCluster.default.addClusterListener=Add Default ClusterListener at cluster {0}
+SimpleTcpCluster.default.addClusterValves=Add Default ClusterValves at cluster {0}
+SimpleTcpCluster.default.addClusterReceiver=Add Default ClusterReceiver at cluster {0}
+SimpleTcpCluster.default.addClusterSender=Add Default ClusterSender at cluster {0}
+SimpleTcpCluster.default.addMembershipService=Add Default Membership Service at cluster {0}
+SimpleTcpCluster.log.receive=RECEIVE {0,date}:{0,time} {1,number} {2}:{3,number,integer} {4} {5}
+SimpleTcpCluster.log.send=SEND {0,date}:{0,time} {1,number} {2}:{3,number,integer} {4}
+SimpleTcpCluster.log.send.all=SEND {0,date}:{0,time} {1,number} - {2}
+SocketReplictionListener.allreadyExists=ServerSocket [{0}:{1}] allready started!
+SocketReplictionListener.accept.failure=ServerSocket [{0}:{1}] - Exception to start thread or accept server socket
+SocketReplictionListener.open=Open Socket at [{0}:{1}]
+SocketReplictionListener.openclose.failure=ServerSocket [{0}:{1}] - Exception to open or close server socket
+SocketReplictionListener.portbusy=Port busy at [{0}:{i}] - reason [{2}]
+SocketReplictionListener.serverSocket.notExists=Fatal error: Receiver socket not bound address={0} port={1} maxport={2}
+SocketReplictionListener.timeout=Receiver ServerSocket no started [{0}:{1}] - reason: timeout={2} or listen={3}
+SocketReplictionListener.unlockSocket.failure=UnLocksocket failure at ServerSocket [{0:{1}]
diff --git a/java/org/apache/catalina/ha/tcp/ReplicationValve.java b/java/org/apache/catalina/ha/tcp/ReplicationValve.java
new file mode 100644
index 000000000..198204dee
--- /dev/null
+++ b/java/org/apache/catalina/ha/tcp/ReplicationValve.java
@@ -0,0 +1,658 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.ha.tcp;
+
+import java.io.IOException;
+import java.util.StringTokenizer;
+import java.util.regex.Pattern;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Iterator;
+import javax.servlet.ServletException;
+
+import org.apache.catalina.Manager;
+import org.apache.catalina.Session;
+import org.apache.catalina.Context;
+import org.apache.catalina.core.StandardContext;
+import org.apache.catalina.ha.CatalinaCluster;
+import org.apache.catalina.ha.ClusterManager;
+import org.apache.catalina.ha.ClusterMessage;
+import org.apache.catalina.ha.ClusterSession;
+import org.apache.catalina.ha.ClusterValve;
+import org.apache.catalina.ha.session.DeltaManager;
+import org.apache.catalina.ha.session.DeltaSession;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.catalina.util.StringManager;
+import org.apache.catalina.valves.ValveBase;
+
+/**
+ * notifyLifecycleListenerOnFailure
+ */
+ private boolean notifyLifecycleListenerOnFailure = false;
+
+ /**
+ * dynamic sender properties
+ */
+ private Map properties = new HashMap();
+
+ private int channelSendOptions =
+ Channel.SEND_OPTIONS_ASYNCHRONOUS |
+ Channel.SEND_OPTIONS_SYNCHRONIZED_ACK |
+ Channel.SEND_OPTIONS_USE_ACK;
+
+ // ------------------------------------------------------------- Properties
+
+ public SimpleTcpCluster() {
+ }
+
+ /**
+ * Return descriptive information about this Cluster implementation and the
+ * corresponding version number, in the format
+ * <description>/<version>.
+ */
+ public String getInfo() {
+ return (info);
+ }
+
+ /**
+ * Set the name of the cluster to join, if no cluster with this name is
+ * present create one.
+ *
+ * @param clusterName
+ * The clustername to join
+ */
+ public void setClusterName(String clusterName) {
+ this.clusterName = clusterName;
+ }
+
+ /**
+ * Return the name of the cluster that this Server is currently configured
+ * to operate within.
+ *
+ * @return The name of the cluster associated with this server
+ */
+ public String getClusterName() {
+ if(clusterName == null && container != null)
+ return container.getName() ;
+ return clusterName;
+ }
+
+ /**
+ * Set the Container associated with our Cluster
+ *
+ * @param container
+ * The Container to use
+ */
+ public void setContainer(Container container) {
+ Container oldContainer = this.container;
+ this.container = container;
+ support.firePropertyChange("container", oldContainer, this.container);
+ }
+
+ /**
+ * Get the Container associated with our Cluster
+ *
+ * @return The Container associated with our Cluster
+ */
+ public Container getContainer() {
+ return (this.container);
+ }
+
+ /**
+ * @return Returns the notifyLifecycleListenerOnFailure.
+ */
+ public boolean isNotifyLifecycleListenerOnFailure() {
+ return notifyLifecycleListenerOnFailure;
+ }
+
+ /**
+ * @param notifyListenerOnFailure
+ * The notifyLifecycleListenerOnFailure to set.
+ */
+ public void setNotifyLifecycleListenerOnFailure(
+ boolean notifyListenerOnFailure) {
+ boolean oldNotifyListenerOnFailure = this.notifyLifecycleListenerOnFailure;
+ this.notifyLifecycleListenerOnFailure = notifyListenerOnFailure;
+ support.firePropertyChange("notifyLifecycleListenerOnFailure",
+ oldNotifyListenerOnFailure,
+ this.notifyLifecycleListenerOnFailure);
+ }
+
+ public String getManagerClassName() {
+ if(managerClassName != null)
+ return managerClassName;
+ return (String)getProperty("manager.className");
+ }
+
+ public void setManagerClassName(String managerClassName) {
+ this.managerClassName = managerClassName;
+ }
+
+ /**
+ * Add cluster valve
+ * Cluster Valves are only add to container when cluster is started!
+ * @param valve The new cluster Valve.
+ */
+ public void addValve(Valve valve) {
+ if (valve instanceof ClusterValve)
+ valves.add(valve);
+ }
+
+ /**
+ * get all cluster valves
+ * @return current cluster valves
+ */
+ public Valve[] getValves() {
+ return (Valve[]) valves.toArray(new Valve[valves.size()]);
+ }
+
+ /**
+ * Get the cluster listeners associated with this cluster. If this Array has
+ * no listeners registered, a zero-length array is returned.
+ */
+ public ClusterListener[] findClusterListeners() {
+ if (clusterListeners.size() > 0) {
+ ClusterListener[] listener = new ClusterListener[clusterListeners.size()];
+ clusterListeners.toArray(listener);
+ return listener;
+ } else
+ return new ClusterListener[0];
+
+ }
+
+ /**
+ * add cluster message listener and register cluster to this listener
+ *
+ * @see org.apache.catalina.ha.CatalinaCluster#addClusterListener(org.apache.catalina.ha.MessageListener)
+ */
+ public void addClusterListener(ClusterListener listener) {
+ if (listener != null && !clusterListeners.contains(listener)) {
+ clusterListeners.add(listener);
+ listener.setCluster(this);
+ }
+ }
+
+ /**
+ * remove message listener and deregister Cluster from listener
+ *
+ * @see org.apache.catalina.ha.CatalinaCluster#removeClusterListener(org.apache.catalina.ha.MessageListener)
+ */
+ public void removeClusterListener(ClusterListener listener) {
+ if (listener != null) {
+ clusterListeners.remove(listener);
+ listener.setCluster(null);
+ }
+ }
+
+ /**
+ * get current Deployer
+ */
+ public org.apache.catalina.ha.ClusterDeployer getClusterDeployer() {
+ return clusterDeployer;
+ }
+
+ /**
+ * set a new Deployer, must be set before cluster started!
+ */
+ public void setClusterDeployer(
+ org.apache.catalina.ha.ClusterDeployer clusterDeployer) {
+ this.clusterDeployer = clusterDeployer;
+ }
+
+ public void setChannel(Channel channel) {
+ this.channel = channel;
+ }
+
+ /**
+ * has members
+ */
+ protected boolean hasMembers = false;
+ public boolean hasMembers() {
+ return hasMembers;
+ }
+
+ /**
+ * Get all current cluster members
+ * @return all members or empty array
+ */
+ public Member[] getMembers() {
+ return channel.getMembers();
+ }
+
+ /**
+ * Return the member that represents this node.
+ *
+ * @return Member
+ */
+ public Member getLocalMember() {
+ return channel.getLocalMember(true);
+ }
+
+ // ------------------------------------------------------------- dynamic
+ // manager property handling
+
+ /**
+ * JMX hack to direct use at jconsole
+ *
+ * @param name
+ * @param value
+ */
+ public void setProperty(String name, String value) {
+ setProperty(name, (Object) value);
+ }
+
+ /**
+ * set config attributes with reflect and propagate to all managers
+ *
+ * @param name
+ * @param value
+ */
+ public void setProperty(String name, Object value) {
+ if (log.isTraceEnabled())
+ log.trace(sm.getString("SimpleTcpCluster.setProperty", name, value,
+ properties.get(name)));
+
+ properties.put(name, value);
+ if(started) {
+ // FIXME Hmm, is that correct when some DeltaManagers are direct configured inside Context?
+ // Why we not support it for other elements, like sender, receiver or membership?
+ // Must we restart element after change?
+ if (name.startsWith("manager")) {
+ String key = name.substring("manager".length() + 1);
+ String pvalue = value.toString();
+ for (Iterator iter = managers.values().iterator(); iter.hasNext();) {
+ Manager manager = (Manager) iter.next();
+ if(manager instanceof DeltaManager && ((ClusterManager) manager).isDefaultMode()) {
+ IntrospectionUtils.setProperty(manager, key, pvalue );
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * get current config
+ *
+ * @param key
+ * @return The property
+ */
+ public Object getProperty(String key) {
+ if (log.isTraceEnabled())
+ log.trace(sm.getString("SimpleTcpCluster.getProperty", key));
+ return properties.get(key);
+ }
+
+ /**
+ * Get all properties keys
+ *
+ * @return An iterator over the property names.
+ */
+ public Iterator getPropertyNames() {
+ return properties.keySet().iterator();
+ }
+
+ /**
+ * remove a configured property.
+ *
+ * @param key
+ */
+ public void removeProperty(String key) {
+ properties.remove(key);
+ }
+
+ /**
+ * transfer properties from cluster configuration to subelement bean.
+ * @param prefix
+ * @param bean
+ */
+ protected void transferProperty(String prefix, Object bean) {
+ if (prefix != null) {
+ for (Iterator iter = getPropertyNames(); iter.hasNext();) {
+ String pkey = (String) iter.next();
+ if (pkey.startsWith(prefix)) {
+ String key = pkey.substring(prefix.length() + 1);
+ Object value = getProperty(pkey);
+ IntrospectionUtils.setProperty(bean, key, value.toString());
+ }
+ }
+ }
+ }
+
+ // --------------------------------------------------------- Public Methods
+
+ /**
+ * @return Returns the managers.
+ */
+ public Map getManagers() {
+ return managers;
+ }
+
+ public Channel getChannel() {
+ return channel;
+ }
+
+ /**
+ * Create new Manager without add to cluster (comes with start the manager)
+ *
+ * @param name
+ * Context Name of this manager
+ * @see org.apache.catalina.Cluster#createManager(java.lang.String)
+ * @see #addManager(String, Manager)
+ * @see DeltaManager#start()
+ */
+ public synchronized Manager createManager(String name) {
+ if (log.isDebugEnabled()) log.debug("Creating ClusterManager for context " + name + " using class " + getManagerClassName());
+ Manager manager = null;
+ ClassLoader oldCtxLoader = Thread.currentThread().getContextClassLoader();
+ try {
+ Thread.currentThread().setContextClassLoader(SimpleTcpCluster.class.getClassLoader());
+ manager = (Manager) getClass().getClassLoader().loadClass(getManagerClassName()).newInstance();
+ } catch (Exception x) {
+ log.error("Unable to load class for replication manager", x);
+ manager = new org.apache.catalina.ha.session.DeltaManager();
+ } finally {
+ Thread.currentThread().setContextClassLoader(oldCtxLoader);
+ if(manager != null) {
+ manager.setDistributable(true);
+ if (manager instanceof ClusterManager) {
+ ClusterManager cmanager = (ClusterManager) manager ;
+ cmanager.setDefaultMode(true);
+ cmanager.setName(getManagerName(name,manager));
+ cmanager.setCluster(this);
+ }
+ }
+ }
+ return manager;
+ }
+
+ /**
+ * remove an application form cluster replication bus
+ *
+ * @see org.apache.catalina.ha.CatalinaCluster#removeManager(java.lang.String,Manager)
+ */
+ public void removeManager(String name,Manager manager) {
+ if (manager != null) {
+ // Notify our interested LifecycleListeners
+ lifecycle.fireLifecycleEvent(BEFORE_MANAGERUNREGISTER_EVENT,manager);
+ managers.remove(getManagerName(name,manager));
+ if (manager instanceof ClusterManager) ((ClusterManager) manager).setCluster(null);
+ // Notify our interested LifecycleListeners
+ lifecycle.fireLifecycleEvent(AFTER_MANAGERUNREGISTER_EVENT, manager);
+ }
+ }
+
+ /**
+ * add an application to cluster replication bus
+ *
+ * @param name
+ * of the context
+ * @param manager
+ * manager to register
+ * @see org.apache.catalina.ha.CatalinaCluster#addManager(java.lang.String,
+ * org.apache.catalina.Manager)
+ */
+ public void addManager(String name, Manager manager) {
+ if (!manager.getDistributable()) {
+ log.warn("Manager with name " + name + " is not distributable, can't add as cluster manager");
+ return;
+ }
+ // Notify our interested LifecycleListeners
+ lifecycle.fireLifecycleEvent(BEFORE_MANAGERREGISTER_EVENT, manager);
+ String clusterName = getManagerName(name, manager);
+ if (manager instanceof ClusterManager) {
+ ClusterManager cmanager = (ClusterManager) manager ;
+ cmanager.setName(clusterName);
+ cmanager.setCluster(this);
+ if(cmanager.isDefaultMode()) transferProperty("manager",cmanager);
+ }
+ managers.put(clusterName, manager);
+ // Notify our interested LifecycleListeners
+ lifecycle.fireLifecycleEvent(AFTER_MANAGERREGISTER_EVENT, manager);
+ }
+
+ /**
+ * @param name
+ * @param manager
+ * @return
+ */
+ public String getManagerName(String name, Manager manager) {
+ String clusterName = name ;
+ if(getContainer() instanceof Engine) {
+ Container context = manager.getContainer() ;
+ if(context != null && context instanceof Context) {
+ Container host = ((Context)context).getParent();
+ if(host != null && host instanceof Host)
+ clusterName = host.getName() + name ;
+ }
+ }
+ return clusterName;
+ }
+
+ /*
+ * Get Manager
+ *
+ * @see org.apache.catalina.ha.CatalinaCluster#getManager(java.lang.String)
+ */
+ public Manager getManager(String name) {
+ return (Manager) managers.get(name);
+ }
+
+ // ------------------------------------------------------ Lifecycle Methods
+
+ /**
+ * Execute a periodic task, such as reloading, etc. This method will be
+ * invoked inside the classloading context of this container. Unexpected
+ * throwables will be caught and logged.
+ * @see org.apache.catalina.ha.deploy.FarmWarDeployer#backgroundProcess()
+ * @see ReplicationTransmitter#backgroundProcess()
+ */
+ public void backgroundProcess() {
+ if (clusterDeployer != null) clusterDeployer.backgroundProcess();
+ //send a heartbeat through the channel
+ if ( channel !=null ) channel.heartbeat();
+ }
+
+ /**
+ * Add a lifecycle event listener to this component.
+ *
+ * @param listener
+ * The listener to add
+ */
+ public void addLifecycleListener(LifecycleListener listener) {
+ lifecycle.addLifecycleListener(listener);
+ }
+
+ /**
+ * Get the lifecycle listeners associated with this lifecycle. If this
+ * Lifecycle has no listeners registered, a zero-length array is returned.
+ */
+ public LifecycleListener[] findLifecycleListeners() {
+
+ return lifecycle.findLifecycleListeners();
+
+ }
+
+ /**
+ * Remove a lifecycle event listener from this component.
+ *
+ * @param listener
+ * The listener to remove
+ */
+ public void removeLifecycleListener(LifecycleListener listener) {
+ lifecycle.removeLifecycleListener(listener);
+ }
+
+ /**
+ * Use as base to handle start/stop/periodic Events from host. Currently
+ * only log the messages as trace level.
+ *
+ * @see org.apache.catalina.LifecycleListener#lifecycleEvent(org.apache.catalina.LifecycleEvent)
+ */
+ public void lifecycleEvent(LifecycleEvent lifecycleEvent) {
+ if (log.isTraceEnabled())
+ log.trace(sm.getString("SimpleTcpCluster.event.log", lifecycleEvent.getType(), lifecycleEvent.getData()));
+ }
+
+ // ------------------------------------------------------ public
+
+ /**
+ * Prepare for the beginning of active use of the public methods of this
+ * component. This method should be called after configure(),
+ * and before any of the public methods of the component are utilized.
+ * Starts the cluster communication channel, this will connect with the
+ * other nodes in the cluster, and request the current session state to be
+ * transferred to this node.
+ *
+ * @exception IllegalStateException
+ * if this component has already been started
+ * @exception LifecycleException
+ * if this component detects a fatal error that prevents this
+ * component from being used
+ */
+ public void start() throws LifecycleException {
+ if (started)
+ throw new LifecycleException(sm.getString("cluster.alreadyStarted"));
+ if (log.isInfoEnabled()) log.info("Cluster is about to start");
+
+ // Notify our interested LifecycleListeners
+ lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, this);
+ try {
+ if ( clusterDeployer != null ) clusterDeployer.setCluster(this);
+ this.registerClusterValve();
+ if ( channel == null ) channel = new GroupChannel();
+ channel.addMembershipListener(this);
+ channel.addChannelListener(this);
+ channel.start(channel.DEFAULT);
+ if (clusterDeployer != null) clusterDeployer.start();
+ this.started = true;
+ // Notify our interested LifecycleListeners
+ lifecycle.fireLifecycleEvent(AFTER_START_EVENT, this);
+ } catch (Exception x) {
+ log.error("Unable to start cluster.", x);
+ throw new LifecycleException(x);
+ }
+ }
+
+ /**
+ * register all cluster valve to host or engine
+ * @throws Exception
+ * @throws ClassNotFoundException
+ */
+ protected void registerClusterValve() throws Exception {
+ if(container != null ) {
+ for (Iterator iter = valves.iterator(); iter.hasNext();) {
+ ClusterValve valve = (ClusterValve) iter.next();
+ if (log.isDebugEnabled())
+ log.debug("Invoking addValve on " + getContainer()
+ + " with class=" + valve.getClass().getName());
+ if (valve != null) {
+ IntrospectionUtils.callMethodN(getContainer(), "addValve",
+ new Object[] { valve },
+ new Class[] { org.apache.catalina.Valve.class });
+
+ }
+ valve.setCluster(this);
+ }
+ }
+ }
+
+ /**
+ * unregister all cluster valve to host or engine
+ * @throws Exception
+ * @throws ClassNotFoundException
+ */
+ protected void unregisterClusterValve() throws Exception {
+ for (Iterator iter = valves.iterator(); iter.hasNext();) {
+ ClusterValve valve = (ClusterValve) iter.next();
+ if (log.isDebugEnabled())
+ log.debug("Invoking removeValve on " + getContainer()
+ + " with class=" + valve.getClass().getName());
+ if (valve != null) {
+ IntrospectionUtils.callMethodN(getContainer(), "removeValve",
+ new Object[] { valve }, new Class[] { org.apache.catalina.Valve.class });
+ }
+ valve.setCluster(this);
+ }
+ }
+
+ /**
+ * Gracefully terminate the active cluster component.
+ * This will disconnect the cluster communication channel, stop the
+ * listener and deregister the valves from host or engine.
+ * Note:
The sub elements receiver, sender, membership,
+ * listener or valves are not removed. You can easily start the cluster again.
+ *
+ * @exception IllegalStateException
+ * if this component has not been started
+ * @exception LifecycleException
+ * if this component detects a fatal error that needs to be
+ * reported
+ */
+ public void stop() throws LifecycleException {
+
+ if (!started)
+ throw new IllegalStateException(sm.getString("cluster.notStarted"));
+ // Notify our interested LifecycleListeners
+ lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, this);
+
+ if (clusterDeployer != null) clusterDeployer.stop();
+ this.managers.clear();
+ try {
+ if ( clusterDeployer != null ) clusterDeployer.setCluster(null);
+ channel.stop(Channel.DEFAULT);
+ channel.removeChannelListener(this);
+ channel.removeMembershipListener(this);
+ this.unregisterClusterValve();
+ } catch (Exception x) {
+ log.error("Unable to stop cluster valve.", x);
+ }
+ started = false;
+ // Notify our interested LifecycleListeners
+ lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, this);
+ }
+
+
+
+
+ /**
+ * send message to all cluster members
+ * @param msg message to transfer
+ *
+ * @see org.apache.catalina.ha.CatalinaCluster#send(org.apache.catalina.ha.ClusterMessage)
+ */
+ public void send(ClusterMessage msg) {
+ send(msg, null);
+ }
+
+ /**
+ * send message to all cluster members same cluster domain
+ *
+ * @see org.apache.catalina.ha.CatalinaCluster#send(org.apache.catalina.ha.ClusterMessage)
+ */
+ public void sendClusterDomain(ClusterMessage msg) {
+ send(msg,null);
+ }
+
+
+ /**
+ * send a cluster message to one member
+ *
+ * @param msg message to transfer
+ * @param dest Receiver member
+ * @see org.apache.catalina.ha.CatalinaCluster#send(org.apache.catalina.ha.ClusterMessage,
+ * org.apache.catalina.ha.Member)
+ */
+ public void send(ClusterMessage msg, Member dest) {
+ try {
+ msg.setAddress(getLocalMember());
+ if (dest != null) {
+ if (!getLocalMember().equals(dest)) {
+ channel.send(new Member[] {dest}, msg,channelSendOptions);
+ } else
+ log.error("Unable to send message to local member " + msg);
+ } else {
+ channel.send(channel.getMembers(),msg,channelSendOptions);
+ }
+ } catch (Exception x) {
+ log.error("Unable to send message through cluster sender.", x);
+ }
+ }
+
+ /**
+ * New cluster member is registered
+ *
+ * @see org.apache.catalina.ha.MembershipListener#memberAdded(org.apache.catalina.ha.Member)
+ */
+ public void memberAdded(Member member) {
+ try {
+ hasMembers = channel.hasMembers();
+ if (log.isInfoEnabled()) log.info("Replication member added:" + member);
+ // Notify our interested LifecycleListeners
+ lifecycle.fireLifecycleEvent(BEFORE_MEMBERREGISTER_EVENT, member);
+ // Notify our interested LifecycleListeners
+ lifecycle.fireLifecycleEvent(AFTER_MEMBERREGISTER_EVENT, member);
+ } catch (Exception x) {
+ log.error("Unable to connect to replication system.", x);
+ }
+
+ }
+
+ /**
+ * Cluster member is gone
+ *
+ * @see org.apache.catalina.ha.MembershipListener#memberDisappeared(org.apache.catalina.ha.Member)
+ */
+ public void memberDisappeared(Member member) {
+ try {
+ hasMembers = channel.hasMembers();
+ if (log.isInfoEnabled()) log.info("Received member disappeared:" + member);
+ // Notify our interested LifecycleListeners
+ lifecycle.fireLifecycleEvent(BEFORE_MEMBERUNREGISTER_EVENT, member);
+ // Notify our interested LifecycleListeners
+ lifecycle.fireLifecycleEvent(AFTER_MEMBERUNREGISTER_EVENT, member);
+ } catch (Exception x) {
+ log.error("Unable remove cluster node from replication system.", x);
+ }
+ }
+
+ // --------------------------------------------------------- receiver
+ // messages
+
+ /**
+ * notify all listeners from receiving a new message is not ClusterMessage
+ * emitt Failure Event to LifecylceListener
+ *
+ * @param message
+ * receveived Message
+ */
+ public boolean accept(Serializable msg, Member sender) {
+ return (msg instanceof ClusterMessage);
+ }
+
+
+ public void messageReceived(Serializable message, Member sender) {
+ ClusterMessage fwd = (ClusterMessage)message;
+ fwd.setAddress(sender);
+ messageReceived(fwd);
+ }
+
+ public void messageReceived(ClusterMessage message) {
+
+ long start = 0;
+ if (log.isDebugEnabled() && message != null)
+ log.debug("Assuming clocks are synched: Replication for "
+ + message.getUniqueId() + " took="
+ + (System.currentTimeMillis() - (message).getTimestamp())
+ + " ms.");
+
+ //invoke all the listeners
+ boolean accepted = false;
+ if (message != null) {
+ for (Iterator iter = clusterListeners.iterator(); iter.hasNext();) {
+ ClusterListener listener = (ClusterListener) iter.next();
+ if (listener.accept(message)) {
+ accepted = true;
+ listener.messageReceived(message);
+ }
+ }
+ }
+ if (!accepted && log.isDebugEnabled()) {
+ if (notifyLifecycleListenerOnFailure) {
+ Member dest = message.getAddress();
+ // Notify our interested LifecycleListeners
+ lifecycle.fireLifecycleEvent(RECEIVE_MESSAGE_FAILURE_EVENT,
+ new SendMessageData(message, dest, null));
+ }
+ log.debug("Message " + message.toString() + " from type "
+ + message.getClass().getName()
+ + " transfered but no listener registered");
+ }
+ return;
+ }
+
+ // --------------------------------------------------------- Logger
+
+ public Log getLogger() {
+ return log;
+ }
+
+
+
+
+ // ------------------------------------------------------------- deprecated
+
+ /**
+ *
+ * @see org.apache.catalina.Cluster#setProtocol(java.lang.String)
+ */
+ public void setProtocol(String protocol) {
+ }
+
+ /**
+ * @see org.apache.catalina.Cluster#getProtocol()
+ */
+ public String getProtocol() {
+ return null;
+ }
+
+ /**
+ * @see org.apache.catalina.Cluster#startContext(java.lang.String)
+ */
+ public void startContext(String contextPath) throws IOException {
+
+ }
+
+ /**
+ * @see org.apache.catalina.Cluster#installContext(java.lang.String, java.net.URL)
+ */
+ public void installContext(String contextPath, URL war) {
+
+ }
+
+ /**
+ * @see org.apache.catalina.Cluster#stop(java.lang.String)
+ */
+ public void stop(String contextPath) throws IOException {
+
+ }
+}
diff --git a/java/org/apache/catalina/ha/tcp/mbeans-descriptors.xml b/java/org/apache/catalina/ha/tcp/mbeans-descriptors.xml
new file mode 100644
index 000000000..ac0414079
--- /dev/null
+++ b/java/org/apache/catalina/ha/tcp/mbeans-descriptors.xml
@@ -0,0 +1,1046 @@
+
+
+
+ * By default Tribes uses java serialization when it receives an object
+ * to be sent over the wire. Java serialization is not the most
+ * efficient of serializing data, and Tribes might not even
+ * have access to the correct class loaders to deserialize the object properly.
+ *
+ * The ByteMessage class is a class where the channel when it receives it will
+ * not attempt to perform serialization, instead it will simply stream the getMessage()
+ * bytes.
+ * If you are using multiple applications on top of Tribes you should add some sort of header
+ * so that you can decide with the ChannelListener.accept() whether this message was intended
+ * for you.
+ * @author Filip Hanik
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+ */
+
+public class ByteMessage implements Serializable, Externalizable {
+ /**
+ * Storage for the message to be sent
+ */
+ private byte[] message;
+
+
+ /**
+ * Creates an empty byte message
+ * Constructor also for deserialization
+ */
+ public ByteMessage() {
+ }
+
+ /**
+ * Creates a byte message wit h
+ * @param data byte[] - the message contents
+ */
+ public ByteMessage(byte[] data) {
+ message = data;
+ }
+
+ /**
+ * Returns the message contents of this byte message
+ * @return byte[] - message contents, can be null
+ */
+ public byte[] getMessage() {
+ return message;
+ }
+
+ /**
+ * Sets the message contents of this byte message
+ * @param message byte[]
+ */
+ public void setMessage(byte[] message) {
+ this.message = message;
+ }
+
+ /**
+ * @see java.io.Externalizable#readExternal
+ * @param in ObjectInput
+ * @throws IOException
+ */
+ public void readExternal(ObjectInput in ) throws IOException {
+ int length = in.readInt();
+ message = new byte[length];
+ in.read(message,0,length);
+ }
+
+ /**
+ * @see java.io.Externalizable#writeExternal
+ * @param out ObjectOutput
+ * @throws IOException
+ */
+ public void writeExternal(ObjectOutput out) throws IOException {
+ out.writeInt(message!=null?message.length:0);
+ if ( message!=null ) out.write(message,0,message.length);
+ }
+
+}
diff --git a/java/org/apache/catalina/tribes/Channel.java b/java/org/apache/catalina/tribes/Channel.java
new file mode 100644
index 000000000..1212ba145
--- /dev/null
+++ b/java/org/apache/catalina/tribes/Channel.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes;
+
+import java.io.Serializable;
+
+/**
+ * Channel interface
+ * A channel is a representation of a group of nodes all participating in some sort of
+ * communication with each other.
+ * The channel is the main API class for Tribes, this is essentially the only class
+ * that an application needs to be aware of. Through the channel the application can:
+ * 1. send messages
+ * 2. receive message (by registering a ChannelListener
+ * 3. get all members of the group getMembers()
+ * 4. receive notifications of members added and members disappeared by
+ * registerering a MembershipListener
+ *
+ * The channel has 5 major components:
+ * 1. Data receiver, with a built in thread pool to receive messages from other peers
+ * 2. Data sender, an implementation for sending data using NIO or java.io
+ * 3. Membership listener,listens for membership broadcasts
+ * 4. Membership broadcaster, broadcasts membership pings.
+ * 5. Channel interceptors, the ability to manipulate messages as they are sent or arrive
+ * The channel layout is:
+ *
+ *
+ * For example usage @see org.apache.catalina.tribes.group.GroupChannel
+ * @author Filip Hanik
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+ */
+public interface Channel {
+
+ /**
+ * Start and stop sequences can be controlled by these constants
+ * This allows you to start separate components of the channel
+ * ChannelListener_1..ChannelListener_N MembershipListener_1..MembershipListener_N [Application Layer]
+ * \ \ / /
+ * \ \ / /
+ * \ \ / /
+ * \ \ / /
+ * \ \ / /
+ * \ \ / /
+ * ---------------------------------------
+ * |
+ * |
+ * Channel
+ * |
+ * ChannelInterceptor_1
+ * | [Channel stack]
+ * ChannelInterceptor_N
+ * |
+ * Coordinator (implements MessageListener,MembershipListener,ChannelInterceptor)
+ * --------------------
+ * / | \
+ * / | \
+ * / | \
+ * / | \
+ * / | \
+ * MembershipService ChannelSender ChannelReceiver [IO layer]
+ *
+ * DEFAULT - starts or stops all components in the channel
+ * @see #start(int)
+ * @see #stop(int)
+ */
+ public static final int DEFAULT = 15;
+
+ /**
+ * Start and stop sequences can be controlled by these constants
+ * This allows you to start separate components of the channel
+ * SND_RX_SEQ - starts or stops the data receiver. Start means opening a server socket
+ * in case of a TCP implementation
+ * @see #start(int)
+ * @see #stop(int)
+ */
+ public static final int SND_RX_SEQ = 1;
+
+ /**
+ * Start and stop sequences can be controlled by these constants
+ * This allows you to start separate components of the channel
+ * SND_TX_SEQ - starts or stops the data sender. This should not open any sockets,
+ * as sockets are opened on demand when a message is being sent
+ * @see #start(int)
+ * @see #stop(int)
+ */
+ public static final int SND_TX_SEQ = 2;
+
+ /**
+ * Start and stop sequences can be controlled by these constants
+ * This allows you to start separate components of the channel
+ * MBR_RX_SEQ - starts or stops the membership listener. In a multicast implementation
+ * this will open a datagram socket and join a group and listen for membership messages
+ * members joining
+ * @see #start(int)
+ * @see #stop(int)
+ */
+ public static final int MBR_RX_SEQ = 4;
+
+ /**
+ * Start and stop sequences can be controlled by these constants
+ * This allows you to start separate components of the channel
+ * MBR_TX_SEQ - starts or stops the membership broadcaster. In a multicast implementation
+ * this will open a datagram socket and join a group and broadcast the local member information
+ * @see #start(int)
+ * @see #stop(int)
+ */
+ public static final int MBR_TX_SEQ = 8;
+
+ /**
+ * Send options, when a message is sent, it can have an option flag
+ * to trigger certain behavior. Most flags are used to trigger channel interceptors
+ * as the message passes through the channel stack.
+ * However, there are four default flags that every channel implementation must implement
+ * SEND_OPTIONS_BYTE_MESSAGE - The message is a pure byte message and no marshalling or unmarshalling will
+ * be performed.
+ *
+ * @see #send(Member[], Serializable , int)
+ * @see #send(Member[], Serializable, int, ErrorHandler)
+ */
+ public static final int SEND_OPTIONS_BYTE_MESSAGE = 0x0001;
+
+ /**
+ * Send options, when a message is sent, it can have an option flag
+ * to trigger certain behavior. Most flags are used to trigger channel interceptors
+ * as the message passes through the channel stack.
+ * However, there are four default flags that every channel implementation must implement
+ * SEND_OPTIONS_USE_ACK - Message is sent and an ACK is received when the message has been received by the recipient
+ * If no ack is received, the message is not considered successful
+ * @see #send(Member[], Serializable , int)
+ * @see #send(Member[], Serializable, int, ErrorHandler)
+ */
+ public static final int SEND_OPTIONS_USE_ACK = 0x0002;
+
+ /**
+ * Send options, when a message is sent, it can have an option flag
+ * to trigger certain behavior. Most flags are used to trigger channel interceptors
+ * as the message passes through the channel stack.
+ * However, there are four default flags that every channel implementation must implement
+ * SEND_OPTIONS_SYNCHRONIZED_ACK - Message is sent and an ACK is received when the message has been received and
+ * processed by the recipient
+ * If no ack is received, the message is not considered successful
+ * @see #send(Member[], Serializable , int)
+ * @see #send(Member[], Serializable, int, ErrorHandler)
+ */
+ public static final int SEND_OPTIONS_SYNCHRONIZED_ACK = 0x0004;
+
+ /**
+ * Send options, when a message is sent, it can have an option flag
+ * to trigger certain behavior. Most flags are used to trigger channel interceptors
+ * as the message passes through the channel stack.
+ * However, there are four default flags that every channel implementation must implement
+ * SEND_OPTIONS_ASYNCHRONOUS - Message is sent and an ACK is received when the message has been received and
+ * processed by the recipient
+ * If no ack is received, the message is not considered successful
+ * @see #send(Member[], Serializable , int)
+ * @see #send(Member[], Serializable, int, ErrorHandler)
+ */
+ public static final int SEND_OPTIONS_ASYNCHRONOUS = 0x0008;
+
+
+ /**
+ * Send options, when a message is sent, it can have an option flag
+ * to trigger certain behavior. Most flags are used to trigger channel interceptors
+ * as the message passes through the channel stack.
+ * However, there are four default flags that every channel implementation must implement
+ * SEND_OPTIONS_DEFAULT - the default sending options, just a helper variable.
+ * The default is int SEND_OPTIONS_DEFAULT = SEND_OPTIONS_USE_ACK;
+ * @see #SEND_OPTIONS_USE_ACK
+ * @see #send(Member[], Serializable , int)
+ * @see #send(Member[], Serializable, int, ErrorHandler)
+ */
+ public static final int SEND_OPTIONS_DEFAULT = SEND_OPTIONS_USE_ACK;
+
+
+ /**
+ * Adds an interceptor to the channel message chain.
+ * @param interceptor ChannelInterceptor
+ */
+ public void addInterceptor(ChannelInterceptor interceptor);
+
+ /**
+ * Starts up the channel. This can be called multiple times for individual services to start
+ * The svc parameter can be the logical or value of any constants
+ * @param svc int value of
+ * DEFAULT - will start all services
+ * MBR_RX_SEQ - starts the membership receiver
+ * MBR_TX_SEQ - starts the membership broadcaster
+ * SND_TX_SEQ - starts the replication transmitter
+ * SND_RX_SEQ - starts the replication receiver
+ * Note: In order for the membership broadcaster to
+ * transmit the correct information, it has to be started after the replication receiver.
+ * @throws ChannelException if a startup error occurs or the service is already started or an error occurs.
+ */
+ public void start(int svc) throws ChannelException;
+
+ /**
+ * Shuts down the channel. This can be called multiple times for individual services to shutdown
+ * The svc parameter can be the logical or value of any constants
+ * @param svc int value of
+ * DEFAULT - will shutdown all services
+ * MBR_RX_SEQ - stops the membership receiver
+ * MBR_TX_SEQ - stops the membership broadcaster
+ * SND_TX_SEQ - stops the replication transmitter
+ * SND_RX_SEQ - stops the replication receiver
+ * @throws ChannelException if a startup error occurs or the service is already stopped or an error occurs.
+ */
+ public void stop(int svc) throws ChannelException;
+
+ /**
+ * Send a message to one or more members in the cluster
+ * @param destination Member[] - the destinations, can not be null or zero length, the reason for that
+ * is that a membership change can occur and at that time the application is uncertain what group the message
+ * actually got sent to.
+ * @param msg Serializable - the message to send, has to be serializable, or a ByteMessage to
+ * send a pure byte array
+ * @param options int - sender options, see class documentation for each interceptor that is configured in order to trigger interceptors
+ * @return a unique Id that identifies the message that is sent
+ * @see ByteMessage
+ * @see #SEND_OPTIONS_USE_ACK
+ * @see #SEND_OPTIONS_ASYNCHRONOUS
+ * @see #SEND_OPTIONS_SYNCHRONIZED_ACK
+ */
+ public UniqueId send(Member[] destination, Serializable msg, int options) throws ChannelException;
+
+ /**
+ * Send a message to one or more members in the cluster
+ * @param destination Member[] - the destinations, null or zero length means all
+ * @param msg ClusterMessage - the message to send
+ * @param options int - sender options, see class documentation
+ * @param handler ErrorHandler - handle errors through a callback, rather than throw it
+ * @return a unique Id that identifies the message that is sent
+ * @exception ChannelException - if a serialization error happens.
+ */
+ public UniqueId send(Member[] destination, Serializable msg, int options, ErrorHandler handler) throws ChannelException;
+
+ /**
+ * Sends a heart beat through the interceptor stacks
+ * Use this method to alert interceptors and other components to
+ * clean up garbage, timed out messages etc.
+ * If you application has a background thread, then you can save one thread,
+ * by configuring your channel to not use an internal heartbeat thread
+ * and invoking this method.
+ * @see #setHeartbeat(boolean)
+ */
+ public void heartbeat();
+
+ /**
+ * Enables or disables internal heartbeat.
+ * @param enable boolean - default value is implementation specific
+ * @see #heartbeat()
+ */
+ public void setHeartbeat(boolean enable);
+
+ /**
+ * Add a membership listener, will get notified when a new member joins, leaves or crashes
+ *
If the membership listener implements the Heartbeat interface
+ * the heartbeat() method will be invoked when the heartbeat runs on the channel
+ * @param listener MembershipListener
+ * @see MembershipListener
+ */
+ public void addMembershipListener(MembershipListener listener);
+
+ /**
+ * Add a channel listener, this is a callback object when messages are received
+ *
If the channel listener implements the Heartbeat interface
+ * the heartbeat() method will be invoked when the heartbeat runs on the channel
+ * @param listener ChannelListener
+ * @see ChannelListener
+ * @see Heartbeat
+ */
+ public void addChannelListener(ChannelListener listener);
+
+ /**
+ * remove a membership listener, listeners are removed based on Object.hashCode and Object.equals
+ * @param listener MembershipListener
+ * @see MembershipListener
+ */
+ public void removeMembershipListener(MembershipListener listener);
+ /**
+ * remove a channel listener, listeners are removed based on Object.hashCode and Object.equals
+ * @param listener ChannelListener
+ * @see ChannelListener
+ */
+ public void removeChannelListener(ChannelListener listener);
+
+ /**
+ * Returns true if there are any members in the group,
+ * this call is the same as getMembers().length>0
+ * @return boolean - true if there are any members automatically discovered
+ */
+ public boolean hasMembers() ;
+
+ /**
+ * Get all current group members
+ * @return all members or empty array, never null
+ */
+ public Member[] getMembers() ;
+
+ /**
+ * Return the member that represents this node. This is also the data
+ * that gets broadcasted through the membership broadcaster component
+ * @param incAlive - optimization, true if you want it to calculate alive time
+ * since the membership service started.
+ * @return Member
+ */
+ public Member getLocalMember(boolean incAlive);
+
+ /**
+ * Returns the member from the membership service with complete and
+ * recent data. Some implementations might serialize and send
+ * membership information along with a message, and instead of sending
+ * complete membership details, only send the primary identifier for the member
+ * but not the payload or other information. When such message is received
+ * the application can retrieve the cached member through this call.
+ * In most cases, this is not necessary.
+ * @param mbr Member
+ * @return Member
+ */
+ public Member getMember(Member mbr);
+
+
+}
diff --git a/java/org/apache/catalina/tribes/ChannelException.java b/java/org/apache/catalina/tribes/ChannelException.java
new file mode 100644
index 000000000..7d5f04c8c
--- /dev/null
+++ b/java/org/apache/catalina/tribes/ChannelException.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes;
+
+import java.util.ArrayList;
+
+/**
+ * Channel Exception
+ * A channel exception is thrown when an internal error happens
+ * somewhere in the channel.
+ * When a global error happens, the cause can be retrieved using getCause()
+ * If an application is sending a message and some of the recipients fail to receive it,
+ * the application can retrieve what recipients failed by using the getFaultyMembers()
+ * method. This way, an application will always know if a message was delivered successfully or not.
+ * @author Filip Hanik
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+ */
+
+public class ChannelException extends Exception {
+ /*
+ * Holds a list of faulty members
+ */
+ private ArrayList faultyMembers=null;
+
+ /**
+ * Constructor, creates a ChannelException
+ * @see java.lang.Exception#Exception()
+ */
+ public ChannelException() {
+ super();
+ }
+
+ /**
+ * Constructor, creates a ChannelException with an error message
+ * @see java.lang.Exception#Exception(String)
+ */
+ public ChannelException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructor, creates a ChannelException with an error message and a cause
+ * @param message String
+ * @param cause Throwable
+ * @see java.lang.Exception#Exception(String,Throwable)
+ */
+ public ChannelException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructor, creates a ChannelException with a cause
+ * @param cause Throwable
+ * @see java.lang.Exception#Exception(Throwable)
+ */
+ public ChannelException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Returns the message for this exception
+ * @return String
+ * @see java.lang.Exception#getMessage()
+ */
+ public String getMessage() {
+ StringBuffer buf = new StringBuffer(super.getMessage());
+ if (faultyMembers==null || faultyMembers.size() == 0 ) {
+ buf.append("; No faulty members identified.");
+ } else {
+ buf.append("; Faulty members:");
+ for ( int i=0; i
Description: Represent a failure to a specific member when a message was sent + * to more than one member
+ * + * @author Filip Hanik + * @version 1.0 + */ + public static class FaultyMember { + protected Exception cause; + protected Member member; + public FaultyMember(Member mbr, Exception x) { + this.member = mbr; + this.cause = x; + } + + public Member getMember() { + return member; + } + + public Exception getCause() { + return cause; + } + + public String toString() { + return "FaultyMember:"+member.toString(); + } + } + +} diff --git a/java/org/apache/catalina/tribes/ChannelInterceptor.java b/java/org/apache/catalina/tribes/ChannelInterceptor.java new file mode 100644 index 000000000..14b8f1160 --- /dev/null +++ b/java/org/apache/catalina/tribes/ChannelInterceptor.java @@ -0,0 +1,179 @@ +/* + * Copyright 1999,2004-2006 The Apache Software Foundation. + * + * Licensed 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.catalina.tribes; + +import org.apache.catalina.tribes.group.InterceptorPayload; + +/** + * A ChannelInterceptor is an interceptor that intercepts + * messages and membership messages in the channel stack. + * This allows interceptors to modify the message or perform + * other actions when a message is sent or received.boolean react = (getOptionFlag() == (getOptionFlag() & ChannelMessage.getOptions()));sendMessage method is called when a message is being sent to one more destinations.
+ * The interceptor can modify any of the parameters and then pass on the message down the stack by
+ * invoking getNext().sendMessage(destination,msg,payload)getNext().sendMessage(destination,msg,payload)messageReceived is invoked when a message is received.
+ * ChannelMessage.getAddress() is the sender, or the reply-to address
+ * if it has been overwritten.
+ * @param data ChannelMessage
+ */
+ public void messageReceived(ChannelMessage data);
+
+ /**
+ * The heartbeat() method gets invoked periodically
+ * to allow interceptors to clean up resources, time out object and
+ * perform actions that are unrelated to sending/receiving data.
+ */
+ public void heartbeat();
+
+ /**
+ * Intercepts the Channel.hasMembers() method
+ * @return boolean - if the channel has members in its membership group
+ * @see Channel#hasMembers()
+ */
+ public boolean hasMembers() ;
+
+ /**
+ * Intercepts the code>Channel.getMembers() method
+ * @return Member[]
+ * @see Channel#getMembers()
+ */
+ public Member[] getMembers() ;
+
+ /**
+ * Intercepts the code>Channel.getLocalMember(boolean) method
+ * @param incAliveTime boolean
+ * @return Member
+ * @see Channel#getLocalMember(boolean)
+ */
+ public Member getLocalMember(boolean incAliveTime) ;
+
+ /**
+ * Intercepts the code>Channel.getMember(Member) method
+ * @param mbr Member
+ * @return Member - the actual member information, including stay alive
+ * @see Channel#getMember(Member)
+ */
+ public Member getMember(Member mbr);
+
+ /**
+ * Starts up the channel. This can be called multiple times for individual services to start
+ * The svc parameter can be the logical or value of any constants
+ * @param svc int value of Title: ChannelListener
+ * + *Description: An interface to listens to incoming messages from a channel
+ * When a message is received, the Channel will invoke the channel listener in a conditional sequence. + *if ( listener.accept(msg,sender) ) listener.messageReceived(msg,sender);accept(Serializable, Member)
+ * if it doesn't intend to process the message. The channel can this way track whether a message
+ * was processed by an above application or if it was just received and forgot about, a featuer required
+ * to support message-response(RPC) callsChannel.getLocalMember(boolean)ChannelReceiver interface is the data receiver component
+ * at the bottom layer, the IO layer (for layers see the javadoc for the {@link Channel} interface).
+ * This class may optionally implement a thread pool for parallel processing of incoming messages.
+ * @author Filip Hanik
+ * @version $Revision: 379904 $, $Date: 2006-02-22 15:16:25 -0600 (Wed, 22 Feb 2006) $
+ */
+public interface ChannelReceiver extends Heartbeat {
+ /**
+ * Start listening for incoming messages on the host/port
+ * @throws java.io.IOException
+ */
+ public void start() throws java.io.IOException;
+
+ /**
+ * Stop listening for messages
+ */
+ public void stop();
+
+ /**
+ * String representation of the IPv4 or IPv6 address that this host is listening
+ * to.
+ * @return the host that this receiver is listening to
+ */
+ public String getHost();
+
+
+ /**
+ * Returns the listening port
+ * @return port
+ */
+ public int getPort();
+
+ /**
+ * Sets the message listener to receive notification of incoming
+ * @param listener MessageListener
+ * @see MessageListener
+ */
+ public void setMessageListener(MessageListener listener);
+
+ /**
+ * Returns the message listener that is associated with this receiver
+ * @return MessageListener
+ * @see MessageListener
+ */
+ public MessageListener getMessageListener();
+
+}
diff --git a/java/org/apache/catalina/tribes/ChannelSender.java b/java/org/apache/catalina/tribes/ChannelSender.java
new file mode 100644
index 000000000..fd245e130
--- /dev/null
+++ b/java/org/apache/catalina/tribes/ChannelSender.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes;
+
+
+/**
+ * ChannelReceiver InterfaceChannelSender interface is the data sender component
+ * at the bottom layer, the IO layer (for layers see the javadoc for the {@link Channel} interface).org.apache.catalina.tribes
+ * package.
+ *
+ * @author Bip Thelin
+ * @author Filip Hanik
+ * @version $Revision: 302726 $, $Date: 2004-02-27 08:59:07 -0600 (Fri, 27 Feb 2004) $
+ */
+
+public final class Constants {
+ public static final String Package = "org.apache.catalina.tribes";
+}
diff --git a/java/org/apache/catalina/tribes/ErrorHandler.java b/java/org/apache/catalina/tribes/ErrorHandler.java
new file mode 100644
index 000000000..6496acecb
--- /dev/null
+++ b/java/org/apache/catalina/tribes/ErrorHandler.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 1999,2004-2006 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes;
+
+
+
+/**
+ * The ErrorHandler class is used when sending messages
+ * that are sent asynchronously and the application still needs to get
+ * confirmation when the message was sent successfully or when a message errored out.
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public interface ErrorHandler {
+
+ /**
+ * Invoked if the message is dispatched asynch, and an error occurs
+ * @param x ChannelException - the error that happened
+ * @param id - the unique id for the message
+ * @see Channel#send(Member[], Serializable, int, ErrorHandler)
+ */
+ public void handleError(ChannelException x, UniqueId id);
+
+ /**
+ * Invoked when the message has been sent successfully.
+ * @param id - the unique id for the message
+ * @see Channel#send(Member[], Serializable, int, ErrorHandler)
+ */
+ public void handleCompletion(UniqueId id);
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/Heartbeat.java b/java/org/apache/catalina/tribes/Heartbeat.java
new file mode 100644
index 000000000..ba5db2b4d
--- /dev/null
+++ b/java/org/apache/catalina/tribes/Heartbeat.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes;
+
+/**
+ * Can be implemented by the ChannelListener and Membership listeners to receive heartbeat
+ * notifications from the Channel
+ * @author Filip Hanik
+ * @version 1.0
+ * @see Channel
+ * @see Channel#heartbeat()
+ */
+public interface Heartbeat {
+
+ /**
+ * Heartbeat invokation for resources cleanup etc
+ */
+ public void heartbeat();
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/LocalStrings.properties b/java/org/apache/catalina/tribes/LocalStrings.properties
new file mode 100644
index 000000000..0dafa4672
--- /dev/null
+++ b/java/org/apache/catalina/tribes/LocalStrings.properties
@@ -0,0 +1 @@
+cluster.mbean.register.already=MBean {0} already registered!
diff --git a/java/org/apache/catalina/tribes/ManagedChannel.java b/java/org/apache/catalina/tribes/ManagedChannel.java
new file mode 100644
index 000000000..aa56d3ac3
--- /dev/null
+++ b/java/org/apache/catalina/tribes/ManagedChannel.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes;
+
+import java.util.Iterator;
+
+/**
+ * Channel interface
+ * A managed channel interface gives you access to the components of the channels
+ * such as senders, receivers, interceptors etc for configurations purposes
+ * @author Filip Hanik
+ * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $
+ */
+public interface ManagedChannel extends Channel {
+
+ /**
+ * Sets the channel sender
+ * @param sender ChannelSender
+ * @see ChannelSender
+ */
+ public void setChannelSender(ChannelSender sender);
+
+ /**
+ * Sets the channel receiver
+ * @param receiver ChannelReceiver
+ * @see ChannelReceiver
+ */
+ public void setChannelReceiver(ChannelReceiver receiver);
+
+ /**
+ * Sets the membership service
+ * @param service MembershipService
+ * @see MembershipService
+ */
+ public void setMembershipService(MembershipService service);
+
+ /**
+ * returns the channel sender
+ * @return ChannelSender
+ * @see ChannelSender
+ */
+ public ChannelSender getChannelSender();
+
+ /**
+ * returns the channel receiver
+ * @return ChannelReceiver
+ * @see ChannelReceiver
+ */
+ public ChannelReceiver getChannelReceiver();
+
+ /**
+ * Returns the membership service
+ * @return MembershipService
+ * @see MembershipService
+ */
+ public MembershipService getMembershipService();
+
+ /**
+ * Returns the interceptor stack
+ * @return Iterator
+ * @see Channel#addInterceptor(ChannelInterceptor)
+ */
+ public Iterator getInterceptors();
+}
diff --git a/java/org/apache/catalina/tribes/Member.java b/java/org/apache/catalina/tribes/Member.java
new file mode 100644
index 000000000..622bab842
--- /dev/null
+++ b/java/org/apache/catalina/tribes/Member.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes;
+
+/**
+ * The Member interface, defines a member in the group.
+ * Each member can carry a set of properties, defined by the actual implementation.MembershipService interface is the membership component
+ * at the bottom layer, the IO layer (for layers see the javadoc for the {@link Channel} interface).start() method is called.
+ * The properties are implementation specific.
+ * @param properties - to be used to configure the membership service.
+ */
+ public void setProperties(java.util.Properties properties);
+ /**
+ * Returns the properties for the configuration used.
+ */
+ public java.util.Properties getProperties();
+ /**
+ * Starts the membership service. If a membership listeners is added
+ * the listener will start to receive membership events.
+ * Performs a start level 1 and 2
+ * @throws java.lang.Exception if the service fails to start.
+ */
+ public void start() throws java.lang.Exception;
+
+ /**
+ * Starts the membership service. If a membership listeners is added
+ * the listener will start to receive membership events.
+ * @param level - level MBR_RX starts listening for members, level MBR_TX
+ * starts broad casting the server
+ * @throws java.lang.Exception if the service fails to start.
+ * @throws java.lang.IllegalArgumentException if the level is incorrect.
+ */
+ public void start(int level) throws java.lang.Exception;
+
+
+ /**
+ * Starts the membership service. If a membership listeners is added
+ * the listener will start to receive membership events.
+ * @param level - level MBR_RX stops listening for members, level MBR_TX
+ * stops broad casting the server
+ * @throws java.lang.Exception if the service fails to stop
+ * @throws java.lang.IllegalArgumentException if the level is incorrect.
+ */
+
+ public void stop(int level);
+
+ /**
+ * @return true if the the group contains members
+ */
+ public boolean hasMembers();
+
+
+ /**
+ *
+ * @param mbr Member
+ * @return Member
+ */
+ public Member getMember(Member mbr);
+ /**
+ * Returns a list of all the members in the cluster.
+ */
+
+ public Member[] getMembers();
+
+ /**
+ * Returns the member object that defines this member
+ */
+ public Member getLocalMember(boolean incAliveTime);
+
+ /**
+ * Return all members by name
+ */
+ public String[] getMembersByName() ;
+
+ /**
+ * Return the member by name
+ */
+ public Member findMemberByName(String name) ;
+
+ /**
+ * Sets the local member properties for broadcasting
+ */
+ public void setLocalMemberProperties(String listenHost, int listenPort);
+
+ /**
+ * Sets the membership listener, only one listener can be added.
+ * If you call this method twice, the last listener will be used.
+ * @param listener The listener
+ */
+ public void setMembershipListener(MembershipListener listener);
+
+ /**
+ * removes the membership listener.
+ */
+ public void removeMembershipListener();
+
+ /**
+ * Set a payload to be broadcasted with each membership
+ * broadcast.
+ * @param payload byte[]
+ */
+ public void setPayload(byte[] payload);
+
+ public void setDomain(byte[] domain);
+
+}
diff --git a/java/org/apache/catalina/tribes/MessageListener.java b/java/org/apache/catalina/tribes/MessageListener.java
new file mode 100644
index 000000000..a6da94a36
--- /dev/null
+++ b/java/org/apache/catalina/tribes/MessageListener.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes;
+
+/**
+ *
+ * Title: MessageListener
+ * + *Description: The listener to be registered with the ChannelReceiver, internal Tribes component
+ * + * @author Filip Hanik + * @version 1.0 + */ + +public interface MessageListener { + + /** + * Receive a message from the IO components in the Channel stack + * @param msg ChannelMessage + */ + public void messageReceived(ChannelMessage msg); + + public boolean accept(ChannelMessage msg); + + public boolean equals(Object listener); + + public int hashCode(); + +} diff --git a/java/org/apache/catalina/tribes/RemoteProcessException.java b/java/org/apache/catalina/tribes/RemoteProcessException.java new file mode 100644 index 000000000..b83e5ed35 --- /dev/null +++ b/java/org/apache/catalina/tribes/RemoteProcessException.java @@ -0,0 +1,46 @@ +/* + * Copyright 1999,2006 The Apache Software Foundation. + * + * Licensed 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.catalina.tribes; + +/** + *Title: RemoteProcessException
+ * + *Description: Message thrown by a sender when USE_SYNC_ACK receives a FAIL_ACK_COMMAND.
+ * This means that the message was received on the remote node but the processing of the message failed.
+ * This message will be embedded in a ChannelException.FaultyMember
+ *
Title: Represents a globabally unique Id
+ * + *Company:
+ * + * @author Filip Hanik + * @version 1.0 + */ +public final class UniqueId implements Serializable{ + protected byte[] id; + + public UniqueId() { + } + + public UniqueId(byte[] id) { + this.id = id; + } + + public UniqueId(byte[] id, int offset, int length) { + this.id = new byte[length]; + System.arraycopy(id,offset,this.id,0,length); + } + + public int hashCode() { + if ( id == null ) return 0; + return Arrays.hashCode(id); + } + + public boolean equals(Object other) { + boolean result = (other instanceof UniqueId); + if ( result ) { + UniqueId uid = (UniqueId)other; + if ( this.id == null && uid.id == null ) result = true; + else if ( this.id == null && uid.id != null ) result = false; + else if ( this.id != null && uid.id == null ) result = false; + else result = Arrays.equals(this.id,uid.id); + }//end if + return result; + } + + public byte[] getBytes() { + return id; + } + + public String toString() { + StringBuffer buf = new StringBuffer("UniqueId"); + buf.append(org.apache.catalina.tribes.util.Arrays.toString(id)); + return buf.toString(); + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/AbsoluteOrder.java b/java/org/apache/catalina/tribes/group/AbsoluteOrder.java new file mode 100644 index 000000000..1542ff73a --- /dev/null +++ b/java/org/apache/catalina/tribes/group/AbsoluteOrder.java @@ -0,0 +1,100 @@ +package org.apache.catalina.tribes.group; + +import org.apache.catalina.tribes.Member; +import java.util.Comparator; +import java.util.Arrays; + +/** + *Title: Membership - Absolute Order
+ * + *Description: A simple, yet agreeable and efficient way of ordering members
+ *
+ * Ordering members can serve as a basis for electing a leader or coordinating efforts.
+ * This is stinky simple, it works on the basis of the Member interface
+ * and orders members in the following format:
+ *
+ *
heartbeat == true then how often do we want this
+ * heartbeat to run. default is one minute
+ */
+ protected long heartbeatSleeptime = 5*1000;//every 5 seconds
+
+ /**
+ * Internal heartbeat thread
+ */
+ protected HeartbeatThread hbthread = null;
+
+ /**
+ * The ChannelCoordinator coordinates the bottom layer components:channel.addInterceptor(A);channel.addInterceptor(C);channel.addInterceptor(B);A -> C -> BChannel -> A -> C -> B -> ChannelCoordinatorchannel.setHeartbeat(false)
+ */
+ public void heartbeat() {
+ super.heartbeat();
+ Iterator i = membershipListeners.iterator();
+ while ( i.hasNext() ) {
+ Object o = i.next();
+ if ( o instanceof Heartbeat ) ((Heartbeat)o).heartbeat();
+ }
+ i = channelListeners.iterator();
+ while ( i.hasNext() ) {
+ Object o = i.next();
+ if ( o instanceof Heartbeat ) ((Heartbeat)o).heartbeat();
+ }
+
+ }
+
+
+ /**
+ * Send a message to the destinations specified
+ * @param destination Member[] - destination.length > 1
+ * @param msg Serializable - the message to send
+ * @param options int - sender options, options can trigger guarantee levels and different interceptors to
+ * react to the message see class documentation for the Channel object.Channel object.Channel.SEND_OPTIONS_ASYNCHRONOUS flag enabled.
+ * @return UniqueId - the unique Id that was assigned to this message
+ * @throws ChannelException - if an error occurs processing the message
+ * @see org.apache.catalina.tribes.Channel
+ */
+ public UniqueId send(Member[] destination, Serializable msg, int options, ErrorHandler handler) throws ChannelException {
+ if ( msg == null ) throw new ChannelException("Cant send a NULL message");
+ XByteBuffer buffer = null;
+ try {
+ if ( destination == null || destination.length == 0) throw new ChannelException("No destination given");
+ ChannelData data = new ChannelData(true);//generates a unique Id
+ data.setAddress(getLocalMember(false));
+ data.setTimestamp(System.currentTimeMillis());
+ byte[] b = null;
+ if ( msg instanceof ByteMessage ){
+ b = ((ByteMessage)msg).getMessage();
+ options = options | SEND_OPTIONS_BYTE_MESSAGE;
+ } else {
+ b = XByteBuffer.serialize(msg);
+ options = options & (~SEND_OPTIONS_BYTE_MESSAGE);
+ }
+ data.setOptions(options);
+ //XByteBuffer buffer = new XByteBuffer(b.length+128,false);
+ buffer = BufferPool.getBufferPool().getBuffer(b.length+128, false);
+ buffer.append(b,0,b.length);
+ data.setMessage(buffer);
+ InterceptorPayload payload = null;
+ if ( handler != null ) {
+ payload = new InterceptorPayload();
+ payload.setErrorHandler(handler);
+ }
+ getFirstInterceptor().sendMessage(destination, data, payload);
+ if ( Logs.MESSAGES.isTraceEnabled() ) {
+ Logs.MESSAGES.trace("GroupChannel - Sent msg:" + new UniqueId(data.getUniqueId()) + " at " +new java.sql.Timestamp(System.currentTimeMillis())+ " to "+Arrays.toNameString(destination));
+ Logs.MESSAGES.trace("GroupChannel - Send Message:" + new UniqueId(data.getUniqueId()) + " is " +msg);
+ }
+
+ return new UniqueId(data.getUniqueId());
+ }catch ( Exception x ) {
+ if ( x instanceof ChannelException ) throw (ChannelException)x;
+ throw new ChannelException(x);
+ } finally {
+ if ( buffer != null ) BufferPool.getBufferPool().returnBuffer(buffer);
+ }
+ }
+
+
+ /**
+ * Callback from the interceptor stack. getHeartbeat()==true
+ * @param heartbeatSleeptime long - time in milliseconds to sleep between heartbeats
+ */
+ public void setHeartbeatSleeptime(long heartbeatSleeptime) {
+ this.heartbeatSleeptime = heartbeatSleeptime;
+ }
+
+ /**
+ * Enables or disables local heartbeat.
+ * if setHeartbeat(true) is invoked then the channel will start an internal
+ * thread to invoke Channel.heartbeat() every getHeartbeatSleeptime milliseconds
+ * @param heartbeat boolean
+ */
+ public void setHeartbeat(boolean heartbeat) {
+ this.heartbeat = heartbeat;
+ }
+
+ /**
+ * @see #setOptionCheck(boolean)
+ * @return boolean
+ */
+ public boolean getOptionCheck() {
+ return optionCheck;
+ }
+
+ /**
+ * @see #setHeartbeat(boolean)
+ * @return boolean
+ */
+ public boolean getHeartbeat() {
+ return heartbeat;
+ }
+
+ /**
+ * Returns the sleep time in milliseconds that the internal heartbeat will
+ * sleep in between invokations of Channel.heartbeat()
+ * @return long
+ */
+ public long getHeartbeatSleeptime() {
+ return heartbeatSleeptime;
+ }
+
+ /**
+ *
+ * Title: Interceptor Iterator
+ * + *Description: An iterator to loop through the interceptors in a channel
+ * + * @version 1.0 + */ + public static class InterceptorIterator implements Iterator { + private ChannelInterceptor end; + private ChannelInterceptor start; + public InterceptorIterator(ChannelInterceptor start, ChannelInterceptor end) { + this.end = end; + this.start = start; + } + + public boolean hasNext() { + return start!=null && start != end; + } + + public Object next() { + Object result = null; + if ( hasNext() ) { + result = start; + start = start.getNext(); + } + return result; + } + + public void remove() { + //empty operation + } + } + + /** + * + *Title: Internal heartbeat thread
+ * + *Description: if Channel.getHeartbeat()==true then a thread of this class
+ * is created
Description:
+ * + *Copyright: Copyright (c) 2005
+ * + *Company:
+ * + * @author not attributable + * @version 1.0 + */ +public class RpcMessage implements Externalizable { + + protected Serializable message; + protected byte[] uuid; + protected byte[] rpcId; + protected boolean reply = false; + + public RpcMessage() { + //for serialization + } + + public RpcMessage(byte[] rpcId, byte[] uuid, Serializable message) { + this.rpcId = rpcId; + this.uuid = uuid; + this.message = message; + } + + public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException { + reply = in.readBoolean(); + int length = in.readInt(); + uuid = new byte[length]; + in.read(uuid, 0, length); + length = in.readInt(); + rpcId = new byte[length]; + in.read(rpcId, 0, length); + message = (Serializable)in.readObject(); + } + + public void writeExternal(ObjectOutput out) throws IOException { + out.writeBoolean(reply); + out.writeInt(uuid.length); + out.write(uuid, 0, uuid.length); + out.writeInt(rpcId.length); + out.write(rpcId, 0, rpcId.length); + out.writeObject(message); + } + + public static class NoRpcChannelReply extends RpcMessage { + public NoRpcChannelReply() { + + } + + public NoRpcChannelReply(byte[] rpcid, byte[] uuid) { + super(rpcid,uuid,null); + reply = true; + } + + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + reply = true; + int length = in.readInt(); + uuid = new byte[length]; + in.read(uuid, 0, length); + length = in.readInt(); + rpcId = new byte[length]; + in.read(rpcId, 0, length); + } + + public void writeExternal(ObjectOutput out) throws IOException { + out.writeInt(uuid.length); + out.write(uuid, 0, uuid.length); + out.writeInt(rpcId.length); + out.write(rpcId, 0, rpcId.length); + } + } + + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptor.java new file mode 100644 index 000000000..ebe4e94e9 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/DomainFilterInterceptor.java @@ -0,0 +1,101 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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 + */ +package org.apache.catalina.tribes.group.interceptors; + +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.membership.MemberImpl; +import org.apache.catalina.tribes.membership.Membership; +import java.util.Arrays; + +/** + *Title: Member domain filter interceptor
+ * + *Description: Filters membership based on domain. + *
+ * + * @author Filip Hanik + * @version 1.0 + */ +public class DomainFilterInterceptor extends ChannelInterceptorBase { + + private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( DomainFilterInterceptor.class ); + + protected Membership membership = null; + + protected byte[] domain = new byte[0]; + + public void messageReceived(ChannelMessage msg) { + //should we filter incoming based on domain? + super.messageReceived(msg); + }//messageReceived + + + public void memberAdded(Member member) { + if ( membership == null ) setupMembership(); + boolean notify = false; + synchronized (membership) { + notify = Arrays.equals(domain,member.getDomain()); + if ( notify ) notify = membership.memberAlive((MemberImpl)member); + } + if ( notify ) super.memberAdded(member); + } + + public void memberDisappeared(Member member) { + if ( membership == null ) setupMembership(); + boolean notify = false; + synchronized (membership) { + notify = Arrays.equals(domain,member.getDomain()); + membership.removeMember((MemberImpl)member); + } + if ( notify ) super.memberDisappeared(member); + } + + public boolean hasMembers() { + if ( membership == null ) setupMembership(); + return membership.hasMembers(); + } + + public Member[] getMembers() { + if ( membership == null ) setupMembership(); + return membership.getMembers(); + } + + public Member getMember(Member mbr) { + if ( membership == null ) setupMembership(); + return membership.getMember(mbr); + } + + public Member getLocalMember(boolean incAlive) { + return super.getLocalMember(incAlive); + } + + + protected synchronized void setupMembership() { + if ( membership == null ) { + membership = new Membership((MemberImpl)super.getLocalMember(true)); + } + + } + + public byte[] getDomain() { + return domain; + } + + public void setDomain(byte[] domain) { + this.domain = domain; + } +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptor.java new file mode 100644 index 000000000..cdd615bab --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/FragmentationInterceptor.java @@ -0,0 +1,241 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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 + */ + +package org.apache.catalina.tribes.group.interceptors; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Set; + +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.group.InterceptorPayload; +import org.apache.catalina.tribes.io.XByteBuffer; + +/** + * + * The fragmentation interceptor splits up large messages into smaller messages and assembles them on the other end. + * This is very useful when you don't want large messages hogging the sending sockets + * and smaller messages can make it through. + * + *Channel.SEND_OPTIONS_ASYNCHRONOUS
+ * flag to be set, if it is, it will queue the message for delivery and immediately return to the sender.
+ *
+ *
+ *
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public class MessageDispatchInterceptor extends ChannelInterceptorBase implements Runnable {
+ protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(MessageDispatchInterceptor.class);
+
+ protected long maxQueueSize = 1024*1024*64; //64MB
+ protected FastQueue queue = new FastQueue();
+ protected boolean run = false;
+ protected Thread msgDispatchThread = null;
+ protected long currentSize = 0;
+ protected boolean useDeepClone = true;
+ protected boolean alwaysSend = true;
+
+ public MessageDispatchInterceptor() {
+ setOptionFlag(Channel.SEND_OPTIONS_ASYNCHRONOUS);
+ }
+
+ public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException {
+ boolean async = (msg.getOptions() & Channel.SEND_OPTIONS_ASYNCHRONOUS) == Channel.SEND_OPTIONS_ASYNCHRONOUS;
+ if ( async && run ) {
+ if ( (getCurrentSize()+msg.getMessage().getLength()) > maxQueueSize ) {
+ if ( alwaysSend ) {
+ super.sendMessage(destination,msg,payload);
+ return;
+ } else {
+ throw new ChannelException("Asynchronous queue is full, reached its limit of " + maxQueueSize +" bytes, current:" + getCurrentSize() + " bytes.");
+ }//end if
+ }//end if
+ //add to queue
+ if ( useDeepClone ) msg = (ChannelMessage)msg.deepclone();
+ if (!addToQueue(msg, destination, payload) ) {
+ throw new ChannelException("Unable to add the message to the async queue, queue bug?");
+ }
+ addAndGetCurrentSize(msg.getMessage().getLength());
+ } else {
+ super.sendMessage(destination, msg, payload);
+ }
+ }
+
+ public boolean addToQueue(ChannelMessage msg, Member[] destination, InterceptorPayload payload) {
+ return queue.add(msg,destination,payload);
+ }
+
+ public LinkObject removeFromQueue() {
+ return queue.remove();
+ }
+
+ public void startQueue() {
+ msgDispatchThread = new Thread(this);
+ msgDispatchThread.setName("MessageDispatchInterceptor.MessageDispatchThread");
+ msgDispatchThread.setDaemon(true);
+ msgDispatchThread.setPriority(Thread.MAX_PRIORITY);
+ queue.setEnabled(true);
+ run = true;
+ msgDispatchThread.start();
+ }
+
+ public void stopQueue() {
+ run = false;
+ msgDispatchThread.interrupt();
+ queue.setEnabled(false);
+ setAndGetCurrentSize(0);
+ }
+
+
+ public void setOptionFlag(int flag) {
+ if ( flag != Channel.SEND_OPTIONS_ASYNCHRONOUS ) log.warn("Warning, you are overriding the asynchronous option flag, this will disable the Channel.SEND_OPTIONS_ASYNCHRONOUS that other apps might use.");
+ super.setOptionFlag(flag);
+ }
+
+ public void setMaxQueueSize(long maxQueueSize) {
+ this.maxQueueSize = maxQueueSize;
+ }
+
+ public void setUseDeepClone(boolean useDeepClone) {
+ this.useDeepClone = useDeepClone;
+ }
+
+ public long getMaxQueueSize() {
+ return maxQueueSize;
+ }
+
+ public boolean getUseDeepClone() {
+ return useDeepClone;
+ }
+
+ public long getCurrentSize() {
+ return currentSize;
+ }
+
+ public synchronized long addAndGetCurrentSize(long inc) {
+ currentSize += inc;
+ return currentSize;
+ }
+
+ public synchronized long setAndGetCurrentSize(long value) {
+ currentSize = value;
+ return value;
+ }
+
+ public void start(int svc) throws ChannelException {
+ //start the thread
+ if (!run ) {
+ synchronized (this) {
+ if ( !run && ((svc & Channel.SND_TX_SEQ)==Channel.SND_TX_SEQ) ) {//only start with the sender
+ startQueue();
+ }//end if
+ }//sync
+ }//end if
+ super.start(svc);
+ }
+
+
+ public void stop(int svc) throws ChannelException {
+ //stop the thread
+ if ( run ) {
+ synchronized (this) {
+ if ( run && ((svc & Channel.SND_TX_SEQ)==Channel.SND_TX_SEQ)) {
+ stopQueue();
+ }//end if
+ }//sync
+ }//end if
+
+ super.stop(svc);
+ }
+
+ public void run() {
+ while ( run ) {
+ LinkObject link = removeFromQueue();
+ if ( link == null ) continue; //should not happen unless we exceed wait time
+ while ( link != null && run ) {
+ link = sendAsyncData(link);
+ }//while
+ }//while
+ }//run
+
+ protected LinkObject sendAsyncData(LinkObject link) {
+ ChannelMessage msg = link.data();
+ Member[] destination = link.getDestination();
+ try {
+ super.sendMessage(destination,msg,null);
+ try {
+ if ( link.getHandler() != null ) link.getHandler().handleCompletion(new UniqueId(msg.getUniqueId()));
+ } catch ( Exception ex ) {
+ log.error("Unable to report back completed message.",ex);
+ }
+ } catch ( Exception x ) {
+ ChannelException cx = null;
+ if ( x instanceof ChannelException ) cx = (ChannelException)x;
+ else cx = new ChannelException(x);
+ if ( log.isDebugEnabled() ) log.debug("Error while processing async message.",x);
+ try {
+ if (link.getHandler() != null) link.getHandler().handleError(cx, new UniqueId(msg.getUniqueId()));
+ } catch ( Exception ex ) {
+ log.error("Unable to report back error message.",ex);
+ }
+ } finally {
+ addAndGetCurrentSize(-msg.getMessage().getLength());
+ link = link.next();
+ }//try
+ return link;
+ }
+
+
+}
diff --git a/java/org/apache/catalina/tribes/group/interceptors/NonBlockingCoordinator.java b/java/org/apache/catalina/tribes/group/interceptors/NonBlockingCoordinator.java
new file mode 100644
index 000000000..8267f8f34
--- /dev/null
+++ b/java/org/apache/catalina/tribes/group/interceptors/NonBlockingCoordinator.java
@@ -0,0 +1,839 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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
+ */
+package org.apache.catalina.tribes.group.interceptors;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.catalina.tribes.Channel;
+import org.apache.catalina.tribes.ChannelException;
+import org.apache.catalina.tribes.ChannelInterceptor;
+import org.apache.catalina.tribes.ChannelInterceptor.InterceptorEvent;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.UniqueId;
+import org.apache.catalina.tribes.group.AbsoluteOrder;
+import org.apache.catalina.tribes.group.ChannelInterceptorBase;
+import org.apache.catalina.tribes.group.InterceptorPayload;
+import org.apache.catalina.tribes.io.ChannelData;
+import org.apache.catalina.tribes.io.XByteBuffer;
+import org.apache.catalina.tribes.membership.MemberImpl;
+import org.apache.catalina.tribes.membership.Membership;
+import org.apache.catalina.tribes.util.Arrays;
+import org.apache.catalina.tribes.util.UUIDGenerator;
+
+/**
+ * Title: Auto merging leader election algorithm
+ * + *Description: Implementation of a simple coordinator algorithm that not only selects a coordinator, + * it also merges groups automatically when members are discovered that werent part of the + *
+ *This algorithm is non blocking meaning it allows for transactions while the coordination phase is going on + *
+ *This implementation is based on a home brewed algorithm that uses the AbsoluteOrder of a membership
+ * to pass a token ring of the current membership.
+ * This is not the same as just using AbsoluteOrder! Consider the following scenario:
+ * Nodes, A,B,C,D,E on a network, in that priority. AbsoluteOrder will only work if all
+ * nodes are receiving pings from all the other nodes.
+ * meaning, that node{i} receives pings from node{all}-node{i}
+ * but the following could happen if a multicast problem occurs.
+ * A has members {B,C,D}
+ * B has members {A,C}
+ * C has members {D,E}
+ * D has members {A,B,C,E}
+ * E has members {A,C,D}
+ * Because the default Tribes membership implementation, relies on the multicast packets to
+ * arrive at all nodes correctly, there is nothing guaranteeing that it will.
+ *
+ * To best explain how this algorithm works, lets take the above example:
+ * For simplicity we assume that a send operation is O(1) for all nodes, although this algorithm will work
+ * where messages overlap, as they all depend on absolute order
+ * Scenario 1: A,B,C,D,E all come online at the same time
+ * Eval phase, A thinks of itself as leader, B thinks of A as leader,
+ * C thinks of itself as leader, D,E think of A as leader
+ * Token phase:
+ * (1) A sends out a message X{A-ldr, A-src, mbrs-A,B,C,D} to B where X is the id for the message(and the view)
+ * (1) C sends out a message Y{C-ldr, C-src, mbrs-C,D,E} to D where Y is the id for the message(and the view)
+ * (2) B receives X{A-ldr, A-src, mbrs-A,B,C,D}, sends X{A-ldr, A-src, mbrs-A,B,C,D} to C
+ * (2) D receives Y{C-ldr, C-src, mbrs-C,D,E} D is aware of A,B, sends Y{A-ldr, C-src, mbrs-A,B,C,D,E} to E
+ * (3) C receives X{A-ldr, A-src, mbrs-A,B,C,D}, sends X{A-ldr, A-src, mbrs-A,B,C,D,E} to D
+ * (3) E receives Y{A-ldr, C-src, mbrs-A,B,C,D,E} sends Y{A-ldr, C-src, mbrs-A,B,C,D,E} to A
+ * (4) D receives X{A-ldr, A-src, mbrs-A,B,C,D,E} sends sends X{A-ldr, A-src, mbrs-A,B,C,D,E} to A
+ * (4) A receives Y{A-ldr, C-src, mbrs-A,B,C,D,E}, holds the message, add E to its list of members
+ * (5) A receives X{A-ldr, A-src, mbrs-A,B,C,D,E}
+ * At this point, the state looks like
+ * A - {A-ldr, mbrs-A,B,C,D,E, id=X}
+ * B - {A-ldr, mbrs-A,B,C,D, id=X}
+ * C - {A-ldr, mbrs-A,B,C,D,E, id=X}
+ * D - {A-ldr, mbrs-A,B,C,D,E, id=X}
+ * E - {A-ldr, mbrs-A,B,C,D,E, id=Y}
+ *
+ * A message doesn't stop until it reaches its original sender, unless its dropped by a higher leader.
+ * As you can see, E still thinks the viewId=Y, which is not correct. But at this point we have
+ * arrived at the same membership and all nodes are informed of each other.
+ * To synchronize the rest we simply perform the following check at A when A receives X:
+ * Original X{A-ldr, A-src, mbrs-A,B,C,D} == Arrived X{A-ldr, A-src, mbrs-A,B,C,D,E}
+ * Since the condition is false, A, will resend the token, and A sends X{A-ldr, A-src, mbrs-A,B,C,D,E} to B
+ * When A receives X again, the token is complete.
+ * Optionally, A can send a message X{A-ldr, A-src, mbrs-A,B,C,D,E confirmed} to A,B,C,D,E who then
+ * install and accept the view.
+ *
+ * Lets assume that C1 arrives, C1 has lower priority than C, but higher priority than D.
+ * Lets also assume that C1 sees the following view {B,D,E}
+ * C1 waits for a token to arrive. When the token arrives, the same scenario as above will happen.
+ * In the scenario where C1 sees {D,E} and A,B,C can not see C1, no token will ever arrive.
+ * In this case, C1 sends a Z{C1-ldr, C1-src, mbrs-C1,D,E} to D
+ * D receives Z{C1-ldr, C1-src, mbrs-C1,D,E} and sends Z{A-ldr, C1-src, mbrs-A,B,C,C1,D,E} to E
+ * E receives Z{A-ldr, C1-src, mbrs-A,B,C,C1,D,E} and sends it to A
+ * A sends Z{A-ldr, A-src, mbrs-A,B,C,C1,D,E} to B and the chain continues until A receives the token again.
+ * At that time A optionally sends out Z{A-ldr, A-src, mbrs-A,B,C,C1,D,E, confirmed} to A,B,C,C1,D,E
+ *
To ensure that the view gets implemented at all nodes at the same time, + * A will send out a VIEW_CONF message, this is the 'confirmed' message that is optional above. + *
Ideally, the interceptor below this one would be the TcpFailureDetector to ensure correct memberships
+ * + *The example above, of course can be simplified with a finite statemachine:
+ * But I suck at writing state machines, my head gets all confused. One day I will document this algorithm though.
+ * Maybe I'll do a state diagram :)
+ *
Description: The TcpFailureDetector is a useful interceptor + * that adds reliability to the membership layer.
+ *+ * If the network is busy, or the system is busy so that the membership receiver thread + * is not getting enough time to update its table, members can be "timed out" + * This failure detector will intercept the memberDisappeared message(unless its a true shutdown message) + * and connect to the member using TCP. + *
+ *
+ * The TcpFailureDetector works in two ways.
+ * 1. It intercepts memberDisappeared events
+ * 2. It catches send errors
+ *
Title:
+ * + *Description:
+ * + *Copyright: Copyright (c) 2005
+ * + *Company:
+ * + * @author not attributable + * @version 1.0 + */ +public class TwoPhaseCommitInterceptor extends ChannelInterceptorBase { + + public static final byte[] START_DATA = new byte[] {113, 1, -58, 2, -34, -60, 75, -78, -101, -12, 32, -29, 32, 111, -40, 4}; + public static final byte[] END_DATA = new byte[] {54, -13, 90, 110, 47, -31, 75, -24, -81, -29, 36, 52, -58, 77, -110, 56}; + private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(TwoPhaseCommitInterceptor.class); + + protected HashMap messages = new HashMap(); + protected long expire = 1000 * 60; //one minute expiration + protected boolean deepclone = true; + + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws + ChannelException { + //todo, optimize, if destination.length==1, then we can do + //msg.setOptions(msg.getOptions() & (~getOptionFlag()) + //and just send one message + if (okToProcess(msg.getOptions()) ) { + super.sendMessage(destination, msg, null); + ChannelMessage confirmation = null; + if ( deepclone ) confirmation = (ChannelMessage)msg.deepclone(); + else confirmation = (ChannelMessage)msg.clone(); + confirmation.getMessage().reset(); + UUIDGenerator.randomUUID(false,confirmation.getUniqueId(),0); + confirmation.getMessage().append(START_DATA,0,START_DATA.length); + confirmation.getMessage().append(msg.getUniqueId(),0,msg.getUniqueId().length); + confirmation.getMessage().append(END_DATA,0,END_DATA.length); + super.sendMessage(destination,confirmation,payload); + } else { + //turn off two phase commit + //this wont work if the interceptor has 0 as a flag + //since there is no flag to turn off + //msg.setOptions(msg.getOptions() & (~getOptionFlag())); + super.sendMessage(destination, msg, payload); + } + } + + public void messageReceived(ChannelMessage msg) { + if (okToProcess(msg.getOptions())) { + if ( msg.getMessage().getLength() == (START_DATA.length+msg.getUniqueId().length+END_DATA.length) && + Arrays.contains(msg.getMessage().getBytesDirect(),0,START_DATA,0,START_DATA.length) && + Arrays.contains(msg.getMessage().getBytesDirect(),START_DATA.length+msg.getUniqueId().length,END_DATA,0,END_DATA.length) ) { + UniqueId id = new UniqueId(msg.getMessage().getBytesDirect(),START_DATA.length,msg.getUniqueId().length); + MapEntry original = (MapEntry)messages.get(id); + if ( original != null ) { + super.messageReceived(original.msg); + messages.remove(id); + } else log.warn("Received a confirmation, but original message is missing. Id:"+Arrays.toString(id.getBytes())); + } else { + UniqueId id = new UniqueId(msg.getUniqueId()); + MapEntry entry = new MapEntry((ChannelMessage)msg.deepclone(),id,System.currentTimeMillis()); + messages.put(id,entry); + } + } else { + super.messageReceived(msg); + } + } + + public boolean getDeepclone() { + return deepclone; + } + + public long getExpire() { + return expire; + } + + public void setDeepclone(boolean deepclone) { + this.deepclone = deepclone; + } + + public void setExpire(long expire) { + this.expire = expire; + } + + public void heartbeat() { + try { + long now = System.currentTimeMillis(); + Map.Entry[] entries = (Map.Entry[])messages.entrySet().toArray(new Map.Entry[messages.size()]); + for (int i=0; iChannelData object is used to transfer a message through the
+ * channel interceptor stack and eventually out on a transport to be sent
+ * to another node. While the message is being processed by the different
+ * interceptors, the message data can be manipulated as each interceptor seems appropriate.
+ * @author Peter Rossbach
+ * @author Filip Hanik
+ * @version $Revision: 377484 $ $Date: 2006-02-13 15:00:05 -0600 (Mon, 13 Feb 2006) $
+ *
+ */
+public class ChannelData implements ChannelMessage {
+ public static ChannelData[] EMPTY_DATA_ARRAY = new ChannelData[0];
+
+ public static boolean USE_SECURE_RANDOM_FOR_UUID = false;
+
+ /**
+ * The options this message was sent with
+ */
+ private int options = 0 ;
+ /**
+ * The message data, stored in a dynamic buffer
+ */
+ private XByteBuffer message ;
+ /**
+ * The timestamp that goes with this message
+ */
+ private long timestamp ;
+ /**
+ * A unique message id
+ */
+ private byte[] uniqueId ;
+ /**
+ * The source or reply-to address for this message
+ */
+ private Member address;
+
+ /**
+ * Creates an empty channel data with a new unique Id
+ * @see #ChannelData(boolean)
+ */
+ public ChannelData() {
+ this(true);
+ }
+
+ /**
+ * Create an empty channel data object
+ * @param generateUUID boolean - if true, a unique Id will be generated
+ */
+ public ChannelData(boolean generateUUID) {
+ if ( generateUUID ) generateUUID();
+ }
+
+
+
+ /**
+ * Creates a new channel data object with data
+ * @param uniqueId - unique message id
+ * @param message - message data
+ * @param timestamp - message timestamp
+ */
+ public ChannelData(byte[] uniqueId, XByteBuffer message, long timestamp) {
+ this.uniqueId = uniqueId;
+ this.message = message;
+ this.timestamp = timestamp;
+ }
+
+ /**
+ * @return Returns the message byte buffer
+ */
+ public XByteBuffer getMessage() {
+ return message;
+ }
+ /**
+ * @param message The message to send.
+ */
+ public void setMessage(XByteBuffer message) {
+ this.message = message;
+ }
+ /**
+ * @return Returns the timestamp.
+ */
+ public long getTimestamp() {
+ return timestamp;
+ }
+ /**
+ * @param timestamp The timestamp to send
+ */
+ public void setTimestamp(long timestamp) {
+ this.timestamp = timestamp;
+ }
+ /**
+ * @return Returns the uniqueId.
+ */
+ public byte[] getUniqueId() {
+ return uniqueId;
+ }
+ /**
+ * @param uniqueId The uniqueId to send.
+ */
+ public void setUniqueId(byte[] uniqueId) {
+ this.uniqueId = uniqueId;
+ }
+ /**
+ * @return returns the message options
+ * see org.apache.catalina.tribes.Channel#sendMessage(org.apache.catalina.tribes.Member[], java.io.Serializable, int)
+ *
+ */
+ public int getOptions() {
+ return options;
+ }
+ /**
+ * @param sets the message options
+ */
+ public void setOptions(int options) {
+ this.options = options;
+ }
+
+ /**
+ * Returns the source or reply-to address
+ * @return Member
+ */
+ public Member getAddress() {
+ return address;
+ }
+
+ /**
+ * Sets the source or reply-to address
+ * @param address Member
+ */
+ public void setAddress(Member address) {
+ this.address = address;
+ }
+
+ /**
+ * Generates a UUID and invokes setUniqueId
+ */
+ public void generateUUID() {
+ byte[] data = new byte[16];
+ UUIDGenerator.randomUUID(USE_SECURE_RANDOM_FOR_UUID,data,0);
+ setUniqueId(data);
+ }
+
+ public int getDataPackageLength() {
+ int length =
+ 4 + //options
+ 8 + //timestamp off=4
+ 4 + //unique id length off=12
+ uniqueId.length+ //id data off=12+uniqueId.length
+ 4 + //addr length off=12+uniqueId.length+4
+ ((MemberImpl)address).getDataLength()+ //member data off=12+uniqueId.length+4+add.length
+ 4 + //message length off=12+uniqueId.length+4+add.length+4
+ message.getLength();
+ return length;
+
+ }
+
+ /**
+ * Serializes the ChannelData object into a byte[] array
+ * @return byte[]
+ */
+ public byte[] getDataPackage() {
+ int length = getDataPackageLength();
+ byte[] data = new byte[length];
+ int offset = 0;
+ return getDataPackage(data,offset);
+ }
+
+ public byte[] getDataPackage(byte[] data, int offset) {
+ byte[] addr = ((MemberImpl)address).getData(false);
+ XByteBuffer.toBytes(options,data,offset);
+ offset += 4; //options
+ XByteBuffer.toBytes(timestamp,data,offset);
+ offset += 8; //timestamp
+ XByteBuffer.toBytes(uniqueId.length,data,offset);
+ offset += 4; //uniqueId.length
+ System.arraycopy(uniqueId,0,data,offset,uniqueId.length);
+ offset += uniqueId.length; //uniqueId data
+ XByteBuffer.toBytes(addr.length,data,offset);
+ offset += 4; //addr.length
+ System.arraycopy(addr,0,data,offset,addr.length);
+ offset += addr.length; //addr data
+ XByteBuffer.toBytes(message.getLength(),data,offset);
+ offset += 4; //message.length
+ System.arraycopy(message.getBytesDirect(),0,data,offset,message.getLength());
+ offset += message.getLength(); //message data
+ return data;
+ }
+
+ /**
+ * Deserializes a ChannelData object from a byte array
+ * @param b byte[]
+ * @return ChannelData
+ */
+ public static ChannelData getDataFromPackage(XByteBuffer xbuf) {
+ ChannelData data = new ChannelData(false);
+ int offset = 0;
+ data.setOptions(XByteBuffer.toInt(xbuf.getBytesDirect(),offset));
+ offset += 4; //options
+ data.setTimestamp(XByteBuffer.toLong(xbuf.getBytesDirect(),offset));
+ offset += 8; //timestamp
+ data.uniqueId = new byte[XByteBuffer.toInt(xbuf.getBytesDirect(),offset)];
+ offset += 4; //uniqueId length
+ System.arraycopy(xbuf.getBytesDirect(),offset,data.uniqueId,0,data.uniqueId.length);
+ offset += data.uniqueId.length; //uniqueId data
+ byte[] addr = new byte[XByteBuffer.toInt(xbuf.getBytesDirect(),offset)];
+ offset += 4; //addr length
+ System.arraycopy(xbuf.getBytesDirect(),offset,addr,0,addr.length);
+ data.setAddress(MemberImpl.getMember(addr));
+ offset += addr.length; //addr data
+ int xsize = XByteBuffer.toInt(xbuf.getBytesDirect(),offset);
+ offset += 4; //xsize length
+ System.arraycopy(xbuf.getBytesDirect(),offset,xbuf.getBytesDirect(),0,xsize);
+ xbuf.setLength(xsize);
+ data.message = xbuf;
+ return data;
+
+ }
+
+ public static ChannelData getDataFromPackage(byte[] b) {
+ ChannelData data = new ChannelData(false);
+ int offset = 0;
+ data.setOptions(XByteBuffer.toInt(b,offset));
+ offset += 4; //options
+ data.setTimestamp(XByteBuffer.toLong(b,offset));
+ offset += 8; //timestamp
+ data.uniqueId = new byte[XByteBuffer.toInt(b,offset)];
+ offset += 4; //uniqueId length
+ System.arraycopy(b,offset,data.uniqueId,0,data.uniqueId.length);
+ offset += data.uniqueId.length; //uniqueId data
+ byte[] addr = new byte[XByteBuffer.toInt(b,offset)];
+ offset += 4; //addr length
+ System.arraycopy(b,offset,addr,0,addr.length);
+ data.setAddress(MemberImpl.getMember(addr));
+ offset += addr.length; //addr data
+ int xsize = XByteBuffer.toInt(b,offset);
+ //data.message = new XByteBuffer(new byte[xsize],false);
+ data.message = BufferPool.getBufferPool().getBuffer(xsize,false);
+ offset += 4; //message length
+ System.arraycopy(b,offset,data.message.getBytesDirect(),0,xsize);
+ data.message.append(b,offset,xsize);
+ offset += xsize; //message data
+ return data;
+ }
+
+ public int hashCode() {
+ return XByteBuffer.toInt(getUniqueId(),0);
+ }
+
+ /**
+ * Compares to ChannelData objects, only compares on getUniqueId().equals(o.getUniqueId())
+ * @param o Object
+ * @return boolean
+ */
+ public boolean equals(Object o) {
+ if ( o instanceof ChannelData ) {
+ return Arrays.equals(getUniqueId(),((ChannelData)o).getUniqueId());
+ } else return false;
+ }
+
+ /**
+ * Create a shallow clone, only the data gets recreated
+ * @return ClusterData
+ */
+ public Object clone() {
+// byte[] d = this.getDataPackage();
+// return ClusterData.getDataFromPackage(d);
+ ChannelData clone = new ChannelData(false);
+ clone.options = this.options;
+ clone.message = new XByteBuffer(this.message.getBytesDirect(),false);
+ clone.timestamp = this.timestamp;
+ clone.uniqueId = this.uniqueId;
+ clone.address = this.address;
+ return clone;
+ }
+
+ /**
+ * Complete clone
+ * @return ClusterData
+ */
+ public Object deepclone() {
+ byte[] d = this.getDataPackage();
+ return ChannelData.getDataFromPackage(d);
+ }
+
+ /**
+ * Utility method, returns true if the options flag indicates that an ack
+ * is to be sent after the message has been received and processed
+ * @param options int - the options for the message
+ * @return boolean
+ * @see org.apache.catalina.tribes.Channel#SEND_OPTIONS_USE_ACK
+ * @see org.apache.catalina.tribes.Channel#SEND_OPTIONS_SYNCHRONIZED_ACK
+ */
+ public static boolean sendAckSync(int options) {
+ return ( (Channel.SEND_OPTIONS_USE_ACK & options) == Channel.SEND_OPTIONS_USE_ACK) &&
+ ( (Channel.SEND_OPTIONS_SYNCHRONIZED_ACK & options) == Channel.SEND_OPTIONS_SYNCHRONIZED_ACK);
+ }
+
+
+ /**
+ * Utility method, returns true if the options flag indicates that an ack
+ * is to be sent after the message has been received but not yet processed
+ * @param options int - the options for the message
+ * @return boolean
+ * @see org.apache.catalina.tribes.Channel#SEND_OPTIONS_USE_ACK
+ * @see org.apache.catalina.tribes.Channel#SEND_OPTIONS_SYNCHRONIZED_ACK
+ */
+ public static boolean sendAckAsync(int options) {
+ return ( (Channel.SEND_OPTIONS_USE_ACK & options) == Channel.SEND_OPTIONS_USE_ACK) &&
+ ( (Channel.SEND_OPTIONS_SYNCHRONIZED_ACK & options) != Channel.SEND_OPTIONS_SYNCHRONIZED_ACK);
+ }
+
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("ClusterData[src=");
+ buf.append(getAddress()).append("; id=");
+ buf.append(bToS(getUniqueId())).append("; sent=");
+ buf.append(new Timestamp(this.getTimestamp()).toString()).append("]");
+ return buf.toString();
+ }
+
+ public static String bToS(byte[] data) {
+ StringBuffer buf = new StringBuffer(4*16);
+ buf.append("{");
+ for (int i=0; data!=null && iIOException may be thrown if the output stream has
+ * been closed.
+ * @todo Implement this java.io.OutputStream method
+ */
+ public void write(int b) throws IOException {
+ buffer.append((byte)b);
+ }
+
+ public int size() {
+ return buffer.getLength();
+ }
+
+ public byte[] getArrayDirect() {
+ return buffer.getBytesDirect();
+ }
+
+ public byte[] getArray() {
+ return buffer.getBytes();
+ }
+
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/io/ListenCallback.java b/java/org/apache/catalina/tribes/io/ListenCallback.java
new file mode 100644
index 000000000..c0cc9a099
--- /dev/null
+++ b/java/org/apache/catalina/tribes/io/ListenCallback.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes.io;
+
+import org.apache.catalina.tribes.ChannelMessage;
+
+
+
+/**
+ * Internal interface, similar to the MessageListener but used
+ * at the IO base
+ * The listen callback interface is used by the replication system
+ * when data has been received. The interface does not care about
+ * objects and marshalling and just passes the bytes straight through.
+ * @author Filip Hanik
+ * @version $Revision: 303987 $, $Date: 2005-07-08 15:50:30 -0500 (Fri, 08 Jul 2005) $
+ */
+public interface ListenCallback
+{
+ /**
+ * This method is invoked on the callback object to notify it that new data has
+ * been received from one of the cluster nodes.
+ * @param data - the message bytes received from the cluster/replication system
+ */
+ public void messageDataReceived(ChannelMessage data);
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/io/ObjectReader.java b/java/org/apache/catalina/tribes/io/ObjectReader.java
new file mode 100644
index 000000000..c69e8d23e
--- /dev/null
+++ b/java/org/apache/catalina/tribes/io/ObjectReader.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes.io;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+
+import org.apache.catalina.tribes.ChannelMessage;
+
+
+
+/**
+ * The object reader object is an object used in conjunction with
+ * java.nio TCP messages. This object stores the message bytes in a
+ * XByteBuffer until a full package has been received.
+ * This object uses an XByteBuffer which is an extendable object buffer that also allows
+ * for message encoding and decoding.
+ *
+ * @author Filip Hanik
+ * @version $Revision: 377484 $, $Date: 2006-02-13 15:00:05 -0600 (Mon, 13 Feb 2006) $
+ */
+public class ObjectReader {
+
+ protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ObjectReader.class);
+
+ private XByteBuffer buffer;
+
+ protected long lastAccess = System.currentTimeMillis();
+
+ protected boolean accessed = false;
+ private boolean cancelled;
+
+ /**
+ * Creates an ObjectReader for a TCP NIO socket channel
+ * @param channel - the channel to be read.
+ */
+ public ObjectReader(SocketChannel channel) {
+ this(channel.socket());
+ }
+
+ /**
+ * Creates an ObjectReader for a TCP socket
+ * @param socket Socket
+ */
+ public ObjectReader(Socket socket) {
+ try{
+ this.buffer = new XByteBuffer(socket.getReceiveBufferSize(), true);
+ }catch ( IOException x ) {
+ //unable to get buffer size
+ log.warn("Unable to retrieve the socket receiver buffer size, setting to default 43800 bytes.");
+ this.buffer = new XByteBuffer(43800,true);
+ }
+ }
+
+ public synchronized void access() {
+ this.accessed = true;
+ this.lastAccess = System.currentTimeMillis();
+ }
+
+ public synchronized void finish() {
+ this.accessed = false;
+ this.lastAccess = System.currentTimeMillis();
+ }
+
+ public boolean isAccessed() {
+ return this.accessed;
+ }
+
+ /**
+ * Append new bytes to buffer.
+ * @see XByteBuffer#countPackages()
+ * @param data new transfer buffer
+ * @param off offset
+ * @param len length in buffer
+ * @return number of messages that sended to callback
+ * @throws java.io.IOException
+ */
+ public int append(ByteBuffer data, int len, boolean count) throws java.io.IOException {
+ buffer.append(data,len);
+ int pkgCnt = -1;
+ if ( count ) pkgCnt = buffer.countPackages();
+ return pkgCnt;
+ }
+
+ public int append(byte[] data,int off,int len, boolean count) throws java.io.IOException {
+ buffer.append(data,off,len);
+ int pkgCnt = -1;
+ if ( count ) pkgCnt = buffer.countPackages();
+ return pkgCnt;
+ }
+
+ /**
+ * Send buffer to cluster listener (callback).
+ * Is message complete receiver send message to callback?
+ *
+ * @see org.apache.catalina.tribes.transport.ClusterReceiverBase#messageDataReceived(ChannelMessage)
+ * @see XByteBuffer#doesPackageExist()
+ * @see XByteBuffer#extractPackage(boolean)
+ *
+ * @return number of received packages/messages
+ * @throws java.io.IOException
+ */
+ public ChannelMessage[] execute() throws java.io.IOException {
+ int pkgCnt = buffer.countPackages();
+ ChannelMessage[] result = new ChannelMessage[pkgCnt];
+ for (int i=0; iObjectInputStream that loads from the
+ * class loader for this web application. This allows classes defined only
+ * with the web application to be found correctly.
+ *
+ * @author Craig R. McClanahan
+ * @author Bip Thelin
+ * @author Filip Hanik
+ * @version $Revision: 377484 $, $Date: 2006-02-13 15:00:05 -0600 (Mon, 13 Feb 2006) $
+ */
+
+public final class ReplicationStream extends ObjectInputStream {
+
+
+ /**
+ * The class loader we will use to resolve classes.
+ */
+ private ClassLoader[] classLoaders = null;
+
+
+ /**
+ * Construct a new instance of CustomObjectInputStream
+ *
+ * @param stream The input stream we will read from
+ * @param classLoader The class loader used to instantiate objects
+ *
+ * @exception IOException if an input/output error occurs
+ */
+ public ReplicationStream(InputStream stream,
+ ClassLoader[] classLoaders)
+ throws IOException {
+
+ super(stream);
+ this.classLoaders = classLoaders;
+ }
+
+ /**
+ * Load the local class equivalent of the specified stream class
+ * description, by using the class loader assigned to this Context.
+ *
+ * @param classDesc Class description from the input stream
+ *
+ * @exception ClassNotFoundException if this class cannot be found
+ * @exception IOException if an input/output error occurs
+ */
+ public Class resolveClass(ObjectStreamClass classDesc)
+ throws ClassNotFoundException, IOException {
+ String name = classDesc.getName();
+ boolean tryRepFirst = name.startsWith("org.apache.catalina.tribes");
+ try {
+ try
+ {
+ if ( tryRepFirst ) return findReplicationClass(name);
+ else return findExternalClass(name);
+ }
+ catch ( Exception x )
+ {
+ if ( tryRepFirst ) return findExternalClass(name);
+ else return findReplicationClass(name);
+ }
+ } catch (ClassNotFoundException e) {
+ return super.resolveClass(classDesc);
+ }
+ }
+
+ public Class findReplicationClass(String name)
+ throws ClassNotFoundException, IOException {
+ Class clazz = Class.forName(name, false, getClass().getClassLoader());
+ return clazz;
+ }
+
+ public Class findExternalClass(String name) throws ClassNotFoundException {
+ ClassNotFoundException cnfe = null;
+ for (int i=0; iSTART_DATA it will be thrown away.
+ *
+ */
+ protected boolean discard = true;
+
+ /**
+ * Constructs a new XByteBuffer
+ * @param size - the initial size of the byte buffer
+ * @todo use a pool of byte[] for performance
+ */
+ public XByteBuffer(int size, boolean discard) {
+ buf = new byte[size];
+ this.discard = discard;
+ }
+
+ public XByteBuffer(byte[] data,boolean discard) {
+ this(data,data.length+128,discard);
+ }
+
+ public XByteBuffer(byte[] data, int size,boolean discard) {
+ int length = Math.max(data.length,size);
+ buf = new byte[length];
+ System.arraycopy(data,0,buf,0,data.length);
+ bufSize = data.length;
+ this.discard = discard;
+ }
+
+ public int getLength() {
+ return bufSize;
+ }
+
+ public void setLength(int size) {
+ if ( size > buf.length ) throw new ArrayIndexOutOfBoundsException("Size is larger than existing buffer.");
+ bufSize = size;
+ }
+
+ public void trim(int length) {
+ if ( (bufSize - length) < 0 )
+ throw new ArrayIndexOutOfBoundsException("Can't trim more bytes than are available. length:"+bufSize+" trim:"+length);
+ bufSize -= length;
+ }
+
+ public void reset() {
+ bufSize = 0;
+ }
+
+ public byte[] getBytesDirect() {
+ return this.buf;
+ }
+
+ /**
+ * Returns the bytes in the buffer, in its exact length
+ */
+ public byte[] getBytes() {
+ byte[] b = new byte[bufSize];
+ System.arraycopy(buf,0,b,0,bufSize);
+ return b;
+ }
+
+ /**
+ * Resets the buffer
+ */
+ public void clear() {
+ bufSize = 0;
+ }
+
+ /**
+ * Appends the data to the buffer. If the data is incorrectly formatted, ie, the data should always start with the
+ * header, false will be returned and the data will be discarded.
+ * @param b - bytes to be appended
+ * @param off - the offset to extract data from
+ * @param len - the number of bytes to append.
+ * @return true if the data was appended correctly. Returns false if the package is incorrect, ie missing header or something, or the length of data is 0
+ */
+ public boolean append(ByteBuffer b, int len) {
+ int newcount = bufSize + len;
+ if (newcount > buf.length) {
+ expand(newcount);
+ }
+ b.get(buf,bufSize,len);
+
+ bufSize = newcount;
+
+ if ( discard ) {
+ if (bufSize > START_DATA.length && (firstIndexOf(buf, 0, START_DATA) == -1)) {
+ bufSize = 0;
+ log.error("Discarded the package, invalid header");
+ return false;
+ }
+ }
+ return true;
+
+ }
+
+ public boolean append(byte i) {
+ int newcount = bufSize + 1;
+ if (newcount > buf.length) {
+ expand(newcount);
+ }
+ buf[bufSize] = i;
+ bufSize = newcount;
+ return true;
+ }
+
+
+ public boolean append(boolean i) {
+ int newcount = bufSize + 1;
+ if (newcount > buf.length) {
+ expand(newcount);
+ }
+ XByteBuffer.toBytes(i,buf,bufSize);
+ bufSize = newcount;
+ return true;
+ }
+
+ public boolean append(long i) {
+ int newcount = bufSize + 8;
+ if (newcount > buf.length) {
+ expand(newcount);
+ }
+ XByteBuffer.toBytes(i,buf,bufSize);
+ bufSize = newcount;
+ return true;
+ }
+
+ public boolean append(int i) {
+ int newcount = bufSize + 4;
+ if (newcount > buf.length) {
+ expand(newcount);
+ }
+ XByteBuffer.toBytes(i,buf,bufSize);
+ bufSize = newcount;
+ return true;
+ }
+
+ public boolean append(byte[] b, int off, int len) {
+ if ((off < 0) || (off > b.length) || (len < 0) ||
+ ((off + len) > b.length) || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ } else if (len == 0) {
+ return false;
+ }
+
+ int newcount = bufSize + len;
+ if (newcount > buf.length) {
+ expand(newcount);
+ }
+ System.arraycopy(b, off, buf, bufSize, len);
+ bufSize = newcount;
+
+ if ( discard ) {
+ if (bufSize > START_DATA.length && (firstIndexOf(buf, 0, START_DATA) == -1)) {
+ bufSize = 0;
+ log.error("Discarded the package, invalid header");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public void expand(int newcount) {
+ //don't change the allocation strategy
+ byte newbuf[] = new byte[Math.max(buf.length << 1, newcount)];
+ System.arraycopy(buf, 0, newbuf, 0, bufSize);
+ buf = newbuf;
+ }
+
+ public int getCapacity() {
+ return buf.length;
+ }
+
+
+ /**
+ * Internal mechanism to make a check if a complete package exists
+ * within the buffer
+ * @return - true if a complete package (header,compress,size,data,footer) exists within the buffer
+ */
+ public int countPackages() {
+ return countPackages(false);
+ }
+
+ public int countPackages(boolean first)
+ {
+ int cnt = 0;
+ int pos = START_DATA.length;
+ int start = 0;
+
+ while ( start < bufSize ) {
+ //first check start header
+ int index = XByteBuffer.firstIndexOf(buf,start,START_DATA);
+ //if the header (START_DATA) isn't the first thing or
+ //the buffer isn't even 14 bytes
+ if ( index != start || ((bufSize-start)<14) ) break;
+ //next 4 bytes are compress flag not needed for count packages
+ //then get the size 4 bytes
+ int size = toInt(buf, pos);
+ //now the total buffer has to be long enough to hold
+ //START_DATA.length+4+size+END_DATA.length
+ pos = start + START_DATA.length + 4 + size;
+ if ( (pos + END_DATA.length) > bufSize) break;
+ //and finally check the footer of the package END_DATA
+ int newpos = firstIndexOf(buf, pos, END_DATA);
+ //mismatch, there is no package
+ if (newpos != pos) break;
+ //increase the packet count
+ cnt++;
+ //reset the values
+ start = pos + END_DATA.length;
+ pos = start + START_DATA.length;
+ //we only want to verify that we have at least one package
+ if ( first ) break;
+ }
+ return cnt;
+ }
+
+ /**
+ * Method to check if a package exists in this byte buffer.
+ * @return - true if a complete package (header,options,size,data,footer) exists within the buffer
+ */
+ public boolean doesPackageExist() {
+ return (countPackages(true)>0);
+ }
+
+ /**
+ * Extracts the message bytes from a package.
+ * If no package exists, a IllegalStateException will be thrown.
+ * @param clearFromBuffer - if true, the package will be removed from the byte buffer
+ * @return - returns the actual message bytes (header, compress,size and footer not included).
+ */
+ public XByteBuffer extractDataPackage(boolean clearFromBuffer) {
+ int psize = countPackages(true);
+ if (psize == 0) {
+ throw new java.lang.IllegalStateException("No package exists in XByteBuffer");
+ }
+ int size = toInt(buf, START_DATA.length);
+ XByteBuffer xbuf = BufferPool.getBufferPool().getBuffer(size,false);
+ xbuf.setLength(size);
+ System.arraycopy(buf, START_DATA.length + 4, xbuf.getBytesDirect(), 0, size);
+ if (clearFromBuffer) {
+ int totalsize = START_DATA.length + 4 + size + END_DATA.length;
+ bufSize = bufSize - totalsize;
+ System.arraycopy(buf, totalsize, buf, 0, bufSize);
+ }
+ return xbuf;
+
+ }
+
+ public ChannelData extractPackage(boolean clearFromBuffer) throws java.io.IOException {
+ XByteBuffer xbuf = extractDataPackage(clearFromBuffer);
+ ChannelData cdata = ChannelData.getDataFromPackage(xbuf);
+ return cdata;
+ }
+
+ /**
+ * Creates a complete data package
+ * @param indata - the message data to be contained within the package
+ * @param compressed - compression flag for the indata buffer
+ * @return - a full package (header,size,data,footer)
+ *
+ */
+ public static byte[] createDataPackage(ChannelData cdata) {
+// return createDataPackage(cdata.getDataPackage());
+ //avoid one extra byte array creation
+ int dlength = cdata.getDataPackageLength();
+ int length = getDataPackageLength(dlength);
+ byte[] data = new byte[length];
+ int offset = 0;
+ System.arraycopy(START_DATA, 0, data, offset, START_DATA.length);
+ offset += START_DATA.length;
+ toBytes(dlength,data, START_DATA.length);
+ offset += 4;
+ cdata.getDataPackage(data,offset);
+ offset += dlength;
+ System.arraycopy(END_DATA, 0, data, offset, END_DATA.length);
+ offset += END_DATA.length;
+ return data;
+ }
+
+ public static byte[] createDataPackage(byte[] data, int doff, int dlength, byte[] buffer, int bufoff) {
+ if ( (buffer.length-bufoff) > getDataPackageLength(dlength) ) {
+ throw new ArrayIndexOutOfBoundsException("Unable to create data package, buffer is too small.");
+ }
+ System.arraycopy(START_DATA, 0, buffer, bufoff, START_DATA.length);
+ toBytes(data.length,buffer, bufoff+START_DATA.length);
+ System.arraycopy(data, doff, buffer, bufoff+START_DATA.length + 4, dlength);
+ System.arraycopy(END_DATA, 0, buffer, bufoff+START_DATA.length + 4 + data.length, END_DATA.length);
+ return buffer;
+ }
+
+
+ public static int getDataPackageLength(int datalength) {
+ int length =
+ START_DATA.length + //header length
+ 4 + //data length indicator
+ datalength + //actual data length
+ END_DATA.length; //footer length
+ return length;
+
+ }
+
+ public static byte[] createDataPackage(byte[] data) {
+ int length = getDataPackageLength(data.length);
+ byte[] result = new byte[length];
+ return createDataPackage(data,0,data.length,result,0);
+ }
+
+
+
+// public static void fillDataPackage(byte[] data, int doff, int dlength, XByteBuffer buf) {
+// int pkglen = getDataPackageLength(dlength);
+// if ( buf.getCapacity() < pkglen ) buf.expand(pkglen);
+// createDataPackage(data,doff,dlength,buf.getBytesDirect(),buf.getLength());
+// }
+
+ /**
+ * Convert four bytes to an int
+ * @param b - the byte array containing the four bytes
+ * @param off - the offset
+ * @return the integer value constructed from the four bytes
+ * @exception java.lang.ArrayIndexOutOfBoundsException
+ */
+ public static int toInt(byte[] b,int off){
+ return ( ( (int) b[off+3]) & 0xFF) +
+ ( ( ( (int) b[off+2]) & 0xFF) << 8) +
+ ( ( ( (int) b[off+1]) & 0xFF) << 16) +
+ ( ( ( (int) b[off+0]) & 0xFF) << 24);
+ }
+
+ /**
+ * Convert eight bytes to a long
+ * @param b - the byte array containing the four bytes
+ * @param off - the offset
+ * @return the long value constructed from the eight bytes
+ * @exception java.lang.ArrayIndexOutOfBoundsException
+ */
+ public static long toLong(byte[] b,int off){
+ return ( ( (long) b[off+7]) & 0xFF) +
+ ( ( ( (long) b[off+6]) & 0xFF) << 8) +
+ ( ( ( (long) b[off+5]) & 0xFF) << 16) +
+ ( ( ( (long) b[off+4]) & 0xFF) << 24) +
+ ( ( ( (long) b[off+3]) & 0xFF) << 32) +
+ ( ( ( (long) b[off+2]) & 0xFF) << 40) +
+ ( ( ( (long) b[off+1]) & 0xFF) << 48) +
+ ( ( ( (long) b[off+0]) & 0xFF) << 56);
+ }
+
+
+ /**
+ * Converts an integer to four bytes
+ * @param n - the integer
+ * @return - four bytes in an array
+ * @deprecated use toBytes(boolean,byte[],int)
+ */
+ public static byte[] toBytes(boolean bool) {
+ byte[] b = new byte[1] ;
+ return toBytes(bool,b,0);
+
+ }
+
+ public static byte[] toBytes(boolean bool, byte[] data, int offset) {
+ data[offset] = (byte)(bool?1:0);
+ return data;
+ }
+
+ /**
+ *
+ * @param org.apache.catalina.tribes.membership
+ * package.
+ *
+ * @author Peter Rossbach
+ * @version $Revision: 303950 $ $Date: 2005-06-09 15:38:30 -0500 (Thu, 09 Jun 2005) $
+ * @author Filip Hanik
+ */
+
+public class Constants {
+
+ public static final String Package = "org.apache.catalina.tribes.membership";
+ public static void main(String[] args) throws Exception {
+ System.out.println(Arrays.toString("TRIBES-B".getBytes()));
+ System.out.println(Arrays.toString("TRIBES-E".getBytes()));
+ }
+}
diff --git a/java/org/apache/catalina/tribes/membership/LocalStrings.properties b/java/org/apache/catalina/tribes/membership/LocalStrings.properties
new file mode 100644
index 000000000..0dafa4672
--- /dev/null
+++ b/java/org/apache/catalina/tribes/membership/LocalStrings.properties
@@ -0,0 +1 @@
+cluster.mbean.register.already=MBean {0} already registered!
diff --git a/java/org/apache/catalina/tribes/membership/McastService.java b/java/org/apache/catalina/tribes/membership/McastService.java
new file mode 100644
index 000000000..4a7aeb962
--- /dev/null
+++ b/java/org/apache/catalina/tribes/membership/McastService.java
@@ -0,0 +1,448 @@
+/*
+ * Copyright 1999,2004-2005 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes.membership;
+
+import java.util.Properties;
+
+import org.apache.catalina.tribes.Member;
+import org.apache.catalina.tribes.MembershipListener;
+import org.apache.catalina.tribes.MembershipService;
+import org.apache.catalina.tribes.util.StringManager;
+import org.apache.catalina.tribes.util.UUIDGenerator;
+import java.io.IOException;
+
+/**
+ * A membership implementation using simple multicast.
+ * This is the representation of a multicast membership service.
+ * This class is responsible for maintaining a list of active cluster nodes in the cluster.
+ * If a node fails to send out a heartbeat, the node will be dismissed.
+ *
+ * @author Filip Hanik
+ * @version $Revision: 378093 $, $Date: 2006-02-15 15:13:45 -0600 (Wed, 15 Feb 2006) $
+ */
+
+
+public class McastService implements MembershipService,MembershipListener {
+
+ private static org.apache.commons.logging.Log log =
+ org.apache.commons.logging.LogFactory.getLog( McastService.class );
+
+ /**
+ * The string manager for this package.
+ */
+ protected StringManager sm = StringManager.getManager(Constants.Package);
+
+ /**
+ * The descriptive information about this implementation.
+ */
+ private static final String info = "McastService/2.1";
+
+ /**
+ * The implementation specific properties
+ */
+ protected Properties properties = new Properties();
+ /**
+ * A handle to the actual low level implementation
+ */
+ protected McastServiceImpl impl;
+ /**
+ * A membership listener delegate (should be the cluster :)
+ */
+ protected MembershipListener listener;
+ /**
+ * The local member
+ */
+ protected MemberImpl localMember ;
+ private int mcastSoTimeout;
+ private int mcastTTL;
+
+ protected byte[] payload;
+
+ protected byte[] domain;
+
+ /**
+ * Create a membership service.
+ */
+ public McastService() {
+ //default values
+ properties.setProperty("mcastPort","45564");
+ properties.setProperty("mcastAddress","228.0.0.4");
+ properties.setProperty("memberDropTime","3000");
+ properties.setProperty("mcastFrequency","500");
+
+ }
+
+ /**
+ * Return descriptive information about this implementation and the
+ * corresponding version number, in the format
+ * <description>/<version>.
+ */
+ public String getInfo() {
+ return (info);
+ }
+
+ /**
+ *
+ * @param properties
+ *
+ //create a channel
+ Channel myChannel = new GroupChannel();
+
+ //create my listeners
+ MyMessageListener msgListener = new MyMessageListener();
+ MyMemberListener mbrListener = new MyMemberListener();
+
+ //attach the listeners to the channel
+ myChannel.addMembershipListener(mbrListener);
+ myChannel.addChannelListener(msgListener);
+
+ //start the channel
+ myChannel.start(Channel.DEFAULT);
+
+ //create a message to be sent, message must implement java.io.Serializable
+ //for performance reasons you probably want them to implement java.io.Externalizable
+ Serializable myMsg = new MyMessage();
+
+ //retrieve my current members
+ Member[] group = myChannel.getMembers();
+
+ //send the message
+ channel.send(group,myMsg,Channel.SEND_OPTIONS_DEFAULT);
+
+
+org.apache.catalina.tribes.Channel
+ Main component to interact with to send messages
+ org.apache.catalina.tribes.MembershipListener
+ Listen to membership changes
+ org.apache.catalina.tribes.ChannelListener
+ Listen to data messages
+ org.apache.catalina.tribes.Member
+ Identifies a node, implementation specific, default is org.apache.catalina.tribes.membership.MemberImpl
+ org.apache.catalina.tribes.Channel
+ Main component to that the application interacts with
+ org.apache.catalina.tribes.ChannelReceiver
+ IO Component to receive messages over some network transport
+ org.apache.catalina.tribes.ChannelSender
+ IO Component to send messages over some network transport
+ org.apache.catalina.tribes.MembershipService
+ IO Component that handles membership discovery and
+ org.apache.catalina.tribes.ChannelInterceptor
+ interceptors between the Channel and the IO layer
+ org.apache.catalina.tribes.ChannelMessage
+ The message that is sent through the interceptor stack down to the IO layer
+ org.apache.catalina.tribes.Member
+ Identifies a node, implementation specific to the underlying IO logic
+ Title:
+ * + *Description:
+ * + *Copyright: Copyright (c) 2005
+ * + *Company:
+ * + * @author not attributable + * @version 1.0 + */ +public abstract class AbstractReplicatedMap extends LinkedHashMap implements RpcCallback, ChannelListener, MembershipListener, Heartbeat { + protected static Log log = LogFactory.getLog(AbstractReplicatedMap.class); + + /** + * The default initial capacity - MUST be a power of two. + */ + public static final int DEFAULT_INITIAL_CAPACITY = 16; + + /** + * The load factor used when none specified in constructor. + **/ + public static final float DEFAULT_LOAD_FACTOR = 0.75f; + + /** + * Used to identify the map + */ + final String chset = "ISO-8859-1"; + +//------------------------------------------------------------------------------ +// INSTANCE VARIABLES +//------------------------------------------------------------------------------ + private transient long rpcTimeout = 5000; + private transient Channel channel; + private transient RpcChannel rpcChannel; + private transient byte[] mapContextName; + private transient boolean stateTransferred = false; + private transient Object stateMutex = new Object(); + private transient HashMap mapMembers = new HashMap(); + private transient int channelSendOptions = Channel.SEND_OPTIONS_DEFAULT; + private transient Object mapOwner; + private transient ClassLoader[] externalLoaders; + protected transient int currentNode = 0; + private transient long accessTimeout = 5000; + private transient String mapname = ""; + + +//------------------------------------------------------------------------------ +// CONSTRUCTORS +//------------------------------------------------------------------------------ + + /** + * Creates a new map + * @param channel The channel to use for communication + * @param timeout long - timeout for RPC messags + * @param mapContextName String - unique name for this map, to allow multiple maps per channel + * @param initialCapacity int - the size of this map, see HashMap + * @param loadFactor float - load factor, see HashMap + * @param cls - a list of classloaders to be used for deserialization of objects. + */ + public AbstractReplicatedMap(Object owner, + Channel channel, + long timeout, + String mapContextName, + int initialCapacity, + float loadFactor, + int channelSendOptions, + ClassLoader[] cls) { + super(initialCapacity, loadFactor); + init(owner, channel, mapContextName, timeout, channelSendOptions, cls); + + } + + protected Member[] wrap(Member m) { + if ( m == null ) return new Member[0]; + else return new Member[] {m}; + } + + private void init(Object owner, Channel channel, String mapContextName, long timeout, int channelSendOptions,ClassLoader[] cls) { + log.info("Initializing AbstractReplicatedMap with context name:"+mapContextName); + this.mapOwner = owner; + this.externalLoaders = cls; + this.channelSendOptions = channelSendOptions; + this.channel = channel; + this.rpcTimeout = timeout; + + try { + this.mapname = mapContextName; + //unique context is more efficient if it is stored as bytes + this.mapContextName = mapContextName.getBytes(chset); + } catch (UnsupportedEncodingException x) { + log.warn("Unable to encode mapContextName[" + mapContextName + "] using getBytes(" + chset +") using default getBytes()", x); + this.mapContextName = mapContextName.getBytes(); + } + if ( log.isTraceEnabled() ) log.trace("Created Lazy Map with name:"+mapContextName+", bytes:"+Arrays.toString(this.mapContextName)); + + //create an rpc channel and add the map as a listener + this.rpcChannel = new RpcChannel(this.mapContextName, channel, this); + this.channel.addChannelListener(this); + this.channel.addMembershipListener(this); + + + try { + broadcast(MapMessage.MSG_INIT, true); + //transfer state from another map + transferState(); + broadcast(MapMessage.MSG_START, true); + } catch (ChannelException x) { + log.warn("Unable to send map start message."); + throw new RuntimeException("Unable to start replicated map.",x); + } + + } + + + private void ping(long timeout) throws ChannelException { + //send out a map membership message, only wait for the first reply + MapMessage msg = new MapMessage(this.mapContextName, MapMessage.MSG_INIT, + false, null, null, null, wrap(channel.getLocalMember(false))); + Response[] resp = rpcChannel.send(channel.getMembers(), msg, rpcChannel.ALL_REPLY, (channelSendOptions), (int)accessTimeout); + for (int i = 0; i < resp.length; i++) { + memberAlive(resp[i].getSource()); + }//for + + synchronized (mapMembers) { + Iterator it = mapMembers.entrySet().iterator(); + long now = System.currentTimeMillis(); + while ( it.hasNext() ) { + Map.Entry entry = (Map.Entry)it.next(); + long access = ((Long)entry.getValue()).longValue(); + if ( (now - access) > timeout ) memberDisappeared((Member)entry.getKey()); + } + }//synch + } + + private void memberAlive(Member member) { + synchronized (mapMembers) { + if (!mapMembers.containsKey(member)) { + mapMemberAdded(member); + } //end if + mapMembers.put(member, new Long(System.currentTimeMillis())); + } + } + + private void broadcast(int msgtype, boolean rpc) throws ChannelException { + //send out a map membership message, only wait for the first reply + MapMessage msg = new MapMessage(this.mapContextName, msgtype, + false, null, null, null, wrap(channel.getLocalMember(false))); + if ( rpc) { + Response[] resp = rpcChannel.send(channel.getMembers(), msg, rpcChannel.FIRST_REPLY, (channelSendOptions),rpcTimeout); + for (int i = 0; i < resp.length; i++) { + mapMemberAdded(resp[i].getSource()); + messageReceived(resp[i].getMessage(), resp[i].getSource()); + } + } else { + channel.send(channel.getMembers(),msg,channelSendOptions); + } + } + + public void breakdown() { + finalize(); + } + + public void finalize() { + try {broadcast(MapMessage.MSG_STOP,false); }catch ( Exception ignore){} + //cleanup + if (this.rpcChannel != null) { + this.rpcChannel.breakdown(); + } + if (this.channel != null) { + this.channel.removeChannelListener(this); + this.channel.removeMembershipListener(this); + } + this.rpcChannel = null; + this.channel = null; + this.mapMembers.clear(); + super.clear(); + this.stateTransferred = false; + this.externalLoaders = null; + } + + public int hashCode() { + return Arrays.hashCode(this.mapContextName); + } + + public boolean equals(Object o) { + if ( o == null ) return false; + if ( !(o instanceof AbstractReplicatedMap)) return false; + if ( !(o.getClass().equals(this.getClass())) ) return false; + AbstractReplicatedMap other = (AbstractReplicatedMap)o; + return Arrays.equals(mapContextName,other.mapContextName); + } + +//------------------------------------------------------------------------------ +// GROUP COM INTERFACES +//------------------------------------------------------------------------------ + public Member[] getMapMembers(HashMap members) { + synchronized (members) { + Member[] result = new Member[members.size()]; + members.keySet().toArray(result); + return result; + } + } + public Member[] getMapMembers() { + return getMapMembers(this.mapMembers); + } + + public Member[] getMapMembersExcl(Member[] exclude) { + synchronized (mapMembers) { + HashMap list = (HashMap)mapMembers.clone(); + for (int i=0; iput, putAll, remove methods.
+ * entrySet, entrySetFull, keySet, keySetFull, returns all non modifiable sets.put() or remove()
+ * the data can be distributed using two different methods:replicate(boolean) and replicate(Object, boolean)ReplicatedMapEntry interface allows you to decide what objects
+ * get replicated and how much data gets replicated each time.replicate(Object,boolean) - replicates only the object that belongs to the keyreplicate(boolean) - Scans the entire map for changes and replicates databoolean value in the replicate method used to decide
+ * whether to only replicate objects that implement the ReplicatedMapEntry interface
+ * or to replicate all objects. If an object doesn't implement the ReplicatedMapEntry interface
+ * each time the object gets replicated the entire object gets serialized, hence a call to replicate(true)
+ * will replicate all objects in this map that are using this node as primary.
+ *
+ * breakdown() or finalize() when you are done with the map to
+ * avoid memory leaks.replicate(Object,boolean) - replicates only the object that belongs to the keyreplicate(boolean) - Scans the entire map for changes and replicates databoolean value in the replicate method used to decide
+ * whether to only replicate objects that implement the ReplicatedMapEntry interface
+ * or to replicate all objects. If an object doesn't implement the ReplicatedMapEntry interface
+ * each time the object gets replicated the entire object gets serialized, hence a call to replicate(true)
+ * will replicate all objects in this map that are using this node as primary.
+ *
+ * breakdown() or finalize() when you are done with the map to
+ * avoid memory leaks.
+ * 1. if ( entry.isDirty() )
+ * try {
+ * 2. entry.lock();
+ * 3. byte[] diff = entry.getDiff();
+ * 4. entry.reset();
+ * } finally {
+ * 5. entry.unlock();
+ * }
+ * }
+ *
+ *
+ * 1. ReplicatedMapEntry entry = (ReplicatedMapEntry)objectIn.readObject();
+ * 2. if ( isBackup(entry)||isPrimary(entry) ) entry.setOwner(owner);
+ *
+ *
+ * byte[] data = new byte[1024];
+ * Streamable st = ....;
+ * while ( !st.eof() ) {
+ * int length = st.read(data,0,data.length);
+ * String s = new String(data,0,length);
+ * System.out.println(s);
+ * }
+ *
+ * @author Filip Hanik
+ * @version 1.0
+ */
+public interface Streamable {
+
+ /**
+ * returns true if the stream has reached its end
+ * @return boolean
+ */
+ public boolean eof();
+
+ /**
+ * write data into the byte array starting at offset, maximum bytes read are (data.length-offset)
+ * @param data byte[] - the array to read data into
+ * @param offset int - start position for writing data
+ * @return int - the number of bytes written into the data buffer
+ */
+ public int write(byte[] data, int offset, int length) throws IOException;
+
+ /**
+ * read data into the byte array starting at offset
+ * @param data byte[] - the array to read data into
+ * @param offset int - start position for writing data
+ * @param length - the desired read length
+ * @return int - the number of bytes read from the data buffer
+ */
+ public int read(byte[] data, int offset, int length) throws IOException;
+
+
+}
\ No newline at end of file
diff --git a/java/org/apache/catalina/tribes/transport/AbstractSender.java b/java/org/apache/catalina/tribes/transport/AbstractSender.java
new file mode 100644
index 000000000..5ffb7caf1
--- /dev/null
+++ b/java/org/apache/catalina/tribes/transport/AbstractSender.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 1999,2006 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes.transport;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import org.apache.catalina.tribes.Member;
+
+/**
+ * Title:
+ * + *Description:
+ * + *Copyright: Copyright (c) 2005
+ * + *Company:
+ * + * @author not attributable + * @version 1.0 + */ +public abstract class AbstractSender implements DataSender { + + private boolean connected = false; + private int rxBufSize = 25188; + private int txBufSize = 43800; + private boolean directBuffer = false; + private int keepAliveCount = -1; + private int requestCount = 0; + private long connectTime; + private long keepAliveTime = -1; + private long timeout = 3000; + private Member destination; + private InetAddress address; + private int port; + private int maxRetryAttempts = 1;//1 resends + private int attempt; + private boolean tcpNoDelay = true; + private boolean soKeepAlive = false; + private boolean ooBInline = true; + private boolean soReuseAddress = true; + private boolean soLingerOn = false; + private int soLingerTime = 3; + private int soTrafficClass = 0x04 | 0x08 | 0x010; + private boolean throwOnFailedAck = false; + + /** + * transfers sender properties from one sender to another + * @param from AbstractSender + * @param to AbstractSender + */ + public static void transferProperties(AbstractSender from, AbstractSender to) { + to.rxBufSize = from.rxBufSize; + to.txBufSize = from.txBufSize; + to.directBuffer = from.directBuffer; + to.keepAliveCount = from.keepAliveCount; + to.keepAliveTime = from.keepAliveTime; + to.timeout = from.timeout; + to.destination = from.destination; + to.address = from.address; + to.port = from.port; + to.maxRetryAttempts = from.maxRetryAttempts; + to.tcpNoDelay = from.tcpNoDelay; + to.soKeepAlive = from.soKeepAlive; + to.ooBInline = from.ooBInline; + to.soReuseAddress = from.soReuseAddress; + to.soLingerOn = from.soLingerOn; + to.soLingerTime = from.soLingerTime; + to.soTrafficClass = from.soTrafficClass; + to.throwOnFailedAck = from.throwOnFailedAck; + } + + + public AbstractSender() { + + } + + /** + * connect + * + * @throws IOException + * @todo Implement this org.apache.catalina.tribes.transport.DataSender method + */ + public abstract void connect() throws IOException; + + /** + * disconnect + * + * @todo Implement this org.apache.catalina.tribes.transport.DataSender method + */ + public abstract void disconnect(); + + /** + * keepalive + * + * @return boolean + * @todo Implement this org.apache.catalina.tribes.transport.DataSender method + */ + public boolean keepalive() { + boolean disconnect = false; + if ( keepAliveCount >= 0 && requestCount>keepAliveCount ) disconnect = true; + else if ( keepAliveTime >= 0 && keepAliveTime> (System.currentTimeMillis()-connectTime) ) disconnect = true; + if ( disconnect ) disconnect(); + return disconnect; + } + + protected void setConnected(boolean connected){ + this.connected = connected; + } + + public boolean isConnected() { + return connected; + } + + public long getConnectTime() { + return connectTime; + } + + public Member getDestination() { + return destination; + } + + + public int getKeepAliveCount() { + return keepAliveCount; + } + + public long getKeepAliveTime() { + return keepAliveTime; + } + + public int getRequestCount() { + return requestCount; + } + + public int getRxBufSize() { + return rxBufSize; + } + + public long getTimeout() { + return timeout; + } + + public int getTxBufSize() { + return txBufSize; + } + + public InetAddress getAddress() { + return address; + } + + public int getPort() { + return port; + } + + public int getMaxRetryAttempts() { + return maxRetryAttempts; + } + + public void setDirectBuffer(boolean directBuffer) { + this.directBuffer = directBuffer; + } + + public boolean getDirectBuffer() { + return this.directBuffer; + } + + public int getAttempt() { + return attempt; + } + + public boolean getTcpNoDelay() { + return tcpNoDelay; + } + + public boolean getSoKeepAlive() { + return soKeepAlive; + } + + public boolean getOoBInline() { + return ooBInline; + } + + public boolean getSoReuseAddress() { + return soReuseAddress; + } + + public boolean getSoLingerOn() { + return soLingerOn; + } + + public int getSoLingerTime() { + return soLingerTime; + } + + public int getSoTrafficClass() { + return soTrafficClass; + } + + public boolean getThrowOnFailedAck() { + return throwOnFailedAck; + } + + public void setKeepAliveCount(int keepAliveCount) { + this.keepAliveCount = keepAliveCount; + } + + public void setKeepAliveTime(long keepAliveTime) { + this.keepAliveTime = keepAliveTime; + } + + public void setRequestCount(int requestCount) { + this.requestCount = requestCount; + } + + public void setRxBufSize(int rxBufSize) { + this.rxBufSize = rxBufSize; + } + + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + public void setTxBufSize(int txBufSize) { + this.txBufSize = txBufSize; + } + + public void setConnectTime(long connectTime) { + this.connectTime = connectTime; + } + + public void setMaxRetryAttempts(int maxRetryAttempts) { + this.maxRetryAttempts = maxRetryAttempts; + } + + public void setAttempt(int attempt) { + this.attempt = attempt; + } + + public void setTcpNoDelay(boolean tcpNoDelay) { + this.tcpNoDelay = tcpNoDelay; + } + + public void setSoKeepAlive(boolean soKeepAlive) { + this.soKeepAlive = soKeepAlive; + } + + public void setOoBInline(boolean ooBInline) { + this.ooBInline = ooBInline; + } + + public void setSoReuseAddress(boolean soReuseAddress) { + this.soReuseAddress = soReuseAddress; + } + + public void setSoLingerOn(boolean soLingerOn) { + this.soLingerOn = soLingerOn; + } + + public void setSoLingerTime(int soLingerTime) { + this.soLingerTime = soLingerTime; + } + + public void setSoTrafficClass(int soTrafficClass) { + this.soTrafficClass = soTrafficClass; + } + + public void setThrowOnFailedAck(boolean throwOnFailedAck) { + this.throwOnFailedAck = throwOnFailedAck; + } + + public void setDestination(Member destination) throws UnknownHostException { + this.destination = destination; + this.address = InetAddress.getByAddress(destination.getHost()); + this.port = destination.getPort(); + + } + + public void setPort(int port) { + this.port = port; + } + + public void setAddress(InetAddress address) { + this.address = address; + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/transport/Constants.java b/java/org/apache/catalina/tribes/transport/Constants.java new file mode 100644 index 000000000..c738adb3c --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/Constants.java @@ -0,0 +1,42 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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.catalina.tribes.transport; + +import org.apache.catalina.tribes.io.XByteBuffer; + +/** + * Manifest constants for theorg.apache.catalina.tribes.transport
+ * package.
+ * @author Filip Hanik
+ * @author Peter Rossbach
+ * @version $Revision: 303753 $ $Date: 2005-03-14 15:24:30 -0600 (Mon, 14 Mar 2005) $
+ */
+
+public class Constants {
+
+ public static final String Package = "org.apache.catalina.tribes.transport";
+
+ /*
+ * Do not change any of these values!
+ */
+ public static final byte[] ACK_DATA = new byte[] {6, 2, 3};
+ public static final byte[] FAIL_ACK_DATA = new byte[] {11, 0, 5};
+ public static final byte[] ACK_COMMAND = XByteBuffer.createDataPackage(ACK_DATA);
+ public static final byte[] FAIL_ACK_COMMAND = XByteBuffer.createDataPackage(FAIL_ACK_DATA);
+
+}
diff --git a/java/org/apache/catalina/tribes/transport/DataSender.java b/java/org/apache/catalina/tribes/transport/DataSender.java
new file mode 100644
index 000000000..51cd3fc90
--- /dev/null
+++ b/java/org/apache/catalina/tribes/transport/DataSender.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 1999,2006 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes.transport;
+
+import java.io.IOException;
+
+/**
+ * Title:
+ * + *Description:
+ * + *Copyright: Copyright (c) 2005
+ * + *Company:
+ * + * @author not attributable + * @version 1.0 + */ +public interface DataSender { + public void connect() throws IOException; + public void disconnect(); + public boolean isConnected(); + public void setRxBufSize(int size); + public void setTxBufSize(int size); + public boolean keepalive(); + public void setTimeout(long timeout); + public void setKeepAliveCount(int maxRequests); + public void setKeepAliveTime(long keepAliveTimeInMs); + public int getRequestCount(); + public long getConnectTime(); + + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/transport/LocalStrings.properties b/java/org/apache/catalina/tribes/transport/LocalStrings.properties new file mode 100644 index 000000000..ad54ca0dd --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/LocalStrings.properties @@ -0,0 +1,69 @@ +AsyncSocketSender.create.thread=Create sender [{0}:{1,number,integer}] queue thread to tcp background replication +AsyncSocketSender.queue.message=Queue message to [{0}:{1,number,integer}] id=[{2}] size={3} +AsyncSocketSender.send.error=Unable to asynchronously send session with id=[{0}] - message will be ignored. +AsyncSocketSender.queue.empty=Queue in sender [{0}:{1,number,integer}] returned null element! +cluster.mbean.register.already=MBean {0} already registered! +IDataSender.ack.eof=EOF reached at local port [{0}:{1,number,integer}] +IDataSender.ack.receive=Got ACK at local port [{0}:{1,number,integer}] +IDataSender.ack.missing=Unable to read acknowledgement from [{0}:{1,number,integer}] in {2,number,integer} ms. Disconnecting socket, and trying again. +IDataSender.ack.read=Read wait ack char '{2}' [{0}:{1,number,integer}] +IDataSender.ack.start=Waiting for ACK message [{0}:{1,number,integer}] +IDataSender.ack.wrong=Missing correct ACK after 10 bytes read at local port [{0}:{1,number,integer}] +IDataSender.closeSocket=Sender close socket to [{0}:{1,number,integer}] (close count {2,number,integer}) +IDataSender.connect=Sender connect to [{0}:{1,number,integer}] (connect count {2,number,integer}) +IDataSender.create=Create sender [{0}:{1,number,integer}] +IDataSender.disconnect=Sender disconnect from [{0}:{1,number,integer}] (disconnect count {2,number,integer}) +IDataSender.message.disconnect=Message transfered: Sender can't disconnect from [{0}:{1,number,integer}] +IDataSender.message.create=Message transfered: Sender can't create current socket [{0}:{1,number,integer}] +IDataSender.openSocket=Sender open socket to [{0}:{1,number,integer}] (open count {2,number,integer}) +IDataSender.openSocket.failure=Open sender socket [{0}:{1,number,integer}] failure! (open failure count {2,number,integer}) +IDataSender.send.again=Send data again to [{0}:{1,number,integer}] +IDataSender.send.crash=Send message crashed [{0}:{1,number,integer}] type=[{2}], id=[{3}] +IDataSender.send.message=Send message to [{0}:{1,number,integer}] id=[{2}] size={3,number,integer} +IDataSender.send.lost=Message lost: [{0}:{1,number,integer}] type=[{2}], id=[{3}] +IDataSender.senderModes.Configured=Configured a data replication sender for mode {0} +IDataSender.senderModes.Instantiate=Can't instantiate a data replication sender of class {0} +IDataSender.senderModes.Missing=Can't configure a data replication sender for mode {0} +IDataSender.senderModes.Resources=Can't load data replication sender mapping list +IDataSender.stats=Send stats from [{0}:{1,number,integer}], Nr of bytes sent={2,number,integer} over {3} = {4,number,integer} bytes/request, processing time {5,number,integer} msec, avg processing time {6,number,integer} msec +PoolSocketSender.senderQueue.sender.failed=PoolSocketSender create new sender to [{0}:{1,number,integer}] failed +PoolSocketSender.noMoreSender=No socket sender available for client [{0}:{1,number,integer}] did it disappeared? +ReplicationTransmitter.getProperty=get property {0} +ReplicationTransmitter.setProperty=set property {0}: {1} old value {2} +ReplicationTransmitter.started=Start ClusterSender at cluster {0} with name {1} +ReplicationTransmitter.stopped=Stopped ClusterSender at cluster {0} with name {1} +ReplicationValve.crossContext.add=add Cross Context session replication container to replicationValve threadlocal +ReplicationValve.crossContext.registerSession=register Cross context session id={0} from context {1} +ReplicationValve.crossContext.remove=remove Cross Context session replication container from replicationValve threadlocal +ReplicationValve.crossContext.sendDelta=send Cross Context session delta from context {0}. +ReplicationValve.filter.loading=Loading request filters={0} +ReplicationValve.filter.token=Request filter={0} +ReplicationValve.filter.token.failure=Unable to compile filter={0} +ReplicationValve.invoke.uri=Invoking replication request on {0} +ReplicationValve.nocluster=No cluster configured for this request. +ReplicationValve.resetDeltaRequest=Cluster is standalone: reset Session Request Delta at context {0} +ReplicationValve.send.failure=Unable to perform replication request. +ReplicationValve.send.invalid.failure=Unable to send session [id={0}] invalid message over cluster. +ReplicationValve.session.found=Context {0}: Found session {1} but it isn't a ClusterSession. +ReplicationValve.session.indicator=Context {0}: Primarity of session {0} in request attribute {1} is {2}. +ReplicationValve.session.invalid=Context {0}: Requested session {1} is invalid, removed or not replicated at this node. +ReplicationValve.stats=Average request time= {0} ms for Cluster overhead time={1} ms for {2} requests {3} filter requests {4} send requests {5} cross context requests (Request={6} ms Cluster={7} ms). +SimpleTcpCluster.event.log=Cluster receive listener event {0} with data {1} +SimpleTcpCluster.getProperty=get property {0} +SimpleTcpCluster.setProperty=set property {0}: {1} old value {2} +SimpleTcpCluster.default.addClusterListener=Add Default ClusterListener at cluster {0} +SimpleTcpCluster.default.addClusterValves=Add Default ClusterValves at cluster {0} +SimpleTcpCluster.default.addClusterReceiver=Add Default ClusterReceiver at cluster {0} +SimpleTcpCluster.default.addClusterSender=Add Default ClusterSender at cluster {0} +SimpleTcpCluster.default.addMembershipService=Add Default Membership Service at cluster {0} +SimpleTcpCluster.log.receive=RECEIVE {0,date}:{0,time} {1,number} {2}:{3,number,integer} {4} {5} +SimpleTcpCluster.log.send=SEND {0,date}:{0,time} {1,number} {2}:{3,number,integer} {4} +SimpleTcpCluster.log.send.all=SEND {0,date}:{0,time} {1,number} - {2} +SocketReplictionListener.allreadyExists=ServerSocket [{0}:{1}] allready started! +SocketReplictionListener.accept.failure=ServerSocket [{0}:{1}] - Exception to start thread or accept server socket +SocketReplictionListener.open=Open Socket at [{0}:{1}] +SocketReplictionListener.openclose.failure=ServerSocket [{0}:{1}] - Exception to open or close server socket +SocketReplictionListener.portbusy=Port busy at [{0}:{i}] - reason [{2}] +SocketReplictionListener.serverSocket.notExists=Fatal error: Receiver socket not bound address={0} port={1} maxport={2} +SocketReplictionListener.timeout=Receiver ServerSocket no started [{0}:{1}] - reason: timeout={2} or listen={3} +SocketReplictionListener.unlockSocket.failure=UnLocksocket failure at ServerSocket [{0:{1}] diff --git a/java/org/apache/catalina/tribes/transport/MultiPointSender.java b/java/org/apache/catalina/tribes/transport/MultiPointSender.java new file mode 100644 index 000000000..51722da4f --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/MultiPointSender.java @@ -0,0 +1,37 @@ +/* + * Copyright 1999,2006 The Apache Software Foundation. + * + * Licensed 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.catalina.tribes.transport; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.Member; + +/** + * @author Filip Hanik + * @version $Revision: 303993 $ $Date: 2005-07-16 16:05:54 -0500 (Sat, 16 Jul 2005) $ + * @since 5.5.16 + */ + +public interface MultiPointSender extends DataSender +{ + public void sendMessage(Member[] destination, ChannelMessage data) throws ChannelException; + public void setRxBufSize(int size); + public void setTxBufSize(int size); + public void setMaxRetryAttempts(int attempts); + public void setDirectBuffer(boolean directBuf); + public void memberAdded(Member member); + public void memberDisappeared(Member member); +} diff --git a/java/org/apache/catalina/tribes/transport/PooledSender.java b/java/org/apache/catalina/tribes/transport/PooledSender.java new file mode 100644 index 000000000..221a92368 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/PooledSender.java @@ -0,0 +1,197 @@ +/* + * Copyright 1999,2004-2005 The Apache Software Foundation. + * + * Licensed 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.catalina.tribes.transport; + +import java.io.IOException; +import java.util.List; + +/** + *Title:
+ * + *Description:
+ * + *Copyright: Copyright (c) 2005
+ * + *Company:
+ * + * @author not attributable + * @version 1.0 + */ +public abstract class PooledSender extends AbstractSender implements MultiPointSender { + + private SenderQueue queue = null; + private int poolSize = 25; + public PooledSender() { + queue = new SenderQueue(this,poolSize); + } + + public abstract DataSender getNewDataSender(); + + public DataSender getSender() { + return queue.getSender(getTimeout()); + } + + public void returnSender(DataSender sender) { + sender.keepalive(); + queue.returnSender(sender); + } + + public synchronized void connect() throws IOException { + //do nothing, happens in the socket sender itself + queue.open(); + setConnected(true); + } + + public synchronized void disconnect() { + queue.close(); + setConnected(false); + } + + + public int getInPoolSize() { + return queue.getInPoolSize(); + } + + public int getInUsePoolSize() { + return queue.getInUsePoolSize(); + } + + + public void setPoolSize(int poolSize) { + this.poolSize = poolSize; + queue.setLimit(poolSize); + } + + public int getPoolSize() { + return poolSize; + } + + public boolean keepalive() { + //do nothing, the pool checks on every return + return false; + } + + + + // ----------------------------------------------------- Inner Class + + private class SenderQueue { + private int limit = 25; + + PooledSender parent = null; + + private List notinuse = null; + + private List inuse = null; + + private boolean isOpen = true; + + public SenderQueue(PooledSender parent, int limit) { + this.limit = limit; + this.parent = parent; + notinuse = new java.util.LinkedList(); + inuse = new java.util.LinkedList(); + } + + /** + * @return Returns the limit. + */ + public int getLimit() { + return limit; + } + /** + * @param limit The limit to set. + */ + public void setLimit(int limit) { + this.limit = limit; + } + /** + * @return + */ + public int getInUsePoolSize() { + return inuse.size(); + } + + /** + * @return + */ + public int getInPoolSize() { + return notinuse.size(); + } + + public synchronized DataSender getSender(long timeout) { + long start = System.currentTimeMillis(); + while ( true ) { + if (!isOpen)throw new IllegalStateException("Queue is closed"); + DataSender sender = null; + if (notinuse.size() == 0 && inuse.size() < limit) { + sender = parent.getNewDataSender(); + } else if (notinuse.size() > 0) { + sender = (DataSender) notinuse.remove(0); + } + if (sender != null) { + inuse.add(sender); + return sender; + }//end if + long delta = System.currentTimeMillis() - start; + if ( delta > timeout && timeout>0) return null; + else { + try { + wait(Math.max(timeout - delta,1)); + }catch (InterruptedException x){} + }//end if + } + } + + public synchronized void returnSender(DataSender sender) { + if ( !isOpen) { + sender.disconnect(); + return; + } + //to do + inuse.remove(sender); + //just in case the limit has changed + if ( notinuse.size() < this.getLimit() ) notinuse.add(sender); + else try {sender.disconnect(); } catch ( Exception ignore){} + notify(); + } + + public synchronized void close() { + isOpen = false; + Object[] unused = notinuse.toArray(); + Object[] used = inuse.toArray(); + for (int i = 0; i < unused.length; i++) { + DataSender sender = (DataSender) unused[i]; + sender.disconnect(); + }//for + for (int i = 0; i < used.length; i++) { + DataSender sender = (DataSender) used[i]; + sender.disconnect(); + }//for + notinuse.clear(); + inuse.clear(); + notify(); + + + + } + + public synchronized void open() { + isOpen = true; + notify(); + } + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/transport/ReceiverBase.java b/java/org/apache/catalina/tribes/transport/ReceiverBase.java new file mode 100644 index 000000000..f09b956e9 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/ReceiverBase.java @@ -0,0 +1,383 @@ +/* + * Copyright 1999,2006 The Apache Software Foundation. + * + * Licensed 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.catalina.tribes.transport; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; + +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.ChannelReceiver; +import org.apache.catalina.tribes.MessageListener; +import org.apache.catalina.tribes.io.ListenCallback; +import org.apache.commons.logging.Log; + +/** + *Title:
+ * + *Description:
+ * + *Copyright: Copyright (c) 2005
+ * + *Company:
+ * + * @author not attributable + * @version 1.0 + */ +public abstract class ReceiverBase implements ChannelReceiver, ListenCallback, ThreadPool.ThreadCreator { + + public static final int OPTION_DIRECT_BUFFER = 0x0004; + + + protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ReceiverBase.class); + + private MessageListener listener; + private String host = "auto"; + private InetAddress bind; + private int port = 4000; + private int rxBufSize = 43800; + private int txBufSize = 25188; + private boolean listen = false; + private ThreadPool pool; + private boolean direct = true; + private long tcpSelectorTimeout = 5000; + //how many times to search for an available socket + private int autoBind = 10; + private int maxThreads = 15; + private int minThreads = 6; + private boolean tcpNoDelay = true; + private boolean soKeepAlive = false; + private boolean ooBInline = true; + private boolean soReuseAddress = true; + private boolean soLingerOn = true; + private int soLingerTime = 3; + private int soTrafficClass = 0x04 | 0x08 | 0x010; + private int timeout = 3000; //3 seconds + private boolean useBufferPool = true; + + + public ReceiverBase() { + } + + /** + * getMessageListener + * + * @return MessageListener + * @todo Implement this org.apache.catalina.tribes.ChannelReceiver method + */ + public MessageListener getMessageListener() { + return listener; + } + + /** + * + * @return The port + * @todo Implement this org.apache.catalina.tribes.ChannelReceiver method + */ + public int getPort() { + return port; + } + + public int getRxBufSize() { + return rxBufSize; + } + + public int getTxBufSize() { + return txBufSize; + } + + /** + * @deprecated use getMinThreads()/getMaxThreads() + * @return int + */ + public int getTcpThreadCount() { + return getMinThreads(); + } + + /** + * setMessageListener + * + * @param listener MessageListener + * @todo Implement this org.apache.catalina.tribes.ChannelReceiver method + */ + public void setMessageListener(MessageListener listener) { + this.listener = listener; + } + + public void setTcpListenPort(int tcpListenPort) { + this.port = tcpListenPort; + } + + public void setTcpListenAddress(String tcpListenHost) { + this.host = tcpListenHost; + } + + public void setRxBufSize(int rxBufSize) { + this.rxBufSize = rxBufSize; + } + + public void setTxBufSize(int txBufSize) { + this.txBufSize = txBufSize; + } + + public void setTcpThreadCount(int tcpThreadCount) { + setMinThreads(tcpThreadCount); + } + + /** + * @return Returns the bind. + */ + public InetAddress getBind() { + if (bind == null) { + try { + if ("auto".equals(host)) { + host = java.net.InetAddress.getLocalHost().getHostAddress(); + } + if (log.isDebugEnabled()) + log.debug("Starting replication listener on address:"+ host); + bind = java.net.InetAddress.getByName(host); + } catch (IOException ioe) { + log.error("Failed bind replication listener on address:"+ host, ioe); + } + } + return bind; + } + + /** + * recursive bind to find the next available port + * @param socket ServerSocket + * @param portstart int + * @param retries int + * @return int + * @throws IOException + */ + protected int bind(ServerSocket socket, int portstart, int retries) throws IOException { + InetSocketAddress addr = null; + while ( retries > 0 ) { + try { + addr = new InetSocketAddress(getBind(), portstart); + socket.bind(addr); + setTcpListenPort(portstart); + log.info("Receiver Server Socket bound to:"+addr); + return 0; + }catch ( IOException x) { + retries--; + if ( retries <= 0 ) { + log.info("Unable to bind server socket to:"+addr+" throwing error."); + throw x; + } + portstart++; + try {Thread.sleep(25);}catch( InterruptedException ti){Thread.currentThread().interrupted();} + retries = bind(socket,portstart,retries); + } + } + return retries; + } + + public void messageDataReceived(ChannelMessage data) { + if ( this.listener != null ) { + if ( listener.accept(data) ) listener.messageReceived(data); + } + } + + public int getWorkerThreadOptions() { + int options = 0; + if ( getDirect() ) options = options | OPTION_DIRECT_BUFFER; + return options; + } + + + /** + * @param bind The bind to set. + */ + public void setBind(java.net.InetAddress bind) { + this.bind = bind; + } + + + public int getTcpListenPort() { + return this.port; + } + + public boolean getDirect() { + return direct; + } + + + + public void setDirect(boolean direct) { + this.direct = direct; + } + + + + public String getHost() { + getBind(); + return this.host; + } + + public long getTcpSelectorTimeout() { + return tcpSelectorTimeout; + } + + public boolean doListen() { + return listen; + } + + public MessageListener getListener() { + return listener; + } + + public ThreadPool getPool() { + return pool; + } + + public String getTcpListenAddress() { + return getHost(); + } + + public int getAutoBind() { + return autoBind; + } + + public int getMaxThreads() { + return maxThreads; + } + + public int getMinThreads() { + return minThreads; + } + + public boolean getTcpNoDelay() { + return tcpNoDelay; + } + + public boolean getSoKeepAlive() { + return soKeepAlive; + } + + public boolean getOoBInline() { + return ooBInline; + } + + + public boolean getSoLingerOn() { + return soLingerOn; + } + + public int getSoLingerTime() { + return soLingerTime; + } + + public boolean getSoReuseAddress() { + return soReuseAddress; + } + + public int getSoTrafficClass() { + return soTrafficClass; + } + + public int getTimeout() { + return timeout; + } + + public boolean getUseBufferPool() { + return useBufferPool; + } + + public void setTcpSelectorTimeout(long selTimeout) { + tcpSelectorTimeout = selTimeout; + } + + public void setListen(boolean doListen) { + this.listen = doListen; + } + + public void setHost(String host) { + this.host = host; + } + + public void setListener(MessageListener listener) { + this.listener = listener; + } + + public void setLog(Log log) { + this.log = log; + } + + public void setPool(ThreadPool pool) { + this.pool = pool; + } + + public void setPort(int port) { + this.port = port; + } + + public void setAutoBind(int autoBind) { + this.autoBind = autoBind; + if ( this.autoBind <= 0 ) this.autoBind = 1; + } + + public void setMaxThreads(int maxThreads) { + this.maxThreads = maxThreads; + } + + public void setMinThreads(int minThreads) { + this.minThreads = minThreads; + } + + public void setTcpNoDelay(boolean tcpNoDelay) { + this.tcpNoDelay = tcpNoDelay; + } + + public void setSoKeepAlive(boolean soKeepAlive) { + this.soKeepAlive = soKeepAlive; + } + + public void setOoBInline(boolean ooBInline) { + this.ooBInline = ooBInline; + } + + + public void setSoLingerOn(boolean soLingerOn) { + this.soLingerOn = soLingerOn; + } + + public void setSoLingerTime(int soLingerTime) { + this.soLingerTime = soLingerTime; + } + + public void setSoReuseAddress(boolean soReuseAddress) { + this.soReuseAddress = soReuseAddress; + } + + public void setSoTrafficClass(int soTrafficClass) { + this.soTrafficClass = soTrafficClass; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public void setUseBufferPool(boolean useBufferPool) { + this.useBufferPool = useBufferPool; + } + + public void heartbeat() { + //empty operation + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java b/java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java new file mode 100644 index 000000000..6b3b18a46 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/ReplicationTransmitter.java @@ -0,0 +1,134 @@ +/* + * Copyright 1999,2004-2005 The Apache Software Foundation. + * + * Licensed 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.catalina.tribes.transport; + +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.ChannelSender; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.util.StringManager; +import org.apache.catalina.tribes.transport.nio.PooledParallelSender; + +/** + * Transmit message to other cluster members + * Actual senders are created based on the replicationMode + * type + * + * @author Filip Hanik + * @version $Revision: 379956 $ $Date: 2006-02-22 16:57:35 -0600 (Wed, 22 Feb 2006) $ + */ +public class ReplicationTransmitter implements ChannelSender { + private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ReplicationTransmitter.class); + + /** + * The descriptive information about this implementation. + */ + private static final String info = "ReplicationTransmitter/3.0"; + + /** + * The string manager for this package. + */ + protected StringManager sm = StringManager.getManager(Constants.Package); + + + + public ReplicationTransmitter() { + } + + private MultiPointSender transport = new PooledParallelSender(); + + /** + * Return descriptive information about this implementation and the + * corresponding version number, in the format + *<description>/<version>.
+ */
+ public String getInfo() {
+ return (info);
+ }
+
+ public MultiPointSender getTransport() {
+ return transport;
+ }
+
+ public void setTransport(MultiPointSender transport) {
+ this.transport = transport;
+ }
+
+ // ------------------------------------------------------------- public
+
+ /**
+ * Send data to one member
+ * @see org.apache.catalina.tribes.ClusterSender#sendMessage(org.apache.catalina.tribes.ClusterMessage, org.apache.catalina.tribes.Member)
+ */
+ public void sendMessage(ChannelMessage message, Member[] destination) throws ChannelException {
+ MultiPointSender sender = getTransport();
+ sender.sendMessage(destination,message);
+ }
+
+
+ /**
+ * start the sender and register transmitter mbean
+ *
+ * @see org.apache.catalina.tribes.ClusterSender#start()
+ */
+ public void start() throws java.io.IOException {
+ getTransport().connect();
+ }
+
+ /*
+ * stop the sender and deregister mbeans (transmitter, senders)
+ *
+ * @see org.apache.catalina.tribes.ClusterSender#stop()
+ */
+ public synchronized void stop() {
+ getTransport().disconnect();
+ }
+
+ /**
+ * Call transmitter to check for sender socket status
+ *
+ * @see SimpleTcpCluster#backgroundProcess()
+ */
+
+ public void heartbeat() {
+
+ }
+
+ /**
+ * add new cluster member and create sender ( s. replicationMode) transfer
+ * current properties to sender
+ *
+ * @see org.apache.catalina.tribes.ClusterSender#add(org.apache.catalina.tribes.Member)
+ */
+ public synchronized void add(Member member) {
+ getTransport().memberAdded(member);
+ }
+
+ /**
+ * remove sender from transmitter. ( deregister mbean and disconnect sender )
+ *
+ * @see org.apache.catalina.tribes.ClusterSender#remove(org.apache.catalina.tribes.Member)
+ */
+ public synchronized void remove(Member member) {
+ getTransport().memberDisappeared(member);
+ }
+
+ // ------------------------------------------------------------- protected
+
+
+
+}
diff --git a/java/org/apache/catalina/tribes/transport/SenderState.java b/java/org/apache/catalina/tribes/transport/SenderState.java
new file mode 100644
index 000000000..be7152305
--- /dev/null
+++ b/java/org/apache/catalina/tribes/transport/SenderState.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes.transport;
+
+import org.apache.catalina.tribes.Member;
+import java.util.HashMap;
+
+
+/**
+ *
+ * @author Filip Hanik
+ * @version 1.0
+ * @since 5.5.16
+ */
+
+public class SenderState {
+
+ public static final int READY = 0;
+ public static final int SUSPECT = 1;
+ public static final int FAILING = 2;
+ /**
+ * The descriptive information about this implementation.
+ */
+ private static final String info = "SenderState/1.0";
+
+
+ protected static HashMap memberStates = new HashMap();
+
+ public static SenderState getSenderState(Member member) {
+ return getSenderState(member,true);
+ }
+
+ public static SenderState getSenderState(Member member, boolean create) {
+ SenderState state = (SenderState)memberStates.get(member);
+ if ( state == null && create) {
+ synchronized ( memberStates ) {
+ state = (SenderState)memberStates.get(member);
+ if ( state == null ) {
+ state = new SenderState();
+ memberStates.put(member,state);
+ }
+ }
+ }
+ return state;
+ }
+
+ public static void removeSenderState(Member member) {
+ synchronized ( memberStates ) {
+ memberStates.remove(member);
+ }
+ }
+
+
+ // ----------------------------------------------------- Instance Variables
+
+ private int state = READY;
+
+ // ----------------------------------------------------- Constructor
+
+
+ private SenderState() {
+ this(READY);
+ }
+
+ private SenderState(int state) {
+ this.state = state;
+ }
+
+ /**
+ *
+ * @return boolean
+ */
+ public boolean isSuspect() {
+ return (state == SUSPECT) || (state == FAILING);
+ }
+
+ public void setSuspect() {
+ state = SUSPECT;
+ }
+
+ public boolean isReady() {
+ return state == READY;
+ }
+
+ public void setReady() {
+ state = READY;
+ }
+
+ public boolean isFailing() {
+ return state == FAILING;
+ }
+
+ public void setFailing() {
+ state = FAILING;
+ }
+
+
+ // ----------------------------------------------------- Public Properties
+
+}
diff --git a/java/org/apache/catalina/tribes/transport/ThreadPool.java b/java/org/apache/catalina/tribes/transport/ThreadPool.java
new file mode 100644
index 000000000..32f3dd336
--- /dev/null
+++ b/java/org/apache/catalina/tribes/transport/ThreadPool.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes.transport;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author not attributable
+ * @version 1.0
+ */
+
+public class ThreadPool
+{
+ /**
+ * A very simple thread pool class. The pool size is set at
+ * construction time and remains fixed. Threads are cycled
+ * through a FIFO idle queue.
+ */
+
+ List idle = new LinkedList();
+ List used = new LinkedList();
+
+ Object mutex = new Object();
+ boolean running = true;
+
+ private static int counter = 1;
+ private int maxThreads;
+ private int minThreads;
+
+ private ThreadCreator creator = null;
+
+ private static synchronized int inc() {
+ return counter++;
+ }
+
+
+ public ThreadPool (int maxThreads, int minThreads, ThreadCreator creator) throws Exception {
+ // fill up the pool with worker threads
+ this.maxThreads = maxThreads;
+ this.minThreads = minThreads;
+ this.creator = creator;
+ //for (int i = 0; i < minThreads; i++) {
+ for (int i = 0; i < maxThreads; i++) { //temporary fix for thread hand off problem
+ WorkerThread thread = creator.getWorkerThread();
+ setupThread(thread);
+ idle.add (thread);
+ }
+ }
+
+ protected void setupThread(WorkerThread thread) {
+ synchronized (thread) {
+ thread.setPool(this);
+ thread.setName(thread.getClass().getName() + "[" + inc() + "]");
+ thread.setDaemon(true);
+ thread.setPriority(Thread.MAX_PRIORITY);
+ thread.start();
+ try {thread.wait(500); }catch ( InterruptedException x ) {}
+ }
+ }
+
+ /**
+ * Find an idle worker thread, if any. Could return null.
+ */
+ public WorkerThread getWorker()
+ {
+ WorkerThread worker = null;
+ synchronized (mutex) {
+ while ( worker == null && running ) {
+ if (idle.size() > 0) {
+ try {
+ worker = (WorkerThread) idle.remove(0);
+ } catch (java.util.NoSuchElementException x) {
+ //this means that there are no available workers
+ worker = null;
+ }
+ } else if ( used.size() < this.maxThreads && creator != null) {
+ worker = creator.getWorkerThread();
+ setupThread(worker);
+ } else {
+ try { mutex.wait(); } catch ( java.lang.InterruptedException x ) {Thread.currentThread().interrupted();}
+ }
+ }//while
+ if ( worker != null ) used.add(worker);
+ }
+ return (worker);
+ }
+
+ public int available() {
+ return idle.size();
+ }
+
+ /**
+ * Called by the worker thread to return itself to the
+ * idle pool.
+ */
+ public void returnWorker (WorkerThread worker) {
+ if ( running ) {
+ synchronized (mutex) {
+ used.remove(worker);
+ //if ( idle.size() < minThreads && !idle.contains(worker)) idle.add(worker);
+ if ( idle.size() < maxThreads && !idle.contains(worker)) idle.add(worker); //let max be the upper limit
+ else {
+ worker.setDoRun(false);
+ synchronized (worker){worker.notify();}
+ }
+ mutex.notify();
+ }
+ }else {
+ worker.setDoRun(false);
+ synchronized (worker){worker.notify();}
+ }
+ }
+
+ public int getMaxThreads() {
+ return maxThreads;
+ }
+
+ public int getMinThreads() {
+ return minThreads;
+ }
+
+ public void stop() {
+ running = false;
+ synchronized (mutex) {
+ Iterator i = idle.iterator();
+ while ( i.hasNext() ) {
+ WorkerThread worker = (WorkerThread)i.next();
+ returnWorker(worker);
+ i.remove();
+ }
+ }
+ }
+
+ public void setMaxThreads(int maxThreads) {
+ this.maxThreads = maxThreads;
+ }
+
+ public void setMinThreads(int minThreads) {
+ this.minThreads = minThreads;
+ }
+
+ public ThreadCreator getThreadCreator() {
+ return this.creator;
+ }
+
+ public static interface ThreadCreator {
+ public WorkerThread getWorkerThread();
+ }
+}
diff --git a/java/org/apache/catalina/tribes/transport/WorkerThread.java b/java/org/apache/catalina/tribes/transport/WorkerThread.java
new file mode 100644
index 000000000..70d951250
--- /dev/null
+++ b/java/org/apache/catalina/tribes/transport/WorkerThread.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes.transport;
+
+import org.apache.catalina.tribes.io.ListenCallback;
+
+
+
+
+/**
+ * @author Filip Hanik
+ * @version $Revision: 366253 $ $Date: 2006-01-05 13:30:42 -0600 (Thu, 05 Jan 2006) $
+ */
+public abstract class WorkerThread extends Thread
+{
+
+ public static final int OPTION_DIRECT_BUFFER = ReceiverBase.OPTION_DIRECT_BUFFER;
+
+ private ListenCallback callback;
+ private ThreadPool pool;
+ private boolean doRun = true;
+ private int options;
+ protected boolean useBufferPool = true;
+
+ public WorkerThread(ListenCallback callback) {
+ this.callback = callback;
+ }
+
+ public void setPool(ThreadPool pool) {
+ this.pool = pool;
+ }
+
+ public void setOptions(int options) {
+ this.options = options;
+ }
+
+ public void setCallback(ListenCallback callback) {
+ this.callback = callback;
+ }
+
+ public void setDoRun(boolean doRun) {
+ this.doRun = doRun;
+ }
+
+ public ThreadPool getPool() {
+ return pool;
+ }
+
+ public int getOptions() {
+ return options;
+ }
+
+ public ListenCallback getCallback() {
+ return callback;
+ }
+
+ public boolean isDoRun() {
+ return doRun;
+ }
+
+ public void close()
+ {
+ doRun = false;
+ notify();
+ }
+
+ public void setUseBufferPool(boolean usebufpool) {
+ useBufferPool = usebufpool;
+ }
+
+ public boolean getUseBufferPool() {
+ return useBufferPool;
+ }
+}
diff --git a/java/org/apache/catalina/tribes/transport/bio/BioReceiver.java b/java/org/apache/catalina/tribes/transport/bio/BioReceiver.java
new file mode 100644
index 000000000..ab4b67b03
--- /dev/null
+++ b/java/org/apache/catalina/tribes/transport/bio/BioReceiver.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 1999,2006 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes.transport.bio;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+import org.apache.catalina.tribes.ChannelReceiver;
+import org.apache.catalina.tribes.io.ListenCallback;
+import org.apache.catalina.tribes.io.ObjectReader;
+import org.apache.catalina.tribes.transport.ReceiverBase;
+import org.apache.catalina.tribes.transport.ThreadPool;
+import org.apache.catalina.tribes.transport.WorkerThread;
+
+/**
+ * Title:
+ * + *Description:
+ * + *Copyright: Copyright (c) 2005
+ * + *Company:
+ * + * @author not attributable + * @version 1.0 + */ +public class BioReceiver extends ReceiverBase implements Runnable, ChannelReceiver, ListenCallback { + + protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(BioReceiver.class); + + protected ServerSocket serverSocket; + + public BioReceiver() { + } + + /** + * + * @throws IOException + * @todo Implement this org.apache.catalina.tribes.ChannelReceiver method + */ + public void start() throws IOException { + try { + setPool(new ThreadPool(getMaxThreads(),getMinThreads(),this)); + } catch (Exception x) { + log.fatal("ThreadPool can initilzed. Listener not started", x); + if ( x instanceof IOException ) throw (IOException)x; + else throw new IOException(x.getMessage()); + } + try { + getBind(); + bind(); + Thread t = new Thread(this, "BioReceiver"); + t.setDaemon(true); + t.start(); + } catch (Exception x) { + log.fatal("Unable to start cluster receiver", x); + if ( x instanceof IOException ) throw (IOException)x; + else throw new IOException(x.getMessage()); + } + } + + public WorkerThread getWorkerThread() { + return getReplicationThread(); + } + + protected BioReplicationThread getReplicationThread() { + BioReplicationThread result = new BioReplicationThread(this); + result.setOptions(getWorkerThreadOptions()); + result.setUseBufferPool(this.getUseBufferPool()); + return result; + } + + /** + * + * @todo Implement this org.apache.catalina.tribes.ChannelReceiver method + */ + public void stop() { + setListen(false); + try { + this.serverSocket.close(); + }catch ( Exception x ) {} + } + + + + + protected void bind() throws IOException { + // allocate an unbound server socket channel + serverSocket = new ServerSocket(); + // set the port the server channel will listen to + //serverSocket.bind(new InetSocketAddress(getBind(), getTcpListenPort())); + bind(serverSocket,getPort(),getAutoBind()); + } + + + + public void run() { + try { + listen(); + } catch (Exception x) { + log.error("Unable to run replication listener.", x); + } + } + + public void listen() throws Exception { + if (doListen()) { + log.warn("ServerSocket already started"); + return; + } + setListen(true); + + while ( doListen() ) { + Socket socket = null; + if ( getPool().available() < 1 ) { + if ( log.isWarnEnabled() ) + log.warn("All BIO server replication threads are busy, unable to handle more requests until a thread is freed up."); + } + BioReplicationThread thread = (BioReplicationThread)getPool().getWorker(); + if ( thread == null ) continue; //should never happen + try { + socket = serverSocket.accept(); + }catch ( Exception x ) { + if ( doListen() ) throw x; + } + if ( !doListen() ) { + thread.setDoRun(false); + thread.serviceSocket(null,null); + break; //regular shutdown + } + if ( socket == null ) continue; + socket.setReceiveBufferSize(getRxBufSize()); + socket.setSendBufferSize(getRxBufSize()); + socket.setTcpNoDelay(getTcpNoDelay()); + socket.setKeepAlive(getSoKeepAlive()); + socket.setOOBInline(getOoBInline()); + socket.setReuseAddress(getSoReuseAddress()); + socket.setSoLinger(getSoLingerOn(),getSoLingerTime()); + socket.setTrafficClass(getSoTrafficClass()); + socket.setSoTimeout(getTimeout()); + ObjectReader reader = new ObjectReader(socket); + thread.serviceSocket(socket,reader); + }//while + } + + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/transport/bio/BioReplicationThread.java b/java/org/apache/catalina/tribes/transport/bio/BioReplicationThread.java new file mode 100644 index 000000000..eacf6eb48 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/bio/BioReplicationThread.java @@ -0,0 +1,180 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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.catalina.tribes.transport.bio; +import java.io.IOException; + +import org.apache.catalina.tribes.io.ObjectReader; +import org.apache.catalina.tribes.transport.Constants; +import org.apache.catalina.tribes.transport.WorkerThread; +import java.net.Socket; +import java.io.InputStream; +import org.apache.catalina.tribes.transport.ReceiverBase; +import java.io.OutputStream; +import org.apache.catalina.tribes.io.ListenCallback; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.BufferPool; + +/** + * A worker thread class which can drain channels and echo-back the input. Each + * instance is constructed with a reference to the owning thread pool object. + * When started, the thread loops forever waiting to be awakened to service the + * channel associated with a SelectionKey object. The worker is tasked by + * calling its serviceChannel() method with a SelectionKey object. The + * serviceChannel() method stores the key reference in the thread object then + * calls notify() to wake it up. When the channel has been drained, the worker + * thread returns itself to its parent pool. + * + * @author Filip Hanik + * + * @version $Revision: 378050 $, $Date: 2006-02-15 12:30:02 -0600 (Wed, 15 Feb 2006) $ + */ +public class BioReplicationThread extends WorkerThread { + + + protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( BioReplicationThread.class ); + + protected Socket socket; + protected ObjectReader reader; + + public BioReplicationThread (ListenCallback callback) { + super(callback); + } + + // loop forever waiting for work to do + public synchronized void run() + { + this.notify(); + while (isDoRun()) { + try { + // sleep and release object lock + this.wait(); + } catch (InterruptedException e) { + if(log.isInfoEnabled()) + log.info("TCP worker thread interrupted in cluster",e); + // clear interrupt status + Thread.interrupted(); + } + if ( socket == null ) continue; + try { + drainSocket(); + } catch ( Exception x ) { + log.error("Unable to service bio socket"); + }finally { + try {socket.close();}catch ( Exception ignore){} + try {reader.close();}catch ( Exception ignore){} + reader = null; + socket = null; + } + // done, ready for more, return to pool + if ( getPool() != null ) getPool().returnWorker (this); + else setDoRun(false); + } + } + + + public synchronized void serviceSocket(Socket socket, ObjectReader reader) { + this.socket = socket; + this.reader = reader; + this.notify(); // awaken the thread + } + + protected void execute(ObjectReader reader) throws Exception{ + int pkgcnt = reader.count(); + + if ( pkgcnt > 0 ) { + ChannelMessage[] msgs = reader.execute(); + for ( int i=0; i<description>/<version>.
+ */
+ public String getInfo() {
+ return (info);
+ }
+
+ // --------------------------------------------------------- Public Methods
+
+ /**
+ * Connect other cluster member receiver
+ * @see org.apache.catalina.tribes.transport.IDataSender#connect()
+ */
+ public void connect() throws IOException {
+ openSocket();
+ }
+
+
+ /**
+ * disconnect and close socket
+ *
+ * @see IDataSender#disconnect()
+ */
+ public void disconnect() {
+ boolean connect = isConnected();
+ closeSocket();
+ if (connect) {
+ if (log.isDebugEnabled())
+ log.debug(sm.getString("IDataSender.disconnect", getAddress().getHostAddress(), new Integer(getPort()), new Long(0)));
+ }
+
+ }
+
+ /**
+ * Send message
+ *
+ * @see org.apache.catalina.tribes.transport.IDataSender#sendMessage(,
+ * ChannelMessage)
+ */
+ public void sendMessage(byte[] data, boolean waitForAck) throws IOException {
+ boolean messageTransfered = false ;
+ IOException exception = null;
+ setAttempt(0);
+ try {
+ // first try with existing connection
+ pushMessage(data,false,waitForAck);
+ messageTransfered = true ;
+ } catch (IOException x) {
+ SenderState.getSenderState(getDestination()).setSuspect();
+ exception = x;
+ if (log.isTraceEnabled()) log.trace(sm.getString("IDataSender.send.again", getAddress().getHostAddress(),new Integer(getPort())),x);
+ while ( getAttempt()Description:
+ * + *Copyright: Copyright (c) 2005
+ * + *Company:
+ * + * @author not attributable + * @version 1.0 + */ +public class MultipointBioSender extends AbstractSender implements MultiPointSender { + public MultipointBioSender() { + } + + protected long selectTimeout = 1000; + protected HashMap bioSenders = new HashMap(); + private boolean autoConnect; + + public synchronized void sendMessage(Member[] destination, ChannelMessage msg) throws ChannelException { + byte[] data = XByteBuffer.createDataPackage((ChannelData)msg); + BioSender[] senders = setupForSend(destination); + ChannelException cx = null; + for ( int i=0; iDescription:
+ * + *Copyright: Copyright (c) 2005
+ * + *Company:
+ * + * @author not attributable + * @version 1.0 + */ +public class PooledMultiSender extends PooledSender { + + + public PooledMultiSender() { + } + + public void sendMessage(Member[] destination, ChannelMessage msg) throws ChannelException { + MultiPointSender sender = null; + try { + sender = (MultiPointSender)getSender(); + if (sender == null) { + ChannelException cx = new ChannelException("Unable to retrieve a data sender, time out error."); + for (int i = 0; i < destination.length; i++) cx.addFaultyMember(destination[i], new NullPointerException("Unable to retrieve a sender from the sender pool")); + throw cx; + } else { + sender.sendMessage(destination, msg); + } + sender.keepalive(); + }finally { + if ( sender != null ) returnSender(sender); + } + } + + /** + * getNewDataSender + * + * @return DataSender + * @todo Implement this org.apache.catalina.tribes.transport.PooledSender + * method + */ + public DataSender getNewDataSender() { + MultipointBioSender sender = new MultipointBioSender(); + sender.transferProperties(this,sender); + return sender; + } + + + public void memberAdded(Member member) { + + } + + public void memberDisappeared(Member member) { + //disconnect senders + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/transport/bio/util/FastQueue.java b/java/org/apache/catalina/tribes/transport/bio/util/FastQueue.java new file mode 100644 index 000000000..0c34e3c39 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/bio/util/FastQueue.java @@ -0,0 +1,393 @@ +/* + * Copyright 1999,2004-2005 The Apache Software Foundation. + * + * Licensed 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.catalina.tribes.transport.bio.util; + +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.ErrorHandler; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.InterceptorPayload; + + + +/** + * A fast queue that remover thread lock the adder thread.<description>/<version>.
+ */
+ public String getInfo() {
+ return (info);
+ }
+
+// public Object getInterestOpsMutex() {
+// return interestOpsMutex;
+// }
+
+ public void stop() {
+ this.stopListening();
+ }
+
+ /**
+ * start cluster receiver
+ * @throws Exception
+ * @see org.apache.catalina.tribes.ClusterReceiver#start()
+ */
+ public void start() throws IOException {
+ try {
+// setPool(new ThreadPool(interestOpsMutex, getMaxThreads(),getMinThreads(),this));
+ setPool(new ThreadPool(getMaxThreads(),getMinThreads(),this));
+ } catch (Exception x) {
+ log.fatal("ThreadPool can initilzed. Listener not started", x);
+ if ( x instanceof IOException ) throw (IOException)x;
+ else throw new IOException(x.getMessage());
+ }
+ try {
+ getBind();
+ bind();
+ Thread t = new Thread(this, "NioReceiver");
+ t.setDaemon(true);
+ t.start();
+ } catch (Exception x) {
+ log.fatal("Unable to start cluster receiver", x);
+ if ( x instanceof IOException ) throw (IOException)x;
+ else throw new IOException(x.getMessage());
+ }
+ }
+
+ public WorkerThread getWorkerThread() {
+ NioReplicationThread thread = new NioReplicationThread(this,this);
+ thread.setUseBufferPool(this.getUseBufferPool());
+ thread.setRxBufSize(getRxBufSize());
+ thread.setOptions(getWorkerThreadOptions());
+ return thread;
+ }
+
+
+
+ protected void bind() throws IOException {
+ // allocate an unbound server socket channel
+ serverChannel = ServerSocketChannel.open();
+ // Get the associated ServerSocket to bind it with
+ ServerSocket serverSocket = serverChannel.socket();
+ // create a new Selector for use below
+ selector = Selector.open();
+ // set the port the server channel will listen to
+ //serverSocket.bind(new InetSocketAddress(getBind(), getTcpListenPort()));
+ bind(serverSocket,getTcpListenPort(),getAutoBind());
+ // set non-blocking mode for the listening socket
+ serverChannel.configureBlocking(false);
+ // register the ServerSocketChannel with the Selector
+ serverChannel.register(selector, SelectionKey.OP_ACCEPT);
+
+ }
+
+ public void addEvent(Runnable event) {
+ if ( selector != null ) {
+ synchronized (events) {
+ events.add(event);
+ }
+ if ( log.isTraceEnabled() ) log.trace("Adding event to selector:"+event);
+ selector.wakeup();
+ }
+ }
+
+ public void events() {
+ if ( events.size() == 0 ) return;
+ synchronized (events) {
+ Runnable r = null;
+ while ( (events.size() > 0) && (r = (Runnable)events.removeFirst()) != null ) {
+ try {
+ if ( log.isTraceEnabled() ) log.trace("Processing event in selector:"+r);
+ r.run();
+ } catch ( Exception x ) {
+ log.error("",x);
+ }
+ }
+ events.clear();
+ }
+ }
+
+ public static void cancelledKey(SelectionKey key) {
+ ObjectReader reader = (ObjectReader)key.attachment();
+ if ( reader != null ) {
+ reader.setCancelled(true);
+ reader.finish();
+ }
+ key.cancel();
+ key.attach(null);
+ try { ((SocketChannel)key.channel()).socket().close(); } catch (IOException e) { if (log.isDebugEnabled()) log.debug("", e); }
+ try { key.channel().close(); } catch (IOException e) { if (log.isDebugEnabled()) log.debug("", e); }
+
+ }
+
+ protected void socketTimeouts() {
+ //timeout
+ Set keys = selector.keys();
+ long now = System.currentTimeMillis();
+ for (Iterator iter = keys.iterator(); iter.hasNext(); ) {
+ SelectionKey key = (SelectionKey) iter.next();
+ try {
+// if (key.interestOps() == SelectionKey.OP_READ) {
+// //only timeout sockets that we are waiting for a read from
+// ObjectReader ka = (ObjectReader) key.attachment();
+// long delta = now - ka.getLastAccess();
+// if (delta > (long) getTimeout()) {
+// cancelledKey(key);
+// }
+// }
+// else
+ if ( key.interestOps() == 0 ) {
+ //check for keys that didn't make it in.
+ ObjectReader ka = (ObjectReader) key.attachment();
+ if ( ka != null ) {
+ long delta = now - ka.getLastAccess();
+ if (delta > (long) getTimeout() && (!ka.isAccessed())) {
+ log.warn("Channel key is registered, but has had no interest ops for the last "+getTimeout()+" ms. (cancelled:"+ka.isCancelled()+"):"+key+" last access:"+new java.sql.Timestamp(ka.getLastAccess()));
+// System.out.println("Interest:"+key.interestOps());
+// System.out.println("Ready Ops:"+key.readyOps());
+// System.out.println("Valid:"+key.isValid());
+ ka.setLastAccess(now);
+ //key.interestOps(SelectionKey.OP_READ);
+ }//end if
+ } else {
+ cancelledKey(key);
+ }//end if
+ }//end if
+ }catch ( CancelledKeyException ckx ) {
+ cancelledKey(key);
+ }
+ }
+ }
+
+
+ /**
+ * get data from channel and store in byte array
+ * send it to cluster
+ * @throws IOException
+ * @throws java.nio.channels.ClosedChannelException
+ */
+ protected void listen() throws Exception {
+ if (doListen()) {
+ log.warn("ServerSocketChannel already started");
+ return;
+ }
+
+ setListen(true);
+
+ while (doListen() && selector != null) {
+ // this may block for a long time, upon return the
+ // selected set contains keys of the ready channels
+ try {
+ events();
+ socketTimeouts();
+ int n = selector.select(getTcpSelectorTimeout());
+ if (n == 0) {
+ //there is a good chance that we got here
+ //because the TcpReplicationThread called
+ //selector wakeup().
+ //if that happens, we must ensure that that
+ //thread has enough time to call interestOps
+// synchronized (interestOpsMutex) {
+ //if we got the lock, means there are no
+ //keys trying to register for the
+ //interestOps method
+// }
+ continue; // nothing to do
+ }
+ // get an iterator over the set of selected keys
+ Iterator it = selector.selectedKeys().iterator();
+ // look at each key in the selected set
+ while (it.hasNext()) {
+ SelectionKey key = (SelectionKey) it.next();
+ // Is a new connection coming in?
+ if (key.isAcceptable()) {
+ ServerSocketChannel server = (ServerSocketChannel) key.channel();
+ SocketChannel channel = server.accept();
+ channel.socket().setReceiveBufferSize(getRxBufSize());
+ channel.socket().setSendBufferSize(getTxBufSize());
+ channel.socket().setTcpNoDelay(getTcpNoDelay());
+ channel.socket().setKeepAlive(getSoKeepAlive());
+ channel.socket().setOOBInline(getOoBInline());
+ channel.socket().setReuseAddress(getSoReuseAddress());
+ channel.socket().setSoLinger(getSoLingerOn(),getSoLingerTime());
+ channel.socket().setTrafficClass(getSoTrafficClass());
+ channel.socket().setSoTimeout(getTimeout());
+ Object attach = new ObjectReader(channel);
+ registerChannel(selector,
+ channel,
+ SelectionKey.OP_READ,
+ attach);
+ }
+ // is there data to read on this channel?
+ if (key.isReadable()) {
+ readDataFromSocket(key);
+ } else {
+ key.interestOps(key.interestOps() & (~SelectionKey.OP_WRITE));
+ }
+
+ // remove key from selected set, it's been handled
+ it.remove();
+ }
+ } catch (java.nio.channels.ClosedSelectorException cse) {
+ // ignore is normal at shutdown or stop listen socket
+ } catch (java.nio.channels.CancelledKeyException nx) {
+ log.warn("Replication client disconnected, error when polling key. Ignoring client.");
+ } catch (Throwable x) {
+ try {
+ log.error("Unable to process request in NioReceiver", x);
+ }catch ( Throwable tx ) {
+ //in case an out of memory error, will affect the logging framework as well
+ tx.printStackTrace();
+ }
+ }
+
+ }
+ serverChannel.close();
+ if (selector != null)
+ selector.close();
+ }
+
+
+
+ /**
+ * Close Selector.
+ *
+ * @see org.apache.catalina.tribes.transport.ClusterReceiverBase#stopListening()
+ */
+ protected void stopListening() {
+ // Bugzilla 37529: http://issues.apache.org/bugzilla/show_bug.cgi?id=37529
+ setListen(false);
+ if (selector != null) {
+ try {
+ for (int i = 0; i < getMaxThreads(); i++) {
+ selector.wakeup();
+ }
+ selector.close();
+ } catch (Exception x) {
+ log.error("Unable to close cluster receiver selector.", x);
+ } finally {
+ selector = null;
+ }
+ }
+ }
+
+ // ----------------------------------------------------------
+
+ /**
+ * Register the given channel with the given selector for
+ * the given operations of interest
+ */
+ protected void registerChannel(Selector selector,
+ SelectableChannel channel,
+ int ops,
+ Object attach) throws Exception {
+ if (channel == null)return; // could happen
+ // set the new channel non-blocking
+ channel.configureBlocking(false);
+ // register it with the selector
+ channel.register(selector, ops, attach);
+ }
+
+ /**
+ * Start thread and listen
+ */
+ public void run() {
+ try {
+ listen();
+ } catch (Exception x) {
+ log.error("Unable to run replication listener.", x);
+ }
+ }
+
+ // ----------------------------------------------------------
+
+ /**
+ * Sample data handler method for a channel with data ready to read.
+ * @param key A SelectionKey object associated with a channel
+ * determined by the selector to be ready for reading. If the
+ * channel returns an EOF condition, it is closed here, which
+ * automatically invalidates the associated key. The selector
+ * will then de-register the channel on the next select call.
+ */
+ protected void readDataFromSocket(SelectionKey key) throws Exception {
+ NioReplicationThread worker = (NioReplicationThread) getPool().getWorker();
+ if (worker == null) {
+ // No threads available, do nothing, the selection
+ // loop will keep calling this method until a
+ // thread becomes available, the thread pool itself has a waiting mechanism
+ // so we will not wait here.
+ if (log.isDebugEnabled())
+ log.debug("No TcpReplicationThread available");
+ } else {
+ // invoking this wakes up the worker thread then returns
+ worker.serviceChannel(key);
+ }
+ }
+
+
+}
diff --git a/java/org/apache/catalina/tribes/transport/nio/NioReplicationThread.java b/java/org/apache/catalina/tribes/transport/nio/NioReplicationThread.java
new file mode 100644
index 000000000..d19f13145
--- /dev/null
+++ b/java/org/apache/catalina/tribes/transport/nio/NioReplicationThread.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright 1999,2004 The Apache Software Foundation.
+ *
+ * Licensed 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.catalina.tribes.transport.nio;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+
+import org.apache.catalina.tribes.io.ObjectReader;
+import org.apache.catalina.tribes.transport.Constants;
+import org.apache.catalina.tribes.transport.WorkerThread;
+import org.apache.catalina.tribes.ChannelMessage;
+import org.apache.catalina.tribes.io.ListenCallback;
+import org.apache.catalina.tribes.io.ChannelData;
+import org.apache.catalina.tribes.io.BufferPool;
+import java.nio.channels.CancelledKeyException;
+import org.apache.catalina.tribes.UniqueId;
+import org.apache.catalina.tribes.RemoteProcessException;
+import org.apache.catalina.tribes.util.Logs;
+
+/**
+ * A worker thread class which can drain channels and echo-back the input. Each
+ * instance is constructed with a reference to the owning thread pool object.
+ * When started, the thread loops forever waiting to be awakened to service the
+ * channel associated with a SelectionKey object. The worker is tasked by
+ * calling its serviceChannel() method with a SelectionKey object. The
+ * serviceChannel() method stores the key reference in the thread object then
+ * calls notify() to wake it up. When the channel has been drained, the worker
+ * thread returns itself to its parent pool.
+ *
+ * @author Filip Hanik
+ *
+ * @version $Revision: 378050 $, $Date: 2006-02-15 12:30:02 -0600 (Wed, 15 Feb 2006) $
+ */
+public class NioReplicationThread extends WorkerThread {
+
+ private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( NioReplicationThread.class );
+
+ private ByteBuffer buffer = null;
+ private SelectionKey key;
+ private int rxBufSize;
+ private NioReceiver receiver;
+ public NioReplicationThread (ListenCallback callback, NioReceiver receiver)
+ {
+ super(callback);
+ this.receiver = receiver;
+ }
+
+ // loop forever waiting for work to do
+ public synchronized void run() {
+ this.notify();
+ if ( (getOptions() & OPTION_DIRECT_BUFFER) == OPTION_DIRECT_BUFFER ) {
+ buffer = ByteBuffer.allocateDirect(getRxBufSize());
+ }else {
+ buffer = ByteBuffer.allocate (getRxBufSize());
+ }
+ while (isDoRun()) {
+ try {
+ // sleep and release object lock
+ this.wait();
+ } catch (InterruptedException e) {
+ if(log.isInfoEnabled()) log.info("TCP worker thread interrupted in cluster",e);
+ // clear interrupt status
+ Thread.interrupted();
+ }
+ if (key == null) {
+ continue; // just in case
+ }
+ if ( log.isTraceEnabled() )
+ log.trace("Servicing key:"+key);
+
+ try {
+ ObjectReader reader = (ObjectReader)key.attachment();
+ if ( reader == null ) {
+ if ( log.isTraceEnabled() )
+ log.trace("No object reader, cancelling:"+key);
+ cancelKey(key);
+ } else {
+ if ( log.isTraceEnabled() )
+ log.trace("Draining channel:"+key);
+
+ drainChannel(key, reader);
+ }
+ } catch (Exception e) {
+ //this is common, since the sockets on the other
+ //end expire after a certain time.
+ if ( e instanceof CancelledKeyException ) {
+ //do nothing
+ } else if ( e instanceof IOException ) {
+ //dont spew out stack traces for IO exceptions unless debug is enabled.
+ if (log.isDebugEnabled()) log.debug ("IOException in replication worker, unable to drain channel. Probable cause: Keep alive socket closed["+e.getMessage()+"].", e);
+ else log.warn ("IOException in replication worker, unable to drain channel. Probable cause: Keep alive socket closed["+e.getMessage()+"].");
+ } else if ( log.isErrorEnabled() ) {
+ //this is a real error, log it.
+ log.error("Exception caught in TcpReplicationThread.drainChannel.",e);
+ }
+ cancelKey(key);
+ } finally {
+
+ }
+ key = null;
+ // done, ready for more, return to pool
+ getPool().returnWorker (this);
+ }
+ }
+
+ /**
+ * Called to initiate a unit of work by this worker thread
+ * on the provided SelectionKey object. This method is
+ * synchronized, as is the run() method, so only one key
+ * can be serviced at a given time.
+ * Before waking the worker thread, and before returning
+ * to the main selection loop, this key's interest set is
+ * updated to remove OP_READ. This will cause the selector
+ * to ignore read-readiness for this channel while the
+ * worker thread is servicing it.
+ */
+ public synchronized void serviceChannel (SelectionKey key) {
+ if ( log.isTraceEnabled() )
+ log.trace("About to service key:"+key);
+ ObjectReader reader = (ObjectReader)key.attachment();
+ if ( reader != null ) reader.setLastAccess(System.currentTimeMillis());
+ this.key = key;
+ key.interestOps (key.interestOps() & (~SelectionKey.OP_READ));
+ key.interestOps (key.interestOps() & (~SelectionKey.OP_WRITE));
+ this.notify(); // awaken the thread
+ }
+
+ /**
+ * The actual code which drains the channel associated with
+ * the given key. This method assumes the key has been
+ * modified prior to invocation to turn off selection
+ * interest in OP_READ. When this method completes it
+ * re-enables OP_READ and calls wakeup() on the selector
+ * so the selector will resume watching this channel.
+ */
+ protected void drainChannel (final SelectionKey key, ObjectReader reader) throws Exception {
+ reader.setLastAccess(System.currentTimeMillis());
+ reader.access();
+ SocketChannel channel = (SocketChannel) key.channel();
+ int count;
+ buffer.clear(); // make buffer empty
+
+ // loop while data available, channel is non-blocking
+ while ((count = channel.read (buffer)) > 0) {
+ buffer.flip(); // make buffer readable
+ if ( buffer.hasArray() )
+ reader.append(buffer.array(),0,count,false);
+ else
+ reader.append(buffer,count,false);
+ buffer.clear(); // make buffer empty
+ //do we have at least one package?
+ if ( reader.hasPackage() ) break;
+ }
+
+ int pkgcnt = reader.count();
+
+ if (count < 0 && pkgcnt == 0 ) {
+ //end of stream, and no more packages to process
+ remoteEof(key);
+ return;
+ }
+
+ ChannelMessage[] msgs = pkgcnt == 0? ChannelData.EMPTY_DATA_ARRAY : reader.execute();
+
+ registerForRead(key,reader);//register to read new data, before we send it off to avoid dead locks
+
+ for ( int i=0; iTitle:
+ * + *Description:
+ * + *Copyright: Copyright (c) 2005
+ * + *Company:
+ * + * @author not attributable + * @version 1.0 + */ +public class ParallelNioSender extends AbstractSender implements MultiPointSender { + + protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ParallelNioSender.class); + protected long selectTimeout = 5000; //default 5 seconds, same as send timeout + protected Selector selector; + protected HashMap nioSenders = new HashMap(); + + public ParallelNioSender() throws IOException { + selector = Selector.open(); + setConnected(true); + } + + + public synchronized void sendMessage(Member[] destination, ChannelMessage msg) throws ChannelException { + long start = System.currentTimeMillis(); + byte[] data = XByteBuffer.createDataPackage((ChannelData)msg); + NioSender[] senders = setupForSend(destination); + connect(senders); + setData(senders,data); + + int remaining = senders.length; + ChannelException cx = null; + try { + //loop until complete, an error happens, or we timeout + long delta = System.currentTimeMillis() - start; + boolean waitForAck = (Channel.SEND_OPTIONS_USE_ACK & msg.getOptions()) == Channel.SEND_OPTIONS_USE_ACK; + while ( (remaining>0) && (deltaDescription:
+ * + *Copyright: Copyright (c) 2005
+ * + *Company:
+ * + * @author not attributable + * @version 1.0 + */ +public class PooledParallelSender extends PooledSender implements MultiPointSender { + protected boolean connected = true; + public PooledParallelSender() { + super(); + } + + public void sendMessage(Member[] destination, ChannelMessage message) throws ChannelException { + if ( !connected ) throw new ChannelException("Sender not connected."); + ParallelNioSender sender = (ParallelNioSender)getSender(); + try { + sender.sendMessage(destination, message); + sender.keepalive(); + }finally { + if ( !connected ) disconnect(); + returnSender(sender); + } + } + + public DataSender getNewDataSender() { + try { + ParallelNioSender sender = new ParallelNioSender(); + sender.transferProperties(this,sender); + return sender; + } catch ( IOException x ) { + throw new RuntimeException("Unable to open NIO selector.",x); + } + } + + public synchronized void disconnect() { + this.connected = false; + super.disconnect(); + } + + public synchronized void connect() throws IOException { + this.connected = true; + super.connect(); + } + + public void memberAdded(Member member) { + + } + + public void memberDisappeared(Member member) { + //disconnect senders + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/util/Arrays.java b/java/org/apache/catalina/tribes/util/Arrays.java new file mode 100644 index 000000000..ef075a8b0 --- /dev/null +++ b/java/org/apache/catalina/tribes/util/Arrays.java @@ -0,0 +1,195 @@ +/* + * Copyright 1999,2004-2006 The Apache Software Foundation. + * + * Licensed 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.catalina.tribes.util; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.UniqueId; +import org.apache.catalina.tribes.group.AbsoluteOrder; +import org.apache.catalina.tribes.membership.MemberImpl; +import org.apache.catalina.tribes.membership.Membership; + +/** + * @author Filip Hanik + * @version 1.0 + */ +public class Arrays { + + public static boolean contains(byte[] source, int srcoffset, byte[] key, int keyoffset, int length) { + if ( srcoffset < 0 || srcoffset >= source.length) throw new ArrayIndexOutOfBoundsException("srcoffset is out of bounds."); + if ( keyoffset < 0 || keyoffset >= key.length) throw new ArrayIndexOutOfBoundsException("keyoffset is out of bounds."); + if ( length > (key.length-keyoffset) ) throw new ArrayIndexOutOfBoundsException("not enough data elements in the key, length is out of bounds."); + //we don't have enough data to validate it + if ( length > (source.length-srcoffset) ) return false; + boolean match = true; + int pos = keyoffset; + for ( int i=srcoffset; match && iThe StringManager will look for a ResourceBundle named by + * the package name given plus the suffix of "LocalStrings". In + * practice, this means that the localized information will be contained + * in a LocalStrings.properties file located in the package + * directory of the classpath. + * + *
Please see the documentation for java.util.ResourceBundle for
+ * more information.
+ *
+ * @author James Duncan Davidson [duncan@eng.sun.com]
+ * @author James Todd [gonzo@eng.sun.com]
+ */
+
+public class StringManager {
+
+ /**
+ * The ResourceBundle for this StringManager.
+ */
+
+ private ResourceBundle bundle;
+
+ private static org.apache.commons.logging.Log log=
+ org.apache.commons.logging.LogFactory.getLog( StringManager.class );
+
+ /**
+ * Creates a new StringManager for a given package. This is a
+ * private method and all access to it is arbitrated by the
+ * static getManager method call so that only one StringManager
+ * per package will be created.
+ *
+ * @param packageName Name of package to create StringManager for.
+ */
+
+ private StringManager(String packageName) {
+ String bundleName = packageName + ".LocalStrings";
+ try {
+ bundle = ResourceBundle.getBundle(bundleName);
+ return;
+ } catch( MissingResourceException ex ) {
+ // Try from the current loader ( that's the case for trusted apps )
+ ClassLoader cl=Thread.currentThread().getContextClassLoader();
+ if( cl != null ) {
+ try {
+ bundle=ResourceBundle.getBundle(bundleName, Locale.getDefault(), cl);
+ return;
+ } catch(MissingResourceException ex2) {
+ }
+ }
+ if( cl==null )
+ cl=this.getClass().getClassLoader();
+
+ if (log.isDebugEnabled())
+ log.debug("Can't find resource " + bundleName +
+ " " + cl);
+ if( cl instanceof URLClassLoader ) {
+ if (log.isDebugEnabled())
+ log.debug( ((URLClassLoader)cl).getURLs());
+ }
+ }
+ }
+
+ /**
+ * Get a string from the underlying resource bundle.
+ *
+ * @param key The resource name
+ */
+ public String getString(String key) {
+ return MessageFormat.format(getStringInternal(key), (Object [])null);
+ }
+
+
+ protected String getStringInternal(String key) {
+ if (key == null) {
+ String msg = "key is null";
+
+ throw new NullPointerException(msg);
+ }
+
+ String str = null;
+
+ if( bundle==null )
+ return key;
+ try {
+ str = bundle.getString(key);
+ } catch (MissingResourceException mre) {
+ str = "Cannot find message associated with key '" + key + "'";
+ }
+
+ return str;
+ }
+
+ /**
+ * Get a string from the underlying resource bundle and format
+ * it with the given set of arguments.
+ *
+ * @param key The resource name
+ * @param args Formatting directives
+ */
+
+ public String getString(String key, Object[] args) {
+ String iString = null;
+ String value = getStringInternal(key);
+
+ // this check for the runtime exception is some pre 1.1.6
+ // VM's don't do an automatic toString() on the passed in
+ // objects and barf out
+
+ try {
+ // ensure the arguments are not null so pre 1.2 VM's don't barf
+ Object nonNullArgs[] = args;
+ for (int i=0; i