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 14c215da326c CAMEL-25834: camel-core - dev mode: a properties file
that fails in a batch reload is reported, as when it changed alone (#28125)
14c215da326c is described below
commit 14c215da326cd08a29cc006301531ffd49eb4725
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 19 21:25:17 2026 +0200
CAMEL-25834: camel-core - dev mode: a properties file that fails in a batch
reload is reported, as when it changed alone (#28125)
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../RouteWatcherReloadBatchFailureTest.java | 193 +++++++++++++++++++++
.../support/FileWatcherResourceReloadStrategy.java | 26 ++-
.../camel/support/RouteWatcherReloadStrategy.java | 46 +++--
3 files changed, 238 insertions(+), 27 deletions(-)
diff --git
a/core/camel-core/src/test/java/org/apache/camel/support/RouteWatcherReloadBatchFailureTest.java
b/core/camel-core/src/test/java/org/apache/camel/support/RouteWatcherReloadBatchFailureTest.java
new file mode 100644
index 000000000000..1e6c1d9f1c80
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/support/RouteWatcherReloadBatchFailureTest.java
@@ -0,0 +1,193 @@
+/*
+ * 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.support;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.impl.engine.DefaultRoutesLoader;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.spi.PropertiesReload;
+import org.apache.camel.spi.Resource;
+import org.apache.camel.spi.RoutesLoader;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Property;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-25834: the files of one scan of the folder are reloaded together.
When a properties file of such a batch failed
+ * to reload, the failure was only logged at DEBUG: the batch was reloaded
again one file at a time, and by then the
+ * properties component had the new values of the file, so the file had no
changes left and nothing failed.
+ */
+public class RouteWatcherReloadBatchFailureTest extends ContextTestSupport {
+
+ private static final String WRONG = "the configuration in the file is
wrong";
+
+ @TempDir
+ Path dir;
+
+ private final List<String> applied = new CopyOnWriteArrayList<>();
+ private final List<CamelEvent.CamelContextReloadFailureEvent> failures =
new CopyOnWriteArrayList<>();
+ private final List<String> warnings = new CopyOnWriteArrayList<>();
+ private Path wrong;
+ private Path other;
+ private Appender appender;
+ private RecordingRoutesLoader loader;
+ private RouteWatcherReloadStrategy strategy;
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ wrong = Files.writeString(dir.resolve("app.properties"), "greeting =
hello\n");
+ other = Files.writeString(dir.resolve("other.properties"), "name =
world\n");
+ context.getPropertiesComponent().setLocation("file:" + wrong +
",file:" + other);
+ context.addService(new FailingPropertiesReload("file:" + wrong,
applied));
+ context.getManagementStrategy().addEventNotifier(new
EventNotifierSupport() {
+ @Override
+ public void notify(CamelEvent event) {
+ if (event instanceof CamelEvent.CamelContextReloadFailureEvent
failure) {
+ failures.add(failure);
+ }
+ }
+ });
+ return context;
+ }
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+ loader = new RecordingRoutesLoader();
+ loader.setCamelContext(context);
+
context.getCamelContextExtension().addContextPlugin(RoutesLoader.class, loader);
+ // started without a folder: it has the listener that reloads one
file, and watches nothing
+ strategy = new RouteWatcherReloadStrategy();
+ strategy.setCamelContext(context);
+ strategy.start();
+
+ appender = new AbstractAppender("reload-warnings", null, null, true,
Property.EMPTY_ARRAY) {
+ @Override
+ public void append(LogEvent event) {
+ if (event.getLevel().isMoreSpecificThan(Level.WARN)) {
+ warnings.add(event.getMessage().getFormattedMessage());
+ }
+ }
+ };
+ appender.start();
+ LoggerContext logging = (LoggerContext) LogManager.getContext(false);
+ logging.getConfiguration().getRootLogger().addAppender(appender,
Level.ALL, null);
+ logging.updateLoggers();
+ }
+
+ @AfterEach
+ public void stopCapture() {
+ strategy.stop();
+ LoggerContext logging = (LoggerContext) LogManager.getContext(false);
+
logging.getConfiguration().getRootLogger().removeAppender(appender.getName());
+ logging.updateLoggers();
+ appender.stop();
+ }
+
+ @Test
+ public void aPropertiesFileThatFailsIsReportedAndTheRouteStillReloads()
throws Exception {
+ Files.writeString(wrong, "greeting = hello\ncamel.beans.faker =
#class:com.github.javafaker.Faker\n");
+ File route = Files.writeString(dir.resolve("route.yaml"), "- route:
{}\n").toFile();
+
+ strategy.onReloadBatch(List.of(wrong.toFile(), route));
+
+ assertReported();
+ // the route of the batch reloads once, and counts as reloaded
+ assertEquals(List.of("file:" + route.getPath()), loader.loaded);
+ assertEquals(1, strategy.getReloadCounter());
+ }
+
+ @Test
+ public void aPropertiesFileThatFailsDoesNotStopTheOtherPropertiesFile()
throws Exception {
+ Files.writeString(wrong, "greeting = hello\ncamel.beans.faker =
#class:com.github.javafaker.Faker\n");
+ Files.writeString(other, "name = world\ncamel.component.log.level =
WARN\n");
+
+ strategy.onReloadBatch(List.of(wrong.toFile(), other.toFile()));
+
+ assertReported();
+ assertEquals(List.of("file:" + other + ": camel.component.log.level"),
applied);
+ assertEquals(1, strategy.getReloadCounter());
+ }
+
+ // reported as when the file is reloaded on its own: a WARN, the failed
counter, the last error and the event
+ private void assertReported() {
+ assertTrue(warnings.stream().anyMatch(w ->
w.contains(wrong.toString()) && w.contains(WRONG)),
+ "A WARN names the file and why it failed: " + warnings);
+ assertEquals(1, strategy.getFailedCounter());
+ assertEquals(WRONG, strategy.getLastError().getMessage());
+ assertEquals(1, failures.size());
+ assertEquals(WRONG, failures.get(0).getCause().getMessage());
+ }
+
+ // the listener that applies the configuration of a properties file, as
camel-main does: it fails on one file
+ private static class FailingPropertiesReload extends ServiceSupport
implements PropertiesReload {
+
+ private final String wrong;
+ private final List<String> applied;
+
+ FailingPropertiesReload(String wrong, List<String> applied) {
+ this.wrong = wrong;
+ this.applied = applied;
+ }
+
+ @Override
+ public void onReload(String name, Properties properties) {
+ if (wrong.equals(name)) {
+ throw new IllegalArgumentException(WRONG);
+ }
+ applied.add(name + ": " + String.join(", ",
properties.stringPropertyNames()));
+ }
+ }
+
+ // loads no routes: it records the files
+ private static class RecordingRoutesLoader extends DefaultRoutesLoader {
+ private final List<String> loaded = new ArrayList<>();
+
+ @Override
+ public Set<String> updateRoutes(Collection<Resource> resources) {
+ for (Resource resource : resources) {
+ loaded.add(resource.getLocation());
+ }
+ return Set.of();
+ }
+ }
+}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
index 9e92185c506a..5739f843dd57 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
@@ -415,16 +415,24 @@ public class FileWatcherResourceReloadStrategy extends
ResourceReloadStrategySup
getResourceReload().onReload(name, resource);
incSucceededCounter();
} catch (Exception e) {
- setLastError(e);
- incFailedCounter();
- // the same event a failed context reload emits, so a listener can
act on the file
- EventHelper.notifyContextReloadFailure(getCamelContext(), name, e);
- String msg = e.getMessage();
- if (msg != null && msg.endsWith(".")) {
- msg = msg.substring(0, msg.length() - 1);
- }
- LOG.warn("Error reloading routes from file: {} due to: {}. This
exception is ignored.", name, msg, e);
+ reloadFailed(name, e);
+ }
+ }
+
+ /**
+ * Counts and reports a file that failed to reload: the last error, the
failed counter, the event of a failed
+ * context reload, and a WARN.
+ */
+ void reloadFailed(String name, Exception e) {
+ setLastError(e);
+ incFailedCounter();
+ // the same event a failed context reload emits, so a listener can act
on the file
+ EventHelper.notifyContextReloadFailure(getCamelContext(), name, e);
+ String msg = e.getMessage();
+ if (msg != null && msg.endsWith(".")) {
+ msg = msg.substring(0, msg.length() - 1);
}
+ LOG.warn("Error reloading routes from file: {} due to: {}. This
exception is ignored.", name, msg, e);
}
}
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 5c91fff49ae1..eb2027a783f4 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
@@ -487,8 +487,12 @@ public class RouteWatcherReloadStrategy extends
FileWatcherResourceReloadStrateg
* saved together with a property it uses is therefore built with that
property already in place, instead of failing
* on it and being restored (CAMEL-25032).
* <p/>
- * If the batch fails, the files are reloaded one at a time: the reload
then fails on the one file that is wrong and
- * says which it was, so a mistake in one file does not hide behind the
others (CAMEL-24860).
+ * A properties or Groovy file that fails is reported at once, as when it
changed alone, and the other files still
+ * reload. A properties file is not reloaded again: the properties
component has its new values already, so the file
+ * would have no changes left, and its failure would be lost (CAMEL-25834).
+ * <p/>
+ * If the routes fail, the route files are reloaded one at a time: the
reload then fails on the one file that is
+ * wrong and says which it was, so a mistake in one file does not hide
behind the others (CAMEL-24860).
*/
@Override
protected void onReloadBatch(List<File> changed) {
@@ -497,21 +501,27 @@ public class RouteWatcherReloadStrategy extends
FileWatcherResourceReloadStrateg
super.onReloadBatch(changed);
return;
}
+ setLastError(null);
+ List<File> routeFiles = new ArrayList<>();
List<Resource> routes = new ArrayList<>();
boolean others = false;
- try {
- setLastError(null);
- for (File file : changed) {
- String name = FileUtil.compactPath(file.getPath());
- Resource resource =
PluginHelper.getResourceLoader(getCamelContext()).resolveResource("file:" +
name);
- if (name.endsWith(".properties")) {
- others |= onPropertiesReload(resource, false);
- } else if (name.endsWith(".groovy")) {
- others |= onGroovyReload(resource, false);
- } else {
- routes.add(resource);
+ for (File file : changed) {
+ String name = FileUtil.compactPath(file.getPath());
+ Resource resource =
PluginHelper.getResourceLoader(getCamelContext()).resolveResource("file:" +
name);
+ if (name.endsWith(".properties") || name.endsWith(".groovy")) {
+ try {
+ others |= name.endsWith(".properties")
+ ? onPropertiesReload(resource, false) :
onGroovyReload(resource, false);
+ incSucceededCounter();
+ } catch (Exception e) {
+ reloadFailed(name, e);
}
+ } else {
+ routeFiles.add(file);
+ routes.add(resource);
}
+ }
+ try {
if (!routes.isEmpty()) {
onRouteReload(routes, false);
} else if (others) {
@@ -519,14 +529,14 @@ public class RouteWatcherReloadStrategy extends
FileWatcherResourceReloadStrateg
retryFailedOrReloadAll();
}
// the counter is files reloaded, as it is when they are reloaded
one at a time
- for (int i = 0; i < changed.size(); i++) {
+ for (int i = 0; i < routes.size(); i++) {
incSucceededCounter();
}
} catch (Exception e) {
- LOG.debug("Reloading {} changed file(s) together failed, reloading
them one at a time to find the file"
- + " that is wrong: {}",
- changed.size(), e.getMessage(), e);
- super.onReloadBatch(changed);
+ LOG.debug("Reloading {} changed route file(s) together failed,
reloading them one at a time to find the"
+ + " file that is wrong: {}",
+ routes.size(), e.getMessage(), e);
+ super.onReloadBatch(routeFiles);
}
}