Files
graphql-monitoring-proxy/pkg/pools/buffer.go
lukaszraczylo 546ded1d9c fix(pools): return pooled gzip reader on Reset error
GetGzipReader got a reader from the pool, Reset it, and on error returned a
fresh gzip.NewReader while never returning the pooled reader to the pool —
one leaked reader per reset error (non-gzip input), silently draining the
pool. Return the pooled reader without Close (Close panics because the
decompressor is nil until a header parses); the next caller's Reset
reinitialises it.

Test: TestGetGzipReader_ResetErrorPath (error path leaves the pool usable
for subsequent valid gzip reads).
2026-08-13 03:10:21 +01:00

116 lines
2.8 KiB
Go

// Package pools provides memory-efficient buffer and gzip reader pools
// for reducing allocations in high-throughput request processing.
// Buffers are automatically sized and recycled to minimize GC pressure.
package pools
import (
"bytes"
"compress/gzip"
"io"
"sync"
)
const (
// MaxBufferSize is the maximum size of a buffer that will be returned to the pool
MaxBufferSize = 1024 * 1024 // 1MB
// InitialBufferSize is the initial capacity of buffers in the pool
InitialBufferSize = 4096 // 4KB
)
// bufferPool is the global pool for reusable buffers
var bufferPool = &sync.Pool{
New: func() any {
return bytes.NewBuffer(make([]byte, 0, InitialBufferSize))
},
}
// gzipWriterPool is the global pool for reusable gzip writers
var gzipWriterPool = &sync.Pool{
New: func() any {
return gzip.NewWriter(nil)
},
}
// gzipReaderPool is the global pool for reusable gzip readers
var gzipReaderPool = &sync.Pool{
New: func() any {
return new(gzip.Reader)
},
}
// GetBuffer retrieves a buffer from the pool
func GetBuffer() *bytes.Buffer {
buf := bufferPool.Get().(*bytes.Buffer)
buf.Reset()
return buf
}
// PutBuffer returns a buffer to the pool
func PutBuffer(buf *bytes.Buffer) {
if buf == nil {
return
}
// Don't pool large buffers to avoid memory bloat
if buf.Cap() > MaxBufferSize {
return
}
buf.Reset()
bufferPool.Put(buf)
}
// GetGzipWriter retrieves a gzip writer from the pool
func GetGzipWriter(w io.Writer) *gzip.Writer {
gz := gzipWriterPool.Get().(*gzip.Writer)
gz.Reset(w)
return gz
}
// PutGzipWriter returns a gzip writer to the pool
func PutGzipWriter(gz *gzip.Writer) {
if gz == nil {
return
}
gz.Reset(nil)
gzipWriterPool.Put(gz)
}
// GetGzipReader retrieves a gzip reader from the pool
func GetGzipReader(r io.Reader) (*gzip.Reader, error) {
gr := gzipReaderPool.Get().(*gzip.Reader)
if err := gr.Reset(r); err != nil {
// Reset failed (e.g. non-gzip data): return the pooled reader so it
// isn't leaked, and hand back a fresh one for this call. We must not
// call gr.Close() here — the decompressor is nil until a header
// parses, so Close would panic. The next GetGzipReader's Reset
// reinitialises it.
gzipReaderPool.Put(gr)
return gzip.NewReader(r)
}
return gr, nil
}
// PutGzipReader returns a gzip reader to the pool
func PutGzipReader(gr *gzip.Reader) {
if gr == nil {
return
}
gr.Close()
gzipReaderPool.Put(gr)
}
// Stats provides statistics about the buffer pool usage
type Stats struct {
BuffersInUse int
MaxBufferSize int
}
// GetStats returns current pool statistics (placeholder for future monitoring)
func GetStats() Stats {
// This is a placeholder for future implementation
// sync.Pool doesn't provide direct statistics access
return Stats{
BuffersInUse: 0,
MaxBufferSize: MaxBufferSize,
}
}