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++; + } + } +}