diff --git a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioConnection.java b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioConnection.java index 8c40fb0aff..fbc396d56b 100644 --- a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioConnection.java +++ b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioConnection.java @@ -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; } /** diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/TcpNioConnectionTests.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/TcpNioConnectionTests.java index fc444866dc..36a91fb73c 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/TcpNioConnectionTests.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/TcpNioConnectionTests.java @@ -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); } diff --git a/src/reference/docbook/ip.xml b/src/reference/docbook/ip.xml index 7302ba6323..dd1c944d74 100644 --- a/src/reference/docbook/ip.xml +++ b/src/reference/docbook/ip.xml @@ -1027,7 +1027,7 @@ CompositeExecutor allows the configuration of two distinct executors; one for performing IO operations, and one for message assembly. The CallerBlocksPolicy - (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); }]]> @@ -1060,7 +1060,7 @@ private CompositeExecutor compositeExecutor() { - + @@ -1073,11 +1073,9 @@ private CompositeExecutor compositeExecutor() { - + - - - +