/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.synapse.endpoints;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.clustering.Member;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.protocol.HTTP;
import org.apache.synapse.MessageContext;
import org.apache.synapse.SynapseConstants;
import org.apache.synapse.SynapseException;
import org.apache.synapse.aspects.ComponentType;
import org.apache.synapse.aspects.flow.statistics.collectors.CloseEventCollector;
import org.apache.synapse.aspects.flow.statistics.collectors.OpenEventCollector;
import org.apache.synapse.aspects.flow.statistics.collectors.RuntimeStatisticCollector;
import org.apache.synapse.core.LoadBalanceMembershipHandler;
import org.apache.synapse.core.SynapseEnvironment;
import org.apache.synapse.core.axis2.Axis2MessageContext;
import org.apache.synapse.core.axis2.Axis2SynapseEnvironment;
import org.apache.synapse.endpoints.algorithms.AlgorithmContext;
import org.apache.synapse.endpoints.dispatch.Dispatcher;
import org.apache.synapse.endpoints.dispatch.HttpSessionDispatcher;
import org.apache.synapse.endpoints.dispatch.SALSessions;
import org.apache.synapse.endpoints.dispatch.SessionInformation;
import org.apache.synapse.transport.nhttp.NhttpConstants;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
/**
* Represents a dynamic load balance endpoint. The application membership is not static,
* but discovered through some mechanism such as using a GCF
*/
public class DynamicLoadbalanceEndpoint extends LoadbalanceEndpoint {
private static final Log log = LogFactory.getLog(DynamicLoadbalanceEndpoint.class);
private static final String PORT_MAPPING_PREFIX = "port.mapping.";
/**
* Flag to enable session affinity based load balancing.
*/
protected boolean sessionAffinity = false;
/**
* Dispatcher used for session affinity.
*/
protected Dispatcher dispatcher = null;
/* Sessions time out interval*/
protected long sessionTimeout = -1;
/**
* The algorithm context , place holder for keep any runtime states related to the load balance
* algorithm
*/
private AlgorithmContext algorithmContext;
@Override
public void init(SynapseEnvironment synapseEnvironment) {
ConfigurationContext cc =
((Axis2SynapseEnvironment) synapseEnvironment).getAxis2ConfigurationContext();
if (!initialized) {
super.init(synapseEnvironment);
if (algorithmContext == null) {
algorithmContext = new AlgorithmContext(isClusteringEnabled, cc, getName());
}
// Initialize the SAL Sessions if already has not been initialized.
SALSessions salSessions = SALSessions.getInstance();
if (!salSessions.isInitialized()) {
salSessions.initialize(isClusteringEnabled, cc);
}
}
log.info("Dynamic load balance endpoint initialized");
}
private LoadBalanceMembershipHandler lbMembershipHandler;
public DynamicLoadbalanceEndpoint() {
}
public void setLoadBalanceMembershipHandler(LoadBalanceMembershipHandler lbMembershipHandler) {
this.lbMembershipHandler = lbMembershipHandler;
}
public LoadBalanceMembershipHandler getLbMembershipHandler() {
return lbMembershipHandler;
}
public void send(MessageContext synCtx) {
if (RuntimeStatisticCollector.isStatisticsEnabled()) {
Integer currentIndex = null;
boolean retry = (synCtx.getProperty(SynapseConstants.LAST_ENDPOINT) != null);
if ((getDefinition() != null) && !retry) {
currentIndex = OpenEventCollector.reportChildEntryEvent(synCtx, getReportingName(),
ComponentType.ENDPOINT, getDefinition().getAspectConfiguration(), true);
}
try {
sendMessage(synCtx);
} finally {
if (currentIndex != null) {
CloseEventCollector.closeEntryEvent(synCtx, getReportingName(),
ComponentType.MEDIATOR, currentIndex, false);
}
}
} else {
sendMessage(synCtx);
}
}
private void sendMessage(MessageContext synCtx) {
logSetter();
SessionInformation sessionInformation = null;
Member currentMember = null;
//TODO Temp hack: ESB removes the session id from request in a random manner.
setCookieHeader(synCtx);
ConfigurationContext configCtx =
((Axis2MessageContext) synCtx).getAxis2MessageContext().getConfigurationContext();
if (lbMembershipHandler.getConfigurationContext() == null) {
lbMembershipHandler.setConfigurationContext(configCtx);
}
if (isSessionAffinityBasedLB()) {
// first check if this session is associated with a session. if so, get the endpoint
// associated for that session.
sessionInformation =
(SessionInformation) synCtx.getProperty(
SynapseConstants.PROP_SAL_CURRENT_SESSION_INFORMATION);
currentMember = (Member) synCtx.getProperty(
SynapseConstants.PROP_SAL_ENDPOINT_CURRENT_MEMBER);
if (sessionInformation == null && currentMember == null) {
sessionInformation = dispatcher.getSession(synCtx);
if (sessionInformation != null) {
if (log.isDebugEnabled()) {
log.debug("Current session id : " + sessionInformation.getId());
}
currentMember = sessionInformation.getMember();
synCtx.setProperty(
SynapseConstants.PROP_SAL_ENDPOINT_CURRENT_MEMBER, currentMember);
// This is for reliably recovery any session information if while response is getting ,
// session information has been removed by cleaner.
// This will not be a cost as session information a not heavy data structure
synCtx.setProperty(
SynapseConstants.PROP_SAL_CURRENT_SESSION_INFORMATION, sessionInformation);
}
}
}
DynamicLoadbalanceFaultHandlerImpl faultHandler = new DynamicLoadbalanceFaultHandlerImpl();
if (sessionInformation != null && currentMember != null) {
//send message on current session
sessionInformation.updateExpiryTime();
sendToApplicationMember(synCtx, currentMember, faultHandler, false);
} else {
// prepare for a new session
currentMember = lbMembershipHandler.getNextApplicationMember(algorithmContext);
if (currentMember == null) {
String msg = "No application members available";
log.error(msg);
throw new SynapseException(msg);
}
sendToApplicationMember(synCtx, currentMember, faultHandler, true);
}
}
protected void setCookieHeader(MessageContext synCtx) {
String cookieHeader = extractSessionID(synCtx, "Cookie");
if (cookieHeader != null) {
synCtx.setProperty("LB_COOKIE_HEADER", cookieHeader);
}
}
//TODO following methods are to extract the session ID temporary hack for Stratos 1.0.0 release
protected String extractSessionID(MessageContext synCtx, String key) {
if (key != null) {
Map headerMap = getTransportHeaderMap(synCtx);
if (headerMap != null) {
Object cookieObj = headerMap.get(key);
if (cookieObj instanceof String) {
return (String) cookieObj;
} else {
if (log.isDebugEnabled()) {
log.debug("Couldn't find the " + key + " header to find the session");
}
}
} else {
if (log.isDebugEnabled()) {
log.debug("Couldn't find the TRANSPORT_HEADERS to find the session");
}
}
}
return null;
}
private Map getTransportHeaderMap(MessageContext synCtx) {
org.apache.axis2.context.MessageContext axis2MessageContext =
((Axis2MessageContext) synCtx).getAxis2MessageContext();
Object o = axis2MessageContext.getProperty("TRANSPORT_HEADERS");
if (o != null && o instanceof Map) {
return (Map) o;
}
return null;
}
/**
* Adding the X-Forwarded-For/X-Originating-IP headers to the outgoing message.
*
* @param synCtx Current message context
*/
protected void setupTransportHeaders(MessageContext synCtx) {
Axis2MessageContext axis2smc = (Axis2MessageContext) synCtx;
org.apache.axis2.context.MessageContext axis2MessageCtx = axis2smc.getAxis2MessageContext();
Object headers = axis2MessageCtx.getProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);
if (headers != null && headers instanceof Map) {
Map headersMap = (Map) headers;
String xForwardFor = (String) headersMap.get(NhttpConstants.HEADER_X_FORWARDED_FOR);
String remoteHost = (String) axis2MessageCtx.getProperty(org.apache.axis2.context.MessageContext.REMOTE_ADDR);
if (xForwardFor != null && !"".equals(xForwardFor)) {
StringBuilder xForwardedForString = new StringBuilder();
xForwardedForString.append(xForwardFor);
if (remoteHost != null && !"".equals(remoteHost)) {
xForwardedForString.append(",").append(remoteHost);
}
headersMap.put(NhttpConstants.HEADER_X_FORWARDED_FOR, xForwardedForString.toString());
} else {
headersMap.put(NhttpConstants.HEADER_X_FORWARDED_FOR, remoteHost);
}
//Extracting information of X-Originating-IP
if (headersMap.get(NhttpConstants.HEADER_X_ORIGINATING_IP_FORM_1) != null) {
headersMap.put(NhttpConstants.HEADER_X_ORIGINATING_IP_FORM_1, headersMap.get(NhttpConstants.HEADER_X_ORIGINATING_IP_FORM_1));
} else if (headersMap.get(NhttpConstants.HEADER_X_ORIGINATING_IP_FORM_2) != null) {
headersMap.put(NhttpConstants.HEADER_X_ORIGINATING_IP_FORM_2, headersMap.get(NhttpConstants.HEADER_X_ORIGINATING_IP_FORM_2));
}
}
}
public void setName(String name) {
super.setName(name);
// algorithmContext.setContextID(name);
}
public Dispatcher getDispatcher() {
return dispatcher;
}
public void setDispatcher(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public long getSessionTimeout() {
return sessionTimeout;
}
public void setSessionTimeout(long sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
public void setSessionAffinity(boolean sessionAffinity){
this.sessionAffinity = sessionAffinity;
}
public boolean isSessionAffinityBasedLB(){
return sessionAffinity;
}
protected void sendToApplicationMember(MessageContext synCtx,
Member currentMember,
DynamicLoadbalanceFaultHandler faultHandler,
boolean newSession) {
//Rewriting the URL
org.apache.axis2.context.MessageContext axis2MsgCtx =
((Axis2MessageContext) synCtx).getAxis2MessageContext();
//Removing the REST_URL_POSTFIX - this is a hack.
//In this loadbalance endpoint we create an endpoint per request by setting the complete url as the adress.
//If a REST message comes Axis2FlexibleMEPClient append the REST_URL_POSTFIX to the adress. Hence endpoint fails
//do send the request. e.g. http://localhost:8080/example/index.html/example/index.html
axis2MsgCtx.removeProperty(NhttpConstants.REST_URL_POSTFIX);
String transport = axis2MsgCtx.getTransportIn().getName();
String address = synCtx.getTo().getAddress();
int incomingPort = extractPort(synCtx, transport);
EndpointReference to = getEndpointReferenceAfterURLRewrite(currentMember,
transport, address, incomingPort);
synCtx.setTo(to);
faultHandler.setTo(to);
faultHandler.setCurrentMember(currentMember);
synCtx.pushFaultHandler(faultHandler);
if (isFailover()) {
synCtx.getEnvelope().build();
}
Endpoint endpoint = getEndpoint(to, currentMember, synCtx);
faultHandler.setCurrentEp(endpoint);
if (isSessionAffinityBasedLB()) {
synCtx.setProperty(SynapseConstants.PROP_SAL_ENDPOINT_DEFAULT_SESSION_TIMEOUT, getSessionTimeout());
synCtx.setProperty(SynapseConstants.PROP_SAL_ENDPOINT_CURRENT_DISPATCHER, dispatcher);
prepareEndPointSequence(synCtx, endpoint);
if(newSession) {
synCtx.setProperty(SynapseConstants.PROP_SAL_ENDPOINT_CURRENT_MEMBER, currentMember);
// we should also indicate that this is the first message in the session. so that
// onFault(...) method can resend only the failed attempts for the first message.
synCtx.setProperty(SynapseConstants.PROP_SAL_ENDPOINT_FIRST_MESSAGE_IN_SESSION, Boolean.TRUE);
}
}
Map<String, String> memberHosts;
if ((memberHosts = (Map<String, String>) currentMember.getProperties().get(HttpSessionDispatcher.HOSTS)) == null) {
currentMember.getProperties().put(HttpSessionDispatcher.HOSTS,
memberHosts = new HashMap<String, String>());
}
memberHosts.put(extractHost(synCtx), "true");
setupTransportHeaders(synCtx);
try {
endpoint.send(synCtx);
} catch (Exception e) {
if(e.getMessage().toLowerCase().contains("io reactor shutdown")){
log.fatal("System cannot continue normal operation. Restarting", e);
System.exit(121); // restart
} else {
throw new SynapseException(e);
}
}
}
/*
* Preparing the endpoint sequence for a new session establishment request
*/
private void prepareEndPointSequence(MessageContext synCtx, Endpoint endpoint) {
Object o = synCtx.getProperty(SynapseConstants.PROP_SAL_ENDPOINT_ENDPOINT_LIST);
List<Endpoint> endpointList;
if (o instanceof List) {
endpointList = (List<Endpoint>) o;
endpointList.add(this);
} else {
// this is the first endpoint in the hierarchy. so create the queue and
// insert this as the first element.
endpointList = new ArrayList<Endpoint>();
endpointList.add(this);
synCtx.setProperty(SynapseConstants.PROP_SAL_ENDPOINT_ENDPOINT_LIST, endpointList);
synCtx.setProperty(SynapseConstants.PROP_SAL_ENDPOINT_CURRENT_DISPATCHER, dispatcher);
}
// if the next endpoint is not a session affinity one, endpoint sequence ends
// here. but we have to add the next endpoint to the list.
if (!(endpoint instanceof DynamicLoadbalanceEndpoint)) {
endpointList.add(endpoint);
// Clearing out if there any any session information with current message
if (dispatcher.isServerInitiatedSession()) {
dispatcher.removeSessionID(synCtx);
}
}
}
private EndpointReference getEndpointReferenceAfterURLRewrite(Member currentMember,
String transport,
String address,
int incomingPort) {
if (transport.startsWith("https")) {
transport = "https";
} else if (transport.startsWith("http")) {
transport = "http";
} else {
String msg = "Cannot load balance for non-HTTP/S transport " + transport;
log.error(msg);
throw new SynapseException(msg);
}
// URL Rewrite
if (transport.startsWith("http") || transport.startsWith("https")) {
if (address.startsWith("http://") || address.startsWith("https://")) {
try {
String _address= address.indexOf("?")>0? address.substring(address.indexOf("?"), address.length()):"";
address = new URL(address).getPath()+_address;
} catch (MalformedURLException e) {
String msg = "URL " + address + " is malformed";
log.error(msg, e);
throw new SynapseException(msg, e);
}
}
int port;
Properties memberProperties = currentMember.getProperties();
String mappedPort = memberProperties.getProperty(PORT_MAPPING_PREFIX + incomingPort);
if (mappedPort != null) {
port = Integer.parseInt(mappedPort);
} else if (transport.startsWith("https")) {
port = currentMember.getHttpsPort();
} else {
port = currentMember.getHttpPort();
}
String remoteHost = memberProperties.getProperty("remoteHost");
String hostName = (remoteHost == null) ? currentMember.getHostName() : remoteHost;
return new EndpointReference(transport + "://" + hostName +
":" + port + address);
} else {
String msg = "Cannot load balance for non-HTTP/S transport " + transport;
log.error(msg);
throw new SynapseException(msg);
}
}
/**
*
* @param to get an endpoint to send the information
* @param member The member to which an EP has to be created
* @param synCtx synapse context
* @return the created endpoint
*/
private Endpoint getEndpoint(EndpointReference to, Member member, MessageContext synCtx) {
AddressEndpoint endpoint = new AddressEndpoint();
endpoint.setEnableMBeanStats(false);
endpoint.setName("DLB:" + member.getHostName() +
":" + member.getPort() + ":" + UUID.randomUUID());
EndpointDefinition definition = new EndpointDefinition();
definition.setSuspendMaximumDuration(10000);
definition.setReplicationDisabled(true);
definition.setAddress(to.getAddress());
endpoint.setDefinition(definition);
endpoint.init((SynapseEnvironment)
((Axis2MessageContext) synCtx).getAxis2MessageContext().
getConfigurationContext().getAxisConfiguration().
getParameterValue(SynapseConstants.SYNAPSE_ENV));
return endpoint;
}
private String extractHost(MessageContext synCtx) {
org.apache.axis2.context.MessageContext msgCtx =
((Axis2MessageContext) synCtx).getAxis2MessageContext();
Map headerMap = (Map) msgCtx.getProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);
String hostName = null;
if (headerMap != null) {
Object hostObj = headerMap.get(HTTP.TARGET_HOST);
hostName = (String) hostObj;
if (hostName.contains(":")) {
hostName = hostName.substring(0, hostName.indexOf(":"));
}
}
return hostName;
}
private int extractPort(MessageContext synCtx, String transport) {
org.apache.axis2.context.MessageContext msgCtx =
((Axis2MessageContext) synCtx).getAxis2MessageContext();
Map headerMap = (Map) msgCtx.getProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);
int port = -1;
if (headerMap != null) {
String hostHeader = (String) headerMap.get(HTTP.TARGET_HOST);
int preIndex = hostHeader.indexOf(":");
int postIndex = hostHeader.indexOf("/");
if (preIndex != -1 && postIndex != -1) {
port = Integer.parseInt(hostHeader.trim().substring(preIndex + 1, postIndex));
} else if (preIndex != -1) {
port = Integer.parseInt(hostHeader.trim().substring(preIndex + 1));
} else {
if ("http".equals(transport)) {
port = 80;
} else if ("https".equals(transport)) {
port = 443;
}
}
}
return port;
}
/**
* This FaultHandler will try to resend the message to another member if an error occurs
* while sending to some member. This is a failover mechanism
*/
private class DynamicLoadbalanceFaultHandlerImpl extends DynamicLoadbalanceFaultHandler {
private EndpointReference to;
private Member currentMember;
private Endpoint currentEp;
public void setCurrentMember(Member currentMember) {
this.currentMember = currentMember;
}
public void setTo(EndpointReference to) {
this.to = to;
}
private DynamicLoadbalanceFaultHandlerImpl() {
}
public void onFault(MessageContext synCtx) {
logSetter();
//cleanup endpoint if exists
if(currentEp != null){
currentEp.destroy();
}
if (currentMember == null) {
return;
}
Stack faultStack = synCtx.getFaultStack();
if (faultStack != null && !faultStack.isEmpty()) {
faultStack.pop(); // Remove the LoadbalanceFaultHandler
}
currentMember = lbMembershipHandler.getNextApplicationMember(algorithmContext);
if(currentMember == null){
String msg = "No application members available";
log.error(msg);
throw new SynapseException(msg);
}
synCtx.setTo(to);
if(isSessionAffinityBasedLB()){
//We are sending the this message on a new session,
// hence we need to remove previous session information
Set pros = synCtx.getPropertyKeySet();
if (pros != null) {
pros.remove(SynapseConstants.PROP_SAL_CURRENT_SESSION_INFORMATION);
}
}
sendToApplicationMember(synCtx, currentMember, this, true);
}
public void setCurrentEp(Endpoint currentEp) {
this.currentEp = currentEp;
}
}
}