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 4b504fd443b4 CAMEL-25730: camel-web3j - subscribe again when the 
subscription of the consumer ends with an error (#28037)
4b504fd443b4 is described below

commit 4b504fd443b41b8f6096854cb44effd16688d4c0
Author: allthingssecurity <[email protected]>
AuthorDate: Sun Oct 18 21:00:32 2026 +0530

    CAMEL-25730: camel-web3j - subscribe again when the subscription of the 
consumer ends with an error (#28037)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/component/web3j/Web3jConsumer.java       | 122 ++++++++++++-
 .../web3j/Web3jConsumerResubscribeTest.java        | 196 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  11 ++
 3 files changed, 323 insertions(+), 6 deletions(-)

diff --git 
a/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jConsumer.java
 
b/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jConsumer.java
index 1716ab9e8efb..ebff5d510a80 100644
--- 
a/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jConsumer.java
+++ 
b/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jConsumer.java
@@ -16,6 +16,10 @@
  */
 package org.apache.camel.component.web3j;
 
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
 import io.reactivex.disposables.Disposable;
 import org.apache.camel.Exchange;
 import org.apache.camel.Processor;
@@ -35,10 +39,16 @@ import static 
org.apache.camel.component.web3j.Web3jHelper.toDefaultBlockParamet
  */
 public class Web3jConsumer extends DefaultConsumer {
     private static final Logger LOG = 
LoggerFactory.getLogger(Web3jConsumer.class);
+    // the delay before subscribing again after a subscription ended with an 
error
+    private static final long DEFAULT_RESUBSCRIBE_DELAY = 15000;
     private final Web3jConfiguration configuration;
     private Web3j web3j;
-    private Disposable subscription;
+    private volatile Disposable subscription;
     private Web3jEndpoint endpoint;
+    private long resubscribeDelay = DEFAULT_RESUBSCRIBE_DELAY;
+    private volatile ScheduledExecutorService resubscribeExecutor;
+    // true from a subscription error until the next item, so the failures 
while subscribing again are routed once
+    private volatile boolean failing;
 
     public Web3jConsumer(Web3jEndpoint endpoint, Processor processor, 
Web3jConfiguration configuration) {
         super(endpoint, processor);
@@ -51,13 +61,31 @@ public class Web3jConsumer extends DefaultConsumer {
         return (Web3jEndpoint) super.getEndpoint();
     }
 
+    /**
+     * Sets the delay in millis before subscribing again after a subscription 
ended with an error. Only for tests.
+     */
+    void setResubscribeDelay(long resubscribeDelay) {
+        this.resubscribeDelay = resubscribeDelay;
+    }
+
     @Override
     protected void doStart() throws Exception {
         super.doStart();
 
         this.web3j = getEndpoint().getWeb3j();
+        failing = false;
+        resubscribeExecutor = 
getEndpoint().getCamelContext().getExecutorServiceManager()
+                .newSingleThreadScheduledExecutor(this, 
"Web3jConsumerResubscribe");
 
-        LOG.info("Subscribing to: {}", endpoint.getNodeAddress());
+        subscribe();
+    }
+
+    private void subscribe() {
+        if (failing) {
+            LOG.debug("Subscribing again to: {}", endpoint.getNodeAddress());
+        } else {
+            LOG.info("Subscribing to: {}", endpoint.getNodeAddress());
+        }
         switch (configuration.getOperation()) {
             case Web3jConstants.ETH_LOG_OBSERVABLE:
                 EthFilter ethFilter = 
Web3jEndpoint.buildEthFilter(toDefaultBlockParameter(configuration.getFromBlock()),
@@ -125,10 +153,25 @@ public class Web3jConsumer extends DefaultConsumer {
                 throw new IllegalArgumentException("Unsupported operation " + 
configuration.getOperation());
         }
 
-        LOG.info("Subscribed: {}", this.configuration);
+        if (failing) {
+            LOG.debug("Subscribed again: {}", this.configuration);
+        } else {
+            LOG.info("Subscribed: {}", this.configuration);
+        }
+    }
+
+    /**
+     * An item was received, so the subscription works: an error after this 
one is routed and logged again.
+     */
+    private void received() {
+        if (failing) {
+            failing = false;
+            LOG.info("Receiving again from: {}", endpoint.getNodeAddress());
+        }
     }
 
     private void ethBlockHashObservable(String x) {
+        received();
         LOG.debug("processEthBlock {}", x);
         Exchange exchange = this.getEndpoint().createExchange();
         exchange.getIn().setBody(x);
@@ -136,6 +179,7 @@ public class Web3jConsumer extends DefaultConsumer {
     }
 
     private void ethPendingTransactionHashObservable(String x) {
+        received();
         LOG.debug("processEthBlock {}", x);
         Exchange exchange = this.getEndpoint().createExchange();
         exchange.getIn().setBody(x);
@@ -143,6 +187,7 @@ public class Web3jConsumer extends DefaultConsumer {
     }
 
     private void blockObservable(EthBlock x) {
+        received();
         EthBlock.Block block = x.getBlock();
         LOG.debug("processEthBlock {}", block);
         Exchange exchange = this.getEndpoint().createExchange();
@@ -151,6 +196,7 @@ public class Web3jConsumer extends DefaultConsumer {
     }
 
     private void processTransaction(Transaction x) {
+        received();
         LOG.debug("processTransaction {}", x);
         Exchange exchange = this.getEndpoint().createExchange();
         exchange.getIn().setBody(x);
@@ -158,6 +204,7 @@ public class Web3jConsumer extends DefaultConsumer {
     }
 
     private void ethLogObservable(Log x) {
+        received();
         LOG.debug("processLogObservable {}", x);
         Exchange exchange = this.getEndpoint().createExchange();
         exchange.getIn().setBody(x);
@@ -183,13 +230,76 @@ public class Web3jConsumer extends DefaultConsumer {
 
     private void processError(Throwable throwable, String operation) {
         LOG.debug("processError for operation: {} {}", operation, 
throwable.getMessage(), throwable);
-        Exchange exchange = this.getEndpoint().createExchange();
-        exchange.setException(throwable);
-        processEvent(exchange);
+        boolean again = resubscribes(operation) && isRunAllowed();
+        if (!failing) {
+            failing = true;
+            if (again) {
+                LOG.warn("The {} subscription to {} ended with an error, 
subscribing again every {} ms: {}", operation,
+                        endpoint.getNodeAddress(), resubscribeDelay, 
throwable.getMessage());
+            }
+            Exchange exchange = this.getEndpoint().createExchange();
+            exchange.setException(throwable);
+            processEvent(exchange);
+        } else {
+            LOG.debug("Subscribing again to {} for operation {} failed: {}", 
endpoint.getNodeAddress(), operation,
+                    throwable.getMessage());
+        }
+        ScheduledExecutorService executor = resubscribeExecutor;
+        if (again && executor != null) {
+            try {
+                executor.schedule(() -> resubscribe(operation), 
resubscribeDelay, TimeUnit.MILLISECONDS);
+            } catch (RejectedExecutionException e) {
+                // the consumer is stopping
+                LOG.debug("Not subscribing again to {} as the consumer is 
stopping", endpoint.getNodeAddress());
+            }
+        }
+    }
+
+    /**
+     * Whether a subscription that ended with an error is subscribed again: 
the live operations, which start again at
+     * the latest block. Not the replay of a block range, nor a log filter 
from a given block, which would route the
+     * blocks or logs from the start of the range again.
+     */
+    private boolean resubscribes(String operation) {
+        if (Web3jConstants.REPLAY_BLOCKS_OBSERVABLE.equals(operation)
+                || 
Web3jConstants.REPLAY_TRANSACTIONS_OBSERVABLE.equals(operation)) {
+            return false;
+        }
+        if (Web3jConstants.ETH_LOG_OBSERVABLE.equals(operation)) {
+            String fromBlock = configuration.getFromBlock();
+            return fromBlock == null || "latest".equalsIgnoreCase(fromBlock) 
|| "pending".equalsIgnoreCase(fromBlock);
+        }
+        return true;
+    }
+
+    private void resubscribe(String operation) {
+        if (!isRunAllowed()) {
+            return;
+        }
+        Disposable old = subscription;
+        if (old != null && !old.isDisposed()) {
+            old.dispose();
+        }
+        try {
+            subscribe();
+        } catch (Exception e) {
+            processError(e, operation);
+            return;
+        }
+        // doStop may have run while subscribing (it does not wait for this 
task), and disposed the previous
+        // subscription only
+        Disposable current = subscription;
+        if (!isRunAllowed() && current != null && !current.isDisposed()) {
+            current.dispose();
+        }
     }
 
     @Override
     protected void doStop() throws Exception {
+        if (resubscribeExecutor != null) {
+            
getEndpoint().getCamelContext().getExecutorServiceManager().shutdownNow(resubscribeExecutor);
+            resubscribeExecutor = null;
+        }
         if (subscription != null && !subscription.isDisposed()) {
             subscription.dispose();
         }
diff --git 
a/components/camel-web3j/src/test/java/org/apache/camel/component/web3j/Web3jConsumerResubscribeTest.java
 
b/components/camel-web3j/src/test/java/org/apache/camel/component/web3j/Web3jConsumerResubscribeTest.java
new file mode 100644
index 000000000000..294a71d8f0d8
--- /dev/null
+++ 
b/components/camel-web3j/src/test/java/org/apache/camel/component/web3j/Web3jConsumerResubscribeTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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.web3j;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import io.reactivex.Flowable;
+import org.apache.camel.EndpointInject;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.web3j.protocol.Web3j;
+import org.web3j.protocol.core.methods.response.EthBlock;
+import org.web3j.protocol.core.methods.response.Log;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * A live subscription that ends with an error (such as a failed 
eth_getBlockByHash request inside web3j's
+ * blockFlowable) is subscribed again, instead of leaving a started consumer 
that receives nothing.
+ */
+class Web3jConsumerResubscribeTest extends CamelTestSupport {
+
+    private final Web3j web3j = mock(Web3j.class);
+
+    @EndpointInject("mock:result")
+    private MockEndpoint result;
+
+    @EndpointInject("mock:error")
+    private MockEndpoint error;
+
+    Web3jConsumerResubscribeTest() {
+        // the mocked client is stubbed before the context starts
+        testConfiguration().withUseAdviceWith(true);
+    }
+
+    @Override
+    protected void bindToRegistry(Registry registry) {
+        registry.bind("mockWeb3j", web3j);
+    }
+
+    private static EthBlock block(String number) {
+        EthBlock.Block block = new EthBlock.Block();
+        block.setNumber(number);
+        EthBlock ethBlock = new EthBlock();
+        ethBlock.setResult(block);
+        return ethBlock;
+    }
+
+    private void startRoute(String operation) throws Exception {
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                onException(Exception.class).handled(true).to("mock:error");
+                
from("web3j://http://127.0.0.1:8545?web3j=#mockWeb3j&operation="; + operation)
+                        .routeId("web3j").autoStartup(false)
+                        .to("mock:result");
+            }
+        });
+        context.start();
+        ((Web3jConsumer) 
context.getRoute("web3j").getConsumer()).setResubscribeDelay(10);
+        context.getRouteController().startRoute("web3j");
+    }
+
+    @Test
+    void blockSubscriptionEndedByAFailedRequestIsSubscribedAgain() throws 
Exception {
+        when(web3j.blockFlowable(anyBoolean())).thenReturn(
+                Flowable.concat(Flowable.just(block("0x1")), 
Flowable.error(new IOException("Connection reset"))),
+                Flowable.concat(Flowable.just(block("0x2")), 
Flowable.never()));
+        result.expectedMessageCount(2);
+        error.expectedMessageCount(1);
+
+        startRoute(Web3jConstants.BLOCK_OBSERVABLE);
+
+        MockEndpoint.assertIsSatisfied(context, 5, TimeUnit.SECONDS);
+        assertEquals("0x2", 
result.getReceivedExchanges().get(1).getIn().getBody(EthBlock.Block.class).getNumberRaw());
+    }
+
+    @Test
+    void failuresWhileSubscribingAgainAreRoutedOnce() throws Exception {
+        // the node is unreachable for three attempts, then it answers again
+        IOException down = new IOException("Connection refused");
+        when(web3j.blockFlowable(anyBoolean())).thenReturn(
+                Flowable.error(down), Flowable.error(down), 
Flowable.error(down),
+                Flowable.concat(Flowable.just(block("0x1")), 
Flowable.never()));
+        result.expectedMessageCount(1);
+        error.expectedMessageCount(1);
+
+        startRoute(Web3jConstants.BLOCK_OBSERVABLE);
+
+        MockEndpoint.assertIsSatisfied(context, 5, TimeUnit.SECONDS);
+        verify(web3j, times(4)).blockFlowable(anyBoolean());
+    }
+
+    @Test
+    void replayOfARangeIsNotSubscribedAgain() throws Exception {
+        // a replay of a block range would route the blocks of the range again
+        when(web3j.replayPastBlocksFlowable(any(), any(), anyBoolean()))
+                .thenReturn(Flowable.concat(Flowable.just(block("0x1")), 
Flowable.error(new IOException("Connection reset"))));
+        result.expectedMessageCount(1);
+        error.expectedMessageCount(1);
+
+        startRoute(Web3jConstants.REPLAY_BLOCKS_OBSERVABLE);
+
+        MockEndpoint.assertIsSatisfied(context, 5, TimeUnit.SECONDS);
+        verify(web3j, after(200).times(1)).replayPastBlocksFlowable(any(), 
any(), anyBoolean());
+    }
+
+    @Test
+    void logFilterFromAGivenBlockIsNotSubscribedAgain() throws Exception {
+        // a new filter from the same block would route the logs from that 
block again
+        when(web3j.ethLogFlowable(any())).thenReturn(Flowable.error(new 
IOException("Connection refused")));
+        error.expectedMessageCount(1);
+
+        startRoute(Web3jConstants.ETH_LOG_OBSERVABLE + "&fromBlock=16");
+
+        MockEndpoint.assertIsSatisfied(context, 5, TimeUnit.SECONDS);
+        verify(web3j, after(200).times(1)).ethLogFlowable(any());
+    }
+
+    @Test
+    void logFilterFromTheLatestBlockIsSubscribedAgain() throws Exception {
+        Log log = new Log();
+        log.setData("0x01");
+        when(web3j.ethLogFlowable(any())).thenReturn(
+                Flowable.error(new IOException("Connection refused")),
+                Flowable.concat(Flowable.just(log), Flowable.never()));
+        result.expectedMessageCount(1);
+        error.expectedMessageCount(1);
+
+        startRoute(Web3jConstants.ETH_LOG_OBSERVABLE);
+
+        MockEndpoint.assertIsSatisfied(context, 5, TimeUnit.SECONDS);
+        verify(web3j, times(2)).ethLogFlowable(any());
+    }
+
+    @Test
+    void subscriptionMadeWhileTheConsumerStopsIsDisposed() throws Exception {
+        // the second subscription is made while the route stops: the request 
to the node is slow and is not
+        // interrupted by the shutdown of the executor, so it ends after 
doStop disposed the previous subscription
+        CountDownLatch subscribing = new CountDownLatch(1);
+        CountDownLatch release = new CountDownLatch(1);
+        CountDownLatch cancelled = new CountDownLatch(1);
+        Flowable<EthBlock> slow = Flowable.defer(() -> {
+            subscribing.countDown();
+            boolean released = false;
+            while (!released) {
+                try {
+                    released = release.await(10, TimeUnit.SECONDS);
+                    if (!released) {
+                        throw new IllegalStateException("not released");
+                    }
+                } catch (InterruptedException e) {
+                    // ignore the interrupt of the executor shutdown, as a 
blocking request may do
+                }
+            }
+            return Flowable.<EthBlock> never();
+        }).doOnCancel(cancelled::countDown);
+        when(web3j.blockFlowable(anyBoolean())).thenReturn(Flowable.error(new 
IOException("Connection reset")), slow);
+        error.expectedMessageCount(1);
+
+        startRoute(Web3jConstants.BLOCK_OBSERVABLE);
+        assertTrue(subscribing.await(5, TimeUnit.SECONDS));
+        context.getRouteController().stopRoute("web3j");
+        release.countDown();
+
+        assertTrue(cancelled.await(5, TimeUnit.SECONDS), "the subscription 
made while stopping must be disposed");
+        MockEndpoint.assertIsSatisfied(context);
+    }
+}
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 861d15d4719a..5d31b92772b7 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
@@ -990,6 +990,17 @@ new `compile` option of the endpoint (default `false`). 
The module is now loaded
 every producer, so stopping and starting a route does not read the module 
again: remove the route and add it
 again, or restart the application, to load a module that has changed.
 
+=== camel-web3j - the consumer subscribes again after an error
+
+When the subscription of a consumer ends with an error (for example a request 
to the node that fails inside
+`BLOCK_OBSERVABLE` or `TRANSACTION_OBSERVABLE`, or a node that cannot be 
reached when the route starts), the consumer
+now subscribes again every 15 seconds until it succeeds. The error is routed 
as before, and logged at `WARN`; the
+failed attempts that follow are logged at `DEBUG`. Previously the consumer 
received nothing more while the route
+stayed started. A new subscription starts at the latest block, so the blocks 
between the error and the new
+subscription are not routed. `REPLAY_BLOCKS_OBSERVABLE`, 
`REPLAY_TRANSACTIONS_OBSERVABLE` and an
+`ETH_LOG_OBSERVABLE` with `fromBlock` set to a block number are not subscribed 
again, as that would route the same
+blocks or logs again.
+
 === camel-vertx-http - a header with several values is sent as several headers
 
 A message header whose value is a `java.util.Collection` or an object array 
(as the HTTP consumers map a request

Reply via email to