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 0000000000000..72c17d2439f6b --- /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+. + *

+ * 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 0000000000000..e9a3f3b6f1afe --- /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 4ba1b98e3afb2..b72a8fd6ded64 100644 --- a/core/camel-util/pom.xml +++ b/core/camel-util/pom.xml @@ -351,6 +351,17 @@ + + org.apache.maven.plugins + maven-jar-plugin + + + + true + + + + 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 4f2790ca73fa0..43f1023556a44 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 { /** * Returns the value of this context variable for the current thread, or the given default value if no value is * bound. + *

+ * {@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 bd089e18ebcdf..0b0f95f8d70a9 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 @@ public T get() { @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 0000000000000..37cb50e1904a5 --- /dev/null +++ b/core/camel-util/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Multi-Release: true +