go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/common/storage/bigtable/bigtable.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 bigtable
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  
    21  	"go.chromium.org/luci/logdog/common/storage"
    22  
    23  	"go.chromium.org/luci/common/errors"
    24  	"go.chromium.org/luci/common/logging"
    25  	"go.chromium.org/luci/common/retry/transient"
    26  	"go.chromium.org/luci/grpc/grpcutil"
    27  
    28  	"cloud.google.com/go/bigtable"
    29  	"google.golang.org/grpc/codes"
    30  )
    31  
    32  const (
    33  	logColumnFamily = "log"
    34  
    35  	// The data column stores raw low row data (RecordIO blob).
    36  	logColumn  = "data"
    37  	logColName = logColumnFamily + ":" + logColumn
    38  )
    39  
    40  // Limits taken from here:
    41  // https://cloud.google.com/bigtable/docs/schema-design
    42  const (
    43  	// bigTableRowMaxBytes is the maximum number of bytes that a single BigTable
    44  	// row may hold.
    45  	bigTableRowMaxBytes = 1024 * 1024 * 10 // 10MB
    46  )
    47  
    48  // btGetCallback is a callback that is invoked for each log data row returned
    49  // by getLogData.
    50  //
    51  // If an error is encountered, no more log data will be fetched. The error will
    52  // be propagated to the getLogData call.
    53  type btGetCallback func(*rowKey, []byte) error
    54  
    55  // btIface is a general interface for BigTable operations intended to enable
    56  // unit tests to stub out BigTable.
    57  type btIface interface {
    58  	// putLogData adds new log data to BigTable.
    59  	//
    60  	// If data already exists for the named row, it will return storage.ErrExists
    61  	// and not add the data.
    62  	putLogData(context.Context, *rowKey, []byte) error
    63  
    64  	// getLogData retrieves rows belonging to the supplied stream record, starting
    65  	// with the first index owned by that record. The supplied callback is invoked
    66  	// once per retrieved row.
    67  	//
    68  	// rk is the starting row key.
    69  	//
    70  	// If the supplied limit is nonzero, no more than limit rows will be
    71  	// retrieved.
    72  	//
    73  	// If keysOnly is true, then the callback will return nil row data.
    74  	getLogData(c context.Context, rk *rowKey, limit int, keysOnly bool, cb btGetCallback) error
    75  
    76  	// Drops all rows given the path prefix of rk.
    77  	dropRowRange(c context.Context, rkPrefix *rowKey) error
    78  
    79  	// getMaxRowSize returns the maximum row size that this implementation
    80  	// supports.
    81  	getMaxRowSize() int
    82  }
    83  
    84  // prodBTIface is a production implementation of a "btIface".
    85  type prodBTIface struct {
    86  	*Storage
    87  }
    88  
    89  func (bti prodBTIface) getLogTable() (*bigtable.Table, error) {
    90  	if bti.Client == nil {
    91  		return nil, errors.New("no client configured")
    92  	}
    93  	return bti.Client.Open(bti.LogTable), nil
    94  }
    95  
    96  func (bti prodBTIface) putLogData(c context.Context, rk *rowKey, data []byte) error {
    97  	logTable, err := bti.getLogTable()
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	m := bigtable.NewMutation()
   103  	m.Set(logColumnFamily, logColumn, bigtable.ServerTime, data)
   104  	cm := bigtable.NewCondMutation(bigtable.RowKeyFilter(rk.encode()), nil, m)
   105  
   106  	rowExists := false
   107  	if err := logTable.Apply(c, rk.encode(), cm, bigtable.GetCondMutationResult(&rowExists)); err != nil {
   108  		return wrapIfTransientForApply(err)
   109  	}
   110  	if rowExists {
   111  		return storage.ErrExists
   112  	}
   113  	return nil
   114  }
   115  
   116  func (bti prodBTIface) dropRowRange(c context.Context, rk *rowKey) error {
   117  	logTable, err := bti.getLogTable()
   118  	if err != nil {
   119  		return err
   120  	}
   121  
   122  	// ApplyBulk claims to be able to apply 100k mutations. Keep it small here to
   123  	// stay well within the stated guidelines.
   124  	const maxBatchSize = 100000 / 4
   125  
   126  	del := bigtable.NewMutation()
   127  	del.DeleteRow()
   128  
   129  	allMuts := make([]*bigtable.Mutation, maxBatchSize)
   130  	for i := range allMuts {
   131  		allMuts[i] = del
   132  	}
   133  
   134  	prefix, upperBound := rk.pathPrefix(), rk.pathPrefixUpperBound()
   135  	rng := bigtable.NewRange(prefix, upperBound)
   136  	// apply paranoia mode
   137  	if rng.Contains("") || prefix == "" || upperBound == "" {
   138  		panic(fmt.Sprintf("NOTHING MAKES SENSE: %q %q %q", rng, prefix, upperBound))
   139  	}
   140  
   141  	keyC := make(chan string)
   142  
   143  	// TODO(iannucci): parallelize row scan?
   144  
   145  	// buffered to avoid deadlocking main thread below
   146  	readerC := make(chan error, 1)
   147  	go func() {
   148  		defer close(readerC)
   149  		defer close(keyC)
   150  		readerC <- logTable.ReadRows(c, rng, func(row bigtable.Row) bool {
   151  			keyC <- row.Key()
   152  			return true
   153  		},
   154  			bigtable.RowFilter(bigtable.FamilyFilter(logColumnFamily)),
   155  			bigtable.RowFilter(bigtable.ColumnFilter(logColumn)),
   156  			bigtable.RowFilter(bigtable.StripValueFilter()),
   157  		)
   158  	}()
   159  
   160  	keys := make([]string, maxBatchSize)
   161  	batchNum := 0
   162  	var totalDropped int64
   163  	for {
   164  		batchNum++
   165  		batch := keys[:0]
   166  		for key := range keyC {
   167  			batch = append(batch, key)
   168  			if len(batch) >= maxBatchSize {
   169  				break
   170  			}
   171  		}
   172  		if len(batch) == 0 {
   173  			err, _ := <-readerC
   174  			return err
   175  		}
   176  
   177  		errs, err := logTable.ApplyBulk(c, batch, allMuts[:len(batch)])
   178  		if err != nil {
   179  			logging.WithError(err).Errorf(c, "dropRowRange: ApplyBulk failed")
   180  			return errors.Annotate(err, "ApplyBulk failed on batch %d", batchNum).Err()
   181  		}
   182  		if len(errs) > 0 {
   183  			logging.Warningf(c, "ApplyBulk: got %d errors: first: %q", len(errs), errs[0])
   184  		}
   185  		totalDropped += int64(len(batch) - len(errs))
   186  	}
   187  }
   188  
   189  func (bti prodBTIface) getLogData(c context.Context, rk *rowKey, limit int, keysOnly bool, cb btGetCallback) error {
   190  	logTable, err := bti.getLogTable()
   191  	if err != nil {
   192  		return err
   193  	}
   194  
   195  	// Construct read options based on Get request.
   196  	ropts := []bigtable.ReadOption{
   197  		bigtable.RowFilter(bigtable.FamilyFilter(logColumnFamily)),
   198  		bigtable.RowFilter(bigtable.ColumnFilter(logColumn)),
   199  		nil,
   200  	}[:2]
   201  	if keysOnly {
   202  		ropts = append(ropts, bigtable.RowFilter(bigtable.StripValueFilter()))
   203  	}
   204  	if limit > 0 {
   205  		ropts = append(ropts, bigtable.LimitRows(int64(limit)))
   206  	}
   207  
   208  	// This will limit the range to the immediate row key ("ASDF~INDEX") to
   209  	// immediately after the row key ("ASDF~~"). See rowKey for more information.
   210  	rng := bigtable.NewRange(rk.encode(), rk.pathPrefixUpperBound())
   211  
   212  	var innerErr error
   213  	err = logTable.ReadRows(c, rng, func(row bigtable.Row) bool {
   214  		data, err := getLogRowData(row)
   215  		if err != nil {
   216  			innerErr = storage.ErrBadData
   217  			return false
   218  		}
   219  
   220  		drk, err := decodeRowKey(row.Key())
   221  		if err != nil {
   222  			innerErr = err
   223  			return false
   224  		}
   225  
   226  		if err := cb(drk, data); err != nil {
   227  			innerErr = err
   228  			return false
   229  		}
   230  		return true
   231  	}, ropts...)
   232  	if err != nil {
   233  		return grpcutil.WrapIfTransient(err)
   234  	}
   235  	if innerErr != nil {
   236  		return innerErr
   237  	}
   238  	return nil
   239  }
   240  
   241  func (bti prodBTIface) getMaxRowSize() int { return bigTableRowMaxBytes }
   242  
   243  // getLogRowData loads the []byte contents of the supplied log row.
   244  //
   245  // If the row doesn't exist, storage.ErrDoesNotExist will be returned.
   246  func getLogRowData(row bigtable.Row) (data []byte, err error) {
   247  	items, ok := row[logColumnFamily]
   248  	if !ok {
   249  		err = storage.ErrDoesNotExist
   250  		return
   251  	}
   252  
   253  	for _, item := range items {
   254  		switch item.Column {
   255  		case logColName:
   256  			data = item.Value
   257  			return
   258  		}
   259  	}
   260  
   261  	// If no fields could be extracted, the rows does not exist.
   262  	err = storage.ErrDoesNotExist
   263  	return
   264  }
   265  
   266  // getReadItem retrieves a specific RowItem from the supplied Row.
   267  func getReadItem(row bigtable.Row, family, column string) *bigtable.ReadItem {
   268  	// Get the row for our family.
   269  	items, ok := row[logColumnFamily]
   270  	if !ok {
   271  		return nil
   272  	}
   273  
   274  	// Get the specific ReadItem for our column
   275  	colName := fmt.Sprintf("%s:%s", family, column)
   276  	for _, item := range items {
   277  		if item.Column == colName {
   278  			return &item
   279  		}
   280  	}
   281  	return nil
   282  }
   283  
   284  func wrapIfTransientForApply(err error) error {
   285  	if err == nil {
   286  		return nil
   287  	}
   288  
   289  	// For Apply, assume that anything other than InvalidArgument (bad data) is
   290  	// transient. We exempt InvalidArgument because our data construction is
   291  	// deterministic, and so this request can never succeed.
   292  	switch code := grpcutil.Code(err); code {
   293  	case codes.InvalidArgument:
   294  		return err
   295  	default:
   296  		return transient.Tag.Apply(err)
   297  	}
   298  }