This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch reload
in repository https://gitbox.apache.org/repos/asf/camel.git

commit a8d8788bdda4f2a0e48d7d613a0ea20a709f975d
Author: Claus Ibsen <claus.ib...@gmail.com>
AuthorDate: Thu Sep 1 15:00:00 2022 +0200

    CAMEL-18267: ContextReloadStrategy to reload all routes after external 
changes such as properties/vault updated.
---
 .../apache/camel/spi/ContextReloadStrategy.java    |  57 ++++++++++
 .../org/apache/camel/spi/PropertiesComponent.java  |   5 +
 .../java/org/apache/camel/spi/RouteController.java |  16 +++
 .../camel/impl/engine/AbstractCamelContext.java    |  14 +++
 .../impl/engine/DefaultContextReloadStrategy.java  | 118 +++++++++++++++++++++
 .../camel/impl/engine/DefaultRouteController.java  |  10 ++
 .../camel/impl/engine/InternalRouteController.java |  21 ++++
 .../AbstractLocationPropertiesSource.java          |   5 +
 .../properties/DefaultPropertiesLookup.java        |   2 +-
 .../component/properties/PropertiesComponent.java  |   3 +-
 .../org/apache/camel/impl/DefaultCamelContext.java |  24 +++--
 .../impl/lw/LightweightRuntimeCamelContext.java    |  10 ++
 .../camel/impl/CamelContextReloadStrategyTest.java | 107 +++++++++++++++++++
 13 files changed, 384 insertions(+), 8 deletions(-)

diff --git 
a/core/camel-api/src/main/java/org/apache/camel/spi/ContextReloadStrategy.java 
b/core/camel-api/src/main/java/org/apache/camel/spi/ContextReloadStrategy.java
new file mode 100644
index 00000000000..b240f26dc04
--- /dev/null
+++ 
b/core/camel-api/src/main/java/org/apache/camel/spi/ContextReloadStrategy.java
@@ -0,0 +1,57 @@
+/*
+ * 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.spi;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.StaticService;
+
+/**
+ * SPI strategy for reloading {@link CamelContext}.
+ *
+ * The reloading is limited to:
+ * - all routes (reload only changes from routes that has been loaded as a 
{@link Resource}.
+ * - all {@link LoadablePropertiesSource} properties.
+ *
+ * General services in the {@link CamelContext} is not reloaded.
+ *
+ * @see ResourceReloadStrategy
+ */
+public interface ContextReloadStrategy extends StaticService, 
CamelContextAware {
+
+    /**
+     * Trigger reload of the {@link CamelContext}.
+     *
+     * @param source source that triggers the reloading.
+     */
+    void onReload(Object source);
+
+    /**
+     * Number of reloads succeeded.
+     */
+    int getReloadCounter();
+
+    /**
+     * Number of reloads failed.
+     */
+    int getFailedCounter();
+
+    /**
+     * Reset the counters.
+     */
+    void resetCounters();
+}
diff --git 
a/core/camel-api/src/main/java/org/apache/camel/spi/PropertiesComponent.java 
b/core/camel-api/src/main/java/org/apache/camel/spi/PropertiesComponent.java
index 8b12fe48243..f225f255977 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/PropertiesComponent.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/PropertiesComponent.java
@@ -176,6 +176,11 @@ public interface PropertiesComponent extends StaticService 
{
      */
     PropertiesSource getPropertiesSource(String name);
 
+    /**
+     * Gets the properties sources
+     */
+    List<PropertiesSource> getPropertiesSources();
+
     /**
      * Registers the {@link PropertiesFunction} as a function to this 
component.
      */
diff --git 
a/core/camel-api/src/main/java/org/apache/camel/spi/RouteController.java 
b/core/camel-api/src/main/java/org/apache/camel/spi/RouteController.java
index 3f1c4f6b524..5b7eefcff34 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/RouteController.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/RouteController.java
@@ -99,6 +99,22 @@ public interface RouteController extends CamelContextAware, 
StaticService {
      */
     boolean isStartingRoutes();
 
+    /**
+     * Reloads all the routes
+     *
+     * @throws Exception is thrown if a route could not be reloaded for 
whatever reason
+     */
+    void reloadAllRoutes() throws Exception;
+
+    /**
+     * Indicates whether current thread is reloading route(s).
+     * <p/>
+     * This can be useful to know by {@link LifecycleStrategy} or the likes, 
in case they need to react differently.
+     *
+     * @return <tt>true</tt> if current thread is reloading route(s), or 
<tt>false</tt> if not.
+     */
+    boolean isReloadingRoutes();
+
     /**
      * Returns the current status of the given route
      *
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
index e685def233e..86960f4d2be 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
@@ -223,6 +223,7 @@ public abstract class AbstractCamelContext extends 
BaseService
     private final List<LifecycleStrategy> lifecycleStrategies = new 
CopyOnWriteArrayList<>();
     private final ThreadLocal<Boolean> isStartingRoutes = new ThreadLocal<>();
     private final ThreadLocal<Boolean> isSetupRoutes = new ThreadLocal<>();
+    private final ThreadLocal<Boolean> isLockModel = new ThreadLocal<>();
     private final Map<String, FactoryFinder> factories = new 
ConcurrentHashMap<>();
     private final Map<String, FactoryFinder> bootstrapFactories = new 
ConcurrentHashMap<>();
     private volatile FactoryFinder bootstrapFactoryFinder;
@@ -1228,6 +1229,19 @@ public abstract class AbstractCamelContext extends 
BaseService
         }
     }
 
+    public boolean isLockModel() {
+        Boolean answer = isLockModel.get();
+        return answer != null && answer;
+    }
+
+    public void setLockModel(boolean lockModel) {
+        if (lockModel) {
+            isLockModel.set(true);
+        } else {
+            isLockModel.remove();
+        }
+    }
+
     @Override
     public boolean isSetupRoutes() {
         Boolean answer = isSetupRoutes.get();
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultContextReloadStrategy.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultContextReloadStrategy.java
new file mode 100644
index 00000000000..5d3700dae71
--- /dev/null
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultContextReloadStrategy.java
@@ -0,0 +1,118 @@
+/*
+ * 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.impl.engine;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.api.management.ManagedAttribute;
+import org.apache.camel.api.management.ManagedOperation;
+import org.apache.camel.spi.ContextReloadStrategy;
+import org.apache.camel.spi.PropertiesComponent;
+import org.apache.camel.spi.PropertiesSource;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.support.service.ServiceSupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DefaultContextReloadStrategy extends ServiceSupport implements 
ContextReloadStrategy {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(DefaultContextReloadStrategy.class);
+
+    private CamelContext camelContext;
+    private int succeeded;
+    private int failed;
+
+    @Override
+    public CamelContext getCamelContext() {
+        return camelContext;
+    }
+
+    @Override
+    public void setCamelContext(CamelContext camelContext) {
+        this.camelContext = camelContext;
+    }
+
+    @Override
+    public void onReload(Object source) {
+        LOG.info("Reloading CamelContext ({}) triggered by: {}", 
camelContext.getName(), source);
+
+        try {
+            reloadProperties(source);
+            reloadRoutes(source);
+            incSucceededCounter();
+        } catch (Exception e) {
+            incFailedCounter();
+            LOG.warn("Error reloading CamelContext (" + camelContext.getName() 
+ ") due to: " + e.getMessage(), e);
+        }
+    }
+
+    protected void reloadRoutes(Object source) throws Exception {
+        getCamelContext().getRouteController().reloadAllRoutes();
+    }
+
+    protected void reloadProperties(Object source) throws Exception {
+        PropertiesComponent pc = getCamelContext().getPropertiesComponent();
+        for (PropertiesSource ps : pc.getPropertiesSources()) {
+            // reload by restarting
+            ServiceHelper.stopAndShutdownService(ps);
+            ServiceHelper.startService(ps);
+        }
+    }
+
+    @ManagedAttribute(description = "Number of reloads succeeded")
+    public int getReloadCounter() {
+        return succeeded;
+    }
+
+    @ManagedAttribute(description = "Number of reloads failed")
+    public int getFailedCounter() {
+        return failed;
+    }
+
+    public void setSucceeded(int succeeded) {
+        this.succeeded = succeeded;
+    }
+
+    public void setFailed(int failed) {
+        this.failed = failed;
+    }
+
+    @ManagedOperation(description = "Reset counters")
+    public void resetCounters() {
+        succeeded = 0;
+        failed = 0;
+    }
+
+    protected void incSucceededCounter() {
+        succeeded++;
+    }
+
+    protected void incFailedCounter() {
+        failed++;
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        // noop
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        // noop
+    }
+
+}
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRouteController.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRouteController.java
index 0fc55b76019..3b249751e12 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRouteController.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRouteController.java
@@ -109,6 +109,16 @@ public class DefaultRouteController extends ServiceSupport 
implements RouteContr
         return getInternalRouteController().isStartingRoutes();
     }
 
+    @Override
+    public void reloadAllRoutes() throws Exception {
+        getInternalRouteController().reloadAllRoutes();
+    }
+
+    @Override
+    public boolean isReloadingRoutes() {
+        return getInternalRouteController().isReloadingRoutes();
+    }
+
     @Override
     public ServiceStatus getRouteStatus(String routeId) {
         return getInternalRouteController().getRouteStatus(routeId);
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/InternalRouteController.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/InternalRouteController.java
index 97130b57a76..78e3ba51ede 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/InternalRouteController.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/InternalRouteController.java
@@ -83,6 +83,27 @@ class InternalRouteController implements RouteController {
         abstractCamelContext.removeAllRoutes();
     }
 
+    @Override
+    public void reloadAllRoutes() throws Exception {
+        // lock model as we need to preserve the model definitions
+        // during reloading because we need to create new processors from the 
models
+        abstractCamelContext.setLockModel(true);
+        try {
+            abstractCamelContext.removeAllRoutes();
+            // remove left-over route templates and endpoints, so we can start 
on a fresh
+            abstractCamelContext.getEndpointRegistry().clear();
+            // start all routes again
+            abstractCamelContext.startRouteDefinitions();
+        } finally {
+            abstractCamelContext.setLockModel(false);
+        }
+    }
+
+    @Override
+    public boolean isReloadingRoutes() {
+        return abstractCamelContext.isLockModel();
+    }
+
     @Override
     public boolean isStartingRoutes() {
         return abstractCamelContext.isStartingRoutes();
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/AbstractLocationPropertiesSource.java
 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/AbstractLocationPropertiesSource.java
index b4bd4084e85..a11044d76dc 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/AbstractLocationPropertiesSource.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/AbstractLocationPropertiesSource.java
@@ -109,6 +109,11 @@ public abstract class AbstractLocationPropertiesSource 
extends ServiceSupport
         }
     }
 
+    @Override
+    protected void doShutdown() throws Exception {
+        properties.clear();
+    }
+
     /**
      * Strategy to prepare loaded properties before being used by Camel.
      * <p/>
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesLookup.java
 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesLookup.java
index f912b62021c..1fc42e2c653 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesLookup.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesLookup.java
@@ -73,7 +73,7 @@ public class DefaultPropertiesLookup implements 
PropertiesLookup {
         }
         if (answer == null) {
             // try till first found source
-            for (PropertiesSource ps : component.getSources()) {
+            for (PropertiesSource ps : component.getPropertiesSources()) {
                 answer = ps.getProperty(name);
                 if (answer != null) {
                     String source = ps.getName();
diff --git 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/PropertiesComponent.java
 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/PropertiesComponent.java
index 47723392a64..852fc155be8 100644
--- 
a/core/camel-base/src/main/java/org/apache/camel/component/properties/PropertiesComponent.java
+++ 
b/core/camel-base/src/main/java/org/apache/camel/component/properties/PropertiesComponent.java
@@ -666,7 +666,8 @@ public class PropertiesComponent extends ServiceSupport
         return null;
     }
 
-    public List<PropertiesSource> getSources() {
+    @Override
+    public List<PropertiesSource> getPropertiesSources() {
         return sources;
     }
 
diff --git 
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultCamelContext.java
 
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultCamelContext.java
index 1a02fe0c44b..35d9a52bc72 100644
--- 
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultCamelContext.java
+++ 
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultCamelContext.java
@@ -364,7 +364,9 @@ public class DefaultCamelContext extends SimpleCamelContext 
implements ModelCame
         if (model == null && isLightweight()) {
             throw new IllegalStateException("Access to model not supported in 
lightweight mode");
         }
-        model.removeRouteDefinitions(routeDefinitions);
+        if (!isLockModel()) {
+            model.removeRouteDefinitions(routeDefinitions);
+        }
     }
 
     @Override
@@ -372,7 +374,9 @@ public class DefaultCamelContext extends SimpleCamelContext 
implements ModelCame
         if (model == null && isLightweight()) {
             throw new IllegalStateException("Access to model not supported in 
lightweight mode");
         }
-        model.removeRouteDefinition(routeDefinition);
+        if (!isLockModel()) {
+            model.removeRouteDefinition(routeDefinition);
+        }
     }
 
     @Override
@@ -412,7 +416,9 @@ public class DefaultCamelContext extends SimpleCamelContext 
implements ModelCame
         if (model == null && isLightweight()) {
             throw new IllegalStateException("Access to model not supported in 
lightweight mode");
         }
-        model.removeRouteTemplateDefinitions(routeTemplateDefinitions);
+        if (!isLockModel()) {
+            model.removeRouteTemplateDefinitions(routeTemplateDefinitions);
+        }
     }
 
     @Override
@@ -420,7 +426,9 @@ public class DefaultCamelContext extends SimpleCamelContext 
implements ModelCame
         if (model == null && isLightweight()) {
             throw new IllegalStateException("Access to model not supported in 
lightweight mode");
         }
-        model.removeRouteTemplateDefinition(routeTemplateDefinition);
+        if (!isLockModel()) {
+            model.removeRouteTemplateDefinition(routeTemplateDefinition);
+        }
     }
 
     @Override
@@ -428,7 +436,9 @@ public class DefaultCamelContext extends SimpleCamelContext 
implements ModelCame
         if (model == null && isLightweight()) {
             throw new IllegalStateException("Access to model not supported in 
lightweight mode");
         }
-        model.removeRouteTemplateDefinitions(pattern);
+        if (!isLockModel()) {
+            model.removeRouteTemplateDefinitions(pattern);
+        }
     }
 
     @Override
@@ -471,7 +481,9 @@ public class DefaultCamelContext extends SimpleCamelContext 
implements ModelCame
         if (model == null && isLightweight()) {
             throw new IllegalStateException("Access to model not supported in 
lightweight mode");
         }
-        model.removeRouteTemplateDefinitions(pattern);
+        if (!isLockModel()) {
+            model.removeRouteTemplateDefinitions(pattern);
+        }
     }
 
     @Override
diff --git 
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/lw/LightweightRuntimeCamelContext.java
 
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/lw/LightweightRuntimeCamelContext.java
index d653af68007..38fa50f34d8 100644
--- 
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/lw/LightweightRuntimeCamelContext.java
+++ 
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/lw/LightweightRuntimeCamelContext.java
@@ -2177,6 +2177,16 @@ public class LightweightRuntimeCamelContext implements 
ExtendedCamelContext, Cat
                 return false;
             }
 
+            @Override
+            public void reloadAllRoutes() throws Exception {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public boolean isReloadingRoutes() {
+                return false;
+            }
+
             @Override
             public ServiceStatus getRouteStatus(String routeId) {
                 return ServiceStatus.Started;
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/CamelContextReloadStrategyTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/CamelContextReloadStrategyTest.java
new file mode 100644
index 00000000000..56d3abf00ba
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/CamelContextReloadStrategyTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.impl;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.impl.engine.DefaultContextReloadStrategy;
+import org.apache.camel.spi.ContextReloadStrategy;
+import org.apache.camel.spi.PropertiesComponent;
+import org.apache.camel.spi.PropertiesSource;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.support.service.ServiceSupport;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class CamelContextReloadStrategyTest extends ContextTestSupport {
+
+    @Test
+    public void testContextReload() throws Exception {
+        Assertions.assertEquals("Hello 1", 
context.resolvePropertyPlaceholders("{{hello}}"));
+
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedBodiesReceived("Hello 1");
+        template.sendBody("direct:start", "Camel");
+        mock.assertIsSatisfied();
+
+        ContextReloadStrategy crs = 
context.hasService(ContextReloadStrategy.class);
+        Assertions.assertNotNull(crs);
+        crs.onReload("CamelContextReloadStrategyTest");
+
+        Assertions.assertEquals("Hello 2", 
context.resolvePropertyPlaceholders("{{hello}}"));
+
+        // need to re-get endpoint after reload
+        mock = getMockEndpoint("mock:result");
+
+        mock.reset();
+        mock.expectedBodiesReceived("Hello 2");
+        template.sendBody("direct:start", "World");
+        mock.assertIsSatisfied();
+    }
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+
+        PropertiesComponent pc = context.getPropertiesComponent();
+        MySource my = new MySource();
+        ServiceHelper.startService(my);
+        pc.addPropertiesSource(my);
+
+        ContextReloadStrategy crs = new DefaultContextReloadStrategy();
+        context.addService(crs);
+
+        return context;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() throws Exception {
+        return new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                from("direct:start")
+                        .setBody(constant("{{hello}}"))
+                        .to("mock:result");
+            }
+        };
+    }
+
+    private class MySource extends ServiceSupport implements PropertiesSource {
+
+        private int counter;
+
+        @Override
+        public String getName() {
+            return "my";
+        }
+
+        @Override
+        public String getProperty(String name) {
+            if ("hello".equals(name)) {
+                return "Hello " + counter;
+            }
+            return null;
+        }
+
+        @Override
+        protected void doStart() throws Exception {
+            counter++;
+        }
+    }
+}

Reply via email to