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)

Reply via email to