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 43a45c138075 CAMEL-26444: camel-core - dev mode: the first failed 
reload after start goes back to the routes loaded at startup (#28789)
43a45c138075 is described below

commit 43a45c13807598f0dc1feb9a961fd66a487faf55
Author: Serdar Gökay <[email protected]>
AuthorDate: Sun Nov 1 12:16:50 2026 +0300

    CAMEL-26444: camel-core - dev mode: the first failed reload after start 
goes back to the routes loaded at startup (#28789)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/support/RouteWatcherReloadStrategy.java  |  96 +++++++--
 .../modules/ROOT/pages/route-reload.adoc           |  12 ++
 .../camel/dsl/yaml/RouteReloadBatchTest.java       |   5 +-
 .../yaml/RouteReloadOnDemandRemovedFileTest.java   |   4 -
 .../dsl/yaml/RouteReloadPropertiesRetryTest.java   |   3 -
 .../camel/dsl/yaml/RouteReloadRollbackTest.java    | 216 ++++++++++++++++++++-
 6 files changed, 299 insertions(+), 37 deletions(-)

diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
index 1d97fec5da47..460c99da95e9 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
@@ -47,6 +47,7 @@ import org.apache.camel.spi.PropertiesComponent;
 import org.apache.camel.spi.PropertiesReload;
 import org.apache.camel.spi.PropertiesSource;
 import org.apache.camel.spi.Resource;
+import org.apache.camel.spi.ResourceLoader;
 import org.apache.camel.util.AntPathMatcher;
 import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.IOHelper;
@@ -77,8 +78,13 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
     private String pattern;
     private boolean removeAllRoutes = true;
     private final List<Resource> previousSources = new ArrayList<>();
-    /** The content of the sources of the last successful reload, by location: 
what a failed reload goes back to. */
+    /**
+     * The content of the sources as they last loaded, at startup and then at 
each successful reload, by location: what
+     * a failed reload goes back to.
+     */
     private final Map<String, byte[]> lastGoodContent = new 
ConcurrentHashMap<>();
+    /** Whether a reload began: the routes that run from then on are not those 
loaded at startup. */
+    private volatile boolean reloadBegun;
     /** The sources of the last failed reload: retried when the properties 
change, as a missing property may be why. */
     private final List<Resource> failedSources = new ArrayList<>();
     private Supplier<Collection<Resource>> beanResources;
@@ -167,6 +173,13 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
             });
         }
 
+        // the content of the routes loaded at startup is what the first 
failed reload goes back to, as the content of
+        // a successful reload is for the next (CAMEL-26444). When a reload 
began already, that reload is starting this
+        // service by adding the first route (see startupSources): the routes 
are its own, not from startup
+        if (!reloadBegun) {
+            rememberContent(startupSources());
+        }
+
         // the following is all related to watching files on disk
         if (folder == null) {
             return;
@@ -557,6 +570,8 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
 
     @SuppressWarnings("unchecked")
     protected void onRouteReload(Collection<Resource> resources, boolean 
removeEverything) {
+        reloadBegun = true;
+
         // notify context service plugins before reloading routes
         ContextServiceLoaderPluginResolver pluginResolver
                 = 
getCamelContext().getCamelContextExtension().getContextPlugin(ContextServiceLoaderPluginResolver.class);
@@ -596,14 +611,7 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
                 // (the file with the templated route is only loaded again 
when its route template comes from a file:
                 // a route template added from Java is removed below and 
nothing adds it again, so loading the
                 // templated route would fail the whole reload, CAMEL-25616)
-                getCamelContext().getRoutes().forEach(r -> {
-                    if (!r.isCreatedByKamelet()) {
-                        addRouteSource(resources, sources, 
r.getSourceResource());
-                        if (r.getSourceResource() != null) {
-                            addRouteSource(resources, sources, 
r.getTemplatedRouteResource());
-                        }
-                    }
-                });
+                getCamelContext().getRoutes().forEach(r -> 
addRouteSources(resources, sources, r));
                 // first stop and remove all routes
                 getCamelContext().getRouteController().removeAllRoutes();
                 // remove left-over route templates and endpoints, so we can 
start on a fresh
@@ -786,12 +794,50 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
         lastGoodContent.keySet().retainAll(current);
     }
 
+    /**
+     * The content of the resource as it last loaded, else null. The location 
it was remembered by can be written
+     * another way than the saved file: with the Camel Main defaults the 
routes load at startup from
+     * {@code target/classes} and are saved under {@code src/main/resources}, 
so a remembered file is also found by the
+     * comparison the reload uses to tell that two resources are the same file.
+     */
+    private byte[] lastGoodContentOf(Resource rs) {
+        byte[] content = lastGoodContent.get(rs.getLocation());
+        if (content == null) {
+            ResourceLoader loader = 
PluginHelper.getResourceLoader(getCamelContext());
+            for (Map.Entry<String, byte[]> e : lastGoodContent.entrySet()) {
+                if (e.getKey().startsWith("file:") && 
equalResourceLocation(loader.resolveResource(e.getKey()), rs)) {
+                    return e.getValue();
+                }
+            }
+        }
+        return content;
+    }
+
+    /**
+     * The sources of the routes loaded at startup. The supervising route 
controller starts the routes after the
+     * context, and this service together with the first of them, so the 
routes it has yet to start are only known to
+     * the route controller; with no routes at startup, the first route a 
reload adds starts this service.
+     */
+    private List<Resource> startupSources() {
+        List<Resource> sources = new ArrayList<>();
+        for (Route route : getCamelContext().getRoutes()) {
+            addRouteSources(null, sources, route);
+        }
+        for (Route route : 
getCamelContext().getRouteController().getControlledRoutes()) {
+            addRouteSources(null, sources, route);
+        }
+        return sources;
+    }
+
     /**
      * Reloads the sources of the routes that ran before a failed reload, so a 
mistake in one file does not leave the
      * application without routes: the other files from disk, and the failed 
file from the content that last loaded,
-     * which is kept in memory for a project whose routes are all in one file 
(CAMEL-24899). After a successful restore
-     * the remembered set is cleared: the running routes are the last working 
set again, and the next reload collects
-     * their sources itself. The file on disk is untouched and its next save 
is loaded as usual.
+     * which is kept in memory (CAMEL-24899, CAMEL-26444). After a successful 
restore the remembered set is cleared: the
+     * running routes are the last working set again, and the next reload 
collects their sources itself. The file on
+     * disk is untouched and its next save is loaded as usual.
+     * <p/>
+     * The routes a partial load left are removed also when there is nothing 
to restore, so no route of the failed load
+     * runs, as the warning says (CAMEL-26444).
      */
     protected void restorePreviousRoutes(Collection<Resource> failed, 
Exception cause) {
         if (!removeAllRoutes || previousSources.isEmpty()) {
@@ -808,23 +854,23 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
         int fromMemory = 0;
         if (failed != null) {
             for (Resource rs : failed) {
-                byte[] content = rs != null ? 
lastGoodContent.get(rs.getLocation()) : null;
+                byte[] content = rs != null ? lastGoodContentOf(rs) : null;
                 if (content != null && !equalResourceLocation(restore, rs)) {
                     restore.add(ResourceHelper.fromBytes(rs.getLocation(), 
content));
                     fromMemory++;
                 }
             }
         }
-        if (restore.isEmpty()) {
-            LOG.warn("Reload failed and there are no previous routes to 
restore: the application runs without routes"
-                     + " until the file is fixed");
-            return;
-        }
         try {
             // a partial load may have left routes or endpoints behind
             getCamelContext().getRouteController().removeAllRoutes();
             getCamelContext().removeRouteTemplates("*");
             getCamelContext().getEndpointRegistry().clear();
+            if (restore.isEmpty()) {
+                LOG.warn("Reload failed and there are no previous routes to 
restore: the application runs without"
+                         + " routes until the file is fixed");
+                return;
+            }
             Set<String> ids = 
PluginHelper.getRoutesLoader(getCamelContext()).updateRoutes(restore);
             // the running routes are the last working set again: the next 
reload collects their sources itself
             previousSources.clear();
@@ -839,6 +885,20 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
         }
     }
 
+    /**
+     * Adds the sources of a route to the sources to load again: the file it 
was loaded from and, for a route from a
+     * templated route, the file of the templated route. A route from a 
Kamelet has none of its own: its parent route
+     * creates it again.
+     */
+    private static void addRouteSources(Collection<Resource> resources, 
List<Resource> sources, Route route) {
+        if (!route.isCreatedByKamelet()) {
+            addRouteSource(resources, sources, route.getSourceResource());
+            if (route.getSourceResource() != null) {
+                addRouteSource(resources, sources, 
route.getTemplatedRouteResource());
+            }
+        }
+    }
+
     /**
      * Adds the source of a running route to the sources to load again, unless 
it is one of the updated resources, it is
      * already added, or its file is gone.
diff --git a/docs/user-manual/modules/ROOT/pages/route-reload.adoc 
b/docs/user-manual/modules/ROOT/pages/route-reload.adoc
index f1f394dc56ce..551bc191cd3d 100644
--- a/docs/user-manual/modules/ROOT/pages/route-reload.adoc
+++ b/docs/user-manual/modules/ROOT/pages/route-reload.adoc
@@ -87,6 +87,18 @@ Routes reloaded summary (total:3 started:3 changed:2)
 
 The revision of a route counts the times a reload changed it in this run.
 
+=== When a reload fails
+
+When a saved file has a mistake, the reload fails and the routes that ran 
before keep running: the other files are
+loaded again from disk, and the file with the mistake goes back to the content 
it had when it last loaded, at startup
+or at the last successful reload. Camel keeps that content in memory. The file 
on disk is not changed, and its next
+save is loaded as usual. A WARN in the log says that the previous routes were 
restored.
+
+A file that has not loaded before, such as a new file, has no content to go 
back to: none of its routes run until it
+is fixed.
+
+This applies when `routesReloadRemoveAllRoutes` is `true`, which is the 
default.
+
 === Live reload of resource files
 
 In addition to reloading routes, when `routesReloadEnabled=true` Camel also 
disables the
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadBatchTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadBatchTest.java
index 9b7694c9d5bd..575b46675ef4 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadBatchTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadBatchTest.java
@@ -196,12 +196,13 @@ class RouteReloadBatchTest extends YamlTestSupport {
                                 uri: mock:other-v2
                     """);
 
-            // the batch fails, so the files are reloaded one at a time and 
the good one gets through
+            // the batch fails, so the files are reloaded one at a time: the 
good one gets through, and the broken one
+            // runs the version loaded at startup
             strategy.onReloadBatch(List.of(route.toFile(), other.toFile()));
 
             
assertThat(context.getRouteController().getRouteStatus("other")).isEqualTo(ServiceStatus.Started);
             assertThat(context.hasEndpoint("mock://other-v2")).isNotNull();
-            assertThat(context.getRoute("shop")).isNull();
+            
assertThat(context.getRouteController().getRouteStatus("shop")).isEqualTo(ServiceStatus.Started);
         } finally {
             strategy.doStop();
         }
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadOnDemandRemovedFileTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadOnDemandRemovedFileTest.java
index 50456d46c58e..4bede926c650 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadOnDemandRemovedFileTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadOnDemandRemovedFileTest.java
@@ -99,10 +99,6 @@ class RouteReloadOnDemandRemovedFileTest extends 
YamlTestSupport {
     void aFileRestoredFromMemoryCanBeRemoved() throws Exception {
         TestableStrategy strategy = newStrategy();
         try {
-            // one successful reload, so the content that runs is remembered
-            strategy.onReload("test");
-            assertThat(strategy.getFailedCounter()).isZero();
-
             // b is saved with a mistake (pollEnrich takes an expression, not 
a uri): the reload fails and the routes
             // are restored from the content that last loaded
             Files.writeString(b, """
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadPropertiesRetryTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadPropertiesRetryTest.java
index 4288d29a163f..2a7546ad4bf3 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadPropertiesRetryTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadPropertiesRetryTest.java
@@ -110,9 +110,6 @@ class RouteReloadPropertiesRetryTest extends 
YamlTestSupport {
         strategy.setPattern("*.yaml,*.properties");
         strategy.doStart();
         try {
-            // one successful reload, so the content that runs is remembered
-            strategy.getResourceReload().onReload(route.toString(),
-                    ResourceHelper.resolveResource(context, "file:" + route));
             
assertThat(context.getRouteController().getRouteStatus("shop")).isEqualTo(ServiceStatus.Started);
 
             // the route is saved using a property that is not in 
application.properties yet, which is what an edit
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadRollbackTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadRollbackTest.java
index 374ffd4f6c42..e0bbe2469516 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadRollbackTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/RouteReloadRollbackTest.java
@@ -24,27 +24,33 @@ import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.Collection;
 
+import org.apache.camel.CamelContext;
 import org.apache.camel.ServiceStatus;
 import org.apache.camel.component.mock.MockEndpoint;
 import org.apache.camel.dsl.yaml.support.YamlTestSupport;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.support.RouteOnDemandReloadStrategy;
 import org.apache.camel.support.RouteWatcherReloadStrategy;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /**
  * CAMEL-24860: a reload that fails (a route file saved with a mistake) 
restores the routes that ran before, instead of
  * leaving the application without routes until the next successful save. 
CAMEL-24899: a project whose routes are all in
- * one file goes back to the content that last loaded, which is kept in memory.
+ * one file goes back to the content that last loaded, which is kept in 
memory. CAMEL-26444: the content loaded at
+ * startup is kept too, so the first broken save after start goes back to it; 
and a failed reload with nothing to go
+ * back to leaves none of the routes it loaded running.
  * <p>
- * The mistake the tests save is always the same one, and it is a real one 
(CAMEL-24850): the endpoint of pollEnrich is
- * an expression, so {@code pollEnrich: {uri: "file:./order.json"}} has no uri 
property to bind and the loader rejects
- * the file with "pollEnrich: unsupported field: uri". It is the right kind of 
mistake here because it fails while the
+ * The mistake the tests save is the same one, and it is a real one 
(CAMEL-24850): the endpoint of pollEnrich is an
+ * expression, so {@code pollEnrich: {uri: "file:./order.json"}} has no uri 
property to bind and the loader rejects the
+ * file with "pollEnrich: unsupported field: uri". It is the right kind of 
mistake here because it fails while the
  * routes are built, not while the YAML is parsed, which is what a reload has 
to survive. The form that works is
- * {@code pollEnrich: {expression: {constant: {expression: 
"file:./order.json"}}}}.
+ * {@code pollEnrich: {expression: {constant: {expression: 
"file:./order.json"}}}}. Only the file that must fail after
+ * its first route was added has another mistake: an option its endpoint does 
not have.
  */
 class RouteReloadRollbackTest extends YamlTestSupport {
 
@@ -136,10 +142,11 @@ class RouteReloadRollbackTest extends YamlTestSupport {
                 failure = e;
             }
 
-            // the reload fails, and the route of the other file runs again
+            // the reload fails, the route of the other file runs again, and 
the failed file runs the version loaded at
+            // startup
             assertThat(failure).isNotNull();
             
assertThat(context.getRouteController().getRouteStatus("good")).isEqualTo(ServiceStatus.Started);
-            assertThat(context.getRoute("bad")).isNull();
+            
assertThat(context.getRouteController().getRouteStatus("bad")).isEqualTo(ServiceStatus.Started);
 
             // the file is fixed
             Files.writeString(bad, """
@@ -187,9 +194,6 @@ class RouteReloadRollbackTest extends YamlTestSupport {
             strategy.setPattern("*.yaml");
             strategy.doStart();
             try {
-                // one successful reload, so the content that runs is 
remembered
-                strategy.getResourceReload().onReload(only.toString(),
-                        ResourceHelper.resolveResource(context2, "file:" + 
only));
                 
assertThat(context2.getRouteController().getRouteStatus("only")).isEqualTo(ServiceStatus.Started);
 
                 // the only route file is saved with a mistake (pollEnrich 
takes an expression, not a uri)
@@ -307,6 +311,198 @@ class RouteReloadRollbackTest extends YamlTestSupport {
         }
     }
 
+    @Test
+    void theFirstBrokenSaveAfterStartGoesBackToTheVersionLoadedAtStartup() 
throws Exception {
+        Path solo = Files.createTempDirectory("camel-reload-startup");
+        Path only = Files.writeString(solo.resolve("only.camel.yaml"), 
route("only", "mock:only"));
+        RouteWatcherReloadStrategy strategy = new 
RouteWatcherReloadStrategy(solo.toString());
+        strategy.setPattern("*.yaml");
+        // no watcher thread: the test saves the file and reloads it
+        strategy.setScheduler(false);
+        CamelContext ctx = startWithRoutes(strategy, false, only);
+        try {
+            // the first save after start is broken (pollEnrich takes an 
expression, not a uri)
+            Files.writeString(only, brokenRoute("only"));
+            assertThatThrownBy(() -> reload(strategy, ctx, 
only)).hasMessageContaining("pollEnrich");
+
+            // the version loaded at startup still runs
+            
assertThat(ctx.getRouteController().getRouteStatus("only")).isEqualTo(ServiceStatus.Started);
+            assertRunsTo(ctx, "direct:only", "mock:only");
+
+            // the file is fixed, and the fixed version runs
+            Files.writeString(only, route("only", "mock:fixed"));
+            reload(strategy, ctx, only);
+            assertRunsTo(ctx, "direct:only", "mock:fixed");
+
+            // the next broken save goes back to the fixed version, which 
loaded last, not to the one loaded at startup
+            Files.writeString(only, brokenRoute("only"));
+            assertThatThrownBy(() -> reload(strategy, ctx, 
only)).hasMessageContaining("pollEnrich");
+            assertRunsTo(ctx, "direct:only", "mock:fixed");
+        } finally {
+            ctx.stop();
+            deleteRecursively(solo);
+        }
+    }
+
+    @Test
+    void 
theFirstBrokenSaveUnderSrcMainResourcesGoesBackToTheVersionLoadedFromTargetClasses()
 throws Exception {
+        // the Camel Main defaults: the routes load from the classpath, 
target/classes, and the reload watches
+        // src/main/resources, so the file that loaded and the file that is 
saved have different locations
+        Path project = Files.createTempDirectory("camel-reload-layout");
+        Path classes = 
Files.createDirectories(project.resolve("target/classes/camel"));
+        Path resources = 
Files.createDirectories(project.resolve("src/main/resources/camel"));
+        Path loaded = Files.writeString(classes.resolve("only.camel.yaml"), 
route("only", "mock:only"));
+        Path saved = Files.writeString(resources.resolve("only.camel.yaml"), 
route("only", "mock:only"));
+        RouteWatcherReloadStrategy strategy = new 
RouteWatcherReloadStrategy(resources.toString());
+        strategy.setPattern("*.yaml");
+        strategy.setScheduler(false);
+        CamelContext ctx = startWithRoutes(strategy, false, loaded);
+        try {
+            Files.writeString(saved, brokenRoute("only"));
+            assertThatThrownBy(() -> reload(strategy, ctx, 
saved)).hasMessageContaining("pollEnrich");
+
+            // the version loaded at startup still runs
+            
assertThat(ctx.getRouteController().getRouteStatus("only")).isEqualTo(ServiceStatus.Started);
+            assertRunsTo(ctx, "direct:only", "mock:only");
+        } finally {
+            ctx.stop();
+            deleteRecursively(project);
+        }
+    }
+
+    @Test
+    void 
theFirstFailedOnDemandReloadAfterStartGoesBackToEveryFileLoadedAtStartup() 
throws Exception {
+        Path source = Files.createTempDirectory("camel-reload-on-demand");
+        Path a = Files.writeString(source.resolve("a.camel.yaml"), route("a", 
"mock:a"));
+        Path b = Files.writeString(source.resolve("b.camel.yaml"), route("b", 
"mock:b"));
+        // as camel run --source-dir sets it up: every file is reloaded as 
changed
+        RouteOnDemandReloadStrategy strategy = new 
RouteOnDemandReloadStrategy(source.toString(), true);
+        strategy.setPattern("*");
+        // the supervising route controller starts the routes after the 
context, and the strategy with the first of
+        // them: the content of the other routes is remembered too
+        CamelContext ctx = startWithRoutes(strategy, true, a, b);
+        try {
+            
assertThat(ctx.getRouteController().getRouteStatus("a")).isEqualTo(ServiceStatus.Started);
+            
assertThat(ctx.getRouteController().getRouteStatus("b")).isEqualTo(ServiceStatus.Started);
+
+            // the first reload after start fails on one file (pollEnrich 
takes an expression, not a uri)
+            Files.writeString(b, brokenRoute("b"));
+            strategy.onReload("test");
+            assertThat(strategy.getFailedCounter()).isEqualTo(1);
+
+            // both files run the version loaded at startup, the broken one 
from memory
+            
assertThat(ctx.getRouteController().getRouteStatus("a")).isEqualTo(ServiceStatus.Started);
+            
assertThat(ctx.getRouteController().getRouteStatus("b")).isEqualTo(ServiceStatus.Started);
+            assertRunsTo(ctx, "direct:a", "mock:a");
+            assertRunsTo(ctx, "direct:b", "mock:b");
+
+            // the file is fixed, and the fixed version runs
+            Files.writeString(b, route("b", "mock:fixed"));
+            strategy.onReload("test");
+            assertThat(strategy.getReloadCounter()).isEqualTo(1);
+            assertRunsTo(ctx, "direct:b", "mock:fixed");
+        } finally {
+            ctx.stop();
+            deleteRecursively(source);
+        }
+    }
+
+    @Test
+    void aFailedReloadWithNothingToGoBackToLeavesNoneOfItsRoutesRunning() 
throws Exception {
+        Path source = Files.createTempDirectory("camel-reload-halfway");
+        RouteOnDemandReloadStrategy strategy = new 
RouteOnDemandReloadStrategy(source.toString(), true);
+        strategy.setPattern("*");
+        // no routes at startup: the supervising route controller starts the 
strategy with the first route the reload
+        // adds, in the middle of the reload
+        CamelContext ctx = startWithRoutes(strategy, true);
+        try {
+            // the first route file: its second route has an option its 
endpoint does not have, which fails after the
+            // first route was added
+            Files.writeString(source.resolve("multi.camel.yaml"), """
+                    - route:
+                        id: m1
+                        from:
+                          uri: direct:m1
+                          steps:
+                            - to:
+                                uri: mock:m1
+                    - route:
+                        id: m2
+                        from:
+                          uri: direct:m2?noSuchOption=1
+                          steps:
+                            - to:
+                                uri: mock:m2
+                    """);
+            strategy.onReload("test");
+            assertThat(strategy.getFailedCounter()).isEqualTo(1);
+
+            // nothing ran before the reload, and nothing of the failed file 
runs after it
+            assertThat(ctx.getRoutes()).isEmpty();
+        } finally {
+            ctx.stop();
+            deleteRecursively(source);
+        }
+    }
+
+    /**
+     * A context that loads the routes of the files and then starts, with the 
reload strategy added as a service, the
+     * way Camel Main adds it: the context starts the strategy together with 
the routes.
+     */
+    private static CamelContext startWithRoutes(RouteWatcherReloadStrategy 
strategy, boolean supervising, Path... files)
+            throws Exception {
+        DefaultCamelContext ctx = new DefaultCamelContext();
+        ctx.disableJMX();
+        if (supervising) {
+            ctx.getRouteController().supervising();
+        }
+        for (Path file : files) {
+            
PluginHelper.getRoutesLoader(ctx).loadRoutes(ResourceHelper.resolveResource(ctx,
 "file:" + file));
+        }
+        ctx.addService(strategy);
+        ctx.start();
+        return ctx;
+    }
+
+    // reloads the saved file as the file watcher does
+    private static void reload(RouteWatcherReloadStrategy strategy, 
CamelContext ctx, Path file) throws Exception {
+        strategy.getResourceReload().onReload(file.toString(), 
ResourceHelper.resolveResource(ctx, "file:" + file));
+    }
+
+    // a message sent to the route arrives at the target of the version that 
runs
+    private static void assertRunsTo(CamelContext ctx, String from, String to) 
throws InterruptedException {
+        MockEndpoint mock = ctx.getEndpoint(to, MockEndpoint.class);
+        mock.reset();
+        mock.expectedMessageCount(1);
+        ctx.createProducerTemplate().sendBody(from, "x");
+        MockEndpoint.assertIsSatisfied(ctx);
+    }
+
+    private static String route(String id, String to) {
+        return """
+                - route:
+                    id: %s
+                    from:
+                      uri: direct:%s
+                      steps:
+                        - to:
+                            uri: %s
+                """.formatted(id, id, to);
+    }
+
+    // pollEnrich takes an expression, not a uri
+    private static String brokenRoute(String id) {
+        return """
+                - route:
+                    id: %s
+                    from:
+                      uri: direct:%s
+                      steps:
+                        - pollEnrich:
+                            uri: file:./order.json
+                """.formatted(id, id);
+    }
+
     private static void deleteRecursively(Path path) throws IOException {
         if (path == null || !Files.exists(path)) {
             return;

Reply via email to