Class PooledPayloadWriter
A growable, pooled IBufferWriter<T> that plugs the gap between the bare IBufferWriter<T> contract and what a zero-copy transport actually needs: the ability to (a) read back the bytes already written, (b) patch already-written bytes at an absolute position (reserve-then-patch for length prefixes and headers whose value is only known once later bytes have been written), and (c) hand off pooled-buffer OWNERSHIP so a transport can complete an asynchronous socket write and only then return the memory to the pool, with no intermediate copy.
This contract was first invented privately, as Artery G1's internal PooledFrameWriter,
because IBufferWriter<T> alone could not express it. It belongs on the core
SerializerV2 surface: any V2 encode path -- Artery, a future sourcegen'd
serializer, or a user transport -- needs the same reserve/patch/detach shape. Semantically it
plays the same role as Pekko's EnvelopeBufferPool: a reusable wire buffer whose lifetime
is decoupled from the encode call that filled it.
Ownership rules (read this before using the type):
- A freshly constructed writer owns one array rented from its buffer source: the ArrayPool<T> passed to the constructor, or Shared when none is given. It grows by renting a bigger array, copying the already-written prefix, and returning the old array -- never by copying on every write. EVERY rent and return in the writer's lifetime -- the initial rent, growth, Dispose(), and the disposal of the owner returned by Detach() -- goes through that same pool.
- Dispose() (without a prior Detach()) returns the current backing array to the pool. This is the "I'm done, nobody else needs these bytes" path -- e.g. an encode that threw partway through.
- Detach() is the "hand the bytes to someone else" path: it returns an IMemoryOwner<T> whose Memory is exactly the written slice, and whose own Dispose() is what returns the array to the pool (e.g. once an async socket write against that memory has completed). After Detach(), this writer is SPENT: every member except Dispose() throws ObjectDisposedException, calling Detach() again throws, and this writer's own Dispose() becomes a no-op because ownership already moved.
- Reset() reuses the current rented array for a new encode (no re-rent); it is only valid while the writer is still alive (neither disposed nor detached).
Why ArrayPool<T> and not a custom buffer-source abstraction (or MemoryPool<T>)? Deliberate: ArrayPool<T> is the narrowest standard type that covers every buffer-source strategy this writer needs -- the shared pool, a dedicated per-transport pool, and POH-pinned arrays via a custom ArrayPool<T> subclass (artery design.md, Decision 9: POH-pinned buffers only if pinning churn shows up in measurement). A bespoke interface would duplicate it; MemoryPool<T> would force Memory<T>-based internals and give up the raw array the patch/growth paths rely on.
Inherited Members
Namespace: Akka.Serialization
Assembly: Akka.dll
Syntax
public sealed class PooledPayloadWriter : IBufferWriter<byte>, IDisposable
Constructors
| Edit this page View SourcePooledPayloadWriter(int, int, ArrayPool<byte>?)
Initializes a new instance of the PooledPayloadWriter class.
Declaration
public PooledPayloadWriter(int initialCapacityHint = 256, int maxCapacity = 2147483647, ArrayPool<byte>? pool = null)
Parameters
| Type | Name | Description |
|---|---|---|
| int | initialCapacityHint | A hint for the initial rented buffer size; the writer grows automatically if exceeded
(bounded by |
| int | maxCapacity | The maximum total number of bytes this writer will ever hold. Any GetSpan(int), GetMemory(int), or Advance(int) call that would push the written count past this bound throws PayloadSizeExceededException. Defaults to MaxValue (effectively unbounded). |
| ArrayPool<byte> | pool | The buffer source for every rent and return in this writer's lifetime (initial rent, growth, Dispose(), and the disposal of the owner returned by Detach()). null (the default) means Shared. Pass a dedicated pool -- or a custom ArrayPool<T> subclass over POH-pinned arrays -- to control buffer placement without changing this type. |
Exceptions
| Type | Condition |
|---|---|
| ArgumentOutOfRangeException |
|
Properties
| Edit this page View SourceWrittenCount
The number of bytes written so far.
Declaration
public int WrittenCount { get; }
Property Value
| Type | Description |
|---|---|
| int |
WrittenMemory
A read-only view over the bytes written so far.
Declaration
public ReadOnlyMemory<byte> WrittenMemory { get; }
Property Value
| Type | Description |
|---|---|
| ReadOnlyMemory<byte> |
WrittenSpan
A read-only view over the bytes written so far.
Declaration
public ReadOnlySpan<byte> WrittenSpan { get; }
Property Value
| Type | Description |
|---|---|
| ReadOnlySpan<byte> |
Methods
| Edit this page View SourceAdvance(int)
Notifies the IBufferWriter<T> that count data items were written to the output Span<T> or Memory<T>.
Declaration
public void Advance(int count)
Parameters
| Type | Name | Description |
|---|---|---|
| int | count | The number of data items written to the Span<T> or Memory<T>. |
Exceptions
| Type | Condition |
|---|---|
| PayloadSizeExceededException | Advancing by |
Detach()
Detaches ownership of the backing buffer from this writer: returns an
IMemoryOwner<T> whose Memory is exactly the
written slice. Disposing the returned owner returns the underlying array to the SAME pool
this writer rented it from (the constructor's pool, or Shared).
The IMemoryOwner<T> return type deliberately leaves room for a future
pooled-owner implementation (Pekko EnvelopeBufferPool-style) without an API change.
After this call the writer is SPENT: every member except Dispose() throws ObjectDisposedException -- including a second call to Detach(). This writer's own Dispose() becomes a no-op, because ownership of the buffer has moved to the returned owner.
Declaration
public IMemoryOwner<byte> Detach()
Returns
| Type | Description |
|---|---|
| IMemoryOwner<byte> | An owner of the written bytes; the caller is responsible for disposing it. |
Exceptions
| Type | Condition |
|---|---|
| ObjectDisposedException | The writer has already been detached or disposed. |
Dispose()
Returns the rented buffer to the pool it was rented from. A no-op if this writer has already been detached (ownership moved to the IMemoryOwner<T> returned by Detach()) or already disposed.
Declaration
public void Dispose()
GetMemory(int)
Returns a Memory<T> to write to that is at least the requested size (specified by sizeHint).
Declaration
public Memory<byte> GetMemory(int sizeHint = 0)
Parameters
| Type | Name | Description |
|---|---|---|
| int | sizeHint | The minimum length of the returned Memory<T>. If 0, a non-empty buffer is returned. |
Returns
| Type | Description |
|---|---|
| Memory<byte> | A Memory<T> of at least the size |
Exceptions
| Type | Condition |
|---|---|
| OutOfMemoryException | The requested buffer size is not available. |
| PayloadSizeExceededException | Satisfying |
GetPatchSpan(int, int)
Returns a mutable view over an already-written absolute byte range, so a caller can back-patch a field (a frame length, a fixed header) whose value is only known after later bytes have been written.
Declaration
public Span<byte> GetPatchSpan(int start, int length)
Parameters
| Type | Name | Description |
|---|---|---|
| int | start | The absolute start offset, in bytes, from the start of the writer. |
| int | length | The number of bytes to expose for patching. |
Returns
| Type | Description |
|---|---|
| Span<byte> |
Exceptions
| Type | Condition |
|---|---|
| ArgumentOutOfRangeException | The patch range falls outside the already-written bytes. |
GetSpan(int)
Returns a Span<T> to write to that is at least the requested size (specified by sizeHint).
Declaration
public Span<byte> GetSpan(int sizeHint = 0)
Parameters
| Type | Name | Description |
|---|---|---|
| int | sizeHint | The minimum length of the returned Span<T>. If 0, a non-empty buffer is returned. |
Returns
| Type | Description |
|---|---|
| Span<byte> | A Span<T> of at least the size |
Exceptions
| Type | Condition |
|---|---|
| PayloadSizeExceededException | Satisfying |
Reset()
Resets the writer for reuse without re-renting: the currently rented array is kept and the written count is set back to zero. Only valid while the writer is still alive (has not been detached or disposed).
Declaration
public void Reset()
Exceptions
| Type | Condition |
|---|---|
| ObjectDisposedException | The writer has been detached or disposed. |
Edit this page