/**
* Copyright (c) 2009 - 2012 Red Hat, Inc.
*
* This software is licensed to you under the GNU General Public License,
* version 2 (GPLv2). There is NO WARRANTY for this software, express or
* implied, including the implied warranties of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
* along with this software; if not, see
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
*
* Red Hat trademarks are not licensed under GPLv2. No permission is
* granted to use or replicate Red Hat trademarks that are incorporated
* in this software or its documentation.
*/
package org.candlepin.audit;
import org.candlepin.audit.Event.Target;
import org.candlepin.audit.Event.Type;
import org.candlepin.common.config.Configuration;
import org.candlepin.config.ConfigProperties;
import org.candlepin.controller.ModeManager;
import org.candlepin.controller.SuspendModeTransitioner;
import org.candlepin.model.CandlepinModeChange.Mode;
import org.candlepin.util.Util;
import com.google.inject.Inject;
import org.apache.qpid.client.AMQConnectionFactory;
import org.apache.qpid.jms.BrokerDetails;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.Map.Entry;
import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Session;
import javax.jms.Topic;
import javax.jms.TopicConnection;
import javax.jms.TopicPublisher;
import javax.jms.TopicSession;
import javax.naming.InitialContext;
import javax.naming.NamingException;
/**
* Connection to a Qpid Broker. It can be in two states: CONNECTED or DOWN.
* The class has ability to reconnect to the broker.
*
* This class also orchestrates all the initial configuration of the
* Qpid connection
* @author fnguyen
*
*/
public class QpidConnection {
/**
* This connection factory is created only once upon startup,
* it is configured using many options that we also allow user
* to override in candlepin.conf
*/
private AMQConnectionFactory connectionFactory;
/**
* Candlepin holds only one connection to the Qpid broker. It may be
* closed in case of network or qpid server issues. In that case,
* the client must use connect() method to try establish new connection
*/
private Connection connection;
/**
* A Topic session is associated with connection, when the connection is
* recreated, so must be the TopicSession
*/
private TopicSession session;
/**
* For each combination of target (Consumer, Pool, etc) and type of event
* (CREATED, DELETED, ...) we have a special TopicPublisher. The reason we have
* one publisher for every combo is that we want the events to end up in
* event exchange under specific routing key. The qpid jms client allows us to
* do this using TopicPublisher
*/
private Map<Target, Map<Type, TopicPublisher>> producerMap;
private static Logger log = LoggerFactory.getLogger(QpidConnection.class);
private InitialContext ctx = null;
private STATUS connectionStatus = STATUS.JMS_OBJECTS_STALE;
private QpidConfigBuilder config;
private SuspendModeTransitioner modeTransitioner;
private ModeManager modeManager;
private Configuration candlepinConfig;
/**
* This class is a singleton, just in case that multiple threads
* try to reconnect concurrently, we want to shield ourselves
*/
private static Object connectionLock = new Object();
/**
* Status of the connection as Candlepin sees it
* @author fnguyen
*
*/
public enum STATUS {
CONNECTED,
/**
* Represents situation when connection to Qpid was disrupted.
* JMS objects becomes stale and need to be recreated as per
* JMS specification
*/
JMS_OBJECTS_STALE
}
public void setConnectionStatus(STATUS connectionStatus) {
this.connectionStatus = connectionStatus;
}
@Inject
public QpidConnection(QpidConfigBuilder config, SuspendModeTransitioner modeTransitioner,
ModeManager modeManager, Configuration candlepinConfiguration) {
try {
this.config = config;
this.modeTransitioner = modeTransitioner;
this.modeManager = modeManager;
this.candlepinConfig = candlepinConfiguration;
ctx = new InitialContext(config.buildConfigurationProperties());
connectionFactory = createConnectionFactory();
}
catch (NamingException e) {
throw new RuntimeException(e);
}
}
/**
* Sends a text message to a Qpid Broker. The message will be sent with a binding key that is
* appropriate to the provided Target and Type enumerations. One example one such binding key
* is CONSUMER.CREATED. This further allows Qpid clients to filter out events that
* they are interested in. The reason that for each binding key we have separate
* TopicPublisher is an implementation detail of the Qpid java client.
* @param target enumeration
* @param type enumeration
* @param msg Usually contains serialized JSON with the message
* @throws Exception
*/
public void sendTextMessage(Target target, Type type, String msg) {
try {
/**
* When Candlepin is in NORMAL mode and at the same time the
* JMS objects are stale, it is necessary to recreate them.
*/
if (connectionStatus == STATUS.JMS_OBJECTS_STALE &&
modeManager.getLastCandlepinModeChange().getMode() == Mode.NORMAL) {
log.debug("Recreating the stale JMS objects");
connect();
}
Map<Type, TopicPublisher> m = this.producerMap.get(target);
if (m != null) {
TopicPublisher tp = m.get(type);
tp.send(session.createTextMessage(msg));
}
}
catch (Exception ex) {
log.error("Error sending text message");
connectionStatus = STATUS.JMS_OBJECTS_STALE;
if (candlepinConfig
.getBoolean(ConfigProperties.SUSPEND_MODE_ENABLED)) {
modeTransitioner.transitionAppropriately();
}
throw new RuntimeException("Error sending event to message bus", ex);
}
}
/**
* This idempotent method will establish connection to Qpid Broker. Per JMS standard
* it must recreate all JMS objects such as Connection, TopicSession, TopicPublisher.
* @throws Exception errors during connecting to the Broker
*/
public void connect() throws Exception {
synchronized (connectionLock) {
connection = newConnection();
log.debug("creating topic session");
session = createTopicSession();
log.info("AMQP session created successfully...");
Map<Target, Map<Type, TopicPublisher>> pm = Util.newMap();
buildAllTopicPublishers(pm);
producerMap = pm;
connectionStatus = STATUS.CONNECTED;
}
}
/**
* Creates a new connection to Qpid and starts the connection
* @return normal JMS Connection object
* @throws JMSException possibly when Broker is down
*/
public Connection newConnection() throws JMSException {
log.debug("creating connection");
Connection conn = null;
conn = connectionFactory.createConnection();
conn.start();
return conn;
}
/**
* Closes off all the resources held
*/
public void close() {
connectionStatus = STATUS.JMS_OBJECTS_STALE;
for (Entry<Target, Map<Type, TopicPublisher>> entry : this.producerMap.entrySet()) {
for (Entry<Type, TopicPublisher> tpMap : entry.getValue().entrySet()) {
Util.closeSafely(tpMap.getValue(),
String.format("TopicPublisherOf[%s, %s]", entry.getKey(), tpMap.getKey()));
}
}
Util.closeSafely(this.session, "AMQPSession");
Util.closeSafely(this.connection, "AMQPConnection");
Util.closeSafely(this.ctx, "AMQPContext");
Util.closeSafely(this.connectionFactory, "AMQPConnection");
}
private AMQConnectionFactory createConnectionFactory()
throws NamingException {
log.debug("looking up QpidConnectionfactory");
AMQConnectionFactory connectionFactory = (AMQConnectionFactory) ctx.lookup("qpidConnectionfactory");
Map<String, String> configProperties = config.buildBrokerDetails(ctx);
for (BrokerDetails broker : connectionFactory.getConnectionURL().getAllBrokerDetails()) {
for (Entry<String, String> prop : configProperties.entrySet()) {
// It is important that broker urls are configured with retries and connection
// delays to help avoid issues when the qpidd connection is lost. Candlepin
// will set defaults, or configured value automatically if they are not
// specified in the broker urls.
if (prop.getKey().equals("retries") ||
prop.getKey().equals("connectdelay")) {
if (broker.getProperty(prop.getKey()) != null) {
continue;
}
}
broker.setProperty(prop.getKey(), prop.getValue());
}
log.debug("Broker configured: " + broker);
}
log.info("AMQP connection factory created.");
return connectionFactory;
}
/**
* Creates new topic session on this connection. It is important to understand that when
* Connection to Qpid fails, we need to reestablish all the JMS objects, as per JMS
* specification.
* @return TopicSession for the connection
* @throws JMSException possibly when connection to Qpid is down
*/
public TopicSession createTopicSession() throws JMSException {
return ((TopicConnection) connection).createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
}
/**
* Qpid JMS client requires JNDI. It runs it internally, but we have to use it for both
* configuration and lookup (inconvenient, but necessity)
* @param name Name of the topic that was previously created using QpidConfigBuilder
* @return The topic configured
* @throws NamingException when nothing is found in the JNDI
*/
public Topic lookupTopic(String name) throws NamingException {
return (Topic) ctx.lookup(name);
}
/**
* We create all the topic publishers in advance and reuse them for sending.
* @param pm
* @throws JMSException
* @throws NamingException
*/
private void buildAllTopicPublishers(Map<Target, Map<Type, TopicPublisher>> pm)
throws JMSException, NamingException {
for (Target target : Target.values()) {
Map<Type, TopicPublisher> typeToTpMap = Util.newMap();
for (Type type : Type.values()) {
storeTopicProducer(type, target, typeToTpMap);
}
pm.put(target, typeToTpMap);
}
}
private void storeTopicProducer(Type type, Target target, Map<Type, TopicPublisher> map)
throws JMSException, NamingException {
String name = config.getTopicName(type, target);
Topic topic = lookupTopic(name);
log.debug("Creating publisher for topic: {}", name);
TopicPublisher tp = this.session.createPublisher(topic);
map.put(type, tp);
}
}