From f26962882a5bf5379b9d33c204935ed30eadcc65 Mon Sep 17 00:00:00 2001 From: Omar Atie Date: Thu, 30 Jul 2026 21:00:58 -0700 Subject: [PATCH 1/6] CAMEL-24320: Fix Kamelet route creation with virtual threads on JDK 25 Make ScopedValue-backed ContextValue.orElse null-tolerant so getCreateRoute()/getCreateProcessor() can return null outside a binding scope. Add regression tests for the ScopedValue path and Kamelet startup. Co-authored-by: Cursor --- ...ameletVirtualThreadsRouteCreationTest.java | 71 ++++++++++++++++++ .../kamelets/vt-repro-source.kamelet.yaml | 38 ++++++++++ .../impl/engine/CreateContextValueTest.java | 44 ++++++++++++ .../util/concurrent/ContextValueFactory.java | 2 +- .../ScopedValueContextValueOrElseTest.java | 72 +++++++++++++++++++ 5 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletVirtualThreadsRouteCreationTest.java create mode 100644 components/camel-kamelet/src/test/resources/kamelets/vt-repro-source.kamelet.yaml create mode 100644 core/camel-core/src/test/java/org/apache/camel/impl/engine/CreateContextValueTest.java create mode 100644 core/camel-util/src/test/java/org/apache/camel/util/concurrent/ScopedValueContextValueOrElseTest.java 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..13e93a86b737c --- /dev/null +++ b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletVirtualThreadsRouteCreationTest.java @@ -0,0 +1,71 @@ +/* + * 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.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.parallel.Isolated; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * CAMEL-24320: Kamelet route creation must not NPE when virtual threads are enabled on JDK 25+. + *

+ * Virtual threads must be selected before the first {@code ContextValue} is created; the system property is set in + * a static initializer so {@code ContextValueFactory} picks the ScopedValue backend when this class loads first in an + * isolated fork. {@link org.apache.camel.util.concurrent.ScopedValueContextValueOrElseTest} is the primary unit guard. + */ +@Isolated +@EnabledForJreRange(min = JRE.JAVA_25) +class KameletVirtualThreadsRouteCreationTest { + + static { + System.setProperty("camel.threads.virtual.enabled", "true"); + } + + @AfterEach + void resetThreadType() throws Exception { + Field field = ThreadType.class.getDeclaredField("current"); + field.setAccessible(true); + field.set(null, null); + System.clearProperty("camel.threads.virtual.enabled"); + } + + @Test + void mainStartsKameletRouteWithVirtualThreadsEnabled() { + assertThatCode(() -> { + Main main = new Main(); + main.configure().withVirtualThreadsEnabled(true).addRoutesBuilder(new RouteBuilder() { + @Override + public void configure() { + from("kamelet:vt-repro-source").routeId("vt-kamelet-repro").to("mock:vt-out"); + } + }); + main.start(); + assertThat(main.getCamelContext().getRoute("vt-kamelet-repro")).isNotNull(); + main.stop(); + }).doesNotThrowAnyException(); + } +} diff --git a/components/camel-kamelet/src/test/resources/kamelets/vt-repro-source.kamelet.yaml b/components/camel-kamelet/src/test/resources/kamelets/vt-repro-source.kamelet.yaml new file mode 100644 index 0000000000000..27c9f986a980b --- /dev/null +++ b/components/camel-kamelet/src/test/resources/kamelets/vt-repro-source.kamelet.yaml @@ -0,0 +1,38 @@ +# +# 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. +# + +apiVersion: camel.apache.org/v1 +kind: Kamelet +metadata: + name: vt-repro-source + labels: + camel.apache.org/kamelet.type: source +spec: + definition: + title: VT repro source + template: + from: + uri: timer:tick + parameters: + period: "1000" + repeatCount: "1" + delay: "-1" + steps: + - setBody: + constant: hello + - to: + uri: kamelet:sink 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..5d5a8c6dadebb --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/impl/engine/CreateContextValueTest.java @@ -0,0 +1,44 @@ +/* + * 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.ExtendedCamelContext; +import org.apache.camel.impl.DefaultCamelContext; +import org.junit.jupiter.api.Test; + +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 { + + @Test + void getCreateRouteReturnsNullOutsideScope() { + ExtendedCamelContext extension = new DefaultCamelContext().getCamelContextExtension(); + + assertThat(extension.getCreateRoute()).isNull(); + } + + @Test + void getCreateProcessorReturnsNullOutsideScope() { + ExtendedCamelContext extension = new DefaultCamelContext().getCamelContextExtension(); + + assertThat(extension.getCreateProcessor()).isNull(); + } +} 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..fe185797e1ba9 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 @@ -138,7 +138,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/test/java/org/apache/camel/util/concurrent/ScopedValueContextValueOrElseTest.java b/core/camel-util/src/test/java/org/apache/camel/util/concurrent/ScopedValueContextValueOrElseTest.java new file mode 100644 index 0000000000000..0093d581d8b77 --- /dev/null +++ b/core/camel-util/src/test/java/org/apache/camel/util/concurrent/ScopedValueContextValueOrElseTest.java @@ -0,0 +1,72 @@ +/* + * 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.lang.reflect.Constructor; +import java.lang.reflect.Method; + +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.Isolated; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * CAMEL-24320: {@link java.lang.ScopedValue#orElse} rejects a null fallback via {@code Objects.requireNonNull}, so the + * ScopedValue-backed {@link ContextValue} must check {@link java.lang.ScopedValue#isBound()} first. + */ +@Isolated +@EnabledForJreRange(min = JRE.JAVA_25) +class ScopedValueContextValueOrElseTest { + + @Test + void orElseNullWhenUnboundDoesNotThrow() throws Exception { + Object contextValue = newScopedValueContextValue("testOrElseNull"); + + Method orElse = contextValue.getClass().getMethod("orElse", Object.class); + Object result = orElse.invoke(contextValue, new Object[] { null }); + + assertThat(result).isNull(); + assertThat(contextValue.getClass().getMethod("isBound").invoke(contextValue)).isEqualTo(false); + } + + @Test + void orElseReturnsDefaultWhenUnbound() throws Exception { + Object contextValue = newScopedValueContextValue("testOrElseDefault"); + + Method orElse = contextValue.getClass().getMethod("orElse", Object.class); + Object result = orElse.invoke(contextValue, "fallback"); + + assertThat(result).isEqualTo("fallback"); + } + + @Test + void orElseReturnsBoundValue() throws Exception { + ContextValue routeId = ContextValue.newInstance("boundRoute"); + String result = ContextValue.where(routeId, "myRoute", () -> routeId.orElse(null)); + + assertThat(result).isEqualTo("myRoute"); + } + + private static Object newScopedValueContextValue(String name) throws Exception { + Class svClass = Class.forName("org.apache.camel.util.concurrent.ContextValueFactory$ScopedValueContextValue"); + Constructor ctor = svClass.getDeclaredConstructor(String.class); + ctor.setAccessible(true); + return ctor.newInstance(name); + } +} From 899c0c4e2afc685340d8b0ed091cce248e58ad16 Mon Sep 17 00:00:00 2001 From: Omar Atie Date: Fri, 31 Jul 2026 09:08:39 -0700 Subject: [PATCH 2/6] CAMEL-24320: Apply formatter to KameletVirtualThreadsRouteCreationTest Fix sourcecheck uncommitted-changes failure from javadoc line wrapping. Co-authored-by: Cursor --- .../kamelet/KameletVirtualThreadsRouteCreationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 index 13e93a86b737c..d5f9f568d0a15 100644 --- 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 @@ -33,8 +33,8 @@ /** * CAMEL-24320: Kamelet route creation must not NPE when virtual threads are enabled on JDK 25+. *

- * Virtual threads must be selected before the first {@code ContextValue} is created; the system property is set in - * a static initializer so {@code ContextValueFactory} picks the ScopedValue backend when this class loads first in an + * Virtual threads must be selected before the first {@code ContextValue} is created; the system property is set in a + * static initializer so {@code ContextValueFactory} picks the ScopedValue backend when this class loads first in an * isolated fork. {@link org.apache.camel.util.concurrent.ScopedValueContextValueOrElseTest} is the primary unit guard. */ @Isolated From 58bc2359d1dd31b116d9363e3954480a77d078a5 Mon Sep 17 00:00:00 2001 From: Omar Atie Date: Fri, 31 Jul 2026 09:12:46 -0700 Subject: [PATCH 3/6] CAMEL-24320: Address apupier review on Kamelet VT test Save and restore camel.threads.virtual.enabled, use ResourceLock for system properties instead of @Isolated, and drop redundant assertThatCode wrapper. Co-authored-by: Cursor --- ...ameletVirtualThreadsRouteCreationTest.java | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 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 index d5f9f568d0a15..4be01c8cbc592 100644 --- 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 @@ -22,50 +22,66 @@ 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.Isolated; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.api.parallel.Resources; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; /** * CAMEL-24320: Kamelet route creation must not NPE when virtual threads are enabled on JDK 25+. *

- * Virtual threads must be selected before the first {@code ContextValue} is created; the system property is set in a - * static initializer so {@code ContextValueFactory} picks the ScopedValue backend when this class loads first in an - * isolated fork. {@link org.apache.camel.util.concurrent.ScopedValueContextValueOrElseTest} is the primary unit guard. + * {@link org.apache.camel.util.concurrent.ScopedValueContextValueOrElseTest} is the primary unit guard for the + * ScopedValue {@code orElse(null)} regression. */ -@Isolated @EnabledForJreRange(min = JRE.JAVA_25) +@ResourceLock(Resources.SYSTEM_PROPERTIES) class KameletVirtualThreadsRouteCreationTest { - static { - System.setProperty("camel.threads.virtual.enabled", "true"); + 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 resetThreadType() throws Exception { - Field field = ThreadType.class.getDeclaredField("current"); - field.setAccessible(true); - field.set(null, null); - System.clearProperty("camel.threads.virtual.enabled"); + void restoreVirtualThreadsProperty() throws Exception { + if (previousVirtualThreadsProperty == null) { + System.clearProperty(VIRTUAL_THREADS_PROPERTY); + } else { + System.setProperty(VIRTUAL_THREADS_PROPERTY, previousVirtualThreadsProperty); + } + resetThreadTypeField(); } @Test void mainStartsKameletRouteWithVirtualThreadsEnabled() { - assertThatCode(() -> { - Main main = new Main(); - main.configure().withVirtualThreadsEnabled(true).addRoutesBuilder(new RouteBuilder() { - @Override - public void configure() { - from("kamelet:vt-repro-source").routeId("vt-kamelet-repro").to("mock:vt-out"); - } - }); - main.start(); + Main main = new Main(); + main.configure().withVirtualThreadsEnabled(true).addRoutesBuilder(new RouteBuilder() { + @Override + public void configure() { + 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(); - }).doesNotThrowAnyException(); + } + } + + private static void resetThreadTypeField() throws Exception { + Field field = ThreadType.class.getDeclaredField("current"); + field.setAccessible(true); + field.set(null, null); } } From 51eb055109742653a02039d58d3b07362cfce366 Mon Sep 17 00:00:00 2001 From: Omar Atie Date: Sun, 2 Aug 2026 10:08:55 -0700 Subject: [PATCH 4/6] CAMEL-24320: Document null orElse and add JDK 25 context test Address review follow-up: clarify ContextValue.orElse accepts null fallback, and add CreateContextValueTest guard for getCreateRoute() with virtual threads enabled on JDK 25+. Co-authored-by: Cursor --- .../impl/engine/CreateContextValueTest.java | 47 +++++++++++++++++++ .../camel/util/concurrent/ContextValue.java | 5 +- 2 files changed, 51 insertions(+), 1 deletion(-) 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 index 5d5a8c6dadebb..e9a3f3b6f1afe 100644 --- 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 @@ -16,9 +16,16 @@ */ 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; @@ -28,6 +35,10 @@ */ class CreateContextValueTest { + private static final String VIRTUAL_THREADS_PROPERTY = "camel.threads.virtual.enabled"; + + private String previousVirtualThreadsProperty; + @Test void getCreateRouteReturnsNullOutsideScope() { ExtendedCamelContext extension = new DefaultCamelContext().getCamelContextExtension(); @@ -41,4 +52,40 @@ void getCreateProcessorReturnsNullOutsideScope() { 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/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); From cbea25eb79a3e434cd67510a0f3c8c6fc59416a0 Mon Sep 17 00:00:00 2001 From: Omar Atie Date: Sun, 2 Aug 2026 13:15:42 -0700 Subject: [PATCH 5/6] CAMEL-24320: Fix JDK 25 CI failure for ScopedValue ContextValue tests Remove ScopedValueContextValueOrElseTest that used reflection against MRJ inner classes unavailable on the Surefire classpath. Evaluate virtual-thread ScopedValue selection lazily and add Multi-Release manifest for java-25. Co-authored-by: Cursor --- ...ameletVirtualThreadsRouteCreationTest.java | 5 +- core/camel-util/pom.xml | 11 +++ .../util/concurrent/ContextValueFactory.java | 20 ++---- .../src/main/resources/META-INF/MANIFEST.MF | 2 + .../ScopedValueContextValueOrElseTest.java | 72 ------------------- 5 files changed, 23 insertions(+), 87 deletions(-) create mode 100644 core/camel-util/src/main/resources/META-INF/MANIFEST.MF delete mode 100644 core/camel-util/src/test/java/org/apache/camel/util/concurrent/ScopedValueContextValueOrElseTest.java 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 index 4be01c8cbc592..46f4fb4f0df2d 100644 --- 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 @@ -34,8 +34,9 @@ /** * CAMEL-24320: Kamelet route creation must not NPE when virtual threads are enabled on JDK 25+. *

- * {@link org.apache.camel.util.concurrent.ScopedValueContextValueOrElseTest} is the primary unit guard for the - * ScopedValue {@code orElse(null)} regression. + * 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) 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/java25/org/apache/camel/util/concurrent/ContextValueFactory.java b/core/camel-util/src/main/java25/org/apache/camel/util/concurrent/ContextValueFactory.java index fe185797e1ba9..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; } /** 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 + diff --git a/core/camel-util/src/test/java/org/apache/camel/util/concurrent/ScopedValueContextValueOrElseTest.java b/core/camel-util/src/test/java/org/apache/camel/util/concurrent/ScopedValueContextValueOrElseTest.java deleted file mode 100644 index 0093d581d8b77..0000000000000 --- a/core/camel-util/src/test/java/org/apache/camel/util/concurrent/ScopedValueContextValueOrElseTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.lang.reflect.Constructor; -import java.lang.reflect.Method; - -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.Isolated; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * CAMEL-24320: {@link java.lang.ScopedValue#orElse} rejects a null fallback via {@code Objects.requireNonNull}, so the - * ScopedValue-backed {@link ContextValue} must check {@link java.lang.ScopedValue#isBound()} first. - */ -@Isolated -@EnabledForJreRange(min = JRE.JAVA_25) -class ScopedValueContextValueOrElseTest { - - @Test - void orElseNullWhenUnboundDoesNotThrow() throws Exception { - Object contextValue = newScopedValueContextValue("testOrElseNull"); - - Method orElse = contextValue.getClass().getMethod("orElse", Object.class); - Object result = orElse.invoke(contextValue, new Object[] { null }); - - assertThat(result).isNull(); - assertThat(contextValue.getClass().getMethod("isBound").invoke(contextValue)).isEqualTo(false); - } - - @Test - void orElseReturnsDefaultWhenUnbound() throws Exception { - Object contextValue = newScopedValueContextValue("testOrElseDefault"); - - Method orElse = contextValue.getClass().getMethod("orElse", Object.class); - Object result = orElse.invoke(contextValue, "fallback"); - - assertThat(result).isEqualTo("fallback"); - } - - @Test - void orElseReturnsBoundValue() throws Exception { - ContextValue routeId = ContextValue.newInstance("boundRoute"); - String result = ContextValue.where(routeId, "myRoute", () -> routeId.orElse(null)); - - assertThat(result).isEqualTo("myRoute"); - } - - private static Object newScopedValueContextValue(String name) throws Exception { - Class svClass = Class.forName("org.apache.camel.util.concurrent.ContextValueFactory$ScopedValueContextValue"); - Constructor ctor = svClass.getDeclaredConstructor(String.class); - ctor.setAccessible(true); - return ctor.newInstance(name); - } -} From 4464a74808cbbd1774e4e9b4f3324400b4df0f29 Mon Sep 17 00:00:00 2001 From: Omar Atie Date: Sun, 2 Aug 2026 23:47:19 -0700 Subject: [PATCH 6/6] CAMEL-24320: Fix VT kamelet test without yaml-dsl on classpath Use an inline routeTemplate instead of a classpath kamelet.yaml file so the JDK 25+ integration test does not require KameletRoutesBuilderLoader. Co-authored-by: Cursor --- ...ameletVirtualThreadsRouteCreationTest.java | 5 +++ .../kamelets/vt-repro-source.kamelet.yaml | 38 ------------------- 2 files changed, 5 insertions(+), 38 deletions(-) delete mode 100644 components/camel-kamelet/src/test/resources/kamelets/vt-repro-source.kamelet.yaml 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 index 46f4fb4f0df2d..72c17d2439f6b 100644 --- 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 @@ -69,6 +69,11 @@ void mainStartsKameletRouteWithVirtualThreadsEnabled() { 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"); } }); diff --git a/components/camel-kamelet/src/test/resources/kamelets/vt-repro-source.kamelet.yaml b/components/camel-kamelet/src/test/resources/kamelets/vt-repro-source.kamelet.yaml deleted file mode 100644 index 27c9f986a980b..0000000000000 --- a/components/camel-kamelet/src/test/resources/kamelets/vt-repro-source.kamelet.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# -# 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. -# - -apiVersion: camel.apache.org/v1 -kind: Kamelet -metadata: - name: vt-repro-source - labels: - camel.apache.org/kamelet.type: source -spec: - definition: - title: VT repro source - template: - from: - uri: timer:tick - parameters: - period: "1000" - repeatCount: "1" - delay: "-1" - steps: - - setBody: - constant: hello - - to: - uri: kamelet:sink