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

Claus Ibsen resolved CAMEL-24130.
---------------------------------
    Resolution: Fixed

> camel-jpa: JpaMessageIdRepository can close the EntityManager owned by a JPA 
> consumer sharing the same exchange
> ---------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-24130
>                 URL: https://issues.apache.org/jira/browse/CAMEL-24130
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-jpa
>            Reporter: Federico Mariani
>            Assignee: Claus Ibsen
>            Priority: Major
>             Fix For: 4.22.0
>
>
> h3. Problem
> All {{JpaMessageIdRepository}} operations ({{add}}, {{contains}}, {{remove}}, 
> JpaMessageIdRepository.java:93,142,184) resolve their {{EntityManager}} with:
> {code:java}
> getTargetEntityManager(exchange, entityManagerFactory, /* 
> usePassedInEntityManager */ true, sharedEntityManager, true);
> {code}
> {{usePassedInEntityManager}} is hardcoded to {{true}}. Per 
> {{JpaHelper.getTargetEntityManager}} (JpaHelper.java:53-55), when that flag 
> is {{true}} and the exchange carries the {{CamelEntityManager}} header, 
> *that* {{EntityManager}} is used -- with no check that it was created against 
> the same {{EntityManagerFactory}}/persistence unit as the repository's own 
> {{entityManagerFactory}}.
> The JPA *consumer* puts its own long-lived, poll-scoped {{EntityManager}} 
> into exactly that header on every exchange it creates 
> ({{JpaConsumer.createExchange}}, JpaConsumer.java:507-512).
> Every repository method then closes the resolved {{EntityManager}} 
> unconditionally in a {{finally}} block (e.g. 
> JpaMessageIdRepository.java:118-126), regardless of whether the repository 
> created it or merely borrowed it from the header.
> h3. Impact
> A route combining a JPA consumer with a JPA-backed idempotent repository on 
> the *same* exchange, e.g.:
> {code:java}
> from("jpa:com.foo.MyEntity")
>     .idempotentConsumer(header("id"), jpaMessageIdRepository(emf, "foo"))
>     .to(...)
> {code}
> causes the idempotent repository to run its {{MessageProcessed}} query 
> against the *consumer's* {{EntityManager}} (which may not even map 
> {{MessageProcessed}} if it belongs to a different persistence unit) and then 
> *close* that {{EntityManager}} mid-batch. The subsequent 
> {{@Consumed}}/delete-handler and {{entityManager.flush()}} calls in 
> {{JpaConsumer.processBatch()}}/{{poll()}} then fail with an 
> {{IllegalStateException}} ("EntityManager is closed") once per batch, causing 
> the consumer to dispose and recreate its {{EntityManager}} -- and, since the 
> delete never completed, to reprocess the same rows on the next poll.
> h3. Suggested fix
> The repository should only close {{EntityManager}}s that it itself created 
> (i.e. skip the close when the EM came from the passed-in header or the 
> exchange-scoped map, whose lifecycle is owned by the creator / an 
> {{onCompletion}}). Consider defaulting {{usePassedInEntityManager}} to 
> {{false}} for the repository (matching {{JpaEndpoint}}'s own default), or at 
> least verifying EM/factory identity before reuse.
> Found during a broader camel-jpa code review (July 2026).
> _Claude Code on behalf of Croway_



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

Reply via email to