Federico Mariani created CAMEL-24130:
----------------------------------------
Summary: 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
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)