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 }