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

Claus Ibsen updated CAMEL-24065:
--------------------------------
    Fix Version/s: 4.22.0

> camel-spring - spring-event: ConcurrentModificationException when a route is 
> stopped while an ApplicationEvent is dispatched
> ----------------------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-24065
>                 URL: https://issues.apache.org/jira/browse/CAMEL-24065
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-spring
>    Affects Versions: 4.21.0
>            Reporter: Federico Mariani
>            Assignee: Claus Ibsen
>            Priority: Major
>             Fix For: 4.22.0
>
>
> {{EventComponent.endpoints}} is a plain {{LinkedHashSet}}:
> * it is *iterated* by {{EventComponent.onApplicationEvent(...)}} on whatever 
> thread publishes the Spring application event
> * it is *mutated* by {{consumerStarted(...)}}/{{consumerStopped(...)}} when a 
> {{spring-event}} route is started or stopped
> There is no synchronization on the component side (the lock taken in 
> {{EventEndpoint.consumerStarted}} is per-endpoint, so two endpoints mutate 
> the same set under different locks, and the event-dispatch iteration takes no 
> lock at all).
> Stopping or starting a {{spring-event}} route while events are flowing 
> (supervising route controller, {{stopRoute()}} via JMX/actuator, or - as in 
> the reproducer below - a route that stops another route) throws 
> {{java.util.ConcurrentModificationException}} out of 
> {{ApplicationContext.publishEvent(...)}}. The reproducer is fully 
> deterministic because the mutation happens on the dispatching thread itself; 
> with concurrent publishers it is a (rarer) race with the same effect.
> Reproducer (verified failing on 4.22.0-SNAPSHOT, dependencies: camel-core, 
> camel-spring, junit-jupiter):
> {code:java}
> package org.apache.camel.spring.repro;
> import org.apache.camel.builder.RouteBuilder;
> import org.apache.camel.spring.SpringCamelContext;
> import org.junit.jupiter.api.Test;
> import org.springframework.context.ApplicationEvent;
> import 
> org.springframework.context.annotation.AnnotationConfigApplicationContext;
> import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
> public class SpringEventConcurrentModificationTest {
>     @Test
>     void stoppingRouteDuringEventDispatchMustNotThrow() throws Exception {
>         try (AnnotationConfigApplicationContext appCtx = new 
> AnnotationConfigApplicationContext()) {
>             appCtx.refresh();
>             SpringCamelContext camel = new SpringCamelContext(appCtx);
>             // wire the camel context as listener, as 
> camel-spring-xml/spring-boot would do
>             appCtx.addApplicationListener(camel);
>             camel.addRoutes(new RouteBuilder() {
>                 @Override
>                 public void configure() {
>                     from("spring-event:first").routeId("first")
>                             // stopping any other spring-event route while 
> the event is being
>                             // dispatched mutates EventComponent.endpoints 
> during iteration
>                             .process(e -> 
> e.getContext().getRouteController().stopRoute("second"));
>                     from("spring-event:second").routeId("second")
>                             .to("log:second");
>                 }
>             });
>             camel.start();
>             try {
>                 // EXPECTED: the event is delivered and route "second" is 
> stopped
>                 // ACTUAL: ConcurrentModificationException from 
> EventComponent.onApplicationEvent
>                 assertDoesNotThrow(() -> appCtx.publishEvent(new 
> MyEvent(this)));
>             } finally {
>                 camel.stop();
>             }
>         }
>     }
>     static class MyEvent extends ApplicationEvent {
>         MyEvent(Object source) {
>             super(source);
>         }
>     }
> }
> {code}
> Result:
> {noformat}
> Unexpected exception thrown: java.util.ConcurrentModificationException
>   at 
> java.base/java.util.LinkedHashMap$LinkedHashIterator.nextNode(LinkedHashMap.java:756)
>   ...
>   at 
> org.apache.camel.component.event.EventComponent.onApplicationEvent(EventComponent.java:89)
> {noformat}
> Suggested fix: use a {{CopyOnWriteArraySet}} for {{EventComponent.endpoints}} 
> (read-mostly pattern, iteration must be safe against concurrent add/remove).
> Related note: {{consumerStopped(endpoint)}} also removes the endpoint from 
> the set even when a *second* consumer on the same endpoint is still running, 
> so the surviving consumer stops receiving events. Since the fix is in the 
> same two methods (reference counting, or only removing when the endpoint's 
> load balancer has no processors left), it could be addressed together with 
> this issue.
> _This issue was found during an AI-assisted code review of the camel-spring 
> component. Reported by Claude Code on behalf of Federico Mariani. The 
> reproducers were executed and verified against 4.22.0-SNAPSHOT (main) before 
> filing._



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

Reply via email to