spring提供了一个简化JMS API的框架,利用spring可以不用关心connection和session的管理,并且提供了JmsTemplate
模板使开发者可以专注于业务上的收发消息
为开发者提供了便利。在使用spring框架集成activeMQ5.12版本时遇到一个问题,activeMQ5.12独立部署于10.137.100.54机器上本地开发
使用配置
<bean id="jmsFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop">
<property name="connectionFactory">
<bean class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL">
<value>failover:(tcp://10.137.100.54:61616)?timeout=500</value>
</property>
<property name="sendTimeout" value="1000"/>
<property name="clientID" value="useTimeout2000"/>
</bean>
</property>
</bean>
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="jmsFactory"/>
<property name="defaultDestination" ref="topicDestination"/>
</bean>
如果54机器上的MQ Broker正常运行Spring能够稳定的管理JMS并且 平稳的收发消息,如果这时停掉Broker的服务
发送消息的线程将会发生阻塞,虽然设置了timeout超时但是丝毫不起作用。
如果发生这种情况将会对整个应用造成性能影响甚至宕机。
在这个问题的分析过程中尝试把activeMQ的客户端版本降为5.11.1结果还是会发生阻塞,最后版本降为5.10.0竟然可以正常抛出异常了。
5.10.0和5.12.0在版本升级的过程中究竟发生了什么变更导致前后出现如此的差异,下面是两个版本的代码
5.10版本代码
org.apache.activemq.transport.failover.FailoverTransport类里面有个方法
@Override
public void oneway(Object o) throws IOException {
Command command = (Command) o;
Exception error = null;
try {
synchronized (reconnectMutex) {
if (command != null && connectedTransport.get() == null) {
if (command.isShutdownInfo()) {
// Skipping send of ShutdownInfo command when not connected.
return;
} else if (command instanceof RemoveInfo || command.isMessageAck()) {
// Simulate response to RemoveInfo command or MessageAck (as it will be stale)
stateTracker.track(command);
if (command.isResponseRequired()) {
Response response = new Response();
response.setCorrelationId(command.getCommandId());
myTransportListener.onCommand(response);
}
return;
} else if (command instanceof MessagePull) {
// Simulate response to MessagePull if timed as we can't honor that now.
MessagePull pullRequest = (MessagePull) command;
if (pullRequest.getTimeout() != 0) {
MessageDispatch dispatch = new MessageDispatch();
dispatch.setConsumerId(pullRequest.getConsumerId());
dispatch.setDestination(pullRequest.getDestination());
myTransportListener.onCommand(dispatch);
}
return;
}
}
// Keep trying until the message is sent.
for (int i = 0; !disposed; i++) {
try {
// Wait for transport to be connected.
Transport transport = connectedTransport.get();
long start = System.currentTimeMillis();
boolean timedout = false;
while (transport == null && !disposed && connectionFailure == null
&& !Thread.currentThread().isInterrupted()) {
if (LOG.isTraceEnabled()) {
LOG.trace("Waiting for transport to reconnect..: " + command);
}
long end = System.currentTimeMillis();
if (timeout > 0 && (end - start > timeout)) {
timedout = true;
//5.10.0版本只要超时就退出
if (LOG.isInfoEnabled()) {
LOG.info("Failover timed out after " + (end - start) + "ms");
}
break;
}
try {
reconnectMutex.wait(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (LOG.isDebugEnabled()) {
LOG.debug("Interupted: " + e, e);
}
}
transport = connectedTransport.get();
}
if (transport == null) {
// Previous loop may have exited due to use being
// disposed.
if (disposed) {
error = new IOException("Transport disposed.");
} else if (connectionFailure != null) {
error = connectionFailure;
} else if (timedout == true) {
error = new IOException("Failover timeout of " + timeout + " ms reached.");
} else {
error = new IOException("Unexpected failure.");
}
break;
}
Tracked tracked = null;
try {
tracked = stateTracker.track(command);
} catch (IOException ioe) {
LOG.debug("Cannot track the command " + command, ioe);
}
// If it was a request and it was not being tracked by
// the state tracker,
// then hold it in the requestMap so that we can replay
// it later.
synchronized (requestMap) {
if (tracked != null && tracked.isWaitingForResponse()) {
requestMap.put(Integer.valueOf(command.getCommandId()), tracked);
} else if (tracked == null && command.isResponseRequired()) {
requestMap.put(Integer.valueOf(command.getCommandId()), command);
}
}
// Send the message.
try {
transport.oneway(command);
stateTracker.trackBack(command);
} catch (IOException e) {
// If the command was not tracked.. we will retry in
// this method
if (tracked == null) {
// since we will retry in this method.. take it
// out of the request
// map so that it is not sent 2 times on
// recovery
if (command.isResponseRequired()) {
requestMap.remove(Integer.valueOf(command.getCommandId()));
}
// Rethrow the exception so it will handled by
// the outer catch
throw e;
} else {
// Handle the error but allow the method to return since the
// tracked commands are replayed on reconnect.
if (LOG.isDebugEnabled()) {
LOG.debug("Send oneway attempt: " + i + " failed for command:" + command);
}
handleTransportFailure(e);
}
}
return;
} catch (IOException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Send oneway attempt: " + i + " failed for command:" + command);
}
handleTransportFailure(e);
}
}
}
} catch (InterruptedException e) {
// Some one may be trying to stop our thread.
Thread.currentThread().interrupt();
throw new InterruptedIOException();
}
if (!disposed) {
if (error != null) {
if (error instanceof IOException) {
throw (IOException) error;
}
throw IOExceptionSupport.create(error);
}
}
}
5.12版本代码
@Override
public void oneway(Object o) throws IOException {
Command command = (Command) o;
Exception error = null;
try {
synchronized (reconnectMutex) {
if (command != null && connectedTransport.get() == null) {
if (command.isShutdownInfo()) {
// Skipping send of ShutdownInfo command when not connected.
return;
} else if (command instanceof RemoveInfo || command.isMessageAck()) {
// Simulate response to RemoveInfo command or MessageAck (as it will be stale)
stateTracker.track(command);
if (command.isResponseRequired()) {
Response response = new Response();
response.setCorrelationId(command.getCommandId());
myTransportListener.onCommand(response);
}
return;
} else if (command instanceof MessagePull) {
// Simulate response to MessagePull if timed as we can't honor that now.
MessagePull pullRequest = (MessagePull) command;
if (pullRequest.getTimeout() != 0) {
MessageDispatch dispatch = new MessageDispatch();
dispatch.setConsumerId(pullRequest.getConsumerId());
dispatch.setDestination(pullRequest.getDestination());
myTransportListener.onCommand(dispatch);
}
return;
}
}
// Keep trying until the message is sent.
for (int i = 0; !disposed; i++) {
try {
// Wait for transport to be connected.
Transport transport = connectedTransport.get();
long start = System.currentTimeMillis();
boolean timedout = false;
while (transport == null && !disposed && connectionFailure == null
&& !Thread.currentThread().isInterrupted()) {
if (LOG.isTraceEnabled()) {
LOG.trace("Waiting for transport to reconnect..: " + command);
}
long end = System.currentTimeMillis();
if (command.isMessage() && timeout > 0 && (end - start > timeout)) {
//只有发送消息时的超时才会退出,导致最终抛出timeout的异常
timedout = true;
if (LOG.isInfoEnabled()) {
LOG.info("Failover timed out after " + (end - start) + "ms");
}
break;
}
try {
reconnectMutex.wait(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (LOG.isDebugEnabled()) {
LOG.debug("Interupted: " + e, e);
}
}
transport = connectedTransport.get();
}
if (transport == null) {
// Previous loop may have exited due to use being
// disposed.
if (disposed) {
error = new IOException("Transport disposed.");
} else if (connectionFailure != null) {
error = connectionFailure;
} else if (timedout == true) {
error = new IOException("Failover timeout of " + timeout + " ms reached.");
} else {
error = new IOException("Unexpected failure.");
}
break;
}
Tracked tracked = null;
try {
tracked = stateTracker.track(command);
} catch (IOException ioe) {
LOG.debug("Cannot track the command " + command, ioe);
}
// If it was a request and it was not being tracked by
// the state tracker,
// then hold it in the requestMap so that we can replay
// it later.
synchronized (requestMap) {
if (tracked != null && tracked.isWaitingForResponse()) {
requestMap.put(Integer.valueOf(command.getCommandId()), tracked);
} else if (tracked == null && command.isResponseRequired()) {
requestMap.put(Integer.valueOf(command.getCommandId()), command);
}
}
// Send the message.
try {
transport.oneway(command);
stateTracker.trackBack(command);
if (command.isShutdownInfo()) {
shuttingDown = true;
}
} catch (IOException e) {
// If the command was not tracked.. we will retry in
// this method
if (tracked == null) {
// since we will retry in this method.. take it
// out of the request
// map so that it is not sent 2 times on
// recovery
if (command.isResponseRequired()) {
requestMap.remove(Integer.valueOf(command.getCommandId()));
}
// Rethrow the exception so it will handled by
// the outer catch
throw e;
} else {
// Handle the error but allow the method to return since the
// tracked commands are replayed on reconnect.
if (LOG.isDebugEnabled()) {
LOG.debug("Send oneway attempt: " + i + " failed for command:" + command);
}
handleTransportFailure(e);
}
}
return;
} catch (IOException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Send oneway attempt: " + i + " failed for command:" + command);
}
handleTransportFailure(e);
}
}
}
} catch (InterruptedException e) {
// Some one may be trying to stop our thread.
Thread.currentThread().interrupt();
throw new InterruptedIOException();
}
if (!disposed) {
if (error != null) {
if (error instanceof IOException) {
throw (IOException) error;
}
throw IOExceptionSupport.create(error);
}
}
}
可以看出一个显著的区别5.12.0版本多了一个command.isMessage()的判断,也就是说只有是消息类型的操作时才会发生timeout的异常
当创建连接,建立session的过程中5.12.0版本不会抛出异常,而实际上创建connection和session都会执行oneway(Object o)的操作。
再来看看spring JmsTemplate发送消息的代码
public <T> T execute(SessionCallback<T> action, boolean startConnection) throws JmsException {
Assert.notNull(action, "Callback object must not be null");
Connection conToClose = null;
Session sessionToClose = null;
try {
Session sessionToUse = ConnectionFactoryUtils.doGetTransactionalSession(
getConnectionFactory(), this.transactionalResourceFactory, startConnection);
if (sessionToUse == null) {
conToClose = createConnection();
sessionToClose = createSession(conToClose);
if (startConnection) {
conToClose.start();
}
sessionToUse = sessionToClose;
}
if (logger.isDebugEnabled()) {
logger.debug("Executing callback on JMS Session: " + sessionToUse);
}
return action.doInJms(sessionToUse);
}
catch (JMSException ex) {
throw convertJmsAccessException(ex);
}
finally {
JmsUtils.closeSession(sessionToClose);
ConnectionFactoryUtils.releaseConnection(conToClose, getConnectionFactory(), startConnection);
}
可以看出在发送消息时JmsTemplate会不断地获取获取connection和session这就造成了阻塞的问题。
这个问题应该是伴随着ActiveMQ从5.10.0向上升级的过程中产生的很可能是升级过程中产生的一个新BUG。