/* * 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.qpid.jms.provider.discovery.multicast; import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.InterfaceAddress; import java.net.MulticastSocket; import java.net.NetworkInterface; import java.net.SocketAddress; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.qpid.jms.provider.ProviderException; import org.apache.qpid.jms.provider.discovery.DiscoveryAgent; import org.apache.qpid.jms.provider.discovery.DiscoveryListener; import org.apache.qpid.jms.provider.discovery.multicast.DiscoveryEvent.EventType; import org.apache.qpid.jms.provider.exceptions.ProviderExceptionSupport; 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("qpidjms.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 static final List<String> DEFAULT_EXCLUSIONS = new ArrayList<String>(); static { DEFAULT_EXCLUSIONS.add("vnic"); DEFAULT_EXCLUSIONS.add("tun0"); } private DiscoveryListener listener; private URI discoveryURI; private int timeToLive = 1; private boolean loopBackMode; private final Map<URI, RemoteBrokerData> brokersByService = new ConcurrentHashMap<URI, 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 setScheduler(ScheduledExecutorService scheduler) { // Not needed for this agent } @Override public boolean isSchedulerRequired() { return false; } @Override public void start() throws ProviderException, IllegalStateException { if (listener == null) { throw new IllegalStateException("No DiscoveryListener configured."); } if (started.compareAndSet(false, true)) { if (group == null || group.length() == 0) { throw new ProviderException("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. } if (discoveryURI == null) { throw new RuntimeException("Discovery URI unexpectedly null"); } } LOG.trace("mcast - discoveryURI = {}", discoveryURI); String myHost = discoveryURI.getHost(); int myPort = discoveryURI.getPort(); if (myHost == null || 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); try { 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 { if (mcNetworkInterface != null) { mcast.setNetworkInterface(NetworkInterface.getByName(mcNetworkInterface)); } else { trySetNetworkInterface(mcast); } mcast.joinGroup(inetAddress); } mcast.setSoTimeout((int) keepAliveInterval); if (mcInterface != null) { mcast.setInterface(InetAddress.getByName(mcInterface)); } if (mcNetworkInterface != null) { mcast.setNetworkInterface(NetworkInterface.getByName(mcNetworkInterface)); } } catch (IOException e) { throw ProviderExceptionSupport.createOrPassthroughFatal(e); } 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) { URI 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.getPeerUri()); } } private void fireServiceAddEvent(final RemoteBrokerData data) { if (listener != null && started.get()) { listener.onServiceAdd(data.getPeerUri()); } } // ---------- 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; } public static void trySetNetworkInterface(MulticastSocket mcastSock) throws SocketException { List<NetworkInterface> interfaces = findNetworkInterfaces(); SocketException lastError = null; boolean found = false; for (NetworkInterface networkInterface : interfaces) { try { mcastSock.setNetworkInterface(networkInterface); LOG.debug("Configured mcast socket {} to network interface {}", mcastSock, networkInterface); found = true; break; } catch (SocketException error) { lastError = error; } } if (!found) { if (lastError != null) { throw lastError; } else { throw new SocketException("No NetworkInterface available for this socket."); } } } private static List<NetworkInterface> findNetworkInterfaces() throws SocketException { Enumeration<NetworkInterface> ifcs = NetworkInterface.getNetworkInterfaces(); List<NetworkInterface> interfaces = new ArrayList<NetworkInterface>(); while (ifcs.hasMoreElements()) { NetworkInterface ni = ifcs.nextElement(); LOG.trace("findNetworkInterfaces checking interface: {}", ni); if (ni.supportsMulticast() && ni.isUp()) { for (InterfaceAddress ia : ni.getInterfaceAddresses()) { if (ia.getAddress() instanceof java.net.Inet4Address && !ia.getAddress().isLoopbackAddress() && !DEFAULT_EXCLUSIONS.contains(ni.getName())) { // Add at the start, make usage order consistent with the // existing ActiveMQ releases discovery will be used with. interfaces.add(0, ni); break; } } } } LOG.trace("findNetworkInterfaces returning: {}", interfaces); return interfaces; } // ---------- Discovered Peer Bookkeeping Class ---------------------------// private static class RemoteBrokerData extends DiscoveryEvent { long lastHeartBeat; public RemoteBrokerData(URI 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; } } }