From: fhanik Date: Tue, 22 Aug 2006 17:28:09 +0000 (+0000) Subject: First step in cluster integration X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=22419c050f033f4957cf8ba556bf94fa8a5db823;p=tomcat7.0 First step in cluster integration git-svn-id: https://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk@433703 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/build.xml b/build.xml index 2d6185c43..9657b1a6e 100644 --- a/build.xml +++ b/build.xml @@ -42,6 +42,8 @@ + + @@ -186,10 +188,31 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/ha/CatalinaCluster.java b/java/org/apache/catalina/ha/CatalinaCluster.java new file mode 100644 index 000000000..d4a9e6e36 --- /dev/null +++ b/java/org/apache/catalina/ha/CatalinaCluster.java @@ -0,0 +1,130 @@ +/* + * 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 java.util.Map; + +import org.apache.catalina.Cluster; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Manager; +import org.apache.catalina.Valve; +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.Member; +import org.apache.commons.logging.Log; + + + +/** + * A CatalinaCluster interface allows to plug in and out the + * different cluster implementations + * + * @author Filip Hanik + * @version $Revision: 379550 $, $Date: 2006-02-21 12:06:35 -0600 (Tue, 21 Feb 2006) $ + */ + +public interface CatalinaCluster extends Cluster { + // ----------------------------------------------------- Instance Variables + + /** + * Descriptive information about this component implementation. + */ + public String info = "CatalinaCluster/2.0"; + + /** + * Start the cluster, the owning container will invoke this + * @throws Exception - if failure to start cluster + */ + public void start() throws Exception; + + /** + * Stops the cluster, the owning container will invoke this + * @throws LifecycleException + */ + public void stop() throws LifecycleException; + + /** + * Returns the associates logger with this cluster. + * + * @return Log + */ + public Log getLogger(); + + /** + * Sends a message to all the members in the cluster + * @param msg ClusterMessage + */ + public void send(ClusterMessage msg); + + /** + * Sends a message to a specific member in the cluster. + * + * @param msg ClusterMessage + * @param dest Member + */ + public void send(ClusterMessage msg, Member dest); + + /** + * Sends a message to a all members at local cluster domain + * + * @param msg ClusterMessage + */ + public void sendClusterDomain(ClusterMessage msg); + + /** + * Returns that cluster has members. + */ + public boolean hasMembers(); + + /** + * Returns all the members currently participating in the cluster. + * + * @return Member[] + */ + public Member[] getMembers(); + + /** + * Return the member that represents this node. + * + * @return Member + */ + public Member getLocalMember(); + + public void addValve(Valve valve); + + public void addClusterListener(ClusterListener listener); + + public void removeClusterListener(ClusterListener listener); + + public void setClusterDeployer(ClusterDeployer deployer); + + public ClusterDeployer getClusterDeployer(); + + /** + * @return The map of managers + */ + public Map getManagers(); + + public Manager getManager(String name); + public void removeManager(String name,Manager manager); + public void addManager(String name,Manager manager); + public String getManagerName(String name, Manager manager); + public Valve[] getValves(); + + public void setChannel(Channel channel); + public Channel getChannel(); + +} diff --git a/java/org/apache/catalina/ha/ClusterDeployer.java b/java/org/apache/catalina/ha/ClusterDeployer.java new file mode 100644 index 000000000..39c9322ab --- /dev/null +++ b/java/org/apache/catalina/ha/ClusterDeployer.java @@ -0,0 +1,120 @@ +/* + * 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; + +/** + * A ClusterDeployer interface allows to plug in and out the + * different deployment implementations + * + * @author Filip Hanik + * @version $Revision: 378050 $, $Date: 2006-02-15 12:30:02 -0600 (Wed, 15 Feb 2006) $ + */ +import org.apache.catalina.LifecycleException; +import java.io.IOException; +import java.net.URL; +import org.apache.catalina.tribes.ChannelListener; + +public interface ClusterDeployer extends ChannelListener { + /** + * Descriptive information about this component implementation. + */ + public String info = "ClusterDeployer/1.0"; + /** + * Start the cluster deployer, the owning container will invoke this + * @throws Exception - if failure to start cluster + */ + public void start() throws Exception; + + /** + * Stops the cluster deployer, the owning container will invoke this + * @throws LifecycleException + */ + public void stop() throws LifecycleException; + + /** + * Sets the deployer for this cluster deployer to use. + * @param deployer Deployer + */ + // FIXME + //public void setDeployer(Deployer deployer); + + /** + * 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. + *

+ * If this application is successfully installed locally, + * a ContainerEvent of type + * 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; + + +/** + *

Title:

+ * + *

Description:

+ * + *

Copyright: Copyright (c) 2005

+ * + *

Company:

+ * + * @author not attributable + * @version 1.0 + */ +public class ClusterMessageBase implements ClusterMessage { + + protected transient Member address; + private String uniqueId; + private long timestamp; + public ClusterMessageBase() { + } + + /** + * getAddress + * + * @return Member + * @todo Implement this org.apache.catalina.ha.ClusterMessage method + */ + public Member getAddress() { + return address; + } + + public String getUniqueId() { + return uniqueId; + } + + public long getTimestamp() { + return timestamp; + } + + /** + * setAddress + * + * @param member Member + * @todo Implement this org.apache.catalina.ha.ClusterMessage method + */ + public void setAddress(Member member) { + this.address = member; + } + + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/ha/ClusterRuleSet.java b/java/org/apache/catalina/ha/ClusterRuleSet.java new file mode 100644 index 000000000..1f5ab4a17 --- /dev/null +++ b/java/org/apache/catalina/ha/ClusterRuleSet.java @@ -0,0 +1,176 @@ +/* + * Copyright 1999-2001,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.tomcat.util.digester.Digester; +import org.apache.tomcat.util.digester.RuleSetBase; + + +/** + *

RuleSet for processing the contents of a + * Cluster definition element.

+ * + * @author Filip Hanik + * @author Peter Rossbach + * @version $Revision: 387285 $ $Date: 2006-03-20 13:30:50 -0600 (Mon, 20 Mar 2006) $ + */ + +public class ClusterRuleSet extends RuleSetBase { + + + // ----------------------------------------------------- Instance Variables + + + /** + * The matching pattern prefix to use for recognizing our elements. + */ + protected String prefix = null; + + + // ------------------------------------------------------------ Constructor + + + /** + * Construct an instance of this 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 + + + /** + *

Add the set of Rule instances defined in this RuleSet to the + * specified Digester instance, associating them with + * our namespace URI (if any). This method should only be called + * by a Digester instance.

+ * + * @param digester Digester instance to which the new Rule instances + * should be added. + */ + public void addRuleInstances(Digester digester) { + //Cluster configuration start + + digester.addObjectCreate(prefix + "Channel", + null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Channel"); + digester.addSetNext(prefix + "Channel", + "setChannel", + "org.apache.catalina.tribes.Channel"); + + + String channelPrefix = prefix + "Channel/"; + { //channel properties + digester.addObjectCreate(channelPrefix + "Membership", + null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Membership"); + digester.addSetNext(channelPrefix + "Membership", + "setMembershipService", + "org.apache.catalina.tribes.MembershipService"); + + digester.addObjectCreate(channelPrefix + "Sender", + null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Sender"); + digester.addSetNext(channelPrefix + "Sender", + "setChannelSender", + "org.apache.catalina.tribes.ChannelSender"); + + digester.addObjectCreate(channelPrefix + "Sender/Transport", + null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Sender/Transport"); + digester.addSetNext(channelPrefix + "Sender/Transport", + "setTransport", + "org.apache.catalina.tribes.tcp.MultiPointSender"); + + + digester.addObjectCreate(channelPrefix + "Receiver", + null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Receiver"); + digester.addSetNext(channelPrefix + "Receiver", + "setChannelReceiver", + "org.apache.catalina.tribes.ChannelReceiver"); + + digester.addObjectCreate(channelPrefix + "Interceptor", + null, // MUST be specified in the element + "className"); + digester.addSetProperties(channelPrefix + "Interceptor"); + digester.addSetNext(channelPrefix + "Interceptor", + "addInterceptor", + "org.apache.catalina.tribes.ChannelInterceptor"); + } + + digester.addObjectCreate(prefix + "Valve", + null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Valve"); + digester.addSetNext(prefix + "Valve", + "addValve", + "org.apache.catalina.Valve"); + + digester.addObjectCreate(prefix + "Deployer", + null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Deployer"); + digester.addSetNext(prefix + "Deployer", + "setClusterDeployer", + "org.apache.catalina.ha.ClusterDeployer"); + + digester.addObjectCreate(prefix + "Listener", + null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "Listener"); + digester.addSetNext(prefix + "Listener", + "addLifecycleListener", + "org.apache.catalina.LifecycleListener"); + + digester.addObjectCreate(prefix + "ClusterListener", + null, // MUST be specified in the element + "className"); + digester.addSetProperties(prefix + "ClusterListener"); + digester.addSetNext(prefix + "ClusterListener", + "addClusterListener", + "org.apache.catalina.ha.ClusterListener"); + //Cluster configuration end + } + +} diff --git a/java/org/apache/catalina/ha/ClusterSession.java b/java/org/apache/catalina/ha/ClusterSession.java new file mode 100644 index 000000000..8eb897822 --- /dev/null +++ b/java/org/apache/catalina/ha/ClusterSession.java @@ -0,0 +1,39 @@ +/* + * 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.Session; +import javax.servlet.http.HttpSession; + +public interface ClusterSession extends Session, HttpSession { + /** + * returns true if this session is the primary session, if that is the + * case, the manager can expire it upon timeout. + * @return True if this session is primary + */ + public boolean isPrimarySession(); + + /** + * Sets whether this is the primary session or not. + * @param primarySession Flag value + */ + public void setPrimarySession(boolean primarySession); + + + +} diff --git a/java/org/apache/catalina/ha/ClusterValve.java b/java/org/apache/catalina/ha/ClusterValve.java new file mode 100644 index 000000000..d82f6df89 --- /dev/null +++ b/java/org/apache/catalina/ha/ClusterValve.java @@ -0,0 +1,36 @@ +/* + * 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; + +/** + * Cluster Valve Interface to mark all Cluster Valves + * Only those Valve can'be configured as Cluster Valves + * @author Peter Rossbach + * @version $Revision: 303842 $, $Date: 2005-04-10 11:20:46 -0500 (Sun, 10 Apr 2005) $ + */ +public interface ClusterValve { + /** + * 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/Constants.java b/java/org/apache/catalina/ha/Constants.java new file mode 100644 index 000000000..7b7f04148 --- /dev/null +++ b/java/org/apache/catalina/ha/Constants.java @@ -0,0 +1,30 @@ +/* + * 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; + +/** + * Manifest constants for the 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; + + +/** + *

+ * A farm war deployer is a class that is able to deploy/undeploy web + * applications in WAR form within the cluster. + *

+ * Any host can act as the admin, and will have three directories + *
    + *
  • deployDir - the directory where we watch for changes
  • + *
  • applicationDir - the directory where we install applications
  • + *
  • tempDir - a temporaryDirectory to store binary data when downloading a + * war from the cluster
  • + *
+ * 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. + *

+ * If this application is successfully installed locally, a ContainerEvent + * of type 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; + +/** + *

+ * 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 + *

+ * + * @author Filip Hanik + * @author Peter Rossbach + * @version 1.1 + */ + +public class WarWatcher { + + /*--Static Variables----------------------------------------*/ + public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory + .getLog(WarWatcher.class); + + /*--Instance Variables--------------------------------------*/ + /** + * Directory to watch for war files + */ + protected File watchDir = null; + + /** + * Parent to be notified of changes + */ + protected FileChangeListener listener = null; + + /** + * Currently deployed files + */ + protected Map currentStatus = new HashMap(); + + /*--Constructor---------------------------------------------*/ + + public WarWatcher() { + } + + public WarWatcher(FileChangeListener listener, File watchDir) { + this.listener = listener; + this.watchDir = watchDir; + } + + /*--Logic---------------------------------------------------*/ + + /** + * check for modification and send notifcation to listener + */ + public void check() { + if (log.isInfoEnabled()) + log.info("check cluster wars at " + watchDir); + File[] list = watchDir.listFiles(new WarFilter()); + if (list == null) + list = new File[0]; + //first make sure all the files are listed in our current status + for (int i = 0; i < list.length; i++) { + addWarInfo(list[i]); + } + + //check all the status codes and update the FarmDeployer + for (Iterator i = currentStatus.entrySet().iterator(); i.hasNext();) { + Map.Entry entry = (Map.Entry) i.next(); + WarInfo info = (WarInfo) entry.getValue(); + int check = info.check(); + if (check == 1) { + listener.fileModified(info.getWar()); + } else if (check == -1) { + listener.fileRemoved(info.getWar()); + //no need to keep in memory + currentStatus.remove(info.getWar()); + } + } + + } + + /** + * add cluster war to the watcher state + * @param warfile + */ + protected void addWarInfo(File warfile) { + WarInfo info = (WarInfo) currentStatus.get(warfile.getAbsolutePath()); + if (info == null) { + info = new WarInfo(warfile); + info.setLastState(-1); //assume file is non existent + currentStatus.put(warfile.getAbsolutePath(), info); + } + } + + /** + * clear watcher state + */ + public void clear() { + currentStatus.clear(); + } + + /** + * @return Returns the watchDir. + */ + public File getWatchDir() { + return watchDir; + } + + /** + * @param watchDir + * The watchDir to set. + */ + public void setWatchDir(File watchDir) { + this.watchDir = watchDir; + } + + /** + * @return Returns the listener. + */ + public FileChangeListener getListener() { + return listener; + } + + /** + * @param listener + * The listener to set. + */ + public void setListener(FileChangeListener listener) { + this.listener = listener; + } + + /*--Inner classes-------------------------------------------*/ + + /** + * File name filter for war files + */ + protected class WarFilter implements java.io.FilenameFilter { + public boolean accept(File path, String name) { + if (name == null) + return false; + return name.endsWith(".war"); + } + } + + /** + * File information on existing WAR files + */ + protected class WarInfo { + protected File war = null; + + protected long lastChecked = 0; + + protected long lastState = 0; + + public WarInfo(File war) { + this.war = war; + this.lastChecked = war.lastModified(); + if (!war.exists()) + lastState = -1; + } + + public boolean modified() { + return war.exists() && war.lastModified() > lastChecked; + } + + public boolean exists() { + return war.exists(); + } + + /** + * Returns 1 if the file has been added/modified, 0 if the file is + * unchanged and -1 if the file has been removed + * + * @return int 1=file added; 0=unchanged; -1=file removed + */ + public int check() { + //file unchanged by default + int result = 0; + + if (modified()) { + //file has changed - timestamp + result = 1; + lastState = result; + } else if ((!exists()) && (!(lastState == -1))) { + //file was removed + result = -1; + lastState = result; + } else if ((lastState == -1) && exists()) { + //file was added + result = 1; + lastState = result; + } + this.lastChecked = System.currentTimeMillis(); + return result; + } + + public File getWar() { + return war; + } + + public int hashCode() { + return war.getAbsolutePath().hashCode(); + } + + public boolean equals(Object other) { + if (other instanceof WarInfo) { + WarInfo wo = (WarInfo) other; + return wo.getWar().equals(getWar()); + } else { + return false; + } + } + + protected void setLastState(int lastState) { + this.lastState = lastState; + } + + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/ha/mbeans-descriptors.xml b/java/org/apache/catalina/ha/mbeans-descriptors.xml new file mode 100644 index 000000000..7f54550a8 --- /dev/null +++ b/java/org/apache/catalina/ha/mbeans-descriptors.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/ha/package.html b/java/org/apache/catalina/ha/package.html new file mode 100644 index 000000000..ce83a0d79 --- /dev/null +++ b/java/org/apache/catalina/ha/package.html @@ -0,0 +1,11 @@ + + +

This package contains code for Clustering, the base class +of a Cluster is org.apache.catalina.Cluster implementations +of this class is done when implementing a new Cluster protocol

+ +

The only Cluster protocol currently implemented is a JavaGroups based
+    JGCluster.java +

+ + diff --git a/java/org/apache/catalina/ha/session/BackupManager.java b/java/org/apache/catalina/ha/session/BackupManager.java new file mode 100644 index 000000000..fcaa1cf68 --- /dev/null +++ b/java/org/apache/catalina/ha/session/BackupManager.java @@ -0,0 +1,271 @@ +/* + * 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.ByteArrayInputStream; +import java.io.IOException; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Loader; +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.session.StandardManager; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.io.ReplicationStream; +import org.apache.catalina.tribes.tipis.LazyReplicatedMap; +import org.apache.catalina.tribes.Channel; + +/** + *@author Filip Hanik + *@version 1.0 + */ +public class BackupManager extends StandardManager implements ClusterManager +{ + public static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( BackupManager.class ); + + protected static long DEFAULT_REPL_TIMEOUT = 15000;//15 seconds + + /** Set to true if we don't want the sessions to expire on shutdown */ + protected boolean mExpireSessionsOnShutdown = true; + + /** + * The name of this manager + */ + protected String name; + + /** + * A reference to the cluster + */ + protected CatalinaCluster cluster; + + /** + * Should listeners be notified? + */ + private boolean notifyListenersOnReplication; + /** + * + */ + private int mapSendOptions = Channel.SEND_OPTIONS_SYNCHRONIZED_ACK|Channel.SEND_OPTIONS_USE_ACK; + + /** + * Constructor, just calls super() + * + */ + public BackupManager() { + super(); + } + + +//******************************************************************************/ +// ClusterManager Interface +//******************************************************************************/ + + public void messageDataReceived(ClusterMessage msg) { + } + + public boolean isSendClusterDomainOnly() { + return false; + } + + /** + * @param sendClusterDomainOnly The sendClusterDomainOnly to set. + */ + public void setSendClusterDomainOnly(boolean sendClusterDomainOnly) { + } + + /** + * @return Returns the defaultMode. + */ + public boolean isDefaultMode() { + return false; + } + /** + * @param defaultMode The defaultMode to set. + */ + public void setDefaultMode(boolean defaultMode) { + } + + 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; + } + + + /** + * Override persistence since they don't go hand in hand with replication for now. + */ + public void unload() throws IOException { + } + + public ClusterMessage requestCompleted(String sessionId) { + if ( !this.started ) return null; + LazyReplicatedMap map = (LazyReplicatedMap)sessions; + map.replicate(sessionId,false); + return null; + } + + +//========================================================================= +// OVERRIDE THESE METHODS TO IMPLEMENT THE REPLICATION +//========================================================================= + + public Session createEmptySession() { + return new DeltaSession(this); + } + + public ClassLoader[] getClassLoaders() { + return ClusterManagerBase.getClassLoaders(this.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()); + } + + + + + 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 { + 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; i 0) { + try { + info = (AttributeInfo) actionPool.removeFirst(); + } catch ( Exception x ) { + log.error("Unable to remove element",x); + info = new AttributeInfo(-1,-1,null,null); + } + } + else { + info = new AttributeInfo(-1,-1,null,null); + } + info.readExternal(in); + actions.addLast(info); + }//for + } + + + + public synchronized void writeExternal(java.io.ObjectOutput out ) throws java.io.IOException { + //sessionId - String + //recordAll - boolean + //size - int + //AttributeInfo - in an array + out.writeUTF(getSessionId()); + out.writeBoolean(recordAllActions); + out.writeInt(getSize()); + for ( int i=0; i0; + } + + /** + * If this returns true, the map will extract the diff using getDiff() + * Otherwise it will serialize the entire object. + * @return boolean + */ + public boolean isDiffable() { + return true; + } + + /** + * Returns a diff and sets the dirty map to false + * @return byte[] + * @throws IOException + */ + public byte[] getDiff() throws IOException { + return getDeltaRequest().serialize(); + } + + public ClassLoader[] getClassLoaders() { + if ( manager instanceof BackupManager ) return ((BackupManager)manager).getClassLoaders(); + else if ( manager instanceof ClusterManagerBase ) return ((ClusterManagerBase)manager).getClassLoaders(); + else if ( manager instanceof StandardManager ) { + StandardManager sm = (StandardManager)manager; + return ClusterManagerBase.getClassLoaders(sm.getContainer()); + } else if ( manager instanceof ManagerBase ) { + ManagerBase mb = (ManagerBase)manager; + return ClusterManagerBase.getClassLoaders(mb.getContainer()); + }//end if + return null; + } + + /** + * Applies a diff to an existing object. + * @param diff byte[] + * @param offset int + * @param length int + * @throws IOException + */ + public void applyDiff(byte[] diff, int offset, int length) throws IOException, ClassNotFoundException { + ReplicationStream stream = ((ClusterManager)getManager()).getReplicationStream(diff,offset,length); + getDeltaRequest().readExternal(stream); + ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); + try { + ClassLoader[] loaders = getClassLoaders(); + if ( loaders != null && loaders.length >0 ) Thread.currentThread().setContextClassLoader(loaders[0]); + getDeltaRequest().execute(this); + }finally { + Thread.currentThread().setContextClassLoader(contextLoader); + } + } + + /** + * Resets the current diff state and resets the dirty flag + */ + public void resetDiff() { + resetDeltaRequest(); + } + + /** + * Lock during serialization + */ + public void lock() { + diffLock.lock(); + } + + /** + * Unlock after serialization + */ + public void unlock() { + diffLock.unlock(); + } + + public void setOwner(Object owner) { + if ( owner instanceof ClusterManager && getManager()==null) { + ClusterManager cm = (ClusterManager)owner; + this.setManager(cm); + this.setValid(true); + this.setPrimarySession(false); + this.access(); + if (cm.isNotifyListenersOnReplication()) this.setId(getIdInternal()); + this.resetDeltaRequest(); + this.endAccess(); + } + } + // ----------------------------------------------------- Session Properties + + /** + * 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; + } + + /** + * Set the session identifier for this session without notify listeners. + * + * @param id + * The new session identifier + */ + public void setIdInternal(String id) { + super.setId(id); + resetDeltaRequest(); + } + + /** + * Set the session identifier for this session. + * + * @param id + * The new session identifier + */ + public void setId(String id) { + setIdInternal(id); + } + + + + /** + * Return the last client access time without invalidation check + * @see #getLastAccessedTime(). + */ + public long getLastAccessedTimeInternal() { + return (this.lastAccessedTime); + } + + + + public void setMaxInactiveInterval(int interval, boolean addDeltaRequest) { + super.maxInactiveInterval = interval; + if (isValid && interval == 0) { + expire(); + } else { + if (addDeltaRequest && (deltaRequest != null)) + deltaRequest.setMaxInactiveInterval(interval); + } + } + + /** + * Set the isNew 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. + *

+ * After this method executes, and if the object implements + * 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. + *

+ * After this method executes, and if the object implements + * 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. + *

+ * 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 + * readObject(), you must set the associated Manager + * explicitly. + *

+ * 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 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 + * Filip's Tomcat Page
+ * + * 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; iClass Description:
+ * 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 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
  • + *
+ * + */ + +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 constructor
+ * 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 + * Filip's Tomcat Page
+ * + * 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; i + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/ha/tcp/Constants.java b/java/org/apache/catalina/ha/tcp/Constants.java new file mode 100644 index 000000000..7ed1dd583 --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/Constants.java @@ -0,0 +1,32 @@ +/* + * 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.tcp; + +/** + * Manifest constants for the org.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; + +/** + *

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. + *

+ * + * @author Craig R. McClanahan + * @author Filip Hanik + * @author Peter Rossbach + * @version $Revision: 375709 $ $Date: 2006-02-07 15:13:25 -0600 (Tue, 07 Feb 2006) $ + */ + +public class ReplicationValve + extends ValveBase implements ClusterValve { + + private static org.apache.commons.logging.Log log = + org.apache.commons.logging.LogFactory.getLog( ReplicationValve.class ); + + // ----------------------------------------------------- Instance Variables + + /** + * The descriptive information related to this implementation. + */ + private static final String info = + "org.apache.catalina.ha.tcp.ReplicationValve/2.0"; + + + /** + * The StringManager for this package. + */ + protected static StringManager sm = + StringManager.getManager(Constants.Package); + + private CatalinaCluster cluster = null ; + + /** + * holds file endings to not call for like images and others + */ + protected java.util.regex.Pattern[] reqFilters = new java.util.regex.Pattern[0]; + + /** + * Orginal filter + */ + protected String filter ; + + /** + * crossContext session container + */ + protected ThreadLocal crossContextSessions = new ThreadLocal() ; + + /** + * doProcessingStats (default = off) + */ + protected boolean doProcessingStats = false; + + protected long totalRequestTime = 0; + protected long totalSendTime = 0; + protected long nrOfRequests = 0; + protected long lastSendTime = 0; + protected long nrOfFilterRequests = 0; + protected long nrOfSendRequests = 0; + protected long nrOfCrossContextSendRequests = 0; + + /** + * must primary change indicator set + */ + protected boolean primaryIndicator = false ; + + /** + * Name of primary change indicator as request attribute + */ + protected String primaryIndicatorName = "org.apache.catalina.ha.tcp.isPrimarySession"; + + // ------------------------------------------------------------- Properties + + public ReplicationValve() { + } + + /** + * Return descriptive information about this Valve implementation. + */ + public String getInfo() { + + return (info); + + } + + /** + * @return Returns the cluster. + */ + public CatalinaCluster getCluster() { + return cluster; + } + + /** + * @param cluster The cluster to set. + */ + public void setCluster(CatalinaCluster cluster) { + this.cluster = cluster; + } + + /** + * @return Returns the filter + */ + public String getFilter() { + return filter ; + } + + /** + * compile filter string to regular expressions + * @see Pattern#compile(java.lang.String) + * @param filter + * The filter to set. + */ + public void setFilter(String filter) { + if (log.isDebugEnabled()) + log.debug(sm.getString("ReplicationValve.filter.loading", filter)); + this.filter = filter; + StringTokenizer t = new StringTokenizer(filter, ";"); + this.reqFilters = new Pattern[t.countTokens()]; + int i = 0; + while (t.hasMoreTokens()) { + String s = t.nextToken(); + if (log.isTraceEnabled()) + log.trace(sm.getString("ReplicationValve.filter.token", s)); + try { + reqFilters[i++] = Pattern.compile(s); + } catch (Exception x) { + log.error(sm.getString("ReplicationValve.filter.token.failure", + s), x); + } + } + } + + /** + * @return Returns the primaryIndicator. + */ + public boolean isPrimaryIndicator() { + return primaryIndicator; + } + + /** + * @param primaryIndicator The primaryIndicator to set. + */ + public void setPrimaryIndicator(boolean primaryIndicator) { + this.primaryIndicator = primaryIndicator; + } + + /** + * @return Returns the primaryIndicatorName. + */ + public String getPrimaryIndicatorName() { + return primaryIndicatorName; + } + + /** + * @param primaryIndicatorName The primaryIndicatorName to set. + */ + public void setPrimaryIndicatorName(String primaryIndicatorName) { + this.primaryIndicatorName = primaryIndicatorName; + } + + /** + * Calc processing stats + */ + public boolean isDoProcessingStats() { + return doProcessingStats; + } + + /** + * Set Calc processing stats + * @see #resetStatistics() + */ + public void setDoProcessingStats(boolean doProcessingStats) { + this.doProcessingStats = doProcessingStats; + } + + /** + * @return Returns the lastSendTime. + */ + public long getLastSendTime() { + return lastSendTime; + } + + /** + * @return Returns the nrOfRequests. + */ + public long getNrOfRequests() { + return nrOfRequests; + } + + /** + * @return Returns the nrOfFilterRequests. + */ + public long getNrOfFilterRequests() { + return nrOfFilterRequests; + } + + /** + * @return Returns the nrOfCrossContextSendRequests. + */ + public long getNrOfCrossContextSendRequests() { + return nrOfCrossContextSendRequests; + } + + /** + * @return Returns the nrOfSendRequests. + */ + public long getNrOfSendRequests() { + return nrOfSendRequests; + } + + /** + * @return Returns the totalRequestTime. + */ + public long getTotalRequestTime() { + return totalRequestTime; + } + + /** + * @return Returns the totalSendTime. + */ + public long getTotalSendTime() { + return totalSendTime; + } + + /** + * @return Returns the reqFilters. + */ + protected java.util.regex.Pattern[] getReqFilters() { + return reqFilters; + } + + /** + * @param reqFilters The reqFilters to set. + */ + protected void setReqFilters(java.util.regex.Pattern[] reqFilters) { + this.reqFilters = reqFilters; + } + + + // --------------------------------------------------------- Public Methods + + /** + * Register all cross context sessions inside endAccess. + * Use a list with contains check, that the Portlet API can include a lot of fragments from same or + * different applications with session changes. + * + * @param session cross context session + */ + public void registerReplicationSession(DeltaSession session) { + List sessions = (List)crossContextSessions.get(); + if(sessions != null) { + if(!sessions.contains(session)) { + if(log.isDebugEnabled()) + log.debug(sm.getString("ReplicationValve.crossContext.registerSession", + session.getIdInternal(), + session.getManager().getContainer().getName())); + sessions.add(session); + } + } + } + + /** + * Log the interesting request parameters, invoke the next Valve in the + * sequence, and log the interesting response parameters. + * + * @param request The servlet request to be processed + * @param response The servlet response to be created + * + * @exception IOException if an input/output error occurs + * @exception ServletException if a servlet error occurs + */ + public void invoke(Request request, Response response) + throws IOException, ServletException + { + long totalstart = 0; + + //this happens before the request + if(isDoProcessingStats()) { + totalstart = System.currentTimeMillis(); + } + if (primaryIndicator) { + createPrimaryIndicator(request) ; + } + Context context = request.getContext(); + boolean isCrossContext = context != null + && context instanceof StandardContext + && ((StandardContext) context).getCrossContext(); + try { + if(isCrossContext) { + if(log.isDebugEnabled()) + log.debug(sm.getString("ReplicationValve.crossContext.add")); + //FIXME add Pool of Arraylists + crossContextSessions.set(new ArrayList()); + } + getNext().invoke(request, response); + Manager manager = request.getContext().getManager(); + if (manager != null && manager instanceof ClusterManager) { + ClusterManager clusterManager = (ClusterManager) manager; + CatalinaCluster containerCluster = (CatalinaCluster) getContainer().getCluster(); + if (containerCluster == null) { + if (log.isWarnEnabled()) + log.warn(sm.getString("ReplicationValve.nocluster")); + return; + } + // valve cluster can access manager - other cluster handle replication + // at host level - hopefully! + if(containerCluster.getManager(clusterManager.getName()) == null) + return ; + if(containerCluster.hasMembers()) { + sendReplicationMessage(request, totalstart, isCrossContext, clusterManager, containerCluster); + } else { + resetReplicationRequest(request,isCrossContext); + } + } + } finally { + // Array must be remove: Current master request send endAccess at recycle. + // Don't register this request session again! + if(isCrossContext) { + if(log.isDebugEnabled()) + log.debug(sm.getString("ReplicationValve.crossContext.remove")); + // crossContextSessions.remove() only exist at Java 5 + // register ArrayList at a pool + crossContextSessions.set(null); + } + } + } + + + /** + * reset the active statitics + */ + public void resetStatistics() { + totalRequestTime = 0 ; + totalSendTime = 0 ; + lastSendTime = 0 ; + nrOfFilterRequests = 0 ; + nrOfRequests = 0 ; + nrOfSendRequests = 0; + nrOfCrossContextSendRequests = 0; + } + + /** + * Return a String rendering of this object. + */ + public String toString() { + + StringBuffer sb = new StringBuffer("ReplicationValve["); + if (container != null) + sb.append(container.getName()); + sb.append("]"); + return (sb.toString()); + + } + + // --------------------------------------------------------- Protected Methods + + /** + * @param request + * @param totalstart + * @param isCrossContext + * @param clusterManager + * @param containerCluster + */ + protected void sendReplicationMessage(Request request, long totalstart, boolean isCrossContext, ClusterManager clusterManager, CatalinaCluster containerCluster) { + //this happens after the request + long start = 0; + if(isDoProcessingStats()) { + start = System.currentTimeMillis(); + } + try { + // send invalid sessions + // DeltaManager returns String[0] + if (!(clusterManager instanceof DeltaManager)) + sendInvalidSessions(clusterManager, containerCluster); + // send replication + sendSessionReplicationMessage(request, clusterManager, containerCluster); + if(isCrossContext) + sendCrossContextSession(containerCluster); + } catch (Exception x) { + // FIXME we have a lot of sends, but the trouble with one node stops the correct replication to other nodes! + log.error(sm.getString("ReplicationValve.send.failure"), x); + } finally { + // FIXME this stats update are not cheap!! + if(isDoProcessingStats()) { + updateStats(totalstart,start); + } + } + } + + /** + * Send all changed cross context sessions to backups + * @param containerCluster + */ + protected void sendCrossContextSession(CatalinaCluster containerCluster) { + Object sessions = crossContextSessions.get(); + if(sessions != null && sessions instanceof List + && ((List)sessions).size() >0) { + for(Iterator iter = ((List)sessions).iterator(); iter.hasNext() ;) { + Session session = (Session)iter.next(); + if(log.isDebugEnabled()) + log.debug(sm.getString("ReplicationValve.crossContext.sendDelta", + session.getManager().getContainer().getName() )); + sendMessage(session,(ClusterManager)session.getManager(),containerCluster); + if(isDoProcessingStats()) { + nrOfCrossContextSendRequests++; + } + } + } + } + + /** + * Fix memory leak for long sessions with many changes, when no backup member exists! + * @param request current request after responce is generated + * @param isCrossContext check crosscontext threadlocal + */ + protected void resetReplicationRequest(Request request, boolean isCrossContext) { + Session contextSession = request.getSessionInternal(false); + if(contextSession != null & contextSession instanceof DeltaSession){ + resetDeltaRequest(contextSession); + ((DeltaSession)contextSession).setPrimarySession(true); + } + if(isCrossContext) { + Object sessions = crossContextSessions.get(); + if(sessions != null && sessions instanceof List + && ((List)sessions).size() >0) { + Iterator iter = ((List)sessions).iterator(); + for(; iter.hasNext() ;) { + Session session = (Session)iter.next(); + resetDeltaRequest(session); + if(session instanceof DeltaSession) + ((DeltaSession)contextSession).setPrimarySession(true); + + } + } + } + } + + /** + * Reset DeltaRequest from session + * @param session HttpSession from current request or cross context session + */ + protected void resetDeltaRequest(Session session) { + if(log.isDebugEnabled()) { + log.debug(sm.getString("ReplicationValve.resetDeltaRequest" , + session.getManager().getContainer().getName() )); + } + ((DeltaSession)session).resetDeltaRequest(); + } + + /** + * Send Cluster Replication Request + * @param request current request + * @param manager session manager + * @param cluster replication cluster + */ + protected void sendSessionReplicationMessage(Request request, + ClusterManager manager, CatalinaCluster cluster) { + Session session = request.getSessionInternal(false); + if (session != null) { + String uri = request.getDecodedRequestURI(); + // request without session change + if (!isRequestWithoutSessionChange(uri)) { + if (log.isDebugEnabled()) + log.debug(sm.getString("ReplicationValve.invoke.uri", uri)); + sendMessage(session,manager,cluster); + } else + if(isDoProcessingStats()) + nrOfFilterRequests++; + } + + } + + /** + * Send message delta message from request session + * @param request current request + * @param manager session manager + * @param cluster replication cluster + */ + protected void sendMessage(Session session, + ClusterManager manager, CatalinaCluster cluster) { + String id = session.getIdInternal(); + if (id != null) { + send(manager, cluster, id); + } + } + + /** + * send manager requestCompleted message to cluster + * @param manager SessionManager + * @param cluster replication cluster + * @param sessionId sessionid from the manager + * @see DeltaManager#requestCompleted(String) + * @see SimpleTcpCluster#send(ClusterMessage) + */ + protected void send(ClusterManager manager, CatalinaCluster cluster, String sessionId) { + ClusterMessage msg = manager.requestCompleted(sessionId); + if (msg != null) { + if(manager.isSendClusterDomainOnly()) { + cluster.sendClusterDomain(msg); + } else { + cluster.send(msg); + } + if(isDoProcessingStats()) + nrOfSendRequests++; + } + } + + /** + * check for session invalidations + * @param manager + * @param cluster + */ + protected void sendInvalidSessions(ClusterManager manager, CatalinaCluster cluster) { + String[] invalidIds=manager.getInvalidatedSessions(); + if ( invalidIds.length > 0 ) { + for ( int i=0;i 0)) { + Manager manager = request.getContext().getManager(); + Session session = manager.findSession(id); + if (session instanceof ClusterSession) { + ClusterSession cses = (ClusterSession) session; + if (cses != null) { + Boolean isPrimary = new Boolean(cses.isPrimarySession()); + if (log.isDebugEnabled()) + log.debug(sm.getString( + "ReplicationValve.session.indicator", request.getContext().getName(),id, + primaryIndicatorName, isPrimary)); + request.setAttribute(primaryIndicatorName, isPrimary); + } + } else { + if (log.isDebugEnabled()) { + if (session != null) { + log.debug(sm.getString( + "ReplicationValve.session.found", request.getContext().getName(),id)); + } else { + log.debug(sm.getString( + "ReplicationValve.session.invalid", request.getContext().getName(),id)); + } + } + } + } + } + +} diff --git a/java/org/apache/catalina/ha/tcp/SendMessageData.java b/java/org/apache/catalina/ha/tcp/SendMessageData.java new file mode 100644 index 000000000..4fa914f44 --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/SendMessageData.java @@ -0,0 +1,81 @@ +/* + * 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.tcp; + +import org.apache.catalina.tribes.Member; + +/** + * @author Peter Rossbach + * @version $Revision: 303987 $ $Date: 2005-07-08 15:50:30 -0500 (Fri, 08 Jul 2005) $ + */ +public class SendMessageData { + + private Object message ; + private Member destination ; + private Exception exception ; + + + /** + * @param message + * @param destination + * @param exception + */ + public SendMessageData(Object message, Member destination, + Exception exception) { + super(); + this.message = message; + this.destination = destination; + this.exception = exception; + } + + /** + * @return Returns the destination. + */ + public Member getDestination() { + return destination; + } + /** + * @param destination The destination to set. + */ + public void setDestination(Member destination) { + this.destination = destination; + } + /** + * @return Returns the exception. + */ + public Exception getException() { + return exception; + } + /** + * @param exception The exception to set. + */ + public void setException(Exception exception) { + this.exception = exception; + } + /** + * @return Returns the message. + */ + public Object getMessage() { + return message; + } + /** + * @param message The message to set. + */ + public void setMessage(Object message) { + this.message = message; + } +} diff --git a/java/org/apache/catalina/ha/tcp/SimpleTcpCluster.java b/java/org/apache/catalina/ha/tcp/SimpleTcpCluster.java new file mode 100644 index 000000000..3e6a58981 --- /dev/null +++ b/java/org/apache/catalina/ha/tcp/SimpleTcpCluster.java @@ -0,0 +1,932 @@ +/* + * 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.beans.PropertyChangeSupport; +import java.io.IOException; +import java.io.Serializable; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.Engine; +import org.apache.catalina.Host; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Manager; +import org.apache.catalina.Valve; +import org.apache.catalina.ha.CatalinaCluster; +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.ha.ClusterListener; +import org.apache.catalina.ha.ClusterManager; +import org.apache.catalina.ha.ClusterMessage; +import org.apache.catalina.ha.ClusterValve; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.group.GroupChannel; + +import org.apache.catalina.ha.session.DeltaManager; +import org.apache.catalina.ha.util.IDynamicProperty; +import org.apache.catalina.util.LifecycleSupport; +import org.apache.catalina.util.StringManager; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.tomcat.util.IntrospectionUtils; + +/** + * A Cluster implementation using simple multicast. Responsible for + * setting up a cluster and provides callers with a valid multicast + * receiver/sender. + * + * FIXME remove install/remove/start/stop context dummys + * FIXME wrote testcases + * + * @author Filip Hanik + * @author Remy Maucherat + * @author Peter Rossbach + * @version $Revision: 379550 $, $Date: 2006-02-21 12:06:35 -0600 (Tue, 21 Feb 2006) $ + */ +public class SimpleTcpCluster + implements CatalinaCluster, Lifecycle, LifecycleListener, IDynamicProperty, + MembershipListener, ChannelListener{ + + public static Log log = LogFactory.getLog(SimpleTcpCluster.class); + + // ----------------------------------------------------- Instance Variables + + /** + * Descriptive information about this component implementation. + */ + protected static final String info = "SimpleTcpCluster/2.2"; + + public static final String BEFORE_MEMBERREGISTER_EVENT = "before_member_register"; + + public static final String AFTER_MEMBERREGISTER_EVENT = "after_member_register"; + + public static final String BEFORE_MANAGERREGISTER_EVENT = "before_manager_register"; + + public static final String AFTER_MANAGERREGISTER_EVENT = "after_manager_register"; + + public static final String BEFORE_MANAGERUNREGISTER_EVENT = "before_manager_unregister"; + + public static final String AFTER_MANAGERUNREGISTER_EVENT = "after_manager_unregister"; + + public static final String BEFORE_MEMBERUNREGISTER_EVENT = "before_member_unregister"; + + public static final String AFTER_MEMBERUNREGISTER_EVENT = "after_member_unregister"; + + public static final String SEND_MESSAGE_FAILURE_EVENT = "send_message_failure"; + + public static final String RECEIVE_MESSAGE_FAILURE_EVENT = "receive_message_failure"; + + /** + * Group channel. + */ + protected Channel channel = new GroupChannel(); + + + /** + * Name for logging purpose + */ + protected String clusterImpName = "SimpleTcpCluster"; + + /** + * The string manager for this package. + */ + protected StringManager sm = StringManager.getManager(Constants.Package); + + /** + * The cluster name to join + */ + protected String clusterName ; + + /** + * The Container associated with this Cluster. + */ + protected Container container = null; + + /** + * The lifecycle event support for this component. + */ + protected LifecycleSupport lifecycle = new LifecycleSupport(this); + + /** + * Has this component been started? + */ + protected boolean started = false; + + /** + * The property change support for this component. + */ + protected PropertyChangeSupport support = new PropertyChangeSupport(this); + + /** + * The context name <->manager association for distributed contexts. + */ + protected Map managers = new HashMap(); + + private String managerClassName = "org.apache.catalina.ha.session.DeltaManager"; + + + private List valves = new ArrayList(); + + private org.apache.catalina.ha.ClusterDeployer clusterDeployer; + + /** + * Listeners of messages + */ + protected List clusterListeners = new ArrayList(); + + /** + * Comment for 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/ha/util/IDynamicProperty.java b/java/org/apache/catalina/ha/util/IDynamicProperty.java new file mode 100644 index 000000000..b3a2ba23b --- /dev/null +++ b/java/org/apache/catalina/ha/util/IDynamicProperty.java @@ -0,0 +1,57 @@ +/* + * 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.util; + +import java.util.Iterator; + +/** + * @author Peter Rossbach + * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $ + */ + +public interface IDynamicProperty { + + /** + * set config attributes with reflect + * + * @param name + * @param value + */ + public void setProperty(String name, Object value) ; + + /** + * get current config + * + * @param key + * @return The property + */ + public Object getProperty(String key) ; + /** + * Get all properties keys + * + * @return An iterator over the property names + */ + public Iterator getPropertyNames() ; + + /** + * remove a configured property. + * + * @param key + */ + public void removeProperty(String key) ; + +} diff --git a/java/org/apache/catalina/tribes/ByteMessage.java b/java/org/apache/catalina/tribes/ByteMessage.java new file mode 100644 index 000000000..7ce371db4 --- /dev/null +++ b/java/org/apache/catalina/tribes/ByteMessage.java @@ -0,0 +1,101 @@ +/* + * 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; +import java.io.Externalizable; +import java.io.ObjectInput; +import java.io.IOException; +import java.io.ObjectOutput; + +/** + * A byte message is not serialized and deserialized by the channel + * instead it is sent as a byte array
+ * 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: + *

+ *  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]
+ * 
+ * + * 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
+ * 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; iTitle: FaultyMember class

+ * + *

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.
+ * Interceptors are tied together in a linked list. + * @see org.apache.catalina.tribes.group.ChannelInterceptorBase + * @author Filip Hanik + * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $ + */ + +public interface ChannelInterceptor extends MembershipListener, Heartbeat { + + /** + * An interceptor can react to a message based on a set bit on the + * message options.
+ * When a message is sent, the options can be retrieved from ChannelMessage.getOptions() + * and if the bit is set, this interceptor will react to it.
+ * A simple evaluation if an interceptor should react to the message would be:
+ * boolean react = (getOptionFlag() == (getOptionFlag() & ChannelMessage.getOptions()));
+ * The default option is 0, meaning there is no way for the application to trigger the + * interceptor. The interceptor itself will decide.
+ * @return int + * @see ChannelMessage#getOptions() + */ + public int getOptionFlag(); + + /** + * Sets the option flag + * @param flag int + * @see #getOptionFlag() + */ + public void setOptionFlag(int flag); + + /** + * Set the next interceptor in the list of interceptors + * @param next ChannelInterceptor + */ + public void setNext(ChannelInterceptor next) ; + + /** + * Retrieve the next interceptor in the list + * @return ChannelInterceptor - returns the next interceptor in the list or null if no more interceptors exist + */ + public ChannelInterceptor getNext(); + + /** + * Set the previous interceptor in the list + * @param previous ChannelInterceptor + */ + public void setPrevious(ChannelInterceptor previous); + + /** + * Retrieve the previous interceptor in the list + * @return ChannelInterceptor - returns the previous interceptor in the list or null if no more interceptors exist + */ + public ChannelInterceptor getPrevious(); + + /** + * The 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)
+ * Alternatively the interceptor can stop the message from being sent by not invoking + * getNext().sendMessage(destination,msg,payload)
+ * If the message is to be sent asynchronous the application can be notified of completion and + * errors by passing in an error handler attached to a payload object.
+ * The ChannelMessage.getAddress contains Channel.getLocalMember, and can be overwritten + * to simulate a message sent from another node.
+ * @param destination Member[] - the destination for this message + * @param msg ChannelMessage - the message to be sent + * @param payload InterceptorPayload - the payload, carrying an error handler and future useful data, can be null + * @throws ChannelException + * @see ErrorHandler + * @see InterceptorPayload + */ + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException; + + /** + * the 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
+ * Channel.DEFAULT - will start all services
+ * Channel.MBR_RX_SEQ - starts the membership receiver
+ * Channel.MBR_TX_SEQ - starts the membership broadcaster
+ * Channel.SND_TX_SEQ - starts the replication transmitter
+ * Channel.SND_RX_SEQ - starts the replication receiver
+ * @throws ChannelException if a startup error occurs or the service is already started. + * @see Channel + */ + 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
+ * Channel.DEFAULT - will shutdown all services
+ * Channel.MBR_RX_SEQ - stops the membership receiver
+ * Channel.MBR_TX_SEQ - stops the membership broadcaster
+ * Channel.SND_TX_SEQ - stops the replication transmitter
+ * Channel.SND_RX_SEQ - stops the replication receiver
+ * @throws ChannelException if a startup error occurs or the service is already started. + * @see Channel + */ + public void stop(int svc) throws ChannelException; + + public void fireInterceptorEvent(InterceptorEvent event); + + interface InterceptorEvent { + int getEventType(); + String getEventTypeDesc(); + ChannelInterceptor getInterceptor(); + } + + +} diff --git a/java/org/apache/catalina/tribes/ChannelListener.java b/java/org/apache/catalina/tribes/ChannelListener.java new file mode 100644 index 000000000..97819b667 --- /dev/null +++ b/java/org/apache/catalina/tribes/ChannelListener.java @@ -0,0 +1,67 @@ +/* + * 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; + +import java.io.Serializable; +/** + * + *

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);
+ * A ChannelListener implementation MUST NOT return true on 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) calls
+ * + * @author Filip Hanik + * @version 1.0 + */ + +public interface ChannelListener { + + /** + * Receive a message from the channel + * @param msg Serializable + * @param sender - the source of the message + */ + public void messageReceived(Serializable msg, Member sender); + + /** + * Invoked by the channel to determine if the listener will process this message or not. + * @param msg Serializable + * @param sender Member + * @return boolean + */ + public boolean accept(Serializable msg, Member sender); + + /** + * + * @param listener Object + * @return boolean + * @see Object#equals(Object) + */ + public boolean equals(Object listener); + + /** + * + * @return int + * @see Object#hashCode(int) + */ + public int hashCode(); + +} diff --git a/java/org/apache/catalina/tribes/ChannelMessage.java b/java/org/apache/catalina/tribes/ChannelMessage.java new file mode 100644 index 000000000..21a434b08 --- /dev/null +++ b/java/org/apache/catalina/tribes/ChannelMessage.java @@ -0,0 +1,108 @@ +/* + * 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.tribes; + +import java.io.Serializable; +import org.apache.catalina.tribes.io.XByteBuffer; + +/** + * Message that is passed through the interceptor stack after the + * data serialized in the Channel object and then passed down to the + * interceptor and eventually down to the ChannelSender component + * @author Filip Hanik + * + */ +public interface ChannelMessage extends Serializable { + + + + + /** + * Get the address that this message originated from. + * Almost always Channel.getLocalMember(boolean)
+ * This would be set to a different address + * if the message was being relayed from a host other than the one + * that originally sent it. + * @return the source or reply-to address of this message + */ + public Member getAddress(); + + /** + * Sets the source or reply-to address of this message + * @param member Member + */ + public void setAddress(Member member); + + /** + * Timestamp of when the message was created. + * @return long timestamp in milliseconds + */ + public long getTimestamp(); + + /** + * + * Sets the timestamp of this message + * @param timestamp The timestamp + */ + public void setTimestamp(long timestamp); + + /** + * Each message must have a globally unique Id. + * interceptors heavily depend on this id for message processing + * @return byte + */ + public byte[] getUniqueId(); + + /** + * The byte buffer that contains the actual message payload + * @param buf XByteBuffer + */ + public void setMessage(XByteBuffer buf); + + /** + * returns the byte buffer that contains the actual message payload + * @return XByteBuffer + */ + public XByteBuffer getMessage(); + + /** + * The message options is a 32 bit flag set + * that triggers interceptors and message behavior. + * @see Channel#send(Member[], Serializable, int) + * @see ChannelInterceptor#getOptionFlag + * @return int - the option bits set for this message + */ + public int getOptions(); + + /** + * sets the option bits for this message + * @param options int + * @see #getOptions() + */ + public void setOptions(int options); + + /** + * Shallow clone, what gets cloned depends on the implementation + * @return ChannelMessage + */ + public Object clone(); + + /** + * Deep clone, all fields MUST get cloned + * @return ChannelMessage + */ + public Object deepclone(); +} diff --git a/java/org/apache/catalina/tribes/ChannelReceiver.java b/java/org/apache/catalina/tribes/ChannelReceiver.java new file mode 100644 index 000000000..346092ea9 --- /dev/null +++ b/java/org/apache/catalina/tribes/ChannelReceiver.java @@ -0,0 +1,68 @@ +/* + * 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; + + +/** + * ChannelReceiver Interface
+ * The 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 Interface
+ * The ChannelSender interface is the data sender component + * at the bottom layer, the IO layer (for layers see the javadoc for the {@link Channel} interface).
+ * The channel sender must support "silent" members, ie, be able to send a message to a member + * that is not in the membership, but is part of the destination parameter + * @author Filip Hanik + * @version $Revision: 379904 $, $Date: 2006-02-22 15:16:25 -0600 (Wed, 22 Feb 2006) $ + */ +public interface ChannelSender extends Heartbeat +{ + /** + * Notify the sender of a member being added to the group.
+ * Optional. This can be an empty implementation, that does nothing + * @param member Member + */ + public void add(Member member); + /** + * Notification that a member has been removed or crashed. + * Can be used to clean up open connections etc + * @param member Member + */ + public void remove(Member member); + + /** + * Start the channel sender + * @throws IOException if preprocessing takes place and an error happens + */ + public void start() throws java.io.IOException; + /** + * Stop the channel sender + */ + public void stop(); + + /** + * A channel heartbeat, use this method to clean up resources + */ + public void heartbeat() ; + + /** + * Send a message to one or more recipients. + * @param message ChannelMessage - the message to be sent + * @param destination Member[] - the destinations + * @throws ChannelException - if an error happens, the ChannelSender MUST report + * individual send failures on a per member basis, using ChannelException.addFaultyMember + * @see ChannelException#addFaultyMember(Member,java.lang.Exception) + */ + public void sendMessage(ChannelMessage message, Member[] destination) throws ChannelException; +} diff --git a/java/org/apache/catalina/tribes/Constants.java b/java/org/apache/catalina/tribes/Constants.java new file mode 100644 index 000000000..019d1062c --- /dev/null +++ b/java/org/apache/catalina/tribes/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.tribes; + +/** + * Manifest constants for the 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.
+ * A member is identified by the host/ip/uniqueId
+ * The host is what interface the member is listening to, to receive data
+ * The port is what port the member is listening to, to receive data
+ * The uniqueId defines the session id for the member. This is an important feature + * since a member that has crashed and the starts up again on the same port/host is + * not guaranteed to be the same member, so no state transfers will ever be confused + * @author Filip Hanik + * @version $Revision: 303950 $, $Date: 2005-06-09 15:38:30 -0500 (Thu, 09 Jun 2005) $ + */ + + +public interface Member { + + /** + * When a member leaves the cluster, the payload of the memberDisappeared member + * will be the following bytes. This indicates a soft shutdown, and not a crash + */ + public static final byte[] SHUTDOWN_PAYLOAD = new byte[] {66, 65, 66, 89, 45, 65, 76, 69, 88}; + + /** + * Returns the name of this node, should be unique within the group. + */ + public String getName(); + + /** + * Returns the listen host for the ChannelReceiver implementation + * @return IPv4 or IPv6 representation of the host address this member listens to incoming data + * @see ChannelReceiver + */ + public byte[] getHost(); + + /** + * Returns the listen port for the ChannelReceiver implementation + * @return IPv4 or IPv6 representation of the host address this member listens to incoming data + * @see ChannelReceiver + */ + public int getPort(); + + /** + * Contains information on how long this member has been online. + * The result is the number of milli seconds this member has been + * broadcasting its membership to the group. + * @return nr of milliseconds since this member started. + */ + public long getMemberAliveTime(); + + /** + * The current state of the member + * @return boolean - true if the member is functioning correctly + */ + public boolean isReady(); + /** + * The current state of the member + * @return boolean - true if the member is suspect, but the crash has not been confirmed + */ + public boolean isSuspect(); + + /** + * + * @return boolean - true if the member has been confirmed to malfunction + */ + public boolean isFailing(); + + /** + * returns a UUID unique for this member over all sessions. + * If the member crashes and restarts, the uniqueId will be different. + * @return byte[] + */ + public byte[] getUniqueId(); + + /** + * returns the payload associated with this member + * @return byte[] + */ + public byte[] getPayload(); + + /** + * returns the command associated with this member + * @return byte[] + */ + public byte[] getCommand(); + + /** + * Domain for this cluster + * @return byte[] + */ + public byte[] getDomain(); +} diff --git a/java/org/apache/catalina/tribes/MembershipListener.java b/java/org/apache/catalina/tribes/MembershipListener.java new file mode 100644 index 000000000..d21b38669 --- /dev/null +++ b/java/org/apache/catalina/tribes/MembershipListener.java @@ -0,0 +1,44 @@ +/* + * 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 MembershipListener interface is used as a callback to the + * membership service. It has two methods that will notify the listener + * when a member has joined the group and when a member has disappeared (crashed) + * + * @author Filip Hanik + * @version $Revision: 302726 $, $Date: 2004-02-27 08:59:07 -0600 (Fri, 27 Feb 2004) $ + */ + + +public interface MembershipListener { + /** + * A member was added to the group + * @param member Member - the member that was added + */ + public void memberAdded(Member member); + + /** + * A member was removed from the group
+ * If the member left voluntarily, the payload will contain the Member.SHUTDOWN_PAYLOAD data + * @param member Member + * @see Member#SHUTDOWN_PAYLOAD + */ + public void memberDisappeared(Member member); + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/MembershipService.java b/java/org/apache/catalina/tribes/MembershipService.java new file mode 100644 index 000000000..023cb83b6 --- /dev/null +++ b/java/org/apache/catalina/tribes/MembershipService.java @@ -0,0 +1,134 @@ +/* + * 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; + + +/** + * MembershipService Interface
+ * The MembershipService interface is the membership component + * at the bottom layer, the IO layer (for layers see the javadoc for the {@link Channel} interface).
+ * @author Filip Hanik + * @version $Revision: 378093 $, $Date: 2006-02-15 15:13:45 -0600 (Wed, 15 Feb 2006) $ + */ + + +public interface MembershipService { + + public static final int MBR_RX = Channel.MBR_RX_SEQ; + public static final int MBR_TX = Channel.MBR_TX_SEQ; + + /** + * Sets the properties for the membership service. This must be called before + * the 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 + *

+ * @see ChannelException + * @author Filip Hanik + * @version 1.0 + */ +public class RemoteProcessException extends RuntimeException { + public RemoteProcessException() { + super(); + } + + public RemoteProcessException(String message) { + super(message); + } + + public RemoteProcessException(String message, Throwable cause) { + super(message, cause); + } + + public RemoteProcessException(Throwable cause) { + super(cause); + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/UniqueId.java b/java/org/apache/catalina/tribes/UniqueId.java new file mode 100644 index 000000000..3e5852793 --- /dev/null +++ b/java/org/apache/catalina/tribes/UniqueId.java @@ -0,0 +1,71 @@ +/* + * 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 org.apache.catalina.tribes.util.Arrays; +import java.io.Serializable; + +/** + *

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: + * + *

    + *
  1. IP comparison - byte by byte, lower byte higher rank
  2. + *
  3. IPv4 addresses rank higher than IPv6, ie the lesser number of bytes, the higher rank
  4. + *
  5. Port comparison - lower port, higher rank
  6. + *
  7. UniqueId comparison- byte by byte, lower byte higher rank
  8. + *
+ * + *

+ * + * @author Filip Hanik + * @version 1.0 + * @see org.apache.catalina.tribes.Member + */ +public class AbsoluteOrder { + public static final AbsoluteComparator comp = new AbsoluteComparator(); + + protected AbsoluteOrder() { + super(); + } + + + + public static void absoluteOrder(Member[] members) { + if ( members == null || members.length == 0 ) return; + Arrays.sort(members,comp); + } + + + public static class AbsoluteComparator implements Comparator { + public int compare(Object o1, Object o2) { + if ( !((o1 instanceof Member) && (o2 instanceof Member)) ) return 0; + return compareMembers((Member)o1,(Member)o2); + } + + public int compareMembers(Member m1, Member m2) { + int result = compareIps(m1,m2); + if ( result == 0 ) result = comparePorts(m1,m2); + if ( result == 0 ) result = compareIds(m1,m2); + return result; + } + + public int compareIps(Member m1, Member m2) { + return compareBytes(m1.getHost(),m2.getHost()); + } + + public int comparePorts(Member m1, Member m2) { + return compareInts(m1.getPort(),m2.getPort()); + } + + public int compareIds(Member m1, Member m2) { + return compareBytes(m1.getUniqueId(),m2.getUniqueId()); + } + + protected int compareBytes(byte[] d1, byte[] d2) { + int result = 0; + if ( d1.length == d2.length ) { + for (int i=0; (result==0) && (i + * 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
+ * @throws ChannelException if a startup error occurs or the service is already started. + */ + public void start(int svc) throws ChannelException { + this.internalStart(svc); + } + + /** + * 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 started. + */ + public void stop(int svc) throws ChannelException { + this.internalStop(svc); + } + + + /** + * 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
+ * @throws ChannelException if a startup error occurs or the service is already started. + */ + protected synchronized void internalStart(int svc) throws ChannelException { + try { + boolean valid = false; + //make sure we don't pass down any flags that are unrelated to the bottom layer + svc = svc & Channel.DEFAULT; + + if (startLevel == Channel.DEFAULT) return; //we have already started up all components + if (svc == 0 ) return;//nothing to start + + if (svc == (svc & startLevel)) throw new ChannelException("Channel already started for level:"+svc); + + //must start the receiver first so that we can coordinate the port it + //listens to with the local membership settings + if ( Channel.SND_RX_SEQ==(svc & Channel.SND_RX_SEQ) ) { + clusterReceiver.setMessageListener(this); + clusterReceiver.start(); + //synchronize, big time FIXME + membershipService.setLocalMemberProperties(getClusterReceiver().getHost(), getClusterReceiver().getPort()); + valid = true; + } + if ( Channel.SND_TX_SEQ==(svc & Channel.SND_TX_SEQ) ) { + clusterSender.start(); + valid = true; + } + + if ( Channel.MBR_RX_SEQ==(svc & Channel.MBR_RX_SEQ) ) { + membershipService.setMembershipListener(this); + membershipService.start(MembershipService.MBR_RX); + valid = true; + } + if ( Channel.MBR_TX_SEQ==(svc & Channel.MBR_TX_SEQ) ) { + membershipService.start(MembershipService.MBR_TX); + valid = true; + } + + if ( !valid) { + throw new IllegalArgumentException("Invalid start level, valid levels are:SND_RX_SEQ,SND_TX_SEQ,MBR_TX_SEQ,MBR_RX_SEQ"); + } + startLevel = (startLevel | svc); + }catch ( ChannelException cx ) { + throw cx; + }catch ( Exception x ) { + throw new ChannelException(x); + } + } + + /** + * 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 - 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
+ * @throws ChannelException if a startup error occurs or the service is already started. + */ + protected synchronized void internalStop(int svc) throws ChannelException { + try { + //make sure we don't pass down any flags that are unrelated to the bottom layer + svc = svc & Channel.DEFAULT; + + if (startLevel == 0) return; //we have already stopped up all components + if (svc == 0 ) return;//nothing to stop + + boolean valid = false; + if ( Channel.SND_RX_SEQ==(svc & Channel.SND_RX_SEQ) ) { + clusterReceiver.stop(); + clusterReceiver.setMessageListener(null); + valid = true; + } + if ( Channel.SND_TX_SEQ==(svc & Channel.SND_TX_SEQ) ) { + clusterSender.stop(); + valid = true; + } + + if ( Channel.MBR_RX_SEQ==(svc & Channel.MBR_RX_SEQ) ) { + membershipService.stop(MembershipService.MBR_RX); + membershipService.setMembershipListener(null); + valid = true; + + } + if ( Channel.MBR_TX_SEQ==(svc & Channel.MBR_TX_SEQ) ) { + valid = true; + membershipService.stop(MembershipService.MBR_TX); + } + if ( !valid) { + throw new IllegalArgumentException("Invalid start level, valid levels are:SND_RX_SEQ,SND_TX_SEQ,MBR_TX_SEQ,MBR_RX_SEQ"); + } + + startLevel = (startLevel & (~svc)); + + }catch ( Exception x ) { + throw new ChannelException(x); + } finally { + + } + + } + + public void memberAdded(Member member){ + SenderState.getSenderState(member); + if ( clusterSender!=null ) clusterSender.add(member); + super.memberAdded(member); + } + + public void memberDisappeared(Member member){ + SenderState.removeSenderState(member); + if ( clusterSender!=null ) clusterSender.remove(member); + super.memberDisappeared(member); + } + + public void messageReceived(ChannelMessage msg) { + if ( Logs.MESSAGES.isTraceEnabled() ) { + Logs.MESSAGES.trace("ChannelCoordinator - Received msg:" + new UniqueId(msg.getUniqueId()) + " at " +new java.sql.Timestamp(System.currentTimeMillis())+ " from "+msg.getAddress().getName()); + } + super.messageReceived(msg); + } + + + public ChannelReceiver getClusterReceiver() { + return clusterReceiver; + } + + public ChannelSender getClusterSender() { + return clusterSender; + } + + public MembershipService getMembershipService() { + return membershipService; + } + + public void setClusterReceiver(ChannelReceiver clusterReceiver) { + if ( clusterReceiver != null ) { + this.clusterReceiver = clusterReceiver; + this.clusterReceiver.setMessageListener(this); + } else { + if (this.clusterReceiver!=null ) this.clusterReceiver.setMessageListener(null); + this.clusterReceiver = null; + } + } + + public void setClusterSender(ChannelSender clusterSender) { + this.clusterSender = clusterSender; + } + + public void setMembershipService(MembershipService membershipService) { + this.membershipService = membershipService; + this.membershipService.setMembershipListener(this); + } + + public void hearbeat() { + if ( clusterSender!=null ) clusterSender.heartbeat(); + super.heartbeat(); + } + + /** + * has members + */ + public boolean hasMembers() { + return this.getMembershipService().hasMembers(); + } + + /** + * Get all current cluster members + * @return all members or empty array + */ + public Member[] getMembers() { + return this.getMembershipService().getMembers(); + } + + /** + * + * @param mbr Member + * @return Member + */ + public Member getMember(Member mbr){ + return this.getMembershipService().getMember(mbr); + } + + + /** + * Return the member that represents this node. + * + * @return Member + */ + public Member getLocalMember(boolean incAlive) { + return this.getMembershipService().getLocalMember(incAlive); + } + + +} diff --git a/java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java b/java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java new file mode 100644 index 000000000..e67199ccd --- /dev/null +++ b/java/org/apache/catalina/tribes/group/ChannelInterceptorBase.java @@ -0,0 +1,171 @@ +/* + * 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.group; + +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelInterceptor; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; + +/** + * Abstract class for the interceptor base class. + * @author Filip Hanik + * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $ + */ + +public abstract class ChannelInterceptorBase implements ChannelInterceptor { + + protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( + ChannelInterceptorBase.class); + + private ChannelInterceptor next; + private ChannelInterceptor previous; + //default value, always process + protected int optionFlag = 0; + + public ChannelInterceptorBase() { + + } + + public boolean okToProcess(int messageFlags) { + if (this.optionFlag == 0 ) return true; + return ((optionFlag&messageFlags) == optionFlag); + } + + public final void setNext(ChannelInterceptor next) { + this.next = next; + } + + public final ChannelInterceptor getNext() { + return next; + } + + public final void setPrevious(ChannelInterceptor previous) { + this.previous = previous; + } + + public void setOptionFlag(int optionFlag) { + this.optionFlag = optionFlag; + } + + public final ChannelInterceptor getPrevious() { + return previous; + } + + public int getOptionFlag() { + return optionFlag; + } + + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws + ChannelException { + if (getNext() != null) getNext().sendMessage(destination, msg, payload); + } + + public void messageReceived(ChannelMessage msg) { + if (getPrevious() != null) getPrevious().messageReceived(msg); + } + + public boolean accept(ChannelMessage msg) { + return true; + } + + public void memberAdded(Member member) { + //notify upwards + if (getPrevious() != null) getPrevious().memberAdded(member); + } + + public void memberDisappeared(Member member) { + //notify upwards + if (getPrevious() != null) getPrevious().memberDisappeared(member); + } + + public void heartbeat() { + if (getNext() != null) getNext().heartbeat(); + } + + /** + * has members + */ + public boolean hasMembers() { + if ( getNext()!=null )return getNext().hasMembers(); + else return false; + } + + /** + * Get all current cluster members + * @return all members or empty array + */ + public Member[] getMembers() { + if ( getNext()!=null ) return getNext().getMembers(); + else return null; + } + + /** + * + * @param mbr Member + * @return Member + */ + public Member getMember(Member mbr) { + if ( getNext()!=null) return getNext().getMember(mbr); + else return null; + } + + /** + * Return the member that represents this node. + * + * @return Member + */ + public Member getLocalMember(boolean incAlive) { + if ( getNext()!=null ) return getNext().getLocalMember(incAlive); + else return null; + } + + /** + * 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
+ * @throws ChannelException if a startup error occurs or the service is already started. + */ + public void start(int svc) throws ChannelException { + if ( getNext()!=null ) getNext().start(svc); + } + + /** + * 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 started. + */ + public void stop(int svc) throws ChannelException { + if (getNext() != null) getNext().stop(svc); + } + + public void fireInterceptorEvent(InterceptorEvent event) { + //empty operation + } + + +} diff --git a/java/org/apache/catalina/tribes/group/GroupChannel.java b/java/org/apache/catalina/tribes/group/GroupChannel.java new file mode 100644 index 000000000..ff4a08ef3 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/GroupChannel.java @@ -0,0 +1,666 @@ +/* + * 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.group; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Iterator; + +import org.apache.catalina.tribes.ByteMessage; +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelInterceptor; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.ChannelReceiver; +import org.apache.catalina.tribes.ChannelSender; +import org.apache.catalina.tribes.ErrorHandler; +import org.apache.catalina.tribes.ManagedChannel; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.MembershipService; +import org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.UniqueId; +import org.apache.catalina.tribes.Heartbeat; +import org.apache.catalina.tribes.io.BufferPool; +import java.io.IOException; +import org.apache.catalina.tribes.RemoteProcessException; +import org.apache.catalina.tribes.util.Logs; +import org.apache.catalina.tribes.util.Arrays; + +/** + * The default implementation of a Channel.
+ * The GroupChannel manages the replication channel. It coordinates + * message being sent and received with membership announcements. + * The channel has an chain of interceptors that can modify the message or perform other logic.
+ * It manages a complete group, both membership and replication. + * @author Filip Hanik + * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $ + */ +public class GroupChannel extends ChannelInterceptorBase implements ManagedChannel { + /** + * Flag to determine if the channel manages its own heartbeat + * If set to true, the channel will start a local thread for the heart beat. + */ + protected boolean heartbeat = true; + /** + * If 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:
+ * - MembershipService
+ * - ChannelSender
+ * - ChannelReceiver
+ */ + protected ChannelCoordinator coordinator = new ChannelCoordinator(); + + /** + * The first interceptor in the inteceptor stack. + * The interceptors are chained in a linked list, so we only need a reference to the + * first one + */ + protected ChannelInterceptor interceptors = null; + + /** + * A list of membership listeners that subscribe to membership announcements + */ + protected ArrayList membershipListeners = new ArrayList(); + + /** + * A list of channel listeners that subscribe to incoming messages + */ + protected ArrayList channelListeners = new ArrayList(); + + /** + * If set to true, the GroupChannel will check to make sure that + */ + protected boolean optionCheck = false; + + /** + * Creates a GroupChannel. This constructor will also + * add the first interceptor in the GroupChannel.
+ * The first interceptor is always the channel itself. + */ + public GroupChannel() { + addInterceptor(this); + } + + + /** + * Adds an interceptor to the stack for message processing
+ * Interceptors are ordered in the way they are added.
+ * channel.addInterceptor(A);
+ * channel.addInterceptor(C);
+ * channel.addInterceptor(B);
+ * Will result in a interceptor stack like this:
+ * A -> C -> B
+ * The complete stack will look like this:
+ * Channel -> A -> C -> B -> ChannelCoordinator
+ * @param interceptor ChannelInterceptorBase + */ + public void addInterceptor(ChannelInterceptor interceptor) { + if ( interceptors == null ) { + interceptors = interceptor; + interceptors.setNext(coordinator); + interceptors.setPrevious(null); + coordinator.setPrevious(interceptors); + } else { + ChannelInterceptor last = interceptors; + while ( last.getNext() != coordinator ) { + last = last.getNext(); + } + last.setNext(interceptor); + interceptor.setNext(coordinator); + interceptor.setPrevious(last); + coordinator.setPrevious(interceptor); + } + } + + /** + * Sends a heartbeat through the interceptor stack.
+ * Invoke this method from the application on a periodic basis if + * you have turned off internal heartbeats channel.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.
+ * @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) throws ChannelException { + return send(destination,msg,options,null); + } + + /** + * + * @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.
+ * @param handler - callback object for error handling and completion notification, used when a message is + * sent asynchronously using the 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.
+ * When a message is received from a remote node, this method will be invoked by + * the previous interceptor.
+ * This method can also be used to send a message to other components within the same application, + * but its an extreme case, and you're probably better off doing that logic between the applications itself. + * @param msg ChannelMessage + */ + public void messageReceived(ChannelMessage msg) { + if ( msg == null ) return; + try { + if ( Logs.MESSAGES.isTraceEnabled() ) { + Logs.MESSAGES.trace("GroupChannel - Received msg:" + new UniqueId(msg.getUniqueId()) + " at " +new java.sql.Timestamp(System.currentTimeMillis())+ " from "+msg.getAddress().getName()); + } + + Serializable fwd = null; + if ( (msg.getOptions() & SEND_OPTIONS_BYTE_MESSAGE) == SEND_OPTIONS_BYTE_MESSAGE ) { + fwd = new ByteMessage(msg.getMessage().getBytes()); + } else { + fwd = XByteBuffer.deserialize(msg.getMessage().getBytesDirect(),0,msg.getMessage().getLength()); + } + if ( Logs.MESSAGES.isTraceEnabled() ) { + Logs.MESSAGES.trace("GroupChannel - Receive Message:" + new UniqueId(msg.getUniqueId()) + " is " +fwd); + } + + //get the actual member with the correct alive time + Member source = msg.getAddress(); + boolean rx = false; + boolean delivered = false; + for ( int i=0; iNoRpcChannelReply message to a member
+ * This method gets invoked by the channel if a RPC message comes in + * and no channel listener accepts the message. This avoids timeout + * @param msg RpcMessage + * @param destination Member - the destination for the reply + */ + protected void sendNoRpcChannelReply(RpcMessage msg, Member destination) { + try { + //avoid circular loop + if ( msg instanceof RpcMessage.NoRpcChannelReply) return; + RpcMessage.NoRpcChannelReply reply = new RpcMessage.NoRpcChannelReply(msg.rpcId,msg.uuid); + send(new Member[]{destination},reply,Channel.SEND_OPTIONS_ASYNCHRONOUS); + } catch ( Exception x ) { + log.error("Unable to find rpc channel, failed to send NoRpcChannelReply.",x); + } + } + + /** + * memberAdded gets invoked by the interceptor below the channel + * and the channel will broadcast it to the membership listeners + * @param member Member - the new member + */ + public void memberAdded(Member member) { + //notify upwards + for (int i=0; i 0 ) throw new ChannelException("Interceptor option flag conflict: "+conflicts.toString()); + + } + + /** + * Starts the channel + * @param svc int - what service to start + * @throws ChannelException + * @see org.apache.catalina.tribes.Channel#start(int) + */ + public synchronized void start(int svc) throws ChannelException { + setupDefaultStack(); + if (optionCheck) checkOptionFlags(); + super.start(svc); + if ( hbthread == null && heartbeat ) { + hbthread = new HeartbeatThread(this,heartbeatSleeptime); + hbthread.start(); + } + } + + /** + * Stops the channel + * @param svc int + * @throws ChannelException + * @see org.apache.catalina.tribes.Channel#stop(int) + */ + public synchronized void stop(int svc) throws ChannelException { + if (hbthread != null) { + hbthread.stopHeartbeat(); + hbthread = null; + } + super.stop(svc); + } + + /** + * Returns the first interceptor of the stack. Useful for traversal. + * @return ChannelInterceptor + */ + public ChannelInterceptor getFirstInterceptor() { + if (interceptors != null) return interceptors; + else return coordinator; + } + + /** + * Returns the channel receiver component + * @return ChannelReceiver + */ + public ChannelReceiver getChannelReceiver() { + return coordinator.getClusterReceiver(); + } + + /** + * Returns the channel sender component + * @return ChannelSender + */ + public ChannelSender getChannelSender() { + return coordinator.getClusterSender(); + } + + /** + * Returns the membership service component + * @return MembershipService + */ + public MembershipService getMembershipService() { + return coordinator.getMembershipService(); + } + + /** + * Sets the channel receiver component + * @param clusterReceiver ChannelReceiver + */ + public void setChannelReceiver(ChannelReceiver clusterReceiver) { + coordinator.setClusterReceiver(clusterReceiver); + } + + /** + * Sets the channel sender component + * @param clusterSender ChannelSender + */ + public void setChannelSender(ChannelSender clusterSender) { + coordinator.setClusterSender(clusterSender); + } + + /** + * Sets the membership component + * @param membershipService MembershipService + */ + public void setMembershipService(MembershipService membershipService) { + coordinator.setMembershipService(membershipService); + } + + /** + * Adds a membership listener to the channel.
+ * Membership listeners are uniquely identified using the equals(Object) method + * @param membershipListener MembershipListener + */ + public void addMembershipListener(MembershipListener membershipListener) { + if (!this.membershipListeners.contains(membershipListener) ) + this.membershipListeners.add(membershipListener); + } + + /** + * Removes a membership listener from the channel.
+ * Membership listeners are uniquely identified using the equals(Object) method + * @param membershipListener MembershipListener + */ + + public void removeMembershipListener(MembershipListener membershipListener) { + membershipListeners.remove(membershipListener); + } + + /** + * Adds a channel listener to the channel.
+ * Channel listeners are uniquely identified using the equals(Object) method + * @param channelListener ChannelListener + */ + public void addChannelListener(ChannelListener channelListener) { + if (!this.channelListeners.contains(channelListener) ) { + this.channelListeners.add(channelListener); + } else { + throw new IllegalArgumentException("Listener already exists:"+channelListener); + } + } + + /** + * + * Removes a channel listener from the channel.
+ * Channel listeners are uniquely identified using the equals(Object) method + * @param channelListener ChannelListener + */ + public void removeChannelListener(ChannelListener channelListener) { + channelListeners.remove(channelListener); + } + + /** + * Returns an iterator of all the interceptors in this stack + * @return Iterator + */ + public Iterator getInterceptors() { + return new InterceptorIterator(this.getNext(),this.coordinator); + } + + /** + * Enables/disables the option check
+ * Setting this to true, will make the GroupChannel perform a conflict check + * on the interceptors. If two interceptors are using the same option flag + * and throw an error upon start. + * @param optionCheck boolean + */ + public void setOptionCheck(boolean optionCheck) { + this.optionCheck = optionCheck; + } + + /** + * Configure local heartbeat sleep time
+ * Only used when 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

+ * + * @version 1.0 + */ + public static class HeartbeatThread extends Thread { + protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(HeartbeatThread.class); + protected static int counter = 1; + protected static synchronized int inc() { + return counter++; + } + + protected boolean doRun = true; + protected GroupChannel channel; + protected long sleepTime; + public HeartbeatThread(GroupChannel channel, long sleepTime) { + super(); + this.setPriority(MIN_PRIORITY); + setName("GroupChannel-Heartbeat-"+inc()); + setDaemon(true); + this.channel = channel; + this.sleepTime = sleepTime; + } + public void stopHeartbeat() { + doRun = false; + interrupt(); + } + + public void run() { + while (doRun) { + try { + Thread.sleep(sleepTime); + channel.heartbeat(); + } catch ( InterruptedException x ) { + interrupted(); + } catch ( Exception x ) { + log.error("Unable to send heartbeat through Tribes interceptor stack. Will try to sleep again.",x); + }//catch + }//while + }//run + }//HeartbeatThread + + + +} diff --git a/java/org/apache/catalina/tribes/group/InterceptorPayload.java b/java/org/apache/catalina/tribes/group/InterceptorPayload.java new file mode 100644 index 000000000..96902ef0b --- /dev/null +++ b/java/org/apache/catalina/tribes/group/InterceptorPayload.java @@ -0,0 +1,34 @@ +/* + * 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.group; + +import org.apache.catalina.tribes.ErrorHandler; + +/** + * @author Filip Hanik + * @version 1.0 + */ +public class InterceptorPayload { + private ErrorHandler errorHandler; + + public ErrorHandler getErrorHandler() { + return errorHandler; + } + + public void setErrorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/Response.java b/java/org/apache/catalina/tribes/group/Response.java new file mode 100644 index 000000000..1a7f88f2f --- /dev/null +++ b/java/org/apache/catalina/tribes/group/Response.java @@ -0,0 +1,53 @@ +/* + * 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.group; + +import java.io.Serializable; + +import org.apache.catalina.tribes.Member; + +/** + * A response object holds a message from a responding partner. + * @author Filip Hanik + * @version 1.0 + */ +public class Response { + private Member source; + private Serializable message; + public Response() { + } + + public Response(Member source, Serializable message) { + this.source = source; + this.message = message; + } + + public void setSource(Member source) { + this.source = source; + } + + public void setMessage(Serializable message) { + this.message = message; + } + + public Member getSource() { + return source; + } + + public Serializable getMessage() { + return message; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/RpcCallback.java b/java/org/apache/catalina/tribes/group/RpcCallback.java new file mode 100644 index 000000000..4309a2403 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/RpcCallback.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.group; + +import java.io.Serializable; + +import org.apache.catalina.tribes.Member; + +/** + * The RpcCallback interface is an interface for the Tribes channel to request a + * response object to a request that came in. + * @author not attributable + * @version 1.0 + */ +public interface RpcCallback { + + /** + * + * @param msg Serializable + * @return Serializable - null if no reply should be sent + */ + public Serializable replyRequest(Serializable msg, Member sender); + + /** + * If the reply has already been sent to the requesting thread, + * the rpc callback can handle any data that comes in after the fact. + * @param msg Serializable + * @param sender Member + */ + public void leftOver(Serializable msg, Member sender); + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/RpcChannel.java b/java/org/apache/catalina/tribes/group/RpcChannel.java new file mode 100644 index 000000000..e73dcb662 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/RpcChannel.java @@ -0,0 +1,261 @@ +/* + * 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.group; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.util.UUIDGenerator; + +/** + * A channel to handle RPC messaging + * @author Filip Hanik + */ +public class RpcChannel implements ChannelListener{ + protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(RpcChannel.class); + + public static final int FIRST_REPLY = 1; + public static final int MAJORITY_REPLY = 2; + public static final int ALL_REPLY = 3; + public static final int NO_REPLY = 4; + + private Channel channel; + private RpcCallback callback; + private byte[] rpcId; + + private HashMap responseMap = new HashMap(); + + /** + * Create an RPC channel. You can have several RPC channels attached to a group + * all separated out by the uniqueness + * @param rpcId - the unique Id for this RPC group + * @param channel Channel + * @param callback RpcCallback + */ + public RpcChannel(byte[] rpcId, Channel channel, RpcCallback callback) { + this.channel = channel; + this.callback = callback; + this.rpcId = rpcId; + channel.addChannelListener(this); + } + + + /** + * Send a message and wait for the response. + * @param destination Member[] - the destination for the message, and the members you request a reply from + * @param message Serializable - the message you are sending out + * @param options int - FIRST_REPLY, MAJORITY_REPLY or ALL_REPLY + * @param timeout long - timeout in milliseconds, if no reply is received within this time null is returned + * @return Response[] - an array of response objects. + * @throws ChannelException + */ + public Response[] send(Member[] destination, + Serializable message, + int rpcOptions, + int channelOptions, + long timeout) throws ChannelException { + + if ( destination==null || destination.length == 0 ) return new Response[0]; + + //avoid dead lock + channelOptions = channelOptions & ~Channel.SEND_OPTIONS_SYNCHRONIZED_ACK; + + RpcCollectorKey key = new RpcCollectorKey(UUIDGenerator.randomUUID(false)); + RpcCollector collector = new RpcCollector(key,rpcOptions,destination.length,timeout); + try { + synchronized (collector) { + if ( rpcOptions != NO_REPLY ) responseMap.put(key, collector); + RpcMessage rmsg = new RpcMessage(rpcId, key.id, message); + channel.send(destination, rmsg, channelOptions); + if ( rpcOptions != NO_REPLY ) collector.wait(timeout); + } + } catch ( InterruptedException ix ) { + Thread.currentThread().interrupted(); + //throw new ChannelException(ix); + }finally { + responseMap.remove(key); + } + return collector.getResponses(); + } + + public void messageReceived(Serializable msg, Member sender) { + RpcMessage rmsg = (RpcMessage)msg; + RpcCollectorKey key = new RpcCollectorKey(rmsg.uuid); + if ( rmsg.reply ) { + RpcCollector collector = (RpcCollector)responseMap.get(key); + if (collector == null) { + callback.leftOver(rmsg.message, sender); + } else { + synchronized (collector) { + //make sure it hasn't been removed + if ( responseMap.containsKey(key) ) { + if ( (rmsg instanceof RpcMessage.NoRpcChannelReply) ) + collector.destcnt--; + else + collector.addResponse(rmsg.message, sender); + if (collector.isComplete()) collector.notifyAll(); + } else { + if (! (rmsg instanceof RpcMessage.NoRpcChannelReply) ) + callback.leftOver(rmsg.message, sender); + } + }//synchronized + }//end if + } else{ + Serializable reply = callback.replyRequest(rmsg.message,sender); + rmsg.reply = true; + rmsg.message = reply; + try { + channel.send(new Member[] {sender}, rmsg,0); + }catch ( Exception x ) { + log.error("Unable to send back reply in RpcChannel.",x); + } + }//end if + } + + public void breakdown() { + channel.removeChannelListener(this); + } + + public void finalize() { + breakdown(); + } + + public boolean accept(Serializable msg, Member sender) { + if ( msg instanceof RpcMessage ) { + RpcMessage rmsg = (RpcMessage)msg; + return Arrays.equals(rmsg.rpcId,rpcId); + }else return false; + } + + public Channel getChannel() { + return channel; + } + + public RpcCallback getCallback() { + return callback; + } + + public byte[] getRpcId() { + return rpcId; + } + + public void setChannel(Channel channel) { + this.channel = channel; + } + + public void setCallback(RpcCallback callback) { + this.callback = callback; + } + + public void setRpcId(byte[] rpcId) { + this.rpcId = rpcId; + } + + + + /** + * + * Class that holds all response. + * @author not attributable + * @version 1.0 + */ + public static class RpcCollector { + public ArrayList responses = new ArrayList(); + public RpcCollectorKey key; + public int options; + public int destcnt; + public long timeout; + + public RpcCollector(RpcCollectorKey key, int options, int destcnt, long timeout) { + this.key = key; + this.options = options; + this.destcnt = destcnt; + this.timeout = timeout; + } + + public void addResponse(Serializable message, Member sender){ + Response resp = new Response(sender,message); + responses.add(resp); + } + + public boolean isComplete() { + if ( destcnt <= 0 ) return true; + switch (options) { + case ALL_REPLY: + return destcnt == responses.size(); + case MAJORITY_REPLY: + { + float perc = ((float)responses.size()) / ((float)destcnt); + return perc >= 0.50f; + } + case FIRST_REPLY: + return responses.size()>0; + default: + return false; + } + } + + public int hashCode() { + return key.hashCode(); + } + + public boolean equals(Object o) { + if ( o instanceof RpcCollector ) { + RpcCollector r = (RpcCollector)o; + return r.key.equals(this.key); + } else return false; + } + + public Response[] getResponses() { + return (Response[])responses.toArray(new Response[responses.size()]); + } + } + + public static class RpcCollectorKey { + byte[] id; + public RpcCollectorKey(byte[] id) { + this.id = id; + } + + public int hashCode() { + return id[0]+id[1]+id[2]+id[3]; + } + + public boolean equals(Object o) { + if ( o instanceof RpcCollectorKey ) { + RpcCollectorKey r = (RpcCollectorKey)o; + return Arrays.equals(id,r.id); + } else return false; + } + + } + + protected static String bToS(byte[] data) { + StringBuffer buf = new StringBuffer(4*16); + buf.append("{"); + for (int i=0; data!=null && iTitle:

+ * + *

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. + * + *
Configuration Options
+ * OrderInteceptor.expire= - how long do we keep the fragments in memory and wait for the rest to arrivedefault=60,000ms -> 60seconds + * This setting is useful to avoid OutOfMemoryErrors
+ * OrderInteceptor.maxSize= - message size in bytes default=1024*100 (around a tenth of a MB)
+ * @author Filip Hanik + * @version 1.0 + */ +public class FragmentationInterceptor extends ChannelInterceptorBase { + private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( FragmentationInterceptor.class ); + + protected HashMap fragpieces = new HashMap(); + private int maxSize = 1024*100; + private long expire = 1000 * 60; //one minute expiration + protected boolean deepclone = true; + + + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { + int size = msg.getMessage().getLength(); + boolean frag = (size>maxSize) && okToProcess(msg.getOptions()); + if ( frag ) { + frag(destination, msg, payload); + } else { + msg.getMessage().append(frag); + super.sendMessage(destination, msg, payload); + } + } + + public void messageReceived(ChannelMessage msg) { + boolean isFrag = XByteBuffer.toBoolean(msg.getMessage().getBytesDirect(),msg.getMessage().getLength()-1); + msg.getMessage().trim(1); + if ( isFrag ) { + defrag(msg); + } else { + super.messageReceived(msg); + } + } + + + public FragCollection getFragCollection(FragKey key, ChannelMessage msg) { + FragCollection coll = (FragCollection)fragpieces.get(key); + if ( coll == null ) { + synchronized (fragpieces) { + coll = (FragCollection)fragpieces.get(key); + if ( coll == null ) { + coll = new FragCollection(msg); + fragpieces.put(key, coll); + } + } + } + return coll; + } + + public void removeFragCollection(FragKey key) { + fragpieces.remove(key); + } + + public void defrag(ChannelMessage msg ) { + FragKey key = new FragKey(msg.getUniqueId()); + FragCollection coll = getFragCollection(key,msg); + coll.addMessage((ChannelMessage)msg.deepclone()); + + if ( coll.complete() ) { + removeFragCollection(key); + ChannelMessage complete = coll.assemble(); + super.messageReceived(complete); + + } + } + + public void frag(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { + int size = msg.getMessage().getLength(); + + int count = ((size / maxSize )+(size%maxSize==0?0:1)); + ChannelMessage[] messages = new ChannelMessage[count]; + int remaining = size; + for ( int i=0; iexpire; + } + + + + } + + public static class FragKey { + private byte[] uniqueId; + private long received = System.currentTimeMillis(); + public FragKey(byte[] id ) { + this.uniqueId = id; + } + public int hashCode() { + return XByteBuffer.toInt(uniqueId,0); + } + + public boolean equals(Object o ) { + if ( o instanceof FragKey ) { + return Arrays.equals(uniqueId,((FragKey)o).uniqueId); + } else return false; + + } + + public boolean expired(long expire) { + return (System.currentTimeMillis()-received)>expire; + } + + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/interceptors/GzipInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/GzipInterceptor.java new file mode 100644 index 000000000..18b3024b9 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/GzipInterceptor.java @@ -0,0 +1,99 @@ +/* + * 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +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; + + + +/** + * + * + * @author Filip Hanik + * @version 1.0 + */ +public class GzipInterceptor extends ChannelInterceptorBase { + public static final int DEFAULT_BUFFER_SIZE = 2048; + + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { + try { + byte[] data = compress(msg.getMessage().getBytes()); + msg.getMessage().trim(msg.getMessage().getLength()); + msg.getMessage().append(data,0,data.length); + getNext().sendMessage(destination, msg, payload); + } catch ( IOException x ) { + log.error("Unable to compress byte contents"); + throw new ChannelException(x); + } + } + + public void messageReceived(ChannelMessage msg) { + try { + byte[] data = decompress(msg.getMessage().getBytes()); + msg.getMessage().trim(msg.getMessage().getLength()); + msg.getMessage().append(data,0,data.length); + getPrevious().messageReceived(msg); + } catch ( IOException x ) { + log.error("Unable to decompress byte contents",x); + } + } + + public static byte[] compress(byte[] data) throws IOException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + GZIPOutputStream gout = new GZIPOutputStream(bout); + gout.write(data); + gout.flush(); + gout.close(); + return bout.toByteArray(); + } + + /** + * @todo Fix to create an automatically growing buffer. + * @param data byte[] + * @return byte[] + * @throws IOException + */ + public static byte[] decompress(byte[] data) throws IOException { + ByteArrayInputStream bin = new ByteArrayInputStream(data); + GZIPInputStream gin = new GZIPInputStream(bin); + byte[] tmp = new byte[DEFAULT_BUFFER_SIZE]; + int length = gin.read(tmp); + byte[] result = new byte[length]; + System.arraycopy(tmp,0,result,0,length); + return result; + } + + public static void main(String[] arg) throws Exception { + byte[] data = new byte[1024]; + Arrays.fill(data,(byte)1); + byte[] compress = compress(data); + byte[] decompress = decompress(compress); + System.out.println("Debug test"); + + } + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/MessageDispatch15Interceptor.java b/java/org/apache/catalina/tribes/group/interceptors/MessageDispatch15Interceptor.java new file mode 100644 index 000000000..c0198ba95 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/MessageDispatch15Interceptor.java @@ -0,0 +1,111 @@ +/* + * 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.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.group.InterceptorPayload; +import org.apache.catalina.tribes.transport.bio.util.LinkObject; +import java.util.concurrent.TimeUnit; + +/** + * + * Same implementation as the MessageDispatchInterceptor + * except is ues an atomic long for the currentSize calculation + * and uses a thread pool for message sending. + * + * @author Filip Hanik + * @version 1.0 + */ + +public class MessageDispatch15Interceptor extends MessageDispatchInterceptor { + + protected AtomicLong currentSize = new AtomicLong(0); + protected ThreadPoolExecutor executor = null; + protected int maxThreads = 10; + protected int maxSpareThreads = 2; + protected long keepAliveTime = 5000; + protected LinkedBlockingQueue runnablequeue = new LinkedBlockingQueue(); + + public long getCurrentSize() { + return currentSize.get(); + } + + public long addAndGetCurrentSize(long inc) { + return currentSize.addAndGet(inc); + } + + public long setAndGetCurrentSize(long value) { + currentSize.set(value); + return value; + } + + public boolean addToQueue(ChannelMessage msg, Member[] destination, InterceptorPayload payload) { + final LinkObject obj = new LinkObject(msg,destination,payload); + Runnable r = new Runnable() { + public void run() { + sendAsyncData(obj); + } + }; + executor.execute(r); + return true; + } + + public LinkObject removeFromQueue() { + return null; //not used, thread pool contains its own queue. + } + + public void startQueue() { + if ( run ) return; + executor = new ThreadPoolExecutor(maxSpareThreads,maxThreads,keepAliveTime,TimeUnit.MILLISECONDS,runnablequeue); + run = true; + } + + public void stopQueue() { + run = false; + executor.shutdownNow(); + setAndGetCurrentSize(0); + runnablequeue.clear(); + } + + public long getKeepAliveTime() { + return keepAliveTime; + } + + public int getMaxSpareThreads() { + return maxSpareThreads; + } + + public int getMaxThreads() { + return maxThreads; + } + + public void setKeepAliveTime(long keepAliveTime) { + this.keepAliveTime = keepAliveTime; + } + + public void setMaxSpareThreads(int maxSpareThreads) { + this.maxSpareThreads = maxSpareThreads; + } + + public void setMaxThreads(int maxThreads) { + this.maxThreads = maxThreads; + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptor.java new file mode 100644 index 000000000..a9eb91715 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/MessageDispatchInterceptor.java @@ -0,0 +1,201 @@ +/* + * 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.Channel; +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.transport.bio.util.FastQueue; +import org.apache.catalina.tribes.transport.bio.util.LinkObject; +import org.apache.catalina.tribes.UniqueId; + +/** + * + * The message dispatcher is a way to enable asynchronous communication + * through a channel. The dispatcher will look for the 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 :) + *

+ *

State Diagrams

+ * Initiate an election

+ * Receive an election message

+ * + * @author Filip Hanik + * @version 1.0 + * + * + * + */ +public class NonBlockingCoordinator extends ChannelInterceptorBase { + + /** + * header for a coordination message + */ + protected static final byte[] COORD_HEADER = new byte[] {-86, 38, -34, -29, -98, 90, 65, 63, -81, -122, -6, -110, 99, -54, 13, 63}; + /** + * Coordination request + */ + protected static final byte[] COORD_REQUEST = new byte[] {104, -95, -92, -42, 114, -36, 71, -19, -79, 20, 122, 101, -1, -48, -49, 30}; + /** + * Coordination confirmation, for blocking installations + */ + protected static final byte[] COORD_CONF = new byte[] {67, 88, 107, -86, 69, 23, 76, -70, -91, -23, -87, -25, -125, 86, 75, 20}; + + /** + * Alive message + */ + protected static final byte[] COORD_ALIVE = new byte[] {79, -121, -25, -15, -59, 5, 64, 94, -77, 113, -119, -88, 52, 114, -56, -46, + -18, 102, 10, 34, -127, -9, 71, 115, -70, 72, -101, 88, 72, -124, 127, 111, + 74, 76, -116, 50, 111, 103, 65, 3, -77, 51, -35, 0, 119, 117, 9, -26, + 119, 50, -75, -105, -102, 36, 79, 37, -68, -84, -123, 15, -22, -109, 106, -55}; + /** + * Time to wait for coordination timeout + */ + protected long waitForCoordMsgTimeout = 15000; + /** + * Our current view + */ + protected Membership view = null; + /** + * Out current viewId + */ + protected UniqueId viewId; + + /** + * Our nonblocking membership + */ + protected Membership membership = null; + + /** + * indicates that we are running an election + * and this is the one we are running + */ + protected UniqueId suggestedviewId; + protected Membership suggestedView; + + protected boolean started = false; + protected final int startsvc = 0xFFFF; + + protected Object electionMutex = new Object(); + + protected AtomicBoolean coordMsgReceived = new AtomicBoolean(false); + + public NonBlockingCoordinator() { + super(); + } + +//============================================================================================================ +// COORDINATION HANDLING +//============================================================================================================ + + public void startElection(boolean force) throws ChannelException { + synchronized (electionMutex) { + MemberImpl local = (MemberImpl)getLocalMember(false); + MemberImpl[] others = (MemberImpl[])membership.getMembers(); + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_START_ELECT,this,"Election initated")); + if ( others.length == 0 ) { + this.viewId = new UniqueId(UUIDGenerator.randomUUID(false)); + this.view = new Membership(local,AbsoluteOrder.comp, true); + this.handleViewConf(this.createElectionMsg(local,others,local),local,view); + return; //the only member, no need for an election + } + if ( suggestedviewId != null ) { + + if ( view != null && Arrays.diff(view,suggestedView,local).length == 0 && Arrays.diff(suggestedView,view,local).length == 0) { + suggestedviewId = null; + suggestedView = null; + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_ELECT_ABANDONED,this,"Election abandoned, running election matches view")); + } else { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_ELECT_ABANDONED,this,"Election abandoned, election running")); + } + return; //election already running, I'm not allowed to have two of them + } + if ( view != null && Arrays.diff(view,membership,local).length == 0 && Arrays.diff(membership,view,local).length == 0) { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_ELECT_ABANDONED,this,"Election abandoned, view matches membership")); + return; //already have this view installed + } + int prio = AbsoluteOrder.comp.compare(local,others[0]); + MemberImpl leader = ( prio < 0 )?local:others[0];//am I the leader in my view? + if ( local.equals(leader) || force ) { + CoordinationMessage msg = createElectionMsg(local, others, leader); + suggestedviewId = msg.getId(); + suggestedView = new Membership(local,AbsoluteOrder.comp,true); + Arrays.fill(suggestedView,msg.getMembers()); + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_PROCESS_ELECT,this,"Election, sending request")); + sendElectionMsg(local,others[0],msg); + } else { + try { + coordMsgReceived.set(false); + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_WAIT_FOR_MSG,this,"Election, waiting for request")); + electionMutex.wait(waitForCoordMsgTimeout); + }catch ( InterruptedException x ) { + Thread.currentThread().interrupted(); + } + if ( suggestedviewId == null && (!coordMsgReceived.get())) { + //no message arrived, send the coord msg +// fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_WAIT_FOR_MSG,this,"Election, waiting timed out.")); +// startElection(true); + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_ELECT_ABANDONED,this,"Election abandoned, waiting timed out.")); + } else { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_ELECT_ABANDONED,this,"Election abandoned, received a message")); + } + }//end if + + } + } + + private CoordinationMessage createElectionMsg(MemberImpl local, MemberImpl[] others, MemberImpl leader) { + Membership m = new Membership(local,AbsoluteOrder.comp,true); + Arrays.fill(m,others); + MemberImpl[] mbrs = m.getMembers(); + m.reset(); + CoordinationMessage msg = new CoordinationMessage(leader, local, mbrs,new UniqueId(UUIDGenerator.randomUUID(true)), this.COORD_REQUEST); + return msg; + } + + protected void sendElectionMsg(MemberImpl local, MemberImpl next, CoordinationMessage msg) throws ChannelException { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_SEND_MSG,this,"Sending election message to("+next.getName()+")")); + super.sendMessage(new Member[] {next}, createData(msg, local), null); + } + + protected void sendElectionMsgToNextInline(MemberImpl local, CoordinationMessage msg) throws ChannelException { + int next = Arrays.nextIndex(local,msg.getMembers()); + int current = next; + msg.leader = msg.getMembers()[0]; + boolean sent = false; + while ( !sent && current >= 0 ) { + try { + sendElectionMsg(local, (MemberImpl) msg.getMembers()[current], msg); + sent = true; + }catch ( ChannelException x ) { + log.warn("Unable to send election message to:"+msg.getMembers()[current]); + current = Arrays.nextIndex(msg.getMembers()[current],msg.getMembers()); + if ( current == next ) throw x; + } + } + } + + public Member getNextInLine(MemberImpl local, MemberImpl[] others) { + MemberImpl result = null; + for ( int i=0; i 0); + + } + + + /** + * Returns coordinator if one is available + * @return Member + */ + public Member getCoordinator() { + return (view != null && view.hasMembers()) ? view.getMembers()[0] : null; + } + + public Member[] getView() { + return (view != null && view.hasMembers()) ? view.getMembers() : new Member[0]; + } + + public UniqueId getViewId() { + return viewId; + } + + /** + * Block in/out messages while a election is going on + */ + protected void halt() { + + } + + /** + * Release lock for in/out messages election is completed + */ + protected void release() { + + } + + /** + * Wait for an election to end + */ + protected void waitForRelease() { + + } + + +//============================================================================================================ +// OVERRIDDEN METHODS FROM CHANNEL INTERCEPTOR BASE +//============================================================================================================ + public void start(int svc) throws ChannelException { + if (membership == null) setupMembership(); + if (started)return; + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_START, this, "Before start")); + super.start(startsvc); + started = true; + if (view == null) view = new Membership( (MemberImpl)super.getLocalMember(true), AbsoluteOrder.comp, true); + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_START, this, "After start")); + startElection(false); + } + + public void stop(int svc) throws ChannelException { + try { + halt(); + synchronized (electionMutex) { + if (!started)return; + started = false; + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_STOP, this, "Before stop")); + super.stop(startsvc); + this.view = null; + this.viewId = null; + this.suggestedView = null; + this.suggestedviewId = null; + this.membership.reset(); + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_STOP, this, "After stop")); + } + }finally { + release(); + } + } + + + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { + waitForRelease(); + super.sendMessage(destination, msg, payload); + } + + public void messageReceived(ChannelMessage msg) { + if ( Arrays.contains(msg.getMessage().getBytesDirect(),0,COORD_ALIVE,0,COORD_ALIVE.length) ) { + //ignore message, its an alive message + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_MSG_ARRIVE,this,"Alive Message")); + + } else if ( Arrays.contains(msg.getMessage().getBytesDirect(),0,COORD_HEADER,0,COORD_HEADER.length) ) { + try { + CoordinationMessage cmsg = new CoordinationMessage(msg.getMessage()); + Member[] cmbr = cmsg.getMembers(); + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_MSG_ARRIVE,this,"Coord Msg Arrived("+Arrays.toNameString(cmbr)+")")); + processCoordMessage(cmsg, msg.getAddress()); + }catch ( ChannelException x ) { + log.error("Error processing coordination message. Could be fatal.",x); + } + } else { + super.messageReceived(msg); + } + } + + public boolean accept(ChannelMessage msg) { + return super.accept(msg); + } + + public void memberAdded(Member member) { + memberAdded(member,true); + } + + public void memberAdded(Member member,boolean elect) { + try { + if ( membership == null ) setupMembership(); + if ( membership.memberAlive((MemberImpl)member) ) super.memberAdded(member); + try { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_MBR_ADD,this,"Member add("+member.getName()+")")); + if (started && elect) startElection(false); + }catch ( ChannelException x ) { + log.error("Unable to start election when member was added.",x); + } + }finally { + } + + } + + public void memberDisappeared(Member member) { + try { + + membership.removeMember((MemberImpl)member); + super.memberDisappeared(member); + try { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_MBR_DEL,this,"Member remove("+member.getName()+")")); + if ( started && (isCoordinator() || isHighest()) ) + startElection(true); //to do, if a member disappears, only the coordinator can start + }catch ( ChannelException x ) { + log.error("Unable to start election when member was removed.",x); + } + }finally { + } + } + + public boolean isHighest() { + Member local = getLocalMember(false); + if ( membership.getMembers().length == 0 ) return true; + else return AbsoluteOrder.comp.compare(local,membership.getMembers()[0])<=0; + } + + public boolean isCoordinator() { + Member coord = getCoordinator(); + return coord != null && getLocalMember(false).equals(coord); + } + + public void heartbeat() { + try { + MemberImpl local = (MemberImpl)getLocalMember(false); + if ( view != null && (Arrays.diff(view,membership,local).length != 0 || Arrays.diff(membership,view,local).length != 0) ) { + if ( isHighest() ) { + fireInterceptorEvent(new CoordinationEvent(CoordinationEvent.EVT_START_ELECT, this, + "Heartbeat found inconsistency, restart election")); + startElection(true); + } + } + } catch ( Exception x ){ + log.error("Unable to perform heartbeat.",x); + } finally { + super.heartbeat(); + } + } + + /** + * has members + */ + public boolean hasMembers() { + + return membership.hasMembers(); + } + + /** + * Get all current cluster members + * @return all members or empty array + */ + public Member[] getMembers() { + + return membership.getMembers(); + } + + /** + * + * @param mbr Member + * @return Member + */ + public Member getMember(Member mbr) { + + return membership.getMember(mbr); + } + + /** + * Return the member that represents this node. + * + * @return Member + */ + public Member getLocalMember(boolean incAlive) { + Member local = super.getLocalMember(incAlive); + if ( view == null && (local != null)) setupMembership(); + return local; + } + + protected synchronized void setupMembership() { + if ( membership == null ) { + membership = new Membership((MemberImpl)super.getLocalMember(true),AbsoluteOrder.comp,false); + } + } + + +//============================================================================================================ +// HELPER CLASSES FOR COORDINATION +//============================================================================================================ + + + + + public static class CoordinationMessage { + //X{A-ldr, A-src, mbrs-A,B,C,D} + protected XByteBuffer buf; + protected MemberImpl leader; + protected MemberImpl source; + protected MemberImpl[] view; + protected UniqueId id; + protected byte[] type; + protected long timestamp = System.currentTimeMillis(); + + public CoordinationMessage(XByteBuffer buf) { + this.buf = buf; + parse(); + } + + public CoordinationMessage(MemberImpl leader, + MemberImpl source, + MemberImpl[] view, + UniqueId id, + byte[] type) { + this.buf = new XByteBuffer(4096,false); + this.leader = leader; + this.source = source; + this.view = view; + this.id = id; + this.type = type; + this.write(); + } + + + public byte[] getHeader() { + return NonBlockingCoordinator.COORD_HEADER; + } + + public MemberImpl getLeader() { + if ( leader == null ) parse(); + return leader; + } + + public MemberImpl getSource() { + if ( source == null ) parse(); + return source; + } + + public UniqueId getId() { + if ( id == null ) parse(); + return id; + } + + public MemberImpl[] getMembers() { + if ( view == null ) parse(); + return view; + } + + public byte[] getType() { + if (type == null ) parse(); + return type; + } + + public XByteBuffer getBuffer() { + return this.buf; + } + + public void parse() { + //header + int offset = 16; + //leader + int ldrLen = buf.toInt(buf.getBytesDirect(),offset); + offset += 4; + byte[] ldr = new byte[ldrLen]; + System.arraycopy(buf.getBytesDirect(),offset,ldr,0,ldrLen); + leader = MemberImpl.getMember(ldr); + offset += ldrLen; + //source + int srcLen = buf.toInt(buf.getBytesDirect(),offset); + offset += 4; + byte[] src = new byte[srcLen]; + System.arraycopy(buf.getBytesDirect(),offset,src,0,srcLen); + source = MemberImpl.getMember(src); + offset += srcLen; + //view + int mbrCount = buf.toInt(buf.getBytesDirect(),offset); + offset += 4; + view = new MemberImpl[mbrCount]; + for (int i=0; i + * There is no point in + * using this with the replicationMode="fastasynchqueue" as this mode guarantees ordering.
+ * If you are using the mode ack=false replicationMode=pooled, and have a lot of concurrent threads, + * this interceptor can really slow you down, as many messages will be completely out of order + * and the queue might become rather large. If this is the case, then you might want to set + * the value OrderInterceptor.maxQueue = 25 (meaning that we will never keep more than 25 messages in our queue) + *
Configuration Options
+ * OrderInteceptor.expire= - if a message arrives out of order, how long before we act on it default=3000ms
+ * OrderInteceptor.maxQueue= - how much can the queue grow to ensure ordering. + * This setting is useful to avoid OutOfMemoryErrorsdefault=Integer.MAX_VALUE
+ * OrderInterceptor.forwardExpired= - this flag tells the interceptor what to + * do when a message has expired or the queue has grown larger than the maxQueue value. + * true means that the message is sent up the stack to the receiver that will receive and out of order message + * false means, forget the message and reset the message counter. default=true + * + * + * @author Filip Hanik + * @version 1.0 + */ +public class OrderInterceptor extends ChannelInterceptorBase { + private HashMap outcounter = new HashMap(); + private HashMap incounter = new HashMap(); + private HashMap incoming = new HashMap(); + private long expire = 3000; + private boolean forwardExpired = true; + private int maxQueue = Integer.MAX_VALUE; + + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { + for ( int i=0; i cnt.getCounter() ) cnt.setCounter(order.getMsgNr()); + super.messageReceived(order.getMessage()); + order.setMessage(null); + order = order.next; + } + MessageOrder head = order; + MessageOrder prev = null; + tmp = order; + //flag to empty out the queue when it larger than maxQueue + boolean empty = order!=null?order.getCount()>=maxQueue:false; + while ( tmp != null ) { + //process expired messages or empty out the queue + if ( tmp.isExpired(expire) || empty ) { + //reset the head + if ( tmp == head ) head = tmp.next; + cnt.setCounter(tmp.getMsgNr()+1); + if ( getForwardExpired() ) super.messageReceived(tmp.getMessage()); + tmp.setMessage(null); + tmp = tmp.next; + if ( prev != null ) prev.next = tmp; + result = true; + } else { + prev = tmp; + tmp = tmp.next; + } + } + if ( head == null ) incoming.remove(member); + else incoming.put(member, head); + return result; + } + + public void memberAdded(Member member) { + //notify upwards + getInCounter(member); + getOutCounter(member); + super.memberAdded(member); + } + + public void memberDisappeared(Member member) { + //notify upwards + outcounter.remove(member); + incounter.remove(member); + //clear the remaining queue + processLeftOvers(member,true); + super.memberDisappeared(member); + } + + public int incCounter(Member mbr) { + Counter cnt = getOutCounter(mbr); + return cnt.inc(); + } + + public synchronized Counter getInCounter(Member mbr) { + Counter cnt = (Counter)incounter.get(mbr); + if ( cnt == null ) { + cnt = new Counter(); + cnt.inc(); //always start at 1 for incoming + incounter.put(mbr,cnt); + } + return cnt; + } + + public synchronized Counter getOutCounter(Member mbr) { + Counter cnt = (Counter)outcounter.get(mbr); + if ( cnt == null ) { + cnt = new Counter(); + outcounter.put(mbr,cnt); + } + return cnt; + } + + public static class Counter { + private int value = 0; + + public int getCounter() { + return value; + } + + public synchronized void setCounter(int counter) { + this.value = counter; + } + + public synchronized int inc() { + return ++value; + } + } + + public static class MessageOrder { + private long received = System.currentTimeMillis(); + private MessageOrder next; + private int msgNr; + private ChannelMessage msg = null; + public MessageOrder(int msgNr,ChannelMessage msg) { + this.msgNr = msgNr; + this.msg = msg; + } + + public boolean isExpired(long expireTime) { + return (System.currentTimeMillis()-received) > expireTime; + } + + public ChannelMessage getMessage() { + return msg; + } + + public void setMessage(ChannelMessage msg) { + this.msg = msg; + } + + public void setNext(MessageOrder order) { + this.next = order; + } + public MessageOrder getNext() { + return next; + } + + public int getCount() { + int counter = 1; + MessageOrder tmp = next; + while ( tmp != null ) { + counter++; + tmp = tmp.next; + } + return counter; + } + + public static MessageOrder add(MessageOrder head, MessageOrder add) { + if ( head == null ) return add; + if ( add == null ) return head; + if ( head == add ) return add; + + if ( head.getMsgNr() > add.getMsgNr() ) { + add.next = head; + return add; + } + + MessageOrder iter = head; + MessageOrder prev = null; + while ( iter.getMsgNr() < add.getMsgNr() && (iter.next !=null ) ) { + prev = iter; + iter = iter.next; + } + if ( iter.getMsgNr() < add.getMsgNr() ) { + //add after + add.next = iter.next; + iter.next = add; + } else if (iter.getMsgNr() > add.getMsgNr()) { + //add before + prev.next = add; + add.next = iter; + + } else { + throw new ArithmeticException("Message added has the same counter, synchronization bug. Disable the order interceptor"); + } + + return head; + } + + public int getMsgNr() { + return msgNr; + } + + + + } + + public void setExpire(long expire) { + this.expire = expire; + } + + public void setForwardExpired(boolean forwardExpired) { + this.forwardExpired = forwardExpired; + } + + public void setMaxQueue(int maxQueue) { + this.maxQueue = maxQueue; + } + + public long getExpire() { + return expire; + } + + public boolean getForwardExpired() { + return forwardExpired; + } + + public int getMaxQueue() { + return maxQueue; + } + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java new file mode 100644 index 000000000..6ee873621 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/StaticMembershipInterceptor.java @@ -0,0 +1,101 @@ +package org.apache.catalina.tribes.group.interceptors; + +import org.apache.catalina.tribes.group.ChannelInterceptorBase; +import org.apache.catalina.tribes.Member; +import java.util.ArrayList; +import org.apache.catalina.tribes.group.AbsoluteOrder; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.Channel; + +public class StaticMembershipInterceptor + extends ChannelInterceptorBase { + protected ArrayList members = new ArrayList(); + protected Member localMember = null; + + public StaticMembershipInterceptor() { + super(); + } + + public void addStaticMember(Member member) { + synchronized (members) { + if (!members.contains(member)) members.add(member); + } + } + + public void removeStaticMember(Member member) { + synchronized (members) { + if (members.contains(member)) members.remove(member); + } + } + + public void setLocalMember(Member member) { + this.localMember = member; + } + + /** + * has members + */ + public boolean hasMembers() { + return super.hasMembers() || (members.size()>0); + } + + /** + * Get all current cluster members + * @return all members or empty array + */ + public Member[] getMembers() { + if ( members.size() == 0 ) return super.getMembers(); + else { + synchronized (members) { + Member[] others = super.getMembers(); + Member[] result = new Member[members.size() + others.length]; + for (int i = 0; i < others.length; i++) result[i] = others[i]; + for (int i = 0; i < members.size(); i++) result[i + others.length] = (Member) members.get(i); + AbsoluteOrder.absoluteOrder(result); + return result; + }//sync + }//end if + } + + /** + * + * @param mbr Member + * @return Member + */ + public Member getMember(Member mbr) { + if ( members.contains(mbr) ) return (Member)members.get(members.indexOf(mbr)); + else return super.getMember(mbr); + } + + /** + * Return the member that represents this node. + * + * @return Member + */ + public Member getLocalMember(boolean incAlive) { + if (this.localMember != null ) return localMember; + else return super.getLocalMember(incAlive); + } + + /** + * Send notifications upwards + * @param svc int + * @throws ChannelException + */ + public void start(int svc) throws ChannelException { + if ( (Channel.SND_RX_SEQ&svc)==Channel.SND_RX_SEQ ) super.start(Channel.SND_RX_SEQ); + if ( (Channel.SND_TX_SEQ&svc)==Channel.SND_TX_SEQ ) super.start(Channel.SND_TX_SEQ); + final Member[] mbrs = (Member[])members.toArray(new Member[members.size()]); + final ChannelInterceptorBase base = this; + Thread t = new Thread() { + public void run() { + for (int i=0; iTitle: A perfect failure detector

+ * + *

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 + *

+ * + * @author Filip Hanik + * @version 1.0 + */ +public class TcpFailureDetector extends ChannelInterceptorBase { + + private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog( TcpFailureDetector.class ); + + protected static byte[] TCP_FAIL_DETECT = new byte[] { + 79, -89, 115, 72, 121, -126, 67, -55, -97, 111, -119, -128, -95, 91, 7, 20, + 125, -39, 82, 91, -21, -15, 67, -102, -73, 126, -66, -113, -127, 103, 30, -74, + 55, 21, -66, -121, 69, 126, 76, -88, -65, 10, 77, 19, 83, 56, 21, 50, + 85, -10, -108, -73, 58, -6, 64, 120, -111, 4, 125, -41, 114, -124, -64, -43}; + + protected boolean performConnectTest = true; + + protected long connectTimeout = 1000;//1 second default + + protected boolean performSendTest = true; + + protected boolean performReadTest = false; + + protected long readTestTimeout = 5000;//5 seconds + + protected Membership membership = null; + + protected HashMap removeSuspects = new HashMap(); + + protected HashMap addSuspects = new HashMap(); + + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { + try { + super.sendMessage(destination, msg, payload); + }catch ( ChannelException cx ) { + FaultyMember[] mbrs = cx.getFaultyMembers(); + for ( int i=0; i 0; + } + }//end if + return true; + } catch ( SocketTimeoutException sx) { + //do nothing, we couldn't connect + } catch ( ConnectException cx) { + //do nothing, we couldn't connect + }catch (Exception x ) { + log.error("Unable to perform failure detection check, assuming member down.",x); + } finally { + try {socket.close(); } catch ( Exception ignore ){} + } + return false; + } + + + + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptor.java new file mode 100644 index 000000000..972a7c569 --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/ThroughputInterceptor.java @@ -0,0 +1,119 @@ +/* + * 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.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.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import java.text.DecimalFormat; +import org.apache.catalina.tribes.membership.MemberImpl; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + + + +/** + * + * + * @author Filip Hanik + * @version 1.0 + */ +public class ThroughputInterceptor extends ChannelInterceptorBase { + protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ThroughputInterceptor.class); + + double mbTx = 0; + double mbAppTx = 0; + double mbRx = 0; + double timeTx = 0; + double lastCnt = 0; + AtomicLong msgTxCnt = new AtomicLong(1); + AtomicLong msgRxCnt = new AtomicLong(0); + AtomicLong msgTxErr = new AtomicLong(0); + int interval = 10000; + AtomicInteger access = new AtomicInteger(0); + long txStart = 0; + long rxStart = 0; + DecimalFormat df = new DecimalFormat("#0.00"); + + + public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException { + if ( access.addAndGet(1) == 1 ) txStart = System.currentTimeMillis(); + long bytes = XByteBuffer.getDataPackageLength(((ChannelData)msg).getDataPackageLength()); + try { + super.sendMessage(destination, msg, payload); + }catch ( ChannelException x ) { + msgTxErr.addAndGet(1); + access.addAndGet(-1); + throw x; + } + mbTx += ((double)(bytes*destination.length))/(1024d*1024d); + mbAppTx += ((double)(bytes))/(1024d*1024d); + if ( access.addAndGet(-1) == 0 ) { + long stop = System.currentTimeMillis(); + timeTx += ( (double) (stop - txStart)) / 1000d; + if ((msgTxCnt.get() / interval) >= lastCnt) { + lastCnt++; + report(timeTx); + } + } + msgTxCnt.addAndGet(1); + } + + public void messageReceived(ChannelMessage msg) { + if ( rxStart == 0 ) rxStart = System.currentTimeMillis(); + long bytes = XByteBuffer.getDataPackageLength(((ChannelData)msg).getDataPackageLength()); + mbRx += ((double)bytes)/(1024d*1024d); + msgRxCnt.addAndGet(1); + if ( msgRxCnt.get() % interval == 0 ) report(timeTx); + super.messageReceived(msg); + + } + + public void report(double timeTx) { + StringBuffer buf = new StringBuffer("ThroughputInterceptor Report[\n\tTx Msg:"); + buf.append(msgTxCnt).append(" messages\n\tSent:"); + buf.append(df.format(mbTx)); + buf.append(" MB (total)\n\tSent:"); + buf.append(df.format(mbAppTx)); + buf.append(" MB (application)\n\tTime:"); + buf.append(df.format(timeTx)); + buf.append(" seconds\n\tTx Speed:"); + buf.append(df.format(mbTx/timeTx)); + buf.append(" MB/sec (total)\n\tTxSpeed:"); + buf.append(df.format(mbAppTx/timeTx)); + buf.append(" MB/sec (application)\n\tError Msg:"); + buf.append(msgTxErr).append("\n\tRx Msg:"); + buf.append(msgRxCnt); + buf.append(" messages\n\tRx Speed:"); + buf.append(df.format(mbRx/((double)((System.currentTimeMillis()-rxStart)/1000)))); + buf.append(" MB/sec (since 1st msg)\n\tReceived:"); + buf.append(df.format(mbRx)).append(" MB]\n"); + if ( log.isInfoEnabled() ) log.info(buf); + } + + public void setInterval(int interval) { + this.interval = interval; + } + + public int getInterval() { + return interval; + } + +} diff --git a/java/org/apache/catalina/tribes/group/interceptors/TwoPhaseCommitInterceptor.java b/java/org/apache/catalina/tribes/group/interceptors/TwoPhaseCommitInterceptor.java new file mode 100644 index 000000000..bb531493f --- /dev/null +++ b/java/org/apache/catalina/tribes/group/interceptors/TwoPhaseCommitInterceptor.java @@ -0,0 +1,148 @@ +/* + * 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.group.interceptors; + +import java.util.HashMap; + +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.util.UUIDGenerator; +import org.apache.catalina.tribes.util.Arrays; +import org.apache.catalina.tribes.UniqueId; +import java.util.Map; + +/** + *

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; i expiration; + } + + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/io/BufferPool.java b/java/org/apache/catalina/tribes/io/BufferPool.java new file mode 100644 index 000000000..48b5e8b91 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/BufferPool.java @@ -0,0 +1,93 @@ +/* + * 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.io; + +import org.apache.commons.logging.LogFactory; + +import org.apache.commons.logging.Log; + +/** + * + * @author Filip Hanik + * + * @version 1.0 + */ +public class BufferPool { + protected static Log log = LogFactory.getLog(BufferPool.class); + + public static int DEFAULT_POOL_SIZE = 100*1024*1024; //100MB + + + + protected static BufferPool instance = null; + protected BufferPoolAPI pool = null; + + private BufferPool(BufferPoolAPI pool) { + this.pool = pool; + } + + public XByteBuffer getBuffer(int minSize, boolean discard) { + if ( pool != null ) return pool.getBuffer(minSize, discard); + else return new XByteBuffer(minSize,discard); + } + + public void returnBuffer(XByteBuffer buffer) { + if ( pool != null ) pool.returnBuffer(buffer); + } + + public void clear() { + if ( pool != null ) pool.clear(); + } + + + public static BufferPool getBufferPool() { + if ( (instance == null) ) { + synchronized (BufferPool.class) { + if ( instance == null ) { + BufferPoolAPI pool = null; + Class clazz = null; + try { + clazz = Class.forName("org.apache.catalina.tribes.io.BufferPool15Impl"); + pool = (BufferPoolAPI)clazz.newInstance(); + } catch ( Throwable x ) { + try { + clazz = Class.forName("org.apache.catalina.tribes.io.BufferPool14Impl"); + pool = (BufferPoolAPI)clazz.newInstance(); + } catch ( Throwable e ) { + log.warn("Unable to initilize BufferPool, not pooling XByteBuffer objects:"+x.getMessage()); + if ( log.isDebugEnabled() ) log.debug("Unable to initilize BufferPool, not pooling XByteBuffer objects:",x); + } + } + pool.setMaxSize(DEFAULT_POOL_SIZE); + log.info("Created a buffer pool with max size:"+DEFAULT_POOL_SIZE+" bytes of type:"+(clazz!=null?clazz.getName():"null")); + instance = new BufferPool(pool); + }//end if + }//sync + }//end if + return instance; + } + + + public static interface BufferPoolAPI { + public void setMaxSize(int bytes); + + public XByteBuffer getBuffer(int minSize, boolean discard); + + public void returnBuffer(XByteBuffer buffer); + + public void clear(); + } +} diff --git a/java/org/apache/catalina/tribes/io/BufferPool14Impl.java b/java/org/apache/catalina/tribes/io/BufferPool14Impl.java new file mode 100644 index 000000000..f2b875077 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/BufferPool14Impl.java @@ -0,0 +1,69 @@ +/* + * 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.io; + +import java.util.Queue; +import java.util.LinkedList; + + +/** + * + * @author Filip Hanik + * @version 1.0 + */ +class BufferPool14Impl implements BufferPool.BufferPoolAPI { + protected int maxSize; + protected int size = 0; + protected LinkedList queue = new LinkedList(); + + public void setMaxSize(int bytes) { + this.maxSize = bytes; + } + + public synchronized int addAndGet(int val) { + size = size + (val); + return size; + } + + + + public synchronized XByteBuffer getBuffer(int minSize, boolean discard) { + XByteBuffer buffer = (XByteBuffer)(queue.size()>0?queue.remove(0):null); + if ( buffer != null ) addAndGet(-buffer.getCapacity()); + if ( buffer == null ) buffer = new XByteBuffer(minSize,discard); + else if ( buffer.getCapacity() <= minSize ) buffer.expand(minSize); + buffer.setDiscard(discard); + buffer.reset(); + return buffer; + } + + public synchronized void returnBuffer(XByteBuffer buffer) { + if ( (size + buffer.getCapacity()) <= maxSize ) { + addAndGet(buffer.getCapacity()); + queue.add(buffer); + } + } + + public synchronized void clear() { + queue.clear(); + size = 0; + } + + public int getMaxSize() { + return maxSize; + } + +} diff --git a/java/org/apache/catalina/tribes/io/BufferPool15Impl.java b/java/org/apache/catalina/tribes/io/BufferPool15Impl.java new file mode 100644 index 000000000..7cf5e5f06 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/BufferPool15Impl.java @@ -0,0 +1,62 @@ +/* + * 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.io; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * + * @author Filip Hanik + * @version 1.0 + */ +class BufferPool15Impl implements BufferPool.BufferPoolAPI { + protected int maxSize; + protected AtomicInteger size = new AtomicInteger(0); + protected ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); + + public void setMaxSize(int bytes) { + this.maxSize = bytes; + } + + + public XByteBuffer getBuffer(int minSize, boolean discard) { + XByteBuffer buffer = (XByteBuffer)queue.poll(); + if ( buffer != null ) size.addAndGet(-buffer.getCapacity()); + if ( buffer == null ) buffer = new XByteBuffer(minSize,discard); + else if ( buffer.getCapacity() <= minSize ) buffer.expand(minSize); + buffer.setDiscard(discard); + buffer.reset(); + return buffer; + } + + public void returnBuffer(XByteBuffer buffer) { + if ( (size.get() + buffer.getCapacity()) <= maxSize ) { + size.addAndGet(buffer.getCapacity()); + queue.offer(buffer); + } + } + + public void clear() { + queue.clear(); + size.set(0); + } + + public int getMaxSize() { + return maxSize; + } + +} diff --git a/java/org/apache/catalina/tribes/io/ChannelData.java b/java/org/apache/catalina/tribes/io/ChannelData.java new file mode 100644 index 000000000..42bdb83f5 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/ChannelData.java @@ -0,0 +1,356 @@ +/* + * 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.tribes.io; + +import java.util.Arrays; + +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.membership.MemberImpl; +import org.apache.catalina.tribes.util.UUIDGenerator; +import org.apache.catalina.tribes.Channel; +import java.sql.Timestamp; + +/** + * The ChannelData 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 && ibyte. + * @throws IOException if an I/O error occurs. In particular, an + * IOException 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; i0; + } + /** + * Returns the number of packages that the reader has read + * @return int + */ + public int count() { + return buffer.countPackages(); + } + + public void close() { + this.buffer = null; + } + + public long getLastAccess() { + return lastAccess; + } + + public boolean isCancelled() { + return cancelled; + } + + public void setLastAccess(long lastAccess) { + this.lastAccess = lastAccess; + } + + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + +} diff --git a/java/org/apache/catalina/tribes/io/ReplicationStream.java b/java/org/apache/catalina/tribes/io/ReplicationStream.java new file mode 100644 index 000000000..a08a27ac8 --- /dev/null +++ b/java/org/apache/catalina/tribes/io/ReplicationStream.java @@ -0,0 +1,116 @@ +/* + * 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.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; + +/** + * Custom subclass of ObjectInputStream 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; i + * Two, it can encode and decode packages so that they can be defined and identified + * as they come in on a socket. + *
+ * THIS CLASS IS NOT THREAD SAFE
+ *
+ * Transfer package: + *
    + *
  • START_DATA/b> - 7 bytes - FLT2002
  • + *
  • SIZE - 4 bytes - size of the data package
  • + *
  • DATA - should be as many bytes as the prev SIZE
  • + *
  • END_DATA - 7 bytes - TLF2003
  • + *
+ * @author Filip Hanik + * @version $Revision: 377484 $, $Date: 2006-02-13 15:00:05 -0600 (Mon, 13 Feb 2006) $ + */ +public class XByteBuffer +{ + + public static org.apache.commons.logging.Log log = + org.apache.commons.logging.LogFactory.getLog( XByteBuffer.class ); + + /** + * This is a package header, 7 bytes (FLT2002) + */ + public static final byte[] START_DATA = {70,76,84,50,48,48,50}; + + /** + * This is the package footer, 7 bytes (TLF2003) + */ + public static final byte[] END_DATA = {84,76,70,50,48,48,51}; + + /** + * Default size on the initial byte buffer + */ + private static final int DEF_SIZE = 2048; + + /** + * Default size to extend the buffer with + */ + private static final int DEF_EXT = 1024; + + /** + * Variable to hold the data + */ + protected byte[] buf = null; + + /** + * Current length of data in the buffer + */ + protected int bufSize = 0; + + /** + * Flag for discarding invalid packages + * If this flag is set to true, and append(byte[],...) is called, + * the data added will be inspected, and if it doesn't start with + * START_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 long + * @return use + */ + public static boolean toBoolean(byte[] b, int offset) { + return b[offset] != 0; + } + + + /** + * Converts an integer to four bytes + * @param n - the integer + * @return - four bytes in an array + * @deprecated use toBytes(int,byte[],int) + */ + public static byte[] toBytes(int n) { + return toBytes(n,new byte[4],0); + } + + public static byte[] toBytes(int n,byte[] b, int offset) { + b[offset+3] = (byte) (n); + n >>>= 8; + b[offset+2] = (byte) (n); + n >>>= 8; + b[offset+1] = (byte) (n); + n >>>= 8; + b[offset+0] = (byte) (n); + return b; + } + + /** + * Converts an long to eight bytes + * @param n - the long + * @return - eight bytes in an array + * @deprecated use toBytes(long,byte[],int) + */ + public static byte[] toBytes(long n) { + return toBytes(n,new byte[8],0); + } + public static byte[] toBytes(long n, byte[] b, int offset) { + b[offset+7] = (byte) (n); + n >>>= 8; + b[offset+6] = (byte) (n); + n >>>= 8; + b[offset+5] = (byte) (n); + n >>>= 8; + b[offset+4] = (byte) (n); + n >>>= 8; + b[offset+3] = (byte) (n); + n >>>= 8; + b[offset+2] = (byte) (n); + n >>>= 8; + b[offset+1] = (byte) (n); + n >>>= 8; + b[offset+0] = (byte) (n); + return b; + } + + /** + * Similar to a String.IndexOf, but uses pure bytes + * @param src - the source bytes to be searched + * @param srcOff - offset on the source buffer + * @param find - the string to be found within src + * @return - the index of the first matching byte. -1 if the find array is not found + */ + public static int firstIndexOf(byte[] src, int srcOff, byte[] find){ + int result = -1; + if (find.length > src.length) return result; + if (find.length == 0 || src.length == 0) return result; + if (srcOff >= src.length ) throw new java.lang.ArrayIndexOutOfBoundsException(); + boolean found = false; + int srclen = src.length; + int findlen = find.length; + byte first = find[0]; + int pos = srcOff; + while (!found) { + //find the first byte + while (pos < srclen){ + if (first == src[pos]) + break; + pos++; + } + if (pos >= srclen) + return -1; + + //we found the first character + //match the rest of the bytes - they have to match + if ( (srclen - pos) < findlen) + return -1; + //assume it does exist + found = true; + for (int i = 1; ( (i < findlen) && found); i++) + found = found && (find[i] == src[pos + i]); + if (found) + result = pos; + else if ( (srclen - pos) < findlen) + return -1; //no more matches possible + else + pos++; + } + return result; + } + + + public static Serializable deserialize(byte[] data) + throws IOException, ClassNotFoundException, ClassCastException { + return deserialize(data,0,data.length); + } + + public static Serializable deserialize(byte[] data, int offset, int length) + throws IOException, ClassNotFoundException, ClassCastException { + return deserialize(data,offset,length,null); + } + public static int invokecount = 0; + public static Serializable deserialize(byte[] data, int offset, int length, ClassLoader[] cls) + throws IOException, ClassNotFoundException, ClassCastException { + synchronized (XByteBuffer.class) { invokecount++;} + Object message = null; + if ( cls == null ) cls = new ClassLoader[0]; + if (data != null) { + InputStream instream = new ByteArrayInputStream(data,offset,length); + ObjectInputStream stream = null; + stream = (cls.length>0)? new ReplicationStream(instream,cls):new ObjectInputStream(instream); + message = stream.readObject(); + instream.close(); + stream.close(); + } + if ( message == null ) { + return null; + } else if (message instanceof Serializable) + return (Serializable) message; + else { + throw new ClassCastException("Message has the wrong class. It should implement Serializable, instead it is:"+message.getClass().getName()); + } + } + + /** + * Serializes a message into cluster data + * @param msg ClusterMessage + * @param compress boolean + * @return + * @throws IOException + */ + public static byte[] serialize(Serializable msg) throws IOException { + ByteArrayOutputStream outs = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(outs); + out.writeObject(msg); + out.flush(); + byte[] data = outs.toByteArray(); + return data; + } + + public void setDiscard(boolean discard) { + this.discard = discard; + } + + public boolean getDiscard() { + return discard; + } + +} diff --git a/java/org/apache/catalina/tribes/mbeans-descriptors.xml b/java/org/apache/catalina/tribes/mbeans-descriptors.xml new file mode 100644 index 000000000..3f5bf56e5 --- /dev/null +++ b/java/org/apache/catalina/tribes/mbeans-descriptors.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/tribes/membership/Constants.java b/java/org/apache/catalina/tribes/membership/Constants.java new file mode 100644 index 000000000..29aaf5a46 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/Constants.java @@ -0,0 +1,39 @@ +/* + * 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.membership; + +import org.apache.catalina.tribes.util.Arrays; + + +/** + * Manifest constants for the 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 + *
All are required
+ * 1. mcastPort - the port to listen to
+ * 2. mcastAddress - the mcast group address
+ * 4. bindAddress - the bind address if any - only one that can be null
+ * 5. memberDropTime - the time a member is gone before it is considered gone.
+ * 6. mcastFrequency - the frequency of sending messages
+ * 7. tcpListenPort - the port this member listens to
+ * 8. tcpListenHost - the bind address of this member
+ * @exception java.lang.IllegalArgumentException if a property is missing. + */ + public void setProperties(Properties properties) { + hasProperty(properties,"mcastPort"); + hasProperty(properties,"mcastAddress"); + hasProperty(properties,"memberDropTime"); + hasProperty(properties,"mcastFrequency"); + hasProperty(properties,"tcpListenPort"); + hasProperty(properties,"tcpListenHost"); + this.properties = properties; + } + + /** + * Return the properties, see setProperties + */ + public Properties getProperties() { + return properties; + } + + /** + * Return the local member name + */ + public String getLocalMemberName() { + return localMember.toString() ; + } + + /** + * Return the local member + */ + public Member getLocalMember(boolean alive) { + if ( alive && localMember != null && impl != null) localMember.setMemberAliveTime(System.currentTimeMillis()-impl.getServiceStartTime()); + return localMember; + } + + /** + * Sets the local member properties for broadcasting + */ + public void setLocalMemberProperties(String listenHost, int listenPort) { + properties.setProperty("tcpListenHost",listenHost); + properties.setProperty("tcpListenPort",String.valueOf(listenPort)); + try { + if (localMember != null) { + localMember.setHostname(listenHost); + localMember.setPort(listenPort); + } else { + localMember = new MemberImpl(listenHost, listenPort, 0); + localMember.setUniqueId(UUIDGenerator.randomUUID(true)); + localMember.setPayload(getPayload()); + localMember.setDomain(getDomain()); + } + localMember.getData(true, true); + }catch ( IOException x ) { + throw new IllegalArgumentException(x); + } + } + + public void setMcastAddr(String addr) { + properties.setProperty("mcastAddress", addr); + } + + public String getMcastAddr() { + return properties.getProperty("mcastAddress"); + } + + public void setMcastBindAddress(String bindaddr) { + properties.setProperty("mcastBindAddress", bindaddr); + } + + public String getMcastBindAddress() { + return properties.getProperty("mcastBindAddress"); + } + + public void setMcastPort(int port) { + properties.setProperty("mcastPort", String.valueOf(port)); + } + + public int getMcastPort() { + String p = properties.getProperty("mcastPort"); + return new Integer(p).intValue(); + } + + public void setMcastFrequency(long time) { + properties.setProperty("mcastFrequency", String.valueOf(time)); + } + + public long getMcastFrequency() { + String p = properties.getProperty("mcastFrequency"); + return new Long(p).longValue(); + } + + public void setMcastDropTime(long time) { + properties.setProperty("memberDropTime", String.valueOf(time)); + } + + public long getMcastDropTime() { + String p = properties.getProperty("memberDropTime"); + return new Long(p).longValue(); + } + + /** + * Check if a required property is available. + * @param properties The set of properties + * @param name The property to check for + */ + protected void hasProperty(Properties properties, String name){ + if ( properties.getProperty(name)==null) throw new IllegalArgumentException("McastService:Required property \""+name+"\" is missing."); + } + + /** + * Start broadcasting and listening to membership pings + * @throws java.lang.Exception if a IO error occurs + */ + public void start() throws java.lang.Exception { + start(MembershipService.MBR_RX); + start(MembershipService.MBR_TX); + } + + public void start(int level) throws java.lang.Exception { + hasProperty(properties,"mcastPort"); + hasProperty(properties,"mcastAddress"); + hasProperty(properties,"memberDropTime"); + hasProperty(properties,"mcastFrequency"); + hasProperty(properties,"tcpListenPort"); + hasProperty(properties,"tcpListenHost"); + + if ( impl != null ) { + impl.start(level); + return; + } + String host = getProperties().getProperty("tcpListenHost"); + int port = Integer.parseInt(getProperties().getProperty("tcpListenPort")); + + if ( localMember == null ) { + localMember = new MemberImpl(host, port, 100); + localMember.setUniqueId(UUIDGenerator.randomUUID(true)); + } else { + localMember.setHostname(host); + localMember.setPort(port); + localMember.setMemberAliveTime(100); + } + if ( this.payload != null ) localMember.setPayload(payload); + if ( this.domain != null ) localMember.setDomain(domain); + localMember.setServiceStartTime(System.currentTimeMillis()); + java.net.InetAddress bind = null; + if ( properties.getProperty("mcastBindAddress")!= null ) { + bind = java.net.InetAddress.getByName(properties.getProperty("mcastBindAddress")); + } + int ttl = -1; + int soTimeout = -1; + if ( properties.getProperty("mcastTTL") != null ) { + try { + ttl = Integer.parseInt(properties.getProperty("mcastTTL")); + } catch ( Exception x ) { + log.error("Unable to parse mcastTTL="+properties.getProperty("mcastTTL"),x); + } + } + if ( properties.getProperty("mcastSoTimeout") != null ) { + try { + soTimeout = Integer.parseInt(properties.getProperty("mcastSoTimeout")); + } catch ( Exception x ) { + log.error("Unable to parse mcastSoTimeout="+properties.getProperty("mcastSoTimeout"),x); + } + } + + impl = new McastServiceImpl((MemberImpl)localMember,Long.parseLong(properties.getProperty("mcastFrequency")), + Long.parseLong(properties.getProperty("memberDropTime")), + Integer.parseInt(properties.getProperty("mcastPort")), + bind, + java.net.InetAddress.getByName(properties.getProperty("mcastAddress")), + ttl, + soTimeout, + this); + + impl.start(level); + + + } + + + /** + * Stop broadcasting and listening to membership pings + */ + public void stop(int svc) { + try { + if ( impl != null && impl.stop(svc) ) impl = null; + } catch ( Exception x) { + log.error("Unable to stop the mcast service, level:"+svc+".",x); + } + } + + + /** + * Return all the members by name + */ + public String[] getMembersByName() { + Member[] currentMembers = getMembers(); + String [] membernames ; + if(currentMembers != null) { + membernames = new String[currentMembers.length]; + for (int i = 0; i < currentMembers.length; i++) { + membernames[i] = currentMembers[i].toString() ; + } + } else + membernames = new String[0] ; + return membernames ; + } + + /** + * Return the member by name + */ + public Member findMemberByName(String name) { + Member[] currentMembers = getMembers(); + for (int i = 0; i < currentMembers.length; i++) { + if (name.equals(currentMembers[i].toString())) + return currentMembers[i]; + } + return null; + } + + /** + * has members? + */ + public boolean hasMembers() { + if ( impl == null || impl.membership == null ) return false; + return impl.membership.hasMembers(); + } + + public Member getMember(Member mbr) { + if ( impl == null || impl.membership == null ) return null; + return impl.membership.getMember(mbr); + } + + /** + * Return all the members + */ + public Member[] getMembers() { + if ( impl == null || impl.membership == null ) return null; + return impl.membership.getMembers(); + } + /** + * Add a membership listener, this version only supports one listener per service, + * so calling this method twice will result in only the second listener being active. + * @param listener The listener + */ + public void setMembershipListener(MembershipListener listener) { + this.listener = listener; + } + /** + * Remove the membership listener + */ + public void removeMembershipListener(){ + listener = null; + } + + public void memberAdded(Member member) { + if ( listener!=null ) listener.memberAdded(member); + } + + /** + * Callback from the impl when a new member has been received + * @param member The member + */ + public void memberDisappeared(Member member) + { + if ( listener!=null ) listener.memberDisappeared(member); + } + + public int getMcastSoTimeout() { + return mcastSoTimeout; + } + public void setMcastSoTimeout(int mcastSoTimeout) { + this.mcastSoTimeout = mcastSoTimeout; + properties.setProperty("mcastSoTimeout", String.valueOf(mcastSoTimeout)); + } + public int getMcastTTL() { + return mcastTTL; + } + + public byte[] getPayload() { + return payload; + } + + public byte[] getDomain() { + return domain; + } + + public void setMcastTTL(int mcastTTL) { + this.mcastTTL = mcastTTL; + properties.setProperty("mcastTTL", String.valueOf(mcastTTL)); + } + + public void setPayload(byte[] payload) { + this.payload = payload; + if ( localMember != null ) { + localMember.setPayload(payload); + localMember.getData(true,true); + try { + if (impl != null) impl.send(false); + }catch ( Exception x ) { + log.error("Unable to send payload update.",x); + } + } + } + + public void setDomain(byte[] domain) { + this.domain = domain; + if ( localMember != null ) { + localMember.setDomain(domain); + localMember.getData(true,true); + try { + if (impl != null) impl.send(false); + }catch ( Exception x ) { + log.error("Unable to send domain update.",x); + } + } + } + + /** + * Simple test program + * @param args Command-line arguments + * @throws Exception If an error occurs + */ + public static void main(String args[]) throws Exception { + if(log.isInfoEnabled()) + log.info("Usage McastService hostname tcpport"); + McastService service = new McastService(); + java.util.Properties p = new java.util.Properties(); + p.setProperty("mcastPort","5555"); + p.setProperty("mcastAddress","224.10.10.10"); + p.setProperty("mcastClusterDomain","catalina"); + p.setProperty("bindAddress","localhost"); + p.setProperty("memberDropTime","3000"); + p.setProperty("mcastFrequency","500"); + p.setProperty("tcpListenPort","4000"); + p.setProperty("tcpListenHost","127.0.0.1"); + service.setProperties(p); + service.start(); + Thread.sleep(60*1000*60); + } +} diff --git a/java/org/apache/catalina/tribes/membership/McastServiceImpl.java b/java/org/apache/catalina/tribes/membership/McastServiceImpl.java new file mode 100644 index 000000000..999b1f012 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/McastServiceImpl.java @@ -0,0 +1,397 @@ +/* + * 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.membership; + + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.MulticastSocket; + +import org.apache.catalina.tribes.MembershipListener; +import java.util.Arrays; +import java.net.SocketTimeoutException; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.Channel; +import java.net.InetSocketAddress; + +/** + * 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. + * This is the low level implementation that handles the multicasting sockets. + * Need to fix this, could use java.nio and only need one thread to send and receive, or + * just use a timeout on the receive + * @author Filip Hanik + * @version $Revision: 356540 $, $Date: 2005-12-13 10:53:40 -0600 (Tue, 13 Dec 2005) $ + */ +public class McastServiceImpl +{ + private static org.apache.commons.logging.Log log = + org.apache.commons.logging.LogFactory.getLog( McastService.class ); + + protected static int MAX_PACKET_SIZE = 65535; + /** + * Internal flag used for the listen thread that listens to the multicasting socket. + */ + protected boolean doRunSender = false; + protected boolean doRunReceiver = false; + protected int startLevel = 0; + /** + * Socket that we intend to listen to + */ + protected MulticastSocket socket; + /** + * The local member that we intend to broad cast over and over again + */ + protected MemberImpl member; + /** + * The multicast address + */ + protected InetAddress address; + /** + * The multicast port + */ + protected int port; + /** + * The time it takes for a member to expire. + */ + protected long timeToExpiration; + /** + * How often to we send out a broadcast saying we are alive, must be smaller than timeToExpiration + */ + protected long sendFrequency; + /** + * Reuse the sendPacket, no need to create a new one everytime + */ + protected DatagramPacket sendPacket; + /** + * Reuse the receivePacket, no need to create a new one everytime + */ + protected DatagramPacket receivePacket; + /** + * The membership, used so that we calculate memberships when they arrive or don't arrive + */ + protected Membership membership; + /** + * The actual listener, for callback when shits goes down + */ + protected MembershipListener service; + /** + * Thread to listen for pings + */ + protected ReceiverThread receiver; + /** + * Thread to send pings + */ + protected SenderThread sender; + + /** + * When was the service started + */ + protected long serviceStartTime = System.currentTimeMillis(); + + /** + * Time to live for the multicast packets that are being sent out + */ + protected int mcastTTL = -1; + /** + * Read timeout on the mcast socket + */ + protected int mcastSoTimeout = -1; + /** + * bind address + */ + protected InetAddress mcastBindAddress = null; + + /** + * Create a new mcast service impl + * @param member - the local member + * @param sendFrequency - the time (ms) in between pings sent out + * @param expireTime - the time (ms) for a member to expire + * @param port - the mcast port + * @param bind - the bind address (not sure this is used yet) + * @param mcastAddress - the mcast address + * @param service - the callback service + * @throws IOException + */ + public McastServiceImpl( + MemberImpl member, + long sendFrequency, + long expireTime, + int port, + InetAddress bind, + InetAddress mcastAddress, + int ttl, + int soTimeout, + MembershipListener service) + throws IOException { + this.member = member; + this.address = mcastAddress; + this.port = port; + this.mcastSoTimeout = soTimeout; + this.mcastTTL = ttl; + this.mcastBindAddress = bind; + this.timeToExpiration = expireTime; + this.service = service; + this.sendFrequency = sendFrequency; + setupSocket(); + sendPacket = new DatagramPacket(new byte[MAX_PACKET_SIZE],MAX_PACKET_SIZE); + sendPacket.setAddress(address); + sendPacket.setPort(port); + receivePacket = new DatagramPacket(new byte[MAX_PACKET_SIZE],MAX_PACKET_SIZE); + receivePacket.setAddress(address); + receivePacket.setPort(port); + membership = new Membership(member); + } + + protected void setupSocket() throws IOException { + if (mcastBindAddress != null) socket = new MulticastSocket(new InetSocketAddress(mcastBindAddress, port)); + else socket = new MulticastSocket(port); + if (mcastBindAddress != null) { + if(log.isInfoEnabled()) + log.info("Setting multihome multicast interface to:" +mcastBindAddress); + socket.setInterface(mcastBindAddress); + } //end if + //force a so timeout so that we don't block forever + if ( mcastSoTimeout <= 0 ) mcastSoTimeout = (int)sendFrequency; + if(log.isInfoEnabled()) + log.info("Setting cluster mcast soTimeout to "+mcastSoTimeout); + socket.setSoTimeout(mcastSoTimeout); + + if ( mcastTTL >= 0 ) { + if(log.isInfoEnabled()) + log.info("Setting cluster mcast TTL to " + mcastTTL); + socket.setTimeToLive(mcastTTL); + } + } + + + + /** + * Start the service + * @param level 1 starts the receiver, level 2 starts the sender + * @throws IOException if the service fails to start + * @throws IllegalStateException if the service is already started + */ + public synchronized void start(int level) throws IOException { + boolean valid = false; + if ( (level & Channel.MBR_RX_SEQ)==Channel.MBR_RX_SEQ ) { + if ( receiver != null ) throw new IllegalStateException("McastService.receive already running."); + if ( sender == null ) socket.joinGroup(address); + doRunReceiver = true; + receiver = new ReceiverThread(); + receiver.setDaemon(true); + receiver.start(); + valid = true; + } + if ( (level & Channel.MBR_TX_SEQ)==Channel.MBR_TX_SEQ ) { + if ( sender != null ) throw new IllegalStateException("McastService.send already running."); + if ( receiver == null ) socket.joinGroup(address); + //make sure at least one packet gets out there + send(false); + doRunSender = true; + serviceStartTime = System.currentTimeMillis(); + sender = new SenderThread(sendFrequency); + sender.setDaemon(true); + sender.start(); + //we have started the receiver, but not yet waited for membership to establish + valid = true; + } + if (!valid) { + throw new IllegalArgumentException("Invalid start level. Only acceptable levels are Channel.MBR_RX_SEQ and Channel.MBR_TX_SEQ"); + } + //pause, once or twice + waitForMembers(level); + startLevel = (startLevel | level); + } + + private void waitForMembers(int level) { + long memberwait = sendFrequency*2; + if(log.isInfoEnabled()) + log.info("Sleeping for "+memberwait+" milliseconds to establish cluster membership, start level:"+level); + try {Thread.sleep(memberwait);}catch (InterruptedException ignore){} + if(log.isInfoEnabled()) + log.info("Done sleeping, membership established, start level:"+level); + } + + /** + * Stops the service + * @throws IOException if the service fails to disconnect from the sockets + */ + public synchronized boolean stop(int level) throws IOException { + boolean valid = false; + + if ( (level & Channel.MBR_RX_SEQ)==Channel.MBR_RX_SEQ ) { + valid = true; + doRunReceiver = false; + if ( receiver !=null ) receiver.interrupt(); + receiver = null; + } + if ( (level & Channel.MBR_TX_SEQ)==Channel.MBR_TX_SEQ ) { + valid = true; + doRunSender = false; + if ( sender != null )sender.interrupt(); + sender = null; + } + + if (!valid) { + throw new IllegalArgumentException("Invalid stop level. Only acceptable levels are Channel.MBR_RX_SEQ and Channel.MBR_TX_SEQ"); + } + startLevel = (startLevel & (~level)); + //we're shutting down, send a shutdown message and close the socket + if ( startLevel == 0 ) { + //send a stop message + member.setCommand(Member.SHUTDOWN_PAYLOAD); + member.getData(true, true); + send(false); + //leave mcast group + try {socket.leaveGroup(address);}catch ( Exception ignore){} + serviceStartTime = Long.MAX_VALUE; + } + return (startLevel == 0); + } + + /** + * Receive a datagram packet, locking wait + * @throws IOException + */ + public void receive() throws IOException { + try { + socket.receive(receivePacket); + if(receivePacket.getLength() > MAX_PACKET_SIZE) { + log.error("Multicast packet received was too long, dropping package:"+receivePacket.getLength()); + } else { + byte[] data = new byte[receivePacket.getLength()]; + System.arraycopy(receivePacket.getData(), receivePacket.getOffset(), data, 0, data.length); + final MemberImpl m = MemberImpl.getMember(data); + if (log.isTraceEnabled()) log.trace("Mcast receive ping from member " + m); + Thread t = null; + if (Arrays.equals(m.getCommand(), Member.SHUTDOWN_PAYLOAD)) { + if (log.isDebugEnabled()) log.debug("Member has shutdown:" + m); + membership.removeMember(m); + t = new Thread() { + public void run() { + service.memberDisappeared(m); + } + }; + } else if (membership.memberAlive(m)) { + if (log.isDebugEnabled()) log.debug("Mcast add member " + m); + t = new Thread() { + public void run() { + service.memberAdded(m); + } + }; + } //end if + if ( t != null ) t.start(); + } + } catch (SocketTimeoutException x ) { + //do nothing, this is normal, we don't want to block forever + //since the receive thread is the same thread + //that does membership expiration + } + checkExpired(); + } + + protected Object expiredMutex = new Object(); + protected void checkExpired() { + synchronized (expiredMutex) { + MemberImpl[] expired = membership.expire(timeToExpiration); + for (int i = 0; i < expired.length; i++) { + final MemberImpl member = expired[i]; + if (log.isDebugEnabled()) + log.debug("Mcast exipre member " + expired[i]); + try { + Thread t = new Thread() { + public void run() { + service.memberDisappeared(member); + } + }; + t.start(); + } catch (Exception x) { + log.error("Unable to process member disappeared message.", x); + } + } + } + } + + /** + * Send a ping + * @throws Exception + */ + public void send(boolean checkexpired) throws IOException{ + //ignore if we haven't started the sender + //if ( (startLevel&Channel.MBR_TX_SEQ) != Channel.MBR_TX_SEQ ) return; + member.inc(); + if(log.isTraceEnabled()) + log.trace("Mcast send ping from member " + member); + byte[] data = member.getData(); + DatagramPacket p = new DatagramPacket(data,data.length); + p.setAddress(address); + p.setPort(port); + socket.send(p); + if ( checkexpired ) checkExpired(); + } + + public long getServiceStartTime() { + return this.serviceStartTime; + } + + + public class ReceiverThread extends Thread { + public ReceiverThread() { + super(); + setName("Cluster-MembershipReceiver"); + } + public void run() { + while ( doRunReceiver ) { + try { + receive(); + } catch ( ArrayIndexOutOfBoundsException ax ) { + //we can ignore this, as it means we have an invalid package + //but we will log it to debug + if ( log.isDebugEnabled() ) + log.debug("Invalid member mcast package.",ax); + } catch ( Exception x ) { + log.warn("Error receiving mcast package. Sleeping 500ms",x); + try { Thread.sleep(500); } catch ( Exception ignore ){} + + } + } + } + }//class ReceiverThread + + public class SenderThread extends Thread { + long time; + public SenderThread(long time) { + this.time = time; + setName("Cluster-MembershipSender"); + + } + public void run() { + while ( doRunSender ) { + try { + send(true); + } catch ( Exception x ) { + log.warn("Unable to send mcast message.",x); + } + try { Thread.sleep(time); } catch ( Exception ignore ) {} + } + } + }//class SenderThread +} diff --git a/java/org/apache/catalina/tribes/membership/MemberImpl.java b/java/org/apache/catalina/tribes/membership/MemberImpl.java new file mode 100644 index 000000000..cd9f00fd7 --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/MemberImpl.java @@ -0,0 +1,570 @@ +/* + * 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.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.Arrays; + +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.transport.SenderState; + +/** + * A membership implementation using simple multicast. + * This is the representation of a multicast member. + * Carries the host, and port of the this or other cluster nodes. + * + * @author Filip Hanik + * @version $Revision: 304032 $, $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $ + */ +public class MemberImpl implements Member, java.io.Externalizable { + + /** + * Public properties specific to this implementation + */ + public static final transient String TCP_LISTEN_PORT = "tcpListenPort"; + public static final transient String TCP_LISTEN_HOST = "tcpListenHost"; + public static final transient String MEMBER_NAME = "memberName"; + + public static final transient byte[] TRIBES_MBR_BEGIN = new byte[] {84, 82, 73, 66, 69, 83, 45, 66}; + public static final transient byte[] TRIBES_MBR_END = new byte[] {84, 82, 73, 66, 69, 83, 45, 69}; + + /** + * The listen host for this member + */ + protected byte[] host; + protected transient String hostname; + /** + * The tcp listen port for this member + */ + protected int port; + + /** + * Counter for how many broadcast messages have been sent from this member + */ + protected int msgCount = 0; + /** + * The number of milliseconds since this members was + * created, is kept track of using the start time + */ + protected long memberAliveTime = 0; + + /** + * For the local member only + */ + protected transient long serviceStartTime; + + /** + * To avoid serialization over and over again, once the local dataPkg + * has been set, we use that to transmit data + */ + protected transient byte[] dataPkg = null; + + /** + * Unique session Id for this member + */ + protected byte[] uniqueId = new byte[16]; + + /** + * Custom payload that an app framework can broadcast + * Also used to transport stop command. + */ + protected byte[] payload = new byte[0]; + + /** + * Command, so that the custom payload doesn't have to be used + * This is for internal tribes use, such as SHUTDOWN_COMMAND + */ + protected byte[] command = new byte[0]; + + /** + * Domain if we want to filter based on domain. + */ + protected byte[] domain = new byte[0]; + + /** + * Empty constructor for serialization + */ + public MemberImpl() { + + } + + /** + * Construct a new member object + * @param name - the name of this member, cluster unique + * @param domain - the cluster domain name of this member + * @param host - the tcp listen host + * @param port - the tcp listen port + */ + public MemberImpl(String host, + int port, + long aliveTime) throws IOException { + setHostname(host); + this.port = port; + this.memberAliveTime=aliveTime; + } + + public MemberImpl(String host, + int port, + long aliveTime, + byte[] payload) throws IOException { + this(host,port,aliveTime); + setPayload(payload); + } + + public boolean isReady() { + return SenderState.getSenderState(this).isReady(); + } + public boolean isSuspect() { + return SenderState.getSenderState(this).isSuspect(); + } + public boolean isFailing() { + return SenderState.getSenderState(this).isFailing(); + } + + /** + * Increment the message count. + */ + protected void inc() { + msgCount++; + } + + /** + * Create a data package to send over the wire representing this member. + * This is faster than serialization. + * @return - the bytes for this member deserialized + * @throws Exception + */ + public byte[] getData() { + return getData(true); + } + /** + * Highly optimized version of serializing a member into a byte array + * Returns a cached byte[] reference, do not modify this data + * @param getalive boolean + * @return byte[] + */ + public byte[] getData(boolean getalive) { + return getData(getalive,false); + } + + + public int getDataLength() { + return TRIBES_MBR_BEGIN.length+ //start pkg + 4+ //data length + 8+ //alive time + 4+ //port + 1+ //host length + host.length+ //host + 4+ //command length + command.length+ //command + 4+ //domain length + domain.length+ //domain + 16+ //unique id + 4+ //payload length + payload.length+ //payload + TRIBES_MBR_END.length; //end pkg + } + + /** + * + * @param getalive boolean - calculate memberAlive time + * @param reset boolean - reset the cached data package, and create a new one + * @return byte[] + */ + public byte[] getData(boolean getalive, boolean reset) { + if ( reset ) dataPkg = null; + //look in cache first + if ( dataPkg!=null ) { + if ( getalive ) { + //you'd be surprised, but System.currentTimeMillis + //shows up on the profiler + long alive=System.currentTimeMillis()-getServiceStartTime(); + XByteBuffer.toBytes( (long) alive, dataPkg, TRIBES_MBR_BEGIN.length+4); + } + return dataPkg; + } + + //package looks like + //start package TRIBES_MBR_BEGIN.length + //package length - 4 bytes + //alive - 8 bytes + //port - 4 bytes + //host length - 1 byte + //host - hl bytes + //clen - 4 bytes + //command - clen bytes + //dlen - 4 bytes + //domain - dlen bytes + //uniqueId - 16 bytes + //payload length - 4 bytes + //payload plen bytes + //end package TRIBES_MBR_END.length + byte[] addr = host; + long alive=System.currentTimeMillis()-getServiceStartTime(); + byte hl = (byte)addr.length; + byte[] data = new byte[getDataLength()]; + + int bodylength = (getDataLength() - TRIBES_MBR_BEGIN.length - TRIBES_MBR_END.length - 4); + + int pos = 0; + + //TRIBES_MBR_BEGIN + System.arraycopy(TRIBES_MBR_BEGIN,0,data,pos,TRIBES_MBR_BEGIN.length); + pos += TRIBES_MBR_BEGIN.length; + + //body length + XByteBuffer.toBytes(bodylength,data,pos); + pos += 4; + + //alive data + XByteBuffer.toBytes((long)alive,data,pos); + pos += 8; + //port + XByteBuffer.toBytes(port,data,pos); + pos += 4; + //host length + data[pos++] = hl; + //host + System.arraycopy(addr,0,data,pos,addr.length); + pos+=addr.length; + //clen - 4 bytes + XByteBuffer.toBytes(command.length,data,pos); + pos+=4; + //command - clen bytes + System.arraycopy(command,0,data,pos,command.length); + pos+=command.length; + //dlen - 4 bytes + XByteBuffer.toBytes(domain.length,data,pos); + pos+=4; + //domain - dlen bytes + System.arraycopy(domain,0,data,pos,domain.length); + pos+=domain.length; + //unique Id + System.arraycopy(uniqueId,0,data,pos,uniqueId.length); + pos+=uniqueId.length; + //payload + XByteBuffer.toBytes(payload.length,data,pos); + pos+=4; + System.arraycopy(payload,0,data,pos,payload.length); + pos+=payload.length; + + //TRIBES_MBR_END + System.arraycopy(TRIBES_MBR_END,0,data,pos,TRIBES_MBR_END.length); + pos += TRIBES_MBR_END.length; + + //create local data + dataPkg = data; + return data; + } + /** + * Deserializes a member from data sent over the wire + * @param data - the bytes received + * @return a member object. + */ + public static MemberImpl getMember(byte[] data, MemberImpl member) { + return getMember(data,0,data.length,member); + } + + public static MemberImpl getMember(byte[] data, int offset, int length, MemberImpl member) { + //package looks like + //start package TRIBES_MBR_BEGIN.length + //package length - 4 bytes + //alive - 8 bytes + //port - 4 bytes + //host length - 1 byte + //host - hl bytes + //clen - 4 bytes + //command - clen bytes + //dlen - 4 bytes + //domain - dlen bytes + //uniqueId - 16 bytes + //payload length - 4 bytes + //payload plen bytes + //end package TRIBES_MBR_END.length + + int pos = offset; + + if (XByteBuffer.firstIndexOf(data,offset,TRIBES_MBR_BEGIN)!=pos) { + throw new IllegalArgumentException("Invalid package, should start with:"+org.apache.catalina.tribes.util.Arrays.toString(TRIBES_MBR_BEGIN)); + } + + if ( length < (TRIBES_MBR_BEGIN.length+4) ) { + throw new ArrayIndexOutOfBoundsException("Member package to small to validate."); + } + + pos += TRIBES_MBR_BEGIN.length; + + int bodylength = XByteBuffer.toInt(data,pos); + pos += 4; + + if ( length < (bodylength+4+TRIBES_MBR_BEGIN.length+TRIBES_MBR_END.length) ) { + throw new ArrayIndexOutOfBoundsException("Not enough bytes in member package."); + } + + int endpos = pos+bodylength; + if (XByteBuffer.firstIndexOf(data,endpos,TRIBES_MBR_END)!=endpos) { + throw new IllegalArgumentException("Invalid package, should end with:"+org.apache.catalina.tribes.util.Arrays.toString(TRIBES_MBR_END)); + } + + + byte[] alived = new byte[8]; + System.arraycopy(data, pos, alived, 0, 8); + pos += 8; + byte[] portd = new byte[4]; + System.arraycopy(data, pos, portd, 0, 4); + pos += 4; + + byte hl = data[pos++]; + byte[] addr = new byte[hl]; + System.arraycopy(data, pos, addr, 0, hl); + pos += hl; + + int cl = XByteBuffer.toInt(data, pos); + pos += 4; + + byte[] command = new byte[cl]; + System.arraycopy(data, pos, command, 0, command.length); + pos += command.length; + + int dl = XByteBuffer.toInt(data, pos); + pos += 4; + + byte[] domain = new byte[dl]; + System.arraycopy(data, pos, domain, 0, domain.length); + pos += domain.length; + + byte[] uniqueId = new byte[16]; + System.arraycopy(data, pos, uniqueId, 0, 16); + pos += 16; + + int pl = XByteBuffer.toInt(data, pos); + pos += 4; + + byte[] payload = new byte[pl]; + System.arraycopy(data, pos, payload, 0, payload.length); + pos += payload.length; + + member.setHost(addr); + member.setPort(XByteBuffer.toInt(portd, 0)); + member.setMemberAliveTime(XByteBuffer.toLong(alived, 0)); + member.setUniqueId(uniqueId); + member.payload = payload; + member.domain = domain; + member.command = command; + + member.dataPkg = new byte[length]; + System.arraycopy(data, offset, member.dataPkg, 0, length); + + return member; + } + + public static MemberImpl getMember(byte[] data) { + return getMember(data,new MemberImpl()); + } + + /** + * Return the name of this object + * @return a unique name to the cluster + */ + public String getName() { + return "tcp://"+getHostname()+":"+getPort(); + } + + /** + * Return the listen port of this member + * @return - tcp listen port + */ + public int getPort() { + return this.port; + } + + /** + * Return the TCP listen host for this member + * @return IP address or host name + */ + public byte[] getHost() { + return host; + } + + public String getHostname() { + if ( this.hostname != null ) return hostname; + else { + try { + this.hostname = java.net.InetAddress.getByAddress(host).getHostName(); + return this.hostname; + }catch ( IOException x ) { + throw new RuntimeException("Unable to parse hostname.",x); + } + } + } + + /** + * Contains information on how long this member has been online. + * The result is the number of milli seconds this member has been + * broadcasting its membership to the cluster. + * @return nr of milliseconds since this member started. + */ + public long getMemberAliveTime() { + return memberAliveTime; + } + + public long getServiceStartTime() { + return serviceStartTime; + } + + public byte[] getUniqueId() { + return uniqueId; + } + + public byte[] getPayload() { + return payload; + } + + public byte[] getCommand() { + return command; + } + + public byte[] getDomain() { + return domain; + } + + public void setMemberAliveTime(long time) { + memberAliveTime=time; + } + + + + /** + * String representation of this object + */ + public String toString() { + StringBuffer buf = new StringBuffer("org.apache.catalina.tribes.membership.MemberImpl["); + buf.append(getName()).append(","); + buf.append(getHostname()).append(","); + buf.append(port).append(", alive="); + buf.append(memberAliveTime).append(","); + buf.append("id=").append(bToS(this.uniqueId)).append(", "); + buf.append("payload=").append(bToS(this.payload,8)).append(", "); + buf.append("command=").append(bToS(this.command,8)).append(", "); + buf.append("domain=").append(bToS(this.domain,8)).append(", "); + buf.append("]"); + return buf.toString(); + } + public static String bToS(byte[] data) { + return bToS(data,data.length); + } + public static String bToS(byte[] data, int max) { + StringBuffer buf = new StringBuffer(4*16); + buf.append("{"); + for (int i=0; data!=null && i McastServiceImpl.MAX_PACKET_SIZE ) { + this.payload = oldpayload; + throw new IllegalArgumentException("Payload is to large for tribes to handle."); + } + + } + + public void setCommand(byte[] command) { + this.command = command!=null?command:new byte[0]; + getData(true,true); + } + + public void setDomain(byte[] domain) { + this.domain = domain!=null?domain:new byte[0]; + getData(true,true); + } + + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + int length = in.readInt(); + byte[] message = new byte[length]; + in.read(message); + getMember(message,this); + + } + + public void writeExternal(ObjectOutput out) throws IOException { + byte[] data = this.getData(); + out.writeInt(data.length); + out.write(data); + } + +} diff --git a/java/org/apache/catalina/tribes/membership/Membership.java b/java/org/apache/catalina/tribes/membership/Membership.java new file mode 100644 index 000000000..c4cabdead --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/Membership.java @@ -0,0 +1,324 @@ +/* + * 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.membership; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.catalina.tribes.Member; +import java.util.Comparator; + +/** + * A membership implementation using simple multicast. + * This is the representation of a multicast membership. + * 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 + * @author Peter Rossbach + * @version $Revision: 356540 $, $Date: 2005-12-13 10:53:40 -0600 (Tue, 13 Dec 2005) $ + */ +public class Membership +{ + protected static final MemberImpl[] EMPTY_MEMBERS = new MemberImpl[0]; + + /** + * The name of this membership, has to be the same as the name for the local + * member + */ + protected MemberImpl local; + + /** + * A map of all the members in the cluster. + */ + protected HashMap map = new HashMap(); + + /** + * A list of all the members in the cluster. + */ + protected MemberImpl[] members = EMPTY_MEMBERS; + + /** + * sort members by alive time + */ + protected Comparator memberComparator = new MemberComparator(); + + public Object clone() { + synchronized (members) { + Membership clone = new Membership(local, memberComparator); + clone.map = (HashMap) map.clone(); + clone.members = new MemberImpl[members.length]; + System.arraycopy(members,0,clone.members,0,members.length); + return clone; + } + } + + /** + * Constructs a new membership + * @param name - has to be the name of the local member. Used to filter the local member from the cluster membership + */ + public Membership(MemberImpl local, boolean includeLocal) { + this.local = local; + if ( includeLocal ) addMember(local); + } + + public Membership(MemberImpl local) { + this(local,false); + } + + public Membership(MemberImpl local, Comparator comp) { + this(local,comp,false); + } + + public Membership(MemberImpl local, Comparator comp, boolean includeLocal) { + this(local,includeLocal); + this.memberComparator = comp; + } + /** + * Reset the membership and start over fresh. + * Ie, delete all the members and wait for them to ping again and join this membership + */ + public synchronized void reset() { + map.clear(); + members = EMPTY_MEMBERS ; + } + + /** + * Notify the membership that this member has announced itself. + * + * @param member - the member that just pinged us + * @return - true if this member is new to the cluster, false otherwise. + * @return - false if this member is the local member or updated. + */ + public synchronized boolean memberAlive(MemberImpl member) { + boolean result = false; + //ignore ourselves + if ( member.equals(local) ) return result; + + //return true if the membership has changed + MbrEntry entry = (MbrEntry)map.get(member); + if ( entry == null ) { + entry = addMember(member); + result = true; + } else { + //update the member alive time + MemberImpl updateMember = entry.getMember() ; + if(updateMember.getMemberAliveTime() != member.getMemberAliveTime()) { + //update fields that can change + updateMember.setMemberAliveTime(member.getMemberAliveTime()); + updateMember.setPayload(member.getPayload()); + updateMember.setCommand(member.getCommand()); + Arrays.sort(members, memberComparator); + } + } + entry.accessed(); + return result; + } + + /** + * Add a member to this component and sort array with memberComparator + * @param member The member to add + */ + public synchronized MbrEntry addMember(MemberImpl member) { + synchronized (members) { + MbrEntry entry = new MbrEntry(member); + if (!map.containsKey(member) ) { + map.put(member, entry); + MemberImpl results[] = new MemberImpl[members.length + 1]; + for (int i = 0; i < members.length; i++) results[i] = members[i]; + results[members.length] = member; + members = results; + Arrays.sort(members, memberComparator); + } + return entry; + } + } + + /** + * Remove a member from this component. + * + * @param member The member to remove + */ + public void removeMember(MemberImpl member) { + map.remove(member); + synchronized (members) { + int n = -1; + for (int i = 0; i < members.length; i++) { + if (members[i] == member || members[i].equals(member)) { + n = i; + break; + } + } + if (n < 0) return; + MemberImpl results[] = new MemberImpl[members.length - 1]; + int j = 0; + for (int i = 0; i < members.length; i++) { + if (i != n) + results[j++] = members[i]; + } + members = results; + } + } + + /** + * Runs a refresh cycle and returns a list of members that has expired. + * This also removes the members from the membership, in such a way that + * getMembers() = getMembers() - expire() + * @param maxtime - the max time a member can remain unannounced before it is considered dead. + * @return the list of expired members + */ + public synchronized MemberImpl[] expire(long maxtime) { + if(!hasMembers() ) + return EMPTY_MEMBERS; + + ArrayList list = null; + Iterator i = map.values().iterator(); + while(i.hasNext()) { + MbrEntry entry = (MbrEntry)i.next(); + if( entry.hasExpired(maxtime) ) { + if(list == null) // only need a list when members are expired (smaller gc) + list = new java.util.ArrayList(); + list.add(entry.getMember()); + } + } + + if(list != null) { + MemberImpl[] result = new MemberImpl[list.size()]; + list.toArray(result); + for( int j=0; j 0 ; + } + + + public MemberImpl getMember(Member mbr) { + if(hasMembers()) { + MemberImpl result = null; + for ( int i=0; i maxtime; + } + } +} diff --git a/java/org/apache/catalina/tribes/membership/mbeans-descriptors.xml b/java/org/apache/catalina/tribes/membership/mbeans-descriptors.xml new file mode 100644 index 000000000..d6e3fc4fb --- /dev/null +++ b/java/org/apache/catalina/tribes/membership/mbeans-descriptors.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/tribes/package.html b/java/org/apache/catalina/tribes/package.html new file mode 100644 index 000000000..85a60e950 --- /dev/null +++ b/java/org/apache/catalina/tribes/package.html @@ -0,0 +1,70 @@ + +Apache Tribes - The Tomcat Cluster Communication Module +

QuickStart

+

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

Interfaces for the Application Developer

+
    +
  1. org.apache.catalina.tribes.Channel + Main component to interact with to send messages +
  2. +
  3. org.apache.catalina.tribes.MembershipListener + Listen to membership changes +
  4. +
  5. org.apache.catalina.tribes.ChannelListener + Listen to data messages +
  6. +
  7. org.apache.catalina.tribes.Member + Identifies a node, implementation specific, default is org.apache.catalina.tribes.membership.MemberImpl +
  8. +
+

Interfaces for the Tribes Component Developer

+
    +
  1. org.apache.catalina.tribes.Channel + Main component to that the application interacts with +
  2. +
  3. org.apache.catalina.tribes.ChannelReceiver + IO Component to receive messages over some network transport +
  4. +
  5. org.apache.catalina.tribes.ChannelSender + IO Component to send messages over some network transport +
  6. +
  7. org.apache.catalina.tribes.MembershipService + IO Component that handles membership discovery and +
  8. +
  9. org.apache.catalina.tribes.ChannelInterceptor + interceptors between the Channel and the IO layer +
  10. +
  11. org.apache.catalina.tribes.ChannelMessage + The message that is sent through the interceptor stack down to the IO layer +
  12. + +
  13. org.apache.catalina.tribes.Member + Identifies a node, implementation specific to the underlying IO logic +
  14. +
+ diff --git a/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java b/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java new file mode 100644 index 000000000..13459e401 --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java @@ -0,0 +1,1271 @@ +/* + * 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.tipis; + +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.Heartbeat; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.group.Response; +import org.apache.catalina.tribes.group.RpcCallback; +import org.apache.catalina.tribes.group.RpcChannel; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.membership.MemberImpl; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.catalina.tribes.util.Arrays; +import java.util.ConcurrentModificationException; + +/** + *

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; i + * @param complete - if set to true, the object is replicated to its backup + * if set to false, only objects that implement ReplicatedMapEntry and the isDirty() returns true will + * be replicated + */ + public void replicate(Object key, boolean complete) { + if ( log.isTraceEnabled() ) + log.trace("Replicate invoked on key:"+key); + MapEntry entry = (MapEntry)super.get(key); + if ( !entry.isSerializable() ) return; + if (entry != null && entry.isPrimary() && entry.getBackupNodes()!= null && entry.getBackupNodes().length > 0) { + Object value = entry.getValue(); + //check to see if we need to replicate this object isDirty()||complete + boolean repl = complete || ( (value instanceof ReplicatedMapEntry) && ( (ReplicatedMapEntry) value).isDirty()); + + if (!repl) { + if ( log.isTraceEnabled() ) + log.trace("Not replicating:"+key+", no change made"); + + return; + } + //check to see if the message is diffable + boolean diff = ( (value instanceof ReplicatedMapEntry) && ( (ReplicatedMapEntry) value).isDiffable()); + MapMessage msg = null; + if (diff) { + ReplicatedMapEntry rentry = (ReplicatedMapEntry)entry.getValue(); + try { + rentry.lock(); + //construct a diff message + msg = new MapMessage(mapContextName, MapMessage.MSG_BACKUP, + true, (Serializable) entry.getKey(), null, + rentry.getDiff(), + entry.getBackupNodes()); + } catch (IOException x) { + log.error("Unable to diff object. Will replicate the entire object instead.", x); + } finally { + rentry.unlock(); + } + + } + if (msg == null) { + //construct a complete + msg = new MapMessage(mapContextName, MapMessage.MSG_BACKUP, + false, (Serializable) entry.getKey(), + (Serializable) entry.getValue(), + null, entry.getBackupNodes()); + + } + try { + if ( channel!=null && entry.getBackupNodes()!= null && entry.getBackupNodes().length > 0 ) { + channel.send(entry.getBackupNodes(), msg, channelSendOptions); + } + } catch (ChannelException x) { + log.error("Unable to replicate data.", x); + } + } //end if + + } + + /** + * This can be invoked by a periodic thread to replicate out any changes. + * For maps that don't store objects that implement ReplicatedMapEntry, this + * method should be used infrequently to avoid large amounts of data transfer + * @param complete boolean + */ + public void replicate(boolean complete) { + Iterator i = super.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry e = (Map.Entry) i.next(); + replicate(e.getKey(), complete); + } //while + + } + + public void transferState() { + try { + Member[] members = getMapMembers(); + Member backup = members.length > 0 ? (Member) members[0] : null; + if (backup != null) { + MapMessage msg = new MapMessage(mapContextName, MapMessage.MSG_STATE, false, + null, null, null, null); + Response[] resp = rpcChannel.send(new Member[] {backup}, msg, rpcChannel.FIRST_REPLY, channelSendOptions, rpcTimeout); + if (resp.length > 0) { + synchronized (stateMutex) { + msg = (MapMessage) resp[0].getMessage(); + msg.deserialize(getExternalLoaders()); + ArrayList list = (ArrayList) msg.getValue(); + for (int i = 0; i < list.size(); i++) { + messageReceived( (Serializable) list.get(i), resp[0].getSource()); + } //for + } + } else { + log.warn("Transfer state, 0 replies, probably a timeout."); + } + } + } catch (ChannelException x) { + log.error("Unable to transfer LazyReplicatedMap state.", x); + } catch (IOException x) { + log.error("Unable to transfer LazyReplicatedMap state.", x); + } catch (ClassNotFoundException x) { + log.error("Unable to transfer LazyReplicatedMap state.", x); + } + stateTransferred = true; + } + + /** + * @todo implement state transfer + * @param msg Serializable + * @return Serializable - null if no reply should be sent + */ + public Serializable replyRequest(Serializable msg, final Member sender) { + if (! (msg instanceof MapMessage))return null; + MapMessage mapmsg = (MapMessage) msg; + + //map init request + if (mapmsg.getMsgType() == mapmsg.MSG_INIT) { + mapmsg.setBackUpNodes(wrap(channel.getLocalMember(false))); + return mapmsg; + } + + //map start request + if (mapmsg.getMsgType() == mapmsg.MSG_START) { + mapmsg.setBackUpNodes(wrap(channel.getLocalMember(false))); + mapMemberAdded(sender); + return mapmsg; + } + + //backup request + if (mapmsg.getMsgType() == mapmsg.MSG_RETRIEVE_BACKUP) { + MapEntry entry = (MapEntry)super.get(mapmsg.getKey()); + if (entry == null || (!entry.isSerializable()) )return null; + mapmsg.setValue( (Serializable) entry.getValue()); + return mapmsg; + } + + //state transfer request + if (mapmsg.getMsgType() == mapmsg.MSG_STATE) { + synchronized (stateMutex) { //make sure we dont do two things at the same time + ArrayList list = new ArrayList(); + Iterator i = super.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry e = (Map.Entry) i.next(); + MapEntry entry = (MapEntry) e.getValue(); + if ( entry.isSerializable() ) { + MapMessage me = new MapMessage(mapContextName, MapMessage.MSG_PROXY, + false, (Serializable) entry.getKey(), null, null, entry.getBackupNodes()); + list.add(me); + } + } + mapmsg.setValue(list); + return mapmsg; + + } //synchronized + } + + return null; + + } + + /** + * If the reply has already been sent to the requesting thread, + * the rpc callback can handle any data that comes in after the fact. + * @param msg Serializable + * @param sender Member + */ + public void leftOver(Serializable msg, Member sender) { + //left over membership messages + if (! (msg instanceof MapMessage))return; + + MapMessage mapmsg = (MapMessage) msg; + try { + mapmsg.deserialize(getExternalLoaders()); + if (mapmsg.getMsgType() == MapMessage.MSG_START) { + mapMemberAdded(mapmsg.getBackupNodes()[0]); + } else if (mapmsg.getMsgType() == MapMessage.MSG_INIT) { + memberAlive(mapmsg.getBackupNodes()[0]); + } + } catch (IOException x ) { + log.error("Unable to deserialize MapMessage.",x); + } catch (ClassNotFoundException x ) { + log.error("Unable to deserialize MapMessage.",x); + } + } + + public void messageReceived(Serializable msg, Member sender) { + if (! (msg instanceof MapMessage)) return; + + MapMessage mapmsg = (MapMessage) msg; + if ( log.isTraceEnabled() ) { + log.trace("Map["+mapname+"] received message:"+mapmsg); + } + + try { + mapmsg.deserialize(getExternalLoaders()); + } catch (IOException x) { + log.error("Unable to deserialize MapMessage.", x); + return; + } catch (ClassNotFoundException x) { + log.error("Unable to deserialize MapMessage.", x); + return; + } + if ( log.isTraceEnabled() ) + log.trace("Map message received from:"+sender.getName()+" msg:"+mapmsg); + if (mapmsg.getMsgType() == MapMessage.MSG_START) { + mapMemberAdded(mapmsg.getBackupNodes()[0]); + } + + if (mapmsg.getMsgType() == MapMessage.MSG_STOP) { + memberDisappeared(mapmsg.getBackupNodes()[0]); + } + + if (mapmsg.getMsgType() == MapMessage.MSG_PROXY) { + MapEntry entry = (MapEntry)super.get(mapmsg.getKey()); + if ( entry==null ) { + entry = new MapEntry(mapmsg.getKey(), mapmsg.getValue()); + entry.setBackup(false); + entry.setProxy(true); + entry.setBackupNodes(mapmsg.getBackupNodes()); + super.put(entry.getKey(), entry); + } else { + entry.setProxy(true); + entry.setBackup(false); + entry.setBackupNodes(mapmsg.getBackupNodes()); + } + } + + if (mapmsg.getMsgType() == MapMessage.MSG_REMOVE) { + super.remove(mapmsg.getKey()); + } + + if (mapmsg.getMsgType() == MapMessage.MSG_BACKUP) { + MapEntry entry = (MapEntry)super.get(mapmsg.getKey()); + if (entry == null) { + entry = new MapEntry(mapmsg.getKey(), mapmsg.getValue()); + entry.setBackup(true); + entry.setProxy(false); + entry.setBackupNodes(mapmsg.getBackupNodes()); + if (mapmsg.getValue()!=null && mapmsg.getValue() instanceof ReplicatedMapEntry ) { + ((ReplicatedMapEntry)mapmsg.getValue()).setOwner(getMapOwner()); + } + } else { + entry.setBackup(true); + entry.setProxy(false); + entry.setBackupNodes(mapmsg.getBackupNodes()); + if (entry.getValue() instanceof ReplicatedMapEntry) { + ReplicatedMapEntry diff = (ReplicatedMapEntry) entry.getValue(); + if (mapmsg.isDiff()) { + try { + diff.lock(); + diff.applyDiff(mapmsg.getDiffValue(), 0, mapmsg.getDiffValue().length); + } catch (Exception x) { + log.error("Unable to apply diff to key:" + entry.getKey(), x); + } finally { + diff.unlock(); + } + } else { + if ( mapmsg.getValue()!=null ) entry.setValue(mapmsg.getValue()); + ((ReplicatedMapEntry)entry.getValue()).setOwner(getMapOwner()); + } //end if + } else if (mapmsg.getValue() instanceof ReplicatedMapEntry) { + ReplicatedMapEntry re = (ReplicatedMapEntry)mapmsg.getValue(); + re.setOwner(getMapOwner()); + entry.setValue(re); + } else { + if ( mapmsg.getValue()!=null ) entry.setValue(mapmsg.getValue()); + } //end if + } //end if + super.put(entry.getKey(), entry); + } //end if + } + + public boolean accept(Serializable msg, Member sender) { + boolean result = false; + if (msg instanceof MapMessage) { + if ( log.isTraceEnabled() ) log.trace("Map["+mapname+"] accepting...."+msg); + result = Arrays.equals(mapContextName, ( (MapMessage) msg).getMapId()); + if ( log.isTraceEnabled() ) log.trace("Msg["+mapname+"] accepted["+result+"]...."+msg); + } + return result; + } + + public void mapMemberAdded(Member member) { + if ( member.equals(getChannel().getLocalMember(false)) ) return; + boolean memberAdded = false; + //select a backup node if we don't have one + synchronized (mapMembers) { + if (!mapMembers.containsKey(member) ) { + mapMembers.put(member, new Long(System.currentTimeMillis())); + memberAdded = true; + } + } + if ( memberAdded ) { + synchronized (stateMutex) { + Iterator i = super.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry e = (Map.Entry) i.next(); + MapEntry entry = (MapEntry) e.getValue(); + if ( entry == null ) continue; + if (entry.isPrimary() && (entry.getBackupNodes() == null || entry.getBackupNodes().length == 0)) { + try { + Member[] backup = publishEntryInfo(entry.getKey(), entry.getValue()); + entry.setBackupNodes(backup); + } catch (ChannelException x) { + log.error("Unable to select backup node.", x); + } //catch + } //end if + } //while + } //synchronized + }//end if + } + + public boolean inSet(Member m, Member[] set) { + if ( set == null ) return false; + boolean result = false; + for (int i=0; i= size) { + node = 0; + currentNode = 0; + } + return node; + } + public Member getNextBackupNode() { + Member[] members = getMapMembers(); + int node = getNextBackupIndex(); + if ( members.length == 0 || node==-1) return null; + if ( node >= members.length ) node = 0; + return members[node]; + } + + protected abstract Member[] publishEntryInfo(Object key, Object value) throws ChannelException; + + public void heartbeat() { + try { + ping(accessTimeout); + }catch ( Exception x ) { + log.error("Unable to send AbstractReplicatedMap.ping message",x); + } + } + +//------------------------------------------------------------------------------ +// METHODS TO OVERRIDE +//------------------------------------------------------------------------------ + + /** + * Removes an object from this map, it will also remove it from + * + * @param key Object + * @return Object + */ + public Object remove(Object key) { + MapEntry entry = (MapEntry)super.remove(key); + + try { + MapMessage msg = new MapMessage(getMapContextName(),MapMessage.MSG_REMOVE,false,(Serializable)key,null,null,null); + getChannel().send(getMapMembers(), msg,getChannelSendOptions()); + } catch ( ChannelException x ) { + log.error("Unable to replicate out data for a LazyReplicatedMap.remove operation",x); + } + return entry!=null?entry.getValue():null; + } + + public Object get(Object key) { + MapEntry entry = (MapEntry)super.get(key); + if (log.isTraceEnabled()) log.trace("Requesting id:"+key+" entry:"+entry); + if ( entry == null ) return null; + if ( !entry.isPrimary() ) { + //if the message is not primary, we need to retrieve the latest value + try { + Member[] backup = null; + MapMessage msg = null; + if ( !entry.isBackup() ) { + //make sure we don't retrieve from ourselves + msg = new MapMessage(getMapContextName(), MapMessage.MSG_RETRIEVE_BACKUP, false, + (Serializable) key, null, null, null); + Response[] resp = getRpcChannel().send(entry.getBackupNodes(),msg, this.getRpcChannel().FIRST_REPLY, Channel.SEND_OPTIONS_DEFAULT, getRpcTimeout()); + if (resp == null || resp.length == 0) { + //no responses + log.warn("Unable to retrieve remote object for key:" + key); + return null; + } + msg = (MapMessage) resp[0].getMessage(); + msg.deserialize(getExternalLoaders()); + backup = entry.getBackupNodes(); + if ( entry.getValue() instanceof ReplicatedMapEntry ) { + ReplicatedMapEntry val = (ReplicatedMapEntry)entry.getValue(); + val.setOwner(getMapOwner()); + } + if ( msg.getValue()!=null ) entry.setValue(msg.getValue()); + } + if (entry.isBackup()) { + //select a new backup node + backup = publishEntryInfo(key, entry.getValue()); + } else if ( entry.isProxy() ) { + //invalidate the previous primary + msg = new MapMessage(getMapContextName(),MapMessage.MSG_PROXY,false,(Serializable)key,null,null,backup); + Member[] dest = getMapMembersExcl(backup); + if ( dest!=null && dest.length >0) { + getChannel().send(dest, msg, getChannelSendOptions()); + } + } + + entry.setBackupNodes(backup); + entry.setBackup(false); + entry.setProxy(false); + + + } catch (Exception x) { + log.error("Unable to replicate out data for a LazyReplicatedMap.get operation", x); + return null; + } + } + if (log.isTraceEnabled()) log.trace("Requesting id:"+key+" result:"+entry.getValue()); + return entry.getValue(); + } + + + protected void printMap(String header) { + try { + System.out.println("\nDEBUG MAP:"+header); + System.out.println("Map["+ new String(mapContextName, chset) + ", Map Size:" + super.size()); + Member[] mbrs = getMapMembers(); + for ( int i=0; iget(key) + * will make this entry primary for the group + * @param key Object + * @return boolean + */ + public boolean containsKey(Object key) { + return super.containsKey(key); + } + + + public Object put(Object key, Object value) { + MapEntry entry = new MapEntry(key,value); + entry.setBackup(false); + entry.setProxy(false); + + Object old = null; + + //make sure that any old values get removed + if ( containsKey(key) ) old = remove(key); + try { + Member[] backup = publishEntryInfo(key, value); + entry.setBackupNodes(backup); + } catch (ChannelException x) { + log.error("Unable to replicate out data for a LazyReplicatedMap.put operation", x); + } + super.put(key,entry); + return old; + } + + + /** + * Copies all values from one map to this instance + * @param m Map + */ + public void putAll(Map m) { + Iterator i = m.entrySet().iterator(); + while ( i.hasNext() ) { + Map.Entry entry = (Map.Entry)i.next(); + put(entry.getKey(),entry.getValue()); + } + } + + public void clear() { + //only delete active keys + Iterator keys = keySet().iterator(); + while ( keys.hasNext() ) remove(keys.next()); + } + + public boolean containsValue(Object value) { + if ( value == null ) { + return super.containsValue(value); + } else { + Iterator i = super.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry e = (Map.Entry) i.next(); + MapEntry entry = (MapEntry) e.getValue(); + if (entry.isPrimary() && value.equals(entry.getValue())) return true; + }//while + return false; + }//end if + } + + public Object clone() { + throw new UnsupportedOperationException("This operation is not valid on a replicated map"); + } + + /** + * Returns the entire contents of the map + * Map.Entry.getValue() will return a LazyReplicatedMap.MapEntry object containing all the information + * about the object. + * @return Set + */ + public Set entrySetFull() { + return super.entrySet(); + } + + public Set keySetFull() { + return super.keySet(); + } + + public int sizeFull() { + return super.size(); + } + + public Set entrySet() { + LinkedHashSet set = new LinkedHashSet(super.size()); + Iterator i = super.entrySet().iterator(); + while ( i.hasNext() ) { + Map.Entry e = (Map.Entry)i.next(); + MapEntry entry = (MapEntry)e.getValue(); + if ( entry.isPrimary() ) set.add(entry); + } + return Collections.unmodifiableSet(set); + } + + public Set keySet() { + //todo implement + //should only return keys where this is active. + LinkedHashSet set = new LinkedHashSet(super.size()); + Iterator i = super.entrySet().iterator(); + while ( i.hasNext() ) { + Map.Entry e = (Map.Entry)i.next(); + MapEntry entry = (MapEntry)e.getValue(); + if ( entry.isPrimary() ) set.add(entry.getKey()); + } + return Collections.unmodifiableSet(set); + } + + + public int size() { + //todo, implement a counter variable instead + //only count active members in this node + int counter = 0; + Object[] items = super.entrySet().toArray(); + for (int i=0; i 0) members[i] = MemberImpl.getMember(d); + } + return members; + } + + protected void writeMembers(ObjectOutput out,Member[] members) throws IOException { + if ( members == null ) members = new Member[0]; + out.writeInt(members.length); + for (int i=0; i + * A perfect usage for this map would be a session map for a session manager in a clustered environment.
+ * The only way to modify this list is to use the put, putAll, remove methods. + * entrySet, entrySetFull, keySet, keySetFull, returns all non modifiable sets.

+ * If objects (values) in the map change without invoking put() or remove() + * the data can be distributed using two different methods:
+ * replicate(boolean) and replicate(Object, boolean)
+ * These two methods are very important two understand. The map can work with two set of value objects:
+ * 1. Serializable - the entire object gets serialized each time it is replicated
+ * 2. ReplicatedMapEntry - this interface allows for a isDirty() flag and to replicate diffs if desired.
+ * Implementing the ReplicatedMapEntry interface allows you to decide what objects + * get replicated and how much data gets replicated each time.
+ * If you implement a smart AOP mechanism to detect changes in underlying objects, you can replicate + * only those changes by implementing the ReplicatedMapEntry interface, and return true when isDiffable() + * is invoked.

+ * + * This map implementation doesn't have a background thread running to replicate changes. + * If you do have changes without invoking put/remove then you need to invoke one of the following methods: + *
    + *
  • replicate(Object,boolean) - replicates only the object that belongs to the key
  • + *
  • replicate(boolean) - Scans the entire map for changes and replicates data
  • + *
+ * the boolean 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. + * + *

REMBER TO CALL breakdown() or finalize() when you are done with the map to + * avoid memory leaks.

+ * @todo implement periodic sync/transfer thread + * @author Filip Hanik + * @version 1.0 + */ +public class LazyReplicatedMap extends AbstractReplicatedMap + implements RpcCallback, ChannelListener, MembershipListener { + protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(LazyReplicatedMap.class); + + + +//------------------------------------------------------------------------------ +// CONSTRUCTORS / DESTRUCTORS +//------------------------------------------------------------------------------ + /** + * 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 + */ + public LazyReplicatedMap(Object owner, Channel channel, long timeout, String mapContextName, int initialCapacity, float loadFactor, ClassLoader[] cls) { + super(owner,channel,timeout,mapContextName,initialCapacity,loadFactor, Channel.SEND_OPTIONS_DEFAULT,cls); + } + + /** + * 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 + */ + public LazyReplicatedMap(Object owner, Channel channel, long timeout, String mapContextName, int initialCapacity, ClassLoader[] cls) { + super(owner, channel,timeout,mapContextName,initialCapacity, LazyReplicatedMap.DEFAULT_LOAD_FACTOR, Channel.SEND_OPTIONS_DEFAULT, cls); + } + + /** + * 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 + */ + public LazyReplicatedMap(Object owner, Channel channel, long timeout, String mapContextName, ClassLoader[] cls) { + super(owner, channel,timeout,mapContextName, LazyReplicatedMap.DEFAULT_INITIAL_CAPACITY,LazyReplicatedMap.DEFAULT_LOAD_FACTOR,Channel.SEND_OPTIONS_DEFAULT, cls); + } + + + + + +//------------------------------------------------------------------------------ +// METHODS TO OVERRIDE +//------------------------------------------------------------------------------ + /** + * publish info about a map pair (key/value) to other nodes in the cluster + * @param key Object + * @param value Object + * @return Member - the backup node + * @throws ChannelException + */ + protected Member[] publishEntryInfo(Object key, Object value) throws ChannelException { + if (! (key instanceof Serializable && value instanceof Serializable) ) return new Member[0]; + Member[] members = getMapMembers(); + int firstIdx = getNextBackupIndex(); + int nextIdx = firstIdx; + Member[] backup = new Member[0]; + + //there are no backups + if ( members.length == 0 || firstIdx == -1 ) return backup; + + boolean success = false; + do { + //select a backup node + Member next = members[firstIdx]; + + //increment for the next round of back up selection + nextIdx = firstIdx + 1; + if ( nextIdx >= members.length ) nextIdx = 0; + + if (next == null) { + continue; + } + MapMessage msg = null; + try { + backup = wrap(next); + //publish the backup data to one node + msg = new MapMessage(getMapContextName(), MapMessage.MSG_BACKUP, false, + (Serializable) key, (Serializable) value, null, backup); + if ( log.isTraceEnabled() ) + log.trace("Publishing backup data:"+msg+" to: "+next.getName()); + UniqueId id = getChannel().send(backup, msg, getChannelSendOptions()); + if ( log.isTraceEnabled() ) + log.trace("Data published:"+msg+" msg Id:"+id); + //we published out to a backup, mark the test success + success = true; + }catch ( ChannelException x ) { + log.error("Unable to replicate backup key:"+key+" to backup:"+next+". Reason:"+x.getMessage(),x); + } + try { + //publish the data out to all nodes + Member[] proxies = excludeFromSet(backup, getMapMembers()); + if (success && proxies.length > 0 ) { + msg = new MapMessage(getMapContextName(), MapMessage.MSG_PROXY, false, + (Serializable) key, null, null, backup); + if ( log.isTraceEnabled() ) + log.trace("Publishing proxy data:"+msg+" to: "+Arrays.toNameString(proxies)); + getChannel().send(proxies, msg, getChannelSendOptions()); + } + }catch ( ChannelException x ) { + //log the error, but proceed, this should only happen if a node went down, + //and if the node went down, then it can't receive the message, the others + //should still get it. + log.error("Unable to replicate proxy key:"+key+" to backup:"+next+". Reason:"+x.getMessage(),x); + } + } while ( !success && (firstIdx!=nextIdx)); + return backup; + } + + + + + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/tipis/ReplicatedMap.java b/java/org/apache/catalina/tribes/tipis/ReplicatedMap.java new file mode 100644 index 000000000..01bb3ba61 --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/ReplicatedMap.java @@ -0,0 +1,117 @@ +/* + * 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.tipis; + +import java.io.Serializable; + +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelListener; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.MembershipListener; +import org.apache.catalina.tribes.group.RpcCallback; + +/** + * All-to-all replication for a hash map implementation. Each node in the cluster will carry an identical + * copy of the map.

+ * This map implementation doesn't have a background thread running to replicate changes. + * If you do have changes without invoking put/remove then you need to invoke one of the following methods: + *
    + *
  • replicate(Object,boolean) - replicates only the object that belongs to the key
  • + *
  • replicate(boolean) - Scans the entire map for changes and replicates data
  • + *
+ * the boolean 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. + * + *

REMBER TO CALL breakdown() or finalize() when you are done with the map to + * avoid memory leaks.

+ * @todo implement periodic sync/transfer thread + * @author Filip Hanik + * @version 1.0 + * + * @todo memberDisappeared, should do nothing except change map membership + * by default it relocates the primary objects + */ +public class ReplicatedMap extends AbstractReplicatedMap implements RpcCallback, ChannelListener, MembershipListener { + + protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(ReplicatedMap.class); + +//------------------------------------------------------------------------------ +// CONSTRUCTORS / DESTRUCTORS +//------------------------------------------------------------------------------ + /** + * 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 + */ + public ReplicatedMap(Object owner, Channel channel, long timeout, String mapContextName, int initialCapacity,float loadFactor, ClassLoader[] cls) { + super(owner,channel, timeout, mapContextName, initialCapacity, loadFactor, Channel.SEND_OPTIONS_DEFAULT, cls); + } + + /** + * 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 + */ + public ReplicatedMap(Object owner, Channel channel, long timeout, String mapContextName, int initialCapacity, ClassLoader[] cls) { + super(owner,channel, timeout, mapContextName, initialCapacity, AbstractReplicatedMap.DEFAULT_LOAD_FACTOR,Channel.SEND_OPTIONS_DEFAULT, cls); + } + + /** + * 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 + */ + public ReplicatedMap(Object owner, Channel channel, long timeout, String mapContextName, ClassLoader[] cls) { + super(owner, channel, timeout, mapContextName,AbstractReplicatedMap.DEFAULT_INITIAL_CAPACITY, AbstractReplicatedMap.DEFAULT_LOAD_FACTOR, Channel.SEND_OPTIONS_DEFAULT, cls); + } + +//------------------------------------------------------------------------------ +// METHODS TO OVERRIDE +//------------------------------------------------------------------------------ + /** + * publish info about a map pair (key/value) to other nodes in the cluster + * @param key Object + * @param value Object + * @return Member - the backup node + * @throws ChannelException + */ + protected Member[] publishEntryInfo(Object key, Object value) throws ChannelException { + if (! (key instanceof Serializable && value instanceof Serializable) ) return new Member[0]; + //select a backup node + Member[] backup = getMapMembers(); + + if (backup == null || backup.length == 0) return null; + + //publish the data out to all nodes + MapMessage msg = new MapMessage(getMapContextName(), MapMessage.MSG_BACKUP, false, + (Serializable) key, null, null, backup); + + getChannel().send(getMapMembers(), msg, getChannelSendOptions()); + + return backup; + } + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/tipis/ReplicatedMapEntry.java b/java/org/apache/catalina/tribes/tipis/ReplicatedMapEntry.java new file mode 100644 index 000000000..bc5b4045a --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/ReplicatedMapEntry.java @@ -0,0 +1,123 @@ +/* + * 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.tipis; + +import java.io.IOException; +import java.io.Serializable; + +/** + * + * For smarter replication, an object can implement this interface to replicate diffs
+ * The replication logic will call the methods in the following order:
+ * + * 1. if ( entry.isDirty() )
+ * try { + * 2. entry.lock();
+ * 3. byte[] diff = entry.getDiff();
+ * 4. entry.reset();
+ * } finally {
+ * 5. entry.unlock();
+ * }
+ * }
+ *
+ *
+ *
+ * When the data is deserialized the logic is called in the following order
+ * + * 1. ReplicatedMapEntry entry = (ReplicatedMapEntry)objectIn.readObject();
+ * 2. if ( isBackup(entry)||isPrimary(entry) ) entry.setOwner(owner);
+ *
+ *
+ * + * + * @author Filip Hanik + * @version 1.0 + */ +public interface ReplicatedMapEntry extends Serializable { + + /** + * Has the object changed since last replication + * and is not in a locked state + * @return boolean + */ + public boolean isDirty(); + + /** + * If this returns true, the map will extract the diff using getDiff() + * Otherwise it will serialize the entire object. + * @return boolean + */ + public boolean isDiffable(); + + /** + * Returns a diff and sets the dirty map to false + * @return byte[] + * @throws IOException + */ + public byte[] getDiff() throws IOException; + + + /** + * Applies a diff to an existing object. + * @param diff byte[] + * @param offset int + * @param length int + * @throws IOException + */ + public void applyDiff(byte[] diff, int offset, int length) throws IOException, ClassNotFoundException; + + /** + * Resets the current diff state and resets the dirty flag + */ + public void resetDiff(); + + /** + * Lock during serialization + */ + public void lock(); + + /** + * Unlock after serialization + */ + public void unlock(); + + /** + * This method is called after the object has been + * created on a remote map. On this method, + * the object can initialize itself for any data that wasn't + * + * @param owner Object + */ + public void setOwner(Object owner); + + /** + * For accuracy checking, a serialized attribute can contain a version number + * This number increases as modifications are made to the data. + * The replicated map can use this to ensure accuracy on a periodic basis + * @return long - the version number or -1 if the data is not versioned + */ + public long getVersion(); + + /** + * Forces a certain version to a replicated map entry
+ * @param version long + */ + public void setVersion(long version); + + + + +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/tipis/Streamable.java b/java/org/apache/catalina/tribes/tipis/Streamable.java new file mode 100644 index 000000000..e1e7c9b75 --- /dev/null +++ b/java/org/apache/catalina/tribes/tipis/Streamable.java @@ -0,0 +1,60 @@ +/* + * 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.tipis; + +import java.io.IOException; + +/** + * Example usage: + *
+ * 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 the org.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= 0 ) { + int count = reader.append(buf,0,length,true); + if ( count > 0 ) execute(reader); + length = in.read(buf); + } + } + + + + + /** + * send a reply-acknowledgement (6,2,3) + * @param key + * @param channel + */ + protected void sendAck(byte[] command) { + try { + OutputStream out = socket.getOutputStream(); + out.write(command); + out.flush(); + if (log.isTraceEnabled()) { + log.trace("ACK sent to " + socket.getPort()); + } + } catch ( java.io.IOException x ) { + log.warn("Unable to send ACK back through channel, channel disconnected?: "+x.getMessage()); + } + } + + public void close() { + setDoRun(false); + try {socket.close();}catch ( Exception ignore){} + try {reader.close();}catch ( Exception ignore){} + reader = null; + socket = null; + super.close(); + } +} diff --git a/java/org/apache/catalina/tribes/transport/bio/BioSender.java b/java/org/apache/catalina/tribes/transport/bio/BioSender.java new file mode 100644 index 000000000..8a1aead8e --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/bio/BioSender.java @@ -0,0 +1,305 @@ +/* + * 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.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Arrays; + +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.RemoteProcessException; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.transport.AbstractSender; +import org.apache.catalina.tribes.transport.Constants; +import org.apache.catalina.tribes.transport.DataSender; +import org.apache.catalina.tribes.transport.SenderState; +import org.apache.catalina.tribes.util.StringManager; + +/** + * Send cluster messages with only one socket. Ack and keep Alive Handling is + * supported + * + * @author Peter Rossbach + * @author Filip Hanik + * @version $Revision: 377484 $ $Date: 2006-02-13 15:00:05 -0600 (Mon, 13 Feb 2006) $ + * @since 5.5.16 + */ +public class BioSender extends AbstractSender implements DataSender { + + private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(BioSender.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 = "DataSender/3.0"; + + + /** + * current sender socket + */ + private Socket socket = null; + private OutputStream soOut = null; + private InputStream soIn = null; + + protected XByteBuffer ackbuf = new XByteBuffer(Constants.ACK_COMMAND.length,true); + + + // ------------------------------------------------------------- Constructor + + public BioSender() { + } + + + // ------------------------------------------------------------- Properties + + /** + * Return descriptive information about this implementation and the + * corresponding version number, in the format + * <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()Title:

+ * + *

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; iTitle:

+ * + *

Description:

+ * + *

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.
Limit the queue + * length when you have strange producer thread problemes. + * + * FIXME add i18n support to log messages + * @author Rainer Jung + * @author Peter Rossbach + * @version $Revision: 345567 $ $Date: 2005-11-18 15:07:23 -0600 (Fri, 18 Nov 2005) $ + */ +public class FastQueue { + + private static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(FastQueue.class); + + /** + * This is the actual queue + */ + private SingleRemoveSynchronizedAddLock lock = null; + + /** + * First Object at queue (consumer message) + */ + private LinkObject first = null; + + /** + * Last object in queue (producer Object) + */ + private LinkObject last = null; + + /** + * Current Queue elements size + */ + private int size = 0; + + /** + * check lock to detect strange threadings things + */ + private boolean checkLock = false; + + /** + * protocol the thread wait times + */ + private boolean timeWait = false; + + private boolean inAdd = false; + + private boolean inRemove = false; + + private boolean inMutex = false; + + /** + * limit the queue legnth ( default is unlimited) + */ + private int maxQueueLength = 0; + + /** + * addWaitTimeout for producer + */ + private long addWaitTimeout = 10000L; + + + /** + * removeWaitTimeout for consumer + */ + private long removeWaitTimeout = 30000L; + + /** + * enabled the queue + */ + private boolean enabled = true; + + /** + * max queue size + */ + private int maxSize = 0; + + /** + * avg size sample interval + */ + private int sampleInterval = 100; + + /** + * Generate Queue SingleRemoveSynchronizedAddLock and set add and wait + * Timeouts + */ + public FastQueue() { + lock = new SingleRemoveSynchronizedAddLock(); + lock.setAddWaitTimeout(addWaitTimeout); + lock.setRemoveWaitTimeout(removeWaitTimeout); + } + + /** + * get current add wait timeout + * + * @return current wait timeout + */ + public long getAddWaitTimeout() { + addWaitTimeout = lock.getAddWaitTimeout(); + return addWaitTimeout; + } + + /** + * Set add wait timeout (default 10000 msec) + * + * @param timeout + */ + public void setAddWaitTimeout(long timeout) { + addWaitTimeout = timeout; + lock.setAddWaitTimeout(addWaitTimeout); + } + + /** + * get current remove wait timeout + * + * @return The timeout + */ + public long getRemoveWaitTimeout() { + removeWaitTimeout = lock.getRemoveWaitTimeout(); + return removeWaitTimeout; + } + + /** + * set remove wait timeout ( default 30000 msec) + * + * @param timeout + */ + public void setRemoveWaitTimeout(long timeout) { + removeWaitTimeout = timeout; + lock.setRemoveWaitTimeout(removeWaitTimeout); + } + + /** + * get Max Queue length + * + * @see org.apache.catalina.tribes.util.IQueue#getMaxQueueLength() + */ + public int getMaxQueueLength() { + return maxQueueLength; + } + + public void setMaxQueueLength(int length) { + maxQueueLength = length; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enable) { + enabled = enable; + if (!enabled) { + lock.abortRemove(); + last = first = null; + } + } + + /** + * @return Returns the checkLock. + */ + public boolean isCheckLock() { + return checkLock; + } + + /** + * @param checkLock The checkLock to set. + */ + public void setCheckLock(boolean checkLock) { + this.checkLock = checkLock; + } + + + /** + * @return The max size + */ + public int getMaxSize() { + return maxSize; + } + + /** + * @param size + */ + public void setMaxSize(int size) { + maxSize = size; + } + + + /** + * unlock queue for next add + */ + public void unlockAdd() { + lock.unlockAdd(size > 0 ? true : false); + } + + /** + * unlock queue for next remove + */ + public void unlockRemove() { + lock.unlockRemove(); + } + + /** + * start queuing + */ + public void start() { + setEnabled(true); + } + + /** + * start queuing + */ + public void stop() { + setEnabled(false); + } + + public int getSize() { + return size; + } + + public SingleRemoveSynchronizedAddLock getLock() { + return lock; + } + + /** + * Add new data to the queue + * @see org.apache.catalina.tribes.util.IQueue#add(java.lang.String, java.lang.Object) + * FIXME extract some method + */ + public boolean add(ChannelMessage msg, Member[] destination, InterceptorPayload payload) { + boolean ok = true; + long time = 0; + + if (!enabled) { + if (log.isInfoEnabled()) + log.info("FastQueue.add: queue disabled, add aborted"); + return false; + } + + if (timeWait) { + time = System.currentTimeMillis(); + } + lock.lockAdd(); + try { + if (log.isTraceEnabled()) { + log.trace("FastQueue.add: starting with size " + size); + } + if (checkLock) { + if (inAdd) + log.warn("FastQueue.add: Detected other add"); + inAdd = true; + if (inMutex) + log.warn("FastQueue.add: Detected other mutex in add"); + inMutex = true; + } + + if ((maxQueueLength > 0) && (size >= maxQueueLength)) { + ok = false; + if (log.isTraceEnabled()) { + log.trace("FastQueue.add: Could not add, since queue is full (" + size + ">=" + maxQueueLength + ")"); + } + } else { + LinkObject element = new LinkObject(msg,destination, payload); + if (size == 0) { + first = last = element; + size = 1; + } else { + if (last == null) { + ok = false; + log.error("FastQueue.add: Could not add, since last is null although size is "+ size + " (>0)"); + } else { + last.append(element); + last = element; + size++; + } + } + } + + if (first == null) { + log.error("FastQueue.add: first is null, size is " + size + " at end of add"); + } + if (last == null) { + log.error("FastQueue.add: last is null, size is " + size+ " at end of add"); + } + + if (checkLock) { + if (!inMutex) log.warn("FastQueue.add: Cancelled by other mutex in add"); + inMutex = false; + if (!inAdd) log.warn("FastQueue.add: Cancelled by other add"); + inAdd = false; + } + if (log.isTraceEnabled()) log.trace("FastQueue.add: add ending with size " + size); + + } finally { + lock.unlockAdd(true); + } + return ok; + } + + /** + * remove the complete queued object list + * @see org.apache.catalina.tribes.util.IQueue#remove() + * FIXME extract some method + */ + public LinkObject remove() { + LinkObject element; + boolean gotLock; + long time = 0; + + if (!enabled) { + if (log.isInfoEnabled()) + log.info("FastQueue.remove: queue disabled, remove aborted"); + return null; + } + + if (timeWait) { + time = System.currentTimeMillis(); + } + gotLock = lock.lockRemove(); + try { + + if (!gotLock) { + if (enabled) { + if (log.isInfoEnabled()) + log.info("FastQueue.remove: Remove aborted although queue enabled"); + } else { + if (log.isInfoEnabled()) + log.info("FastQueue.remove: queue disabled, remove aborted"); + } + return null; + } + + if (log.isTraceEnabled()) { + log.trace("FastQueue.remove: remove starting with size " + size); + } + if (checkLock) { + if (inRemove) + log.warn("FastQueue.remove: Detected other remove"); + inRemove = true; + if (inMutex) + log.warn("FastQueue.remove: Detected other mutex in remove"); + inMutex = true; + } + + element = first; + + first = last = null; + size = 0; + + if (checkLock) { + if (!inMutex) + log.warn("FastQueue.remove: Cancelled by other mutex in remove"); + inMutex = false; + if (!inRemove) + log.warn("FastQueue.remove: Cancelled by other remove"); + inRemove = false; + } + if (log.isTraceEnabled()) { + log.trace("FastQueue.remove: remove ending with size " + size); + } + + if (timeWait) { + time = System.currentTimeMillis(); + } + } finally { + lock.unlockRemove(); + } + return element; + } + +} diff --git a/java/org/apache/catalina/tribes/transport/bio/util/LinkObject.java b/java/org/apache/catalina/tribes/transport/bio/util/LinkObject.java new file mode 100644 index 000000000..9000f9c66 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/bio/util/LinkObject.java @@ -0,0 +1,107 @@ +/* + * 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; + +/** + * The class LinkObject implements an element + * for a linked list, consisting of a general + * data object and a pointer to the next element. + * + * @author Rainer Jung + * @author Peter Rossbach + * @author Filip Hanik + * @version $Revision: 304032 $ $Date: 2005-07-27 10:11:55 -0500 (Wed, 27 Jul 2005) $ + + */ + +public class LinkObject { + + private ChannelMessage msg; + private LinkObject next; + private byte[] key ; + private Member[] destination; + private InterceptorPayload payload; + + /** + * Construct a new element from the data object. + * Sets the pointer to null. + * + * @param key The key + * @param payload The data object. + */ + public LinkObject(ChannelMessage msg, Member[] destination, InterceptorPayload payload) { + this.msg = msg; + this.next = null; + this.key = msg.getUniqueId(); + this.payload = payload; + this.destination = destination; + } + + /** + * Set the next element. + * @param next The next element. + */ + public void append(LinkObject next) { + this.next = next; + } + + /** + * Get the next element. + * @return The next element. + */ + public LinkObject next() { + return next; + } + + public void setNext(LinkObject next) { + this.next = next; + } + + /** + * Get the data object from the element. + * @return The data object from the element. + */ + public ChannelMessage data() { + return msg; + } + + /** + * Get the unique message id + * @return the unique message id + */ + public byte[] getKey() { + return key; + } + + public ErrorHandler getHandler() { + return payload!=null?payload.getErrorHandler():null; + } + + public InterceptorPayload getPayload() { + return payload; + } + + public Member[] getDestination() { + return destination; + } + +} diff --git a/java/org/apache/catalina/tribes/transport/bio/util/SingleRemoveSynchronizedAddLock.java b/java/org/apache/catalina/tribes/transport/bio/util/SingleRemoveSynchronizedAddLock.java new file mode 100644 index 000000000..fe030db69 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/bio/util/SingleRemoveSynchronizedAddLock.java @@ -0,0 +1,253 @@ +/* + * 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.util; + +/** + * The class SingleRemoveSynchronizedAddLock implement locking for accessing the queue + * by a single remove thread and multiple add threads. + * + * A thread is only allowed to be either the remove or + * an add thread. + * + * The lock can either be owned by the remove thread + * or by a single add thread. + * + * If the remove thread tries to get the lock, + * but the queue is empty, it will block (poll) + * until an add threads adds an entry to the queue and + * releases the lock. + * + * If the remove thread and add threads compete for + * the lock and an add thread releases the lock, then + * the remove thread will get the lock first. + * + * The remove thread removes all entries in the queue + * at once and proceeses them without further + * polling the queue. + * + * The lock is not reentrant, in the sense, that all + * threads must release an owned lock before competing + * for the lock again! + * + * @author Rainer Jung + * @author Peter Rossbach + * @version 1.1 + */ + +public class SingleRemoveSynchronizedAddLock { + + public SingleRemoveSynchronizedAddLock() { + } + + public SingleRemoveSynchronizedAddLock(boolean dataAvailable) { + this.dataAvailable=dataAvailable; + } + + /** + * Time in milliseconds after which threads + * waiting for an add lock are woken up. + * This is used as a safety measure in case + * thread notification via the unlock methods + * has a bug. + */ + private long addWaitTimeout = 10000L; + + /** + * Time in milliseconds after which threads + * waiting for a remove lock are woken up. + * This is used as a safety measure in case + * thread notification via the unlock methods + * has a bug. + */ + private long removeWaitTimeout = 30000L; + + /** + * The current remove thread. + * It is set to the remove thread polling for entries. + * It is reset to null when the remove thread + * releases the lock and proceeds processing + * the removed entries. + */ + private Thread remover = null; + + /** + * A flag indicating, if an add thread owns the lock. + */ + private boolean addLocked = false; + + /** + * A flag indicating, if the remove thread owns the lock. + */ + private boolean removeLocked = false; + + /** + * A flag indicating, if the remove thread is allowed + * to wait for the lock. The flag is set to false, when aborting. + */ + private boolean removeEnabled = true; + + /** + * A flag indicating, if the remover needs polling. + * It indicates, if the locked object has data available + * to be removed. + */ + private boolean dataAvailable = false; + + /** + * @return Value of addWaitTimeout + */ + public synchronized long getAddWaitTimeout() { + return addWaitTimeout; + } + + /** + * Set value of addWaitTimeout + */ + public synchronized void setAddWaitTimeout(long timeout) { + addWaitTimeout = timeout; + } + + /** + * @return Value of removeWaitTimeout + */ + public synchronized long getRemoveWaitTimeout() { + return removeWaitTimeout; + } + + /** + * Set value of removeWaitTimeout + */ + public synchronized void setRemoveWaitTimeout(long timeout) { + removeWaitTimeout = timeout; + } + + /** + * Check if the locked object has data available + * i.e. the remover can stop poling and get the lock. + * @return True iff the lock Object has data available. + */ + public synchronized boolean isDataAvailable() { + return dataAvailable; + } + + /** + * Check if an add thread owns the lock. + * @return True iff an add thread owns the lock. + */ + public synchronized boolean isAddLocked() { + return addLocked; + } + + /** + * Check if the remove thread owns the lock. + * @return True iff the remove thread owns the lock. + */ + public synchronized boolean isRemoveLocked() { + return removeLocked; + } + + /** + * Check if the remove thread is polling. + * @return True iff the remove thread is polling. + */ + public synchronized boolean isRemovePolling() { + if ( remover != null ) { + return true; + } + return false; + } + + /** + * Acquires the lock by an add thread and sets the add flag. + * If any add thread or the remove thread already acquired the lock + * this add thread will block until the lock is released. + */ + public synchronized void lockAdd() { + if ( addLocked || removeLocked ) { + do { + try { + wait(addWaitTimeout); + } catch ( InterruptedException e ) { + Thread.currentThread().interrupted(); + } + } while ( addLocked || removeLocked ); + } + addLocked=true; + } + + /** + * Acquires the lock by the remove thread and sets the remove flag. + * If any add thread already acquired the lock or the queue is + * empty, the remove thread will block until the lock is released + * and the queue is not empty. + */ + public synchronized boolean lockRemove() { + removeLocked=false; + removeEnabled=true; + if ( ( addLocked || ! dataAvailable ) && removeEnabled ) { + remover=Thread.currentThread(); + do { + try { + wait(removeWaitTimeout); + } catch ( InterruptedException e ) { + Thread.currentThread().interrupted(); + } + } while ( ( addLocked || ! dataAvailable ) && removeEnabled ); + remover=null; + } + if ( removeEnabled ) { + removeLocked=true; + } + return removeLocked; + } + + /** + * Releases the lock by an add thread and reset the remove flag. + * If the reader thread is polling, notify it. + */ + public synchronized void unlockAdd(boolean dataAvailable) { + addLocked=false; + this.dataAvailable=dataAvailable; + if ( ( remover != null ) && ( dataAvailable || ! removeEnabled ) ) { + remover.interrupt(); + } else { + notifyAll(); + } + } + + /** + * Releases the lock by the remove thread and reset the add flag. + * Notify all waiting add threads, + * that the lock has been released by the remove thread. + */ + public synchronized void unlockRemove() { + removeLocked=false; + dataAvailable=false; + notifyAll(); + } + + /** + * Abort any polling remover thread + */ + public synchronized void abortRemove() { + removeEnabled=false; + if ( remover != null ) { + remover.interrupt(); + } + } + +} diff --git a/java/org/apache/catalina/tribes/transport/mbeans-descriptors.xml b/java/org/apache/catalina/tribes/transport/mbeans-descriptors.xml new file mode 100644 index 000000000..e7b862400 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/mbeans-descriptors.xml @@ -0,0 +1,835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java b/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java new file mode 100644 index 000000000..7cf5e2843 --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/NioReceiver.java @@ -0,0 +1,382 @@ +/* + * 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.nio; + +import java.io.IOException; +import java.net.ServerSocket; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; + +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.Constants; +import org.apache.catalina.tribes.transport.ReceiverBase; +import org.apache.catalina.tribes.transport.ThreadPool; +import org.apache.catalina.tribes.transport.WorkerThread; +import org.apache.catalina.tribes.util.StringManager; +import java.util.LinkedList; +import java.util.Set; +import java.nio.channels.CancelledKeyException; + +/** + * @author Filip Hanik + * @version $Revision: 379904 $ $Date: 2006-02-22 15:16:25 -0600 (Wed, 22 Feb 2006) $ + */ +public class NioReceiver extends ReceiverBase implements Runnable, ChannelReceiver, ListenCallback { + + protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(NioReceiver.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 = "NioReceiver/1.0"; + + private Selector selector = null; + private ServerSocketChannel serverChannel = null; + + protected LinkedList events = new LinkedList(); +// private Object interestOpsMutex = new Object(); + + public NioReceiver() { + } + + /** + * Return descriptive information about this implementation and the + * corresponding version number, in the format + * <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; i connect() -> CONNECTED + * - CONNECTED -> setMessage() -> READY TO WRITE + * - READY_TO_WRITE -> write() -> READY TO WRITE | READY TO READ + * - READY_TO_READ -> read() -> READY_TO_READ | TRANSFER_COMPLETE + * - TRANSFER_COMPLETE -> CONNECTED + * + * @author Filip Hanik + * @version 1.0 + */ +public class NioSender extends AbstractSender implements DataSender{ + + protected static org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(NioSender.class); + + + + protected Selector selector; + protected SocketChannel socketChannel; + + /* + * STATE VARIABLES * + */ + protected ByteBuffer readbuf = null; + protected ByteBuffer writebuf = null; + protected byte[] current = null; + protected XByteBuffer ackbuf = new XByteBuffer(128,true); + protected int remaining = 0; + protected boolean complete; + + protected boolean connecting = false; + + public NioSender() { + super(); + + } + + /** + * State machine to send data + * @param key SelectionKey + * @return boolean + * @throws IOException + */ + public boolean process(SelectionKey key, boolean waitForAck) throws IOException { + int ops = key.readyOps(); + key.interestOps(key.interestOps() & ~ops); + //in case disconnect has been called + if ((!isConnected()) && (!connecting)) throw new IOException("Sender has been disconnected, can't selection key."); + if ( !key.isValid() ) throw new IOException("Key is not valid, it must have been cancelled."); + if ( key.isConnectable() ) { + if ( socketChannel.finishConnect() ) { + //we connected, register ourselves for writing + setConnected(true); + connecting = false; + setRequestCount(0); + setConnectTime(System.currentTimeMillis()); + socketChannel.socket().setSendBufferSize(getTxBufSize()); + socketChannel.socket().setReceiveBufferSize(getRxBufSize()); + socketChannel.socket().setSoTimeout((int)getTimeout()); + socketChannel.socket().setSoLinger(false,0); + socketChannel.socket().setTcpNoDelay(getTcpNoDelay()); + socketChannel.socket().setKeepAlive(getSoKeepAlive()); + socketChannel.socket().setReuseAddress(getSoReuseAddress()); + socketChannel.socket().setOOBInline(getOoBInline()); + socketChannel.socket().setSoLinger(getSoLingerOn(),getSoLingerTime()); + socketChannel.socket().setTrafficClass(getSoTrafficClass()); + if ( current != null ) key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); + return false; + } else { + //wait for the connection to finish + key.interestOps(key.interestOps() | SelectionKey.OP_CONNECT); + return false; + }//end if + } else if ( key.isWritable() ) { + boolean writecomplete = write(key); + if ( writecomplete ) { + //we are completed, should we read an ack? + if ( waitForAck ) { + //register to read the ack + key.interestOps(key.interestOps() | SelectionKey.OP_READ); + } else { + //if not, we are ready, setMessage will reregister us for another write interest + //do a health check, we have no way of verify a disconnected + //socket since we don't register for OP_READ on waitForAck=false + read(key);//this causes overhead + setRequestCount(getRequestCount()+1); + return true; + } + } else { + //we are not complete, lets write some more + key.interestOps(key.interestOps()|SelectionKey.OP_WRITE); + }//end if + } else if ( key.isReadable() ) { + boolean readcomplete = read(key); + if ( readcomplete ) { + setRequestCount(getRequestCount()+1); + return true; + } else { + key.interestOps(key.interestOps() | SelectionKey.OP_READ); + }//end if + } else { + //unknown state, should never happen + log.warn("Data is in unknown state. readyOps="+ops); + throw new IOException("Data is in unknown state. readyOps="+ops); + }//end if + return false; + } + + + + protected boolean read(SelectionKey key) throws IOException { + //if there is no message here, we are done + if ( current == null ) return true; + int read = socketChannel.read(readbuf); + //end of stream + if ( read == -1 ) throw new IOException("Unable to receive an ack message. EOF on socket channel has been reached."); + //no data read + else if ( read == 0 ) return false; + readbuf.flip(); + ackbuf.append(readbuf,read); + readbuf.clear(); + if (ackbuf.doesPackageExist() ) { + byte[] ackcmd = ackbuf.extractDataPackage(true).getBytes(); + boolean ack = Arrays.equals(ackcmd,org.apache.catalina.tribes.transport.Constants.ACK_DATA); + boolean fack = Arrays.equals(ackcmd,org.apache.catalina.tribes.transport.Constants.FAIL_ACK_DATA); + if ( fack && getThrowOnFailedAck() ) throw new RemoteProcessException("Received a failed ack:org.apache.catalina.tribes.transport.Constants.FAIL_ACK_DATA"); + return ack || fack; + } else { + return false; + } + } + + + protected boolean write(SelectionKey key) throws IOException { + if ( (!isConnected()) || (this.socketChannel==null)) { + throw new IOException("NioSender is not connected, this should not occur."); + } + if ( current != null ) { + if ( remaining > 0 ) { + //weve written everything, or we are starting a new package + //protect against buffer overwrite + int byteswritten = socketChannel.write(writebuf); + remaining -= byteswritten; + //if the entire message was written from the buffer + //reset the position counter + if ( remaining < 0 ) { + remaining = 0; + } + } + return (remaining==0); + } + //no message to send, we can consider that complete + return true; + } + + /** + * connect - blocking in this operation + * + * @throws IOException + * @todo Implement this org.apache.catalina.tribes.transport.IDataSender method + */ + public synchronized void connect() throws IOException { + if ( connecting ) return; + connecting = true; + if ( isConnected() ) throw new IOException("NioSender is already in connected state."); + if ( readbuf == null ) { + readbuf = getReadBuffer(); + } else { + readbuf.clear(); + } + if ( writebuf == null ) { + writebuf = getWriteBuffer(); + } else { + writebuf.clear(); + } + + InetSocketAddress addr = new InetSocketAddress(getAddress(),getPort()); + if ( socketChannel != null ) throw new IOException("Socket channel has already been established. Connection might be in progress."); + socketChannel = SocketChannel.open(); + socketChannel.configureBlocking(false); + socketChannel.connect(addr); + socketChannel.register(getSelector(),SelectionKey.OP_CONNECT,this); + } + + + /** + * disconnect + * + * @todo Implement this org.apache.catalina.tribes.transport.IDataSender method + */ + public void disconnect() { + try { + connecting = false; + setConnected(false); + if ( socketChannel != null ) { + try { + try {socketChannel.socket().close();}catch ( Exception x){} + //error free close, all the way + //try {socket.shutdownOutput();}catch ( Exception x){} + //try {socket.shutdownInput();}catch ( Exception x){} + //try {socket.close();}catch ( Exception x){} + try {socketChannel.close();}catch ( Exception x){} + }finally { + socketChannel = null; + } + } + } catch ( Exception x ) { + log.error("Unable to disconnect NioSender. msg="+x.getMessage()); + if ( log.isDebugEnabled() ) log.debug("Unable to disconnect NioSender. msg="+x.getMessage(),x); + } finally { + } + + } + + public void reset() { + if ( isConnected() && readbuf == null) { + readbuf = getReadBuffer(); + } + if ( readbuf != null ) readbuf.clear(); + if ( writebuf != null ) writebuf.clear(); + current = null; + ackbuf.clear(); + remaining = 0; + complete = false; + setAttempt(0); + setRequestCount(0); + setConnectTime(-1); + } + + private ByteBuffer getReadBuffer() { + return getBuffer(getRxBufSize()); + } + + private ByteBuffer getWriteBuffer() { + return getBuffer(getTxBufSize()); + } + + private ByteBuffer getBuffer(int size) { + return (getDirectBuffer()?ByteBuffer.allocateDirect(size):ByteBuffer.allocate(size)); + } + + /** + * sendMessage + * + * @param data ChannelMessage + * @throws IOException + * @todo Implement this org.apache.catalina.tribes.transport.IDataSender method + */ + public synchronized void setMessage(byte[] data) throws IOException { + setMessage(data,0,data.length); + } + + public synchronized void setMessage(byte[] data,int offset, int length) throws IOException { + if ( data != null ) { + current = data; + remaining = length; + ackbuf.clear(); + if ( writebuf != null ) writebuf.clear(); + else writebuf = getBuffer(length); + if ( writebuf.capacity() < length ) writebuf = getBuffer(length); + writebuf.put(data,offset,length); + //writebuf.rewind(); + //set the limit so that we don't write non wanted data + //writebuf.limit(length); + writebuf.flip(); + if (isConnected()) { + socketChannel.register(getSelector(), SelectionKey.OP_WRITE, this); + } + } + } + + public byte[] getMessage() { + return current; + } + + + + public boolean isComplete() { + return complete; + } + + public Selector getSelector() { + return selector; + } + + public void setSelector(Selector selector) { + this.selector = selector; + } + + + public void setComplete(boolean complete) { + this.complete = complete; + } +} \ No newline at end of file diff --git a/java/org/apache/catalina/tribes/transport/nio/ParallelNioSender.java b/java/org/apache/catalina/tribes/transport/nio/ParallelNioSender.java new file mode 100644 index 000000000..5e92c5e6d --- /dev/null +++ b/java/org/apache/catalina/tribes/transport/nio/ParallelNioSender.java @@ -0,0 +1,288 @@ +/* + * 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.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.catalina.tribes.ChannelException; +import org.apache.catalina.tribes.ChannelMessage; +import org.apache.catalina.tribes.Member; +import org.apache.catalina.tribes.io.ChannelData; +import org.apache.catalina.tribes.io.XByteBuffer; +import org.apache.catalina.tribes.transport.MultiPointSender; +import org.apache.catalina.tribes.transport.SenderState; +import org.apache.catalina.tribes.transport.AbstractSender; +import java.net.UnknownHostException; +import org.apache.catalina.tribes.Channel; +import org.apache.catalina.tribes.group.RpcChannel; +import org.apache.catalina.tribes.util.Logs; +import org.apache.catalina.tribes.UniqueId; + +/** + *

Title:

+ * + *

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) && (delta 0 ) { + //timeout has occured + cx = new ChannelException("Operation has timed out("+getTimeout()+" ms.)."); + for (int i=0; i0); + synchronized (state) { + + //sk.cancel(); + if (state.isSuspect()) state.setFailing(); + if (state.isReady()) { + state.setSuspect(); + if ( retry ) + log.warn("Member send is failing for:" + sender.getDestination().getName() +" ; Setting to suspect and retrying."); + else + log.warn("Member send is failing for:" + sender.getDestination().getName() +" ; Setting to suspect.", x); + } + } + if ( !isConnected() ) { + log.warn("Not retrying send for:" + sender.getDestination().getName() + "; Sender is disconnected."); + ChannelException cx = new ChannelException("Send failed, and sender is disconnected. Not retrying.",x); + cx.addFaultyMember(sender.getDestination(),x); + throw cx; + } + + byte[] data = sender.getMessage(); + if ( retry ) { + try { + sender.disconnect(); + sender.connect(); + sender.setAttempt(attempt); + sender.setMessage(data); + }catch ( Exception ignore){ + state.setFailing(); + } + } else { + ChannelException cx = new ChannelException("Send failed, attempt:"+sender.getAttempt()+" max:"+maxAttempts,x); + cx.addFaultyMember(sender.getDestination(),x); + throw cx; + }//end if + } + } + return completed; + + } + + private void connect(NioSender[] senders) throws ChannelException { + ChannelException x = null; + for (int i=0; iTitle:

+ * + *

Description:

+ * + *

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 && i 0 ) { + buf.append(data[offset++]); + for (int i = offset; i < length; i++) { + buf.append(", ").append(data[i]); + } + } + buf.append("}"); + return buf.toString(); + } + + public static String toString(Object[] data) { + return toString(data,0,data!=null?data.length:0); + } + + public static String toString(Object[] data, int offset, int length) { + StringBuffer buf = new StringBuffer("{"); + if ( data != null && length > 0 ) { + buf.append(data[offset++]); + for (int i = offset; i < length; i++) { + buf.append(", ").append(data[i]); + } + } + buf.append("}"); + return buf.toString(); + } + + public static String toNameString(Member[] data) { + return toNameString(data,0,data!=null?data.length:0); + } + + public static String toNameString(Member[] data, int offset, int length) { + StringBuffer buf = new StringBuffer("{"); + if ( data != null && length > 0 ) { + buf.append(data[offset++].getName()); + for (int i = offset; i < length; i++) { + buf.append(", ").append(data[i].getName()); + } + } + buf.append("}"); + return buf.toString(); + } + + public static int add(int[] data) { + int result = 0; + for (int i=0;i= members.length ) idx = ((members.length>0)?0:-1); + +//System.out.println("Next index:"+idx); +//System.out.println("Member:"+member.getName()); +//System.out.println("Members:"+toNameString(members)); + return idx; + } + + public static int hashCode(byte a[]) { + if (a == null) + return 0; + + int result = 1; + for (int i=0; iThe StringManager operates on a package basis. One StringManager + * per package can be created and accessed via the getManager method + * call. + * + *

The 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; iinto.length ) + throw new ArrayIndexOutOfBoundsException("Unable to fit "+UUID_LENGTH+" bytes into the array. length:"+into.length+" required length:"+(offset+UUID_LENGTH)); + Random r = (secure&&(secrand!=null))?secrand:rand; + nextBytes(into,offset,UUID_LENGTH,r); + into[6+offset] &= 0x0F; + into[6+offset] |= (UUID_VERSION << 4); + into[8+offset] &= 0x3F; //0011 1111 + into[8+offset] |= 0x80; //1000 0000 + return into; + } + + /** + * Same as java.util.Random.nextBytes except this one we dont have to allocate a new byte array + * @param into byte[] + * @param offset int + * @param length int + * @param r Random + */ + public static void nextBytes(byte[] into, int offset, int length, Random r) { + int numRequested = length; + int numGot = 0, rnd = 0; + while (true) { + for (int i = 0; i < BYTES_PER_INT; i++) { + if (numGot == numRequested) return; + rnd = (i == 0 ? r.nextInt() : rnd >> BITS_PER_BYTE); + into[offset+numGot] = (byte) rnd; + numGot++; + } + } + } + +} \ No newline at end of file