Federico Mariani created CAMEL-26692:
----------------------------------------
Summary: camel-jms, camel-amqp, camel-activemq - a send from a
transacted route is not rolled back when username/password are set
Key: CAMEL-26692
URL: https://issues.apache.org/jira/browse/CAMEL-26692
Project: Camel
Issue Type: Bug
Components: camel-activemq, camel-amqp, camel-jms
Affects Versions: 4.18.5, 4.23.0, 4.22.2
Reporter: Federico Mariani
h3. Problem
In a transacted route such as
{{from("jms:queue:in?transacted=true").to("jms:queue:out")}}, the message sent
to {{out}} takes part in the transaction of the consumed message: when the
route fails, both are rolled back and the broker delivers the consumed message
again. This no longer holds when {{username}} and {{password}} are set, on the
component or on the endpoints. The message sent to {{out}} is committed at
once, the consumed message is rolled back and delivered again, and the route
sends it a second time: {{out}} gets duplicates.
Source on main (48d063cfde0a),
{{components/camel-jms/src/main/java/org/apache/camel/component/jms/}}:
* {{JmsComponent.java}} lines 1244-1259 ({{createEndpoint}}): when a username
and a password are set, it wraps the connection factory of the endpoint in a
new {{UserCredentialsConnectionFactoryAdapter}} (line 1253), for each endpoint.
Lines 1245-1246 take the username and password of the component when the
endpoint has none, so with credentials on the component every endpoint gets its
own adapter instance.
* {{JmsConfiguration.java}}: the consumer endpoint gives its own connection
factory to the listener container (line 1697, through
{{getOrCreateListenerConnectionFactory}}, line 1869) and to the lazily created
{{JmsTransactionManager}} (line 1884). The producer endpoint gives its own to
the {{JmsTemplate}} (line 780, through {{createTemplateConnectionFactory}},
line 1876).
* Spring looks up the session of the transaction by the connection factory
instance: {{JmsTransactionManager}} binds it under its factory (spring-jms
7.0.10, {{JmsTransactionManager.java}} line 234), and {{JmsTemplate.execute}}
(line 525) looks it up with its own factory ({{ConnectionFactoryUtils.java}}
line 286). Two adapter instances never match, so the producer opens its own
session and commits the send at once.
* Without credentials, the endpoints share the connection factory instance of
the component, and the send is part of the transaction.
h3. How to reproduce
Measured on main 48d063cfde0a with a test in each module (not committed):
* camel-jms: embedded Artemis ({{ArtemisServiceFactory.createVMService()}}),
{{JmsComponent}} with an Artemis {{ActiveMQConnectionFactory}} as
{{connectionFactory}}.
* camel-amqp: embedded Artemis with the AMQP acceptor
({{ArtemisServiceFactory.createAMQPService()}}), {{AMQPComponent}} with
{{brokerUrl}}.
* Route:
{{from("<scheme>:queue:in?transacted=true").to("<scheme>:queue:out")}}, then a
processor that throws on the first delivery only. A second route consumes
{{out}} into a mock. One message is sent to {{in}}; the test waits for the
redelivery and a quiet period of 2 seconds, then counts the messages on {{out}}.
* The test brokers do not check credentials, so the username and password
change only how the endpoints are wired.
||Component||Credentials||Deliveries of the consumed message||Messages on out||
|camel-jms|none (control)|2|1|
|camel-jms|{{username}}/{{password}} on the component|2|2|
|camel-jms|{{username=camel&password=rider}} on both endpoints|2|2|
|camel-amqp|none (control)|2|1|
|camel-amqp|{{username}}/{{password}} on the component|2|2|
|camel-amqp|{{username=camel&password=rider}} on both endpoints|2|2|
*camel-activemq* was not run against a broker (its tests use a container).
{{ActiveMQComponent}} does not override {{createEndpoint}}, so it takes the
same path. A check without a broker, which only creates the endpoints and
compares the instances, gives:
* {{connectionFactory}} set on the component, no credentials: the transaction
manager of the consumer endpoint and the template of the producer endpoint use
the same instance.
* The same with {{username}}/{{password}} on the component: two different
{{UserCredentialsConnectionFactoryAdapter}} instances.
* Also, with only {{brokerURL}} set ({{usePooledConnection=false}} for the
check), each endpoint creates its own {{ActiveMQConnectionFactory}}:
{{ActiveMQConfiguration#createConnectionFactory}} (line 170) runs lazily on the
configuration copy of each endpoint. So the instances differ even without
credentials. With the default pooled factory, that would be one pool per
endpoint; this comes from reading the source only, as pooled-jms is not on the
test classpath of the module.
h3. Proposed change
* In {{JmsComponent#createEndpoint}}, reuse one adapter for the endpoints that
wrap the same connection factory with the same username and password (for
example created on the component), instead of a new one per endpoint. Endpoints
with other credentials or their own {{connectionFactory}} keep their own
factory: they cannot share a transaction anyway.
* Keep credentials re-resolved when the routes are rebuilt: CAMEL-24637 (in
progress) relies on the fresh adapter of each endpoint to pick up rotated
credentials, so the shared adapter must be rebuilt with the component.
* camel-activemq with {{brokerURL}} only: create the connection factory once
for the component rather than per endpoint configuration. This may be better
tracked as its own issue.
* Add a test per component that sends from a transacted consumer route with
credentials and checks the rollback.
* Workaround today: set the credentials on the connection factory itself (the
user and password of the client factory), not with the {{username}} and
{{password}} options.
h3. Impact / upgrade
* It changes behaviour for transacted routes with {{username}}/{{password}}:
the message sent from the route becomes part of the transaction, as without
credentials. This needs an entry in the 4.24 upgrade guide.
* A producer in the thread of a consumer would then also use the session of the
consumer, so it no longer opens a connection of its own per message.
* Not affected: credentials set on the connection factory, and routes without a
transaction.
* The same code is in 4.18.5, 4.22.2 and 4.23.0 (read from the source;
spring-jms 6.2.19 on camel-4.18.x does the same lookup by factory instance,
{{ConnectionFactoryUtils.java}} line 297).
h3. Related
* CAMEL-26658: found while adding a shared producer connection to camel-amqp;
that change keeps the factory of each endpoint for the transaction lookup, so
it does not change this behaviour.
* CAMEL-24637: re-authentication on secret rotation in camel-jms, which relies
on the adapter of each endpoint.
* CAMEL-246 (commit b4e7378ab6cf, 2008): wraps the connection factory with the
username and password in {{createEndpoint}}. CAMEL-4329 (commit 153aca005cc1,
2011): the same with a {{connectionFactory}} in the endpoint URI. Commit
6b15b6536cae (2016) made the username and password of the component the default
of the endpoints.
* CAMEL-20521: transacted sends with camel-amqp.
_Claude Code on behalf of Federico Mariani_
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)