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 04cfe8066ea3 CAMEL-24860: a failed route reload restores the routes
that ran before, instead of leaving the application without routes
04cfe8066ea3 is described below
commit 04cfe8066ea3ffdeb34f3791ec6a86f98c40eb80
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 21 09:44:13 2026 +0200
CAMEL-24860: a failed route reload restores the routes that ran before,
instead of leaving the application without routes
When a route file was saved with a mistake in dev mode, the route watcher
reload strategy first stopped and removed every route, then failed to load
the new set, and the application ran with no routes at all until the next
successful save; the previous sources were remembered for that next save,
nothing used them until then.
The catch now restores the previous routes: the remembered sources minus the
failed resources are reloaded after clearing what a partial load left, a
WARN
names the load error it recovered from and says the previous routes were
restored, and the exception is rethrown as before so the watcher's error
line,
the CamelContextReloadFailure event and the validator's report still follow.
The remembered set is cleared after the restore. Only the removeAllRoutes
mode
is concerned. Pre-validating the file before removing the routes was
considered and rejected: preParseRoute misses the model errors the
deserializers throw.
Closes #26641
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---
.../camel/support/RouteWatcherReloadStrategy.java | 43 ++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 8 ++
.../camel/dsl/yaml/RouteReloadRollbackTest.groovy | 115 +++++++++++++++++++++
3 files changed, 166 insertions(+)
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 8a1d09885194..316078843636 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
@@ -385,10 +385,53 @@ public class RouteWatcherReloadStrategy extends
FileWatcherResourceReloadStrateg
}
}
} catch (Exception e) {
+ // the routes that ran before were removed above and the new ones
failed to load: the app has no routes
+ // until the next successful reload. Restore the previous routes
now, without the failed resources, so a
+ // mistake in one file leaves the rest running (CAMEL-24860); the
failed file loads on its next save
+ restorePreviousRoutes(resources, e);
throw RuntimeCamelException.wrapRuntimeException(e);
}
}
+ /**
+ * Reloads the sources of the routes that ran before a failed reload,
without the resources that failed, so a
+ * mistake in one file does not leave the application without routes.
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 failed file is loaded again on its next save.
+ */
+ protected void restorePreviousRoutes(Collection<Resource> failed,
Exception cause) {
+ if (!removeAllRoutes || previousSources.isEmpty()) {
+ return;
+ }
+ List<Resource> restore = new ArrayList<>();
+ for (Resource rs : previousSources) {
+ if (rs != null && (failed == null ||
!equalResourceLocation(failed, rs))) {
+ restore.add(rs);
+ }
+ }
+ 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();
+ 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();
+ LOG.warn("Reload failed due to: {}. The previous routes were
restored ({} route(s) running); the changed"
+ + " file loads on its next save",
+ cause.getMessage(), ids.size());
+ } catch (Exception e) {
+ LOG.warn("Reload failed and the previous routes could not be
restored due to: {}. The application runs"
+ + " without routes until the file is fixed",
+ e.getMessage(), e);
+ }
+ }
+
/**
* Whether the target is loading any of the given sources
*/
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 4bef77ec5004..4f794e193dd9 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
@@ -473,6 +473,14 @@ set here, because that would reject input that parses
today. Routes that genuine
an external DTD or parameter entity through this converter must supply their
own
`SAXParserFactory`.
+=== camel-core - a failed route reload restores the previous routes
+
+When a route file is reloaded in dev mode (`camel run --dev`, or the route
watcher reload strategy in general) and
+the new content fails to load, the routes that ran before are restored right
away, without the routes of the failed
+file, and a WARN says so. Before, the previous routes stayed stopped until the
next successful reload, so a mistake
+in one file left the application without routes. The failed file loads again
on its next save. The
+`CamelContextReloadFailure` event and the reload error log line are unchanged.
+
=== camel-core - the type of a bean created by a script or a builder is
optional
The `type` (class name) of a bean definition — `bean` under `beans`,
`templateBean` of a route
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/RouteReloadRollbackTest.groovy
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/RouteReloadRollbackTest.groovy
new file mode 100644
index 000000000000..f28491c5eb10
--- /dev/null
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/RouteReloadRollbackTest.groovy
@@ -0,0 +1,115 @@
+/*
+ * 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.dsl.yaml
+
+import org.apache.camel.ServiceStatus
+import org.apache.camel.dsl.yaml.support.YamlTestSupport
+import org.apache.camel.spi.Resource
+import org.apache.camel.support.ResourceHelper
+import org.apache.camel.support.RouteWatcherReloadStrategy
+
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * 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.
+ */
+class RouteReloadRollbackTest extends YamlTestSupport {
+
+ Path dir
+ Path good
+ Path bad
+
+ @Override
+ def doSetup() {
+ dir = Files.createTempDirectory("camel-reload")
+ good = dir.resolve("good.camel.yaml")
+ bad = dir.resolve("bad.camel.yaml")
+ Files.writeString(good, '''
+ - route:
+ id: good
+ from:
+ uri: direct:good
+ steps:
+ - to:
+ uri: mock:good
+ ''')
+ Files.writeString(bad, '''
+ - route:
+ id: bad
+ from:
+ uri: direct:bad
+ steps:
+ - to:
+ uri: mock:bad
+ ''')
+ context.start()
+ loadRoutes(ResourceHelper.resolveResource(context, "file:" + good),
ResourceHelper.resolveResource(context, "file:" + bad))
+ }
+
+ def cleanup() {
+ dir.toFile().deleteDir()
+ }
+
+ def 'a failed reload restores the previous routes'() {
+ setup:
+ def strategy = new RouteWatcherReloadStrategy(dir.toString())
+ strategy.setCamelContext(context)
+ strategy.setPattern("*.yaml")
+ // the strategy is not started (no file watcher in the test): its
reload callback is driven by hand
+ strategy.doStart()
+ assert context.getRouteController().getRouteStatus("good") ==
ServiceStatus.Started
+ assert context.getRouteController().getRouteStatus("bad") ==
ServiceStatus.Started
+ when: 'the second file is saved with a mistake'
+ Files.writeString(bad, '''
+ - route:
+ id: bad
+ from:
+ uri: direct:bad
+ steps:
+ - pollEnrich:
+ uri: file:./order.json
+ ''')
+ def failure = null
+ try {
+ strategy.getResourceReload().onReload(bad.toString(),
ResourceHelper.resolveResource(context, "file:" + bad))
+ } catch (Exception e) {
+ failure = e
+ }
+ then: 'the reload fails, and the route of the other file runs again'
+ failure != null
+ context.getRouteController().getRouteStatus("good") ==
ServiceStatus.Started
+ context.getRoute("bad") == null
+ when: 'the file is fixed'
+ Files.writeString(bad, '''
+ - route:
+ id: bad
+ from:
+ uri: direct:bad
+ steps:
+ - to:
+ uri: mock:bad
+ ''')
+ strategy.getResourceReload().onReload(bad.toString(),
ResourceHelper.resolveResource(context, "file:" + bad))
+ then: 'both run'
+ context.getRouteController().getRouteStatus("good") ==
ServiceStatus.Started
+ context.getRouteController().getRouteStatus("bad") ==
ServiceStatus.Started
+ cleanup:
+ strategy.doStop()
+ }
+}