[ 
https://issues.apache.org/jira/browse/CAMEL-8088?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Claus Ibsen reassigned CAMEL-8088:
----------------------------------

    Assignee: Claus Ibsen  (was: Willem Jiang)

> FTP can wait indefinitely when connection timeout occurs during connect
> -----------------------------------------------------------------------
>
>                 Key: CAMEL-8088
>                 URL: https://issues.apache.org/jira/browse/CAMEL-8088
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-ftp
>    Affects Versions: 2.13.3
>            Reporter: Bob Browning
>            Assignee: Claus Ibsen
>            Priority: Minor
>             Fix For: 2.13.4, 2.14.2, 2.15.0
>
>
> In our production system we have seen cases where the FTP thread is waiting 
> for a response indefinitely despite having set _soTimeout_ on the connection. 
> On investigation this is due to a condition that can occur where a socket is 
> able to connect yet a firewall or the ilk then blocks further traffic.
> This can be over come by setting the property _ftpClient.defaultTimeout_ to a 
> non-zero value.
> It should be the case where if upon initial socket connection no response 
> occurs that the socket should be deemed dead, however this is not the case.
> When the following exception is thrown during initial connect to an FTP 
> server, after the socket has connected but whilst awaiting the inital reply 
> it can leave the RemoteFileProducer in a state where it is connected but not 
> logged in and no attempt reconnect is attempted, if the soTimeout as set by 
> _ftpClient.defaultTimeout_ is set to zero then it can cause a subsequent 
> command will wait for a reply indefinitely.
> {noformat}
> Caused by: java.io.IOException: Timed out waiting for initial connect reply
>       at org.apache.commons.net.ftp.FTP._connectAction_(FTP.java:389) 
> ~[commons-net-3.1.jar:3.1]
>       at 
> org.apache.commons.net.ftp.FTPClient._connectAction_(FTPClient.java:796) 
> ~[commons-net-3.1.jar:3.1]
>       at org.apache.commons.net.SocketClient.connect(SocketClient.java:172) 
> ~[commons-net-3.1.jar:3.1]
>       at org.apache.commons.net.SocketClient.connect(SocketClient.java:192) 
> ~[commons-net-3.1.jar:3.1]
>       at 
> org.apache.camel.component.file.remote.FtpOperations.connect(FtpOperations.java:95)
>  ~[camel-ftp-2.13.1.jar:2.13.1]
> {noformat}
> The RemoteFileProducer will enter this block as the loggedIn state has not 
> yet been reached, however the existing broken socket is reused.
> {code}
>         // recover by re-creating operations which should most likely be able 
> to recover
>         if (!loggedIn) {
>             log.debug("Trying to recover connection to: {} with a fresh 
> client.", getEndpoint());
>             setOperations(getEndpoint().createRemoteFileOperations());
>             connectIfNecessary();
>         }
> {code}
> Yet the _connectIfNecessary()_ method will return immediately since the check 
> condition is based on socket connection and takes no account of whether login 
> was achieved so the 'dead' socket is reused.
> {code}
>     protected void connectIfNecessary() throws 
> GenericFileOperationFailedException {
>         // This will be skipped when loggedIn = false and the socket is 
> connected
>         if (!getOperations().isConnected()) {
>             log.debug("Not already connected/logged in. Connecting to: {}", 
> getEndpoint());
>             RemoteFileConfiguration config = getEndpoint().getConfiguration();
>             loggedIn = getOperations().connect(config);
>             if (!loggedIn) {
>                 return;
>             }
>             log.info("Connected and logged in to: " + getEndpoint());
>         }
>     }
> {code}
> A dirty test that blocks of this blocking condition:
> {code}
> package ftp;
> import org.apache.camel.builder.RouteBuilder;
> import org.apache.camel.impl.JndiRegistry;
> import org.apache.camel.test.junit4.CamelTestSupport;
> import org.apache.commons.net.ftp.FTPClient;
> import org.junit.After;
> import org.junit.Before;
> import org.junit.Test;
> import org.mockftpserver.fake.FakeFtpServer;
> import org.mockito.Mockito;
> import org.mockito.invocation.InvocationOnMock;
> import org.mockito.stubbing.Answer;
> import java.io.IOException;
> import java.io.InputStream;
> import java.net.Socket;
> import java.net.SocketException;
> import java.net.SocketTimeoutException;
> import java.util.concurrent.atomic.AtomicBoolean;
> import javax.net.SocketFactory;
> import static org.mockito.Matchers.anyInt;
> import static org.mockito.Mockito.doAnswer;
> import static org.mockito.Mockito.mock;
> import static org.mockito.Mockito.when;
> public class FtpInitialConnectTimeoutTest extends CamelTestSupport {
>   private static final int CONNECT_TIMEOUT = 11223;
>   /**
>    * Create the answer for the socket factory that causes a 
> SocketTimeoutException to occur in connect.
>    */
>   private static class SocketAnswer implements Answer<Socket> {
>     @Override
>     public Socket answer(InvocationOnMock invocation) throws Throwable {
>       final Socket socket = Mockito.spy(new Socket());
>       final AtomicBoolean timeout = new AtomicBoolean();
>       try {
>         doAnswer(new Answer<InputStream>() {
>           @Override
>           public InputStream answer(InvocationOnMock invocation) throws 
> Throwable {
>             final InputStream stream = (InputStream) 
> invocation.callRealMethod();
>             InputStream inputStream = new InputStream() {
>               @Override
>               public int read() throws IOException {
>                 if (timeout.get()) {
>                   // emulate a timeout occuring in _getReply()
>                   throw new SocketTimeoutException();
>                 }
>                 return stream.read();
>               }
>             };
>             return inputStream;
>           }
>         }).when(socket).getInputStream();
>       } catch (IOException ignored) {
>       }
>       try {
>         doAnswer(new Answer() {
>           @Override
>           public Object answer(InvocationOnMock invocation) throws Throwable {
>             if ((Integer) invocation.getArguments()[0] == CONNECT_TIMEOUT) {
>               // setting of connect timeout
>               timeout.set(true);
>             } else {
>               // non-connect timeout
>               timeout.set(false);
>             }
>             return invocation.callRealMethod();
>           }
>         }).when(socket).setSoTimeout(anyInt());
>       } catch (SocketException e) {
>         throw new RuntimeException(e);
>       }
>       return socket;
>     }
>   }
>   private FakeFtpServer fakeFtpServer;
>   @Override
>   @Before
>   public void setUp() throws Exception {
>     fakeFtpServer = new FakeFtpServer();
>     fakeFtpServer.setServerControlPort(0);
>     fakeFtpServer.start();
>     super.setUp();
>   }
>   @Override
>   @After
>   public void tearDown() throws Exception {
>     super.tearDown();
>     if (fakeFtpServer != null) {
>       fakeFtpServer.stop();
>     }
>   }
>   @Test
>   public void testName() throws Exception {
>     sendBody("direct:start", "test");
>   }
>   private FTPClient mockedClient() throws IOException {
>     FTPClient client = new FTPClient();
>     client.setSocketFactory(createSocketFactory());
>     return client;
>   }
>   private SocketFactory createSocketFactory() throws IOException {
>     SocketFactory socketFactory = mock(SocketFactory.class);
>     when(socketFactory.createSocket()).thenAnswer(new SocketAnswer());
>     return socketFactory;
>   }
>   @Override
>   protected JndiRegistry createRegistry() throws Exception {
>     JndiRegistry registry = super.createRegistry();
>     registry.bind("mocked", mockedClient());
>     return registry;
>   }
>   @Override
>   protected RouteBuilder createRouteBuilder() throws Exception {
>     return new RouteBuilder() {
>       @Override
>       public void configure() throws Exception {
>         from("direct:start")
>             .to("ftp://localhost:"; + fakeFtpServer.getServerControlPort()
>                 + "?ftpClient=#mocked"
>                 + "&soTimeout=1234&"
>                 + "connectTimeout=" + CONNECT_TIMEOUT);
>       }
>     };
>   }
> }
> {code}



--
This message was sent by Atlassian JIRA
(v6.3.4#6332)

Reply via email to