/*******************************************************************************
* Copyright (C) 2013, 2014, International Business Machines Corporation
* All Rights Reserved
*******************************************************************************/
package com.ibm.streamsx.messaging.jms;
import com.ibm.streams.operator.metrics.Metric;
import com.ibm.streamsx.messaging.common.PropertyProvider;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.InvalidSelectorException;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageFormatException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import com.ibm.streams.operator.logging.LogLevel;
/* This class contains all the connection related information, creating maintaining and closing a connection to the JMSProvider
* Sending and Receiving JMS messages
*/
class JMSConnectionHelper {
// variables required to create connection
// connection factory
private ConnectionFactory connFactory = null;
// destination
private Destination dest = null;
// jndi context
private Context jndiContext = null;
// connection
private Connection connect = null;
// JMS message producer
private MessageProducer producer = null;
// JMS message consumer
private MessageConsumer consumer = null;
// JMS session
private Session session = null;
// The reconnection Policy specified by the user
// defaults to bounderRetry.
private final ReconnectionPolicies reconnectionPolicy;
// ReconnectionBound
private final int reconnectionBound;
// Period
private final double period;
// Is is a producer(JMSSink) or Consumer(JMSSource)
// set to true for JMSSink,false for JMSSource
private final boolean isProducer;
// the delivery mode
private final String deliveryMode;
// the metric which specifies the number of reconnection attempts
// made in case of initial or transient connection failures.
private Metric nReconnectionAttempts;
// Metric to indicate the number of failed inserts to the JMS Provider by
// JMSSink
private Metric nFailedInserts;
// userPrincipal and userCredential will be initialized by
// createAdministeredObjects and used for connection
private String userPrincipal = null;
private String userCredential = null;
// Max number of retries on message send
private final int maxMessageRetries;
// Time to wait before try to resend failed message
private final long messageRetryDelay;
// Indicate message ack mode is client or not
private final boolean useClientAckMode;
// JMS message selector
private String messageSelector;
// Timestamp of session creation
private long sessionCreationTime;
// Consistent region destination object
private Destination destCR = null;
// message producer of the CR queue
private MessageProducer producerCR = null;
// message consumer of the CR queue
private MessageConsumer consumerCR = null;
private PropertyProvider propertyProvider = null;
private String userPropName;
private String passwordPropName;
// CR queue name
private String destinationCR;
private ConnectionDocumentParser connectionDocumentParser = null;
private synchronized MessageConsumer getConsumerCR() {
return consumerCR;
}
private synchronized void setConsumerCR(MessageConsumer consumerCR) {
this.consumerCR = consumerCR;
}
// getter for CR queue producer
private synchronized MessageProducer getProducerCR() {
return producerCR;
}
// setter for CR queue producer
private synchronized void setProducerCR(MessageProducer producer) {
this.producerCR = producer;
}
public long getSessionCreationTime() {
return sessionCreationTime;
}
private void setSessionCreationTime(long sessionCreationTime) {
this.sessionCreationTime = sessionCreationTime;
}
// procedure to detrmine if there exists a valid connection or not
private boolean isConnectValid() {
if (connect != null)
return true;
return false;
}
// getter for consumer
private synchronized MessageConsumer getConsumer() {
return consumer;
}
// setter for consumer
private synchronized void setConsumer(MessageConsumer consumer) {
this.consumer = consumer;
}
// getter for producer
private synchronized MessageProducer getProducer() {
return producer;
}
// setter for producer
private synchronized void setProducer(MessageProducer producer) {
this.producer = producer;
}
// getter for session
synchronized Session getSession() {
return session;
}
// setter for session, synchnized to avoid concurrent access to session
// object
private synchronized void setSession(Session session) {
this.session = session;
this.setSessionCreationTime(System.currentTimeMillis());
}
// setter for connect
// connect is thread safe.Hence not synchronizing.
private void setConnect(Connection connect) {
this.connect = connect;
}
// getter for connect
private Connection getConnect() {
return connect;
}
// logger to get error messages
private Logger logger;
// This constructor sets the parameters required to create a connection for
// JMSSource
JMSConnectionHelper(ConnectionDocumentParser connectionDocumentParser,ReconnectionPolicies reconnectionPolicy,
int reconnectionBound, double period, boolean isProducer,
int maxMessageRetry, long messageRetryDelay,
Metric nReconnectionAttempts, Logger logger, boolean useClientAckMode, String messageSelector,
PropertyProvider propertyProvider, String userPropName, String passwordPropName, String destinationCR) throws NamingException {
this.reconnectionPolicy = reconnectionPolicy;
this.reconnectionBound = reconnectionBound;
this.period = period;
this.isProducer = isProducer;
this.deliveryMode = connectionDocumentParser.getDeliveryMode();
this.logger = logger;
this.nReconnectionAttempts = nReconnectionAttempts;
this.maxMessageRetries = maxMessageRetry;
this.messageRetryDelay = messageRetryDelay;
this.useClientAckMode = useClientAckMode;
this.messageSelector = messageSelector;
this.propertyProvider = propertyProvider;
this.userPropName = userPropName;
this.passwordPropName = passwordPropName;
this.connectionDocumentParser = connectionDocumentParser;
this.userPrincipal = connectionDocumentParser.getUserPrincipal();
this.userCredential = connectionDocumentParser.getUserCredential();
this.destinationCR = destinationCR;
refreshUserCredential();
createAdministeredObjects();
}
// This constructor sets the parameters required to create a connection for
// JMSSink
JMSConnectionHelper(ConnectionDocumentParser connectionDocumentParser, ReconnectionPolicies reconnectionPolicy,
int reconnectionBound, double period, boolean isProducer,
int maxMessageRetry, long messageRetryDelay,
Metric nReconnectionAttempts, Metric nFailedInserts, Logger logger, boolean useClientAckMode, String msgSelectorCR,
PropertyProvider propertyProvider, String userPropName, String passwordPropName, String destinationCR) throws NamingException {
this(connectionDocumentParser, reconnectionPolicy, reconnectionBound, period, isProducer,
maxMessageRetry, messageRetryDelay, nReconnectionAttempts, logger, useClientAckMode, msgSelectorCR, propertyProvider, userPropName, passwordPropName, destinationCR);
this.nFailedInserts = nFailedInserts;
}
// Method to create the initial connection
public void createInitialConnection() throws ConnectionException,
InterruptedException {
createConnection();
return;
}
// Method to create initial connection without retry
public void createInitialConnectionNoRetry() throws ConnectionException {
createConnectionNoRetry();
}
// this subroutine creates the initial jndi context by taking the mandatory
// and optional parameters
private void createAdministeredObjects()
throws NamingException {
// Create a JNDI API InitialContext object if none exists
// create a properties object and add all the mandatory and optional
// parameter
// required to create the jndi context as specified in connection
// document
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, connectionDocumentParser.getInitialContextFactory());
props.put(Context.PROVIDER_URL, connectionDocumentParser.getProviderURL());
// Add the optional elements
if (userPrincipal != null && userCredential != null) {
props.put(Context.SECURITY_PRINCIPAL, userPrincipal);
props.put(Context.SECURITY_CREDENTIALS, userCredential);
}
// create the jndi context
jndiContext = new InitialContext(props);
// Look up connection factory and destination. If either does not exist,
// exit, throws a NamingException if lookup fails
connFactory = (ConnectionFactory) jndiContext.lookup(connectionDocumentParser.getConnectionFactory());
dest = (Destination) jndiContext.lookup(connectionDocumentParser.getDestination());
// Look up CR queue only for producer and when producer is in a CR
if(this.isProducer && this.useClientAckMode) {
destCR = (Destination) jndiContext.lookup(destinationCR);
}
return;
}
// this subroutine creates the connection, it always verifies if we have a
// successfull existing connection before attempting to create one.
private synchronized void createConnection() throws ConnectionException,
InterruptedException {
int nConnectionAttempts = 0;
// Check if connection exists or not.
if (!isConnectValid()) {
// Delay in miliseconds as specified in period parameter
final long delay = TimeUnit.MILLISECONDS.convert((long) period,
TimeUnit.SECONDS);
while (!Thread.interrupted()) {
// make a call to connect subroutine to create a connection
// for each unsuccesfull attempt increment the
// nConnectionAttempts
try {
nConnectionAttempts++;
if(refreshUserCredential()) {
createAdministeredObjects();
}
if (connect(isProducer)) {
// got a successfull connection,
// come out of while loop.
break;
}
} catch (InvalidSelectorException e) {
throw new ConnectionException(
Messages.getString("CONNECTION_TO_JMS_FAILED_INVALID_MSG_SELECTOR")); //$NON-NLS-1$
} catch (JMSException | NamingException e) {
logger.log(LogLevel.ERROR, "RECONNECTION_EXCEPTION", //$NON-NLS-1$
new Object[] { e.toString() });
// Get the reconnectionPolicy
// Apply reconnection policies if the connection was
// unsuccessful
if (reconnectionPolicy == ReconnectionPolicies.NoRetry) {
// Check if ReconnectionPolicy is noRetry, then abort
throw new ConnectionException(
Messages.getString("CONNECTION_TO_JMS_FAILED_DID_NOT_TRY_RECONNECT")); //$NON-NLS-1$
}
// Check if ReconnectionPolicy is BoundedRetry, then try
// once in
// interval defined by period till the reconnectionBound
// If no ReconnectionPolicy is mentioned then also we have a
// default value of reconnectionBound and period
else if (reconnectionPolicy == ReconnectionPolicies.BoundedRetry
&& nConnectionAttempts == reconnectionBound) {
// Bounded number of retries has exceeded.
throw new ConnectionException(
Messages.getString("CONNECTION_TO_JMS_FAILED_NUMBER_OF_TRIES_EXCEDED")); //$NON-NLS-1$
}
// sleep for delay interval
Thread.sleep(delay);
// Incremet the metric nReconnectionAttempts
nReconnectionAttempts.incrementValue(1);
}
}
}
}
private synchronized void createConnectionNoRetry() throws ConnectionException {
if (!isConnectValid()) {
try {
connect(isProducer);
} catch (JMSException e) {
logger.log(LogLevel.ERROR, "CONNECTION_TO_JMS_FAILED", new Object[] { e.toString() }); //$NON-NLS-1$
throw new ConnectionException(
Messages.getString("CONNECTION_TO_JMS_FAILED_NO_RECONNECT_AS_RECONNECT_POLICY_DOES_NOT_APPLY")); //$NON-NLS-1$
}
}
}
// this subroutine creates the connection, producer and consumer, throws a
// JMSException if it fails
private boolean connect(boolean isProducer) throws JMSException {
// Create connection.
if (userPrincipal != null && !userPrincipal.isEmpty() &&
userCredential != null && !userCredential.isEmpty() )
setConnect(connFactory.createConnection(userPrincipal, userCredential));
else
setConnect(connFactory.createConnection());
// Create session from connection; false means
// session is not transacted.
if(isProducer) {
setSession(getConnect().createSession(this.useClientAckMode, Session.AUTO_ACKNOWLEDGE));
}
else {
setSession(getConnect().createSession(false, (this.useClientAckMode) ? Session.CLIENT_ACKNOWLEDGE : Session.AUTO_ACKNOWLEDGE));
}
if (isProducer == true) {
// Its JMSSink, So we will create a producer
setProducer(getSession().createProducer(dest));
if(useClientAckMode) {
// Create producer/consumer of the CR queue
setConsumerCR(getSession().createConsumer(destCR, messageSelector));
setProducerCR(getSession().createProducer(destCR));
// Set time to live to 1 week for CR messages and delivery mode to persistent
getProducerCR().setTimeToLive(TimeUnit.MILLISECONDS.convert(7L, TimeUnit.DAYS));
getProducerCR().setDeliveryMode(DeliveryMode.PERSISTENT);
// start the connection
getConnect().start();
}
// set the delivery mode if it is specified
// default is non-persistent
if (deliveryMode == null) {
getProducer().setDeliveryMode(DeliveryMode.NON_PERSISTENT);
} else {
if (deliveryMode.trim().toLowerCase().equals("non_persistent")) { //$NON-NLS-1$
getProducer().setDeliveryMode(DeliveryMode.NON_PERSISTENT);
}
if (deliveryMode.trim().toLowerCase().equals("persistent")) { //$NON-NLS-1$
getProducer().setDeliveryMode(DeliveryMode.PERSISTENT);
}
}
} else {
// Its JMSSource, So we will create a consumer
setConsumer(getSession().createConsumer(dest, messageSelector));
// start the connection
getConnect().start();
}
// create connection is successful, return true
return true;
}
private boolean refreshUserCredential() {
if(propertyProvider == null) {
return false;
}
String userName = propertyProvider.getProperty(userPropName);
String password = propertyProvider.getProperty(passwordPropName);
if(this.userPrincipal == userName && this.userCredential == password) {
return false;
}
if((this.userPrincipal != null && userName != null && this.userPrincipal.equals(userName))
&& (this.userCredential != null && password != null && this.userCredential.equals(password))) {
return false;
}
logger.log(LogLevel.INFO, "USER_CREDENTIALS_UPDATED"); //$NON-NLS-1$
this.userPrincipal = userName;
this.userCredential = password;
return true;
}
// subroutine which on receiving a message, send it to the
// destination,update the metric
// nFailedInserts if the send fails
boolean sendMessage(Message message) throws ConnectionException,
InterruptedException {
boolean res = false;
int count = 0;
do {
try {
// This is retry, wait before retry
if(count > 0) {
logger.log(LogLevel.INFO, "ATTEMPT_TO_RESEND_MESSAGE", new Object[] { count }); //$NON-NLS-1$
// Wait for a while before next delivery attempt
Thread.sleep(messageRetryDelay);
}
// try to send the message
synchronized (getSession()) {
getProducer().send(message);
res = true;
}
}
catch (JMSException e) {
// error has occurred, log error and try sending message again
logger.log(LogLevel.WARN, "ERROR_DURING_SEND", new Object[] { e.toString() }); //$NON-NLS-1$
logger.log(LogLevel.INFO, "ATTEMPT_TO_RECONNECT"); //$NON-NLS-1$
// Recreate the connection objects if we don't have any (this
// could happen after a connection failure)
setConnect(null);
createConnection();
}
count++;
} while(count < maxMessageRetries && !res);
if(!res) {
nFailedInserts.incrementValue(1);
}
return res;
}
// this subroutine receives messages from a message consumer
// This method supports the receive method with timeout
Message receiveMessage(long timeout) throws ConnectionException, InterruptedException,
JMSException {
try {
// try to receive a message via blocking method
synchronized (getSession()) {
return (getConsumer().receive(timeout));
}
}
catch (JMSException e) {
// If the JMSSource operator was interrupted in middle
if (e.toString().contains("java.lang.InterruptedException")) { //$NON-NLS-1$
throw new java.lang.InterruptedException();
}
// Recreate the connection objects if we don't have any (this
// could happen after a connection failure)
setConnect(null);
logger.log(LogLevel.WARN, "ERROR_DURING_RECEIVE", //$NON-NLS-1$
new Object[] { e.toString() });
logger.log(LogLevel.INFO, "ATTEMPT_TO_RECONNECT"); //$NON-NLS-1$
createConnection();
// retry to receive again
// try to receive a message via blocking method
synchronized (getSession()) {
return (getConsumer().receive(timeout));
}
}
}
// Send message without retry in case of failure
// i.e connection problems, this method raise the error back to caller.
// No connection or message retry will be attempted.
boolean sendMessageNoRetry(Message message) throws JMSException {
boolean res = false;
try {
// try to send the message
synchronized (getSession()) {
getProducer().send(message);
res = true;
}
}
catch (JMSException e) {
// error has occurred, log error and try sending message again
logger.log(LogLevel.WARN, "ERROR_DURING_SEND", new Object[] { e.toString() }); //$NON-NLS-1$
// If the exception is caused by message format, then we can return peacefully as connection is still good.
if(!(e instanceof MessageFormatException)) {
throw e;
}
}
if(!res) {
nFailedInserts.incrementValue(1);
}
return res;
}
// send a consistent region message to the consistent region queue
void sendCRMessage(Message message) throws JMSException {
synchronized (getSession()) {
getProducerCR().send(message);
}
}
// receive a message from consistent region queue
Message receiveCRMessage(long timeout) throws JMSException {
synchronized (getSession()) {
return (getConsumerCR().receive(timeout));
}
}
// Recovers session causing unacknowledged message to be re-delivered
public void recoverSession() throws JMSException, ConnectionException, InterruptedException {
try {
synchronized (getSession()) {
getSession().recover();
}
} catch (JMSException e) {
logger.log(LogLevel.INFO, "ATTEMPT_TO_RECONNECT"); //$NON-NLS-1$
setConnect(null);
createConnection();
synchronized (getSession()) {
getSession().recover();
}
}
}
public void commitSession() throws JMSException {
synchronized (getSession()) {
getSession().commit();
}
}
public void roolbackSession() throws JMSException {
synchronized (getSession()) {
getSession().rollback();
}
}
// close the connection
public void closeConnection() throws JMSException {
if (getSession() != null) {
getSession().close();
}
if (getConnect() != null) {
getConnect().close();
}
}
}