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;