awips2/edexOsgi/com.raytheon.uf.common.jms/src/com/raytheon/uf/common/jms/JmsPooledConnection.java
Richard Peter ea806139d3 Issue #1642: Fix QPID Deadlock scenario
- Address peer review comments

Change-Id: I3c52fd57f762a762d54fdce3651c7f17a8fc6462

Former-commit-id: 7d4206420c [formerly 9a0177861f] [formerly 5b2650599d [formerly 2071d4f3cc54a3da43443bc71cecf038736194d8]]
Former-commit-id: 5b2650599d
Former-commit-id: 4deaf372a1
2013-02-26 16:01:25 -06:00

549 lines
18 KiB
Java

/**
* This software was developed and / or modified by Raytheon Company,
* pursuant to Contract DG133W-05-CQ-1067 with the US Government.
*
* U.S. EXPORT CONTROLLED TECHNICAL DATA
* This software product contains export-restricted data whose
* export/transfer/disclosure is restricted by U.S. law. Dissemination
* to non-U.S. persons whether in the United States or abroad requires
* an export license or other authorization.
*
* Contractor Name: Raytheon Company
* Contractor Address: 6825 Pine Street, Suite 340
* Mail Stop B8
* Omaha, NE 68106
* 402.291.0100
*
* See the AWIPS II Master Rights File ("Master Rights File.pdf") for
* further licensing information.
**/
package com.raytheon.uf.common.jms;
import java.util.ArrayList;
import java.util.List;
import javax.jms.Connection;
import javax.jms.ConnectionMetaData;
import javax.jms.ExceptionListener;
import javax.jms.JMSException;
import javax.jms.Session;
import com.raytheon.uf.common.jms.wrapper.JmsConnectionWrapper;
import com.raytheon.uf.common.jms.wrapper.JmsSessionWrapper;
import com.raytheon.uf.common.status.IUFStatusHandler;
import com.raytheon.uf.common.status.UFStatus;
import com.raytheon.uf.common.status.UFStatus.Priority;
/**
* Jms Pooled Connection. Tracks references to the connection to know when the
* connection can be released to the pool. Any exception will close pooled
* session instead of returning to the pool. The sessions are tracked in both
* active and available states. An available session can be reused by the next
* client.
*
* Synchronization Principle To prevent deadlocks: Chained sync blocks can only
* happen in a downward direction. A manager has a synchronized lock can make a
* call down to a wrapper, but not nice versa. Also a session inside a sync
* block can make a call down to a producer but not vice versa.
*
* <pre>
*
* SOFTWARE HISTORY
*
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Apr 15, 2011 rjpeter Initial creation
* Mar 08, 2012 194 njensen Improved safety of close()
* Feb 21, 2013 1642 rjpeter Fix deadlock scenario
* </pre>
*
* @author rjpeter
* @version 1.0
*/
public class JmsPooledConnection implements ExceptionListener {
private static final long ERROR_BROADCAST_INTERVAL = 30000;
private final IUFStatusHandler statusHandler = UFStatus
.getHandler(JmsPooledConnection.class);
private final JmsPooledConnectionFactory connFactory;
private volatile Connection conn = null;
// keeps track of number of creates vs. closes to know when it can be
// returned to the pool
private final List<JmsConnectionWrapper> references = new ArrayList<JmsConnectionWrapper>(
1);
private final Object stateLock = new Object();
private volatile State state = State.InUse;
// technically can have multiple sessions to one connection and sessions can
// differ by transaction mode and acknowledgement mode, currently not
// supported
private volatile JmsPooledSession session = null;
private volatile AvailableJmsPooledObject<JmsPooledSession> availableSession = null;
private volatile String key = null;
private final String clientId;
private volatile long connectionStartTime = 0;
private volatile boolean exceptionOccurred = false;
public JmsPooledConnection(JmsPooledConnectionFactory connFactory) {
this.connFactory = connFactory;
this.clientId = null;
getConnection();
}
public JmsSessionWrapper getSession(boolean transacted, int acknowledgeMode)
throws JMSException {
// pooled objects are always valid, the underlying object may not be
if (!isValid()) {
// throw exception
throw new IllegalStateException("Connection is closed");
}
synchronized (stateLock) {
// TODO: Add multiple session support
if (session != null) {
JmsSessionWrapper ref = session.createReference();
if (ref != null) {
return ref;
} else {
this.session.close();
this.session = null;
}
}
if (availableSession != null) {
JmsPooledSession availSess = availableSession.getPooledObject();
synchronized (availSess.getStateLock()) {
if (availSess.isValid()) {
availSess.setState(State.InUse);
session = availSess;
} else {
availSess.close();
}
availableSession = null;
}
}
if (session == null) {
session = new JmsPooledSession(this, conn.createSession(
transacted, acknowledgeMode));
}
}
return session.createReference();
}
/*
* (non-Javadoc)
*
* @see javax.jms.Connection#getClientID()
*/
public String getClientID() throws JMSException {
return clientId;
}
/*
* (non-Javadoc)
*
* @see javax.jms.Connection#getMetaData()
*/
public ConnectionMetaData getMetaData() throws JMSException {
return getConnection().getMetaData();
}
/*
* (non-Javadoc)
*
* @see javax.jms.ExceptionListener#onException(javax.jms.JMSException)
*/
@Override
public void onException(JMSException jmsExc) {
// need to worry about multiple exceptions causing repeated
// disconnect/reconnect
statusHandler.handle(Priority.WARN, "Caught Exception on "
+ connFactory.getProvider()
+ " connection. Closing connection", jmsExc);
close();
}
public void close() {
boolean canClose = false;
synchronized (stateLock) {
if (!State.Closed.equals(state)) {
canClose = true;
state = State.Closed;
}
}
if (canClose) {
// njensen: I moved removing the connection from the pool to be
// the first thing in this block instead of last thing so
// there's no chance it could be closed and then retrieved from
// the pool by something else
connFactory.removeConnectionFromPool(this);
if (conn != null) {
try {
conn.stop();
} catch (Exception e) {
statusHandler.handle(Priority.WARN,
"Failed to stop connection", e);
}
}
synchronized (references) {
for (JmsConnectionWrapper wrapper : references) {
wrapper.closeWrapper();
}
references.clear();
}
if (session != null) {
session.close();
session = null;
}
if (availableSession != null) {
availableSession.getPooledObject().close();
availableSession = null;
}
try {
conn.close();
} catch (Exception e) {
statusHandler.handle(Priority.WARN,
"Failed to close connection " + conn, e);
}
}
conn = null;
}
/**
* Closing all resources that have been idle in the pool for more than
* resourceRetention millis.
*
* @param resourceRetention
* @return
*/
public int closeOldPooledResources(int resourceRetention) {
if (!isValid()) {
return 0;
}
int count = 0;
JmsPooledSession sessionToCheck = null;
synchronized (stateLock) {
if (session != null) {
sessionToCheck = session;
}
}
if (sessionToCheck != null) {
count += sessionToCheck.closeOldPooledResources(resourceRetention);
}
synchronized (stateLock) {
if (availableSession != null) {
// njensen: I added the synchronized line below so we're
// synchronized on availableSession.stateLock
synchronized (availableSession.getPooledObject().getStateLock()) {
if (availableSession.expired(System.currentTimeMillis(),
resourceRetention)) {
availableSession.getPooledObject().close();
count++;
availableSession = null;
} else {
sessionToCheck = availableSession.getPooledObject();
}
}
}
}
if (sessionToCheck != null) {
count += sessionToCheck.closeOldPooledResources(resourceRetention);
}
return count;
}
public Connection getConnection() {
// lazy initialized so that we can handle reconnect and logging since
// spring reconnect doesn't give any status information and appears to
// the user that the process is hung
if (conn == null) {
// safe since conn is volatile
synchronized (stateLock) {
if (conn == null) {
long exceptionLastHandled = 0;
boolean connected = false;
while (!connected) {
Connection tmp = null;
try {
tmp = connFactory.createInternalConnection();
if (clientId != null) {
tmp.setClientID(clientId);
}
tmp.setExceptionListener(this);
tmp.start();
conn = tmp;
connectionStartTime = System.currentTimeMillis();
connected = true;
} catch (Exception e) {
if (exceptionLastHandled + ERROR_BROADCAST_INTERVAL < System
.currentTimeMillis()) {
exceptionLastHandled = System
.currentTimeMillis();
statusHandler.handle(
Priority.ERROR,
"Unable to connect to "
+ connFactory.getProvider(), e);
}
if (tmp != null) {
try {
tmp.close();
} catch (Exception e2) {
statusHandler
.handle(Priority.INFO,
"Failed to close failed connection to "
+ connFactory
.getProvider(),
e2);
}
}
if (connFactory.getReconnectInterval() > 0) {
try {
Thread.sleep(connFactory
.getReconnectInterval());
} catch (InterruptedException e2) {
// ignore
}
}
}
}
}
}
}
return conn;
}
public long getConnectionStartTime() {
return connectionStartTime;
}
public boolean returnSessionToPool(JmsPooledSession sess) {
boolean valid = false;
if (sess.isValid()) {
try {
// ensure transaction is complete
Session underlyingSession = sess.getSession();
if (underlyingSession.getTransacted()) {
underlyingSession.commit();
}
valid = true;
} catch (JMSException e) {
statusHandler.handle(Priority.INFO,
"Failed to commit session on returing it to pool", e);
}
}
if (!valid) {
removeSession(sess);
} else {
synchronized (stateLock) {
// should only be able to have 1 session
if (availableSession != null) {
statusHandler
.warn("Pooled session already existed for this connection, closing previous session");
availableSession.getPooledObject().close();
}
availableSession = new AvailableJmsPooledObject<JmsPooledSession>(
session);
session = null;
}
}
return valid;
}
/**
* Removes this pooled session from the pooled connection. Does NOT close
* the session, this should be handled independently.
*
* @param sess
*/
public void removeSession(JmsPooledSession sess) {
synchronized (stateLock) {
if (sess != null) {
if (this.session == sess) {
this.session = null;
} else if ((availableSession != null)
&& (availableSession.getPooledObject() == sess)) {
this.availableSession = null;
}
}
}
}
public JmsConnectionWrapper createReference() {
synchronized (stateLock) {
if (isValid(State.InUse, true)) {
JmsConnectionWrapper wrapper = new JmsConnectionWrapper(this);
synchronized (references) {
references.add(wrapper);
}
return wrapper;
}
}
return null;
}
public boolean isValid() {
return isValid(State.Closed, false);
}
/**
* Verifies if an exception has occurred, the state is the desired state,
* and the underlying resource is still valid.
*
* @param requiredState
* @param mustBeRequiredState
* If true, current state must match requiredState for isValid to
* be true. If false, current state must not be the
* requiredState.
* @return
*/
public boolean isValid(State requiredState, boolean mustBeRequiredState) {
boolean valid = false;
if (!exceptionOccurred) {
valid = state.equals(requiredState);
if (!mustBeRequiredState) {
valid = !valid;
}
if (valid) {
// check underlying resource
try {
if (conn != null) {
conn.getClientID();
}
} catch (JMSException e) {
// underlying connection has been closed
valid = false;
}
}
}
return valid;
}
public int getReferenceCount() {
synchronized (references) {
return references.size();
}
}
public void removeReference(JmsConnectionWrapper wrapper) {
boolean returnToPool = false;
synchronized (stateLock) {
synchronized (references) {
if (references.remove(wrapper) && references.isEmpty()
&& State.InUse.equals(state)) {
state = State.Available;
returnToPool = true;
// double check state of session, should be available
if (session != null) {
statusHandler
.warn("Connection marked available, but Session not Available. Sessions state is: "
+ session.getState());
session.close();
session = null;
}
if (availableSession != null) {
JmsPooledSession availSess = availableSession
.getPooledObject();
synchronized (availSess.getStateLock()) {
if ((availSess != null)
&& !State.Available.equals(availSess
.getState())) {
statusHandler
.warn("Connection marked available, but Session not Available. Sessions state is: "
+ availSess.getState());
availSess.close();
availableSession = null;
}
}
}
}
}
}
boolean valid = isValid();
if (valid && returnToPool) {
valid = connFactory.returnConnectionToPool(this);
}
if (!valid) {
close();
}
}
public void setKey(String key) {
this.key = key;
}
public String getKey() {
return key;
}
/**
* @return the exceptionOccurred
*/
public boolean isExceptionOccurred() {
return exceptionOccurred;
}
/**
* @param exceptionOccurred
* the exceptionOccurred to set
*/
public void setExceptionOccurred(boolean exceptionOccurred) {
this.exceptionOccurred = exceptionOccurred;
}
/**
* @return the state
*/
public State getState() {
return state;
}
/**
* @param state
* the state to set
*/
public void setState(State state) {
this.state = state;
}
/**
* @return the stateLock
*/
public Object getStateLock() {
return stateLock;
}
}