193 lines
5.9 KiB
Go
193 lines
5.9 KiB
Go
package utils
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"compress/zlib"
|
|
"io"
|
|
"sync"
|
|
|
|
"github.com/andybalholm/brotli"
|
|
"github.com/klauspost/compress/zstd"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Pooled decompression readers
|
|
//
|
|
// Each encoding gets a sync.Pool with Acquire/Release helpers that follow the
|
|
// same contract:
|
|
// - Acquire(r io.Reader) returns a ready-to-read decompressor, reusing a
|
|
// pooled instance when possible, falling back to a fresh allocation.
|
|
// - Release returns the decompressor to the pool for future reuse.
|
|
// Callers MUST call Release exactly once after the reader is fully consumed.
|
|
//
|
|
// All pool operations are panic-safe: type assertions use the comma-ok form,
|
|
// Reset calls are wrapped in recover, and nil checks guard every dereference.
|
|
// A corrupt or wrong-typed pooled instance is silently discarded (GC reclaims
|
|
// it) and a fresh allocation takes its place.
|
|
//
|
|
// Gzip, deflate, and brotli readers are stateless between streams — Close (if
|
|
// applicable) then Reset is safe. Zstd decoders run background goroutines so
|
|
// Close is terminal; pooled decoders are reset without closing.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// safeReset calls resetFn and recovers from any panic. Returns true on success.
|
|
// A corrupt pooled reader may panic inside Reset; this prevents that from
|
|
// bringing down the server.
|
|
func safeReset(resetFn func() error) (ok bool) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
ok = false
|
|
}
|
|
}()
|
|
return resetFn() == nil
|
|
}
|
|
|
|
// ---- gzip ----
|
|
|
|
var gzipReaderPool = sync.Pool{
|
|
New: func() any {
|
|
return &gzip.Reader{}
|
|
},
|
|
}
|
|
|
|
// AcquireGzipReader gets a gzip.Reader from the pool and resets it to read from r,
|
|
// or creates a new one if the pool is empty or reset fails.
|
|
func AcquireGzipReader(r io.Reader) (*gzip.Reader, error) {
|
|
if v := gzipReaderPool.Get(); v != nil {
|
|
if gz, ok := v.(*gzip.Reader); ok {
|
|
if safeReset(func() error { return gz.Reset(r) }) {
|
|
return gz, nil
|
|
}
|
|
}
|
|
// Wrong type or reset failed/panicked — discard, let GC reclaim.
|
|
}
|
|
return gzip.NewReader(r)
|
|
}
|
|
|
|
// ReleaseGzipReader closes and returns a gzip.Reader to the pool.
|
|
func ReleaseGzipReader(gz *gzip.Reader) {
|
|
if gz == nil {
|
|
return
|
|
}
|
|
_ = gz.Close()
|
|
gzipReaderPool.Put(gz)
|
|
}
|
|
|
|
// ---- deflate ----
|
|
//
|
|
// HTTP Content-Encoding "deflate" is zlib-wrapped DEFLATE (RFC 1950), NOT raw
|
|
// DEFLATE (RFC 1951). This matches fasthttp's implementation and the HTTP spec
|
|
// (RFC 9110 §8.4.1.2). We use compress/zlib, not compress/flate.
|
|
|
|
// deflateReader is the interface that zlib readers support for pooling.
|
|
// The concrete type from zlib.NewReader is unexported, but implements Resetter.
|
|
type deflateReader interface {
|
|
io.ReadCloser
|
|
Reset(r io.Reader, dict []byte) error
|
|
}
|
|
|
|
// No New func: zlib.NewReader validates the header eagerly, so it cannot be
|
|
// created with a nil reader. Pooled readers are populated via Release.
|
|
var deflateReaderPool = sync.Pool{}
|
|
|
|
// AcquireFlateReader gets a zlib (HTTP "deflate") reader from the pool and
|
|
// resets it to read from r, or creates a new one if the pool is empty or
|
|
// reset fails.
|
|
func AcquireFlateReader(r io.Reader) (io.ReadCloser, error) {
|
|
if v := deflateReaderPool.Get(); v != nil {
|
|
if dr, ok := v.(deflateReader); ok {
|
|
if safeReset(func() error { return dr.Reset(r, nil) }) {
|
|
return dr, nil
|
|
}
|
|
}
|
|
}
|
|
return zlib.NewReader(r)
|
|
}
|
|
|
|
// ReleaseFlateReader closes and returns a deflate reader to the pool.
|
|
func ReleaseFlateReader(fr io.ReadCloser) {
|
|
if fr == nil {
|
|
return
|
|
}
|
|
_ = fr.Close()
|
|
deflateReaderPool.Put(fr)
|
|
}
|
|
|
|
// ---- brotli ----
|
|
|
|
var brotliReaderPool = sync.Pool{
|
|
New: func() any {
|
|
return brotli.NewReader(nil)
|
|
},
|
|
}
|
|
|
|
// AcquireBrotliReader gets a brotli.Reader from the pool and resets it to read
|
|
// from r, or creates a new one if the pool is empty or reset panics.
|
|
func AcquireBrotliReader(r io.Reader) *brotli.Reader {
|
|
if v := brotliReaderPool.Get(); v != nil {
|
|
if br, ok := v.(*brotli.Reader); ok {
|
|
// brotli.Reset is void (no error), but wrap in safeReset for
|
|
// consistency: a corrupt pooled reader could panic on Reset.
|
|
if safeReset(func() error { br.Reset(r); return nil }) {
|
|
return br
|
|
}
|
|
}
|
|
// Wrong type or reset panicked — discard, let GC reclaim.
|
|
}
|
|
return brotli.NewReader(r)
|
|
}
|
|
|
|
// ReleaseBrotliReader returns a brotli.Reader to the pool.
|
|
// Brotli readers have no Close method; Reset(nil) is sufficient to drop the
|
|
// reference to the underlying reader.
|
|
func ReleaseBrotliReader(br *brotli.Reader) {
|
|
if br == nil {
|
|
return
|
|
}
|
|
br.Reset(nil)
|
|
brotliReaderPool.Put(br)
|
|
}
|
|
|
|
// ---- zstd ----
|
|
|
|
var zstdDecoderPool = sync.Pool{
|
|
New: func() any {
|
|
dec, err := zstd.NewReader(nil, zstd.WithDecoderConcurrency(1))
|
|
if err != nil {
|
|
// NewReader(nil) failing is unexpected; return nil so Acquire
|
|
// falls through to a fresh allocation with the real reader.
|
|
return nil
|
|
}
|
|
return dec
|
|
},
|
|
}
|
|
|
|
// AcquireZstdDecoder gets a zstd.Decoder from the pool and resets it to read
|
|
// from r, or creates a new one if the pool is empty or reset fails.
|
|
// Decoders are created with concurrency=1 to minimise goroutine overhead.
|
|
func AcquireZstdDecoder(r io.Reader) (*zstd.Decoder, error) {
|
|
if v := zstdDecoderPool.Get(); v != nil {
|
|
if dec, ok := v.(*zstd.Decoder); ok && dec != nil {
|
|
if safeReset(func() error { return dec.Reset(r) }) {
|
|
return dec, nil
|
|
}
|
|
// Reset failed/panicked — release references before discarding.
|
|
// Don't call Close (terminal); Reset(nil) is safe per pool contract.
|
|
_ = dec.Reset(nil)
|
|
}
|
|
}
|
|
return zstd.NewReader(r, zstd.WithDecoderConcurrency(1))
|
|
}
|
|
|
|
// ReleaseZstdDecoder returns a zstd.Decoder to the pool.
|
|
// Unlike other decoders, zstd.Close() is terminal (stops background goroutines
|
|
// permanently). We only call Reset(nil) to release the source reference, then
|
|
// re-pool. Close is never called on pooled decoders.
|
|
func ReleaseZstdDecoder(dec *zstd.Decoder) {
|
|
if dec == nil {
|
|
return
|
|
}
|
|
_ = dec.Reset(nil)
|
|
zstdDecoderPool.Put(dec)
|
|
}
|