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 8b1cb4437b12 CAMEL-26310: camel-core - Do not use a component created
on first use before it has started (#28710)
8b1cb4437b12 is described below
commit 8b1cb4437b123542418a24be7722319510494f94
Author: allthingssecurity <[email protected]>
AuthorDate: Sat Oct 31 01:50:35 2026 +0530
CAMEL-26310: camel-core - Do not use a component created on first use
before it has started (#28710)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/impl/engine/AbstractCamelContext.java | 76 ++++-
.../camel/impl/ComponentStartedBeforeUseTest.java | 374 +++++++++++++++++++++
2 files changed, 448 insertions(+), 2 deletions(-)
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
index 39c3206fe97d..4e0673a16ca1 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/AbstractCamelContext.java
@@ -240,6 +240,13 @@ public abstract class AbstractCamelContext extends
BaseService
private final Set<AutoMockInterceptStrategy> autoMockInterceptStrategies =
ConcurrentHashMap.newKeySet();
private final GlobalEndpointConfiguration globalEndpointConfiguration =
new DefaultGlobalEndpointConfiguration();
private final Map<String, Component> components = new
ConcurrentHashMap<>();
+ // the components that getComponent created with autoStart=false: their
caller configures and then starts them
+ // (such as RestProducerFactoryHelper.setupComponent), so another thread
must not start them meanwhile
+ private final Map<String, Component> componentsCreatedWithoutAutoStart =
new ConcurrentHashMap<>();
+ // the locks under which getComponent starts a component created on first
use, so that it is started only once
+ private final Map<String, Lock> componentStartLocks = new
ConcurrentHashMap<>();
+ // whether doStartCamel has started the components it has (until then it
starts them itself, as startup steps)
+ private volatile boolean componentsStarted;
private final Set<Route> routes = new LinkedHashSet<>();
private final List<StartupListener> startupListeners = new
CopyOnWriteArrayList<>();
private final Map<String, Language> languages = new ConcurrentHashMap<>();
@@ -559,11 +566,20 @@ public abstract class AbstractCamelContext extends
BaseService
try {
// Flag used to mark a component of being created.
final AtomicBoolean created = new AtomicBoolean();
+ // whether its creator starts the component also when it is not
new (such as a stopped bean from the registry)
+ final AtomicBoolean startAlways = new AtomicBoolean();
// atomic operation to get/create a component. Avoid global locks.
final Component component = components.computeIfAbsent(name, comp
-> {
created.set(true);
- return initComponent(name, autoCreateComponents);
+ Component answer = initComponent(name, autoCreateComponents);
+ startAlways.set(answer != null && !isNotStartedYet(answer));
+ if (answer != null && !autoStart) {
+ // marked before other threads can get it
+ // (kept after the creator starts it: not consulted again,
removed by removeComponent or context stop)
+ componentsCreatedWithoutAutoStart.put(name, answer);
+ }
+ return answer;
});
// Start the component after its creation as if it is a component
proxy
@@ -573,8 +589,19 @@ public abstract class AbstractCamelContext extends
BaseService
// lets start it up.
final StartupStepRecorder startupStepRecorder =
camelContextExtension.getStartupStepRecorder();
StartupStep step =
startupStepRecorder.beginStep(Component.class, name, "Start Component");
- startService(component);
+ startComponentCreatedOnFirstUse(name, component,
startAlways.get());
startupStepRecorder.endStep(step);
+ } else if (autoStart && (isStarted() || (isStarting() &&
componentsStarted)) && isNotStartedYet(component)
+ && componentsCreatedWithoutAutoStart.get(name) !=
component) {
+ // another thread has just created the component and may still
be starting it: start it (or wait for
+ // the other thread to have started it), so the component is
not used before it has been started.
+ // A component created with autoStart=false is left to its
creator, which configures it first, and
+ // until doStartCamel has started its components, it starts
them itself.
+ // This thread waits for the start lock of the component, also
when it holds the lock of this
+ // CamelContext (such as when a route is started): the doStart
of a component must therefore not take
+ // the lock of this CamelContext (such as by adding or
starting routes), nor get a component whose
+ // doStart gets it back, or the threads deadlock.
+ startComponentCreatedOnFirstUse(name, component, false);
}
return component;
@@ -586,6 +613,45 @@ public abstract class AbstractCamelContext extends
BaseService
}
}
+ /**
+ * Starts a component that getComponent has created on first use. Its
creator, and the threads that get it
+ * meanwhile, start it under the same lock, so it is started once: a
thread that gets the lock after the component
+ * has started, or has failed to start, gets the component as it is (only
the thread that started it gets the
+ * failure). The creator of a component that was not new when it was
created (a component that is not a BaseService,
+ * or a bean from the registry that has been stopped) starts it always, as
before.
+ */
+ private void startComponentCreatedOnFirstUse(String name, Component
component, boolean always) throws Exception {
+ Lock lock = componentStartLocks.computeIfAbsent(name, k -> new
ReentrantLock());
+ lock.lock();
+ try {
+ if (always || isNotStartedYet(component)) {
+ startService(component);
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Whether the component has not been started yet, or is being started. A
component that has been stopped, or has
+ * failed to start, is not.
+ */
+ private static boolean isNotStartedYet(Component component) {
+ if (component instanceof BaseService bs) {
+ if (bs.isStarted()) {
+ // the common case
+ return false;
+ }
+ if (bs.isNew() || bs.isBuild()) {
+ return true;
+ }
+ ServiceStatus status = bs.getStatus();
+ return status == ServiceStatus.Initializing || status ==
ServiceStatus.Initialized
+ || status == ServiceStatus.Starting;
+ }
+ return false;
+ }
+
/**
* Function to initialize a component and auto start. Returns null if the
autoCreateComponents is disabled
*/
@@ -661,6 +727,8 @@ public abstract class AbstractCamelContext extends
BaseService
public Component removeComponent(String componentName) {
Component oldComponent = components.remove(componentName);
if (oldComponent != null) {
+ componentsCreatedWithoutAutoStart.remove(componentName,
oldComponent);
+ componentStartLocks.remove(componentName);
try {
stopServices(oldComponent);
} catch (Exception e) {
@@ -3201,6 +3269,7 @@ public abstract class AbstractCamelContext extends
BaseService
startupStepRecorder.endStep(step);
}
}
+ componentsStarted = true;
if (!startupListeners.isEmpty()) {
StartupStep subStep
@@ -3421,6 +3490,9 @@ public abstract class AbstractCamelContext extends
BaseService
InternalServiceManager.shutdownServices(this, components.values());
components.clear();
+ componentsCreatedWithoutAutoStart.clear();
+ componentStartLocks.clear();
+ componentsStarted = false;
InternalServiceManager.shutdownServices(this, languages.values());
languages.clear();
diff --git
a/core/camel-core/src/test/java/org/apache/camel/impl/ComponentStartedBeforeUseTest.java
b/core/camel-core/src/test/java/org/apache/camel/impl/ComponentStartedBeforeUseTest.java
new file mode 100644
index 000000000000..eca34d1cebdf
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/impl/ComponentStartedBeforeUseTest.java
@@ -0,0 +1,374 @@
+/*
+ * 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;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Component;
+import org.apache.camel.Consumer;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.ServiceStatus;
+import org.apache.camel.StartupListener;
+import org.apache.camel.StartupStep;
+import org.apache.camel.support.DefaultComponent;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.support.startup.DefaultStartupStepRecorder;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A component that is created on first use, after CamelContext has started or
while it starts, is started by the thread
+ * that created it. Another thread that gets the same component meanwhile must
not create endpoints from it before it
+ * has started.
+ */
+class ComponentStartedBeforeUseTest extends ContextTestSupport {
+
+ private final CountDownLatch starting = new CountDownLatch(1);
+ private final CountDownLatch release = new CountDownLatch(1);
+ private final CountDownLatch created = new CountDownLatch(2);
+ private final CountDownLatch resumeCreator = new CountDownLatch(1);
+ private final SlowComponent component = new SlowComponent();
+
+ @Override
+ @AfterEach
+ public void tearDown() throws Exception {
+ release.countDown();
+ resumeCreator.countDown();
+ super.tearDown();
+ }
+
+ @Test
+ void testOtherThreadWaitsForTheComponentToStart() throws Exception {
+ context.getRegistry().bind("slow", component);
+ resolveFromTwoThreads(context);
+ }
+
+ @Test
+ void testOtherThreadWaitsForTheComponentToStartWhileCamelContextStarts()
throws Exception {
+ // the consumers of the routes that have started can send while
CamelContext is still starting
+ CamelContext startingContext = new DefaultCamelContext();
+ startingContext.getRegistry().bind("slow", component);
+ AtomicReference<Throwable> failure = new AtomicReference<>();
+ startingContext.addStartupListener(new StartupListener() {
+ @Override
+ public void onCamelContextStarting(CamelContext camelContext,
boolean alreadyStarted) {
+ try {
+ assertTrue(camelContext.isStarting());
+ resolveFromTwoThreads(camelContext);
+ } catch (Throwable e) {
+ failure.set(e);
+ }
+ }
+
+ @Override
+ public void onCamelContextStarted(CamelContext camelContext,
boolean alreadyStarted) {
+ // noop
+ }
+ });
+ try {
+ startingContext.start();
+ assertNull(failure.get(), () -> String.valueOf(failure.get()));
+ } finally {
+ startingContext.stop();
+ }
+ }
+
+ @Test
+ void testOtherThreadStartsTheComponentThatItsCreatorHasNotStartedYet()
throws Exception {
+ // the creator thread has created the component (BUILT), and has not
started it yet
+ CountDownLatch creatorPaused = new CountDownLatch(1);
+ context.getCamelContextExtension().setStartupStepRecorder(new
DefaultStartupStepRecorder() {
+ @Override
+ public StartupStep beginStep(Class<?> type, String id, String
description) {
+ if ("creator".equals(Thread.currentThread().getName()) &&
"Start Component".equals(description)) {
+ creatorPaused.countDown();
+ try {
+ resumeCreator.await(5, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ return super.beginStep(type, id, description);
+ }
+ });
+ context.getRegistry().bind("slow", component);
+ AtomicReference<Throwable> first = new AtomicReference<>();
+ Thread creator = new Thread(() -> getEndpoint(context, "slow:a",
first), "creator");
+ creator.start();
+ assertTrue(creatorPaused.await(5, TimeUnit.SECONDS));
+ assertTrue(component.isBuild(), "created, and not started yet");
+
+ AtomicReference<Throwable> second = new AtomicReference<>();
+ Thread other = new Thread(() -> getEndpoint(context, "slow:b",
second), "other");
+ other.start();
+ assertTrue(starting.await(5, TimeUnit.SECONDS), "the other thread
starts the component");
+ release.countDown();
+ resumeCreator.countDown();
+ creator.join(5000);
+ other.join(5000);
+ assertFalse(creator.isAlive());
+ assertFalse(other.isAlive());
+
+ assertNull(first.get(), () -> String.valueOf(first.get()));
+ assertNull(second.get(), () -> String.valueOf(second.get()));
+ assertEquals("other", component.startedBy);
+ assertEquals(ServiceStatus.Started, component.statusAtCreate.get("a"));
+ assertEquals(ServiceStatus.Started, component.statusAtCreate.get("b"));
+ }
+
+ @Test
+ void testComponentCreatedWithoutAutoStartIsNotStartedByAnotherThread()
throws Exception {
+ release.countDown();
+ context.getRegistry().bind("slow", component);
+ // created to be configured before it starts, as
RestProducerFactoryHelper.setupComponent does
+ assertSame(component, context.getComponent("slow", true, false));
+ assertTrue(component.isBuild());
+
+ // another thread gets the component before its creator has configured
and started it
+ AtomicReference<Component> seen = new AtomicReference<>();
+ Thread other = new Thread(() ->
seen.set(context.getComponent("slow")), "other");
+ other.start();
+ other.join(5000);
+ assertFalse(other.isAlive());
+ assertSame(component, seen.get());
+ assertFalse(component.isStarted(), "left to its creator, which
configures it first");
+
+ // the creator configures and then starts it
+ component.setAutowiredEnabled(false);
+ ServiceHelper.startService(component);
+ assertEquals(Thread.currentThread().getName(), component.startedBy);
+ }
+
+ @Test
+ void testStoppedComponentIsNotStartedAgain() {
+ // a control (as on main): only a component that has not started yet
is started, not one that has been stopped
+ // (such as by removeComponent while another thread still uses it)
+ release.countDown();
+ context.getRegistry().bind("slow", component);
+ assertNotNull(context.getEndpoint("slow:a"));
+ assertTrue(component.isStarted());
+
+ component.stop();
+ assertSame(component, context.getComponent("slow"));
+ assertTrue(component.isStopped());
+ }
+
+ @Test
+ void testOtherThreadDoesNotStartTheComponentAgainWhenItFailedToStart()
throws Exception {
+ FailingComponent failing = new FailingComponent();
+ context.getRegistry().bind("failing", failing);
+ AtomicReference<Throwable> first = new AtomicReference<>();
+ Thread creator = new Thread(() -> getEndpoint(context, "failing:a",
first), "creator");
+ creator.start();
+ // the creator thread has created the component and is starting it
+ assertTrue(starting.await(5, TimeUnit.SECONDS));
+
+ AtomicReference<Component> seen = new AtomicReference<>();
+ AtomicReference<Throwable> second = new AtomicReference<>();
+ Thread other = new Thread(() -> {
+ try {
+ seen.set(context.getComponent("failing"));
+ } catch (Throwable e) {
+ second.set(e);
+ }
+ }, "other");
+ other.start();
+ // the other thread waits for the creator to have started the component
+ await().atMost(5, TimeUnit.SECONDS).until(() -> isWaiting(other));
+
+ release.countDown();
+ creator.join(5000);
+ other.join(5000);
+ assertFalse(creator.isAlive());
+ assertFalse(other.isAlive());
+
+ assertNotNull(first.get(), "the creator gets the failure of the
start");
+ assertNull(second.get(), () -> String.valueOf(second.get()));
+ assertSame(failing, seen.get());
+ assertEquals(1, failing.starts.get(), "started once");
+ assertTrue(failing.failed());
+ }
+
+ @Test
+ void testStoppedComponentFromTheRegistryIsStartedByItsCreator() {
+ // a control (as on main): a bean from the registry that is created
again after removeComponent stopped it, is
+ // started again by the thread that creates it
+ release.countDown();
+ context.getRegistry().bind("slow", component);
+ assertNotNull(context.getEndpoint("slow:a"));
+ assertNotNull(context.removeComponent("slow"));
+ assertTrue(component.isStopped());
+
+ assertNotNull(context.getEndpoint("slow:b"));
+ assertTrue(component.isStarted());
+ assertEquals(ServiceStatus.Started, component.statusAtCreate.get("b"));
+ }
+
+ @Test
+ void testComponentAddedBeforeCamelContextStartsIsStartedByCamelContext()
throws Exception {
+ // another thread that gets the component while CamelContext starts,
before CamelContext has started its
+ // components, leaves it to CamelContext, which starts it as a startup
step
+ release.countDown();
+ CamelContext startingContext = new DefaultCamelContext();
+ startingContext.addComponent("slow", component);
+ AtomicReference<String> startedByWhenGot = new AtomicReference<>("not
got");
+ startingContext.getCamelContextExtension().setStartupStepRecorder(new
DefaultStartupStepRecorder() {
+ @Override
+ public StartupStep beginStep(Class<?> type, String id, String
description) {
+ if ("slow".equals(id) && "Start
Component".equals(description)) {
+ Thread other = new Thread(() -> {
+ startingContext.getComponent("slow");
+ startedByWhenGot.set(component.startedBy);
+ }, "other");
+ other.start();
+ try {
+ other.join(5000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ return super.beginStep(type, id, description);
+ }
+ });
+ try {
+ startingContext.start();
+ assertNull(startedByWhenGot.get(), "not started by the other
thread");
+ assertEquals(Thread.currentThread().getName(),
component.startedBy);
+ } finally {
+ startingContext.stop();
+ }
+ }
+
+ private void resolveFromTwoThreads(CamelContext camelContext) throws
Exception {
+ AtomicReference<Throwable> first = new AtomicReference<>();
+ Thread creator = new Thread(() -> getEndpoint(camelContext, "slow:a",
first), "creator");
+ creator.start();
+ // the creator thread has created the component and is starting it
+ assertTrue(starting.await(5, TimeUnit.SECONDS));
+
+ AtomicReference<Throwable> second = new AtomicReference<>();
+ Thread other = new Thread(() -> getEndpoint(camelContext, "slow:b",
second), "other");
+ other.start();
+ // the other thread either created its endpoint already, or waits for
the component to be started
+ await().atMost(5, TimeUnit.SECONDS).until(() -> created.getCount() ==
1 || isWaiting(other));
+
+ release.countDown();
+ creator.join(5000);
+ other.join(5000);
+ assertFalse(creator.isAlive());
+ assertFalse(other.isAlive());
+
+ assertNull(first.get(), () -> String.valueOf(first.get()));
+ assertNull(second.get(), () -> String.valueOf(second.get()));
+ assertEquals(ServiceStatus.Started, component.statusAtCreate.get("a"));
+ assertEquals(ServiceStatus.Started, component.statusAtCreate.get("b"));
+ }
+
+ private static boolean isWaiting(Thread thread) {
+ Thread.State state = thread.getState();
+ return state == Thread.State.WAITING || state ==
Thread.State.TIMED_WAITING || state == Thread.State.BLOCKED;
+ }
+
+ private static void getEndpoint(CamelContext camelContext, String uri,
AtomicReference<Throwable> failure) {
+ try {
+ assertNotNull(camelContext.getEndpoint(uri));
+ } catch (Throwable e) {
+ failure.set(e);
+ }
+ }
+
+ /**
+ * A component that fails to start (such as when it cannot connect).
+ */
+ private final class FailingComponent extends DefaultComponent {
+ private final AtomicInteger starts = new AtomicInteger();
+
+ @Override
+ protected void doStart() throws Exception {
+ super.doStart();
+ starts.incrementAndGet();
+ starting.countDown();
+ assertTrue(release.await(5, TimeUnit.SECONDS));
+ throw new IllegalStateException("Cannot connect");
+ }
+
+ boolean failed() {
+ return status == FAILED;
+ }
+
+ @Override
+ protected Endpoint createEndpoint(String uri, String remaining,
Map<String, Object> parameters) {
+ throw new UnsupportedOperationException(uri);
+ }
+ }
+
+ /**
+ * A component that creates the client its endpoints use when it starts
(as for example vertx-http creates its Vertx
+ * instance).
+ */
+ private final class SlowComponent extends DefaultComponent {
+ private final Map<String, ServiceStatus> statusAtCreate = new
ConcurrentHashMap<>();
+ private volatile Object client;
+ private volatile String startedBy;
+
+ @Override
+ protected void doStart() throws Exception {
+ super.doStart();
+ startedBy = Thread.currentThread().getName();
+ starting.countDown();
+ assertTrue(release.await(5, TimeUnit.SECONDS));
+ client = new Object();
+ }
+
+ @Override
+ protected Endpoint createEndpoint(String uri, String remaining,
Map<String, Object> parameters) {
+ statusAtCreate.put(remaining, getStatus());
+ created.countDown();
+ // the endpoint needs the client of the component
+ String name = remaining + "@" + client.hashCode();
+ return new DefaultEndpoint(uri, this) {
+ @Override
+ public Producer createProducer() {
+ throw new UnsupportedOperationException(name);
+ }
+
+ @Override
+ public Consumer createConsumer(Processor processor) {
+ throw new UnsupportedOperationException(name);
+ }
+ };
+ }
+ }
+}