Federico Mariani created CAMEL-24065:
----------------------------------------

             Summary: 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


{{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