/*
* 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.protocol.core.impl;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import io.netty.channel.ChannelPipeline;
import org.apache.activemq.artemis.api.core.ActiveMQBuffer;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.ActiveMQExceptionType;
import org.apache.activemq.artemis.api.core.ActiveMQInterruptedException;
import org.apache.activemq.artemis.api.core.Interceptor;
import org.apache.activemq.artemis.api.core.Pair;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory;
import org.apache.activemq.artemis.core.client.ActiveMQClientMessageBundle;
import org.apache.activemq.artemis.core.client.impl.ClientSessionFactoryInternal;
import org.apache.activemq.artemis.core.protocol.ClientPacketDecoder;
import org.apache.activemq.artemis.core.protocol.core.Channel;
import org.apache.activemq.artemis.core.protocol.core.ChannelHandler;
import org.apache.activemq.artemis.core.protocol.core.CoreRemotingConnection;
import org.apache.activemq.artemis.core.protocol.core.Packet;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CheckFailoverMessage;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CheckFailoverReplyMessage;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage_V2;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ClusterTopologyChangeMessage_V3;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionMessage;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionResponseMessage;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.DisconnectMessage;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.DisconnectMessage_V2;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.Ping;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.SubscribeClusterTopologyUpdatesMessageV2;
import org.apache.activemq.artemis.core.remoting.impl.netty.ActiveMQFrameDecoder2;
import org.apache.activemq.artemis.core.version.Version;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.spi.core.remoting.ClientProtocolManager;
import org.apache.activemq.artemis.spi.core.remoting.Connection;
import org.apache.activemq.artemis.spi.core.remoting.SessionContext;
import org.apache.activemq.artemis.spi.core.remoting.TopologyResponseHandler;
import org.apache.activemq.artemis.utils.VersionLoader;
import org.jboss.logging.Logger;
/**
* This class will return specific packets for different types of actions happening on a messaging protocol.
*
* This is trying to unify the Core client into multiple protocols.
*
* Returning null in certain packets means no action is taken on this specific protocol.
*
* Semantic properties could also be added to this implementation.
*
* Implementations of this class need to be stateless.
*/
public class ActiveMQClientProtocolManager implements ClientProtocolManager {
private static final Logger logger = Logger.getLogger(ActiveMQClientProtocolManager.class);
private static final String handshake = "ARTEMIS";
private final int versionID = VersionLoader.getVersion().getIncrementingVersion();
private ClientSessionFactoryInternal factoryInternal;
/**
* Guards assignments to {@link #inCreateSession} and {@link #inCreateSessionLatch}
*/
private final Object inCreateSessionGuard = new Object();
/**
* Flag that tells whether we are trying to create a session.
*/
private boolean inCreateSession;
/**
* Used to wait for the creation of a session.
*/
private CountDownLatch inCreateSessionLatch;
protected volatile RemotingConnectionImpl connection;
protected TopologyResponseHandler topologyResponseHandler;
/**
* Flag that signals that the communication is closing. Causes many processes to exit.
*/
private volatile boolean alive = true;
private final CountDownLatch waitLatch = new CountDownLatch(1);
public ActiveMQClientProtocolManager() {
}
@Override
public String getName() {
return ActiveMQClient.DEFAULT_CORE_PROTOCOL;
}
@Override
public void setSessionFactory(ClientSessionFactory factory) {
this.factoryInternal = (ClientSessionFactoryInternal) factory;
}
@Override
public ClientSessionFactory getSessionFactory() {
return this.factoryInternal;
}
@Override
public void addChannelHandlers(ChannelPipeline pipeline) {
pipeline.addLast("activemq-decoder", new ActiveMQFrameDecoder2());
}
@Override
public boolean waitOnLatch(long milliseconds) throws InterruptedException {
return waitLatch.await(milliseconds, TimeUnit.MILLISECONDS);
}
public Channel getChannel0() {
if (connection == null) {
return null;
} else {
return connection.getChannel(ChannelImpl.CHANNEL_ID.PING.id, -1);
}
}
@Override
public RemotingConnection getCurrentConnection() {
return connection;
}
public Channel getChannel1() {
if (connection == null) {
return null;
} else {
return connection.getChannel(1, -1);
}
}
@Override
public Lock lockSessionCreation() {
try {
Lock localFailoverLock = factoryInternal.lockFailover();
try {
if (connection == null) {
return null;
}
Lock lock = getChannel1().getLock();
// Lock it - this must be done while the failoverLock is held
while (isAlive() && !lock.tryLock(100, TimeUnit.MILLISECONDS)) {
}
return lock;
} finally {
localFailoverLock.unlock();
}
// We can now release the failoverLock
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
@Override
public void stop() {
alive = false;
synchronized (inCreateSessionGuard) {
if (inCreateSessionLatch != null)
inCreateSessionLatch.countDown();
}
Channel channel1 = getChannel1();
if (channel1 != null) {
channel1.returnBlocking();
}
waitLatch.countDown();
}
@Override
public boolean isAlive() {
return alive;
}
@Override
public void ping(long connectionTTL) {
Channel channel = connection.getChannel(ChannelImpl.CHANNEL_ID.PING.id, -1);
Ping ping = new Ping(connectionTTL);
channel.send(ping);
connection.flush();
}
@Override
public void sendSubscribeTopology(final boolean isServer) {
getChannel0().send(new SubscribeClusterTopologyUpdatesMessageV2(isServer, VersionLoader.getVersion().getIncrementingVersion()));
}
@Override
public SessionContext createSessionContext(String name,
String username,
String password,
boolean xa,
boolean autoCommitSends,
boolean autoCommitAcks,
boolean preAcknowledge,
int minLargeMessageSize,
int confirmationWindowSize) throws ActiveMQException {
for (Version clientVersion : VersionLoader.getClientVersions()) {
try {
return createSessionContext(clientVersion, name, username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, minLargeMessageSize, confirmationWindowSize);
} catch (ActiveMQException e) {
if (e.getType() != ActiveMQExceptionType.INCOMPATIBLE_CLIENT_SERVER_VERSIONS) {
throw e;
}
}
}
connection.destroy();
throw new ActiveMQException(ActiveMQExceptionType.INCOMPATIBLE_CLIENT_SERVER_VERSIONS);
}
public SessionContext createSessionContext(Version clientVersion,
String name,
String username,
String password,
boolean xa,
boolean autoCommitSends,
boolean autoCommitAcks,
boolean preAcknowledge,
int minLargeMessageSize,
int confirmationWindowSize) throws ActiveMQException {
if (!isAlive())
throw ActiveMQClientMessageBundle.BUNDLE.clientSessionClosed();
Channel sessionChannel = null;
CreateSessionResponseMessage response = null;
boolean retry;
do {
retry = false;
Lock lock = null;
try {
lock = lockSessionCreation();
// We now set a flag saying createSession is executing
synchronized (inCreateSessionGuard) {
if (!isAlive())
throw ActiveMQClientMessageBundle.BUNDLE.clientSessionClosed();
inCreateSession = true;
inCreateSessionLatch = new CountDownLatch(1);
}
long sessionChannelID = connection.generateChannelID();
Packet request = newCreateSessionPacket(clientVersion, name, username, password, xa, autoCommitSends, autoCommitAcks, preAcknowledge, minLargeMessageSize, confirmationWindowSize, sessionChannelID);
try {
// channel1 reference here has to go away
response = (CreateSessionResponseMessage) getChannel1().sendBlocking(request, PacketImpl.CREATESESSION_RESP);
} catch (ActiveMQException cause) {
if (!isAlive())
throw cause;
if (cause.getType() == ActiveMQExceptionType.UNBLOCKED) {
// This means the thread was blocked on create session and failover unblocked it
// so failover could occur
retry = true;
continue;
} else {
throw cause;
}
}
sessionChannel = connection.getChannel(sessionChannelID, confirmationWindowSize);
} catch (Throwable t) {
if (lock != null) {
lock.unlock();
lock = null;
}
if (t instanceof ActiveMQException) {
throw (ActiveMQException) t;
} else {
throw ActiveMQClientMessageBundle.BUNDLE.failedToCreateSession(t);
}
} finally {
if (lock != null) {
lock.unlock();
}
// Execution has finished so notify any failover thread that may be waiting for us to be done
inCreateSession = false;
inCreateSessionLatch.countDown();
}
} while (retry);
return newSessionContext(name, confirmationWindowSize, sessionChannel, response);
}
protected Packet newCreateSessionPacket(Version clientVersion,
String name,
String username,
String password,
boolean xa,
boolean autoCommitSends,
boolean autoCommitAcks,
boolean preAcknowledge,
int minLargeMessageSize,
int confirmationWindowSize,
long sessionChannelID) {
return new CreateSessionMessage(name, sessionChannelID, clientVersion.getIncrementingVersion(), username, password, minLargeMessageSize, xa, autoCommitSends, autoCommitAcks, preAcknowledge, confirmationWindowSize, null);
}
protected SessionContext newSessionContext(String name,
int confirmationWindowSize,
Channel sessionChannel,
CreateSessionResponseMessage response) {
// these objects won't be null, otherwise it would keep retrying on the previous loop
return new ActiveMQSessionContext(name, connection, sessionChannel, response.getServerVersion(), confirmationWindowSize);
}
@Override
public boolean cleanupBeforeFailover(ActiveMQException cause) {
boolean needToInterrupt;
CountDownLatch exitLockLatch;
Lock lock = lockSessionCreation();
if (lock == null) {
return false;
}
try {
synchronized (inCreateSessionGuard) {
needToInterrupt = inCreateSession;
exitLockLatch = inCreateSessionLatch;
}
} finally {
lock.unlock();
}
if (needToInterrupt) {
forceReturnChannel1(cause);
// Now we need to make sure that the thread has actually exited and returned it's
// connections
// before failover occurs
while (inCreateSession && isAlive()) {
try {
if (exitLockLatch != null) {
exitLockLatch.await(500, TimeUnit.MILLISECONDS);
}
} catch (InterruptedException e1) {
throw new ActiveMQInterruptedException(e1);
}
}
}
return true;
}
@Override
public boolean checkForFailover(String liveNodeID) throws ActiveMQException {
CheckFailoverMessage packet = new CheckFailoverMessage(liveNodeID);
CheckFailoverReplyMessage message = (CheckFailoverReplyMessage) getChannel1().sendBlocking(packet, PacketImpl.CHECK_FOR_FAILOVER_REPLY);
return message.isOkToFailover();
}
@Override
public RemotingConnection connect(Connection transportConnection,
long callTimeout,
long callFailoverTimeout,
List<Interceptor> incomingInterceptors,
List<Interceptor> outgoingInterceptors,
TopologyResponseHandler topologyResponseHandler) {
this.connection = new RemotingConnectionImpl(getPacketDecoder(), transportConnection, callTimeout, callFailoverTimeout, incomingInterceptors, outgoingInterceptors);
this.topologyResponseHandler = topologyResponseHandler;
getChannel0().setHandler(new Channel0Handler(connection));
sendHandshake(transportConnection);
return connection;
}
protected void sendHandshake(Connection transportConnection) {
if (transportConnection.isUsingProtocolHandling()) {
// no need to send handshake on inVM as inVM is not using the NettyProtocolHandling
ActiveMQBuffer amqbuffer = connection.createTransportBuffer(handshake.length());
amqbuffer.writeBytes(handshake.getBytes());
transportConnection.write(amqbuffer);
}
}
private class Channel0Handler implements ChannelHandler {
private final CoreRemotingConnection conn;
private Channel0Handler(final CoreRemotingConnection conn) {
this.conn = conn;
}
@Override
public void handlePacket(final Packet packet) {
final byte type = packet.getType();
if (type == PacketImpl.DISCONNECT || type == PacketImpl.DISCONNECT_V2) {
final DisconnectMessage msg = (DisconnectMessage) packet;
String scaleDownTargetNodeID = null;
SimpleString nodeID = msg.getNodeID();
if (packet instanceof DisconnectMessage_V2) {
final DisconnectMessage_V2 msg_v2 = (DisconnectMessage_V2) packet;
scaleDownTargetNodeID = msg_v2.getScaleDownNodeID() == null ? null : msg_v2.getScaleDownNodeID().toString();
}
if (topologyResponseHandler != null)
topologyResponseHandler.nodeDisconnected(conn, nodeID == null ? null : nodeID.toString(), scaleDownTargetNodeID);
} else if (type == PacketImpl.CLUSTER_TOPOLOGY) {
ClusterTopologyChangeMessage topMessage = (ClusterTopologyChangeMessage) packet;
notifyTopologyChange(topMessage);
} else if (type == PacketImpl.CLUSTER_TOPOLOGY_V2) {
ClusterTopologyChangeMessage_V2 topMessage = (ClusterTopologyChangeMessage_V2) packet;
notifyTopologyChange(topMessage);
} else if (type == PacketImpl.CLUSTER_TOPOLOGY || type == PacketImpl.CLUSTER_TOPOLOGY_V2 || type == PacketImpl.CLUSTER_TOPOLOGY_V3) {
ClusterTopologyChangeMessage topMessage = (ClusterTopologyChangeMessage) packet;
notifyTopologyChange(topMessage);
} else if (type == PacketImpl.CHECK_FOR_FAILOVER_REPLY) {
System.out.println("Channel0Handler.handlePacket");
}
}
/**
* @param topMessage
*/
private void notifyTopologyChange(final ClusterTopologyChangeMessage topMessage) {
final long eventUID;
final String backupGroupName;
final String scaleDownGroupName;
if (topMessage instanceof ClusterTopologyChangeMessage_V3) {
eventUID = ((ClusterTopologyChangeMessage_V3) topMessage).getUniqueEventID();
backupGroupName = ((ClusterTopologyChangeMessage_V3) topMessage).getBackupGroupName();
scaleDownGroupName = ((ClusterTopologyChangeMessage_V3) topMessage).getScaleDownGroupName();
} else if (topMessage instanceof ClusterTopologyChangeMessage_V2) {
eventUID = ((ClusterTopologyChangeMessage_V2) topMessage).getUniqueEventID();
backupGroupName = ((ClusterTopologyChangeMessage_V2) topMessage).getBackupGroupName();
scaleDownGroupName = null;
} else {
eventUID = System.currentTimeMillis();
backupGroupName = null;
scaleDownGroupName = null;
}
if (topMessage.isExit()) {
if (logger.isDebugEnabled()) {
logger.debug("Notifying " + topMessage.getNodeID() + " going down");
}
if (topologyResponseHandler != null) {
topologyResponseHandler.notifyNodeDown(eventUID, topMessage.getNodeID());
}
} else {
Pair<TransportConfiguration, TransportConfiguration> transportConfig = topMessage.getPair();
if (transportConfig.getA() == null && transportConfig.getB() == null) {
transportConfig = new Pair<>(conn.getTransportConnection().getConnectorConfig(), null);
}
if (topologyResponseHandler != null) {
topologyResponseHandler.notifyNodeUp(eventUID, topMessage.getNodeID(), backupGroupName, scaleDownGroupName, transportConfig, topMessage.isLast());
}
}
}
}
protected PacketDecoder getPacketDecoder() {
return ClientPacketDecoder.INSTANCE;
}
private void forceReturnChannel1(ActiveMQException cause) {
if (connection != null) {
Channel channel1 = connection.getChannel(1, -1);
if (channel1 != null) {
channel1.returnBlocking(cause);
}
}
}
}