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: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to