You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@qpid.apache.org by "Julio J. Gomez Diaz (Jira)" <ji...@apache.org> on 2026/10/22 07:48:00 UTC
[jira] [Created] (QPIDJMS-635) Allow a pooled/reusable codec instead of the per-thread AmqpCodec ThreadLocal
Julio J. Gomez Diaz created QPIDJMS-635:
-------------------------------------------
Summary: Allow a pooled/reusable codec instead of the per-thread AmqpCodec ThreadLocal
Key: QPIDJMS-635
URL: https://issues.apache.org/jira/browse/QPIDJMS-635
Project: Qpid JMS
Issue Type: Improvement
Components: qpid-jms-client
Affects Versions: 2.11.0
Reporter: Julio J. Gomez Diaz
h2. Description
{{org.apache.qpid.jms.provider.amqp.message.AmqpCodec}} keeps its {{{}EncoderImpl{}}}/{{{}DecoderImpl{}}} in a {{{}private static final ThreadLocal<EncoderDecoderContext> TLS_CODEC{}}}, created via {{{}ThreadLocal.withInitial(...){}}}. {{AmqpCodec.encodeMessage(...)}} / {{decodeMessage(...)}} and {{{}getEncoder(){}}}/{{{}getDecoder(){}}} all go through {{{}TLS_CODEC.get(){}}}, so calling them *creates* the codec as a side effect of the first use on a thread.
On the synchronous producer path ({{{}JmsSession.send{}}} → {{AmqpJmsMessageFacade.encodeMessage}} → {{{}AmqpCodec.encodeMessage{}}}) the encode runs on the {*}calling thread{*}. There is no offload, and there is no {{remove()}} anywhere, so a thread that sends holds the codec until it dies. With platform thread pools this is amortised. With virtual threads, each new VT that sends pays the full creation cost.
h2. Measured cost
On a fresh virtual thread a single producer encode allocates ~16.3 KB ({{{}gc.alloc.rate.norm{}}}, JMH), vs ~0 B on a warm platform thread:
||Scenario||bytes/op||ns/op||
|Empty fresh VT baseline|~502|~41 000|
|qpid-jms encode on fresh VT ({{{}AmqpCodec.TLS_CODEC{}}})|~16 320|~83 800|
|qpid-jms encode on warm platform thread|~0|~9|
Measured against {{qpid-jms-client}} 2.11.0 + {{proton-j}} 0.35.0, real producer {{send}} to an embedded Artemis AMQP acceptor, JDK 25.
h2. Precedent
Apache Fory solves exactly this by preferring a shared, bounded pool over a ThreadLocal for virtual threads ({{{}buildThreadSafeFory(){}}} → {{{}ThreadPoolFory{}}}, fixed size, slot preference with fallback to any free slot, released on completion; {{buildThreadSafeForyPool(int)}} for an explicit size). Kryo has the equivalent {{{}com.esotericsoftware.kryo.kryo5.util.Pool<T>{}}}. gemmellr's own remark above ("could actually do the same without that thread local") points the same way.
h2. Proposal (opt-in, default-preserving)
Keep {{TLS_CODEC}} as the default. Add an opt-in hook so the codec can come from a bounded pool instead of the ThreadLocal:
* A static provider/pool hook on {{{}AmqpCodec{}}}, e.g. {{AmqpCodec.setCodecProvider(...)}} (or a connection-level pool), used by {{{}encodeMessage{}}}/{{{}decodeMessage{}}}/{{{}getEncoder{}}}/{{{}getDecoder{}}}. Unset → unchanged behaviour.
* The provider borrows an {{EncoderDecoderContext}} for the duration of the call and returns it, capping memory at the pool size instead of at the number of threads.
Additive; existing callers see no change.
Related: PROTON-2994 raises the same per-thread-codec concern for
MessageImpl.tlsCodec (proton-j), which serves the embedded/standalone and
Azure Event Hubs paths. The two are independent changes: qpid-jms does not
use proton's ThreadLocal on its send path, it has its own (AmqpCodec.
TLS_CODEC), so each fix stands on its own. Linked for shared context on
the pool-based alternative.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@qpid.apache.org
For additional commands, e-mail: dev-help@qpid.apache.org