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 2bf02ec2df17 CAMEL-24320: Fix Kamelet route creation with virtual 
threads on JDK 25
2bf02ec2df17 is described below

commit 2bf02ec2df1724e869be37b71422526cccbbdadb
Author: Omar Atie <[email protected]>
AuthorDate: Mon Aug 3 03:06:51 2026 -0700

    CAMEL-24320: Fix Kamelet route creation with virtual threads on JDK 25
    
    Make ScopedValue-backed ContextValue.orElse null-tolerant so
    getCreateRoute()/getCreateProcessor() return null outside a binding scope
    instead of throwing NPE on JDK 25+. Evaluate virtual-thread ScopedValue
    selection lazily and add Multi-Release JAR manifest for java-25.
    
    Closes #25265
    
    Co-authored-by: Cursor <[email protected]>
---
 .../KameletVirtualThreadsRouteCreationTest.java    | 93 ++++++++++++++++++++++
 .../camel/impl/engine/CreateContextValueTest.java  | 91 +++++++++++++++++++++
 core/camel-util/pom.xml                            | 11 +++
 .../apache/camel/util/concurrent/ContextValue.java |  5 +-
 .../camel/util/concurrent/ContextValueFactory.java | 22 ++---
 .../src/main/resources/META-INF/MANIFEST.MF        |  2 +
 6 files changed, 209 insertions(+), 15 deletions(-)

diff --git 
a/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletVirtualThreadsRouteCreationTest.java
 
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletVirtualThreadsRouteCreationTest.java
new file mode 100644
index 000000000000..72c17d2439f6
--- /dev/null
+++ 
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletVirtualThreadsRouteCreationTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.kamelet;
+
+import java.lang.reflect.Field;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.main.Main;
+import org.apache.camel.util.concurrent.ThreadType;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+import org.junit.jupiter.api.condition.JRE;
+import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.parallel.Resources;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-24320: Kamelet route creation must not NPE when virtual threads are 
enabled on JDK 25+.
+ * <p>
+ * Regression guard for {@link 
ContextValueFactory.ScopedValueContextValue#orElse(Object)} when the fallback is
+ * {@code null} (see CAMEL-24320). ScopedValue unit coverage lives in the 
integration path because MRJ classes are
+ * packaged under {@code META-INF/versions/25}.
+ */
+@EnabledForJreRange(min = JRE.JAVA_25)
+@ResourceLock(Resources.SYSTEM_PROPERTIES)
+class KameletVirtualThreadsRouteCreationTest {
+
+    private static final String VIRTUAL_THREADS_PROPERTY = 
"camel.threads.virtual.enabled";
+
+    private String previousVirtualThreadsProperty;
+
+    @BeforeEach
+    void enableVirtualThreads() throws Exception {
+        previousVirtualThreadsProperty = 
System.getProperty(VIRTUAL_THREADS_PROPERTY);
+        System.setProperty(VIRTUAL_THREADS_PROPERTY, "true");
+        resetThreadTypeField();
+    }
+
+    @AfterEach
+    void restoreVirtualThreadsProperty() throws Exception {
+        if (previousVirtualThreadsProperty == null) {
+            System.clearProperty(VIRTUAL_THREADS_PROPERTY);
+        } else {
+            System.setProperty(VIRTUAL_THREADS_PROPERTY, 
previousVirtualThreadsProperty);
+        }
+        resetThreadTypeField();
+    }
+
+    @Test
+    void mainStartsKameletRouteWithVirtualThreadsEnabled() {
+        Main main = new Main();
+        main.configure().withVirtualThreadsEnabled(true).addRoutesBuilder(new 
RouteBuilder() {
+            @Override
+            public void configure() {
+                routeTemplate("vt-repro-source")
+                        .from("timer:vt-tick?repeatCount=1&delay=-1")
+                        .setBody(constant("hello"))
+                        .to("kamelet:sink");
+
+                
from("kamelet:vt-repro-source").routeId("vt-kamelet-repro").to("mock:vt-out");
+            }
+        });
+        main.start();
+        try {
+            
assertThat(main.getCamelContext().getRoute("vt-kamelet-repro")).isNotNull();
+        } finally {
+            main.stop();
+        }
+    }
+
+    private static void resetThreadTypeField() throws Exception {
+        Field field = ThreadType.class.getDeclaredField("current");
+        field.setAccessible(true);
+        field.set(null, null);
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/engine/CreateContextValueTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/CreateContextValueTest.java
new file mode 100644
index 000000000000..e9a3f3b6f1af
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/CreateContextValueTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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 java.lang.reflect.Field;
+
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.util.concurrent.ThreadType;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+import org.junit.jupiter.api.condition.JRE;
+import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.parallel.Resources;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-24320: {@link ExtendedCamelContext#getCreateRoute()} and {@link 
ExtendedCamelContext#getCreateProcessor()} must
+ * return null outside a binding scope (used by Kamelet endpoint init).
+ */
+class CreateContextValueTest {
+
+    private static final String VIRTUAL_THREADS_PROPERTY = 
"camel.threads.virtual.enabled";
+
+    private String previousVirtualThreadsProperty;
+
+    @Test
+    void getCreateRouteReturnsNullOutsideScope() {
+        ExtendedCamelContext extension = new 
DefaultCamelContext().getCamelContextExtension();
+
+        assertThat(extension.getCreateRoute()).isNull();
+    }
+
+    @Test
+    void getCreateProcessorReturnsNullOutsideScope() {
+        ExtendedCamelContext extension = new 
DefaultCamelContext().getCamelContextExtension();
+
+        assertThat(extension.getCreateProcessor()).isNull();
+    }
+
+    @EnabledForJreRange(min = JRE.JAVA_25)
+    @ResourceLock(Resources.SYSTEM_PROPERTIES)
+    @Test
+    void getCreateRouteDoesNotThrowWithVirtualThreadsEnabled() throws 
Exception {
+        enableVirtualThreads();
+        try {
+            ExtendedCamelContext extension = new 
DefaultCamelContext().getCamelContextExtension();
+
+            assertThat(extension.getCreateRoute()).isNull();
+            assertThat(extension.getCreateProcessor()).isNull();
+        } finally {
+            restoreVirtualThreadsProperty();
+        }
+    }
+
+    private void enableVirtualThreads() throws Exception {
+        previousVirtualThreadsProperty = 
System.getProperty(VIRTUAL_THREADS_PROPERTY);
+        System.setProperty(VIRTUAL_THREADS_PROPERTY, "true");
+        resetThreadTypeField();
+    }
+
+    private void restoreVirtualThreadsProperty() throws Exception {
+        if (previousVirtualThreadsProperty == null) {
+            System.clearProperty(VIRTUAL_THREADS_PROPERTY);
+        } else {
+            System.setProperty(VIRTUAL_THREADS_PROPERTY, 
previousVirtualThreadsProperty);
+        }
+        resetThreadTypeField();
+    }
+
+    private static void resetThreadTypeField() throws Exception {
+        Field field = ThreadType.class.getDeclaredField("current");
+        field.setAccessible(true);
+        field.set(null, null);
+    }
+}
diff --git a/core/camel-util/pom.xml b/core/camel-util/pom.xml
index 4ba1b98e3afb..b72a8fd6ded6 100644
--- a/core/camel-util/pom.xml
+++ b/core/camel-util/pom.xml
@@ -351,6 +351,17 @@
                             </execution>
                         </executions>
                     </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-jar-plugin</artifactId>
+                        <configuration>
+                            <archive>
+                                <manifestEntries>
+                                    <Multi-Release>true</Multi-Release>
+                                </manifestEntries>
+                            </archive>
+                        </configuration>
+                    </plugin>
                 </plugins>
             </build>
         </profile>
diff --git 
a/core/camel-util/src/main/java/org/apache/camel/util/concurrent/ContextValue.java
 
b/core/camel-util/src/main/java/org/apache/camel/util/concurrent/ContextValue.java
index 4f2790ca73fa..43f1023556a4 100644
--- 
a/core/camel-util/src/main/java/org/apache/camel/util/concurrent/ContextValue.java
+++ 
b/core/camel-util/src/main/java/org/apache/camel/util/concurrent/ContextValue.java
@@ -74,8 +74,11 @@ public interface ContextValue<T> {
     /**
      * Returns the value of this context variable for the current thread, or 
the given default value if no value is
      * bound.
+     * <p>
+     * {@code defaultValue} may be {@code null}; implementations must return 
{@code null} when unbound and the caller
+     * passes {@code null} as the fallback (for example {@code 
getCreateRoute()} outside a binding scope).
      *
-     * @param  defaultValue the value to return if no value is bound
+     * @param  defaultValue the value to return if no value is bound (may be 
{@code null})
      * @return              the current value, or {@code defaultValue} if not 
bound
      */
     T orElse(T defaultValue);
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 bd089e18ebcd..0b0f95f8d70a 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
@@ -31,25 +31,19 @@ class ContextValueFactory {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(ContextValueFactory.class);
 
-    // Use lazy holder pattern to avoid resolving ThreadType before 
configuration is loaded
-    private static final class ScopedValueHolder {
-        static final boolean USE_SCOPED_VALUES = shouldUseScopedValues();
+    private static volatile boolean scopedValueUsageLogged;
 
-        static {
-            if (useScopedValues()) {
+    private static boolean useScopedValues() {
+        boolean use = ThreadType.current() == ThreadType.VIRTUAL;
+        if (!scopedValueUsageLogged) {
+            scopedValueUsageLogged = true;
+            if (use) {
                 LOG.info("ContextValue will use ScopedValue for virtual thread 
optimization");
             } else {
                 LOG.debug("ContextValue will use ThreadLocal");
             }
         }
-
-        private static boolean shouldUseScopedValues() {
-            return ThreadType.current() == ThreadType.VIRTUAL;
-        }
-    }
-
-    private static boolean useScopedValues() {
-        return ScopedValueHolder.USE_SCOPED_VALUES;
+        return use;
     }
 
     /**
@@ -138,7 +132,7 @@ class ContextValueFactory {
 
         @Override
         public T orElse(T defaultValue) {
-            return scopedValue.orElse(defaultValue);
+            return scopedValue.isBound() ? scopedValue.get() : defaultValue;
         }
 
         @Override
diff --git a/core/camel-util/src/main/resources/META-INF/MANIFEST.MF 
b/core/camel-util/src/main/resources/META-INF/MANIFEST.MF
new file mode 100644
index 000000000000..37cb50e1904a
--- /dev/null
+++ b/core/camel-util/src/main/resources/META-INF/MANIFEST.MF
@@ -0,0 +1,2 @@
+Multi-Release: true
+

Reply via email to