From b8ec9b1d15a76c0407154c27c32f78fbb5180614 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Mon, 21 Sep 2026 08:18:50 +0200 Subject: [PATCH 1/2] CAMEL-24860: a failed route reload restores the routes that ran before, without the failed file, instead of leaving the application without routes Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj --- .../support/RouteWatcherReloadStrategy.java | 42 +++++++ .../pages/camel-4x-upgrade-guide-4_23.adoc | 8 ++ .../dsl/yaml/RouteReloadRollbackTest.groovy | 115 ++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/RouteReloadRollbackTest.groovy 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 8a1d09885194d..07ce78d2ae672 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,52 @@ protected void onRouteReload(Collection resources, boolean removeEvery } } } 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. The previous set stays remembered, so the next + * successful reload of the failed file brings everything back together. + */ + protected void restorePreviousRoutes(Collection failed, Exception cause) { + if (!removeAllRoutes || previousSources.isEmpty()) { + return; + } + List 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 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: the previous routes were restored ({} route(s) running); the changed file loads" + + " on its next save", + 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 4bef77ec50044..4f794e193dd9c 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 0000000000000..f28491c5eb10b --- /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() + } +} From 3a18dc2c1989b477cf6a63c58ab380a463fb84b9 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Mon, 21 Sep 2026 08:42:36 +0200 Subject: [PATCH 2/2] CAMEL-24860: Address review feedback: the restore warning names the load error it recovered from, and the Javadoc says the remembered set is cleared after a restore Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj --- .../camel/support/RouteWatcherReloadStrategy.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 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 07ce78d2ae672..3160788436361 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 @@ -395,8 +395,9 @@ protected void onRouteReload(Collection resources, boolean removeEvery /** * 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. The previous set stays remembered, so the next - * successful reload of the failed file brings everything back together. + * 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 failed, Exception cause) { if (!removeAllRoutes || previousSources.isEmpty()) { @@ -421,9 +422,9 @@ protected void restorePreviousRoutes(Collection failed, Exception caus Set 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: the previous routes were restored ({} route(s) running); the changed file loads" - + " on its next save", - ids.size()); + 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",