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 caa6f9fe3b9d CAMEL-25112: camel-core - The local properties of the
properties component should be per thread (#27018)
caa6f9fe3b9d is described below
commit caa6f9fe3b9d8a6524b5ee95a567394c427a1167
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 28 22:44:50 2026 +0200
CAMEL-25112: camel-core - The local properties of the properties component
should be per thread (#27018)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../component/properties/PropertiesComponent.java | 26 ++++---
.../org/apache/camel/impl/DefaultCamelContext.java | 8 +-
...opertiesComponentLocalPropertiesThreadTest.java | 89 ++++++++++++++++++++++
.../org/apache/camel/main/BaseMainSupport.java | 10 +--
...AiObservabilityConfigurationPropertiesTest.java | 32 ++++++++
5 files changed, 147 insertions(+), 18 deletions(-)
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 b6fb3823653a..7afb4c9fb2f8 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
@@ -16,13 +16,14 @@
*/
package org.apache.camel.component.properties;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Deque;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
-import java.util.Stack;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -123,7 +124,8 @@ public class PropertiesComponent extends ServiceSupport
private boolean defaultFallbackEnabled = true;
private Properties initialProperties;
private Properties overrideProperties;
- private final Stack<Properties> localProperties = new Stack<>();;
+ // the local properties are per thread (such as route template parameters
when creating a route from a template)
+ private final ThreadLocal<Deque<Properties>> localProperties = new
ThreadLocal<>();
private int systemPropertiesMode = SYSTEM_PROPERTIES_MODE_OVERRIDE;
private int environmentVariableMode = ENVIRONMENT_VARIABLES_MODE_OVERRIDE;
private boolean autoDiscoverPropertiesSources = true;
@@ -562,10 +564,18 @@ public class PropertiesComponent extends ServiceSupport
@Override
public void setLocalProperties(Properties localProperties) {
+ Deque<Properties> stack = this.localProperties.get();
if (localProperties != null) {
- this.localProperties.push(localProperties);
- } else if (!this.localProperties.isEmpty()) {
- this.localProperties.pop();
+ if (stack == null) {
+ stack = new ArrayDeque<>();
+ this.localProperties.set(stack);
+ }
+ stack.push(localProperties);
+ } else if (stack != null) {
+ stack.poll();
+ if (stack.isEmpty()) {
+ this.localProperties.remove();
+ }
}
}
@@ -574,10 +584,8 @@ public class PropertiesComponent extends ServiceSupport
* currently in use.
*/
public Properties getLocalProperties() {
- if (localProperties.isEmpty()) {
- return null;
- }
- return localProperties.peek();
+ Deque<Properties> stack = this.localProperties.get();
+ return stack != null ? stack.peek() : null;
}
@Override
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 50631a7fffd6..0a2f8fab2927 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
@@ -655,6 +655,7 @@ public class DefaultCamelContext extends SimpleCamelContext
implements ModelCame
RouteDefinitionHelper.forceAssignIds(getCamelContextReference(),
routeDefinitions);
List<RouteDefinition> routeDefinitionsToRemove = null;
for (RouteDefinition routeDefinition : routeDefinitions) {
+ boolean localPropertiesAdded = false;
try {
// assign ids to the routes and validate that the id's is all
unique
String duplicate =
RouteDefinitionHelper.validateUniqueIds(routeDefinition, routeDefinitions,
@@ -730,6 +731,7 @@ public class DefaultCamelContext extends SimpleCamelContext
implements ModelCame
prop.putAll(routeDefinition.getLocation(), params);
}
pc.setLocalProperties(prop);
+ localPropertiesAdded = true;
// we need to shadow the bean registry on the CamelContext
with the local beans
// from the route template context
@@ -780,8 +782,10 @@ public class DefaultCamelContext extends
SimpleCamelContext implements ModelCame
routeDefinitionsToRemove.add(routeDefinition);
}
} finally {
- // clear local after the route is created via the reifier
- pc.setLocalProperties(null);
+ // clear local after the route is created via the reifier
(only if added for this route)
+ if (localPropertiesAdded) {
+ pc.setLocalProperties(null);
+ }
if (localBeans != null) {
localBeans.setLocalBeanRepository(null);
}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesComponentLocalPropertiesThreadTest.java
b/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesComponentLocalPropertiesThreadTest.java
new file mode 100644
index 000000000000..8160cec79a85
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesComponentLocalPropertiesThreadTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.component.properties;
+
+import java.util.Properties;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * The local properties (such as the parameters of a route template when
creating a route) are only for the current
+ * thread.
+ */
+public class PropertiesComponentLocalPropertiesThreadTest extends
ContextTestSupport {
+
+ @Override
+ public boolean isUseRouteBuilder() {
+ return false;
+ }
+
+ @Test
+ public void testLocalPropertiesPerThread() throws Exception {
+ context.getPropertiesComponent().addInitialProperty("name", "global");
+ context.start();
+
+ Properties local = new Properties();
+ local.setProperty("name", "local");
+ PropertiesComponent pc = (PropertiesComponent)
context.getPropertiesComponent();
+ pc.setLocalProperties(local);
+ try {
+ assertEquals("local",
context.resolvePropertyPlaceholders("{{name}}"));
+ // another thread does not see the local properties of this thread
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try {
+ String other = executor.submit(() ->
context.resolvePropertyPlaceholders("{{name}}")).get();
+ assertEquals("global", other);
+ } finally {
+ executor.shutdownNow();
+ }
+ } finally {
+ pc.setLocalProperties(null);
+ }
+ assertNull(pc.getLocalProperties());
+ assertEquals("global",
context.resolvePropertyPlaceholders("{{name}}"));
+ }
+
+ @Test
+ public void testAddRouteKeepsOuterLocalProperties() throws Exception {
+ context.start();
+
+ Properties outer = new Properties();
+ outer.setProperty("name", "outer");
+ PropertiesComponent pc = (PropertiesComponent)
context.getPropertiesComponent();
+ pc.setLocalProperties(outer);
+ try {
+ // adding a route that is not from a route template must not
remove the local properties
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start").to("mock:result");
+ }
+ });
+ assertSame(outer, pc.getLocalProperties());
+ } finally {
+ pc.setLocalProperties(null);
+ }
+ }
+}
diff --git
a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
index b4f11105541e..58528d1dc5e7 100644
--- a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
+++ b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
@@ -2782,13 +2782,9 @@ public abstract class BaseMainSupport extends
BaseService {
failIfNotSet, true, autoConfiguredProperties);
if (mainConfigurationProperties.hasAiObservabilityConfiguration() ||
!properties.isEmpty()) {
- PropertiesComponent pc = camelContext.getPropertiesComponent();
- Properties local = pc.getLocalProperties();
- if (local == null) {
- local = new Properties();
- pc.setLocalProperties(local);
- }
- local.setProperty("camel.aiObservability.enabled",
Boolean.toString(config.isEnabled()));
+ // make the setting available to the components when they are in
use (at runtime from any thread)
+
camelContext.getPropertiesComponent().addOverrideProperty("camel.aiObservability.enabled",
+ Boolean.toString(config.isEnabled()));
}
}
diff --git
a/core/camel-main/src/test/java/org/apache/camel/main/AiObservabilityConfigurationPropertiesTest.java
b/core/camel-main/src/test/java/org/apache/camel/main/AiObservabilityConfigurationPropertiesTest.java
index 1518938daec3..aa050fb27d05 100644
---
a/core/camel-main/src/test/java/org/apache/camel/main/AiObservabilityConfigurationPropertiesTest.java
+++
b/core/camel-main/src/test/java/org/apache/camel/main/AiObservabilityConfigurationPropertiesTest.java
@@ -16,7 +16,11 @@
*/
package org.apache.camel.main;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
import org.apache.camel.PropertyBindingException;
+import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.ai.observability.GenAiObservability;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.util.OrderedLocationProperties;
@@ -41,6 +45,34 @@ class AiObservabilityConfigurationPropertiesTest {
}
}
+ @Test
+ void shouldDisableGenAiObservabilityWithRoutesAndOtherThreads() throws
Exception {
+ Main main = new Main();
+ main.configure().aiObservability().withEnabled(false);
+ main.configure().addRoutesBuilder(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start").to("log:foo");
+ }
+ });
+
+ main.start();
+
+ try {
+ // the setting must be available after the routes are started, and
from other threads (such as the
+ // threads routing messages)
+
assertThat(GenAiObservability.isEnabled(main.getCamelContext())).isFalse();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try {
+ assertThat(executor.submit(() ->
GenAiObservability.isEnabled(main.getCamelContext())).get()).isFalse();
+ } finally {
+ executor.shutdownNow();
+ }
+ } finally {
+ main.stop();
+ }
+ }
+
@Test
void shouldDisableGenAiObservabilityViaApplicationProperties() throws
Exception {
Main main = new Main();