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 7836b5102c1f CAMEL-25960: camel-file - keep the done file of a poll
for its last file that completes asynchronously (#29290)
7836b5102c1f is described below
commit 7836b5102c1fdc12c56e7a51e187a8a2d9e811cc
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Nov 6 12:50:38 2026 +0530
CAMEL-25960: camel-file - keep the done file of a poll for its last file
that completes asynchronously (#29290)
Each file now holds a view of the kept done files of its own poll, so the
last file of a poll that completes asynchronously no longer deletes (or keeps)
the done file depending on when the next poll starts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/component/file/GenericFileConsumer.java | 5 +-
.../component/file/GenericFileKeptDoneFiles.java | 53 ++++++--
.../component/file/GenericFileOnCompletion.java | 8 +-
.../file/GenericFileKeptDoneFilesTest.java | 22 ++++
.../FileConsumerDoneFileNameAsyncLastFileTest.java | 134 +++++++++++++++++++++
5 files changed, 209 insertions(+), 13 deletions(-)
diff --git
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileConsumer.java
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileConsumer.java
index 72827db5a9bb..c36d7e223ec4 100644
---
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileConsumer.java
+++
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileConsumer.java
@@ -604,7 +604,10 @@ public abstract class GenericFileConsumer<T> extends
ScheduledBatchPollingConsum
// (for instance to move the file after we have processed it)
GenericFileOnCompletion<T> onCompletion
= new GenericFileOnCompletion<>(endpoint, operations,
processStrategy, target, absoluteFileName);
- onCompletion.setKeptDoneFiles(getKeptDoneFiles());
+ GenericFileKeptDoneFiles kept = getKeptDoneFiles();
+ if (kept != null) {
+ onCompletion.setKeptDoneFiles(kept.batch());
+ }
exchange.getExchangeExtension().addOnCompletion(onCompletion);
LOG.debug("About to process file: {} using exchange: {}", target,
exchange);
diff --git
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileKeptDoneFiles.java
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileKeptDoneFiles.java
index e9998adddedb..d3820718a525 100644
---
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileKeptDoneFiles.java
+++
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileKeptDoneFiles.java
@@ -31,18 +31,25 @@ import java.util.Set;
* file that was taken and failed is not left: as before, the done file is
deleted after the last file of the poll
* unless that file failed.
* <p/>
- * The state is in memory and cleared when the consumer stops.
+ * The files of a poll can complete on another thread while the next poll runs
(the route continues asynchronously), so
+ * the last file of a poll checks the done files kept by its own poll as well
as those kept by the current poll (see
+ * {@link Batch}).
+ * <p/>
+ * The state is in memory, and the done files kept by the current poll are
forgotten when the consumer stops.
*/
final class GenericFileKeptDoneFiles {
- private final Set<String> kept = new HashSet<>();
+ // the done files kept by the current poll: a complete listing starts a
new set, and the set of the previous poll
+ // stays with the files of that poll that have not completed yet
+ private Set<String> kept = new HashSet<>();
/**
* Called once the directory is listed, before the files are consumed.
When the listing was complete, forgets the
- * done files kept by the last poll, as the files of this poll decide
again whether they are kept. When the listing
- * was cut (it stopped at the limit), the kept done files are left as they
are: a done file not listed is still
- * kept, as its files may be among those not listed, and the caller then
keeps the done file of every listed file,
- * as the poll leaves files for the next one.
+ * done files kept by the last poll, as the files of this poll decide
again whether they are kept; the files of the
+ * last poll that have not completed yet still find them kept (see {@link
#batch()}). When the listing was cut (it
+ * stopped at the limit), the kept done files are left as they are: a done
file not listed is still kept, as its
+ * files may be among those not listed, and the caller then keeps the done
file of every listed file, as the poll
+ * leaves files for the next one.
*
* @param listedDoneFiles the done files of the files this poll found
* @param listingComplete whether every file was listed
@@ -60,7 +67,7 @@ final class GenericFileKeptDoneFiles {
answer.add(doneFileName);
}
}
- kept.clear();
+ kept = new HashSet<>();
}
return answer;
}
@@ -73,13 +80,43 @@ final class GenericFileKeptDoneFiles {
}
/**
- * Whether the done file must be kept after the last file of the poll.
+ * Whether the current poll keeps the done file (the files of a poll check
it through {@link #batch()}).
*/
synchronized boolean isKept(String doneFileName) {
return kept.contains(doneFileName);
}
+ /**
+ * The kept done files as the files of the current poll see them, to take
when a file of this poll is started.
+ */
+ synchronized Batch batch() {
+ return new Batch(kept);
+ }
+
synchronized void clear() {
kept.clear();
}
+
+ /**
+ * The kept done files as the files of one poll see them.
+ */
+ final class Batch {
+
+ private final Set<String> keptByBatch;
+
+ private Batch(Set<String> keptByBatch) {
+ this.keptByBatch = keptByBatch;
+ }
+
+ /**
+ * Whether the done file must be kept after the last file of this
poll: this poll or the current one left files
+ * of its directory. A file of this poll that completes while the next
poll runs must not delete a done file
+ * that this poll kept before the next poll has decided again whether
it keeps it.
+ */
+ boolean isKept(String doneFileName) {
+ synchronized (GenericFileKeptDoneFiles.this) {
+ return keptByBatch.contains(doneFileName) ||
kept.contains(doneFileName);
+ }
+ }
+ }
}
diff --git
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileOnCompletion.java
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileOnCompletion.java
index bd5529765ac2..82f3cdfd1fd9 100644
---
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileOnCompletion.java
+++
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileOnCompletion.java
@@ -40,7 +40,7 @@ public class GenericFileOnCompletion<T> implements
Synchronization {
private ExceptionHandler exceptionHandler;
private final GenericFile<T> file;
private final String absoluteFileName;
- private GenericFileKeptDoneFiles keptDoneFiles;
+ private GenericFileKeptDoneFiles.Batch keptDoneFiles;
public GenericFileOnCompletion(GenericFileEndpoint<T> endpoint,
GenericFileOperations<T> operations,
GenericFileProcessStrategy processStrategy,
GenericFile<T> file,
@@ -67,10 +67,10 @@ public class GenericFileOnCompletion<T> implements
Synchronization {
}
/**
- * The done files kept for the files the poll left, when the endpoint uses
a done file with a fixed name;
- * <tt>null</tt> otherwise.
+ * The done files kept for the files the poll left, as the files of the
poll of this file see them, when the
+ * endpoint uses a done file with a fixed name; <tt>null</tt> otherwise.
*/
- void setKeptDoneFiles(GenericFileKeptDoneFiles keptDoneFiles) {
+ void setKeptDoneFiles(GenericFileKeptDoneFiles.Batch keptDoneFiles) {
this.keptDoneFiles = keptDoneFiles;
}
diff --git
a/components/camel-file/src/test/java/org/apache/camel/component/file/GenericFileKeptDoneFilesTest.java
b/components/camel-file/src/test/java/org/apache/camel/component/file/GenericFileKeptDoneFilesTest.java
index 37960c00481c..750cc4494dfe 100644
---
a/components/camel-file/src/test/java/org/apache/camel/component/file/GenericFileKeptDoneFilesTest.java
+++
b/components/camel-file/src/test/java/org/apache/camel/component/file/GenericFileKeptDoneFilesTest.java
@@ -55,4 +55,26 @@ class GenericFileKeptDoneFilesTest {
assertEquals(List.of("dir/ready"), keptDoneFiles.startBatch(Set.of(),
true));
assertFalse(keptDoneFiles.isKept("dir/ready"));
}
+
+ @Test
+ void fileOfTheLastPollFindsItsKeptDoneFileWhileTheNextPollDecidesAgain() {
+ GenericFileKeptDoneFiles.Batch lastPoll = keptDoneFiles.batch();
+ keptDoneFiles.keep("dir/ready");
+ assertTrue(lastPoll.isKept("dir/ready"));
+
+ // the next poll lists the directory completely: until it keeps the
done file again for the files it leaves, a
+ // file of the last poll that completes on another thread must still
find the done file kept by its poll
+ assertNull(keptDoneFiles.startBatch(Set.of("dir/ready"), true));
+ assertFalse(keptDoneFiles.isKept("dir/ready"));
+ assertTrue(lastPoll.isKept("dir/ready"));
+
+ // the files of the next poll decide again: it leaves no file, so its
last file deletes the done file
+ GenericFileKeptDoneFiles.Batch nextPoll = keptDoneFiles.batch();
+ assertFalse(nextPoll.isKept("dir/ready"));
+
+ // a done file kept by the next poll is kept for the files of the last
poll as well
+ keptDoneFiles.keep("other/ready");
+ assertTrue(nextPoll.isKept("other/ready"));
+ assertTrue(lastPoll.isKept("other/ready"));
+ }
}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumerDoneFileNameAsyncLastFileTest.java
b/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumerDoneFileNameAsyncLastFileTest.java
new file mode 100644
index 000000000000..91fa9f91dccc
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumerDoneFileNameAsyncLastFileTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.file;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.Consumer;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.NotifyBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.support.DefaultPollingConsumerPollStrategy;
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+/**
+ * The last file of a poll that left files completes on another thread while
the next poll runs. The next poll lists the
+ * directory completely and leaves a file too, but it keeps the done file
again only after it sorted its files. The last
+ * file of the first poll completes in between, and must not delete the done
file, or the file left by the next poll
+ * waits for a new done file.
+ */
+public class FileConsumerDoneFileNameAsyncLastFileTest extends
ContextTestSupport {
+
+ private final AtomicInteger polls = new AtomicInteger();
+ private final AtomicBoolean secondPollSorting = new AtomicBoolean();
+ private final CountDownLatch bStarted = new CountDownLatch(1);
+ private final CountDownLatch releaseB = new CountDownLatch(1);
+ private volatile NotifyBuilder bDone;
+ private volatile Boolean doneFileAfterB;
+
+ @Override
+ protected Registry createCamelRegistry() throws Exception {
+ Registry registry = super.createCamelRegistry();
+ registry.bind("countPolls", new DefaultPollingConsumerPollStrategy() {
+ @Override
+ public boolean begin(Consumer consumer, Endpoint endpoint) {
+ polls.incrementAndGet();
+ return true;
+ }
+ });
+ // sorts by name; the sorter is called after the listing and before
the done file is kept for the files cut by
+ // maxMessagesPerPoll, so in the second poll it lets b.txt, the last
file of the first poll, complete there
+ Comparator<GenericFile<File>> sorter = (f1, f2) -> {
+ if (polls.get() == 2 && secondPollSorting.compareAndSet(false,
true)) {
+ letBComplete();
+ }
+ return f1.getFileName().compareTo(f2.getFileName());
+ };
+ registry.bind("byNameLettingBComplete", sorter);
+ return registry;
+ }
+
+ private void letBComplete() {
+ try {
+ if (bStarted.await(20, TimeUnit.SECONDS)) {
+ releaseB.countDown();
+ if (bDone.matches(20, TimeUnit.SECONDS)) {
+ doneFileAfterB = Files.exists(testFile("async/ready"));
+ }
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Test
+ public void
testLastFileOfPollCompletingWhileNextPollDecidesKeepsDoneFile() throws
Exception {
+ Files.createDirectories(testDirectory("async"));
+ for (String name : List.of("a", "b", "c", "d", "e")) {
+ Files.writeString(testFile("async/" + name + ".txt"),
name.toUpperCase());
+ }
+ Files.writeString(testFile("async/ready"), "");
+ bDone = new
NotifyBuilder(context).whenAnyDoneMatches(header(Exchange.FILE_NAME_ONLY).isEqualTo("b.txt")).create();
+ getMockEndpoint("mock:async").expectedBodiesReceivedInAnyOrder("A",
"B", "C", "D", "E");
+
+ // the first poll takes a.txt and b.txt and leaves c.txt, d.txt and
e.txt; the second takes c.txt and d.txt and
+ // leaves e.txt, so the done file must stay until the third poll
+ context.getRouteController().startRoute("async");
+
+ Awaitility.await().atMost(30, TimeUnit.SECONDS).until(() ->
doneFileAfterB != null);
+ assertEquals(Boolean.TRUE, doneFileAfterB,
+ "The done file must not be deleted by b.txt, as the second
poll leaves e.txt");
+ assertMockEndpointsSatisfied();
+ Awaitility.await().atMost(10, TimeUnit.SECONDS).untilAsserted(
+ () -> assertFalse(Files.exists(testFile("async/ready")), "The
done file must be deleted after the last file"));
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from(fileUri("async?doneFileName=ready&maxMessagesPerPoll=2&eagerMaxMessagesPerPoll=false"
+ +
"&sorter=#byNameLettingBComplete&pollStrategy=#countPolls&initialDelay=0&delay=10"))
+ .routeId("async").autoStartup(false)
+ .threads()
+ .convertBodyTo(String.class)
+ .process(e -> {
+ if ("B".equals(e.getMessage().getBody())) {
+ bStarted.countDown();
+ releaseB.await(20, TimeUnit.SECONDS);
+ }
+ })
+ .to("mock:async");
+ }
+ };
+ }
+}