shashank created CAMEL-25728:
--------------------------------

             Summary: camel-file - a done file with a fixed name is deleted by 
the last file of a poll although the poll left files of the directory 
(maxMessagesPerPoll, not started), so they are left until a new done file is 
written
                 Key: CAMEL-25728
                 URL: https://issues.apache.org/jira/browse/CAMEL-25728
             Project: Camel
          Issue Type: Bug
          Components: camel-file
            Reporter: shashank
            Assignee: shashank


With a done file of a fixed name ({{doneFileName=ready}}), the file consumer 
only picks up the files of a directory while the done file exists, and deletes 
the done file when the last file of the batch ({{CamelBatchComplete}}) is 
committed ({{GenericFileOnCompletion.handleDoneFile}}, main 55ed20f14e90 line 
189; CAMEL-5883). The batch is the poll, not the directory. When the poll does 
not take some files of the directory, the done file is still deleted, and these 
files are left in the directory until a new done file is written, without any 
log:

* {{maxMessagesPerPoll}}: the poll takes N files; the last of them deletes the 
done file; the other files stay;
* a file whose processing cannot start (read lock not acquired, begin or 
download failed) is skipped by the poll, and a later file of the same poll 
deletes the done file.

Trigger: {{file:in?doneFileName=ready&maxMessagesPerPoll=2}} with three files 
and {{ready}}: two files are routed, the third stays in {{in}} until the 
producer writes {{ready}} again. A done file with a fixed name is the 
documented way to say that "all files of this directory are ready" ("Camel will 
automatically delete the done file when it's done consuming the files"), and a 
producer usually writes the next done file only with the next batch. The same 
base classes are used by camel-ftp (ftp, ftps, sftp), camel-smb and 
camel-azure-files.

A file that was processed and failed is not part of this issue: it is rolled 
back and waits for the next done file (or is retried under the same done file 
when it was the last file of the poll), and {{moveFailed}} or redelivery are 
the tools for it. The fix keeps that behaviour.

h3. Reproduction

New test {{FileConsumerDoneFileNameFilesLeftTest}} (11 tests). Without the fix 
four fail:
* {{maxMessagesPerPoll=2}}, files a, b, c and {{ready}}: {{mock://limit 
Received message count. Expected: <3> but was: <2>}};
* the same with {{eagerMaxMessagesPerPoll=false}}: {{mock://noteager ... 
Expected: <3> but was: <2>}};
* the read lock of b not acquired the first time (a custom 
{{exclusiveReadLockStrategy}}): {{mock://notstarted ... Expected: <3> but was: 
<2>}};
* {{maxMessagesPerPoll=1}}, files a, b, c and {{ready}}, a file x written while 
the first file is routed: {{mock://gate ... Expected: <3> but was: <1>}} (x 
must wait for a new done file, a, b and c must not).
The other seven pass on main as well and pin the behaviour that must not 
change: exactly {{maxMessagesPerPoll}} files and {{ready}} deleted; the same 
with a file written meanwhile, not consumed; a file failing once waits for the 
next done file; a last file failing once keeps the done file and is retried 
with the file written meanwhile (as on main); a file that always fails does not 
hold the done file, and the next batch (d and {{ready}} again) is consumed, 
also with {{maxMessagesPerPoll}}; a dynamic {{$\{file:name\}.done}} unchanged. 
A test that expects a file not to be consumed checks it once the done file is 
gone (the routes are synchronous), so it does not depend on timing.

A TLA+ model (one directory: files, done file, limit per poll, files failing 
once or always, files not started once, a producer that writes files of a next 
batch at any time and announces them with a new done file, consumer restart) 
checks with the fix: no file consumed unless a done file was created after it 
(except after a rolled back last file, where main does the same), every left 
file eventually consumed, no orphan done file, and every later file eventually 
consumed (no permanent block by a failing file). On main a left file is 
stranded. A previous version of this fix, which kept a failed file in the set, 
blocked the directory behind a file that always fails; the model shows that 
too. Two limits show up as expected (below): a restart while the done file is 
kept, and a producer that rewrites a done file that is still there. Not 
modelled: asynchronous routing, recursive directories, other consumers.

h3. Proposed fix

The poll remembers the files it did not take (beyond {{maxMessagesPerPoll}}, or 
not started), per done file, and the last file of the batch does not delete the 
done file while there are any. The next poll only consumes these files, so a 
file written to the directory meanwhile waits for a new done file as on main; 
the done file is then deleted after its last file as before, or by the next 
poll if the files left are gone. A file that was taken and failed is not left 
(released), so it cannot hold the done file. With exactly 
{{maxMessagesPerPoll}} files nothing is left and nothing changes.

To know the files beyond the limit, such a consumer checks every file of the 
directory also with {{eagerMaxMessagesPerPoll=true}} (the default), and then 
cuts the list to the same files the eager limit takes (no Exchange for the 
others). Only for a fixed {{doneFileName}} with {{maxMessagesPerPoll}}: each 
poll then costs what {{eagerMaxMessagesPerPoll=false}} costs for the listing 
(20 000 files and {{ready}}: about 0.5 s per poll instead of 0.1 s, as long as 
files are left; with {{recursive=true}} every sub-directory is listed too, one 
more listing each on a remote server). The state is per consumer, bounded by 
the files left, and cleared on stop. After a restart a kept done file admits 
the files present at the first poll, as main does with any done file it finds. 
A producer must not rewrite a done file that is still there to announce new 
files. Dynamic done files ({{$\{file:name\}.done}}), {{noop=true}} and 
{{recursive=true}} (only the done file of the last file's directory is deleted 
per poll) are unchanged.

Not changed: with asynchronous routing a poll can still run while the last file 
is completing (the same window as on main). A file that can never be started 
(for example behind a stale lock file) keeps the done file until it is gone or 
can be started.

Affected: main (tested); camel-4.23.x and camel-4.22.x have the same lines 
(169/189), camel-4.18.x and camel-4.14.x (173/193) (read, not run).

Duplicate check (2026-10-17): JIRA text "done file" with "maxMessagesPerPoll" 
(0), with "rollback"/"retry" (CAMEL-6964, moveFailed only), "doneFileName" (16, 
none about files left); GitHub pull requests "doneFileName": none open or 
related.

_Filed with Claude Code on behalf of allthingssecurity._




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

Reply via email to