This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-25661-reload-interval in repository https://gitbox.apache.org/repos/asf/camel.git
commit 5070839b7fe1a074e36d6a67553dc3ef52d0d4ed Author: Claus Ibsen <[email protected]> AuthorDate: Thu Oct 15 14:30:59 2026 +0200 CAMEL-25661: camel-jbang - AI write tools reload sooner: camel_run scans every 500 ms The write tools wait for dev mode to reload the file they wrote, and dev mode finds a change at its next folder scan, every 2000 ms. - camel-main: camel.main.routesReloadInterval (millis, default 2000) sets how often dev mode scans; the reload strategy shows it as a JMX attribute. - camel_run starts integrations in dev mode with an interval of 500 ms, as the optional key camel.main.?routesReloadInterval, so an older camel on the PATH still starts. camel run --dev for people keeps 2000 ms. - The write tools read the log every 100 ms for the reload and its output, and a properties reload is watched 500 ms longer for the routes reload after it. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m --- .../org/apache/camel/catalog/docs/main.adoc | 3 +- .../main/camel-main-configuration-metadata.json | 1 + .../MainConfigurationPropertiesConfigurer.java | 7 ++ .../camel-main-configuration-metadata.json | 1 + core/camel-main/src/main/docs/main.adoc | 3 +- .../camel/main/DefaultConfigurationConfigurer.java | 3 + .../camel/main/DefaultConfigurationProperties.java | 25 ++++++++ .../camel/main/MainRoutesReloadIntervalTest.java | 74 ++++++++++++++++++++++ .../support/FileWatcherResourceReloadStrategy.java | 5 ++ .../modules/ROOT/pages/camel-jbang-mcp.adoc | 5 +- .../modules/ROOT/pages/camel-jbang-running.adoc | 11 ++++ .../modules/ROOT/pages/route-reload.adoc | 1 + .../core/commands/ai/IntegrationLauncher.java | 12 ++++ .../dsl/jbang/core/commands/ai/ReloadOutcome.java | 40 ++++++++++-- .../core/commands/ai/IntegrationLauncherTest.java | 10 +-- .../jbang/core/commands/ai/ReloadOutcomeTest.java | 28 +++++++- 16 files changed, 214 insertions(+), 15 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc index 3412a9aad3c3..a780e2eabb67 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc @@ -19,7 +19,7 @@ The following tables lists all the options: // main options: START === Camel Main configurations -The camel.main supports 131 options, which are listed below. +The camel.main supports 132 options, which are listed below. [width="100%",cols="2,5,^1,2",options="header"] |=== @@ -111,6 +111,7 @@ The camel.main supports 131 options, which are listed below. | `camel.main.routesReloadDirectory` | Directory to scan for route changes. Camel cannot scan the classpath, so this must be configured to a file directory. Development with Maven as build tool, you can configure the directory to be src/main/resources to scan for Camel routes in XML or YAML files. | src/main/resources/camel | String | `camel.main.routesReloadDirectoryRecursive` | Whether the directory to scan should include sub directories. Depending on the number of sub directories, then this can cause the JVM to startup slower as Camel uses the JDK file-watch service to scan for file changes. | false | boolean | `camel.main.routesReloadEnabled` | Used for enabling automatic routes reloading. If enabled then Camel will watch for file changes in the given reload directory, and trigger reloading routes if files are changed. | false | boolean +| `camel.main.routesReloadInterval` | How often to scan the reload directory for changed files, in millis. A change is reloaded at the next scan, so a shorter interval reloads it sooner. A longer interval groups more of one save together: two files saved a second apart are one change at 2000 and two at 1000. | 2000 | long | `camel.main.routesReloadPattern` | Used for inclusive filtering of routes from directories. Typical used for specifying to accept routes in XML or YAML files, such as .yaml,.xml. Multiple patterns can be specified separated by comma. | | String | `camel.main.routesReloadRemoveAllRoutes` | When reloading routes should all existing routes be stopped and removed. By default, Camel will stop and remove all existing routes before reloading routes. This ensures that only the reloaded routes will be active. If disabled then only routes with the same route id is updated, and any existing routes are continued to run. | true | boolean | `camel.main.routesReloadRestartDuration` | Whether to restart max duration when routes are reloaded. For example if max duration is 60 seconds, and a route is reloaded after 25 seconds, then this will restart the count and wait 60 seconds again. | false | boolean diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json index 2de54506bdbe..cd2101ee33b7 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json @@ -125,6 +125,7 @@ { "name": "camel.main.routesReloadDirectory", "required": false, "description": "Directory to scan for route changes. Camel cannot scan the classpath, so this must be configured to a file directory. Development with Maven as build tool, you can configure the directory to be src\/main\/resources to scan for Camel routes in XML or YAML files.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType": "java.lang.String", "defaultValue": "src\/m [...] { "name": "camel.main.routesReloadDirectoryRecursive", "required": false, "description": "Whether the directory to scan should include sub directories. Depending on the number of sub directories, then this can cause the JVM to startup slower as Camel uses the JDK file-watch service to scan for file changes.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, { "name": "camel.main.routesReloadEnabled", "required": false, "description": "Used for enabling automatic routes reloading. If enabled then Camel will watch for file changes in the given reload directory, and trigger reloading routes if files are changed.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, + { "name": "camel.main.routesReloadInterval", "required": false, "description": "How often to scan the reload directory for changed files, in millis. A change is reloaded at the next scan, so a shorter interval reloads it sooner. A longer interval groups more of one save together: two files saved a second apart are one change at 2000 and two at 1000.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "integer", "javaType": "long", "defaultValue": 2000, "se [...] { "name": "camel.main.routesReloadPattern", "required": false, "description": "Used for inclusive filtering of routes from directories. Typical used for specifying to accept routes in XML or YAML files, such as .yaml,.xml. Multiple patterns can be specified separated by comma.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType": "java.lang.String", "secret": false }, { "name": "camel.main.routesReloadRemoveAllRoutes", "required": false, "description": "When reloading routes should all existing routes be stopped and removed. By default, Camel will stop and remove all existing routes before reloading routes. This ensures that only the reloaded routes will be active. If disabled then only routes with the same route id is updated, and any existing routes are continued to run.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "ty [...] { "name": "camel.main.routesReloadRestartDuration", "required": false, "description": "Whether to restart max duration when routes are reloaded. For example if max duration is 60 seconds, and a route is reloaded after 25 seconds, then this will restart the count and wait 60 seconds again.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, diff --git a/core/camel-main/src/generated/java/org/apache/camel/main/MainConfigurationPropertiesConfigurer.java b/core/camel-main/src/generated/java/org/apache/camel/main/MainConfigurationPropertiesConfigurer.java index c4757a38ff27..46ae4a90a907 100644 --- a/core/camel-main/src/generated/java/org/apache/camel/main/MainConfigurationPropertiesConfigurer.java +++ b/core/camel-main/src/generated/java/org/apache/camel/main/MainConfigurationPropertiesConfigurer.java @@ -109,6 +109,7 @@ public class MainConfigurationPropertiesConfigurer extends org.apache.camel.supp map.put("RoutesReloadDirectory", java.lang.String.class); map.put("RoutesReloadDirectoryRecursive", boolean.class); map.put("RoutesReloadEnabled", boolean.class); + map.put("RoutesReloadInterval", long.class); map.put("RoutesReloadPattern", java.lang.String.class); map.put("RoutesReloadRemoveAllRoutes", boolean.class); map.put("RoutesReloadRestartDuration", boolean.class); @@ -328,6 +329,8 @@ public class MainConfigurationPropertiesConfigurer extends org.apache.camel.supp case "routesReloadDirectoryRecursive": target.setRoutesReloadDirectoryRecursive(property(camelContext, boolean.class, value)); return true; case "routesreloadenabled": case "routesReloadEnabled": target.setRoutesReloadEnabled(property(camelContext, boolean.class, value)); return true; + case "routesreloadinterval": + case "routesReloadInterval": target.setRoutesReloadInterval(property(camelContext, long.class, value)); return true; case "routesreloadpattern": case "routesReloadPattern": target.setRoutesReloadPattern(property(camelContext, java.lang.String.class, value)); return true; case "routesreloadremoveallroutes": @@ -595,6 +598,8 @@ public class MainConfigurationPropertiesConfigurer extends org.apache.camel.supp case "routesReloadDirectoryRecursive": return boolean.class; case "routesreloadenabled": case "routesReloadEnabled": return boolean.class; + case "routesreloadinterval": + case "routesReloadInterval": return long.class; case "routesreloadpattern": case "routesReloadPattern": return java.lang.String.class; case "routesreloadremoveallroutes": @@ -858,6 +863,8 @@ public class MainConfigurationPropertiesConfigurer extends org.apache.camel.supp case "routesReloadDirectoryRecursive": return target.isRoutesReloadDirectoryRecursive(); case "routesreloadenabled": case "routesReloadEnabled": return target.isRoutesReloadEnabled(); + case "routesreloadinterval": + case "routesReloadInterval": return target.getRoutesReloadInterval(); case "routesreloadpattern": case "routesReloadPattern": return target.getRoutesReloadPattern(); case "routesreloadremoveallroutes": diff --git a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json index 2de54506bdbe..cd2101ee33b7 100644 --- a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json +++ b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json @@ -125,6 +125,7 @@ { "name": "camel.main.routesReloadDirectory", "required": false, "description": "Directory to scan for route changes. Camel cannot scan the classpath, so this must be configured to a file directory. Development with Maven as build tool, you can configure the directory to be src\/main\/resources to scan for Camel routes in XML or YAML files.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType": "java.lang.String", "defaultValue": "src\/m [...] { "name": "camel.main.routesReloadDirectoryRecursive", "required": false, "description": "Whether the directory to scan should include sub directories. Depending on the number of sub directories, then this can cause the JVM to startup slower as Camel uses the JDK file-watch service to scan for file changes.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, { "name": "camel.main.routesReloadEnabled", "required": false, "description": "Used for enabling automatic routes reloading. If enabled then Camel will watch for file changes in the given reload directory, and trigger reloading routes if files are changed.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, + { "name": "camel.main.routesReloadInterval", "required": false, "description": "How often to scan the reload directory for changed files, in millis. A change is reloaded at the next scan, so a shorter interval reloads it sooner. A longer interval groups more of one save together: two files saved a second apart are one change at 2000 and two at 1000.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "integer", "javaType": "long", "defaultValue": 2000, "se [...] { "name": "camel.main.routesReloadPattern", "required": false, "description": "Used for inclusive filtering of routes from directories. Typical used for specifying to accept routes in XML or YAML files, such as .yaml,.xml. Multiple patterns can be specified separated by comma.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType": "java.lang.String", "secret": false }, { "name": "camel.main.routesReloadRemoveAllRoutes", "required": false, "description": "When reloading routes should all existing routes be stopped and removed. By default, Camel will stop and remove all existing routes before reloading routes. This ensures that only the reloaded routes will be active. If disabled then only routes with the same route id is updated, and any existing routes are continued to run.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "ty [...] { "name": "camel.main.routesReloadRestartDuration", "required": false, "description": "Whether to restart max duration when routes are reloaded. For example if max duration is 60 seconds, and a route is reloaded after 25 seconds, then this will restart the count and wait 60 seconds again.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": false }, diff --git a/core/camel-main/src/main/docs/main.adoc b/core/camel-main/src/main/docs/main.adoc index 3412a9aad3c3..a780e2eabb67 100644 --- a/core/camel-main/src/main/docs/main.adoc +++ b/core/camel-main/src/main/docs/main.adoc @@ -19,7 +19,7 @@ The following tables lists all the options: // main options: START === Camel Main configurations -The camel.main supports 131 options, which are listed below. +The camel.main supports 132 options, which are listed below. [width="100%",cols="2,5,^1,2",options="header"] |=== @@ -111,6 +111,7 @@ The camel.main supports 131 options, which are listed below. | `camel.main.routesReloadDirectory` | Directory to scan for route changes. Camel cannot scan the classpath, so this must be configured to a file directory. Development with Maven as build tool, you can configure the directory to be src/main/resources to scan for Camel routes in XML or YAML files. | src/main/resources/camel | String | `camel.main.routesReloadDirectoryRecursive` | Whether the directory to scan should include sub directories. Depending on the number of sub directories, then this can cause the JVM to startup slower as Camel uses the JDK file-watch service to scan for file changes. | false | boolean | `camel.main.routesReloadEnabled` | Used for enabling automatic routes reloading. If enabled then Camel will watch for file changes in the given reload directory, and trigger reloading routes if files are changed. | false | boolean +| `camel.main.routesReloadInterval` | How often to scan the reload directory for changed files, in millis. A change is reloaded at the next scan, so a shorter interval reloads it sooner. A longer interval groups more of one save together: two files saved a second apart are one change at 2000 and two at 1000. | 2000 | long | `camel.main.routesReloadPattern` | Used for inclusive filtering of routes from directories. Typical used for specifying to accept routes in XML or YAML files, such as .yaml,.xml. Multiple patterns can be specified separated by comma. | | String | `camel.main.routesReloadRemoveAllRoutes` | When reloading routes should all existing routes be stopped and removed. By default, Camel will stop and remove all existing routes before reloading routes. This ensures that only the reloaded routes will be active. If disabled then only routes with the same route id is updated, and any existing routes are continued to run. | true | boolean | `camel.main.routesReloadRestartDuration` | Whether to restart max duration when routes are reloaded. For example if max duration is 60 seconds, and a route is reloaded after 25 seconds, then this will restart the count and wait 60 seconds again. | false | boolean diff --git a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java index 4e11c0784e4e..b183dc4d7471 100644 --- a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java +++ b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java @@ -306,6 +306,9 @@ public final class DefaultConfigurationConfigurer { config.getRoutesReloadDirectory(), config.isRoutesReloadDirectoryRecursive()); reloader.setPattern(config.getRoutesReloadPattern()); reloader.setRemoveAllRoutes(config.isRoutesReloadRemoveAllRoutes()); + if (config.getRoutesReloadInterval() > 0) { + reloader.setPollTimeout(config.getRoutesReloadInterval()); + } // the files that declare beans, so a bean is created again when its Java class changes reloader.setBeanResources(() -> { Model model = camelContext.getCamelContextExtension().getContextPlugin(Model.class); diff --git a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationProperties.java b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationProperties.java index ae5d2560af40..667b49423f1b 100644 --- a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationProperties.java +++ b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationProperties.java @@ -142,6 +142,8 @@ public abstract class DefaultConfigurationProperties<T> { @Metadata(defaultValue = "true") private boolean routesReloadRemoveAllRoutes = true; private boolean routesReloadRestartDuration; + @Metadata(defaultValue = "2000") + private long routesReloadInterval = 2000; private String groovyScriptPattern = "classpath:camel-groovy/*,classpath:camel-groovy-compiled/*"; @Metadata(label = "advanced") private boolean groovyPreloadCompiled; @@ -1498,6 +1500,19 @@ public abstract class DefaultConfigurationProperties<T> { this.routesReloadRestartDuration = routesReloadRestartDuration; } + public long getRoutesReloadInterval() { + return routesReloadInterval; + } + + /** + * How often to scan the reload directory for changed files, in millis. A change is reloaded at the next scan, so a + * shorter interval reloads it sooner. A longer interval groups more of one save together: two files saved a second + * apart are one change at 2000 and two at 1000. + */ + public void setRoutesReloadInterval(long routesReloadInterval) { + this.routesReloadInterval = routesReloadInterval; + } + public boolean isJmxUpdateRouteEnabled() { return jmxUpdateRouteEnabled; } @@ -2833,6 +2848,16 @@ public abstract class DefaultConfigurationProperties<T> { return (T) this; } + /** + * How often to scan the reload directory for changed files, in millis. A change is reloaded at the next scan, so a + * shorter interval reloads it sooner. A longer interval groups more of one save together: two files saved a second + * apart are one change at 2000 and two at 1000. + */ + public T withRoutesReloadInterval(long routesReloadInterval) { + this.routesReloadInterval = routesReloadInterval; + return (T) this; + } + /** * Controls whether to pool (reuse) exchanges or create new fresh exchanges (default). Using pooled will reduce JVM * garbage collection overhead by avoiding to re-create Exchange instances per message each consumer receives. diff --git a/core/camel-main/src/test/java/org/apache/camel/main/MainRoutesReloadIntervalTest.java b/core/camel-main/src/test/java/org/apache/camel/main/MainRoutesReloadIntervalTest.java new file mode 100644 index 000000000000..23c970e2d26e --- /dev/null +++ b/core/camel-main/src/test/java/org/apache/camel/main/MainRoutesReloadIntervalTest.java @@ -0,0 +1,74 @@ +/* + * 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.main; + +import java.nio.file.Path; + +import org.apache.camel.support.RouteWatcherReloadStrategy; +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.assertNotNull; + +public class MainRoutesReloadIntervalTest { + + @TempDir + Path dir; + + @Test + public void shouldScanEveryTwoSecondsByDefault() { + assertEquals(2000, scanInterval(null)); + } + + @Test + public void shouldScanAtTheConfiguredInterval() { + assertEquals(500, scanInterval("500")); + } + + @Test + public void shouldScanAtTheIntervalOfAnOptionalKey() { + // the camel_run AI tool passes the optional key, which a Camel without the option ignores + assertEquals(500, scanInterval("camel.main.?routesReloadInterval", "500")); + } + + @Test + public void shouldKeepTheDefaultForAnIntervalOfZero() { + // zero would scan without a pause + assertEquals(2000, scanInterval("0")); + } + + private long scanInterval(String interval) { + return scanInterval("camel.main.routesReloadInterval", interval); + } + + private long scanInterval(String key, String interval) { + Main main = new Main(); + main.configure().withRoutesReloadEnabled(true).withRoutesReloadDirectory(dir.toString()); + if (interval != null) { + main.addInitialProperty(key, interval); + } + main.start(); + try { + RouteWatcherReloadStrategy reloader = main.getCamelContext().hasService(RouteWatcherReloadStrategy.class); + assertNotNull(reloader, "routesReloadEnabled=true should add the reload strategy"); + return reloader.getPollTimeout(); + } finally { + main.stop(); + } + } +} 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 4b3ae2e3f609..9e92185c506a 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 @@ -127,6 +127,11 @@ public class FileWatcherResourceReloadStrategy extends ResourceReloadStrategySup return folder; } + @ManagedAttribute(description = "How often the folder is scanned for changed files, in millis") + public long getPollTimeout() { + return pollTimeout; + } + @ManagedAttribute(description = "Whether the reload strategy watches directory recursively") public boolean isRecursive() { return isRecursive; diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc index 3b289667a76c..a45aacc05e7f 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc @@ -398,8 +398,9 @@ project `directory` as an argument, the runtime tools take the integration `name | `camel_run` | Starts an integration from a project directory with `camel run --source-dir` in a separate process, in dev mode by default: the directory is watched, so a changed or added file (a route, a bean file, a Java class) is - reloaded. Name files to run only those, for a directory that holds several apps. Returns the pid, name and - log file once the integration is up. When an integration already runs from the directory, or one started + reloaded. It is scanned every 500 ms (`camel.main.routesReloadInterval`), not every 2 seconds as by + `camel run --dev`, so a write is reloaded and answered sooner. Name files to run only those, for a directory + that holds several apps. Returns the pid, name and log file once the integration is up. When an integration already runs from the directory, or one started earlier is still starting, no second copy is started: the answer is that one (`status: running` with its pid, name and log file, or `status: starting`), with what dev mode and `camel_control restart` do for it. When the start fails, the answer has the cause from the console output: each log record with its exception diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc index 9505dc9628c7..bcf3613c7fef 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc @@ -17,6 +17,17 @@ camel run foo.yaml --dev This works for all DSLs (YAML, Java, XML). +Dev mode looks for changed files every 2 seconds. To look more often, set `camel.main.routesReloadInterval` +(in millis): + +[source,bash] +---- +camel run foo.yaml --dev --prop=camel.main.routesReloadInterval=500 +---- + +A shorter interval reloads a change sooner. A longer one groups more of one save together, such as several files +an editor saves one after the other. + `camel dev` is a shortcut for `camel run --dev`, and without files it runs the current folder (as `--source-dir=.`): diff --git a/docs/user-manual/modules/ROOT/pages/route-reload.adoc b/docs/user-manual/modules/ROOT/pages/route-reload.adoc index ad80593a7b25..3ffcd96d5350 100644 --- a/docs/user-manual/modules/ROOT/pages/route-reload.adoc +++ b/docs/user-manual/modules/ROOT/pages/route-reload.adoc @@ -57,6 +57,7 @@ You can configure the `RouteWatcherReloadStrategy` using the following options: | `routesReloadDirectory` | Directory to scan for route changes. Camel cannot scan the classpath, so this must be configured to a file directory. Development with Maven as build tool, you can configure the directory to be `src/main/resources` to scan for Camel routes in XML or YAML files. | `src/main/resources/camel` | String | `routesReloadDirectoryRecursive` | Whether the directory to scan should include subdirectories. Depending on the number of sub-directories, then this can cause the JVM to start up slower as Camel uses the JDK file-watch service to scan for file changes. | `false` | boolean | `routesReloadEnabled` | Used for enabling automatic routes reloading. If enabled, then Camel will watch for file changes in the given reload directory, and trigger reloading routes if files are changed. | `false` | boolean +| `routesReloadInterval` | How often to scan the reload directory for changed files, in millis. A change is reloaded at the next scan, so a shorter interval reloads it sooner. A longer interval groups more of one save together: two files saved a second apart are one change at 2000 and two at 1000. | `2000` | long | `routesReloadPattern` | Used for inclusive filtering of routes from directories. Typical used for specifying to accept routes in XML or YAML files. The default pattern is \*.yaml,*.xml Multiple patterns can be specified separated by comma. | `\*.yaml,*.xml` | String | `routesReloadRemoveAllRoutes` | When reloading routes should all existing routes be stopped and removed. By default, Camel will stop and remove all existing routes before reloading routes. This ensures that only the reloaded routes will be active. If disabled, then only routes with the same route id are updated, and any existing routes are continued to run. | `true` | boolean |======================================================================= diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java index 75d8046ccd8f..8577ede67471 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java @@ -49,6 +49,16 @@ public final class IntegrationLauncher { /** The last log records of a failed start that are returned. */ static final int FAILURE_RECORDS = 15; + /** + * How often an integration started in dev mode scans for changed files (CAMEL-25661). A write tool waits for the + * reload of the file it wrote, and dev mode finds the change at its next scan: at the default of 2000 ms that is + * about a second per write. Each write tool call writes one file and waits for its reload, so a shorter interval + * does not split one change into two reloads. The key is optional (the ? before the name): the camel command can be + * older than these tools (the MCP server run as LATEST, an older camel on the PATH), and a Camel without the option + * logs a warning and starts, where an unknown key would stop it from starting. + */ + static final String DEV_RELOAD_INTERVAL = "--prop=camel.main.?routesReloadInterval=500"; + /** A stack frame, or the count of the frames a cause shares with the exception it caused. */ private static final Pattern FRAME = Pattern.compile("^\\s*(at \\S.*|\\.\\.\\. \\d+ (more|common frames omitted))$"); @@ -214,6 +224,7 @@ public final class IntegrationLauncher { cmd.add("--source-dir=."); if (dev) { cmd.add("--dev"); + cmd.add(DEV_RELOAD_INTERVAL); } if (name != null && !name.isBlank()) { cmd.add("--name=" + name); @@ -240,6 +251,7 @@ public final class IntegrationLauncher { cmd.addAll(files); if (dev) { cmd.add("--dev"); + cmd.add(DEV_RELOAD_INTERVAL); } if (name != null && !name.isBlank()) { cmd.add("--name=" + name); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java index d3759fcc391b..f816c0b87369 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.function.Supplier; import java.util.regex.Pattern; import org.apache.camel.util.StopWatch; @@ -38,6 +39,18 @@ public final class ReloadOutcome { static final String PROPERTIES = "Reloading properties"; static final String REPORT = "did not load"; + /** + * How often the log is read while waiting: a reload or a line the routes logged is seen at most this long after it + * was logged, and the write answers that much sooner (CAMEL-25661). Only the tail of the log is read. + */ + static final long POLL_MILLIS = 100; + + /** + * How long a properties reload is watched for the reload of the routes after it: the routes are reloaded after the + * properties they use, and a route that fails with the new values is the outcome the agent must hear. + */ + static final long ROUTES_AFTER_PROPERTIES_MILLIS = 500; + private ReloadOutcome() { } @@ -120,19 +133,38 @@ public final class ReloadOutcome { /** Polls the integration's log for the reload of a just written file, up to the timeout. */ public static JsonObject await(long pid, String name, String sinceKey, long timeoutMillis) { + return await(() -> records(pid, name), sinceKey, timeoutMillis); + } + + /** + * Polls the records for the reload of a just written file, up to the timeout. A properties reload is watched + * {@link #ROUTES_AFTER_PROPERTIES_MILLIS} longer for the routes reload after it, whose outcome is then the answer. + */ + static JsonObject await(Supplier<List<JsonObject>> records, String sinceKey, long timeoutMillis) { StopWatch watch = new StopWatch(); + JsonObject properties = null; + long propertiesAt = 0; while (watch.taken() < timeoutMillis) { - JsonObject outcome = classify(records(pid, name), sinceKey); - if (outcome != null) { + JsonObject outcome = classify(records.get(), sinceKey); + if (outcome != null && !"properties".equals(outcome.getString("status"))) { return outcome; } + if (outcome != null && properties == null) { + properties = outcome; + propertiesAt = watch.taken(); + } else if (properties != null && watch.taken() - propertiesAt >= ROUTES_AFTER_PROPERTIES_MILLIS) { + return properties; + } try { - Thread.sleep(500); + Thread.sleep(POLL_MILLIS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } + if (properties != null) { + return properties; + } JsonObject out = new JsonObject(); out.put("status", "unknown"); out.put("message", "no reload seen in the log within " + timeoutMillis / 1000 + "s: the integration may not run" @@ -159,7 +191,7 @@ public final class ReloadOutcome { JsonArray out = output(records(pid, name)); while (out.isEmpty() && watch.taken() < timeoutMillis) { try { - Thread.sleep(300); + Thread.sleep(POLL_MILLIS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java index daf494767ceb..5e1eda1d6eef 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java @@ -42,7 +42,7 @@ class IntegrationLauncherTest { .containsExactly("a.camel.yaml", "application.properties", "b.camel.yaml"); assertThat(IntegrationLauncher.runArguments(IntegrationLauncher.sourceFiles(dir), null, true, null)) .containsExactly("run", "a.camel.yaml", "application.properties", "b.camel.yaml", "--dev", - "--logging-color=false"); + "--prop=camel.main.?routesReloadInterval=500", "--logging-color=false"); assertThat(IntegrationLauncher.sourceFiles(dir.resolve("nope"))).isEmpty(); } @@ -50,7 +50,8 @@ class IntegrationLauncherTest { @Test void noFilesRunsTheDirectoryAsTheApp() { assertThat(IntegrationLauncher.sourceDirArguments(null, true, null)) - .containsExactly("run", "--source-dir=.", "--dev", "--logging-color=false"); + .containsExactly("run", "--source-dir=.", "--dev", "--prop=camel.main.?routesReloadInterval=500", + "--logging-color=false"); assertThat(IntegrationLauncher.sourceDirArguments("demo", false, List.of("--port=9000"))) .containsExactly("run", "--source-dir=.", "--name=demo", "--logging-color=false", "--port=9000"); } @@ -59,8 +60,9 @@ class IntegrationLauncherTest { void filesNameAndExtraArgumentsArePassedThrough() { assertThat(IntegrationLauncher.runArguments(List.of("a.camel.yaml", "application.properties"), "demo", true, List.of("--port=9000"))) - .containsExactly("run", "a.camel.yaml", "application.properties", "--dev", "--name=demo", - "--logging-color=false", "--port=9000"); + .containsExactly("run", "a.camel.yaml", "application.properties", "--dev", + "--prop=camel.main.?routesReloadInterval=500", "--name=demo", "--logging-color=false", + "--port=9000"); } /** diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java index 11343b1776c1..82b97f3e4904 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java @@ -17,6 +17,7 @@ package org.apache.camel.dsl.jbang.core.commands.ai; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; @@ -44,6 +45,9 @@ class ReloadOutcomeTest { "Caused by: java.lang.IllegalArgumentException: Option name is required when creating endpoint uri with syntax cron:name", "\tat org.apache.camel.support.component.AbstractApiEndpoint.x(Foo.java:1)"); + private static final List<String> PROPERTIES = List.of( + "2026-09-21 10:00:30.000 INFO 42 --- [rReloadStrategy] org.apache.camel.main.DefaultConfigurationConfigurer : Reloading properties: file:application.properties"); + private static final List<String> RELOADED = List.of( "2026-09-21 10:00:40.000 INFO 42 --- [rReloadStrategy] org.apache.camel.support.RouteWatcherReloadStrategy : Routes reloaded summary (total:1 started:1)", "2026-09-21 10:00:41.000 INFO 42 --- [ timer://tick] route1 : Hello again"); @@ -80,14 +84,32 @@ class ReloadOutcomeTest { @Test void aPropertiesReloadIsReportedAsSuch() { - List<String> props = List.of( - "2026-09-21 10:00:30.000 INFO 42 --- [rReloadStrategy] org.apache.camel.main.DefaultConfigurationConfigurer : Reloading properties: file:application.properties"); String since = ReloadOutcome.latestReloadKey(records(BEFORE)); - JsonObject out = ReloadOutcome.classify(records(BEFORE, props), since); + JsonObject out = ReloadOutcome.classify(records(BEFORE, PROPERTIES), since); assertEquals("properties", out.getString("status")); assertEquals("Reloading properties: file:application.properties", out.getString("message")); } + /** CAMEL-25661: the routes are reloaded after the properties they use, and their outcome is the answer. */ + @Test + void aPropertiesReloadIsAnsweredWithTheRoutesReloadAfterIt() { + String since = ReloadOutcome.latestReloadKey(records(BEFORE)); + List<List<JsonObject>> polls + = List.of(records(BEFORE), records(BEFORE, PROPERTIES), records(BEFORE, PROPERTIES, RELOADED)); + AtomicInteger poll = new AtomicInteger(); + JsonObject out = ReloadOutcome.await(() -> polls.get(Math.min(poll.getAndIncrement(), polls.size() - 1)), since, + 5000); + assertEquals("reloaded", out.getString("status")); + assertEquals("Routes reloaded summary (total:1 started:1)", out.getString("message")); + } + + @Test + void aPropertiesReloadWithoutARoutesReloadAfterItIsReportedAsSuch() { + String since = ReloadOutcome.latestReloadKey(records(BEFORE)); + JsonObject out = ReloadOutcome.await(() -> records(BEFORE, PROPERTIES), since, 5000); + assertEquals("properties", out.getString("status")); + } + @Test void aReloadAfterTheFailureIsReloaded() { String since = ReloadOutcome.latestReloadKey(records(BEFORE));
