go.uber.org/yarpc@v1.72.1/compressor/gzip/gzip.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  // Package yarpcgzip provides a YARPC binding for GZIP compression.
    22  package yarpcgzip
    23  
    24  import (
    25  	"compress/gzip"
    26  	"encoding/binary"
    27  	"io"
    28  	"sync"
    29  
    30  	"go.uber.org/yarpc/api/transport"
    31  )
    32  
    33  const name = "gzip"
    34  
    35  // Option is an option argument for the Gzip compressor constructor, New.
    36  type Option interface {
    37  	apply(*Compressor)
    38  }
    39  
    40  // Level sets the compression level for the compressor.
    41  func Level(level int) Option {
    42  	return levelOption{level: level}
    43  }
    44  
    45  type levelOption struct {
    46  	level int
    47  }
    48  
    49  func (o levelOption) apply(opts *Compressor) {
    50  	opts.level = o.level
    51  }
    52  
    53  // New returns a GZIP compression strategy, suitable for configuring an
    54  // outbound dialer.
    55  //
    56  // The compressor needs to be adapted and registered to be compatible
    57  // with the gRPC compressor system.
    58  // Since gRPC requires global registration of compressors, you must arrange for
    59  // the compressor to be registered in your application initialization.
    60  // The adapter converts an io.Reader into an io.ReadCloser so that reading EOF
    61  // will implicitly trigger Close, a behavior gRPC-go relies upon to reuse
    62  // readers.
    63  //
    64  //  import (
    65  //      "compress/gzip"
    66  //
    67  //      "google.golang.org/grpc/encoding"
    68  //      "go.uber.org/yarpc/compressor/grpc"
    69  //      "go.uber.org/yarpc/compressor/gzip"
    70  //  )
    71  //
    72  //  var GZIPCompressor = yarpcgzip.New(yarpcgzip.Level(gzip.BestCompression))
    73  //
    74  //  func init()
    75  //      gz := yarpcgrpccompressor.New(GZIPCompressor)
    76  //      encoding.RegisterCompressor(gz)
    77  //  }
    78  //
    79  // If you are constructing your YARPC clients directly through the API,
    80  // create a gRPC dialer with the Compressor option.
    81  //
    82  //  trans := grpc.NewTransport()
    83  //  dialer := trans.NewDialer(GZIPCompressor)
    84  //  peers := roundrobin.New(dialer)
    85  //  outbound := trans.NewOutbound(peers)
    86  //
    87  // If you are using the YARPC configurator to create YARPC objects
    88  // using config files, you will also need to register the compressor
    89  // with your configurator.
    90  //
    91  //  configurator := yarpcconfig.New()
    92  //  configurator.MustRegisterCompressor(GZIPCompressor)
    93  //
    94  // Then, using the compression strategy for outbound requests
    95  // on a particular client, just set the compressor to gzip.
    96  //
    97  //  outbounds:
    98  //    theirsecureservice:
    99  //      grpc:
   100  //        address: ":443"
   101  //        tls:
   102  //          enabled: true
   103  //        compressor: gzip
   104  //
   105  func New(opts ...Option) *Compressor {
   106  	c := &Compressor{
   107  		level: gzip.DefaultCompression,
   108  	}
   109  	for _, opt := range opts {
   110  		opt.apply(c)
   111  	}
   112  	return c
   113  }
   114  
   115  // Compressor represents the gzip compression strategy.
   116  type Compressor struct {
   117  	level         int
   118  	compressors   sync.Pool
   119  	decompressors sync.Pool
   120  }
   121  
   122  var _ transport.Compressor = (*Compressor)(nil)
   123  
   124  // Name is gzip.
   125  func (*Compressor) Name() string {
   126  	return name
   127  }
   128  
   129  // Compress creates a gzip compressor.
   130  func (c *Compressor) Compress(w io.Writer) (io.WriteCloser, error) {
   131  	if cw, got := c.compressors.Get().(*writer); got {
   132  		cw.writer.Reset(w)
   133  		return cw, nil
   134  	}
   135  
   136  	cw, err := gzip.NewWriterLevel(w, c.level)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	return &writer{
   142  		writer: cw,
   143  		pool:   &c.compressors,
   144  	}, nil
   145  }
   146  
   147  type writer struct {
   148  	writer *gzip.Writer
   149  	pool   *sync.Pool
   150  }
   151  
   152  var _ io.WriteCloser = (*writer)(nil)
   153  
   154  func (w *writer) Write(buf []byte) (int, error) {
   155  	return w.writer.Write(buf)
   156  }
   157  
   158  func (w *writer) Close() error {
   159  	defer w.pool.Put(w)
   160  	return w.writer.Close()
   161  }
   162  
   163  // Decompress obtains a gzip decompressor.
   164  func (c *Compressor) Decompress(r io.Reader) (io.ReadCloser, error) {
   165  	if dr, got := c.decompressors.Get().(*reader); got {
   166  		if err := dr.reader.Reset(r); err != nil {
   167  			c.decompressors.Put(r)
   168  			return nil, err
   169  		}
   170  
   171  		return dr, nil
   172  	}
   173  
   174  	dr, err := gzip.NewReader(r)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	return &reader{
   180  		reader: dr,
   181  		pool:   &c.decompressors,
   182  	}, nil
   183  }
   184  
   185  type reader struct {
   186  	reader *gzip.Reader
   187  	pool   *sync.Pool
   188  }
   189  
   190  var _ io.ReadCloser = (*reader)(nil)
   191  
   192  func (r *reader) Read(buf []byte) (n int, err error) {
   193  	return r.reader.Read(buf)
   194  }
   195  
   196  func (r *reader) Close() error {
   197  	r.pool.Put(r)
   198  	return nil
   199  }
   200  
   201  // DecompressedSize returns the decompressed size of the given GZIP compressed
   202  // bytes.
   203  //
   204  // gRPC specifically casts the compressor to a DecompressedSizer
   205  // to pre-check message length.
   206  //
   207  // Per gRPC-go, on which this is based:
   208  // https://github.com/grpc/grpc-go/blob/master/encoding/gzip/gzip.go
   209  //
   210  // RFC1952 specifies that the last four bytes "contains the size of
   211  // the original (uncompressed) input data modulo 2^32."
   212  // gRPC has a max message size of 2GB so we don't need to worry about
   213  // wraparound for that transport protocol.
   214  func (c *Compressor) DecompressedSize(buf []byte) int {
   215  	last := len(buf)
   216  	if last < 4 {
   217  		return -1
   218  	}
   219  	return int(binary.LittleEndian.Uint32(buf[last-4 : last]))
   220  }