/*
* Tigase Jabber/XMPP Server
* Copyright (C) 2004-2012 "Artur Hefczyc" <artur.hefczyc@tigase.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. Look for COPYING file in the top folder.
* If not, see http://www.gnu.org/licenses/.
*
* $Rev$
* Last modified by $Author$
* $Date$
*/
package tigase.cluster;
//~--- non-JDK imports --------------------------------------------------------
import tigase.annotations.TODO;
import tigase.cluster.api.ClusterCommandException;
import tigase.cluster.api.ClusterControllerIfc;
import tigase.cluster.api.ClusterElement;
import tigase.cluster.api.ClusteredComponentIfc;
import tigase.cluster.api.CommandListener;
import tigase.net.ConnectionType;
//import tigase.net.IOService;
import tigase.net.SocketType;
import tigase.server.ConnectionManager;
import tigase.server.Packet;
import tigase.server.ServiceChecker;
import tigase.server.xmppserver.CID;
import tigase.stats.StatisticType;
import tigase.stats.StatisticsList;
import tigase.util.Algorithms;
import tigase.util.DNSResolver;
import tigase.util.TigaseStringprepException;
import tigase.util.TimeUtils;
import tigase.xml.Element;
import tigase.xmpp.Authorization;
import tigase.xmpp.BareJID;
import tigase.xmpp.JID;
import tigase.xmpp.PacketErrorTypeException;
import tigase.xmpp.XMPPIOService;
//~--- JDK imports ------------------------------------------------------------
import java.net.UnknownHostException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.Deflater;
import javax.script.Bindings;
//~--- classes ----------------------------------------------------------------
/**
* Class ClusterConnectionManager
*
* Created: Tue Nov 22 07:07:11 2005
*
* @author <a href="mailto:artur.hefczyc@tigase.org">Artur Hefczyc</a>
* @version $Rev$
*/
public class ClusterConnectionManager extends ConnectionManager<XMPPIOService<Object>>
implements ClusteredComponentIfc {
/**
* Variable <code>log</code> is a class logger.
*/
private static final Logger log = Logger.getLogger(ClusterConnectionManager.class
.getName());
/** Field description */
public static final String SECRET_PROP_KEY = "secret";
/** Field description */
public static final String PORT_LOCAL_HOST_PROP_KEY = "local-host";
/** Field description */
public static final String PORT_ROUTING_TABLE_PROP_KEY = "routing-table";
/** Field description */
public static final String RETURN_SERVICE_DISCO_KEY = "service-disco";
/** Field description */
public static final boolean RETURN_SERVICE_DISCO_VAL = true;
/** Field description */
public static final String IDENTITY_TYPE_KEY = "identity-type";
/** Field description */
public static final String IDENTITY_TYPE_VAL = "generic";
/** Field description */
public static final String CONNECT_ALL_PAR = "--cluster-connect-all";
/** Field description */
public static final String CLUSTER_CONNECTIONS_PER_NODE_PAR =
"--cluster-connections-per-node";
/** Field description */
public static final int CLUSTER_CONNECTIONS_PER_NODE_VAL = 2;
/** Field description */
public static final String CLUSTER_CONNECTIONS_PER_NODE_PROP_KEY =
"cluster-connections-per-node";
/** Field description */
public static final String CONNECT_ALL_PROP_KEY = "connect-all";
/** Field description */
public static final String CLUSTER_CONTR_ID_PROP_KEY = "cluster-controller-id";
/** Field description */
public static final boolean CONNECT_ALL_PROP_VAL = false;
/** Field description */
public static final String COMPRESS_STREAM_PROP_KEY = "compress-stream";
/** Field description */
public static final boolean COMPRESS_STREAM_PROP_VAL = false;
/** Field description */
public static final String XMLNS = "tigase:cluster";
private static final String SERVICE_CONNECTED_TIMER = "service-connected-timer";
/** Field description */
public int[] PORTS = { 5277 };
/** Field description */
public String[] PORT_IFC_PROP_VAL = { "*" };
/** Field description */
public String SECRET_PROP_VAL = "someSecret";
private ClusterControllerIfc clusterController = null;
// private String cluster_controller_id = null;
private IOServiceStatisticsGetter ioStatsGetter = new IOServiceStatisticsGetter();
private String identity_type = IDENTITY_TYPE_VAL;
private Map<String, CopyOnWriteArrayList<XMPPIOService<Object>>> connectionsPool =
new ConcurrentSkipListMap<String, CopyOnWriteArrayList<XMPPIOService<Object>>>();
private boolean connect_all = CONNECT_ALL_PROP_VAL;
private boolean compress_stream = COMPRESS_STREAM_PROP_VAL;
private long[] lastDay = new long[24];
private int lastDayIdx = 0;
private long[] lastHour = new long[60];
private int lastHourIdx = 0;
private int nodesNo = 0;
private int per_node_conns = CLUSTER_CONNECTIONS_PER_NODE_VAL;
private long servConnectedTimeouts = 0;
private long totalNodeDisconnects = 0;
// private long packetsSent = 0;
// private long packetsReceived = 0;
private CommandListener sendPacket = new SendPacket();
/**
* Method description
*
*
* @param params
*
* @return
*/
@Override
public Map<String, Object> getDefaults(Map<String, Object> params) {
Map<String, Object> props = super.getDefaults(params);
props.put(RETURN_SERVICE_DISCO_KEY, RETURN_SERVICE_DISCO_VAL);
props.put(IDENTITY_TYPE_KEY, IDENTITY_TYPE_VAL);
if ((params.get(CONNECT_ALL_PAR) == null)
|| !((String) params.get(CONNECT_ALL_PAR)).equals("true")) {
props.put(CONNECT_ALL_PROP_KEY, false);
} else {
props.put(CONNECT_ALL_PROP_KEY, true);
}
if (params.get(CLUSTER_NODES) != null) {
String[] cl_nodes = ((String) params.get(CLUSTER_NODES)).split(",");
for (int i = 0; i < cl_nodes.length; i++) {
cl_nodes[i] = BareJID.parseJID(cl_nodes[i])[1];
}
nodesNo = cl_nodes.length;
props.put(CLUSTER_NODES_PROP_KEY, cl_nodes);
} else {
props.put(CLUSTER_NODES_PROP_KEY, new String[] { getDefHostName().getDomain() });
}
props.put(CLUSTER_CONTR_ID_PROP_KEY, DEF_CLUST_CONTR_NAME + "@" + getDefHostName());
props.put(COMPRESS_STREAM_PROP_KEY, COMPRESS_STREAM_PROP_VAL);
String conns = (String) params.get(CLUSTER_CONNECTIONS_PER_NODE_PAR);
int conns_int = CLUSTER_CONNECTIONS_PER_NODE_VAL;
if (conns != null) {
try {
conns_int = Integer.parseInt(conns);
} catch (Exception e) {
conns_int = CLUSTER_CONNECTIONS_PER_NODE_VAL;
}
}
props.put(CLUSTER_CONNECTIONS_PER_NODE_PROP_KEY, conns_int);
return props;
}
/**
* Method description
*
*
* @return
*/
@Override
public String getDiscoCategoryType() {
return identity_type;
}
/**
* Method description
*
*
* @return
*/
@Override
public String getDiscoDescription() {
return XMLNS + " " + getName();
}
/**
* Method description
*
*
* @param list
*/
@Override
public void getStatistics(StatisticsList list) {
super.getStatistics(list);
list.add(getName(), "Total disconnects", totalNodeDisconnects, Level.FINE);
list.add(getName(), "Service connected time-outs", servConnectedTimeouts, Level.FINE);
list.add(getName(), "Last day disconnects", Arrays.toString(lastDay), Level.FINE);
list.add(getName(), "Last hour disconnects", Arrays.toString(lastHour), Level.FINE);
ioStatsGetter.reset();
doForAllServices(ioStatsGetter);
list.add(getName(), "Average compression ratio",
ioStatsGetter.getAverageCompressionRatio(), Level.FINE);
list.add(getName(), "Average decompression ratio",
ioStatsGetter.getAverageDecompressionRatio(), Level.FINE);
list.add(getName(), "Waiting to send", ioStatsGetter.getWaitingToSend(), Level.FINE);
// list.add(getName(), StatisticType.MSG_RECEIVED_OK.getDescription(),
// packetsReceived,
// Level.FINE);
// list.add(getName(), StatisticType.MSG_SENT_OK.getDescription(),
// packetsSent,
// Level.FINE);
}
/**
* This method can be overwritten in extending classes to get a different
* packets distribution to different threads. For PubSub, probably better
* packets distribution to different threads would be based on the sender
* address rather then destination address.
*
* @param packet
* @return
*/
@Override
public int hashCodeForPacket(Packet packet) {
// If this is a cluster packet let's try to do a bit more smart hashing
// based on the stanza from/to addresses
if (packet.getElemName() == ClusterElement.CLUSTER_EL_NAME) {
// TODO: Look for a simpler, more efficient algorithm to distribute
// cluster packets among different threads.
// This looks like an overkill to me, however I don't see any better way
ClusterElement clel = new ClusterElement(packet.getElement());
// If there is no XMPP stanzas with an address inside the cluster packet,
// we can try Map data and User ID inside it if it exists.
String userId = clel.getMethodParam("userId");
if (userId != null) {
return userId.hashCode();
}
Queue<Element> children = clel.getDataPackets();
if ((children != null) && (children.size() > 0)) {
Element child = children.peek();
String stanzaAdd = child.getAttribute("to");
if (stanzaAdd != null) {
return stanzaAdd.hashCode();
} else {
// This might be user's initial presence. In such a case we take
// stanzaFrom instead
stanzaAdd = child.getAttribute("from");
if (stanzaAdd != null) {
return stanzaAdd.hashCode();
} else {
// This may happen for some cluster packets, like:
// resp-sync-online-sm-cmd and this is correct
log.log(Level.FINE, "No stanzaTo or from for cluster packet: {0}", packet);
}
}
}
}
// There is a separate connection to each cluster node, ideally we want to
// process packets in a separate thread for each connection, so let's try
// to get the hash code by the destination node address
if (packet.getStanzaTo() != null) {
return packet.getStanzaTo().hashCode();
}
return packet.getTo().hashCode();
}
/**
* Method description
*
*
* @param binds
*/
@Override
public void initBindings(Bindings binds) {
super.initBindings(binds);
binds.put("clusterCM", this);
}
/**
* Method description
*
*
* @param node
*/
@Override
public void nodeConnected(String node) {
}
/**
* Method description
*
*
* @param node
*/
@Override
public void nodeDisconnected(String node) {
}
/**
* Method description
*
*
* @param packet
*/
@Override
public void processPacket(Packet packet) {
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "Processing packet: {0}", packet);
}
if ((packet.getStanzaTo() != null) && packet.getStanzaTo().equals(getComponentId())) {
try {
addOutPacket(Authorization.FEATURE_NOT_IMPLEMENTED.getResponseMessage(packet,
"Not implemented", true));
} catch (PacketErrorTypeException e) {
log.log(Level.WARNING, "Packet processing exception: {0}", e);
}
return;
}
if (packet.getElemName() == ClusterElement.CLUSTER_EL_NAME) {
writePacketToSocket(packet);
} else {
writePacketToSocket(packet.packRouted());
}
}
/**
* Method description
*
*
* @param serv
*
* @return
*/
@Override
public Queue<Packet> processSocketData(XMPPIOService<Object> serv) {
Packet p = null;
while ((p = serv.getReceivedPackets().poll()) != null) {
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "Processing socket data: {0}", p);
}
if (p.getElemName().equals("handshake")) {
processHandshake(p, serv);
} else {
// ++packetsReceived;
Packet result = p;
if (p.isRouted()) {
// processReceivedRid(p, serv);
// processReceivedAck(p, serv);
try {
result = p.unpackRouted();
} catch (TigaseStringprepException ex) {
log.log(Level.WARNING,
"Packet stringprep addressing problem, dropping packet: {0}", p);
return null;
}
} // end of if (p.isRouted())
addOutPacket(result);
}
} // end of while ()
return null;
}
@Override
public void processOutPacket(Packet packet) {
if (packet.getElemName() == ClusterElement.CLUSTER_EL_NAME) {
clusterController.handleClusterPacket(packet.getElement());
} else {
// This should, actually, not happen. Let's log it here
if (log.isLoggable(Level.INFO)) {
log.log(Level.INFO, "Unexpected packet on cluster connection: {0}", packet);
}
super.processOutPacket(packet);
}
}
/**
*
* @return
*/
@Override
public int processingInThreads() {
// TODO: The number of threads should be equal or greater to number of
// cluster nodes.
// This should work well as far as nodesNo is initialized before this
// method is called which is true only during program startup time.
// In case of reconfiguration or new node joining this might not be
// the case. Low priority issue though.
return Math.max(Runtime.getRuntime().availableProcessors(), nodesNo) * 8;
}
public int processingOutThreads() {
// TODO: The number of threads should be equal or greater to number of
// cluster nodes.
// This should work well as far as nodesNo is initialized before this
// method is called which is true only during program startup time.
// In case of reconfiguration or new node joining this might not be
// the case. Low priority issue though.
return Math.max(Runtime.getRuntime().availableProcessors(), nodesNo) * 8;
}
/**
* Method description
*
*
* @param port_props
*/
@Override
public void reconnectionFailed(Map<String, Object> port_props) {
// TODO: handle this somehow
}
/**
* Method description
*
*
* @param serv
*/
@Override
public void serviceStarted(XMPPIOService<Object> serv) {
ServiceConnectedTimer task = new ServiceConnectedTimer(serv);
serv.getSessionData().put(SERVICE_CONNECTED_TIMER, task);
addTimerTask(task, 10, TimeUnit.SECONDS);
super.serviceStarted(serv);
log.log(
Level.INFO,
"cluster connection opened: {0}, type: {1}, id={2}",
new Object[] { serv.getRemoteAddress(), serv.connectionType().toString(),
serv.getUniqueId() });
if (compress_stream) {
log.log(Level.INFO, "Starting stream compression for: {0}", serv.getUniqueId());
serv.startZLib(Deflater.BEST_COMPRESSION);
}
switch (serv.connectionType()) {
case connect:
// Send init xmpp stream here
String remote_host =
(String) serv.getSessionData().get(PORT_REMOTE_HOST_PROP_KEY);
serv.getSessionData().put(XMPPIOService.HOSTNAME_KEY, remote_host);
serv.getSessionData().put(PORT_ROUTING_TABLE_PROP_KEY,
new String[] { remote_host, ".*@" + remote_host, ".*\\." + remote_host });
String data =
"<stream:stream" + " xmlns='" + XMLNS + "'"
+ " xmlns:stream='http://etherx.jabber.org/streams'" + " from='"
+ getDefHostName() + "'" + " to='" + remote_host + "'" + ">";
log.log(Level.INFO, "cid: {0}, sending: {1}", new Object[] {
(String) serv.getSessionData().get("cid"), data });
serv.xmppStreamOpen(data);
break;
default:
// Do nothing, more data should come soon...
break;
} // end of switch (service.connectionType())
}
/**
* Method description
*
*
* @param service
*
* @return
*/
@Override
public boolean serviceStopped(XMPPIOService<Object> service) {
boolean result = super.serviceStopped(service);
// Make sure it runs just once for each disconnect
if (result) {
Map<String, Object> sessionData = service.getSessionData();
String[] routings = (String[]) sessionData.get(PORT_ROUTING_TABLE_PROP_KEY);
// String ip = service.getRemoteAddress();
// CopyOnWriteArrayList<XMPPIOService<Object>> conns = connectionsPool.get(ip);
//
// if (conns == null) {
// conns = new CopyOnWriteArrayList<XMPPIOService<Object>>();
// connectionsPool.put(ip, conns);
// }
String addr = (String) sessionData.get(PORT_REMOTE_HOST_PROP_KEY);
CopyOnWriteArrayList<XMPPIOService<Object>> conns = connectionsPool.get(addr);
if (conns == null) {
conns = new CopyOnWriteArrayList<XMPPIOService<Object>>();
connectionsPool.put(addr, conns);
}
int size = conns.size();
conns.remove(service);
if (size == 1) {
if (routings != null) {
updateRoutings(routings, false);
}
// removeRouting(serv.getRemoteHost());
log.log(Level.INFO, "Disonnected from: {0}", addr);
updateServiceDiscoveryItem(addr, addr, XMLNS + " disconnected", true);
clusterController.nodeDisconnected(addr);
}
ConnectionType type = service.connectionType();
if (type == ConnectionType.connect) {
addWaitingTask(sessionData);
} // end of if (type == ConnectionType.connect)
++totalNodeDisconnects;
int hour = TimeUtils.getHourNow();
if (lastDayIdx != hour) {
lastDayIdx = hour;
lastDay[hour] = 0;
Arrays.fill(lastHour, 0);
}
++lastDay[hour];
int minute = TimeUtils.getMinuteNow();
++lastHour[minute];
}
return result;
}
/**
* Method description
*
*
* @param cl_controller
*/
@Override
public void setClusterController(ClusterControllerIfc cl_controller) {
clusterController = cl_controller;
clusterController.removeCommandListener(
ClusterControllerIfc.DELIVER_CLUSTER_PACKET_CMD, sendPacket);
clusterController.setCommandListener(ClusterControllerIfc.DELIVER_CLUSTER_PACKET_CMD,
sendPacket);
}
/**
* Method description
*
*
* @param props
*/
@Override
public void setProperties(Map<String, Object> props) {
super.setProperties(props);
if (props.get(IDENTITY_TYPE_KEY) != null) {
identity_type = (String) props.get(IDENTITY_TYPE_KEY);
}
if (props.get(COMPRESS_STREAM_PROP_KEY) != null) {
compress_stream = (Boolean) props.get(COMPRESS_STREAM_PROP_KEY);
}
if (props.get(CONNECT_ALL_PROP_KEY) != null) {
connect_all = (Boolean) props.get(CONNECT_ALL_PROP_KEY);
}
// cluster_controller_id = (String) props.get(CLUSTER_CONTR_ID_PROP_KEY);
if (props.get(CLUSTER_CONNECTIONS_PER_NODE_PROP_KEY) != null) {
per_node_conns = (Integer) props.get(CLUSTER_CONNECTIONS_PER_NODE_PROP_KEY);
}
connectionDelay = 5 * SECOND;
if (props.size() == 1) {
// If props.size() == 1, it means this is a single property update
// and this component does not support single property change for the rest
// of it's settings
return;
}
String[] cl_nodes = (String[]) props.get(CLUSTER_NODES_PROP_KEY);
int[] ports = (int[]) props.get(PORTS_PROP_KEY);
if (ports != null) {
PORTS = ports;
}
if (cl_nodes != null) {
nodesNo = cl_nodes.length;
for (String node : cl_nodes) {
String host = BareJID.parseJID(node)[1];
log.log(Level.CONFIG, "Found cluster node host: {0}", host);
if (!host.equals(getDefHostName().getDomain())
&& ((host.hashCode() > getDefHostName().hashCode()) || connect_all)) {
for (int i = 0; i < per_node_conns; ++i) {
log.log(Level.CONFIG, "Trying to connect to cluster node: {0}", host);
Map<String, Object> port_props = new LinkedHashMap<String, Object>(12);
port_props.put(SECRET_PROP_KEY, SECRET_PROP_VAL);
port_props.put(PORT_LOCAL_HOST_PROP_KEY, getDefHostName());
port_props.put(PORT_TYPE_PROP_KEY, ConnectionType.connect);
port_props.put(PORT_SOCKET_PROP_KEY, SocketType.plain);
port_props.put(PORT_REMOTE_HOST_PROP_KEY, host);
port_props.put(PORT_IFC_PROP_KEY, new String[] { host });
port_props.put(MAX_RECONNECTS_PROP_KEY, 99999999);
port_props.put(PORT_KEY, PORTS[0]);
addWaitingTask(port_props);
}
// reconnectService(port_props, connectionDelay);
}
}
}
}
/**
* Method description
*
*
* @param service
*/
@Override
public void tlsHandshakeCompleted(XMPPIOService<Object> service) {
}
/**
* Method description
*
*
* @param serv
*/
@Override
public void xmppStreamClosed(XMPPIOService<Object> serv) {
log.info("Stream closed.");
}
/**
* Method description
*
*
* @param service
* @param attribs
*
* @return
*/
@Override
public String xmppStreamOpened(XMPPIOService<Object> service,
Map<String, String> attribs) {
log.log(Level.INFO, "Stream opened: {0}", attribs);
switch (service.connectionType()) {
case connect: {
String id = attribs.get("id");
service.getSessionData().put(XMPPIOService.SESSION_ID_KEY, id);
String secret = (String) service.getSessionData().get(SECRET_PROP_KEY);
try {
String digest = Algorithms.hexDigest(id, secret, "SHA");
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "Calculating digest: id={0}, secret={1}, digest={2}",
new Object[] { id, secret, digest });
}
return "<handshake>" + digest + "</handshake>";
} catch (NoSuchAlgorithmException e) {
log.log(Level.SEVERE, "Can not generate digest for pass phrase.", e);
return null;
}
}
case accept: {
String remote_host = attribs.get("from");
service.getSessionData().put(XMPPIOService.HOSTNAME_KEY, remote_host);
service.getSessionData().put(PORT_REMOTE_HOST_PROP_KEY, remote_host);
service.getSessionData().put(PORT_ROUTING_TABLE_PROP_KEY,
new String[] { remote_host, ".*@" + remote_host, ".*\\." + remote_host });
String id = UUID.randomUUID().toString();
service.getSessionData().put(XMPPIOService.SESSION_ID_KEY, id);
return "<stream:stream" + " xmlns='" + XMLNS + "'"
+ " xmlns:stream='http://etherx.jabber.org/streams'" + " from='"
+ getDefHostName() + "'" + " to='" + remote_host + "'" + " id='" + id + "'"
+ ">";
}
default:
// Do nothing, more data should come soon...
break;
} // end of switch (service.connectionType())
return null;
}
@Override
protected int[] getDefPlainPorts() {
return PORTS;
}
/**
* Method <code>getMaxInactiveTime</code> returns max keep-alive time for
* inactive connection. we shoulnd not really close external component
* connection at all, so let's say something like: 1000 days...
*
* @return a <code>long</code> value
*/
@Override
protected long getMaxInactiveTime() {
return 1000 * 24 * HOUR;
}
@Override
protected Integer getMaxQueueSize(int def) {
return def * 10;
}
@Override
protected Map<String, Object> getParamsForPort(int port) {
Map<String, Object> defs = new LinkedHashMap<String, Object>(10);
defs.put(SECRET_PROP_KEY, SECRET_PROP_VAL);
defs.put(PORT_TYPE_PROP_KEY, ConnectionType.accept);
defs.put(PORT_SOCKET_PROP_KEY, SocketType.plain);
defs.put(PORT_IFC_PROP_KEY, PORT_IFC_PROP_VAL);
return defs;
}
@Override
protected XMPPIOService<Object> getXMPPIOServiceInstance() {
return new XMPPIOService<Object>();
}
@Override
protected boolean isHighThroughput() {
return true;
}
protected void serviceConnected(XMPPIOService<Object> serv) {
String[] routings = (String[]) serv.getSessionData().get(PORT_ROUTING_TABLE_PROP_KEY);
String addr = (String) serv.getSessionData().get(PORT_REMOTE_HOST_PROP_KEY);
// String ip = serv.getRemoteAddress();
// CopyOnWriteArrayList<XMPPIOService<Object>> conns = connectionsPool.get(ip);
//
// if (conns == null) {
// conns = new CopyOnWriteArrayList<XMPPIOService<Object>>();
// connectionsPool.put(ip, conns);
// }
CopyOnWriteArrayList<XMPPIOService<Object>> conns = connectionsPool.get(addr);
if (conns == null) {
conns = new CopyOnWriteArrayList<XMPPIOService<Object>>();
connectionsPool.put(addr, conns);
}
int size = conns.size();
conns.add(serv);
if (size == 0) {
updateRoutings(routings, true);
log.log(Level.INFO, "Connected to: {0}", addr);
updateServiceDiscoveryItem(addr, addr, XMLNS + " connected", true);
clusterController.nodeConnected(addr);
}
ServiceConnectedTimer task =
(ServiceConnectedTimer) serv.getSessionData().get(SERVICE_CONNECTED_TIMER);
if (task == null) {
log.log(Level.WARNING, "Missing service connected timer task: {0}", serv);
} else {
task.cancel();
}
}
@Override
protected boolean writePacketToSocket(Packet p) {
// ++packetsSent;
String ip = p.getTo().getDomain();
// try {
// ip = DNSResolver.getHostIP(p.getTo().getDomain());
// } catch (UnknownHostException ex) {
// ip = p.getTo().getDomain();
// }
int code = Math.abs(hashCodeForPacket(p));
CopyOnWriteArrayList<XMPPIOService<Object>> conns = connectionsPool.get(ip);
if ((conns != null) && (conns.size() > 0)) {
XMPPIOService<Object> serv = conns.get(code % conns.size());
return super.writePacketToSocket(serv, p);
} else {
log.log(Level.WARNING, "No cluster connection to send a packet: {0}", p);
return false;
}
// return super.writePacketToSocket(p);
}
private void processHandshake(Packet p, XMPPIOService<Object> serv) {
switch (serv.connectionType()) {
case connect: {
String data = p.getElemCData();
if (data == null) {
serviceConnected(serv);
} else {
log.log(Level.WARNING, "Incorrect packet received: {0}", p);
}
break;
}
case accept: {
String digest = p.getElemCData();
String id = (String) serv.getSessionData().get(XMPPIOService.SESSION_ID_KEY);
String secret = (String) serv.getSessionData().get(SECRET_PROP_KEY);
try {
String loc_digest = Algorithms.hexDigest(id, secret, "SHA");
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "Calculating digest: id={0}, secret={1}, digest={2}",
new Object[] { id, secret, loc_digest });
}
if ((digest != null) && digest.equals(loc_digest)) {
Packet resp = Packet.packetInstance(new Element("handshake"), null, null);
writePacketToSocket(serv, resp);
serviceConnected(serv);
} else {
log.warning("Handshaking password doesn't match, disconnecting...");
serv.stop();
}
} catch (Exception e) {
log.log(Level.SEVERE, "Handshaking error.", e);
}
break;
}
default:
// Do nothing, more data should come soon...
break;
} // end of switch (service.connectionType())
}
private void updateRoutings(String[] routings, boolean add) {
if (add) {
for (String route : routings) {
try {
addRegexRouting(route);
} catch (Exception e) {
log.log(Level.WARNING, "Can not add regex routing ''{0}'' : {1}", new Object[] {
route, e });
}
}
} else {
for (String route : routings) {
try {
removeRegexRouting(route);
} catch (Exception e) {
log.log(Level.WARNING, "Can not remove regex routing ''{0}'' : {1}",
new Object[] { route, e });
}
}
}
}
private class IOServiceStatisticsGetter implements
ServiceChecker<XMPPIOService<Object>> {
private int clIOQueue = 0;
private float compressionRatio = 0f;
private int counter = 0;
private float decompressionRatio = 0f;
private StatisticsList list = new StatisticsList(Level.ALL);
/**
* Method description
*
*
* @param service
*/
@Override
public void check(XMPPIOService<Object> service) {
service.getStatistics(list, true);
compressionRatio += list.getValue("zlibio", "Average compression rate", -1f);
decompressionRatio += list.getValue("zlibio", "Average decompression rate", -1f);
++counter;
clIOQueue += service.waitingToSendSize();
}
/**
* Method description
*
*
* @return
*/
public float getAverageCompressionRatio() {
return compressionRatio / counter;
}
/**
* Method description
*
*
* @return
*/
public float getAverageDecompressionRatio() {
return decompressionRatio / counter;
}
/**
* Method description
*
*
* @return
*/
public int getWaitingToSend() {
return clIOQueue;
}
/**
* Method description
*
*/
public void reset() {
// Statistics are reset on the low socket level instead. This way we do
// not loose
// any stats in case of the disconnection.
// bytesReceived = 0;
// bytesSent = 0;
clIOQueue = 0;
counter = 0;
compressionRatio = 0f;
decompressionRatio = 0f;
}
}
private class ServiceConnectedTimer extends TimerTask {
private XMPPIOService<Object> serv = null;
private ServiceConnectedTimer(XMPPIOService<Object> serv) {
this.serv = serv;
}
/**
* Method description
*
*/
@Override
public void run() {
++servConnectedTimeouts;
log.log(Level.INFO,
"ServiceConnectedTimer timeout expired, closing connection: {0}", serv);
serv.forceStop();
}
}
private class SendPacket implements CommandListener {
/*
* (non-Javadoc)
*
* @see tigase.cluster.api.CommandListener#executeCommand(java.util.Map)
*/
@Override
public void executeCommand(JID fromNode, Set<JID> visitedNodes,
Map<String, String> data, Queue<Element> packets) throws ClusterCommandException {
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST,
"Called fromNode: {0}, visitedNodes: {1}, data: {2}, packets: {3}",
new Object[] { fromNode, visitedNodes, data, packets });
}
for (Element element : packets) {
try {
addPacketNB(Packet.packetInstance(element));
// writePacketToSocket();
} catch (TigaseStringprepException ex) {
log.log(Level.WARNING, "Stringprep exception for packet: {0}", element);
}
}
}
}
}