Skip to content

Commit

Permalink
feat: compress (#601)
Browse files Browse the repository at this point in the history
  • Loading branch information
fracasula authored Aug 27, 2024
1 parent 80bb37b commit cda0a40
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 8 deletions.
17 changes: 17 additions & 0 deletions compress/benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package compress

import (
"testing"
)

// BenchmarkNew-24 55165 22851 ns/op 23884 B/op 2 allocs/op
func BenchmarkNew(b *testing.B) {
b.ReportAllocs()
c, _ := New(CompressionAlgoZstd, CompressionLevelZstdBest)
defer func() { _ = c.Close() }()

for i := 0; i < b.N; i++ {
r, _ := c.Compress(loremIpsumDolor)
_, _ = c.Decompress(r)
}
}
154 changes: 154 additions & 0 deletions compress/compress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package compress

import (
"fmt"

"github.com/klauspost/compress/zstd"
)

// CompressionAlgorithm is the interface that wraps the compression algorithm method.
type CompressionAlgorithm int

func (c CompressionAlgorithm) String() string {
switch c {
case CompressionAlgoZstd:
return "zstd"
default:
return ""
}
}

func (c CompressionAlgorithm) isValid() bool {
return c == CompressionAlgoZstd
}

func NewCompressionAlgorithm(s string) (CompressionAlgorithm, error) {
switch s {
case "zstd":
return CompressionAlgoZstd, nil
default:
return 0, fmt.Errorf("unknown compression algorithm: %s", s)
}
}

// CompressionLevel is the interface that wraps the compression level method.
type CompressionLevel int

func (c CompressionLevel) String() string {
switch c {
case CompressionLevelZstdFastest:
return "fastest"
case CompressionLevelZstdDefault:
return "default"
case CompressionLevelZstdBetter:
return "better"
case CompressionLevelZstdBest:
return "best"
default:
return ""
}
}

func (c CompressionLevel) isValid() bool {
switch c {
case CompressionLevelZstdFastest,
CompressionLevelZstdDefault,
CompressionLevelZstdBetter,
CompressionLevelZstdBest:
return true
default:
return false
}
}

func NewCompressionLevel(s string) (CompressionLevel, error) {
switch s {
case "fastest":
return CompressionLevelZstdFastest, nil
case "default":
return CompressionLevelZstdDefault, nil
case "better":
return CompressionLevelZstdBetter, nil
case "best":
return CompressionLevelZstdBest, nil
default:
return 0, fmt.Errorf("unknown compression level: %s", s)
}
}

var (
CompressionAlgoZstd = CompressionAlgorithm(1)

CompressionLevelZstdFastest = CompressionLevel(zstd.SpeedFastest)
CompressionLevelZstdDefault = CompressionLevel(zstd.SpeedDefault) // "pretty fast" compression
CompressionLevelZstdBetter = CompressionLevel(zstd.SpeedBetterCompression)
CompressionLevelZstdBest = CompressionLevel(zstd.SpeedBestCompression)
)

func New(algo CompressionAlgorithm, level CompressionLevel) (*Compressor, error) {
if !algo.isValid() {
return nil, fmt.Errorf("invalid compression algorithm: %d", algo)
}
if !level.isValid() {
return nil, fmt.Errorf("invalid compression level: %d", level)
}

encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.EncoderLevel(level)))
if err != nil {
return nil, fmt.Errorf("cannot create zstd encoder: %w", err)
}

decoder, err := zstd.NewReader(nil)
if err != nil {
return nil, fmt.Errorf("cannot create zstd decoder: %w", err)
}

return &Compressor{
encoder: encoder,
decoder: decoder,
}, nil
}

type Compressor struct {
encoder *zstd.Encoder
decoder *zstd.Decoder
}

func (c *Compressor) Compress(src []byte) ([]byte, error) {
return c.encoder.EncodeAll(src, nil), nil
}

func (c *Compressor) Decompress(src []byte) ([]byte, error) {
return c.decoder.DecodeAll(src, nil)
}

func (c *Compressor) Close() error {
c.decoder.Close()
return c.encoder.Close()
}

// SerializeSettings serializes the compression settings.
func SerializeSettings(algo CompressionAlgorithm, level CompressionLevel) string {
return fmt.Sprintf("%d:%d", algo, level)
}

// DeserializeSettings deserializes the compression settings.
func DeserializeSettings(s string) (CompressionAlgorithm, CompressionLevel, error) {
var algoInt, levelInt int
_, err := fmt.Sscanf(s, "%d:%d", &algoInt, &levelInt)
if err != nil {
return 0, 0, fmt.Errorf("cannot deserialize settings: %w", err)
}

algo := CompressionAlgorithm(algoInt)
if !algo.isValid() {
return 0, 0, fmt.Errorf("invalid compression algorithm: %d", algoInt)
}

level := CompressionLevel(levelInt)
if !level.isValid() {
return 0, 0, fmt.Errorf("invalid compression level: %d", levelInt)
}

return algo, level, nil
}
75 changes: 75 additions & 0 deletions compress/compress_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package compress

import (
"testing"

"github.com/stretchr/testify/require"
)

var loremIpsumDolor = []byte(`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`)

func TestCompress(t *testing.T) {
compressionLevels := []CompressionLevel{
CompressionLevelZstdFastest,
CompressionLevelZstdDefault,
CompressionLevelZstdBetter,
CompressionLevelZstdBest,
}
for _, level := range compressionLevels {
c, err := New(CompressionAlgoZstd, level)
require.NoError(t, err)

t.Cleanup(func() { _ = c.Close() })

compressed, err := c.Compress(loremIpsumDolor)
require.NoError(t, err)
require.Less(t, len(compressed), len(loremIpsumDolor))

decompressed, err := c.Decompress(compressed)
require.NoError(t, err)
require.Equal(t, string(loremIpsumDolor), string(decompressed))
}
}

func TestSerialization(t *testing.T) {
algo, err := NewCompressionAlgorithm("zstd")
require.NoError(t, err)
require.Equal(t, CompressionAlgoZstd, algo)

level, err := NewCompressionLevel("best")
require.NoError(t, err)
require.Equal(t, CompressionLevelZstdBest, level)

serialized := SerializeSettings(algo, level)
require.Equal(t, "1:4", serialized)

algo, level, err = DeserializeSettings(serialized)
require.NoError(t, err)
require.Equal(t, CompressionAlgoZstd, algo)
require.Equal(t, CompressionLevelZstdBest, level)
}

func TestDeserializationError(t *testing.T) {
// valid algo is 1
// valid level is 1-4
testCases := []string{
"0:0", "0:1", "1:0", "2:1", "1:5",
}
for _, tc := range testCases {
_, _, err := DeserializeSettings(tc)
require.Error(t, err)
}
}

func TestNewError(t *testing.T) {
c, err := New(CompressionAlgorithm(0), CompressionLevelZstdDefault)
require.Nil(t, c)
require.Error(t, err)

c, err = New(CompressionAlgoZstd, CompressionLevel(0))
require.Nil(t, c)
require.Error(t, err)
}
13 changes: 5 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/json-iterator/go v1.1.12
github.com/klauspost/compress v1.17.9
github.com/lib/pq v1.10.9
github.com/linkedin/goavro/v2 v2.13.0
github.com/melbahja/goph v1.4.0
Expand Down Expand Up @@ -61,13 +62,6 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)

require (
github.com/containerd/platforms v0.2.1 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
)

require (
cloud.google.com/go v0.115.1 // indirect
cloud.google.com/go/auth v0.9.1 // indirect
Expand All @@ -84,6 +78,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/continuity v0.4.3 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
Expand All @@ -95,6 +90,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsouza/fake-gcs-server v1.49.3
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
Expand All @@ -116,16 +112,17 @@ require (
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/heetch/avro v0.4.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-ieproxy v0.0.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opencontainers/runc v1.1.13 // indirect
Expand Down

0 comments on commit cda0a40

Please sign in to comment.