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()
+    }
+}

Reply via email to