/**
* 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.common.config.Configuration;
import org.candlepin.config.ConfigProperties;
import com.google.inject.Inject;
import org.apache.qpid.client.AMQAnyDestination;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Session;
/**
* Class that is able to determine status of Qpid Broker. It can determine whether
* the Qpid Broker is available and also can determine whether it is not FLOW_STOPPED.
*
* To achieve this, we use QMF (Qpid Management Framework)
* @author fnguyen
*
*/
public class QpidQmf {
private QpidConnection qpidConnection;
private static Logger log = LoggerFactory.getLogger(QpidQmf.class);
private String lastFlowStoppedQueue = "";
private Configuration config;
/**
* Status of the connection to Qpid Broker
* @author fnguyen
*/
public enum QpidStatus {
/**
* Qpid is up and running
*/
CONNECTED,
/**
* Qpid is up but the exchange is flow stopped
*/
FLOW_STOPPED,
/**
* Qpid is down
*/
DOWN
}
@Inject
public QpidQmf(QpidConnection qpidConnection, Configuration config)
throws URISyntaxException {
this.qpidConnection = qpidConnection;
this.config = config;
}
/**
* Indempotent method that connects to Qpid and uses QMF to find information about a given
* targetType. The best reference about how to interact with QMF can be found here:
*
* https://access.redhat.com/documentation/en-US/
* Red_Hat_Enterprise_MRG/2/html-single/Messaging_Programming_Reference/index.html
*
* Other concepts and basic docs here
*
* https://qpid.apache.org/releases/qpid-cpp-1.35.0/cpp-broker/book/ch02s02.html
*
* @param targetType
* @param query
* @return
* @throws JMSException
*/
private List<Map<String, Object>> runQuery(String targetType,
Map<Object, Object> query) throws JMSException {
Session session = null;
List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
Connection connection = null;
try {
AMQAnyDestination qmfQueue = null;
AMQAnyDestination responseQueue = null;
try {
qmfQueue = new AMQAnyDestination("qmf.default.direct/broker");
responseQueue = new AMQAnyDestination(
"#reply-queue; {create:always, node:{x-declare:{auto-delete:true}}}");
}
catch (URISyntaxException e) {
throw new RuntimeException("Couldn't create destinations", e);
}
connection = qpidConnection.newConnection();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer sender = session.createProducer(qmfQueue);
MessageConsumer receiver = session.createConsumer(responseQueue);
MapMessage request = session.createMapMessage();
request.setJMSReplyTo(responseQueue);
request.setStringProperty("x-amqp-0-10.app-id", "qmf2");
request.setStringProperty("qmf.opcode", "_query_request");
request.setObject(targetType, query);
request.setObject("_what", "OBJECT"); // method name to be
request.setJMSType("amqp/map");
sender.send(request);
Message response = receiver.receive(
config.getInt(ConfigProperties.QPID_QMF_RECEIVE_TIMEOUT));
if (response != null) {
if (response instanceof MapMessage) {
log.debug("Result received {}", response);
MapMessage mm = (MapMessage) response;
Enumeration en = mm.getMapNames();
while (en.hasMoreElements()) {
Map<String, Object> next = (Map<String, Object>)
mm.getObject(en.nextElement().toString());
result.add(next);
}
return result;
}
else {
log.error("Received response in incorrect format: {}", response);
}
}
else {
log.error("No response received");
}
}
catch (JMSException e) {
throw e;
}
finally {
try {
if (connection != null) {
connection.close();
}
if (session != null) {
session.close();
}
}
catch (JMSException e) {
log.warn("Error closing the Qpid connection", e);
}
}
return null;
}
/**
* There are three situations we might end up. The simples is that Qpid connection might be
* down. Second case is that Qpid is up but it is not responsive, because 'event' exchange
* is flow stopped. This situation might occur when one of its queues is flow stopped. Last
* situation is that Qpid is up and running without any difficulties.
*
*
* More about flow control and flowStopped:
*
* https://qpid.apache.org/releases/qpid-cpp-1.35.0/cpp-broker/book/producer-flow-control.html
*
* @return Enum value. In case of FLOW_STOPPEED, the client can also use getLastFlowStoppedQueue
* to find out which queue caused the flow stop
*
*/
public QpidStatus getStatus() {
try {
for (String queue : getExchangeBoundQueueNames("event")) {
Object qinfo = getQueueInfo(queue);
boolean flowStopped = QpidQmf.<Boolean>extractValue(qinfo,
"_values", "flowStopped");
/**
* The reason we need to indicate this state of FLOW_STOPPED is that it only
* takes a single queue that is flow stopped to block whole 'event' exchange.
* In other words even if just one queue is flow stopped, Candlepin will start
* failing when sending messages to 'event' exchange.
*
* In this flow stopped state, clients normally receive the following
* exception:
*
* JMSException: Exception when sending message:timed out waiting for sync
*/
if (flowStopped) {
lastFlowStoppedQueue = queue;
log.info("Exchange 'event' is flow stopped because of queue {}", queue);
return QpidStatus.FLOW_STOPPED;
}
}
}
catch (JMSException e) {
log.debug("The Qpid is down, received error when communicating with the Qpid", e);
return QpidStatus.DOWN;
}
return QpidStatus.CONNECTED;
}
public String getLastFlowStoppedQueue() {
return lastFlowStoppedQueue;
}
/**
* Finds fully qualified names of queues that are bound to an exchange
*
* @param exchangeName
* @return Fully qualified names of queues
* @throws JMSException Error connecting
*/
private Set<String> getExchangeBoundQueueNames(String exchangeName) throws JMSException {
List<Map<String, Object>> mm = runQuery("_schema_id", singularMap("_class_name", "binding"));
Set<String> result = new HashSet<String>();
for (Map<String, Object> res : mm) {
if (extractValue(res, "_values", "exchangeRef", "_object_name")
.equals("org.apache.qpid.broker:exchange:" + exchangeName)) {
result.add(QpidQmf.<String>extractValue(res, "_values", "queueRef", "_object_name"));
}
}
return result;
}
/**
* Get information about a Queue with a given name
* @param queueName fully qualified queueName
* @return A Map that contains variuos information about the queue
* @throws JMSException When not connected to Broker
*/
private Map<String, Object> getQueueInfo(String queueName) throws JMSException {
log.debug("Getting info about queue {}", queueName);
List<Map<String, Object>> mm = runQuery("_object_id", singularMap("_object_name", queueName));
if (mm.size() == 0) {
throw new RuntimeException("Couldn't find a queue in Qpid: " + queueName);
}
if (mm.size() > 1) {
throw new RuntimeException("Found unexpected amount of information about queue: " + queueName);
}
return mm.get(0);
}
/**
* Map with one entry, key and value
* @param key
* @param value
* @return Map with one entry
*/
private static Map<Object, Object> singularMap(Object key, Object value) {
Map<Object, Object> query = new HashMap<Object, Object>();
query.put(key, value);
return query;
}
/**
* The responses from Qpid come in a form of nested maps. The nested map might have
* several sub-levels and at the end there is some non-map result. It might be a
* String value represented as a byte array. Another possibility is Boolean value.
* This method helps parse the nested map and get the final String value.
* @param object The nested map that contains Maps and byte arrays
* @param mapKeys succession of keys to be extracted. For example if 2 mapKeys
* (key1, key2) are provided, it means that there is a map on level 1 that has key1
* and under that key1 there is a map that contains key2. Under
* key2 there is non-map value
* @return non-map value that resides under the last mapKey
* @throws JMSException Error connecting
*/
public static <T> T extractValue(Object object, String ... mapKeys) throws JMSException {
log.debug("Extracting [{}] from {}", Arrays.toString(mapKeys), object);
if (object == null) {
throw new IllegalArgumentException("Map Names is null");
}
for (String key : mapKeys) {
log.debug("Extracting key {} from the object", key);
if (!(object instanceof Map)) {
throw new RuntimeException("The object under key " + key + " is not a map! The object:" +
object);
}
object = ((Map<String, Object>) object).get(key);
log.debug("Extracted {} under key {}", object, key);
if (object == null) {
throw new RuntimeException("The extracted value at key " + key + " was null!: ");
}
}
if (object instanceof byte[]) {
log.debug("Found byte array that will be Stringified to {}",
new String((byte[]) object));
return (T) new String((byte[]) object);
}
if (object instanceof Boolean) {
log.debug("Found boolean");
return (T) object;
}
else {
throw new RuntimeException(
"Expected the value to be byte[] but found: " + object.getClass());
}
}
}