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 e4315e71a3e1 CAMEL-26634: camel-core - Resequence EIP: batch mode can 
run two senders after a route restart (#29370)
e4315e71a3e1 is described below

commit e4315e71a3e12ac99c4738cfc89f542400851758
Author: allthingssecurity <[email protected]>
AuthorDate: Sat Nov 7 13:31:26 2026 +0530

    CAMEL-26634: camel-core - Resequence EIP: batch mode can run two senders 
after a route restart (#29370)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/processor/Resequencer.java    |  46 ++++++-
 .../processor/BatchResequencerRestartTest.java     | 146 +++++++++++++++++++++
 2 files changed, 189 insertions(+), 3 deletions(-)

diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/Resequencer.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/Resequencer.java
index b84d3b6fb9f4..89232bbf0972 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/Resequencer.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/Resequencer.java
@@ -67,6 +67,7 @@ public class Resequencer extends BaseProcessorSupport
     public static final int DEFAULT_BATCH_SIZE = 100;
 
     private static final Logger LOG = 
LoggerFactory.getLogger(Resequencer.class);
+    private static final long PREVIOUS_SENDER_WARN_TIMEOUT = 10000;
 
     private String id;
     private String routeId;
@@ -87,6 +88,8 @@ public class Resequencer extends BaseProcessorSupport
     private final Collection<Exchange> collection;
     private ExceptionHandler exceptionHandler;
     private BatchSender sender;
+    // the sender of the last start, which may still be delivering an exchange 
when the route is started again
+    private BatchSender stoppedSender;
 
     public Resequencer(CamelContext camelContext, Processor processor, 
Expression expression) {
         this(camelContext, processor, createSet(expression, false, false), 
expression);
@@ -350,13 +353,15 @@ public class Resequencer extends BaseProcessorSupport
     @Override
     protected void doStart() throws Exception {
         ServiceHelper.startService(processor);
-        sender = new BatchSender();
+        sender = new BatchSender(stoppedSender);
+        stoppedSender = null;
         sender.start();
     }
 
     @Override
     protected void doStop() throws Exception {
         if (sender != null) {
+            stoppedSender = sender;
             try {
                 sender.cancel();
             } catch (Exception e) {
@@ -427,14 +432,25 @@ public class Resequencer extends BaseProcessorSupport
         private final AtomicBoolean exchangeEnqueued = new AtomicBoolean();
         private final Queue<String> completionPredicateMatched = new 
ConcurrentLinkedQueue<>();
         private final Condition exchangeEnqueuedCondition = 
queueLock.newCondition();
+        private volatile boolean cancelled;
+        private BatchSender previous;
 
-        BatchSender() {
+        BatchSender(BatchSender previous) {
             
super(camelContext.getExecutorServiceManager().resolveThreadName("Batch 
Sender"));
             this.queue = new LinkedList<>();
+            this.previous = previous;
         }
 
         @Override
         public void run() {
+            // the previous sender may still be sending from the shared 
collection, so wait for it to end
+            if (previous != null) {
+                awaitPrevious();
+            }
+            if (cancelled) {
+                return;
+            }
+
             // Wait until one of either:
             // * an exchange being queued;
             // * the batch timeout expiring; or
@@ -512,7 +528,7 @@ public class Resequencer extends BaseProcessorSupport
                         break;
                     }
 
-                } while (isRunAllowed());
+                } while (isRunAllowed() && !cancelled);
 
             } finally {
                 queueLock.unlock();
@@ -543,7 +559,31 @@ public class Resequencer extends BaseProcessorSupport
             }
         }
 
+        private void awaitPrevious() {
+            boolean interrupted = false;
+            boolean logged = false;
+            while (previous.isAlive()) {
+                try {
+                    previous.join(PREVIOUS_SENDER_WARN_TIMEOUT);
+                    if (!logged && previous.isAlive()) {
+                        logged = true;
+                        LOG.warn("Route: {} waiting for previous batch sender: 
{} to complete sending its batch", routeId,
+                                previous.getName());
+                    }
+                } catch (InterruptedException e) {
+                    // keep waiting, as a sender started later waits for this 
one
+                    interrupted = true;
+                }
+            }
+            previous = null;
+            if (interrupted) {
+                Thread.currentThread().interrupt();
+            }
+        }
+
         public void cancel() {
+            // also end on a flag, as the processing may clear the interrupt 
status
+            cancelled = true;
             interrupt();
         }
 
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/BatchResequencerRestartTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/BatchResequencerRestartTest.java
new file mode 100644
index 000000000000..735a2652260d
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/BatchResequencerRestartTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.processor;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+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.assertTrue;
+
+/**
+ * The batch resequencer must deliver one exchange at a time, in order, when 
its route is stopped and started while the
+ * sender thread of the previous start is still delivering an exchange, and 
that sender must end.
+ */
+public class BatchResequencerRestartTest extends ContextTestSupport {
+
+    private final List<String> events = new CopyOnWriteArrayList<>();
+    private final CountDownLatch firstInside = new CountDownLatch(1);
+    private final CountDownLatch otherStarted = new CountDownLatch(1);
+    private volatile boolean restoreInterrupt = true;
+
+    @Test
+    public void testRestartWhileDeliveringKeepsOrder() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedBodiesReceived("1", "2", "3", "4");
+
+        restartWhileDeliveringFirst();
+
+        mock.assertIsSatisfied();
+        assertEquals(List.of("start-1", "end-1", "start-2", "end-2", 
"start-3", "end-3", "start-4", "end-4"), events);
+    }
+
+    @Test
+    public void testRestartTwiceWhileDeliveringKeepsOrder() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedBodiesReceived("1", "2", "3", "4");
+
+        restartWhileDeliveringFirst(2);
+
+        mock.assertIsSatisfied();
+        assertEquals(List.of("start-1", "end-1", "start-2", "end-2", 
"start-3", "end-3", "start-4", "end-4"), events);
+        await().atMost(10, TimeUnit.SECONDS).until(() -> senderThreads() == 1);
+    }
+
+    @Test
+    public void testSenderThreadEndsWhenRestartedWhileDelivering() throws 
Exception {
+        // the processing of the first exchange does not keep the interrupt 
status of the thread, as some libraries do
+        restoreInterrupt = false;
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedBodiesReceivedInAnyOrder("1", "2", "3", "4");
+
+        restartWhileDeliveringFirst();
+
+        mock.assertIsSatisfied();
+        // only the sender of the last start is left
+        await().atMost(10, TimeUnit.SECONDS).until(() -> senderThreads() == 1);
+    }
+
+    private void restartWhileDeliveringFirst() throws Exception {
+        restartWhileDeliveringFirst(1);
+    }
+
+    private void restartWhileDeliveringFirst(int restarts) throws Exception {
+        // a batch of 2: the sender delivers 1, and 2 waits in the batch
+        template.sendBodyAndHeader("direct:start", "1", "seq", 1);
+        template.sendBodyAndHeader("direct:start", "2", "seq", 2);
+        assertTrue(firstInside.await(10, TimeUnit.SECONDS));
+
+        // the route is stopped and started while 1 is being delivered (the 
stop does not wait for it)
+        for (int i = 0; i < restarts; i++) {
+            context.getRouteController().stopRoute("batch");
+            context.getRouteController().startRoute("batch");
+        }
+
+        template.sendBodyAndHeader("direct:start", "3", "seq", 3);
+        template.sendBodyAndHeader("direct:start", "4", "seq", 4);
+    }
+
+    private void deliver(Exchange exchange) {
+        String body = exchange.getMessage().getBody(String.class);
+        events.add("start-" + body);
+        if ("1".equals(body)) {
+            firstInside.countDown();
+            // the window in which no other exchange may start while 1 is 
processed
+            boolean interrupted = false;
+            long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1);
+            while (otherStarted.getCount() > 0 && System.nanoTime() < 
deadline) {
+                try {
+                    otherStarted.await(deadline - System.nanoTime(), 
TimeUnit.NANOSECONDS);
+                } catch (InterruptedException e) {
+                    interrupted = true;
+                }
+            }
+            if (interrupted && restoreInterrupt) {
+                Thread.currentThread().interrupt();
+            }
+        } else {
+            otherStarted.countDown();
+        }
+        events.add("end-" + body);
+    }
+
+    private long senderThreads() {
+        return Thread.getAllStackTraces().keySet().stream()
+                .filter(Thread::isAlive)
+                .filter(t -> t.getName().contains("(" + context.getName() + 
")"))
+                .filter(t -> t.getName().endsWith("Batch Sender"))
+                .count();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").routeId("batch")
+                        
.resequence(header("seq")).batch().size(2).timeout(60000)
+                        .process(BatchResequencerRestartTest.this::deliver)
+                        .to("mock:result");
+            }
+        };
+    }
+}

Reply via email to