INT-3646: Add TCP Server Exception Events

JIRA: https://jira.spring.io/browse/INT-3646

Publish `TcpConnectionServerExceptionEvent`s when unexpected
exceptions occur on server sockets.

INT-3646: Polishing
Fix Reactor tests
This commit is contained in:
Gary Russell
2015-02-18 20:16:51 +02:00
committed by Artem Bilan
parent f248394682
commit f4e775ec18
9 changed files with 251 additions and 97 deletions

View File

@@ -18,6 +18,7 @@ package org.springframework.integration.gateway;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
@@ -84,7 +85,7 @@ public class AsyncGatewayTests {
Object result = f.get(1000, TimeUnit.MILLISECONDS); Object result = f.get(1000, TimeUnit.MILLISECONDS);
long elapsed = System.currentTimeMillis() - start; long elapsed = System.currentTimeMillis() - start;
assertTrue(elapsed >= 200); assertTrue(elapsed >= 200);
assertTrue(result instanceof Message<?>); assertNotNull(result);
assertEquals("foobar", ((Message<?>) result).getPayload()); assertEquals("foobar", ((Message<?>) result).getPayload());
} }
@@ -149,7 +150,6 @@ public class AsyncGatewayTests {
long elapsed = System.currentTimeMillis() - start; long elapsed = System.currentTimeMillis() - start;
assertTrue(elapsed >= 200); assertTrue(elapsed >= 200);
assertEquals("foobar", result.get().getPayload()); assertEquals("foobar", result.get().getPayload());
Object thread = result.get().getHeaders().get("thread"); Object thread = result.get().getHeaders().get("thread");
assertNotEquals(Thread.currentThread(), thread); assertNotEquals(Thread.currentThread(), thread);
} }
@@ -169,7 +169,6 @@ public class AsyncGatewayTests {
CustomFuture f = service.returnCustomFuture("foo"); CustomFuture f = service.returnCustomFuture("foo");
String result = f.get(1000, TimeUnit.MILLISECONDS); String result = f.get(1000, TimeUnit.MILLISECONDS);
assertEquals("foobar", result); assertEquals("foobar", result);
assertEquals(Thread.currentThread(), f.thread); assertEquals(Thread.currentThread(), f.thread);
} }
@@ -184,14 +183,13 @@ public class AsyncGatewayTests {
proxyFactory.setBeanName("testGateway"); proxyFactory.setBeanName("testGateway");
proxyFactory.setBeanFactory(mock(BeanFactory.class)); proxyFactory.setBeanFactory(mock(BeanFactory.class));
proxyFactory.setAsyncExecutor(null); // Not async - user flow returns Future<?> proxyFactory.setAsyncExecutor(null); // Not async - user flow returns Future<?>
proxyFactory.afterPropertiesSet(); proxyFactory.afterPropertiesSet();
TestEchoService service = (TestEchoService) proxyFactory.getObject(); TestEchoService service = (TestEchoService) proxyFactory.getObject();
CustomFuture f = (CustomFuture) service.returnCustomFutureWithTypeFuture("foo"); CustomFuture f = (CustomFuture) service.returnCustomFutureWithTypeFuture("foo");
String result = f.get(1000, TimeUnit.MILLISECONDS); String result = f.get(1000, TimeUnit.MILLISECONDS);
assertEquals("foobar", result); assertEquals("foobar", result);
assertEquals(Thread.currentThread(), f.thread); assertEquals(Thread.currentThread(), f.thread);
} }
@@ -204,6 +202,7 @@ public class AsyncGatewayTests {
.setHeader("thread", Thread.currentThread()) .setHeader("thread", Thread.currentThread())
.build(); .build();
} }
}); });
} }
@@ -222,9 +221,8 @@ public class AsyncGatewayTests {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
Object result = f.get(1000, TimeUnit.MILLISECONDS); Object result = f.get(1000, TimeUnit.MILLISECONDS);
long elapsed = System.currentTimeMillis() - start; long elapsed = System.currentTimeMillis() - start;
assertTrue(elapsed >= 200 - safety); assertTrue(elapsed >= 200 - safety);
assertTrue(result instanceof String); assertNotNull(result);
assertEquals("foobar", result); assertEquals("foobar", result);
} }
@@ -243,7 +241,6 @@ public class AsyncGatewayTests {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
Object result = f.get(1000, TimeUnit.MILLISECONDS); Object result = f.get(1000, TimeUnit.MILLISECONDS);
long elapsed = System.currentTimeMillis() - start; long elapsed = System.currentTimeMillis() - start;
assertTrue(elapsed >= 200 - safety); assertTrue(elapsed >= 200 - safety);
assertTrue(result instanceof String); assertTrue(result instanceof String);
assertEquals("foobar", result); assertEquals("foobar", result);
@@ -263,10 +260,7 @@ public class AsyncGatewayTests {
proxyFactory.afterPropertiesSet(); proxyFactory.afterPropertiesSet();
TestEchoService service = (TestEchoService) proxyFactory.getObject(); TestEchoService service = (TestEchoService) proxyFactory.getObject();
Promise<Message<?>> promise = service.returnMessagePromise("foo"); Promise<Message<?>> promise = service.returnMessagePromise("foo");
long start = System.currentTimeMillis();
Object result = promise.await(1, TimeUnit.SECONDS); Object result = promise.await(1, TimeUnit.SECONDS);
long elapsed = System.currentTimeMillis() - start;
assertTrue(elapsed <= 200);
assertEquals("foobar", ((Message<?>) result).getPayload()); assertEquals("foobar", ((Message<?>) result).getPayload());
} }
@@ -283,11 +277,7 @@ public class AsyncGatewayTests {
proxyFactory.afterPropertiesSet(); proxyFactory.afterPropertiesSet();
TestEchoService service = (TestEchoService) proxyFactory.getObject(); TestEchoService service = (TestEchoService) proxyFactory.getObject();
Promise<String> promise = service.returnStringPromise("foo"); Promise<String> promise = service.returnStringPromise("foo");
long start = System.currentTimeMillis();
Object result = promise.await(1, TimeUnit.SECONDS); Object result = promise.await(1, TimeUnit.SECONDS);
long elapsed = System.currentTimeMillis() - start;
assertTrue(elapsed <= 200 - safety);
assertEquals("foobar", result); assertEquals("foobar", result);
} }
@@ -304,12 +294,8 @@ public class AsyncGatewayTests {
proxyFactory.afterPropertiesSet(); proxyFactory.afterPropertiesSet();
TestEchoService service = (TestEchoService) proxyFactory.getObject(); TestEchoService service = (TestEchoService) proxyFactory.getObject();
Promise<?> promise = service.returnSomethingPromise("foo"); Promise<?> promise = service.returnSomethingPromise("foo");
long start = System.currentTimeMillis();
Object result = promise.await(1, TimeUnit.SECONDS); Object result = promise.await(1, TimeUnit.SECONDS);
long elapsed = System.currentTimeMillis() - start; assertNotNull(result);
assertTrue(elapsed <= 200 - safety);
assertTrue(result instanceof String);
assertEquals("foobar", result); assertEquals("foobar", result);
} }
@@ -326,7 +312,6 @@ public class AsyncGatewayTests {
proxyFactory.afterPropertiesSet(); proxyFactory.afterPropertiesSet();
TestEchoService service = (TestEchoService) proxyFactory.getObject(); TestEchoService service = (TestEchoService) proxyFactory.getObject();
Promise<String> promise = service.returnStringPromise("foo"); Promise<String> promise = service.returnStringPromise("foo");
long start = System.currentTimeMillis();
final AtomicReference<String> result = new AtomicReference<String>(); final AtomicReference<String> result = new AtomicReference<String>();
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);
@@ -340,9 +325,6 @@ public class AsyncGatewayTests {
}); });
latch.await(1, TimeUnit.SECONDS); latch.await(1, TimeUnit.SECONDS);
long elapsed = System.currentTimeMillis() - start;
assertTrue(elapsed <= 200 - safety);
assertEquals("foobar", result.get()); assertEquals("foobar", result.get());
} }
@@ -366,6 +348,7 @@ public class AsyncGatewayTests {
private static void startResponder(final PollableChannel requestChannel) { private static void startResponder(final PollableChannel requestChannel) {
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
Message<?> input = requestChannel.receive(); Message<?> input = requestChannel.receive();
@@ -389,11 +372,12 @@ public class AsyncGatewayTests {
} }
((MessageChannel) input.getHeaders().getReplyChannel()).send(reply); ((MessageChannel) input.getHeaders().getReplyChannel()).send(reply);
} }
}).start(); }).start();
} }
static interface TestEchoService { interface TestEchoService {
Future<String> returnString(String s); Future<String> returnString(String s);
@@ -403,10 +387,10 @@ public class AsyncGatewayTests {
ListenableFuture<Message<?>> returnMessageListenable(String s); ListenableFuture<Message<?>> returnMessageListenable(String s);
@Gateway(headers=@GatewayHeader(name="method", expression="#gatewayMethod.name")) @Gateway(headers = @GatewayHeader(name = "method", expression = "#gatewayMethod.name"))
CustomFuture returnCustomFuture(String s); CustomFuture returnCustomFuture(String s);
@Gateway(headers=@GatewayHeader(name="method", expression="#gatewayMethod.name")) @Gateway(headers = @GatewayHeader(name = "method", expression = "#gatewayMethod.name"))
Future<?> returnCustomFutureWithTypeFuture(String s); Future<?> returnCustomFutureWithTypeFuture(String s);
Promise<String> returnStringPromise(String s); Promise<String> returnStringPromise(String s);

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2001-2014 the original author or authors. * Copyright 2001-2015 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -57,10 +57,10 @@ public abstract class AbstractServerConnectionFactory
@Override @Override
public void start() { public void start() {
synchronized (this.lifecycleMonitor) { synchronized (this.lifecycleMonitor) {
if (!this.isActive()) { if (!isActive()) {
this.setActive(true); this.setActive(true);
this.shuttingDown = false; this.shuttingDown = false;
this.getTaskExecutor().execute(this); getTaskExecutor().execute(this);
} }
} }
super.start(); super.start();
@@ -102,22 +102,22 @@ public abstract class AbstractServerConnectionFactory
* @param socket The new socket. * @param socket The new socket.
*/ */
protected void initializeConnection(TcpConnectionSupport connection, Socket socket) { protected void initializeConnection(TcpConnectionSupport connection, Socket socket) {
TcpListener listener = this.getListener(); TcpListener listener = getListener();
if (listener != null) { if (listener != null) {
connection.registerListener(listener); connection.registerListener(listener);
} }
connection.registerSender(this.getSender()); connection.registerSender(getSender());
connection.setMapper(this.getMapper()); connection.setMapper(getMapper());
connection.setDeserializer(this.getDeserializer()); connection.setDeserializer(getDeserializer());
connection.setSerializer(this.getSerializer()); connection.setSerializer(getSerializer());
connection.setSingleUse(this.isSingleUse()); connection.setSingleUse(isSingleUse());
/* /*
* If we are configured * If we are configured
* for single use; need to enforce a timeout on the socket so we will close * for single use; need to enforce a timeout on the socket so we will close
* if the client connects, but sends nothing. (Protect against DoS). * if the client connects, but sends nothing. (Protect against DoS).
* Behavior can be overridden by explicitly setting the timeout to zero. * Behavior can be overridden by explicitly setting the timeout to zero.
*/ */
if (this.isSingleUse() && this.getSoTimeout() < 0) { if (isSingleUse() && getSoTimeout() < 0) {
try { try {
socket.setSoTimeout(DEFAULT_REPLY_TIMEOUT); socket.setSoTimeout(DEFAULT_REPLY_TIMEOUT);
} catch (SocketException e) { } catch (SocketException e) {
@@ -128,7 +128,7 @@ public abstract class AbstractServerConnectionFactory
} }
protected void postProcessServerSocket(ServerSocket serverSocket) { protected void postProcessServerSocket(ServerSocket serverSocket) {
this.getTcpSocketSupport().postProcessServerSocket(serverSocket); getTcpSocketSupport().postProcessServerSocket(serverSocket);
} }
/** /**
@@ -154,7 +154,7 @@ public abstract class AbstractServerConnectionFactory
* @return The backlog. * @return The backlog.
*/ */
public int getBacklog() { public int getBacklog() {
return backlog; return this.backlog;
} }
/** /**
@@ -175,8 +175,12 @@ public abstract class AbstractServerConnectionFactory
@Override @Override
public int afterShutdown() { public int afterShutdown() {
this.stop(); stop();
return 0; return 0;
} }
protected void publishServerExceptionEvent(Exception e) {
getApplicationEventPublisher().publishEvent(new TcpConnectionServerExceptionEvent(this, e));
}
} }

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.ip.tcp.connection;
import org.springframework.integration.ip.event.IpIntegrationEvent;
import org.springframework.util.Assert;
/**
* {@link IpIntegrationEvent} representing exceptions on a TCP server socket/channel.
*
* @author Gary Russell
* @since 4.0.7
*/
@SuppressWarnings("serial")
public class TcpConnectionServerExceptionEvent extends IpIntegrationEvent {
public TcpConnectionServerExceptionEvent(AbstractServerConnectionFactory connectionFactory, Throwable cause) {
super(connectionFactory, cause);
Assert.notNull(cause, "'cause' cannot be null");
Assert.notNull(connectionFactory, "'connectionFactory' cannot be null");
}
/**
* The connection factory that experienced the exception; examine it to determine the port etc.
* @return the connection factory.
*/
public AbstractServerConnectionFactory getConnectionFactory() {
return (AbstractServerConnectionFactory) getSource();
}
@Override
public String toString() {
return super.toString() +
", [factory=" + getConnectionFactory().getComponentType() +
":" + getConnectionFactory().getComponentName() +
", port=" + getConnectionFactory().getPort() + "]";
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2014 the original author or authors. * Copyright 2002-2015 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import org.springframework.util.Assert;
/** /**
* Implements a server connection factory that produces {@link TcpNetConnection}s using * Implements a server connection factory that produces {@link TcpNetConnection}s using
* a {@link ServerSocket}. Must have a {@link TcpListener} registered. * a {@link ServerSocket}. Must have a {@link TcpListener} registered.
*
* @author Gary Russell * @author Gary Russell
* @since 2.0 * @since 2.0
* *
@@ -48,6 +49,11 @@ public class TcpNetServerConnectionFactory extends AbstractServerConnectionFacto
super(port); super(port);
} }
@Override
public String getComponentType() {
return "tcp-net-server-connection-factory";
}
/** /**
* If no listener registers, exits. * If no listener registers, exits.
* Accepts incoming connections and creates TcpConnections for each new connection. * Accepts incoming connections and creates TcpConnections for each new connection.
@@ -58,22 +64,22 @@ public class TcpNetServerConnectionFactory extends AbstractServerConnectionFacto
@Override @Override
public void run() { public void run() {
ServerSocket theServerSocket = null; ServerSocket theServerSocket = null;
if (this.getListener() == null) { if (getListener() == null) {
logger.info("No listener bound to server connection factory; will not read; exiting..."); logger.info("No listener bound to server connection factory; will not read; exiting...");
return; return;
} }
try { try {
if (this.getLocalAddress() == null) { if (getLocalAddress() == null) {
theServerSocket = createServerSocket(this.getPort(), this.getBacklog(), null); theServerSocket = createServerSocket(getPort(), getBacklog(), null);
} }
else { else {
InetAddress whichNic = InetAddress.getByName(this.getLocalAddress()); InetAddress whichNic = InetAddress.getByName(getLocalAddress());
theServerSocket = createServerSocket(this.getPort(), this.getBacklog(), whichNic); theServerSocket = createServerSocket(getPort(), getBacklog(), whichNic);
} }
this.getTcpSocketSupport().postProcessServerSocket(theServerSocket); getTcpSocketSupport().postProcessServerSocket(theServerSocket);
this.serverSocket = theServerSocket; this.serverSocket = theServerSocket;
this.setListening(true); setListening(true);
logger.info("Listening on port " + this.getPort()); logger.info("Listening on port " + getPort());
while (true) { while (true) {
final Socket socket; final Socket socket;
/* /*
@@ -89,7 +95,7 @@ public class TcpNetServerConnectionFactory extends AbstractServerConnectionFacto
} }
continue; continue;
} }
if (this.isShuttingDown()) { if (isShuttingDown()) {
if (logger.isInfoEnabled()) { if (logger.isInfoEnabled()) {
logger.info("New connection from " + socket.getInetAddress().getHostAddress() logger.info("New connection from " + socket.getInetAddress().getHostAddress()
+ " rejected; the server is in the process of shutting down."); + " rejected; the server is in the process of shutting down.");
@@ -101,12 +107,12 @@ public class TcpNetServerConnectionFactory extends AbstractServerConnectionFacto
logger.debug("Accepted connection from " + socket.getInetAddress().getHostAddress()); logger.debug("Accepted connection from " + socket.getInetAddress().getHostAddress());
} }
setSocketAttributes(socket); setSocketAttributes(socket);
TcpConnectionSupport connection = new TcpNetConnection(socket, true, this.isLookupHost(), TcpConnectionSupport connection = new TcpNetConnection(socket, true, isLookupHost(),
this.getApplicationEventPublisher(), this.getComponentName()); getApplicationEventPublisher(), getComponentName());
connection = wrapConnection(connection); connection = wrapConnection(connection);
this.initializeConnection(connection, socket); initializeConnection(connection, socket);
this.getTaskExecutor().execute(connection); getTaskExecutor().execute(connection);
this.harvestClosedConnections(); harvestClosedConnections();
connection.publishConnectionOpenEvent(); connection.publishConnectionOpenEvent();
} }
} }
@@ -115,20 +121,21 @@ public class TcpNetServerConnectionFactory extends AbstractServerConnectionFacto
// don't log an error if we had a good socket once and now it's closed // don't log an error if we had a good socket once and now it's closed
if (e instanceof SocketException && theServerSocket != null) { if (e instanceof SocketException && theServerSocket != null) {
logger.warn("Server Socket closed"); logger.warn("Server Socket closed");
} else if (this.isActive()) { }
logger.error("Error on ServerSocket", e); else if (isActive()) {
logger.error("Error on ServerSocket; port = " + getPort(), e);
publishServerExceptionEvent(e);
} }
} }
finally { finally {
this.setListening(false); setListening(false);
this.setActive(false); setActive(false);
} }
} }
/** /**
* Create a new {@link ServerSocket}. This default implementation uses the default * Create a new {@link ServerSocket}. This default implementation uses the default
* {@link ServerSocketFactory}. Override to use some other mechanism * {@link ServerSocketFactory}. Override to use some other mechanism
*
* @param port The port. * @param port The port.
* @param backlog The server socket backlog. * @param backlog The server socket backlog.
* @param whichNic An InetAddress if binding to a specific network interface. Set to * @param whichNic An InetAddress if binding to a specific network interface. Set to
@@ -139,11 +146,10 @@ public class TcpNetServerConnectionFactory extends AbstractServerConnectionFacto
protected ServerSocket createServerSocket(int port, int backlog, InetAddress whichNic) throws IOException { protected ServerSocket createServerSocket(int port, int backlog, InetAddress whichNic) throws IOException {
ServerSocketFactory serverSocketFactory = this.tcpSocketFactorySupport.getServerSocketFactory(); ServerSocketFactory serverSocketFactory = this.tcpSocketFactorySupport.getServerSocketFactory();
if (whichNic == null) { if (whichNic == null) {
return serverSocketFactory.createServerSocket(port, return serverSocketFactory.createServerSocket(port, Math.abs(backlog));
Math.abs(backlog)); }
} else { else {
return serverSocketFactory.createServerSocket(port, return serverSocketFactory.createServerSocket(port, Math.abs(backlog), whichNic);
Math.abs(backlog), whichNic);
} }
} }
@@ -171,8 +177,7 @@ public class TcpNetServerConnectionFactory extends AbstractServerConnectionFacto
return tcpSocketFactorySupport; return tcpSocketFactorySupport;
} }
public void setTcpSocketFactorySupport( public void setTcpSocketFactorySupport(TcpSocketFactorySupport tcpSocketFactorySupport) {
TcpSocketFactorySupport tcpSocketFactorySupport) {
Assert.notNull(tcpSocketFactorySupport, "TcpSocketFactorySupport may not be null"); Assert.notNull(tcpSocketFactorySupport, "TcpSocketFactorySupport may not be null");
this.tcpSocketFactorySupport = tcpSocketFactorySupport; this.tcpSocketFactorySupport = tcpSocketFactorySupport;
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2014 the original author or authors. * Copyright 2002-2015 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -61,6 +61,11 @@ public class TcpNioServerConnectionFactory extends AbstractServerConnectionFacto
super(port); super(port);
} }
@Override
public String getComponentType() {
return "tcp-nio-server-connection-factory";
}
/** /**
* If no listener registers, exits. * If no listener registers, exits.
* Accepts incoming connections and creates TcpConnections for each new connection. * Accepts incoming connections and creates TcpConnections for each new connection.
@@ -70,39 +75,41 @@ public class TcpNioServerConnectionFactory extends AbstractServerConnectionFacto
*/ */
@Override @Override
public void run() { public void run() {
if (this.getListener() == null) { if (getListener() == null) {
logger.info("No listener bound to server connection factory; will not read; exiting..."); logger.info("No listener bound to server connection factory; will not read; exiting...");
return; return;
} }
try { try {
this.serverChannel = ServerSocketChannel.open(); this.serverChannel = ServerSocketChannel.open();
int port = this.getPort(); int port = getPort();
this.getTcpSocketSupport().postProcessServerSocket(this.serverChannel.socket()); getTcpSocketSupport().postProcessServerSocket(this.serverChannel.socket());
if (logger.isInfoEnabled()) { if (logger.isInfoEnabled()) {
logger.info("Listening on port " + port); logger.info("Listening on port " + port);
} }
this.serverChannel.configureBlocking(false); this.serverChannel.configureBlocking(false);
if (this.getLocalAddress() == null) { if (getLocalAddress() == null) {
this.serverChannel.socket().bind(new InetSocketAddress(port), this.serverChannel.socket().bind(new InetSocketAddress(port), Math.abs(getBacklog()));
Math.abs(this.getBacklog()));
} }
else { else {
InetAddress whichNic = InetAddress.getByName(this.getLocalAddress()); InetAddress whichNic = InetAddress.getByName(getLocalAddress());
this.serverChannel.socket().bind(new InetSocketAddress(whichNic, port), this.serverChannel.socket().bind(new InetSocketAddress(whichNic, port), Math.abs(getBacklog()));
Math.abs(this.getBacklog()));
} }
final Selector selector = Selector.open(); final Selector selector = Selector.open();
this.serverChannel.register(selector, SelectionKey.OP_ACCEPT); this.serverChannel.register(selector, SelectionKey.OP_ACCEPT);
this.setListening(true); setListening(true);
this.selector = selector; this.selector = selector;
doSelect(this.serverChannel, selector); doSelect(this.serverChannel, selector);
} }
catch (IOException e) { catch (IOException e) {
this.stop(); if (isActive()) {
logger.error("Error on ServerChannel; port = " + getPort(), e);
publishServerExceptionEvent(e);
}
stop();
} }
finally { finally {
this.setListening(false); setListening(false);
} }
} }
@@ -119,8 +126,8 @@ public class TcpNioServerConnectionFactory extends AbstractServerConnectionFacto
* @throws IOException * @throws IOException
*/ */
private void doSelect(ServerSocketChannel server, final Selector selector) throws IOException { private void doSelect(ServerSocketChannel server, final Selector selector) throws IOException {
while (this.isActive()) { while (isActive()) {
int soTimeout = this.getSoTimeout(); int soTimeout = getSoTimeout();
int selectionCount = 0; int selectionCount = 0;
try { try {
long timeout = soTimeout < 0 ? 0 : soTimeout; long timeout = soTimeout < 0 ? 0 : soTimeout;
@@ -131,7 +138,7 @@ public class TcpNioServerConnectionFactory extends AbstractServerConnectionFacto
logger.trace("Delayed reads:" + getDelayedReads().size() + " timeout " + timeout); logger.trace("Delayed reads:" + getDelayedReads().size() + " timeout " + timeout);
} }
selectionCount = selector.select(timeout); selectionCount = selector.select(timeout);
this.processNioSelections(selectionCount, selector, server, this.channelMap); processNioSelections(selectionCount, selector, server, this.channelMap);
} }
catch (CancelledKeyException cke) { catch (CancelledKeyException cke) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
@@ -139,8 +146,9 @@ public class TcpNioServerConnectionFactory extends AbstractServerConnectionFacto
} }
} }
catch (ClosedSelectorException cse) { catch (ClosedSelectorException cse) {
if (this.isActive()) { if (isActive()) {
logger.error("Selector closed", cse); logger.error("Selector closed", cse);
publishServerExceptionEvent(cse);
break; break;
} }
} }
@@ -157,7 +165,7 @@ public class TcpNioServerConnectionFactory extends AbstractServerConnectionFacto
protected void doAccept(final Selector selector, ServerSocketChannel server, long now) throws IOException { protected void doAccept(final Selector selector, ServerSocketChannel server, long now) throws IOException {
logger.debug("New accept"); logger.debug("New accept");
SocketChannel channel = server.accept(); SocketChannel channel = server.accept();
if (this.isShuttingDown()) { if (isShuttingDown()) {
if (logger.isInfoEnabled()) { if (logger.isInfoEnabled()) {
logger.info("New connection from " + channel.socket().getInetAddress().getHostAddress() logger.info("New connection from " + channel.socket().getInetAddress().getHostAddress()
+ " rejected; the server is in the process of shutting down."); + " rejected; the server is in the process of shutting down.");
@@ -173,7 +181,7 @@ public class TcpNioServerConnectionFactory extends AbstractServerConnectionFacto
if (connection == null) { if (connection == null) {
return; return;
} }
connection.setTaskExecutor(this.getTaskExecutor()); connection.setTaskExecutor(getTaskExecutor());
connection.setLastRead(now); connection.setLastRead(now);
this.channelMap.put(channel, connection); this.channelMap.put(channel, connection);
channel.register(selector, SelectionKey.OP_READ, connection); channel.register(selector, SelectionKey.OP_READ, connection);
@@ -188,12 +196,11 @@ public class TcpNioServerConnectionFactory extends AbstractServerConnectionFacto
private TcpNioConnection createTcpNioConnection(SocketChannel socketChannel) { private TcpNioConnection createTcpNioConnection(SocketChannel socketChannel) {
try { try {
TcpNioConnection connection = this.tcpNioConnectionSupport TcpNioConnection connection = this.tcpNioConnectionSupport.createNewConnection(socketChannel, true,
.createNewConnection(socketChannel, true, isLookupHost(), getApplicationEventPublisher(), getComponentName());
this.isLookupHost(), this.getApplicationEventPublisher(), this.getComponentName());
connection.setUsingDirectBuffers(this.usingDirectBuffers); connection.setUsingDirectBuffers(this.usingDirectBuffers);
TcpConnectionSupport wrappedConnection = wrapConnection(connection); TcpConnectionSupport wrappedConnection = wrapConnection(connection);
this.initializeConnection(wrappedConnection, socketChannel.socket()); initializeConnection(wrappedConnection, socketChannel.socket());
return connection; return connection;
} }
catch (Exception e) { catch (Exception e) {
@@ -204,7 +211,7 @@ public class TcpNioServerConnectionFactory extends AbstractServerConnectionFacto
@Override @Override
public void stop() { public void stop() {
this.setActive(false); setActive(false);
if (this.selector != null) { if (this.selector != null) {
try { try {
this.selector.close(); this.selector.close();

View File

@@ -16,25 +16,49 @@
package org.springframework.integration.ip.tcp.connection; package org.springframework.integration.ip.tcp.connection;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.BindException;
import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ServerSocketFactory;
import org.apache.commons.logging.Log;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.internal.stubbing.answers.DoesNothing;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.serializer.Serializer; import org.springframework.core.serializer.Serializer;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage; import org.springframework.messaging.support.GenericMessage;
import org.springframework.util.SocketUtils;
/** /**
* @author Gary Russell * @author Gary Russell
@@ -44,7 +68,7 @@ import org.springframework.messaging.support.GenericMessage;
public class ConnectionEventTests { public class ConnectionEventTests {
@Test @Test
public void test() throws Exception { public void testConnectionEvents() throws Exception {
Socket socket = mock(Socket.class); Socket socket = mock(Socket.class);
final List<TcpConnectionEvent> theEvent = new ArrayList<TcpConnectionEvent>(); final List<TcpConnectionEvent> theEvent = new ArrayList<TcpConnectionEvent>();
TcpNetConnection conn = new TcpNetConnection(socket, false, false, new ApplicationEventPublisher() { TcpNetConnection conn = new TcpNetConnection(socket, false, false, new ApplicationEventPublisher() {
@@ -85,7 +109,72 @@ public class ConnectionEventTests {
assertSame(toBeThrown, event.getCause()); assertSame(toBeThrown, event.getCause());
assertTrue(theEvent.size() > 1); assertTrue(theEvent.size() > 1);
assertNotNull(theEvent.get(1)); assertNotNull(theEvent.get(1));
assertTrue(theEvent.get(1).toString().endsWith("[factory=foo, connectionId=" + conn.getConnectionId() + "] **CLOSED**")); assertTrue(theEvent.get(1).toString()
.endsWith("[factory=foo, connectionId=" + conn.getConnectionId() + "] **CLOSED**"));
}
@Test
public void testNetServerExceptionEvent() throws Exception {
int port = SocketUtils.findAvailableTcpPort();
AbstractServerConnectionFactory factory = new TcpNetServerConnectionFactory(port);
testServerExceptionGuts(port, factory);
}
@Test
public void testNioServerExceptionEvent() throws Exception {
int port = SocketUtils.findAvailableTcpPort();
AbstractServerConnectionFactory factory = new TcpNioServerConnectionFactory(port);
testServerExceptionGuts(port, factory);
}
private void testServerExceptionGuts(int port, AbstractServerConnectionFactory factory) throws Exception {
ServerSocket ss = ServerSocketFactory.getDefault().createServerSocket(port);
final AtomicReference<TcpConnectionServerExceptionEvent> theEvent =
new AtomicReference<TcpConnectionServerExceptionEvent>();
final CountDownLatch latch = new CountDownLatch(1);
factory.setApplicationEventPublisher(new ApplicationEventPublisher() {
@Override
public void publishEvent(ApplicationEvent event) {
theEvent.set((TcpConnectionServerExceptionEvent) event);
latch.countDown();
}
@Override
public void publishEvent(Object event) {
}
});
factory.setBeanName("sf");
factory.registerListener(new TcpListener() {
@Override
public boolean onMessage(Message<?> message) {
return false;
}
});
Log logger = spy(TestUtils.getPropertyValue(factory, "logger", Log.class));
doAnswer(new DoesNothing()).when(logger).error(anyString(), any(Throwable.class));
new DirectFieldAccessor(factory).setPropertyValue("logger", logger);
factory.start();
assertTrue(latch.await(10, TimeUnit.SECONDS));
String actual = theEvent.toString();
assertThat(actual, containsString("cause=java.net.BindException"));
assertThat(actual, containsString("[factory="
+ (factory instanceof TcpNetServerConnectionFactory
? "tcp-net-server-connection-factory"
: "tcp-nio-server-connection-factory")
+ ":sf, port=" + port + "]"));
ArgumentCaptor<String> reasonCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(logger).error(reasonCaptor.capture(), throwableCaptor.capture());
assertThat(reasonCaptor.getValue(), startsWith("Error on Server"));
assertThat(reasonCaptor.getValue(), endsWith("; port = " + port));
assertThat(throwableCaptor.getValue(), instanceOf(BindException.class));
ss.close();
} }
} }

View File

@@ -2,7 +2,6 @@ log4j.rootCategory=WARN, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %c{1} [%t] : %m%n log4j.appender.stdout.layout.ConversionPattern=%d %5p %c{1} [%t] : %m%n
log4j.category.org.springframework.integration=WARN log4j.category.org.springframework.integration=WARN
log4j.category.org.springframework.integration.ip=WARN

View File

@@ -508,6 +508,11 @@
point the exception occurred. Applications can use a normal <interfacename>ApplicationListener</interfacename>, point the exception occurred. Applications can use a normal <interfacename>ApplicationListener</interfacename>,
or see <xref linkend="applicationevent-inbound"/>, to capture these events, allowing analysis of the problem. or see <xref linkend="applicationevent-inbound"/>, to capture these events, allowing analysis of the problem.
</para> </para>
<para>
Starting with <emphasis>versions 4.0.7, 4.1.3</emphasis> <classname>TcpConnectionServerExceptionEvent</classname>s
are published whenever an unexpected exception occurs on a server socket (such as a <classname>BindException</classname>
when the server socket is in use). These events have a reference to the connection factory and the cause.
</para>
</section> </section>
<section id="tcp-adapters"> <section id="tcp-adapters">
<title>TCP Adapters</title> <title>TCP Adapters</title>

View File

@@ -39,5 +39,13 @@
<code>org.springframework.integration.scattergather</code>. <code>org.springframework.integration.scattergather</code>.
</para> </para>
</section> </section>
<section id="4.2-tcp-server-exceptions">
<title>Server Socket Exceptions</title>
<para>
<classname>TcpConnectionServerExceptionEvent</classname>s are now published whenever an
unexpected exception occurs on a TCP server socket (also added to 4.1.3, 4.0.7).
See <xref linkend="tcp-events"/> for more information.
</para>
</section>
</section> </section>
</chapter> </chapter>