/** * 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 io.hawtjms.provider.discovery.multicast; import io.hawtjms.provider.discovery.DiscoveryAgent; import io.hawtjms.provider.discovery.DiscoveryEvent; import io.hawtjms.provider.discovery.DiscoveryListener; import io.hawtjms.provider.discovery.DiscoveryEvent.EventType; import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.MulticastSocket; import java.net.NetworkInterface; import java.net.SocketAddress; import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Discovery agent that listens on a multicast address for new Broker advisories. */ public class MulticastDiscoveryAgent implements DiscoveryAgent, Runnable { public static final String DEFAULT_DISCOVERY_URI_STRING = "multicast://239.255.2.3:6155"; public static final String DEFAULT_HOST_STR = "default"; public static final String DEFAULT_HOST_IP = System.getProperty("hawtjms.partition.discovery", "239.255.2.3"); public static final int DEFAULT_PORT = 6155; private static final Logger LOG = LoggerFactory.getLogger(MulticastDiscoveryAgent.class); private static final int BUFF_SIZE = 8192; private static final int DEFAULT_IDLE_TIME = 500; private static final int HEARTBEAT_MISS_BEFORE_DEATH = 10; private DiscoveryListener listener; private URI discoveryURI; private int timeToLive = 1; private boolean loopBackMode; private final Map<String, RemoteBrokerData> brokersByService = new ConcurrentHashMap<String, RemoteBrokerData>(); private String group = "default"; private InetAddress inetAddress; private SocketAddress sockAddress; private MulticastSocket mcast; private Thread runner; private long keepAliveInterval = DEFAULT_IDLE_TIME; private String mcInterface; private String mcNetworkInterface; private String mcJoinNetworkInterface; private String service; private final AtomicBoolean started = new AtomicBoolean(false); private PacketParser parser; public MulticastDiscoveryAgent(URI discoveryURI) { this.discoveryURI = discoveryURI; } @Override public void setDiscoveryListener(DiscoveryListener listener) { this.listener = listener; } public DiscoveryListener getDiscoveryListener() { return this.listener; } @Override public void start() throws IOException, IllegalStateException { if (listener == null) { throw new IllegalStateException("No DiscoveryListener configured."); } if (started.compareAndSet(false, true)) { if (group == null || group.length() == 0) { throw new IOException("You must specify a group to discover"); } if (discoveryURI == null) { try { discoveryURI = new URI(DEFAULT_DISCOVERY_URI_STRING); } catch (URISyntaxException e) { // Default is always valid. } } LOG.trace("mcast - discoveryURI = {}", discoveryURI); String myHost = discoveryURI.getHost(); int myPort = discoveryURI.getPort(); if (DEFAULT_HOST_STR.equals(myHost)) { myHost = DEFAULT_HOST_IP; } if (myPort < 0) { myPort = DEFAULT_PORT; } LOG.trace("mcast - myHost = {}", myHost); LOG.trace("mcast - myPort = {}", myPort); LOG.trace("mcast - group = {}", group); LOG.trace("mcast - interface = {}", mcInterface); LOG.trace("mcast - network interface = {}", mcNetworkInterface); LOG.trace("mcast - join network interface = {}", mcJoinNetworkInterface); this.inetAddress = InetAddress.getByName(myHost); this.sockAddress = new InetSocketAddress(this.inetAddress, myPort); mcast = new MulticastSocket(myPort); mcast.setLoopbackMode(loopBackMode); mcast.setTimeToLive(getTimeToLive()); if (mcJoinNetworkInterface != null) { mcast.joinGroup(sockAddress, NetworkInterface.getByName(mcJoinNetworkInterface)); } else { mcast.joinGroup(inetAddress); } mcast.setSoTimeout((int) keepAliveInterval); if (mcInterface != null) { mcast.setInterface(InetAddress.getByName(mcInterface)); } if (mcNetworkInterface != null) { mcast.setNetworkInterface(NetworkInterface.getByName(mcNetworkInterface)); } runner = new Thread(this); runner.setName(this.toString() + ":" + runner.getName()); runner.setDaemon(true); runner.start(); } } @Override public void close() { if (started.compareAndSet(true, false)) { if (mcast != null) { mcast.close(); } if (runner != null) { runner.interrupt(); } } } @Override public void suspend() { // We don't suspend multicast as it's mostly a passive listener. } @Override public void resume() { // We don't suspend multicast as it's mostly a passive listener. } @Override public void run() { byte[] buf = new byte[BUFF_SIZE]; DatagramPacket packet = new DatagramPacket(buf, 0, buf.length); while (started.get()) { expireOldServices(); try { mcast.receive(packet); if (packet.getLength() > 0) { DiscoveryEvent event = parser.processPacket(packet.getData(), packet.getOffset(), packet.getLength()); if (event != null) { if (event.getType() == EventType.ALIVE) { processAlive(event); } else { processShutdown(event); } } } } catch (SocketTimeoutException se) { // ignore } catch (IOException e) { if (started.get()) { LOG.error("failed to process packet: {}", e.getMessage()); LOG.trace(" packet processing failed by: {}", e); } } } } @Override public String toString() { return "MulticastDiscoveryAgent: listener:" + getDiscvoeryURI(); } //---------- Internal Implementation -------------------------------------// private void processAlive(DiscoveryEvent event) { RemoteBrokerData data = brokersByService.get(event.getPeerUri()); if (data == null) { String peerUri = event.getPeerUri(); data = new RemoteBrokerData(event.getPeerUri()); brokersByService.put(peerUri, data); fireServiceAddEvent(data); } else { data.updateHeartBeat(); } } private void processShutdown(DiscoveryEvent event) { RemoteBrokerData data = brokersByService.remove(event.getPeerUri()); if (data != null) { fireServiceRemovedEvent(data); } } private void expireOldServices() { long expireTime = System.currentTimeMillis() - (keepAliveInterval * HEARTBEAT_MISS_BEFORE_DEATH); for (Iterator<RemoteBrokerData> i = brokersByService.values().iterator(); i.hasNext();) { RemoteBrokerData data = i.next(); if (data.getLastHeartBeat() < expireTime) { processShutdown(data.asShutdownEvent()); } } } private void fireServiceRemovedEvent(final RemoteBrokerData data) { if (listener != null && started.get()) { listener.onServiceRemove(data); } } private void fireServiceAddEvent(final RemoteBrokerData data) { if (listener != null && started.get()) { listener.onServiceAdd(data); } } // ---------- Property Accessors ------------------------------------------// /** * @return the original URI used to create the Discovery Agent. */ public URI getDiscvoeryURI() { return this.discoveryURI; } /** * @return Returns the loopBackMode. */ public boolean isLoopBackMode() { return loopBackMode; } /** * @param loopBackMode * The loopBackMode to set. */ public void setLoopBackMode(boolean loopBackMode) { this.loopBackMode = loopBackMode; } /** * @return Returns the timeToLive. */ public int getTimeToLive() { return timeToLive; } /** * @param timeToLive * The timeToLive to set. */ public void setTimeToLive(int timeToLive) { this.timeToLive = timeToLive; } public long getKeepAliveInterval() { return keepAliveInterval; } public void setKeepAliveInterval(long keepAliveInterval) { this.keepAliveInterval = keepAliveInterval; } public void setInterface(String mcInterface) { this.mcInterface = mcInterface; } public void setNetworkInterface(String mcNetworkInterface) { this.mcNetworkInterface = mcNetworkInterface; } public void setJoinNetworkInterface(String mcJoinNetwrokInterface) { this.mcJoinNetworkInterface = mcJoinNetwrokInterface; } /** * @return the multicast group this agent is assigned to. */ public String getGroup() { return this.group; } /** * Sets the multicast group this agent is assigned to. The group can only be set * prior to starting the agent, once started the group change will never take effect. * * @param group * the multicast group the agent is assigned to. */ public void setGroup(String group) { this.group = group; } /** * Returns the name of the service that is providing the discovery data for this agent such * as ActiveMQ. * * @return the name of the service that is advertising remote peer data. */ public String getService() { return this.service; } /** * Sets the name of the service that is providing the remote peer discovery data. * * @param name * the name of the service that provides this agent with remote peer data. */ public void setService(String name) { this.service = name; } /** * @return the currently configured datagram packet parser for this agent. */ public PacketParser getParser() { return parser; } /** * Sets the datagram packet parser used to read the discovery data broadcast by the service * being monitored for remote peers. * * @param parser * the datagram packet parser to use. */ public void setParser(PacketParser parser) { this.parser = parser; } // ---------- Discovered Peer Bookkeeping Class ---------------------------// private class RemoteBrokerData extends DiscoveryEvent { long lastHeartBeat; public RemoteBrokerData(String peerUri) { super(peerUri, EventType.ALIVE); this.lastHeartBeat = System.currentTimeMillis(); } /** * @return an event representing this remote peers shutdown event. */ public DiscoveryEvent asShutdownEvent() { return new DiscoveryEvent(getPeerUri(), EventType.SHUTDOWN); } public synchronized void updateHeartBeat() { lastHeartBeat = System.currentTimeMillis(); } public synchronized long getLastHeartBeat() { return lastHeartBeat; } } }