/*
* JBoss, Home of Professional Open Source
* Copyright 2011, Red Hat, Inc. and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.mobicents.tools.sip.balancer;
import gov.nist.javax.sip.ListeningPointExt;
import gov.nist.javax.sip.header.SIPHeader;
import gov.nist.javax.sip.header.Via;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.validator.routines.InetAddressValidator;
import org.apache.log4j.Logger;
import org.mobicents.tools.heartbeat.api.Node;
import javax.sip.ListeningPoint;
import javax.sip.address.SipURI;
import javax.sip.header.CSeqHeader;
import javax.sip.header.RouteHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
/**
* This algorthim will send each new transaction to a new node triggering all replication corner cases
*
* @author vladimirralev
*
*/
public class WorstCaseUdpTestAffinityAlgorithm extends DefaultBalancerAlgorithm {
protected ConcurrentHashMap<String, Node> txToNode = new ConcurrentHashMap<String, Node>();
protected ConcurrentHashMap<String, Long> txTimestamps = new ConcurrentHashMap<String, Long>();
protected boolean earlyDialogWorstCase = false;
public synchronized Node getNodeA(String tx) {
return txToNode.get(tx);
}
public synchronized void setNodeA(String tx, Node node) {
txToNode.put(tx, node);
txTimestamps.put(tx, System.currentTimeMillis());
}
static int y =0;
public Node processAssignedExternalRequest(Request request,
Node assignedNode) {
Boolean isIpV6=LbUtils.isValidInet6Address(assignedNode.getIp());
//if((y++)%2==0) if(request.getHeader("CSeq").toString().contains("1")) return assignedNode;
String callId = ((SIPHeader) request.getHeader(headerName)).getValue();
CSeqHeader cs = (CSeqHeader) request.getHeader(CSeqHeader.NAME);
long cseq = cs.getSeqNumber();
if(callIdMap.get(callId) != null) {
assignedNode = callIdMap.get(callId);
}
ViaHeader via = (ViaHeader) request.getHeader(Via.NAME);
String transport = via.getTransport().toLowerCase();
RouteHeader route = (RouteHeader) request.getHeader(RouteHeader.NAME);
SipURI uri = null;
if(route != null) {
uri = (SipURI) route.getAddress().getURI();
} else {
uri = (SipURI) request.getRequestURI();
}
try {
Node node;
if(!request.getMethod().equalsIgnoreCase("ACK")) {
//Gvag: new transaction should go to a new node
Node newNode = nextAvailableNode(isIpV6);//getNodeA(callId+cseq);
if(newNode == null) {
//for(Node currNode:invocationContext.nodes) {
for(Node currNode:invocationContext.sipNodeMap(isIpV6).values()) {
if(!currNode.equals(assignedNode)) {
newNode = currNode;
}
}
}
node = newNode;
}
else
node=assignedNode;
uri.setHost(node.getIp());
if(balancerContext.internalTransport!=null)
{
transport = balancerContext.internalTransport.toLowerCase();
}else if(balancerContext.terminateTLSTraffic)
if(transport.equalsIgnoreCase(ListeningPoint.TLS))
transport=ListeningPoint.TCP.toLowerCase();
else if (transport.equalsIgnoreCase(ListeningPointExt.WSS))
transport=ListeningPointExt.WS.toLowerCase();
Integer port = Integer.parseInt(node.getProperties().get(transport + "Port"));
uri.setPort(port);
callIdMap.put(callId, node);
setNodeA(callId + cseq, node);
// For http://code.google.com/p/mobicents/issues/detail?id=2132
if(request.getRequestURI().isSipURI()) {
SipURI ruri = (SipURI) request.getRequestURI();
String rurihostid = ruri.getHost() + ruri.getPort();
String originalhostid = assignedNode.getIp() + assignedNode.getProperties().get(transport + "Port");
if(rurihostid.equals(originalhostid)) {
ruri.setPort(port);
ruri.setHost(node.getIp());
}
}
return node;
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return assignedNode;
}
private static Logger logger = Logger.getLogger(WorstCaseUdpTestAffinityAlgorithm.class.getCanonicalName());
protected String headerName = "Call-ID";
protected ConcurrentHashMap<String, Node> callIdMap = new ConcurrentHashMap<String, Node>();
protected ConcurrentHashMap<String, Long> callIdTimestamps = new ConcurrentHashMap<String, Long>();
protected AtomicInteger nextNodeCounter = new AtomicInteger(0);
protected int maxCallIdleTime = 500;
protected boolean groupedFailover = false;
protected Timer cacheEvictionTimer = new Timer();
public void processInternalRequest(Request request) {
logger.debug("internal request");
}
public void processInternalResponse(Response request) {
logger.debug("internal response");
}
public void processExternalResponse(Response response,Boolean isIpV6) {
Via via = (Via) response.getHeader(Via.NAME);
String transport = via.getTransport().toLowerCase();
String host = via.getHost();
boolean found = false;
//for(Node node : invocationContext.nodes) {
for(Node node : invocationContext.sipNodeMap(isIpV6).values()) {
if(node.getIp().equals(host)) found = true;
}
if(!found) {
String callId = ((SIPHeader) response.getHeader(headerName))
.getValue();
Node node = callIdMap.get(callId);
//if(node == null || !invocationContext.nodes.contains(node)) {
if(node == null || !invocationContext.sipNodeMap(isIpV6).containsValue(node)) {
node = selectNewNode(node, callId);
try {
via.setHost(node.getIp());
String transportProperty = transport.toLowerCase() + "Port";
Integer port = Integer.parseInt(node.getProperties().get(transportProperty));
if(port == null) throw new RuntimeException("No transport found for node " + node + " " + transportProperty);
via.setPort(port);
} catch (Exception e) {
throw new RuntimeException("Error", e);
}
if(!ListeningPoint.UDP.equalsIgnoreCase(transport)) {
via.setRPort();
}
}
} else {
if(earlyDialogWorstCase && response.getStatusCode()>100) {
String callId = ((SIPHeader) response.getHeader(headerName))
.getValue();
Node node = callIdMap.get(callId);
for(int q=0; q<3; q++) {
Node other = selectNewNode(node, callId);
if(other!= null && !other.equals(node)) {
node = other; break;
}
}
try {
via.setHost(node.getIp());
String transportProperty = transport.toLowerCase() + "Port";
Integer port = Integer.parseInt(node.getProperties().get(transportProperty));
if(port == null) throw new RuntimeException("No transport found for node " + node + " " + transportProperty);
via.setPort(port);
} catch (Exception e) {
throw new RuntimeException("Error", e);
}
if(!ListeningPoint.UDP.equalsIgnoreCase(transport)) {
via.setRPort();
}
}
}
}
public Node processExternalRequest(Request request,Boolean isIpV6) {
String callId = ((SIPHeader) request.getHeader(headerName))
.getValue();
Node node;
CSeqHeader cs = (CSeqHeader) request.getHeader(CSeqHeader.NAME);
long cseq = cs.getSeqNumber();
node = callIdMap.get(callId);
callIdTimestamps.put(callId, System.currentTimeMillis());
if(node == null) { //
node = nextAvailableNode(isIpV6);
if(node == null) return null;
callIdMap.put(callId, node);
if(logger.isDebugEnabled()) {
logger.debug("No node found in the affinity map. It is null. We select new node: " + node);
}
} else {
//if(!invocationContext.nodes.contains(node)) { // If the assigned node is now dead
if(!invocationContext.sipNodeMap(isIpV6).containsValue(node)) { // If the assigned node is now dead
node = selectNewNode(node, callId);
} else { // ..else it's alive and we can route there
//.. and we just leave it like that
if(logger.isDebugEnabled()) {
logger.debug("The assigned node in the affinity map is still alive: " + node);
}
if(!request.getMethod().equals("ACK")) {
//for(Node n:invocationContext.nodes) {
for(Node n:invocationContext.sipNodeMap(isIpV6).values()) {
if(!n.equals(node)) node = n;
break;
}
}
}
}
setNodeA(callId+cseq,node);
callIdMap.put(callId, node);
// Don't try to be smart here, the retransmissions of BYE will come and will not know where to go.
// if(request.getMethod().equals("BYE")) {
// callIdMap.remove(callId);
// callIdTimestamps.remove(callId);
// }
return node;
}
protected Node selectNewNode(Node node, String callId) {
if(logger.isDebugEnabled()) {
logger.debug("The assigned node has died. This is the dead node: " + node);
}
if(groupedFailover) {
// This will occur very rarely because we re-assign all calls from the dead node in
// a single operation
Node oldNode = node;
node = leastBusyTargetNode(oldNode);
if(node == null) return null;
groupedFailover(oldNode, node);
} else {
Boolean isIpV6=LbUtils.isValidInet6Address(node.getIp());
node = nextAvailableNode(isIpV6);
if(node == null) return null;
callIdMap.put(callId, node);
}
if(logger.isDebugEnabled()) {
logger.debug("So, we must select new node: " + node);
}
return node;
}
protected synchronized Node nextAvailableNode(Boolean isIpV6) {
// if(invocationContext.nodes.size() == 0) return null;
// int nextNode = nextNodeCounter.incrementAndGet();
// nextNode %= invocationContext.nodes.size();
// return invocationContext.nodes.get(nextNode);
if(invocationContext.sipNodeMap(isIpV6).size() == 0) return null;
Iterator<Entry<KeySip, Node>> currIt = null;
if(isIpV6)
currIt = ipv6It;
else
currIt = ipv4It;
if(currIt==null)
{
currIt = invocationContext.sipNodeMap(isIpV6).entrySet().iterator();
if(isIpV6)
ipv6It = currIt;
else
ipv4It = currIt;
}
Entry<KeySip, Node> pair = null;
if(currIt.hasNext())
{
pair = currIt.next();
if(!currIt.hasNext())
currIt = invocationContext.sipNodeMap(isIpV6).entrySet().iterator();
if(isIpV6)
ipv6It = currIt;
else
ipv4It = currIt;
}
else
{
currIt = invocationContext.sipNodeMap(isIpV6).entrySet().iterator();
if(isIpV6)
ipv6It = currIt;
else
ipv4It = currIt;
}
return pair.getValue();
}
protected synchronized Node leastBusyTargetNode(Node deadNode) {
HashMap<Node, Integer> nodeUtilization = new HashMap<Node, Integer>();
for(Node node : callIdMap.values()) {
Integer n = nodeUtilization.get(node);
if(n == null) {
nodeUtilization.put(node, 0);
} else {
nodeUtilization.put(node, n+1);
}
}
int minUtil = Integer.MAX_VALUE;
Node minUtilNode = null;
for(Node node : nodeUtilization.keySet()) {
Integer util = nodeUtilization.get(node);
if(!node.equals(deadNode) && (util < minUtil)) {
minUtil = util;
minUtilNode = node;
}
}
logger.info("Least busy node selected " + minUtilNode + " with " + minUtil + " calls");
return minUtilNode;
}
public void init() {
Integer maxTimeInCacheString = getConfiguration().getSipConfiguration().getAlgorithmConfiguration().getCallIdAffinityMaxTimeInCache();
if(maxTimeInCacheString != null) {
this.maxCallIdleTime = maxTimeInCacheString;
}
logger.info("Call Idle Time is " + this.maxCallIdleTime + " seconds. Inactive calls will be evicted.");
earlyDialogWorstCase = lbConfig.getSipConfiguration().getAlgorithmConfiguration().isEarlyDialogWorstCase();
logger.info("Early dialog worst case is " + this.earlyDialogWorstCase);
final WorstCaseUdpTestAffinityAlgorithm thisAlgorithm = this;
this.cacheEvictionTimer.schedule(new TimerTask() {
@Override
public void run() {
try {
synchronized (thisAlgorithm) {
ArrayList<String> oldCalls = new ArrayList<String>();
Iterator<String> keys = callIdTimestamps.keySet().iterator();
while(keys.hasNext()) {
String key = keys.next();
long time = callIdTimestamps.get(key);
if(System.currentTimeMillis() - time > 1000*maxCallIdleTime) {
oldCalls.add(key);
}
}
for(String key : oldCalls) {
callIdMap.remove(key);
callIdTimestamps.remove(key);
}
if(oldCalls.size()>0) {
logger.info("Reaping idle calls... Evicted " + oldCalls.size() + " calls.");
}
// tx
oldCalls = new ArrayList<String>();
keys = txTimestamps.keySet().iterator();
while(keys.hasNext()) {
String key = keys.next();
long time = txTimestamps.get(key);
if(System.currentTimeMillis() - time > 1000*maxCallIdleTime) {
oldCalls.add(key);
}
}
for(String key : oldCalls) {
txToNode.remove(key);
txTimestamps.remove(key);
}
if(oldCalls.size()>0) {
logger.info("Reaping idle transactions... Evicted " + oldCalls.size() + " calls.");
}}
} catch (Exception e) {
logger.warn("Failed to clean up old calls. If you continue to se this message frequestly and the memory is growing, report this problem.", e);
}
}
}, 0, 6000);
this.groupedFailover = getConfiguration().getSipConfiguration().getAlgorithmConfiguration().isCallIdAffinityGroupFailover();
logger.info("Grouped failover is set to " + this.groupedFailover);
}
public void assignToNode(String id, Node node) {
callIdMap.put(id, node);
callIdTimestamps.put(id, System.currentTimeMillis());
}
@Override
public void jvmRouteSwitchover(String fromJvmRoute, String toJvmRoute) {
try {
Node oldNode = getBalancerContext().jvmRouteToSipNode.get(fromJvmRoute);
Node newNode = getBalancerContext().jvmRouteToSipNode.get(toJvmRoute);
if(oldNode != null && newNode != null) {
int updatedRoutes = 0;
for(String key : callIdMap.keySet()) {
Node n = callIdMap.get(key);
if(n.equals(oldNode)) {
callIdMap.replace(key, newNode);
updatedRoutes++;
}
}
if(logger.isInfoEnabled()) {
logger.info("Switchover occured where fromJvmRoute=" + fromJvmRoute + " and toJvmRoute=" + toJvmRoute + " with " +
updatedRoutes + " updated routes.");
}
} else {
if(logger.isInfoEnabled()) {
logger.info("Switchover failed where fromJvmRoute=" + fromJvmRoute + " and toJvmRoute=" + toJvmRoute);
}
}
} catch (Throwable t) {
if(logger.isInfoEnabled()) {
logger.info("Switchover failed where fromJvmRoute=" + fromJvmRoute + " and toJvmRoute=" + toJvmRoute);
logger.info("This is not a fatal failure, logging the reason for the failure ", t);
}
}
}
synchronized public void groupedFailover(Node oldNode, Node newNode) {
try {
if(oldNode != null && newNode != null) {
int updatedRoutes = 0;
for(String key : callIdMap.keySet()) {
Node n = callIdMap.get(key);
if(n.equals(oldNode)) {
callIdMap.replace(key, newNode);
updatedRoutes++;
}
}
if(logger.isInfoEnabled()) {
logger.info("Switchover occured where oldNode=" + oldNode + " and newNode=" + newNode + " with " +
updatedRoutes + " updated routes.");
}
} else {
if(logger.isInfoEnabled()) {
logger.info("Switchover failed where fromJvmRoute=" + oldNode + " and toJvmRoute=" + newNode);
}
}
} catch (Throwable t) {
if(logger.isInfoEnabled()) {
logger.info("Switchover failed where fromJvmRoute=" + oldNode + " and toJvmRoute=" + newNode);
logger.info("This is not a fatal failure, logging the reason for the failure ", t);
}
}
}
}