INT-3433 NIO Thread Starvation with Fixed Pool
JIRA: https://jira.spring.io/browse/INT-3433 INT-3433 added the test-case Conflicts: spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioConnection.java
This commit is contained in:
@@ -26,11 +26,11 @@ import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.Selector;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -189,67 +189,97 @@ public class TcpNioConnection extends TcpConnectionSupport {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(this.getConnectionId() + " Nio message assembler running...");
|
||||
}
|
||||
try {
|
||||
if (this.getListener() == null && !this.isSingleUse()) {
|
||||
logger.debug("TcpListener exiting - no listener and not single use");
|
||||
return;
|
||||
}
|
||||
boolean moreDataAvailable = true;
|
||||
while(moreDataAvailable) {
|
||||
try {
|
||||
if (dataAvailable()) {
|
||||
Message<?> message = convert();
|
||||
if (this.getListener() == null && !this.isSingleUse()) {
|
||||
logger.debug("TcpListener exiting - no listener and not single use");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (dataAvailable()) {
|
||||
// there is more data in the pipe; run another assembler
|
||||
// to assemble the next message, while we send ours
|
||||
this.executionControl.incrementAndGet();
|
||||
this.taskExecutor.execute(this);
|
||||
Message<?> message = convert();
|
||||
if (dataAvailable()) {
|
||||
// there is more data in the pipe; run another assembler
|
||||
// to assemble the next message, while we send ours
|
||||
this.executionControl.incrementAndGet();
|
||||
try {
|
||||
this.taskExecutor.execute2(this);
|
||||
}
|
||||
catch (RejectedExecutionException e) {
|
||||
this.executionControl.decrementAndGet();
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Insufficient threads in the assembler fixed thread pool; consider " +
|
||||
"increasing this task executor pool size");
|
||||
}
|
||||
}
|
||||
}
|
||||
this.executionControl.decrementAndGet();
|
||||
if (message != null) {
|
||||
sendToChannel(message);
|
||||
}
|
||||
}
|
||||
this.executionControl.decrementAndGet();
|
||||
if (message != null) {
|
||||
sendToChannel(message);
|
||||
else {
|
||||
this.executionControl.decrementAndGet();
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.executionControl.decrementAndGet();
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.error("Read exception " +
|
||||
this.getConnectionId(), e);
|
||||
}
|
||||
else if (!this.isNoReadErrorOnClose()) {
|
||||
logger.error("Read exception " +
|
||||
this.getConnectionId() + " " +
|
||||
e.getClass().getSimpleName() +
|
||||
":" + e.getCause() + ":" + e.getMessage());
|
||||
}
|
||||
else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Read exception " +
|
||||
catch (Exception e) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.error("Read exception " +
|
||||
this.getConnectionId(), e);
|
||||
}
|
||||
else if (!this.isNoReadErrorOnClose()) {
|
||||
logger.error("Read exception " +
|
||||
this.getConnectionId() + " " +
|
||||
e.getClass().getSimpleName() +
|
||||
":" + e.getCause() + ":" + e.getMessage());
|
||||
}
|
||||
}
|
||||
this.closeConnection(true);
|
||||
this.sendExceptionToListener(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(this.getConnectionId() + " Nio message assembler exiting...");
|
||||
}
|
||||
// Final check in case new data came in and the
|
||||
// timing was such that we were the last assembler and
|
||||
// a new one wasn't run
|
||||
try {
|
||||
if (this.isOpen() && dataAvailable()) {
|
||||
checkForAssembler();
|
||||
else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Read exception " +
|
||||
this.getConnectionId() + " " +
|
||||
e.getClass().getSimpleName() +
|
||||
":" + e.getCause() + ":" + e.getMessage());
|
||||
}
|
||||
}
|
||||
this.closeConnection(true);
|
||||
this.sendExceptionToListener(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
logger.error("Exception when checking for assembler", e);
|
||||
finally {
|
||||
moreDataAvailable = false;
|
||||
// Final check in case new data came in and the
|
||||
// timing was such that we were the last assembler and
|
||||
// a new one wasn't run
|
||||
try {
|
||||
if (this.isOpen() && dataAvailable()) {
|
||||
synchronized(this.executionControl) {
|
||||
if (this.executionControl.incrementAndGet() <= 1) {
|
||||
// only continue if we don't already have another assembler running
|
||||
this.executionControl.set(1);
|
||||
moreDataAvailable = true;
|
||||
|
||||
} else {
|
||||
this.executionControl.decrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (moreDataAvailable) {
|
||||
Thread.yield();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(this.getConnectionId() + " Nio message assembler continuing...");
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(this.getConnectionId() + " Nio message assembler exiting...");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
logger.error("Exception when checking for assembler", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,47 +363,25 @@ public class TcpNioConnection extends TcpConnectionSupport {
|
||||
this.taskExecutor = new CompositeExecutor(executor, executor);
|
||||
}
|
||||
// If there is no assembler running, start one
|
||||
checkForAssembler();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Before read:" + this.rawBuffer.position() + "/" + this.rawBuffer.limit());
|
||||
}
|
||||
int len = this.socketChannel.read(this.rawBuffer);
|
||||
if (len < 0) {
|
||||
this.writingToPipe = false;
|
||||
this.closeConnection(true);
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("After read:" + this.rawBuffer.position() + "/" + this.rawBuffer.limit());
|
||||
}
|
||||
this.rawBuffer.flip();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("After flip:" + this.rawBuffer.position() + "/" + this.rawBuffer.limit());
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Read " + rawBuffer.limit() + " into raw buffer");
|
||||
}
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
/*
|
||||
* If there are insufficient threads, either to run the
|
||||
* write to the pipe, or to assemble the data, we need
|
||||
* avoid a deadlock (block on the write to the pipe).
|
||||
* Hence the count down latch.
|
||||
*/
|
||||
this.taskExecutor.execute2(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
TcpNioConnection.this.sendToPipe(rawBuffer);
|
||||
latch.countDown();
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.error(getConnectionId() + " Failed to write to pipe", e);
|
||||
}
|
||||
if (checkForAssembler()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Before read:" + this.rawBuffer.position() + "/" + this.rawBuffer.limit()); }
|
||||
int len = this.socketChannel.read(this.rawBuffer);
|
||||
if (len < 0) {
|
||||
this.writingToPipe = false;
|
||||
this.closeConnection(true);
|
||||
}
|
||||
});
|
||||
if (!latch.await(this.pipeTimeout , TimeUnit.MILLISECONDS)) {
|
||||
this.close();
|
||||
throw new MessagingException("Timed out writing to ChannelInputStream, probably due to insufficient threads in " +
|
||||
"a fixed thread pool; consider increasing this task executor pool size");
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("After read:" + this.rawBuffer.position() + "/" + this.rawBuffer.limit());
|
||||
}
|
||||
this.rawBuffer.flip();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("After flip:" + this.rawBuffer.position() + "/" + this.rawBuffer.limit());
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Read " + rawBuffer.limit() + " into raw buffer");
|
||||
}
|
||||
this.sendToPipe(rawBuffer);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
@@ -394,7 +402,7 @@ public class TcpNioConnection extends TcpConnectionSupport {
|
||||
rawBuffer.clear();
|
||||
}
|
||||
|
||||
private void checkForAssembler() {
|
||||
private boolean checkForAssembler() {
|
||||
synchronized(this.executionControl) {
|
||||
if (this.executionControl.incrementAndGet() <= 1) {
|
||||
// only execute run() if we don't already have one running
|
||||
@@ -402,11 +410,22 @@ public class TcpNioConnection extends TcpConnectionSupport {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(this.getConnectionId() + " Running an assembler");
|
||||
}
|
||||
this.taskExecutor.execute2(this);
|
||||
try {
|
||||
this.taskExecutor.execute2(this);
|
||||
}
|
||||
catch (RejectedExecutionException e) {
|
||||
this.executionControl.decrementAndGet();
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Insufficient threads in the assembler fixed thread pool; consider increasing " +
|
||||
"this task executor pool size");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
this.executionControl.decrementAndGet();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -45,6 +45,7 @@ import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ThreadPoolExecutor.AbortPolicy;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@@ -329,9 +330,7 @@ public class TcpNioConnectionTests {
|
||||
fail("Expected exception, got " + o);
|
||||
}
|
||||
catch (ExecutionException e) {
|
||||
assertEquals("Timed out writing to ChannelInputStream, probably due to insufficient threads in " +
|
||||
"a fixed thread pool; consider increasing this task executor pool size", e.getCause()
|
||||
.getMessage());
|
||||
assertEquals("Timed out waiting for buffer space", e.getCause().getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,18 +600,74 @@ public class TcpNioConnectionTests {
|
||||
factory.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllMessagesDelivered() throws Exception {
|
||||
final int numberOfSockets = 25;
|
||||
final int port = SocketUtils.findAvailableServerSocket();
|
||||
TcpNioServerConnectionFactory factory = new TcpNioServerConnectionFactory(port);
|
||||
factory.setApplicationEventPublisher(mock(ApplicationEventPublisher.class));
|
||||
|
||||
CompositeExecutor compositeExec = compositeExecutor();
|
||||
|
||||
factory.setTaskExecutor(compositeExec);
|
||||
final CountDownLatch latch = new CountDownLatch(numberOfSockets);
|
||||
factory.registerListener(new TcpListener() {
|
||||
|
||||
@Override
|
||||
public boolean onMessage(Message<?> message) {
|
||||
if (!(message instanceof ErrorMessage)) {
|
||||
latch.countDown();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
factory.start();
|
||||
|
||||
Socket[] sockets = new Socket[numberOfSockets];
|
||||
for (int i = 0; i < numberOfSockets; i++) {
|
||||
Socket socket = null;
|
||||
int n = 0;
|
||||
while (n++ < 100) {
|
||||
try {
|
||||
socket = SocketFactory.getDefault().createSocket("localhost", port);
|
||||
break;
|
||||
}
|
||||
catch (ConnectException e) {}
|
||||
Thread.sleep(100);
|
||||
}
|
||||
assertTrue("Could not open socket to localhost:" + port, n < 100);
|
||||
sockets[i] = socket;
|
||||
}
|
||||
for (int i = 0; i < numberOfSockets; i++) {
|
||||
sockets[i].getOutputStream().write("foo1 and...".getBytes());
|
||||
sockets[i].getOutputStream().flush();
|
||||
}
|
||||
Thread.sleep(100);
|
||||
for (int i = 0; i < numberOfSockets; i++) {
|
||||
sockets[i].getOutputStream().write(("...foo2\r\n").getBytes());
|
||||
sockets[i].close();
|
||||
}
|
||||
|
||||
assertTrue(latch.await(10, TimeUnit.SECONDS));
|
||||
|
||||
factory.stop();
|
||||
}
|
||||
|
||||
private CompositeExecutor compositeExecutor() {
|
||||
ThreadPoolTaskExecutor ioExec = new ThreadPoolTaskExecutor();
|
||||
ioExec.setCorePoolSize(2);
|
||||
ioExec.setQueueCapacity(10);
|
||||
ioExec.setMaxPoolSize(4);
|
||||
ioExec.setQueueCapacity(0);
|
||||
ioExec.setThreadNamePrefix("io-");
|
||||
ioExec.setRejectedExecutionHandler(new CallerBlocksPolicy(10000));
|
||||
ioExec.setRejectedExecutionHandler(new CallerBlocksPolicy(5000));
|
||||
ioExec.initialize();
|
||||
ThreadPoolTaskExecutor assemblerExec = new ThreadPoolTaskExecutor();
|
||||
assemblerExec.setCorePoolSize(2);
|
||||
assemblerExec.setQueueCapacity(10);
|
||||
assemblerExec.setMaxPoolSize(5);
|
||||
assemblerExec.setQueueCapacity(0);
|
||||
assemblerExec.setThreadNamePrefix("assembler-");
|
||||
assemblerExec.setRejectedExecutionHandler(new CallerBlocksPolicy(10000));
|
||||
assemblerExec.setRejectedExecutionHandler(new AbortPolicy());
|
||||
assemblerExec.initialize();
|
||||
return new CompositeExecutor(ioExec, assemblerExec);
|
||||
}
|
||||
|
||||
@@ -1027,7 +1027,7 @@
|
||||
<classname>CompositeExecutor</classname> allows the configuration
|
||||
of two distinct executors; one for performing IO operations, and
|
||||
one for message assembly. The <classname>CallerBlocksPolicy</classname>
|
||||
(which should be configured for both task executors) will suspend
|
||||
(which should be configured for the first task executors) will suspend
|
||||
the IO operation until an assembler thread is available (or a timeout
|
||||
occurs). In this environment, an IO thread can never
|
||||
become an assembler thread, and the deadlock cannot occur.
|
||||
@@ -1041,16 +1041,16 @@ private CompositeExecutor compositeExecutor() {
|
||||
ThreadPoolTaskExecutor ioExec = new ThreadPoolTaskExecutor();
|
||||
ioExec.setCorePoolSize(4);
|
||||
ioExec.setMaxPoolSize(8);
|
||||
ioExec.setQueueCapacity(10);
|
||||
ioExec.setQueueCapacity(0);
|
||||
ioExec.setThreadNamePrefix("io-");
|
||||
ioExec.setRejectedExecutionHandler(new CallerRunsPolicy());
|
||||
ioExec.initialize();
|
||||
ThreadPoolTaskExecutor assemblerExec = new ThreadPoolTaskExecutor();
|
||||
assemblerExec.setCorePoolSize(2);
|
||||
assemblerExec.setCorePoolSize(4);
|
||||
assemblerExec.setMaxPoolSize(10);
|
||||
assemblerExec.setQueueCapacity(12);
|
||||
assemblerExec.setQueueCapacity(0);
|
||||
assemblerExec.setThreadNamePrefix("assembler-");
|
||||
assemblerExec.setRejectedExecutionHandler(new CallerBlocksPolicy(10000));
|
||||
assemblerExec.setRejectedExecutionHandler(new AbortPolicy());
|
||||
assemblerExec.initialize();
|
||||
return new CompositeExecutor(ioExec, assemblerExec);
|
||||
}]]></programlisting>
|
||||
@@ -1060,7 +1060,7 @@ private CompositeExecutor compositeExecutor() {
|
||||
<property name="threadNamePrefix" value="io-" />
|
||||
<property name="corePoolSize" value="4" />
|
||||
<property name="maxPoolSize" value="8" />
|
||||
<property name="queueCapacity" value="10" />
|
||||
<property name="queueCapacity" value="0" />
|
||||
<property name="rejectedExecutionHandler">
|
||||
<bean class="org.springframework.integration.util.CallerBlocksPolicy">
|
||||
<constructor-arg value="10000" />
|
||||
@@ -1073,11 +1073,9 @@ private CompositeExecutor compositeExecutor() {
|
||||
<property name="threadNamePrefix" value="assembler-" />
|
||||
<property name="corePoolSize" value="4" />
|
||||
<property name="maxPoolSize" value="10" />
|
||||
<property name="queueCapacity" value="10" />
|
||||
<property name="queueCapacity" value="0" />
|
||||
<property name="rejectedExecutionHandler">
|
||||
<bean class="org.springframework.integration.util.CallerBlocksPolicy">
|
||||
<constructor-arg value="10000" />
|
||||
</bean>
|
||||
<bean class="java.util.concurrent.ThreadPoolExecutor.AbortPolicy" />
|
||||
</property>
|
||||
</bean>
|
||||
</constructor-arg>
|
||||
|
||||
Reference in New Issue
Block a user