shashank created CAMEL-25016:
--------------------------------

             Summary: Remote file consumers (FTP, FTPS, SFTP, SMB, Azure 
Files): with noop=true or idempotent=true, a file whose download fails is never 
retried
                 Key: CAMEL-25016
                 URL: https://issues.apache.org/jira/browse/CAMEL-25016
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: shashank


In {{GenericFileConsumer.processExchange}} 
({{GenericFileConsumer.java:469-513}}) a file is retrieved after 
{{processStrategy.begin(...)}} ({{:426}}) succeeded. When the retrieve fails, 
{{tryRetrievingFile}} ({{:516-556}}) throws, and the catch block at 
{{:499-511}} only removes the file from the in-progress repository, calls the 
exception handler and returns {{true}}. So {{processBatch}} counts the file as 
started, and {{GenericFileOnCompletion}}, which is only registered after a 
successful retrieve ({{:482}}), never runs.

With {{idempotent=true}} and the default {{idempotentEager=true}}, which 
includes every {{noop=true}} endpoint, the idempotent key is added while 
polling ({{isValidFile}} -> {{notUnique}}, {{:669-689}}). Nothing removes it 
after the failed retrieve: {{processBatch}} only removes the key of the files 
for which {{processExchange}} returned {{false}} ({{:256-266}}, added by 
CAMEL-21947 for the read-lock-not-acquired case). The next polls skip the file 
because its key is present, so it is never retried, and only a WARN "Cannot 
retrieve file ..." is logged once. With the default memory repository this 
lasts until the application is restarted. With a persistent idempotent 
repository (JDBC, Infinispan, Hazelcast, ...) the file is never consumed again, 
not even after a restart.

This happens on the remote file components, whose downloads fail on ordinary 
transient errors: FTP and FTPS (socket timeout, connection reset, {{426 
Transfer aborted}}), SFTP, SMB and Azure Files. The local file component is not 
affected in practice, because {{FileOperations.retrieveFile}} always returns 
true ({{FileOperations.java:247-251}}).

With {{preMove}}, {{GenericFileRenameProcessStrategy.begin}} binds the pre 
moved file to the exchange ({{FILE_EXCHANGE_FILE}}), while the eager key was 
added for the original path, so removing the key of the exchange file would 
remove the wrong key.

The same catch block also skips {{processStrategy.abort}}, so the exclusive 
read lock taken by {{begin}} is not released on a failed retrieve. For the 
built-in read locks of the remote components ({{none}}, {{changed}}, 
{{rename}}) that has no lasting effect, but a custom 
{{exclusiveReadLockStrategy}} bean would keep its lock.

*Reproduction*: the real {{FileConsumer}} / {{GenericFileConsumer}} / process 
strategies, with a {{FileOperations}} subclass (plugged in through the 
protected {{FileEndpoint.newFileConsumer}}) whose {{retrieveFile}} throws 
{{GenericFileOperationFailedException("Connection reset")}} once, as an FTP 
download failure does. {{FileConsumer}} shares {{GenericFileConsumer}} with the 
remote consumers, so this exercises the same code path. One file, poll delay 
100 ms, result after 3 s:
{noformat}
noop=true                         no failure: processed=1 | 1 failure: 
processed=0 retrieveCalls=1 fileStillInDir=true keyInRepository=true
idempotent=true (default move)    no failure: processed=1 | 1 failure: 
processed=0 retrieveCalls=1 fileStillInDir=true keyInRepository=true
{noformat}
Expected: the next poll retrieves and processes the file. Actual: the file is 
never retried.

A TLA+ model of the consumer (list, begin, retrieve, commit/rollback) violates 
"a listed file is eventually processed" in 4 states (List -> Begin -> 
RetrieveFail); with the fix below it holds.

*Proposed fix:* handle a failed or ignored retrieve like a failed begin: call 
{{processStrategy.abort}} (releases the read lock, deletes a partial local work 
file, releases the retrieved resources), remove the in-progress entry and the 
eager idempotent key of the original file (by {{absoluteFileName}} or the 
{{FILE_IDEMPOTENT_KEY}} snapshot, as 
{{GenericFileOnCompletion.processStrategyRollback}} does, so it also works with 
{{preMove}}), and return {{false}}, then report the exception as today.

Behaviour change: a poll in which no file could be downloaded now counts as 
idle ({{backoffIdleThreshold}}, {{sendEmptyMessageWhenIdle}}, {{greedy}}), the 
same as a poll in which no read lock was acquired.

_Filed with Claude Code on behalf of allthingssecurity._




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to