[ 
https://issues.apache.org/jira/browse/CAMEL-25609?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Work on CAMEL-25609 started by shashank.
----------------------------------------
> camel-browse - browseLimit (advanced, off by default) does not release old 
> exchanges (the limit is a sub list view), and with concurrent producers a 
> stale view makes the endpoint fail with ConcurrentModificationException until 
> restart
> ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-25609
>                 URL: https://issues.apache.org/jira/browse/CAMEL-25609
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-core
>            Reporter: shashank
>            Assignee: shashank
>            Priority: Major
>
> Scope: {{browseLimit}} on the browse endpoint is an advanced option that is 
> off by default (0 = keep everything), on a component used for testing and 
> debugging. Nothing in Camel sets it: the only caller of {{setBrowseLimit}} on 
> a browse endpoint is {{BrowseComponent}}, which copies its own option of the 
> same name (default 0). Only users who set the option are affected.
> {{BrowseEndpoint.onExchange}} (lines 141-151 at main a7169d7c6ab6) makes room 
> for a new exchange like this:
> {code:java}
> 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);
>     }
> }
> exchanges.add(exchange);
> {code}
> {{exchanges}} is a {{CopyOnWriteArrayList}} ({{createExchangeList}}). 
> {{subList}} returns a view of that list, and a sub list of a sub list is 
> again a view of the same backing list; {{add}} on the view appends to the 
> backing list. Nothing is ever removed from the backing list.
> Two effects:
> # *Exchanges are not released (reproduced).* The option says "Maximum number 
> of messages to keep in memory available for browsing". With 
> {{browse:foo?browseLimit=10}} a browser sees the last 10 exchanges, but the 
> endpoint holds every exchange that was ever sent to it (1000 after 1000 
> messages), with bodies and headers, until the endpoint is stopped. The limit 
> has no effect on memory, and because the list is copy-on-write every new 
> exchange copies the whole, growing backing array.
> # *A stale view fails (reproduced).* A sub list of a {{CopyOnWriteArrayList}} 
> throws {{ConcurrentModificationException}} when the backing list was changed 
> through another view.
> #* A list that {{getExchanges()}} returned can no longer be read after one 
> more exchange arrived ({{size()}}, {{get}}, iteration all fail). Readers such 
> as {{ManagedBrowsableEndpoint}} ({{getExchanges().size()}}, then 
> {{get(index)}}) can hit this when a message arrives in between.
> #* Two threads sending to the same endpoint: thread A creates its view, 
> thread B creates its own view and adds, then A stores its view in the field, 
> which is now outdated. A's {{add}} fails, the outdated view stays in the 
> field, and from then on every exchange sent to the endpoint 
> ({{exchanges.size()}} in {{onExchange}}) and every read of it fails with 
> {{ConcurrentModificationException}} until the endpoint is restarted. 
> Reproduced with a test that holds thread A exactly between creating and 
> storing the view (not a load test). This failure is loud: the producer throws 
> and the route's error handling sees it.
> A smaller point in the same lines: the view (with {{browseLimit - 1}} 
> entries) is stored before the add, so a reader can see one exchange fewer 
> than the limit while an exchange arrives, and an empty endpoint with 
> {{browseLimit=1}} ({{BrowsableEndpoint.getBrowseStatus}} reports size 0). 
> Reproduced.
> Without {{browseLimit}} the endpoint keeps everything by design and none of 
> this applies.
> h3. Reproduction
> {{BrowseLimitMemoryTest}} in camel-core, 5 tests; all 5 fail on main:
> * {{testOldExchangesAreReleased}}: a {{BrowseEndpoint}} with 
> {{browseLimit=10}} whose {{createExchangeList}} hands the created list to the 
> test; 1000 messages. The browser sees the last ten on main and with the fix; 
> the list that holds the exchanges must have 10 entries. On main: {{expected: 
> <10> but was: <1000>}}.
> * {{testBrowsedListStaysUsableWhenMoreExchangesArrive}}: 
> {{browse:live?browseLimit=2}}, three messages, {{getExchanges()}}, one more 
> message, then {{size()}} of the list from before. On main: 
> {{java.util.ConcurrentModificationException}}.
> * {{testSecondProducerWhileFirstMakesRoom}}: {{browseLimit=2}}, two 
> exchanges, then producers A and B with the interleaving above (a delegating 
> list holds A when it asks for the sub list, until B has completed), then a 
> third exchange C and a read. On main: {{failures: [producer A: 
> java.util.ConcurrentModificationException, producer C, after A and B: 
> java.util.ConcurrentModificationException, browsing, after A, B and C: 
> java.util.ConcurrentModificationException]}}.
> * {{testManyProducerThreads}}: 4 threads, 250 exchanges each, 
> {{browseLimit=10}}. Depends on scheduling; on main all 4 threads failed with 
> the exception in each of the 3 attempts of the test run.
> * {{testReaderDoesNotSeeTheEndpointEmptyWithLimitOne}}: {{browseLimit=1}}, 
> the browse status is read while the producer is inside the list. On main: 
> {{sizes seen by a reader while exchanges arrive: [1, 0, 1, 0]}}.
> h3. Proposed fix
> Add the exchange, then remove the oldest ones from the list itself: 
> {{exchanges.subList(0, excess).clear()}} with {{excess = size - browseLimit}} 
> (one array copy for a {{CopyOnWriteArrayList}}). Both steps are done under a 
> lock, so that concurrent producers keep exactly {{browseLimit}} exchanges; 
> the lock is only taken when {{browseLimit}} is set. The filter is not touched.
> What a browser sees does not change: the last {{browseLimit}} exchanges in 
> arrival order ({{BrowseLimitTest}} is unchanged and passes). Because the add 
> comes first, a reader never sees fewer than the limit once it was reached; 
> while an exchange arrives it can see {{browseLimit + 1}} for a moment. 
> {{getExchanges()}} now always returns the endpoint's list, as it already does 
> without a limit and before the limit is reached.
> No upgrade note is needed: no option, default or result changes; a route that 
> worked keeps working and a browser sees the same exchanges. What goes away is 
> the retained memory and the exception.
> Tests with the fix: {{BrowseLimitMemoryTest}} 5 of 5 pass; the full 
> camel-core suite: 8171 run, 0 failures, 0 errors, 45 skipped; the browse 
> tests of camel-management: 19 run, 0 failures.
> How it was found: a boundary sweep with a small Lean 4 model of the window 
> (main: backing list plus offset of the view; fix: a list). The model proves 
> that main keeps every exchange in the backing list for any limit (so 1000 for 
> the example above), that the fixed list never has more than {{browseLimit}} 
> entries, and that the fix shows a browser exactly what main showed. The model 
> does not cover the {{ConcurrentModificationException}}, concurrent producers 
> or the moment between add and remove; those are in the Java tests.
> Affected: every release since 4.9.0, where CAMEL-21183 added the option (the 
> 4.9.0 tag has the line, 4.8.0 does not). main (a7169d7c6ab6; tested); 
> camel-4.23.x, camel-4.22.x, camel-4.18.x and camel-4.14.x have the same line 
> 147 (read, not run).
> Duplicate check (2026-10-13): JIRA text "browseLimit" (2: CAMEL-24076 and 
> CAMEL-24078, the browseLimit option of camel-jms / camel-amqp, other code), 
> "BrowseEndpoint subList" (none), "browse endpoint memory leak" (5, none about 
> the browse component); GitHub pull requests "browseLimit": #24735 
> (camel-jms), no other.
> _Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to