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 b0fb55a79e20 CAMEL-23918: Fix flaky tests in camel-reactive-streams
b0fb55a79e20 is described below

commit b0fb55a79e2002ba532878cc943b46b5a5cca0f8
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Jul 8 07:09:23 2026 +0200

    CAMEL-23918: Fix flaky tests in camel-reactive-streams
    
    Replace Thread.sleep calls with Awaitility and Camel-idiomatic patterns
    across 5 test classes. Fix a race condition in BackpressureStrategyTest
    where the async flush in CamelSubscription caused exact-value assertions
    to fail non-deterministically — replaced with invariant-based assertions
    that verify actual backpressure strategy behavior (OLDEST keeps first
    item, LATEST keeps most recent).
    
    Closes #24448
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 components/camel-reactive-streams/pom.xml          |  6 ++
 .../BackpressurePublisherRoutePolicyTest.java      | 13 ++--
 .../reactive/streams/BackpressureStrategyTest.java | 79 +++++++++++++++++-----
 .../streams/BackpressureSubscriberTest.java        |  6 +-
 .../reactive/streams/BasicPublisherTest.java       |  5 +-
 .../reactive/streams/DelayedMonoPublisherTest.java | 36 +++++-----
 6 files changed, 97 insertions(+), 48 deletions(-)

diff --git a/components/camel-reactive-streams/pom.xml 
b/components/camel-reactive-streams/pom.xml
index 73ad33c8fa20..348873ed66d0 100644
--- a/components/camel-reactive-streams/pom.xml
+++ b/components/camel-reactive-streams/pom.xml
@@ -87,6 +87,12 @@
             <artifactId>assertj-core</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <version>${awaitility-version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git 
a/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BackpressurePublisherRoutePolicyTest.java
 
b/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BackpressurePublisherRoutePolicyTest.java
index c3c603012c73..b61e678d1ac0 100644
--- 
a/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BackpressurePublisherRoutePolicyTest.java
+++ 
b/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BackpressurePublisherRoutePolicyTest.java
@@ -30,6 +30,7 @@ import 
org.apache.camel.throttling.ThrottlingInflightRoutePolicy;
 import org.junit.jupiter.api.Test;
 import org.reactivestreams.Publisher;
 
+import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -82,11 +83,11 @@ public class BackpressurePublisherRoutePolicyTest extends 
BaseReactiveTest {
 
         // fire a delayed request from the subscriber (required by camel core)
         subscriber.request(1);
-        Thread.sleep(250);
 
+        // Wait for the route policy to stop or suspend the route
         StatefulService service = (StatefulService) 
context().getRoute("policy-route").getConsumer();
-        // ensure the route is stopped or suspended
-        assertTrue(service.isStopped() || service.isSuspended());
+        await().atMost(5, TimeUnit.SECONDS)
+                .untilAsserted(() -> assertTrue(service.isStopped() || 
service.isSuspended()));
 
         // request all the remaining exchanges
         subscriber.request(24);
@@ -141,11 +142,11 @@ public class BackpressurePublisherRoutePolicyTest extends 
BaseReactiveTest {
 
         // fire a delayed request from the subscriber (required by camel core)
         subscriber.request(1);
-        Thread.sleep(250);
 
+        // Wait for the route policy to stop or suspend the route
         StatefulService service = (StatefulService) 
context().getRoute("policy-route").getConsumer();
-        // ensure the route is stopped or suspended
-        assertTrue(service.isStopped() || service.isSuspended());
+        await().atMost(5, TimeUnit.SECONDS)
+                .untilAsserted(() -> assertTrue(service.isStopped() || 
service.isSuspended()));
         subscriber.cancel();
 
         // request other exchanges to ensure that the route works
diff --git 
a/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BackpressureStrategyTest.java
 
b/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BackpressureStrategyTest.java
index 46c52aa23262..53c9ba7ac865 100644
--- 
a/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BackpressureStrategyTest.java
+++ 
b/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BackpressureStrategyTest.java
@@ -16,9 +16,13 @@
  */
 package org.apache.camel.component.reactive.streams;
 
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import io.reactivex.Flowable;
 import org.apache.camel.Exchange;
@@ -27,6 +31,7 @@ import 
org.apache.camel.component.reactive.streams.api.CamelReactiveStreams;
 import org.apache.camel.component.reactive.streams.support.TestSubscriber;
 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;
 
@@ -73,11 +78,14 @@ public class BackpressureStrategyTest extends 
BaseReactiveTest {
         ReactiveStreamsComponent comp = (ReactiveStreamsComponent) 
context().getComponent("reactive-streams");
         
comp.setBackpressureStrategy(ReactiveStreamsBackpressureStrategy.OLDEST);
 
+        AtomicInteger timerCount = new AtomicInteger(0);
+
         new RouteBuilder() {
             @Override
             public void configure() {
                 from("timer:gen?period=20&repeatCount=20&includeMetadata=true")
                         .setBody().header(Exchange.TIMER_COUNTER)
+                        .process(e -> timerCount.incrementAndGet())
                         .to("reactive-streams:integers");
             }
         }.addRoutesToCamelContext(context);
@@ -100,14 +108,25 @@ public class BackpressureStrategyTest extends 
BaseReactiveTest {
         context().start();
 
         assertTrue(latch.await(5, TimeUnit.SECONDS));
-        Thread.sleep(1000); // wait for all numbers to be generated
+        // Wait for all 20 timer events to be processed
+        await().atMost(5, TimeUnit.SECONDS).until(() -> timerCount.get() >= 
20);
 
         subscriber.request(19);
-        assertTrue(latch2.await(1, TimeUnit.SECONDS));
-        Thread.sleep(200); // add other time to ensure no other items arrive
-        assertEquals(2, queue.size());
-        int sum = queue.stream().reduce((i, j) -> i + j).get();
-        assertEquals(3, sum); // 1 + 2 = 3
+        assertTrue(latch2.await(2, TimeUnit.SECONDS));
+        // Verify exactly 2 items received and no more arrive
+        // Use during() to ensure queue size remains stable at 2
+        await().during(Duration.ofMillis(500))
+                .atMost(Duration.ofSeconds(2))
+                .until(() -> queue.size() == 2);
+        // Verify OLDEST strategy behavior:
+        // - First item is always 1 (OLDEST keeps the first buffered item)
+        // - Second item is an early value (the first item buffered after the 
initial flush)
+        // The exact second value depends on flush timing vs timer events,
+        // but it must be > 1 and much less than 20 (the last timer event)
+        List<Integer> items = new ArrayList<>(queue);
+        assertEquals(1, items.get(0).intValue(), "OLDEST strategy should 
deliver item 1 first");
+        assertTrue(items.get(1) > 1 && items.get(1) < 20,
+                "OLDEST strategy should keep an early buffered item, got: " + 
items.get(1));
 
         subscriber.cancel();
     }
@@ -118,11 +137,14 @@ public class BackpressureStrategyTest extends 
BaseReactiveTest {
         ReactiveStreamsComponent comp = (ReactiveStreamsComponent) 
context().getComponent("reactive-streams");
         
comp.setBackpressureStrategy(ReactiveStreamsBackpressureStrategy.LATEST);
 
+        AtomicInteger timerCount = new AtomicInteger(0);
+
         new RouteBuilder() {
             @Override
             public void configure() {
                 from("timer:gen?period=20&repeatCount=20&includeMetadata=true")
                         .setBody().header(Exchange.TIMER_COUNTER)
+                        .process(e -> timerCount.incrementAndGet())
                         .to("reactive-streams:integers");
             }
         }.addRoutesToCamelContext(context);
@@ -145,25 +167,38 @@ public class BackpressureStrategyTest extends 
BaseReactiveTest {
         context().start();
 
         assertTrue(latch.await(5, TimeUnit.SECONDS));
-        Thread.sleep(1000); // wait for all numbers to be generated
+        // Wait for all 20 timer events to be processed
+        await().atMost(5, TimeUnit.SECONDS).until(() -> timerCount.get() >= 
20);
 
         subscriber.request(19);
-        assertTrue(latch2.await(1, TimeUnit.SECONDS));
-        Thread.sleep(200); // add other time to ensure no other items arrive
-        assertEquals(2, queue.size());
-        int sum = queue.stream().reduce((i, j) -> i + j).get();
-        assertEquals(21, sum); // 1 + 20 = 21
+        assertTrue(latch2.await(2, TimeUnit.SECONDS));
+        // Verify exactly 2 items received and no more arrive
+        // Use during() to ensure queue size remains stable at 2
+        await().during(Duration.ofMillis(500))
+                .atMost(Duration.ofSeconds(2))
+                .until(() -> queue.size() == 2);
+        // Verify LATEST strategy behavior:
+        // - Second item is always 20 (LATEST always replaces with the newest)
+        // - First item depends on flush timing but is always < 20
+        List<Integer> items = new ArrayList<>(queue);
+        assertTrue(items.get(0) >= 1 && items.get(0) < 20,
+                "First item should be less than 20, got: " + items.get(0));
+        assertEquals(20, items.get(1).intValue(), "LATEST strategy should keep 
the most recent item (20)");
 
         subscriber.cancel();
     }
 
     @Test
     public void testBackpressureDropStrategyInEndpoint() throws Exception {
+
+        AtomicInteger timerCount = new AtomicInteger(0);
+
         new RouteBuilder() {
             @Override
             public void configure() {
                 from("timer:gen?period=20&repeatCount=20&includeMetadata=true")
                         .setBody().header(Exchange.TIMER_COUNTER)
+                        .process(e -> timerCount.incrementAndGet())
                         
.to("reactive-streams:integers?backpressureStrategy=OLDEST");
             }
         }.addRoutesToCamelContext(context);
@@ -186,14 +221,22 @@ public class BackpressureStrategyTest extends 
BaseReactiveTest {
         context().start();
 
         assertTrue(latch.await(5, TimeUnit.SECONDS));
-        Thread.sleep(1000); // wait for all numbers to be generated
+        // Wait for all 20 timer events to be processed
+        await().atMost(5, TimeUnit.SECONDS).until(() -> timerCount.get() >= 
20);
 
         subscriber.request(19);
-        assertTrue(latch2.await(1, TimeUnit.SECONDS));
-        Thread.sleep(200); // add other time to ensure no other items arrive
-        assertEquals(2, queue.size());
-        int sum = queue.stream().reduce((i, j) -> i + j).get();
-        assertEquals(3, sum); // 1 + 2 = 3
+        assertTrue(latch2.await(2, TimeUnit.SECONDS));
+        // Verify exactly 2 items received and no more arrive
+        // Use during() to ensure queue size remains stable at 2
+        await().during(Duration.ofMillis(500))
+                .atMost(Duration.ofSeconds(2))
+                .until(() -> queue.size() == 2);
+        // Verify OLDEST strategy behavior (same invariant as 
testBackpressureDropStrategy):
+        // - First item is always 1, second is an early buffered item
+        List<Integer> items = new ArrayList<>(queue);
+        assertEquals(1, items.get(0).intValue(), "OLDEST strategy should 
deliver item 1 first");
+        assertTrue(items.get(1) > 1 && items.get(1) < 20,
+                "OLDEST strategy should keep an early buffered item, got: " + 
items.get(1));
 
         subscriber.cancel();
     }
diff --git 
a/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BackpressureSubscriberTest.java
 
b/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BackpressureSubscriberTest.java
index 591138817804..70eb55b3aa95 100644
--- 
a/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BackpressureSubscriberTest.java
+++ 
b/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BackpressureSubscriberTest.java
@@ -93,15 +93,15 @@ public class BackpressureSubscriberTest extends 
BaseReactiveTest {
             @Override
             public void configure() {
                 
from("reactive-streams:slowNumbers?concurrentConsumers=10&maxInflightExchanges=1")
-                        .process(x -> Thread.sleep(50))
+                        .delay(50)
                         .to("mock:endpoint");
 
                 
from("reactive-streams:slowerNumbers?concurrentConsumers=10&maxInflightExchanges=1")
-                        .process(x -> Thread.sleep(300))
+                        .delay(300)
                         .to("mock:endpoint");
 
                 
from("reactive-streams:parallelSlowNumbers?concurrentConsumers=10&maxInflightExchanges=5")
-                        .process(x -> Thread.sleep(100))
+                        .delay(100)
                         .to("mock:endpoint");
             }
         };
diff --git 
a/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BasicPublisherTest.java
 
b/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BasicPublisherTest.java
index 594d8792d654..fc126168ee0b 100644
--- 
a/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BasicPublisherTest.java
+++ 
b/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/BasicPublisherTest.java
@@ -95,10 +95,7 @@ public class BasicPublisherTest extends BaseReactiveTest {
         disp1.dispose();
         disp2.dispose();
 
-        // No active subscriptions, warnings expected
-        Thread.sleep(60);
-
-        // Add another subscription
+        // Add another subscription after disposal
         CountDownLatch latch3 = new CountDownLatch(5);
         Disposable disp3 = 
Observable.fromPublisher(CamelReactiveStreams.get(context).fromStream("unbounded",
 Integer.class))
                 .subscribe(n -> latch3.countDown());
diff --git 
a/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/DelayedMonoPublisherTest.java
 
b/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/DelayedMonoPublisherTest.java
index f34ee0e7e40c..603ff9bf3ccf 100644
--- 
a/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/DelayedMonoPublisherTest.java
+++ 
b/components/camel-reactive-streams/src/test/java/org/apache/camel/component/reactive/streams/DelayedMonoPublisherTest.java
@@ -20,8 +20,8 @@ import java.util.LinkedList;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.ConcurrentLinkedDeque;
 import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
 import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
@@ -33,13 +33,14 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 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.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class DelayedMonoPublisherTest {
 
-    private ExecutorService service;
+    private ScheduledExecutorService service;
 
     @BeforeEach
     public void init() {
@@ -131,10 +132,9 @@ public class DelayedMonoPublisherTest {
                 .doOnComplete(latch::countDown)
                 .subscribe();
 
-        Thread.sleep(200);
-        pub.setData(5);
+        service.schedule(() -> pub.setData(5), 200, TimeUnit.MILLISECONDS);
 
-        assertTrue(latch.await(1, TimeUnit.SECONDS));
+        assertTrue(latch.await(2, TimeUnit.SECONDS));
 
         assertEquals(1, data.size());
         assertEquals(5, data.get(0).intValue());
@@ -158,10 +158,9 @@ public class DelayedMonoPublisherTest {
                 .doOnComplete(latch::countDown)
                 .subscribe();
 
-        Thread.sleep(200);
-        pub.setData(5);
+        service.schedule(() -> pub.setData(5), 200, TimeUnit.MILLISECONDS);
 
-        assertTrue(latch.await(1, TimeUnit.SECONDS));
+        assertTrue(latch.await(2, TimeUnit.SECONDS));
 
         assertEquals(2, data.size());
         for (Integer n : data) {
@@ -182,15 +181,17 @@ public class DelayedMonoPublisherTest {
                 .doOnComplete(latch::countDown)
                 .subscribe();
 
-        Thread.sleep(200);
-        pub.setData(5);
+        service.schedule(() -> pub.setData(5), 200, TimeUnit.MILLISECONDS);
+
+        // Wait for first subscriber to receive data before subscribing second
+        await().atMost(2, TimeUnit.SECONDS).until(() -> data.size() >= 1);
 
         Flowable.fromPublisher(pub)
                 .doOnNext(data::add)
                 .doOnComplete(latch::countDown)
                 .subscribe();
 
-        assertTrue(latch.await(1, TimeUnit.SECONDS));
+        assertTrue(latch.await(2, TimeUnit.SECONDS));
 
         assertEquals(2, data.size());
         for (Integer n : data) {
@@ -215,8 +216,10 @@ public class DelayedMonoPublisherTest {
                     latch.countDown();
                 });
 
-        Thread.sleep(200);
-        pub.setException(ex);
+        service.schedule(() -> pub.setException(ex), 200, 
TimeUnit.MILLISECONDS);
+
+        // Wait for first subscriber to receive exception before subscribing 
second
+        await().atMost(2, TimeUnit.SECONDS).until(() -> exceptions.size() >= 
1);
 
         Flowable.fromPublisher(pub)
                 .subscribe(item -> {
@@ -225,7 +228,7 @@ public class DelayedMonoPublisherTest {
                     latch.countDown();
                 });
 
-        assertTrue(latch.await(1, TimeUnit.SECONDS));
+        assertTrue(latch.await(2, TimeUnit.SECONDS));
 
         assertEquals(2, exceptions.size());
         for (Throwable t : exceptions) {
@@ -251,10 +254,9 @@ public class DelayedMonoPublisherTest {
 
         pub.subscribe(sub);
 
-        Thread.sleep(100);
-        sub.request(1);
+        service.schedule(() -> sub.request(1), 100, TimeUnit.MILLISECONDS);
 
-        Integer res = queue.poll(1, TimeUnit.SECONDS);
+        Integer res = queue.poll(2, TimeUnit.SECONDS);
         assertEquals(Integer.valueOf(2), res);
     }
 

Reply via email to