/**
*
* Copyright 2004 Protique Ltd
*
* 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.codehaus.activemq.transport.multicast;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.Map;
import javax.jms.JMSException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.activemq.io.impl.DefaultWireFormat;
import org.codehaus.activemq.message.ActiveMQMessage;
import org.codehaus.activemq.message.ActiveMQObjectMessage;
import org.codehaus.activemq.message.Packet;
import org.codehaus.activemq.message.PacketListener;
import org.codehaus.activemq.transport.DiscoveryAgentSupport;
import org.codehaus.activemq.transport.DiscoveryEvent;
import org.codehaus.activemq.util.IdGenerator;
import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedBoolean;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedLong;
/**
* An agent used to discover other instances of a service
*
* @version $Revision: 1.5 $
*/
public class MulticastDiscoveryAgent extends DiscoveryAgentSupport implements PacketListener, Runnable {
private static final Log log = LogFactory.getLog(MulticastDiscoveryAgent.class);
/**
* default URI used for discovery
*/
public static final String DEFAULT_DISCOVERY_URI = "multicast://224.1.2.3:6066";
private static final String KEEP_ALIVE_TYPE = "KEEP_ALIVE";
private static final String SERVICE_TYPE = "SERVICE";
private static final String STARTED_TYPE = "STARTED_TYPE";
private static final String SERVICE_NAME = "SERVICE_NAME";
private static final String CHANNEL_NAME = "CHANNEL_NAME";
private static final long DEFAULT_KEEP_ALIVE_TIMEOUT = 5000;
private static final int DEFAULT_TIMEOUT_COUNT = 2;
private ConcurrentHashMap services;
private ConcurrentHashMap keepAliveMap;
private SynchronizedBoolean started;
private MulticastTransportChannel channel;
private Thread runner;
private IdGenerator idGen;
private String localId;
private URI uri;
private int timeoutCount;
private long keepAliveTimeout;
private long timeoutExpiration;
private ActiveMQMessage keepAliveMessage;
private ActiveMQObjectMessage serviceMessage;
private String serviceName = "";
private int timeToLive = 1;
private String channelName;
/**
* Construct a discovery agent that uses multicast
*
* @param channelName
* @throws JMSException
*/
public MulticastDiscoveryAgent(String channelName) throws JMSException {
this.channelName = channelName;
this.started = new SynchronizedBoolean(false);
this.services = new ConcurrentHashMap();
this.keepAliveMap = new ConcurrentHashMap();
this.idGen = new IdGenerator();
this.localId = idGen.generateId();
this.keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT;
this.timeoutCount = DEFAULT_TIMEOUT_COUNT;
this.timeoutExpiration = this.keepAliveTimeout * timeoutCount;
try {
setUri(new URI(DEFAULT_DISCOVERY_URI));
}
catch (URISyntaxException e) {
JMSException jmsEx = new JMSException("URI Syntax exception: " + e.getMessage());
jmsEx.setLinkedException(e);
throw jmsEx;
}
}
/**
* @return Returns the keepAliveTimeout.
*/
public long getKeepAliveTimeout() {
return keepAliveTimeout;
}
/**
* @param keepAliveTimeout The keepAliveTimeout to set.
*/
public void setKeepAliveTimeout(long keepAliveTimeout) {
this.keepAliveTimeout = keepAliveTimeout;
}
/**
* @return Returns the timeoutCount.
*/
public int getTimeoutCount() {
return timeoutCount;
}
/**
* @param timeoutCount The timeoutCount to set.
*/
public void setTimeoutCount(int timeoutCount) {
this.timeoutCount = timeoutCount;
}
/**
* @return Returns the localId.
*/
public String getLocalId() {
return localId;
}
/**
* @param localId The localId to set.
*/
public void setLocalId(String localId) {
this.localId = localId;
}
/**
* @return Returns the uri.
*/
public URI getUri() {
return uri;
}
/**
* @param uri The uri to set.
*/
public void setUri(URI uri) {
this.uri = uri;
}
/**
* @return the timeToLive of multicast packets used for discovery
*/
public int getTimeToLive() {
return this.timeToLive;
}
/**
* @param timeToLive The timeToLive for multicast packets used in discovery.
* @throws IOException
*/
public void setTimeToLive(int timeToLive) throws IOException {
this.timeToLive = timeToLive;
if (channel != null) {
channel.setTimeToLive(timeToLive);
}
}
/**
* @return Returns the channelName.
*/
public String getChannelName() {
return channelName;
}
/**
* @param channelName The channelName to set.
*/
public void setChannelName(String channelName) {
this.channelName = channelName;
}
/**
* @return a pretty print of this instance
*/
public String toString() {
return "MulticastDiscoveryAgent:" + serviceName;
}
/**
* @return the number of active services, including self
*/
public int getServicesCount() {
return (keepAliveMessage != null ? 1 : 0) + services.size();
}
/**
* Register a service for other discover nodes
*
* @param name
* @param details
* @throws JMSException
*/
public void registerService(String name, Map details) throws JMSException {
if (this.keepAliveMessage != null) {
this.keepAliveMessage.setBooleanProperty(STARTED_TYPE, true);
sendKeepAlive();
}
this.serviceName = name;
this.serviceMessage = new ActiveMQObjectMessage();
this.serviceMessage.setJMSType(SERVICE_TYPE);
this.serviceMessage.setStringProperty(SERVICE_NAME, name);
this.serviceMessage.setStringProperty(CHANNEL_NAME, channelName);
this.serviceMessage.setObject((Serializable) details);
sendService();
this.keepAliveMessage = new ActiveMQMessage();
this.keepAliveMessage.setJMSType(KEEP_ALIVE_TYPE);
this.keepAliveMessage.setStringProperty(SERVICE_NAME, name);
this.keepAliveMessage.setStringProperty(CHANNEL_NAME, channelName);
this.keepAliveMessage.setBooleanProperty(STARTED_TYPE, true);
sendKeepAlive();
}
/**
* start this discovery agent
*
* @throws JMSException
*/
public void start() throws JMSException {
if (started.commit(false, true)) {
this.timeoutExpiration = this.keepAliveTimeout * timeoutCount;
channel = new MulticastTransportChannel(new DefaultWireFormat(), uri);
channel.setClientID(localId);
channel.setPacketListener(this);
try {
channel.setTimeToLive(getTimeToLive());
}
catch (IOException e) {
JMSException jmsEx = new JMSException("Set time to live failed");
jmsEx.setLinkedException(e);
throw jmsEx;
}
channel.start();
runner = new Thread(this);
runner.setName(toString());
runner.setDaemon(true);
runner.setPriority(Thread.MAX_PRIORITY);
runner.start();
sendService();
sendKeepAlive();
fireServiceStarted(serviceMessage);
}
}
/**
* stop this discovery agent
*
* @throws JMSException
*/
public void stop() throws JMSException {
boolean doStop = false;
synchronized (started) {
doStop = started.get();
if (doStop) {
if (keepAliveMessage != null) {
keepAliveMessage.setBooleanProperty(STARTED_TYPE, false);
sendKeepAlive();
}
channel.stop();
started.set(false);
}
}
if (doStop) {
fireServiceStopped(serviceMessage);
}
}
/**
* send a keep alive message
*/
public void run() {
try {
int count = 0;
while (started.get()) {
sendKeepAlive();
log.debug(serviceName + " sent keep alive");
if (++count >= timeoutCount) {
count = 0;
checkNodesAlive();
}
Thread.sleep(getKeepAliveTimeout());
}
}
catch (Throwable e) {
log.error(toString() + " run failed", e);
}
}
/**
* Consume multicast packets
*
* @param packet
*/
public void consume(Packet packet) {
try {
if (packet != null && packet.isJMSMessage()) {
ActiveMQMessage msg = (ActiveMQMessage) packet;
String receivedChannelName = msg.getStringProperty(CHANNEL_NAME);
if (receivedChannelName != null && receivedChannelName.equals(channelName)) {
String type = msg.getJMSType();
if (type != null) {
if (type.equals(KEEP_ALIVE_TYPE)) {
processKeepAlive(msg);
}
else if (type.equals(SERVICE_TYPE)) {
processService(msg);
}
else {
log.warn(toString() + " received Message of unknown type: " + type);
}
}else {
log.error(toString() + " message type is null");
}
}
}
else {
log.warn(toString() + " received unexpected packet: " + packet);
}
}
catch (Throwable e) {
log.error(toString() + " couldn't process packet: " + packet, e);
}
}
private void sendKeepAlive() throws JMSException {
if (started.get() && channel != null && !channel.isPendingStop() && keepAliveMessage != null) {
channel.asyncSend(keepAliveMessage);
}
}
private void sendService() throws JMSException {
if (started.get() && channel != null && !channel.isPendingStop() && serviceMessage != null) {
channel.asyncSend(serviceMessage);
}
}
private void processKeepAlive(ActiveMQMessage message) throws JMSException {
String name = message.getStringProperty(SERVICE_NAME);
if (message.getBooleanProperty(STARTED_TYPE)) {
addService(name);
}
else {
removeService(name);
}
}
private void processService(ActiveMQMessage message) throws JMSException {
if (message != null) {
ActiveMQObjectMessage objMsg = (ActiveMQObjectMessage) message;
String name = objMsg.getStringProperty(SERVICE_NAME);
addService(name);
ActiveMQObjectMessage oldMsg = (ActiveMQObjectMessage) services.get(name);
services.put(name, objMsg);
if (oldMsg == null) {
fireServiceStarted(objMsg);
//send out that we are here!
sendService();
}
}
}
private void fireServiceStarted(ActiveMQObjectMessage message) throws JMSException {
if (message != null) {
String name = message.getStringProperty(SERVICE_NAME);
Map map = (Map) message.getObject();
DiscoveryEvent event = new DiscoveryEvent(this, name, map);
fireAddService(event);
}
}
private void fireServiceStopped(ActiveMQObjectMessage message) throws JMSException {
if (message != null) {
String name = message.getStringProperty(SERVICE_NAME);
Map map = (Map) message.getObject();
DiscoveryEvent event = new DiscoveryEvent(this, name, map);
fireRemoveService(event);
}
}
private void addService(String name) {
long timestamp = System.currentTimeMillis();
SynchronizedLong activeTime = (SynchronizedLong) keepAliveMap.get(name);
if (activeTime == null) {
activeTime = new SynchronizedLong(0);
keepAliveMap.put(name, activeTime);
}
activeTime.set(timestamp);
}
private void removeService(String name) throws JMSException {
keepAliveMap.remove(name);
ActiveMQObjectMessage message = (ActiveMQObjectMessage) services.remove(name);
if (message != null) {
fireServiceStopped(message);
}
}
private void checkNodesAlive() throws JMSException {
long timestamp = System.currentTimeMillis();
long timeout = timestamp - timeoutExpiration;
for (Iterator i = keepAliveMap.entrySet().iterator();i.hasNext();) {
Map.Entry entry = (Map.Entry) i.next();
SynchronizedLong activeTime = (SynchronizedLong) entry.getValue();
if (activeTime.get() < timeout) {
String name = entry.getKey().toString();
removeService(name);
log.warn(serviceName + " Expiring node: " + name);
}
}
}
}