go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/pubsubprotocol/proto.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package pubsubprotocol
    16  
    17  import (
    18  	"bytes"
    19  	"compress/zlib"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  
    24  	"google.golang.org/protobuf/proto"
    25  
    26  	"go.chromium.org/luci/common/data/recordio"
    27  	"go.chromium.org/luci/logdog/api/logpb"
    28  	"go.chromium.org/luci/logdog/common/types"
    29  )
    30  
    31  const (
    32  	// DefaultCompressThreshold is the byte size threshold for compressing message
    33  	// data. Messages whose byte count is less than or equal to this threshold
    34  	// will not be compressed.
    35  	//
    36  	// This is the value used by Akamai for its compression threshold:
    37  	// "The reasons 860 bytes is the minimum size for compression is twofold:
    38  	// (1) The overhead of compressing an object under 860 bytes outweighs
    39  	// performance gain. (2) Objects under 860 bytes can be transmitted via a
    40  	// single packet anyway, so there isn't a compelling reason to compress them."
    41  	DefaultCompressThreshold = 860
    42  )
    43  
    44  // protoBase is the base type of protocol reader/writer objects.
    45  type protoBase struct {
    46  	// maxSize is the maximum Butler protocol data size. By default, it is
    47  	// types.MaxButlerLogBundleSize. However, it can be overridden for testing
    48  	// here.
    49  	maxSize int64
    50  }
    51  
    52  func (p *protoBase) getMaxSize() int64 {
    53  	if p.maxSize == 0 {
    54  		return types.MaxButlerLogBundleSize
    55  	}
    56  	return p.maxSize
    57  }
    58  
    59  // Reader is a protocol reader instance.
    60  type Reader struct {
    61  	protoBase
    62  
    63  	// Metadata is the unpacked ButlerMetadata. It is populated when the
    64  	// metadata has been read.
    65  	Metadata *logpb.ButlerMetadata
    66  
    67  	// Bundle is the unpacked ButlerLogBundle. It is populated when the
    68  	// protocol data has been read and the Metadata indicates a ButlerLogBundle
    69  	// type.
    70  	Bundle *logpb.ButlerLogBundle
    71  }
    72  
    73  // ReadMetadata reads the metadata header frame.
    74  func (r *Reader) readMetadata(fr recordio.Reader) error {
    75  	data, err := fr.ReadFrameAll()
    76  	if err != nil {
    77  		return err
    78  	}
    79  
    80  	md := logpb.ButlerMetadata{}
    81  	if err := proto.Unmarshal(data, &md); err != nil {
    82  		return fmt.Errorf("butlerproto: failed to unmarshal Metadata frame: %s", err)
    83  	}
    84  	r.Metadata = &md
    85  	return nil
    86  }
    87  
    88  func (r *Reader) readData(fr recordio.Reader) ([]byte, error) {
    89  	var br io.Reader
    90  	size, br, err := fr.ReadFrame()
    91  	if err != nil {
    92  		return nil, fmt.Errorf("failed to read bundle frame: %s", err)
    93  	}
    94  
    95  	// Read the frame through a zlib reader.
    96  	switch r.Metadata.Compression {
    97  	case logpb.ButlerMetadata_NONE:
    98  		break
    99  
   100  	case logpb.ButlerMetadata_ZLIB:
   101  		br, err = zlib.NewReader(br)
   102  		if err != nil {
   103  			return nil, fmt.Errorf("failed to initialize zlib reader: %s", err)
   104  		}
   105  
   106  	default:
   107  		return nil, fmt.Errorf("unknown compression type: %v", r.Metadata.Compression)
   108  	}
   109  
   110  	// Wrap our reader in a limitErrorReader so we don't pull data beyond our
   111  	// soft maximum.
   112  	br = &limitErrorReader{
   113  		Reader: br,
   114  		limit:  r.getMaxSize(),
   115  	}
   116  
   117  	buf := bytes.Buffer{}
   118  	buf.Grow(int(size))
   119  	_, err = buf.ReadFrom(br)
   120  	if err != nil {
   121  		return nil, fmt.Errorf("butlerproto: failed to buffer bundle frame: %s", err)
   122  	}
   123  	return buf.Bytes(), nil
   124  }
   125  
   126  func (r *Reader) Read(ir io.Reader) error {
   127  	fr := recordio.NewReader(ir, r.getMaxSize())
   128  
   129  	// Ensure that we have our Metadata.
   130  	if err := r.readMetadata(fr); err != nil {
   131  		return err
   132  	}
   133  
   134  	switch r.Metadata.Type {
   135  	case logpb.ButlerMetadata_ButlerLogBundle:
   136  		data, err := r.readData(fr)
   137  		if err != nil {
   138  			return fmt.Errorf("butlerproto: failed to read Bundle data: %s", err)
   139  		}
   140  
   141  		if r.Metadata.ProtoVersion == logpb.Version {
   142  			bundle := logpb.ButlerLogBundle{}
   143  			if err := proto.Unmarshal(data, &bundle); err != nil {
   144  				return fmt.Errorf("butlerproto: failed to unmarshal Bundle frame: %s", err)
   145  			}
   146  			r.Bundle = &bundle
   147  		}
   148  		return nil
   149  
   150  	default:
   151  		return fmt.Errorf("butlerproto: unknown data type: %s", r.Metadata.Type)
   152  	}
   153  }
   154  
   155  // limitErrorReader is similar to io.LimitReader, except that it returns
   156  // a custom error instead of io.EOF.
   157  //
   158  // This is important, as it allows us to distinguish between the end of
   159  // the compressed reader's data and a limit being hit.
   160  type limitErrorReader struct {
   161  	io.Reader       // underlying reader
   162  	limit     int64 // max bytes remaining
   163  }
   164  
   165  func (r *limitErrorReader) Read(p []byte) (int, error) {
   166  	if r.limit <= 0 {
   167  		return 0, errors.New("limit exceeded")
   168  	}
   169  	if int64(len(p)) > r.limit {
   170  		p = p[0:r.limit]
   171  	}
   172  	n, err := r.Reader.Read(p)
   173  	r.limit -= int64(n)
   174  	return n, err
   175  }
   176  
   177  // Writer writes Butler messages that the Reader can read.
   178  type Writer struct {
   179  	protoBase
   180  
   181  	// ProtoVersion is the protocol version string to use. If empty, the current
   182  	// ProtoVersion will be used.
   183  	ProtoVersion string
   184  
   185  	// Compress, if true, allows the Writer to choose to compress data when
   186  	// applicable.
   187  	Compress bool
   188  
   189  	// CompressThreshold is the minimum size that data must be in order to
   190  	CompressThreshold int
   191  
   192  	compressBuf    bytes.Buffer
   193  	compressWriter *zlib.Writer
   194  }
   195  
   196  func (w *Writer) writeData(fw recordio.Writer, t logpb.ButlerMetadata_ContentType, data []byte) error {
   197  	if int64(len(data)) > w.getMaxSize() {
   198  		return fmt.Errorf("butlerproto: serialized size exceeds soft cap (%d > %d)", len(data), w.getMaxSize())
   199  	}
   200  
   201  	pv := w.ProtoVersion
   202  	if pv == "" {
   203  		pv = logpb.Version
   204  	}
   205  	md := logpb.ButlerMetadata{
   206  		Type:         t,
   207  		ProtoVersion: pv,
   208  	}
   209  
   210  	// If we're configured to compress and the data is below our threshold,
   211  	// compress.
   212  	if w.Compress && len(data) >= w.CompressThreshold {
   213  		w.compressBuf.Reset()
   214  		if w.compressWriter == nil {
   215  			w.compressWriter = zlib.NewWriter(&w.compressBuf)
   216  		} else {
   217  			w.compressWriter.Reset(&w.compressBuf)
   218  		}
   219  		if _, err := w.compressWriter.Write(data); err != nil {
   220  			return err
   221  		}
   222  		if err := w.compressWriter.Close(); err != nil {
   223  			return err
   224  		}
   225  
   226  		compressed := true
   227  		if compressed {
   228  			md.Compression = logpb.ButlerMetadata_ZLIB
   229  		}
   230  		data = w.compressBuf.Bytes()
   231  	}
   232  
   233  	// Write metadata frame.
   234  	mdData, err := proto.Marshal(&md)
   235  	if err != nil {
   236  		return fmt.Errorf("butlerproto: failed to marshal Metadata: %s", err)
   237  	}
   238  	_, err = fw.Write(mdData)
   239  	if err != nil {
   240  		return fmt.Errorf("butlerproto: failed to write Metadata frame: %s", err)
   241  	}
   242  	if err := fw.Flush(); err != nil {
   243  		return fmt.Errorf("butlerproto: failed to flush Metadata frame: %s", err)
   244  	}
   245  
   246  	// Write data frame.
   247  	_, err = fw.Write(data)
   248  	if err != nil {
   249  		return fmt.Errorf("butlerproto: failed to write data frame: %s", err)
   250  	}
   251  	if err := fw.Flush(); err != nil {
   252  		return fmt.Errorf("butlerproto: failed to flush data frame: %s", err)
   253  	}
   254  	return nil
   255  }
   256  
   257  // WriteWith writes a ButlerLogBundle to the supplied Writer.
   258  func (w *Writer) Write(iw io.Writer, b *logpb.ButlerLogBundle) error {
   259  	return w.WriteWith(recordio.NewWriter(iw), b)
   260  }
   261  
   262  // WriteWith writes a ButlerLogBundle to the supplied recordio.Writer.
   263  func (w *Writer) WriteWith(fw recordio.Writer, b *logpb.ButlerLogBundle) error {
   264  	data, err := proto.Marshal(b)
   265  	if err != nil {
   266  		entries := b.GetEntries()
   267  		// TODO(tandrii, hinoka): leave just error after crbug.com/859995 is fixed.
   268  		if len(entries) > 100 {
   269  			return fmt.Errorf("butlerproto: failed to marshal Bundle of len %d with first 100 entries %s: %s",
   270  				len(entries), entries[:100], err)
   271  		}
   272  		return fmt.Errorf("butlerproto: failed to marshal Bundle %s: %s", entries, err)
   273  	}
   274  
   275  	return w.writeData(fw, logpb.ButlerMetadata_ButlerLogBundle, data)
   276  }