using System.Buffers;
using System.Collections.Concurrent;
namespace NATS.Server.IO;
///
/// Tiered write buffer pool with broadcast drain capability.
/// Maintains internal pools for common sizes (512, 4096, 65536) to reduce
/// allocation overhead on the hot publish path.
/// Go reference: client.go — dynamic buffer sizing and broadcast flush coalescing for fan-out.
///
public sealed class OutboundBufferPool
{
private const int SmallSize = 512;
private const int MediumSize = 4096;
private const int LargeSize = 65536;
private readonly ConcurrentBag _small = new(); // 512 B
private readonly ConcurrentBag _medium = new(); // 4 KiB
private readonly ConcurrentBag _large = new(); // 64 KiB
private long _rentCount;
private long _returnCount;
private long _broadcastCount;
/// Total buffer rent operations served by the pool.
public long RentCount => Interlocked.Read(ref _rentCount);
/// Total buffer return operations accepted by the pool.
public long ReturnCount => Interlocked.Read(ref _returnCount);
/// Total broadcast-drain operations performed.
public long BroadcastCount => Interlocked.Read(ref _broadcastCount);
// -----------------------------------------------------------------------
// IMemoryOwner surface (preserves existing callers)
// -----------------------------------------------------------------------
///
/// Rents an whose Memory.Length is at least
/// bytes. Tries the internal pool first; falls back to
/// .
///
/// Minimum required buffer size.
public IMemoryOwner Rent(int size)
{
Interlocked.Increment(ref _rentCount);
// Try to serve from the internal pool so that Dispose() returns the
// raw buffer back to us rather than to MemoryPool.Shared.
if (size <= SmallSize && _small.TryTake(out var sb))
return new PooledMemoryOwner(sb, _small);
if (size <= MediumSize && _medium.TryTake(out var mb))
return new PooledMemoryOwner(mb, _medium);
if (size <= LargeSize && _large.TryTake(out var lb))
return new PooledMemoryOwner(lb, _large);
// Nothing cached — rent from the system pool (which may return a larger
// buffer; that's fine, callers must honour Memory.Length, not the
// requested size).
int rounded = size <= SmallSize ? SmallSize
: size <= MediumSize ? MediumSize
: LargeSize;
return MemoryPool.Shared.Rent(rounded);
}
// -----------------------------------------------------------------------
// Raw byte[] surface
// -----------------------------------------------------------------------
///
/// Returns a byte[] from the internal pool whose length is at least
/// bytes. The caller is responsible for calling
/// when finished.
///
/// Minimum required buffer size.
public byte[] RentBuffer(int size)
{
Interlocked.Increment(ref _rentCount);
if (size <= SmallSize)
{
if (_small.TryTake(out var b)) return b;
return new byte[SmallSize];
}
if (size <= MediumSize)
{
if (_medium.TryTake(out var b)) return b;
return new byte[MediumSize];
}
if (_large.TryTake(out var lb)) return lb;
return new byte[LargeSize];
}
///
/// Returns to the appropriate tier so it can be
/// reused by a subsequent call.
///
/// Buffer previously rented from this pool.
public void ReturnBuffer(byte[] buffer)
{
Interlocked.Increment(ref _returnCount);
switch (buffer.Length)
{
case SmallSize:
_small.Add(buffer);
break;
case MediumSize:
_medium.Add(buffer);
break;
case LargeSize:
_large.Add(buffer);
break;
// Buffers of unexpected sizes are simply dropped (GC reclaims them).
}
}
// -----------------------------------------------------------------------
// Broadcast drain
// -----------------------------------------------------------------------
///
/// Coalesces multiple pending payloads into a single contiguous buffer for
/// batch writing. Copies every entry in
/// sequentially into and returns the total
/// number of bytes written.
///
/// The caller must ensure is large enough
/// (use to pre-check).
///
/// Go reference: client.go — broadcast flush coalescing for fan-out.
///
/// Pending write segments to coalesce.
/// Destination buffer receiving the concatenated payloads.
public int BroadcastDrain(IReadOnlyList> pendingWrites, byte[] destination)
{
var offset = 0;
foreach (var write in pendingWrites)
{
write.Span.CopyTo(destination.AsSpan(offset));
offset += write.Length;
}
Interlocked.Increment(ref _broadcastCount);
return offset;
}
///
/// Returns the total number of bytes needed to coalesce all
/// into a single buffer.
///
/// Pending write segments to size.
public static int CalculateBroadcastSize(IReadOnlyList> pendingWrites)
{
var total = 0;
foreach (var w in pendingWrites) total += w.Length;
return total;
}
// -----------------------------------------------------------------------
// Inner type: pooled IMemoryOwner
// -----------------------------------------------------------------------
///
/// Wraps a raw byte[] rented from an internal
/// and returns it to that bag on disposal.
///
private sealed class PooledMemoryOwner : IMemoryOwner
{
private readonly ConcurrentBag _pool;
private byte[]? _buffer;
///
/// Creates a pooled memory owner backed by a reusable byte array.
///
/// Rented backing buffer.
/// Pool to return the buffer to on disposal.
public PooledMemoryOwner(byte[] buffer, ConcurrentBag pool)
{
_buffer = buffer;
_pool = pool;
}
/// Memory view over the currently owned buffer.
public Memory Memory =>
_buffer is { } b ? b.AsMemory() : Memory.Empty;
/// Returns the owned buffer to the originating pool.
public void Dispose()
{
if (Interlocked.Exchange(ref _buffer, null) is { } b)
_pool.Add(b);
}
}
}