/**
* @(#)$Id: AbstractDetector.java,v 1.18 2003/01/14 13:40:52 juhalindfors Exp $
*/
package org.jboss.mx.remote.discovery;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.io.InvalidClassException;
import javax.management.AttributeChangeNotification;
import javax.management.Notification;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanServer;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import EDU.oswego.cs.dl.util.concurrent.*;
import org.jboss.mx.logging.Logger;
import org.jboss.mx.logging.LoggerManager;
import org.jboss.mx.remote.*;
import org.jboss.mx.remote.connector.ConnectorFactory;
import org.jboss.mx.remote.connector.ConnectorMBean;
import org.jboss.mx.util.MBeanTyper;
import org.jboss.mx.util.ThreadPool;
/**
* AbstractDetector is an abstract DetectorMBean class for building Connector implementations
*
* @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
* @author <a href="mailto:telrod@e2technologies.net">Tom Elrod</a>
* */
public abstract class AbstractDetector extends RemoteServiceMBeanSupport
implements DetectorMBean, ConnectorFactory.Listener
{
protected final Logger log = LoggerManager.getLoggerManager().getLogger(getClass().getName());
protected final SynchronizedLong attributeSequence = new SynchronizedLong(0);
protected static final boolean logMethod = Boolean.getBoolean("jboss.mx.detector.debug");
protected String type;
protected long interval = 1500;
protected String serverId;
protected String instanceid;
protected final Map connectors = Collections.synchronizedMap(new HashMap());
protected long timeout = (interval + (interval / 2));
protected long timeoutInterval = 2000;
protected Timer timer;
protected Pinger pinger;
protected Publisher publisher;
protected Detector detector;
protected NotificationChannel notificationChannel;
protected String detectionDomain = System.getProperty("jboss.mx.detector.domain","JBOSS");
protected ThreadPool threadPool = new ThreadPool(false);
//TODO -TME This is a hack so can only detect locally for testing.
private boolean localOnly = Boolean.getBoolean("jboss.mx.detector.local");
final class NotificationChannel extends Thread
{
boolean stopped = false;
Channel channel = new LinkedQueue();
private NotificationChannel()
{
super("Detector-NotificationChannel");
threadPool.setMaximumSize(5);
threadPool.setActive(true);
}
public void run()
{
while(stopped == false)
{
try
{
Runnable r = (Runnable) channel.take();
if(r != null)
{
threadPool.run(r);
}
}
catch(InterruptedException e)
{
break;
}
}
}
}
/**
* set the detection interval in milliseconds
*
* @param interval
*/
public void setInterval(long interval)
{
long old = this.interval;
this.interval = interval;
fireAttributeChange("detector.interval.changed", "Interval", Long.TYPE, new Long(old), new Long(this.interval));
}
/**
* get the detection interval in milliseconds
*
* @return interval
*/
public long getInterval()
{
return interval;
}
/**
* get the type of detector
*
* @return detector type
*/
public String getDetectorType()
{
return type;
}
/**
* return the domain that this detector is part
*
* @return
*/
public String getDetectionDomain ()
{
return detectionDomain;
}
/**
* set the domain for detection - detectors will only process notifications
* from other detectors within the same domain
*
* @param domain
*/
public void setDetectionDomain (String domain)
{
String oldVal = this.detectionDomain;
this.detectionDomain = domain;
fireAttributeChange("detector.domain.change","DetectionDomain",String.class,oldVal,detectionDomain);
}
/**
* return the notification info
*
* @return notification metadata
*/
public MBeanNotificationInfo[] getNotificationInfo()
{
MBeanNotificationInfo ni[] = new MBeanNotificationInfo[2];
ni[0] = new MBeanNotificationInfo(new String[]{"detector.interval.changed", "detector.instanceid.changed", "detector.partition.changed"}, AttributeChangeNotification.class.getName(), "attribute changed notification");
ni[1] = new MBeanNotificationInfo(new String[]{DetectionNotification.STARTUP, DetectionNotification.FAILURE}, DetectionNotification.class.getName(), "detection notification");
return ni;
}
/**
* fire a connector found notification to any listeners
*
* @param local
* @param remote
* @param serverId
* @param transport
* @param properties
* @return notification instance
*/
protected DetectionNotification fireConnectorFound(String detectionDomain, ObjectName local, ObjectName remote, String serverId, String instanceid, String transport, Map properties)
{
DetectionNotification msg = createDetectionStartupNotification(detectionDomain, local, remote, serverId, instanceid, transport, properties);
putNotificationOnChannel(msg);
return msg;
}
/**
* create a detection startup notification object - allows subclasses to override or change values from the base
* class
*
* @param local
* @param remote
* @param serverId
* @param transport
* @param properties
* @return detection startup notification
*/
protected DetectionNotification createDetectionStartupNotification(String detectionDomain, ObjectName local, ObjectName remote, String serverId, String instanceid, String transport, Map properties)
{
DetectionNotification msg = new DetectionNotification(detectionDomain, remote, serverId, instanceid, DetectionNotification.STARTUP, "Detected a new Connector with transport: " + transport + " at: " + serverId);
msg.setRemoteName(remote.getCanonicalName());
msg.setLocalName(local.getCanonicalName());
msg.setTransport(transport);
msg.setProperties(properties);
return msg;
}
/**
* create a detection failure notification object - allows subclasses to override or change values from the base
* class
*
* @param local
* @param remote
* @param serverId
* @param transport
* @param properties
* @return detection failure notification
*/
protected DetectionNotification createDetectionFailureNotification(String detectionDomain, ObjectName local, ObjectName remote, String serverId, String instanceid, String transport, Map properties)
{
DetectionNotification msg = new DetectionNotification(detectionDomain, remote, serverId, instanceid, DetectionNotification.FAILURE, "Detected a lost Connector with transport: " + transport + " at: " + serverId);
msg.setRemoteName(remote.getCanonicalName());
msg.setLocalName(local.getCanonicalName());
msg.setTransport(transport);
msg.setProperties(properties);
return msg;
}
/**
* fire a connector lost notification to any listeners
*
* @param local
* @param remote
* @param serverId
* @param transport
* @param properties
* @return notification instance
*/
protected DetectionNotification fireConnectorLost(String detectionDomain, ObjectName local, ObjectName remote, String serverId, String instanceid, String transport, Map properties)
{
final DetectionNotification msg = createDetectionFailureNotification(detectionDomain, local, remote, serverId, instanceid, transport, properties);
putNotificationOnChannel(msg);
return msg;
}
protected void putNotificationOnChannel(final Notification n)
{
if(notificationChannel != null)
{
try
{
notificationChannel.channel.put(new Runnable()
{
public void run()
{
sendNotification(n);
}
});
}
catch(InterruptedException e)
{
log.warn("Error sending notification: " + n, e);
}
}
}
/**
* helper method for firing an attribute change event
*
* @param event
* @param attribute
* @param type
* @param oldVal
* @param newVal
*/
protected void fireAttributeChange(String event, String attribute, Class type, Object oldVal, Object newVal)
{
if((oldVal == null && newVal != null) ||
(oldVal != null && newVal == null) ||
(oldVal.equals(newVal) == false))
{
sendNotification(
new AttributeChangeNotification(
this,
attributeSequence.increment(),
System.currentTimeMillis(),
event,
attribute,
type.getName(),
oldVal,
newVal));
}
}
/**
* Sub-classes should override this method to provide
* custum 'create' logic.
*
* <p>This method is empty, and is provided for convenience
* when concrete service classes do not need to perform
* anything specific for this state change.
*/
protected void createService() throws Exception
{
super.createService();
// get the instance id
this.instanceid = InstanceID.getID(getServer());
}
/**
* return the instance ID of the Detector MBeanServer
*
* @return instance id
*/
public String getInstanceID()
{
return instanceid;
}
protected MBeanServer getServer()
{
return super.getServer();
}
/**
* inner class for book-keeping of connectors
*/
protected class Connector
{
private String key;
public Map properties;
public long lastDetection;
public String serverId;
public String instanceId;
public String type;
public ObjectName remotename;
public ObjectName localname;
public String transport;
public MBeanServer server;
public String domain;
public String toString()
{
return "Connector [serverId=" + serverId + ",transport=" + transport + ",instanceId=" + instanceId + ",lastDetection=" + new Date(lastDetection) + "]";
}
}
/**
* remove the connector
*
* @param c
*/
protected void remove(Connector c)
{
if(log.isDebugEnabled())
{
log.debug("removing connector = " + c);
}
fireConnectorLost(c.domain, c.localname, c.remotename, c.serverId, c.instanceId, c.transport, c.properties);
try
{
// clean up the connector
ConnectorFactory.destroyConnector(c.serverId);
}
catch(Exception ex)
{
// don't handle since the remote side may be dead -- and that's ok
}
connectors.remove(c.key);
c = null;
}
/**
* remove all
*/
protected void removeAllConnectors()
{
// remove all connectors
synchronized(connectors)
{
Iterator i = connectors.values().iterator();
while(i.hasNext())
{
Connector c = (Connector) i.next();
i.remove();
remove(c);
c = null;
}
}
}
/**
* Sub-classes should override this method to provide
* custum 'stop' logic.
*
* <p>This method is empty, and is provided for convenience
* when concrete service classes do not need to perform
* anything specific for this state change.
*/
protected void stopService() throws Exception
{
super.stopService();
ConnectorFactory.removeListener(this);
stopTimer();
stopPublisher();
stopDetector();
stopNotificationChannel();
removeAllConnectors();
}
/**
* Sub-classes should override this method to provide
* custum 'start' logic.
*
* <p>This method is empty, and is provided for convenience
* when concrete service classes do not need to perform
* anything specific for this state change.
*/
protected void startService() throws Exception
{
super.startService();
serverId = JMXUtil.getServerId(getServer());
if(log.isDebugEnabled())
{
log.debug(getName() + "- serverid = " + serverId);
}
ConnectorFactory.addListener(this);
startNotificationChannel();
startPublisher();
startDetector();
startTimer();
}
/**
* start the notification channel
*/
protected void startNotificationChannel()
{
notificationChannel = new NotificationChannel();
notificationChannel.start();
}
/**
* stop the notification channel
*/
protected void stopNotificationChannel()
{
if(notificationChannel != null)
{
notificationChannel.stopped = true;
try
{
notificationChannel.interrupt();
}
catch(Exception e)
{
}
notificationChannel = null;
}
}
/**
* start the pinger timer
*
*/
protected void startTimer()
{
timer = new Timer(true);
timer.scheduleAtFixedRate(new Pinger(), timeoutInterval * 2, timeoutInterval);
}
/**
* stop the pinger timer
*
*/
protected void stopTimer()
{
if(timer != null)
{
timer.cancel();
timer = null;
}
}
protected class Pinger extends TimerTask
{
public void run()
{
List list = null;
// make a copy so we don't block others...
synchronized(connectors)
{
list = new ArrayList(connectors.values());
}
Iterator i = list.iterator();
long now = System.currentTimeMillis();
while(i.hasNext())
{
Connector c = (Connector) i.next();
if(now - c.lastDetection >= timeout)
{
if(logMethod && log.isDebugEnabled())
{
log.debug("Pinging connector: " + c);
}
// we haven't received a ping in more than timeout
// double check that we're still alive
boolean remove = true;
for(int x = 0; x < 3; x++)
{
try
{
Integer state = (Integer) c.server.getAttribute(c.remotename, "State");
if(logMethod && log.isDebugEnabled())
{
log.debug("State of Connector [" + c + "] is=" + state);
}
if(state != null && state.intValue() == STARTED)
{
c.lastDetection = System.currentTimeMillis();
remove = false;
break;
}
else
{
// pause just a split msec...
Thread.currentThread().sleep(50);
}
}
catch(Throwable ex)
{
break;
}
}
if(remove)
{
if(logMethod && log.isDebugEnabled())
{
log.debug("Ping failed - removing Connector: " + c);
}
remove(c);
connectors.remove(c.key);
i.remove();
c = null;
}
}
}
}
}
/**
* start the publisher thread
*
* @throws Exception
*/
protected void startPublisher()
throws Exception
{
publisher = new Publisher();
publisher.start();
}
/**
* stop the publisher thread
*
* @throws Exception
*/
protected void stopPublisher()
throws Exception
{
if(publisher != null)
{
publisher.running = false;
publisher.interrupt();
publisher = null;
}
}
/**
* subclass should override to publish in the protocol transport specify way
*
* @param notification
* @throws Exception
*/
protected abstract void publish(DetectionNotification notification)
throws Exception;
/**
* inner class to publish our local transports
*/
private final class Publisher extends Thread
{
final InetAddress addr;
final String serverid;
boolean running = true;
Publisher()
throws Exception
{
setName("Detector-Publisher");
setPriority(Thread.NORM_PRIORITY - 1);
setDaemon(false);
addr = InetAddress.getLocalHost();
MBeanServer server = getServer();
serverid = JMXUtil.getServerId(server);
}
public void run()
{
while(running)
{
String transports[] = ConnectorFactory.getConnectorTransports();
try
{
for(int c = 0; c < transports.length; c++)
{
ObjectName query = new ObjectName("jmx.remoting:type=Connector,transport=" + transports[c] + ",*");
/*if (log.isDebugEnabled())
{
log.debug("query => "+query);
} */
Set beans = JMXUtil.queryLocalMBeans(getServer(), query, null);
Iterator i = beans.iterator();
while(i.hasNext())
{
ObjectInstance oi = (ObjectInstance) i.next();
try
{
Integer state = (Integer) getServer().getAttribute(oi.getObjectName(), "State");
if(state.intValue() != ConnectorMBean.STARTED)
{
if(log.isDebugEnabled())
{
log.debug("Connector: " + oi.getObjectName() + " is not started - ignoring publish");
}
continue;
}
}
catch(Exception ex)
{
continue;
}
ConnectorMBean mbean = (ConnectorMBean) MBeanTyper.typeMBean(getServer(), oi.getObjectName(), ConnectorMBean.class);
String type = mbean.getTransportType();
Map props = mbean.getTransportProperties();
ObjectName co = oi.getObjectName();
// convert object name in to a suitable JMXRemoting objectname
ObjectName on = JMXRemotingObjectName.create(serverid, addr, co, co.getKeyProperty("name"), instanceid);
DetectionNotification msg = createDetectionStartupNotification(detectionDomain, co, on, serverId, instanceid, type, props);
if(logMethod && log.isDebugEnabled())
{
log.debug(">> publishing connector detection msg: " + msg);
}
publish(msg);
}
}
}
catch(Exception ex)
{
log.warn("Error sending detection message", ex);
}
finally
{
try
{
sleep(interval);
}
catch(InterruptedException ex)
{
break;
}
}
}
}
}
/**
* make a connector key for the map
*
* @param msg
* @return key
*/
protected String makeKey(DetectionNotification msg)
{
return msg.getServerId() + ":" + msg.getTransport();
}
/**
* receive the detection message in some protocol or transport specific way
*
* @return detection notification
*/
protected abstract DetectionNotification receiveDetection()
throws Exception;
/**
* //TODO -TME This is actually a short term hack to be able to avoid detection of other remote
* servers while testing. Need to come back and add a real, more abstract, way to do this (such
* as being able to add detection strategy).
* @param localOnly
*/
public void setDetectLocalOnly(boolean localOnly)
{
this.localOnly = localOnly;
}
/**
* Determins if detection message should continue to be processed base on some
* kind of preference or strategy (such as if running on local machine).
* @param msg
* @return
*/
protected boolean shouldProcessStartup(DetectionNotification msg)
{
if(localOnly)
{
try
{
String localIp = InetAddress.getLocalHost().getHostAddress();
String msgIp = msg.getIPAddress().getHostAddress();
if(!localIp.equals(msgIp))
{
return false;
}
}
catch(UnknownHostException e)
{
log.warn("can not get local host ip");
}
}
return detectionDomain.equals(msg.getDetectionDomain());
}
protected void register (String key, DetectionNotification msg, Connector connector)
{
try
{
// this are opposites since our local name used is actually the
// remote name to register locally, and vise versa
MBeanServerConnection conn = new MBeanServerConnection(msg);
connector.server = ConnectorFactory.createConnector(conn);
if(logMethod && log.isDebugEnabled())
{
log.debug("*** Registered Connector to remote server id: " + connector.serverId);
}
}
catch(Throwable ex)
{
if(logMethod && log.isDebugEnabled())
{
ex.printStackTrace();
}
if(ex instanceof java.net.ConnectException)
{
// we failed to connect to remote connector, discard
}
else
{
log.error("Couldn't create a Connector to server: " + connector.serverId + ", transport: " + connector.transport + ", localname: " + connector.localname + ", remotename: " + connector.remotename, ex);
}
if(connector.server != null)
{
try
{
ConnectorFactory.destroyConnector(connector.serverId);
}
catch(Exception ex1)
{
//
}
}
connectors.remove(key);
return;
}
if(log.isDebugEnabled())
{
log.debug("<< FOUND! Detected new Connector ... " + msg);
}
// fire detection notification
fireConnectorFound(connector.domain, connector.localname, connector.remotename, msg.getServerId(), msg.getInstanceId(), msg.getTransport(), msg.getProperties());
}
protected synchronized void process(final DetectionNotification msg)
{
if(msg == null)
{
return;
}
if(msg.getServerId().equals(serverId))
{
// ignore detections from ourself
return;
}
// process the detection message
String key = makeKey(msg);
Connector connector = (Connector) connectors.get(key);
if(connector != null && msg.getInstanceId() != null && connector.instanceId.equals(msg.getInstanceId()) == false)
{
// we have detected a notification that indicates the instance id has changed for the same serverid
remove(connector);
// null which will force it to reload
connector = null;
}
if(connector != null && msg.getProperties() != null && msg.getProperties().equals(connector.properties) == false && msg.getType().equals(DetectionNotification.STARTUP))
{
// we have dedicated a connector
remove(connector);
connector = null;
}
//log.debug("key="+key+", found="+connector);
if(connector == null && msg.getType().equals(DetectionNotification.STARTUP))
{
if(shouldProcessStartup(msg))
{
if(ConnectorFactory.hasMBeanServerForServerId(msg.getServerId()))
{
// ignore if we have it already registered
return;
}
// found a new connector
connector = new Connector();
connector.properties = msg.getProperties();
connector.serverId = msg.getServerId();
connector.transport = msg.getTransport();
connector.instanceId = msg.getInstanceId();
connector.key = key;
connector.lastDetection = System.currentTimeMillis();
try
{
connector.localname = new ObjectName(msg.getRemoteName());
connector.remotename = new ObjectName(msg.getLocalName());
connector.domain = msg.getDetectionDomain();
}
catch (Exception ex)
{
// this should never happen
throw new RuntimeException(ex);
}
if(connector.instanceId == null)
{
connector.instanceId = connector.serverId;
}
final Connector _conn = connector;
final String _key = key;
connectors.put(key, connector);
threadPool.run(
new Runnable()
{
public void run ()
{
register(_key,msg,_conn);
}
}
);
return;
}
}
if(connector != null)
{
connector.lastDetection = System.currentTimeMillis();
}
else
if(msg.getType().equals(DetectionNotification.FAILURE) && connector != null)
{
// destroy
try
{
ConnectorFactory.destroyConnector(connector.serverId);
}
catch(Exception ex)
{
if(log.isDebugEnabled())
{
log.debug("Error destroying connector on remote failure - " + connector.serverId, ex);
}
}
connectors.remove(key);
if(connector.server != null)
{
try
{
ConnectorFactory.destroyConnector(connector.serverId);
}
catch(Exception ex1)
{
//
}
}
fireConnectorLost(connector.domain,connector.localname, connector.remotename, msg.getServerId(), msg.getInstanceId(), msg.getTransport(), msg.getProperties());
}
}
/**
* start the detector thread
*
*/
protected void startDetector()
{
detector = new Detector();
detector.start();
}
/**
* stop the detector thread
*
*/
protected void stopDetector()
{
if(detector != null)
{
detector.running = false;
detector.interrupt();
detector = null;
}
}
/**
* event is fired when a connector is created
*
* @param server
* @param conn
*/
public void connectorCreated(MBeanServer server, MBeanServerConnection conn)
{
// NO OP - will catch in publish
}
/**
* event is fired when a connector is destroyed
*
* @param server
* @param conn
*/
public void connectorDestroyed(MBeanServer server, MBeanServerConnection conn)
{
List list = null;
// make a copy so we don't block others
synchronized(connectors)
{
list = new ArrayList(connectors.values());
}
Iterator iter = list.iterator();
while(iter.hasNext())
{
Connector con = (Connector) iter.next();
// look for connector
// ideally, we need to also include instanceid
if(con.serverId.equals(serverId) && con.properties.equals(conn.getProperties()) && conn.getTransport().equals(con.transport))
{
// remove by key (this is a copy, so move from the source)
Object obj = connectors.remove(con.key);
if(obj != null)
{
// someone removed connector before we detected it gone, fire lost notification
fireConnectorLost(con.domain, con.localname, con.remotename, con.serverId, con.instanceId, con.transport, con.properties);
}
obj = null;
break;
}
}
list = null;
iter = null;
}
/**
* class that will do the detection
*/
protected class Detector extends Thread
{
boolean running = true;
Detector()
{
setName("Detector");
setPriority(Thread.NORM_PRIORITY - 1);
setDaemon(false);
}
public void run()
{
while(running)
{
try
{
DetectionNotification msg = receiveDetection();
process(msg);
}
catch (InvalidClassException ice)
{
// our detection notification fails because a remote server
// has a different version of our detection class
log.warn("Received and out-of-date Detection Notification from remote detector");
}
catch(Exception ex)
{
log.warn("Error receiving detection", ex);
}
}
}
}
}