[
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)