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 }