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

davsclaus pushed a commit to branch fix/CAMEL-25041
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 43d6c03cbc3530b0857ae5f8ac4e8f973285df2c
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Sep 26 15:51:04 2026 +0200

    CAMEL-25041: find changed files for a reload by scanning the folder, not 
with the JDK WatchService
    
    The reload strategy needs the files of one save together: a route is built 
with
    the properties of the moment, so a route and the property it uses must be
    reloaded together. The watch service reports one file at a time with no 
batch
    boundary, which had to be reconstructed by collecting a key's events plus
    whatever arrived in the next polls. A scan returns exactly the files that
    changed since the last one, so the batch comes for free.
    
    It also removes the macOS penalty. Java has no native file notification 
there,
    so the watch service falls back to polling; Camel applied
    com.sun.nio.file.SensitivityWatchEventModifier.HIGH to get that down to 2s, 
but
    on a current JDK the class resolves, HIGH is applied, and the interval is 
still
    about 10s. Measured on JDK 25, the gaps between reloads were 8-10s. A scan 
at
    its own interval is within a second on every platform, and the dependency 
on a
    com.sun.nio.file class that was never standard API is gone.
    
    What the watch service needed and a scan does not: registering each 
directory,
    registering one created while running (the workaround of CAMEL-24862, so
    registerNewDirectory goes), and depending on ENTRY_DELETE for a file that is
    gone, which is simply an entry the next scan does not find. A scan also 
cannot
    drop changes to OVERFLOW.
    
    setPollTimeout is now the scan interval, default 1000 rather than 2000. A 
new
    setStableTimeout (default 200) leaves a file modified within it for the next
    scan, so a save still being written is not reloaded half-finished; a
    modification time in the future is reported rather than waited out.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---
 .../apache/camel/support/FileScanReloadTest.java   | 167 ++++++++++++
 .../support/FileWatcherResourceReloadStrategy.java | 299 +++++++--------------
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  14 +
 3 files changed, 285 insertions(+), 195 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/support/FileScanReloadTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/support/FileScanReloadTest.java
new file mode 100644
index 000000000000..98f2f75d3b22
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/support/FileScanReloadTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.support;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.List;
+
+import org.apache.camel.ContextTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-25041: the folder is scanned and each file's modification time and 
length compared with the previous scan, so
+ * one save of several files is one set of changed files - which is what a 
reload needs, since a route and a property
+ * saved together must be reloaded together.
+ */
+public class FileScanReloadTest extends ContextTestSupport {
+
+    private Path dir;
+    private FileWatcherResourceReloadStrategy strategy;
+
+    @BeforeEach
+    void createFolder() throws Exception {
+        dir = Files.createTempDirectory("camel-file-scan");
+        strategy = new FileWatcherResourceReloadStrategy(dir.toString(), true);
+        strategy.setCamelContext(context);
+        strategy.setFileFilter(f -> f.getName().endsWith(".yaml") || 
f.getName().endsWith(".properties"));
+        // the test writes and scans at once, so no quiet period
+        strategy.setStableTimeout(0);
+    }
+
+    @AfterEach
+    void removeFolder() throws Exception {
+        deleteRecursively(dir);
+    }
+
+    private List<String> scanNames() {
+        return strategy.scan().stream().map(File::getName).sorted().toList();
+    }
+
+    @Test
+    public void testOneSaveOfSeveralFilesIsOneSetOfChanges() throws Exception {
+        Files.writeString(dir.resolve("shop.yaml"), "one");
+        Files.writeString(dir.resolve("application.properties"), 
"shop.name=Camel Shop");
+
+        // the first scan is the starting point: both files are new to it
+        assertThat(scanNames()).containsExactly("application.properties", 
"shop.yaml");
+        // nothing changed since
+        assertThat(scanNames()).isEmpty();
+
+        // the save: a route and the property it uses, together
+        Files.writeString(dir.resolve("shop.yaml"), "two");
+        Files.writeString(dir.resolve("application.properties"), 
"shop.name=Camel Shop\nshop.currency=EUR");
+
+        // one scan, both files: the reload can apply the property before it 
builds the route
+        assertThat(scanNames()).containsExactly("application.properties", 
"shop.yaml");
+        assertThat(scanNames()).isEmpty();
+    }
+
+    @Test
+    public void testAFileWrittenWithTheSameLengthIsStillAChange() throws 
Exception {
+        Path f = dir.resolve("shop.yaml");
+        Files.writeString(f, "one");
+        scanNames();
+
+        // rewritten with the same length, so only the modification time tells 
them apart
+        Files.setLastModifiedTime(f, 
java.nio.file.attribute.FileTime.fromMillis(System.currentTimeMillis() - 5000));
+        assertThat(scanNames()).containsExactly("shop.yaml");
+    }
+
+    @Test
+    public void testADeletedFileIsAChange() throws Exception {
+        Files.writeString(dir.resolve("shop.yaml"), "one");
+        Files.writeString(dir.resolve("other.yaml"), "one");
+        scanNames();
+
+        Files.delete(dir.resolve("other.yaml"));
+        assertThat(scanNames()).containsExactly("other.yaml");
+        // and only once
+        assertThat(scanNames()).isEmpty();
+    }
+
+    @Test
+    public void testFilesTheFilterRejectsAreNotChanges() throws Exception {
+        Files.writeString(dir.resolve("shop.yaml"), "one");
+        Files.writeString(dir.resolve("notes.txt"), "not mine");
+        assertThat(scanNames()).containsExactly("shop.yaml");
+
+        Files.writeString(dir.resolve("notes.txt"), "still not mine");
+        assertThat(scanNames()).isEmpty();
+    }
+
+    @Test
+    public void testASubdirectoryIsFoundWithoutRegisteringIt() throws 
Exception {
+        // a tree created while running: the watch service only reported what 
was registered when the event happened,
+        // which needed a workaround of its own (CAMEL-24862); a scan walks 
the tree every time
+        Path sub = dir.resolve("src/main/java/com/acme");
+        Files.createDirectories(sub);
+        Files.writeString(sub.resolve("Bean.yaml"), "one");
+        assertThat(scanNames()).containsExactly("Bean.yaml");
+    }
+
+    @Test
+    public void 
testAModificationTimeInTheFutureIsReportedRatherThanWaitedOut() throws 
Exception {
+        // a clock askew on a network share, or a touch -t: waiting for it to 
settle would mean waiting for the clock
+        strategy.setStableTimeout(60_000);
+        Path f = dir.resolve("shop.yaml");
+        Files.writeString(f, "one");
+        Files.setLastModifiedTime(f, 
java.nio.file.attribute.FileTime.fromMillis(System.currentTimeMillis() + 
60_000));
+        assertThat(scanNames()).containsExactly("shop.yaml");
+    }
+
+    @Test
+    public void testASaveStillBeingWrittenIsLeftForTheNextScan() throws 
Exception {
+        strategy.setStableTimeout(60_000);
+        Files.writeString(dir.resolve("shop.yaml"), "one");
+
+        // just written, so not reported yet
+        assertThat(scanNames()).isEmpty();
+
+        // once it has settled it is
+        strategy.setStableTimeout(0);
+        assertThat(scanNames()).containsExactly("shop.yaml");
+    }
+
+    private static void deleteRecursively(Path path) throws IOException {
+        if (path == null || !Files.exists(path)) {
+            return;
+        }
+        Files.walkFileTree(path, new SimpleFileVisitor<>() {
+            @Override
+            public FileVisitResult visitFile(Path file, BasicFileAttributes 
attrs) throws IOException {
+                Files.delete(file);
+                return FileVisitResult.CONTINUE;
+            }
+
+            @Override
+            public FileVisitResult postVisitDirectory(Path d, IOException exc) 
throws IOException {
+                Files.delete(d);
+                return FileVisitResult.CONTINUE;
+            }
+        });
+    }
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
index 2882fafc1260..59945b15365c 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
@@ -23,66 +23,58 @@ import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.SimpleFileVisitor;
-import java.nio.file.WatchEvent;
-import java.nio.file.WatchKey;
-import java.nio.file.WatchService;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
 
-import org.apache.camel.RuntimeCamelException;
 import org.apache.camel.api.management.ManagedAttribute;
 import org.apache.camel.api.management.ManagedResource;
 import org.apache.camel.spi.CompileStrategy;
 import org.apache.camel.spi.Resource;
 import org.apache.camel.spi.ResourceReload;
 import org.apache.camel.util.FileUtil;
-import org.apache.camel.util.IOHelper;
-import org.apache.camel.util.ObjectHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
-import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
-import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
-
 /**
  * A file based {@link org.apache.camel.spi.ResourceReloadStrategy} which 
watches a file folder for modified files and
  * reload on file changes.
  * <p/>
- * This implementation uses the JDK {@link WatchService} to watch for when 
files are created or modified. Mac OS X users
- * should be noted the osx JDK does not support native file system changes and 
therefore the watch service is much
- * slower than on Linux or Windows systems.
+ * The folder is scanned on an interval and each file's modification time and 
size compared with the previous scan, the
+ * way the file component finds changed files. A scan therefore reports all 
the files of one save together, which is
+ * what a reload needs: a route is built with the properties of the moment, so 
a route and a property saved together
+ * must be reloaded together (CAMEL-25032). The JDK {@link 
java.nio.file.WatchService} reports one file at a time with
+ * no batch boundary, and on macOS has no native backend at all, where it fell 
back to a poll of about ten seconds
+ * (CAMEL-25041).
  */
 @ManagedResource(description = "Managed FileWatcherResourceReloadStrategy")
 public class FileWatcherResourceReloadStrategy extends 
ResourceReloadStrategySupport {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(FileWatcherResourceReloadStrategy.class);
 
-    WatchService watcher;
     ExecutorService executorService;
     WatchFileChangesTask task;
-    Map<WatchKey, Path> folderKeys;
-    Set<Path> watchedFolders;
-    WatchEvent.Modifier watchModifier;
+    /** The modification time and length of every file of the previous scan, 
by path: what a change is measured from. */
+    final Map<String, long[]> known = new HashMap<>();
     /** The compile work directory, resolved once at start (null when there is 
none). */
     Path compileWorkDir;
     FileFilter fileFilter;
     String folder;
     boolean isRecursive;
     boolean scheduler = true;
-    long pollTimeout = 2000;
-    /** How long a poll that came back empty ends the collecting of one 
change, and the bound on collecting. */
-    static final long COALESCE_QUIET = 400;
-    static final long COALESCE_MAX = 2000;
+    long pollTimeout = 1000;
+    /**
+     * A file modified less than this ago is left for the next scan: a save 
still being written would otherwise be
+     * reloaded half-finished. The file component leaves a file alone the same 
way.
+     */
+    long stableTimeout = 200;
 
     public FileWatcherResourceReloadStrategy() {
         setRecursive(false);
@@ -115,12 +107,20 @@ public class FileWatcherResourceReloadStrategy extends 
ResourceReloadStrategySup
     }
 
     /**
-     * Sets the poll timeout in millis. The default value is 2000.
+     * Sets how often the folder is scanned for changed files, in millis. The 
default value is 1000.
      */
     public void setPollTimeout(long pollTimeout) {
         this.pollTimeout = pollTimeout;
     }
 
+    /**
+     * Sets how long a file must have been unchanged before a scan reports it, 
in millis. The default value is 200, so
+     * that a save still being written is reloaded once it is complete and not 
half-finished.
+     */
+    public void setStableTimeout(long stableTimeout) {
+        this.stableTimeout = stableTimeout;
+    }
+
     @ManagedAttribute(description = "Folder being watched")
     public String getFolder() {
         return folder;
@@ -172,54 +172,16 @@ public class FileWatcherResourceReloadStrategy extends 
ResourceReloadStrategySup
                 LOG.info(msg);
             }
 
-            WatchEvent.Modifier modifier = null;
-
-            // if its mac OSX then attempt to apply workaround or warn its 
slower
-            String os = ObjectHelper.getSystemProperty("os.name", "");
-            if (os.toLowerCase(Locale.US).startsWith("mac")) {
-                // this modifier can speedup the scanner on mac osx (as java 
on mac has no native file notification integration)
-                Class<WatchEvent.Modifier> clazz = 
getCamelContext().getClassResolver()
-                        
.resolveClass("com.sun.nio.file.SensitivityWatchEventModifier", 
WatchEvent.Modifier.class);
-                if (clazz != null) {
-                    WatchEvent.Modifier[] modifiers = clazz.getEnumConstants();
-                    for (WatchEvent.Modifier mod : modifiers) {
-                        if ("HIGH".equals(mod.name())) {
-                            modifier = mod;
-                            break;
-                        }
-                    }
-                }
-                if (modifier != null) {
-                    LOG.debug(
-                            "On Mac OS X the JDK WatchService is slow by 
default so enabling SensitivityWatchEventModifier.HIGH as workaround");
-                } else {
-                    LOG.warn(
-                            "On Mac OS X the JDK WatchService is slow and it 
may take up till 10 seconds to notice file changes");
-                }
-            }
-
-            try {
-                Path path = dir.toPath();
-                watcher = path.getFileSystem().newWatchService();
-                // we cannot support deleting files as we don't know which 
routes that would be
-                this.watchModifier = modifier;
-                this.compileWorkDir = resolveCompileWorkDir();
-                if (isRecursive) {
-                    this.folderKeys = new HashMap<>();
-                    this.watchedFolders = new HashSet<>();
-                    registerRecursive(watcher, path, modifier);
-                } else {
-                    registerPathToWatcher(modifier, path, watcher);
-                }
+            this.compileWorkDir = resolveCompileWorkDir();
+            // the files as they are now are the starting point, not a change: 
record them without reloading
+            known.clear();
+            scan();
 
-                task = new WatchFileChangesTask(watcher, path);
+            task = new WatchFileChangesTask(dir.toPath());
 
-                executorService = 
getCamelContext().getExecutorServiceManager().newSingleThreadExecutor(this,
-                        "FileWatcherReloadStrategy");
-                executorService.submit(task);
-            } catch (IOException e) {
-                throw RuntimeCamelException.wrapRuntimeCamelException(e);
-            }
+            executorService = 
getCamelContext().getExecutorServiceManager().newSingleThreadExecutor(this,
+                    "FileWatcherReloadStrategy");
+            executorService.submit(task);
         }
     }
 
@@ -237,61 +199,84 @@ public class FileWatcherResourceReloadStrategy extends 
ResourceReloadStrategySup
         changed.sort(Comparator.comparingInt(f -> 
f.getName().endsWith(".properties") ? 0 : 1));
     }
 
-    private WatchKey registerPathToWatcher(WatchEvent.Modifier modifier, Path 
path, WatchService watcher) throws IOException {
-        WatchKey key;
-        if (modifier != null) {
-            key = path.register(watcher, new WatchEvent.Kind<?>[] { 
ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE }, modifier);
-        } else {
-            key = path.register(watcher, ENTRY_CREATE, ENTRY_MODIFY, 
ENTRY_DELETE);
-        }
-        return key;
-    }
-
-    private void registerRecursive(final WatchService watcher, final Path 
root, final WatchEvent.Modifier modifier)
-            throws IOException {
-        Files.walkFileTree(root, new SimpleFileVisitor<>() {
-            @Override
-            public FileVisitResult preVisitDirectory(Path dir, 
BasicFileAttributes attrs) throws IOException {
-                if (isCompileWorkDir(dir)) {
-                    return FileVisitResult.SKIP_SUBTREE;
-                }
-                WatchKey key = registerPathToWatcher(modifier, dir, watcher);
-                folderKeys.put(key, dir);
-                watchedFolders.add(dir);
-                return FileVisitResult.CONTINUE;
-            }
-        });
-    }
-
     /**
-     * Registers a directory created while watching recursively, and its 
subdirectories, and collects the files already
-     * in them as changes: a tree such as src/main/java/com/acme is usually 
created with its first file in it, before
-     * the watcher can see the directory.
+     * The files whose modification time or length changed since the previous 
scan, plus the files that are gone. The
+     * snapshot is updated as it goes, so a file is reported once per change.
+     * <p/>
+     * A file modified less than {@link #stableTimeout} ago is left for the 
next scan: a save still being written would
+     * otherwise be reloaded half-finished.
      */
-    private void registerNewDirectory(Path dir, List<File> changed) {
+    protected List<File> scan() {
+        List<File> changed = new ArrayList<>();
+        Set<String> seen = new HashSet<>();
+        Path root = new File(folder).toPath();
         try {
-            Files.walkFileTree(dir, new SimpleFileVisitor<>() {
+            Files.walkFileTree(root, new SimpleFileVisitor<>() {
                 @Override
-                public FileVisitResult preVisitDirectory(Path d, 
BasicFileAttributes attrs) throws IOException {
-                    if (isCompileWorkDir(d) || watchedFolders.contains(d)) {
+                public FileVisitResult preVisitDirectory(Path dir, 
BasicFileAttributes attrs) {
+                    if (isCompileWorkDir(dir)) {
+                        // the class files the runtime writes while compiling 
a Java source are not changes of ours,
+                        // and would trigger a reload, which compiles again, 
which writes again (CAMEL-24862)
                         return FileVisitResult.SKIP_SUBTREE;
                     }
-                    WatchKey k = registerPathToWatcher(watchModifier, d, 
watcher);
-                    folderKeys.put(k, d);
-                    watchedFolders.add(d);
-                    LOG.debug("Watching new directory: {}", d);
+                    if (!isRecursive && !dir.equals(root)) {
+                        return FileVisitResult.SKIP_SUBTREE;
+                    }
+                    return FileVisitResult.CONTINUE;
+                }
+
+                @Override
+                public FileVisitResult visitFile(Path file, 
BasicFileAttributes attrs) {
+                    if (!attrs.isRegularFile()) {
+                        return FileVisitResult.CONTINUE;
+                    }
+                    File f = file.toFile();
+                    if (fileFilter != null && !fileFilter.accept(f)) {
+                        return FileVisitResult.CONTINUE;
+                    }
+                    String name = FileUtil.compactPath(f.getPath());
+                    seen.add(name);
+                    long modified = attrs.lastModifiedTime().toMillis();
+                    long length = attrs.size();
+                    long[] previous = known.get(name);
+                    if (previous != null && previous[0] == modified && 
previous[1] == length) {
+                        return FileVisitResult.CONTINUE;
+                    }
+                    long age = System.currentTimeMillis() - modified;
+                    if (age >= 0 && age < stableTimeout) {
+                        // still being written: leave it, and let the next 
scan report it. A modification time in the
+                        // future (a clock askew on a network share, a touch 
-t) is reported now rather than waited out
+                        seen.remove(name);
+                        return FileVisitResult.CONTINUE;
+                    }
+                    known.put(name, new long[] { modified, length });
+                    LOG.debug("Detected Modified/Created file: {}", name);
+                    changed.add(f);
                     return FileVisitResult.CONTINUE;
                 }
 
                 @Override
-                public FileVisitResult visitFile(Path f, BasicFileAttributes 
attrs) {
-                    changed.add(f.toFile());
+                public FileVisitResult visitFileFailed(Path file, IOException 
exc) {
+                    // a file that went away between the walk and reading it 
is a change like any other
                     return FileVisitResult.CONTINUE;
                 }
             });
         } catch (IOException e) {
-            LOG.warn("Cannot watch new directory: {} due to: {}. This 
exception is ignored.", dir, e.getMessage(), e);
+            LOG.warn("Cannot scan directory: {} due to: {}. This exception is 
ignored.", folder, e.getMessage(), e);
+            return changed;
+        }
+        // what the snapshot has and the scan did not see is gone; a file 
still being written was taken out of seen,
+        // so only drop what the scan positively did not find
+        for (Iterator<Map.Entry<String, long[]>> it = 
known.entrySet().iterator(); it.hasNext();) {
+            Map.Entry<String, long[]> entry = it.next();
+            File f = new File(entry.getKey());
+            if (!seen.contains(entry.getKey()) && !f.exists()) {
+                LOG.debug("Detected Deleted file: {}", entry.getKey());
+                it.remove();
+                changed.add(f);
+            }
         }
+        return changed;
     }
 
     /**
@@ -319,9 +304,6 @@ public class FileWatcherResourceReloadStrategy extends 
ResourceReloadStrategySup
             executorService = null;
         }
 
-        if (watcher != null) {
-            IOHelper.close(watcher);
-        }
     }
 
     /**
@@ -329,12 +311,10 @@ public class FileWatcherResourceReloadStrategy extends 
ResourceReloadStrategySup
      */
     protected class WatchFileChangesTask implements Runnable {
 
-        private final WatchService watcher;
         private final Path folder;
         private volatile boolean running;
 
-        public WatchFileChangesTask(WatchService watcher, Path folder) {
-            this.watcher = watcher;
+        public WatchFileChangesTask(Path folder) {
             this.folder = folder;
         }
 
@@ -349,49 +329,19 @@ public class FileWatcherResourceReloadStrategy extends 
ResourceReloadStrategySup
             while (isStarting() || isRunAllowed()) {
                 running = true;
 
-                WatchKey key;
                 try {
-                    LOG.trace("FileReloadStrategy is polling for file changes 
in directory: {}", folder);
-                    // wait for a key to be available
-                    key = watcher.poll(pollTimeout, TimeUnit.MILLISECONDS);
-                } catch (InterruptedException ex) {
-                    LOG.info("Interrupted while polling for file changes");
+                    Thread.sleep(pollTimeout);
+                } catch (InterruptedException e) {
+                    LOG.info("Interrupted while waiting to scan for file 
changes");
                     Thread.currentThread().interrupt();
                     break;
                 }
 
-                if (key != null) {
-                    List<File> changed = new ArrayList<>();
-                    collect(key, changed);
-                    // the key must be reset after processed
-                    boolean valid = key.reset();
-
-                    // one save of several files is one change: an editor 
saving a route and the properties it uses,
-                    // or a tool writing both, may spread them over more than 
one poll. Keep collecting until a poll
-                    // comes back empty (bounded), so they are reloaded 
together and a route is built with the
-                    // properties of the same save already applied 
(CAMEL-25032)
-                    long deadline = System.currentTimeMillis() + COALESCE_MAX;
-                    while (valid && !changed.isEmpty() && 
System.currentTimeMillis() < deadline) {
-                        WatchKey next;
-                        try {
-                            next = watcher.poll(COALESCE_QUIET, 
TimeUnit.MILLISECONDS);
-                        } catch (InterruptedException ex) {
-                            Thread.currentThread().interrupt();
-                            break;
-                        }
-                        if (next == null) {
-                            break;
-                        }
-                        collect(next, changed);
-                        valid = next.reset();
-                    }
-
-                    if (!changed.isEmpty()) {
-                        onReloadBatch(changed);
-                    }
-                    if (!valid) {
-                        break;
-                    }
+                LOG.trace("FileReloadStrategy is scanning for file changes in 
directory: {}", folder);
+                List<File> changed = scan();
+                if (!changed.isEmpty()) {
+                    // the files of one scan are the files of one save: 
reloaded together
+                    onReloadBatch(changed);
                 }
             }
 
@@ -399,47 +349,6 @@ public class FileWatcherResourceReloadStrategy extends 
ResourceReloadStrategySup
 
             LOG.debug("FileReloadStrategy is stopping watching folder: {}", 
folder);
         }
-
-        /**
-         * The accepted files of one key's events, added to changed: the files 
of the events, plus the files of a
-         * directory created under a watched one when recursive (registered 
here, since the watch service only reports
-         * what is registered at the time; a class under src/main/java added 
while running was never seen, CAMEL-24862).
-         */
-        private void collect(WatchKey key, List<File> changed) {
-            Path pathToReload = isRecursive ? folderKeys.get(key) : folder;
-            if (pathToReload == null) {
-                return;
-            }
-            List<File> found = new ArrayList<>();
-            for (WatchEvent<?> event : key.pollEvents()) {
-                WatchEvent<Path> we = (WatchEvent<Path>) event;
-                Path path = we.context();
-                File file = pathToReload.resolve(path).toFile();
-                LOG.trace("File watch-event: {} on file: {}", we, file);
-                if (file.isDirectory()) {
-                    if (isRecursive && we.kind() == ENTRY_CREATE && 
!isCompileWorkDir(file.toPath())) {
-                        registerNewDirectory(file.toPath(), found);
-                    }
-                    continue;
-                }
-                if (isCompileWorkDir(file.toPath().getParent())) {
-                    // a class file the runtime wrote while compiling: not a 
change of ours
-                    continue;
-                }
-                found.add(file);
-            }
-            for (File file : found) {
-                String name = FileUtil.compactPath(file.getPath());
-                LOG.debug("Detected Modified/Created file: {}", name);
-                if (fileFilter != null && !fileFilter.accept(file)) {
-                    continue;
-                }
-                LOG.debug("Accepted Modified/Created file: {}", name);
-                if (!changed.contains(file)) {
-                    changed.add(file);
-                }
-            }
-        }
     }
 
     /**
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index c2d3df2b39d4..b6a1d67a1038 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -2921,3 +2921,17 @@ This is the intended behaviour and makes the `keepOpen` 
toggle deterministic, bu
 relied on the setter being inert until an exchange arrived must be aware of 
the change.
 Setting `keepOpen` back to `false` remains deferred: the half-open timer 
attempts to close the circuit on
 its next tick (after `halfOpenAfter` milliseconds, default 30 s).
+
+== Route reload
+
+Dev-mode route reloading now finds changed files by scanning the watched 
folder on an interval and comparing each
+file's modification time and length with the previous scan, instead of using 
the JDK `WatchService`. A scan reports
+all the files of one save together, which is what a reload needs: a route is 
built with the properties of the moment,
+so a route and a property saved together must be reloaded together. It also 
removes the macOS penalty, where Java has
+no native file notification and the watch service fell back to a poll of about 
ten seconds.
+
+`FileWatcherResourceReloadStrategy.setPollTimeout` is now how often the folder 
is scanned, and its default changed
+from 2000 to 1000 milliseconds. On Linux and Windows a change is therefore 
noticed within a second rather than
+immediately; on macOS within a second rather than up to ten. A new 
`setStableTimeout` (default 200 milliseconds)
+leaves a file that was only just modified for the next scan, so a save still 
being written is not reloaded
+half-finished.

Reply via email to