[
https://issues.apache.org/jira/browse/CAMEL-24065?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Claus Ibsen closed CAMEL-24065.
-------------------------------
Resolution: Fixed
> 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: Minor
> 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)