Julio J. Gomez Diaz created PROTON-2994:
-------------------------------------------

             Summary: Allow a pooled/reusable codec instead of the per-thread 
AmqpCodec ThreadLocal
                 Key: PROTON-2994
                 URL: https://issues.apache.org/jira/browse/PROTON-2994
             Project: Qpid Proton
          Issue Type: Improvement
          Components: proton-j
    Affects Versions: proton-j-0.35.0
            Reporter: Julio J. Gomez Diaz


h2. Description

{{org.apache.qpid.proton.message.impl.MessageImpl}} keeps its 
{{EncoderImpl}}/{{DecoderImpl}} in a {{private static final 
ThreadLocal<EncoderDecoderPair> tlsCodec}}, created via 
{{ThreadLocal.withInitial(...)}}. This was introduced by PROTON-467 to avoid 
the expensive {{registerAllTypes()}} on every {{encode}}/{{decode}} call, and 
the reasoning still holds — but the ThreadLocal shape has two drawbacks that 
matter on JDK 21+ with virtual threads:

# *It allocates per thread and is never reclaimed.* There is no {{remove()}} 
anywhere in {{MessageImpl}} (or {{EncoderDecoderPair}}), so once a thread calls 
{{encode}}/{{decode}} it holds the pair until the thread dies. On a platform 
thread pool that is amortised; on virtual threads each new carrier/runtime 
thread that encodes pays the full creation cost, and short-lived threads keep 
the pair reachable until they terminate.

# *Measured cost.* On a fresh virtual thread a single {{Message.encode}} 
allocates ~16.5 KB ({{gc.alloc.rate.norm}}, JMH), vs ~224 B on a warm platform 
thread. Reproducer and numbers available (see below).

h2. Precedent

Apache Fory ships exactly this pattern and, for virtual threads, recommends the 
pool over the ThreadLocal:

* {{Fory.builder().buildThreadLocalFory()}} → {{ThreadLocalFory}} (the shape 
used here).
* {{Fory.builder().buildThreadSafeFory()}} → {{ThreadPoolFory}}, fixed pool of 
{{4 * availableProcessors()}}.
* {{Fory.builder().buildThreadSafeForyPool(int)}} → explicit fixed pool size.

{{ThreadPoolFory}} picks a preferred slot by {{identityHashCode(thread) % 
poolSize}} and falls back to any free slot ({{tryBorrowPreferredSlots}}), 
releasing on completion — thread-agnostic, bounded memory. Fory's "Virtual 
Threads" guide explicitly recommends the pool variant. Kryo has the equivalent 
{{com.esotericsoftware.kryo.kryo5.util.Pool<T>}} ({{obtain()}}/{{free()}}).

Relevant prior discussion in this project:
* *PROTON-467* — introduced the ThreadLocal (Fixed).
* *PROTON-1410* — "Move MessageImpl::TLS to an utility class" (Won't Fix). In 
that discussion tabish121 already floated the direction _"if the Encoders were 
say made to not require being managed as thread locals for instance, e.g. we 
made them stateless"_ — which is the same idea from the other end.

h2. Proposal (opt-in, default-preserving)

Keep {{tlsCodec}} as the default. Add an opt-in way to use a pooled/borrowable 
pair:

* A static hook on {{MessageImpl}}, e.g. 
{{MessageImpl.setCodecProvider(CodecProvider)}} (or a 
{{setEncoderDecoderPool(...)}}), where the provider supplies an 
{{EncoderDecoderPair}} for the duration of an {{encode}}/{{decode}} call and 
returns it afterwards. When unset, behaviour is unchanged ({{tlsCodec}}).
* A bounded pool implementation of that provider (fixed size, borrow/release), 
so memory is capped by the pool size rather than by thread count.

This is additive; existing callers see no change.



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