/**
* 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.camel.component.rabbitmq;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.apache.camel.AsyncCallback;
import org.apache.camel.Exchange;
import org.apache.camel.FailedToCreateProducerException;
import org.apache.camel.component.rabbitmq.pool.PoolableChannelFactory;
import org.apache.camel.component.rabbitmq.reply.ReplyManager;
import org.apache.camel.component.rabbitmq.reply.TemporaryQueueReplyManager;
import org.apache.camel.impl.DefaultAsyncProducer;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.ServiceHelper;
import org.apache.commons.pool.ObjectPool;
import org.apache.commons.pool.impl.GenericObjectPool;
public class RabbitMQProducer extends DefaultAsyncProducer {
private static final String GENERATED_CORRELATION_ID_PREFIX = "Camel-";
private Connection conn;
private ObjectPool<Channel> channelPool;
private ExecutorService executorService;
private int closeTimeout = 30 * 1000;
private final AtomicBoolean started = new AtomicBoolean(false);
private ReplyManager replyManager;
public RabbitMQProducer(RabbitMQEndpoint endpoint) throws IOException {
super(endpoint);
}
@Override
public RabbitMQEndpoint getEndpoint() {
return (RabbitMQEndpoint) super.getEndpoint();
}
/**
* Channel callback (similar to Spring JDBC ConnectionCallback)
*/
private interface ChannelCallback<T> {
T doWithChannel(Channel channel) throws Exception;
}
/**
* Do something with a pooled channel (similar to Spring JDBC TransactionTemplate#execute)
*/
private <T> T execute(ChannelCallback<T> callback) throws Exception {
Channel channel;
try {
channel = channelPool.borrowObject();
} catch (IllegalStateException e) {
// Since this method is not synchronized its possible the
// channelPool has been cleared by another thread
checkConnectionAndChannelPool();
channel = channelPool.borrowObject();
}
if (!channel.isOpen()) {
log.warn("Got a closed channel from the pool");
// Reconnect if another thread hasn't yet
checkConnectionAndChannelPool();
channel = channelPool.borrowObject();
}
try {
return callback.doWithChannel(channel);
} finally {
channelPool.returnObject(channel);
}
}
/**
* Open connection and initialize channel pool
* @throws Exception
*/
private synchronized void openConnectionAndChannelPool() throws Exception {
log.trace("Creating connection...");
this.conn = getEndpoint().connect(executorService);
log.debug("Created connection: {}", conn);
log.trace("Creating channel pool...");
channelPool = new GenericObjectPool<Channel>(new PoolableChannelFactory(this.conn), getEndpoint().getChannelPoolMaxSize(),
GenericObjectPool.WHEN_EXHAUSTED_BLOCK, getEndpoint().getChannelPoolMaxWait());
if (getEndpoint().isDeclare()) {
execute(new ChannelCallback<Void>() {
@Override
public Void doWithChannel(Channel channel) throws Exception {
getEndpoint().declareExchangeAndQueue(channel);
return null;
}
});
}
}
/**
* This will reconnect only if the connection is closed.
* @throws Exception
*/
private synchronized void checkConnectionAndChannelPool() throws Exception {
if (this.conn == null || !this.conn.isOpen()) {
log.info("Reconnecting to RabbitMQ");
try {
closeConnectionAndChannel();
} catch (Exception e) {
// no op
}
openConnectionAndChannelPool();
}
}
@Override
protected void doStart() throws Exception {
this.executorService = getEndpoint().getCamelContext().getExecutorServiceManager().newSingleThreadExecutor(this, "CamelRabbitMQProducer[" + getEndpoint().getQueue() + "]");
try {
openConnectionAndChannelPool();
} catch (IOException e) {
log.warn("Failed to create connection. It will attempt to connect again when publishing a message.", e);
}
}
/**
* If needed, close Connection and Channel
* @throws IOException
*/
private synchronized void closeConnectionAndChannel() throws IOException {
if (channelPool != null) {
try {
channelPool.close();
channelPool = null;
} catch (Exception e) {
throw new IOException("Error closing channelPool", e);
}
}
if (conn != null) {
log.debug("Closing connection: {} with timeout: {} ms.", conn, closeTimeout);
conn.close(closeTimeout);
conn = null;
}
}
@Override
protected void doStop() throws Exception {
unInitReplyManager();
closeConnectionAndChannel();
if (executorService != null) {
getEndpoint().getCamelContext().getExecutorServiceManager().shutdownNow(executorService);
executorService = null;
}
}
public boolean process(Exchange exchange, AsyncCallback callback) {
// deny processing if we are not started
if (!isRunAllowed()) {
if (exchange.getException() == null) {
exchange.setException(new RejectedExecutionException());
}
// we cannot process so invoke callback
callback.done(true);
return true;
}
try {
if (exchange.getPattern().isOutCapable()) {
// in out requires a bit more work than in only
return processInOut(exchange, callback);
} else {
// in only
return processInOnly(exchange, callback);
}
} catch (Throwable e) {
// must catch exception to ensure callback is invoked as expected
// to let Camel error handling deal with this
exchange.setException(e);
callback.done(true);
return true;
}
}
protected boolean processInOut(final Exchange exchange, final AsyncCallback callback) throws Exception {
final org.apache.camel.Message in = exchange.getIn();
initReplyManager();
// the request timeout can be overruled by a header otherwise the endpoint configured value is used
final long timeout = exchange.getIn().getHeader(RabbitMQConstants.REQUEST_TIMEOUT, getEndpoint().getRequestTimeout(), long.class);
final String originalCorrelationId = in.getHeader(RabbitMQConstants.CORRELATIONID, String.class);
// we append the 'Camel-' prefix to know it was generated by us
String correlationId = GENERATED_CORRELATION_ID_PREFIX + getEndpoint().getCamelContext().getUuidGenerator().generateUuid();
in.setHeader(RabbitMQConstants.CORRELATIONID, correlationId);
in.setHeader(RabbitMQConstants.REPLY_TO, replyManager.getReplyTo());
String exchangeName = in.getHeader(RabbitMQConstants.EXCHANGE_NAME, String.class);
// If it is BridgeEndpoint we should ignore the message header of EXCHANGE_NAME
if (exchangeName == null || getEndpoint().isBridgeEndpoint()) {
exchangeName = getEndpoint().getExchangeName();
}
String key = in.getHeader(RabbitMQConstants.ROUTING_KEY, String.class);
// we just need to make sure RoutingKey option take effect if it is not BridgeEndpoint
if (key == null || getEndpoint().isBridgeEndpoint()) {
key = getEndpoint().getRoutingKey() == null ? "" : getEndpoint().getRoutingKey();
}
if (ObjectHelper.isEmpty(key) && ObjectHelper.isEmpty(exchangeName)) {
throw new IllegalArgumentException("ExchangeName and RoutingKey is not provided in the endpoint: " + getEndpoint());
}
log.debug("Registering reply for {}", correlationId);
replyManager.registerReply(replyManager, exchange, callback, originalCorrelationId, correlationId, timeout);
try {
basicPublish(exchange, exchangeName, key);
} catch (Exception e) {
replyManager.cancelCorrelationId(correlationId);
exchange.setException(e);
return true;
}
// continue routing asynchronously (reply will be processed async when its received)
return false;
}
private boolean processInOnly(Exchange exchange, AsyncCallback callback) throws Exception {
String exchangeName = getEndpoint().getExchangeName(exchange.getIn());
String key = exchange.getIn().getHeader(RabbitMQConstants.ROUTING_KEY, String.class);
// we just need to make sure RoutingKey option take effect if it is not BridgeEndpoint
if (key == null || getEndpoint().isBridgeEndpoint()) {
key = getEndpoint().getRoutingKey() == null ? "" : getEndpoint().getRoutingKey();
}
if (ObjectHelper.isEmpty(key) && ObjectHelper.isEmpty(exchangeName)) {
throw new IllegalArgumentException("ExchangeName and RoutingKey is not provided in the endpoint: " + getEndpoint());
}
basicPublish(exchange, exchangeName, key);
callback.done(true);
return true;
}
/**
* Send a message borrowing a channel from the pool.
*/
private void basicPublish(final Exchange camelExchange, final String rabbitExchange, final String routingKey) throws Exception {
if (channelPool == null) {
// Open connection and channel lazily if another thread hasn't
checkConnectionAndChannelPool();
}
execute(new ChannelCallback<Void>() {
@Override
public Void doWithChannel(Channel channel) throws Exception {
getEndpoint().publishExchangeToChannel(camelExchange, channel, routingKey);
return null;
}
});
}
AMQP.BasicProperties.Builder buildProperties(Exchange exchange) {
return getEndpoint().getMessageConverter().buildProperties(exchange);
}
public int getCloseTimeout() {
return closeTimeout;
}
public void setCloseTimeout(int closeTimeout) {
this.closeTimeout = closeTimeout;
}
protected void initReplyManager() {
if (!started.get()) {
synchronized (this) {
if (started.get()) {
return;
}
log.debug("Starting reply manager");
// must use the classloader from the application context when creating reply manager,
// as it should inherit the classloader from app context and not the current which may be
// a different classloader
ClassLoader current = Thread.currentThread().getContextClassLoader();
ClassLoader ac = getEndpoint().getCamelContext().getApplicationContextClassLoader();
try {
if (ac != null) {
Thread.currentThread().setContextClassLoader(ac);
}
// validate that replyToType and replyTo is configured accordingly
if (getEndpoint().getReplyToType() != null) {
// setting temporary with a fixed replyTo is not supported
if (getEndpoint().getReplyTo() != null && getEndpoint().getReplyToType().equals(ReplyToType.Temporary.name())) {
throw new IllegalArgumentException("ReplyToType " + ReplyToType.Temporary
+ " is not supported when replyTo " + getEndpoint().getReplyTo() + " is also configured.");
}
}
if (getEndpoint().getReplyTo() != null) {
// specifying reply queues is not currently supported
throw new IllegalArgumentException("Specifying replyTo " + getEndpoint().getReplyTo() + " is currently not supported.");
} else {
replyManager = createReplyManager();
log.debug("Using RabbitMQReplyManager: {} to process replies from temporary queue", replyManager);
}
} catch (Exception e) {
throw new FailedToCreateProducerException(getEndpoint(), e);
} finally {
if (ac != null) {
Thread.currentThread().setContextClassLoader(current);
}
}
started.set(true);
}
}
}
protected void unInitReplyManager() {
try {
if (replyManager != null) {
if (log.isDebugEnabled()) {
log.debug("Stopping JmsReplyManager: {} from processing replies from: {}", replyManager,
getEndpoint().getReplyTo() != null ? getEndpoint().getReplyTo() : "temporary queue");
}
ServiceHelper.stopService(replyManager);
}
} catch (Exception e) {
throw ObjectHelper.wrapRuntimeCamelException(e);
} finally {
started.set(false);
}
}
protected ReplyManager createReplyManager() throws Exception {
// use a temporary queue
ReplyManager replyManager = new TemporaryQueueReplyManager(getEndpoint().getCamelContext());
replyManager.setEndpoint(getEndpoint());
String name = "RabbitMQReplyManagerTimeoutChecker[" + getEndpoint().getExchangeName() + "]";
ScheduledExecutorService replyManagerExecutorService = getEndpoint().getCamelContext().getExecutorServiceManager().newSingleThreadScheduledExecutor(name, name);
replyManager.setScheduledExecutorService(replyManagerExecutorService);
log.info("Starting reply manager service " + name);
ServiceHelper.startService(replyManager);
return replyManager;
}
}