/* * 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.activemq.artemis.core.cluster; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.activemq.artemis.api.core.ActiveMQBuffer; import org.apache.activemq.artemis.api.core.ActiveMQBuffers; import org.apache.activemq.artemis.api.core.ActiveMQInterruptedException; import org.apache.activemq.artemis.api.core.BroadcastEndpoint; import org.apache.activemq.artemis.api.core.BroadcastEndpointFactory; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.TransportConfiguration; import org.apache.activemq.artemis.api.core.management.CoreNotificationType; import org.apache.activemq.artemis.core.client.ActiveMQClientLogger; import org.apache.activemq.artemis.core.server.ActiveMQComponent; import org.apache.activemq.artemis.core.server.management.Notification; import org.apache.activemq.artemis.core.server.management.NotificationService; import org.apache.activemq.artemis.utils.collections.TypedProperties; import org.jboss.logging.Logger; /** * This class is used to search for members on the cluster through the opaque interface {@link BroadcastEndpoint}. * * There are two current implementations, and that's probably all we will ever need. * * We will probably keep both interfaces for a while as UDP is a simple solution requiring no extra dependencies which * is suitable for users looking for embedded solutions. */ public final class DiscoveryGroup implements ActiveMQComponent { private static final Logger logger = Logger.getLogger(DiscoveryGroup.class); private final List<DiscoveryListener> listeners = new ArrayList<>(); private final String name; private Thread thread; private boolean received; private final Object waitLock = new Object(); private final Map<String, DiscoveryEntry> connectors = new ConcurrentHashMap<>(); private final long timeout; private volatile boolean started; private final String nodeID; private final Map<String, String> uniqueIDMap = new HashMap<>(); private final BroadcastEndpoint endpoint; private final NotificationService notificationService; /** * This is the main constructor, intended to be used * * @param nodeID * @param name * @param timeout * @param endpointFactory * @param service * @throws Exception */ public DiscoveryGroup(final String nodeID, final String name, final long timeout, BroadcastEndpointFactory endpointFactory, NotificationService service) throws Exception { this.nodeID = nodeID; this.name = name; this.timeout = timeout; this.endpoint = endpointFactory.createBroadcastEndpoint(); this.notificationService = service; } @Override public synchronized void start() throws Exception { if (started) { return; } endpoint.openClient(); started = true; thread = new Thread(new DiscoveryRunnable(), "activemq-discovery-group-thread-" + name); thread.setDaemon(true); thread.start(); if (notificationService != null) { TypedProperties props = new TypedProperties(); props.putSimpleStringProperty(new SimpleString("name"), new SimpleString(name)); Notification notification = new Notification(nodeID, CoreNotificationType.DISCOVERY_GROUP_STARTED, props); notificationService.sendNotification(notification); } } /** * This will start the DiscoveryRunnable and run it directly. * This is useful for a test process where we need this execution blocking a thread. */ public void internalRunning() throws Exception { endpoint.openClient(); started = true; DiscoveryRunnable runnable = new DiscoveryRunnable(); runnable.run(); } @Override public void stop() { synchronized (this) { if (!started) { return; } started = false; } synchronized (waitLock) { waitLock.notifyAll(); } try { endpoint.close(false); } catch (Exception e1) { ActiveMQClientLogger.LOGGER.errorStoppingDiscoveryBroadcastEndpoint(endpoint, e1); } try { if (thread != null) { thread.interrupt(); thread.join(10000); if (thread.isAlive()) { ActiveMQClientLogger.LOGGER.timedOutStoppingDiscovery(); } } } catch (InterruptedException e) { throw new ActiveMQInterruptedException(e); } thread = null; if (notificationService != null) { TypedProperties props = new TypedProperties(); props.putSimpleStringProperty(new SimpleString("name"), new SimpleString(name)); Notification notification = new Notification(nodeID, CoreNotificationType.DISCOVERY_GROUP_STOPPED, props); try { notificationService.sendNotification(notification); } catch (Exception e) { ActiveMQClientLogger.LOGGER.errorSendingNotifOnDiscoveryStop(e); } } } @Override public boolean isStarted() { return started; } public String getName() { return name; } public synchronized List<DiscoveryEntry> getDiscoveryEntries() { List<DiscoveryEntry> list = new ArrayList<>(connectors.values()); return list; } public boolean waitForBroadcast(final long timeout) { synchronized (waitLock) { long start = System.currentTimeMillis(); long toWait = timeout; while (started && !received && (toWait > 0 || timeout == 0)) { try { waitLock.wait(toWait); } catch (InterruptedException e) { throw new ActiveMQInterruptedException(e); } if (timeout != 0) { long now = System.currentTimeMillis(); toWait -= now - start; start = now; } } boolean ret = received; received = false; return ret; } } /* * This is a sanity check to catch any cases where two different nodes are broadcasting the same node id either * due to misconfiguration or problems in failover */ private void checkUniqueID(final String originatingNodeID, final String uniqueID) { String currentUniqueID = uniqueIDMap.get(originatingNodeID); if (currentUniqueID == null) { uniqueIDMap.put(originatingNodeID, uniqueID); } else { if (!currentUniqueID.equals(uniqueID)) { ActiveMQClientLogger.LOGGER.multipleServersBroadcastingSameNode(originatingNodeID); uniqueIDMap.put(originatingNodeID, uniqueID); } } } class DiscoveryRunnable implements Runnable { @Override public void run() { byte[] data = null; while (started) { try { try { data = endpoint.receiveBroadcast(); if (data == null) { if (started) { // This is totally unexpected, so I'm not even bothering on creating // a log entry for that ActiveMQClientLogger.LOGGER.warn("Unexpected null data received from DiscoveryEndpoint"); } break; } } catch (Exception e) { if (!started) { return; } else { ActiveMQClientLogger.LOGGER.errorReceivingPacketInDiscovery(e); } } ActiveMQBuffer buffer = ActiveMQBuffers.wrappedBuffer(data); String originatingNodeID = buffer.readString(); String uniqueID = buffer.readString(); checkUniqueID(originatingNodeID, uniqueID); if (nodeID.equals(originatingNodeID)) { if (checkExpiration()) { callListeners(); } // Ignore traffic from own node continue; } int size = buffer.readInt(); boolean changed = false; DiscoveryEntry[] entriesRead = new DiscoveryEntry[size]; // Will first decode all the elements outside of any lock for (int i = 0; i < size; i++) { TransportConfiguration connector = new TransportConfiguration(); connector.decode(buffer); entriesRead[i] = new DiscoveryEntry(originatingNodeID, connector, System.currentTimeMillis()); } synchronized (DiscoveryGroup.this) { for (DiscoveryEntry entry : entriesRead) { if (connectors.put(originatingNodeID, entry) == null) { changed = true; } } changed = changed || checkExpiration(); } //only call the listeners if we have changed //also make sure that we aren't stopping to avoid deadlock if (changed && started) { if (logger.isTraceEnabled()) { logger.trace("Connectors changed on Discovery:"); for (DiscoveryEntry connector : connectors.values()) { logger.trace(connector); } } callListeners(); } synchronized (waitLock) { received = true; waitLock.notifyAll(); } } catch (Throwable e) { ActiveMQClientLogger.LOGGER.failedToReceiveDatagramInDiscovery(e); } } } } public synchronized void registerListener(final DiscoveryListener listener) { listeners.add(listener); if (!connectors.isEmpty()) { listener.connectorsChanged(getDiscoveryEntries()); } } public synchronized void unregisterListener(final DiscoveryListener listener) { listeners.remove(listener); } private void callListeners() { for (DiscoveryListener listener : listeners) { try { listener.connectorsChanged(getDiscoveryEntries()); } catch (Throwable t) { // Catch it so exception doesn't prevent other listeners from running ActiveMQClientLogger.LOGGER.failedToCallListenerInDiscovery(t); } } } private boolean checkExpiration() { boolean changed = false; long now = System.currentTimeMillis(); Iterator<Map.Entry<String, DiscoveryEntry>> iter = connectors.entrySet().iterator(); // Weed out any expired connectors while (iter.hasNext()) { Map.Entry<String, DiscoveryEntry> entry = iter.next(); if (entry.getValue().getLastUpdate() + timeout <= now) { if (logger.isTraceEnabled()) { logger.trace("Timed out node on discovery:" + entry.getValue()); } iter.remove(); changed = true; } } return changed; } }