Files
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Mario
a739cb2805 Bump github.com/twmb/franz-go from 1.18.1 to 1.20.0 (#5766)
* Bump github.com/twmb/franz-go from 1.18.1 to 1.20.0

Bumps [github.com/twmb/franz-go](https://github.com/twmb/franz-go) from 1.18.1 to 1.20.0.
- [Changelog](https://github.com/twmb/franz-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/twmb/franz-go/compare/v1.18.1...v1.20.0)

---
updated-dependencies:
- dependency-name: github.com/twmb/franz-go
  dependency-version: 1.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

* Update kfake

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mario <[email protected]>
2025-10-20 13:55:55 +02:00

454 lines
14 KiB
Go

package kgo
import (
"bytes"
"compress/gzip"
"encoding/binary"
"errors"
"io"
"runtime"
"slices"
"sync"
"github.com/klauspost/compress/s2"
"github.com/klauspost/compress/zstd"
"github.com/pierrec/lz4/v4"
)
var byteBuffers = sync.Pool{New: func() any { return bytes.NewBuffer(make([]byte, 8<<10)) }}
// CompressionCodecType is a bitfield specifying a Kafka-defined compression
// codec. Per spec, only four compression codecs are supported. However, if
// you control both the producer and consumer, you can technically override the
// codec to anything.
type CompressionCodecType int8
const (
// CodecNone is a compression codec signifying no compression is used.
CodecNone CompressionCodecType = iota
// CodecGzip is a compression codec signifying gzip compression.
CodecGzip
// CodecSnappy is a compression codec signifying snappy compression.
CodecSnappy
// CodecLz4 is a compression codec signifying lz4 compression.
CodecLz4
// CodecZstd is a compression codec signifying zstd compression.
CodecZstd
// CodecError is returned from compressing or decompressing if an error
// occurred.
CodecError = -1
)
// CompressionCodec configures how records are compressed before being sent.
//
// Records are compressed within individual topics and partitions, inside of a
// RecordBatch. All records in a RecordBatch are compressed into one record
// for that batch.
type CompressionCodec struct {
codec CompressionCodecType
level int
}
// NoCompression is a compression option that avoids compression. This can
// always be used as a fallback compression.
func NoCompression() CompressionCodec { return CompressionCodec{CodecNone, 0} }
// GzipCompression enables gzip compression with the default compression level.
func GzipCompression() CompressionCodec { return CompressionCodec{CodecGzip, gzip.DefaultCompression} }
// SnappyCompression enables snappy compression.
func SnappyCompression() CompressionCodec { return CompressionCodec{CodecSnappy, 0} }
// Lz4Compression enables lz4 compression with the fastest compression level.
func Lz4Compression() CompressionCodec { return CompressionCodec{CodecLz4, 0} }
// ZstdCompression enables zstd compression with the default compression level.
func ZstdCompression() CompressionCodec { return CompressionCodec{CodecZstd, 0} }
// CompressFlag is a flag to instruct the compressor.
type CompressFlag uint16
const (
// CompressDisableZstd instructs the compressor that zstd should not be
// used, even if the compressor supports it. This is used when
// producing to an old broker (pre Kafka v2.1) that does not yet
// support zstd compression. If you are confident you will only produce
// to new brokers, you can ignore this flag.
CompressDisableZstd CompressFlag = 1 + iota
)
func mkCompressFlags(produceRequestVersion int16) []CompressFlag {
if produceRequestVersion < 7 {
return []CompressFlag{CompressDisableZstd}
}
return nil
}
// Compressor is an interface that defines how produce batches are compressed.
// You can override the default client internal compressor for more control
// over what compressors to use, level, and memory reuse.
type Compressor interface {
// Compress compresses src and returns the compressed data as well as
// the codec type that was used. The 'dst' [bytes.Buffer] argument is
// pooled within the client and reused across calls to Compress. You
// can use 'dst' to save memory and return 'dst.Bytes()'. The returned
// slice is fully used *before* 'dst' is put back into the internal
// pool. As an example, you can look at the franz-go internal
// implementation of the default compressor in compression.go.
//
// Flags may optionally be provided to direct the compressor to enable
// or disable features. New backwards compatible flags may be
// introduced. If you add features to your compressor, be sure to
// evaluate if new flags exist to opt into or out of features.
Compress(dst *bytes.Buffer, src []byte, flags ...CompressFlag) ([]byte, CompressionCodecType)
}
// Decompressor is an interface that defines how fetch batches are
// decompressed. You can override the default client internal decompressor for
// more control over what decompressors to use and memory reuse.
type Decompressor interface {
// Decompress decompresses src, which is compressed with codecType,
// and returns the decompressed data or an error.
//
// If the decompression codec type is CodecNone, this should return
// the input slice.
Decompress(src []byte, codecType CompressionCodecType) ([]byte, error)
}
// WithLevel changes the compression codec's "level", effectively allowing for
// higher or lower compression ratios at the expense of CPU speed.
//
// For the zstd package, the level is a typed int; simply convert the type back
// to an int for this function.
//
// If the level is invalid, compressors just use a default level.
func (c CompressionCodec) WithLevel(level int) CompressionCodec {
c.level = level
return c
}
type compressor struct {
options []CompressionCodecType
gzPool sync.Pool
lz4Pool sync.Pool
zstdPool sync.Pool
}
// DefaultCompressor returns the default client compressor. The returned
// compressor will compress produce batches in preference-order of the
// specified codecs. Usually, you only need to specify one codec. If you are
// speaking to an old broker that may not support zstd, you may need to specify
// a second compressor as fallback (old Kafka did not support zstd). If no
// codecs are specified, or the specified codec is CodecNone, this returns
// 'nil, nil'. A compressor is only used within the client if it is non-nil.
func DefaultCompressor(codecs ...CompressionCodec) (Compressor, error) {
if len(codecs) == 0 {
return nil, nil
}
used := make(map[CompressionCodecType]bool) // we keep one type of codec per CompressionCodec
var keepIdx int
for _, codec := range codecs {
if _, exists := used[codec.codec]; exists {
continue
}
used[codec.codec] = true
codecs[keepIdx] = codec
keepIdx++
}
codecs = codecs[:keepIdx]
for _, codec := range codecs {
if codec.codec < 0 || codec.codec > 4 {
return nil, errors.New("unknown compression codec")
}
}
c := new(compressor)
out:
for _, codec := range codecs {
c.options = append(c.options, codec.codec)
switch codec.codec {
case CodecNone:
break out
case CodecGzip:
level := gzip.DefaultCompression
if codec.level != 0 {
if _, err := gzip.NewWriterLevel(nil, codec.level); err != nil {
level = codec.level
}
}
c.gzPool = sync.Pool{New: func() any { c, _ := gzip.NewWriterLevel(nil, level); return c }}
case CodecSnappy: // (no pool needed for snappy)
case CodecLz4:
level := max(codec.level, 0)
fn := func() any { return lz4.NewWriter(new(bytes.Buffer)) }
w := lz4.NewWriter(new(bytes.Buffer))
if err := w.Apply(lz4.CompressionLevelOption(lz4.CompressionLevel(level))); err == nil {
fn = func() any {
w := lz4.NewWriter(new(bytes.Buffer))
w.Apply(lz4.CompressionLevelOption(lz4.CompressionLevel(level)))
return w
}
}
w.Close()
c.lz4Pool = sync.Pool{New: fn}
case CodecZstd:
opts := []zstd.EOption{
zstd.WithWindowSize(64 << 10),
zstd.WithEncoderConcurrency(1),
zstd.WithZeroFrames(true),
}
fn := func() any {
zstdEnc, _ := zstd.NewWriter(nil, opts...)
r := &zstdEncoder{zstdEnc}
runtime.SetFinalizer(r, func(r *zstdEncoder) { r.inner.Close() })
return r
}
zstdEnc, err := zstd.NewWriter(nil, append(opts, zstd.WithEncoderLevel(zstd.EncoderLevel(codec.level)))...)
if err == nil {
zstdEnc.Close()
opts = append(opts, zstd.WithEncoderLevel(zstd.EncoderLevel(codec.level)))
}
c.zstdPool = sync.Pool{New: fn}
}
}
if c.options[0] == CodecNone {
return nil, nil // first codec was passthrough
}
return c, nil
}
type zstdEncoder struct {
inner *zstd.Encoder
}
func (c *compressor) Compress(dst *bytes.Buffer, src []byte, flags ...CompressFlag) ([]byte, CompressionCodecType) {
var disableZstd bool
for _, flag := range flags {
if flag == CompressDisableZstd {
disableZstd = true
}
}
var use CompressionCodecType
for _, option := range c.options {
if option == CodecZstd && disableZstd {
continue
}
use = option
break
}
var out []byte
switch use {
case CodecNone:
return src, 0
case CodecGzip:
gz := c.gzPool.Get().(*gzip.Writer)
defer c.gzPool.Put(gz)
gz.Reset(dst)
if _, err := gz.Write(src); err != nil {
return nil, CodecError
}
if err := gz.Close(); err != nil {
return nil, CodecError
}
out = dst.Bytes()
case CodecLz4:
lz := c.lz4Pool.Get().(*lz4.Writer)
defer c.lz4Pool.Put(lz)
lz.Reset(dst)
if _, err := lz.Write(src); err != nil {
return nil, CodecError
}
if err := lz.Close(); err != nil {
return nil, CodecError
}
out = dst.Bytes()
case CodecSnappy:
// Because the Snappy and Zstd codecs do not accept an io.Writer interface
// and directly take a []byte slice, here, the underlying []byte slice (`dst`)
// obtained from the bytes.Buffer{} from the pool is passed.
// As the `Write()` method on the buffer isn't used, its internal
// book-keeping goes out of sync, making the buffer unusable for further
// reading and writing via it's (eg: accessing via `Byte()`). For subsequent
// reads, the underlying slice has to be used directly.
//
// In this particular context, it is acceptable as there are no subsequent
// operations performed on the buffer and it is immediately returned to the
// pool and `Reset()` the next time it is obtained and used where `compress()`
// is called.
if l := s2.MaxEncodedLen(len(src)); l > dst.Cap() {
dst.Grow(l)
}
out = s2.EncodeSnappy(dst.Bytes(), src)
case CodecZstd:
zstdEnc := c.zstdPool.Get().(*zstdEncoder)
defer c.zstdPool.Put(zstdEnc)
if l := zstdEnc.inner.MaxEncodedSize(len(src)); l > dst.Cap() {
dst.Grow(l)
}
out = zstdEnc.inner.EncodeAll(src, dst.Bytes())
}
return out, use
}
type decompressor struct {
ungzPool sync.Pool
unlz4Pool sync.Pool
unzstdPool sync.Pool
pools pools
}
// DefaultDecompressor returns the default decompressor used by clients.
// The first pool provided that implements PoolDecompressBytes will be
// used where possible.
func DefaultDecompressor(pools ...Pool) Decompressor {
d := &decompressor{
ungzPool: sync.Pool{
New: func() any { return new(gzip.Reader) },
},
unlz4Pool: sync.Pool{
New: func() any { return lz4.NewReader(nil) },
},
unzstdPool: sync.Pool{
New: func() any {
zstdDec, _ := zstd.NewReader(nil,
zstd.WithDecoderLowmem(true),
zstd.WithDecoderConcurrency(1),
)
r := &zstdDecoder{zstdDec}
runtime.SetFinalizer(r, func(r *zstdDecoder) {
r.inner.Close()
})
return r
},
},
pools: pools,
}
return d
}
type zstdDecoder struct {
inner *zstd.Decoder
}
func (d *decompressor) Decompress(src []byte, codecType CompressionCodecType) ([]byte, error) {
if codecType == CodecNone {
return src, nil
}
var (
out *bytes.Buffer
rfn func() []byte
userPooled bool
)
d.pools.each(func(p Pool) bool {
if pdecompressBytes, ok := p.(PoolDecompressBytes); ok {
s := pdecompressBytes.GetDecompressBytes(src, codecType)
out = bytes.NewBuffer(s)
rfn = out.Bytes
userPooled = true
return true
}
return false
})
if out == nil {
out = byteBuffers.Get().(*bytes.Buffer)
out.Reset()
defer byteBuffers.Put(out)
// We clone out.Bytes since we are pooling out ourselves; we
// need to clone before return since we immediately put into
// the pool.
//
// For user provided slices, we put back into the pool only
// after the user calls Recycle on every record that has a
// reference to the slice. Thus, we can return the original
// slice from the user-provided pool: it is only recycled
// at the end when the user says they are done.
rfn = func() []byte { return slices.Clone(out.Bytes()) }
}
switch codecType {
case CodecGzip:
ungz := d.ungzPool.Get().(*gzip.Reader)
defer d.ungzPool.Put(ungz)
if err := ungz.Reset(bytes.NewReader(src)); err != nil {
return nil, err
}
if _, err := io.Copy(out, ungz); err != nil {
return nil, err
}
return rfn(), nil
case CodecSnappy:
if len(src) > 16 && bytes.HasPrefix(src, xerialPfx) {
return xerialDecode(src)
}
decoded, err := s2.Decode(out.Bytes(), src)
if err != nil {
return nil, err
}
if userPooled {
return decoded, nil
}
return slices.Clone(decoded), nil
case CodecLz4:
unlz4 := d.unlz4Pool.Get().(*lz4.Reader)
defer d.unlz4Pool.Put(unlz4)
unlz4.Reset(bytes.NewReader(src))
if _, err := io.Copy(out, unlz4); err != nil {
return nil, err
}
return rfn(), nil
case CodecZstd:
unzstd := d.unzstdPool.Get().(*zstdDecoder)
defer d.unzstdPool.Put(unzstd)
decoded, err := unzstd.inner.DecodeAll(src, out.Bytes())
if err != nil {
return nil, err
}
if userPooled {
return decoded, nil
}
return slices.Clone(decoded), nil
default:
return nil, errors.New("unknown compression codec")
}
}
var xerialPfx = []byte{130, 83, 78, 65, 80, 80, 89, 0}
var errMalformedXerial = errors.New("malformed xerial framing")
func xerialDecode(src []byte) ([]byte, error) {
// bytes 0-8: xerial header
// bytes 8-16: xerial version
// everything after: uint32 chunk size, snappy chunk
// we come into this function knowing src is at least 16
src = src[16:]
var dst, chunk []byte
var err error
for len(src) > 0 {
if len(src) < 4 {
return nil, errMalformedXerial
}
size := int32(binary.BigEndian.Uint32(src))
src = src[4:]
if size < 0 || len(src) < int(size) {
return nil, errMalformedXerial
}
if chunk, err = s2.Decode(chunk[:cap(chunk)], src[:size]); err != nil {
return nil, err
}
src = src[size:]
dst = append(dst, chunk...)
}
return dst, nil
}