This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 708a656e368b CAMEL-25915: camel-ftp - Reconnect after a failed poll 
with sendNoop=false and a custom poll strategy (#28232)
708a656e368b is described below

commit 708a656e368b131183251aefefb84611f262defe
Author: Johan Almqvist <[email protected]>
AuthorDate: Wed Oct 21 21:07:00 2026 +0200

    CAMEL-25915: camel-ftp - Reconnect after a failed poll with sendNoop=false 
and a custom poll strategy (#28232)
    
    With sendNoop=false, an ftp or ftps consumer whose poll failed on a
    connection that the server had closed relied on the default poll
    strategy to disconnect. With a custom pollStrategy nothing disconnected,
    and every following poll failed. A failure caused by an I/O error that
    is thrown out of the poll now also makes the consumer send a noop
    before the next poll, as a failure while processing a file already did.
    
    The upgrade guide and the setSendNoop Javadoc now say that with
    sendNoop=false a mina-sftp connection that dropped silently is found by
    the next poll, and point to serverAliveInterval.
    
    Claude-Session: https://claude.ai/code/session_01SonJJYXyt16gNRQkSu72AH
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../file/remote/RemoteFileConfiguration.java       |  4 +-
 .../component/file/remote/RemoteFileConsumer.java  | 12 +++++
 .../remote/RemoteFileConsumerSendNoopTest.java     | 52 ++++++++++++++++++++--
 .../integration/FtpConsumerSendNoopFalseIT.java    | 46 ++++++++++++++++++-
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  5 ++-
 5 files changed, 112 insertions(+), 7 deletions(-)

diff --git 
a/components/camel-ftp-common/src/main/java/org/apache/camel/component/file/remote/RemoteFileConfiguration.java
 
b/components/camel-ftp-common/src/main/java/org/apache/camel/component/file/remote/RemoteFileConfiguration.java
index fca7581f7fa0..e53f504e120d 100644
--- 
a/components/camel-ftp-common/src/main/java/org/apache/camel/component/file/remote/RemoteFileConfiguration.java
+++ 
b/components/camel-ftp-common/src/main/java/org/apache/camel/component/file/remote/RemoteFileConfiguration.java
@@ -418,7 +418,9 @@ public abstract class RemoteFileConfiguration extends 
GenericFileConfiguration {
      * before the next poll, and connects again if the noop fails. If the 
server closes idle connections and the delay
      * between polls is longer than its idle timeout, every other poll then 
fails, so keep this option on for such a
      * consumer. For sftp and mina-sftp the client notices a closed 
connection, but not one that dropped silently. For
-     * sftp the noop is a keep-alive message that does not wait for a reply.
+     * sftp the noop is a keep-alive message that does not wait for a reply, 
so it does not find such a connection
+     * either. For mina-sftp such a connection is then found by the next poll, 
which fails before the consumer connects
+     * again. Set serverAliveInterval to let the client close such a 
connection itself.
      */
     public void setSendNoop(boolean sendNoop) {
         this.sendNoop = sendNoop;
diff --git 
a/components/camel-ftp-common/src/main/java/org/apache/camel/component/file/remote/RemoteFileConsumer.java
 
b/components/camel-ftp-common/src/main/java/org/apache/camel/component/file/remote/RemoteFileConsumer.java
index fede2bf00662..7f7d6d565a46 100644
--- 
a/components/camel-ftp-common/src/main/java/org/apache/camel/component/file/remote/RemoteFileConsumer.java
+++ 
b/components/camel-ftp-common/src/main/java/org/apache/camel/component/file/remote/RemoteFileConsumer.java
@@ -283,6 +283,18 @@ public abstract class RemoteFileConsumer<T> extends 
GenericFileConsumer<T> {
         }
     }
 
+    @Override
+    protected int poll(Exchange dynamic) throws Exception {
+        try {
+            return super.poll(dynamic);
+        } catch (Exception e) {
+            // a failure thrown out of the poll, such as on a connection that 
the server has closed, goes to the poll
+            // strategy, and only the default poll strategy disconnects
+            sendNoopOnNextPollIfIOFailure(e);
+            throw e;
+        }
+    }
+
     @Override
     protected void handleException(String message, Exchange exchange, 
Throwable t) {
         // a failure while retrieving or processing a file is not thrown out 
of the poll, so the poll strategy does not
diff --git 
a/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/RemoteFileConsumerSendNoopTest.java
 
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/RemoteFileConsumerSendNoopTest.java
index 4b1151e1f6d3..c17192411fac 100644
--- 
a/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/RemoteFileConsumerSendNoopTest.java
+++ 
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/RemoteFileConsumerSendNoopTest.java
@@ -20,12 +20,14 @@ import java.io.IOException;
 import java.net.SocketException;
 
 import org.apache.camel.component.file.GenericFileOperationFailedException;
+import org.apache.camel.component.file.GenericFileProcessStrategy;
 import org.apache.camel.test.junit6.CamelTestSupport;
 import org.apache.commons.net.ftp.FTPFile;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.Mockito;
 
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
@@ -38,8 +40,9 @@ import static org.mockito.Mockito.when;
 /**
  * Unit tests for the connection check that {@link 
RemoteFileConsumer#connectIfNecessary()} does before each poll: by
  * default it sends a noop, and with sendNoop=false it only checks the 
connection state of the client. With
- * sendNoop=false the consumer sends a noop before the next poll after an I/O 
failure, also when the ftp consumer
- * ignores the failure as a file that does not exist, unless it disconnects 
before that poll.
+ * sendNoop=false the consumer sends a noop before the next poll after an I/O 
failure, whether the failure is thrown out
+ * of the poll or happens while processing a file, also when the ftp consumer 
ignores the failure as a file that does
+ * not exist, unless it disconnects before that poll.
  */
 class RemoteFileConsumerSendNoopTest extends CamelTestSupport {
 
@@ -52,13 +55,13 @@ class RemoteFileConsumerSendNoopTest extends 
CamelTestSupport {
             RemoteFileOperations<SftpRemoteFile> sftpOperations = 
mock(RemoteFileOperations.class);
             operations = sftpOperations;
             return new SftpConsumer(sftpEndpoint, exchange -> {
-            }, sftpOperations, null);
+            }, sftpOperations, mock(GenericFileProcessStrategy.class));
         }
         // the ftp consumer (also used by ftps) needs the ftp specific 
operations
         FtpOperations ftpOperations = mock(FtpOperations.class);
         operations = ftpOperations;
         return new FtpConsumer((RemoteFileEndpoint<FTPFile>) endpoint, 
exchange -> {
-        }, ftpOperations, null);
+        }, ftpOperations, mock(GenericFileProcessStrategy.class));
     }
 
     /**
@@ -138,6 +141,47 @@ class RemoteFileConsumerSendNoopTest extends 
CamelTestSupport {
         verify(operations, times(1)).connect(any(), any());
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = { "ftp", "ftps", "sftp" })
+    void testSendNoopFalseSendsNoopAfterPollFailure(String scheme) {
+        RemoteFileConsumer<?> consumer = createLoggedInConsumer(scheme + 
"://localhost/inbox?sendNoop=false");
+        // the client still regards itself as connected, as an ftp client does 
after the server closed the connection
+        when(operations.isConnected()).thenReturn(true);
+        when(operations.sendNoop()).thenReturn(false);
+        // so the first command of the poll fails
+        when(operations.getCurrentDirectory()).thenThrow(
+                new GenericFileOperationFailedException(
+                        "Cannot get current directory", new 
SocketException("Connection reset")));
+
+        // the failure is thrown out of the poll, and a custom poll strategy 
does not disconnect
+        assertThrows(GenericFileOperationFailedException.class, () -> 
consumer.poll(null));
+        verify(operations, never()).sendNoop();
+        verify(operations, never()).forceDisconnect();
+
+        // the next poll sends a noop, which fails, so it connects again
+        consumer.connectIfNecessary();
+
+        verify(operations, times(1)).sendNoop();
+        verify(operations, times(1)).connect(any(), any());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "ftp", "ftps", "sftp" })
+    void testSendNoopFalseSendsNoNoopAfterOtherPollFailure(String scheme) {
+        RemoteFileConsumer<?> consumer = createLoggedInConsumer(scheme + 
"://localhost/inbox?sendNoop=false");
+        when(operations.isConnected()).thenReturn(true);
+        when(operations.getCurrentDirectory())
+                .thenThrow(new GenericFileOperationFailedException("Cannot get 
current directory"));
+
+        assertThrows(GenericFileOperationFailedException.class, () -> 
consumer.poll(null));
+
+        consumer.connectIfNecessary();
+
+        verify(operations, times(2)).isConnected();
+        verify(operations, never()).sendNoop();
+        verify(operations, never()).connect(any(), any());
+    }
+
     @ParameterizedTest
     @ValueSource(strings = { "ftp", "ftps", "sftp" })
     void testSendNoopFalseKeepsConnectionWhenNoopAfterIOFailureSucceeds(String 
scheme) {
diff --git 
a/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/integration/FtpConsumerSendNoopFalseIT.java
 
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/integration/FtpConsumerSendNoopFalseIT.java
index 3a30777c49d2..7403476167bb 100644
--- 
a/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/integration/FtpConsumerSendNoopFalseIT.java
+++ 
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/integration/FtpConsumerSendNoopFalseIT.java
@@ -21,10 +21,12 @@ import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import org.apache.camel.BindToRegistry;
 import org.apache.camel.Exchange;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.file.remote.BaseServerTestSupport;
 import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.support.DefaultPollingConsumerPollStrategy;
 import org.apache.camel.test.infra.ftp.services.embedded.EmbeddedConfiguration;
 import org.apache.camel.test.infra.ftp.services.embedded.FtpEmbeddedService;
 import org.apache.ftpserver.FtpServerFactory;
@@ -43,13 +45,18 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 /**
  * The consumer counterpart of {@link FtpProducerSendNoopFalseIT}: with 
sendNoop=false the consumer does not send a NOOP
  * command before each poll, and stays logged in between polls, and it 
connects again after the server has closed the
- * connection.
+ * connection, also with a poll strategy that does not disconnect.
  */
 public class FtpConsumerSendNoopFalseIT extends BaseServerTestSupport {
 
     private final AtomicInteger noops = new AtomicInteger();
     private final AtomicInteger logins = new AtomicInteger();
     private final AtomicInteger retrieves = new AtomicInteger();
+    private final AtomicInteger pwds = new AtomicInteger();
+
+    // a poll strategy that does not disconnect when a poll fails, unlike the 
default one of the ftp consumer
+    @BindToRegistry("myPollStrategy")
+    private final DefaultPollingConsumerPollStrategy pollStrategy = new 
DefaultPollingConsumerPollStrategy();
 
     @RegisterExtension
     public FtpEmbeddedService service = new FtpEmbeddedService() {
@@ -67,6 +74,8 @@ public class FtpConsumerSendNoopFalseIT extends 
BaseServerTestSupport {
                         logins.incrementAndGet();
                     } else if ("RETR".equalsIgnoreCase(request.getCommand())) {
                         retrieves.incrementAndGet();
+                    } else if ("PWD".equalsIgnoreCase(request.getCommand())) {
+                        pwds.incrementAndGet();
                     }
                     return super.beforeCommand(session, request);
                 }
@@ -143,6 +152,36 @@ public class FtpConsumerSendNoopFalseIT extends 
BaseServerTestSupport {
         assertEquals(2, logins.get(), "The consumer should connect again after 
the server closed the connection");
     }
 
+    @Test
+    public void 
testSendNoopFalseConnectsAgainAfterServerClosedConnectionWithCustomPollStrategy()
 throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedBodiesReceived("one");
+
+        template.sendBodyAndHeader("file://" + service.getFtpRootDir() + 
"/pollstrategy", "one", Exchange.FILE_NAME,
+                "hello.txt");
+        context.getRouteController().startRoute("recoverPollStrategy");
+
+        MockEndpoint.assertIsSatisfied(context);
+        Awaitility.await().atMost(5, TimeUnit.SECONDS)
+                .until(() -> 
!service.ftpFile("pollstrategy/hello.txt").toFile().exists());
+        // and the consumer has polled again, as each poll starts with a PWD 
command (stepwise=true)
+        int pwdsAfterDelete = pwds.get();
+        Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() -> pwds.get() 
> pwdsAfterDelete + 1);
+
+        // the server closes the connection between polls. The client does not 
notice, so the first command of the next
+        // poll fails, and the failure is thrown out of the poll to a poll 
strategy that does not disconnect
+        service.disconnectAllSessions();
+
+        mock.reset();
+        mock.expectedBodiesReceived("two");
+
+        template.sendBodyAndHeader("file://" + service.getFtpRootDir() + 
"/pollstrategy", "two", Exchange.FILE_NAME,
+                "hello.txt");
+
+        MockEndpoint.assertIsSatisfied(context);
+        assertEquals(2, logins.get(), "The consumer should connect again after 
the server closed the connection");
+    }
+
     private void consumeFileAndPollAgain(String routeId) throws Exception {
         template.sendBodyAndHeader("file://" + service.getFtpRootDir() + 
"/sendnoop", "Hello World", Exchange.FILE_NAME,
                 "hello.txt");
@@ -174,6 +213,11 @@ public class FtpConsumerSendNoopFalseIT extends 
BaseServerTestSupport {
                 from(getRecoverUrl() + 
"&ignoreFileNotFoundOrPermissionError=true")
                         
.routeId("recoverIgnoreFileNotFound").autoStartup(false)
                         .convertBodyTo(String.class).to("mock:result");
+
+                
from("ftp://admin@localhost:{{ftp.server.port}}/pollstrategy?password=admin&delete=true&delay=100";
+                     + "&sendNoop=false&pollStrategy=#myPollStrategy")
+                        .routeId("recoverPollStrategy").autoStartup(false)
+                        .convertBodyTo(String.class).to("mock:result");
             }
         };
     }
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 94e1f7e5073f..b992efb54f7e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -2274,4 +2274,7 @@ consumer still sends a noop before it renames or deletes 
a file on the server, a
 unless `noop=true`.
 
 For sftp and mina-sftp the client notices a closed connection, but not one 
that dropped silently. For sftp the noop is
-a keep-alive message that does not wait for a reply, so it does not find such 
a connection either.
+a keep-alive message that does not wait for a reply, so it does not find such 
a connection either. For mina-sftp the
+noop is a request that waits for the reply, so with `sendNoop=false` such a 
connection is found by the next poll,
+which fails and is logged before the consumer connects again. For sftp and 
mina-sftp, set `serverAliveInterval` to let
+the client close such a connection itself, so that the consumer connects again 
before the poll.

Reply via email to