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 001ade0c3090 CAMEL-25609: camel-browse - Release the exchanges that 
browseLimit drops (#27796)
001ade0c3090 is described below

commit 001ade0c3090821cfdcc58f9c8312181735c6821
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Oct 15 11:18:46 2026 +0530

    CAMEL-25609: camel-browse - Release the exchanges that browseLimit drops 
(#27796)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/component/browse/BrowseEndpoint.java     |  23 +-
 .../org/apache/camel/spi/BrowsableEndpoint.java    |  20 +-
 .../component/browse/BrowseLimitMemoryTest.java    | 302 +++++++++++++++++++++
 .../management/mbean/ManagedBrowsableEndpoint.java |  50 ++--
 .../ManagedBrowsableEndpointSnapshotTest.java      | 174 ++++++++++++
 5 files changed, 536 insertions(+), 33 deletions(-)

diff --git 
a/components/camel-browse/src/main/java/org/apache/camel/component/browse/BrowseEndpoint.java
 
b/components/camel-browse/src/main/java/org/apache/camel/component/browse/BrowseEndpoint.java
index 5462f9dae33f..35e842ba0eee 100644
--- 
a/components/camel-browse/src/main/java/org/apache/camel/component/browse/BrowseEndpoint.java
+++ 
b/components/camel-browse/src/main/java/org/apache/camel/component/browse/BrowseEndpoint.java
@@ -18,6 +18,8 @@ package org.apache.camel.component.browse;
 
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Predicate;
 
 import org.apache.camel.Category;
@@ -55,6 +57,7 @@ public class BrowseEndpoint extends DefaultEndpoint 
implements BrowsableEndpoint
               description = "To use a predicate to filter whether to include 
the message for browsing. Return true to include, false to exclude.")
     private Predicate<Exchange> filter;
 
+    private final Lock limitLock = new ReentrantLock();
     private List<Exchange> exchanges;
     private volatile Processor onExchangeProcessor;
 
@@ -140,14 +143,22 @@ public class BrowseEndpoint extends DefaultEndpoint 
implements BrowsableEndpoint
      */
     protected void onExchange(Exchange exchange) throws Exception {
         if (browseLimit > 0) {
-            // make room if end of capacity
-            int max = exchanges.size();
-            if (max >= browseLimit) {
-                int space = Math.abs(max - (browseLimit - 1));
-                exchanges = exchanges.subList(space, max);
+            // add first and then remove the oldest from the list itself: a 
sub list is only a view that keeps them
+            // all in memory, and a reader never sees fewer than the limit 
once it was reached. Under a lock, as
+            // concurrent producers would otherwise remove too many or 
overtake the limit
+            limitLock.lock();
+            try {
+                exchanges.add(exchange);
+                int excess = exchanges.size() - browseLimit;
+                if (excess > 0) {
+                    exchanges.subList(0, excess).clear();
+                }
+            } finally {
+                limitLock.unlock();
             }
+        } else {
+            exchanges.add(exchange);
         }
-        exchanges.add(exchange);
 
         // now fire the consumer
         if (onExchangeProcessor != null) {
diff --git 
a/core/camel-api/src/main/java/org/apache/camel/spi/BrowsableEndpoint.java 
b/core/camel-api/src/main/java/org/apache/camel/spi/BrowsableEndpoint.java
index d65845200dd9..2420f2526543 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/BrowsableEndpoint.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/BrowsableEndpoint.java
@@ -57,16 +57,26 @@ public interface BrowsableEndpoint extends Endpoint {
      * @return       the status
      */
     default BrowseStatus getBrowseStatus(int limit) {
-        List<Exchange> list = getExchanges();
+        // iterate once, so size, first and last come from the same snapshot 
(the iterator of a copy-on-write list), as
+        // the list can change between size() and get(index), such as a browse 
limit removing the oldest exchange
+        int size = 0;
+        Exchange first = null;
+        Exchange last = null;
+        for (Exchange exchange : getExchanges()) {
+            if (size++ == 0) {
+                first = exchange;
+            }
+            last = exchange;
+        }
         long ts = 0;
         long ts2 = 0;
-        if (!list.isEmpty()) {
-            Long v1 = 
list.get(0).getMessage().getHeader(Exchange.MESSAGE_TIMESTAMP, 0L, long.class);
-            Long v2 = list.get(list.size() - 
1).getMessage().getHeader(Exchange.MESSAGE_TIMESTAMP, 0L, long.class);
+        if (size > 0) {
+            Long v1 = first.getMessage().getHeader(Exchange.MESSAGE_TIMESTAMP, 
0L, long.class);
+            Long v2 = last.getMessage().getHeader(Exchange.MESSAGE_TIMESTAMP, 
0L, long.class);
             ts = v1 != null ? v1 : 0L;
             ts2 = v2 != null ? v2 : 0L;
         }
-        return new BrowseStatus(list.size(), ts, ts2);
+        return new BrowseStatus(size, ts, ts2);
     }
 
     /**
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/browse/BrowseLimitMemoryTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/browse/BrowseLimitMemoryTest.java
new file mode 100644
index 000000000000..841551b3af7f
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/browse/BrowseLimitMemoryTest.java
@@ -0,0 +1,302 @@
+/*
+ * 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.browse;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * With browseLimit the endpoint must keep at most that many exchanges in 
memory, a list handed out to a browser must
+ * stay usable when more exchanges arrive, and producers that send at the same 
time must not break the endpoint.
+ */
+public class BrowseLimitMemoryTest extends ContextTestSupport {
+
+    private static final int LIMIT = 10;
+    private static final int TOTAL = 1000;
+
+    @Test
+    public void testOldExchangesAreReleased() throws Exception {
+        AtomicReference<List<Exchange>> store = new AtomicReference<>();
+        BrowseEndpoint endpoint = new BrowseEndpoint("browse:store", 
context.getComponent("browse")) {
+            @Override
+            protected List<Exchange> createExchangeList() {
+                // the list that holds the exchanges in memory
+                List<Exchange> answer = super.createExchangeList();
+                store.set(answer);
+                return answer;
+            }
+        };
+        endpoint.setName("store");
+        endpoint.setBrowseLimit(LIMIT);
+        context.addEndpoint("browse:store", endpoint);
+
+        for (int i = 0; i < TOTAL; i++) {
+            template.sendBody(endpoint, "msg-" + i);
+        }
+
+        // what a browser sees: the last ten
+        List<Exchange> browsed = endpoint.getExchanges();
+        assertEquals(LIMIT, browsed.size());
+        assertEquals("msg-" + (TOTAL - LIMIT), 
browsed.get(0).getMessage().getBody());
+        assertEquals("msg-" + (TOTAL - 1), browsed.get(LIMIT - 
1).getMessage().getBody());
+
+        // what is kept in memory: not more than the limit
+        assertEquals(LIMIT, store.get().size(), "exchanges kept in memory with 
browseLimit=" + LIMIT);
+    }
+
+    @Test
+    public void testBrowsedListStaysUsableWhenMoreExchangesArrive() throws 
Exception {
+        String uri = "browse:live?browseLimit=2";
+        template.sendBody(uri, "one");
+        template.sendBody(uri, "two");
+        template.sendBody(uri, "three");
+
+        BrowseEndpoint endpoint = context.getEndpoint(uri, 
BrowseEndpoint.class);
+        List<Exchange> browsed = endpoint.getExchanges();
+        assertEquals(2, browsed.size());
+
+        // a new exchange arrives while the browser still holds the list
+        template.sendBody(uri, "four");
+
+        assertEquals(2, browsed.size());
+        assertEquals("three", browsed.get(0).getMessage().getBody());
+        assertEquals("four", browsed.get(1).getMessage().getBody());
+    }
+
+    @Test
+    public void testSecondProducerWhileFirstMakesRoom() throws Exception {
+        // producer A is held at the moment it asks the list for a sub list 
(the step that makes room), and producer B
+        // sends meanwhile: B either completes, or waits for A when the 
endpoint serializes the two
+        Queue<String> failures = new ConcurrentLinkedQueue<>();
+        AtomicReference<Thread> second = new AtomicReference<>();
+        CountDownLatch secondDone = new CountDownLatch(1);
+        AtomicReference<Runnable> onSubList = new AtomicReference<>();
+
+        BrowseEndpoint endpoint = hookedEndpoint("pair", 2, null, onSubList);
+        Producer producer = endpoint.createProducer();
+        producer.process(exchange(endpoint, "one"));
+        producer.process(exchange(endpoint, "two"));
+
+        Exchange a = exchange(endpoint, "A");
+        Exchange b = exchange(endpoint, "B");
+        Exchange c = exchange(endpoint, "C");
+        second.set(new Thread(() -> {
+            try {
+                producer.process(b);
+            } catch (Throwable e) {
+                failures.add("producer B: " + e);
+            } finally {
+                secondDone.countDown();
+            }
+        }, "browse-producer-B"));
+
+        onSubList.set(() -> {
+            // only the first sub list is held
+            onSubList.set(null);
+            Thread t = second.get();
+            t.start();
+            await().atMost(20, TimeUnit.SECONDS)
+                    .until(() -> secondDone.getCount() == 0 || t.getState() == 
Thread.State.WAITING);
+        });
+        try {
+            producer.process(a);
+        } catch (Throwable e) {
+            failures.add("producer A: " + e);
+        }
+        assertTrue(secondDone.await(20, TimeUnit.SECONDS), "producer B did not 
complete");
+
+        // the endpoint must still accept exchanges and be readable afterwards
+        try {
+            producer.process(c);
+        } catch (Throwable e) {
+            failures.add("producer C, after A and B: " + e);
+        }
+        try {
+            assertEquals(2, endpoint.getExchanges().size());
+            assertEquals("C", 
endpoint.getExchanges().get(1).getMessage().getBody());
+        } catch (Throwable e) {
+            failures.add("browsing, after A, B and C: " + e);
+        }
+        assertTrue(failures.isEmpty(), "failures: " + failures);
+    }
+
+    @Test
+    public void testManyProducerThreads() throws Exception {
+        int threads = 4;
+        int perThread = 250;
+        AtomicReference<List<Exchange>> store = new AtomicReference<>();
+        BrowseEndpoint endpoint = new BrowseEndpoint("browse:many", 
context.getComponent("browse")) {
+            @Override
+            protected List<Exchange> createExchangeList() {
+                List<Exchange> answer = super.createExchangeList();
+                store.set(answer);
+                return answer;
+            }
+        };
+        endpoint.setName("many");
+        endpoint.setBrowseLimit(LIMIT);
+        context.addEndpoint("browse:many", endpoint);
+        Producer producer = endpoint.createProducer();
+
+        Queue<String> failures = new ConcurrentLinkedQueue<>();
+        CountDownLatch go = new CountDownLatch(1);
+        List<Thread> producers = new ArrayList<>();
+        for (int t = 0; t < threads; t++) {
+            List<Exchange> mine = new ArrayList<>();
+            for (int i = 0; i < perThread; i++) {
+                mine.add(exchange(endpoint, t + "-" + i));
+            }
+            Thread thread = new Thread(() -> {
+                try {
+                    if (!go.await(20, TimeUnit.SECONDS)) {
+                        failures.add("not started");
+                        return;
+                    }
+                    for (Exchange exchange : mine) {
+                        producer.process(exchange);
+                    }
+                } catch (Throwable e) {
+                    failures.add(e.toString());
+                }
+            }, "browse-producer-" + t);
+            producers.add(thread);
+            thread.start();
+        }
+        go.countDown();
+        for (Thread thread : producers) {
+            thread.join(20000);
+            assertFalse(thread.isAlive(), thread.getName() + " did not 
complete");
+        }
+
+        assertTrue(failures.isEmpty(), "failures: " + failures);
+        assertEquals(LIMIT, endpoint.getExchanges().size());
+        assertEquals(LIMIT, store.get().size(), "exchanges kept in memory with 
browseLimit=" + LIMIT);
+    }
+
+    @Test
+    public void testReaderDoesNotSeeTheEndpointEmptyWithLimitOne() throws 
Exception {
+        // a reader that looks while a producer is inside the list (before the 
add, and before the oldest is removed)
+        List<Integer> seen = new ArrayList<>();
+        AtomicReference<Runnable> look = new AtomicReference<>();
+        BrowseEndpoint endpoint = hookedEndpoint("single", 1, look, look);
+        Producer producer = endpoint.createProducer();
+        producer.process(exchange(endpoint, "one"));
+
+        look.set(() -> seen.add(endpoint.getBrowseStatus(0).size()));
+        producer.process(exchange(endpoint, "two"));
+        producer.process(exchange(endpoint, "three"));
+        look.set(null);
+
+        assertFalse(seen.isEmpty());
+        assertFalse(seen.contains(0), "sizes seen by a reader while exchanges 
arrive: " + seen);
+        assertEquals(1, endpoint.getExchanges().size());
+        assertEquals("three", 
endpoint.getExchanges().get(0).getMessage().getBody());
+    }
+
+    private static Exchange exchange(BrowseEndpoint endpoint, String body) {
+        Exchange answer = endpoint.createExchange();
+        answer.getMessage().setBody(body);
+        return answer;
+    }
+
+    private BrowseEndpoint hookedEndpoint(
+            String name, int limit, AtomicReference<Runnable> beforeAdd, 
AtomicReference<Runnable> afterSubList)
+            throws Exception {
+        BrowseEndpoint endpoint = new BrowseEndpoint("browse:" + name, 
context.getComponent("browse")) {
+            @Override
+            protected List<Exchange> createExchangeList() {
+                return new HookedList(super.createExchangeList(), beforeAdd, 
afterSubList);
+            }
+        };
+        endpoint.setName(name);
+        endpoint.setBrowseLimit(limit);
+        context.addEndpoint("browse:" + name, endpoint);
+        return endpoint;
+    }
+
+    /**
+     * Delegates to the list of the endpoint (and to its sub lists) and lets a 
test run code just before an add and just
+     * after a sub list was created. It does not change what the list does.
+     */
+    private static final class HookedList extends AbstractList<Exchange> {
+        private final List<Exchange> delegate;
+        private final AtomicReference<Runnable> beforeAdd;
+        private final AtomicReference<Runnable> afterSubList;
+
+        HookedList(List<Exchange> delegate, AtomicReference<Runnable> 
beforeAdd, AtomicReference<Runnable> afterSubList) {
+            this.delegate = delegate;
+            this.beforeAdd = beforeAdd;
+            this.afterSubList = afterSubList;
+        }
+
+        private static void run(AtomicReference<Runnable> hook) {
+            Runnable task = hook != null ? hook.get() : null;
+            if (task != null) {
+                task.run();
+            }
+        }
+
+        @Override
+        public Exchange get(int index) {
+            return delegate.get(index);
+        }
+
+        @Override
+        public int size() {
+            return delegate.size();
+        }
+
+        @Override
+        public boolean add(Exchange exchange) {
+            run(beforeAdd);
+            return delegate.add(exchange);
+        }
+
+        @Override
+        public Exchange remove(int index) {
+            return delegate.remove(index);
+        }
+
+        @Override
+        public void clear() {
+            delegate.clear();
+        }
+
+        @Override
+        public List<Exchange> subList(int fromIndex, int toIndex) {
+            List<Exchange> answer = new HookedList(delegate.subList(fromIndex, 
toIndex), beforeAdd, afterSubList);
+            run(afterSubList);
+            return answer;
+        }
+    }
+}
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedBrowsableEndpoint.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedBrowsableEndpoint.java
index 0a6a7c36cef0..78171b147016 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedBrowsableEndpoint.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedBrowsableEndpoint.java
@@ -16,8 +16,6 @@
  */
 package org.apache.camel.management.mbean;
 
-import java.util.List;
-
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
 import org.apache.camel.api.management.ManagedResource;
@@ -45,6 +43,14 @@ public class ManagedBrowsableEndpoint extends 
ManagedEndpoint implements Managed
         return getEndpoint();
     }
 
+    /**
+     * The exchanges of the endpoint as one snapshot, as the list can change 
between size() and get(index) (such as a
+     * browse limit removing the oldest exchange).
+     */
+    private Exchange[] snapshot() {
+        return getEndpoint().getExchanges().toArray(new Exchange[0]);
+    }
+
     @Override
     public int getBrowseLimit() {
         return getEndpoint().getBrowseLimit();
@@ -57,12 +63,12 @@ public class ManagedBrowsableEndpoint extends 
ManagedEndpoint implements Managed
 
     @Override
     public String browseExchange(Integer index) {
-        List<Exchange> exchanges = getEndpoint().getExchanges();
+        Exchange[] exchanges = snapshot();
 
-        if (index >= exchanges.size()) {
+        if (index >= exchanges.length) {
             return null;
         }
-        Exchange exchange = exchanges.get(index);
+        Exchange exchange = exchanges[index];
         if (exchange == null) {
             return null;
         }
@@ -72,12 +78,12 @@ public class ManagedBrowsableEndpoint extends 
ManagedEndpoint implements Managed
 
     @Override
     public String browseMessageBody(Integer index) {
-        List<Exchange> exchanges = getEndpoint().getExchanges();
+        Exchange[] exchanges = snapshot();
 
-        if (index >= exchanges.size()) {
+        if (index >= exchanges.length) {
             return null;
         }
-        Exchange exchange = exchanges.get(index);
+        Exchange exchange = exchanges[index];
         if (exchange == null) {
             return null;
         }
@@ -88,12 +94,12 @@ public class ManagedBrowsableEndpoint extends 
ManagedEndpoint implements Managed
 
     @Override
     public String browseMessageAsXml(Integer index, Boolean includeBody) {
-        List<Exchange> exchanges = getEndpoint().getExchanges();
+        Exchange[] exchanges = snapshot();
 
-        if (index >= exchanges.size()) {
+        if (index >= exchanges.length) {
             return null;
         }
-        Exchange exchange = exchanges.get(index);
+        Exchange exchange = exchanges[index];
         if (exchange == null) {
             return null;
         }
@@ -120,15 +126,15 @@ public class ManagedBrowsableEndpoint extends 
ManagedEndpoint implements Managed
                     "From index cannot be larger than to index, was: " + 
fromIndex + " > " + toIndex);
         }
 
-        List<Exchange> exchanges = getEndpoint().getExchanges();
-        if (exchanges.isEmpty()) {
+        Exchange[] exchanges = snapshot();
+        if (exchanges.length == 0) {
             return null;
         }
 
         StringBuilder sb = new StringBuilder();
         sb.append("<messages>");
-        for (int i = fromIndex; i < exchanges.size() && i <= toIndex; i++) {
-            Exchange exchange = exchanges.get(i);
+        for (int i = fromIndex; i < exchanges.length && i <= toIndex; i++) {
+            Exchange exchange = exchanges[i];
             Message msg = exchange.getMessage();
             String xml = MessageHelper.dumpAsXml(msg, includeBody);
             sb.append("\n").append(xml);
@@ -139,12 +145,12 @@ public class ManagedBrowsableEndpoint extends 
ManagedEndpoint implements Managed
 
     @Override
     public String browseMessageAsJSon(Integer index, Boolean includeBody) {
-        List<Exchange> exchanges = getEndpoint().getExchanges();
+        Exchange[] exchanges = snapshot();
 
-        if (index >= exchanges.size()) {
+        if (index >= exchanges.length) {
             return null;
         }
-        Exchange exchange = exchanges.get(index);
+        Exchange exchange = exchanges[index];
         if (exchange == null) {
             return null;
         }
@@ -171,14 +177,14 @@ public class ManagedBrowsableEndpoint extends 
ManagedEndpoint implements Managed
                     "From index cannot be larger than to index, was: " + 
fromIndex + " > " + toIndex);
         }
 
-        List<Exchange> exchanges = getEndpoint().getExchanges();
-        if (exchanges.isEmpty()) {
+        Exchange[] exchanges = snapshot();
+        if (exchanges.length == 0) {
             return null;
         }
 
         JsonArray arr = new JsonArray();
-        for (int i = fromIndex; i < exchanges.size() && i <= toIndex; i++) {
-            Exchange exchange = exchanges.get(i);
+        for (int i = fromIndex; i < exchanges.length && i <= toIndex; i++) {
+            Exchange exchange = exchanges[i];
             Message msg = exchange.getMessage();
             JsonObject jo = MessageHelper.dumpAsJSonObject(msg, false, false, 
includeBody, true, true, true, 128 * 1024);
             arr.add(jo);
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedBrowsableEndpointSnapshotTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedBrowsableEndpointSnapshotTest.java
new file mode 100644
index 000000000000..326c315bee26
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedBrowsableEndpointSnapshotTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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.management;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.browse.BrowseEndpoint;
+import org.apache.camel.management.mbean.ManagedBrowsableEndpoint;
+import org.apache.camel.spi.BrowsableEndpoint.BrowseStatus;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.BeforeEach;
+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.assertTrue;
+
+/**
+ * The readers of a browse endpoint must read one snapshot of its list: with a 
browse limit a producer adds the new
+ * exchange and then removes the oldest, so a reader that calls size() and 
then get(index) can find the list shorter
+ * than the size it read.
+ */
+public class ManagedBrowsableEndpointSnapshotTest extends ContextTestSupport {
+
+    private static final int LIMIT = 3;
+
+    private GatedList list;
+    private BrowseEndpoint endpoint;
+
+    @BeforeEach
+    void createEndpoint() throws Exception {
+        endpoint = new BrowseEndpoint("browse:gated", 
context.getComponent("browse")) {
+            @Override
+            protected List<Exchange> createExchangeList() {
+                list = new GatedList();
+                return list;
+            }
+        };
+        endpoint.setName("gated");
+        endpoint.setBrowseLimit(LIMIT);
+        context.addEndpoint("browse:gated", endpoint);
+
+        send("A", 1);
+        send("B", 2);
+        send("C", 3);
+    }
+
+    @Test
+    void testBrowseMessageBodyOfLastWhileOldestIsRemoved() throws Exception {
+        ManagedBrowsableEndpoint mbean = new 
ManagedBrowsableEndpoint(endpoint);
+
+        // the reader sees A, B, C and D and asks for D, and the producer 
removes A before the reader gets it
+        String body = readWhileProducerMakesRoom(1, () -> 
mbean.browseMessageBody(LIMIT));
+
+        assertEquals("D", body);
+    }
+
+    @Test
+    void testBrowseStatusWhileOldestIsRemoved() throws Exception {
+        // the reader gets the first (A), and the producer removes A before 
the reader gets the last
+        BrowseStatus status = readWhileProducerMakesRoom(2, () -> 
endpoint.getBrowseStatus(0));
+
+        // size, first and last all come from the same list: A, B, C and D
+        assertEquals(new BrowseStatus(LIMIT + 1, 1, 4), status);
+    }
+
+    @Test
+    void testBrowseRangeWhileOldestIsRemoved() throws Exception {
+        ManagedBrowsableEndpoint mbean = new 
ManagedBrowsableEndpoint(endpoint);
+
+        // the reader sees four exchanges, and the producer removes A before 
the reader gets the first
+        String json = readWhileProducerMakesRoom(1, () -> 
mbean.browseRangeMessagesAsJSon(0, Integer.MAX_VALUE, true));
+
+        JsonArray arr = (JsonArray) Jsoner.deserialize(json);
+        assertEquals(LIMIT + 1, arr.size(), json);
+        assertTrue(json.contains("\"A\"") && json.contains("\"D\""), json);
+    }
+
+    /**
+     * Sends D from another thread and holds that producer after it added D 
(so the list has one more than the limit)
+     * until the reader calls get(index) for the given time; the producer then 
removes the oldest before that get runs.
+     * A reader that reads one snapshot does not call get(index), and the 
producer completes after the reader.
+     */
+    private <T> T readWhileProducerMakesRoom(int trimBeforeGet, Callable<T> 
reader) throws Exception {
+        Thread producer = new Thread(() -> send("D", 4), "browse-producer");
+        list.trimBeforeGet.set(trimBeforeGet);
+        list.producer = producer;
+        list.holdAfterAdd = true;
+        producer.start();
+        assertTrue(list.added.await(20, TimeUnit.SECONDS), "producer did not 
add");
+        assertEquals(LIMIT + 1, list.size());
+        try {
+            return reader.call();
+        } finally {
+            list.trim.countDown();
+            producer.join(20000);
+            assertFalse(producer.isAlive(), "producer did not complete");
+            assertEquals(LIMIT, endpoint.getExchanges().size());
+        }
+    }
+
+    private void send(String body, long timestamp) {
+        template.sendBodyAndHeader(endpoint, body, Exchange.MESSAGE_TIMESTAMP, 
timestamp);
+    }
+
+    /**
+     * The list of the endpoint, which can hold the producer after an add and 
let it continue at the n-th get(index).
+     */
+    private static final class GatedList extends 
CopyOnWriteArrayList<Exchange> {
+        private final CountDownLatch added = new CountDownLatch(1);
+        private final CountDownLatch trim = new CountDownLatch(1);
+        private final AtomicInteger trimBeforeGet = new AtomicInteger();
+        private volatile boolean holdAfterAdd;
+        private volatile Thread producer;
+
+        @Override
+        public boolean add(Exchange exchange) {
+            boolean answer = super.add(exchange);
+            if (holdAfterAdd) {
+                holdAfterAdd = false;
+                added.countDown();
+                try {
+                    if (!trim.await(20, TimeUnit.SECONDS)) {
+                        throw new IllegalStateException("producer was not 
released");
+                    }
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    throw new IllegalStateException(e);
+                }
+            }
+            return answer;
+        }
+
+        @Override
+        public Exchange get(int index) {
+            if (trimBeforeGet.get() > 0 && trimBeforeGet.decrementAndGet() == 
0) {
+                // let the producer remove the oldest, and wait until it did
+                trim.countDown();
+                try {
+                    producer.join(20000);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    throw new IllegalStateException(e);
+                }
+                if (producer.isAlive()) {
+                    throw new IllegalStateException("producer did not 
complete");
+                }
+            }
+            return super.get(index);
+        }
+    }
+}

Reply via email to