go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/bqlog/bundler.go (about) 1 // Copyright 2021 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 bqlog 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 "sync" 22 "time" 23 24 "cloud.google.com/go/bigquery/storage/apiv1/storagepb" 25 codepb "google.golang.org/genproto/googleapis/rpc/code" 26 "google.golang.org/grpc/codes" 27 "google.golang.org/grpc/status" 28 "google.golang.org/protobuf/proto" 29 "google.golang.org/protobuf/reflect/protodesc" 30 "google.golang.org/protobuf/reflect/protoreflect" 31 "google.golang.org/protobuf/types/descriptorpb" 32 33 "go.chromium.org/luci/common/clock" 34 "go.chromium.org/luci/common/errors" 35 "go.chromium.org/luci/common/logging" 36 "go.chromium.org/luci/common/retry/transient" 37 "go.chromium.org/luci/common/sync/dispatcher" 38 "go.chromium.org/luci/common/sync/dispatcher/buffer" 39 "go.chromium.org/luci/common/tsmon/field" 40 "go.chromium.org/luci/common/tsmon/metric" 41 "go.chromium.org/luci/grpc/grpcutil" 42 ) 43 44 var ( 45 // Roughly a limit on a size of a single AppendRows message. 46 batchSizeMaxBytes = 5 * 1024 * 1024 47 // How long to wait by default before sending an incomplete batch. 48 defaultBatchAgeMax = 10 * time.Second 49 // How many bytes to buffer by default before starting dropping excesses. 50 defaultMaxLiveSizeBytes = 50 * 1024 * 1024 51 ) 52 53 var ( 54 metricSentCounter = metric.NewCounter( 55 "bqlog/sent", 56 "Count of log entries successfully sent", 57 nil, 58 field.String("table"), // full table ID "project.dataset.table" 59 ) 60 metricDroppedCounter = metric.NewCounter( 61 "bqlog/dropped", 62 "Count of log entries dropped for various reasons", 63 nil, 64 field.String("table"), // full table ID "project.dataset.table" 65 field.String("reason"), // reason of why it was dropped if known 66 ) 67 metricErrorsCounter = metric.NewCounter( 68 "bqlog/errors", 69 "Count of encountered RPC errors", 70 nil, 71 field.String("table"), // full table ID "project.dataset.table" 72 field.String("code"), // canonical gRPC code (as string) if known 73 ) 74 ) 75 76 // Bundler buffers logs in memory before sending them to BigQuery. 77 type Bundler struct { 78 CloudProject string // the cloud project with the dataset, required 79 Dataset string // the BQ dataset with log tables, required 80 81 m sync.RWMutex 82 bufs map[protoreflect.FullName]*logBuffer 83 ctx context.Context 84 cancel context.CancelFunc 85 running bool 86 draining bool 87 } 88 89 // Sink describes where and how to log proto messages of the given type. 90 type Sink struct { 91 Prototype proto.Message // used only for its type descriptor, required 92 Table string // the BQ table name within the bundler's dataset, required 93 BatchAgeMax time.Duration // for how long to buffer messages (or 0 for some default) 94 MaxLiveSizeBytes int // approximate limit on memory buffer size (or 0 for some default) 95 } 96 97 // RegisterSink tells the bundler where and how to log messages of some 98 // concrete proto type. 99 // 100 // There can currently be only one sink per proto message type. Must be called 101 // before the bundler is running. Can be called during the init() time. 102 func (b *Bundler) RegisterSink(sink Sink) { 103 if sink.BatchAgeMax == 0 { 104 sink.BatchAgeMax = defaultBatchAgeMax 105 } 106 if sink.MaxLiveSizeBytes == 0 { 107 sink.MaxLiveSizeBytes = defaultMaxLiveSizeBytes 108 } else if sink.MaxLiveSizeBytes < batchSizeMaxBytes { 109 sink.MaxLiveSizeBytes = batchSizeMaxBytes 110 } 111 112 if sink.Table == "" { 113 panic("missing required field Table") 114 } 115 116 b.m.Lock() 117 defer b.m.Unlock() 118 119 if b.running { 120 panic("the bundler is already running") 121 } 122 123 typ := proto.MessageName(sink.Prototype) 124 if b.bufs[typ] != nil { 125 panic(fmt.Sprintf("message type %q was already registered with RegisterSink", typ)) 126 } 127 if b.bufs == nil { 128 b.bufs = make(map[protoreflect.FullName]*logBuffer, 1) 129 } 130 b.bufs[typ] = &logBuffer{ 131 decl: sink, 132 desc: protodesc.ToDescriptorProto(sink.Prototype.ProtoReflect().Descriptor()), 133 } 134 } 135 136 // Log asynchronously logs the given message to a BQ table associated with 137 // the message type via a prior RegisterSink call. 138 // 139 // This is a best effort operation (and thus returns no error). 140 // 141 // Messages are dropped when: 142 // - Writes to BigQuery are failing with a fatal error: 143 // - The table doesn't exist. 144 // - The table has an incompatible schema. 145 // - The server account has no permission to write to the table. 146 // - Etc. 147 // - The server crashes before it manages to flush buffered logs. 148 // - The internal flush buffer is full (per MaxLiveSizeBytes). 149 // 150 // In case of transient write errors messages may be duplicated. 151 // 152 // Panics if `m` was not registered via RegisterSink or if the bundler is not 153 // running yet. 154 func (b *Bundler) Log(ctx context.Context, m proto.Message) { 155 typ := proto.MessageName(m) 156 blob, err := proto.Marshal(m) 157 158 b.m.RLock() 159 defer b.m.RUnlock() 160 161 if !b.running { 162 panic("the bundler is not running yet") 163 } 164 buf := b.bufs[typ] 165 if buf == nil { 166 panic(fmt.Sprintf("message type %q was not registered with RegisterSink", typ)) 167 } 168 169 switch { 170 case err != nil: 171 recordDrop(ctx, buf.tableID, 1, err, "MARSHAL_ERROR") 172 case b.draining: 173 recordDrop(ctx, buf.tableID, 1, errors.New("draining already"), "DRAINING") 174 default: 175 // Note: we explicitly do not select on ctx.Done() since often the context 176 // is already expired (e.g. when logging from handlers that have reached the 177 // deadline). Sending to the channel here should be fast, since buffer's 178 // FullBehavior is set to DropOldestBatch, i.e. it never really blocks. 179 buf.disp.C <- blob 180 } 181 } 182 183 // Start launches the bundler internal goroutines. 184 // 185 // Canceling the context with cease all bundler activities. To gracefully 186 // shutdown the bundler (e.g. by flushing all pending logs) use Shutdown. 187 func (b *Bundler) Start(ctx context.Context, w BigQueryWriter) { 188 if b.CloudProject == "" { 189 panic("missing required field CloudProject") 190 } 191 if b.Dataset == "" { 192 panic("missing required field Dataset") 193 } 194 195 b.m.Lock() 196 defer b.m.Unlock() 197 198 if b.running { 199 panic("the bundler is already running") 200 } 201 b.running = true 202 b.ctx, b.cancel = context.WithCancel(ctx) 203 204 for _, buf := range b.bufs { 205 tableID := fmt.Sprintf("%s.%s.%s", b.CloudProject, b.Dataset, buf.decl.Table) 206 bufCtx := loggingFields(b.ctx, tableID) 207 buf.start(bufCtx, tableID, &logSender{ 208 ctx: bufCtx, 209 w: w, 210 tableID: tableID, 211 desc: buf.desc, 212 dest: fmt.Sprintf("projects/%s/datasets/%s/tables/%s/_default", b.CloudProject, b.Dataset, buf.decl.Table), 213 }) 214 } 215 } 216 217 // Shutdown flushes pending logs and closes streaming RPCs. 218 // 219 // Does nothing if the bundler wasn't running. Gives up waiting for all data to 220 // be flushed (and drops it) after 15s timeout or when `ctx` is canceled. 221 func (b *Bundler) Shutdown(ctx context.Context) { 222 var drained []chan struct{} 223 224 b.m.Lock() 225 if b.running && !b.draining { 226 b.draining = true 227 for _, buf := range b.bufs { 228 drained = append(drained, buf.drain(loggingFields(ctx, buf.tableID))) 229 } 230 // Totally shutdown everything after some deadline by canceling the root 231 // bundler context. It should cause all dispatcher.Channels to give up on 232 // any retries they might be doing. 233 cancel := b.cancel 234 go func() { 235 <-clock.After(ctx, 15*time.Second) 236 cancel() 237 }() 238 } 239 b.m.Unlock() 240 241 for _, ch := range drained { 242 <-ch 243 } 244 } 245 246 //////////////////////////////////////////////////////////////////////////////// 247 248 func loggingFields(ctx context.Context, tableID string) context.Context { 249 return logging.SetFields(ctx, logging.Fields{ 250 "activity": "luci.bqlog", 251 "table": tableID, 252 }) 253 } 254 255 func recordSent(ctx context.Context, tableID string, count int) { 256 metricSentCounter.Add(ctx, int64(count), tableID) 257 } 258 259 func recordDrop(ctx context.Context, tableID string, count int, err error, reason string) { 260 ctx = loggingFields(ctx, tableID) 261 if err != nil { 262 logging.Errorf(ctx, "Dropped %d row(s): %s: %s", count, reason, err) 263 } else { 264 logging.Errorf(ctx, "Dropped %d row(s): %s", count, reason) 265 } 266 metricDroppedCounter.Add(ctx, int64(count), tableID, reason) 267 } 268 269 func recordErr(ctx context.Context, tableID string, count int, err error) { 270 ctx = loggingFields(ctx, tableID) 271 if transient.Tag.In(err) { 272 logging.Warningf(ctx, "Transient error when sending %d row(s): %s", count, err) 273 } else { 274 logging.Errorf(ctx, "Fatal error when sending %d row(s): %s", count, err) 275 } 276 codeStr := "UNKNOWN" 277 if code := grpcutil.Code(err); code != codes.Unknown { 278 if codeStr = codepb.Code_name[int32(code)]; codeStr == "" { 279 codeStr = fmt.Sprintf("CODE_%d", code) 280 } 281 } else if errors.Contains(err, io.EOF) { 282 codeStr = "EOF" 283 } 284 metricErrorsCounter.Add(ctx, 1, tableID, codeStr) 285 } 286 287 //////////////////////////////////////////////////////////////////////////////// 288 289 type logBuffer struct { 290 decl Sink 291 desc *descriptorpb.DescriptorProto 292 tableID string 293 sender *logSender 294 disp dispatcher.Channel 295 } 296 297 func (b *logBuffer) start(ctx context.Context, tableID string, sender *logSender) { 298 b.tableID = tableID 299 b.sender = sender 300 301 opts := dispatcher.Options{ 302 ItemSizeFunc: func(itm any) int { return len(itm.([]byte)) }, 303 DropFn: func(data *buffer.Batch, flush bool) { 304 if data != nil { 305 recordDrop(ctx, b.tableID, len(data.Data), nil, "DISPATCHER") 306 } 307 }, 308 ErrorFn: func(data *buffer.Batch, err error) (retry bool) { 309 recordErr(ctx, b.tableID, len(data.Data), err) 310 return transient.Tag.In(err) 311 }, 312 Buffer: buffer.Options{ 313 MaxLeases: 1, // there can be only one outstanding write per an RPC stream 314 BatchItemsMax: -1, // cut batches based on size 315 BatchSizeMax: batchSizeMaxBytes, 316 BatchAgeMax: b.decl.BatchAgeMax, 317 FullBehavior: &buffer.DropOldestBatch{ 318 MaxLiveSize: b.decl.MaxLiveSizeBytes, 319 }, 320 }, 321 } 322 323 var err error 324 b.disp, err = dispatcher.NewChannel(ctx, &opts, sender.send) 325 if err != nil { 326 panic(fmt.Sprintf("failed to start the dispatcher: %s", err)) // should not be happening 327 } 328 } 329 330 func (b *logBuffer) drain(ctx context.Context) chan struct{} { 331 logging.Debugf(ctx, "Draining...") 332 333 drained := make(chan struct{}) 334 335 b.disp.Close() 336 go func() { 337 // Wait until the dispatcher channel is drained into the gRPC sender. 338 select { 339 case <-ctx.Done(): 340 case <-b.disp.DrainC: 341 } 342 343 // Wait until the pending gRPC data is flushed. 344 b.sender.stop() 345 346 logging.Debugf(ctx, "Drained") 347 close(drained) 348 }() 349 350 return drained 351 } 352 353 //////////////////////////////////////////////////////////////////////////////// 354 355 type logSender struct { 356 ctx context.Context 357 w BigQueryWriter 358 tableID string 359 desc *descriptorpb.DescriptorProto 360 dest string 361 362 m sync.Mutex 363 stream storagepb.BigQueryWrite_AppendRowsClient 364 } 365 366 func (s *logSender) send(data *buffer.Batch) (rerr error) { 367 // There allowed only one concurrent Send or CloseSend per a gRPC stream. 368 s.m.Lock() 369 defer s.m.Unlock() 370 371 // Open the gRPC stream if have none yet. 372 opened := false 373 if s.stream == nil { 374 stream, err := s.w.AppendRows(s.ctx) 375 if err != nil { 376 return grpcutil.WrapIfTransient(err) 377 } 378 opened = true 379 s.stream = stream 380 } 381 382 // Prepare the request. 383 protoData := &storagepb.AppendRowsRequest_ProtoData{ 384 Rows: &storagepb.ProtoRows{ 385 SerializedRows: make([][]byte, len(data.Data)), 386 }, 387 } 388 for i, row := range data.Data { 389 protoData.Rows.SerializedRows[i] = row.Item.([]byte) 390 } 391 392 // WriterSchema field is necessary only in the first request. 393 if opened { 394 protoData.WriterSchema = &storagepb.ProtoSchema{ 395 ProtoDescriptor: s.desc, 396 } 397 } 398 399 err := s.stream.Send(&storagepb.AppendRowsRequest{ 400 WriteStream: s.dest, 401 Rows: &storagepb.AppendRowsRequest_ProtoRows{ 402 ProtoRows: protoData, 403 }, 404 }) 405 406 // If the stream was aborted, properly shut it all down so we can try again 407 // later with a new stream. 408 if err != nil { 409 s.stream.CloseSend() 410 s.stream.Recv() 411 s.stream = nil 412 return errors.Annotate(err, "failed to send data to BQ").Tag(transient.Tag).Err() 413 } 414 415 // Otherwise try to read the acknowledgment from the server. We need to wait 416 // for it to make sure it is OK to "forget" about the `data` batch. 417 resp, err := s.stream.Recv() 418 419 // If there's a gRPC-level error, it means the connection is broken and should 420 // be abandoned. 421 if err != nil { 422 s.stream = nil 423 if err == io.EOF { 424 return errors.Annotate(err, "server unexpected closed the connection").Tag(transient.Tag).Err() 425 } 426 return grpcutil.WrapIfTransient(err) 427 } 428 429 // If the overall connection is fine, but the latest push specifically was 430 // rejected, the error is in `resp`. 431 if sts := resp.GetError(); sts != nil { 432 return grpcutil.WrapIfTransient(status.ErrorProto(sts)) 433 } 434 435 recordSent(s.ctx, s.tableID, len(data.Data)) 436 return nil 437 } 438 439 func (s *logSender) stop() { 440 s.m.Lock() 441 defer s.m.Unlock() 442 if s.stream != nil { 443 s.stream.CloseSend() 444 s.stream.Recv() // wait for ACK 445 s.stream = nil 446 } 447 }