This is an automated email from the ASF dual-hosted git repository.

oscerd 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 aaef7487c407 CAMEL-24789: camel-vertx-websocket - finish an exchange 
with no peer synchronously (#26541)
aaef7487c407 is described below

commit aaef7487c407c6f66f2a4013c224105119a18bc0
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Sep 21 14:47:01 2026 +0200

    CAMEL-24789: camel-vertx-websocket - finish an exchange with no peer 
synchronously (#26541)
    
    * CAMEL-24789: camel-vertx-websocket - finish an exchange with no peer 
synchronously
    
    With no peer to send to, the producer completed its callback with 
doneSync=true
    and then returned false, which tells the caller the opposite: that the 
exchange
    will be finished later, from a write handler that never runs because there 
is
    nothing to write to. AsyncProcessor#process documents the return value as
    doneSync, and the null-body branch a few lines earlier in the same method 
already
    gets this right.
    
    The empty case now completes and returns true, and says at WARN that the 
message
    was not delivered. The path is easy to reach: sendToAll with nothing 
connected
    yet, or a CamelVertxWebsocket.connectionKey that matches no current peer.
    
    A connection key that matches nothing was also dropped in silence, while a 
peer
    whose websocket turned out to be null one line further on did warn. Both 
report
    now.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24789: write the test in the assertion style of the module
    
    camel-vertx-websocket is written against org.junit.jupiter.api.Assertions 
in all
    of its other test classes, so the new one follows suit rather than arriving 
as
    the only AssertJ file, which also takes the assertj test dependency back 
out.
    Same point davsclaus raised on CAMEL-24788.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24789: camel-vertx-websocket - cover the two gaps left in the 
no-peer tests
    
    anExchangeWithNoBodyIsDoneSynchronously asserted only that process() 
returned
    true and that doneSync was true, and a callback completed twice satisfies 
both.
    It now counts callbacks like its sibling does, so a double-invocation fails.
    
    The unmatched connection-key path had no test at all. It is only reachable 
when
    peers exist and the key matches none of them, which needs a real consumer -
    peers are added from a live connection handler, so the producer unit test
    cannot get there. The test therefore lives in VertxWebsocketTest, where the
    server harness already is: one connected peer, a key nobody has, and an
    assertion that the peer receives nothing.
    
    That pins behaviour rather than the log line, which would need a log 
appender
    to assert and would be brittle. It is the behaviour that matters: mutating 
the
    branch to fall back to putAll(peers) - the plausible wrong fix - makes the 
test
    fail, so an unmatched key silently fanning out to whoever is connected is 
now
    caught.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24789: log a broadcast that reaches nobody at debug
    
    Having no peer connected is an ordinary state for a broadcast - a route 
pushing
    status on a timer before any browser attaches hits it on every tick - so 
warning
    each time turns a normal condition into log noise. That line drops to debug.
    
    The misconfiguration keeps its warning: a connection key that matches no 
peer is
    still reported at WARN from getConnectedPeers, which is the case where the 
route
    asked for a specific peer and did not get it.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    ---------
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 .../vertx/websocket/VertxWebsocketProducer.java    |  21 ++++-
 .../VertxWebsocketProducerNoPeerTest.java          | 101 +++++++++++++++++++++
 .../vertx/websocket/VertxWebsocketTest.java        |  29 ++++++
 3 files changed, 147 insertions(+), 4 deletions(-)

diff --git 
a/components/camel-vertx/camel-vertx-websocket/src/main/java/org/apache/camel/component/vertx/websocket/VertxWebsocketProducer.java
 
b/components/camel-vertx/camel-vertx-websocket/src/main/java/org/apache/camel/component/vertx/websocket/VertxWebsocketProducer.java
index a6680c5ed8ed..28ab393b84a3 100644
--- 
a/components/camel-vertx/camel-vertx-websocket/src/main/java/org/apache/camel/component/vertx/websocket/VertxWebsocketProducer.java
+++ 
b/components/camel-vertx/camel-vertx-websocket/src/main/java/org/apache/camel/component/vertx/websocket/VertxWebsocketProducer.java
@@ -60,13 +60,20 @@ public class VertxWebsocketProducer extends 
DefaultAsyncProducer {
             }
 
             Map<String, WebSocketBase> connectedPeers = 
getConnectedPeers(exchange);
-            VertxWebsocketResultHandler vertxWebsocketResultHandler
-                    = new VertxWebsocketResultHandler(exchange, callback, 
connectedPeers.keySet());
 
             if (connectedPeers.isEmpty()) {
+                // nothing was sent, so the exchange is done here rather than 
from a write handler. Having nobody
+                // connected is an ordinary state for a broadcast, so it is 
only worth a debug line: a connection
+                // key that matches no peer is the misconfiguration, and 
getConnectedPeers warns about that one
+                LOG.debug("No WebSocket peer to send to for endpoint {}, the 
message is not delivered",
+                        getEndpoint().getEndpointUri());
                 callback.done(true);
+                return true;
             }
 
+            VertxWebsocketResultHandler vertxWebsocketResultHandler
+                    = new VertxWebsocketResultHandler(exchange, callback, 
connectedPeers.keySet());
+
             // Send message to each peer then record and process the results 
asynchronously
             connectedPeers.forEach((connectionKey, webSocket) -> {
                 Handler<AsyncResult<Void>> handler = result -> {
@@ -121,8 +128,14 @@ public class VertxWebsocketProducer extends 
DefaultAsyncProducer {
             String connectionKey = 
message.getHeader(VertxWebsocketConstants.CONNECTION_KEY, String.class);
             if (connectionKey != null && ObjectHelper.isNotEmpty(peers)) {
                 Stream.of(connectionKey.split(","))
-                        .filter(peers::containsKey)
-                        .forEach(key -> connectedPeers.put(key, 
endpoint.findPeerForConnectionKey(key)));
+                        .forEach(key -> {
+                            if (peers.containsKey(key)) {
+                                connectedPeers.put(key, 
endpoint.findPeerForConnectionKey(key));
+                            } else {
+                                // a key that matches nothing would otherwise 
be dropped without a word
+                                LOG.warn("No WebSocket peer connection found 
for connection key {}", key);
+                            }
+                        });
             } else {
                 // The producer is invoking an external server not managed by 
camel
                 connectedPeers.put(UUID.randomUUID().toString(), 
endpoint.getWebSocket(exchange));
diff --git 
a/components/camel-vertx/camel-vertx-websocket/src/test/java/org/apache/camel/component/vertx/websocket/VertxWebsocketProducerNoPeerTest.java
 
b/components/camel-vertx/camel-vertx-websocket/src/test/java/org/apache/camel/component/vertx/websocket/VertxWebsocketProducerNoPeerTest.java
new file mode 100644
index 000000000000..136017a60c31
--- /dev/null
+++ 
b/components/camel-vertx/camel-vertx-websocket/src/test/java/org/apache/camel/component/vertx/websocket/VertxWebsocketProducerNoPeerTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.websocket;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * An exchange with no peer to send to is finished by the producer itself, so 
it has to report that it was done
+ * synchronously. Builds the endpoint directly, so nothing reaches a server.
+ */
+class VertxWebsocketProducerNoPeerTest {
+
+    private DefaultCamelContext context;
+
+    @AfterEach
+    void tearDown() {
+        if (context != null) {
+            context.stop();
+        }
+    }
+
+    private VertxWebsocketProducer producer() throws Exception {
+        context = new DefaultCamelContext();
+
+        VertxWebsocketComponent component = new VertxWebsocketComponent();
+        component.setCamelContext(context);
+
+        VertxWebsocketEndpoint endpoint
+                = (VertxWebsocketEndpoint) 
component.createEndpoint("vertx-websocket:localhost:1234/test");
+        return (VertxWebsocketProducer) endpoint.createProducer();
+    }
+
+    @Test
+    void anExchangeWithNoPeerIsDoneSynchronously() throws Exception {
+        VertxWebsocketProducer producer = producer();
+
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getIn().setBody("a message nobody is listening for");
+        // broadcasting to an empty host registry is the one path that reaches 
no peer without opening a connection
+        exchange.getIn().setHeader(VertxWebsocketConstants.SEND_TO_ALL, true);
+
+        AtomicInteger callbacks = new AtomicInteger();
+        AtomicReference<Boolean> doneSync = new AtomicReference<>();
+
+        // the callback used to be completed with doneSync=true while the 
method returned false, which says the
+        // opposite: that the exchange would be finished from a write handler 
that never runs
+        boolean result = producer.process(exchange, sync -> {
+            callbacks.incrementAndGet();
+            doneSync.set(sync);
+        });
+
+        assertTrue(result, "process must report that it finished the exchange 
itself");
+        assertEquals(1, callbacks.get());
+        assertTrue(doneSync.get());
+        assertNull(exchange.getException());
+    }
+
+    @Test
+    void anExchangeWithNoBodyIsDoneSynchronously() throws Exception {
+        VertxWebsocketProducer producer = producer();
+
+        Exchange exchange = new DefaultExchange(context);
+
+        AtomicInteger callbacks = new AtomicInteger();
+        AtomicReference<Boolean> doneSync = new AtomicReference<>();
+        boolean result = producer.process(exchange, sync -> {
+            callbacks.incrementAndGet();
+            doneSync.set(sync);
+        });
+
+        assertTrue(result, "process must report that it finished the exchange 
itself");
+        // the same triple as the first test: completing the callback twice 
would also satisfy the other two
+        assertEquals(1, callbacks.get());
+        assertTrue(doneSync.get());
+    }
+}
diff --git 
a/components/camel-vertx/camel-vertx-websocket/src/test/java/org/apache/camel/component/vertx/websocket/VertxWebsocketTest.java
 
b/components/camel-vertx/camel-vertx-websocket/src/test/java/org/apache/camel/component/vertx/websocket/VertxWebsocketTest.java
index 03e006752dff..0a336cb58291 100644
--- 
a/components/camel-vertx/camel-vertx-websocket/src/test/java/org/apache/camel/component/vertx/websocket/VertxWebsocketTest.java
+++ 
b/components/camel-vertx/camel-vertx-websocket/src/test/java/org/apache/camel/component/vertx/websocket/VertxWebsocketTest.java
@@ -38,6 +38,7 @@ import org.apache.camel.component.mock.MockEndpoint;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -117,6 +118,34 @@ public class VertxWebsocketTest extends 
VertxWebSocketTestSupport {
         assertTrue(results.contains("Hello World"));
     }
 
+    @Test
+    void sendWithAnUnmatchedConnectionKeyDeliversToNobody() throws Exception {
+        // the branch this covers needs peers to exist while the key matches 
none of them: a key that is simply
+        // absent from a populated registry is dropped, and before CAMEL-24789 
it was dropped without a word
+        CountDownLatch connected = new CountDownLatch(1);
+        List<String> results = new ArrayList<>();
+        openWebSocketConnection("localhost", port.getPort(), "/test", message 
-> {
+            synchronized (results) {
+                results.add(message);
+                connected.countDown();
+            }
+        });
+
+        VertxWebsocketEndpoint endpoint
+                = context.getEndpoint("vertx-websocket:localhost:" + port + 
"/test", VertxWebsocketEndpoint.class);
+        awaitConnectedPeers(endpoint, 1);
+
+        template.sendBodyAndHeader("vertx-websocket:localhost:" + port + 
"/test", "Hello World",
+                VertxWebsocketConstants.CONNECTION_KEY, 
"a-key-no-peer-ever-had");
+
+        // the send returns rather than hanging, and the connected peer is 
left untouched - the message went
+        // nowhere, which is the point: an unmatched key must not silently fan 
out to whoever happens to be there
+        assertFalse(connected.await(2, TimeUnit.SECONDS), "no peer should have 
received the message");
+        synchronized (results) {
+            assertEquals(List.of(), results);
+        }
+    }
+
     @Test
     void testSendWithConnectionKeyForParameterizedPath() throws Exception {
         int expectedResultCount = 1;

Reply via email to