This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch context-value-scoped-value-support
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to
refs/heads/context-value-scoped-value-support by this push:
new ede2fff4bb33 Extract ThreadLocalContextValue, fix concurrentConsumers
default, fix docs
ede2fff4bb33 is described below
commit ede2fff4bb33531e0ae01e739350f0fc08da9ba9
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Mar 4 09:37:43 2026 +0100
Extract ThreadLocalContextValue, fix concurrentConsumers default, fix docs
- Extract ThreadLocalContextValue from inner class (duplicated in base
and JDK 25 ContextValueFactory) into a shared top-level class
- Default concurrentConsumers to 0 (unlimited) when virtualThreadPerTask
is enabled, since the traditional default of 1 defeats the purpose
- Fix Example 3 in virtual-threads.adoc: ContextValue doesn't propagate
across SEDA boundaries, use exchange properties instead
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
.../apache/camel/component/seda/SedaEndpoint.java | 6 ++
.../camel/util/concurrent/ContextValueFactory.java | 53 ---------------
.../util/concurrent/ThreadLocalContextValue.java | 76 ++++++++++++++++++++++
.../camel/util/concurrent/ContextValueFactory.java | 53 ---------------
.../modules/ROOT/pages/virtual-threads.adoc | 17 ++---
5 files changed, 91 insertions(+), 114 deletions(-)
diff --git
a/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaEndpoint.java
b/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaEndpoint.java
index 5385748e8fb7..8b5cc372819c 100644
---
a/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaEndpoint.java
+++
b/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaEndpoint.java
@@ -630,6 +630,12 @@ public class SedaEndpoint extends DefaultEndpoint
implements AsyncEndpoint, Brow
protected void doInit() throws Exception {
super.doInit();
+ // When virtualThreadPerTask is enabled, default concurrentConsumers
to 0 (unlimited)
+ // since the traditional default of 1 defeats the purpose of
thread-per-task mode
+ if (virtualThreadPerTask && concurrentConsumers == 1) {
+ concurrentConsumers = 0;
+ }
+
if (discardWhenFull && blockWhenFull) {
throw new IllegalArgumentException(
"Cannot enable both discardWhenFull=true and
blockWhenFull=true."
diff --git
a/core/camel-util/src/main/java/org/apache/camel/util/concurrent/ContextValueFactory.java
b/core/camel-util/src/main/java/org/apache/camel/util/concurrent/ContextValueFactory.java
index f3b991c29158..7580d73fcd12 100644
---
a/core/camel-util/src/main/java/org/apache/camel/util/concurrent/ContextValueFactory.java
+++
b/core/camel-util/src/main/java/org/apache/camel/util/concurrent/ContextValueFactory.java
@@ -84,57 +84,4 @@ class ContextValueFactory {
});
}
- /**
- * ThreadLocal-based implementation of ContextValue.
- */
- static class ThreadLocalContextValue<T> implements ContextValue<T> {
- private final String name;
- private final ThreadLocal<T> threadLocal;
-
- ThreadLocalContextValue(String name) {
- this.name = name;
- this.threadLocal = new ThreadLocal<>();
- }
-
- ThreadLocalContextValue(String name, Supplier<T> supplier) {
- this.name = name;
- this.threadLocal = ThreadLocal.withInitial(supplier);
- }
-
- @Override
- public T get() {
- return threadLocal.get();
- }
-
- @Override
- public T orElse(T defaultValue) {
- T value = threadLocal.get();
- return value != null ? value : defaultValue;
- }
-
- @Override
- public boolean isBound() {
- return threadLocal.get() != null;
- }
-
- @Override
- public void set(T value) {
- threadLocal.set(value);
- }
-
- @Override
- public void remove() {
- threadLocal.remove();
- }
-
- @Override
- public String name() {
- return name;
- }
-
- @Override
- public String toString() {
- return "ContextValue[" + name + "]";
- }
- }
}
diff --git
a/core/camel-util/src/main/java/org/apache/camel/util/concurrent/ThreadLocalContextValue.java
b/core/camel-util/src/main/java/org/apache/camel/util/concurrent/ThreadLocalContextValue.java
new file mode 100644
index 000000000000..f2bffdd04d94
--- /dev/null
+++
b/core/camel-util/src/main/java/org/apache/camel/util/concurrent/ThreadLocalContextValue.java
@@ -0,0 +1,76 @@
+/*
+ * 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.util.concurrent;
+
+import java.util.function.Supplier;
+
+/**
+ * ThreadLocal-based implementation of {@link ContextValue}.
+ * <p>
+ * This is the default implementation used on JDK 17-24, and also used on JDK
25+ when virtual threads are not enabled
+ * or when a ThreadLocal is explicitly requested via {@link
ContextValue#newThreadLocal(String)}.
+ */
+class ThreadLocalContextValue<T> implements ContextValue<T> {
+ private final String name;
+ private final ThreadLocal<T> threadLocal;
+
+ ThreadLocalContextValue(String name) {
+ this.name = name;
+ this.threadLocal = new ThreadLocal<>();
+ }
+
+ ThreadLocalContextValue(String name, Supplier<T> supplier) {
+ this.name = name;
+ this.threadLocal = ThreadLocal.withInitial(supplier);
+ }
+
+ @Override
+ public T get() {
+ return threadLocal.get();
+ }
+
+ @Override
+ public T orElse(T defaultValue) {
+ T value = threadLocal.get();
+ return value != null ? value : defaultValue;
+ }
+
+ @Override
+ public boolean isBound() {
+ return threadLocal.get() != null;
+ }
+
+ @Override
+ public void set(T value) {
+ threadLocal.set(value);
+ }
+
+ @Override
+ public void remove() {
+ threadLocal.remove();
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return "ContextValue[" + name + "]";
+ }
+}
diff --git
a/core/camel-util/src/main/java25/org/apache/camel/util/concurrent/ContextValueFactory.java
b/core/camel-util/src/main/java25/org/apache/camel/util/concurrent/ContextValueFactory.java
index 8575a1f91705..b8b835d295df 100644
---
a/core/camel-util/src/main/java25/org/apache/camel/util/concurrent/ContextValueFactory.java
+++
b/core/camel-util/src/main/java25/org/apache/camel/util/concurrent/ContextValueFactory.java
@@ -163,57 +163,4 @@ class ContextValueFactory {
}
}
- /**
- * ThreadLocal-based implementation of ContextValue.
- */
- static class ThreadLocalContextValue<T> implements ContextValue<T> {
- private final String name;
- private final ThreadLocal<T> threadLocal;
-
- ThreadLocalContextValue(String name) {
- this.name = name;
- this.threadLocal = new ThreadLocal<>();
- }
-
- ThreadLocalContextValue(String name, Supplier<T> supplier) {
- this.name = name;
- this.threadLocal = ThreadLocal.withInitial(supplier);
- }
-
- @Override
- public T get() {
- return threadLocal.get();
- }
-
- @Override
- public T orElse(T defaultValue) {
- T value = threadLocal.get();
- return value != null ? value : defaultValue;
- }
-
- @Override
- public boolean isBound() {
- return threadLocal.get() != null;
- }
-
- @Override
- public void set(T value) {
- threadLocal.set(value);
- }
-
- @Override
- public void remove() {
- threadLocal.remove();
- }
-
- @Override
- public String name() {
- return name;
- }
-
- @Override
- public String toString() {
- return "ContextValue[" + name + ",ThreadLocal]";
- }
- }
}
diff --git a/docs/user-manual/modules/ROOT/pages/virtual-threads.adoc
b/docs/user-manual/modules/ROOT/pages/virtual-threads.adoc
index 3a97a1500055..b86c0916f221 100644
--- a/docs/user-manual/modules/ROOT/pages/virtual-threads.adoc
+++ b/docs/user-manual/modules/ROOT/pages/virtual-threads.adoc
@@ -801,7 +801,7 @@ public class ParallelEnrichmentRoute extends RouteBuilder {
}
----
-=== Example 3: Context Propagation Across Routes
+=== Example 3: Context Propagation Within a Route
[source,java]
----
@@ -811,21 +811,20 @@ public class TenantAwareRoute extends RouteBuilder {
@Override
public void configure() {
+ // ContextValue is scoped to the current thread - it works within a
single
+ // route or call chain, not across asynchronous boundaries like SEDA
queues.
+ // For cross-route context, use exchange properties instead.
from("platform-http:/api/{tenant}/orders")
.process(exchange -> {
String tenant = exchange.getMessage().getHeader("tenant",
String.class);
- // Bind tenant for the entire processing scope
- ContextValue.where(TENANT_ID, tenant, () -> {
- exchange.setProperty("tenantId", tenant);
- return null;
- });
+ exchange.setProperty("tenantId", tenant);
})
.to("seda:process?virtualThreadPerTask=true");
from("seda:process?virtualThreadPerTask=true&concurrentConsumers=500")
.process(exchange -> {
- // Access tenant ID in any processor
- String tenant = TENANT_ID.orElse("default");
+ // Use exchange properties for context that crosses async
boundaries
+ String tenant = exchange.getProperty("tenantId", "default",
String.class);
log.info("Processing for tenant: {}", tenant);
})
.toD("jpa:Order?persistenceUnit=${exchangeProperty.tenantId}");
@@ -833,6 +832,8 @@ public class TenantAwareRoute extends RouteBuilder {
}
----
+NOTE: `ContextValue` is scoped to the current thread (or `ScopedValue` scope
on JDK 25+). It does *not* propagate across asynchronous boundaries like SEDA
queues. For data that needs to cross route boundaries, use exchange properties
or headers. `ContextValue` is designed for propagating context *within* a
synchronous call chain (e.g., during route creation or processor
initialization).
+
== Summary
Virtual threads in Apache Camel provide: