github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/executiondatasync/tracker/storage.go (about) 1 package tracker 2 3 import ( 4 "encoding/binary" 5 "errors" 6 "fmt" 7 "sync" 8 9 "github.com/dgraph-io/badger/v2" 10 "github.com/hashicorp/go-multierror" 11 "github.com/ipfs/go-cid" 12 "github.com/rs/zerolog" 13 14 "github.com/onflow/flow-go/module/blobs" 15 ) 16 17 // badger key prefixes 18 const ( 19 prefixGlobalState byte = iota + 1 // global state variables 20 prefixLatestHeight // tracks, for each blob, the latest height at which there exists a block whose execution data contains the blob 21 prefixBlobRecord // tracks the set of blobs at each height 22 ) 23 24 const ( 25 globalStateFulfilledHeight byte = iota + 1 // latest fulfilled block height 26 globalStatePrunedHeight // latest pruned block height 27 ) 28 29 const cidsPerBatch = 16 // number of cids to track per batch 30 31 func retryOnConflict(db *badger.DB, fn func(txn *badger.Txn) error) error { 32 for { 33 err := db.Update(fn) 34 if errors.Is(err, badger.ErrConflict) { 35 continue 36 } 37 return err 38 } 39 } 40 41 const globalStateKeyLength = 2 42 43 func makeGlobalStateKey(state byte) []byte { 44 globalStateKey := make([]byte, globalStateKeyLength) 45 globalStateKey[0] = prefixGlobalState 46 globalStateKey[1] = state 47 return globalStateKey 48 } 49 50 const blobRecordKeyLength = 1 + 8 + blobs.CidLength 51 52 func makeBlobRecordKey(blockHeight uint64, c cid.Cid) []byte { 53 blobRecordKey := make([]byte, blobRecordKeyLength) 54 blobRecordKey[0] = prefixBlobRecord 55 binary.LittleEndian.PutUint64(blobRecordKey[1:], blockHeight) 56 copy(blobRecordKey[1+8:], c.Bytes()) 57 return blobRecordKey 58 } 59 60 func parseBlobRecordKey(key []byte) (uint64, cid.Cid, error) { 61 blockHeight := binary.LittleEndian.Uint64(key[1:]) 62 c, err := cid.Cast(key[1+8:]) 63 return blockHeight, c, err 64 } 65 66 const latestHeightKeyLength = 1 + blobs.CidLength 67 68 func makeLatestHeightKey(c cid.Cid) []byte { 69 latestHeightKey := make([]byte, latestHeightKeyLength) 70 latestHeightKey[0] = prefixLatestHeight 71 copy(latestHeightKey[1:], c.Bytes()) 72 return latestHeightKey 73 } 74 75 func makeUint64Value(v uint64) []byte { 76 value := make([]byte, 8) 77 binary.LittleEndian.PutUint64(value, v) 78 return value 79 } 80 81 func getUint64Value(item *badger.Item) (uint64, error) { 82 value, err := item.ValueCopy(nil) 83 if err != nil { 84 return 0, err 85 } 86 87 return binary.LittleEndian.Uint64(value), nil 88 } 89 90 // getBatchItemCountLimit returns the maximum number of items that can be included in a single batch 91 // transaction based on the number / total size of updates per item. 92 func getBatchItemCountLimit(db *badger.DB, writeCountPerItem int64, writeSizePerItem int64) int { 93 totalSizePerItem := 2*writeCountPerItem + writeSizePerItem // 2 bytes per entry for user and internal meta 94 maxItemCountByWriteCount := db.MaxBatchCount() / writeCountPerItem 95 maxItemCountByWriteSize := db.MaxBatchSize() / totalSizePerItem 96 97 if maxItemCountByWriteCount < maxItemCountByWriteSize { 98 return int(maxItemCountByWriteCount) 99 } else { 100 return int(maxItemCountByWriteSize) 101 } 102 } 103 104 // TrackBlobsFun is passed to the UpdateFn provided to Storage.Update, 105 // and can be called to track a list of cids at a given block height. 106 // It returns an error if the update failed. 107 type TrackBlobsFn func(blockHeight uint64, cids ...cid.Cid) error 108 109 // UpdateFn is implemented by the user and passed to Storage.Update, 110 // which ensures that it will never be run concurrently with any call 111 // to Storage.Prune. 112 // Any returned error will be returned from the surrounding call to Storage.Update. 113 // The function must never make any calls to the Storage interface itself, 114 // and should instead only modify the storage via the provided TrackBlobsFn. 115 type UpdateFn func(TrackBlobsFn) error 116 117 // PruneCallback is a function which can be provided by the user which 118 // is called for each CID when the last height at which that CID appears 119 // is pruned. 120 // Any returned error will be returned from the surrounding call to Storage.Prune. 121 // The prune callback can be used to delete the corresponding 122 // blob data from the blob store. 123 type PruneCallback func(cid.Cid) error 124 125 type Storage interface { 126 // Update is used to track new blob CIDs. 127 // It can be used to track blobs for both sealed and unsealed 128 // heights, and the same blob may be added multiple times for 129 // different heights. 130 // The same blob may also be added multiple times for the same 131 // height, but it will only be tracked once per height. 132 Update(UpdateFn) error 133 134 // GetFulfilledHeight returns the current fulfilled height. 135 // No errors are expected during normal operation. 136 GetFulfilledHeight() (uint64, error) 137 138 // SetFulfilledHeight updates the fulfilled height value, 139 // which is the highest block height `h` such that all 140 // heights <= `h` are sealed and the sealed execution data 141 // has been downloaded. 142 // It is up to the caller to ensure that this is never 143 // called with a value lower than the pruned height. 144 // No errors are expected during normal operation 145 SetFulfilledHeight(height uint64) error 146 147 // GetPrunedHeight returns the current pruned height. 148 // No errors are expected during normal operation. 149 GetPrunedHeight() (uint64, error) 150 151 // PruneUpToHeight removes all data from storage corresponding 152 // to block heights up to and including the given height, 153 // and updates the latest pruned height value. 154 // It locks the Storage and ensures that no other writes 155 // can occur during the pruning. 156 // It is up to the caller to ensure that this is never 157 // called with a value higher than the fulfilled height. 158 PruneUpToHeight(height uint64) error 159 } 160 161 // The storage component tracks the following information: 162 // - the latest pruned height 163 // - the latest fulfilled height 164 // - the set of CIDs of the execution data blobs we know about at each height, so that 165 // once we prune a fulfilled height we can remove the blob data from local storage 166 // - for each CID, the most recent height that it was observed at, so that when pruning 167 // a fulfilled height we don't remove any blob data that is still needed at higher heights 168 // 169 // The storage component calls the given prune callback for a CID when the last height 170 // at which that CID appears is pruned. The prune callback can be used to delete the 171 // corresponding blob data from the blob store. 172 type storage struct { 173 // ensures that pruning operations are not run concurrently with any other db writes 174 // we acquire the read lock when we want to perform a non-prune WRITE 175 // we acquire the write lock when we want to perform a prune WRITE 176 mu sync.RWMutex 177 178 db *badger.DB 179 pruneCallback PruneCallback 180 logger zerolog.Logger 181 } 182 183 type StorageOption func(*storage) 184 185 func WithPruneCallback(callback PruneCallback) StorageOption { 186 return func(s *storage) { 187 s.pruneCallback = callback 188 } 189 } 190 191 func OpenStorage(dbPath string, startHeight uint64, logger zerolog.Logger, opts ...StorageOption) (*storage, error) { 192 lg := logger.With().Str("module", "tracker_storage").Logger() 193 db, err := badger.Open(badger.LSMOnlyOptions(dbPath)) 194 if err != nil { 195 return nil, fmt.Errorf("could not open tracker db: %w", err) 196 } 197 198 storage := &storage{ 199 db: db, 200 pruneCallback: func(c cid.Cid) error { return nil }, 201 logger: lg, 202 } 203 204 for _, opt := range opts { 205 opt(storage) 206 } 207 208 lg.Info().Msgf("initialize storage with start height: %d", startHeight) 209 210 if err := storage.init(startHeight); err != nil { 211 return nil, fmt.Errorf("failed to initialize storage: %w", err) 212 } 213 214 lg.Info().Msgf("storage initialized") 215 216 return storage, nil 217 } 218 219 func (s *storage) init(startHeight uint64) error { 220 fulfilledHeight, fulfilledHeightErr := s.GetFulfilledHeight() 221 prunedHeight, prunedHeightErr := s.GetPrunedHeight() 222 223 if fulfilledHeightErr == nil && prunedHeightErr == nil { 224 if prunedHeight > fulfilledHeight { 225 return fmt.Errorf( 226 "inconsistency detected: pruned height (%d) is greater than fulfilled height (%d)", 227 prunedHeight, 228 fulfilledHeight, 229 ) 230 } 231 232 s.logger.Info().Msgf("prune from height %v up to height %d", fulfilledHeight, prunedHeight) 233 // replay pruning in case it was interrupted during previous shutdown 234 if err := s.PruneUpToHeight(prunedHeight); err != nil { 235 return fmt.Errorf("failed to replay pruning: %w", err) 236 } 237 s.logger.Info().Msgf("finished pruning") 238 } else if errors.Is(fulfilledHeightErr, badger.ErrKeyNotFound) && errors.Is(prunedHeightErr, badger.ErrKeyNotFound) { 239 // db is empty, we need to bootstrap it 240 if err := s.bootstrap(startHeight); err != nil { 241 return fmt.Errorf("failed to bootstrap storage: %w", err) 242 } 243 } else { 244 return multierror.Append(fulfilledHeightErr, prunedHeightErr).ErrorOrNil() 245 } 246 247 return nil 248 } 249 250 func (s *storage) bootstrap(startHeight uint64) error { 251 fulfilledHeightKey := makeGlobalStateKey(globalStateFulfilledHeight) 252 fulfilledHeightValue := makeUint64Value(startHeight) 253 254 prunedHeightKey := makeGlobalStateKey(globalStatePrunedHeight) 255 prunedHeightValue := makeUint64Value(startHeight) 256 257 return s.db.Update(func(txn *badger.Txn) error { 258 if err := txn.Set(fulfilledHeightKey, fulfilledHeightValue); err != nil { 259 return fmt.Errorf("failed to set fulfilled height value: %w", err) 260 } 261 262 if err := txn.Set(prunedHeightKey, prunedHeightValue); err != nil { 263 return fmt.Errorf("failed to set pruned height value: %w", err) 264 } 265 266 return nil 267 }) 268 } 269 270 func (s *storage) Update(f UpdateFn) error { 271 s.mu.RLock() 272 defer s.mu.RUnlock() 273 return f(s.trackBlobs) 274 } 275 276 func (s *storage) SetFulfilledHeight(height uint64) error { 277 fulfilledHeightKey := makeGlobalStateKey(globalStateFulfilledHeight) 278 fulfilledHeightValue := makeUint64Value(height) 279 280 return s.db.Update(func(txn *badger.Txn) error { 281 if err := txn.Set(fulfilledHeightKey, fulfilledHeightValue); err != nil { 282 return fmt.Errorf("failed to set fulfilled height value: %w", err) 283 } 284 285 return nil 286 }) 287 } 288 289 func (s *storage) GetFulfilledHeight() (uint64, error) { 290 fulfilledHeightKey := makeGlobalStateKey(globalStateFulfilledHeight) 291 var fulfilledHeight uint64 292 293 if err := s.db.View(func(txn *badger.Txn) error { 294 item, err := txn.Get(fulfilledHeightKey) 295 if err != nil { 296 return fmt.Errorf("failed to find fulfilled height entry: %w", err) 297 } 298 299 fulfilledHeight, err = getUint64Value(item) 300 if err != nil { 301 return fmt.Errorf("failed to retrieve fulfilled height value: %w", err) 302 } 303 304 return nil 305 }); err != nil { 306 return 0, err 307 } 308 309 return fulfilledHeight, nil 310 } 311 312 func (s *storage) trackBlob(txn *badger.Txn, blockHeight uint64, c cid.Cid) error { 313 if err := txn.Set(makeBlobRecordKey(blockHeight, c), nil); err != nil { 314 return fmt.Errorf("failed to add blob record: %w", err) 315 } 316 317 latestHeightKey := makeLatestHeightKey(c) 318 item, err := txn.Get(latestHeightKey) 319 if err != nil { 320 if !errors.Is(err, badger.ErrKeyNotFound) { 321 return fmt.Errorf("failed to get latest height: %w", err) 322 } 323 } else { 324 latestHeight, err := getUint64Value(item) 325 if err != nil { 326 return fmt.Errorf("failed to retrieve latest height value: %w", err) 327 } 328 329 // don't update the latest height if there is already a higher block height containing this blob 330 if latestHeight >= blockHeight { 331 return nil 332 } 333 } 334 335 latestHeightValue := makeUint64Value(blockHeight) 336 337 if err := txn.Set(latestHeightKey, latestHeightValue); err != nil { 338 return fmt.Errorf("failed to set latest height value: %w", err) 339 } 340 341 return nil 342 } 343 344 func (s *storage) trackBlobs(blockHeight uint64, cids ...cid.Cid) error { 345 cidsPerBatch := cidsPerBatch 346 maxCidsPerBatch := getBatchItemCountLimit(s.db, 2, blobRecordKeyLength+latestHeightKeyLength+8) 347 if maxCidsPerBatch < cidsPerBatch { 348 cidsPerBatch = maxCidsPerBatch 349 } 350 351 for len(cids) > 0 { 352 batchSize := cidsPerBatch 353 if len(cids) < batchSize { 354 batchSize = len(cids) 355 } 356 batch := cids[:batchSize] 357 358 if err := retryOnConflict(s.db, func(txn *badger.Txn) error { 359 for _, c := range batch { 360 if err := s.trackBlob(txn, blockHeight, c); err != nil { 361 return fmt.Errorf("failed to track blob %s: %w", c.String(), err) 362 } 363 } 364 365 return nil 366 }); err != nil { 367 return err 368 } 369 370 cids = cids[batchSize:] 371 } 372 373 return nil 374 } 375 376 func (s *storage) batchDelete(deleteInfos []*deleteInfo) error { 377 return s.db.Update(func(txn *badger.Txn) error { 378 for _, dInfo := range deleteInfos { 379 if err := txn.Delete(makeBlobRecordKey(dInfo.height, dInfo.cid)); err != nil { 380 return fmt.Errorf("failed to delete blob record for Cid %s: %w", dInfo.cid.String(), err) 381 } 382 383 if dInfo.deleteLatestHeightRecord { 384 if err := txn.Delete(makeLatestHeightKey(dInfo.cid)); err != nil { 385 return fmt.Errorf("failed to delete latest height record for Cid %s: %w", dInfo.cid.String(), err) 386 } 387 } 388 } 389 390 return nil 391 }) 392 } 393 394 func (s *storage) batchDeleteItemLimit() int { 395 itemsPerBatch := 256 396 maxItemsPerBatch := getBatchItemCountLimit(s.db, 2, blobRecordKeyLength+latestHeightKeyLength) 397 if maxItemsPerBatch < itemsPerBatch { 398 itemsPerBatch = maxItemsPerBatch 399 } 400 return itemsPerBatch 401 } 402 403 func (s *storage) PruneUpToHeight(height uint64) error { 404 blobRecordPrefix := []byte{prefixBlobRecord} 405 itemsPerBatch := s.batchDeleteItemLimit() 406 var batch []*deleteInfo 407 408 s.mu.Lock() 409 defer s.mu.Unlock() 410 411 if err := s.setPrunedHeight(height); err != nil { 412 return err 413 } 414 415 if err := s.db.View(func(txn *badger.Txn) error { 416 it := txn.NewIterator(badger.IteratorOptions{ 417 PrefetchValues: false, 418 Prefix: blobRecordPrefix, 419 }) 420 defer it.Close() 421 422 // iterate over blob records, calling pruneCallback for any CIDs that should be pruned 423 // and cleaning up the corresponding tracker records 424 for it.Seek(blobRecordPrefix); it.ValidForPrefix(blobRecordPrefix); it.Next() { 425 blobRecordItem := it.Item() 426 blobRecordKey := blobRecordItem.Key() 427 428 blockHeight, blobCid, err := parseBlobRecordKey(blobRecordKey) 429 if err != nil { 430 return fmt.Errorf("malformed blob record key %v: %w", blobRecordKey, err) 431 } 432 433 // iteration occurs in key order, so block heights are guaranteed to be ascending 434 if blockHeight > height { 435 break 436 } 437 438 dInfo := &deleteInfo{ 439 cid: blobCid, 440 height: blockHeight, 441 } 442 443 latestHeightKey := makeLatestHeightKey(blobCid) 444 latestHeightItem, err := txn.Get(latestHeightKey) 445 if err != nil { 446 return fmt.Errorf("failed to get latest height entry for Cid %s: %w", blobCid.String(), err) 447 } 448 449 latestHeight, err := getUint64Value(latestHeightItem) 450 if err != nil { 451 return fmt.Errorf("failed to retrieve latest height value for Cid %s: %w", blobCid.String(), err) 452 } 453 454 // a blob is only removable if it is not referenced by any blob tree at a higher height 455 if latestHeight < blockHeight { 456 // this should never happen 457 return fmt.Errorf( 458 "inconsistency detected: latest height recorded for Cid %s is %d, but blob record exists at height %d", 459 blobCid.String(), latestHeight, blockHeight, 460 ) 461 } 462 463 // the current block height is the last to reference this CID, prune the CID and remove 464 // all tracker records 465 if latestHeight == blockHeight { 466 if err := s.pruneCallback(blobCid); err != nil { 467 return err 468 } 469 dInfo.deleteLatestHeightRecord = true 470 } 471 472 // remove tracker records for pruned heights 473 batch = append(batch, dInfo) 474 if len(batch) == itemsPerBatch { 475 if err := s.batchDelete(batch); err != nil { 476 return err 477 } 478 batch = nil 479 } 480 } 481 482 if len(batch) > 0 { 483 if err := s.batchDelete(batch); err != nil { 484 return err 485 } 486 } 487 488 return nil 489 }); err != nil { 490 return err 491 } 492 493 // this is a good time to do garbage collection 494 if err := s.db.RunValueLogGC(0.5); err != nil { 495 s.logger.Err(err).Msg("failed to run value log garbage collection") 496 } 497 498 return nil 499 } 500 501 func (s *storage) setPrunedHeight(height uint64) error { 502 prunedHeightKey := makeGlobalStateKey(globalStatePrunedHeight) 503 prunedHeightValue := makeUint64Value(height) 504 505 return s.db.Update(func(txn *badger.Txn) error { 506 if err := txn.Set(prunedHeightKey, prunedHeightValue); err != nil { 507 return fmt.Errorf("failed to set pruned height value: %w", err) 508 } 509 510 return nil 511 }) 512 } 513 514 func (s *storage) GetPrunedHeight() (uint64, error) { 515 prunedHeightKey := makeGlobalStateKey(globalStatePrunedHeight) 516 var prunedHeight uint64 517 518 if err := s.db.View(func(txn *badger.Txn) error { 519 item, err := txn.Get(prunedHeightKey) 520 if err != nil { 521 return fmt.Errorf("failed to find pruned height entry: %w", err) 522 } 523 524 prunedHeight, err = getUint64Value(item) 525 if err != nil { 526 return fmt.Errorf("failed to retrieve pruned height value: %w", err) 527 } 528 529 return nil 530 }); err != nil { 531 return 0, err 532 } 533 534 return prunedHeight, nil 535 } 536 537 type deleteInfo struct { 538 cid cid.Cid 539 height uint64 540 deleteLatestHeightRecord bool 541 }