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 a3af37bd8b6c CAMEL-25387: camel-kamelet - A Kamelet whose template
starts from the Kamelet itself fails with a clear message instead of a
StackOverflowError (#27451)
a3af37bd8b6c is described below
commit a3af37bd8b6c3c6c3e820a17ceb23078d717d606
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 6 20:02:51 2026 +0200
CAMEL-25387: camel-kamelet - A Kamelet whose template starts from the
Kamelet itself fails with a clear message instead of a StackOverflowError
(#27451)
Creating a Kamelet's route creates the Kamelets its template uses, on the
same
thread. A template that starts from kamelet:<itself> created the Kamelet
again
while creating it, until the stack overflowed: at startup the JVM died with
only
"Exception in thread main java.lang.StackOverflowError", on a dev-mode
reload the
warning said "due to: null". The component now tracks the Kamelets it is
creating on the thread and fails when one is entered again, saying the
template
starts from kamelet:source. The reload warning names an error without a
message
by its class.
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../camel/component/kamelet/KameletComponent.java | 39 ++++++++-
.../camel/component/kamelet/KameletCycleTest.java | 94 ++++++++++++++++++++++
.../support/FileWatcherResourceReloadStrategy.java | 3 +-
3 files changed, 133 insertions(+), 3 deletions(-)
diff --git
a/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletComponent.java
b/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletComponent.java
index 36d1efa71e38..c3eccedc5a91 100644
---
a/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletComponent.java
+++
b/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletComponent.java
@@ -16,8 +16,10 @@
*/
package org.apache.camel.component.kamelet;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -62,6 +64,9 @@ import static
org.apache.camel.component.kamelet.Kamelet.PARAM_UUID;
@Component(Kamelet.SCHEME)
public class KameletComponent extends DefaultComponent {
+ /** The Kamelets whose route this thread is creating, innermost first: to
find a Kamelet that uses itself. */
+ private static final ThreadLocal<Deque<String>> CREATING =
ThreadLocal.withInitial(ArrayDeque::new);
+
private static final Logger LOG =
LoggerFactory.getLogger(KameletComponent.class);
private final ReentrantLock lock = new ReentrantLock();
@@ -474,11 +479,41 @@ public class KameletComponent extends DefaultComponent {
protected void doCreateRouteForEndpoint(KameletEndpoint endpoint,
String parentRouteId, String parentProcessorId)
throws Exception {
+ final String templateId = endpoint.getTemplateId();
+ final String loc = endpoint.getLocation() != null ?
endpoint.getLocation() : getLocation();
+ // creating a Kamelet's route creates the Kamelets its template
uses, on this thread: one that is already
+ // being created is a cycle, which created routes until the stack
overflowed (CAMEL-25387)
+ Deque<String> creating = CREATING.get();
+ if (creating.contains(templateId)) {
+ List<String> cycle = new ArrayList<>(creating);
+ Collections.reverse(cycle);
+ cycle = cycle.subList(cycle.indexOf(templateId), cycle.size());
+ cycle.add(templateId);
+ String msg = cycle.size() == 2
+ ? "Kamelet " + templateId + " uses itself: its
template uses kamelet:" + templateId
+ + ", which creates the Kamelet again while creating
it. The template of an action or a sink"
+ + " starts from kamelet:source, the message the
route sends to the Kamelet"
+ : "Kamelets use each other in a cycle: " +
String.join(" -> ", cycle)
+ + ". Creating one creates the next, endlessly";
+ throw new FailedToCreateKameletException(templateId, loc, new
IllegalArgumentException(msg));
+ }
+ creating.push(templateId);
+ try {
+ doCreateRouteForEndpoint(endpoint, parentRouteId,
parentProcessorId, templateId, loc);
+ } finally {
+ creating.pop();
+ if (creating.isEmpty()) {
+ CREATING.remove();
+ }
+ }
+ }
+
+ private void doCreateRouteForEndpoint(
+ KameletEndpoint endpoint, String parentRouteId, String
parentProcessorId, String templateId, String loc)
+ throws Exception {
final ModelCamelContext context = (ModelCamelContext)
getCamelContext();
- final String templateId = endpoint.getTemplateId();
final String routeId = endpoint.getRouteId();
- final String loc = endpoint.getLocation() != null ?
endpoint.getLocation() : getLocation();
final String uuid = (String)
endpoint.getKameletProperties().get(PARAM_UUID);
if (context.getRouteTemplateDefinition(templateId) == null && loc
!= null) {
diff --git
a/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletCycleTest.java
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletCycleTest.java
new file mode 100644
index 000000000000..813002e92975
--- /dev/null
+++
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletCycleTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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 org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-25387: a Kamelet whose template starts from the Kamelet itself
created routes until the stack overflowed.
+ */
+public class KameletCycleTest {
+
+ @Test
+ public void aKameletWhoseTemplateStartsFromItself() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ // from kamelet:loop instead of kamelet:source, as a local
model wrote it
+ routeTemplate("loop")
+ .from("kamelet:loop/route")
+ .setBody(simple("${body}!"));
+
+ from("direct:start").to("kamelet:loop");
+ }
+ });
+ Exception e = assertThrows(Exception.class, context::start);
+ String messages = messages(e);
+ assertTrue(messages.contains("Kamelet loop uses itself"),
messages);
+ assertTrue(messages.contains("starts from kamelet:source"),
messages);
+ }
+ }
+
+ @Test
+ public void aKameletThatUsesAnotherKameletIsNotACycle() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
routeTemplate("outer").from("kamelet:source").to("kamelet:inner");
+
routeTemplate("inner").from("kamelet:source").setBody(simple("${body}!"));
+
+ from("direct:start").to("kamelet:outer");
+ }
+ });
+ context.start();
+ assertEquals("hi!",
context.createProducerTemplate().requestBody("direct:start", "hi",
String.class));
+ }
+ }
+
+ @Test
+ public void aKameletUsedTwiceIsNotACycle() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
routeTemplate("upper").from("kamelet:source").setBody(simple("${body.toUpperCase()}"));
+
+
from("direct:start").to("kamelet:upper").to("kamelet:upper");
+ }
+ });
+ context.start();
+ assertEquals("HI",
context.createProducerTemplate().requestBody("direct:start", "hi",
String.class));
+ }
+ }
+
+ private static String messages(Throwable e) {
+ StringBuilder sb = new StringBuilder();
+ for (Throwable t = e; t != null; t = t.getCause()) {
+ sb.append(t.getMessage()).append('\n');
+ }
+ return sb.toString();
+ }
+}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
index 94601e8b4ca5..9e0ed5cd8778 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
@@ -355,8 +355,9 @@ public class FileWatcherResourceReloadStrategy extends
ResourceReloadStrategySup
}
} catch (Throwable e) {
// an error (such as ExceptionInInitializerError when
compiling a route) must not stop watching
+ // an error such as StackOverflowError has no message:
name it rather than print null
LOG.warn("Error reloading files in directory: {} due
to: {}. This exception is ignored.", folder,
- e.getMessage(), e);
+ e.getMessage() != null ? e.getMessage() :
e.getClass().getName(), e);
}
}
} finally {