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 8b9cbcd3748b camel-vertx: fail the start when the EventBus never
becomes ready (#29681)
8b9cbcd3748b is described below
commit 8b9cbcd3748b3acd5c36f014f4d6a8db03c1d12f
Author: dhairya05 <[email protected]>
AuthorDate: Fri Nov 13 12:53:42 2026 +0530
camel-vertx: fail the start when the EventBus never becomes ready (#29681)
Co-authored-by: Guillaume Nodet - AI Bot <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/component/vertx/VertxComponent.java | 51 +++++-
.../camel/component/vertx/VertxStartupTest.java | 190 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 14 ++
3 files changed, 247 insertions(+), 8 deletions(-)
diff --git
a/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxComponent.java
b/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxComponent.java
index 9a9ab509514f..c2ab979c8b4d 100644
---
a/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxComponent.java
+++
b/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxComponent.java
@@ -19,7 +19,9 @@ package org.apache.camel.component.vertx;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.VertxBuilder;
import io.vertx.core.VertxOptions;
@@ -162,34 +164,67 @@ public class VertxComponent extends DefaultComponent {
createdVertx = true;
final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference<Throwable> failure = new AtomicReference<>();
+ // the callback writes here rather than to the field, so a build
that completes after the timeout
+ // cannot leave a half-published Vert.x behind
+ final AtomicReference<Vertx> created = new AtomicReference<>();
+ Future<Vertx> clusteredVertx = null;
// lets using a host / port if a host name is specified
if (clustered) {
+ if (timeout <= 0) {
+ throw new IllegalArgumentException(
+ "The timeout option must be a positive number of
seconds, was: " + timeout);
+ }
+
LOG.info("Creating Clustered Vertx {}:{}",
vertxOptions.getEventBusOptions().getHost(),
vertxOptions.getEventBusOptions().getPort());
// use the async api as we want to wait for the eventbus to be
ready before we are in started state
- vertxFactory.buildClustered().onComplete(event -> {
+ clusteredVertx = vertxFactory.buildClustered();
+ clusteredVertx.onComplete(event -> {
if (event.cause() != null) {
- LOG.warn("Error creating Clustered Vertx {}:{} due
{}", host, port,
- event.cause().getMessage(), event.cause());
+ failure.set(event.cause());
} else if (event.succeeded()) {
- vertx = event.result();
- LOG.info("EventBus is ready: {}", vertx);
+ created.set(event.result());
}
latch.countDown();
});
} else {
LOG.info("Creating Non-Clustered Vertx");
- vertx = vertxFactory.build();
- LOG.info("EventBus is ready: {}", vertx);
+ created.set(vertxFactory.build());
latch.countDown();
}
if (latch.getCount() > 0) {
LOG.info("Waiting for EventBus to be ready using {} sec as
timeout", timeout);
- latch.await(timeout, TimeUnit.SECONDS);
+ if (!latch.await(timeout, TimeUnit.SECONDS)) {
+ // the build is still running: a node that joins the
cluster after we gave up would keep its
+ // event-loop threads alive with nobody left to close it.
onSuccess also fires at once if the
+ // future completed between the await and this
registration, so no window is left open
+ clusteredVertx.onSuccess(v -> {
+ LOG.warn("Closing the Vert.x that became ready after
the timeout of {} seconds,"
+ + " as the component start has already
failed",
+ timeout);
+ v.close();
+ });
+ throw new IllegalStateException(
+ "The Vert.x EventBus was not ready within the
timeout of " + timeout + " seconds");
+ }
}
+
+ // the component must not reach the started state with a
half-built Vert.x: every endpoint created
+ // afterwards would dereference it
+ if (failure.get() != null) {
+ throw new IllegalStateException(
+ "Error creating Clustered Vertx " + host + ":" + port,
failure.get());
+ }
+ if (created.get() == null) {
+ throw new IllegalStateException("The Vert.x instance was not
created");
+ }
+
+ vertx = created.get();
+ LOG.info("EventBus is ready: {}", vertx);
} else {
LOG.debug("Using Vertx instance set on the component level.");
}
diff --git
a/components/camel-vertx/camel-vertx/src/test/java/org/apache/camel/component/vertx/VertxStartupTest.java
b/components/camel-vertx/camel-vertx/src/test/java/org/apache/camel/component/vertx/VertxStartupTest.java
new file mode 100644
index 000000000000..00b069447872
--- /dev/null
+++
b/components/camel-vertx/camel-vertx/src/test/java/org/apache/camel/component/vertx/VertxStartupTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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.vertx;
+
+import java.lang.reflect.Proxy;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.core.Vertx;
+import io.vertx.core.VertxBuilder;
+import io.vertx.core.VertxOptions;
+import io.vertx.core.spi.VertxMetricsFactory;
+import io.vertx.core.spi.VertxTracerFactory;
+import io.vertx.core.spi.cluster.ClusterManager;
+import io.vertx.core.transport.Transport;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The component waits for the clustered EventBus before it reports itself
started, so a build that never finishes or
+ * that fails has to fail the start rather than leave the component without a
Vert.x instance.
+ */
+public class VertxStartupTest {
+
+ @Test
+ public void shouldFailWhenTheEventBusIsNotReadyWithinTheTimeout() throws
Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ // a build that never completes, so the component waits out its
timeout
+ VertxComponent component = clusteredComponent(context,
Promise.<Vertx> promise().future());
+ component.setTimeout(1);
+
+ Exception e = assertThrows(Exception.class, component::start);
+
+ assertTrue(messages(e).contains("not ready within the timeout"),
messages(e));
+ assertNull(component.getVertx(), "the component must not be left
holding a half-built Vert.x");
+ }
+ }
+
+ @Test
+ public void shouldFailWhenBuildingTheClusteredVertxFails() throws
Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ VertxComponent component = clusteredComponent(context,
+ Future.failedFuture(new IllegalStateException("no cluster
manager on the classpath")));
+
+ Exception e = assertThrows(Exception.class, component::start);
+
+ assertTrue(messages(e).contains("Error creating Clustered Vertx"),
messages(e));
+ assertTrue(messages(e).contains("no cluster manager on the
classpath"), messages(e));
+ assertNull(component.getVertx(), "the component must not be left
holding a half-built Vert.x");
+ }
+ }
+
+ @Test
+ public void shouldCloseAClusteredVertxThatArrivesAfterTheTimeout() throws
Exception {
+ final AtomicBoolean closed = new AtomicBoolean();
+ final Promise<Vertx> buildClustered = Promise.promise();
+
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ VertxComponent component = clusteredComponent(context,
buildClustered.future());
+ component.setTimeout(1);
+
+ assertThrows(Exception.class, component::start);
+
+ // the cluster join finishes a moment after the component gave up
on it. A context-less promise runs its
+ // listeners on the completing thread, so close() has been called
by the time complete() returns
+ buildClustered.complete(recordingVertx(closed));
+
+ assertTrue(closed.get(), "a Vert.x that joins the cluster after
the timeout has to be closed again");
+ }
+ }
+
+ @Test
+ public void shouldRejectANonPositiveTimeout() throws Exception {
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ VertxComponent component = clusteredComponent(context,
Promise.<Vertx> promise().future());
+ component.setTimeout(0);
+
+ Exception e = assertThrows(Exception.class, component::start);
+
+ assertTrue(messages(e).contains("must be a positive number of
seconds"), messages(e));
+ }
+ }
+
+ private static VertxComponent clusteredComponent(DefaultCamelContext
context, Future<Vertx> clustered) {
+ VertxComponent component = new VertxComponent();
+ component.setCamelContext(context);
+ // a host makes the component build a clustered Vert.x, which is the
asynchronous path
+ component.setHost("localhost");
+ component.setVertxFactory(new StubVertxBuilder(clustered));
+ return component;
+ }
+
+ /**
+ * A Vert.x that records whether it was closed. A dynamic proxy keeps this
to the one method the test cares about,
+ * rather than stubbing the whole interface.
+ */
+ private static Vertx recordingVertx(AtomicBoolean closed) {
+ return (Vertx) Proxy.newProxyInstance(Vertx.class.getClassLoader(),
new Class<?>[] { Vertx.class },
+ (proxy, method, args) -> {
+ switch (method.getName()) {
+ case "close":
+ closed.set(true);
+ return Future.succeededFuture();
+ case "toString":
+ return "recording Vertx";
+ case "hashCode":
+ return System.identityHashCode(proxy);
+ case "equals":
+ return proxy == args[0];
+ default:
+ return null;
+ }
+ });
+ }
+
+ private static String messages(Throwable t) {
+ StringBuilder sb = new StringBuilder();
+ for (Throwable current = t; current != null; current =
current.getCause()) {
+ sb.append(current).append(System.lineSeparator());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * A builder whose clustered build is whatever the test hands it.
+ */
+ private static final class StubVertxBuilder implements VertxBuilder {
+
+ private final Future<Vertx> clustered;
+
+ private StubVertxBuilder(Future<Vertx> clustered) {
+ this.clustered = clustered;
+ }
+
+ @Override
+ public VertxBuilder with(VertxOptions options) {
+ return this;
+ }
+
+ @Override
+ public VertxBuilder withMetrics(VertxMetricsFactory factory) {
+ return this;
+ }
+
+ @Override
+ public VertxBuilder withTracer(VertxTracerFactory factory) {
+ return this;
+ }
+
+ @Override
+ public VertxBuilder withTransport(Transport transport) {
+ return this;
+ }
+
+ @Override
+ public VertxBuilder withClusterManager(ClusterManager clusterManager) {
+ return this;
+ }
+
+ @Override
+ public Vertx build() {
+ throw new UnsupportedOperationException("this test only builds a
clustered Vert.x");
+ }
+
+ @Override
+ public Future<Vertx> buildClustered() {
+ return clustered;
+ }
+ }
+
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 3c317079f677..8b6a06b99231 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -8489,3 +8489,17 @@ With `allowTemplateFromHeader=true`, a script from the
`CamelLanguageScript` hea
Before, with `cacheScript=true` it could replace the cached script of the
endpoint, so later messages without the header
ran it too. The header is now removed after use, so a later language endpoint
in the route runs its own script instead
of the header's. Set the header again before each language endpoint that
should use it.
+
+=== camel-vertx - the component fails to start when the clustered Vert.x is
not created
+
+When the component builds its own clustered Vert.x (the component `host` or
`port` option is set and no `vertxOptions` is
+given), it waits for the EventBus to be ready before it reports itself
started. That wait used to be ineffective: the
+result of the bounded wait was discarded and a failed build only logged a
warning, so the component started with no
+Vert.x instance and the first endpoint, producer or consumer to use it failed
with a `NullPointerException`.
+
+The component now fails to start, with the cause attached, when the clustered
Vert.x cannot be built or is not ready within
+the `timeout` option. An application whose cluster join is slower than
`timeout` (60 seconds by default) has to raise it.
+A Vert.x that completes after the timeout is closed, rather than being left
joined to the cluster with nobody to stop it.
+
+`timeout` must now be a positive number of seconds on that path: `timeout=0`
used to mean "do not wait", which left the
+component without a Vert.x, and is rejected with a clear message instead.