org.apache.activemq.artemis.core.client.impl.Topology Maven / Gradle / Ivy
/**
* 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.client.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.api.core.client.ClusterTopologyListener;
import org.apache.activemq.artemis.core.client.ActiveMQClientLogger;
import org.apache.activemq.artemis.spi.core.remoting.Connector;
public final class Topology
{
private final Set topologyListeners;
private final Executor executor;
/**
* Used to debug operations.
*
* Someone may argue this is not needed. But it's impossible to debug anything related to
* topology without knowing what node or what object missed a Topology update. Hence I added some
* information to locate debugging here.
*/
private volatile Object owner;
/**
* topology describes the other cluster nodes that this server knows about:
*
* keys are node IDs
* values are a pair of live/backup transport configurations
*/
private final Map topology;
private Map mapDelete;
private static final class DirectExecutor implements Executor
{
public void execute(final Runnable runnable)
{
runnable.run();
}
}
public Topology(final Object owner)
{
this(owner, new DirectExecutor());
}
public Topology(final Object owner, final Executor executor)
{
this.topologyListeners = new HashSet<>();
this.topology = new ConcurrentHashMap<>();
if (executor == null)
{
throw new IllegalArgumentException("Executor is required");
}
this.executor = executor;
this.owner = owner;
if (ActiveMQClientLogger.LOGGER.isTraceEnabled())
{
ActiveMQClientLogger.LOGGER.trace("Topology@" + Integer.toHexString(System.identityHashCode(this)) + " CREATE",
new Exception("trace"));
}
}
/**
* It will remove all elements as if it haven't received anyone from the server.
*/
public void clear()
{
topology.clear();
}
public void addClusterTopologyListener(final ClusterTopologyListener listener)
{
if (ActiveMQClientLogger.LOGGER.isTraceEnabled())
{
ActiveMQClientLogger.LOGGER.trace(this + "::Adding topology listener " + listener, new Exception("Trace"));
}
synchronized (topologyListeners)
{
topologyListeners.add(listener);
}
this.sendTopology(listener);
}
public void removeClusterTopologyListener(final ClusterTopologyListener listener)
{
if (ActiveMQClientLogger.LOGGER.isTraceEnabled())
{
ActiveMQClientLogger.LOGGER.trace(this + "::Removing topology listener " + listener, new Exception("Trace"));
}
synchronized (topologyListeners)
{
topologyListeners.remove(listener);
}
}
/** This is called by the server when the node is activated from backup state. It will always succeed */
public void updateAsLive(final String nodeId, final TopologyMemberImpl memberInput)
{
synchronized (this)
{
if (ActiveMQClientLogger.LOGGER.isDebugEnabled())
{
ActiveMQClientLogger.LOGGER.debug(this + "::node " + nodeId + "=" + memberInput);
}
memberInput.setUniqueEventID(System.currentTimeMillis());
topology.remove(nodeId);
topology.put(nodeId, memberInput);
sendMemberUp(nodeId, memberInput);
}
}
/**
* After the node is started, it will resend the notifyLive a couple of times to avoid gossip between two servers
* @param nodeId
*/
public void resendNode(final String nodeId)
{
synchronized (this)
{
TopologyMemberImpl memberInput = topology.get(nodeId);
if (memberInput != null)
{
memberInput.setUniqueEventID(System.currentTimeMillis());
sendMemberUp(nodeId, memberInput);
}
}
}
/** This is called by the server when the node is activated from backup state. It will always succeed */
public TopologyMemberImpl updateBackup(final TopologyMemberImpl memberInput)
{
final String nodeId = memberInput.getNodeId();
if (ActiveMQClientLogger.LOGGER.isTraceEnabled())
{
ActiveMQClientLogger.LOGGER.trace(this + "::updateBackup::" + nodeId + ", memberInput=" + memberInput);
}
synchronized (this)
{
TopologyMemberImpl currentMember = getMember(nodeId);
if (currentMember == null)
{
if (ActiveMQClientLogger.LOGGER.isTraceEnabled())
{
ActiveMQClientLogger.LOGGER.trace("There's no live to be updated on backup update, node=" + nodeId + " memberInput=" + memberInput,
new Exception("trace"));
}
currentMember = memberInput;
topology.put(nodeId, currentMember);
}
TopologyMemberImpl newMember =
new TopologyMemberImpl(nodeId, currentMember.getBackupGroupName(), currentMember.getScaleDownGroupName(), currentMember.getLive(),
memberInput.getBackup());
newMember.setUniqueEventID(System.currentTimeMillis());
topology.remove(nodeId);
topology.put(nodeId, newMember);
sendMemberUp(nodeId, newMember);
return newMember;
}
}
/**
* @param uniqueEventID an unique identifier for when the change was made. We will use current
* time millis for starts, and a ++ of that number for shutdown.
* @param nodeId
* @param memberInput
* @return {@code true} if an update did take place. Note that backups are *always* updated.
*/
public boolean updateMember(final long uniqueEventID, final String nodeId, final TopologyMemberImpl memberInput)
{
Long deleteTme = getMapDelete().get(nodeId);
if (deleteTme != null && uniqueEventID != 0 && uniqueEventID < deleteTme)
{
ActiveMQClientLogger.LOGGER.debug("Update uniqueEvent=" + uniqueEventID +
", nodeId=" +
nodeId +
", memberInput=" +
memberInput +
" being rejected as there was a delete done after that");
return false;
}
synchronized (this)
{
TopologyMemberImpl currentMember = topology.get(nodeId);
if (currentMember == null)
{
if (ActiveMQClientLogger.LOGGER.isTraceEnabled())
{
ActiveMQClientLogger.LOGGER.trace(this + "::NewMemberAdd nodeId=" + nodeId + " member = " + memberInput,
new Exception("trace"));
}
memberInput.setUniqueEventID(uniqueEventID);
topology.put(nodeId, memberInput);
sendMemberUp(nodeId, memberInput);
return true;
}
if (uniqueEventID > currentMember.getUniqueEventID())
{
TopologyMemberImpl newMember =
new TopologyMemberImpl(nodeId, memberInput.getBackupGroupName(), memberInput.getScaleDownGroupName(), memberInput.getLive(),
memberInput.getBackup());
if (newMember.getLive() == null && currentMember.getLive() != null)
{
newMember.setLive(currentMember.getLive());
}
if (newMember.getBackup() == null && currentMember.getBackup() != null)
{
newMember.setBackup(currentMember.getBackup());
}
if (ActiveMQClientLogger.LOGGER.isTraceEnabled())
{
ActiveMQClientLogger.LOGGER.trace(this + "::updated currentMember=nodeID=" + nodeId + ", currentMember=" +
currentMember + ", memberInput=" + memberInput + "newMember=" +
newMember,
new Exception("trace"));
}
newMember.setUniqueEventID(uniqueEventID);
topology.remove(nodeId);
topology.put(nodeId, newMember);
sendMemberUp(nodeId, newMember);
return true;
}
/*
* always add the backup, better to try to reconnect to something that's not there then to
* not know about it at all
*/
if (currentMember.getBackup() == null && memberInput.getBackup() != null)
{
currentMember.setBackup(memberInput.getBackup());
}
return false;
}
}
/**
* @param nodeId
* @param memberToSend
*/
private void sendMemberUp(final String nodeId, final TopologyMemberImpl memberToSend)
{
final ArrayList copy = copyListeners();
if (ActiveMQClientLogger.LOGGER.isTraceEnabled())
{
ActiveMQClientLogger.LOGGER.trace(this + "::prepare to send " + nodeId + " to " + copy.size() + " elements");
}
if (copy.size() > 0)
{
executor.execute(new Runnable()
{
public void run()
{
for (ClusterTopologyListener listener : copy)
{
if (ActiveMQClientLogger.LOGGER.isTraceEnabled())
{
ActiveMQClientLogger.LOGGER.trace(Topology.this + " informing " +
listener +
" about node up = " +
nodeId +
" connector = " +
memberToSend.getConnector());
}
try
{
listener.nodeUP(memberToSend, false);
}
catch (Throwable e)
{
ActiveMQClientLogger.LOGGER.errorSendingTopology(e);
}
}
}
});
}
}
/**
* @return
*/
private ArrayList copyListeners()
{
ArrayList listenersCopy;
synchronized (topologyListeners)
{
listenersCopy = new ArrayList<>(topologyListeners);
}
return listenersCopy;
}
boolean removeMember(final long uniqueEventID, final String nodeId)
{
TopologyMemberImpl member;
synchronized (this)
{
member = topology.get(nodeId);
if (member != null)
{
if (member.getUniqueEventID() > uniqueEventID)
{
ActiveMQClientLogger.LOGGER.debug("The removeMember was issued before the node " + nodeId + " was started, ignoring call");
member = null;
}
else
{
getMapDelete().put(nodeId, uniqueEventID);
member = topology.remove(nodeId);
}
}
}
if (ActiveMQClientLogger.LOGGER.isTraceEnabled())
{
ActiveMQClientLogger.LOGGER.trace("removeMember " + this +
" removing nodeID=" +
nodeId +
", result=" +
member +
", size = " +
topology.size(), new Exception("trace"));
}
if (member != null)
{
final ArrayList copy = copyListeners();
executor.execute(new Runnable()
{
public void run()
{
for (ClusterTopologyListener listener : copy)
{
if (ActiveMQClientLogger.LOGGER.isTraceEnabled())
{
ActiveMQClientLogger.LOGGER.trace(this + " informing " + listener + " about node down = " + nodeId);
}
try
{
listener.nodeDown(uniqueEventID, nodeId);
}
catch (Exception e)
{
ActiveMQClientLogger.LOGGER.errorSendingTopologyNodedown(e);
}
}
}
});
}
return member != null;
}
public synchronized void sendTopology(final ClusterTopologyListener listener)
{
if (ActiveMQClientLogger.LOGGER.isDebugEnabled())
{
ActiveMQClientLogger.LOGGER.debug(this + " is sending topology to " + listener);
}
executor.execute(new Runnable()
{
public void run()
{
int count = 0;
final Map copy;
synchronized (Topology.this)
{
copy = new HashMap(topology);
}
for (Map.Entry entry : copy.entrySet())
{
if (ActiveMQClientLogger.LOGGER.isDebugEnabled())
{
ActiveMQClientLogger.LOGGER.debug(Topology.this + " sending " +
entry.getKey() +
" / " +
entry.getValue().getConnector() +
" to " +
listener);
}
listener.nodeUP(entry.getValue(), ++count == copy.size());
}
}
});
}
public synchronized TopologyMemberImpl getMember(final String nodeID)
{
return topology.get(nodeID);
}
public synchronized TopologyMemberImpl getMember(final TransportConfiguration configuration)
{
for (TopologyMemberImpl member : topology.values())
{
if (member.isMember(configuration))
{
return member;
}
}
return null;
}
public synchronized boolean isEmpty()
{
return topology.isEmpty();
}
public Collection getMembers()
{
ArrayList members;
synchronized (this)
{
members = new ArrayList<>(topology.values());
}
return members;
}
synchronized int nodes()
{
int count = 0;
for (TopologyMemberImpl member : topology.values())
{
if (member.getLive() != null)
{
count++;
}
if (member.getBackup() != null)
{
count++;
}
}
return count;
}
public synchronized String describe()
{
return describe("");
}
private synchronized String describe(final String text)
{
StringBuilder desc = new StringBuilder(text + "topology on " + this + ":\n");
for (Entry entry : new HashMap<>(topology).entrySet())
{
desc.append("\t").append(entry.getKey()).append(" => ").append(entry.getValue()).append("\n");
}
desc.append("\t" + "nodes=").append(nodes()).append("\t").append("members=").append(members());
if (topology.isEmpty())
{
desc.append("\tEmpty");
}
return desc.toString();
}
private int members()
{
return topology.size();
}
/** The owner exists mainly for debug purposes.
* When enabling logging and tracing, the Topology updates will include the owner, what will enable to identify
* what instances are receiving the updates, what will enable better debugging.*/
public void setOwner(final Object owner)
{
this.owner = owner;
}
public TransportConfiguration getBackupForConnector(final Connector connector)
{
for (TopologyMemberImpl member : topology.values())
{
if (member.getLive() != null && connector.isEquivalent(member.getLive().getParams()))
{
return member.getBackup();
}
}
return null;
}
@Override
public String toString()
{
if (owner == null)
{
return "Topology@" + Integer.toHexString(System.identityHashCode(this));
}
return "Topology@" + Integer.toHexString(System.identityHashCode(this)) + "[owner=" + owner + "]";
}
private synchronized Map getMapDelete()
{
if (mapDelete == null)
{
mapDelete = new ConcurrentHashMap<>();
}
return mapDelete;
}
}