You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@camel.apache.org by "Claus Ibsen (Jira)" <ji...@apache.org> on 2026/07/16 12:06:00 UTC

[jira] [Assigned] (CAMEL-24130) camel-jpa: JpaMessageIdRepository can close the EntityManager owned by a JPA consumer sharing the same exchange

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

Claus Ibsen reassigned CAMEL-24130:
-----------------------------------

    Assignee: Claus Ibsen

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