zstd

package
v0.9.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 24, 2026 License: MIT Imports: 3 Imported by: 0

README

ZSTD Pool Management

Overview

The pkg/zstd package provides a sync.Pool-based implementation for recycling zstd encoder and decoder instances. This reduces allocation overhead when creating multiple compression/decompression operations, which is especially beneficial in high-throughput scenarios like the NCPS cache server.

Motivation

Creating new zstd.Encoder and zstd.Decoder instances is relatively expensive due to internal buffer allocations. When handling many compression/decompression operations (as in chunk storage and HTTP compression), reusing these instances via a pool significantly reduces garbage collection pressure and improves performance.

Quick Reference

Import
import "github.com/kalbasit/ncps/pkg/zstd"
Common Patterns
Compress Data
pw := zstd.NewPooledWriter(&buf)
defer pw.Close()
pw.Write(data)
Decompress Data
pr, err := zstd.NewPooledReader(reader)
if err != nil {
    return err
}
defer pr.Close()
data, _ := io.ReadAll(pr)
One-Shot Encoding
enc := zstd.GetWriter()
defer zstd.PutWriter(enc)
compressed := enc.EncodeAll(data, nil)
One-Shot Decoding
dec := zstd.GetReader()
defer zstd.PutReader(dec)
dec.Reset(reader)
data, _ := io.ReadAll(dec)
API Cheat Sheet
Function Purpose Returns Error
GetWriter() Get encoder from pool *zstd.Encoder N/A
PutWriter(enc) Return encoder to pool void N/A
GetReader() Get decoder from pool *zstd.Decoder N/A
PutReader(dec) Return decoder to pool void N/A
NewPooledWriter(w) Create auto-managed writer *PooledWriter N/A
NewPooledReader(r) Create auto-managed reader *PooledReader error
pw.Close() Close writer, return to pool error compression error
pr.Close() Close reader, return to pool error nil

API Documentation

Low-Level API (Manual Management)

For fine-grained control, use the low-level functions:

Writer Pool
// Get an encoder from the pool
enc := zstd.GetWriter()
defer zstd.PutWriter(enc)

// Reset the encoder to write to a buffer
var buf bytes.Buffer
enc.Reset(&buf)

// Use the encoder
enc.Write(data)
enc.Close()

// The encoder is automatically reset before being returned to the pool
Reader Pool
// Get a decoder from the pool
dec := zstd.GetReader()
defer zstd.PutReader(dec)

// Reset the decoder to read from a compressed source
dec.Reset(compressedReader)

// Use the decoder
decompressed, err := io.ReadAll(dec)
High-Level API (Automatic Management)

For simplicity and to avoid resource leaks, use the wrapped types:

PooledWriter
import "github.com/kalbasit/ncps/pkg/zstd"

// Create a pooled writer - automatically manages the encoder
pw := zstd.NewPooledWriter(&buf)
defer pw.Close()  // Automatically returns encoder to pool

// Use like a normal zstd encoder
pw.Write(data)
pw.Close()
PooledReader
import "github.com/kalbasit/ncps/pkg/zstd"

// Create a pooled reader - automatically manages the decoder
pr, err := zstd.NewPooledReader(compressedReader)
if err != nil {
    return err
}
defer pr.Close()  // Automatically returns decoder to pool

// Use like a normal zstd decoder
data, err := io.ReadAll(pr)

Usage Examples

Example 1: Compressing Multiple Data Chunks
func compressChunks(chunks [][]byte) ([][]byte, error) {
    result := make([][]byte, len(chunks))

    for i, chunk := range chunks {
        var buf bytes.Buffer
        pw := zstd.NewPooledWriter(&buf)

        if _, err := pw.Write(chunk); err != nil {
            pw.Close()
            return nil, err
        }

        if err := pw.Close(); err != nil {
            return nil, err
        }

        result[i] = buf.Bytes()
    }

    return result, nil
}
Example 2: Decompressing Data
func decompressData(compressed []byte) ([]byte, error) {
    pr, err := zstd.NewPooledReader(bytes.NewReader(compressed))
    if err != nil {
        return nil, err
    }
    defer pr.Close()

    return io.ReadAll(pr)
}
Example 3: Direct Encoding (No Streaming)
func quickCompress(data []byte) []byte {
    enc := zstd.GetWriter()
    defer zstd.PutWriter(enc)

    // Use EncodeAll for non-streaming compression
    return enc.EncodeAll(data, nil)
}

Pool Configuration

Both pools use the default zstdion level and settings:

  • WriterPool: Default compression level (fast but good compression)
  • ReaderPool: Default decompression settings

For custom zstdion levels or options, create encoders/decoders directly without pooling:

// For custom compression level
enc, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedDefault))
if err != nil {
    return err
}
defer enc.Close()

Performance Considerations

  1. Pool Benefits: Most beneficial when you have many compression/decompression operations
  2. Memory Trade-off: The pool maintains encoder/decoder instances in memory, ready for reuse
  3. Thread-Safe: sync.Pool is thread-safe and designed for concurrent use
  4. Automatic Cleanup: Decoders and encoders are reset to a clean state before being returned to the pool

Integration Points

The zstd pool is used in:

  • pkg/server/server.go - HTTP response compression
  • pkg/storage/chunk/local.go - Local chunk storage compression
  • pkg/storage/chunk/s3.go - S3 chunk storage compression
  • Test utilities and helpers

Migration Guide

To migrate existing code to use the zstd pool:

Before (Direct Creation)
import "github.com/klauspost/zstd/zstd"

encoder, err := zstd.NewWriter(&buf)
if err != nil {
    return err
}
defer encoder.Close()
encoder.Write(data)
After (Using Pool)
import "github.com/kalbasit/ncps/pkg/zstd"

pw := zstd.NewPooledWriter(&buf)
defer pw.Close()
pw.Write(data)

Best Practices

  1. Always defer Close(): Ensure pooled resources are returned promptly
  2. Use Wrapped Types: Prefer PooledWriter and PooledReader for cleaner code
  3. Handle Errors: Check errors from Close(), Reset(), and Read/Write operations
  4. One Writer/Reader Per Operation: Get/release for each independent compression/decompression
  5. Avoid Nested Pools: Don't hold multiple pooled instances simultaneously unless necessary

Testing

The zstd pool includes comprehensive tests in pkg/zstd/zstd_test.go:

go test ./pkg/zstd -v -run

Tests cover:

  • Pool allocation and reuse
  • Round-trip compression/decompression
  • Error handling
  • Resource cleanup
  • Concurrent pool access

Implementation Details

Files Created
1. pkg/zstd/zstd.go

The main implementation file containing:

  • WriterPool: A sync.Pool managing reusable zstd.Encoder instances
  • ReaderPool: A sync.Pool managing reusable zstd.Decoder instances
2. pkg/zstd/zstd_test.go

Comprehensive test suite covering:

  • Pool get/put operations
  • Pooled wrapper functionality
  • Round-trip compression/decompression
  • Error handling
  • Multiple close operations
  • Nil safety
  • EncodeAll pattern support
Design Decisions
Why sync.Pool?
  • Built into Go standard library
  • Thread-safe without explicit locking
  • Automatically adjusts to contention
  • Zero-copy semantics
Why Two APIs?
  • Low-level: For complex scenarios needing manual control
  • High-level: For common cases with automatic cleanup
  • Recommendation: Use high-level in most cases
Why Default Compression Level?
  • Covers 99% of use cases
  • Custom levels can use direct zstd.NewWriter()
  • Simpler pool implementation
Decoder Reset Pattern
  • Decoders are reset but not explicitly closed when returned to pool
  • Prevents "decoder used after Close" errors
  • Allows safe reuse of pooled decoders

Documentation

Overview

Package zstd provides compression utilities for the NCPS project.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetReader

func GetReader() *zstd.Decoder

GetReader retrieves a zstd.Decoder from the pool, or creates a new one if the pool is empty. The caller must call PutReader or use NewPooledReader for automatic pool management.

Note: Prefer NewPooledReader for automatic resource cleanup.

Example (manual management):

dec := GetReader()
defer PutReader(dec)
dec.Reset(reader)
data, err := io.ReadAll(dec)

func GetWriter

func GetWriter() *zstd.Encoder

GetWriter retrieves a zstd.Encoder from the pool, or creates a new one if the pool is empty. The caller must call PutWriter to return the encoder to the pool when done.

Example:

enc := GetWriter()
defer PutWriter(enc)
enc.Reset(buf)
enc.Write(data)
enc.Close()

func PutReader

func PutReader(dec *zstd.Decoder)

PutReader returns a zstd.Decoder to the pool for reuse. The decoder is reset to nil before being returned to the pool. If dec is nil, this function is a no-op.

Always pair calls to GetReader with PutReader in a defer statement or ensure it's called in all code paths.

func PutWriter

func PutWriter(enc *zstd.Encoder)

PutWriter returns a zstd.Encoder to the pool for reuse. The encoder is reset to nil before being returned to the pool. If enc is nil, this function is a no-op.

Always pair calls to GetWriter with PutWriter in a defer statement or ensure it's called in all code paths.

Types

type PooledReader

type PooledReader struct {
	*zstd.Decoder
	// contains filtered or unexported fields
}

PooledReader wraps a zstd.Decoder with automatic pool management. When closed, the decoder is automatically returned to the pool.

Example:

pr, err := NewPooledReader(compressedReader)
if err != nil {
	return err
}
defer pr.Close()
data, err := io.ReadAll(pr)

func NewPooledReader

func NewPooledReader(r io.Reader) (*PooledReader, error)

NewPooledReader creates a new pooled reader that wraps the given io.Reader. The returned reader will automatically return its decoder to the pool when closed. This is the recommended way to use pooled readers for read operations.

Returns an error if the decoder cannot be reset to read from the given reader.

func (*PooledReader) Close

func (pr *PooledReader) Close() error

Close closes the reader and returns it to the pool. Multiple calls to Close are safe and will not panic. Note: The underlying decoder is not explicitly closed, only reset and returned to the pool.

type PooledWriter

type PooledWriter struct {
	*zstd.Encoder
	// contains filtered or unexported fields
}

PooledWriter wraps a zstd.Encoder with automatic pool management. When closed, the encoder is automatically returned to the pool.

Example:

pw := NewPooledWriter(&buf)
defer pw.Close()
pw.Write(data)

func NewPooledWriter

func NewPooledWriter(w io.Writer) *PooledWriter

NewPooledWriter creates a new pooled writer that wraps the given io.Writer. The returned writer will automatically return its encoder to the pool when closed. This is the recommended way to use pooled writers for write operations.

func (*PooledWriter) Close

func (pw *PooledWriter) Close() error

Close closes the encoder and returns it to the pool. Multiple calls to Close are safe and will not panic.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL