INT-3728: TCP: Fix Early Receive with Caching CCF

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

The `CachingClientConnectionFactory` uses a temporary (rejecting) listener until
the connection is established and then replaces the listener in the connection.

There is a race condition in that if the server starts sending messages before the
listener is replaced, the message is rejected.

Add a mechanism to delay `onMessage` calls until the real listener has been
registered.

Also fix `onMessage` in the cached connection so an `ErrorMessage` is propagated correctly.

To reproduce: revert src/main; add `Thread.sleep(1000)` before `registerListener(tcpListener);`
in `CachedConnection` ctor and run the new test.

To introduce a similar timing hole with the new code, add the sleep before
`this.theConnection.registerListener(this);` in `TcpConnectionInterceptorSupport`.

Summary of changes:

`CachingClientConnectionFactory`
- register the underlying connection's listener in the ctor, utilizing the
    `TcpConnectionInterceptorSupport.registerListener()` method.
- fix `ErrorMessage` propagation.

`AbstractClientConnectionFactory`
- propagate the `enableManualListenerRegistration` to connections.

`TcpConnectionSuport`
- implement delay when manual listener registration is enabled.

Add test case.

Fix Failover Tests

`FailoverClientConnectionFactory`
- propagate enable manual listener registration to underlying factories

Polishing
This commit is contained in:
Gary Russell
2015-06-05 14:52:03 -04:00
committed by Artem Bilan
parent 7c78553609
commit 9e1119b434
5 changed files with 157 additions and 44 deletions

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.
@@ -34,6 +34,8 @@ public abstract class AbstractClientConnectionFactory extends AbstractConnection
private volatile TcpConnectionSupport theConnection; private volatile TcpConnectionSupport theConnection;
private volatile boolean manualListenerRegistration;
/** /**
* Constructs a factory that will established connections to the host and port. * Constructs a factory that will established connections to the host and port.
* @param host The host. * @param host The host.
@@ -43,6 +45,17 @@ public abstract class AbstractClientConnectionFactory extends AbstractConnection
super(host, port); super(host, port);
} }
/**
* Set whether to automatically (default) or manually add a {@link TcpListener} to the
* connections created by this factory. By default, the factory automatically configures
* the listener. When manual registration is in place, incoming messages will be delayed
* until the listener is registered.
* @since 1.4.5
*/
public void enableManualListenerRegistration() {
this.manualListenerRegistration = true;
}
/** /**
* Obtains a connection - if {@link #setSingleUse(boolean)} was called with * Obtains a connection - if {@link #setSingleUse(boolean)} was called with
* true, a new connection is returned; otherwise a single connection is * true, a new connection is returned; otherwise a single connection is
@@ -126,9 +139,14 @@ public abstract class AbstractClientConnectionFactory extends AbstractConnection
* @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(); if (this.manualListenerRegistration) {
if (listener != null) { connection.enableManualListenerRegistration();
connection.registerListener(listener); }
else {
TcpListener listener = this.getListener();
if (listener != null) {
connection.registerListener(listener);
}
} }
TcpSender sender = this.getSender(); TcpSender sender = this.getSender();
if (sender != null) { if (sender != null) {

View File

@@ -16,6 +16,7 @@
package org.springframework.integration.ip.tcp.connection; package org.springframework.integration.ip.tcp.connection;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -141,9 +142,7 @@ public class CachingClientConnectionFactory extends AbstractClientConnectionFact
@Override @Override
public TcpConnectionSupport obtainConnection() throws Exception { public TcpConnectionSupport obtainConnection() throws Exception {
CachedConnection cachedConnection = new CachedConnection(this.pool.getItem()); return new CachedConnection(this.pool.getItem(), getListener());
cachedConnection.registerListener(getListener());
return cachedConnection;
} }
@Override @Override
@@ -168,9 +167,9 @@ public class CachingClientConnectionFactory extends AbstractClientConnectionFact
private volatile boolean released; private volatile boolean released;
public CachedConnection(TcpConnectionSupport connection) { public CachedConnection(TcpConnectionSupport connection, TcpListener tcpListener) {
super.setTheConnection(connection); super.setTheConnection(connection);
connection.registerListener(this); registerListener(tcpListener);
} }
@Override @Override
@@ -225,17 +224,30 @@ public class CachingClientConnectionFactory extends AbstractClientConnectionFact
*/ */
@Override @Override
public boolean onMessage(Message<?> message) { public boolean onMessage(Message<?> message) {
AbstractIntegrationMessageBuilder<?> messageBuilder = Message<?> modifiedMessage;
CachingClientConnectionFactory.this.getMessageBuilderFactory() if (message instanceof ErrorMessage) {
.fromMessage(message) Map<String, Object> headers = new HashMap<String, Object>(message.getHeaders());
.setHeader(IpHeaders.CONNECTION_ID, getConnectionId()); headers.put(IpHeaders.CONNECTION_ID, getConnectionId());
if (message.getHeaders().get(IpHeaders.ACTUAL_CONNECTION_ID) == null) { if (headers.get(IpHeaders.ACTUAL_CONNECTION_ID) == null) {
messageBuilder.setHeader(IpHeaders.ACTUAL_CONNECTION_ID, headers.put(IpHeaders.ACTUAL_CONNECTION_ID,
message.getHeaders().get(IpHeaders.CONNECTION_ID)); message.getHeaders().get(IpHeaders.CONNECTION_ID));
}
modifiedMessage = new ErrorMessage((Throwable) message.getPayload(), headers);
}
else {
AbstractIntegrationMessageBuilder<?> messageBuilder =
CachingClientConnectionFactory.this.getMessageBuilderFactory()
.fromMessage(message)
.setHeader(IpHeaders.CONNECTION_ID, getConnectionId());
if (message.getHeaders().get(IpHeaders.ACTUAL_CONNECTION_ID) == null) {
messageBuilder.setHeader(IpHeaders.ACTUAL_CONNECTION_ID,
message.getHeaders().get(IpHeaders.CONNECTION_ID));
}
modifiedMessage = messageBuilder.build();
} }
TcpListener listener = getListener(); TcpListener listener = getListener();
if (listener != null) { if (listener != null) {
listener.onMessage(messageBuilder.build()); listener.onMessage(modifiedMessage);
} }
else { else {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
@@ -413,17 +425,7 @@ public class CachingClientConnectionFactory extends AbstractClientConnectionFact
@Override @Override
public void registerListener(TcpListener listener) { public void registerListener(TcpListener listener) {
super.registerListener(listener); super.registerListener(listener);
this.targetConnectionFactory.registerListener(new TcpListener() { this.targetConnectionFactory.enableManualListenerRegistration();
@Override
public boolean onMessage(Message<?> message) {
if (!(message instanceof ErrorMessage)) {
throw new UnsupportedOperationException("This should never be called");
}
return false;
}
});
} }
@Override @Override

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.
@@ -93,6 +93,13 @@ public class FailoverClientConnectionFactory extends AbstractClientConnectionFac
} }
} }
@Override
public void enableManualListenerRegistration() {
for (AbstractClientConnectionFactory factory : this.factories) {
factory.enableManualListenerRegistration();
}
}
@Override @Override
public void registerSender(TcpSender sender) { public void registerSender(TcpSender sender) {
for (AbstractClientConnectionFactory factory : this.factories) { for (AbstractClientConnectionFactory factory : this.factories) {

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.
@@ -22,6 +22,8 @@ import java.net.SocketException;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
@@ -34,6 +36,7 @@ import org.springframework.core.serializer.Serializer;
import org.springframework.integration.ip.IpHeaders; import org.springframework.integration.ip.IpHeaders;
import org.springframework.integration.ip.tcp.serializer.AbstractByteArraySerializer; import org.springframework.integration.ip.tcp.serializer.AbstractByteArraySerializer;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.support.ErrorMessage; import org.springframework.messaging.support.ErrorMessage;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@@ -50,6 +53,8 @@ public abstract class TcpConnectionSupport implements TcpConnection {
protected final Log logger = LogFactory.getLog(this.getClass()); protected final Log logger = LogFactory.getLog(this.getClass());
private final CountDownLatch listenerRegisteredLatch = new CountDownLatch(1);
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
private volatile Deserializer deserializer; private volatile Deserializer deserializer;
@@ -88,6 +93,8 @@ public abstract class TcpConnectionSupport implements TcpConnection {
private volatile boolean noReadErrorOnClose; private volatile boolean noReadErrorOnClose;
private volatile boolean manualListenerRegistration;
public TcpConnectionSupport() { public TcpConnectionSupport() {
this(null); this(null);
} }
@@ -175,18 +182,20 @@ public abstract class TcpConnectionSupport implements TcpConnection {
* @param isException true when this call is the result of an Exception. * @param isException true when this call is the result of an Exception.
*/ */
protected void closeConnection(boolean isException) { protected void closeConnection(boolean isException) {
if (!(this.listener instanceof TcpConnectionInterceptor)) { TcpListener listener = getListener();
if (!(listener instanceof TcpConnectionInterceptor)) {
close(); close();
return;
} }
TcpConnectionInterceptor outerInterceptor = (TcpConnectionInterceptor) this.listener; else {
while (outerInterceptor.getListener() instanceof TcpConnectionInterceptor) { TcpConnectionInterceptor outerInterceptor = (TcpConnectionInterceptor) listener;
outerInterceptor = (TcpConnectionInterceptor) outerInterceptor.getListener(); while (outerInterceptor.getListener() instanceof TcpConnectionInterceptor) {
} outerInterceptor = (TcpConnectionInterceptor) outerInterceptor.getListener();
outerInterceptor.close(); }
if (isException) { outerInterceptor.close();
// ensure physical close in case the interceptor did not close if (isException) {
this.close(); // ensure physical close in case the interceptor did not close
this.close();
}
} }
} }
@@ -245,7 +254,7 @@ public abstract class TcpConnectionSupport implements TcpConnection {
} }
/** /**
* Sets the listener that will receive incoming Messages. * Set the listener that will receive incoming Messages.
* @param listener The listener. * @param listener The listener.
*/ */
public void registerListener(TcpListener listener) { public void registerListener(TcpListener listener) {
@@ -253,13 +262,33 @@ public abstract class TcpConnectionSupport implements TcpConnection {
// Determine the actual listener for this connection // Determine the actual listener for this connection
if (!(this.listener instanceof TcpConnectionInterceptor)) { if (!(this.listener instanceof TcpConnectionInterceptor)) {
this.actualListener = this.listener; this.actualListener = this.listener;
} else { }
else {
TcpConnectionInterceptor outerInterceptor = (TcpConnectionInterceptor) this.listener; TcpConnectionInterceptor outerInterceptor = (TcpConnectionInterceptor) this.listener;
while (outerInterceptor.getListener() instanceof TcpConnectionInterceptor) { while (outerInterceptor.getListener() instanceof TcpConnectionInterceptor) {
outerInterceptor = (TcpConnectionInterceptor) outerInterceptor.getListener(); outerInterceptor = (TcpConnectionInterceptor) outerInterceptor.getListener();
} }
this.actualListener = outerInterceptor.getListener(); this.actualListener = outerInterceptor.getListener();
} }
this.listenerRegisteredLatch.countDown();
}
/**
* Set whether or not automatic or manual registration of the {@link TcpListener} is to be
* used. (Default automatic). When manual registration is in place, incoming messages will
* be delayed until the listener is registered.
* @since 1.4.5
*/
public void enableManualListenerRegistration() {
this.manualListenerRegistration = true;
this.listener = new TcpListener() {
@Override
public boolean onMessage(Message<?> message) {
return getListener().onMessage(message);
}
};
} }
/** /**
@@ -280,9 +309,23 @@ public abstract class TcpConnectionSupport implements TcpConnection {
*/ */
@Override @Override
public TcpListener getListener() { public TcpListener getListener() {
if (this.manualListenerRegistration) {
waitForListenerRegistration();
}
return this.listener; return this.listener;
} }
private void waitForListenerRegistration() {
try {
Assert.state(listenerRegisteredLatch.await(1, TimeUnit.MINUTES), "TcpListener not registered");
manualListenerRegistration = false;
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MessagingException("Interrupted while waiting for listener registration", e);
}
}
/** /**
* @return the sender * @return the sender
*/ */

View File

@@ -36,6 +36,7 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.Socket; import java.net.Socket;
@@ -50,6 +51,7 @@ import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.junit.Test; import org.junit.Test;
@@ -694,7 +696,48 @@ public class CachingClientConnectionFactoryTests {
verify(logger, never()).error(anyString()); verify(logger, never()).error(anyString());
} }
public TcpConnectionSupport makeMockConnection() { @Test // INT-3728
public void testEarlyReceive() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
final AbstractClientConnectionFactory factory = new TcpNetClientConnectionFactory("", 0) {
@Override
protected Socket createSocket(String host, int port) throws IOException {
Socket mock = mock(Socket.class);
when(mock.getInputStream()).thenReturn(new ByteArrayInputStream("foo\r\n".getBytes()));
return mock;
}
@Override
public boolean isActive() {
return true;
}
};
factory.setApplicationEventPublisher(mock(ApplicationEventPublisher.class));
final CachingClientConnectionFactory cachingFactory = new CachingClientConnectionFactory(factory, 1);
final AtomicReference<Message<?>> received = new AtomicReference<Message<?>>();
cachingFactory.registerListener(new TcpListener() {
@Override
public boolean onMessage(Message<?> message) {
if (!(message instanceof ErrorMessage)) {
received.set(message);
latch.countDown();
}
return false;
}
});
cachingFactory.start();
cachingFactory.getConnection();
assertTrue(latch.await(10, TimeUnit.SECONDS));
assertNotNull(received.get());
assertNotNull(received.get().getHeaders().get(IpHeaders.ACTUAL_CONNECTION_ID));
cachingFactory.stop();
}
private TcpConnectionSupport makeMockConnection() {
TcpConnectionSupport connection = mock(TcpConnectionSupport.class); TcpConnectionSupport connection = mock(TcpConnectionSupport.class);
when(connection.isOpen()).thenReturn(true); when(connection.isOpen()).thenReturn(true);
return connection; return connection;