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)